diff --git a/Content.Benchmarks/MapLoadBenchmark.cs b/Content.Benchmarks/MapLoadBenchmark.cs
index ce95e9a158d..261e164f175 100644
--- a/Content.Benchmarks/MapLoadBenchmark.cs
+++ b/Content.Benchmarks/MapLoadBenchmark.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@@ -46,7 +46,7 @@ public async Task Cleanup()
PoolManager.Shutdown();
}
- public static readonly string[] MapsSource = { "Empty", "Box", "Bagel", "Dev", "CentComm", "Atlas", "Core", "TestTeg", "Saltern", "Packed", "Omega", "Cluster", "Reach", "Origin", "Meta", "Marathon", "Europa", "MeteorArena", "Fland", "Barratry" };
+ public static readonly string[] MapsSource = { "Empty", "Box", "Bagel", "Dev", "CentComm", "Atlas", "Core", "TestTeg", "Saltern", "Packed", "Omega", "Cluster", "Reach", "Origin", "Meta", "Marathon", "Europa", "MeteorArena", "Fland", "Barratry", "Oasis" };
[ParamsSource(nameof(MapsSource))]
public string Map;
diff --git a/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs b/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs
index de51b2fb192..8512107b69d 100644
--- a/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs
+++ b/Content.Benchmarks/SpawnEquipDeleteBenchmark.cs
@@ -58,7 +58,7 @@ await _pair.Server.WaitPost(() =>
for (var i = 0; i < N; i++)
{
_entity = server.EntMan.SpawnAttachedTo(Mob, _coords);
- _spawnSys.EquipStartingGear(_entity, _gear, null);
+ _spawnSys.EquipStartingGear(_entity, _gear);
server.EntMan.DeleteEntity(_entity);
}
});
diff --git a/Content.Client/Access/IdCardSystem.cs b/Content.Client/Access/IdCardSystem.cs
index fcf2bf57de3..e0c02976f7b 100644
--- a/Content.Client/Access/IdCardSystem.cs
+++ b/Content.Client/Access/IdCardSystem.cs
@@ -2,6 +2,4 @@
namespace Content.Client.Access;
-public sealed class IdCardSystem : SharedIdCardSystem
-{
-}
+public sealed class IdCardSystem : SharedIdCardSystem;
diff --git a/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs b/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs
index 73f18aec8d6..c3fac8cb92a 100644
--- a/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs
+++ b/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs
@@ -40,9 +40,9 @@ private void OnJobChanged(string newJob)
SendMessage(new AgentIDCardJobChangedMessage(newJob));
}
- public void OnJobIconChanged(string newJobIcon)
+ public void OnJobIconChanged(string newJobIconId)
{
- SendMessage(new AgentIDCardJobIconChangedMessage(newJobIcon));
+ SendMessage(new AgentIDCardJobIconChangedMessage(newJobIconId));
}
///
@@ -57,7 +57,7 @@ protected override void UpdateState(BoundUserInterfaceState state)
_window.SetCurrentName(cast.CurrentName);
_window.SetCurrentJob(cast.CurrentJob);
- _window.SetAllowedIcons(cast.Icons);
+ _window.SetAllowedIcons(cast.Icons, cast.CurrentJobIconId);
}
protected override void Dispose(bool disposing)
diff --git a/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs b/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs
index beca0c41ba9..9a38c0c4853 100644
--- a/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs
+++ b/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs
@@ -38,7 +38,7 @@ public AgentIDCardWindow(AgentIDCardBoundUserInterface bui)
JobLineEdit.OnFocusExit += e => OnJobChanged?.Invoke(e.Text);
}
- public void SetAllowedIcons(HashSet icons)
+ public void SetAllowedIcons(HashSet icons, string currentJobIconId)
{
IconGrid.DisposeAllChildren();
@@ -79,6 +79,10 @@ public void SetAllowedIcons(HashSet icons)
jobIconButton.AddChild(jobIconTexture);
jobIconButton.OnPressed += _ => _bui.OnJobIconChanged(jobIcon.ID);
IconGrid.AddChild(jobIconButton);
+
+ if (jobIconId.Equals(currentJobIconId))
+ jobIconButton.Pressed = true;
+
i++;
}
}
diff --git a/Content.Client/Administration/Components/HeadstandComponent.cs b/Content.Client/Administration/Components/HeadstandComponent.cs
index d95e74576bf..a4e3bfc5aaf 100644
--- a/Content.Client/Administration/Components/HeadstandComponent.cs
+++ b/Content.Client/Administration/Components/HeadstandComponent.cs
@@ -3,7 +3,7 @@
namespace Content.Client.Administration.Components;
-[RegisterComponent, NetworkedComponent]
+[RegisterComponent]
public sealed partial class HeadstandComponent : SharedHeadstandComponent
{
diff --git a/Content.Client/Administration/Components/KillSignComponent.cs b/Content.Client/Administration/Components/KillSignComponent.cs
index 1cf47b93ff5..91c44ef3f27 100644
--- a/Content.Client/Administration/Components/KillSignComponent.cs
+++ b/Content.Client/Administration/Components/KillSignComponent.cs
@@ -3,6 +3,5 @@
namespace Content.Client.Administration.Components;
-[NetworkedComponent, RegisterComponent]
-public sealed partial class KillSignComponent : SharedKillSignComponent
-{ }
+[RegisterComponent]
+public sealed partial class KillSignComponent : SharedKillSignComponent;
diff --git a/Content.Client/Administration/Managers/ClientAdminManager.cs b/Content.Client/Administration/Managers/ClientAdminManager.cs
index fdd62fb6a2d..0f740c81045 100644
--- a/Content.Client/Administration/Managers/ClientAdminManager.cs
+++ b/Content.Client/Administration/Managers/ClientAdminManager.cs
@@ -126,12 +126,15 @@ void IPostInjectInit.PostInject()
public AdminData? GetAdminData(EntityUid uid, bool includeDeAdmin = false)
{
- return uid == _player.LocalEntity ? _adminData : null;
+ if (uid == _player.LocalEntity && (_adminData?.Active ?? includeDeAdmin))
+ return _adminData;
+
+ return null;
}
public AdminData? GetAdminData(ICommonSession session, bool includeDeAdmin = false)
{
- if (_player.LocalUser == session.UserId)
+ if (_player.LocalUser == session.UserId && (_adminData?.Active ?? includeDeAdmin))
return _adminData;
return null;
diff --git a/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs b/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs
index 1f32640f7dd..dc263d6055c 100644
--- a/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs
+++ b/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs
@@ -3,6 +3,7 @@
using System.Net.Sockets;
using Content.Client.Administration.UI.CustomControls;
using Content.Shared.Administration;
+using Content.Shared.CCVar;
using Content.Shared.Database;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
@@ -11,6 +12,7 @@
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -32,8 +34,11 @@ public sealed partial class BanPanel : DefaultWindow
// This is less efficient than just holding a reference to the root control and enumerating children, but you
// have to know how the controls are nested, which makes the code more complicated.
private readonly List _roleCheckboxes = new();
+ private readonly ISawmill _banpanelSawmill;
[Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
+ [Dependency] private readonly ILogManager _logManager = default!;
private enum TabNumbers
{
@@ -65,6 +70,7 @@ public BanPanel()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
+ _banpanelSawmill = _logManager.GetSawmill("admin.banpanel");
PlayerList.OnSelectionChanged += OnPlayerSelectionChanged;
PlayerNameLine.OnFocusExit += _ => OnPlayerNameChanged();
PlayerCheckbox.OnPressed += _ =>
@@ -104,6 +110,11 @@ public BanPanel()
};
SubmitButton.OnPressed += SubmitButtonOnOnPressed;
+ IpCheckbox.Pressed = _cfg.GetCVar(CCVars.ServerBanIpBanDefault);
+ HwidCheckbox.Pressed = _cfg.GetCVar(CCVars.ServerBanHwidBanDefault);
+ LastConnCheckbox.Pressed = _cfg.GetCVar(CCVars.ServerBanUseLastDetails);
+ EraseCheckbox.Pressed = _cfg.GetCVar(CCVars.ServerBanErasePlayer);
+
SeverityOption.AddItem(Loc.GetString("admin-note-editor-severity-none"), (int) NoteSeverity.None);
SeverityOption.AddItem(Loc.GetString("admin-note-editor-severity-low"), (int) NoteSeverity.Minor);
SeverityOption.AddItem(Loc.GetString("admin-note-editor-severity-medium"), (int) NoteSeverity.Medium);
@@ -175,6 +186,39 @@ private void CreateRoleGroup(string roleName, IEnumerable roleList, Colo
c.Pressed = args.Pressed;
}
}
+
+ if (args.Pressed)
+ {
+ if (!Enum.TryParse(_cfg.GetCVar(CCVars.DepartmentBanDefaultSeverity), true, out NoteSeverity newSeverity))
+ {
+ _banpanelSawmill
+ .Warning("Departmental role ban severity could not be parsed from config!");
+ return;
+ }
+ SeverityOption.SelectId((int) newSeverity);
+ }
+ else
+ {
+ foreach (var childContainer in RolesContainer.Children)
+ {
+ if (childContainer is Container)
+ {
+ foreach (var child in childContainer.Children)
+ {
+ if (child is CheckBox { Pressed: true })
+ return;
+ }
+ }
+ }
+
+ if (!Enum.TryParse(_cfg.GetCVar(CCVars.RoleBanDefaultSeverity), true, out NoteSeverity newSeverity))
+ {
+ _banpanelSawmill
+ .Warning("Role ban severity could not be parsed from config!");
+ return;
+ }
+ SeverityOption.SelectId((int) newSeverity);
+ }
};
outerContainer.AddChild(innerContainer);
foreach (var role in roleList)
@@ -353,6 +397,35 @@ private void OnTypeChanged()
{
TypeOption.ModulateSelfOverride = null;
Tabs.SetTabVisible((int) TabNumbers.Roles, TypeOption.SelectedId == (int) Types.Role);
+ NoteSeverity? newSeverity = null;
+ switch (TypeOption.SelectedId)
+ {
+ case (int)Types.Server:
+ if (Enum.TryParse(_cfg.GetCVar(CCVars.ServerBanDefaultSeverity), true, out NoteSeverity serverSeverity))
+ newSeverity = serverSeverity;
+ else
+ {
+ _banpanelSawmill
+ .Warning("Server ban severity could not be parsed from config!");
+ }
+
+ break;
+ case (int) Types.Role:
+
+ if (Enum.TryParse(_cfg.GetCVar(CCVars.RoleBanDefaultSeverity), true, out NoteSeverity roleSeverity))
+ {
+ newSeverity = roleSeverity;
+ }
+ else
+ {
+ _banpanelSawmill
+ .Warning("Role ban severity could not be parsed from config!");
+ }
+ break;
+ }
+
+ if (newSeverity != null)
+ SeverityOption.SelectId((int) newSeverity.Value);
}
private void UpdateSubmitEnabled()
diff --git a/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs b/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs
index b105e629cfa..b54af3a5871 100644
--- a/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs
+++ b/Content.Client/Atmos/UI/GasAnalyzerWindow.xaml.cs
@@ -163,6 +163,26 @@ private void GenerateGasDisplay(GasMixEntry gasMix, Control parent)
parent.AddChild(panel);
panel.AddChild(dataContainer);
+ // Volume label
+ var volBox = new BoxContainer { Orientation = BoxContainer.LayoutOrientation.Horizontal };
+
+ volBox.AddChild(new Label
+ {
+ Text = Loc.GetString("gas-analyzer-window-volume-text")
+ });
+ volBox.AddChild(new Control
+ {
+ MinSize = new Vector2(10, 0),
+ HorizontalExpand = true
+ });
+ volBox.AddChild(new Label
+ {
+ Text = Loc.GetString("gas-analyzer-window-volume-val-text", ("volume", $"{gasMix.Volume:0.##}")),
+ Align = Label.AlignMode.Right,
+ HorizontalExpand = true
+ });
+ dataContainer.AddChild(volBox);
+
// Pressure label
var presBox = new BoxContainer { Orientation = BoxContainer.LayoutOrientation.Horizontal };
diff --git a/Content.Client/Audio/Jukebox/JukeboxBoundUserInterface.cs b/Content.Client/Audio/Jukebox/JukeboxBoundUserInterface.cs
new file mode 100644
index 00000000000..072730d65d4
--- /dev/null
+++ b/Content.Client/Audio/Jukebox/JukeboxBoundUserInterface.cs
@@ -0,0 +1,119 @@
+using Content.Shared.Audio.Jukebox;
+using Robust.Client.Audio;
+using Robust.Client.Player;
+using Robust.Shared.Audio.Components;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Audio.Jukebox;
+
+public sealed class JukeboxBoundUserInterface : BoundUserInterface
+{
+ [Dependency] private readonly IPlayerManager _player = default!;
+ [Dependency] private readonly IPrototypeManager _protoManager = default!;
+
+ [ViewVariables]
+ private JukeboxMenu? _menu;
+
+ public JukeboxBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ IoCManager.InjectDependencies(this);
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _menu = new JukeboxMenu();
+ _menu.OnClose += Close;
+ _menu.OpenCentered();
+
+ _menu.OnPlayPressed += args =>
+ {
+ if (args)
+ {
+ SendMessage(new JukeboxPlayingMessage());
+ }
+ else
+ {
+ SendMessage(new JukeboxPauseMessage());
+ }
+ };
+
+ _menu.OnStopPressed += () =>
+ {
+ SendMessage(new JukeboxStopMessage());
+ };
+
+ _menu.OnSongSelected += SelectSong;
+
+ _menu.SetTime += SetTime;
+ PopulateMusic();
+ Reload();
+ }
+
+ ///
+ /// Reloads the attached menu if it exists.
+ ///
+ public void Reload()
+ {
+ if (_menu == null || !EntMan.TryGetComponent(Owner, out JukeboxComponent? jukebox))
+ return;
+
+ _menu.SetAudioStream(jukebox.AudioStream);
+
+ if (_protoManager.TryIndex(jukebox.SelectedSongId, out var songProto))
+ {
+ var length = EntMan.System().GetAudioLength(songProto.Path.Path.ToString());
+ _menu.SetSelectedSong(songProto.Name, (float) length.TotalSeconds);
+ }
+ else
+ {
+ _menu.SetSelectedSong(string.Empty, 0f);
+ }
+ }
+
+ public void PopulateMusic()
+ {
+ _menu?.Populate(_protoManager.EnumeratePrototypes());
+ }
+
+ public void SelectSong(ProtoId songid)
+ {
+ SendMessage(new JukeboxSelectedMessage(songid));
+ }
+
+ public void SetTime(float time)
+ {
+ var sentTime = time;
+
+ // You may be wondering, what the fuck is this
+ // Well we want to be able to predict the playback slider change, of which there are many ways to do it
+ // We can't just use SendPredictedMessage because it will reset every tick and audio updates every frame
+ // so it will go BRRRRT
+ // Using ping gets us close enough that it SHOULD, MOST OF THE TIME, fall within the 0.1 second tolerance
+ // that's still on engine so our playback position never gets corrected.
+ if (EntMan.TryGetComponent(Owner, out JukeboxComponent? jukebox) &&
+ EntMan.TryGetComponent(jukebox.AudioStream, out AudioComponent? audioComp))
+ {
+ audioComp.PlaybackPosition = time;
+ }
+
+ SendMessage(new JukeboxSetTimeMessage(sentTime));
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (!disposing)
+ return;
+
+ if (_menu == null)
+ return;
+
+ _menu.OnClose -= Close;
+ _menu.Dispose();
+ _menu = null;
+ }
+}
+
diff --git a/Content.Client/Audio/Jukebox/JukeboxMenu.xaml b/Content.Client/Audio/Jukebox/JukeboxMenu.xaml
new file mode 100644
index 00000000000..e8d39a9b119
--- /dev/null
+++ b/Content.Client/Audio/Jukebox/JukeboxMenu.xaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Audio/Jukebox/JukeboxMenu.xaml.cs b/Content.Client/Audio/Jukebox/JukeboxMenu.xaml.cs
new file mode 100644
index 00000000000..e0904eece86
--- /dev/null
+++ b/Content.Client/Audio/Jukebox/JukeboxMenu.xaml.cs
@@ -0,0 +1,166 @@
+using Content.Shared.Audio.Jukebox;
+using Robust.Client.Audio;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Audio.Components;
+using Robust.Shared.Input;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+using FancyWindow = Content.Client.UserInterface.Controls.FancyWindow;
+
+namespace Content.Client.Audio.Jukebox;
+
+[GenerateTypedNameReferences]
+public sealed partial class JukeboxMenu : FancyWindow
+{
+ [Dependency] private readonly IEntityManager _entManager = default!;
+ private AudioSystem _audioSystem;
+
+ ///
+ /// Are we currently 'playing' or paused for the play / pause button.
+ ///
+ private bool _playState;
+
+ ///
+ /// True if playing, false if paused.
+ ///
+ public event Action? OnPlayPressed;
+ public event Action? OnStopPressed;
+ public event Action>? OnSongSelected;
+ public event Action? SetTime;
+
+ private EntityUid? _audio;
+
+ private float _lockTimer;
+
+ public JukeboxMenu()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+ _audioSystem = _entManager.System();
+
+ MusicList.OnItemSelected += args =>
+ {
+ var entry = MusicList[args.ItemIndex];
+
+ if (entry.Metadata is not string juke)
+ return;
+
+ OnSongSelected?.Invoke(juke);
+ };
+
+ PlayButton.OnPressed += args =>
+ {
+ OnPlayPressed?.Invoke(!_playState);
+ };
+
+ StopButton.OnPressed += args =>
+ {
+ OnStopPressed?.Invoke();
+ };
+ PlaybackSlider.OnReleased += PlaybackSliderKeyUp;
+
+ SetPlayPauseButton(_audioSystem.IsPlaying(_audio), force: true);
+ }
+
+ public JukeboxMenu(AudioSystem audioSystem)
+ {
+ _audioSystem = audioSystem;
+ }
+
+ public void SetAudioStream(EntityUid? audio)
+ {
+ _audio = audio;
+ }
+
+ private void PlaybackSliderKeyUp(Slider args)
+ {
+ SetTime?.Invoke(PlaybackSlider.Value);
+ _lockTimer = 0.5f;
+ }
+
+ ///
+ /// Re-populates the list of jukebox prototypes available.
+ ///
+ public void Populate(IEnumerable jukeboxProtos)
+ {
+ MusicList.Clear();
+
+ foreach (var entry in jukeboxProtos)
+ {
+ MusicList.AddItem(entry.Name, metadata: entry.ID);
+ }
+ }
+
+ public void SetPlayPauseButton(bool playing, bool force = false)
+ {
+ if (_playState == playing && !force)
+ return;
+
+ _playState = playing;
+
+ if (playing)
+ {
+ PlayButton.Text = Loc.GetString("jukebox-menu-buttonpause");
+ return;
+ }
+
+ PlayButton.Text = Loc.GetString("jukebox-menu-buttonplay");
+ }
+
+ public void SetSelectedSong(string name, float length)
+ {
+ SetSelectedSongText(name);
+ PlaybackSlider.MaxValue = length;
+ PlaybackSlider.SetValueWithoutEvent(0);
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ base.FrameUpdate(args);
+
+ if (_lockTimer > 0f)
+ {
+ _lockTimer -= args.DeltaSeconds;
+ }
+
+ PlaybackSlider.Disabled = _lockTimer > 0f;
+
+ if (_entManager.TryGetComponent(_audio, out AudioComponent? audio))
+ {
+ DurationLabel.Text = $@"{TimeSpan.FromSeconds(audio.PlaybackPosition):mm\:ss} / {_audioSystem.GetAudioLength(audio.FileName):mm\:ss}";
+ }
+ else
+ {
+ DurationLabel.Text = $"00:00 / 00:00";
+ }
+
+ if (PlaybackSlider.Grabbed)
+ return;
+
+ if (audio != null || _entManager.TryGetComponent(_audio, out audio))
+ {
+ PlaybackSlider.SetValueWithoutEvent(audio.PlaybackPosition);
+ }
+ else
+ {
+ PlaybackSlider.SetValueWithoutEvent(0f);
+ }
+
+ SetPlayPauseButton(_audioSystem.IsPlaying(_audio, audio));
+ }
+
+ public void SetSelectedSongText(string? text)
+ {
+ if (!string.IsNullOrEmpty(text))
+ {
+ SongName.Text = text;
+ }
+ else
+ {
+ SongName.Text = "---";
+ }
+ }
+}
diff --git a/Content.Client/Audio/Jukebox/JukeboxSystem.cs b/Content.Client/Audio/Jukebox/JukeboxSystem.cs
new file mode 100644
index 00000000000..dd4a5bbb9b0
--- /dev/null
+++ b/Content.Client/Audio/Jukebox/JukeboxSystem.cs
@@ -0,0 +1,145 @@
+using Content.Shared.Audio.Jukebox;
+using Robust.Client.Animations;
+using Robust.Client.GameObjects;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Audio.Jukebox;
+
+
+public sealed class JukeboxSystem : SharedJukeboxSystem
+{
+ [Dependency] private readonly IPrototypeManager _protoManager = default!;
+ [Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
+ [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnAppearanceChange);
+ SubscribeLocalEvent(OnAnimationCompleted);
+ SubscribeLocalEvent(OnJukeboxAfterState);
+
+ _protoManager.PrototypesReloaded += OnProtoReload;
+ }
+
+ public override void Shutdown()
+ {
+ base.Shutdown();
+ _protoManager.PrototypesReloaded -= OnProtoReload;
+ }
+
+ private void OnProtoReload(PrototypesReloadedEventArgs obj)
+ {
+ if (!obj.WasModified())
+ return;
+
+ var query = AllEntityQuery();
+
+ while (query.MoveNext(out var uid, out _, out var ui))
+ {
+ if (!_uiSystem.TryGetOpenUi((uid, ui), JukeboxUiKey.Key, out var bui))
+ continue;
+
+ bui.PopulateMusic();
+ }
+ }
+
+ private void OnJukeboxAfterState(Entity ent, ref AfterAutoHandleStateEvent args)
+ {
+ if (!_uiSystem.TryGetOpenUi(ent.Owner, JukeboxUiKey.Key, out var bui))
+ return;
+
+ bui.Reload();
+ }
+
+ private void OnAnimationCompleted(EntityUid uid, JukeboxComponent component, AnimationCompletedEvent args)
+ {
+ if (!TryComp(uid, out var sprite))
+ return;
+
+ if (!TryComp(uid, out var appearance) ||
+ !_appearanceSystem.TryGetData(uid, JukeboxVisuals.VisualState, out var visualState, appearance))
+ {
+ visualState = JukeboxVisualState.On;
+ }
+
+ UpdateAppearance(uid, visualState, component, sprite);
+ }
+
+ private void OnAppearanceChange(EntityUid uid, JukeboxComponent component, ref AppearanceChangeEvent args)
+ {
+ if (args.Sprite == null)
+ return;
+
+ if (!args.AppearanceData.TryGetValue(JukeboxVisuals.VisualState, out var visualStateObject) ||
+ visualStateObject is not JukeboxVisualState visualState)
+ {
+ visualState = JukeboxVisualState.On;
+ }
+
+ UpdateAppearance(uid, visualState, component, args.Sprite);
+ }
+
+ private void UpdateAppearance(EntityUid uid, JukeboxVisualState visualState, JukeboxComponent component, SpriteComponent sprite)
+ {
+ SetLayerState(JukeboxVisualLayers.Base, component.OffState, sprite);
+
+ switch (visualState)
+ {
+ case JukeboxVisualState.On:
+ SetLayerState(JukeboxVisualLayers.Base, component.OnState, sprite);
+ break;
+
+ case JukeboxVisualState.Off:
+ SetLayerState(JukeboxVisualLayers.Base, component.OffState, sprite);
+ break;
+
+ case JukeboxVisualState.Select:
+ PlayAnimation(uid, JukeboxVisualLayers.Base, component.SelectState, 1.0f, sprite);
+ break;
+ }
+ }
+
+ private void PlayAnimation(EntityUid uid, JukeboxVisualLayers layer, string? state, float animationTime, SpriteComponent sprite)
+ {
+ if (string.IsNullOrEmpty(state))
+ return;
+
+ if (!_animationPlayer.HasRunningAnimation(uid, state))
+ {
+ var animation = GetAnimation(layer, state, animationTime);
+ sprite.LayerSetVisible(layer, true);
+ _animationPlayer.Play(uid, animation, state);
+ }
+ }
+
+ private static Animation GetAnimation(JukeboxVisualLayers layer, string state, float animationTime)
+ {
+ return new Animation
+ {
+ Length = TimeSpan.FromSeconds(animationTime),
+ AnimationTracks =
+ {
+ new AnimationTrackSpriteFlick
+ {
+ LayerKey = layer,
+ KeyFrames =
+ {
+ new AnimationTrackSpriteFlick.KeyFrame(state, 0f)
+ }
+ }
+ }
+ };
+ }
+
+ private void SetLayerState(JukeboxVisualLayers layer, string? state, SpriteComponent sprite)
+ {
+ if (string.IsNullOrEmpty(state))
+ return;
+
+ sprite.LayerSetVisible(layer, true);
+ sprite.LayerSetAutoAnimated(layer, true);
+ sprite.LayerSetState(layer, state);
+ }
+}
diff --git a/Content.Client/CardboardBox/CardboardBoxSystem.cs b/Content.Client/CardboardBox/CardboardBoxSystem.cs
index 90a21d8e41b..925013db109 100644
--- a/Content.Client/CardboardBox/CardboardBoxSystem.cs
+++ b/Content.Client/CardboardBox/CardboardBoxSystem.cs
@@ -1,4 +1,5 @@
using System.Numerics;
+using Content.Shared.Body.Components;
using Content.Shared.CardboardBox;
using Content.Shared.CardboardBox.Components;
using Content.Shared.Examine;
@@ -13,9 +14,14 @@ public sealed class CardboardBoxSystem : SharedCardboardBoxSystem
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly ExamineSystemShared _examine = default!;
+ private EntityQuery _bodyQuery;
+
public override void Initialize()
{
base.Initialize();
+
+ _bodyQuery = GetEntityQuery();
+
SubscribeNetworkEvent(OnBoxEffect);
}
@@ -59,6 +65,10 @@ private void OnBoxEffect(PlayBoxEffectMessage msg)
if (!_examine.InRangeUnOccluded(sourcePos, mapPos, box.Distance, null))
continue;
+ // no effect for anything too exotic
+ if (!_bodyQuery.HasComp(mob))
+ continue;
+
var ent = Spawn(box.Effect, mapPos);
if (!xformQuery.TryGetComponent(ent, out var entTransform) || !TryComp(ent, out var sprite))
diff --git a/Content.Client/Chemistry/Components/SolutionItemStatusComponent.cs b/Content.Client/Chemistry/Components/SolutionItemStatusComponent.cs
new file mode 100644
index 00000000000..58c5a05894b
--- /dev/null
+++ b/Content.Client/Chemistry/Components/SolutionItemStatusComponent.cs
@@ -0,0 +1,22 @@
+using Content.Client.Chemistry.EntitySystems;
+using Content.Client.Chemistry.UI;
+
+namespace Content.Client.Chemistry.Components;
+
+///
+/// Exposes a solution container's contents via a basic item status control.
+///
+///
+/// Shows the solution volume, max volume, and transfer amount.
+///
+///
+///
+[RegisterComponent]
+public sealed partial class SolutionItemStatusComponent : Component
+{
+ ///
+ /// The ID of the solution that will be shown on the item status control.
+ ///
+ [DataField, ViewVariables(VVAccess.ReadWrite)]
+ public string Solution = "default";
+}
diff --git a/Content.Client/Chemistry/EntitySystems/SolutionItemStatusSystem.cs b/Content.Client/Chemistry/EntitySystems/SolutionItemStatusSystem.cs
new file mode 100644
index 00000000000..76aab516a77
--- /dev/null
+++ b/Content.Client/Chemistry/EntitySystems/SolutionItemStatusSystem.cs
@@ -0,0 +1,22 @@
+using Content.Client.Chemistry.Components;
+using Content.Client.Chemistry.UI;
+using Content.Client.Items;
+using Content.Shared.Chemistry.EntitySystems;
+
+namespace Content.Client.Chemistry.EntitySystems;
+
+///
+/// Wires up item status logic for .
+///
+///
+public sealed class SolutionItemStatusSystem : EntitySystem
+{
+ [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ Subs.ItemStatus(
+ entity => new SolutionStatusControl(entity, EntityManager, _solutionContainerSystem));
+ }
+}
diff --git a/Content.Client/Chemistry/UI/SolutionStatusControl.cs b/Content.Client/Chemistry/UI/SolutionStatusControl.cs
new file mode 100644
index 00000000000..1a33ffb0e14
--- /dev/null
+++ b/Content.Client/Chemistry/UI/SolutionStatusControl.cs
@@ -0,0 +1,59 @@
+using Content.Client.Chemistry.Components;
+using Content.Client.Chemistry.EntitySystems;
+using Content.Client.Items.UI;
+using Content.Client.Message;
+using Content.Client.Stylesheets;
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.FixedPoint;
+using Robust.Client.UserInterface.Controls;
+
+namespace Content.Client.Chemistry.UI;
+
+///
+/// Displays basic solution information for .
+///
+///
+public sealed class SolutionStatusControl : PollingItemStatusControl
+{
+ private readonly Entity _parent;
+ private readonly IEntityManager _entityManager;
+ private readonly SharedSolutionContainerSystem _solutionContainers;
+ private readonly RichTextLabel _label;
+
+ public SolutionStatusControl(
+ Entity parent,
+ IEntityManager entityManager,
+ SharedSolutionContainerSystem solutionContainers)
+ {
+ _parent = parent;
+ _entityManager = entityManager;
+ _solutionContainers = solutionContainers;
+ _label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } };
+ AddChild(_label);
+ }
+
+ protected override Data PollData()
+ {
+ if (!_solutionContainers.TryGetSolution(_parent.Owner, _parent.Comp.Solution, out _, out var solution))
+ return default;
+
+ FixedPoint2? transferAmount = null;
+ if (_entityManager.TryGetComponent(_parent.Owner, out SolutionTransferComponent? transfer))
+ transferAmount = transfer.TransferAmount;
+
+ return new Data(solution.Volume, solution.MaxVolume, transferAmount);
+ }
+
+ protected override void Update(in Data data)
+ {
+ var markup = Loc.GetString("solution-status-volume",
+ ("currentVolume", data.Volume),
+ ("maxVolume", data.MaxVolume));
+ if (data.TransferVolume is { } transferVolume)
+ markup += "\n" + Loc.GetString("solution-status-transfer", ("volume", transferVolume));
+ _label.SetMarkup(markup);
+ }
+
+ public readonly record struct Data(FixedPoint2 Volume, FixedPoint2 MaxVolume, FixedPoint2? TransferVolume);
+}
diff --git a/Content.Client/Clothing/ClientClothingSystem.cs b/Content.Client/Clothing/ClientClothingSystem.cs
index 7e78ac7d707..6d13bf4edab 100644
--- a/Content.Client/Clothing/ClientClothingSystem.cs
+++ b/Content.Client/Clothing/ClientClothingSystem.cs
@@ -11,6 +11,7 @@
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
+using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.TypeSerializers.Implementations;
using Robust.Shared.Utility;
using static Robust.Client.GameObjects.SpriteComponent;
@@ -46,6 +47,7 @@ public sealed class ClientClothingSystem : ClothingSystem
};
[Dependency] private readonly IResourceCache _cache = default!;
+ [Dependency] private readonly ISerializationManager _serialization = default!;
[Dependency] private readonly InventorySystem _inventorySystem = default!;
public override void Initialize()
@@ -265,6 +267,7 @@ private void RenderEquipment(EntityUid equipee, EntityUid equipment, string slot
// temporary, until layer draw depths get added. Basically: a layer with the key "slot" is being used as a
// bookmark to determine where in the list of layers we should insert the clothing layers.
bool slotLayerExists = sprite.LayerMapTryGet(slot, out var index);
+ var displacementData = inventory.Displacements.GetValueOrDefault(slot);
// add the new layers
foreach (var (key, layerData) in ev.Layers)
@@ -304,10 +307,29 @@ private void RenderEquipment(EntityUid equipee, EntityUid equipment, string slot
// Sprite layer redactor when
// Sprite "redactor" just a week away.
if (slot == Jumpsuit)
- layerData.Shader ??= "StencilDraw";
+ layerData.Shader ??= inventory.JumpsuitShader;
sprite.LayerSetData(index, layerData);
layer.Offset += slotDef.Offset;
+
+ if (displacementData != null)
+ {
+ var displacementKey = $"{key}-displacement";
+ if (!revealedLayers.Add(displacementKey))
+ {
+ Log.Warning($"Duplicate key for clothing visuals DISPLACEMENT: {displacementKey}.");
+ continue;
+ }
+
+ var displacementLayer = _serialization.CreateCopy(displacementData.Layer, notNullableOverride: true);
+ displacementLayer.CopyToShaderParameters!.LayerKey = key;
+
+ // Add before main layer for this item.
+ sprite.AddLayer(displacementLayer, index);
+ sprite.LayerMapSet(displacementKey, index);
+
+ revealedLayers.Add(displacementKey);
+ }
}
RaiseLocalEvent(equipment, new EquipmentVisualsUpdatedEvent(equipee, slot, revealedLayers), true);
diff --git a/Content.Client/Clothing/Systems/WaddleClothingSystem.cs b/Content.Client/Clothing/Systems/WaddleClothingSystem.cs
new file mode 100644
index 00000000000..b8ac3c207bf
--- /dev/null
+++ b/Content.Client/Clothing/Systems/WaddleClothingSystem.cs
@@ -0,0 +1,31 @@
+using Content.Shared.Clothing.Components;
+using Content.Shared.Movement.Components;
+using Content.Shared.Inventory.Events;
+
+namespace Content.Client.Clothing.Systems;
+
+public sealed class WaddleClothingSystem : EntitySystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnGotEquipped);
+ SubscribeLocalEvent(OnGotUnequipped);
+ }
+
+ private void OnGotEquipped(EntityUid entity, WaddleWhenWornComponent comp, GotEquippedEvent args)
+ {
+ var waddleAnimComp = EnsureComp(args.Equipee);
+
+ waddleAnimComp.AnimationLength = comp.AnimationLength;
+ waddleAnimComp.HopIntensity = comp.HopIntensity;
+ waddleAnimComp.RunAnimationLengthMultiplier = comp.RunAnimationLengthMultiplier;
+ waddleAnimComp.TumbleIntensity = comp.TumbleIntensity;
+ }
+
+ private void OnGotUnequipped(EntityUid entity, WaddleWhenWornComponent comp, GotUnequippedEvent args)
+ {
+ RemComp(args.Equipee);
+ }
+}
diff --git a/Content.Client/Communications/UI/CommunicationsConsoleMenu.xaml.cs b/Content.Client/Communications/UI/CommunicationsConsoleMenu.xaml.cs
index 90643e45cf7..4d8dd86a4dc 100644
--- a/Content.Client/Communications/UI/CommunicationsConsoleMenu.xaml.cs
+++ b/Content.Client/Communications/UI/CommunicationsConsoleMenu.xaml.cs
@@ -1,7 +1,9 @@
using Content.Client.UserInterface.Controls;
using System.Threading;
+using Content.Shared.CCVar;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Configuration;
using Robust.Shared.Utility;
using Timer = Robust.Shared.Timing.Timer;
@@ -13,6 +15,8 @@ public sealed partial class CommunicationsConsoleMenu : FancyWindow
private CommunicationsConsoleBoundUserInterface Owner { get; set; }
private readonly CancellationTokenSource _timerCancelTokenSource = new();
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
+
public CommunicationsConsoleMenu(CommunicationsConsoleBoundUserInterface owner)
{
IoCManager.InjectDependencies(this);
@@ -23,6 +27,22 @@ public CommunicationsConsoleMenu(CommunicationsConsoleBoundUserInterface owner)
var loc = IoCManager.Resolve();
MessageInput.Placeholder = new Rope.Leaf(loc.GetString("comms-console-menu-announcement-placeholder"));
+ var maxAnnounceLength = _cfg.GetCVar(CCVars.ChatMaxAnnouncementLength);
+ MessageInput.OnTextChanged += (args) =>
+ {
+ if (args.Control.TextLength > maxAnnounceLength)
+ {
+ AnnounceButton.Disabled = true;
+ AnnounceButton.ToolTip = Loc.GetString("comms-console-message-too-long");
+ }
+ else
+ {
+ AnnounceButton.Disabled = !owner.CanAnnounce;
+ AnnounceButton.ToolTip = null;
+
+ }
+ };
+
AnnounceButton.OnPressed += (_) => Owner.AnnounceButtonPressed(Rope.Collapse(MessageInput.TextRope));
AnnounceButton.Disabled = !owner.CanAnnounce;
diff --git a/Content.Client/Corvax/TTS/HumanoidProfileEditor.TTS.cs b/Content.Client/Corvax/TTS/HumanoidProfileEditor.TTS.cs
index ce54a881997..7a75225eaf3 100644
--- a/Content.Client/Corvax/TTS/HumanoidProfileEditor.TTS.cs
+++ b/Content.Client/Corvax/TTS/HumanoidProfileEditor.TTS.cs
@@ -1,38 +1,19 @@
using System.Linq;
using Content.Client.Corvax.TTS;
+using Content.Client.Lobby;
using Content.Shared.Corvax.TTS;
using Content.Shared.Preferences;
-using Robust.Shared.Random;
using Content.Corvax.Interfaces.Client;
namespace Content.Client.Preferences.UI;
public sealed partial class HumanoidProfileEditor
{
- private IRobustRandom _random = default!;
- private TTSSystem _ttsSys = default!;
private IClientSponsorsManager? _sponsorsMgr;
private List _voiceList = default!;
- private readonly List _sampleText = new()
- {
- "Съешь же ещё этих мягких французских булок, да выпей чаю.",
- "Клоун, прекрати разбрасывать банановые кожурки офицерам под ноги!",
- "Капитан, вы уверены что хотите назначить клоуна на должность главы персонала?",
- "Эс Бэ! Тут человек в сером костюме, с тулбоксом и в маске! Помогите!!",
- "Учёные, тут странная аномалия в баре! Она уже съела мима!",
- "Я надеюсь что инженеры внимательно следят за сингулярностью...",
- "Вы слышали эти странные крики в техах? Мне кажется туда ходить небезопасно.",
- "Вы не видели Гамлета? Мне кажется он забегал к вам на кухню.",
- "Здесь есть доктор? Человек умирает от отравленного пончика! Нужна помощь!",
- "Вам нужно согласие и печать квартирмейстера, если вы хотите сделать заказ на партию дробовиков.",
- "Возле эвакуационного шаттла разгерметизация! Инженеры, нам срочно нужна ваша помощь!",
- "Бармен, налей мне самого крепкого вина, которое есть в твоих запасах!"
- };
private void InitializeVoice()
{
- _random = IoCManager.Resolve();
- _ttsSys = _entMan.System();
_voiceList = _prototypeManager
.EnumeratePrototypes()
.Where(o => o.RoundStart)
@@ -45,7 +26,7 @@ private void InitializeVoice()
SetVoice(_voiceList[args.Id].ID);
};
- _voicePlayButton.OnPressed += _ => { PlayTTS(); };
+ _voicePlayButton.OnPressed += _ => { UserInterfaceManager.GetUIController().PlayTTS(); };
IoCManager.Instance!.TryResolveType(out _sponsorsMgr);
}
@@ -85,12 +66,4 @@ private void UpdateTTSVoicesControls()
SetVoice(_voiceList[firstVoiceChoiceId].ID);
}
}
-
- private void PlayTTS()
- {
- if (_previewDummy is null || Profile is null)
- return;
-
- _ttsSys.RequestGlobalTTS(_random.Pick(_sampleText), Profile.Voice);
- }
}
diff --git a/Content.Client/Corvax/TTS/LobbyUIController.TTS.cs b/Content.Client/Corvax/TTS/LobbyUIController.TTS.cs
new file mode 100644
index 00000000000..f59cf353b2b
--- /dev/null
+++ b/Content.Client/Corvax/TTS/LobbyUIController.TTS.cs
@@ -0,0 +1,37 @@
+using Content.Client.Corvax.TTS;
+using Robust.Client.UserInterface;
+using Robust.Shared.Random;
+
+namespace Content.Client.Lobby;
+
+public sealed partial class LobbyUIController
+{
+ [Dependency] private readonly IRobustRandom _rng = default!;
+ [UISystemDependency] private readonly TTSSystem _tts = default!;
+
+ private readonly List _sampleText =
+ new()
+ {
+ "Съешь же ещё этих мягких французских булок, да выпей чаю.",
+ "Клоун, прекрати разбрасывать банановые кожурки офицерам под ноги!",
+ "Капитан, вы уверены что хотите назначить клоуна на должность главы персонала?",
+ "Эс Бэ! Тут человек в сером костюме, с тулбоксом и в маске! Помогите!!",
+ "Учёные, тут странная аномалия в баре! Она уже съела мима!",
+ "Я надеюсь что инженеры внимательно следят за сингулярностью...",
+ "Вы слышали эти странные крики в техах? Мне кажется туда ходить небезопасно.",
+ "Вы не видели Гамлета? Мне кажется он забегал к вам на кухню.",
+ "Здесь есть доктор? Человек умирает от отравленного пончика! Нужна помощь!",
+ "Вам нужно согласие и печать квартирмейстера, если вы хотите сделать заказ на партию дробовиков.",
+ "Возле эвакуационного шаттла разгерметизация! Инженеры, нам срочно нужна ваша помощь!",
+ "Бармен, налей мне самого крепкого вина, которое есть в твоих запасах!"
+ };
+
+ public void PlayTTS()
+ {
+ // Test moment
+ if (_profile == null || _stateManager.CurrentState is not LobbyState)
+ return;
+
+ _tts.RequestGlobalTTS(_rng.Pick(_sampleText), _profile.Voice);
+ }
+}
diff --git a/Content.Client/DeviceNetwork/JammerSystem.cs b/Content.Client/DeviceNetwork/JammerSystem.cs
new file mode 100644
index 00000000000..c7dbf8c8fec
--- /dev/null
+++ b/Content.Client/DeviceNetwork/JammerSystem.cs
@@ -0,0 +1,8 @@
+using Content.Shared.Radio.EntitySystems;
+
+namespace Content.Client.DeviceNetwork;
+
+public sealed class JammerSystem : SharedJammerSystem
+{
+
+}
diff --git a/Content.Client/DoAfter/DoAfterOverlay.cs b/Content.Client/DoAfter/DoAfterOverlay.cs
index 2e23dd44cac..45981159f06 100644
--- a/Content.Client/DoAfter/DoAfterOverlay.cs
+++ b/Content.Client/DoAfter/DoAfterOverlay.cs
@@ -21,7 +21,7 @@ public sealed class DoAfterOverlay : Overlay
private readonly ProgressColorSystem _progressColor;
private readonly Texture _barTexture;
- private readonly ShaderInstance _shader;
+ private readonly ShaderInstance _unshadedShader;
///
/// Flash time for cancelled DoAfters
@@ -45,7 +45,7 @@ public DoAfterOverlay(IEntityManager entManager, IPrototypeManager protoManager,
var sprite = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/progress_bar.rsi"), "icon");
_barTexture = _entManager.EntitySysManager.GetEntitySystem().Frame0(sprite);
- _shader = protoManager.Index("unshaded").Instance();
+ _unshadedShader = protoManager.Index("unshaded").Instance();
}
protected override void Draw(in OverlayDrawArgs args)
@@ -58,7 +58,6 @@ protected override void Draw(in OverlayDrawArgs args)
const float scale = 1f;
var scaleMatrix = Matrix3.CreateScale(new Vector2(scale, scale));
var rotationMatrix = Matrix3.CreateRotation(-rotation);
- handle.UseShader(_shader);
var curTime = _timing.CurTime;
@@ -79,6 +78,13 @@ protected override void Draw(in OverlayDrawArgs args)
if (!bounds.Contains(worldPosition))
continue;
+ // shades the do-after bar if the do-after bar belongs to other players
+ // does not shade do-afters belonging to the local player
+ if (uid != localEnt)
+ handle.UseShader(null);
+ else
+ handle.UseShader(_unshadedShader);
+
// If the entity is paused, we will draw the do-after as it was when the entity got paused.
var meta = metaQuery.GetComponent(uid);
var time = meta.EntityPaused
diff --git a/Content.Client/Extinguisher/FireExtinguisherComponent.cs b/Content.Client/Extinguisher/FireExtinguisherComponent.cs
index 126c172924b..324b05a93d4 100644
--- a/Content.Client/Extinguisher/FireExtinguisherComponent.cs
+++ b/Content.Client/Extinguisher/FireExtinguisherComponent.cs
@@ -3,7 +3,5 @@
namespace Content.Client.Extinguisher;
-[NetworkedComponent, RegisterComponent]
-public sealed partial class FireExtinguisherComponent : SharedFireExtinguisherComponent
-{
-}
+[RegisterComponent]
+public sealed partial class FireExtinguisherComponent : SharedFireExtinguisherComponent;
diff --git a/Content.Client/GameTicking/Managers/ClientGameTicker.cs b/Content.Client/GameTicking/Managers/ClientGameTicker.cs
index df709e94446..309db2eb4e6 100644
--- a/Content.Client/GameTicking/Managers/ClientGameTicker.cs
+++ b/Content.Client/GameTicking/Managers/ClientGameTicker.cs
@@ -1,3 +1,4 @@
+using Content.Client.Administration.Managers;
using Content.Client.Gameplay;
using Content.Client.Lobby;
using Content.Client.RoundEnd;
@@ -6,7 +7,7 @@
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.State;
-using Robust.Shared.Utility;
+using Robust.Client.UserInterface;
namespace Content.Client.GameTicking.Managers
{
@@ -14,17 +15,14 @@ namespace Content.Client.GameTicking.Managers
public sealed class ClientGameTicker : SharedGameTicker
{
[Dependency] private readonly IStateManager _stateManager = default!;
- [Dependency] private readonly IEntityManager _entityManager = default!;
+ [Dependency] private readonly IClientAdminManager _admin = default!;
+ [Dependency] private readonly IClyde _clyde = default!;
+ [Dependency] private readonly SharedMapSystem _map = default!;
+ [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
- [ViewVariables] private bool _initialized;
private Dictionary> _jobsAvailable = new();
private Dictionary _stationNames = new();
- ///
- /// The current round-end window. Could be used to support re-opening the window after closing it.
- ///
- private RoundEndSummaryWindow? _window;
-
[ViewVariables] public bool AreWeReady { get; private set; }
[ViewVariables] public bool IsGameStarted { get; private set; }
[ViewVariables] public string? RestartSound { get; private set; }
@@ -44,8 +42,6 @@ public sealed class ClientGameTicker : SharedGameTicker
public override void Initialize()
{
- DebugTools.Assert(!_initialized);
-
SubscribeNetworkEvent(JoinLobby);
SubscribeNetworkEvent(JoinGame);
SubscribeNetworkEvent(ConnectionStatus);
@@ -53,14 +49,33 @@ public override void Initialize()
SubscribeNetworkEvent(LobbyInfo);
SubscribeNetworkEvent(LobbyCountdown);
SubscribeNetworkEvent(RoundEnd);
- SubscribeNetworkEvent(msg =>
- {
- IoCManager.Resolve().RequestWindowAttention();
- });
+ SubscribeNetworkEvent(OnAttentionRequest);
SubscribeNetworkEvent(LateJoinStatus);
SubscribeNetworkEvent(UpdateJobsAvailable);
- _initialized = true;
+ _admin.AdminStatusUpdated += OnAdminUpdated;
+ OnAdminUpdated();
+ }
+
+ public override void Shutdown()
+ {
+ _admin.AdminStatusUpdated -= OnAdminUpdated;
+ base.Shutdown();
+ }
+
+ private void OnAdminUpdated()
+ {
+ // Hide some map/grid related logs from clients. This is to try prevent some easy metagaming by just
+ // reading the console. E.g., logs like this one could leak the nuke station/grid:
+ // > Grid NT-Arrivals 1101 (122/n25896) changed parent. Old parent: map 10 (121/n25895). New parent: FTL (123/n26470)
+#if !DEBUG
+ _map.Log.Level = _admin.IsAdmin() ? LogLevel.Info : LogLevel.Warning;
+#endif
+ }
+
+ private void OnAttentionRequest(RequestWindowAttentionEvent ev)
+ {
+ _clyde.RequestWindowAttention();
}
private void LateJoinStatus(TickerLateJoinStatusEvent message)
@@ -132,12 +147,7 @@ private void RoundEnd(RoundEndMessageEvent message)
// Force an update in the event of this song being the same as the last.
RestartSound = message.RestartSound;
- // Don't open duplicate windows (mainly for replays).
- if (_window?.RoundId == message.RoundId)
- return;
-
- //This is not ideal at all, but I don't see an immediately better fit anywhere else.
- _window = new RoundEndSummaryWindow(message.GamemodeTitle, message.RoundEndText, message.RoundDuration, message.RoundId, message.AllPlayersEndInfo, _entityManager);
+ _userInterfaceManager.GetUIController().OpenRoundEndSummaryWindow(message);
}
}
}
diff --git a/Content.Client/Ghost/Commands/ToggleGhostVisibilityCommand.cs b/Content.Client/Ghost/Commands/ToggleGhostVisibilityCommand.cs
new file mode 100644
index 00000000000..480da6ad8d9
--- /dev/null
+++ b/Content.Client/Ghost/Commands/ToggleGhostVisibilityCommand.cs
@@ -0,0 +1,26 @@
+using Robust.Shared.Console;
+
+namespace Content.Client.Ghost.Commands;
+
+public sealed class ToggleGhostVisibilityCommand : IConsoleCommand
+{
+ [Dependency] private readonly IEntitySystemManager _entSysMan = default!;
+
+ public string Command => "toggleghostvisibility";
+ public string Description => "Toggles ghost visibility on the client.";
+ public string Help => "toggleghostvisibility [bool]";
+
+ public void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ var ghostSystem = _entSysMan.GetEntitySystem();
+
+ if (args.Length != 0 && bool.TryParse(args[0], out var visibility))
+ {
+ ghostSystem.ToggleGhostVisibility(visibility);
+ }
+ else
+ {
+ ghostSystem.ToggleGhostVisibility();
+ }
+ }
+}
diff --git a/Content.Client/Ghost/GhostSystem.cs b/Content.Client/Ghost/GhostSystem.cs
index c42e7cd0e0c..94872a58ef9 100644
--- a/Content.Client/Ghost/GhostSystem.cs
+++ b/Content.Client/Ghost/GhostSystem.cs
@@ -3,7 +3,6 @@
using Content.Shared.Ghost;
using Robust.Client.Console;
using Robust.Client.GameObjects;
-using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Player;
@@ -177,9 +176,9 @@ public void OpenGhostRoles()
_console.RemoteExecuteCommand(null, "ghostroles");
}
- public void ToggleGhostVisibility()
+ public void ToggleGhostVisibility(bool? visibility = null)
{
- GhostVisibility = !GhostVisibility;
+ GhostVisibility = visibility ?? !GhostVisibility;
}
}
}
diff --git a/Content.Client/Guidebook/GuideEntry.cs b/Content.Client/Guidebook/GuideEntry.cs
index b3c004267db..b7b3b3309e6 100644
--- a/Content.Client/Guidebook/GuideEntry.cs
+++ b/Content.Client/Guidebook/GuideEntry.cs
@@ -42,7 +42,7 @@ public class GuideEntry
}
[Prototype("guideEntry")]
-public sealed class GuideEntryPrototype : GuideEntry, IPrototype
+public sealed partial class GuideEntryPrototype : GuideEntry, IPrototype
{
public string ID => Id;
}
diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
index 1d8d415bab9..0cb3ad144d7 100644
--- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
+++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
@@ -1,5 +1,6 @@
using System.Linq;
using System.Numerics;
+using Content.Shared.Atmos;
using Content.Client.UserInterface.Controls;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
@@ -79,7 +80,7 @@ public void Populate(HealthAnalyzerScannedUserMessage msg)
);
Temperature.Text = Loc.GetString("health-analyzer-window-entity-temperature-text",
- ("temperature", float.IsNaN(msg.Temperature) ? "N/A" : $"{msg.Temperature - 273f:F1} °C ({msg.Temperature:F1} °K)")
+ ("temperature", float.IsNaN(msg.Temperature) ? "N/A" : $"{msg.Temperature - Atmospherics.T0C:F1} °C ({msg.Temperature:F1} K)")
);
BloodLevel.Text = Loc.GetString("health-analyzer-window-entity-blood-level-text",
diff --git a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs
index 5bae35da5ba..6eb5dd9ec98 100644
--- a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs
+++ b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs
@@ -108,8 +108,11 @@ private void SetLayerData(
/// This should not be used if the entity is owned by the server. The server will otherwise
/// override this with the appearance data it sends over.
///
- public override void LoadProfile(EntityUid uid, HumanoidCharacterProfile profile, HumanoidAppearanceComponent? humanoid = null)
+ public override void LoadProfile(EntityUid uid, HumanoidCharacterProfile? profile, HumanoidAppearanceComponent? humanoid = null)
{
+ if (profile == null)
+ return;
+
if (!Resolve(uid, ref humanoid))
{
return;
diff --git a/Content.Client/Implants/UI/ImplanterStatusControl.cs b/Content.Client/Implants/UI/ImplanterStatusControl.cs
index f3f0cdea7d7..e2ffabd17d9 100644
--- a/Content.Client/Implants/UI/ImplanterStatusControl.cs
+++ b/Content.Client/Implants/UI/ImplanterStatusControl.cs
@@ -1,5 +1,6 @@
using Content.Client.Message;
using Content.Client.Stylesheets;
+using Content.Client.UserInterface.Controls;
using Content.Shared.Implants.Components;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
@@ -17,7 +18,7 @@ public ImplanterStatusControl(ImplanterComponent parent)
_parent = parent;
_label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } };
_label.MaxWidth = 350;
- AddChild(_label);
+ AddChild(new ClipControl { Children = { _label } });
Update();
}
@@ -42,17 +43,12 @@ private void Update()
_ => Loc.GetString("injector-invalid-injector-toggle-mode")
};
- var (implantName, implantDescription) = _parent.ImplanterSlot.HasItem switch
- {
- false => (Loc.GetString("implanter-empty-text"), ""),
- true => (_parent.ImplantData.Item1, _parent.ImplantData.Item2),
- };
-
+ var implantName = _parent.ImplanterSlot.HasItem
+ ? _parent.ImplantData.Item1
+ : Loc.GetString("implanter-empty-text");
_label.SetMarkup(Loc.GetString("implanter-label",
("implantName", implantName),
- ("implantDescription", implantDescription),
- ("modeString", modeStringLocalized),
- ("lineBreak", "\n")));
+ ("modeString", modeStringLocalized)));
}
}
diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs
index 2e888b3df98..8a7ca3b7735 100644
--- a/Content.Client/Input/ContentContexts.cs
+++ b/Content.Client/Input/ContentContexts.cs
@@ -38,6 +38,7 @@ public static void SetupContexts(IInputContextContainer contexts)
common.AddFunction(ContentKeyFunctions.ZoomIn);
common.AddFunction(ContentKeyFunctions.ResetZoom);
common.AddFunction(ContentKeyFunctions.InspectEntity);
+ common.AddFunction(ContentKeyFunctions.ToggleRoundEndSummaryWindow);
// Not in engine, because engine cannot check for sanbox/admin status before starting placement.
common.AddFunction(ContentKeyFunctions.EditorCopyObject);
diff --git a/Content.Client/Inventory/ClientInventorySystem.cs b/Content.Client/Inventory/ClientInventorySystem.cs
index 7b98513a929..87cea4e3d2f 100644
--- a/Content.Client/Inventory/ClientInventorySystem.cs
+++ b/Content.Client/Inventory/ClientInventorySystem.cs
@@ -199,7 +199,7 @@ public void UIInventoryActivate(string slot)
public void UIInventoryStorageActivate(string slot)
{
- EntityManager.EntityNetManager?.SendSystemNetworkMessage(new OpenSlotStorageNetworkMessage(slot));
+ EntityManager.RaisePredictiveEvent(new OpenSlotStorageNetworkMessage(slot));
}
public void UIInventoryExamine(string slot, EntityUid uid)
@@ -251,6 +251,7 @@ public sealed class SlotData
public string SlotGroup => SlotDef.SlotGroup;
public string SlotDisplayName => SlotDef.DisplayName;
public string TextureName => "Slots/" + SlotDef.TextureName;
+ public string FullTextureName => SlotDef.FullTextureName;
public SlotData(SlotDefinition slotDef, ContainerSlot? container = null, bool highlighted = false,
bool blocked = false)
diff --git a/Content.Client/Inventory/StrippableBoundUserInterface.cs b/Content.Client/Inventory/StrippableBoundUserInterface.cs
index f8eb12df914..33f38688edf 100644
--- a/Content.Client/Inventory/StrippableBoundUserInterface.cs
+++ b/Content.Client/Inventory/StrippableBoundUserInterface.cs
@@ -219,7 +219,7 @@ private void UpdateEntityIcon(SlotControl button, EntityUid? entity)
if (entity == null)
{
- button.SpriteView.SetEntity(null);
+ button.SetEntity(null);
return;
}
@@ -231,7 +231,7 @@ private void UpdateEntityIcon(SlotControl button, EntityUid? entity)
else
return;
- button.SpriteView.SetEntity(viewEnt);
+ button.SetEntity(viewEnt);
}
}
}
diff --git a/Content.Client/IoC/ClientContentIoC.cs b/Content.Client/IoC/ClientContentIoC.cs
index 18b9852eee3..7362225e05f 100644
--- a/Content.Client/IoC/ClientContentIoC.cs
+++ b/Content.Client/IoC/ClientContentIoC.cs
@@ -22,6 +22,7 @@
using Content.Client.Guidebook;
using Content.Client.Replay;
using Content.Shared.Administration.Managers;
+using Content.Shared.Players.PlayTimeTracking;
namespace Content.Client.IoC
@@ -30,26 +31,29 @@ internal static class ClientContentIoC
{
public static void Register()
{
- IoCManager.Register();
- IoCManager.Register();
- IoCManager.Register();
- IoCManager.Register();
- IoCManager.Register();
- IoCManager.Register();
- IoCManager.Register();
- IoCManager.Register();
- IoCManager.Register();
- IoCManager.Register();
- IoCManager.Register();
- IoCManager.Register();
- IoCManager.Register();
- IoCManager.Register();
- IoCManager.Register();
- IoCManager.Register();
- IoCManager.Register();
- IoCManager.Register();
- IoCManager.Register();
- IoCManager.Register();
+ var collection = IoCManager.Instance!;
+
+ collection.Register();
+ collection.Register();
+ collection.Register();
+ collection.Register();
+ collection.Register();
+ collection.Register();
+ collection.Register();
+ collection.Register();
+ collection.Register();
+ collection.Register();
+ collection.Register();
+ collection.Register();
+ collection.Register();
+ collection.Register();
+ collection.Register();
+ collection.Register();
+ collection.Register();
+ collection.Register();
+ collection.Register();
+ collection.Register();
+ collection.Register();
}
}
}
diff --git a/Content.Client/Items/UI/PollingItemStatusControl.cs b/Content.Client/Items/UI/PollingItemStatusControl.cs
new file mode 100644
index 00000000000..39cffb06f6a
--- /dev/null
+++ b/Content.Client/Items/UI/PollingItemStatusControl.cs
@@ -0,0 +1,28 @@
+using Robust.Client.UserInterface;
+using Robust.Shared.Timing;
+
+namespace Content.Client.Items.UI;
+
+///
+/// A base for item status controls that poll data every frame. Avoids UI updates if data didn't change.
+///
+/// The full status control data that is polled every frame.
+public abstract class PollingItemStatusControl : Control where TData : struct, IEquatable
+{
+ private TData _lastData;
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ base.FrameUpdate(args);
+
+ var newData = PollData();
+ if (newData.Equals(_lastData))
+ return;
+
+ _lastData = newData;
+ Update(newData);
+ }
+
+ protected abstract TData PollData();
+ protected abstract void Update(in TData data);
+}
diff --git a/Content.Client/Lathe/UI/LatheMenu.xaml.cs b/Content.Client/Lathe/UI/LatheMenu.xaml.cs
index ca8d2561270..9e15f8239e5 100644
--- a/Content.Client/Lathe/UI/LatheMenu.xaml.cs
+++ b/Content.Client/Lathe/UI/LatheMenu.xaml.cs
@@ -10,6 +10,8 @@
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
+using Robust.Client.ResourceManagement;
+using Robust.Client.Graphics;
using Robust.Shared.Prototypes;
namespace Content.Client.Lathe.UI;
@@ -19,6 +21,8 @@ public sealed partial class LatheMenu : DefaultWindow
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IResourceCache _resources = default!;
+
private EntityUid _owner;
private readonly SpriteSystem _spriteSystem;
private readonly LatheSystem _lathe;
@@ -104,12 +108,21 @@ public void PopulateRecipes()
RecipeList.Children.Clear();
foreach (var prototype in sortedRecipesToShow)
{
- var icon = prototype.Icon == null
- ? _spriteSystem.GetPrototypeIcon(prototype.Result).Default
- : _spriteSystem.Frame0(prototype.Icon);
+ List textures;
+ if (_prototypeManager.TryIndex(prototype.Result, out EntityPrototype? entityProto) && entityProto != null)
+ {
+ textures = SpriteComponent.GetPrototypeTextures(entityProto, _resources).Select(o => o.Default).ToList();
+ }
+ else
+ {
+ textures = prototype.Icon == null
+ ? new List { _spriteSystem.GetPrototypeIcon(prototype.Result).Default }
+ : new List { _spriteSystem.Frame0(prototype.Icon) };
+ }
+
var canProduce = _lathe.CanProduce(_owner, prototype, quantity);
- var control = new RecipeControl(prototype, () => GenerateTooltipText(prototype), canProduce, icon);
+ var control = new RecipeControl(prototype, () => GenerateTooltipText(prototype), canProduce, textures);
control.OnButtonPressed += s =>
{
if (!int.TryParse(AmountLineEdit.Text, out var amount) || amount <= 0)
diff --git a/Content.Client/Lathe/UI/RecipeControl.xaml b/Content.Client/Lathe/UI/RecipeControl.xaml
index 2e02c8a6147..d1371a026a2 100644
--- a/Content.Client/Lathe/UI/RecipeControl.xaml
+++ b/Content.Client/Lathe/UI/RecipeControl.xaml
@@ -5,11 +5,15 @@
Margin="0"
StyleClasses="ButtonSquare">
-
+ CanShrink="true"
+ />
diff --git a/Content.Client/Lathe/UI/RecipeControl.xaml.cs b/Content.Client/Lathe/UI/RecipeControl.xaml.cs
index bf85ff7d938..47b6b5932c4 100644
--- a/Content.Client/Lathe/UI/RecipeControl.xaml.cs
+++ b/Content.Client/Lathe/UI/RecipeControl.xaml.cs
@@ -2,8 +2,8 @@
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Graphics;
namespace Content.Client.Lathe.UI;
@@ -13,12 +13,12 @@ public sealed partial class RecipeControl : Control
public Action? OnButtonPressed;
public Func TooltipTextSupplier;
- public RecipeControl(LatheRecipePrototype recipe, Func tooltipTextSupplier, bool canProduce, Texture? texture = null)
+ public RecipeControl(LatheRecipePrototype recipe, Func tooltipTextSupplier, bool canProduce, List textures)
{
RobustXamlLoader.Load(this);
RecipeName.Text = recipe.Name;
- RecipeTexture.Texture = texture;
+ RecipeTextures.Textures = textures;
Button.Disabled = !canProduce;
TooltipTextSupplier = tooltipTextSupplier;
Button.TooltipSupplier = SupplyTooltip;
diff --git a/Content.Client/Lobby/LobbyState.cs b/Content.Client/Lobby/LobbyState.cs
index fe31dce0629..91730020a4e 100644
--- a/Content.Client/Lobby/LobbyState.cs
+++ b/Content.Client/Lobby/LobbyState.cs
@@ -64,13 +64,19 @@ protected override void Startup()
_characterSetup.CloseButton.OnPressed += _ =>
{
+ // Reset sliders etc.
+ _characterSetup?.UpdateControls();
+
+ var controller = _userInterfaceManager.GetUIController();
+ controller.SetClothes(true);
+ controller.UpdateProfile();
_lobby.SwitchState(LobbyGui.LobbyGuiState.Default);
};
_characterSetup.SaveButton.OnPressed += _ =>
{
_characterSetup.Save();
- _lobby.CharacterPreview.UpdateUI();
+ _userInterfaceManager.GetUIController().ReloadProfile();
};
LayoutContainer.SetAnchorPreset(_lobby, LayoutContainer.LayoutPreset.Wide);
@@ -84,10 +90,6 @@ protected override void Startup()
_gameTicker.InfoBlobUpdated += UpdateLobbyUi;
_gameTicker.LobbyStatusUpdated += LobbyStatusUpdated;
_gameTicker.LobbyLateJoinStatusUpdated += LobbyLateJoinStatusUpdated;
-
- _preferencesManager.OnServerDataLoaded += PreferencesDataLoaded;
-
- _lobby.CharacterPreview.UpdateUI();
}
protected override void Shutdown()
@@ -109,13 +111,6 @@ protected override void Shutdown()
_characterSetup?.Dispose();
_characterSetup = null;
-
- _preferencesManager.OnServerDataLoaded -= PreferencesDataLoaded;
- }
-
- private void PreferencesDataLoaded()
- {
- _lobby?.CharacterPreview.UpdateUI();
}
private void OnSetupPressed(BaseButton.ButtonEventArgs args)
diff --git a/Content.Client/Lobby/LobbyUIController.cs b/Content.Client/Lobby/LobbyUIController.cs
new file mode 100644
index 00000000000..24c7a5085f8
--- /dev/null
+++ b/Content.Client/Lobby/LobbyUIController.cs
@@ -0,0 +1,287 @@
+using System.Linq;
+using Content.Client.Corvax.TTS;
+using Content.Client.Humanoid;
+using Content.Client.Inventory;
+using Content.Client.Lobby.UI;
+using Content.Client.Preferences;
+using Content.Client.Preferences.UI;
+using Content.Client.Station;
+using Content.Shared.Clothing;
+using Content.Shared.GameTicking;
+using Content.Shared.Humanoid.Prototypes;
+using Content.Shared.Preferences;
+using Content.Shared.Preferences.Loadouts;
+using Content.Shared.Preferences.Loadouts.Effects;
+using Content.Shared.Roles;
+using Robust.Client.State;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controllers;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Lobby;
+
+public sealed partial class LobbyUIController : UIController, IOnStateEntered, IOnStateExited
+{
+ [Dependency] private readonly IClientPreferencesManager _preferencesManager = default!;
+ [Dependency] private readonly IStateManager _stateManager = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [UISystemDependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
+ [UISystemDependency] private readonly ClientInventorySystem _inventory = default!;
+ [UISystemDependency] private readonly StationSpawningSystem _spawn = default!;
+
+ private LobbyCharacterPreviewPanel? _previewPanel;
+
+ private bool _showClothes = true;
+
+ /*
+ * Each character profile has its own dummy. There is also a dummy for the lobby screen + character editor
+ * that is shared too.
+ */
+
+ ///
+ /// Preview dummy for role gear.
+ ///
+ private EntityUid? _previewDummy;
+
+ ///
+ /// If we currently have a job prototype selected.
+ ///
+ private JobPrototype? _dummyJob;
+
+ // TODO: Load the species directly and don't update entity ever.
+ public event Action? PreviewDummyUpdated;
+
+ private HumanoidCharacterProfile? _profile;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ _preferencesManager.OnServerDataLoaded += PreferencesDataLoaded;
+ }
+
+ private void PreferencesDataLoaded()
+ {
+ UpdateProfile();
+ }
+
+ public void OnStateEntered(LobbyState state)
+ {
+ }
+
+ public void OnStateExited(LobbyState state)
+ {
+ EntityManager.DeleteEntity(_previewDummy);
+ _previewDummy = null;
+ }
+
+ public void SetPreviewPanel(LobbyCharacterPreviewPanel? panel)
+ {
+ _previewPanel = panel;
+ ReloadProfile();
+ }
+
+ public void SetClothes(bool value)
+ {
+ if (_showClothes == value)
+ return;
+
+ _showClothes = value;
+ ReloadCharacterUI();
+ }
+
+ public void SetDummyJob(JobPrototype? job)
+ {
+ _dummyJob = job;
+ ReloadCharacterUI();
+ }
+
+ ///
+ /// Updates the character only with the specified profile change.
+ ///
+ public void ReloadProfile()
+ {
+ // Test moment
+ if (_profile == null || _stateManager.CurrentState is not LobbyState)
+ return;
+
+ // Ignore job clothes and the likes so we don't spam entities out every frame of color changes.
+ var previewDummy = EnsurePreviewDummy(_profile);
+ _humanoid.LoadProfile(previewDummy, _profile);
+ }
+
+ ///
+ /// Updates the currently selected character's preview.
+ ///
+ public void ReloadCharacterUI()
+ {
+ // Test moment
+ if (_profile == null || _stateManager.CurrentState is not LobbyState)
+ return;
+
+ EntityManager.DeleteEntity(_previewDummy);
+ _previewDummy = null;
+ _previewDummy = EnsurePreviewDummy(_profile);
+ _previewPanel?.SetSprite(_previewDummy.Value);
+ _previewPanel?.SetSummaryText(_profile.Summary);
+ _humanoid.LoadProfile(_previewDummy.Value, _profile);
+
+ if (_showClothes)
+ GiveDummyJobClothesLoadout(_previewDummy.Value, _profile);
+ }
+
+ ///
+ /// Updates character profile to the default.
+ ///
+ public void UpdateProfile()
+ {
+ if (!_preferencesManager.ServerDataLoaded)
+ {
+ _profile = null;
+ return;
+ }
+
+ if (_preferencesManager.Preferences?.SelectedCharacter is HumanoidCharacterProfile selectedCharacter)
+ {
+ _profile = selectedCharacter;
+ _previewPanel?.SetLoaded(true);
+ }
+ else
+ {
+ _previewPanel?.SetSummaryText(string.Empty);
+ _previewPanel?.SetLoaded(false);
+ }
+
+ ReloadCharacterUI();
+ }
+
+ public void UpdateProfile(HumanoidCharacterProfile? profile)
+ {
+ if (_profile?.Equals(profile) == true)
+ return;
+
+ if (_stateManager.CurrentState is not LobbyState)
+ return;
+
+ _profile = profile;
+ }
+
+ private EntityUid EnsurePreviewDummy(HumanoidCharacterProfile profile)
+ {
+ if (_previewDummy != null)
+ return _previewDummy.Value;
+
+ _previewDummy = EntityManager.SpawnEntity(_prototypeManager.Index(profile.Species).DollPrototype, MapCoordinates.Nullspace);
+ PreviewDummyUpdated?.Invoke(_previewDummy.Value);
+ return _previewDummy.Value;
+ }
+
+ ///
+ /// Applies the highest priority job's clothes to the dummy.
+ ///
+ public void GiveDummyJobClothesLoadout(EntityUid dummy, HumanoidCharacterProfile profile)
+ {
+ var job = _dummyJob ?? GetPreferredJob(profile);
+ GiveDummyJobClothes(dummy, profile, job);
+
+ if (_prototypeManager.HasIndex(LoadoutSystem.GetJobPrototype(job.ID)))
+ {
+ var loadout = profile.GetLoadoutOrDefault(LoadoutSystem.GetJobPrototype(job.ID), EntityManager, _prototypeManager);
+ GiveDummyLoadout(dummy, loadout);
+ }
+ }
+
+ ///
+ /// Gets the highest priority job for the profile.
+ ///
+ public JobPrototype GetPreferredJob(HumanoidCharacterProfile profile)
+ {
+ var highPriorityJob = profile.JobPriorities.FirstOrDefault(p => p.Value == JobPriority.High).Key;
+ // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract (what is resharper smoking?)
+ return _prototypeManager.Index(highPriorityJob ?? SharedGameTicker.FallbackOverflowJob);
+ }
+
+ public void GiveDummyLoadout(EntityUid uid, RoleLoadout? roleLoadout)
+ {
+ if (roleLoadout == null)
+ return;
+
+ foreach (var group in roleLoadout.SelectedLoadouts.Values)
+ {
+ foreach (var loadout in group)
+ {
+ if (!_prototypeManager.TryIndex(loadout.Prototype, out var loadoutProto))
+ continue;
+
+ _spawn.EquipStartingGear(uid, _prototypeManager.Index(loadoutProto.Equipment));
+ }
+ }
+ }
+
+ ///
+ /// Applies the specified job's clothes to the dummy.
+ ///
+ public void GiveDummyJobClothes(EntityUid dummy, HumanoidCharacterProfile profile, JobPrototype job)
+ {
+ if (!_inventory.TryGetSlots(dummy, out var slots))
+ return;
+
+ // Apply loadout
+ if (profile.Loadouts.TryGetValue(job.ID, out var jobLoadout))
+ {
+ foreach (var loadouts in jobLoadout.SelectedLoadouts.Values)
+ {
+ foreach (var loadout in loadouts)
+ {
+ if (!_prototypeManager.TryIndex(loadout.Prototype, out var loadoutProto))
+ continue;
+
+ // TODO: Need some way to apply starting gear to an entity coz holy fucking shit dude.
+ var loadoutGear = _prototypeManager.Index(loadoutProto.Equipment);
+
+ foreach (var slot in slots)
+ {
+ var itemType = loadoutGear.GetGear(slot.Name);
+
+ if (_inventory.TryUnequip(dummy, slot.Name, out var unequippedItem, silent: true, force: true, reparent: false))
+ {
+ EntityManager.DeleteEntity(unequippedItem.Value);
+ }
+
+ if (itemType != string.Empty)
+ {
+ var item = EntityManager.SpawnEntity(itemType, MapCoordinates.Nullspace);
+ _inventory.TryEquip(dummy, item, slot.Name, true, true);
+ }
+ }
+ }
+ }
+ }
+
+ if (job.StartingGear == null)
+ return;
+
+ var gear = _prototypeManager.Index(job.StartingGear);
+
+ foreach (var slot in slots)
+ {
+ var itemType = gear.GetGear(slot.Name);
+
+ if (_inventory.TryUnequip(dummy, slot.Name, out var unequippedItem, silent: true, force: true, reparent: false))
+ {
+ EntityManager.DeleteEntity(unequippedItem.Value);
+ }
+
+ if (itemType != string.Empty)
+ {
+ var item = EntityManager.SpawnEntity(itemType, MapCoordinates.Nullspace);
+ _inventory.TryEquip(dummy, item, slot.Name, true, true);
+ }
+ }
+ }
+
+ public EntityUid? GetPreviewDummy()
+ {
+ return _previewDummy;
+ }
+}
diff --git a/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.cs b/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.cs
deleted file mode 100644
index f9481caa3bb..00000000000
--- a/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.cs
+++ /dev/null
@@ -1,166 +0,0 @@
-using System.Linq;
-using System.Numerics;
-using Content.Client.Alerts;
-using Content.Client.Humanoid;
-using Content.Client.Inventory;
-using Content.Client.Preferences;
-using Content.Client.UserInterface.Controls;
-using Content.Shared.GameTicking;
-using Content.Shared.Humanoid.Prototypes;
-using Content.Shared.Inventory;
-using Content.Shared.Preferences;
-using Content.Shared.Roles;
-using Robust.Client.GameObjects;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Shared.Map;
-using Robust.Shared.Prototypes;
-using static Robust.Client.UserInterface.Controls.BoxContainer;
-
-namespace Content.Client.Lobby.UI
-{
- public sealed class LobbyCharacterPreviewPanel : Control
- {
- [Dependency] private readonly IEntityManager _entityManager = default!;
- [Dependency] private readonly IClientPreferencesManager _preferencesManager = default!;
- [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
-
-
- private EntityUid? _previewDummy;
- private readonly Label _summaryLabel;
- private readonly BoxContainer _loaded;
- private readonly BoxContainer _viewBox;
- private readonly Label _unloaded;
-
- public LobbyCharacterPreviewPanel()
- {
- IoCManager.InjectDependencies(this);
- var header = new NanoHeading
- {
- Text = Loc.GetString("lobby-character-preview-panel-header")
- };
-
- CharacterSetupButton = new Button
- {
- Text = Loc.GetString("lobby-character-preview-panel-character-setup-button"),
- HorizontalAlignment = HAlignment.Center,
- Margin = new Thickness(0, 5, 0, 0),
- };
-
- _summaryLabel = new Label
- {
- HorizontalAlignment = HAlignment.Center,
- Margin = new Thickness(3, 3),
- };
-
- var vBox = new BoxContainer
- {
- Orientation = LayoutOrientation.Vertical
- };
- _unloaded = new Label { Text = Loc.GetString("lobby-character-preview-panel-unloaded-preferences-label") };
-
- _loaded = new BoxContainer
- {
- Orientation = LayoutOrientation.Vertical,
- Visible = false
- };
- _viewBox = new BoxContainer
- {
- Orientation = LayoutOrientation.Horizontal,
- HorizontalAlignment = HAlignment.Center,
- };
- var _vSpacer = new VSpacer();
-
- _loaded.AddChild(_summaryLabel);
- _loaded.AddChild(_viewBox);
- _loaded.AddChild(_vSpacer);
- _loaded.AddChild(CharacterSetupButton);
-
- vBox.AddChild(header);
- vBox.AddChild(_loaded);
- vBox.AddChild(_unloaded);
- AddChild(vBox);
-
- UpdateUI();
- }
-
- public Button CharacterSetupButton { get; }
-
- protected override void Dispose(bool disposing)
- {
- base.Dispose(disposing);
-
- if (!disposing) return;
- if (_previewDummy != null) _entityManager.DeleteEntity(_previewDummy.Value);
- _previewDummy = default;
- }
-
- public void UpdateUI()
- {
- if (!_preferencesManager.ServerDataLoaded)
- {
- _loaded.Visible = false;
- _unloaded.Visible = true;
- }
- else
- {
- _loaded.Visible = true;
- _unloaded.Visible = false;
- if (_preferencesManager.Preferences?.SelectedCharacter is not HumanoidCharacterProfile selectedCharacter)
- {
- _summaryLabel.Text = string.Empty;
- }
- else
- {
- _previewDummy = _entityManager.SpawnEntity(_prototypeManager.Index(selectedCharacter.Species).DollPrototype, MapCoordinates.Nullspace);
- _viewBox.DisposeAllChildren();
- var spriteView = new SpriteView
- {
- OverrideDirection = Direction.South,
- Scale = new Vector2(4f, 4f),
- MaxSize = new Vector2(112, 112),
- Stretch = SpriteView.StretchMode.Fill,
- };
- spriteView.SetEntity(_previewDummy.Value);
- _viewBox.AddChild(spriteView);
- _summaryLabel.Text = selectedCharacter.Summary;
- _entityManager.System().LoadProfile(_previewDummy.Value, selectedCharacter);
- GiveDummyJobClothes(_previewDummy.Value, selectedCharacter);
- }
- }
- }
-
- public static void GiveDummyJobClothes(EntityUid dummy, HumanoidCharacterProfile profile)
- {
- var protoMan = IoCManager.Resolve();
- var entMan = IoCManager.Resolve();
- var invSystem = EntitySystem.Get();
-
- var highPriorityJob = profile.JobPriorities.FirstOrDefault(p => p.Value == JobPriority.High).Key;
-
- // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract (what is resharper smoking?)
- var job = protoMan.Index(highPriorityJob ?? SharedGameTicker.FallbackOverflowJob);
-
- if (job.StartingGear != null && invSystem.TryGetSlots(dummy, out var slots))
- {
- var gear = protoMan.Index(job.StartingGear);
-
- foreach (var slot in slots)
- {
- var itemType = gear.GetGear(slot.Name, profile);
-
- if (invSystem.TryUnequip(dummy, slot.Name, out var unequippedItem, silent: true, force: true, reparent: false))
- {
- entMan.DeleteEntity(unequippedItem.Value);
- }
-
- if (itemType != string.Empty)
- {
- var item = entMan.SpawnEntity(itemType, MapCoordinates.Nullspace);
- invSystem.TryEquip(dummy, item, slot.Name, true, true);
- }
- }
- }
- }
- }
-}
diff --git a/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml b/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml
new file mode 100644
index 00000000000..997507414cd
--- /dev/null
+++ b/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml.cs b/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml.cs
new file mode 100644
index 00000000000..b0dcbc25fdb
--- /dev/null
+++ b/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml.cs
@@ -0,0 +1,45 @@
+using System.Numerics;
+using Content.Client.UserInterface.Controls;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Lobby.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class LobbyCharacterPreviewPanel : Control
+{
+ public Button CharacterSetupButton => CharacterSetup;
+
+ public LobbyCharacterPreviewPanel()
+ {
+ RobustXamlLoader.Load(this);
+ UserInterfaceManager.GetUIController().SetPreviewPanel(this);
+ }
+
+ public void SetLoaded(bool value)
+ {
+ Loaded.Visible = value;
+ Unloaded.Visible = !value;
+ }
+
+ public void SetSummaryText(string value)
+ {
+ Summary.Text = string.Empty;
+ }
+
+ public void SetSprite(EntityUid uid)
+ {
+ ViewBox.DisposeAllChildren();
+ var spriteView = new SpriteView
+ {
+ OverrideDirection = Direction.South,
+ Scale = new Vector2(4f, 4f),
+ MaxSize = new Vector2(112, 112),
+ Stretch = SpriteView.StretchMode.Fill,
+ };
+ spriteView.SetEntity(uid);
+ ViewBox.AddChild(spriteView);
+ }
+}
diff --git a/Content.Client/Lobby/UI/LobbyGui.xaml.cs b/Content.Client/Lobby/UI/LobbyGui.xaml.cs
index 69867ea90cb..5a0b580262e 100644
--- a/Content.Client/Lobby/UI/LobbyGui.xaml.cs
+++ b/Content.Client/Lobby/UI/LobbyGui.xaml.cs
@@ -1,23 +1,9 @@
-using Content.Client.Chat.UI;
-using Content.Client.Info;
using Content.Client.Message;
-using Content.Client.Preferences;
-using Content.Client.Preferences.UI;
-using Content.Client.UserInterface.Screens;
-using Content.Client.UserInterface.Systems.Chat.Widgets;
using Content.Client.UserInterface.Systems.EscapeMenu;
using Robust.Client.AutoGenerated;
using Robust.Client.Console;
-using Robust.Client.Graphics;
-using Robust.Client.State;
using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Maths;
-using Robust.Shared.Prototypes;
-using static Robust.Client.UserInterface.Controls.BoxContainer;
namespace Content.Client.Lobby.UI
{
diff --git a/Content.Client/MagicMirror/MagicMirrorBoundUserInterface.cs b/Content.Client/MagicMirror/MagicMirrorBoundUserInterface.cs
index bfbf2efe4f9..f6979bf8d7b 100644
--- a/Content.Client/MagicMirror/MagicMirrorBoundUserInterface.cs
+++ b/Content.Client/MagicMirror/MagicMirrorBoundUserInterface.cs
@@ -72,9 +72,6 @@ protected override void Dispose(bool disposing)
if (!disposing)
return;
- if (_window != null)
- _window.OnClose -= Close;
-
_window?.Dispose();
}
}
diff --git a/Content.Client/MagicMirror/MagicMirrorSystem.cs b/Content.Client/MagicMirror/MagicMirrorSystem.cs
new file mode 100644
index 00000000000..9b0b1dea0be
--- /dev/null
+++ b/Content.Client/MagicMirror/MagicMirrorSystem.cs
@@ -0,0 +1,8 @@
+using Content.Shared.MagicMirror;
+
+namespace Content.Client.MagicMirror;
+
+public sealed class MagicMirrorSystem : SharedMagicMirrorSystem
+{
+
+}
diff --git a/Content.Client/Movement/Systems/WaddleAnimationSystem.cs b/Content.Client/Movement/Systems/WaddleAnimationSystem.cs
new file mode 100644
index 00000000000..9555c1f6b9e
--- /dev/null
+++ b/Content.Client/Movement/Systems/WaddleAnimationSystem.cs
@@ -0,0 +1,173 @@
+using System.Numerics;
+using Content.Client.Buckle;
+using Content.Client.Gravity;
+using Content.Shared.ActionBlocker;
+using Content.Shared.Buckle.Components;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.Movement.Components;
+using Content.Shared.Movement.Events;
+using Content.Shared.StatusEffect;
+using Content.Shared.Stunnable;
+using Robust.Client.Animations;
+using Robust.Client.GameObjects;
+using Robust.Shared.Animations;
+using Robust.Shared.Timing;
+
+namespace Content.Client.Movement.Systems;
+
+public sealed class WaddleAnimationSystem : EntitySystem
+{
+ [Dependency] private readonly AnimationPlayerSystem _animation = default!;
+ [Dependency] private readonly GravitySystem _gravity = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
+ [Dependency] private readonly BuckleSystem _buckle = default!;
+ [Dependency] private readonly MobStateSystem _mobState = default!;
+
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnMovementInput);
+ SubscribeLocalEvent(OnStartedWalking);
+ SubscribeLocalEvent(OnStoppedWalking);
+ SubscribeLocalEvent(OnAnimationCompleted);
+ SubscribeLocalEvent(OnStunned);
+ SubscribeLocalEvent(OnKnockedDown);
+ SubscribeLocalEvent(OnBuckleChange);
+ }
+
+ private void OnMovementInput(EntityUid entity, WaddleAnimationComponent component, MoveInputEvent args)
+ {
+ // Prediction mitigation. Prediction means that MoveInputEvents are spammed repeatedly, even though you'd assume
+ // they're once-only for the user actually doing something. As such do nothing if we're just repeating this FoR.
+ if (!_timing.IsFirstTimePredicted)
+ {
+ return;
+ }
+
+ if (!args.HasDirectionalMovement && component.IsCurrentlyWaddling)
+ {
+ var stopped = new StoppedWaddlingEvent(entity);
+
+ RaiseLocalEvent(entity, ref stopped);
+
+ return;
+ }
+
+ // Only start waddling if we're not currently AND we're actually moving.
+ if (component.IsCurrentlyWaddling || !args.HasDirectionalMovement)
+ return;
+
+ var started = new StartedWaddlingEvent(entity);
+
+ RaiseLocalEvent(entity, ref started);
+ }
+
+ private void OnStartedWalking(EntityUid uid, WaddleAnimationComponent component, StartedWaddlingEvent args)
+ {
+ if (_animation.HasRunningAnimation(uid, component.KeyName))
+ return;
+
+ if (!TryComp(uid, out var mover))
+ return;
+
+ if (_gravity.IsWeightless(uid))
+ return;
+
+
+ if (!_actionBlocker.CanMove(uid, mover))
+ return;
+
+ // Do nothing if buckled in
+ if (_buckle.IsBuckled(uid))
+ return;
+
+ // Do nothing if crit or dead (for obvious reasons)
+ if (_mobState.IsIncapacitated(uid))
+ return;
+
+ var tumbleIntensity = component.LastStep ? 360 - component.TumbleIntensity : component.TumbleIntensity;
+ var len = mover.Sprinting ? component.AnimationLength * component.RunAnimationLengthMultiplier : component.AnimationLength;
+
+ component.LastStep = !component.LastStep;
+ component.IsCurrentlyWaddling = true;
+
+ var anim = new Animation()
+ {
+ Length = TimeSpan.FromSeconds(len),
+ AnimationTracks =
+ {
+ new AnimationTrackComponentProperty()
+ {
+ ComponentType = typeof(SpriteComponent),
+ Property = nameof(SpriteComponent.Rotation),
+ InterpolationMode = AnimationInterpolationMode.Linear,
+ KeyFrames =
+ {
+ new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(0), 0),
+ new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(tumbleIntensity), len/2),
+ new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(0), len/2),
+ }
+ },
+ new AnimationTrackComponentProperty()
+ {
+ ComponentType = typeof(SpriteComponent),
+ Property = nameof(SpriteComponent.Offset),
+ InterpolationMode = AnimationInterpolationMode.Linear,
+ KeyFrames =
+ {
+ new AnimationTrackProperty.KeyFrame(new Vector2(), 0),
+ new AnimationTrackProperty.KeyFrame(component.HopIntensity, len/2),
+ new AnimationTrackProperty.KeyFrame(new Vector2(), len/2),
+ }
+ }
+ }
+ };
+
+ _animation.Play(uid, anim, component.KeyName);
+ }
+
+ private void OnStoppedWalking(EntityUid uid, WaddleAnimationComponent component, StoppedWaddlingEvent args)
+ {
+ StopWaddling(uid, component);
+ }
+
+ private void OnAnimationCompleted(EntityUid uid, WaddleAnimationComponent component, AnimationCompletedEvent args)
+ {
+ var started = new StartedWaddlingEvent(uid);
+
+ RaiseLocalEvent(uid, ref started);
+ }
+
+ private void OnStunned(EntityUid uid, WaddleAnimationComponent component, StunnedEvent args)
+ {
+ StopWaddling(uid, component);
+ }
+
+ private void OnKnockedDown(EntityUid uid, WaddleAnimationComponent component, KnockedDownEvent args)
+ {
+ StopWaddling(uid, component);
+ }
+
+ private void OnBuckleChange(EntityUid uid, WaddleAnimationComponent component, BuckleChangeEvent args)
+ {
+ StopWaddling(uid, component);
+ }
+
+ private void StopWaddling(EntityUid uid, WaddleAnimationComponent component)
+ {
+ if (!component.IsCurrentlyWaddling)
+ return;
+
+ _animation.Stop(uid, component.KeyName);
+
+ if (!TryComp(uid, out var sprite))
+ {
+ return;
+ }
+
+ sprite.Offset = new Vector2();
+ sprite.Rotation = Angle.FromDegrees(0);
+
+ component.IsCurrentlyWaddling = false;
+ }
+}
diff --git a/Content.Client/NetworkConfigurator/NetworkConfiguratorBoundUserInterface.cs b/Content.Client/NetworkConfigurator/NetworkConfiguratorBoundUserInterface.cs
index 264c297b633..80c98f143b9 100644
--- a/Content.Client/NetworkConfigurator/NetworkConfiguratorBoundUserInterface.cs
+++ b/Content.Client/NetworkConfigurator/NetworkConfiguratorBoundUserInterface.cs
@@ -88,6 +88,7 @@ protected override void Dispose(bool disposing)
base.Dispose(disposing);
if (!disposing) return;
+ _linkMenu?.Dispose();
_listMenu?.Dispose();
_configurationMenu?.Dispose();
}
diff --git a/Content.Client/Nutrition/EntitySystems/DrinkSystem.cs b/Content.Client/Nutrition/EntitySystems/DrinkSystem.cs
new file mode 100644
index 00000000000..16dbecb7936
--- /dev/null
+++ b/Content.Client/Nutrition/EntitySystems/DrinkSystem.cs
@@ -0,0 +1,7 @@
+using Content.Shared.Nutrition.EntitySystems;
+
+namespace Content.Client.Nutrition.EntitySystems;
+
+public sealed class DrinkSystem : SharedDrinkSystem
+{
+}
diff --git a/Content.Client/Options/UI/Tabs/GraphicsTab.xaml b/Content.Client/Options/UI/Tabs/GraphicsTab.xaml
index 118b85b87ba..ec1b9aa002f 100644
--- a/Content.Client/Options/UI/Tabs/GraphicsTab.xaml
+++ b/Content.Client/Options/UI/Tabs/GraphicsTab.xaml
@@ -36,6 +36,9 @@
+
diff --git a/Content.Client/Options/UI/Tabs/GraphicsTab.xaml.cs b/Content.Client/Options/UI/Tabs/GraphicsTab.xaml.cs
index 3113e644bac..a22adf3e632 100644
--- a/Content.Client/Options/UI/Tabs/GraphicsTab.xaml.cs
+++ b/Content.Client/Options/UI/Tabs/GraphicsTab.xaml.cs
@@ -67,6 +67,12 @@ public GraphicsTab()
UpdateApplyButton();
};
+ ViewportVerticalFitCheckBox.OnToggled += _ =>
+ {
+ UpdateViewportScale();
+ UpdateApplyButton();
+ };
+
IntegerScalingCheckBox.OnToggled += OnCheckBoxToggled;
ViewportLowResCheckBox.OnToggled += OnCheckBoxToggled;
ParallaxLowQualityCheckBox.OnToggled += OnCheckBoxToggled;
@@ -79,6 +85,7 @@ public GraphicsTab()
ViewportScaleSlider.Value = _cfg.GetCVar(CCVars.ViewportFixedScaleFactor);
ViewportStretchCheckBox.Pressed = _cfg.GetCVar(CCVars.ViewportStretch);
IntegerScalingCheckBox.Pressed = _cfg.GetCVar(CCVars.ViewportSnapToleranceMargin) != 0;
+ ViewportVerticalFitCheckBox.Pressed = _cfg.GetCVar(CCVars.ViewportVerticalFit);
ViewportLowResCheckBox.Pressed = !_cfg.GetCVar(CCVars.ViewportScaleRender);
ParallaxLowQualityCheckBox.Pressed = _cfg.GetCVar(CCVars.ParallaxLowQuality);
FpsCounterCheckBox.Pressed = _cfg.GetCVar(CCVars.HudFpsCounterVisible);
@@ -111,6 +118,7 @@ private void OnApplyButtonPressed(BaseButton.ButtonEventArgs args)
_cfg.SetCVar(CCVars.ViewportFixedScaleFactor, (int) ViewportScaleSlider.Value);
_cfg.SetCVar(CCVars.ViewportSnapToleranceMargin,
IntegerScalingCheckBox.Pressed ? CCVars.ViewportSnapToleranceMargin.DefaultValue : 0);
+ _cfg.SetCVar(CCVars.ViewportVerticalFit, ViewportVerticalFitCheckBox.Pressed);
_cfg.SetCVar(CCVars.ViewportScaleRender, !ViewportLowResCheckBox.Pressed);
_cfg.SetCVar(CCVars.ParallaxLowQuality, ParallaxLowQualityCheckBox.Pressed);
_cfg.SetCVar(CCVars.HudFpsCounterVisible, FpsCounterCheckBox.Pressed);
@@ -140,6 +148,7 @@ private void UpdateApplyButton()
var isVPStretchSame = ViewportStretchCheckBox.Pressed == _cfg.GetCVar(CCVars.ViewportStretch);
var isVPScaleSame = (int) ViewportScaleSlider.Value == _cfg.GetCVar(CCVars.ViewportFixedScaleFactor);
var isIntegerScalingSame = IntegerScalingCheckBox.Pressed == (_cfg.GetCVar(CCVars.ViewportSnapToleranceMargin) != 0);
+ var isVPVerticalFitSame = ViewportVerticalFitCheckBox.Pressed == _cfg.GetCVar(CCVars.ViewportVerticalFit);
var isVPResSame = ViewportLowResCheckBox.Pressed == !_cfg.GetCVar(CCVars.ViewportScaleRender);
var isPLQSame = ParallaxLowQualityCheckBox.Pressed == _cfg.GetCVar(CCVars.ParallaxLowQuality);
var isFpsCounterVisibleSame = FpsCounterCheckBox.Pressed == _cfg.GetCVar(CCVars.HudFpsCounterVisible);
@@ -152,6 +161,7 @@ private void UpdateApplyButton()
isVPStretchSame &&
isVPScaleSame &&
isIntegerScalingSame &&
+ isVPVerticalFitSame &&
isVPResSame &&
isPLQSame &&
isFpsCounterVisibleSame &&
@@ -235,6 +245,8 @@ private void UpdateViewportScale()
{
ViewportScaleBox.Visible = !ViewportStretchCheckBox.Pressed;
IntegerScalingCheckBox.Visible = ViewportStretchCheckBox.Pressed;
+ ViewportVerticalFitCheckBox.Visible = ViewportStretchCheckBox.Pressed;
+ ViewportWidthSlider.Visible = ViewportWidthSliderDisplay.Visible = !ViewportStretchCheckBox.Pressed || ViewportStretchCheckBox.Pressed && !ViewportVerticalFitCheckBox.Pressed;
ViewportScaleText.Text = Loc.GetString("ui-options-vp-scale", ("scale", ViewportScaleSlider.Value));
}
diff --git a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs
index aca9efcfe26..a575f1ba51c 100644
--- a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs
+++ b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs
@@ -215,6 +215,7 @@ void AddCheckBox(string checkBoxName, bool currentState, Action
+{
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnGetStatusIconsEvent);
+ }
+
+ private void OnGetStatusIconsEvent(EntityUid uid, CriminalRecordComponent component, ref GetStatusIconsEvent ev)
+ {
+ if (!IsActive || ev.InContainer)
+ return;
+
+ if (_prototype.TryIndex(component.StatusIcon.Id, out var iconPrototype))
+ ev.StatusIcons.Add(iconPrototype);
+ }
+}
diff --git a/Content.Client/Overlays/ShowHungerIconsSystem.cs b/Content.Client/Overlays/ShowHungerIconsSystem.cs
index 58551b30c26..b1c0f3a1a0c 100644
--- a/Content.Client/Overlays/ShowHungerIconsSystem.cs
+++ b/Content.Client/Overlays/ShowHungerIconsSystem.cs
@@ -1,14 +1,13 @@
+using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Overlays;
-using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
-using Robust.Shared.Prototypes;
namespace Content.Client.Overlays;
public sealed class ShowHungerIconsSystem : EquipmentHudSystem
{
- [Dependency] private readonly IPrototypeManager _prototypeMan = default!;
+ [Dependency] private readonly HungerSystem _hunger = default!;
public override void Initialize()
{
@@ -17,42 +16,12 @@ public override void Initialize()
SubscribeLocalEvent(OnGetStatusIconsEvent);
}
- private void OnGetStatusIconsEvent(EntityUid uid, HungerComponent hungerComponent, ref GetStatusIconsEvent args)
+ private void OnGetStatusIconsEvent(EntityUid uid, HungerComponent component, ref GetStatusIconsEvent ev)
{
- if (!IsActive || args.InContainer)
+ if (!IsActive || ev.InContainer)
return;
- var hungerIcons = DecideHungerIcon(uid, hungerComponent);
-
- args.StatusIcons.AddRange(hungerIcons);
- }
-
- private IReadOnlyList DecideHungerIcon(EntityUid uid, HungerComponent hungerComponent)
- {
- var result = new List();
-
- switch (hungerComponent.CurrentThreshold)
- {
- case HungerThreshold.Overfed:
- if (_prototypeMan.TryIndex("HungerIconOverfed", out var overfed))
- {
- result.Add(overfed);
- }
- break;
- case HungerThreshold.Peckish:
- if (_prototypeMan.TryIndex("HungerIconPeckish", out var peckish))
- {
- result.Add(peckish);
- }
- break;
- case HungerThreshold.Starving:
- if (_prototypeMan.TryIndex("HungerIconStarving", out var starving))
- {
- result.Add(starving);
- }
- break;
- }
-
- return result;
+ if (_hunger.TryGetStatusIconPrototype(component, out var iconPrototype))
+ ev.StatusIcons.Add(iconPrototype);
}
}
diff --git a/Content.Client/Overlays/ShowJobIconsSystem.cs b/Content.Client/Overlays/ShowJobIconsSystem.cs
new file mode 100644
index 00000000000..e24b99f3e87
--- /dev/null
+++ b/Content.Client/Overlays/ShowJobIconsSystem.cs
@@ -0,0 +1,60 @@
+using Content.Shared.Access.Components;
+using Content.Shared.Access.Systems;
+using Content.Shared.Overlays;
+using Content.Shared.PDA;
+using Content.Shared.StatusIcon;
+using Content.Shared.StatusIcon.Components;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Overlays;
+
+public sealed class ShowJobIconsSystem : EquipmentHudSystem
+{
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+ [Dependency] private readonly AccessReaderSystem _accessReader = default!;
+
+ [ValidatePrototypeId]
+ private const string JobIconForNoId = "JobIconNoId";
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnGetStatusIconsEvent);
+ }
+
+ private void OnGetStatusIconsEvent(EntityUid uid, StatusIconComponent _, ref GetStatusIconsEvent ev)
+ {
+ if (!IsActive || ev.InContainer)
+ return;
+
+ var iconId = JobIconForNoId;
+
+ if (_accessReader.FindAccessItemsInventory(uid, out var items))
+ {
+ foreach (var item in items)
+ {
+ // ID Card
+ if (TryComp(item, out var id))
+ {
+ iconId = id.JobIcon;
+ break;
+ }
+
+ // PDA
+ if (TryComp(item, out var pda)
+ && pda.ContainedId != null
+ && TryComp(pda.ContainedId, out id))
+ {
+ iconId = id.JobIcon;
+ break;
+ }
+ }
+ }
+
+ if (_prototype.TryIndex(iconId, out var iconPrototype))
+ ev.StatusIcons.Add(iconPrototype);
+ else
+ Log.Error($"Invalid job icon prototype: {iconPrototype}");
+ }
+}
diff --git a/Content.Client/Overlays/ShowMindShieldIconsSystem.cs b/Content.Client/Overlays/ShowMindShieldIconsSystem.cs
new file mode 100644
index 00000000000..8bf39b875f6
--- /dev/null
+++ b/Content.Client/Overlays/ShowMindShieldIconsSystem.cs
@@ -0,0 +1,28 @@
+using Content.Shared.Mindshield.Components;
+using Content.Shared.Overlays;
+using Content.Shared.StatusIcon;
+using Content.Shared.StatusIcon.Components;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Overlays;
+
+public sealed class ShowMindShieldIconsSystem : EquipmentHudSystem
+{
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnGetStatusIconsEvent);
+ }
+
+ private void OnGetStatusIconsEvent(EntityUid uid, MindShieldComponent component, ref GetStatusIconsEvent ev)
+ {
+ if (!IsActive || ev.InContainer)
+ return;
+
+ if (_prototype.TryIndex(component.MindShieldStatusIcon.Id, out var iconPrototype))
+ ev.StatusIcons.Add(iconPrototype);
+ }
+}
diff --git a/Content.Client/Overlays/ShowSecurityIconsSystem.cs b/Content.Client/Overlays/ShowSecurityIconsSystem.cs
deleted file mode 100644
index 7a4abd05e00..00000000000
--- a/Content.Client/Overlays/ShowSecurityIconsSystem.cs
+++ /dev/null
@@ -1,86 +0,0 @@
-using Content.Shared.Access.Components;
-using Content.Shared.Access.Systems;
-using Content.Shared.Mindshield.Components;
-using Content.Shared.Overlays;
-using Content.Shared.PDA;
-using Content.Shared.Security.Components;
-using Content.Shared.StatusIcon;
-using Content.Shared.StatusIcon.Components;
-using Robust.Shared.Prototypes;
-
-namespace Content.Client.Overlays;
-
-public sealed class ShowSecurityIconsSystem : EquipmentHudSystem
-{
- [Dependency] private readonly IPrototypeManager _prototypeMan = default!;
- [Dependency] private readonly AccessReaderSystem _accessReader = default!;
-
- [ValidatePrototypeId]
- private const string JobIconForNoId = "JobIconNoId";
-
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent(OnGetStatusIconsEvent);
- }
-
- private void OnGetStatusIconsEvent(EntityUid uid, StatusIconComponent _, ref GetStatusIconsEvent @event)
- {
- if (!IsActive || @event.InContainer)
- {
- return;
- }
-
- var securityIcons = DecideSecurityIcon(uid);
-
- @event.StatusIcons.AddRange(securityIcons);
- }
-
- private IReadOnlyList DecideSecurityIcon(EntityUid uid)
- {
- var result = new List();
-
- var jobIconToGet = JobIconForNoId;
- if (_accessReader.FindAccessItemsInventory(uid, out var items))
- {
- foreach (var item in items)
- {
- // ID Card
- if (TryComp(item, out IdCardComponent? id))
- {
- jobIconToGet = id.JobIcon;
- break;
- }
-
- // PDA
- if (TryComp(item, out PdaComponent? pda)
- && pda.ContainedId != null
- && TryComp(pda.ContainedId, out id))
- {
- jobIconToGet = id.JobIcon;
- break;
- }
- }
- }
-
- if (_prototypeMan.TryIndex(jobIconToGet, out var jobIcon))
- result.Add(jobIcon);
- else
- Log.Error($"Invalid job icon prototype: {jobIcon}");
-
- if (TryComp(uid, out var comp))
- {
- if (_prototypeMan.TryIndex(comp.MindShieldStatusIcon.Id, out var icon))
- result.Add(icon);
- }
-
- if (TryComp(uid, out var record))
- {
- if(_prototypeMan.TryIndex(record.StatusIcon.Id, out var criminalIcon))
- result.Add(criminalIcon);
- }
-
- return result;
- }
-}
diff --git a/Content.Client/Overlays/ShowSyndicateIconsSystem.cs b/Content.Client/Overlays/ShowSyndicateIconsSystem.cs
index a6407266853..660ef198e14 100644
--- a/Content.Client/Overlays/ShowSyndicateIconsSystem.cs
+++ b/Content.Client/Overlays/ShowSyndicateIconsSystem.cs
@@ -1,10 +1,11 @@
using Content.Shared.Overlays;
-using Content.Shared.StatusIcon.Components;
using Content.Shared.NukeOps;
using Content.Shared.StatusIcon;
+using Content.Shared.StatusIcon.Components;
using Robust.Shared.Prototypes;
namespace Content.Client.Overlays;
+
public sealed class ShowSyndicateIconsSystem : EquipmentHudSystem
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
@@ -16,28 +17,13 @@ public override void Initialize()
SubscribeLocalEvent(OnGetStatusIconsEvent);
}
- private void OnGetStatusIconsEvent(EntityUid uid, NukeOperativeComponent nukeOperativeComponent, ref GetStatusIconsEvent args)
+ private void OnGetStatusIconsEvent(EntityUid uid, NukeOperativeComponent component, ref GetStatusIconsEvent ev)
{
- if (!IsActive || args.InContainer)
- {
+ if (!IsActive || ev.InContainer)
return;
- }
-
- var syndicateIcons = SyndicateIcon(uid, nukeOperativeComponent);
-
- args.StatusIcons.AddRange(syndicateIcons);
- }
-
- private IReadOnlyList SyndicateIcon(EntityUid uid, NukeOperativeComponent nukeOperativeComponent)
- {
- var result = new List();
-
- if (_prototype.TryIndex(nukeOperativeComponent.SyndStatusIcon, out var syndicateicon))
- {
- result.Add(syndicateicon);
- }
- return result;
+ if (_prototype.TryIndex(component.SyndStatusIcon, out var iconPrototype))
+ ev.StatusIcons.Add(iconPrototype);
}
}
diff --git a/Content.Client/Overlays/ShowThirstIconsSystem.cs b/Content.Client/Overlays/ShowThirstIconsSystem.cs
index f9d6d0ab259..b08aa4340b2 100644
--- a/Content.Client/Overlays/ShowThirstIconsSystem.cs
+++ b/Content.Client/Overlays/ShowThirstIconsSystem.cs
@@ -1,14 +1,13 @@
+using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Overlays;
-using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
-using Robust.Shared.Prototypes;
namespace Content.Client.Overlays;
public sealed class ShowThirstIconsSystem : EquipmentHudSystem
{
- [Dependency] private readonly IPrototypeManager _prototypeMan = default!;
+ [Dependency] private readonly ThirstSystem _thirst = default!;
public override void Initialize()
{
@@ -17,42 +16,12 @@ public override void Initialize()
SubscribeLocalEvent(OnGetStatusIconsEvent);
}
- private void OnGetStatusIconsEvent(EntityUid uid, ThirstComponent thirstComponent, ref GetStatusIconsEvent args)
+ private void OnGetStatusIconsEvent(EntityUid uid, ThirstComponent component, ref GetStatusIconsEvent ev)
{
- if (!IsActive || args.InContainer)
+ if (!IsActive || ev.InContainer)
return;
- var thirstIcons = DecideThirstIcon(uid, thirstComponent);
-
- args.StatusIcons.AddRange(thirstIcons);
- }
-
- private IReadOnlyList DecideThirstIcon(EntityUid uid, ThirstComponent thirstComponent)
- {
- var result = new List();
-
- switch (thirstComponent.CurrentThirstThreshold)
- {
- case ThirstThreshold.OverHydrated:
- if (_prototypeMan.TryIndex("ThirstIconOverhydrated", out var overhydrated))
- {
- result.Add(overhydrated);
- }
- break;
- case ThirstThreshold.Thirsty:
- if (_prototypeMan.TryIndex("ThirstIconThirsty", out var thirsty))
- {
- result.Add(thirsty);
- }
- break;
- case ThirstThreshold.Parched:
- if (_prototypeMan.TryIndex("ThirstIconParched", out var parched))
- {
- result.Add(parched);
- }
- break;
- }
-
- return result;
+ if (_thirst.TryGetStatusIconPrototype(component, out var iconPrototype))
+ ev.StatusIcons.Add(iconPrototype!);
}
}
diff --git a/Content.Client/PDA/PdaBoundUserInterface.cs b/Content.Client/PDA/PdaBoundUserInterface.cs
index ef9d6e8b9be..07352b512b0 100644
--- a/Content.Client/PDA/PdaBoundUserInterface.cs
+++ b/Content.Client/PDA/PdaBoundUserInterface.cs
@@ -21,7 +21,6 @@ public PdaBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
protected override void Open()
{
base.Open();
- SendMessage(new PdaRequestUpdateInterfaceMessage());
_menu = new PdaMenu();
_menu.OpenCenteredLeft();
_menu.OnClose += Close;
@@ -32,17 +31,17 @@ protected override void Open()
_menu.EjectIdButton.OnPressed += _ =>
{
- SendMessage(new ItemSlotButtonPressedEvent(PdaComponent.PdaIdSlotId));
+ SendPredictedMessage(new ItemSlotButtonPressedEvent(PdaComponent.PdaIdSlotId));
};
_menu.EjectPenButton.OnPressed += _ =>
{
- SendMessage(new ItemSlotButtonPressedEvent(PdaComponent.PdaPenSlotId));
+ SendPredictedMessage(new ItemSlotButtonPressedEvent(PdaComponent.PdaPenSlotId));
};
_menu.EjectPaiButton.OnPressed += _ =>
{
- SendMessage(new ItemSlotButtonPressedEvent(PdaComponent.PdaPaiSlotId));
+ SendPredictedMessage(new ItemSlotButtonPressedEvent(PdaComponent.PdaPaiSlotId));
};
_menu.ActivateMusicButton.OnPressed += _ =>
diff --git a/Content.Client/Paper/PaperComponent.cs b/Content.Client/Paper/PaperComponent.cs
index d197cd3721b..1dc827bf7e6 100644
--- a/Content.Client/Paper/PaperComponent.cs
+++ b/Content.Client/Paper/PaperComponent.cs
@@ -1,9 +1,6 @@
using Content.Shared.Paper;
-using Robust.Shared.GameStates;
namespace Content.Client.Paper;
-[NetworkedComponent, RegisterComponent]
-public sealed partial class PaperComponent : SharedPaperComponent
-{
-}
+[RegisterComponent]
+public sealed partial class PaperComponent : SharedPaperComponent;
diff --git a/Content.Client/Pinpointer/NavMapSystem.cs b/Content.Client/Pinpointer/NavMapSystem.cs
index bd7dfc1117f..868bf1fbc48 100644
--- a/Content.Client/Pinpointer/NavMapSystem.cs
+++ b/Content.Client/Pinpointer/NavMapSystem.cs
@@ -1,18 +1,14 @@
-using System.Numerics;
using Content.Shared.Pinpointer;
-using Robust.Client.Graphics;
-using Robust.Shared.Enums;
using Robust.Shared.GameStates;
-using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
namespace Content.Client.Pinpointer;
-public sealed class NavMapSystem : SharedNavMapSystem
+public sealed partial class NavMapSystem : SharedNavMapSystem
{
public override void Initialize()
{
base.Initialize();
+
SubscribeLocalEvent(OnHandleState);
}
@@ -21,89 +17,47 @@ private void OnHandleState(EntityUid uid, NavMapComponent component, ref Compone
if (args.Current is not NavMapComponentState state)
return;
- component.Chunks.Clear();
-
- foreach (var (origin, data) in state.TileData)
+ if (!state.FullState)
{
- component.Chunks.Add(origin, new NavMapChunk(origin)
+ foreach (var index in component.Chunks.Keys)
{
- TileData = data,
- });
- }
-
- component.Beacons.Clear();
- component.Beacons.AddRange(state.Beacons);
-
- component.Airlocks.Clear();
- component.Airlocks.AddRange(state.Airlocks);
- }
-}
-
-public sealed class NavMapOverlay : Overlay
-{
- private readonly IEntityManager _entManager;
- private readonly IMapManager _mapManager;
-
- public override OverlaySpace Space => OverlaySpace.WorldSpace;
-
- private List> _grids = new();
-
- public NavMapOverlay(IEntityManager entManager, IMapManager mapManager)
- {
- _entManager = entManager;
- _mapManager = mapManager;
- }
-
- protected override void Draw(in OverlayDrawArgs args)
- {
- var query = _entManager.GetEntityQuery();
- var xformQuery = _entManager.GetEntityQuery();
- var scale = Matrix3.CreateScale(new Vector2(1f, 1f));
+ if (!state.AllChunks!.Contains(index))
+ component.Chunks.Remove(index);
+ }
- _grids.Clear();
- _mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds, ref _grids);
+ foreach (var beacon in component.Beacons)
+ {
+ if (!state.AllBeacons!.Contains(beacon))
+ component.Beacons.Remove(beacon);
+ }
+ }
- foreach (var grid in _grids)
+ else
{
- if (!query.TryGetComponent(grid, out var navMap) || !xformQuery.TryGetComponent(grid.Owner, out var xform))
- continue;
-
- // TODO: Faster helper method
- var (_, _, matrix, invMatrix) = xform.GetWorldPositionRotationMatrixWithInv();
-
- var localAABB = invMatrix.TransformBox(args.WorldBounds);
- Matrix3.Multiply(in scale, in matrix, out var matty);
-
- args.WorldHandle.SetTransform(matty);
-
- for (var x = Math.Floor(localAABB.Left); x <= Math.Ceiling(localAABB.Right); x += SharedNavMapSystem.ChunkSize * grid.Comp.TileSize)
+ foreach (var index in component.Chunks.Keys)
{
- for (var y = Math.Floor(localAABB.Bottom); y <= Math.Ceiling(localAABB.Top); y += SharedNavMapSystem.ChunkSize * grid.Comp.TileSize)
- {
- var floored = new Vector2i((int) x, (int) y);
-
- var chunkOrigin = SharedMapSystem.GetChunkIndices(floored, SharedNavMapSystem.ChunkSize);
-
- if (!navMap.Chunks.TryGetValue(chunkOrigin, out var chunk))
- continue;
+ if (!state.Chunks.ContainsKey(index))
+ component.Chunks.Remove(index);
+ }
- // TODO: Okay maybe I should just use ushorts lmao...
- for (var i = 0; i < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; i++)
- {
- var value = (int) Math.Pow(2, i);
+ foreach (var beacon in component.Beacons)
+ {
+ if (!state.Beacons.Contains(beacon))
+ component.Beacons.Remove(beacon);
+ }
+ }
- var mask = chunk.TileData & value;
+ foreach (var ((category, origin), chunk) in state.Chunks)
+ {
+ var newChunk = new NavMapChunk(origin);
- if (mask == 0x0)
- continue;
+ foreach (var (atmosDirection, value) in chunk)
+ newChunk.TileData[atmosDirection] = value;
- var tile = chunk.Origin * SharedNavMapSystem.ChunkSize + SharedNavMapSystem.GetTile(mask);
- args.WorldHandle.DrawRect(new Box2(tile * grid.Comp.TileSize, (tile + 1) * grid.Comp.TileSize), Color.Aqua, false);
- }
- }
- }
+ component.Chunks[(category, origin)] = newChunk;
}
- args.WorldHandle.SetTransform(Matrix3.Identity);
+ foreach (var beacon in state.Beacons)
+ component.Beacons.Add(beacon);
}
}
diff --git a/Content.Client/Pinpointer/UI/NavMapControl.cs b/Content.Client/Pinpointer/UI/NavMapControl.cs
index 677092e1918..3309e7c8df5 100644
--- a/Content.Client/Pinpointer/UI/NavMapControl.cs
+++ b/Content.Client/Pinpointer/UI/NavMapControl.cs
@@ -16,6 +16,8 @@
using Robust.Shared.Timing;
using System.Numerics;
using JetBrains.Annotations;
+using Content.Shared.Atmos;
+using System.Linq;
namespace Content.Client.Pinpointer.UI;
@@ -27,6 +29,7 @@ public partial class NavMapControl : MapGridControl
{
[Dependency] private IResourceCache _cache = default!;
private readonly SharedTransformSystem _transformSystem;
+ private readonly SharedNavMapSystem _navMapSystem;
public EntityUid? Owner;
public EntityUid? MapUid;
@@ -40,7 +43,10 @@ public partial class NavMapControl : MapGridControl
// Tracked data
public Dictionary TrackedCoordinates = new();
public Dictionary TrackedEntities = new();
- public Dictionary>? TileGrid = default!;
+
+ public List<(Vector2, Vector2)> TileLines = new();
+ public List<(Vector2, Vector2)> TileRects = new();
+ public List<(Vector2[], Color)> TilePolygons = new();
// Default colors
public Color WallColor = new(102, 217, 102);
@@ -53,14 +59,23 @@ public partial class NavMapControl : MapGridControl
protected static float MinDisplayedRange = 8f;
protected static float MaxDisplayedRange = 128f;
protected static float DefaultDisplayedRange = 48f;
+ protected float MinmapScaleModifier = 0.075f;
+ protected float FullWallInstep = 0.165f;
+ protected float ThinWallThickness = 0.165f;
+ protected float ThinDoorThickness = 0.30f;
// Local variables
- private float _updateTimer = 0.25f;
+ private float _updateTimer = 1.0f;
private Dictionary _sRGBLookUp = new();
protected Color BackgroundColor;
protected float BackgroundOpacity = 0.9f;
private int _targetFontsize = 8;
+ protected Dictionary<(int, Vector2i), (int, Vector2i)> HorizLinesLookup = new();
+ protected Dictionary<(int, Vector2i), (int, Vector2i)> HorizLinesLookupReversed = new();
+ protected Dictionary<(int, Vector2i), (int, Vector2i)> VertLinesLookup = new();
+ protected Dictionary<(int, Vector2i), (int, Vector2i)> VertLinesLookupReversed = new();
+
// Components
private NavMapComponent? _navMap;
private MapGridComponent? _grid;
@@ -72,6 +87,7 @@ public partial class NavMapControl : MapGridControl
private readonly Label _zoom = new()
{
VerticalAlignment = VAlignment.Top,
+ HorizontalExpand = true,
Margin = new Thickness(8f, 8f),
};
@@ -80,6 +96,7 @@ public partial class NavMapControl : MapGridControl
Text = Loc.GetString("navmap-recenter"),
VerticalAlignment = VAlignment.Top,
HorizontalAlignment = HAlignment.Right,
+ HorizontalExpand = true,
Margin = new Thickness(8f, 4f),
Disabled = true,
};
@@ -87,9 +104,10 @@ public partial class NavMapControl : MapGridControl
private readonly CheckBox _beacons = new()
{
Text = Loc.GetString("navmap-toggle-beacons"),
- Margin = new Thickness(4f, 0f),
VerticalAlignment = VAlignment.Center,
HorizontalAlignment = HAlignment.Center,
+ HorizontalExpand = true,
+ Margin = new Thickness(4f, 0f),
Pressed = true,
};
@@ -98,6 +116,8 @@ public NavMapControl() : base(MinDisplayedRange, MaxDisplayedRange, DefaultDispl
IoCManager.InjectDependencies(this);
_transformSystem = EntManager.System();
+ _navMapSystem = EntManager.System();
+
BackgroundColor = Color.FromSrgb(TileColor.WithAlpha(BackgroundOpacity));
RectClipContent = true;
@@ -112,6 +132,8 @@ public NavMapControl() : base(MinDisplayedRange, MaxDisplayedRange, DefaultDispl
BorderColor = StyleNano.PanelDark
},
VerticalExpand = false,
+ HorizontalExpand = true,
+ SetWidth = 650f,
Children =
{
new BoxContainer()
@@ -130,6 +152,7 @@ public NavMapControl() : base(MinDisplayedRange, MaxDisplayedRange, DefaultDispl
var topContainer = new BoxContainer()
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
+ HorizontalExpand = true,
Children =
{
topPanel,
@@ -157,6 +180,9 @@ public void ForceNavMapUpdate()
{
EntManager.TryGetComponent(MapUid, out _navMap);
EntManager.TryGetComponent(MapUid, out _grid);
+ EntManager.TryGetComponent(MapUid, out _xform);
+ EntManager.TryGetComponent(MapUid, out _physics);
+ EntManager.TryGetComponent(MapUid, out _fixtures);
UpdateNavMap();
}
@@ -251,119 +277,93 @@ protected override void Draw(DrawingHandleScreen handle)
EntManager.TryGetComponent(MapUid, out _physics);
EntManager.TryGetComponent(MapUid, out _fixtures);
+ if (_navMap == null || _grid == null || _xform == null)
+ return;
+
// Map re-centering
_recenter.Disabled = DrawRecenter();
- _zoom.Text = Loc.GetString("navmap-zoom", ("value", $"{(DefaultDisplayedRange / WorldRange ):0.0}"));
-
- if (_navMap == null || _xform == null)
- return;
+ // Update zoom text
+ _zoom.Text = Loc.GetString("navmap-zoom", ("value", $"{(DefaultDisplayedRange / WorldRange):0.0}"));
+ // Update offset with physics local center
var offset = Offset;
if (_physics != null)
offset += _physics.LocalCenter;
- // Draw tiles
- if (_fixtures != null)
+ var offsetVec = new Vector2(offset.X, -offset.Y);
+
+ // Wall sRGB
+ if (!_sRGBLookUp.TryGetValue(WallColor, out var wallsRGB))
+ {
+ wallsRGB = Color.ToSrgb(WallColor);
+ _sRGBLookUp[WallColor] = wallsRGB;
+ }
+
+ // Draw floor tiles
+ if (TilePolygons.Any())
{
Span verts = new Vector2[8];
- foreach (var fixture in _fixtures.Fixtures.Values)
+ foreach (var (polygonVerts, polygonColor) in TilePolygons)
{
- if (fixture.Shape is not PolygonShape poly)
- continue;
-
- for (var i = 0; i < poly.VertexCount; i++)
+ for (var i = 0; i < polygonVerts.Length; i++)
{
- var vert = poly.Vertices[i] - offset;
-
+ var vert = polygonVerts[i] - offset;
verts[i] = ScalePosition(new Vector2(vert.X, -vert.Y));
}
- handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts[..poly.VertexCount], TileColor);
+ handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts[..polygonVerts.Length], polygonColor);
}
}
- var area = new Box2(-WorldRange, -WorldRange, WorldRange + 1f, WorldRange + 1f).Translated(offset);
-
- // Drawing lines can be rather expensive due to the number of neighbors that need to be checked in order
- // to figure out where they should be drawn. However, we don't *need* to do check these every frame.
- // Instead, lets periodically update where to draw each line and then store these points in a list.
- // Then we can just run through the list each frame and draw the lines without any extra computation.
-
- // Draw walls
- if (TileGrid != null && TileGrid.Count > 0)
+ // Draw map lines
+ if (TileLines.Any())
{
- var walls = new ValueList();
+ var lines = new ValueList(TileLines.Count * 2);
- foreach ((var chunk, var chunkedLines) in TileGrid)
+ foreach (var (o, t) in TileLines)
{
- var offsetChunk = new Vector2(chunk.X, chunk.Y) * SharedNavMapSystem.ChunkSize;
-
- if (offsetChunk.X < area.Left - SharedNavMapSystem.ChunkSize || offsetChunk.X > area.Right)
- continue;
-
- if (offsetChunk.Y < area.Bottom - SharedNavMapSystem.ChunkSize || offsetChunk.Y > area.Top)
- continue;
+ var origin = ScalePosition(o - offsetVec);
+ var terminus = ScalePosition(t - offsetVec);
- foreach (var chunkedLine in chunkedLines)
- {
- var start = ScalePosition(chunkedLine.Origin - new Vector2(offset.X, -offset.Y));
- var end = ScalePosition(chunkedLine.Terminus - new Vector2(offset.X, -offset.Y));
-
- walls.Add(start);
- walls.Add(end);
- }
+ lines.Add(origin);
+ lines.Add(terminus);
}
- if (walls.Count > 0)
- {
- if (!_sRGBLookUp.TryGetValue(WallColor, out var sRGB))
- {
- sRGB = Color.ToSrgb(WallColor);
- _sRGBLookUp[WallColor] = sRGB;
- }
-
- handle.DrawPrimitives(DrawPrimitiveTopology.LineList, walls.Span, sRGB);
- }
+ if (lines.Count > 0)
+ handle.DrawPrimitives(DrawPrimitiveTopology.LineList, lines.Span, wallsRGB);
}
- var airlockBuffer = Vector2.One * (MinimapScale / 2.25f) * 0.75f;
- var airlockLines = new ValueList();
- var foobarVec = new Vector2(1, -1);
-
- foreach (var airlock in _navMap.Airlocks)
+ // Draw map rects
+ if (TileRects.Any())
{
- var position = airlock.Position - offset;
- position = ScalePosition(position with { Y = -position.Y });
- airlockLines.Add(position + airlockBuffer);
- airlockLines.Add(position - airlockBuffer * foobarVec);
-
- airlockLines.Add(position + airlockBuffer);
- airlockLines.Add(position + airlockBuffer * foobarVec);
-
- airlockLines.Add(position - airlockBuffer);
- airlockLines.Add(position + airlockBuffer * foobarVec);
-
- airlockLines.Add(position - airlockBuffer);
- airlockLines.Add(position - airlockBuffer * foobarVec);
-
- airlockLines.Add(position + airlockBuffer * -Vector2.UnitY);
- airlockLines.Add(position - airlockBuffer * -Vector2.UnitY);
- }
+ var rects = new ValueList(TileRects.Count * 8);
- if (airlockLines.Count > 0)
- {
- if (!_sRGBLookUp.TryGetValue(WallColor, out var sRGB))
+ foreach (var (lt, rb) in TileRects)
{
- sRGB = Color.ToSrgb(WallColor);
- _sRGBLookUp[WallColor] = sRGB;
+ var leftTop = ScalePosition(lt - offsetVec);
+ var rightBottom = ScalePosition(rb - offsetVec);
+
+ var rightTop = new Vector2(rightBottom.X, leftTop.Y);
+ var leftBottom = new Vector2(leftTop.X, rightBottom.Y);
+
+ rects.Add(leftTop);
+ rects.Add(rightTop);
+ rects.Add(rightTop);
+ rects.Add(rightBottom);
+ rects.Add(rightBottom);
+ rects.Add(leftBottom);
+ rects.Add(leftBottom);
+ rects.Add(leftTop);
}
- handle.DrawPrimitives(DrawPrimitiveTopology.LineList, airlockLines.Span, sRGB);
+ if (rects.Count > 0)
+ handle.DrawPrimitives(DrawPrimitiveTopology.LineList, rects.Span, wallsRGB);
}
+ // Invoke post wall drawing action
if (PostWallDrawingAction != null)
PostWallDrawingAction.Invoke(handle);
@@ -373,7 +373,7 @@ protected override void Draw(DrawingHandleScreen handle)
var rectBuffer = new Vector2(5f, 3f);
// Calculate font size for current zoom level
- var fontSize = (int) Math.Round(1 / WorldRange * DefaultDisplayedRange * UIScale * _targetFontsize , 0);
+ var fontSize = (int) Math.Round(1 / WorldRange * DefaultDisplayedRange * UIScale * _targetFontsize, 0);
var font = new VectorFont(_cache.GetResource("/Fonts/NotoSans/NotoSans-Bold.ttf"), fontSize);
foreach (var beacon in _navMap.Beacons)
@@ -409,8 +409,6 @@ protected override void Draw(DrawingHandleScreen handle)
}
// Tracked entities (can use a supplied sprite as a marker instead; should probably just replace TrackedCoordinates with this eventually)
- var iconVertexUVs = new Dictionary<(Texture, Color), ValueList>();
-
foreach (var blip in TrackedEntities.Values)
{
if (blip.Blinks && !lit)
@@ -419,9 +417,6 @@ protected override void Draw(DrawingHandleScreen handle)
if (blip.Texture == null)
continue;
- if (!iconVertexUVs.TryGetValue((blip.Texture, blip.Color), out var vertexUVs))
- vertexUVs = new();
-
var mapPos = blip.Coordinates.ToMap(EntManager, _transformSystem);
if (mapPos.MapId != MapId.Nullspace)
@@ -429,29 +424,11 @@ protected override void Draw(DrawingHandleScreen handle)
var position = _transformSystem.GetInvWorldMatrix(_xform).Transform(mapPos.Position) - offset;
position = ScalePosition(new Vector2(position.X, -position.Y));
- var scalingCoefficient = 2.5f;
- var positionOffset = scalingCoefficient * float.Sqrt(MinimapScale);
-
- vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X - positionOffset, position.Y - positionOffset), new Vector2(1f, 1f)));
- vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X - positionOffset, position.Y + positionOffset), new Vector2(1f, 0f)));
- vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X + positionOffset, position.Y - positionOffset), new Vector2(0f, 1f)));
- vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X - positionOffset, position.Y + positionOffset), new Vector2(1f, 0f)));
- vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X + positionOffset, position.Y - positionOffset), new Vector2(0f, 1f)));
- vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X + positionOffset, position.Y + positionOffset), new Vector2(0f, 0f)));
- }
-
- iconVertexUVs[(blip.Texture, blip.Color)] = vertexUVs;
- }
+ var scalingCoefficient = MinmapScaleModifier * float.Sqrt(MinimapScale);
+ var positionOffset = new Vector2(scalingCoefficient * blip.Texture.Width, scalingCoefficient * blip.Texture.Height);
- foreach ((var (texture, color), var vertexUVs) in iconVertexUVs)
- {
- if (!_sRGBLookUp.TryGetValue(color, out var sRGB))
- {
- sRGB = Color.ToSrgb(color);
- _sRGBLookUp[color] = sRGB;
+ handle.DrawTextureRect(blip.Texture, new UIBox2(position - positionOffset, position + positionOffset), blip.Color);
}
-
- handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, texture, vertexUVs.Span, sRGB);
}
}
@@ -470,123 +447,293 @@ protected override void FrameUpdate(FrameEventArgs args)
protected virtual void UpdateNavMap()
{
- if (_navMap == null || _grid == null)
+ // Clear stale values
+ TilePolygons.Clear();
+ TileLines.Clear();
+ TileRects.Clear();
+
+ UpdateNavMapFloorTiles();
+ UpdateNavMapWallLines();
+ UpdateNavMapAirlocks();
+ }
+
+ private void UpdateNavMapFloorTiles()
+ {
+ if (_fixtures == null)
return;
- TileGrid = GetDecodedWallChunks(_navMap.Chunks, _grid);
+ var verts = new Vector2[8];
+
+ foreach (var fixture in _fixtures.Fixtures.Values)
+ {
+ if (fixture.Shape is not PolygonShape poly)
+ continue;
+
+ for (var i = 0; i < poly.VertexCount; i++)
+ {
+ var vert = poly.Vertices[i];
+ verts[i] = new Vector2(MathF.Round(vert.X), MathF.Round(vert.Y));
+ }
+
+ TilePolygons.Add((verts[..poly.VertexCount], TileColor));
+ }
}
- public Dictionary> GetDecodedWallChunks
- (Dictionary chunks,
- MapGridComponent grid)
+ private void UpdateNavMapWallLines()
{
- var decodedOutput = new Dictionary>();
+ if (_navMap == null || _grid == null)
+ return;
- foreach ((var chunkOrigin, var chunk) in chunks)
+ // We'll use the following dictionaries to combine collinear wall lines
+ HorizLinesLookup.Clear();
+ HorizLinesLookupReversed.Clear();
+ VertLinesLookup.Clear();
+ VertLinesLookupReversed.Clear();
+
+ foreach ((var (category, chunkOrigin), var chunk) in _navMap.Chunks)
{
- var list = new List();
+ if (category != NavMapChunkType.Wall)
+ continue;
- // TODO: Okay maybe I should just use ushorts lmao...
for (var i = 0; i < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; i++)
{
- var value = (int) Math.Pow(2, i);
-
- var mask = chunk.TileData & value;
+ var value = (ushort) Math.Pow(2, i);
+ var mask = _navMapSystem.GetCombinedEdgesForChunk(chunk.TileData) & value;
if (mask == 0x0)
continue;
- // Alright now we'll work out our edges
var relativeTile = SharedNavMapSystem.GetTile(mask);
- var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relativeTile) * grid.TileSize;
- var position = new Vector2(tile.X, -tile.Y);
+ var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relativeTile) * _grid.TileSize;
+
+ if (!_navMapSystem.AllTileEdgesAreOccupied(chunk.TileData, relativeTile))
+ {
+ AddRectForThinWall(chunk.TileData, tile);
+ continue;
+ }
+
+ tile = tile with { Y = -tile.Y };
+
NavMapChunk? neighborChunk;
bool neighbor;
// North edge
if (relativeTile.Y == SharedNavMapSystem.ChunkSize - 1)
{
- neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(0, 1), out neighborChunk) &&
- (neighborChunk.TileData &
+ neighbor = _navMap.Chunks.TryGetValue((NavMapChunkType.Wall, chunkOrigin + new Vector2i(0, 1)), out neighborChunk) &&
+ (neighborChunk.TileData[AtmosDirection.South] &
SharedNavMapSystem.GetFlag(new Vector2i(relativeTile.X, 0))) != 0x0;
}
else
{
var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(0, 1));
- neighbor = (chunk.TileData & flag) != 0x0;
+ neighbor = (chunk.TileData[AtmosDirection.South] & flag) != 0x0;
}
if (!neighbor)
- {
- // Add points
- list.Add(new NavMapLine(position + new Vector2(0f, -grid.TileSize), position + new Vector2(grid.TileSize, -grid.TileSize)));
- }
+ AddOrUpdateNavMapLine(tile + new Vector2i(0, -_grid.TileSize), tile + new Vector2i(_grid.TileSize, -_grid.TileSize), HorizLinesLookup, HorizLinesLookupReversed);
// East edge
if (relativeTile.X == SharedNavMapSystem.ChunkSize - 1)
{
- neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(1, 0), out neighborChunk) &&
- (neighborChunk.TileData &
+ neighbor = _navMap.Chunks.TryGetValue((NavMapChunkType.Wall, chunkOrigin + new Vector2i(1, 0)), out neighborChunk) &&
+ (neighborChunk.TileData[AtmosDirection.West] &
SharedNavMapSystem.GetFlag(new Vector2i(0, relativeTile.Y))) != 0x0;
}
else
{
var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(1, 0));
- neighbor = (chunk.TileData & flag) != 0x0;
+ neighbor = (chunk.TileData[AtmosDirection.West] & flag) != 0x0;
}
if (!neighbor)
- {
- // Add points
- list.Add(new NavMapLine(position + new Vector2(grid.TileSize, -grid.TileSize), position + new Vector2(grid.TileSize, 0f)));
- }
+ AddOrUpdateNavMapLine(tile + new Vector2i(_grid.TileSize, -_grid.TileSize), tile + new Vector2i(_grid.TileSize, 0), VertLinesLookup, VertLinesLookupReversed);
// South edge
if (relativeTile.Y == 0)
{
- neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(0, -1), out neighborChunk) &&
- (neighborChunk.TileData &
+ neighbor = _navMap.Chunks.TryGetValue((NavMapChunkType.Wall, chunkOrigin + new Vector2i(0, -1)), out neighborChunk) &&
+ (neighborChunk.TileData[AtmosDirection.North] &
SharedNavMapSystem.GetFlag(new Vector2i(relativeTile.X, SharedNavMapSystem.ChunkSize - 1))) != 0x0;
}
else
{
var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(0, -1));
- neighbor = (chunk.TileData & flag) != 0x0;
+ neighbor = (chunk.TileData[AtmosDirection.North] & flag) != 0x0;
}
if (!neighbor)
- {
- // Add points
- list.Add(new NavMapLine(position + new Vector2(grid.TileSize, 0f), position));
- }
+ AddOrUpdateNavMapLine(tile, tile + new Vector2i(_grid.TileSize, 0), HorizLinesLookup, HorizLinesLookupReversed);
// West edge
if (relativeTile.X == 0)
{
- neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(-1, 0), out neighborChunk) &&
- (neighborChunk.TileData &
+ neighbor = _navMap.Chunks.TryGetValue((NavMapChunkType.Wall, chunkOrigin + new Vector2i(-1, 0)), out neighborChunk) &&
+ (neighborChunk.TileData[AtmosDirection.East] &
SharedNavMapSystem.GetFlag(new Vector2i(SharedNavMapSystem.ChunkSize - 1, relativeTile.Y))) != 0x0;
}
else
{
var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(-1, 0));
- neighbor = (chunk.TileData & flag) != 0x0;
+ neighbor = (chunk.TileData[AtmosDirection.East] & flag) != 0x0;
}
if (!neighbor)
+ AddOrUpdateNavMapLine(tile + new Vector2i(0, -_grid.TileSize), tile, VertLinesLookup, VertLinesLookupReversed);
+
+ // Add a diagonal line for interiors. Unless there are a lot of double walls, there is no point combining these
+ TileLines.Add((tile + new Vector2(0, -_grid.TileSize), tile + new Vector2(_grid.TileSize, 0)));
+ }
+ }
+
+ // Record the combined lines
+ foreach (var (origin, terminal) in HorizLinesLookup)
+ TileLines.Add((origin.Item2, terminal.Item2));
+
+ foreach (var (origin, terminal) in VertLinesLookup)
+ TileLines.Add((origin.Item2, terminal.Item2));
+ }
+
+ private void UpdateNavMapAirlocks()
+ {
+ if (_navMap == null || _grid == null)
+ return;
+
+ foreach (var ((category, _), chunk) in _navMap.Chunks)
+ {
+ if (category != NavMapChunkType.Airlock)
+ continue;
+
+ for (var i = 0; i < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; i++)
+ {
+ var value = (int) Math.Pow(2, i);
+ var mask = _navMapSystem.GetCombinedEdgesForChunk(chunk.TileData) & value;
+
+ if (mask == 0x0)
+ continue;
+
+ var relative = SharedNavMapSystem.GetTile(mask);
+ var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relative) * _grid.TileSize;
+
+ // If the edges of an airlock tile are not all occupied, draw a thin airlock for each edge
+ if (!_navMapSystem.AllTileEdgesAreOccupied(chunk.TileData, relative))
{
- // Add point
- list.Add(new NavMapLine(position, position + new Vector2(0f, -grid.TileSize)));
+ AddRectForThinAirlock(chunk.TileData, tile);
+ continue;
}
- // Draw a diagonal line for interiors.
- list.Add(new NavMapLine(position + new Vector2(0f, -grid.TileSize), position + new Vector2(grid.TileSize, 0f)));
+ // Otherwise add a single full tile airlock
+ TileRects.Add((new Vector2(tile.X + FullWallInstep, -tile.Y - FullWallInstep),
+ new Vector2(tile.X - FullWallInstep + 1f, -tile.Y + FullWallInstep - 1)));
+
+ TileLines.Add((new Vector2(tile.X + 0.5f, -tile.Y - FullWallInstep),
+ new Vector2(tile.X + 0.5f, -tile.Y + FullWallInstep - 1)));
}
+ }
+ }
- decodedOutput.Add(chunkOrigin, list);
+ private void AddRectForThinWall(Dictionary tileData, Vector2i tile)
+ {
+ if (_navMapSystem == null || _grid == null)
+ return;
+
+ var leftTop = new Vector2(-0.5f, -0.5f + ThinWallThickness);
+ var rightBottom = new Vector2(0.5f, -0.5f);
+
+ foreach (var (direction, mask) in tileData)
+ {
+ var relative = SharedMapSystem.GetChunkRelative(tile, SharedNavMapSystem.ChunkSize);
+ var flag = (ushort) SharedNavMapSystem.GetFlag(relative);
+
+ if ((mask & flag) == 0)
+ continue;
+
+ var tilePosition = new Vector2(tile.X + 0.5f, -tile.Y - 0.5f);
+ var angle = new Angle(0);
+
+ switch (direction)
+ {
+ case AtmosDirection.East: angle = new Angle(MathF.PI * 0.5f); break;
+ case AtmosDirection.South: angle = new Angle(MathF.PI); break;
+ case AtmosDirection.West: angle = new Angle(MathF.PI * -0.5f); break;
+ }
+
+ TileRects.Add((angle.RotateVec(leftTop) + tilePosition, angle.RotateVec(rightBottom) + tilePosition));
+ }
+ }
+
+ private void AddRectForThinAirlock(Dictionary tileData, Vector2i tile)
+ {
+ if (_navMapSystem == null || _grid == null)
+ return;
+
+ var leftTop = new Vector2(-0.5f + FullWallInstep, -0.5f + FullWallInstep + ThinDoorThickness);
+ var rightBottom = new Vector2(0.5f - FullWallInstep, -0.5f + FullWallInstep);
+ var centreTop = new Vector2(0f, -0.5f + FullWallInstep + ThinDoorThickness);
+ var centreBottom = new Vector2(0f, -0.5f + FullWallInstep);
+
+ foreach (var (direction, mask) in tileData)
+ {
+ var relative = SharedMapSystem.GetChunkRelative(tile, SharedNavMapSystem.ChunkSize);
+ var flag = (ushort) SharedNavMapSystem.GetFlag(relative);
+
+ if ((mask & flag) == 0)
+ continue;
+
+ var tilePosition = new Vector2(tile.X + 0.5f, -tile.Y - 0.5f);
+ var angle = new Angle(0);
+
+ switch (direction)
+ {
+ case AtmosDirection.East: angle = new Angle(MathF.PI * 0.5f);break;
+ case AtmosDirection.South: angle = new Angle(MathF.PI); break;
+ case AtmosDirection.West: angle = new Angle(MathF.PI * -0.5f); break;
+ }
+
+ TileRects.Add((angle.RotateVec(leftTop) + tilePosition, angle.RotateVec(rightBottom) + tilePosition));
+ TileLines.Add((angle.RotateVec(centreTop) + tilePosition, angle.RotateVec(centreBottom) + tilePosition));
+ }
+ }
+
+ protected void AddOrUpdateNavMapLine
+ (Vector2i origin,
+ Vector2i terminus,
+ Dictionary<(int, Vector2i), (int, Vector2i)> lookup,
+ Dictionary<(int, Vector2i), (int, Vector2i)> lookupReversed,
+ int index = 0)
+ {
+ (int, Vector2i) foundTermiusTuple;
+ (int, Vector2i) foundOriginTuple;
+
+ if (lookup.TryGetValue((index, terminus), out foundTermiusTuple) &&
+ lookupReversed.TryGetValue((index, origin), out foundOriginTuple))
+ {
+ lookup[foundOriginTuple] = foundTermiusTuple;
+ lookupReversed[foundTermiusTuple] = foundOriginTuple;
+
+ lookup.Remove((index, terminus));
+ lookupReversed.Remove((index, origin));
+ }
+
+ else if (lookup.TryGetValue((index, terminus), out foundTermiusTuple))
+ {
+ lookup[(index, origin)] = foundTermiusTuple;
+ lookup.Remove((index, terminus));
+ lookupReversed[foundTermiusTuple] = (index, origin);
+ }
+
+ else if (lookupReversed.TryGetValue((index, origin), out foundOriginTuple))
+ {
+ lookupReversed[(index, terminus)] = foundOriginTuple;
+ lookupReversed.Remove(foundOriginTuple);
+ lookup[foundOriginTuple] = (index, terminus);
}
- return decodedOutput;
+ else
+ {
+ lookup.Add((index, origin), (index, terminus));
+ lookupReversed.Add((index, terminus), (index, origin));
+ }
}
protected Vector2 GetOffset()
@@ -612,15 +759,3 @@ public NavMapBlip(EntityCoordinates coordinates, Texture texture, Color color, b
Selectable = selectable;
}
}
-
-public struct NavMapLine
-{
- public readonly Vector2 Origin;
- public readonly Vector2 Terminus;
-
- public NavMapLine(Vector2 origin, Vector2 terminus)
- {
- Origin = origin;
- Terminus = terminus;
- }
-}
diff --git a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs
index 82ac4aa28f3..8193f795696 100644
--- a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs
+++ b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs
@@ -7,12 +7,13 @@
using Robust.Client.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
+using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.Players.PlayTimeTracking;
-public sealed class JobRequirementsManager
+public sealed class JobRequirementsManager : ISharedPlaytimeManager
{
[Dependency] private readonly IBaseClient _client = default!;
[Dependency] private readonly IClientNetManager _net = default!;
@@ -133,5 +134,13 @@ public IEnumerable> FetchPlaytimeByRoles()
}
}
+ public IReadOnlyDictionary GetPlayTimes(ICommonSession session)
+ {
+ if (session != _playerManager.LocalSession)
+ {
+ return new Dictionary();
+ }
+ return _roles;
+ }
}
diff --git a/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs b/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs
new file mode 100644
index 00000000000..5ba4878c6d4
--- /dev/null
+++ b/Content.Client/Polymorph/Systems/ChameleonProjectorSystem.cs
@@ -0,0 +1,33 @@
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Polymorph.Components;
+using Content.Shared.Polymorph.Systems;
+using Robust.Client.GameObjects;
+
+namespace Content.Client.Polymorph.Systems;
+
+public sealed class ChameleonProjectorSystem : SharedChameleonProjectorSystem
+{
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+
+ private EntityQuery _appearanceQuery;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _appearanceQuery = GetEntityQuery();
+
+ SubscribeLocalEvent(OnHandleState);
+ }
+
+ private void OnHandleState(Entity ent, ref AfterAutoHandleStateEvent args)
+ {
+ CopyComp(ent);
+ CopyComp(ent);
+ CopyComp(ent);
+
+ // reload appearance to hopefully prevent any invisible layers
+ if (_appearanceQuery.TryComp(ent, out var appearance))
+ _appearance.QueueUpdate(ent, appearance);
+ }
+}
diff --git a/Content.Client/Popups/PopupSystem.cs b/Content.Client/Popups/PopupSystem.cs
index fcc8bfc420a..1ef8dfba2d1 100644
--- a/Content.Client/Popups/PopupSystem.cs
+++ b/Content.Client/Popups/PopupSystem.cs
@@ -5,7 +5,6 @@
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
-using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
@@ -163,6 +162,15 @@ public override void PopupEntity(string? message, EntityUid uid, Filter filter,
PopupEntity(message, uid, type);
}
+ public override void PopupClient(string? message, EntityUid? recipient, PopupType type = PopupType.Small)
+ {
+ if (recipient == null)
+ return;
+
+ if (_timing.IsFirstTimePredicted)
+ PopupCursor(message, recipient.Value, type);
+ }
+
public override void PopupClient(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small)
{
if (recipient == null)
@@ -172,6 +180,15 @@ public override void PopupClient(string? message, EntityUid uid, EntityUid? reci
PopupEntity(message, uid, recipient.Value, type);
}
+ public override void PopupClient(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small)
+ {
+ if (recipient == null)
+ return;
+
+ if (_timing.IsFirstTimePredicted)
+ PopupCoordinates(message, coordinates, recipient.Value, type);
+ }
+
public override void PopupEntity(string? message, EntityUid uid, PopupType type = PopupType.Small)
{
if (TryComp(uid, out TransformComponent? transform))
@@ -184,6 +201,12 @@ public override void PopupPredicted(string? message, EntityUid uid, EntityUid? r
PopupEntity(message, uid, recipient.Value, type);
}
+ public override void PopupPredicted(string? recipientMessage, string? othersMessage, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small)
+ {
+ if (recipient != null && _timing.IsFirstTimePredicted)
+ PopupEntity(recipientMessage, uid, recipient.Value, type);
+ }
+
#endregion
#region Network Event Handlers
diff --git a/Content.Client/Power/ActivatableUIRequiresPowerSystem.cs b/Content.Client/Power/ActivatableUIRequiresPowerSystem.cs
new file mode 100644
index 00000000000..60ed8d87b9e
--- /dev/null
+++ b/Content.Client/Power/ActivatableUIRequiresPowerSystem.cs
@@ -0,0 +1,21 @@
+using Content.Shared.Power.Components;
+using Content.Shared.UserInterface;
+using Content.Shared.Wires;
+
+namespace Content.Client.Power;
+
+public sealed class ActivatableUIRequiresPowerSystem : EntitySystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnActivate);
+ }
+
+ private void OnActivate(EntityUid uid, ActivatableUIRequiresPowerComponent component, ActivatableUIOpenAttemptEvent args)
+ {
+ // Client can't predict the power properly at the moment so rely upon the server to do it.
+ args.Cancel();
+ }
+}
diff --git a/Content.Client/Power/PowerMonitoringConsoleNavMapControl.cs b/Content.Client/Power/PowerMonitoringConsoleNavMapControl.cs
index 902d6bb7e60..3d94318be82 100644
--- a/Content.Client/Power/PowerMonitoringConsoleNavMapControl.cs
+++ b/Content.Client/Power/PowerMonitoringConsoleNavMapControl.cs
@@ -23,8 +23,8 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
public PowerMonitoringCableNetworksComponent? PowerMonitoringCableNetworks;
public List HiddenLineGroups = new();
- public Dictionary>? PowerCableNetwork;
- public Dictionary>? FocusCableNetwork;
+ public List PowerCableNetwork = new();
+ public List FocusCableNetwork = new();
private MapGridComponent? _grid;
@@ -48,15 +48,15 @@ protected override void UpdateNavMap()
if (!_entManager.TryGetComponent(Owner, out var cableNetworks))
return;
- if (!_entManager.TryGetComponent(MapUid, out _grid))
- return;
-
- PowerCableNetwork = GetDecodedPowerCableChunks(cableNetworks.AllChunks, _grid);
- FocusCableNetwork = GetDecodedPowerCableChunks(cableNetworks.FocusChunks, _grid);
+ PowerCableNetwork = GetDecodedPowerCableChunks(cableNetworks.AllChunks);
+ FocusCableNetwork = GetDecodedPowerCableChunks(cableNetworks.FocusChunks);
}
public void DrawAllCableNetworks(DrawingHandleScreen handle)
{
+ if (!_entManager.TryGetComponent(MapUid, out _grid))
+ return;
+
// Draw full cable network
if (PowerCableNetwork != null && PowerCableNetwork.Count > 0)
{
@@ -69,36 +69,29 @@ public void DrawAllCableNetworks(DrawingHandleScreen handle)
DrawCableNetwork(handle, FocusCableNetwork, Color.White);
}
- public void DrawCableNetwork(DrawingHandleScreen handle, Dictionary> fullCableNetwork, Color modulator)
+ public void DrawCableNetwork(DrawingHandleScreen handle, List fullCableNetwork, Color modulator)
{
+ if (!_entManager.TryGetComponent(MapUid, out _grid))
+ return;
+
var offset = GetOffset();
- var area = new Box2(-WorldRange, -WorldRange, WorldRange + 1f, WorldRange + 1f).Translated(offset);
+ offset = offset with { Y = -offset.Y };
if (WorldRange / WorldMaxRange > 0.5f)
{
var cableNetworks = new ValueList[3];
- foreach ((var chunk, var chunkedLines) in fullCableNetwork)
+ foreach (var line in fullCableNetwork)
{
- var offsetChunk = new Vector2(chunk.X, chunk.Y) * SharedNavMapSystem.ChunkSize;
-
- if (offsetChunk.X < area.Left - SharedNavMapSystem.ChunkSize || offsetChunk.X > area.Right)
- continue;
-
- if (offsetChunk.Y < area.Bottom - SharedNavMapSystem.ChunkSize || offsetChunk.Y > area.Top)
+ if (HiddenLineGroups.Contains(line.Group))
continue;
- foreach (var chunkedLine in chunkedLines)
- {
- if (HiddenLineGroups.Contains(chunkedLine.Group))
- continue;
-
- var start = ScalePosition(chunkedLine.Origin - new Vector2(offset.X, -offset.Y));
- var end = ScalePosition(chunkedLine.Terminus - new Vector2(offset.X, -offset.Y));
+ var cableOffset = _powerCableOffsets[(int) line.Group];
+ var start = ScalePosition(line.Origin + cableOffset - offset);
+ var end = ScalePosition(line.Terminus + cableOffset - offset);
- cableNetworks[(int) chunkedLine.Group].Add(start);
- cableNetworks[(int) chunkedLine.Group].Add(end);
- }
+ cableNetworks[(int) line.Group].Add(start);
+ cableNetworks[(int) line.Group].Add(end);
}
for (int cableNetworkIdx = 0; cableNetworkIdx < cableNetworks.Length; cableNetworkIdx++)
@@ -124,48 +117,39 @@ public void DrawCableNetwork(DrawingHandleScreen handle, Dictionary[3];
- foreach ((var chunk, var chunkedLines) in fullCableNetwork)
+ foreach (var line in fullCableNetwork)
{
- var offsetChunk = new Vector2(chunk.X, chunk.Y) * SharedNavMapSystem.ChunkSize;
-
- if (offsetChunk.X < area.Left - SharedNavMapSystem.ChunkSize || offsetChunk.X > area.Right)
+ if (HiddenLineGroups.Contains(line.Group))
continue;
- if (offsetChunk.Y < area.Bottom - SharedNavMapSystem.ChunkSize || offsetChunk.Y > area.Top)
- continue;
-
- foreach (var chunkedLine in chunkedLines)
- {
- if (HiddenLineGroups.Contains(chunkedLine.Group))
- continue;
-
- var leftTop = ScalePosition(new Vector2
- (Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
- Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
- - new Vector2(offset.X, -offset.Y));
-
- var rightTop = ScalePosition(new Vector2
- (Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
- Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
- - new Vector2(offset.X, -offset.Y));
-
- var leftBottom = ScalePosition(new Vector2
- (Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
- Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
- - new Vector2(offset.X, -offset.Y));
-
- var rightBottom = ScalePosition(new Vector2
- (Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
- Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
- - new Vector2(offset.X, -offset.Y));
-
- cableVertexUVs[(int) chunkedLine.Group].Add(leftBottom);
- cableVertexUVs[(int) chunkedLine.Group].Add(leftTop);
- cableVertexUVs[(int) chunkedLine.Group].Add(rightBottom);
- cableVertexUVs[(int) chunkedLine.Group].Add(leftTop);
- cableVertexUVs[(int) chunkedLine.Group].Add(rightBottom);
- cableVertexUVs[(int) chunkedLine.Group].Add(rightTop);
- }
+ var cableOffset = _powerCableOffsets[(int) line.Group];
+
+ var leftTop = ScalePosition(new Vector2
+ (Math.Min(line.Origin.X, line.Terminus.X) - 0.1f,
+ Math.Min(line.Origin.Y, line.Terminus.Y) - 0.1f)
+ + cableOffset - offset);
+
+ var rightTop = ScalePosition(new Vector2
+ (Math.Max(line.Origin.X, line.Terminus.X) + 0.1f,
+ Math.Min(line.Origin.Y, line.Terminus.Y) - 0.1f)
+ + cableOffset - offset);
+
+ var leftBottom = ScalePosition(new Vector2
+ (Math.Min(line.Origin.X, line.Terminus.X) - 0.1f,
+ Math.Max(line.Origin.Y, line.Terminus.Y) + 0.1f)
+ + cableOffset - offset);
+
+ var rightBottom = ScalePosition(new Vector2
+ (Math.Max(line.Origin.X, line.Terminus.X) + 0.1f,
+ Math.Max(line.Origin.Y, line.Terminus.Y) + 0.1f)
+ + cableOffset - offset);
+
+ cableVertexUVs[(int) line.Group].Add(leftBottom);
+ cableVertexUVs[(int) line.Group].Add(leftTop);
+ cableVertexUVs[(int) line.Group].Add(rightBottom);
+ cableVertexUVs[(int) line.Group].Add(leftTop);
+ cableVertexUVs[(int) line.Group].Add(rightBottom);
+ cableVertexUVs[(int) line.Group].Add(rightTop);
}
for (int cableNetworkIdx = 0; cableNetworkIdx < cableVertexUVs.Length; cableNetworkIdx++)
@@ -188,23 +172,28 @@ public void DrawCableNetwork(DrawingHandleScreen handle, Dictionary>? GetDecodedPowerCableChunks(Dictionary? chunks, MapGridComponent? grid)
+ public List GetDecodedPowerCableChunks(Dictionary? chunks)
{
- if (chunks == null || grid == null)
- return null;
+ var decodedOutput = new List();
+
+ if (!_entManager.TryGetComponent(MapUid, out _grid))
+ return decodedOutput;
+
+ if (chunks == null)
+ return decodedOutput;
- var decodedOutput = new Dictionary>();
+ // We'll use the following dictionaries to combine collinear power cable lines
+ HorizLinesLookup.Clear();
+ HorizLinesLookupReversed.Clear();
+ VertLinesLookup.Clear();
+ VertLinesLookupReversed.Clear();
foreach ((var chunkOrigin, var chunk) in chunks)
{
- var list = new List();
-
for (int cableIdx = 0; cableIdx < chunk.PowerCableData.Length; cableIdx++)
{
var chunkMask = chunk.PowerCableData[cableIdx];
- Vector2 offset = _powerCableOffsets[cableIdx];
-
for (var chunkIdx = 0; chunkIdx < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; chunkIdx++)
{
var value = (int) Math.Pow(2, chunkIdx);
@@ -214,8 +203,8 @@ public void DrawCableNetwork(DrawingHandleScreen handle, Dictionary 0)
- decodedOutput.Add(chunkOrigin, list);
}
+ var gridOffset = new Vector2(_grid.TileSize * 0.5f, -_grid.TileSize * 0.5f);
+
+ foreach (var (origin, terminal) in HorizLinesLookup)
+ decodedOutput.Add(new PowerMonitoringConsoleLine(origin.Item2 + gridOffset, terminal.Item2 + gridOffset, (PowerMonitoringConsoleLineGroup) origin.Item1));
+
+ foreach (var (origin, terminal) in VertLinesLookup)
+ decodedOutput.Add(new PowerMonitoringConsoleLine(origin.Item2 + gridOffset, terminal.Item2 + gridOffset, (PowerMonitoringConsoleLineGroup) origin.Item1));
+
return decodedOutput;
}
}
diff --git a/Content.Client/Power/PowerMonitoringWindow.xaml.cs b/Content.Client/Power/PowerMonitoringWindow.xaml.cs
index 4c692645e80..f4b3db25833 100644
--- a/Content.Client/Power/PowerMonitoringWindow.xaml.cs
+++ b/Content.Client/Power/PowerMonitoringWindow.xaml.cs
@@ -170,9 +170,6 @@ public void ShowEntites
NavMap.TrackedEntities[mon.Value] = blip;
}
- // Update nav map
- NavMap.ForceNavMapUpdate();
-
// If the entry group doesn't match the current tab, the data is out dated, do not use it
if (allEntries.Length > 0 && allEntries[0].Group != GetCurrentPowerMonitoringConsoleGroup())
return;
diff --git a/Content.Client/Preferences/ClientPreferencesManager.cs b/Content.Client/Preferences/ClientPreferencesManager.cs
index 18b855e3cb5..9a7fdb37cd8 100644
--- a/Content.Client/Preferences/ClientPreferencesManager.cs
+++ b/Content.Client/Preferences/ClientPreferencesManager.cs
@@ -1,11 +1,9 @@
-using System;
-using System.Collections.Generic;
using System.Linq;
using Content.Corvax.Interfaces.Client;
using Content.Shared.Preferences;
using Robust.Client;
+using Robust.Client.Player;
using Robust.Shared.Configuration;
-using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
@@ -17,12 +15,11 @@ namespace Content.Client.Preferences
/// connection.
/// Stores preferences on the server through and .
///
- public sealed class ClientPreferencesManager : IClientPreferencesManager
+ public partial class ClientPreferencesManager : IClientPreferencesManager
{
[Dependency] private readonly IClientNetManager _netManager = default!;
[Dependency] private readonly IBaseClient _baseClient = default!;
- [Dependency] private readonly IConfigurationManager _cfg = default!;
- [Dependency] private readonly IPrototypeManager _prototypes = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
private IClientSponsorsManager? _sponsorsManager; // Corvax-Sponsors
public event Action? OnServerDataLoaded;
@@ -67,9 +64,10 @@ public void SelectCharacter(int slot)
public void UpdateCharacter(ICharacterProfile profile, int slot)
{
+ var collection = IoCManager.Instance!;
// Corvax-Sponsors-Start
var sponsorPrototypes = _sponsorsManager?.Prototypes.ToArray() ?? [];
- profile.EnsureValid(_cfg, _prototypes, sponsorPrototypes);
+ profile.EnsureValid(_playerManager.LocalSession!, collection, sponsorPrototypes);
// Corvax-Sponsors-End
var characters = new Dictionary(Preferences.Characters) {[slot] = profile};
Preferences = new PlayerPreferences(characters, Preferences.SelectedCharacterIndex, Preferences.AdminOOCColor);
diff --git a/Content.Client/Preferences/UI/AntagPreferenceSelector.cs b/Content.Client/Preferences/UI/AntagPreferenceSelector.cs
new file mode 100644
index 00000000000..654c393b267
--- /dev/null
+++ b/Content.Client/Preferences/UI/AntagPreferenceSelector.cs
@@ -0,0 +1,41 @@
+using Content.Client.Players.PlayTimeTracking;
+using Content.Shared.Roles;
+using Robust.Client.UserInterface.Controls;
+
+namespace Content.Client.Preferences.UI;
+
+public sealed class AntagPreferenceSelector : RequirementsSelector
+{
+ // 0 is yes and 1 is no
+ public bool Preference
+ {
+ get => Options.SelectedValue == 0;
+ set => Options.Select((value && !Disabled) ? 0 : 1);
+ }
+
+ public event Action? PreferenceChanged;
+
+ public AntagPreferenceSelector(AntagPrototype proto, ButtonGroup btnGroup)
+ : base(proto, btnGroup)
+ {
+ Options.OnItemSelected += args => PreferenceChanged?.Invoke(Preference);
+
+ var items = new[]
+ {
+ ("humanoid-profile-editor-antag-preference-yes-button", 0),
+ ("humanoid-profile-editor-antag-preference-no-button", 1)
+ };
+ var title = Loc.GetString(proto.Name);
+ var description = Loc.GetString(proto.Objective);
+ // Not supported yet get fucked.
+ Setup(null, items, title, 250, description);
+
+ // immediately lock requirements if they arent met.
+ // another function checks Disabled after creating the selector so this has to be done now
+ var requirements = IoCManager.Resolve();
+ if (proto.Requirements != null && !requirements.CheckRoleTime(proto.Requirements, out var reason))
+ {
+ LockRequirements(reason);
+ }
+ }
+}
diff --git a/Content.Client/Preferences/UI/CharacterSetupGui.xaml b/Content.Client/Preferences/UI/CharacterSetupGui.xaml
index 40aa24cd15b..989ad98efe1 100644
--- a/Content.Client/Preferences/UI/CharacterSetupGui.xaml
+++ b/Content.Client/Preferences/UI/CharacterSetupGui.xaml
@@ -46,7 +46,7 @@
-
+
diff --git a/Content.Client/Preferences/UI/CharacterSetupGui.xaml.cs b/Content.Client/Preferences/UI/CharacterSetupGui.xaml.cs
index 8bb123d963f..205735ceac5 100644
--- a/Content.Client/Preferences/UI/CharacterSetupGui.xaml.cs
+++ b/Content.Client/Preferences/UI/CharacterSetupGui.xaml.cs
@@ -3,28 +3,24 @@
using Content.Client.Humanoid;
using Content.Client.Info;
using Content.Client.Info.PlaytimeStats;
-using Content.Client.Lobby.UI;
+using Content.Client.Lobby;
using Content.Client.Resources;
using Content.Client.Stylesheets;
using Content.Corvax.Interfaces.Client;
+using Content.Shared.Clothing;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
+using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
-using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Localization;
using Robust.Shared.Map;
-using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using static Robust.Client.UserInterface.Controls.BoxContainer;
using Direction = Robust.Shared.Maths.Direction;
@@ -37,7 +33,6 @@ public sealed partial class CharacterSetupGui : Control
private readonly IClientPreferencesManager _preferencesManager;
private readonly IEntityManager _entityManager;
private readonly IPrototypeManager _prototypeManager;
- private readonly IConfigurationManager _configurationManager;
private readonly Button _createNewCharacterButton;
private readonly HumanoidProfileEditor _humanoidProfileEditor;
@@ -52,7 +47,6 @@ public CharacterSetupGui(
_entityManager = entityManager;
_prototypeManager = prototypeManager;
_preferencesManager = preferencesManager;
- _configurationManager = configurationManager;
var panelTex = resourceCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
var back = new StyleBoxTexture
@@ -75,7 +69,7 @@ public CharacterSetupGui(
args.Event.Handle();
};
- _humanoidProfileEditor = new HumanoidProfileEditor(preferencesManager, prototypeManager, entityManager, configurationManager);
+ _humanoidProfileEditor = new HumanoidProfileEditor(preferencesManager, prototypeManager, configurationManager);
_humanoidProfileEditor.OnProfileChanged += ProfileChanged;
CharEditor.AddChild(_humanoidProfileEditor);
@@ -111,6 +105,12 @@ private void ProfileChanged(ICharacterProfile profile, int profileSlot)
UpdateUI();
}
+ public void UpdateControls()
+ {
+ // Reset sliders etc. upon going going back to GUI.
+ _humanoidProfileEditor.LoadServerData();
+ }
+
private void UpdateUI()
{
var numberOfFullSlots = 0;
@@ -129,11 +129,6 @@ private void UpdateUI()
var isDisplayedMaxSlots = false; // Corvax-Sponsors: Additional slots possible
foreach (var (slot, character) in _preferencesManager.Preferences!.Characters)
{
- if (character is null)
- {
- continue;
- }
-
// Corvax-Sponsors-Start
isDisplayedMaxSlots = numberOfFullSlots >= _preferencesManager.Settings.MaxCharacterSlots;
if (isDisplayedMaxSlots) break;
@@ -153,6 +148,9 @@ private void UpdateUI()
_humanoidProfileEditor.CharacterSlot = characterIndexCopy;
_humanoidProfileEditor.UpdateControls();
_preferencesManager.SelectCharacter(character);
+ var controller = UserInterfaceManager.GetUIController();
+ controller.UpdateProfile(_humanoidProfileEditor.Profile);
+ controller.ReloadCharacterUI();
UpdateUI();
args.Event.Handle();
};
@@ -160,8 +158,12 @@ private void UpdateUI()
_createNewCharacterButton.Disabled = isDisplayedMaxSlots; // Corvax-Sponsors
Characters.AddChild(_createNewCharacterButton);
+ // TODO: Move this shit to the Lobby UI controller
}
+ ///
+ /// Shows individual characters on the side of the character GUI.
+ ///
private sealed class CharacterPickerButton : ContainerButton
{
private EntityUid _previewDummy;
@@ -192,7 +194,15 @@ public CharacterPickerButton(
if (humanoid != null)
{
- LobbyCharacterPreviewPanel.GiveDummyJobClothes(_previewDummy, humanoid);
+ var controller = UserInterfaceManager.GetUIController();
+ var job = controller.GetPreferredJob(humanoid);
+ controller.GiveDummyJobClothes(_previewDummy, humanoid, job);
+
+ if (prototypeManager.HasIndex(LoadoutSystem.GetJobPrototype(job.ID)))
+ {
+ var loadout = humanoid.GetLoadoutOrDefault(LoadoutSystem.GetJobPrototype(job.ID), entityManager, prototypeManager);
+ controller.GiveDummyLoadout(_previewDummy, loadout);
+ }
}
var isSelectedCharacter = profile == preferencesManager.Preferences?.SelectedCharacter;
diff --git a/Content.Client/Preferences/UI/HighlightedContainer.xaml b/Content.Client/Preferences/UI/HighlightedContainer.xaml
new file mode 100644
index 00000000000..8cf6e2da05b
--- /dev/null
+++ b/Content.Client/Preferences/UI/HighlightedContainer.xaml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/Content.Client/Preferences/UI/HighlightedContainer.xaml.cs b/Content.Client/Preferences/UI/HighlightedContainer.xaml.cs
new file mode 100644
index 00000000000..68294d0f059
--- /dev/null
+++ b/Content.Client/Preferences/UI/HighlightedContainer.xaml.cs
@@ -0,0 +1,14 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Preferences.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class HighlightedContainer : PanelContainer
+{
+ public HighlightedContainer()
+ {
+ RobustXamlLoader.Load(this);
+ }
+}
diff --git a/Content.Client/Preferences/UI/HumanoidProfileEditor.Random.cs b/Content.Client/Preferences/UI/HumanoidProfileEditor.Random.cs
index c9e184dfc23..750006bf7a9 100644
--- a/Content.Client/Preferences/UI/HumanoidProfileEditor.Random.cs
+++ b/Content.Client/Preferences/UI/HumanoidProfileEditor.Random.cs
@@ -5,8 +5,6 @@ namespace Content.Client.Preferences.UI
{
public sealed partial class HumanoidProfileEditor
{
- private readonly IPrototypeManager _prototypeManager;
-
private void RandomizeEverything()
{
Profile = HumanoidCharacterProfile.Random();
diff --git a/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml b/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml
index ed9970ab31b..029b7dad2ce 100644
--- a/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml
+++ b/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml
@@ -1,11 +1,11 @@
-
-
+ xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
+ HorizontalExpand="True">
-
+
@@ -58,7 +58,9 @@
-
+
@@ -85,18 +87,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
@@ -159,7 +149,7 @@
-
+
@@ -167,5 +157,4 @@
-
-
+
diff --git a/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs b/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs
index b3e34ef6173..fa33c40fe27 100644
--- a/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs
+++ b/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs
@@ -2,69 +2,48 @@
using System.Numerics;
using Content.Client.Guidebook;
using Content.Client.Humanoid;
-using Content.Client.Lobby.UI;
+using Content.Client.Lobby;
using Content.Client.Message;
using Content.Client.Players.PlayTimeTracking;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Systems.Guidebook;
using Content.Shared.CCVar;
+using Content.Shared.Clothing;
using Content.Shared.GameTicking;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Markings;
using Content.Shared.Humanoid.Prototypes;
-using Content.Shared.Inventory;
using Content.Shared.Preferences;
+using Content.Shared.Preferences.Loadouts;
+using Content.Shared.Preferences.Loadouts.Effects;
using Content.Shared.Roles;
-using Content.Shared.StatusIcon;
using Content.Shared.Traits;
using Robust.Client.AutoGenerated;
-using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Client.Utility;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
-using Robust.Shared.Map;
using Robust.Shared.Prototypes;
-using Robust.Shared.Random;
-using Robust.Shared.Timing;
using Robust.Shared.Utility;
-using static Robust.Client.UserInterface.Controls.BoxContainer;
using Direction = Robust.Shared.Maths.Direction;
namespace Content.Client.Preferences.UI
{
- public sealed class HighlightedContainer : PanelContainer
- {
- public HighlightedContainer()
- {
- PanelOverride = new StyleBoxFlat()
- {
- BackgroundColor = new Color(47, 47, 53),
- ContentMarginTopOverride = 10,
- ContentMarginBottomOverride = 10,
- ContentMarginLeftOverride = 10,
- ContentMarginRightOverride = 10
- };
- }
- }
-
[GenerateTypedNameReferences]
- public sealed partial class HumanoidProfileEditor : Control
+ public sealed partial class HumanoidProfileEditor : BoxContainer
{
private readonly IClientPreferencesManager _preferencesManager;
- private readonly IEntityManager _entMan;
- private readonly IConfigurationManager _configurationManager;
+ private readonly IPrototypeManager _prototypeManager;
private readonly MarkingManager _markingManager;
private readonly JobRequirementsManager _requirements;
private LineEdit _ageEdit => CAgeEdit;
private LineEdit _nameEdit => CNameEdit;
- private TextEdit _flavorTextEdit = null!;
+ private TextEdit? _flavorTextEdit;
private Button _nameRandomButton => CNameRandomize;
private Button _randomizeEverythingButton => CRandomizeEverything;
private RichTextLabel _warningLabel => CWarningLabel;
@@ -74,8 +53,6 @@ public sealed partial class HumanoidProfileEditor : Control
private OptionButton _voiceButton => CVoiceButton; // Corvax-TTS
private Button _voicePlayButton => CVoicePlayButton; // Corvax-TTS
private Slider _skinColor => CSkin;
- private OptionButton _clothingButton => CClothingButton;
- private OptionButton _backpackButton => CBackpackButton;
private OptionButton _spawnPriorityButton => CSpawnPriorityButton;
private SingleMarkingPicker _hairPicker => CHairStylePicker;
private SingleMarkingPicker _facialHairPicker => CFacialHairPicker;
@@ -90,44 +67,39 @@ public sealed partial class HumanoidProfileEditor : Control
private readonly Dictionary _jobCategories;
// Mildly hacky, as I don't trust prototype order to stay consistent and don't want the UI to break should a new one get added mid-edit. --moony
private readonly List _speciesList;
- private readonly List _antagPreferences;
+ private readonly List _antagPreferences = new();
private readonly List _traitPreferences;
private SpriteView _previewSpriteView => CSpriteView;
private Button _previewRotateLeftButton => CSpriteRotateLeft;
private Button _previewRotateRightButton => CSpriteRotateRight;
private Direction _previewRotation = Direction.North;
- private EntityUid? _previewDummy;
private BoxContainer _rgbSkinColorContainer => CRgbSkinColorContainer;
private ColorSelectorSliders _rgbSkinColorSelector;
private bool _isDirty;
- private bool _needUpdatePreview;
public int CharacterSlot;
public HumanoidCharacterProfile? Profile;
- private MarkingSet _markingSet = new(); // storing this here feels iffy but a few things need it this high up
public event Action? OnProfileChanged;
- public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IPrototypeManager prototypeManager,
- IEntityManager entityManager, IConfigurationManager configurationManager)
+ [ValidatePrototypeId]
+ private const string DefaultSpeciesGuidebook = "Species";
+
+ public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IPrototypeManager prototypeManager, IConfigurationManager configurationManager)
{
RobustXamlLoader.Load(this);
_prototypeManager = prototypeManager;
- _entMan = entityManager;
_preferencesManager = preferencesManager;
- _configurationManager = configurationManager;
_markingManager = IoCManager.Resolve();
+ var controller = UserInterfaceManager.GetUIController();
+ controller.PreviewDummyUpdated += OnDummyUpdate;
- SpeciesInfoButton.ToolTip = Loc.GetString("humanoid-profile-editor-guidebook-button-tooltip");
+ _previewSpriteView.SetEntity(controller.GetPreviewDummy());
#region Left
- #region Randomize
-
- #endregion Randomize
-
#region Name
_nameEdit.OnTextChanged += args => { SetName(args.Text); };
@@ -141,8 +113,6 @@ public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IProt
_tabContainer.SetTabTitle(0, Loc.GetString("humanoid-profile-editor-appearance-tab"));
- ShowClothes.OnPressed += ToggleClothes;
-
#region Sex
_sexButton.OnItemSelected += args =>
@@ -234,7 +204,7 @@ public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IProt
return;
Profile = Profile.WithCharacterAppearance(
Profile.Appearance.WithHairStyleName(newStyle.id));
- IsDirty = true;
+ SetDirty();
};
_hairPicker.OnColorChanged += newColor =>
@@ -244,7 +214,7 @@ public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IProt
Profile = Profile.WithCharacterAppearance(
Profile.Appearance.WithHairColor(newColor.marking.MarkingColors[0]));
UpdateCMarkingsHair();
- IsDirty = true;
+ SetDirty();
};
_facialHairPicker.OnMarkingSelect += newStyle =>
@@ -253,7 +223,7 @@ public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IProt
return;
Profile = Profile.WithCharacterAppearance(
Profile.Appearance.WithFacialHairStyleName(newStyle.id));
- IsDirty = true;
+ SetDirty();
};
_facialHairPicker.OnColorChanged += newColor =>
@@ -263,7 +233,7 @@ public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IProt
Profile = Profile.WithCharacterAppearance(
Profile.Appearance.WithFacialHairColor(newColor.marking.MarkingColors[0]));
UpdateCMarkingsFacialHair();
- IsDirty = true;
+ SetDirty();
};
_hairPicker.OnSlotRemove += _ =>
@@ -275,7 +245,7 @@ public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IProt
);
UpdateHairPickers();
UpdateCMarkingsHair();
- IsDirty = true;
+ SetDirty();
};
_facialHairPicker.OnSlotRemove += _ =>
@@ -287,7 +257,7 @@ public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IProt
);
UpdateHairPickers();
UpdateCMarkingsFacialHair();
- IsDirty = true;
+ SetDirty();
};
_hairPicker.OnSlotAdd += delegate()
@@ -307,7 +277,7 @@ public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IProt
UpdateHairPickers();
UpdateCMarkingsHair();
- IsDirty = true;
+ SetDirty();
};
_facialHairPicker.OnSlotAdd += delegate()
@@ -327,38 +297,11 @@ public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IProt
UpdateHairPickers();
UpdateCMarkingsFacialHair();
- IsDirty = true;
+ SetDirty();
};
#endregion Hair
- #region Clothing
-
- _clothingButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-jumpsuit"), (int) ClothingPreference.Jumpsuit);
- _clothingButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-jumpskirt"), (int) ClothingPreference.Jumpskirt);
-
- _clothingButton.OnItemSelected += args =>
- {
- _clothingButton.SelectId(args.Id);
- SetClothing((ClothingPreference) args.Id);
- };
-
- #endregion Clothing
-
- #region Backpack
-
- _backpackButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-backpack"), (int) BackpackPreference.Backpack);
- _backpackButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-satchel"), (int) BackpackPreference.Satchel);
- _backpackButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-duffelbag"), (int) BackpackPreference.Duffelbag);
-
- _backpackButton.OnItemSelected += args =>
- {
- _backpackButton.SelectId(args.Id);
- SetBackpack((BackpackPreference) args.Id);
- };
-
- #endregion Backpack
-
#region SpawnPriority
foreach (var value in Enum.GetValues())
@@ -383,7 +326,7 @@ public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IProt
Profile = Profile.WithCharacterAppearance(
Profile.Appearance.WithEyeColor(newColor));
CMarkings.CurrentEyeColor = Profile.Appearance.EyeColor;
- IsDirty = true;
+ SetDirty();
};
#endregion Eyes
@@ -407,46 +350,22 @@ public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IProt
_preferenceUnavailableButton.SelectId(args.Id);
Profile = Profile?.WithPreferenceUnavailable((PreferenceUnavailableMode) args.Id);
- IsDirty = true;
+ SetDirty();
};
_jobPriorities = new List();
_jobCategories = new Dictionary();
_requirements = IoCManager.Resolve();
+ // TODO: Move this to the LobbyUIController instead of being spaghetti everywhere.
+ _requirements.Updated += UpdateAntagRequirements;
_requirements.Updated += UpdateRoleRequirements;
+ UpdateAntagRequirements();
UpdateRoleRequirements();
#endregion Jobs
- #region Antags
-
_tabContainer.SetTabTitle(2, Loc.GetString("humanoid-profile-editor-antags-tab"));
- _antagPreferences = new List();
-
- foreach (var antag in prototypeManager.EnumeratePrototypes().OrderBy(a => Loc.GetString(a.Name)))
- {
- if (!antag.SetPreference)
- continue;
-
- var selector = new AntagPreferenceSelector(antag);
- _antagList.AddChild(selector);
- _antagPreferences.Add(selector);
- if (selector.Disabled)
- {
- Profile = Profile?.WithAntagPreference(antag.ID, false);
- IsDirty = true;
- }
-
- selector.PreferenceChanged += preference =>
- {
- Profile = Profile?.WithAntagPreference(antag.ID, preference);
- IsDirty = true;
- };
- }
-
- #endregion Antags
-
#region Traits
var traits = prototypeManager.EnumeratePrototypes().OrderBy(t => Loc.GetString(t.Name)).ToList();
@@ -464,7 +383,7 @@ public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IProt
selector.PreferenceChanged += preference =>
{
Profile = Profile?.WithTraitPreference(trait.ID, preference);
- IsDirty = true;
+ SetDirty();
};
}
}
@@ -497,7 +416,7 @@ public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IProt
#region FlavorText
- if (_configurationManager.GetCVar(CCVars.FlavorText))
+ if (configurationManager.GetCVar(CCVars.FlavorText))
{
var flavorText = new FlavorText.FlavorText();
_tabContainer.AddChild(flavorText);
@@ -514,22 +433,14 @@ public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IProt
_previewRotateLeftButton.OnPressed += _ =>
{
_previewRotation = _previewRotation.TurnCw();
- _needUpdatePreview = true;
+ SetPreviewRotation(_previewRotation);
};
_previewRotateRightButton.OnPressed += _ =>
{
_previewRotation = _previewRotation.TurnCcw();
- _needUpdatePreview = true;
+ SetPreviewRotation(_previewRotation);
};
- var species = Profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies;
- var dollProto = _prototypeManager.Index(species).DollPrototype;
-
- if (_previewDummy != null)
- _entMan.DeleteEntity(_previewDummy!.Value);
-
- _previewDummy = _entMan.SpawnEntity(dollProto, MapCoordinates.Nullspace);
- _previewSpriteView.SetEntity(_previewDummy);
#endregion Dummy
#endregion Left
@@ -539,6 +450,13 @@ public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IProt
LoadServerData();
}
+ ShowClothes.OnToggled += args =>
+ {
+ var lobby = UserInterfaceManager.GetUIController();
+ lobby.SetClothes(args.Pressed);
+ SetDirty();
+ };
+
preferencesManager.OnServerDataLoaded += LoadServerData;
SpeciesInfoButton.OnPressed += OnSpeciesInfoButtonPressed;
@@ -546,28 +464,69 @@ public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IProt
UpdateSpeciesGuidebookIcon();
IsDirty = false;
+ controller.UpdateProfile();
+ }
+
+ private void SetDirty()
+ {
+ var controller = UserInterfaceManager.GetUIController();
+ controller.UpdateProfile(Profile);
+ controller.ReloadCharacterUI();
+ IsDirty = true;
}
private void OnSpeciesInfoButtonPressed(BaseButton.ButtonEventArgs args)
{
var guidebookController = UserInterfaceManager.GetUIController();
var species = Profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies;
- var page = "Species";
+ var page = DefaultSpeciesGuidebook;
if (_prototypeManager.HasIndex(species))
page = species;
- if (_prototypeManager.TryIndex("Species", out var guideRoot))
+ if (_prototypeManager.TryIndex(DefaultSpeciesGuidebook, out var guideRoot))
{
var dict = new Dictionary();
- dict.Add("Species", guideRoot);
+ dict.Add(DefaultSpeciesGuidebook, guideRoot);
//TODO: Don't close the guidebook if its already open, just go to the correct page
guidebookController.ToggleGuidebook(dict, includeChildren:true, selected: page);
}
}
- private void ToggleClothes(BaseButton.ButtonEventArgs obj)
+ private void OnDummyUpdate(EntityUid value)
{
- RebuildSpriteView();
+ _previewSpriteView.SetEntity(value);
+ }
+
+ private void UpdateAntagRequirements()
+ {
+ _antagList.DisposeAllChildren();
+ _antagPreferences.Clear();
+ var btnGroup = new ButtonGroup();
+
+ foreach (var antag in _prototypeManager.EnumeratePrototypes().OrderBy(a => Loc.GetString(a.Name)))
+ {
+ if (!antag.SetPreference)
+ continue;
+
+ var selector = new AntagPreferenceSelector(antag, btnGroup)
+ {
+ Margin = new Thickness(3f, 3f, 3f, 0f),
+ };
+ _antagList.AddChild(selector);
+ _antagPreferences.Add(selector);
+ if (selector.Disabled)
+ {
+ Profile = Profile?.WithAntagPreference(antag.ID, false);
+ SetDirty();
+ }
+
+ selector.PreferenceChanged += preference =>
+ {
+ Profile = Profile?.WithAntagPreference(antag.ID, preference);
+ SetDirty();
+ };
+ }
+
}
private void UpdateRoleRequirements()
@@ -628,10 +587,19 @@ private void UpdateRoleRequirements()
.Where(job => job.SetPreference)
.ToArray();
Array.Sort(jobs, JobUIComparer.Instance);
+ var jobLoadoutGroup = new ButtonGroup();
foreach (var job in jobs)
{
- var selector = new JobPrioritySelector(job, _prototypeManager);
+ RoleLoadout? loadout = null;
+
+ // Clone so we don't modify the underlying loadout.
+ Profile?.Loadouts.TryGetValue(LoadoutSystem.GetJobPrototype(job.ID), out loadout);
+ loadout = loadout?.Clone();
+ var selector = new JobPrioritySelector(loadout, job, jobLoadoutGroup, _prototypeManager)
+ {
+ Margin = new Thickness(3f, 3f, 3f, 0f),
+ };
if (!_requirements.IsAllowed(job, out var reason))
{
@@ -641,10 +609,15 @@ private void UpdateRoleRequirements()
category.AddChild(selector);
_jobPriorities.Add(selector);
+ selector.LoadoutUpdated += args =>
+ {
+ Profile = Profile?.WithLoadout(args);
+ SetDirty();
+ };
+
selector.PriorityChanged += priority =>
{
Profile = Profile?.WithJobPriority(job.ID, priority);
- IsDirty = true;
foreach (var jobSelector in _jobPriorities)
{
@@ -660,6 +633,8 @@ private void UpdateRoleRequirements()
Profile = Profile?.WithJobPriority(jobSelector.Proto.ID, JobPriority.Medium);
}
}
+
+ SetDirty();
};
}
@@ -677,7 +652,7 @@ private void OnFlavorTextChange(string content)
return;
Profile = Profile.WithFlavorText(content);
- IsDirty = true;
+ SetDirty();
}
private void OnMarkingChange(MarkingSet markings)
@@ -686,20 +661,12 @@ private void OnMarkingChange(MarkingSet markings)
return;
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithMarkings(markings.GetForwardEnumerator().ToList()));
- _needUpdatePreview = true;
- IsDirty = true;
- }
-
- private void OnMarkingColorChange(List markings)
- {
- if (Profile is null)
- return;
-
- Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithMarkings(markings));
IsDirty = true;
+ var controller = UserInterfaceManager.GetUIController();
+ controller.UpdateProfile(Profile);
+ controller.ReloadProfile();
}
-
private void OnSkinColorOnValueChanged()
{
if (Profile is null) return;
@@ -744,6 +711,20 @@ private void OnSkinColorOnValueChanged()
var color = SkinColor.TintedHues(_rgbSkinColorSelector.Color);
+ CMarkings.CurrentSkinColor = color;
+ Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
+ break;
+ }
+ case HumanoidSkinColor.VoxFeathers:
+ {
+ if (!_rgbSkinColorContainer.Visible)
+ {
+ _skinColor.Visible = false;
+ _rgbSkinColorContainer.Visible = true;
+ }
+
+ var color = SkinColor.ClosestVoxColor(_rgbSkinColorSelector.Color);
+
CMarkings.CurrentSkinColor = color;
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
break;
@@ -751,6 +732,9 @@ private void OnSkinColorOnValueChanged()
}
IsDirty = true;
+ var controller = UserInterfaceManager.GetUIController();
+ controller.UpdateProfile(Profile);
+ controller.ReloadProfile();
}
protected override void Dispose(bool disposing)
@@ -759,39 +743,28 @@ protected override void Dispose(bool disposing)
if (!disposing)
return;
- if (_previewDummy != null)
- _entMan.DeleteEntity(_previewDummy.Value);
-
+ var controller = UserInterfaceManager.GetUIController();
+ controller.PreviewDummyUpdated -= OnDummyUpdate;
+ _requirements.Updated -= UpdateAntagRequirements;
_requirements.Updated -= UpdateRoleRequirements;
_preferencesManager.OnServerDataLoaded -= LoadServerData;
}
- private void RebuildSpriteView()
- {
- var species = Profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies;
- var dollProto = _prototypeManager.Index(species).DollPrototype;
-
- if (_previewDummy != null)
- _entMan.DeleteEntity(_previewDummy!.Value);
-
- _previewDummy = _entMan.SpawnEntity(dollProto, MapCoordinates.Nullspace);
- _previewSpriteView.SetEntity(_previewDummy);
- _needUpdatePreview = true;
- }
-
- private void LoadServerData()
+ public void LoadServerData()
{
Profile = (HumanoidCharacterProfile) _preferencesManager.Preferences!.SelectedCharacter;
CharacterSlot = _preferencesManager.Preferences.SelectedCharacterIndex;
+ UpdateAntagRequirements();
+ UpdateRoleRequirements();
UpdateControls();
- _needUpdatePreview = true;
+ ShowClothes.Pressed = true;
}
private void SetAge(int newAge)
{
Profile = Profile?.WithAge(newAge);
- IsDirty = true;
+ SetDirty();
}
private void SetSex(Sex newSex)
@@ -813,13 +786,13 @@ private void SetSex(Sex newSex)
UpdateGenderControls();
UpdateTTSVoicesControls(); // Corvax-TTS
CMarkings.SetSex(newSex);
- IsDirty = true;
+ SetDirty();
}
private void SetGender(Gender newGender)
{
Profile = Profile?.WithGender(newGender);
- IsDirty = true;
+ SetDirty();
}
// Corvax-TTS-Start
@@ -836,46 +809,34 @@ private void SetSpecies(string newSpecies)
OnSkinColorOnValueChanged(); // Species may have special color prefs, make sure to update it.
CMarkings.SetSpecies(newSpecies); // Repopulate the markings tab as well.
UpdateSexControls(); // update sex for new species
- RebuildSpriteView(); // they might have different inv so we need a new dummy
UpdateSpeciesGuidebookIcon();
- IsDirty = true;
- _needUpdatePreview = true;
+ SetDirty();
+ UpdatePreview();
}
private void SetName(string newName)
{
Profile = Profile?.WithName(newName);
- IsDirty = true;
- }
-
- private void SetClothing(ClothingPreference newClothing)
- {
- Profile = Profile?.WithClothingPreference(newClothing);
- IsDirty = true;
- }
-
- private void SetBackpack(BackpackPreference newBackpack)
- {
- Profile = Profile?.WithBackpackPreference(newBackpack);
- IsDirty = true;
+ SetDirty();
}
private void SetSpawnPriority(SpawnPriorityPreference newSpawnPriority)
{
Profile = Profile?.WithSpawnPriorityPreference(newSpawnPriority);
- IsDirty = true;
+ SetDirty();
}
public void Save()
{
IsDirty = false;
- if (Profile != null)
- {
- _preferencesManager.UpdateCharacter(Profile, CharacterSlot);
- OnProfileChanged?.Invoke(Profile, CharacterSlot);
- _needUpdatePreview = true;
- }
+ if (Profile == null)
+ return;
+
+ _preferencesManager.UpdateCharacter(Profile, CharacterSlot);
+ OnProfileChanged?.Invoke(Profile, CharacterSlot);
+ // Reset profile to default.
+ UserInterfaceManager.GetUIController().UpdateProfile();
}
private bool IsDirty
@@ -884,7 +845,6 @@ private bool IsDirty
set
{
_isDirty = value;
- _needUpdatePreview = true;
UpdateSaveButton();
}
}
@@ -985,6 +945,18 @@ private void UpdateSkinColor()
_rgbSkinColorSelector.Color = Profile.Appearance.SkinColor;
break;
}
+ case HumanoidSkinColor.VoxFeathers:
+ {
+ if (!_rgbSkinColorContainer.Visible)
+ {
+ _skinColor.Visible = false;
+ _rgbSkinColorContainer.Visible = true;
+ }
+
+ _rgbSkinColorSelector.Color = SkinColor.ClosestVoxColor(Profile.Appearance.SkinColor);
+
+ break;
+ }
}
}
@@ -1004,7 +976,7 @@ public void UpdateSpeciesGuidebookIcon()
if (!_prototypeManager.HasIndex(species))
return;
- var style = speciesProto.GuideBookIcon;
+ const string style = "SpeciesInfoDefault";
SpeciesInfoButton.StyleClasses.Add(style);
}
@@ -1040,26 +1012,6 @@ private void UpdateGenderControls()
_genderButton.SelectId((int) Profile.Gender);
}
- private void UpdateClothingControls()
- {
- if (Profile == null)
- {
- return;
- }
-
- _clothingButton.SelectId((int) Profile.Clothing);
- }
-
- private void UpdateBackpackControls()
- {
- if (Profile == null)
- {
- return;
- }
-
- _backpackButton.SelectId((int) Profile.Backpack);
- }
-
private void UpdateSpawnPriorityControls()
{
if (Profile == null)
@@ -1189,13 +1141,13 @@ private void UpdatePreview()
if (Profile is null)
return;
- var humanoid = _entMan.System();
- humanoid.LoadProfile(_previewDummy!.Value, Profile);
-
- if (ShowClothes.Pressed)
- LobbyCharacterPreviewPanel.GiveDummyJobClothes(_previewDummy!.Value, Profile);
+ UserInterfaceManager.GetUIController().ReloadProfile();
+ SetPreviewRotation(_previewRotation);
+ }
- _previewSpriteView.OverrideDirection = (Direction) ((int) _previewRotation % 4 * 2);
+ private void SetPreviewRotation(Direction direction)
+ {
+ _previewSpriteView.OverrideDirection = (Direction) ((int) direction % 4 * 2);
}
public void UpdateControls()
@@ -1207,17 +1159,16 @@ public void UpdateControls()
UpdateGenderControls();
UpdateSkinColor();
UpdateSpecies();
- UpdateClothingControls();
- UpdateBackpackControls();
UpdateSpawnPriorityControls();
UpdateAgeEdit();
UpdateEyePickers();
UpdateSaveButton();
+ UpdateLoadouts();
+ UpdateRoleRequirements();
UpdateJobPriorities();
UpdateAntagPreferences();
UpdateTraitPreferences();
UpdateMarkings();
- RebuildSpriteView();
UpdateTTSVoicesControls(); // Corvax-TTS
UpdateHairPickers();
UpdateCMarkingsHair();
@@ -1226,17 +1177,6 @@ public void UpdateControls()
_preferenceUnavailableButton.SelectId((int) Profile.PreferenceUnavailable);
}
- protected override void FrameUpdate(FrameEventArgs args)
- {
- base.FrameUpdate(args);
-
- if (_needUpdatePreview)
- {
- UpdatePreview();
- _needUpdatePreview = false;
- }
- }
-
private void UpdateJobPriorities()
{
foreach (var prioritySelector in _jobPriorities)
@@ -1249,143 +1189,11 @@ private void UpdateJobPriorities()
}
}
- private abstract class RequirementsSelector : Control
- {
- public T Proto { get; }
- public bool Disabled => _lockStripe.Visible;
-
- protected readonly RadioOptions Options;
- private StripeBack _lockStripe;
- private Label _requirementsLabel;
-
- protected RequirementsSelector(T proto)
- {
- Proto = proto;
-
- Options = new RadioOptions(RadioOptionsLayout.Horizontal)
- {
- FirstButtonStyle = StyleBase.ButtonOpenRight,
- ButtonStyle = StyleBase.ButtonOpenBoth,
- LastButtonStyle = StyleBase.ButtonOpenLeft
- };
- //Override default radio option button width
- Options.GenerateItem = GenerateButton;
-
- Options.OnItemSelected += args => Options.Select(args.Id);
-
- _requirementsLabel = new Label()
- {
- Text = Loc.GetString("role-timer-locked"),
- Visible = true,
- HorizontalAlignment = HAlignment.Center,
- StyleClasses = {StyleBase.StyleClassLabelSubText},
- };
-
- _lockStripe = new StripeBack()
- {
- Visible = false,
- HorizontalExpand = true,
- MouseFilter = MouseFilterMode.Stop,
- Children =
- {
- _requirementsLabel
- }
- };
-
- // Setup must be called after
- }
-
- ///
- /// Actually adds the controls, must be called in the inheriting class' constructor.
- ///
- protected void Setup((string, int)[] items, string title, int titleSize, string? description, TextureRect? icon = null)
- {
- foreach (var (text, value) in items)
- {
- Options.AddItem(Loc.GetString(text), value);
- }
-
- var titleLabel = new Label()
- {
- Margin = new Thickness(5f, 0, 5f, 0),
- Text = title,
- MinSize = new Vector2(titleSize, 0),
- MouseFilter = MouseFilterMode.Stop,
- ToolTip = description
- };
-
- var container = new BoxContainer
- {
- Orientation = LayoutOrientation.Horizontal,
- };
-
- if (icon != null)
- container.AddChild(icon);
- container.AddChild(titleLabel);
- container.AddChild(Options);
- container.AddChild(_lockStripe);
-
- AddChild(container);
- }
-
- public void LockRequirements(FormattedMessage requirements)
- {
- var tooltip = new Tooltip();
- tooltip.SetMessage(requirements);
- _lockStripe.TooltipSupplier = _ => tooltip;
- _lockStripe.Visible = true;
- Options.Visible = false;
- }
-
- // TODO: Subscribe to roletimers event. I am too lazy to do this RN But I doubt most people will notice fn
- public void UnlockRequirements()
- {
- _lockStripe.Visible = false;
- Options.Visible = true;
- }
-
- private Button GenerateButton(string text, int value)
- {
- return new Button
- {
- Text = text,
- MinWidth = 90
- };
- }
- }
-
- private sealed class JobPrioritySelector : RequirementsSelector
+ private void UpdateLoadouts()
{
- public JobPriority Priority
+ foreach (var prioritySelector in _jobPriorities)
{
- get => (JobPriority) Options.SelectedValue;
- set => Options.SelectByValue((int) value);
- }
-
- public event Action? PriorityChanged;
-
- public JobPrioritySelector(JobPrototype proto, IPrototypeManager protoMan)
- : base(proto)
- {
- Options.OnItemSelected += args => PriorityChanged?.Invoke(Priority);
-
- var items = new[]
- {
- ("humanoid-profile-editor-job-priority-high-button", (int) JobPriority.High),
- ("humanoid-profile-editor-job-priority-medium-button", (int) JobPriority.Medium),
- ("humanoid-profile-editor-job-priority-low-button", (int) JobPriority.Low),
- ("humanoid-profile-editor-job-priority-never-button", (int) JobPriority.Never),
- };
-
- var icon = new TextureRect
- {
- TextureScale = new Vector2(2, 2),
- VerticalAlignment = VAlignment.Center
- };
- var jobIcon = protoMan.Index(proto.Icon);
- icon.Texture = jobIcon.Icon.Frame0();
-
- Setup(items, proto.LocalizedName, 200, proto.LocalizedDescription, icon);
+ prioritySelector.CloseLoadout();
}
}
@@ -1410,41 +1218,6 @@ private void UpdateTraitPreferences()
}
}
- private sealed class AntagPreferenceSelector : RequirementsSelector
- {
- // 0 is yes and 1 is no
- public bool Preference
- {
- get => Options.SelectedValue == 0;
- set => Options.Select((value && !Disabled) ? 0 : 1);
- }
-
- public event Action? PreferenceChanged;
-
- public AntagPreferenceSelector(AntagPrototype proto)
- : base(proto)
- {
- Options.OnItemSelected += args => PreferenceChanged?.Invoke(Preference);
-
- var items = new[]
- {
- ("humanoid-profile-editor-antag-preference-yes-button", 0),
- ("humanoid-profile-editor-antag-preference-no-button", 1)
- };
- var title = Loc.GetString(proto.Name);
- var description = Loc.GetString(proto.Objective);
- Setup(items, title, 250, description);
-
- // immediately lock requirements if they arent met.
- // another function checks Disabled after creating the selector so this has to be done now
- var requirements = IoCManager.Resolve();
- if (proto.Requirements != null && !requirements.CheckRoleTime(proto.Requirements, out var reason))
- {
- LockRequirements(reason);
- }
- }
- }
-
private sealed class TraitPreferenceSelector : Control
{
public TraitPrototype Trait { get; }
diff --git a/Content.Client/Preferences/UI/JobPrioritySelector.cs b/Content.Client/Preferences/UI/JobPrioritySelector.cs
new file mode 100644
index 00000000000..243c78f07eb
--- /dev/null
+++ b/Content.Client/Preferences/UI/JobPrioritySelector.cs
@@ -0,0 +1,46 @@
+using System.Numerics;
+using Content.Shared.Preferences;
+using Content.Shared.Preferences.Loadouts;
+using Content.Shared.Preferences.Loadouts.Effects;
+using Content.Shared.Roles;
+using Content.Shared.StatusIcon;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.Utility;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Preferences.UI;
+
+public sealed class JobPrioritySelector : RequirementsSelector
+{
+ public JobPriority Priority
+ {
+ get => (JobPriority) Options.SelectedValue;
+ set => Options.SelectByValue((int) value);
+ }
+
+ public event Action? PriorityChanged;
+
+ public JobPrioritySelector(RoleLoadout? loadout, JobPrototype proto, ButtonGroup btnGroup, IPrototypeManager protoMan)
+ : base(proto, btnGroup)
+ {
+ Options.OnItemSelected += args => PriorityChanged?.Invoke(Priority);
+
+ var items = new[]
+ {
+ ("humanoid-profile-editor-job-priority-high-button", (int) JobPriority.High),
+ ("humanoid-profile-editor-job-priority-medium-button", (int) JobPriority.Medium),
+ ("humanoid-profile-editor-job-priority-low-button", (int) JobPriority.Low),
+ ("humanoid-profile-editor-job-priority-never-button", (int) JobPriority.Never),
+ };
+
+ var icon = new TextureRect
+ {
+ TextureScale = new Vector2(2, 2),
+ VerticalAlignment = VAlignment.Center
+ };
+ var jobIcon = protoMan.Index(proto.Icon);
+ icon.Texture = jobIcon.Icon.Frame0();
+
+ Setup(loadout, items, proto.LocalizedName, 200, proto.LocalizedDescription, icon);
+ }
+}
diff --git a/Content.Client/Preferences/UI/LoadoutContainer.xaml b/Content.Client/Preferences/UI/LoadoutContainer.xaml
new file mode 100644
index 00000000000..a84a4a96401
--- /dev/null
+++ b/Content.Client/Preferences/UI/LoadoutContainer.xaml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Preferences/UI/LoadoutContainer.xaml.cs b/Content.Client/Preferences/UI/LoadoutContainer.xaml.cs
new file mode 100644
index 00000000000..45a982b5a89
--- /dev/null
+++ b/Content.Client/Preferences/UI/LoadoutContainer.xaml.cs
@@ -0,0 +1,74 @@
+using Content.Shared.Clothing;
+using Content.Shared.Preferences.Loadouts;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Preferences.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class LoadoutContainer : BoxContainer
+{
+ [Dependency] private readonly IEntityManager _entManager = default!;
+ [Dependency] private readonly IPrototypeManager _protoManager = default!;
+
+ private readonly EntityUid? _entity;
+
+ public Button Select => SelectButton;
+
+ public LoadoutContainer(ProtoId proto, bool disabled, FormattedMessage? reason)
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ SelectButton.Disabled = disabled;
+
+ if (disabled && reason != null)
+ {
+ var tooltip = new Tooltip();
+ tooltip.SetMessage(reason);
+ SelectButton.TooltipSupplier = _ => tooltip;
+ }
+
+ if (_protoManager.TryIndex(proto, out var loadProto))
+ {
+ var ent = _entManager.System().GetFirstOrNull(loadProto);
+
+ if (ent != null)
+ {
+ _entity = _entManager.SpawnEntity(ent, MapCoordinates.Nullspace);
+ Sprite.SetEntity(_entity);
+
+ var spriteTooltip = new Tooltip();
+ spriteTooltip.SetMessage(FormattedMessage.FromUnformatted(_entManager.GetComponent(_entity.Value).EntityDescription));
+ Sprite.TooltipSupplier = _ => spriteTooltip;
+ }
+ }
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ if (!disposing)
+ return;
+
+ _entManager.DeleteEntity(_entity);
+ }
+
+ public bool Pressed
+ {
+ get => SelectButton.Pressed;
+ set => SelectButton.Pressed = value;
+ }
+
+ public string? Text
+ {
+ get => SelectButton.Text;
+ set => SelectButton.Text = value;
+ }
+}
diff --git a/Content.Client/Preferences/UI/LoadoutGroupContainer.xaml b/Content.Client/Preferences/UI/LoadoutGroupContainer.xaml
new file mode 100644
index 00000000000..1e3eb14d3fc
--- /dev/null
+++ b/Content.Client/Preferences/UI/LoadoutGroupContainer.xaml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Preferences/UI/LoadoutGroupContainer.xaml.cs b/Content.Client/Preferences/UI/LoadoutGroupContainer.xaml.cs
new file mode 100644
index 00000000000..8dc1c405394
--- /dev/null
+++ b/Content.Client/Preferences/UI/LoadoutGroupContainer.xaml.cs
@@ -0,0 +1,93 @@
+using System.Linq;
+using Content.Shared.Clothing;
+using Content.Shared.Preferences.Loadouts;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Preferences.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class LoadoutGroupContainer : BoxContainer
+{
+ private readonly LoadoutGroupPrototype _groupProto;
+
+ public event Action>? OnLoadoutPressed;
+ public event Action>? OnLoadoutUnpressed;
+
+ public LoadoutGroupContainer(RoleLoadout loadout, LoadoutGroupPrototype groupProto, ICommonSession session, IDependencyCollection collection)
+ {
+ RobustXamlLoader.Load(this);
+ _groupProto = groupProto;
+
+ RefreshLoadouts(loadout, session, collection);
+ }
+
+ ///
+ /// Updates button availabilities and buttons.
+ ///
+ public void RefreshLoadouts(RoleLoadout loadout, ICommonSession session, IDependencyCollection collection)
+ {
+ var protoMan = collection.Resolve();
+ var loadoutSystem = collection.Resolve().System();
+ RestrictionsContainer.DisposeAllChildren();
+
+ if (_groupProto.MinLimit > 0)
+ {
+ RestrictionsContainer.AddChild(new Label()
+ {
+ Text = Loc.GetString("loadouts-min-limit", ("count", _groupProto.MinLimit)),
+ Margin = new Thickness(5, 0, 5, 5),
+ });
+ }
+
+ if (_groupProto.MaxLimit > 0)
+ {
+ RestrictionsContainer.AddChild(new Label()
+ {
+ Text = Loc.GetString("loadouts-max-limit", ("count", _groupProto.MaxLimit)),
+ Margin = new Thickness(5, 0, 5, 5),
+ });
+ }
+
+ if (protoMan.TryIndex(loadout.Role, out var roleProto) && roleProto.Points != null && loadout.Points != null)
+ {
+ RestrictionsContainer.AddChild(new Label()
+ {
+ Text = Loc.GetString("loadouts-points-limit", ("count", loadout.Points.Value), ("max", roleProto.Points.Value)),
+ Margin = new Thickness(5, 0, 5, 5),
+ });
+ }
+
+ LoadoutsContainer.DisposeAllChildren();
+ // Didn't use options because this is more robust in future.
+
+ var selected = loadout.SelectedLoadouts[_groupProto.ID];
+
+ foreach (var loadoutProto in _groupProto.Loadouts)
+ {
+ if (!protoMan.TryIndex(loadoutProto, out var loadProto))
+ continue;
+
+ var matchingLoadout = selected.FirstOrDefault(e => e.Prototype == loadoutProto);
+ var pressed = matchingLoadout != null;
+
+ var enabled = loadout.IsValid(session, loadoutProto, collection, out var reason);
+ var loadoutContainer = new LoadoutContainer(loadoutProto, !enabled, reason);
+ loadoutContainer.Select.Pressed = pressed;
+ loadoutContainer.Text = loadoutSystem.GetName(loadProto);
+
+ loadoutContainer.Select.OnPressed += args =>
+ {
+ if (args.Button.Pressed)
+ OnLoadoutPressed?.Invoke(loadoutProto);
+ else
+ OnLoadoutUnpressed?.Invoke(loadoutProto);
+ };
+
+ LoadoutsContainer.AddChild(loadoutContainer);
+ }
+ }
+}
diff --git a/Content.Client/Preferences/UI/LoadoutWindow.xaml b/Content.Client/Preferences/UI/LoadoutWindow.xaml
new file mode 100644
index 00000000000..afa783c7aa9
--- /dev/null
+++ b/Content.Client/Preferences/UI/LoadoutWindow.xaml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/Content.Client/Preferences/UI/LoadoutWindow.xaml.cs b/Content.Client/Preferences/UI/LoadoutWindow.xaml.cs
new file mode 100644
index 00000000000..8e1ef0f1697
--- /dev/null
+++ b/Content.Client/Preferences/UI/LoadoutWindow.xaml.cs
@@ -0,0 +1,60 @@
+using Content.Client.Lobby;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Preferences.Loadouts;
+using Content.Shared.Preferences.Loadouts.Effects;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Preferences.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class LoadoutWindow : FancyWindow
+{
+ public event Action, ProtoId>? OnLoadoutPressed;
+ public event Action, ProtoId>? OnLoadoutUnpressed;
+
+ private List _groups = new();
+
+ public LoadoutWindow(RoleLoadout loadout, RoleLoadoutPrototype proto, ICommonSession session, IDependencyCollection collection)
+ {
+ RobustXamlLoader.Load(this);
+ var protoManager = collection.Resolve();
+
+ foreach (var group in proto.Groups)
+ {
+ if (!protoManager.TryIndex(group, out var groupProto))
+ continue;
+
+ var container = new LoadoutGroupContainer(loadout, protoManager.Index(group), session, collection);
+ LoadoutGroupsContainer.AddTab(container, Loc.GetString(groupProto.Name));
+ _groups.Add(container);
+
+ container.OnLoadoutPressed += args =>
+ {
+ OnLoadoutPressed?.Invoke(group, args);
+ };
+
+ container.OnLoadoutUnpressed += args =>
+ {
+ OnLoadoutUnpressed?.Invoke(group, args);
+ };
+ }
+ }
+
+ public override void Close()
+ {
+ base.Close();
+ var controller = UserInterfaceManager.GetUIController();
+ controller.SetDummyJob(null);
+ }
+
+ public void RefreshLoadouts(RoleLoadout loadout, ICommonSession session, IDependencyCollection collection)
+ {
+ foreach (var group in _groups)
+ {
+ group.RefreshLoadouts(loadout, session, collection);
+ }
+ }
+}
diff --git a/Content.Client/Preferences/UI/RequirementsSelector.cs b/Content.Client/Preferences/UI/RequirementsSelector.cs
new file mode 100644
index 00000000000..e016661ee6c
--- /dev/null
+++ b/Content.Client/Preferences/UI/RequirementsSelector.cs
@@ -0,0 +1,222 @@
+using System.Numerics;
+using Content.Client.Lobby;
+using Content.Client.Stylesheets;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Clothing;
+using Content.Shared.Preferences.Loadouts;
+using Content.Shared.Preferences.Loadouts.Effects;
+using Content.Shared.Roles;
+using Robust.Client.Player;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Preferences.UI;
+
+public abstract class RequirementsSelector : BoxContainer where T : IPrototype
+{
+ private ButtonGroup _loadoutGroup;
+
+ public T Proto { get; }
+ public bool Disabled => _lockStripe.Visible;
+
+ protected readonly RadioOptions Options;
+ private readonly StripeBack _lockStripe;
+ private LoadoutWindow? _loadoutWindow;
+
+ private RoleLoadout? _loadout;
+
+ ///
+ /// Raised if a loadout has been updated.
+ ///
+ public event Action? LoadoutUpdated;
+
+ protected RequirementsSelector(T proto, ButtonGroup loadoutGroup)
+ {
+ _loadoutGroup = loadoutGroup;
+ Proto = proto;
+
+ Options = new RadioOptions(RadioOptionsLayout.Horizontal)
+ {
+ FirstButtonStyle = StyleBase.ButtonOpenRight,
+ ButtonStyle = StyleBase.ButtonOpenBoth,
+ LastButtonStyle = StyleBase.ButtonOpenLeft,
+ HorizontalExpand = true,
+ };
+ //Override default radio option button width
+ Options.GenerateItem = GenerateButton;
+
+ Options.OnItemSelected += args => Options.Select(args.Id);
+
+ var requirementsLabel = new Label()
+ {
+ Text = Loc.GetString("role-timer-locked"),
+ Visible = true,
+ HorizontalAlignment = HAlignment.Center,
+ StyleClasses = {StyleBase.StyleClassLabelSubText},
+ };
+
+ _lockStripe = new StripeBack()
+ {
+ Visible = false,
+ HorizontalExpand = true,
+ HasMargins = false,
+ MouseFilter = MouseFilterMode.Stop,
+ Children =
+ {
+ requirementsLabel
+ }
+ };
+
+ // Setup must be called after
+ }
+
+ ///
+ /// Actually adds the controls, must be called in the inheriting class' constructor.
+ ///
+ protected void Setup(RoleLoadout? loadout, (string, int)[] items, string title, int titleSize, string? description, TextureRect? icon = null)
+ {
+ _loadout = loadout;
+
+ foreach (var (text, value) in items)
+ {
+ Options.AddItem(Loc.GetString(text), value);
+ }
+
+ var titleLabel = new Label()
+ {
+ Margin = new Thickness(5f, 0, 5f, 0),
+ Text = title,
+ MinSize = new Vector2(titleSize, 0),
+ MouseFilter = MouseFilterMode.Stop,
+ ToolTip = description
+ };
+
+ if (icon != null)
+ AddChild(icon);
+
+ AddChild(titleLabel);
+ AddChild(Options);
+ AddChild(_lockStripe);
+
+ var loadoutWindowBtn = new Button()
+ {
+ Text = Loc.GetString("loadout-window"),
+ HorizontalAlignment = HAlignment.Right,
+ Group = _loadoutGroup,
+ Margin = new Thickness(3f, 0f, 0f, 0f),
+ };
+
+ var collection = IoCManager.Instance!;
+ var protoManager = collection.Resolve();
+
+ // If no loadout found then disabled button
+ if (!protoManager.HasIndex(LoadoutSystem.GetJobPrototype(Proto.ID)))
+ {
+ loadoutWindowBtn.Disabled = true;
+ }
+ // else
+ else
+ {
+ var session = collection.Resolve().LocalSession!;
+ // TODO: Most of lobby state should be a uicontroller
+ // trying to handle all this shit is a big-ass mess.
+ // Every time I touch it I try to make it slightly better but it needs a howitzer dropped on it.
+ loadoutWindowBtn.OnPressed += args =>
+ {
+ if (args.Button.Pressed)
+ {
+ // We only create a loadout when necessary to avoid unnecessary DB entries.
+ _loadout ??= new RoleLoadout(LoadoutSystem.GetJobPrototype(Proto.ID));
+ _loadout.SetDefault(protoManager);
+
+ _loadoutWindow = new LoadoutWindow(_loadout, protoManager.Index(_loadout.Role), session, collection)
+ {
+ Title = Loc.GetString(Proto.ID + "-loadout"),
+ };
+
+ _loadoutWindow.RefreshLoadouts(_loadout, session, collection);
+
+ // If it's a job preview then refresh it.
+ if (Proto is JobPrototype jobProto)
+ {
+ var controller = UserInterfaceManager.GetUIController();
+ controller.SetDummyJob(jobProto);
+ }
+
+ _loadoutWindow.OnLoadoutUnpressed += (selectedGroup, selectedLoadout) =>
+ {
+ if (!_loadout.RemoveLoadout(selectedGroup, selectedLoadout, protoManager))
+ return;
+
+ _loadout.EnsureValid(session, collection);
+ _loadoutWindow.RefreshLoadouts(_loadout, session, collection);
+ var controller = UserInterfaceManager.GetUIController();
+ controller.ReloadProfile();
+ LoadoutUpdated?.Invoke(_loadout);
+ };
+
+ _loadoutWindow.OnLoadoutPressed += (selectedGroup, selectedLoadout) =>
+ {
+ if (!_loadout.AddLoadout(selectedGroup, selectedLoadout, protoManager))
+ return;
+
+ _loadout.EnsureValid(session, collection);
+ _loadoutWindow.RefreshLoadouts(_loadout, session, collection);
+ var controller = UserInterfaceManager.GetUIController();
+ controller.ReloadProfile();
+ LoadoutUpdated?.Invoke(_loadout);
+ };
+
+ _loadoutWindow.OpenCenteredLeft();
+ _loadoutWindow.OnClose += () =>
+ {
+ loadoutWindowBtn.Pressed = false;
+ _loadoutWindow?.Dispose();
+ _loadoutWindow = null;
+ };
+ }
+ else
+ {
+ CloseLoadout();
+ }
+ };
+ }
+
+ AddChild(loadoutWindowBtn);
+ }
+
+ public void CloseLoadout()
+ {
+ _loadoutWindow?.Close();
+ _loadoutWindow?.Dispose();
+ _loadoutWindow = null;
+ }
+
+ public void LockRequirements(FormattedMessage requirements)
+ {
+ var tooltip = new Tooltip();
+ tooltip.SetMessage(requirements);
+ _lockStripe.TooltipSupplier = _ => tooltip;
+ _lockStripe.Visible = true;
+ Options.Visible = false;
+ }
+
+ // TODO: Subscribe to roletimers event. I am too lazy to do this RN But I doubt most people will notice fn
+ public void UnlockRequirements()
+ {
+ _lockStripe.Visible = false;
+ Options.Visible = true;
+ }
+
+ private Button GenerateButton(string text, int value)
+ {
+ return new Button
+ {
+ Text = text,
+ MinWidth = 90,
+ HorizontalExpand = true,
+ };
+ }
+}
diff --git a/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Position.cs b/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Position.cs
index 2ee7e30ec9a..24f0e8a1d34 100644
--- a/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Position.cs
+++ b/Content.Client/Replay/Spectator/ReplaySpectatorSystem.Position.cs
@@ -198,6 +198,13 @@ private void OnParentChanged(EntityUid uid, ReplaySpectatorComponent component,
if (args.Transform.MapUid != null || args.OldMapId == MapId.Nullspace)
return;
+ if (_spectatorData != null)
+ {
+ // Currently scrubbing/setting the replay tick
+ // the observer will get respawned once the state was applied
+ return;
+ }
+
// The entity being spectated from was moved to null-space.
// This was probably because they were spectating some entity in a client-side replay that left PVS range.
// Simple respawn the ghost.
diff --git a/Content.Client/RoundEnd/RoundEndSummaryUIController.cs b/Content.Client/RoundEnd/RoundEndSummaryUIController.cs
new file mode 100644
index 00000000000..cf824833efb
--- /dev/null
+++ b/Content.Client/RoundEnd/RoundEndSummaryUIController.cs
@@ -0,0 +1,51 @@
+using Content.Client.GameTicking.Managers;
+using Content.Shared.GameTicking;
+using Content.Shared.Input;
+using JetBrains.Annotations;
+using Robust.Client.Input;
+using Robust.Client.UserInterface.Controllers;
+using Robust.Shared.Input.Binding;
+using Robust.Shared.Player;
+
+namespace Content.Client.RoundEnd;
+
+[UsedImplicitly]
+public sealed class RoundEndSummaryUIController : UIController,
+ IOnSystemLoaded
+{
+ [Dependency] private readonly IInputManager _input = default!;
+
+ private RoundEndSummaryWindow? _window;
+
+ private void ToggleScoreboardWindow(ICommonSession? session = null)
+ {
+ if (_window == null)
+ return;
+
+ if (_window.IsOpen)
+ {
+ _window.Close();
+ }
+ else
+ {
+ _window.OpenCenteredRight();
+ _window.MoveToFront();
+ }
+ }
+
+ public void OpenRoundEndSummaryWindow(RoundEndMessageEvent message)
+ {
+ // Don't open duplicate windows (mainly for replays).
+ if (_window?.RoundId == message.RoundId)
+ return;
+
+ _window = new RoundEndSummaryWindow(message.GamemodeTitle, message.RoundEndText,
+ message.RoundDuration, message.RoundId, message.AllPlayersEndInfo, EntityManager);
+ }
+
+ public void OnSystemLoaded(ClientGameTicker system)
+ {
+ _input.SetInputCommand(ContentKeyFunctions.ToggleRoundEndSummaryWindow,
+ InputCmdHandler.FromDelegate(ToggleScoreboardWindow));
+ }
+}
diff --git a/Content.Client/RoundEnd/RoundEndSummaryWindow.cs b/Content.Client/RoundEnd/RoundEndSummaryWindow.cs
index 5b73c77934a..9c9f83a4275 100644
--- a/Content.Client/RoundEnd/RoundEndSummaryWindow.cs
+++ b/Content.Client/RoundEnd/RoundEndSummaryWindow.cs
@@ -2,7 +2,6 @@
using System.Numerics;
using Content.Client.Message;
using Content.Shared.GameTicking;
-using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Utility;
diff --git a/Content.Client/Storage/StorageBoundUserInterface.cs b/Content.Client/Storage/StorageBoundUserInterface.cs
index f7fdbb83675..899df30f7fc 100644
--- a/Content.Client/Storage/StorageBoundUserInterface.cs
+++ b/Content.Client/Storage/StorageBoundUserInterface.cs
@@ -17,6 +17,14 @@ public StorageBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKe
_storage = _entManager.System();
}
+ protected override void Open()
+ {
+ base.Open();
+
+ if (_entManager.TryGetComponent(Owner, out var comp))
+ _storage.OpenStorageWindow((Owner, comp));
+ }
+
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
@@ -25,16 +33,5 @@ protected override void Dispose(bool disposing)
_storage.CloseStorageWindow(Owner);
}
-
- protected override void ReceiveMessage(BoundUserInterfaceMessage message)
- {
- base.ReceiveMessage(message);
-
- if (message is StorageModifyWindowMessage)
- {
- if (_entManager.TryGetComponent(Owner, out var comp))
- _storage.OpenStorageWindow((Owner, comp));
- }
- }
}
diff --git a/Content.Client/Storage/Systems/StorageSystem.cs b/Content.Client/Storage/Systems/StorageSystem.cs
index ce0a6bf1ca4..8bf0dcd9812 100644
--- a/Content.Client/Storage/Systems/StorageSystem.cs
+++ b/Content.Client/Storage/Systems/StorageSystem.cs
@@ -26,7 +26,7 @@ public override void Initialize()
SubscribeLocalEvent(OnShutdown);
SubscribeNetworkEvent(HandlePickupAnimation);
- SubscribeNetworkEvent(HandleAnimatingInsertingEntities);
+ SubscribeAllEvent(HandleAnimatingInsertingEntities);
}
public override void UpdateUI(Entity entity)
@@ -111,7 +111,7 @@ private void CloseStorageBoundUserInterface(Entity enti
if (!Resolve(entity, ref entity.Comp, false))
return;
- if (entity.Comp.OpenInterfaces.GetValueOrDefault(StorageComponent.StorageUiKey.Key) is not { } bui)
+ if (entity.Comp.ClientOpenInterfaces.GetValueOrDefault(StorageComponent.StorageUiKey.Key) is not { } bui)
return;
bui.Close();
diff --git a/Content.Client/Store/Ui/StoreBoundUserInterface.cs b/Content.Client/Store/Ui/StoreBoundUserInterface.cs
index f87b92bc615..88ad0e3de8b 100644
--- a/Content.Client/Store/Ui/StoreBoundUserInterface.cs
+++ b/Content.Client/Store/Ui/StoreBoundUserInterface.cs
@@ -17,7 +17,7 @@ public sealed class StoreBoundUserInterface : BoundUserInterface
private string _windowName = Loc.GetString("store-ui-default-title");
[ViewVariables]
- private string _search = "";
+ private string _search = string.Empty;
[ViewVariables]
private HashSet _listings = new();
@@ -41,7 +41,7 @@ protected override void Open()
_menu.OnCategoryButtonPressed += (_, category) =>
{
_menu.CurrentCategory = category;
- SendMessage(new StoreRequestUpdateInterfaceMessage());
+ _menu?.UpdateListing();
};
_menu.OnWithdrawAttempt += (_, type, amount) =>
@@ -49,11 +49,6 @@ protected override void Open()
SendMessage(new StoreRequestWithdrawMessage(type, amount));
};
- _menu.OnRefreshButtonPressed += (_) =>
- {
- SendMessage(new StoreRequestUpdateInterfaceMessage());
- };
-
_menu.SearchTextUpdated += (_, search) =>
{
_search = search.Trim().ToLowerInvariant();
diff --git a/Content.Client/Store/Ui/StoreListingControl.xaml b/Content.Client/Store/Ui/StoreListingControl.xaml
index aefeec17cc8..12b4d7b5b30 100644
--- a/Content.Client/Store/Ui/StoreListingControl.xaml
+++ b/Content.Client/Store/Ui/StoreListingControl.xaml
@@ -15,6 +15,7 @@
Margin="0,0,4,0"
MinSize="48 48"
Stretch="KeepAspectCentered" />
+
diff --git a/Content.Client/Store/Ui/StoreListingControl.xaml.cs b/Content.Client/Store/Ui/StoreListingControl.xaml.cs
index bb600588e04..030f07dc7ca 100644
--- a/Content.Client/Store/Ui/StoreListingControl.xaml.cs
+++ b/Content.Client/Store/Ui/StoreListingControl.xaml.cs
@@ -1,25 +1,91 @@
+using Content.Client.GameTicking.Managers;
+using Content.Shared.Store;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Graphics;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
namespace Content.Client.Store.Ui;
[GenerateTypedNameReferences]
public sealed partial class StoreListingControl : Control
{
- public StoreListingControl(string itemName, string itemDescription,
- string price, bool canBuy, Texture? texture = null)
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+ [Dependency] private readonly IEntityManager _entity = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ private readonly ClientGameTicker _ticker;
+
+ private readonly ListingData _data;
+
+ private readonly bool _hasBalance;
+ private readonly string _price;
+ public StoreListingControl(ListingData data, string price, bool hasBalance, Texture? texture = null)
{
+ IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
- StoreItemName.Text = itemName;
- StoreItemDescription.SetMessage(itemDescription);
+ _ticker = _entity.System();
+
+ _data = data;
+ _hasBalance = hasBalance;
+ _price = price;
- StoreItemBuyButton.Text = price;
- StoreItemBuyButton.Disabled = !canBuy;
+ StoreItemName.Text = ListingLocalisationHelpers.GetLocalisedNameOrEntityName(_data, _prototype);
+ StoreItemDescription.SetMessage(ListingLocalisationHelpers.GetLocalisedDescriptionOrEntityDescription(_data, _prototype));
+
+ UpdateBuyButtonText();
+ StoreItemBuyButton.Disabled = !CanBuy();
StoreItemTexture.Texture = texture;
}
+
+ private bool CanBuy()
+ {
+ if (!_hasBalance)
+ return false;
+
+ var stationTime = _timing.CurTime.Subtract(_ticker.RoundStartTimeSpan);
+ if (_data.RestockTime > stationTime)
+ return false;
+
+ return true;
+ }
+
+ private void UpdateBuyButtonText()
+ {
+ var stationTime = _timing.CurTime.Subtract(_ticker.RoundStartTimeSpan);
+ if (_data.RestockTime > stationTime)
+ {
+ var timeLeftToBuy = stationTime - _data.RestockTime;
+ StoreItemBuyButton.Text = timeLeftToBuy.Duration().ToString(@"mm\:ss");
+ }
+ else
+ {
+ StoreItemBuyButton.Text = _price;
+ }
+ }
+
+ private void UpdateName()
+ {
+ var name = ListingLocalisationHelpers.GetLocalisedNameOrEntityName(_data, _prototype);
+
+ var stationTime = _timing.CurTime.Subtract(_ticker.RoundStartTimeSpan);
+ if (_data.RestockTime > stationTime)
+ {
+ name += Loc.GetString("store-ui-button-out-of-stock");
+ }
+
+ StoreItemName.Text = name;
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ base.FrameUpdate(args);
+
+ UpdateBuyButtonText();
+ UpdateName();
+ StoreItemBuyButton.Disabled = !CanBuy();
+ }
}
diff --git a/Content.Client/Store/Ui/StoreMenu.xaml b/Content.Client/Store/Ui/StoreMenu.xaml
index fc4cbe444fc..843c9dc0296 100644
--- a/Content.Client/Store/Ui/StoreMenu.xaml
+++ b/Content.Client/Store/Ui/StoreMenu.xaml
@@ -12,11 +12,6 @@
HorizontalAlignment="Left"
Access="Public"
HorizontalExpand="True" />
-
CeilInt
}
+
+ ///
+ /// If the viewport is allowed to freely scale, this determines which dimensions should be ignored while fitting the viewport
+ ///
+ public enum ScalingViewportIgnoreDimension
+ {
+ ///
+ /// The viewport won't ignore any dimension.
+ ///
+ None = 0,
+
+ ///
+ /// The viewport will ignore the horizontal dimension, and will exclusively consider the vertical dimension for scaling.
+ ///
+ Horizontal,
+
+ ///
+ /// The viewport will ignore the vertical dimension, and will exclusively consider the horizontal dimension for scaling.
+ ///
+ Vertical
+ }
}
diff --git a/Content.Client/Weapons/Ranged/ItemStatus/BulletRender.cs b/Content.Client/Weapons/Ranged/ItemStatus/BulletRender.cs
new file mode 100644
index 00000000000..8aea5d7ee62
--- /dev/null
+++ b/Content.Client/Weapons/Ranged/ItemStatus/BulletRender.cs
@@ -0,0 +1,252 @@
+using System.Numerics;
+using Content.Client.Resources;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface;
+
+namespace Content.Client.Weapons.Ranged.ItemStatus;
+
+public abstract class BaseBulletRenderer : Control
+{
+ private int _capacity;
+ private LayoutParameters _params;
+
+ public int Rows { get; set; } = 2;
+ public int Count { get; set; }
+
+ public int Capacity
+ {
+ get => _capacity;
+ set
+ {
+ if (_capacity == value)
+ return;
+
+ _capacity = value;
+ InvalidateMeasure();
+ }
+ }
+
+ protected LayoutParameters Parameters
+ {
+ get => _params;
+ set
+ {
+ _params = value;
+ InvalidateMeasure();
+ }
+ }
+
+ protected override Vector2 MeasureOverride(Vector2 availableSize)
+ {
+ var countPerRow = Math.Min(Capacity, CountPerRow(availableSize.X));
+
+ var rows = Math.Min((int) MathF.Ceiling(Capacity / (float) countPerRow), Rows);
+
+ var height = _params.ItemHeight * rows + (_params.VerticalSeparation * rows - 1);
+ var width = RowWidth(countPerRow);
+
+ return new Vector2(width, height);
+ }
+
+ protected override void Draw(DrawingHandleScreen handle)
+ {
+ // Scale rendering in this control by UIScale.
+ var currentTransform = handle.GetTransform();
+ handle.SetTransform(Matrix3.CreateScale(new Vector2(UIScale)) * currentTransform);
+
+ var countPerRow = CountPerRow(Size.X);
+
+ var pos = new Vector2();
+
+ var spent = Capacity - Count;
+
+ var bulletsDone = 0;
+
+ // Draw by rows, bottom to top.
+ for (var row = 0; row < Rows; row++)
+ {
+ var altColor = false;
+
+ var thisRowCount = Math.Min(countPerRow, Capacity - bulletsDone);
+ if (thisRowCount <= 0)
+ break;
+
+ // Handle MinCountPerRow
+ // We only do this if:
+ // 1. The next row would have less than MinCountPerRow bullets.
+ // 2. The next row is actually visible (we aren't the last row).
+ // 3. MinCountPerRow is actually smaller than the count per row (avoid degenerate cases).
+ // 4. There's enough bullets that at least one will end up on the next row.
+ var nextRowCount = Capacity - bulletsDone - thisRowCount;
+ if (nextRowCount < _params.MinCountPerRow && row != Rows - 1 && _params.MinCountPerRow < countPerRow && nextRowCount > 0)
+ thisRowCount -= _params.MinCountPerRow - nextRowCount;
+
+ // Account for row width to right-align.
+ var rowWidth = RowWidth(thisRowCount);
+ pos.X += Size.X - rowWidth;
+
+ // Draw row left to right (so overlapping works)
+ for (var bullet = 0; bullet < thisRowCount; bullet++)
+ {
+ var absIdx = Capacity - bulletsDone - thisRowCount + bullet;
+
+ var renderPos = pos;
+ renderPos.Y = Size.Y - renderPos.Y - _params.ItemHeight;
+
+ DrawItem(handle, renderPos, absIdx < spent, altColor);
+
+ pos.X += _params.ItemSeparation;
+ altColor ^= true;
+ }
+
+ bulletsDone += thisRowCount;
+ pos.X = 0;
+ pos.Y += _params.ItemHeight + _params.VerticalSeparation;
+ }
+ }
+
+ protected abstract void DrawItem(DrawingHandleScreen handle, Vector2 renderPos, bool spent, bool altColor);
+
+ private int CountPerRow(float width)
+ {
+ return (int) ((width - _params.ItemWidth + _params.ItemSeparation) / _params.ItemSeparation);
+ }
+
+ private int RowWidth(int count)
+ {
+ return (count - 1) * _params.ItemSeparation + _params.ItemWidth;
+ }
+
+ protected struct LayoutParameters
+ {
+ public int ItemHeight;
+ public int ItemSeparation;
+ public int ItemWidth;
+ public int VerticalSeparation;
+
+ ///
+ /// Try to ensure there's at least this many bullets on one row.
+ ///
+ ///
+ /// For example, if there are two rows and the second row has only two bullets,
+ /// we "steal" some bullets from the row below it to make it look nicer.
+ ///
+ public int MinCountPerRow;
+ }
+}
+
+///
+/// Renders one or more rows of bullets for item status.
+///
+///
+/// This is a custom control to allow complex responsive layout logic.
+///
+public sealed class BulletRender : BaseBulletRenderer
+{
+ public const int MinCountPerRow = 7;
+
+ public const int BulletHeight = 12;
+ public const int VerticalSeparation = 2;
+
+ private static readonly LayoutParameters LayoutNormal = new LayoutParameters
+ {
+ ItemHeight = BulletHeight,
+ ItemSeparation = 3,
+ ItemWidth = 5,
+ VerticalSeparation = VerticalSeparation,
+ MinCountPerRow = MinCountPerRow
+ };
+
+ private static readonly LayoutParameters LayoutTiny = new LayoutParameters
+ {
+ ItemHeight = BulletHeight,
+ ItemSeparation = 2,
+ ItemWidth = 2,
+ VerticalSeparation = VerticalSeparation,
+ MinCountPerRow = MinCountPerRow
+ };
+
+ private static readonly Color ColorA = Color.FromHex("#b68f0e");
+ private static readonly Color ColorB = Color.FromHex("#d7df60");
+ private static readonly Color ColorGoneA = Color.FromHex("#000000");
+ private static readonly Color ColorGoneB = Color.FromHex("#222222");
+
+ private readonly Texture _bulletTiny;
+ private readonly Texture _bulletNormal;
+
+ private BulletType _type = BulletType.Normal;
+
+ public BulletType Type
+ {
+ get => _type;
+ set
+ {
+ if (_type == value)
+ return;
+
+ Parameters = _type switch
+ {
+ BulletType.Normal => LayoutNormal,
+ BulletType.Tiny => LayoutTiny,
+ _ => throw new ArgumentOutOfRangeException()
+ };
+
+ _type = value;
+ }
+ }
+
+ public BulletRender()
+ {
+ var resC = IoCManager.Resolve();
+ _bulletTiny = resC.GetTexture("/Textures/Interface/ItemStatus/Bullets/tiny.png");
+ _bulletNormal = resC.GetTexture("/Textures/Interface/ItemStatus/Bullets/normal.png");
+ Parameters = LayoutNormal;
+ }
+
+ protected override void DrawItem(DrawingHandleScreen handle, Vector2 renderPos, bool spent, bool altColor)
+ {
+ Color color;
+ if (spent)
+ color = altColor ? ColorGoneA : ColorGoneB;
+ else
+ color = altColor ? ColorA : ColorB;
+
+ var texture = _type == BulletType.Tiny ? _bulletTiny : _bulletNormal;
+ handle.DrawTexture(texture, renderPos, color);
+ }
+
+ public enum BulletType
+ {
+ Normal,
+ Tiny
+ }
+}
+
+public sealed class BatteryBulletRenderer : BaseBulletRenderer
+{
+ private static readonly Color ItemColor = Color.FromHex("#E00000");
+ private static readonly Color ItemColorGone = Color.Black;
+
+ private const int SizeH = 10;
+ private const int SizeV = 10;
+ private const int Separation = 4;
+
+ public BatteryBulletRenderer()
+ {
+ Parameters = new LayoutParameters
+ {
+ ItemWidth = SizeH,
+ ItemHeight = SizeV,
+ ItemSeparation = SizeH + Separation,
+ MinCountPerRow = 3,
+ VerticalSeparation = Separation
+ };
+ }
+
+ protected override void DrawItem(DrawingHandleScreen handle, Vector2 renderPos, bool spent, bool altColor)
+ {
+ var color = spent ? ItemColorGone : ItemColor;
+ handle.DrawRect(UIBox2.FromDimensions(renderPos, new Vector2(SizeH, SizeV)), color);
+ }
+}
diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs
index 32343af56f0..84eaa9af1b0 100644
--- a/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs
+++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs
@@ -4,11 +4,11 @@
using Content.Client.Resources;
using Content.Client.Stylesheets;
using Content.Client.Weapons.Ranged.Components;
+using Content.Client.Weapons.Ranged.ItemStatus;
using Robust.Client.Animations;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
-using Robust.Shared.Graphics;
namespace Content.Client.Weapons.Ranged.Systems;
@@ -91,122 +91,32 @@ public sealed class UpdateAmmoCounterEvent : HandledEntityEventArgs
private sealed class DefaultStatusControl : Control
{
- private readonly BoxContainer _bulletsListTop;
- private readonly BoxContainer _bulletsListBottom;
+ private readonly BulletRender _bulletRender;
public DefaultStatusControl()
{
MinHeight = 15;
HorizontalExpand = true;
- VerticalAlignment = Control.VAlignment.Center;
- AddChild(new BoxContainer
+ VerticalAlignment = VAlignment.Center;
+ AddChild(_bulletRender = new BulletRender
{
- Orientation = BoxContainer.LayoutOrientation.Vertical,
- HorizontalExpand = true,
- VerticalAlignment = VAlignment.Center,
- SeparationOverride = 0,
- Children =
- {
- (_bulletsListTop = new BoxContainer
- {
- Orientation = BoxContainer.LayoutOrientation.Horizontal,
- SeparationOverride = 0
- }),
- new BoxContainer
- {
- Orientation = BoxContainer.LayoutOrientation.Horizontal,
- HorizontalExpand = true,
- Children =
- {
- new Control
- {
- HorizontalExpand = true,
- Children =
- {
- (_bulletsListBottom = new BoxContainer
- {
- Orientation = BoxContainer.LayoutOrientation.Horizontal,
- VerticalAlignment = VAlignment.Center,
- SeparationOverride = 0
- }),
- }
- },
- }
- }
- }
+ HorizontalAlignment = HAlignment.Right,
+ VerticalAlignment = VAlignment.Bottom
});
}
public void Update(int count, int capacity)
{
- _bulletsListTop.RemoveAllChildren();
- _bulletsListBottom.RemoveAllChildren();
-
- string texturePath;
- if (capacity <= 20)
- {
- texturePath = "/Textures/Interface/ItemStatus/Bullets/normal.png";
- }
- else if (capacity <= 30)
- {
- texturePath = "/Textures/Interface/ItemStatus/Bullets/small.png";
- }
- else
- {
- texturePath = "/Textures/Interface/ItemStatus/Bullets/tiny.png";
- }
-
- var texture = StaticIoC.ResC.GetTexture(texturePath);
-
- const int tinyMaxRow = 60;
+ _bulletRender.Count = count;
+ _bulletRender.Capacity = capacity;
- if (capacity > tinyMaxRow)
- {
- FillBulletRow(_bulletsListBottom, Math.Min(tinyMaxRow, count), tinyMaxRow, texture);
- FillBulletRow(_bulletsListTop, Math.Max(0, count - tinyMaxRow), capacity - tinyMaxRow, texture);
- }
- else
- {
- FillBulletRow(_bulletsListBottom, count, capacity, texture);
- }
- }
-
- private static void FillBulletRow(Control container, int count, int capacity, Texture texture)
- {
- var colorA = Color.FromHex("#b68f0e");
- var colorB = Color.FromHex("#d7df60");
- var colorGoneA = Color.FromHex("#000000");
- var colorGoneB = Color.FromHex("#222222");
-
- var altColor = false;
-
- for (var i = count; i < capacity; i++)
- {
- container.AddChild(new TextureRect
- {
- Texture = texture,
- ModulateSelfOverride = altColor ? colorGoneA : colorGoneB
- });
-
- altColor ^= true;
- }
-
- for (var i = 0; i < count; i++)
- {
- container.AddChild(new TextureRect
- {
- Texture = texture,
- ModulateSelfOverride = altColor ? colorA : colorB
- });
-
- altColor ^= true;
- }
+ _bulletRender.Type = capacity > 50 ? BulletRender.BulletType.Tiny : BulletRender.BulletType.Normal;
}
}
public sealed class BoxesStatusControl : Control
{
- private readonly BoxContainer _bulletsList;
+ private readonly BatteryBulletRenderer _bullets;
private readonly Label _ammoCount;
public BoxesStatusControl()
@@ -218,27 +128,18 @@ public BoxesStatusControl()
AddChild(new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal,
- HorizontalExpand = true,
Children =
{
- new Control
+ (_bullets = new BatteryBulletRenderer
{
- HorizontalExpand = true,
- Children =
- {
- (_bulletsList = new BoxContainer
- {
- Orientation = BoxContainer.LayoutOrientation.Horizontal,
- VerticalAlignment = VAlignment.Center,
- SeparationOverride = 4
- }),
- }
- },
- new Control() { MinSize = new Vector2(5, 0) },
+ Margin = new Thickness(0, 0, 5, 0),
+ HorizontalExpand = true
+ }),
(_ammoCount = new Label
{
StyleClasses = { StyleNano.StyleClassItemStatus },
HorizontalAlignment = HAlignment.Right,
+ VerticalAlignment = VAlignment.Bottom
}),
}
});
@@ -246,52 +147,18 @@ public BoxesStatusControl()
public void Update(int count, int max)
{
- _bulletsList.RemoveAllChildren();
-
_ammoCount.Visible = true;
_ammoCount.Text = $"x{count:00}";
- max = Math.Min(max, 8);
- FillBulletRow(_bulletsList, count, max);
- }
-
- private static void FillBulletRow(Control container, int count, int capacity)
- {
- var colorGone = Color.FromHex("#000000");
- var color = Color.FromHex("#E00000");
-
- // Draw the empty ones
- for (var i = count; i < capacity; i++)
- {
- container.AddChild(new PanelContainer
- {
- PanelOverride = new StyleBoxFlat()
- {
- BackgroundColor = colorGone,
- },
- MinSize = new Vector2(10, 15),
- });
- }
- // Draw the full ones, but limit the count to the capacity
- count = Math.Min(count, capacity);
- for (var i = 0; i < count; i++)
- {
- container.AddChild(new PanelContainer
- {
- PanelOverride = new StyleBoxFlat()
- {
- BackgroundColor = color,
- },
- MinSize = new Vector2(10, 15),
- });
- }
+ _bullets.Capacity = max;
+ _bullets.Count = count;
}
}
private sealed class ChamberMagazineStatusControl : Control
{
- private readonly BoxContainer _bulletsList;
+ private readonly BulletRender _bulletRender;
private readonly TextureRect _chamberedBullet;
private readonly Label _noMagazineLabel;
private readonly Label _ammoCount;
@@ -308,23 +175,16 @@ public ChamberMagazineStatusControl()
HorizontalExpand = true,
Children =
{
- (_chamberedBullet = new TextureRect
- {
- Texture = StaticIoC.ResC.GetTexture("/Textures/Interface/ItemStatus/Bullets/chambered_rotated.png"),
- VerticalAlignment = VAlignment.Center,
- HorizontalAlignment = HAlignment.Right,
- }),
- new Control() { MinSize = new Vector2(5,0) },
new Control
{
HorizontalExpand = true,
+ Margin = new Thickness(0, 0, 5, 0),
Children =
{
- (_bulletsList = new BoxContainer
+ (_bulletRender = new BulletRender
{
- Orientation = BoxContainer.LayoutOrientation.Horizontal,
- VerticalAlignment = VAlignment.Center,
- SeparationOverride = 0
+ HorizontalAlignment = HAlignment.Right,
+ VerticalAlignment = VAlignment.Bottom
}),
(_noMagazineLabel = new Label
{
@@ -333,12 +193,25 @@ public ChamberMagazineStatusControl()
})
}
},
- new Control() { MinSize = new Vector2(5,0) },
- (_ammoCount = new Label
+ new BoxContainer
{
- StyleClasses = {StyleNano.StyleClassItemStatus},
- HorizontalAlignment = HAlignment.Right,
- }),
+ Orientation = BoxContainer.LayoutOrientation.Vertical,
+ VerticalAlignment = VAlignment.Bottom,
+ Margin = new Thickness(0, 0, 0, 2),
+ Children =
+ {
+ (_ammoCount = new Label
+ {
+ StyleClasses = {StyleNano.StyleClassItemStatus},
+ HorizontalAlignment = HAlignment.Right,
+ }),
+ (_chamberedBullet = new TextureRect
+ {
+ Texture = StaticIoC.ResC.GetTexture("/Textures/Interface/ItemStatus/Bullets/chambered.png"),
+ HorizontalAlignment = HAlignment.Left,
+ }),
+ }
+ }
}
});
}
@@ -348,61 +221,24 @@ public void Update(bool chambered, bool magazine, int count, int capacity)
_chamberedBullet.ModulateSelfOverride =
chambered ? Color.FromHex("#d7df60") : Color.Black;
- _bulletsList.RemoveAllChildren();
-
if (!magazine)
{
+ _bulletRender.Visible = false;
_noMagazineLabel.Visible = true;
_ammoCount.Visible = false;
return;
}
+ _bulletRender.Visible = true;
_noMagazineLabel.Visible = false;
_ammoCount.Visible = true;
- var texturePath = "/Textures/Interface/ItemStatus/Bullets/normal.png";
- var texture = StaticIoC.ResC.GetTexture(texturePath);
+ _bulletRender.Count = count;
+ _bulletRender.Capacity = capacity;
- _ammoCount.Text = $"x{count:00}";
- capacity = Math.Min(capacity, 20);
- FillBulletRow(_bulletsList, count, capacity, texture);
- }
-
- private static void FillBulletRow(Control container, int count, int capacity, Texture texture)
- {
- var colorA = Color.FromHex("#b68f0e");
- var colorB = Color.FromHex("#d7df60");
- var colorGoneA = Color.FromHex("#000000");
- var colorGoneB = Color.FromHex("#222222");
+ _bulletRender.Type = capacity > 50 ? BulletRender.BulletType.Tiny : BulletRender.BulletType.Normal;
- var altColor = false;
-
- // Draw the empty ones
- for (var i = count; i < capacity; i++)
- {
- container.AddChild(new TextureRect
- {
- Texture = texture,
- ModulateSelfOverride = altColor ? colorGoneA : colorGoneB,
- Stretch = TextureRect.StretchMode.KeepCentered
- });
-
- altColor ^= true;
- }
-
- // Draw the full ones, but limit the count to the capacity
- count = Math.Min(count, capacity);
- for (var i = 0; i < count; i++)
- {
- container.AddChild(new TextureRect
- {
- Texture = texture,
- ModulateSelfOverride = altColor ? colorA : colorB,
- Stretch = TextureRect.StretchMode.KeepCentered
- });
-
- altColor ^= true;
- }
+ _ammoCount.Text = $"x{count:00}";
}
public void PlayAlarmAnimation(Animation animation)
diff --git a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleBoundUserInterface.cs b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleBoundUserInterface.cs
index 143f01b6b7e..2538caf6eb8 100644
--- a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleBoundUserInterface.cs
+++ b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleBoundUserInterface.cs
@@ -39,6 +39,14 @@ protected override void Open()
{
SendMessage(new AnalysisConsoleExtractButtonPressedMessage());
};
+ _consoleMenu.OnUpBiasButtonPressed += () =>
+ {
+ SendMessage(new AnalysisConsoleBiasButtonPressedMessage(false));
+ };
+ _consoleMenu.OnDownBiasButtonPressed += () =>
+ {
+ SendMessage(new AnalysisConsoleBiasButtonPressedMessage(true));
+ };
}
protected override void UpdateState(BoundUserInterfaceState state)
@@ -47,7 +55,7 @@ protected override void UpdateState(BoundUserInterfaceState state)
switch (state)
{
- case AnalysisConsoleScanUpdateState msg:
+ case AnalysisConsoleUpdateState msg:
_consoleMenu?.SetButtonsDisabled(msg);
_consoleMenu?.UpdateInformationDisplay(msg);
_consoleMenu?.UpdateProgressBar(msg);
diff --git a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml
index ed4008004f9..29f4a548479 100644
--- a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml
+++ b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml
@@ -1,29 +1,45 @@
+ xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
+ xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+ xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
+ Title="{Loc 'analysis-console-menu-title'}"
+ MinSize="620 280"
+ SetSize="620 280">
-
+
+ Text="{Loc 'analysis-console-server-list-button'}">
+
+
+
+
+
@@ -36,13 +52,13 @@
-
+
-
+
diff --git a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs
index 90732f814f8..2890bb3dbf7 100644
--- a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs
+++ b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs
@@ -3,6 +3,7 @@
using Content.Shared.Xenoarchaeology.Equipment;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
+using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -19,6 +20,8 @@ public sealed partial class AnalysisConsoleMenu : FancyWindow
public event Action? OnScanButtonPressed;
public event Action? OnPrintButtonPressed;
public event Action? OnExtractButtonPressed;
+ public event Action? OnUpBiasButtonPressed;
+ public event Action? OnDownBiasButtonPressed;
// For rendering the progress bar, updated from BUI state
private TimeSpan? _startTime;
@@ -36,6 +39,12 @@ public AnalysisConsoleMenu()
ScanButton.OnPressed += _ => OnScanButtonPressed?.Invoke();
PrintButton.OnPressed += _ => OnPrintButtonPressed?.Invoke();
ExtractButton.OnPressed += _ => OnExtractButtonPressed?.Invoke();
+ UpBiasButton.OnPressed += _ => OnUpBiasButtonPressed?.Invoke();
+ DownBiasButton.OnPressed += _ => OnDownBiasButtonPressed?.Invoke();
+
+ var buttonGroup = new ButtonGroup(false);
+ UpBiasButton.Group = buttonGroup;
+ DownBiasButton.Group = buttonGroup;
}
protected override void FrameUpdate(FrameEventArgs args)
@@ -60,25 +69,55 @@ protected override void FrameUpdate(FrameEventArgs args)
ProgressBar.Value = Math.Clamp(1.0f - (float) remaining.Divide(total), 0.0f, 1.0f);
}
- public void SetButtonsDisabled(AnalysisConsoleScanUpdateState state)
+ public void SetButtonsDisabled(AnalysisConsoleUpdateState state)
{
ScanButton.Disabled = !state.CanScan;
PrintButton.Disabled = !state.CanPrint;
+ if (state.IsTraversalDown)
+ DownBiasButton.Pressed = true;
+ else
+ UpBiasButton.Pressed = true;
- var disabled = !state.ServerConnected || !state.CanScan || state.PointAmount <= 0;
-
- ExtractButton.Disabled = disabled;
+ ExtractButton.Disabled = false;
+ if (!state.ServerConnected)
+ {
+ ExtractButton.Disabled = true;
+ ExtractButton.ToolTip = Loc.GetString("analysis-console-no-server-connected");
+ }
+ else if (!state.CanScan)
+ {
+ ExtractButton.Disabled = true;
+
+ // CanScan can be false if either there's no analyzer connected or if there's
+ // no entity on the scanner. The `Information` text will always tell the user
+ // of the former case, but in the latter, it'll only show a message if a scan
+ // has never been performed, so add a tooltip to indicate that the artifact
+ // is gone.
+ if (state.AnalyzerConnected)
+ {
+ ExtractButton.ToolTip = Loc.GetString("analysis-console-no-artifact-placed");
+ }
+ else
+ {
+ ExtractButton.ToolTip = null;
+ }
+ }
+ else if (state.PointAmount <= 0)
+ {
+ ExtractButton.Disabled = true;
+ ExtractButton.ToolTip = Loc.GetString("analysis-console-no-points-to-extract");
+ }
- if (disabled)
+ if (ExtractButton.Disabled)
{
ExtractButton.RemoveStyleClass("ButtonColorGreen");
}
else
{
ExtractButton.AddStyleClass("ButtonColorGreen");
+ ExtractButton.ToolTip = null;
}
}
-
private void UpdateArtifactIcon(EntityUid? uid)
{
if (uid == null)
@@ -91,7 +130,7 @@ private void UpdateArtifactIcon(EntityUid? uid)
ArtifactDisplay.SetEntity(uid);
}
- public void UpdateInformationDisplay(AnalysisConsoleScanUpdateState state)
+ public void UpdateInformationDisplay(AnalysisConsoleUpdateState state)
{
var message = new FormattedMessage();
@@ -129,7 +168,7 @@ public void UpdateInformationDisplay(AnalysisConsoleScanUpdateState state)
Information.SetMessage(message);
}
- public void UpdateProgressBar(AnalysisConsoleScanUpdateState state)
+ public void UpdateProgressBar(AnalysisConsoleUpdateState state)
{
ProgressBar.Visible = state.Scanning;
ProgressLabel.Visible = state.Scanning;
diff --git a/Content.IntegrationTests/Pair/TestMapData.cs b/Content.IntegrationTests/Pair/TestMapData.cs
index bdf12080388..343641e1613 100644
--- a/Content.IntegrationTests/Pair/TestMapData.cs
+++ b/Content.IntegrationTests/Pair/TestMapData.cs
@@ -10,9 +10,8 @@ namespace Content.IntegrationTests.Pair;
public sealed class TestMapData
{
public EntityUid MapUid { get; set; }
- public EntityUid GridUid { get; set; }
- public MapId MapId { get; set; }
- public MapGridComponent MapGrid { get; set; } = default!;
+ public Entity Grid;
+ public MapId MapId;
public EntityCoordinates GridCoords { get; set; }
public MapCoordinates MapCoords { get; set; }
public TileRef Tile { get; set; }
@@ -21,4 +20,4 @@ public sealed class TestMapData
public EntityUid CMapUid { get; set; }
public EntityUid CGridUid { get; set; }
public EntityCoordinates CGridCoords { get; set; }
-}
\ No newline at end of file
+}
diff --git a/Content.IntegrationTests/Pair/TestPair.Helpers.cs b/Content.IntegrationTests/Pair/TestPair.Helpers.cs
index 554807b2d25..0ea6d3e2dcc 100644
--- a/Content.IntegrationTests/Pair/TestPair.Helpers.cs
+++ b/Content.IntegrationTests/Pair/TestPair.Helpers.cs
@@ -1,5 +1,6 @@
#nullable enable
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
@@ -14,36 +15,37 @@ public sealed partial class TestPair
///
/// Creates a map, a grid, and a tile, and gives back references to them.
///
- public async Task CreateTestMap()
+ [MemberNotNull(nameof(TestMap))]
+ public async Task CreateTestMap(bool initialized = true, string tile = "Plating")
{
+ var mapData = new TestMapData();
+ TestMap = mapData;
await Server.WaitIdleAsync();
var tileDefinitionManager = Server.ResolveDependency();
- var mapData = new TestMapData();
TestMap = mapData;
await Server.WaitPost(() =>
{
- mapData.MapId = Server.MapMan.CreateMap();
- mapData.MapUid = Server.MapMan.GetMapEntityId(mapData.MapId);
- var mapGrid = Server.MapMan.CreateGridEntity(mapData.MapId);
- mapData.MapGrid = mapGrid;
- mapData.GridUid = mapGrid.Owner; // Fixing this requires an engine PR.
- mapData.GridCoords = new EntityCoordinates(mapData.GridUid, 0, 0);
- var plating = tileDefinitionManager["Plating"];
+ mapData.MapUid = Server.System().CreateMap(out mapData.MapId, runMapInit: initialized);
+ mapData.Grid = Server.MapMan.CreateGridEntity(mapData.MapId);
+ mapData.GridCoords = new EntityCoordinates(mapData.Grid, 0, 0);
+ var plating = tileDefinitionManager[tile];
var platingTile = new Tile(plating.TileId);
- mapData.MapGrid.SetTile(mapData.GridCoords, platingTile);
+ mapData.Grid.Comp.SetTile(mapData.GridCoords, platingTile);
mapData.MapCoords = new MapCoordinates(0, 0, mapData.MapId);
- mapData.Tile = mapData.MapGrid.GetAllTiles().First();
+ mapData.Tile = mapData.Grid.Comp.GetAllTiles().First();
});
+ TestMap = mapData;
if (!Settings.Connected)
return mapData;
await RunTicksSync(10);
mapData.CMapUid = ToClientUid(mapData.MapUid);
- mapData.CGridUid = ToClientUid(mapData.GridUid);
+ mapData.CGridUid = ToClientUid(mapData.Grid);
mapData.CGridCoords = new EntityCoordinates(mapData.CGridUid, 0, 0);
+ TestMap = mapData;
return mapData;
}
diff --git a/Content.IntegrationTests/Pair/TestPair.Recycle.cs b/Content.IntegrationTests/Pair/TestPair.Recycle.cs
index 52fdf600bb4..c0f4b3b745f 100644
--- a/Content.IntegrationTests/Pair/TestPair.Recycle.cs
+++ b/Content.IntegrationTests/Pair/TestPair.Recycle.cs
@@ -131,7 +131,7 @@ public async Task CleanPooledPair(PoolSettings settings, TextWriter testOut)
// Move to pre-round lobby. Required to toggle dummy ticker on and off
if (gameTicker.RunLevel != GameRunLevel.PreRoundLobby)
{
- await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Restarting server.");
+ await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Restarting round.");
Assert.That(gameTicker.DummyTicker, Is.False);
Server.CfgMan.SetCVar(CCVars.GameLobbyEnabled, true);
await Server.WaitPost(() => gameTicker.RestartRound());
@@ -146,6 +146,7 @@ public async Task CleanPooledPair(PoolSettings settings, TextWriter testOut)
// Restart server.
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Restarting server again");
+ await Server.WaitPost(() => Server.EntMan.FlushEntities());
await Server.WaitPost(() => gameTicker.RestartRound());
await RunTicksSync(1);
diff --git a/Content.IntegrationTests/Pair/TestPair.Timing.cs b/Content.IntegrationTests/Pair/TestPair.Timing.cs
index 3487ea68010..e0859660d42 100644
--- a/Content.IntegrationTests/Pair/TestPair.Timing.cs
+++ b/Content.IntegrationTests/Pair/TestPair.Timing.cs
@@ -1,5 +1,4 @@
#nullable enable
-using Robust.Shared.Timing;
namespace Content.IntegrationTests.Pair;
@@ -19,6 +18,22 @@ public async Task RunTicksSync(int ticks)
}
}
+ ///
+ /// Convert a time interval to some number of ticks.
+ ///
+ public int SecondsToTicks(float seconds)
+ {
+ return (int) Math.Ceiling(seconds / Server.Timing.TickPeriod.TotalSeconds);
+ }
+
+ ///
+ /// Run the server & client in sync for some amount of time
+ ///
+ public async Task RunSeconds(float seconds)
+ {
+ await RunTicksSync(SecondsToTicks(seconds));
+ }
+
///
/// Runs the server-client pair in sync, but also ensures they are both idle each tick.
///
@@ -59,4 +74,4 @@ public async Task SyncTicks(int targetDelta = 1)
delta = cTick - sTick;
Assert.That(delta, Is.EqualTo(targetDelta));
}
-}
\ No newline at end of file
+}
diff --git a/Content.IntegrationTests/PoolManager.Cvars.cs b/Content.IntegrationTests/PoolManager.Cvars.cs
index 327ec627f52..d39c7284d06 100644
--- a/Content.IntegrationTests/PoolManager.Cvars.cs
+++ b/Content.IntegrationTests/PoolManager.Cvars.cs
@@ -32,6 +32,7 @@ private static readonly (string cvar, string value)[] TestCvars =
(CCVars.GameLobbyEnabled.Name, "false"),
(CCVars.ConfigPresetDevelopment.Name, "false"),
(CCVars.AdminLogsEnabled.Name, "false"),
+ (CCVars.AutosaveEnabled.Name, "false"),
(CVars.NetBufferSize.Name, "0")
};
diff --git a/Content.IntegrationTests/Tests/Administration/Logs/AddTests.cs b/Content.IntegrationTests/Tests/Administration/Logs/AddTests.cs
index 98c7363a6c4..772af337a1a 100644
--- a/Content.IntegrationTests/Tests/Administration/Logs/AddTests.cs
+++ b/Content.IntegrationTests/Tests/Administration/Logs/AddTests.cs
@@ -32,8 +32,8 @@ public async Task AddAndGetSingleLog()
var guid = Guid.NewGuid();
- var testMap = await pair.CreateTestMap();
- var coordinates = testMap.GridCoords;
+ await pair.CreateTestMap();
+ var coordinates = pair.TestMap.GridCoords;
await server.WaitPost(() =>
{
var entity = sEntities.SpawnEntity(null, coordinates);
diff --git a/Content.IntegrationTests/Tests/DeviceNetwork/DeviceNetworkTest.cs b/Content.IntegrationTests/Tests/DeviceNetwork/DeviceNetworkTest.cs
index 26ea726211b..b37f7cfa468 100644
--- a/Content.IntegrationTests/Tests/DeviceNetwork/DeviceNetworkTest.cs
+++ b/Content.IntegrationTests/Tests/DeviceNetwork/DeviceNetworkTest.cs
@@ -212,7 +212,7 @@ public async Task WiredNetworkDeviceSendAndReceive()
DeviceNetworkComponent networkComponent1 = null;
DeviceNetworkComponent networkComponent2 = null;
WiredNetworkComponent wiredNetworkComponent = null;
- var grid = testMap.MapGrid;
+ var grid = testMap.Grid.Comp;
var testValue = "test";
var payload = new NetworkPayload
diff --git a/Content.IntegrationTests/Tests/DoAfter/DoAfterCancellationTests.cs b/Content.IntegrationTests/Tests/DoAfter/DoAfterCancellationTests.cs
index d47eb13273f..0ebd17d8879 100644
--- a/Content.IntegrationTests/Tests/DoAfter/DoAfterCancellationTests.cs
+++ b/Content.IntegrationTests/Tests/DoAfter/DoAfterCancellationTests.cs
@@ -3,8 +3,6 @@
using Content.IntegrationTests.Tests.Interaction;
using Content.IntegrationTests.Tests.Weldable;
using Content.Shared.Tools.Components;
-using Content.Server.Tools.Components;
-using Content.Shared.DoAfter;
namespace Content.IntegrationTests.Tests.DoAfter;
diff --git a/Content.IntegrationTests/Tests/EntityTest.cs b/Content.IntegrationTests/Tests/EntityTest.cs
index 152eb725221..d3b1fb47221 100644
--- a/Content.IntegrationTests/Tests/EntityTest.cs
+++ b/Content.IntegrationTests/Tests/EntityTest.cs
@@ -354,41 +354,18 @@ public async Task AllComponentsOneToOneDeleteTest()
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
-
- var mapManager = server.ResolveDependency();
var entityManager = server.ResolveDependency();
var componentFactory = server.ResolveDependency();
- var tileDefinitionManager = server.ResolveDependency();
- var mapSystem = entityManager.System();
var logmill = server.ResolveDependency().GetSawmill("EntityTest");
- Entity grid = default!;
-
- await server.WaitPost(() =>
- {
- // Create a one tile grid to stave off the grid 0 monsters
- var mapId = mapManager.CreateMap();
-
- mapManager.AddUninitializedMap(mapId);
-
- grid = mapManager.CreateGridEntity(mapId);
-
- var tileDefinition = tileDefinitionManager["Plating"];
- var tile = new Tile(tileDefinition.TileId);
- var coordinates = new EntityCoordinates(grid.Owner, Vector2.Zero);
-
- mapSystem.SetTile(grid.Owner, grid.Comp!, coordinates, tile);
-
- mapManager.DoMapInitialize(mapId);
- });
-
+ await pair.CreateTestMap();
await server.WaitRunTicks(5);
+ var testLocation = pair.TestMap.GridCoords;
await server.WaitAssertion(() =>
{
Assert.Multiple(() =>
{
- var testLocation = new EntityCoordinates(grid.Owner, Vector2.Zero);
foreach (var type in componentFactory.AllRegisteredTypes)
{
diff --git a/Content.IntegrationTests/Tests/Fluids/PuddleTest.cs b/Content.IntegrationTests/Tests/Fluids/PuddleTest.cs
index 611af673809..a9069892dff 100644
--- a/Content.IntegrationTests/Tests/Fluids/PuddleTest.cs
+++ b/Content.IntegrationTests/Tests/Fluids/PuddleTest.cs
@@ -46,17 +46,14 @@ public async Task SpaceNoPuddleTest()
var server = pair.Server;
var testMap = await pair.CreateTestMap();
+ var grid = testMap.Grid.Comp;
var entitySystemManager = server.ResolveDependency();
var spillSystem = entitySystemManager.GetEntitySystem();
- MapGridComponent grid = null;
-
// Remove all tiles
await server.WaitPost(() =>
{
- grid = testMap.MapGrid;
-
foreach (var tile in grid.GetAllTiles())
{
grid.SetTile(tile.GridIndices, Tile.Empty);
diff --git a/Content.IntegrationTests/Tests/GameRules/FailAndStartPresetTest.cs b/Content.IntegrationTests/Tests/GameRules/FailAndStartPresetTest.cs
new file mode 100644
index 00000000000..1fed226beee
--- /dev/null
+++ b/Content.IntegrationTests/Tests/GameRules/FailAndStartPresetTest.cs
@@ -0,0 +1,152 @@
+#nullable enable
+using Content.Server.GameTicking;
+using Content.Server.GameTicking.Components;
+using Content.Server.GameTicking.Presets;
+using Content.Shared.CCVar;
+using Content.Shared.GameTicking;
+using Robust.Shared.GameObjects;
+
+namespace Content.IntegrationTests.Tests.GameRules;
+
+[TestFixture]
+public sealed class FailAndStartPresetTest
+{
+ [TestPrototypes]
+ private const string Prototypes = @"
+- type: gamePreset
+ id: TestPreset
+ alias:
+ - nukeops
+ name: Test Preset
+ description: """"
+ showInVote: false
+ rules:
+ - TestRule
+
+- type: gamePreset
+ id: TestPresetTenPlayers
+ alias:
+ - nukeops
+ name: Test Preset 10 players
+ description: """"
+ showInVote: false
+ rules:
+ - TestRuleTenPlayers
+
+- type: entity
+ id: TestRule
+ parent: BaseGameRule
+ noSpawn: true
+ components:
+ - type: GameRule
+ minPlayers: 0
+ - type: TestRule
+
+- type: entity
+ id: TestRuleTenPlayers
+ parent: BaseGameRule
+ noSpawn: true
+ components:
+ - type: GameRule
+ minPlayers: 10
+ - type: TestRule
+";
+
+ ///
+ /// Test that a nuke ops gamemode can start after failing to start once.
+ ///
+ [Test]
+ public async Task FailAndStartTest()
+ {
+ await using var pair = await PoolManager.GetServerClient(new PoolSettings
+ {
+ Dirty = true,
+ DummyTicker = false,
+ Connected = true,
+ InLobby = true
+ });
+
+ var server = pair.Server;
+ var client = pair.Client;
+ var entMan = server.EntMan;
+ var ticker = server.System();
+ server.System().Run = true;
+
+ Assert.That(server.CfgMan.GetCVar(CCVars.GridFill), Is.False);
+ Assert.That(server.CfgMan.GetCVar(CCVars.GameLobbyFallbackEnabled), Is.True);
+ Assert.That(server.CfgMan.GetCVar(CCVars.GameLobbyDefaultPreset), Is.EqualTo("secret"));
+ server.CfgMan.SetCVar(CCVars.GridFill, true);
+ server.CfgMan.SetCVar(CCVars.GameLobbyFallbackEnabled, false);
+ server.CfgMan.SetCVar(CCVars.GameLobbyDefaultPreset, "TestPreset");
+
+ // Initially in the lobby
+ Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby));
+ Assert.That(client.AttachedEntity, Is.Null);
+ Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.NotReadyToPlay));
+
+ // Try to start nukeops without readying up
+ await pair.WaitCommand("setgamepreset TestPresetTenPlayers");
+ await pair.WaitCommand("startround");
+ await pair.RunTicksSync(10);
+
+ // Game should not have started
+ Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby));
+ Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.NotReadyToPlay));
+ Assert.That(!client.EntMan.EntityExists(client.AttachedEntity));
+ var player = pair.Player!.AttachedEntity;
+ Assert.That(!entMan.EntityExists(player));
+
+ // Ready up and start nukeops
+ await pair.WaitClientCommand("toggleready True");
+ Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.ReadyToPlay));
+ await pair.WaitCommand("setgamepreset TestPreset");
+ await pair.WaitCommand("startround");
+ await pair.RunTicksSync(10);
+
+ // Game should have started
+ Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.InRound));
+ Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.JoinedGame));
+ Assert.That(client.EntMan.EntityExists(client.AttachedEntity));
+ player = pair.Player!.AttachedEntity!.Value;
+ Assert.That(entMan.EntityExists(player));
+
+ ticker.SetGamePreset((GamePresetPrototype?)null);
+ server.CfgMan.SetCVar(CCVars.GridFill, false);
+ server.CfgMan.SetCVar(CCVars.GameLobbyFallbackEnabled, true);
+ server.CfgMan.SetCVar(CCVars.GameLobbyDefaultPreset, "secret");
+ server.System().Run = false;
+ await pair.CleanReturnAsync();
+ }
+}
+
+public sealed class TestRuleSystem : EntitySystem
+{
+ public bool Run;
+
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnRoundStartAttempt);
+ }
+
+ private void OnRoundStartAttempt(RoundStartAttemptEvent args)
+ {
+ if (!Run)
+ return;
+
+ if (args.Forced || args.Cancelled)
+ return;
+
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out _, out _, out var gameRule))
+ {
+ var minPlayers = gameRule.MinPlayers;
+ if (args.Players.Length >= minPlayers)
+ continue;
+
+ args.Cancel();
+ }
+ }
+}
+
+[RegisterComponent]
+public sealed partial class TestRuleComponent : Component;
diff --git a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs
new file mode 100644
index 00000000000..5bada98a3aa
--- /dev/null
+++ b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs
@@ -0,0 +1,203 @@
+#nullable enable
+using System.Linq;
+using Content.Server.Body.Components;
+using Content.Server.GameTicking;
+using Content.Server.GameTicking.Presets;
+using Content.Server.GameTicking.Rules.Components;
+using Content.Server.Mind;
+using Content.Server.Pinpointer;
+using Content.Server.Roles;
+using Content.Server.Shuttles.Components;
+using Content.Server.Station.Components;
+using Content.Shared.CCVar;
+using Content.Shared.Damage;
+using Content.Shared.FixedPoint;
+using Content.Shared.GameTicking;
+using Content.Shared.Hands.Components;
+using Content.Shared.Inventory;
+using Content.Shared.NPC.Systems;
+using Content.Shared.NukeOps;
+using Robust.Server.GameObjects;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map.Components;
+
+namespace Content.IntegrationTests.Tests.GameRules;
+
+[TestFixture]
+public sealed class NukeOpsTest
+{
+ ///
+ /// Check that a nuke ops game mode can start without issue. I.e., that the nuke station and such all get loaded.
+ ///
+ [Test]
+ public async Task TryStopNukeOpsFromConstantlyFailing()
+ {
+ await using var pair = await PoolManager.GetServerClient(new PoolSettings
+ {
+ Dirty = true,
+ DummyTicker = false,
+ Connected = true,
+ InLobby = true
+ });
+
+ var server = pair.Server;
+ var client = pair.Client;
+ var entMan = server.EntMan;
+ var mapSys = server.System();
+ var ticker = server.System();
+ var mindSys = server.System();
+ var roleSys = server.System();
+ var invSys = server.System();
+ var factionSys = server.System();
+
+ Assert.That(server.CfgMan.GetCVar(CCVars.GridFill), Is.False);
+ server.CfgMan.SetCVar(CCVars.GridFill, true);
+
+ // Initially in the lobby
+ Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby));
+ Assert.That(client.AttachedEntity, Is.Null);
+ Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.NotReadyToPlay));
+
+ // There are no grids or maps
+ Assert.That(entMan.Count(), Is.Zero);
+ Assert.That(entMan.Count(), Is.Zero);
+ Assert.That(entMan.Count(), Is.Zero);
+ Assert.That(entMan.Count(), Is.Zero);
+ Assert.That(entMan.Count(), Is.Zero);
+
+ // And no nukie related components
+ Assert.That(entMan.Count(), Is.Zero);
+ Assert.That(entMan.Count(), Is.Zero);
+ Assert.That(entMan.Count(), Is.Zero);
+ Assert.That(entMan.Count(), Is.Zero);
+ Assert.That(entMan.Count(), Is.Zero);
+
+ // Ready up and start nukeops
+ await pair.WaitClientCommand("toggleready True");
+ Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.ReadyToPlay));
+ await pair.WaitCommand("forcepreset Nukeops");
+ await pair.RunTicksSync(10);
+
+ // Game should have started
+ Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.InRound));
+ Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.JoinedGame));
+ Assert.That(client.EntMan.EntityExists(client.AttachedEntity));
+ var player = pair.Player!.AttachedEntity!.Value;
+ Assert.That(entMan.EntityExists(player));
+
+ // Maps now exist
+ Assert.That(entMan.Count(), Is.GreaterThan(0));
+ Assert.That(entMan.Count(), Is.GreaterThan(0));
+ Assert.That(entMan.Count(), Is.EqualTo(2)); // The main station & nukie station
+ Assert.That(entMan.Count(), Is.GreaterThan(3)); // Each station has at least 1 grid, plus some shuttles
+ Assert.That(entMan.Count(), Is.EqualTo(1));
+
+ // And we now have nukie related components
+ Assert.That(entMan.Count(), Is.EqualTo(1));
+ Assert.That(entMan.Count(), Is.EqualTo(1));
+ Assert.That(entMan.Count(), Is.EqualTo(1));
+ Assert.That(entMan.Count(), Is.EqualTo(1));
+
+ // The player entity should be the nukie commander
+ var mind = mindSys.GetMind(player)!.Value;
+ Assert.That(entMan.HasComponent(player));
+ Assert.That(roleSys.MindIsAntagonist(mind));
+ Assert.That(roleSys.MindHasRole(mind));
+ Assert.That(factionSys.IsMember(player, "Syndicate"), Is.True);
+ Assert.That(factionSys.IsMember(player, "NanoTrasen"), Is.False);
+
+ var roles = roleSys.MindGetAllRoles(mind);
+ var cmdRoles = roles.Where(x => x.Prototype == "NukeopsCommander" && x.Component is NukeopsRoleComponent);
+ Assert.That(cmdRoles.Count(), Is.EqualTo(1));
+
+ // The game rule exists, and all the stations/shuttles/maps are properly initialized
+ var rule = entMan.AllComponents().Single().Component;
+ var mapRule = entMan.AllComponents().Single().Component;
+ foreach (var grid in mapRule.MapGrids)
+ {
+ Assert.That(entMan.EntityExists(grid));
+ Assert.That(entMan.HasComponent(grid));
+ Assert.That(entMan.HasComponent(grid));
+ }
+ Assert.That(entMan.EntityExists(rule.TargetStation));
+
+ Assert.That(entMan.HasComponent(rule.TargetStation));
+
+ var nukieShuttlEnt = entMan.AllComponents().FirstOrDefault().Uid;
+ Assert.That(entMan.EntityExists(nukieShuttlEnt));
+
+ EntityUid? nukieStationEnt = null;
+ foreach (var grid in mapRule.MapGrids)
+ {
+ if (entMan.HasComponent(grid))
+ {
+ nukieStationEnt = grid;
+ break;
+ }
+ }
+
+ Assert.That(entMan.EntityExists(nukieStationEnt));
+ var nukieStation = entMan.GetComponent(nukieStationEnt!.Value);
+
+ Assert.That(entMan.EntityExists(nukieStation.Station));
+ Assert.That(nukieStation.Station, Is.Not.EqualTo(rule.TargetStation));
+
+ Assert.That(server.MapMan.MapExists(mapRule.Map));
+ var nukieMap = mapSys.GetMap(mapRule.Map!.Value);
+
+ var targetStation = entMan.GetComponent(rule.TargetStation!.Value);
+ var targetGrid = targetStation.Grids.First();
+ var targetMap = entMan.GetComponent(targetGrid).MapUid!.Value;
+ Assert.That(targetMap, Is.Not.EqualTo(nukieMap));
+
+ Assert.That(entMan.GetComponent(player).MapUid, Is.EqualTo(nukieMap));
+ Assert.That(entMan.GetComponent(nukieStationEnt.Value).MapUid, Is.EqualTo(nukieMap));
+ Assert.That(entMan.GetComponent(nukieShuttlEnt).MapUid, Is.EqualTo(nukieMap));
+
+ // The maps are all map-initialized, including the player
+ // Yes, this is necessary as this has repeatedly been broken somehow.
+ Assert.That(mapSys.IsInitialized(nukieMap));
+ Assert.That(mapSys.IsInitialized(targetMap));
+ Assert.That(mapSys.IsPaused(nukieMap), Is.False);
+ Assert.That(mapSys.IsPaused(targetMap), Is.False);
+
+ EntityLifeStage LifeStage(EntityUid? uid) => entMan.GetComponent(uid!.Value).EntityLifeStage;
+ Assert.That(LifeStage(player), Is.GreaterThan(EntityLifeStage.Initialized));
+ Assert.That(LifeStage(nukieMap), Is.GreaterThan(EntityLifeStage.Initialized));
+ Assert.That(LifeStage(targetMap), Is.GreaterThan(EntityLifeStage.Initialized));
+ Assert.That(LifeStage(nukieStationEnt.Value), Is.GreaterThan(EntityLifeStage.Initialized));
+ Assert.That(LifeStage(nukieShuttlEnt), Is.GreaterThan(EntityLifeStage.Initialized));
+ Assert.That(LifeStage(rule.TargetStation), Is.GreaterThan(EntityLifeStage.Initialized));
+
+ // Make sure the player has hands. We've had fucking disarmed nukies before.
+ Assert.That(entMan.HasComponent(player));
+ Assert.That(entMan.GetComponent(player).Hands.Count, Is.GreaterThan(0));
+
+ // While we're at it, lets make sure they aren't naked. I don't know how many inventory slots all mobs will be
+ // likely to have in the future. But nukies should probably have at least 3 slots with something in them.
+ var enumerator = invSys.GetSlotEnumerator(player);
+ int total = 0;
+ while (enumerator.NextItem(out _))
+ {
+ total++;
+ }
+ Assert.That(total, Is.GreaterThan(3));
+
+ // Finally lets check the nukie commander passed basic training and figured out how to breathe.
+ var totalSeconds = 30;
+ var totalTicks = (int) Math.Ceiling(totalSeconds / server.Timing.TickPeriod.TotalSeconds);
+ int increment = 5;
+ var resp = entMan.GetComponent(player);
+ var damage = entMan.GetComponent(player);
+ for (var tick = 0; tick < totalTicks; tick += increment)
+ {
+ await pair.RunTicksSync(increment);
+ Assert.That(resp.SuffocationCycles, Is.LessThanOrEqualTo(resp.SuffocationCycleThreshold));
+ Assert.That(damage.TotalDamage, Is.EqualTo(FixedPoint2.Zero));
+ }
+
+ ticker.SetGamePreset((GamePresetPrototype?)null);
+ server.CfgMan.SetCVar(CCVars.GridFill, false);
+ await pair.CleanReturnAsync();
+ }
+}
diff --git a/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs b/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs
index 1e3f9c9854f..20a157e33e8 100644
--- a/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs
+++ b/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs
@@ -1,5 +1,6 @@
using Content.Server.GameTicking;
using Content.Server.GameTicking.Commands;
+using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules;
using Content.Server.GameTicking.Rules.Components;
using Content.Shared.CCVar;
@@ -19,6 +20,9 @@ public async Task RestartTest()
await using var pair = await PoolManager.GetServerClient(new PoolSettings { InLobby = true });
var server = pair.Server;
+ Assert.That(server.EntMan.Count(), Is.Zero);
+ Assert.That(server.EntMan.Count(), Is.Zero);
+
var entityManager = server.ResolveDependency();
var sGameTicker = server.ResolveDependency().GetEntitySystem();
var sGameTiming = server.ResolveDependency();
@@ -26,6 +30,9 @@ public async Task RestartTest()
sGameTicker.StartGameRule("MaxTimeRestart", out var ruleEntity);
Assert.That(entityManager.TryGetComponent(ruleEntity, out var maxTime));
+ Assert.That(server.EntMan.Count(), Is.EqualTo(1));
+ Assert.That(server.EntMan.Count(), Is.EqualTo(1));
+
await server.WaitAssertion(() =>
{
Assert.That(sGameTicker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby));
@@ -33,6 +40,9 @@ await server.WaitAssertion(() =>
sGameTicker.StartRound();
});
+ Assert.That(server.EntMan.Count(), Is.EqualTo(1));
+ Assert.That(server.EntMan.Count(), Is.EqualTo(1));
+
await server.WaitAssertion(() =>
{
Assert.That(sGameTicker.RunLevel, Is.EqualTo(GameRunLevel.InRound));
diff --git a/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs b/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs
index 0f665a63de0..5d7ae8efbf4 100644
--- a/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs
+++ b/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs
@@ -17,6 +17,7 @@ public async Task TestSecretStarts()
var server = pair.Server;
await server.WaitIdleAsync();
+ var entMan = server.ResolveDependency();
var gameTicker = server.ResolveDependency().GetEntitySystem();
await server.WaitAssertion(() =>
@@ -32,10 +33,7 @@ await server.WaitAssertion(() =>
await server.WaitAssertion(() =>
{
- foreach (var rule in gameTicker.GetAddedGameRules())
- {
- Assert.That(gameTicker.GetActiveGameRules(), Does.Contain(rule));
- }
+ Assert.That(gameTicker.GetAddedGameRules().Count(), Is.GreaterThan(1), $"No additional rules started by secret rule.");
// End all rules
gameTicker.ClearGameRules();
diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs
index 88448e7b800..95cf8a06dfa 100644
--- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs
+++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs
@@ -767,14 +767,9 @@ protected async Task RunTicks(int ticks)
await Pair.RunTicksSync(ticks);
}
- protected int SecondsToTicks(float seconds)
- {
- return (int) Math.Ceiling(seconds / TickPeriod);
- }
-
protected async Task RunSeconds(float seconds)
{
- await RunTicks(SecondsToTicks(seconds));
+ await Pair.RunSeconds(seconds);
}
#endregion
@@ -825,7 +820,7 @@ protected bool TryGetBui(Enum key, [NotNullWhen(true)] out BoundUserInterface? b
return false;
}
- if (!ui.OpenInterfaces.TryGetValue(key, out bui))
+ if (!ui.ClientOpenInterfaces.TryGetValue(key, out bui))
{
if (shouldSucceed)
Assert.Fail($"Entity {SEntMan.ToPrettyString(SEntMan.GetEntity(target.Value))} does not have an open bui with key {key.GetType()}.{key}.");
@@ -989,7 +984,7 @@ protected void ToggleNeedPower(NetEntity? target = null)
///
protected async Task AddGravity(EntityUid? uid = null)
{
- var target = uid ?? MapData.GridUid;
+ var target = uid ?? MapData.Grid;
await Server.WaitPost(() =>
{
var gravity = SEntMan.EnsureComponent(target);
diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
index bed27ba6efe..42f64b344cd 100644
--- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
+++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
@@ -12,7 +12,6 @@
using Content.Shared.DoAfter;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
-using Content.Server.Item;
using Content.Shared.Mind;
using Content.Shared.Players;
using Robust.Client.Input;
@@ -184,7 +183,7 @@ public virtual async Task Setup()
await Pair.CreateTestMap();
PlayerCoords = SEntMan.GetNetCoordinates(MapData.GridCoords.Offset(new Vector2(0.5f, 0.5f)).WithEntityId(MapData.MapUid, Transform, SEntMan));
TargetCoords = SEntMan.GetNetCoordinates(MapData.GridCoords.Offset(new Vector2(1.5f, 0.5f)).WithEntityId(MapData.MapUid, Transform, SEntMan));
- await SetTile(Plating, grid: MapData.MapGrid);
+ await SetTile(Plating, grid: MapData.Grid.Comp);
// Get player data
var sPlayerMan = Server.ResolveDependency();
diff --git a/Content.IntegrationTests/Tests/Interaction/MovementTest.cs b/Content.IntegrationTests/Tests/Interaction/MovementTest.cs
index 553b031c2b7..dc5aec92cfc 100644
--- a/Content.IntegrationTests/Tests/Interaction/MovementTest.cs
+++ b/Content.IntegrationTests/Tests/Interaction/MovementTest.cs
@@ -31,7 +31,7 @@ public override async Task Setup()
for (var i = -Tiles; i <= Tiles; i++)
{
- await SetTile(Plating, SEntMan.GetNetCoordinates(pCoords.Offset(new Vector2(i, 0))), MapData.MapGrid);
+ await SetTile(Plating, SEntMan.GetNetCoordinates(pCoords.Offset(new Vector2(i, 0))), MapData.Grid.Comp);
}
AssertGridCount(1);
diff --git a/Content.IntegrationTests/Tests/Linter/StaticFieldValidationTest.cs b/Content.IntegrationTests/Tests/Linter/StaticFieldValidationTest.cs
new file mode 100644
index 00000000000..30724b50a6d
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Linter/StaticFieldValidationTest.cs
@@ -0,0 +1,150 @@
+using System.Collections.Generic;
+using System.Linq;
+using Content.Shared.Tag;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Reflection;
+using Robust.Shared.Serialization.Manager.Attributes;
+
+namespace Content.IntegrationTests.Tests.Linter;
+
+///
+/// Verify that the yaml linter successfully validates static fields
+///
+[TestFixture]
+public sealed class StaticFieldValidationTest
+{
+ [Test]
+ public async Task TestStaticFieldValidation()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var protoMan = pair.Server.ProtoMan;
+
+ var protos = new Dictionary>();
+ foreach (var kind in protoMan.EnumeratePrototypeKinds())
+ {
+ var ids = protoMan.EnumeratePrototypes(kind).Select(x => x.ID).ToHashSet();
+ protos.Add(kind, ids);
+ }
+
+ Assert.That(protoMan.ValidateStaticFields(typeof(StringValid), protos).Count, Is.Zero);
+ Assert.That(protoMan.ValidateStaticFields(typeof(StringArrayValid), protos).Count, Is.Zero);
+ Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdValid), protos).Count, Is.Zero);
+ Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdArrayValid), protos).Count, Is.Zero);
+ Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdTestValid), protos).Count, Is.Zero);
+ Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdArrayValid), protos).Count, Is.Zero);
+ Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdListValid), protos).Count, Is.Zero);
+ Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdSetValid), protos).Count, Is.Zero);
+ Assert.That(protoMan.ValidateStaticFields(typeof(PrivateProtoIdArrayValid), protos).Count, Is.Zero);
+
+ Assert.That(protoMan.ValidateStaticFields(typeof(StringInvalid), protos).Count, Is.EqualTo(1));
+ Assert.That(protoMan.ValidateStaticFields(typeof(StringArrayInvalid), protos).Count, Is.EqualTo(2));
+ Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdInvalid), protos).Count, Is.EqualTo(1));
+ Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdArrayInvalid), protos).Count, Is.EqualTo(2));
+ Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdTestInvalid), protos).Count, Is.EqualTo(1));
+ Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdArrayInvalid), protos).Count, Is.EqualTo(2));
+ Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdListInvalid), protos).Count, Is.EqualTo(2));
+ Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdSetInvalid), protos).Count, Is.EqualTo(2));
+ Assert.That(protoMan.ValidateStaticFields(typeof(PrivateProtoIdArrayInvalid), protos).Count, Is.EqualTo(2));
+
+ await pair.CleanReturnAsync();
+ }
+
+ [TestPrototypes]
+ private const string TestPrototypes = @"
+- type: entity
+ id: StaticFieldTestEnt
+
+- type: Tag
+ id: StaticFieldTestTag
+";
+
+ [Reflect(false)] private sealed class StringValid
+ {
+ [ValidatePrototypeId] public static string Tag = "StaticFieldTestTag";
+ }
+
+ [Reflect(false)] private sealed class StringInvalid
+ {
+ [ValidatePrototypeId] public static string Tag = string.Empty;
+ }
+
+ [Reflect(false)] private sealed class StringArrayValid
+ {
+ [ValidatePrototypeId] public static string[] Tag = {"StaticFieldTestTag", "StaticFieldTestTag"};
+ }
+
+ [Reflect(false)] private sealed class StringArrayInvalid
+ {
+ [ValidatePrototypeId] public static string[] Tag = {string.Empty, "StaticFieldTestTag", string.Empty};
+ }
+
+ [Reflect(false)] private sealed class EntProtoIdValid
+ {
+ public static EntProtoId Tag = "StaticFieldTestEnt";
+ }
+
+ [Reflect(false)] private sealed class EntProtoIdInvalid
+ {
+ public static EntProtoId Tag = string.Empty;
+ }
+
+ [Reflect(false)] private sealed class EntProtoIdArrayValid
+ {
+ public static EntProtoId[] Tag = {"StaticFieldTestEnt", "StaticFieldTestEnt"};
+ }
+
+ [Reflect(false)] private sealed class EntProtoIdArrayInvalid
+ {
+ public static EntProtoId[] Tag = {string.Empty, "StaticFieldTestEnt", string.Empty};
+ }
+
+ [Reflect(false)] private sealed class ProtoIdTestValid
+ {
+ public static ProtoId Tag = "StaticFieldTestTag";
+ }
+
+ [Reflect(false)] private sealed class ProtoIdTestInvalid
+ {
+ public static ProtoId Tag = string.Empty;
+ }
+
+ [Reflect(false)] private sealed class ProtoIdArrayValid
+ {
+ public static ProtoId[] Tag = {"StaticFieldTestTag", "StaticFieldTestTag"};
+ }
+
+ [Reflect(false)] private sealed class ProtoIdArrayInvalid
+ {
+ public static ProtoId[] Tag = {string.Empty, "StaticFieldTestTag", string.Empty};
+ }
+
+ [Reflect(false)] private sealed class ProtoIdListValid
+ {
+ public static List> Tag = new() {"StaticFieldTestTag", "StaticFieldTestTag"};
+ }
+
+ [Reflect(false)] private sealed class ProtoIdListInvalid
+ {
+ public static List> Tag = new() {string.Empty, "StaticFieldTestTag", string.Empty};
+ }
+
+ [Reflect(false)] private sealed class ProtoIdSetValid
+ {
+ public static HashSet> Tag = new() {"StaticFieldTestTag", "StaticFieldTestTag"};
+ }
+
+ [Reflect(false)] private sealed class ProtoIdSetInvalid
+ {
+ public static HashSet> Tag = new() {string.Empty, "StaticFieldTestTag", string.Empty, " "};
+ }
+
+ [Reflect(false)] private sealed class PrivateProtoIdArrayValid
+ {
+ private static ProtoId[] Tag = {"StaticFieldTestTag", "StaticFieldTestTag"};
+ }
+
+ [Reflect(false)] private sealed class PrivateProtoIdArrayInvalid
+ {
+ private static ProtoId[] Tag = {string.Empty, "StaticFieldTestTag", string.Empty};
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Mapping/MappingTests.cs b/Content.IntegrationTests/Tests/Mapping/MappingTests.cs
new file mode 100644
index 00000000000..287e30eb8b1
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Mapping/MappingTests.cs
@@ -0,0 +1,102 @@
+using Robust.Server.GameObjects;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+
+namespace Content.IntegrationTests.Tests.Mapping;
+
+[TestFixture]
+public sealed class MappingTests
+{
+ ///
+ /// Checks that the mapping command creates paused & uninitialized maps.
+ ///
+ [Test]
+ public async Task MappingTest()
+ {
+ await using var pair = await PoolManager.GetServerClient(new PoolSettings {Dirty = true, Connected = true, DummyTicker = false});
+
+ var server = pair.Server;
+ var entMan = server.EntMan;
+ var mapSys = server.System();
+
+ await pair.RunTicksSync(5);
+ var mapId = 1;
+ while (mapSys.MapExists(new(mapId)))
+ {
+ mapId++;
+ }
+
+ await pair.WaitClientCommand($"mapping {mapId}");
+ var map = mapSys.GetMap(new MapId(mapId));
+
+ var mapXform = server.Transform(map);
+ Assert.That(mapXform.MapUid, Is.EqualTo(map));
+ Assert.That(mapXform.MapID, Is.EqualTo(new MapId(mapId)));
+
+ var xform = server.Transform(pair.Player!.AttachedEntity!.Value);
+
+ Assert.That(xform.MapUid, Is.EqualTo(map));
+ Assert.That(mapSys.IsInitialized(map), Is.False);
+ Assert.That(mapSys.IsPaused(map), Is.True);
+ Assert.That(server.MetaData(map).EntityLifeStage, Is.EqualTo(EntityLifeStage.Initialized));
+ Assert.That(server.MetaData(map).EntityPaused, Is.True);
+
+ // Spawn a new entity
+ EntityUid ent = default;
+ await server.WaitPost(() =>
+ {
+ ent = entMan.Spawn(null, new MapCoordinates(default, new(mapId)));
+ });
+ await pair.RunTicksSync(5);
+ Assert.That(server.MetaData(ent).EntityLifeStage, Is.EqualTo(EntityLifeStage.Initialized));
+ Assert.That(server.MetaData(ent).EntityPaused, Is.True);
+
+ // Save the map
+ var file = $"{nameof(MappingTest)}.yml";
+ await pair.WaitClientCommand($"savemap {mapId} {file}");
+
+ // Mapinitialize it
+ await pair.WaitClientCommand($"mapinit {mapId}");
+ Assert.That(mapSys.IsInitialized(map), Is.True);
+ Assert.That(mapSys.IsPaused(map), Is.False);
+ Assert.That(server.MetaData(map).EntityLifeStage, Is.EqualTo(EntityLifeStage.MapInitialized));
+ Assert.That(server.MetaData(map).EntityPaused, Is.False);
+ Assert.That(server.MetaData(ent).EntityLifeStage, Is.EqualTo(EntityLifeStage.MapInitialized));
+ Assert.That(server.MetaData(ent).EntityPaused, Is.False);
+
+ await server.WaitPost(() => entMan.DeleteEntity(map));
+
+ // Load the saved map
+ mapId++;
+ while (mapSys.MapExists(new(mapId)))
+ {
+ mapId++;
+ }
+
+ await pair.WaitClientCommand($"mapping {mapId} {file}");
+ map = mapSys.GetMap(new MapId(mapId));
+
+ // And it should all be paused and un-initialized
+ xform = server.Transform(pair.Player!.AttachedEntity!.Value);
+ Assert.That(xform.MapUid, Is.EqualTo(map));
+ Assert.That(mapSys.IsInitialized(map), Is.False);
+ Assert.That(mapSys.IsPaused(map), Is.True);
+ Assert.That(server.MetaData(map).EntityLifeStage, Is.EqualTo(EntityLifeStage.Initialized));
+ Assert.That(server.MetaData(map).EntityPaused, Is.True);
+
+ mapXform = server.Transform(map);
+ Assert.That(mapXform.MapUid, Is.EqualTo(map));
+ Assert.That(mapXform.MapID, Is.EqualTo(new MapId(mapId)));
+ Assert.That(mapXform.ChildCount, Is.EqualTo(2));
+
+ mapXform.ChildEnumerator.MoveNext(out ent);
+ if (ent == pair.Player.AttachedEntity)
+ mapXform.ChildEnumerator.MoveNext(out ent);
+
+ Assert.That(server.MetaData(ent).EntityLifeStage, Is.EqualTo(EntityLifeStage.Initialized));
+ Assert.That(server.MetaData(ent).EntityPaused, Is.True);
+
+ await server.WaitPost(() => entMan.DeleteEntity(map));
+ await pair.CleanReturnAsync();
+ }
+}
diff --git a/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs b/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs
index a2faef0dd4d..51be2fb4311 100644
--- a/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs
+++ b/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs
@@ -183,7 +183,7 @@ public async Task NoMaterialArbitrage()
var spawnedPrice = await GetSpawnedPrice(spawnedEnts);
var price = await GetPrice(id);
if (spawnedPrice > 0 && price > 0)
- Assert.That(spawnedPrice, Is.LessThanOrEqualTo(price), $"{id} increases in price after being destroyed");
+ Assert.That(spawnedPrice, Is.LessThanOrEqualTo(price), $"{id} increases in price after being destroyed\nEntities spawned on destruction: {string.Join(',', spawnedEnts)}");
// Check lathe production
if (latheRecipes.TryGetValue(id, out var recipe))
@@ -359,7 +359,7 @@ await server.WaitPost(() =>
{
var ent = entManager.SpawnEntity(id, testMap.GridCoords);
stackSys.SetCount(ent, 1);
- priceCache[id] = price = pricing.GetPrice(ent);
+ priceCache[id] = price = pricing.GetPrice(ent, false);
entManager.DeleteEntity(ent);
});
}
diff --git a/Content.IntegrationTests/Tests/Minds/MindTest.DeleteAllThenGhost.cs b/Content.IntegrationTests/Tests/Minds/MindTest.DeleteAllThenGhost.cs
index 980559cc817..7bc62dfe2bc 100644
--- a/Content.IntegrationTests/Tests/Minds/MindTest.DeleteAllThenGhost.cs
+++ b/Content.IntegrationTests/Tests/Minds/MindTest.DeleteAllThenGhost.cs
@@ -19,7 +19,7 @@ public async Task DeleteAllThenGhost()
await using var pair = await PoolManager.GetServerClient(settings);
// Client is connected with a valid entity & mind
- Assert.That(pair.Client.EntMan.EntityExists(pair.Client.Player?.ControlledEntity));
+ Assert.That(pair.Client.EntMan.EntityExists(pair.Client.AttachedEntity));
Assert.That(pair.Server.EntMan.EntityExists(pair.PlayerData?.Mind));
// Delete **everything**
@@ -28,6 +28,12 @@ public async Task DeleteAllThenGhost()
await pair.RunTicksSync(5);
Assert.That(pair.Server.EntMan.EntityCount, Is.EqualTo(0));
+
+ foreach (var ent in pair.Client.EntMan.GetEntities())
+ {
+ Console.WriteLine(pair.Client.EntMan.ToPrettyString(ent));
+ }
+
Assert.That(pair.Client.EntMan.EntityCount, Is.EqualTo(0));
// Create a new map.
@@ -36,7 +42,7 @@ public async Task DeleteAllThenGhost()
await pair.RunTicksSync(5);
// Client is not attached to anything
- Assert.That(pair.Client.Player?.ControlledEntity, Is.Null);
+ Assert.That(pair.Client.AttachedEntity, Is.Null);
Assert.That(pair.PlayerData?.Mind, Is.Null);
// Attempt to ghost
@@ -45,9 +51,9 @@ public async Task DeleteAllThenGhost()
await pair.RunTicksSync(10);
// Client should be attached to a ghost placed on the new map.
- Assert.That(pair.Client.EntMan.EntityExists(pair.Client.Player?.ControlledEntity));
+ Assert.That(pair.Client.EntMan.EntityExists(pair.Client.AttachedEntity));
Assert.That(pair.Server.EntMan.EntityExists(pair.PlayerData?.Mind));
- var xform = pair.Client.Transform(pair.Client.Player!.ControlledEntity!.Value);
+ var xform = pair.Client.Transform(pair.Client.AttachedEntity!.Value);
Assert.That(xform.MapID, Is.EqualTo(new MapId(mapId)));
await pair.CleanReturnAsync();
diff --git a/Content.IntegrationTests/Tests/PostMapInitTest.cs b/Content.IntegrationTests/Tests/PostMapInitTest.cs
index f941a82d6e3..81b5edf6482 100644
--- a/Content.IntegrationTests/Tests/PostMapInitTest.cs
+++ b/Content.IntegrationTests/Tests/PostMapInitTest.cs
@@ -75,7 +75,8 @@ public sealed class PostMapInitTest
"MeteorArena",
"Atlas",
"Reach",
- "Train"
+ "Train",
+ "Oasis"
};
///
@@ -167,7 +168,10 @@ public async Task NoSavedPostMapInitTest()
[Test, TestCaseSource(nameof(GameMaps))]
public async Task GameMapsLoadableTest(string mapProto)
{
- await using var pair = await PoolManager.GetServerClient();
+ await using var pair = await PoolManager.GetServerClient(new PoolSettings
+ {
+ Dirty = true // Stations spawn a bunch of nullspace entities and maps like centcomm.
+ });
var server = pair.Server;
var mapManager = server.ResolveDependency();
diff --git a/Content.IntegrationTests/Tests/Power/PowerTest.cs b/Content.IntegrationTests/Tests/Power/PowerTest.cs
index a6af3e6a65b..a94e94489c0 100644
--- a/Content.IntegrationTests/Tests/Power/PowerTest.cs
+++ b/Content.IntegrationTests/Tests/Power/PowerTest.cs
@@ -143,8 +143,8 @@ public sealed class PowerTest
anchored: true
- type: UserInterface
interfaces:
- - key: enum.ApcUiKey.Key
- type: ApcBoundUserInterface
+ enum.ApcUiKey.Key:
+ type: ApcBoundUserInterface
- type: AccessReader
access: [['Engineering']]
diff --git a/Content.IntegrationTests/Tests/Preferences/LoadoutTests.cs b/Content.IntegrationTests/Tests/Preferences/LoadoutTests.cs
new file mode 100644
index 00000000000..72e35dac057
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Preferences/LoadoutTests.cs
@@ -0,0 +1,44 @@
+using Content.Server.Station.Systems;
+using Content.Shared.Preferences;
+using Content.Shared.Preferences.Loadouts;
+using Content.Shared.Roles.Jobs;
+using Robust.Shared.GameObjects;
+
+namespace Content.IntegrationTests.Tests.Preferences;
+
+[TestFixture]
+[Ignore("HumanoidAppearance crashes upon loading default profiles.")]
+public sealed class LoadoutTests
+{
+ ///
+ /// Checks that an empty loadout still spawns with default gear and not naked.
+ ///
+ [Test]
+ public async Task TestEmptyLoadout()
+ {
+ var pair = await PoolManager.GetServerClient(new PoolSettings()
+ {
+ Dirty = true,
+ });
+ var server = pair.Server;
+
+ var entManager = server.ResolveDependency();
+
+ // Check that an empty role loadout spawns gear
+ var stationSystem = entManager.System();
+ var testMap = await pair.CreateTestMap();
+
+ // That's right I can't even spawn a dummy profile without station spawning / humanoidappearance code crashing.
+ var profile = new HumanoidCharacterProfile();
+
+ profile.SetLoadout(new RoleLoadout("TestRoleLoadout"));
+
+ stationSystem.SpawnPlayerMob(testMap.GridCoords, job: new JobComponent()
+ {
+ // Sue me, there's so much involved in setting up jobs
+ Prototype = "CargoTechnician"
+ }, profile, station: null);
+
+ await pair.CleanReturnAsync();
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Preferences/ServerDbSqliteTests.cs b/Content.IntegrationTests/Tests/Preferences/ServerDbSqliteTests.cs
index b22213dac2b..2dfeebe5dd5 100644
--- a/Content.IntegrationTests/Tests/Preferences/ServerDbSqliteTests.cs
+++ b/Content.IntegrationTests/Tests/Preferences/ServerDbSqliteTests.cs
@@ -4,6 +4,8 @@
using Content.Shared.GameTicking;
using Content.Shared.Humanoid;
using Content.Shared.Preferences;
+using Content.Shared.Preferences.Loadouts;
+using Content.Shared.Preferences.Loadouts.Effects;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Robust.Shared.Configuration;
@@ -59,8 +61,6 @@ private static HumanoidCharacterProfile CharlieCharlieson()
Color.Beige,
new ()
),
- ClothingPreference.Jumpskirt,
- BackpackPreference.Backpack,
SpawnPriorityPreference.None,
new Dictionary
{
@@ -68,7 +68,8 @@ private static HumanoidCharacterProfile CharlieCharlieson()
},
PreferenceUnavailableMode.StayInLobby,
new List (),
- new List()
+ new List(),
+ new Dictionary()
);
}
diff --git a/Content.IntegrationTests/Tests/PrototypeSaveTest.cs b/Content.IntegrationTests/Tests/PrototypeSaveTest.cs
index e4a9c1a840d..9e26fa5eaa2 100644
--- a/Content.IntegrationTests/Tests/PrototypeSaveTest.cs
+++ b/Content.IntegrationTests/Tests/PrototypeSaveTest.cs
@@ -38,31 +38,15 @@ public async Task UninitializedSaveTest()
var mapManager = server.ResolveDependency();
var entityMan = server.ResolveDependency();
var prototypeMan = server.ResolveDependency();
- var tileDefinitionManager = server.ResolveDependency();
var seriMan = server.ResolveDependency();
var compFact = server.ResolveDependency();
var prototypes = new List();
- MapGridComponent grid = default!;
EntityUid uid;
- MapId mapId = default;
- //Build up test environment
- await server.WaitPost(() =>
- {
- // Create a one tile grid to stave off the grid 0 monsters
- mapId = mapManager.CreateMap();
-
- mapManager.AddUninitializedMap(mapId);
-
- grid = mapManager.CreateGrid(mapId);
-
- var tileDefinition = tileDefinitionManager["FloorSteel"]; // Wires n such disable ambiance while under the floor
- var tile = new Tile(tileDefinition.TileId);
- var coordinates = grid.Owner.ToCoordinates();
-
- grid.SetTile(coordinates, tile);
- });
+ await pair.CreateTestMap(false, "FloorSteel"); // Wires n such disable ambiance while under the floor
+ var mapId = pair.TestMap.MapId;
+ var grid = pair.TestMap.Grid;
await server.WaitRunTicks(5);
diff --git a/Content.IntegrationTests/Tests/Shuttle/DockTest.cs b/Content.IntegrationTests/Tests/Shuttle/DockTest.cs
index b6fc273570a..a1aa462a697 100644
--- a/Content.IntegrationTests/Tests/Shuttle/DockTest.cs
+++ b/Content.IntegrationTests/Tests/Shuttle/DockTest.cs
@@ -39,7 +39,7 @@ public async Task TestDockingConfig(Vector2 dock1Pos, Vector2 dock2Pos, Angle do
await server.WaitAssertion(() =>
{
- entManager.DeleteEntity(map.GridUid);
+ entManager.DeleteEntity(map.Grid);
var grid1 = mapManager.CreateGridEntity(mapId);
var grid2 = mapManager.CreateGridEntity(mapId);
var grid1Ent = grid1.Owner;
@@ -104,7 +104,7 @@ public async Task TestPlanetDock()
// Spawn shuttle and affirm no valid docks.
await server.WaitAssertion(() =>
{
- entManager.DeleteEntity(map.GridUid);
+ entManager.DeleteEntity(map.Grid);
Assert.That(entManager.System().TryLoad(otherMap.MapId, "/Maps/Shuttles/emergency.yml", out var rootUids));
shuttle = rootUids[0];
diff --git a/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs b/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs
new file mode 100644
index 00000000000..532e481ac29
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs
@@ -0,0 +1,126 @@
+using System.Linq;
+using Content.Server.GameTicking;
+using Content.Server.Shuttles.Components;
+using Content.Server.Shuttles.Systems;
+using Content.Server.Station.Components;
+using Content.Shared.CCVar;
+using Content.Shared.Shuttles.Components;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map.Components;
+
+namespace Content.IntegrationTests.Tests.Station;
+
+[TestFixture]
+[TestOf(typeof(EmergencyShuttleSystem))]
+public sealed class EvacShuttleTest
+{
+ ///
+ /// Ensure that the emergency shuttle can be called, and that it will travel to centcomm
+ ///
+ [Test]
+ public async Task EmergencyEvacTest()
+ {
+ await using var pair = await PoolManager.GetServerClient(new PoolSettings { DummyTicker = true, Dirty = true });
+ var server = pair.Server;
+ var entMan = server.EntMan;
+ var ticker = server.System();
+
+ // Dummy ticker tests should not have centcomm
+ Assert.That(entMan.Count(), Is.Zero);
+
+ Assert.That(pair.Server.CfgMan.GetCVar(CCVars.GridFill), Is.False);
+ pair.Server.CfgMan.SetCVar(CCVars.EmergencyShuttleEnabled, true);
+ pair.Server.CfgMan.SetCVar(CCVars.GameDummyTicker, false);
+ var gameMap = pair.Server.CfgMan.GetCVar(CCVars.GameMap);
+ pair.Server.CfgMan.SetCVar(CCVars.GameMap, "Saltern");
+
+ await server.WaitPost(() => ticker.RestartRound());
+ await pair.RunTicksSync(25);
+ Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.InRound));
+
+ // Find the station, centcomm, and shuttle, and ftl map.
+
+ Assert.That(entMan.Count(), Is.EqualTo(1));
+ Assert.That(entMan.Count(), Is.EqualTo(1));
+ Assert.That(entMan.Count(), Is.EqualTo(1));
+ Assert.That(entMan.Count(), Is.EqualTo(1));
+ Assert.That(entMan.Count(), Is.EqualTo(0));
+
+ var station = (Entity) entMan.AllComponentsList().Single();
+ var data = entMan.GetComponent(station);
+ var shuttleData = entMan.GetComponent(station);
+
+ var saltern = data.Grids.Single();
+ Assert.That(entMan.HasComponent(saltern));
+
+ var shuttle = shuttleData.EmergencyShuttle!.Value;
+ Assert.That(entMan.HasComponent(shuttle));
+ Assert.That(entMan.HasComponent(shuttle));
+
+ var centcomm = station.Comp.Entity!.Value;
+ Assert.That(entMan.HasComponent(centcomm));
+
+ var centcommMap = station.Comp.MapEntity!.Value;
+ Assert.That(entMan.HasComponent(centcommMap));
+ Assert.That(server.Transform(centcomm).MapUid, Is.EqualTo(centcommMap));
+
+ var salternXform = server.Transform(saltern);
+ Assert.That(salternXform.MapUid, Is.Not.Null);
+ Assert.That(salternXform.MapUid, Is.Not.EqualTo(centcommMap));
+
+ var shuttleXform = server.Transform(shuttle);
+ Assert.That(shuttleXform.MapUid, Is.Not.Null);
+ Assert.That(shuttleXform.MapUid, Is.EqualTo(centcommMap));
+
+ // All of these should have been map-initialized.
+ var mapSys = entMan.System();
+ Assert.That(mapSys.IsInitialized(centcommMap), Is.True);
+ Assert.That(mapSys.IsInitialized(salternXform.MapUid), Is.True);
+ Assert.That(mapSys.IsPaused(centcommMap), Is.False);
+ Assert.That(mapSys.IsPaused(salternXform.MapUid!.Value), Is.False);
+
+ EntityLifeStage LifeStage(EntityUid uid) => entMan.GetComponent(uid).EntityLifeStage;
+ Assert.That(LifeStage(saltern), Is.EqualTo(EntityLifeStage.MapInitialized));
+ Assert.That(LifeStage(shuttle), Is.EqualTo(EntityLifeStage.MapInitialized));
+ Assert.That(LifeStage(centcomm), Is.EqualTo(EntityLifeStage.MapInitialized));
+ Assert.That(LifeStage(centcommMap), Is.EqualTo(EntityLifeStage.MapInitialized));
+ Assert.That(LifeStage(salternXform.MapUid.Value), Is.EqualTo(EntityLifeStage.MapInitialized));
+
+ // Set up shuttle timing
+ var evacSys = server.System();
+ evacSys.TransitTime = ShuttleSystem.DefaultTravelTime; // Absolute minimum transit time, so the test has to run for at least this long
+ // TODO SHUTTLE fix spaghetti
+
+ var dockTime = server.CfgMan.GetCVar(CCVars.EmergencyShuttleDockTime);
+ server.CfgMan.SetCVar(CCVars.EmergencyShuttleDockTime, 2);
+
+ // Call evac shuttle.
+ await pair.WaitCommand("callshuttle 0:02");
+ await pair.RunSeconds(3);
+
+ // Shuttle should have arrived on the station
+ Assert.That(shuttleXform.MapUid, Is.EqualTo(salternXform.MapUid));
+
+ await pair.RunSeconds(2);
+
+ // Shuttle should be FTLing back to centcomm
+ Assert.That(entMan.Count(), Is.EqualTo(1));
+ var ftl = (Entity) entMan.AllComponentsList().Single();
+ Assert.That(entMan.HasComponent(ftl));
+ Assert.That(ftl.Owner, Is.Not.EqualTo(centcommMap));
+ Assert.That(ftl.Owner, Is.Not.EqualTo(salternXform.MapUid));
+ Assert.That(shuttleXform.MapUid, Is.EqualTo(ftl.Owner));
+
+ // Shuttle should have arrived at centcomm
+ await pair.RunSeconds(ShuttleSystem.DefaultTravelTime);
+ Assert.That(shuttleXform.MapUid, Is.EqualTo(centcommMap));
+
+ // Round should be ending now
+ Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PostRound));
+
+ server.CfgMan.SetCVar(CCVars.EmergencyShuttleDockTime, dockTime);
+ pair.Server.CfgMan.SetCVar(CCVars.EmergencyShuttleEnabled, false);
+ pair.Server.CfgMan.SetCVar(CCVars.GameMap, gameMap);
+ await pair.CleanReturnAsync();
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Tiles/TileConstructionTests.cs b/Content.IntegrationTests/Tests/Tiles/TileConstructionTests.cs
index 0a2af88887a..083e817d697 100644
--- a/Content.IntegrationTests/Tests/Tiles/TileConstructionTests.cs
+++ b/Content.IntegrationTests/Tests/Tiles/TileConstructionTests.cs
@@ -37,7 +37,7 @@ public async Task CutThenPlaceLatticeNewGrid()
// Remove grid
await SetTile(null);
await SetTile(null, PlayerCoords);
- Assert.That(MapData.MapGrid.Deleted);
+ Assert.That(MapData.Grid.Comp.Deleted);
AssertGridCount(0);
// Place Lattice
@@ -70,7 +70,7 @@ public async Task FloorConstructDeconstruct()
// Remove grid
await SetTile(null);
await SetTile(null, PlayerCoords);
- Assert.That(MapData.MapGrid.Deleted);
+ Assert.That(MapData.Grid.Comp.Deleted);
AssertGridCount(0);
// Space -> Lattice
diff --git a/Content.Server.Database/Migrations/Postgres/20230319110655_ProfileTraitIndexUnique.Designer.cs b/Content.Server.Database/Migrations/Postgres/20230319110655_ProfileTraitIndexUnique.Designer.cs
index 727891533ee..ed13602d034 100644
--- a/Content.Server.Database/Migrations/Postgres/20230319110655_ProfileTraitIndexUnique.Designer.cs
+++ b/Content.Server.Database/Migrations/Postgres/20230319110655_ProfileTraitIndexUnique.Designer.cs
@@ -664,6 +664,13 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
.HasColumnType("text")
.HasColumnName("species");
+ // Corvax-TTS-Start
+ b.Property("Voice")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("voice");
+ // Corvax-TTS-End
+
b.HasKey("Id")
.HasName("PK_profile");
diff --git a/Content.Server.Database/Migrations/Postgres/20230319112124_ServerBanExemption.Designer.cs b/Content.Server.Database/Migrations/Postgres/20230319112124_ServerBanExemption.Designer.cs
index 672333039f6..9de6e5eff09 100644
--- a/Content.Server.Database/Migrations/Postgres/20230319112124_ServerBanExemption.Designer.cs
+++ b/Content.Server.Database/Migrations/Postgres/20230319112124_ServerBanExemption.Designer.cs
@@ -664,6 +664,13 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
.HasColumnType("text")
.HasColumnName("species");
+ // Corvax-TTS-Start
+ b.Property("Voice")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("voice");
+ // Corvax-TTS-End
+
b.HasKey("Id")
.HasName("PK_profile");
diff --git a/Content.Server.Database/Migrations/Postgres/20230402214647_BanAutoDelete.Designer.cs b/Content.Server.Database/Migrations/Postgres/20230402214647_BanAutoDelete.Designer.cs
index 9786a0c4131..507b2ae410d 100644
--- a/Content.Server.Database/Migrations/Postgres/20230402214647_BanAutoDelete.Designer.cs
+++ b/Content.Server.Database/Migrations/Postgres/20230402214647_BanAutoDelete.Designer.cs
@@ -665,6 +665,13 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
.HasColumnType("text")
.HasColumnName("species");
+ // Corvax-TTS-Start
+ b.Property("Voice")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("voice");
+ // Corvax-TTS-End
+
b.HasKey("Id")
.HasName("PK_profile");
diff --git a/Content.Server.Database/Migrations/Postgres/20230503001749_AdminNotesImprovement.Designer.cs b/Content.Server.Database/Migrations/Postgres/20230503001749_AdminNotesImprovement.Designer.cs
index c020b62da1f..2e122de433a 100644
--- a/Content.Server.Database/Migrations/Postgres/20230503001749_AdminNotesImprovement.Designer.cs
+++ b/Content.Server.Database/Migrations/Postgres/20230503001749_AdminNotesImprovement.Designer.cs
@@ -837,6 +837,13 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
.HasColumnType("text")
.HasColumnName("species");
+ // Corvax-TTS-Start
+ b.Property("Voice")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("voice");
+ // Corvax-TTS-End
+
b.HasKey("Id")
.HasName("PK_profile");
diff --git a/Content.Server.Database/Migrations/Postgres/20230725193102_AdminNotesImprovementsForeignKeys.Designer.cs b/Content.Server.Database/Migrations/Postgres/20230725193102_AdminNotesImprovementsForeignKeys.Designer.cs
index f3ccf61d6d3..80a5ce7e33a 100644
--- a/Content.Server.Database/Migrations/Postgres/20230725193102_AdminNotesImprovementsForeignKeys.Designer.cs
+++ b/Content.Server.Database/Migrations/Postgres/20230725193102_AdminNotesImprovementsForeignKeys.Designer.cs
@@ -837,6 +837,13 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
.HasColumnType("text")
.HasColumnName("species");
+ // Corvax-TTS-Start
+ b.Property("Voice")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("voice");
+ // Corvax-TTS-End
+
b.HasKey("Id")
.HasName("PK_profile");
diff --git a/Content.Server.Database/Migrations/Postgres/20230727190902_AdminLogCompoundKey.Designer.cs b/Content.Server.Database/Migrations/Postgres/20230727190902_AdminLogCompoundKey.Designer.cs
index 0a25569f5d5..1120faa7f43 100644
--- a/Content.Server.Database/Migrations/Postgres/20230727190902_AdminLogCompoundKey.Designer.cs
+++ b/Content.Server.Database/Migrations/Postgres/20230727190902_AdminLogCompoundKey.Designer.cs
@@ -834,6 +834,13 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
.HasColumnType("text")
.HasColumnName("species");
+ // Corvax-TTS-Start
+ b.Property("Voice")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("voice");
+ // Corvax-TTS-End
+
b.HasKey("Id")
.HasName("PK_profile");
diff --git a/Content.Server.Database/Migrations/Postgres/20231021071411_RoundStartDate.Designer.cs b/Content.Server.Database/Migrations/Postgres/20231021071411_RoundStartDate.Designer.cs
index bf3600ba39f..434a34918bb 100644
--- a/Content.Server.Database/Migrations/Postgres/20231021071411_RoundStartDate.Designer.cs
+++ b/Content.Server.Database/Migrations/Postgres/20231021071411_RoundStartDate.Designer.cs
@@ -834,6 +834,13 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
.HasColumnType("text")
.HasColumnName("species");
+ // Corvax-TTS-Start
+ b.Property("Voice")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("voice");
+ // Corvax-TTS-End
+
b.HasKey("Id")
.HasName("PK_profile");
diff --git a/Content.Server.Database/Migrations/Postgres/20231024041204_DropAdminLogEntity.Designer.cs b/Content.Server.Database/Migrations/Postgres/20231024041204_DropAdminLogEntity.Designer.cs
index 5c81f0468db..551bf922b0e 100644
--- a/Content.Server.Database/Migrations/Postgres/20231024041204_DropAdminLogEntity.Designer.cs
+++ b/Content.Server.Database/Migrations/Postgres/20231024041204_DropAdminLogEntity.Designer.cs
@@ -804,6 +804,13 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
.HasColumnType("text")
.HasColumnName("species");
+ // Corvax-TTS-Start
+ b.Property("Voice")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("voice");
+ // Corvax-TTS-End
+
b.HasKey("Id")
.HasName("PK_profile");
diff --git a/Content.Server.Database/Migrations/Postgres/20231126234054_ConnectionLogServer.Designer.cs b/Content.Server.Database/Migrations/Postgres/20231126234054_ConnectionLogServer.Designer.cs
index 71440b79cb8..36de4bb0a74 100644
--- a/Content.Server.Database/Migrations/Postgres/20231126234054_ConnectionLogServer.Designer.cs
+++ b/Content.Server.Database/Migrations/Postgres/20231126234054_ConnectionLogServer.Designer.cs
@@ -813,6 +813,13 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
.HasColumnType("text")
.HasColumnName("species");
+ // Corvax-TTS-Start
+ b.Property("Voice")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("voice");
+ // Corvax-TTS-End
+
b.HasKey("Id")
.HasName("PK_profile");
diff --git a/Content.Server.Database/Migrations/Postgres/20231226154937_AdminLogPk.Designer.cs b/Content.Server.Database/Migrations/Postgres/20231226154937_AdminLogPk.Designer.cs
index 350a00dfaff..fd09365aa9e 100644
--- a/Content.Server.Database/Migrations/Postgres/20231226154937_AdminLogPk.Designer.cs
+++ b/Content.Server.Database/Migrations/Postgres/20231226154937_AdminLogPk.Designer.cs
@@ -811,6 +811,13 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
.HasColumnType("text")
.HasColumnName("species");
+ // Corvax-TTS-Start
+ b.Property("Voice")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("voice");
+ // Corvax-TTS-End
+
b.HasKey("Id")
.HasName("PK_profile");
diff --git a/Content.Server.Database/Migrations/Postgres/20240201091301_SpawnPriorityPreference.Designer.cs b/Content.Server.Database/Migrations/Postgres/20240201091301_SpawnPriorityPreference.Designer.cs
index 0c6c43e180d..8f62c312841 100644
--- a/Content.Server.Database/Migrations/Postgres/20240201091301_SpawnPriorityPreference.Designer.cs
+++ b/Content.Server.Database/Migrations/Postgres/20240201091301_SpawnPriorityPreference.Designer.cs
@@ -816,6 +816,13 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder)
.HasColumnType("text")
.HasColumnName("species");
+ // Corvax-TTS-Start
+ b.Property("Voice")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("voice");
+ // Corvax-TTS-End
+
b.HasKey("Id")
.HasName("PK_profile");
diff --git a/Content.Server.Database/Migrations/Postgres/20240301130641_ClothingRemoval.Designer.cs b/Content.Server.Database/Migrations/Postgres/20240301130641_ClothingRemoval.Designer.cs
new file mode 100644
index 00000000000..0867022d361
--- /dev/null
+++ b/Content.Server.Database/Migrations/Postgres/20240301130641_ClothingRemoval.Designer.cs
@@ -0,0 +1,1845 @@
+//
+using System;
+using System.Net;
+using System.Text.Json;
+using Content.Server.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using NpgsqlTypes;
+
+#nullable disable
+
+namespace Content.Server.Database.Migrations.Postgres
+{
+ [DbContext(typeof(PostgresServerDbContext))]
+ [Migration("20240301130641_ClothingRemoval")]
+ partial class ClothingRemoval
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Content.Server.Database.Admin", b =>
+ {
+ b.Property("UserId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property("AdminRankId")
+ .HasColumnType("integer")
+ .HasColumnName("admin_rank_id");
+
+ b.Property("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.HasKey("UserId")
+ .HasName("PK_admin");
+
+ b.HasIndex("AdminRankId")
+ .HasDatabaseName("IX_admin_admin_rank_id");
+
+ b.ToTable("admin", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_flag_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AdminId")
+ .HasColumnType("uuid")
+ .HasColumnName("admin_id");
+
+ b.Property("Flag")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("flag");
+
+ b.Property("Negative")
+ .HasColumnType("boolean")
+ .HasColumnName("negative");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_flag");
+
+ b.HasIndex("AdminId")
+ .HasDatabaseName("IX_admin_flag_admin_id");
+
+ b.HasIndex("Flag", "AdminId")
+ .IsUnique();
+
+ b.ToTable("admin_flag", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+ {
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("Id")
+ .HasColumnType("integer")
+ .HasColumnName("admin_log_id");
+
+ b.Property("Date")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("date");
+
+ b.Property("Impact")
+ .HasColumnType("smallint")
+ .HasColumnName("impact");
+
+ b.Property("Json")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("json");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("message");
+
+ b.Property("Type")
+ .HasColumnType("integer")
+ .HasColumnName("type");
+
+ b.HasKey("RoundId", "Id")
+ .HasName("PK_admin_log");
+
+ b.HasIndex("Date");
+
+ b.HasIndex("Message")
+ .HasAnnotation("Npgsql:TsVectorConfig", "english");
+
+ NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Message"), "GIN");
+
+ b.HasIndex("Type")
+ .HasDatabaseName("IX_admin_log_type");
+
+ b.ToTable("admin_log", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b =>
+ {
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("LogId")
+ .HasColumnType("integer")
+ .HasColumnName("log_id");
+
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.HasKey("RoundId", "LogId", "PlayerUserId")
+ .HasName("PK_admin_log_player");
+
+ b.HasIndex("PlayerUserId")
+ .HasDatabaseName("IX_admin_log_player_player_user_id");
+
+ b.ToTable("admin_log_player", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminMessage", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_messages_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property("ExpirationTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiration_time");
+
+ b.Property("LastEditedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_edited_at");
+
+ b.Property("LastEditedById")
+ .HasColumnType("uuid")
+ .HasColumnName("last_edited_by_id");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)")
+ .HasColumnName("message");
+
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.Property("PlaytimeAtNote")
+ .HasColumnType("interval")
+ .HasColumnName("playtime_at_note");
+
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("Seen")
+ .HasColumnType("boolean")
+ .HasColumnName("seen");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_messages");
+
+ b.HasIndex("CreatedById");
+
+ b.HasIndex("DeletedById");
+
+ b.HasIndex("LastEditedById");
+
+ b.HasIndex("PlayerUserId")
+ .HasDatabaseName("IX_admin_messages_player_user_id");
+
+ b.HasIndex("RoundId")
+ .HasDatabaseName("IX_admin_messages_round_id");
+
+ b.ToTable("admin_messages", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminNote", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_notes_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property("ExpirationTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiration_time");
+
+ b.Property("LastEditedAt")
+ .IsRequired()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_edited_at");
+
+ b.Property("LastEditedById")
+ .HasColumnType("uuid")
+ .HasColumnName("last_edited_by_id");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)")
+ .HasColumnName("message");
+
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.Property("PlaytimeAtNote")
+ .HasColumnType("interval")
+ .HasColumnName("playtime_at_note");
+
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("Secret")
+ .HasColumnType("boolean")
+ .HasColumnName("secret");
+
+ b.Property("Severity")
+ .HasColumnType("integer")
+ .HasColumnName("severity");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_notes");
+
+ b.HasIndex("CreatedById");
+
+ b.HasIndex("DeletedById");
+
+ b.HasIndex("LastEditedById");
+
+ b.HasIndex("PlayerUserId")
+ .HasDatabaseName("IX_admin_notes_player_user_id");
+
+ b.HasIndex("RoundId")
+ .HasDatabaseName("IX_admin_notes_round_id");
+
+ b.ToTable("admin_notes", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_rank_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_rank");
+
+ b.ToTable("admin_rank", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_rank_flag_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AdminRankId")
+ .HasColumnType("integer")
+ .HasColumnName("admin_rank_id");
+
+ b.Property("Flag")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("flag");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_rank_flag");
+
+ b.HasIndex("AdminRankId");
+
+ b.HasIndex("Flag", "AdminRankId")
+ .IsUnique();
+
+ b.ToTable("admin_rank_flag", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_watchlists_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property("ExpirationTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiration_time");
+
+ b.Property("LastEditedAt")
+ .IsRequired()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_edited_at");
+
+ b.Property("LastEditedById")
+ .HasColumnType("uuid")
+ .HasColumnName("last_edited_by_id");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)")
+ .HasColumnName("message");
+
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.Property("PlaytimeAtNote")
+ .HasColumnType("interval")
+ .HasColumnName("playtime_at_note");
+
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_watchlists");
+
+ b.HasIndex("CreatedById");
+
+ b.HasIndex("DeletedById");
+
+ b.HasIndex("LastEditedById");
+
+ b.HasIndex("PlayerUserId")
+ .HasDatabaseName("IX_admin_watchlists_player_user_id");
+
+ b.HasIndex("RoundId")
+ .HasDatabaseName("IX_admin_watchlists_round_id");
+
+ b.ToTable("admin_watchlists", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Antag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("antag_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AntagName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("antag_name");
+
+ b.Property("ProfileId")
+ .HasColumnType("integer")
+ .HasColumnName("profile_id");
+
+ b.HasKey("Id")
+ .HasName("PK_antag");
+
+ b.HasIndex("ProfileId", "AntagName")
+ .IsUnique();
+
+ b.ToTable("antag", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AssignedUserId", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("assigned_user_id_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property("UserName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("user_name");
+
+ b.HasKey("Id")
+ .HasName("PK_assigned_user_id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.HasIndex("UserName")
+ .IsUnique();
+
+ b.ToTable("assigned_user_id", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("connection_log_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Address")
+ .IsRequired()
+ .HasColumnType("inet")
+ .HasColumnName("address");
+
+ b.Property("Denied")
+ .HasColumnType("smallint")
+ .HasColumnName("denied");
+
+ b.Property("HWId")
+ .HasColumnType("bytea")
+ .HasColumnName("hwid");
+
+ b.Property("ServerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(0)
+ .HasColumnName("server_id");
+
+ b.Property("Time")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("time");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property("UserName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("user_name");
+
+ b.HasKey("Id")
+ .HasName("PK_connection_log");
+
+ b.HasIndex("ServerId")
+ .HasDatabaseName("IX_connection_log_server_id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("connection_log", null, t =>
+ {
+ t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
+ });
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Job", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("job_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("JobName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("job_name");
+
+ b.Property("Priority")
+ .HasColumnType("integer")
+ .HasColumnName("priority");
+
+ b.Property("ProfileId")
+ .HasColumnType("integer")
+ .HasColumnName("profile_id");
+
+ b.HasKey("Id")
+ .HasName("PK_job");
+
+ b.HasIndex("ProfileId");
+
+ b.HasIndex("ProfileId", "JobName")
+ .IsUnique();
+
+ b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority")
+ .IsUnique()
+ .HasFilter("priority = 3");
+
+ b.ToTable("job", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.PlayTime", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("play_time_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("PlayerId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_id");
+
+ b.Property("TimeSpent")
+ .HasColumnType("interval")
+ .HasColumnName("time_spent");
+
+ b.Property("Tracker")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("tracker");
+
+ b.HasKey("Id")
+ .HasName("PK_play_time");
+
+ b.HasIndex("PlayerId", "Tracker")
+ .IsUnique();
+
+ b.ToTable("play_time", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Player", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("player_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property