diff --git a/Content.Benchmarks/DynamicTreeBenchmark.cs b/Content.Benchmarks/DynamicTreeBenchmark.cs
index 4f3dad4073..9086b4d3ac 100644
--- a/Content.Benchmarks/DynamicTreeBenchmark.cs
+++ b/Content.Benchmarks/DynamicTreeBenchmark.cs
@@ -44,7 +44,7 @@ namespace Content.Benchmarks
for (var i = 0; i < Aabbs1.Length; i++)
{
var aabb = Aabbs1[i];
- _b2Tree.CreateProxy(aabb, i);
+ _b2Tree.CreateProxy(aabb, uint.MaxValue, i);
_tree.Add(i);
}
}
diff --git a/Content.Client/Administration/AdminNameOverlay.cs b/Content.Client/Administration/AdminNameOverlay.cs
index 86c9b595f6..3c968ec17c 100644
--- a/Content.Client/Administration/AdminNameOverlay.cs
+++ b/Content.Client/Administration/AdminNameOverlay.cs
@@ -50,6 +50,8 @@ internal sealed class AdminNameOverlay : Overlay
//TODO make this adjustable via GUI
var classic = _config.GetCVar(CCVars.AdminOverlayClassic);
+ var playTime = _config.GetCVar(CCVars.AdminOverlayPlaytime);
+ var startingJob = _config.GetCVar(CCVars.AdminOverlayStartingJob);
foreach (var playerInfo in _system.PlayerList)
{
@@ -76,25 +78,44 @@ internal sealed class AdminNameOverlay : Overlay
}
var uiScale = _userInterfaceManager.RootControl.UIScale;
- var lineoffset = new Vector2(0f, 11f) * uiScale;
+ var lineoffset = new Vector2(0f, 14f) * uiScale;
var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center +
new Angle(-_eyeManager.CurrentEye.Rotation).RotateVec(
aabb.TopRight - aabb.Center)) + new Vector2(1f, 7f);
+ var currentOffset = Vector2.Zero;
+
+ args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.CharacterName, uiScale, playerInfo.Connected ? Color.Aquamarine : Color.White);
+ currentOffset += lineoffset;
+
+ args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.Username, uiScale, playerInfo.Connected ? Color.Yellow : Color.White);
+ currentOffset += lineoffset;
+
+ if (!string.IsNullOrEmpty(playerInfo.PlaytimeString) && playTime)
+ {
+ args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.PlaytimeString, uiScale, playerInfo.Connected ? Color.Orange : Color.White);
+ currentOffset += lineoffset;
+ }
+
+ if (!string.IsNullOrEmpty(playerInfo.StartingJob) && startingJob)
+ {
+ args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, Loc.GetString(playerInfo.StartingJob), uiScale, playerInfo.Connected ? Color.GreenYellow : Color.White);
+ currentOffset += lineoffset;
+ }
+
if (classic && playerInfo.Antag)
{
- args.ScreenHandle.DrawString(_font, screenCoordinates + (lineoffset * 2), _antagLabelClassic, uiScale, _antagColorClassic);
+ args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, _antagLabelClassic, uiScale, Color.OrangeRed);
+ currentOffset += lineoffset;
}
else if (!classic && _filter.Contains(playerInfo.RoleProto))
{
- var label = Loc.GetString(playerInfo.RoleProto.Name).ToUpper();
- var color = playerInfo.RoleProto.Color;
+ var label = Loc.GetString(playerInfo.RoleProto.Name).ToUpper();
+ var color = playerInfo.RoleProto.Color;
- args.ScreenHandle.DrawString(_font, screenCoordinates + (lineoffset * 2), label, uiScale, color);
+ args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, label, uiScale, color);
+ currentOffset += lineoffset;
}
-
- args.ScreenHandle.DrawString(_font, screenCoordinates + lineoffset, playerInfo.Username, uiScale, playerInfo.Connected ? Color.Yellow : Color.White);
- args.ScreenHandle.DrawString(_font, screenCoordinates, playerInfo.CharacterName, uiScale, playerInfo.Connected ? Color.Aquamarine : Color.White);
}
}
}
diff --git a/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml b/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml
index 42d6f4b354..d53101f68e 100644
--- a/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml
+++ b/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml
@@ -2,24 +2,26 @@
xmlns="https://spacestation14.io"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs b/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs
index 3e05018c10..973f1a090b 100644
--- a/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs
+++ b/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs
@@ -36,6 +36,9 @@ namespace Content.Client.Administration.UI.Bwoink
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
+ var newPlayerThreshold = 0;
+ _cfg.OnValueChanged(CCVars.NewPlayerThreshold, (val) => { newPlayerThreshold = val; }, true);
+
var uiController = _ui.GetUIController();
if (uiController.UIHelper is not AdminAHelpUIHandler helper)
return;
@@ -59,9 +62,9 @@ namespace Content.Client.Administration.UI.Bwoink
var sb = new StringBuilder();
if (info.Connected)
- sb.Append('●');
+ sb.Append(info.ActiveThisRound ? '⚫' : '◐');
else
- sb.Append(info.ActiveThisRound ? '○' : '·');
+ sb.Append(info.ActiveThisRound ? '⭘' : '·');
sb.Append(' ');
if (AHelpHelper.TryGetChannel(info.SessionId, out var panel) && panel.Unread > 0)
@@ -73,10 +76,12 @@ namespace Content.Client.Administration.UI.Bwoink
sb.Append(' ');
}
+ // Mark antagonists with symbol
if (info.Antag && info.ActiveThisRound)
sb.Append(new Rune(0x1F5E1)); // 🗡
- if (info.OverallPlaytime <= TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.NewPlayerThreshold)))
+ // Mark new players with symbol
+ if (IsNewPlayer(info))
sb.Append(new Rune(0x23F2)); // ⏲
sb.AppendFormat("\"{0}\"", text);
@@ -84,6 +89,19 @@ namespace Content.Client.Administration.UI.Bwoink
return sb.ToString();
};
+ //
+ // Returns true if the player's overall playtime is under the set threshold
+ //
+ bool IsNewPlayer(PlayerInfo info)
+ {
+ // Don't show every disconnected player as new, don't show 0-minute players as new if threshold is
+ if (newPlayerThreshold <= 0 || info.OverallPlaytime is null && !info.Connected)
+ return false;
+
+ return (info.OverallPlaytime is null
+ || info.OverallPlaytime < TimeSpan.FromMinutes(newPlayerThreshold));
+ }
+
ChannelSelector.Comparison = (a, b) =>
{
var ach = AHelpHelper.EnsurePanel(a.SessionId);
@@ -93,31 +111,37 @@ namespace Content.Client.Administration.UI.Bwoink
if (a.IsPinned != b.IsPinned)
return a.IsPinned ? -1 : 1;
- // First, sort by unread. Any chat with unread messages appears first.
+ // Then, any chat with unread messages.
var aUnread = ach.Unread > 0;
var bUnread = bch.Unread > 0;
if (aUnread != bUnread)
return aUnread ? -1 : 1;
- // Sort by recent messages during the current round.
+ // Then, any chat with recent messages from the current round
var aRecent = a.ActiveThisRound && ach.LastMessage != DateTime.MinValue;
var bRecent = b.ActiveThisRound && bch.LastMessage != DateTime.MinValue;
if (aRecent != bRecent)
return aRecent ? -1 : 1;
- // Next, sort by connection status. Any disconnected players are grouped towards the end.
+ // Sort by connection status. Disconnected players will be last.
if (a.Connected != b.Connected)
return a.Connected ? -1 : 1;
- // Sort connected players by New Player status, then by Antag status
+ // Sort connected players by whether they have joined the round, then by New Player status, then by Antag status
if (a.Connected && b.Connected)
{
- var aNewPlayer = a.OverallPlaytime <= TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.NewPlayerThreshold));
- var bNewPlayer = b.OverallPlaytime <= TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.NewPlayerThreshold));
+ var aNewPlayer = IsNewPlayer(a);
+ var bNewPlayer = IsNewPlayer(b);
+ // Players who have joined the round will be listed before players in the lobby
+ if (a.ActiveThisRound != b.ActiveThisRound)
+ return a.ActiveThisRound ? -1 : 1;
+
+ // Within both the joined group and lobby group, new players will be grouped and listed first
if (aNewPlayer != bNewPlayer)
return aNewPlayer ? -1 : 1;
+ // Within all four previous groups, antagonists will be listed first.
if (a.Antag != b.Antag)
return a.Antag ? -1 : 1;
}
diff --git a/Content.Client/Administration/UI/Bwoink/BwoinkWindow.xaml.cs b/Content.Client/Administration/UI/Bwoink/BwoinkWindow.xaml.cs
index e8653843c7..e6cd4942a6 100644
--- a/Content.Client/Administration/UI/Bwoink/BwoinkWindow.xaml.cs
+++ b/Content.Client/Administration/UI/Bwoink/BwoinkWindow.xaml.cs
@@ -22,12 +22,9 @@ namespace Content.Client.Administration.UI.Bwoink
return;
}
- Title = $"{sel.CharacterName} / {sel.Username}";
+ Title = $"{sel.CharacterName} / {sel.Username} | {Loc.GetString("generic-playtime-title")}: ";
- if (sel.OverallPlaytime != null)
- {
- Title += $" | {Loc.GetString("generic-playtime-title")}: {sel.PlaytimeString}";
- }
+ Title += sel.OverallPlaytime != null ? sel.PlaytimeString : Loc.GetString("generic-unknown-title");
};
OnOpen += () =>
diff --git a/Content.Client/Advertise/Systems/SpeakOnUIClosedSystem.cs b/Content.Client/Advertise/Systems/SpeakOnUIClosedSystem.cs
new file mode 100644
index 0000000000..4e82ec4d00
--- /dev/null
+++ b/Content.Client/Advertise/Systems/SpeakOnUIClosedSystem.cs
@@ -0,0 +1,5 @@
+using Content.Shared.Advertise.Systems;
+
+namespace Content.Client.Advertise.Systems;
+
+public sealed class SpeakOnUIClosedSystem : SharedSpeakOnUIClosedSystem;
diff --git a/Content.Client/Alerts/ClientAlertsSystem.cs b/Content.Client/Alerts/ClientAlertsSystem.cs
index c5ec254c0c..b77bc9e4bc 100644
--- a/Content.Client/Alerts/ClientAlertsSystem.cs
+++ b/Content.Client/Alerts/ClientAlertsSystem.cs
@@ -52,7 +52,7 @@ public sealed class ClientAlertsSystem : AlertsSystem
if (args.Current is not AlertComponentState cast)
return;
- alerts.Comp.Alerts = cast.Alerts;
+ alerts.Comp.Alerts = new(cast.Alerts);
UpdateHud(alerts);
}
diff --git a/Content.Client/CartridgeLoader/Cartridges/LogProbeUi.cs b/Content.Client/CartridgeLoader/Cartridges/LogProbeUi.cs
index d28d3228c9..12b678d5d4 100644
--- a/Content.Client/CartridgeLoader/Cartridges/LogProbeUi.cs
+++ b/Content.Client/CartridgeLoader/Cartridges/LogProbeUi.cs
@@ -1,4 +1,5 @@
using Content.Client.UserInterface.Fragments;
+using Content.Shared.CartridgeLoader;
using Content.Shared.CartridgeLoader.Cartridges;
using Robust.Client.UserInterface;
@@ -13,16 +14,23 @@ public sealed partial class LogProbeUi : UIFragment
return _fragment!;
}
- public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner)
+ public override void Setup(BoundUserInterface ui, EntityUid? fragmentOwner)
{
_fragment = new LogProbeUiFragment();
+
+ _fragment.OnPrintPressed += () =>
+ {
+ var ev = new LogProbePrintMessage();
+ var message = new CartridgeUiMessage(ev);
+ ui.SendMessage(message);
+ };
}
public override void UpdateState(BoundUserInterfaceState state)
{
- if (state is not LogProbeUiState logProbeUiState)
+ if (state is not LogProbeUiState cast)
return;
- _fragment?.UpdateState(logProbeUiState.PulledLogs);
+ _fragment?.UpdateState(cast.EntityName, cast.PulledLogs);
}
}
diff --git a/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml b/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml
index d12fb55cdc..cdbaf7d6ee 100644
--- a/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml
+++ b/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml
@@ -18,4 +18,9 @@
+
+
+
+
+
diff --git a/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml.cs b/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml.cs
index b22e0bc196..d9569bd11d 100644
--- a/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml.cs
+++ b/Content.Client/CartridgeLoader/Cartridges/LogProbeUiFragment.xaml.cs
@@ -8,17 +8,24 @@ namespace Content.Client.CartridgeLoader.Cartridges;
[GenerateTypedNameReferences]
public sealed partial class LogProbeUiFragment : BoxContainer
{
+ ///
+ /// Action invoked when the print button gets pressed.
+ ///
+ public Action? OnPrintPressed;
+
public LogProbeUiFragment()
{
RobustXamlLoader.Load(this);
+
+ PrintButton.OnPressed += _ => OnPrintPressed?.Invoke();
}
- public void UpdateState(List logs)
+ public void UpdateState(string name, List logs)
{
- ProbedDeviceContainer.RemoveAllChildren();
+ EntityName.Text = name;
+ PrintButton.Disabled = string.IsNullOrEmpty(name);
- //Reverse the list so the oldest entries appear at the bottom
- logs.Reverse();
+ ProbedDeviceContainer.RemoveAllChildren();
var count = 1;
foreach (var log in logs)
diff --git a/Content.Client/CartridgeLoader/Cartridges/NanoTaskCartridgeSystem.cs b/Content.Client/CartridgeLoader/Cartridges/NanoTaskCartridgeSystem.cs
new file mode 100644
index 0000000000..85a5659c4b
--- /dev/null
+++ b/Content.Client/CartridgeLoader/Cartridges/NanoTaskCartridgeSystem.cs
@@ -0,0 +1,5 @@
+using Content.Shared.CartridgeLoader.Cartridges;
+
+namespace Content.Client.CartridgeLoader.Cartridges;
+
+public sealed class NanoTaskCartridgeSystem : SharedNanoTaskCartridgeSystem;
diff --git a/Content.Client/CartridgeLoader/Cartridges/NanoTaskItemControl.xaml b/Content.Client/CartridgeLoader/Cartridges/NanoTaskItemControl.xaml
new file mode 100644
index 0000000000..7d7c635de8
--- /dev/null
+++ b/Content.Client/CartridgeLoader/Cartridges/NanoTaskItemControl.xaml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
diff --git a/Content.Client/CartridgeLoader/Cartridges/NanoTaskItemControl.xaml.cs b/Content.Client/CartridgeLoader/Cartridges/NanoTaskItemControl.xaml.cs
new file mode 100644
index 0000000000..5edbee9d44
--- /dev/null
+++ b/Content.Client/CartridgeLoader/Cartridges/NanoTaskItemControl.xaml.cs
@@ -0,0 +1,33 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Maths;
+using Content.Shared.CartridgeLoader.Cartridges;
+
+namespace Content.Client.CartridgeLoader.Cartridges;
+
+///
+/// Represents a single control for a single NanoTask item
+///
+[GenerateTypedNameReferences]
+public sealed partial class NanoTaskItemControl : Control
+{
+ public Action? OnMainPressed;
+ public Action? OnDonePressed;
+
+ public NanoTaskItemControl(NanoTaskItemAndId item)
+ {
+ RobustXamlLoader.Load(this);
+
+ TaskLabel.Text = item.Data.Description;
+ TaskLabel.FontColorOverride = Color.White;
+ TaskForLabel.Text = item.Data.TaskIsFor;
+
+ MainButton.OnPressed += _ => OnMainPressed?.Invoke(item.Id);
+ DoneButton.OnPressed += _ => OnDonePressed?.Invoke(item.Id);
+
+ MainButton.Disabled = item.Data.IsTaskDone;
+ DoneButton.Text = item.Data.IsTaskDone ? Loc.GetString("nano-task-ui-revert-done") : Loc.GetString("nano-task-ui-done");
+ }
+}
diff --git a/Content.Client/CartridgeLoader/Cartridges/NanoTaskItemPopup.xaml b/Content.Client/CartridgeLoader/Cartridges/NanoTaskItemPopup.xaml
new file mode 100644
index 0000000000..ad72df05d9
--- /dev/null
+++ b/Content.Client/CartridgeLoader/Cartridges/NanoTaskItemPopup.xaml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/CartridgeLoader/Cartridges/NanoTaskItemPopup.xaml.cs b/Content.Client/CartridgeLoader/Cartridges/NanoTaskItemPopup.xaml.cs
new file mode 100644
index 0000000000..124b7b7eaa
--- /dev/null
+++ b/Content.Client/CartridgeLoader/Cartridges/NanoTaskItemPopup.xaml.cs
@@ -0,0 +1,109 @@
+using System.Linq;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Client.UserInterface.Controls;
+using Content.Shared.CartridgeLoader.Cartridges;
+
+namespace Content.Client.CartridgeLoader.Cartridges;
+
+///
+/// Popup displayed to edit a NanoTask item
+///
+[GenerateTypedNameReferences]
+public sealed partial class NanoTaskItemPopup : DefaultWindow
+{
+ private readonly ButtonGroup _priorityGroup = new();
+ private int? _editingTaskId = null;
+
+ public Action? TaskSaved;
+ public Action? TaskDeleted;
+ public Action? TaskCreated;
+ public Action? TaskPrinted;
+
+ private NanoTaskItem MakeItem()
+ {
+ return new(
+ description: DescriptionInput.Text,
+ taskIsFor: RequesterInput.Text,
+ isTaskDone: false,
+ priority: _priorityGroup.Pressed switch {
+ var item when item == LowButton => NanoTaskPriority.Low,
+ var item when item == MediumButton => NanoTaskPriority.Medium,
+ var item when item == HighButton => NanoTaskPriority.High,
+ _ => NanoTaskPriority.Medium,
+ }
+ );
+ }
+
+ public NanoTaskItemPopup()
+ {
+ RobustXamlLoader.Load(this);
+
+ LowButton.Group = _priorityGroup;
+ MediumButton.Group = _priorityGroup;
+ HighButton.Group = _priorityGroup;
+
+ CancelButton.OnPressed += _ => Close();
+ DeleteButton.OnPressed += _ =>
+ {
+ if (_editingTaskId is int id)
+ {
+ TaskDeleted?.Invoke(id);
+ }
+ };
+ PrintButton.OnPressed += _ =>
+ {
+ TaskPrinted?.Invoke(MakeItem());
+ };
+ SaveButton.OnPressed += _ =>
+ {
+ if (_editingTaskId is int id)
+ {
+ TaskSaved?.Invoke(id, MakeItem());
+ }
+ else
+ {
+ TaskCreated?.Invoke(MakeItem());
+ }
+ };
+
+ DescriptionInput.OnTextChanged += args =>
+ {
+ if (args.Text.Length > NanoTaskItem.MaximumStringLength)
+ DescriptionInput.Text = args.Text[..NanoTaskItem.MaximumStringLength];
+ };
+ RequesterInput.OnTextChanged += args =>
+ {
+ if (args.Text.Length > NanoTaskItem.MaximumStringLength)
+ RequesterInput.Text = args.Text[..NanoTaskItem.MaximumStringLength];
+ };
+ }
+
+ public void SetEditingTaskId(int? id)
+ {
+ _editingTaskId = id;
+ DeleteButton.Visible = id is not null;
+ }
+
+ public void ResetInputs(NanoTaskItem? item)
+ {
+ if (item is NanoTaskItem task)
+ {
+ var button = task.Priority switch {
+ NanoTaskPriority.High => HighButton,
+ NanoTaskPriority.Medium => MediumButton,
+ NanoTaskPriority.Low => LowButton,
+ };
+ button.Pressed = true;
+ DescriptionInput.Text = task.Description;
+ RequesterInput.Text = task.TaskIsFor;
+ }
+ else
+ {
+ MediumButton.Pressed = true;
+ DescriptionInput.Text = "";
+ RequesterInput.Text = "";
+ }
+ }
+}
diff --git a/Content.Client/CartridgeLoader/Cartridges/NanoTaskUi.cs b/Content.Client/CartridgeLoader/Cartridges/NanoTaskUi.cs
new file mode 100644
index 0000000000..ac08051c89
--- /dev/null
+++ b/Content.Client/CartridgeLoader/Cartridges/NanoTaskUi.cs
@@ -0,0 +1,82 @@
+using System.Linq;
+using Content.Client.UserInterface.Fragments;
+using Content.Shared.CartridgeLoader;
+using Content.Shared.CartridgeLoader.Cartridges;
+using Robust.Client.GameObjects;
+using Robust.Client.UserInterface;
+
+namespace Content.Client.CartridgeLoader.Cartridges;
+
+///
+/// UI fragment responsible for displaying NanoTask controls in a PDA and coordinating with the NanoTaskCartridgeSystem for state
+///
+public sealed partial class NanoTaskUi : UIFragment
+{
+ private NanoTaskUiFragment? _fragment;
+ private NanoTaskItemPopup? _popup;
+
+ public override Control GetUIFragmentRoot()
+ {
+ return _fragment!;
+ }
+
+ public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner)
+ {
+ _fragment = new NanoTaskUiFragment();
+ _popup = new NanoTaskItemPopup();
+ _fragment.NewTask += () =>
+ {
+ _popup.ResetInputs(null);
+ _popup.SetEditingTaskId(null);
+ _popup.OpenCentered();
+ };
+ _fragment.OpenTask += id =>
+ {
+ if (_fragment.Tasks.Find(task => task.Id == id) is not NanoTaskItemAndId task)
+ return;
+
+ _popup.ResetInputs(task.Data);
+ _popup.SetEditingTaskId(task.Id);
+ _popup.OpenCentered();
+ };
+ _fragment.ToggleTaskCompletion += id =>
+ {
+ if (_fragment.Tasks.Find(task => task.Id == id) is not NanoTaskItemAndId task)
+ return;
+
+ userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskUpdateTask(new(id, new(
+ description: task.Data.Description,
+ taskIsFor: task.Data.TaskIsFor,
+ isTaskDone: !task.Data.IsTaskDone,
+ priority: task.Data.Priority
+ ))))));
+ };
+ _popup.TaskSaved += (id, data) =>
+ {
+ userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskUpdateTask(new(id, data)))));
+ _popup.Close();
+ };
+ _popup.TaskDeleted += id =>
+ {
+ userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskDeleteTask(id))));
+ _popup.Close();
+ };
+ _popup.TaskCreated += data =>
+ {
+ userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskAddTask(data))));
+ _popup.Close();
+ };
+ _popup.TaskPrinted += data =>
+ {
+ userInterface.SendMessage(new CartridgeUiMessage(new NanoTaskUiMessageEvent(new NanoTaskPrintTask(data))));
+ };
+ }
+
+ public override void UpdateState(BoundUserInterfaceState state)
+ {
+ if (state is not NanoTaskUiState nanoTaskState)
+ return;
+
+ _fragment?.UpdateState(nanoTaskState.Tasks);
+ }
+}
diff --git a/Content.Client/CartridgeLoader/Cartridges/NanoTaskUiFragment.xaml b/Content.Client/CartridgeLoader/Cartridges/NanoTaskUiFragment.xaml
new file mode 100644
index 0000000000..f3620802f0
--- /dev/null
+++ b/Content.Client/CartridgeLoader/Cartridges/NanoTaskUiFragment.xaml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/CartridgeLoader/Cartridges/NanoTaskUiFragment.xaml.cs b/Content.Client/CartridgeLoader/Cartridges/NanoTaskUiFragment.xaml.cs
new file mode 100644
index 0000000000..38897d6205
--- /dev/null
+++ b/Content.Client/CartridgeLoader/Cartridges/NanoTaskUiFragment.xaml.cs
@@ -0,0 +1,52 @@
+using System.Linq;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Content.Shared.CartridgeLoader.Cartridges;
+
+namespace Content.Client.CartridgeLoader.Cartridges;
+
+///
+/// Class displaying the main UI of NanoTask
+///
+[GenerateTypedNameReferences]
+public sealed partial class NanoTaskUiFragment : BoxContainer
+{
+ public Action? OpenTask;
+ public Action? ToggleTaskCompletion;
+ public Action? NewTask;
+ public List Tasks = new();
+
+ public NanoTaskUiFragment()
+ {
+ RobustXamlLoader.Load(this);
+ Orientation = LayoutOrientation.Vertical;
+ HorizontalExpand = true;
+ VerticalExpand = true;
+ NewTaskButton.OnPressed += _ => NewTask?.Invoke();
+ }
+ public void UpdateState(List tasks)
+ {
+ Tasks = tasks;
+ HighContainer.RemoveAllChildren();
+ MediumContainer.RemoveAllChildren();
+ LowContainer.RemoveAllChildren();
+
+ HighPriority.Text = Loc.GetString("nano-task-ui-heading-high-priority-tasks", ("amount", tasks.Count(task => task.Data.Priority == NanoTaskPriority.High)));
+ MediumPriority.Text = Loc.GetString("nano-task-ui-heading-medium-priority-tasks", ("amount", tasks.Count(task => task.Data.Priority == NanoTaskPriority.Medium)));
+ LowPriority.Text = Loc.GetString("nano-task-ui-heading-low-priority-tasks", ("amount", tasks.Count(task => task.Data.Priority == NanoTaskPriority.Low)));
+
+ foreach (var task in tasks)
+ {
+ var container = task.Data.Priority switch {
+ NanoTaskPriority.High => HighContainer,
+ NanoTaskPriority.Medium => MediumContainer,
+ NanoTaskPriority.Low => LowContainer,
+ };
+ var control = new NanoTaskItemControl(task);
+ container.AddChild(control);
+ control.OnMainPressed += id => OpenTask?.Invoke(id);
+ control.OnDonePressed += id => ToggleTaskCompletion?.Invoke(id);
+ }
+ }
+}
diff --git a/Content.Client/Chat/UI/SpeechBubble.cs b/Content.Client/Chat/UI/SpeechBubble.cs
index aa61e73e31..94505fd892 100644
--- a/Content.Client/Chat/UI/SpeechBubble.cs
+++ b/Content.Client/Chat/UI/SpeechBubble.cs
@@ -217,7 +217,7 @@ namespace Content.Client.Chat.UI
{
StyleClasses = { "speechBox", speechStyleClass },
Children = { label },
- ModulateSelfOverride = Color.White.WithAlpha(0.75f)
+ ModulateSelfOverride = Color.White.WithAlpha(ConfigManager.GetCVar(CCVars.SpeechBubbleBackgroundOpacity))
};
return panel;
@@ -247,21 +247,23 @@ namespace Content.Client.Chat.UI
{
StyleClasses = { "speechBox", speechStyleClass },
Children = { label },
- ModulateSelfOverride = Color.White.WithAlpha(0.75f)
+ ModulateSelfOverride = Color.White.WithAlpha(ConfigManager.GetCVar(CCVars.SpeechBubbleBackgroundOpacity)),
};
return unfanciedPanel;
}
var bubbleHeader = new RichTextLabel
{
- Margin = new Thickness(1, 1, 1, 1)
+ ModulateSelfOverride = Color.White.WithAlpha(ConfigManager.GetCVar(CCVars.SpeechBubbleSpeakerOpacity)),
+ Margin = new Thickness(1, 1, 1, 1),
};
var bubbleContent = new RichTextLabel
{
+ ModulateSelfOverride = Color.White.WithAlpha(ConfigManager.GetCVar(CCVars.SpeechBubbleTextOpacity)),
MaxWidth = SpeechMaxWidth,
Margin = new Thickness(2, 6, 2, 2),
- StyleClasses = { "bubbleContent" }
+ StyleClasses = { "bubbleContent" },
};
//We'll be honest. *Yes* this is hacky. Doing this in a cleaner way would require a bottom-up refactor of how saycode handles sending chat messages. -Myr
@@ -273,7 +275,7 @@ namespace Content.Client.Chat.UI
{
StyleClasses = { "speechBox", speechStyleClass },
Children = { bubbleContent },
- ModulateSelfOverride = Color.White.WithAlpha(0.75f),
+ ModulateSelfOverride = Color.White.WithAlpha(ConfigManager.GetCVar(CCVars.SpeechBubbleBackgroundOpacity)),
HorizontalAlignment = HAlignment.Center,
VerticalAlignment = VAlignment.Bottom,
Margin = new Thickness(4, 14, 4, 2)
@@ -283,7 +285,7 @@ namespace Content.Client.Chat.UI
{
StyleClasses = { "speechBox", speechStyleClass },
Children = { bubbleHeader },
- ModulateSelfOverride = Color.White.WithAlpha(ConfigManager.GetCVar(CCVars.ChatFancyNameBackground) ? 0.75f : 0f),
+ ModulateSelfOverride = Color.White.WithAlpha(ConfigManager.GetCVar(CCVars.ChatFancyNameBackground) ? ConfigManager.GetCVar(CCVars.SpeechBubbleBackgroundOpacity) : 0f),
HorizontalAlignment = HAlignment.Center,
VerticalAlignment = VAlignment.Top
};
diff --git a/Content.Client/Chemistry/UI/ChemMasterBoundUserInterface.cs b/Content.Client/Chemistry/UI/ChemMasterBoundUserInterface.cs
index 3ef7f0ae73..a669a8da4c 100644
--- a/Content.Client/Chemistry/UI/ChemMasterBoundUserInterface.cs
+++ b/Content.Client/Chemistry/UI/ChemMasterBoundUserInterface.cs
@@ -46,6 +46,8 @@ namespace Content.Client.Chemistry.UI
_window.CreateBottleButton.OnPressed += _ => SendMessage(
new ChemMasterOutputToBottleMessage(
(uint) _window.BottleDosage.Value, _window.LabelLine));
+ _window.BufferSortButton.OnPressed += _ => SendMessage(
+ new ChemMasterSortingTypeCycleMessage());
for (uint i = 0; i < _window.PillTypeButtons.Length; i++)
{
diff --git a/Content.Client/Chemistry/UI/ChemMasterWindow.xaml b/Content.Client/Chemistry/UI/ChemMasterWindow.xaml
index b1f4f5917f..aca316f6b3 100644
--- a/Content.Client/Chemistry/UI/ChemMasterWindow.xaml
+++ b/Content.Client/Chemistry/UI/ChemMasterWindow.xaml
@@ -34,6 +34,7 @@
+
diff --git a/Content.Client/Chemistry/UI/ChemMasterWindow.xaml.cs b/Content.Client/Chemistry/UI/ChemMasterWindow.xaml.cs
index 20c61f10cb..807ec4c1e7 100644
--- a/Content.Client/Chemistry/UI/ChemMasterWindow.xaml.cs
+++ b/Content.Client/Chemistry/UI/ChemMasterWindow.xaml.cs
@@ -140,17 +140,17 @@ namespace Content.Client.Chemistry.UI
// Ensure the Panel Info is updated, including UI elements for Buffer Volume, Output Container and so on
UpdatePanelInfo(castState);
-
+
BufferCurrentVolume.Text = $" {castState.BufferCurrentVolume?.Int() ?? 0}u";
-
+
InputEjectButton.Disabled = castState.InputContainerInfo is null;
OutputEjectButton.Disabled = castState.OutputContainerInfo is null;
CreateBottleButton.Disabled = castState.OutputContainerInfo?.Reagents == null;
CreatePillButton.Disabled = castState.OutputContainerInfo?.Entities == null;
-
+
UpdateDosageFields(castState);
}
-
+
//assign default values for pill and bottle fields.
private void UpdateDosageFields(ChemMasterBoundUserInterfaceState castState)
{
@@ -162,8 +162,9 @@ namespace Content.Client.Chemistry.UI
var bufferVolume = castState.BufferCurrentVolume?.Int() ?? 0;
PillDosage.Value = (int)Math.Min(bufferVolume, castState.PillDosageLimit);
-
+
PillTypeButtons[castState.SelectedPillType].Pressed = true;
+
PillNumber.IsValid = x => x >= 0 && x <= pillNumberMax;
PillDosage.IsValid = x => x > 0 && x <= castState.PillDosageLimit;
BottleDosage.IsValid = x => x >= 0 && x <= bottleAmountMax;
@@ -213,6 +214,17 @@ namespace Content.Client.Chemistry.UI
BufferInfo.Children.Clear();
+ // This has to happen here due to people possibly
+ // setting sorting before putting any chemicals
+ BufferSortButton.Text = state.SortingType switch
+ {
+ ChemMasterSortingType.Alphabetical => Loc.GetString("chem-master-window-sort-type-alphabetical"),
+ ChemMasterSortingType.Quantity => Loc.GetString("chem-master-window-sort-type-quantity"),
+ ChemMasterSortingType.Latest => Loc.GetString("chem-master-window-sort-type-latest"),
+ _ => Loc.GetString("chem-master-window-sort-type-none")
+ };
+
+
if (!state.BufferReagents.Any())
{
BufferInfo.Children.Add(new Label { Text = Loc.GetString("chem-master-window-buffer-empty-text") });
@@ -235,19 +247,48 @@ namespace Content.Client.Chemistry.UI
};
bufferHBox.AddChild(bufferVol);
- // initialises rowCount to allow for striped rows
-
- var rowCount = 0;
+ // This sets up the needed data for sorting later in a list
+ // Its done this way to not repeat having to use same code twice (once for sorting
+ // and once for displaying)
+ var reagentList = new List<(ReagentId reagentId, string name, Color color, FixedPoint2 quantity)>();
foreach (var (reagent, quantity) in state.BufferReagents)
{
var reagentId = reagent;
_prototypeManager.TryIndex(reagentId.Prototype, out ReagentPrototype? proto);
var name = proto?.LocalizedName ?? Loc.GetString("chem-master-window-unknown-reagent-text");
var reagentColor = proto?.SubstanceColor ?? default(Color);
- BufferInfo.Children.Add(BuildReagentRow(reagentColor, rowCount++, name, reagentId, quantity, true, true));
+ reagentList.Add(new (reagentId, name, reagentColor, quantity));
+ }
+
+ // We sort here since we need sorted list to be filled first.
+ // You can easily add any new params you need to it.
+ switch (state.SortingType)
+ {
+ case ChemMasterSortingType.Alphabetical:
+ reagentList = reagentList.OrderBy(x => x.name).ToList();
+ break;
+
+ case ChemMasterSortingType.Quantity:
+ reagentList = reagentList.OrderByDescending(x => x.quantity).ToList();
+ break;
+ case ChemMasterSortingType.Latest:
+ reagentList = Enumerable.Reverse(reagentList).ToList();
+ break;
+
+ case ChemMasterSortingType.None:
+ default:
+ // This case is pointless but it is there for readability
+ break;
+ }
+
+ // initialises rowCount to allow for striped rows
+ var rowCount = 0;
+ foreach (var reagent in reagentList)
+ {
+ BufferInfo.Children.Add(BuildReagentRow(reagent.color, rowCount++, reagent.name, reagent.reagentId, reagent.quantity, true, true));
}
}
-
+
private void BuildContainerUI(Control control, ContainerInfo? info, bool addReagentButtons)
{
control.Children.Clear();
@@ -295,7 +336,7 @@ namespace Content.Client.Chemistry.UI
_prototypeManager.TryIndex(reagent.Reagent.Prototype, out ReagentPrototype? proto);
var name = proto?.LocalizedName ?? Loc.GetString("chem-master-window-unknown-reagent-text");
var reagentColor = proto?.SubstanceColor ?? default(Color);
-
+
control.Children.Add(BuildReagentRow(reagentColor, rowCount++, name, reagent.Reagent, reagent.Quantity, false, addReagentButtons));
}
}
@@ -315,7 +356,7 @@ namespace Content.Client.Chemistry.UI
}
//this calls the separated button builder, and stores the return to render after labels
var reagentButtonConstructors = CreateReagentTransferButtons(reagent, isBuffer, addReagentButtons);
-
+
// Create the row layout with the color panel
var rowContainer = new BoxContainer
{
@@ -358,7 +399,7 @@ namespace Content.Client.Chemistry.UI
Children = { rowContainer }
};
}
-
+
public string LabelLine
{
get => LabelLineEdit.Text;
diff --git a/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsSystem.cs b/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsSystem.cs
index 010bfb3184..0ba537e455 100644
--- a/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsSystem.cs
+++ b/Content.Client/Chemistry/Visualizers/SolutionContainerVisualsSystem.cs
@@ -28,7 +28,6 @@ public sealed class SolutionContainerVisualsSystem : VisualizerSystem CommandName;
-
- public override string Help => LocalizationManager.GetString($"cmd-{Command}-help", ("command", Command));
-
- public override void Execute(IConsoleShell shell, string argStr, string[] args)
- {
- _entitySystemManager.GetEntitySystem().ShowAll = true;
-
- var entMan = IoCManager.Resolve();
- var components = entMan.EntityQuery(true);
-
- foreach (var (_, sprite) in components)
- {
- sprite.DrawDepth = (int) DrawDepth.Overlays;
- }
- }
-}
-
internal sealed class NotifyCommand : LocalizedCommands
{
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
diff --git a/Content.Client/Commands/MappingClientSideSetupCommand.cs b/Content.Client/Commands/MappingClientSideSetupCommand.cs
index d17f1fccaf..99a8ba00fe 100644
--- a/Content.Client/Commands/MappingClientSideSetupCommand.cs
+++ b/Content.Client/Commands/MappingClientSideSetupCommand.cs
@@ -24,7 +24,7 @@ internal sealed class MappingClientSideSetupCommand : LocalizedCommands
{
_entitySystemManager.GetEntitySystem().MarkersVisible = true;
_lightManager.Enabled = false;
- shell.ExecuteCommand("showsubfloorforever");
+ shell.ExecuteCommand("showsubfloor");
_entitySystemManager.GetEntitySystem().LoadActionAssignments("/mapping_actions.yml", false);
}
}
diff --git a/Content.Client/CriminalRecords/CriminalRecordsConsoleWindow.xaml.cs b/Content.Client/CriminalRecords/CriminalRecordsConsoleWindow.xaml.cs
index a35cf84c44..85d65020c6 100644
--- a/Content.Client/CriminalRecords/CriminalRecordsConsoleWindow.xaml.cs
+++ b/Content.Client/CriminalRecords/CriminalRecordsConsoleWindow.xaml.cs
@@ -3,6 +3,7 @@ using Content.Shared.Access.Systems;
using Content.Shared.Administration;
using Content.Shared.CriminalRecords;
using Content.Shared.Dataset;
+using Content.Shared.Random.Helpers;
using Content.Shared.Security;
using Content.Shared.StationRecords;
using Robust.Client.AutoGenerated;
@@ -32,7 +33,7 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
public readonly EntityUid Console;
- [ValidatePrototypeId]
+ [ValidatePrototypeId]
private const string ReasonPlaceholders = "CriminalRecordsWantedReasonPlaceholders";
public Action? OnKeySelected;
@@ -333,8 +334,8 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
var field = "reason";
var title = Loc.GetString("criminal-records-status-" + status.ToString().ToLower());
- var placeholders = _proto.Index(ReasonPlaceholders);
- var placeholder = Loc.GetString("criminal-records-console-reason-placeholder", ("placeholder", _random.Pick(placeholders.Values))); // just funny it doesn't actually get used
+ var placeholders = _proto.Index(ReasonPlaceholders);
+ var placeholder = Loc.GetString("criminal-records-console-reason-placeholder", ("placeholder", _random.Pick(placeholders))); // just funny it doesn't actually get used
var prompt = Loc.GetString("criminal-records-console-reason");
var entry = new QuickDialogEntry(field, QuickDialogEntryType.LongText, prompt, placeholder);
var entries = new List() { entry };
diff --git a/Content.Client/Delivery/DeliverySystem.cs b/Content.Client/Delivery/DeliverySystem.cs
new file mode 100644
index 0000000000..c83e09ea74
--- /dev/null
+++ b/Content.Client/Delivery/DeliverySystem.cs
@@ -0,0 +1,5 @@
+using Content.Shared.Delivery;
+
+namespace Content.Client.Delivery;
+
+public sealed class DeliverySystem : SharedDeliverySystem;
diff --git a/Content.Client/Delivery/DeliveryVisualizerSystem.cs b/Content.Client/Delivery/DeliveryVisualizerSystem.cs
new file mode 100644
index 0000000000..8ed15344a4
--- /dev/null
+++ b/Content.Client/Delivery/DeliveryVisualizerSystem.cs
@@ -0,0 +1,45 @@
+using Content.Shared.Delivery;
+using Content.Shared.StatusIcon;
+using Robust.Client.GameObjects;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Delivery;
+
+public sealed class DeliveryVisualizerSystem : VisualizerSystem
+{
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+ [Dependency] private readonly SpriteSystem _sprite = default!;
+
+ private static readonly ProtoId UnknownIcon = "JobIconUnknown";
+
+ protected override void OnAppearanceChange(EntityUid uid, DeliveryComponent component, ref AppearanceChangeEvent args)
+ {
+ if (args.Sprite == null)
+ return;
+
+ _appearance.TryGetData(uid, DeliveryVisuals.JobIcon, out string job, args.Component);
+
+ if (string.IsNullOrEmpty(job))
+ job = UnknownIcon;
+
+ if (!_prototype.TryIndex(job, out var icon))
+ {
+ args.Sprite.LayerSetTexture(DeliveryVisualLayers.JobStamp, _sprite.Frame0(_prototype.Index("JobIconUnknown")));
+ return;
+ }
+
+ args.Sprite.LayerSetTexture(DeliveryVisualLayers.JobStamp, _sprite.Frame0(icon.Icon));
+ }
+}
+
+public enum DeliveryVisualLayers : byte
+{
+ Icon,
+ Lock,
+ FragileStamp,
+ JobStamp,
+ PriorityTape,
+ Breakage,
+ Trash,
+}
diff --git a/Content.Client/Examine/ExamineButton.cs b/Content.Client/Examine/ExamineButton.cs
index 52ff7a972e..839e08f3d4 100644
--- a/Content.Client/Examine/ExamineButton.cs
+++ b/Content.Client/Examine/ExamineButton.cs
@@ -4,6 +4,7 @@ using Content.Shared.Verbs;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Client.Utility;
using Robust.Shared.Utility;
@@ -40,7 +41,17 @@ public sealed class ExamineButton : ContainerButton
Disabled = true;
}
- ToolTip = verb.Message ?? verb.Text;
+ TooltipSupplier = sender =>
+ {
+ var label = new RichTextLabel();
+ label.SetMessage(FormattedMessage.FromMarkupOrThrow(verb.Message ?? verb.Text));
+
+ var tooltip = new Tooltip();
+ tooltip.GetChild(0).Children.Clear();
+ tooltip.GetChild(0).Children.Add(label);
+
+ return tooltip;
+ };
Icon = new TextureRect
{
diff --git a/Content.Client/Examine/ExamineSystem.cs b/Content.Client/Examine/ExamineSystem.cs
index 1c1f1984de..07694ac24a 100644
--- a/Content.Client/Examine/ExamineSystem.cs
+++ b/Content.Client/Examine/ExamineSystem.cs
@@ -298,10 +298,28 @@ namespace Content.Client.Examine
{
Name = "ExamineButtonsHBox",
Orientation = LayoutOrientation.Horizontal,
- HorizontalAlignment = Control.HAlignment.Right,
+ HorizontalAlignment = Control.HAlignment.Stretch,
VerticalAlignment = Control.VAlignment.Bottom,
};
+ var hoverExamineBox = new BoxContainer
+ {
+ Name = "HoverExamineHBox",
+ Orientation = LayoutOrientation.Horizontal,
+ HorizontalAlignment = Control.HAlignment.Left,
+ VerticalAlignment = Control.VAlignment.Center,
+ HorizontalExpand = true
+ };
+
+ var clickExamineBox = new BoxContainer
+ {
+ Name = "ClickExamineHBox",
+ Orientation = LayoutOrientation.Horizontal,
+ HorizontalAlignment = Control.HAlignment.Right,
+ VerticalAlignment = Control.VAlignment.Center,
+ HorizontalExpand = true
+ };
+
// Examine button time
foreach (var verb in verbs)
{
@@ -316,8 +334,15 @@ namespace Content.Client.Examine
var button = new ExamineButton(examine);
- button.OnPressed += VerbButtonPressed;
- buttonsHBox.AddChild(button);
+ if (examine.HoverVerb)
+ {
+ hoverExamineBox.AddChild(button);
+ }
+ else
+ {
+ button.OnPressed += VerbButtonPressed;
+ clickExamineBox.AddChild(button);
+ }
}
var vbox = _examineTooltipOpen?.GetChild(0).GetChild(0);
@@ -334,6 +359,8 @@ namespace Content.Client.Examine
{
vbox.Children.Remove(hbox.First());
}
+ buttonsHBox.AddChild(hoverExamineBox);
+ buttonsHBox.AddChild(clickExamineBox);
vbox.AddChild(buttonsHBox);
}
diff --git a/Content.Client/Gameplay/GameplayState.cs b/Content.Client/Gameplay/GameplayState.cs
index 427dd5c071..0ce6436f2d 100644
--- a/Content.Client/Gameplay/GameplayState.cs
+++ b/Content.Client/Gameplay/GameplayState.cs
@@ -59,13 +59,13 @@ namespace Content.Client.Gameplay
// Version number watermark.
_version = new Label();
+ _version.FontColorOverride = Color.FromHex("#FFFFFF20");
_version.Text = _changelog.GetClientVersion();
- _version.Visible = VersionVisible();
UserInterfaceManager.PopupRoot.AddChild(_version);
- _configurationManager.OnValueChanged(CCVars.HudVersionWatermark, (show) => { _version.Visible = VersionVisible(); });
- _configurationManager.OnValueChanged(CCVars.ForceClientHudVersionWatermark, (show) => { _version.Visible = VersionVisible(); });
+ _configurationManager.OnValueChanged(CCVars.HudVersionWatermark, (show) => { _version.Visible = VersionVisible(); }, true);
+ _configurationManager.OnValueChanged(CCVars.ForceClientHudVersionWatermark, (show) => { _version.Visible = VersionVisible(); }, true);
// TODO make this centered or something
- LayoutContainer.SetPosition(_version, new Vector2(800, 0));
+ LayoutContainer.SetPosition(_version, new Vector2(70, 0));
}
// This allows servers to force the watermark on clients
diff --git a/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml b/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml
index becffbdc6d..5b871644ea 100644
--- a/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml
+++ b/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml
@@ -1,30 +1,34 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Hands/Systems/HandsSystem.cs b/Content.Client/Hands/Systems/HandsSystem.cs
index 68800a2afe..f879e06a20 100644
--- a/Content.Client/Hands/Systems/HandsSystem.cs
+++ b/Content.Client/Hands/Systems/HandsSystem.cs
@@ -349,7 +349,12 @@ namespace Content.Client.Hands.Systems
sprite.LayerSetData(index, layerData);
//Add displacement maps
- if (handComp.HandDisplacement is not null)
+ if (hand.Location == HandLocation.Left && handComp.LeftHandDisplacement is not null)
+ _displacement.TryAddDisplacement(handComp.LeftHandDisplacement, sprite, index, key, revealedLayers);
+ else if (hand.Location == HandLocation.Right && handComp.RightHandDisplacement is not null)
+ _displacement.TryAddDisplacement(handComp.RightHandDisplacement, sprite, index, key, revealedLayers);
+ //Fallback to default displacement map
+ else if (handComp.HandDisplacement is not null)
_displacement.TryAddDisplacement(handComp.HandDisplacement, sprite, index, key, revealedLayers);
}
diff --git a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs
index 6eb5dd9ec9..aa9c4ccce7 100644
--- a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs
+++ b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs
@@ -1,8 +1,11 @@
+using Content.Shared.CCVar;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Markings;
using Content.Shared.Humanoid.Prototypes;
+using Content.Shared.Inventory;
using Content.Shared.Preferences;
using Robust.Client.GameObjects;
+using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
@@ -12,12 +15,15 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly MarkingManager _markingManager = default!;
+ [Dependency] private readonly IConfigurationManager _configurationManager = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent(OnHandleState);
+ Subs.CVar(_configurationManager, CCVars.AccessibilityClientCensorNudity, OnCvarChanged, true);
+ Subs.CVar(_configurationManager, CCVars.AccessibilityServerCensorNudity, OnCvarChanged, true);
}
private void OnHandleState(EntityUid uid, HumanoidAppearanceComponent component, ref AfterAutoHandleStateEvent args)
@@ -25,6 +31,15 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
UpdateSprite(component, Comp(uid));
}
+ private void OnCvarChanged(bool value)
+ {
+ var humanoidQuery = EntityManager.AllEntityQueryEnumerator();
+ while (humanoidQuery.MoveNext(out var _, out var humanoidComp, out var spriteComp))
+ {
+ UpdateSprite(humanoidComp, spriteComp);
+ }
+ }
+
private void UpdateSprite(HumanoidAppearanceComponent component, SpriteComponent sprite)
{
UpdateLayers(component, sprite);
@@ -34,7 +49,7 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
}
private static bool IsHidden(HumanoidAppearanceComponent humanoid, HumanoidVisualLayers layer)
- => humanoid.HiddenLayers.Contains(layer) || humanoid.PermanentlyHidden.Contains(layer);
+ => humanoid.HiddenLayers.ContainsKey(layer) || humanoid.PermanentlyHidden.Contains(layer);
private void UpdateLayers(HumanoidAppearanceComponent component, SpriteComponent sprite)
{
@@ -43,7 +58,7 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
// add default species layers
var speciesProto = _prototypeManager.Index(component.Species);
- var baseSprites = _prototypeManager.Index(speciesProto.SpriteSet);
+ var baseSprites = _prototypeManager.Index(speciesProto.SpriteSet);
foreach (var (key, id) in baseSprites.Sprites)
{
oldLayers.Remove(key);
@@ -189,7 +204,7 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
humanoid.MarkingSet = markings;
humanoid.PermanentlyHidden = new HashSet();
- humanoid.HiddenLayers = new HashSet();
+ humanoid.HiddenLayers = new Dictionary();
humanoid.CustomBaseLayers = customBaseLayers;
humanoid.Sex = profile.Sex;
humanoid.Gender = profile.Gender;
@@ -207,16 +222,30 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
// Really, markings should probably be a separate component altogether.
ClearAllMarkings(humanoid, sprite);
+ var censorNudity = _configurationManager.GetCVar(CCVars.AccessibilityClientCensorNudity) ||
+ _configurationManager.GetCVar(CCVars.AccessibilityServerCensorNudity);
+ // The reason we're splitting this up is in case the character already has undergarment equipped in that slot.
+ var applyUndergarmentTop = censorNudity;
+ var applyUndergarmentBottom = censorNudity;
+
foreach (var markingList in humanoid.MarkingSet.Markings.Values)
{
foreach (var marking in markingList)
{
if (_markingManager.TryGetMarking(marking, out var markingPrototype))
+ {
ApplyMarking(markingPrototype, marking.MarkingColors, marking.Visible, humanoid, sprite);
+ if (markingPrototype.BodyPart == HumanoidVisualLayers.UndergarmentTop)
+ applyUndergarmentTop = false;
+ else if (markingPrototype.BodyPart == HumanoidVisualLayers.UndergarmentBottom)
+ applyUndergarmentBottom = false;
+ }
}
}
humanoid.ClientOldMarkings = new MarkingSet(humanoid.MarkingSet);
+
+ AddUndergarments(humanoid, sprite, applyUndergarmentTop, applyUndergarmentBottom);
}
private void ClearAllMarkings(HumanoidAppearanceComponent humanoid, SpriteComponent sprite)
@@ -264,6 +293,31 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
spriteComp.RemoveLayer(index);
}
}
+
+ private void AddUndergarments(HumanoidAppearanceComponent humanoid, SpriteComponent sprite, bool undergarmentTop, bool undergarmentBottom)
+ {
+ if (undergarmentTop && humanoid.UndergarmentTop != null)
+ {
+ var marking = new Marking(humanoid.UndergarmentTop, new List { new Color() });
+ if (_markingManager.TryGetMarking(marking, out var prototype))
+ {
+ // Markings are added to ClientOldMarkings because otherwise it causes issues when toggling the feature on/off.
+ humanoid.ClientOldMarkings.Markings.Add(MarkingCategories.UndergarmentTop, new List{ marking });
+ ApplyMarking(prototype, null, true, humanoid, sprite);
+ }
+ }
+
+ if (undergarmentBottom && humanoid.UndergarmentBottom != null)
+ {
+ var marking = new Marking(humanoid.UndergarmentBottom, new List { new Color() });
+ if (_markingManager.TryGetMarking(marking, out var prototype))
+ {
+ humanoid.ClientOldMarkings.Markings.Add(MarkingCategories.UndergarmentBottom, new List{ marking });
+ ApplyMarking(prototype, null, true, humanoid, sprite);
+ }
+ }
+ }
+
private void ApplyMarking(MarkingPrototype markingPrototype,
IReadOnlyList? colors,
bool visible,
@@ -338,23 +392,21 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
}
}
- protected override void SetLayerVisibility(
- EntityUid uid,
- HumanoidAppearanceComponent humanoid,
+ public override void SetLayerVisibility(
+ Entity ent,
HumanoidVisualLayers layer,
bool visible,
- bool permanent,
+ SlotFlags? slot,
ref bool dirty)
{
- base.SetLayerVisibility(uid, humanoid, layer, visible, permanent, ref dirty);
+ base.SetLayerVisibility(ent, layer, visible, slot, ref dirty);
- var sprite = Comp(uid);
+ var sprite = Comp(ent);
if (!sprite.LayerMapTryGet(layer, out var index))
{
if (!visible)
return;
- else
- index = sprite.LayerMapReserveBlank(layer);
+ index = sprite.LayerMapReserveBlank(layer);
}
var spriteLayer = sprite[index];
@@ -364,13 +416,14 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
spriteLayer.Visible = visible;
// I fucking hate this. I'll get around to refactoring sprite layers eventually I swear
+ // Just a week away...
- foreach (var markingList in humanoid.MarkingSet.Markings.Values)
+ foreach (var markingList in ent.Comp.MarkingSet.Markings.Values)
{
foreach (var marking in markingList)
{
if (_markingManager.TryGetMarking(marking, out var markingPrototype) && markingPrototype.BodyPart == layer)
- ApplyMarking(markingPrototype, marking.MarkingColors, marking.Visible, humanoid, sprite);
+ ApplyMarking(markingPrototype, marking.MarkingColors, marking.Visible, ent, sprite);
}
}
}
diff --git a/Content.Client/IconSmoothing/IconSmoothComponent.cs b/Content.Client/IconSmoothing/IconSmoothComponent.cs
index 040198529c..7898e9dc1e 100644
--- a/Content.Client/IconSmoothing/IconSmoothComponent.cs
+++ b/Content.Client/IconSmoothing/IconSmoothComponent.cs
@@ -26,6 +26,12 @@ namespace Content.Client.IconSmoothing
[ViewVariables(VVAccess.ReadWrite), DataField("key")]
public string? SmoothKey { get; private set; }
+ ///
+ /// Additional keys to smooth with.
+ ///
+ [DataField]
+ public List AdditionalKeys = new();
+
///
/// Prepended to the RSI state.
///
diff --git a/Content.Client/IconSmoothing/IconSmoothSystem.cs b/Content.Client/IconSmoothing/IconSmoothSystem.cs
index 2654f1ab57..45aafb5288 100644
--- a/Content.Client/IconSmoothing/IconSmoothSystem.cs
+++ b/Content.Client/IconSmoothing/IconSmoothSystem.cs
@@ -376,7 +376,8 @@ namespace Content.Client.IconSmoothing
while (candidates.MoveNext(out var entity))
{
if (smoothQuery.TryGetComponent(entity, out var other) &&
- other.SmoothKey == smooth.SmoothKey &&
+ other.SmoothKey != null &&
+ (other.SmoothKey == smooth.SmoothKey || smooth.AdditionalKeys.Contains(other.SmoothKey)) &&
other.Enabled)
{
return true;
diff --git a/Content.Client/Implants/ImplanterSystem.cs b/Content.Client/Implants/ImplanterSystem.cs
index 13a90f3e39..cca09f5dad 100644
--- a/Content.Client/Implants/ImplanterSystem.cs
+++ b/Content.Client/Implants/ImplanterSystem.cs
@@ -2,11 +2,15 @@
using Content.Client.Items;
using Content.Shared.Implants;
using Content.Shared.Implants.Components;
+using Robust.Shared.Prototypes;
namespace Content.Client.Implants;
public sealed class ImplanterSystem : SharedImplanterSystem
{
+ [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
+ [Dependency] private readonly IPrototypeManager _proto = default!;
+
public override void Initialize()
{
base.Initialize();
@@ -17,6 +21,18 @@ public sealed class ImplanterSystem : SharedImplanterSystem
private void OnHandleImplanterState(EntityUid uid, ImplanterComponent component, ref AfterAutoHandleStateEvent args)
{
+ if (_uiSystem.TryGetOpenUi(uid, DeimplantUiKey.Key, out var bui))
+ {
+ Dictionary implants = new();
+ foreach (var implant in component.DeimplantWhitelist)
+ {
+ if (_proto.TryIndex(implant, out var proto))
+ implants.Add(proto.ID, proto.Name);
+ }
+
+ bui.UpdateState(implants, component.DeimplantChosen);
+ }
+
component.UiUpdateNeeded = true;
}
}
diff --git a/Content.Client/Implants/UI/DeimplantBoundUserInterface.cs b/Content.Client/Implants/UI/DeimplantBoundUserInterface.cs
new file mode 100644
index 0000000000..0857cdf86f
--- /dev/null
+++ b/Content.Client/Implants/UI/DeimplantBoundUserInterface.cs
@@ -0,0 +1,35 @@
+using Content.Shared.Implants;
+using Robust.Client.UserInterface;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Implants.UI;
+
+public sealed class DeimplantBoundUserInterface : BoundUserInterface
+{
+ [Dependency] private readonly IPrototypeManager _protomanager = default!;
+
+ [ViewVariables]
+ private DeimplantChoiceWindow? _window;
+
+ public DeimplantBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _window = this.CreateWindow();
+
+ _window.OnImplantChange += implant => SendMessage(new DeimplantChangeVerbMessage(implant));
+ }
+
+ public void UpdateState(Dictionary implantList, string? implant)
+ {
+ if (_window != null)
+ {
+ _window.UpdateImplantList(implantList);
+ _window.UpdateState(implant);
+ }
+ }
+}
diff --git a/Content.Client/Implants/UI/DeimplantChoiceWindow.xaml b/Content.Client/Implants/UI/DeimplantChoiceWindow.xaml
new file mode 100644
index 0000000000..f0de9f32e0
--- /dev/null
+++ b/Content.Client/Implants/UI/DeimplantChoiceWindow.xaml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Implants/UI/DeimplantChoiceWindow.xaml.cs b/Content.Client/Implants/UI/DeimplantChoiceWindow.xaml.cs
new file mode 100644
index 0000000000..a7ce50d855
--- /dev/null
+++ b/Content.Client/Implants/UI/DeimplantChoiceWindow.xaml.cs
@@ -0,0 +1,53 @@
+using Content.Client.UserInterface.Controls;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.XAML;
+using System.Linq;
+
+namespace Content.Client.Implants.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class DeimplantChoiceWindow : FancyWindow
+{
+ public Action? OnImplantChange;
+
+ private Dictionary _implants = new();
+
+ private string? _chosenImplant;
+
+ public DeimplantChoiceWindow()
+ {
+ RobustXamlLoader.Load(this);
+
+ ImplantSelector.OnItemSelected += args =>
+ {
+ OnImplantChange?.Invoke(_implants.ElementAt(args.Id).Key);
+ ImplantSelector.SelectId(args.Id);
+ };
+ }
+
+ public void UpdateImplantList(Dictionary implants)
+ {
+ _implants = implants;
+ int i = 0;
+ ImplantSelector.Clear();
+ foreach (var implantDict in _implants)
+ {
+ ImplantSelector.AddItem(implantDict.Value, i);
+ i++;
+ }
+ }
+
+ public void UpdateState(string? implant)
+ {
+ _chosenImplant = implant;
+
+ for (int id = 0; id < ImplantSelector.ItemCount; id++)
+ {
+ if (_implants.ElementAt(id).Key.Equals(_chosenImplant))
+ {
+ ImplantSelector.SelectId(id);
+ break;
+ }
+ }
+ }
+}
diff --git a/Content.Client/Implants/UI/ImplanterStatusControl.cs b/Content.Client/Implants/UI/ImplanterStatusControl.cs
index e2ffabd17d..569dd785d7 100644
--- a/Content.Client/Implants/UI/ImplanterStatusControl.cs
+++ b/Content.Client/Implants/UI/ImplanterStatusControl.cs
@@ -4,17 +4,20 @@ using Content.Client.UserInterface.Controls;
using Content.Shared.Implants.Components;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client.Implants.UI;
public sealed class ImplanterStatusControl : Control
{
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
private readonly ImplanterComponent _parent;
private readonly RichTextLabel _label;
public ImplanterStatusControl(ImplanterComponent parent)
{
+ IoCManager.InjectDependencies(this);
_parent = parent;
_label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } };
_label.MaxWidth = 350;
@@ -43,12 +46,25 @@ public sealed class ImplanterStatusControl : Control
_ => Loc.GetString("injector-invalid-injector-toggle-mode")
};
- var implantName = _parent.ImplanterSlot.HasItem
- ? _parent.ImplantData.Item1
- : Loc.GetString("implanter-empty-text");
+ if (_parent.CurrentMode == ImplanterToggleMode.Draw)
+ {
+ string implantName = _parent.DeimplantChosen != null
+ ? (_prototype.TryIndex(_parent.DeimplantChosen.Value, out EntityPrototype? implantProto) ? implantProto.Name : Loc.GetString("implanter-empty-text"))
+ : Loc.GetString("implanter-empty-text");
- _label.SetMarkup(Loc.GetString("implanter-label",
- ("implantName", implantName),
- ("modeString", modeStringLocalized)));
+ _label.SetMarkup(Loc.GetString("implanter-label-draw",
+ ("implantName", implantName),
+ ("modeString", modeStringLocalized)));
+ }
+ else
+ {
+ var implantName = _parent.ImplanterSlot.HasItem
+ ? _parent.ImplantData.Item1
+ : Loc.GetString("implanter-empty-text");
+
+ _label.SetMarkup(Loc.GetString("implanter-label-inject",
+ ("implantName", implantName),
+ ("modeString", modeStringLocalized)));
+ }
}
}
diff --git a/Content.Client/Lathe/UI/LatheMenu.xaml b/Content.Client/Lathe/UI/LatheMenu.xaml
index 5b21f0bae6..d5e3573148 100644
--- a/Content.Client/Lathe/UI/LatheMenu.xaml
+++ b/Content.Client/Lathe/UI/LatheMenu.xaml
@@ -59,6 +59,7 @@
PlaceHolder="0"
Text="1"
HorizontalExpand="True" />
+
diff --git a/Content.Client/Lathe/UI/LatheMenu.xaml.cs b/Content.Client/Lathe/UI/LatheMenu.xaml.cs
index fa99a9dd8e..1e71d1ae89 100644
--- a/Content.Client/Lathe/UI/LatheMenu.xaml.cs
+++ b/Content.Client/Lathe/UI/LatheMenu.xaml.cs
@@ -68,21 +68,13 @@ public sealed partial class LatheMenu : DefaultWindow
{
ServerListButton.Visible = false;
}
+
+ AmountLineEdit.SetText(latheComponent.DefaultProductionAmount.ToString());
}
MaterialsList.SetOwner(Entity);
}
- protected override void Opened()
- {
- base.Opened();
-
- if (_entityManager.TryGetComponent(Entity, out var latheComp))
- {
- AmountLineEdit.SetText(latheComp.DefaultProductionAmount.ToString());
- }
- }
-
///
/// Populates the list of all the recipes
///
@@ -94,8 +86,17 @@ public sealed partial class LatheMenu : DefaultWindow
if (!_prototypeManager.TryIndex(recipe, out var proto))
continue;
- if (CurrentCategory != null && proto.Category != CurrentCategory)
- continue;
+ // Category filtering
+ if (CurrentCategory != null)
+ {
+ if (proto.Categories.Count <= 0)
+ continue;
+
+ var validRecipe = proto.Categories.Any(category => category == CurrentCategory);
+
+ if (!validRecipe)
+ continue;
+ }
if (SearchBar.Text.Trim().Length != 0)
{
@@ -111,6 +112,8 @@ public sealed partial class LatheMenu : DefaultWindow
if (!int.TryParse(AmountLineEdit.Text, out var quantity) || quantity <= 0)
quantity = 1;
+ RecipeCount.Text = Loc.GetString("lathe-menu-recipe-count", ("count", recipesToShow.Count));
+
var sortedRecipesToShow = recipesToShow.OrderBy(_lathe.GetRecipeName);
RecipeList.Children.Clear();
_entityManager.TryGetComponent(Entity, out LatheComponent? lathe);
@@ -179,18 +182,22 @@ public sealed partial class LatheMenu : DefaultWindow
public void UpdateCategories()
{
+ // Get categories from recipes
var currentCategories = new List>();
foreach (var recipeId in Recipes)
{
var recipe = _prototypeManager.Index(recipeId);
- if (recipe.Category == null)
+ if (recipe.Categories.Count <= 0)
continue;
- if (currentCategories.Contains(recipe.Category.Value))
- continue;
+ foreach (var category in recipe.Categories)
+ {
+ if (currentCategories.Contains(category))
+ continue;
- currentCategories.Add(recipe.Category.Value);
+ currentCategories.Add(category);
+ }
}
if (Categories != null && (Categories.Count == currentCategories.Count || !Categories.All(currentCategories.Contains)))
diff --git a/Content.Client/Light/AfterLightTargetOverlay.cs b/Content.Client/Light/AfterLightTargetOverlay.cs
index 06c508a54e..7856fd4ded 100644
--- a/Content.Client/Light/AfterLightTargetOverlay.cs
+++ b/Content.Client/Light/AfterLightTargetOverlay.cs
@@ -54,6 +54,6 @@ public sealed class AfterLightTargetOverlay : Overlay
worldHandle.SetTransform(localMatrix);
worldHandle.DrawTextureRectRegion(lightOverlay.EnlargedLightTarget.Texture, bounds, subRegion: subRegion);
- }, null);
+ }, Color.Transparent);
}
}
diff --git a/Content.Client/Light/EntitySystems/PlanetLightSystem.cs b/Content.Client/Light/EntitySystems/PlanetLightSystem.cs
index 2da67137ed..cbe2f47f78 100644
--- a/Content.Client/Light/EntitySystems/PlanetLightSystem.cs
+++ b/Content.Client/Light/EntitySystems/PlanetLightSystem.cs
@@ -16,6 +16,7 @@ public sealed class PlanetLightSystem : EntitySystem
_overlayMan.AddOverlay(new RoofOverlay(EntityManager));
_overlayMan.AddOverlay(new TileEmissionOverlay(EntityManager));
_overlayMan.AddOverlay(new LightBlurOverlay());
+ _overlayMan.AddOverlay(new SunShadowOverlay());
_overlayMan.AddOverlay(new AfterLightTargetOverlay());
}
@@ -31,6 +32,7 @@ public sealed class PlanetLightSystem : EntitySystem
_overlayMan.RemoveOverlay();
_overlayMan.RemoveOverlay();
_overlayMan.RemoveOverlay();
+ _overlayMan.RemoveOverlay();
_overlayMan.RemoveOverlay();
}
}
diff --git a/Content.Client/Light/EntitySystems/SunShadowSystem.cs b/Content.Client/Light/EntitySystems/SunShadowSystem.cs
new file mode 100644
index 0000000000..6f7a965a61
--- /dev/null
+++ b/Content.Client/Light/EntitySystems/SunShadowSystem.cs
@@ -0,0 +1,92 @@
+using System.Diagnostics.Contracts;
+using System.Numerics;
+using Content.Client.GameTicking.Managers;
+using Content.Shared.Light.Components;
+using Content.Shared.Light.EntitySystems;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Light.EntitySystems;
+
+public sealed class SunShadowSystem : SharedSunShadowSystem
+{
+ [Dependency] private readonly ClientGameTicker _ticker = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly MetaDataSystem _metadata = default!;
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ if (!_timing.IsFirstTimePredicted)
+ return;
+
+ var mapQuery = AllEntityQuery();
+ while (mapQuery.MoveNext(out var uid, out var cycle, out var shadow))
+ {
+ if (!cycle.Running || cycle.Directions.Count == 0)
+ continue;
+
+ var pausedTime = _metadata.GetPauseTime(uid);
+
+ var time = (float)(_timing.CurTime
+ .Add(cycle.Offset)
+ .Subtract(_ticker.RoundStartTimeSpan)
+ .Subtract(pausedTime)
+ .TotalSeconds % cycle.Duration.TotalSeconds);
+
+ var (direction, alpha) = GetShadow((uid, cycle), time);
+ shadow.Direction = direction;
+ shadow.Alpha = alpha;
+ }
+ }
+
+ [Pure]
+ public (Vector2 Direction, float Alpha) GetShadow(Entity entity, float time)
+ {
+ // So essentially the values are stored as the percentages of the total duration just so it adjusts the speed
+ // dynamically and we don't have to manually handle it.
+ // It will lerp from each value to the next one with angle and length handled separately
+ var ratio = (float) (time / entity.Comp.Duration.TotalSeconds);
+
+ for (var i = entity.Comp.Directions.Count - 1; i >= 0; i--)
+ {
+ var dir = entity.Comp.Directions[i];
+
+ if (ratio > dir.Ratio)
+ {
+ var next = entity.Comp.Directions[(i + 1) % entity.Comp.Directions.Count];
+ float nextRatio;
+
+ // Last entry
+ if (i == entity.Comp.Directions.Count - 1)
+ {
+ nextRatio = next.Ratio + 1f;
+ }
+ else
+ {
+ nextRatio = next.Ratio;
+ }
+
+ var range = nextRatio - dir.Ratio;
+ var diff = (ratio - dir.Ratio) / range;
+ DebugTools.Assert(diff is >= 0f and <= 1f);
+
+ // We lerp angle + length separately as we don't want a straight-line lerp and want the rotation to be consistent.
+ var currentAngle = dir.Direction.ToAngle();
+ var nextAngle = next.Direction.ToAngle();
+
+ var angle = Angle.Lerp(currentAngle, nextAngle, diff);
+ // This is to avoid getting weird issues where the angle gets pretty close but length still noticeably catches up.
+ var lengthDiff = MathF.Pow(diff, 1f / 2f);
+ var length = float.Lerp(dir.Direction.Length(), next.Direction.Length(), lengthDiff);
+
+ var vector = angle.ToVec() * length;
+ var alpha = float.Lerp(dir.Alpha, next.Alpha, diff);
+ return (vector, alpha);
+ }
+ }
+
+ throw new InvalidOperationException();
+ }
+}
diff --git a/Content.Client/Light/LightCycleSystem.cs b/Content.Client/Light/LightCycleSystem.cs
index 9e19423cc3..8de0165fd2 100644
--- a/Content.Client/Light/LightCycleSystem.cs
+++ b/Content.Client/Light/LightCycleSystem.cs
@@ -1,6 +1,7 @@
using Content.Client.GameTicking.Managers;
using Content.Shared;
using Content.Shared.Light.Components;
+using Content.Shared.Light.EntitySystems;
using Robust.Shared.Map.Components;
using Robust.Shared.Timing;
@@ -11,19 +12,29 @@ public sealed class LightCycleSystem : SharedLightCycleSystem
{
[Dependency] private readonly ClientGameTicker _ticker = default!;
[Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly MetaDataSystem _metadata = default!;
public override void Update(float frameTime)
{
base.Update(frameTime);
+
+ if (!_timing.IsFirstTimePredicted)
+ return;
+
var mapQuery = AllEntityQuery();
while (mapQuery.MoveNext(out var uid, out var cycle, out var map))
{
if (!cycle.Running)
continue;
+ // We still iterate paused entities as we still want to override the lighting color and not have
+ // it apply the server state
+ var pausedTime = _metadata.GetPauseTime(uid);
+
var time = (float) _timing.CurTime
.Add(cycle.Offset)
.Subtract(_ticker.RoundStartTimeSpan)
+ .Subtract(pausedTime)
.TotalSeconds;
var color = GetColor((uid, cycle), cycle.OriginalColor, time);
diff --git a/Content.Client/Light/RoofOverlay.cs b/Content.Client/Light/RoofOverlay.cs
index 0648f8624f..8944630169 100644
--- a/Content.Client/Light/RoofOverlay.cs
+++ b/Content.Client/Light/RoofOverlay.cs
@@ -94,13 +94,15 @@ public sealed class RoofOverlay : Overlay
// Due to stencilling we essentially draw on unrooved tiles
while (tileEnumerator.MoveNext(out var tileRef))
{
- if (!_roof.IsRooved(roofEnt, tileRef.GridIndices))
+ var color = _roof.GetColor(roofEnt, tileRef.GridIndices);
+
+ if (color == null)
{
continue;
}
var local = _lookup.GetLocalBounds(tileRef, grid.Comp.TileSize);
- worldHandle.DrawRect(local, roof.Color);
+ worldHandle.DrawRect(local, color.Value);
}
}
}, null);
diff --git a/Content.Client/Light/SunShadowOverlay.cs b/Content.Client/Light/SunShadowOverlay.cs
new file mode 100644
index 0000000000..a296f95479
--- /dev/null
+++ b/Content.Client/Light/SunShadowOverlay.cs
@@ -0,0 +1,160 @@
+using System.Numerics;
+using Content.Shared.Light.Components;
+using Robust.Client.Graphics;
+using Robust.Shared.Enums;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Physics;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Light;
+
+public sealed class SunShadowOverlay : Overlay
+{
+ public override OverlaySpace Space => OverlaySpace.BeforeLighting;
+
+ [Dependency] private readonly IClyde _clyde = default!;
+ [Dependency] private readonly IEntityManager _entManager = default!;
+ [Dependency] private readonly IMapManager _mapManager = default!;
+ [Dependency] private readonly IPrototypeManager _protoManager = default!;
+ private readonly EntityLookupSystem _lookup;
+ private readonly SharedTransformSystem _xformSys;
+
+ private readonly HashSet> _shadows = new();
+
+ private IRenderTexture? _blurTarget;
+ private IRenderTexture? _target;
+
+ public SunShadowOverlay()
+ {
+ IoCManager.InjectDependencies(this);
+ _xformSys = _entManager.System();
+ _lookup = _entManager.System();
+ ZIndex = AfterLightTargetOverlay.ContentZIndex + 1;
+ }
+
+ private List> _grids = new();
+
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ var viewport = args.Viewport;
+ var eye = viewport.Eye;
+
+ if (eye == null)
+ return;
+
+ _grids.Clear();
+ _mapManager.FindGridsIntersecting(args.MapId,
+ args.WorldBounds.Enlarged(SunShadowComponent.MaxLength),
+ ref _grids);
+
+ var worldHandle = args.WorldHandle;
+ var mapId = args.MapId;
+ var worldBounds = args.WorldBounds;
+ var targetSize = viewport.LightRenderTarget.Size;
+
+ if (_target?.Size != targetSize)
+ {
+ _target = _clyde
+ .CreateRenderTarget(targetSize,
+ new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
+ name: "sun-shadow-target");
+
+ if (_blurTarget?.Size != targetSize)
+ {
+ _blurTarget = _clyde
+ .CreateRenderTarget(targetSize, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "sun-shadow-blur");
+ }
+ }
+
+ var lightScale = viewport.LightRenderTarget.Size / (Vector2)viewport.Size;
+ var scale = viewport.RenderScale / (Vector2.One / lightScale);
+
+ foreach (var grid in _grids)
+ {
+ if (!_entManager.TryGetComponent(grid.Owner, out SunShadowComponent? sun))
+ {
+ continue;
+ }
+
+ var direction = sun.Direction;
+ var alpha = Math.Clamp(sun.Alpha, 0f, 1f);
+
+ // Nowhere to cast to so ignore it.
+ if (direction.Equals(Vector2.Zero) || alpha == 0f)
+ continue;
+
+ // Feature todo: dynamic shadows for mobs and trees. Also ideally remove the fake tree shadows.
+ // TODO: Jittering still not quite perfect
+
+ var expandedBounds = worldBounds.Enlarged(direction.Length() + 0.01f);
+ _shadows.Clear();
+
+ // Draw shadow polys to stencil
+ args.WorldHandle.RenderInRenderTarget(_target,
+ () =>
+ {
+ var invMatrix =
+ _target.GetWorldToLocalMatrix(eye, scale);
+ var indices = new Vector2[PhysicsConstants.MaxPolygonVertices * 2];
+
+ // Go through shadows in range.
+
+ // For each one we:
+ // - Get the original vertices.
+ // - Extrapolate these along the sun direction.
+ // - Combine the above into 1 single polygon to draw.
+
+ // Note that this is range-limited for accuracy; if you set it too high it will clip through walls or other undesirable entities.
+ // This is probably not noticeable most of the time but if you want something "accurate" you'll want to code a solution.
+ // Ideally the CPU would have its own shadow-map copy that we could just ray-cast each vert into though
+ // You might need to batch verts or the likes as this could get expensive.
+ _lookup.GetEntitiesIntersecting(mapId, expandedBounds, _shadows);
+
+ foreach (var ent in _shadows)
+ {
+ var xform = _entManager.GetComponent(ent.Owner);
+ var (worldPos, worldRot) = _xformSys.GetWorldPositionRotation(xform);
+ // Need no rotation on matrix as sun shadow direction doesn't care.
+ var worldMatrix = Matrix3x2.CreateTranslation(worldPos);
+ var renderMatrix = Matrix3x2.Multiply(worldMatrix, invMatrix);
+ var pointCount = ent.Comp.Points.Length;
+
+ Array.Copy(ent.Comp.Points, indices, pointCount);
+
+ for (var i = 0; i < pointCount; i++)
+ {
+ // Update point based on entity rotation.
+ indices[i] = worldRot.RotateVec(indices[i]);
+
+ // Add the offset point by the sun shadow direction.
+ indices[pointCount + i] = indices[i] + direction;
+ }
+
+ var points = PhysicsHull.ComputePoints(indices, pointCount * 2);
+ worldHandle.SetTransform(renderMatrix);
+
+ worldHandle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, points, Color.White);
+ }
+ },
+ Color.Transparent);
+
+ // Slightly blur it just to avoid aliasing issues on the later viewport-wide blur.
+ _clyde.BlurRenderTarget(viewport, _target, _blurTarget!, eye, 1f);
+
+ // Draw stencil (see roofoverlay).
+ args.WorldHandle.RenderInRenderTarget(viewport.LightRenderTarget,
+ () =>
+ {
+ var invMatrix =
+ viewport.LightRenderTarget.GetWorldToLocalMatrix(eye, scale);
+ worldHandle.SetTransform(invMatrix);
+
+ var maskShader = _protoManager.Index("Mix").Instance();
+ worldHandle.UseShader(maskShader);
+
+ worldHandle.DrawTextureRect(_target.Texture, worldBounds, Color.Black.WithAlpha(alpha));
+ }, null);
+ }
+ }
+}
diff --git a/Content.Client/Movement/Systems/EyeCursorOffsetSystem.cs b/Content.Client/Movement/Systems/EyeCursorOffsetSystem.cs
index f3bff9ef0c..9e8ca9a9c9 100644
--- a/Content.Client/Movement/Systems/EyeCursorOffsetSystem.cs
+++ b/Content.Client/Movement/Systems/EyeCursorOffsetSystem.cs
@@ -10,7 +10,7 @@ using Robust.Client.Player;
namespace Content.Client.Movement.Systems;
-public partial class EyeCursorOffsetSystem : EntitySystem
+public sealed partial class EyeCursorOffsetSystem : EntitySystem
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
diff --git a/Content.Client/Options/UI/OptionsMenu.xaml b/Content.Client/Options/UI/OptionsMenu.xaml
index 90486a196a..21c4b64ce8 100644
--- a/Content.Client/Options/UI/OptionsMenu.xaml
+++ b/Content.Client/Options/UI/OptionsMenu.xaml
@@ -8,5 +8,6 @@
+
diff --git a/Content.Client/Options/UI/OptionsMenu.xaml.cs b/Content.Client/Options/UI/OptionsMenu.xaml.cs
index b749d1f234..9efd04fd0b 100644
--- a/Content.Client/Options/UI/OptionsMenu.xaml.cs
+++ b/Content.Client/Options/UI/OptionsMenu.xaml.cs
@@ -1,4 +1,4 @@
-using Content.Client.Options.UI.Tabs;
+using Content.Client.Administration.Managers;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
@@ -8,6 +8,8 @@ namespace Content.Client.Options.UI
[GenerateTypedNameReferences]
public sealed partial class OptionsMenu : DefaultWindow
{
+ [Dependency] private readonly IClientAdminManager _adminManager = default!;
+
public OptionsMenu()
{
RobustXamlLoader.Load(this);
@@ -18,16 +20,21 @@ namespace Content.Client.Options.UI
Tabs.SetTabTitle(2, Loc.GetString("ui-options-tab-controls"));
Tabs.SetTabTitle(3, Loc.GetString("ui-options-tab-audio"));
Tabs.SetTabTitle(4, Loc.GetString("ui-options-tab-accessibility"));
+ Tabs.SetTabTitle(5, Loc.GetString("ui-options-tab-admin"));
UpdateTabs();
}
public void UpdateTabs()
{
+ var isAdmin = _adminManager.IsAdmin(true);
+ Tabs.SetTabVisible(5, isAdmin);
+
GraphicsTab.Control.ReloadValues();
MiscTab.Control.ReloadValues();
AccessibilityTab.Control.ReloadValues();
AudioTab.Control.ReloadValues();
+ AdminOptionsTab.Control.ReloadValues();
}
}
}
diff --git a/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml b/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml
index 54d92b2b11..5041b498a0 100644
--- a/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml
+++ b/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml
@@ -4,11 +4,19 @@
+
-
+
+
+
+
+
+
diff --git a/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml.cs b/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml.cs
index 15182fbf12..f87cda746c 100644
--- a/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml.cs
+++ b/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml.cs
@@ -15,8 +15,13 @@ public sealed partial class AccessibilityTab : Control
Control.AddOptionCheckBox(CCVars.ChatEnableColorName, EnableColorNameCheckBox);
Control.AddOptionCheckBox(CCVars.AccessibilityColorblindFriendly, ColorblindFriendlyCheckBox);
Control.AddOptionCheckBox(CCVars.ReducedMotion, ReducedMotionCheckBox);
- Control.AddOptionPercentSlider(CCVars.ChatWindowOpacity, ChatWindowOpacitySlider);
Control.AddOptionPercentSlider(CCVars.ScreenShakeIntensity, ScreenShakeIntensitySlider);
+ Control.AddOptionPercentSlider(CCVars.ChatWindowOpacity, ChatWindowOpacitySlider);
+ Control.AddOptionPercentSlider(CCVars.SpeechBubbleTextOpacity, SpeechBubbleTextOpacitySlider);
+ Control.AddOptionPercentSlider(CCVars.SpeechBubbleSpeakerOpacity, SpeechBubbleSpeakerOpacitySlider);
+ Control.AddOptionPercentSlider(CCVars.SpeechBubbleBackgroundOpacity, SpeechBubbleBackgroundOpacitySlider);
+
+ Control.AddOptionCheckBox(CCVars.AccessibilityClientCensorNudity, CensorNudityCheckBox);
Control.Initialize();
}
diff --git a/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml b/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml
new file mode 100644
index 0000000000..38666b7ec1
--- /dev/null
+++ b/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml.cs b/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml.cs
new file mode 100644
index 0000000000..9661adfa20
--- /dev/null
+++ b/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml.cs
@@ -0,0 +1,20 @@
+using Content.Shared.CCVar;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Options.UI.Tabs;
+
+[GenerateTypedNameReferences]
+public sealed partial class AdminOptionsTab : Control
+{
+ public AdminOptionsTab()
+ {
+ RobustXamlLoader.Load(this);
+
+ Control.AddOptionCheckBox(CCVars.AdminOverlayClassic, EnableClassicOverlayCheckBox);
+
+ Control.Initialize();
+ }
+}
+
diff --git a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs
index 24be904e06..d934d331fb 100644
--- a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs
+++ b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs
@@ -265,6 +265,51 @@ namespace Content.Client.Options.UI.Tabs
AddButton(EngineKeyFunctions.HideUI);
AddButton(ContentKeyFunctions.InspectEntity);
+ AddHeader("ui-options-header-text-cursor");
+ AddButton(EngineKeyFunctions.TextCursorLeft);
+ AddButton(EngineKeyFunctions.TextCursorRight);
+ AddButton(EngineKeyFunctions.TextCursorUp);
+ AddButton(EngineKeyFunctions.TextCursorDown);
+ AddButton(EngineKeyFunctions.TextCursorWordLeft);
+ AddButton(EngineKeyFunctions.TextCursorWordRight);
+ AddButton(EngineKeyFunctions.TextCursorBegin);
+ AddButton(EngineKeyFunctions.TextCursorEnd);
+
+ AddHeader("ui-options-header-text-cursor-select");
+ AddButton(EngineKeyFunctions.TextCursorSelect);
+ AddButton(EngineKeyFunctions.TextCursorSelectLeft);
+ AddButton(EngineKeyFunctions.TextCursorSelectRight);
+ AddButton(EngineKeyFunctions.TextCursorSelectUp);
+ AddButton(EngineKeyFunctions.TextCursorSelectDown);
+ AddButton(EngineKeyFunctions.TextCursorSelectWordLeft);
+ AddButton(EngineKeyFunctions.TextCursorSelectWordRight);
+ AddButton(EngineKeyFunctions.TextCursorSelectBegin);
+ AddButton(EngineKeyFunctions.TextCursorSelectEnd);
+
+ AddHeader("ui-options-header-text-edit");
+ AddButton(EngineKeyFunctions.TextBackspace);
+ AddButton(EngineKeyFunctions.TextDelete);
+ AddButton(EngineKeyFunctions.TextWordBackspace);
+ AddButton(EngineKeyFunctions.TextWordDelete);
+ AddButton(EngineKeyFunctions.TextNewline);
+ AddButton(EngineKeyFunctions.TextSubmit);
+ AddButton(EngineKeyFunctions.MultilineTextSubmit);
+ AddButton(EngineKeyFunctions.TextSelectAll);
+ AddButton(EngineKeyFunctions.TextCopy);
+ AddButton(EngineKeyFunctions.TextCut);
+ AddButton(EngineKeyFunctions.TextPaste);
+
+ AddHeader("ui-options-header-text-chat");
+ AddButton(EngineKeyFunctions.TextHistoryPrev);
+ AddButton(EngineKeyFunctions.TextHistoryNext);
+ AddButton(EngineKeyFunctions.TextReleaseFocus);
+ AddButton(EngineKeyFunctions.TextScrollToBottom);
+
+ AddHeader("ui-options-header-text-other");
+ AddButton(EngineKeyFunctions.TextTabComplete);
+ AddButton(EngineKeyFunctions.TextCompleteNext);
+ AddButton(EngineKeyFunctions.TextCompletePrev);
+
foreach (var control in _keyControls.Values)
{
UpdateKeyControl(control);
diff --git a/Content.Client/Parallax/Data/ParallaxPrototype.cs b/Content.Client/Parallax/Data/ParallaxPrototype.cs
index 034d8a3da9..bab14f0be8 100644
--- a/Content.Client/Parallax/Data/ParallaxPrototype.cs
+++ b/Content.Client/Parallax/Data/ParallaxPrototype.cs
@@ -5,7 +5,7 @@ namespace Content.Client.Parallax.Data;
///
/// Prototype data for a parallax.
///
-[Prototype("parallax")]
+[Prototype]
public sealed partial class ParallaxPrototype : IPrototype
{
///
diff --git a/Content.Client/Power/EntitySystems/PowerReceiverSystem.cs b/Content.Client/Power/EntitySystems/PowerReceiverSystem.cs
index ebf6c18c95..319ab5c56d 100644
--- a/Content.Client/Power/EntitySystems/PowerReceiverSystem.cs
+++ b/Content.Client/Power/EntitySystems/PowerReceiverSystem.cs
@@ -27,6 +27,8 @@ public sealed class PowerReceiverSystem : SharedPowerReceiverSystem
return;
component.Powered = state.Powered;
+ component.NeedsPower = state.NeedsPower;
+ component.PowerDisabled = state.PowerDisabled;
}
public override bool ResolveApc(EntityUid entity, [NotNullWhen(true)] ref SharedApcPowerReceiverComponent? component)
diff --git a/Content.Client/SubFloor/SubFloorHideSystem.cs b/Content.Client/SubFloor/SubFloorHideSystem.cs
index 64d3afc640..997d375fbe 100644
--- a/Content.Client/SubFloor/SubFloorHideSystem.cs
+++ b/Content.Client/SubFloor/SubFloorHideSystem.cs
@@ -1,12 +1,15 @@
-using Content.Shared.DrawDepth;
+using Content.Client.UserInterface.Systems.Sandbox;
using Content.Shared.SubFloor;
using Robust.Client.GameObjects;
+using Robust.Client.UserInterface;
+using Robust.Shared.Player;
namespace Content.Client.SubFloor;
public sealed class SubFloorHideSystem : SharedSubFloorHideSystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+ [Dependency] private readonly IUserInterfaceManager _ui = default!;
private bool _showAll;
@@ -18,8 +21,13 @@ public sealed class SubFloorHideSystem : SharedSubFloorHideSystem
{
if (_showAll == value) return;
_showAll = value;
+ _ui.GetUIController().SetToggleSubfloors(value);
- UpdateAll();
+ var ev = new ShowSubfloorRequestEvent()
+ {
+ Value = value,
+ };
+ RaiseNetworkEvent(ev);
}
}
@@ -28,6 +36,20 @@ public sealed class SubFloorHideSystem : SharedSubFloorHideSystem
base.Initialize();
SubscribeLocalEvent(OnAppearanceChanged);
+ SubscribeNetworkEvent(OnRequestReceived);
+ SubscribeLocalEvent(OnPlayerDetached);
+ }
+
+ private void OnPlayerDetached(LocalPlayerDetachedEvent ev)
+ {
+ // Vismask resets so need to reset this.
+ ShowAll = false;
+ }
+
+ private void OnRequestReceived(ShowSubfloorRequestEvent ev)
+ {
+ // When client receives request Queue an update on all vis.
+ UpdateAll();
}
private void OnAppearanceChanged(EntityUid uid, SubFloorHideComponent component, ref AppearanceChangeEvent args)
@@ -64,11 +86,20 @@ public sealed class SubFloorHideSystem : SharedSubFloorHideSystem
args.Sprite.Visible = hasVisibleLayer || revealed;
- // allows a t-ray to show wires/pipes above carpets/puddles
- if (scannerRevealed)
+ if (ShowAll)
{
+ // Allows sandbox mode to make wires visible over other stuff.
component.OriginalDrawDepth ??= args.Sprite.DrawDepth;
- args.Sprite.DrawDepth = (int) Shared.DrawDepth.DrawDepth.FloorObjects + 1;
+ args.Sprite.DrawDepth = (int)Shared.DrawDepth.DrawDepth.Overdoors;
+ }
+ else if (scannerRevealed)
+ {
+ // Allows a t-ray to show wires/pipes above carpets/puddles.
+ if (component.OriginalDrawDepth is not null)
+ return;
+ component.OriginalDrawDepth = args.Sprite.DrawDepth;
+ var drawDepthDifference = Shared.DrawDepth.DrawDepth.ThickPipe - Shared.DrawDepth.DrawDepth.Puddles;
+ args.Sprite.DrawDepth -= drawDepthDifference - 1;
}
else if (component.OriginalDrawDepth.HasValue)
{
diff --git a/Content.Client/SubFloor/TrayScanRevealSystem.cs b/Content.Client/SubFloor/TrayScanRevealSystem.cs
new file mode 100644
index 0000000000..ca3e91a7dc
--- /dev/null
+++ b/Content.Client/SubFloor/TrayScanRevealSystem.cs
@@ -0,0 +1,29 @@
+using System.Linq;
+using Content.Shared.SubFloor;
+using Robust.Shared.Map.Components;
+
+namespace Content.Client.SubFloor;
+
+public sealed class TrayScanRevealSystem : EntitySystem
+{
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly SharedMapSystem _map = default!;
+
+ public bool IsUnderRevealingEntity(EntityUid uid)
+ {
+ var gridUid = _transform.GetGrid(uid);
+ if (gridUid is null)
+ return false;
+
+ var gridComp = Comp(gridUid.Value);
+ var position = _transform.GetGridOrMapTilePosition(uid);
+
+ return HasTrayScanReveal(((EntityUid)gridUid, gridComp), position);
+ }
+
+ private bool HasTrayScanReveal(Entity ent, Vector2i position)
+ {
+ var anchoredEnum = _map.GetAnchoredEntities(ent, position);
+ return anchoredEnum.Any(HasComp);
+ }
+}
diff --git a/Content.Client/SubFloor/TrayScannerSystem.cs b/Content.Client/SubFloor/TrayScannerSystem.cs
index 95bce03c0f..f65c784025 100644
--- a/Content.Client/SubFloor/TrayScannerSystem.cs
+++ b/Content.Client/SubFloor/TrayScannerSystem.cs
@@ -19,6 +19,7 @@ public sealed class TrayScannerSystem : SharedTrayScannerSystem
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly TrayScanRevealSystem _trayScanReveal = default!;
private const string TRayAnimationKey = "trays";
private const double AnimationLength = 0.3;
@@ -82,7 +83,7 @@ public sealed class TrayScannerSystem : SharedTrayScannerSystem
foreach (var (uid, comp) in inRange)
{
- if (comp.IsUnderCover)
+ if (comp.IsUnderCover || _trayScanReveal.IsUnderRevealingEntity(uid))
EnsureComp(uid);
}
}
diff --git a/Content.Client/Toggleable/ToggleableLightVisualsSystem.cs b/Content.Client/Toggleable/ToggleableLightVisualsSystem.cs
index 3507c786b9..a3d35ffeed 100644
--- a/Content.Client/Toggleable/ToggleableLightVisualsSystem.cs
+++ b/Content.Client/Toggleable/ToggleableLightVisualsSystem.cs
@@ -2,6 +2,7 @@ using Content.Client.Clothing;
using Content.Client.Items.Systems;
using Content.Shared.Clothing;
using Content.Shared.Hands;
+using Content.Shared.Inventory;
using Content.Shared.Item;
using Content.Shared.Toggleable;
using Robust.Client.GameObjects;
@@ -62,7 +63,16 @@ public sealed class ToggleableLightVisualsSystem : VisualizerSystem? layers = null;
+
+ // attempt to get species specific data
+ if (inventory.SpeciesId != null)
+ component.ClothingVisuals.TryGetValue($"{args.Slot}-{inventory.SpeciesId}", out layers);
+
+ // No species specific data. Try to default to generic data.
+ if (layers == null && !component.ClothingVisuals.TryGetValue(args.Slot, out layers))
return;
var modulate = AppearanceSystem.TryGetData(uid, ToggleableLightVisuals.Color, out var color, appearance);
diff --git a/Content.Client/UserInterface/Controls/ListContainer.cs b/Content.Client/UserInterface/Controls/ListContainer.cs
index e1b3b948f0..0ee0a67af0 100644
--- a/Content.Client/UserInterface/Controls/ListContainer.cs
+++ b/Content.Client/UserInterface/Controls/ListContainer.cs
@@ -96,9 +96,12 @@ public class ListContainer : Control
{
ListContainerButton control = new(data[0], 0);
GenerateItem?.Invoke(data[0], control);
+ // Yes this AddChild is necessary for reasons (get proper style or whatever?)
+ // without it the DesiredSize may be different to the final DesiredSize.
+ AddChild(control);
control.Measure(Vector2Helpers.Infinity);
_itemHeight = control.DesiredSize.Y;
- control.Dispose();
+ control.Orphan();
}
// Ensure buttons are re-generated.
@@ -384,6 +387,7 @@ public sealed class ListContainerButton : ContainerButton, IEntityControl
public ListContainerButton(ListData data, int index)
{
+ AddStyleClass(StyleClassButton);
Data = data;
Index = index;
// AddChild(Background = new PanelContainer
diff --git a/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs b/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs
index 8f66340a30..8d96a5d670 100644
--- a/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs
+++ b/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs
@@ -467,8 +467,9 @@ public sealed class ChatUIController : UIController
if (existing.Count > SpeechBubbleCap)
{
- // Get the oldest to start fading fast.
- var last = existing[0];
+ // Get the next speech bubble to fade
+ // Any speech bubbles before it are already fading
+ var last = existing[^(SpeechBubbleCap + 1)];
last.FadeNow();
}
}
diff --git a/Content.Client/UserInterface/Systems/CloseWindow/CloseRecentWindowUIController.cs b/Content.Client/UserInterface/Systems/CloseWindow/CloseRecentWindowUIController.cs
index 3517a001c2..0e648c25a0 100644
--- a/Content.Client/UserInterface/Systems/CloseWindow/CloseRecentWindowUIController.cs
+++ b/Content.Client/UserInterface/Systems/CloseWindow/CloseRecentWindowUIController.cs
@@ -74,7 +74,7 @@ public sealed class CloseRecentWindowUIController : UIController
/// internal recentlyInteractedWindows tracking.
///
///
- private void SetMostRecentlyInteractedWindow(BaseWindow window)
+ public void SetMostRecentlyInteractedWindow(BaseWindow window)
{
// Search through the list and see if already added.
// (This search is backwards since it's fairly common that the user is clicking the same
@@ -134,7 +134,6 @@ public sealed class CloseRecentWindowUIController : UIController
if (window.IsOpen)
return true;
- recentlyInteractedWindows.RemoveAt(i);
// continue going down the list, hoping to find a still-open window
}
diff --git a/Content.Client/UserInterface/Systems/DamageOverlays/DamageOverlayUiController.cs b/Content.Client/UserInterface/Systems/DamageOverlays/DamageOverlayUiController.cs
index 9d9dd30911..55d00fec18 100644
--- a/Content.Client/UserInterface/Systems/DamageOverlays/DamageOverlayUiController.cs
+++ b/Content.Client/UserInterface/Systems/DamageOverlays/DamageOverlayUiController.cs
@@ -67,7 +67,7 @@ public sealed class DamageOverlayUiController : UIController
{
_overlay.DeadLevel = 0f;
_overlay.CritLevel = 0f;
- _overlay.BruteLevel = 0f;
+ _overlay.PainLevel = 0f;
_overlay.OxygenLevel = 0f;
}
@@ -95,13 +95,22 @@ public sealed class DamageOverlayUiController : UIController
{
case MobState.Alive:
{
- if (EntityManager.HasComponent(entity))
+ FixedPoint2 painLevel = 0;
+ _overlay.PainLevel = 0;
+
+ if (!EntityManager.HasComponent(entity))
{
- _overlay.BruteLevel = 0;
- }
- else if (damageable.DamagePerGroup.TryGetValue("Brute", out var bruteDamage))
- {
- _overlay.BruteLevel = FixedPoint2.Min(1f, bruteDamage / critThreshold).Float();
+ foreach (var painDamageType in damageable.PainDamageGroups)
+ {
+ damageable.DamagePerGroup.TryGetValue(painDamageType, out var painDamage);
+ painLevel += painDamage;
+ }
+ _overlay.PainLevel = FixedPoint2.Min(1f, painLevel / critThreshold).Float();
+
+ if (_overlay.PainLevel < 0.05f) // Don't show damage overlay if they're near enough to max.
+ {
+ _overlay.PainLevel = 0;
+ }
}
if (damageable.DamagePerGroup.TryGetValue("Airloss", out var oxyDamage))
@@ -109,11 +118,6 @@ public sealed class DamageOverlayUiController : UIController
_overlay.OxygenLevel = FixedPoint2.Min(1f, oxyDamage / critThreshold).Float();
}
- if (_overlay.BruteLevel < 0.05f) // Don't show damage overlay if they're near enough to max.
- {
- _overlay.BruteLevel = 0;
- }
-
_overlay.CritLevel = 0;
_overlay.DeadLevel = 0;
break;
@@ -125,13 +129,13 @@ public sealed class DamageOverlayUiController : UIController
return;
_overlay.CritLevel = critLevel.Value.Float();
- _overlay.BruteLevel = 0;
+ _overlay.PainLevel = 0;
_overlay.DeadLevel = 0;
break;
}
case MobState.Dead:
{
- _overlay.BruteLevel = 0;
+ _overlay.PainLevel = 0;
_overlay.CritLevel = 0;
break;
}
diff --git a/Content.Client/UserInterface/Systems/DamageOverlays/Overlays/DamageOverlay.cs b/Content.Client/UserInterface/Systems/DamageOverlays/Overlays/DamageOverlay.cs
index fd74815316..de70fe16f8 100644
--- a/Content.Client/UserInterface/Systems/DamageOverlays/Overlays/DamageOverlay.cs
+++ b/Content.Client/UserInterface/Systems/DamageOverlays/Overlays/DamageOverlay.cs
@@ -25,9 +25,9 @@ public sealed class DamageOverlay : Overlay
///
/// Handles the red pulsing overlay
///
- public float BruteLevel = 0f;
+ public float PainLevel = 0f;
- private float _oldBruteLevel = 0f;
+ private float _oldPainLevel = 0f;
///
/// Handles the darkening overlay.
@@ -92,14 +92,14 @@ public sealed class DamageOverlay : Overlay
DeadLevel = 0f;
}
- if (!MathHelper.CloseTo(_oldBruteLevel, BruteLevel, 0.001f))
+ if (!MathHelper.CloseTo(_oldPainLevel, PainLevel, 0.001f))
{
- var diff = BruteLevel - _oldBruteLevel;
- _oldBruteLevel += GetDiff(diff, lastFrameTime);
+ var diff = PainLevel - _oldPainLevel;
+ _oldPainLevel += GetDiff(diff, lastFrameTime);
}
else
{
- _oldBruteLevel = BruteLevel;
+ _oldPainLevel = PainLevel;
}
if (!MathHelper.CloseTo(_oldOxygenLevel, OxygenLevel, 0.001f))
@@ -135,7 +135,7 @@ public sealed class DamageOverlay : Overlay
// Makes debugging easier don't @ me
float level = 0f;
- level = _oldBruteLevel;
+ level = _oldPainLevel;
// TODO: Lerping
if (level > 0f && _oldCritLevel <= 0f)
@@ -165,7 +165,7 @@ public sealed class DamageOverlay : Overlay
}
else
{
- _oldBruteLevel = BruteLevel;
+ _oldPainLevel = PainLevel;
}
level = State != MobState.Critical ? _oldOxygenLevel : 1f;
diff --git a/Content.Client/UserInterface/Systems/Ghost/Widgets/GhostGui.xaml.cs b/Content.Client/UserInterface/Systems/Ghost/Widgets/GhostGui.xaml.cs
index 0f64e8a275..db16895d59 100644
--- a/Content.Client/UserInterface/Systems/Ghost/Widgets/GhostGui.xaml.cs
+++ b/Content.Client/UserInterface/Systems/Ghost/Widgets/GhostGui.xaml.cs
@@ -14,6 +14,7 @@ public sealed partial class GhostGui : UIWidget
public event Action? RequestWarpsPressed;
public event Action? ReturnToBodyPressed;
public event Action? GhostRolesPressed;
+ private int _prevNumberRoles;
public GhostGui()
{
@@ -26,6 +27,7 @@ public sealed partial class GhostGui : UIWidget
GhostWarpButton.OnPressed += _ => RequestWarpsPressed?.Invoke();
ReturnToBodyButton.OnPressed += _ => ReturnToBodyPressed?.Invoke();
GhostRolesButton.OnPressed += _ => GhostRolesPressed?.Invoke();
+ GhostRolesButton.OnPressed += _ => GhostRolesButton.StyleClasses.Remove(StyleBase.ButtonCaution);
}
public void Hide()
@@ -41,14 +43,13 @@ public sealed partial class GhostGui : UIWidget
if (roles != null)
{
GhostRolesButton.Text = Loc.GetString("ghost-gui-ghost-roles-button", ("count", roles));
- if (roles > 0)
+
+ if (roles > _prevNumberRoles)
{
GhostRolesButton.StyleClasses.Add(StyleBase.ButtonCaution);
}
- else
- {
- GhostRolesButton.StyleClasses.Remove(StyleBase.ButtonCaution);
- }
+
+ _prevNumberRoles = (int)roles;
}
TargetWindow.Populate();
diff --git a/Content.Client/UserInterface/Systems/Sandbox/SandboxUIController.cs b/Content.Client/UserInterface/Systems/Sandbox/SandboxUIController.cs
index 1924bf2b24..037cc99754 100644
--- a/Content.Client/UserInterface/Systems/Sandbox/SandboxUIController.cs
+++ b/Content.Client/UserInterface/Systems/Sandbox/SandboxUIController.cs
@@ -39,7 +39,6 @@ public sealed class SandboxUIController : UIController, IOnStateChanged { SandboxButton!.Pressed = true; };
_window.OnClose += () => { SandboxButton!.Pressed = false; };
+
+ // TODO: These need moving to opened so at least if they're not synced properly on open they work.
_window.ToggleLightButton.Pressed = !_light.Enabled;
_window.ToggleFovButton.Pressed = !_eye.CurrentEye.DrawFov;
_window.ToggleShadowsButton.Pressed = !_light.DrawShadows;
- _window.ToggleSubfloorButton.Pressed = _subfloorHide.ShowAll;
_window.ShowMarkersButton.Pressed = _marker.MarkersVisible;
_window.ShowBbButton.Pressed = (_debugPhysics.Flags & PhysicsDebugFlags.Shapes) != 0x0;
@@ -219,4 +219,16 @@ public sealed class SandboxUIController : UIController, IOnStateChanged().ShowAll;
}
}
diff --git a/Content.Client/UserInterface/Systems/Storage/StorageUIController.cs b/Content.Client/UserInterface/Systems/Storage/StorageUIController.cs
index 768a434d83..f0c8cfeb21 100644
--- a/Content.Client/UserInterface/Systems/Storage/StorageUIController.cs
+++ b/Content.Client/UserInterface/Systems/Storage/StorageUIController.cs
@@ -5,6 +5,7 @@ using Content.Client.Interaction;
using Content.Client.Storage;
using Content.Client.Storage.Systems;
using Content.Client.UserInterface.Systems.Hotbar.Widgets;
+using Content.Client.UserInterface.Systems.Info;
using Content.Client.UserInterface.Systems.Storage.Controls;
using Content.Client.Verbs.UI;
using Content.Shared.CCVar;
@@ -37,6 +38,7 @@ public sealed class StorageUIController : UIController, IOnSystemChanged()?.StorageContainer.AddChild(window);
+ _closeRecentWindowUIController.SetMostRecentlyInteractedWindow(window);
}
else
{
diff --git a/Content.Client/VendingMachines/UI/VendingMachineItem.xaml.cs b/Content.Client/VendingMachines/UI/VendingMachineItem.xaml.cs
index a7212934fd..0f0564c596 100644
--- a/Content.Client/VendingMachines/UI/VendingMachineItem.xaml.cs
+++ b/Content.Client/VendingMachines/UI/VendingMachineItem.xaml.cs
@@ -16,4 +16,9 @@ public sealed partial class VendingMachineItem : BoxContainer
NameLabel.Text = text;
}
+
+ public void SetText(string text)
+ {
+ NameLabel.Text = text;
+ }
}
diff --git a/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs b/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs
index ee7a0e41fa..899a0208cb 100644
--- a/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs
+++ b/Content.Client/VendingMachines/UI/VendingMachineMenu.xaml.cs
@@ -1,3 +1,4 @@
+using System.Linq;
using System.Numerics;
using Content.Shared.VendingMachines;
using Robust.Client.AutoGenerated;
@@ -19,11 +20,16 @@ namespace Content.Client.VendingMachines.UI
[Dependency] private readonly IEntityManager _entityManager = default!;
private readonly Dictionary _dummies = [];
+ private readonly Dictionary _listItems = new();
+ private readonly Dictionary _amounts = new();
+
+ ///
+ /// Whether the vending machine is able to be interacted with or not.
+ ///
+ private bool _enabled;
public event Action? OnItemSelected;
- private readonly StyleBoxFlat _styleBox = new() { BackgroundColor = new Color(70, 73, 102) };
-
public VendingMachineMenu()
{
MinSize = SetSize = new Vector2(250, 150);
@@ -68,18 +74,23 @@ namespace Content.Client.VendingMachines.UI
if (data is not VendorItemsListData { ItemProtoID: var protoID, ItemText: var text })
return;
- button.AddChild(new VendingMachineItem(protoID, text));
-
- button.ToolTip = text;
- button.StyleBoxOverride = _styleBox;
+ var item = new VendingMachineItem(protoID, text);
+ _listItems[protoID] = (button, item);
+ button.AddChild(item);
+ button.AddStyleClass("ButtonSquare");
+ button.Disabled = !_enabled || _amounts[protoID] == 0;
}
///
/// Populates the list of available items on the vending machine interface
/// and sets icons based on their prototypes
///
- public void Populate(List inventory)
+ public void Populate(List inventory, bool enabled)
{
+ _enabled = enabled;
+ _listItems.Clear();
+ _amounts.Clear();
+
if (inventory.Count == 0 && VendingContents.Visible)
{
SearchBar.Visible = false;
@@ -109,7 +120,10 @@ namespace Content.Client.VendingMachines.UI
var entry = inventory[i];
if (!_prototypeManager.TryIndex(entry.ID, out var prototype))
+ {
+ _amounts[entry.ID] = 0;
continue;
+ }
if (!_dummies.TryGetValue(entry.ID, out var dummy))
{
@@ -119,11 +133,15 @@ namespace Content.Client.VendingMachines.UI
var itemName = Identity.Name(dummy, _entityManager);
var itemText = $"{itemName} [{entry.Amount}]";
+ _amounts[entry.ID] = entry.Amount;
if (itemText.Length > longestEntry.Length)
longestEntry = itemText;
- listData.Add(new VendorItemsListData(prototype.ID, itemText, i));
+ listData.Add(new VendorItemsListData(prototype.ID, i)
+ {
+ ItemText = itemText,
+ });
}
VendingContents.PopulateList(listData);
@@ -131,12 +149,43 @@ namespace Content.Client.VendingMachines.UI
SetSizeAfterUpdate(longestEntry.Length, inventory.Count);
}
+ ///
+ /// Updates text entries for vending data in place without modifying the list controls.
+ ///
+ public void UpdateAmounts(List cachedInventory, bool enabled)
+ {
+ _enabled = enabled;
+
+ foreach (var proto in _dummies.Keys)
+ {
+ if (!_listItems.TryGetValue(proto, out var button))
+ continue;
+
+ var dummy = _dummies[proto];
+ var amount = cachedInventory.First(o => o.ID == proto).Amount;
+ // Could be better? Problem is all inventory entries get squashed.
+ var text = GetItemText(dummy, amount);
+
+ button.Item.SetText(text);
+ button.Button.Disabled = !enabled || amount == 0;
+ }
+ }
+
+ private string GetItemText(EntityUid dummy, uint amount)
+ {
+ var itemName = Identity.Name(dummy, _entityManager);
+ return $"{itemName} [{amount}]";
+ }
+
private void SetSizeAfterUpdate(int longestEntryLength, int contentCount)
{
SetSize = new Vector2(Math.Clamp((longestEntryLength + 2) * 12, 250, 400),
Math.Clamp(contentCount * 50, 150, 350));
}
}
-}
-public record VendorItemsListData(EntProtoId ItemProtoID, string ItemText, int ItemIndex) : ListData;
+ public record VendorItemsListData(EntProtoId ItemProtoID, int ItemIndex) : ListData
+ {
+ public string ItemText = string.Empty;
+ }
+}
diff --git a/Content.Client/VendingMachines/VendingMachineBoundUserInterface.cs b/Content.Client/VendingMachines/VendingMachineBoundUserInterface.cs
index 052bdacb89..874808158d 100644
--- a/Content.Client/VendingMachines/VendingMachineBoundUserInterface.cs
+++ b/Content.Client/VendingMachines/VendingMachineBoundUserInterface.cs
@@ -31,10 +31,21 @@ namespace Content.Client.VendingMachines
public void Refresh()
{
+ var enabled = EntMan.TryGetComponent(Owner, out VendingMachineComponent? bendy) && !bendy.Ejecting;
+
var system = EntMan.System();
_cachedInventory = system.GetAllInventory(Owner);
- _menu?.Populate(_cachedInventory);
+ _menu?.Populate(_cachedInventory, enabled);
+ }
+
+ public void UpdateAmounts()
+ {
+ var enabled = EntMan.TryGetComponent(Owner, out VendingMachineComponent? bendy) && !bendy.Ejecting;
+
+ var system = EntMan.System();
+ _cachedInventory = system.GetAllInventory(Owner);
+ _menu?.UpdateAmounts(_cachedInventory, enabled);
}
private void OnItemSelected(GUIBoundKeyEventArgs args, ListData data)
@@ -53,7 +64,7 @@ namespace Content.Client.VendingMachines
if (selectedItem == null)
return;
- SendMessage(new VendingMachineEjectMessage(selectedItem.Type, selectedItem.ID));
+ SendPredictedMessage(new VendingMachineEjectMessage(selectedItem.Type, selectedItem.ID));
}
protected override void Dispose(bool disposing)
diff --git a/Content.Client/VendingMachines/VendingMachineSystem.cs b/Content.Client/VendingMachines/VendingMachineSystem.cs
index 1b1dde2b67..130296c8a1 100644
--- a/Content.Client/VendingMachines/VendingMachineSystem.cs
+++ b/Content.Client/VendingMachines/VendingMachineSystem.cs
@@ -1,6 +1,8 @@
+using System.Linq;
using Content.Shared.VendingMachines;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
+using Robust.Shared.GameStates;
namespace Content.Client.VendingMachines;
@@ -8,7 +10,6 @@ public sealed class VendingMachineSystem : SharedVendingMachineSystem
{
[Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
- [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
public override void Initialize()
{
@@ -16,14 +17,69 @@ public sealed class VendingMachineSystem : SharedVendingMachineSystem
SubscribeLocalEvent(OnAppearanceChange);
SubscribeLocalEvent(OnAnimationCompleted);
- SubscribeLocalEvent(OnVendingAfterState);
+ SubscribeLocalEvent(OnVendingHandleState);
}
- private void OnVendingAfterState(EntityUid uid, VendingMachineComponent component, ref AfterAutoHandleStateEvent args)
+ private void OnVendingHandleState(Entity entity, ref ComponentHandleState args)
{
- if (_uiSystem.TryGetOpenUi(uid, VendingMachineUiKey.Key, out var bui))
+ if (args.Current is not VendingMachineComponentState state)
+ return;
+
+ var uid = entity.Owner;
+ var component = entity.Comp;
+
+ component.Contraband = state.Contraband;
+ component.EjectEnd = state.EjectEnd;
+ component.DenyEnd = state.DenyEnd;
+ component.DispenseOnHitEnd = state.DispenseOnHitEnd;
+
+ // If all we did was update amounts then we can leave BUI buttons in place.
+ var fullUiUpdate = !component.Inventory.Keys.SequenceEqual(state.Inventory.Keys) ||
+ !component.EmaggedInventory.Keys.SequenceEqual(state.EmaggedInventory.Keys) ||
+ !component.ContrabandInventory.Keys.SequenceEqual(state.ContrabandInventory.Keys);
+
+ component.Inventory.Clear();
+ component.EmaggedInventory.Clear();
+ component.ContrabandInventory.Clear();
+
+ foreach (var entry in state.Inventory)
{
- bui.Refresh();
+ component.Inventory.Add(entry.Key, new(entry.Value));
+ }
+
+ foreach (var entry in state.EmaggedInventory)
+ {
+ component.EmaggedInventory.Add(entry.Key, new(entry.Value));
+ }
+
+ foreach (var entry in state.ContrabandInventory)
+ {
+ component.ContrabandInventory.Add(entry.Key, new(entry.Value));
+ }
+
+ if (UISystem.TryGetOpenUi(uid, VendingMachineUiKey.Key, out var bui))
+ {
+ if (fullUiUpdate)
+ {
+ bui.Refresh();
+ }
+ else
+ {
+ bui.UpdateAmounts();
+ }
+ }
+ }
+
+ protected override void UpdateUI(Entity entity)
+ {
+ if (!Resolve(entity, ref entity.Comp))
+ return;
+
+ if (UISystem.TryGetOpenUi(entity.Owner,
+ VendingMachineUiKey.Key,
+ out var bui))
+ {
+ bui.UpdateAmounts();
}
}
@@ -70,13 +126,13 @@ public sealed class VendingMachineSystem : SharedVendingMachineSystem
if (component.LoopDenyAnimation)
SetLayerState(VendingMachineVisualLayers.BaseUnshaded, component.DenyState, sprite);
else
- PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.DenyState, component.DenyDelay, sprite);
+ PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.DenyState, (float)component.DenyDelay.TotalSeconds, sprite);
SetLayerState(VendingMachineVisualLayers.Screen, component.ScreenState, sprite);
break;
case VendingMachineVisualState.Eject:
- PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.EjectState, component.EjectDelay, sprite);
+ PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.EjectState, (float)component.EjectDelay.TotalSeconds, sprite);
SetLayerState(VendingMachineVisualLayers.Screen, component.ScreenState, sprite);
break;
diff --git a/Content.Client/Verbs/UI/VerbMenuElement.cs b/Content.Client/Verbs/UI/VerbMenuElement.cs
index 1815144f7d..f1f8972c45 100644
--- a/Content.Client/Verbs/UI/VerbMenuElement.cs
+++ b/Content.Client/Verbs/UI/VerbMenuElement.cs
@@ -3,10 +3,12 @@ using Content.Client.ContextMenu.UI;
using Content.Shared.Verbs;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
using Robust.Client.Utility;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
+using Robust.Shared.Utility;
namespace Content.Client.Verbs.UI
{
@@ -27,7 +29,17 @@ namespace Content.Client.Verbs.UI
public VerbMenuElement(Verb verb) : base(verb.Text)
{
- ToolTip = verb.Message;
+ TooltipSupplier = sender =>
+ {
+ var label = new RichTextLabel();
+ label.SetMessage(FormattedMessage.FromMarkupOrThrow(verb.Message ?? verb.Text));
+
+ var tooltip = new Tooltip();
+ tooltip.GetChild(0).Children.Clear();
+ tooltip.GetChild(0).Children.Add(label);
+
+ return tooltip;
+ };
Disabled = verb.Disabled;
Verb = verb;
diff --git a/Content.Client/Verbs/VerbSystem.cs b/Content.Client/Verbs/VerbSystem.cs
index f592303d28..d6513c0a78 100644
--- a/Content.Client/Verbs/VerbSystem.cs
+++ b/Content.Client/Verbs/VerbSystem.cs
@@ -213,7 +213,7 @@ namespace Content.Client.Verbs
{
// maybe send an informative pop-up message.
if (!string.IsNullOrWhiteSpace(verb.Message))
- _popupSystem.PopupEntity(verb.Message, user);
+ _popupSystem.PopupEntity(FormattedMessage.RemoveMarkupOrThrow(verb.Message), user);
return;
}
diff --git a/Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs b/Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs
index 74d0e92421..05e8197c8d 100644
--- a/Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs
+++ b/Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs
@@ -54,6 +54,9 @@ public sealed class CraftingTests : InteractionTest
await CraftItem(Spear);
await FindEntity(Spear);
+ // Reset target because entitylookup will dump this.
+ Target = null;
+
// Player's hands should be full of the remaining rods, except those dropped during the failed crafting attempt.
// Spear and left over stacks should be on the floor.
await AssertEntityLookup((Rod, 2), (Cable, 7), (ShardGlass, 2), (Spear, 1));
diff --git a/Content.IntegrationTests/Tests/ContrabandTest.cs b/Content.IntegrationTests/Tests/ContrabandTest.cs
index ebd6afa7ef..a33e7c2067 100644
--- a/Content.IntegrationTests/Tests/ContrabandTest.cs
+++ b/Content.IntegrationTests/Tests/ContrabandTest.cs
@@ -17,23 +17,26 @@ public sealed class ContrabandTest
await client.WaitAssertion(() =>
{
- foreach (var proto in protoMan.EnumeratePrototypes())
+ Assert.Multiple(() =>
{
- if (proto.Abstract || pair.IsTestPrototype(proto))
- continue;
+ foreach (var proto in protoMan.EnumeratePrototypes())
+ {
+ if (proto.Abstract || pair.IsTestPrototype(proto))
+ continue;
- if (!proto.TryGetComponent(out var contraband, componentFactory))
- continue;
+ if (!proto.TryGetComponent(out var contraband, componentFactory))
+ continue;
- Assert.That(protoMan.TryIndex(contraband.Severity, out var severity, false),
- @$"{proto.ID} has a ContrabandComponent with a unknown severity.");
+ Assert.That(protoMan.TryIndex(contraband.Severity, out var severity, false),
+ @$"{proto.ID} has a ContrabandComponent with a unknown severity.");
- if (!severity.ShowDepartmentsAndJobs)
- continue;
+ if (!severity.ShowDepartmentsAndJobs)
+ continue;
- Assert.That(contraband.AllowedDepartments.Count + contraband.AllowedJobs.Count, Is.Not.EqualTo(0),
- @$"{proto.ID} has a ContrabandComponent with ShowDepartmentsAndJobs but no allowed departments or jobs.");
- }
+ Assert.That(contraband.AllowedDepartments.Count + contraband.AllowedJobs.Count, Is.Not.EqualTo(0),
+ @$"{proto.ID} has a ContrabandComponent with ShowDepartmentsAndJobs but no allowed departments or jobs.");
+ }
+ });
});
await pair.CleanReturnAsync();
diff --git a/Content.IntegrationTests/Tests/DeviceLinking/DeviceLinkingTest.cs b/Content.IntegrationTests/Tests/DeviceLinking/DeviceLinkingTest.cs
new file mode 100644
index 0000000000..6ce6d5d78e
--- /dev/null
+++ b/Content.IntegrationTests/Tests/DeviceLinking/DeviceLinkingTest.cs
@@ -0,0 +1,94 @@
+using System.Collections.Generic;
+using System.Linq;
+using Content.Server.DeviceLinking.Systems;
+using Content.Shared.DeviceLinking;
+using Content.Shared.Prototypes;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Maths;
+using Robust.Shared.Prototypes;
+
+namespace Content.IntegrationTests.Tests.DeviceLinking;
+
+public sealed class DeviceLinkingTest
+{
+ private const string PortTesterProtoId = "DeviceLinkingSinkPortTester";
+
+ [TestPrototypes]
+ private const string Prototypes = $@"
+- type: entity
+ id: {PortTesterProtoId}
+ components:
+ - type: DeviceLinkSource
+ ports:
+ - Output
+";
+
+ ///
+ /// Spawns every entity that has a
+ /// and sends a signal to every port to make sure nothing causes an error.
+ ///
+ [Test]
+ public async Task AllDeviceLinkSinksWorkTest()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+ var compFact = server.ResolveDependency();
+ var mapMan = server.ResolveDependency();
+ var mapSys = server.System();
+ var deviceLinkSys = server.System();
+
+ var prototypes = server.ProtoMan.EnumeratePrototypes();
+
+ await server.WaitAssertion(() =>
+ {
+ Assert.Multiple(() =>
+ {
+ foreach (var proto in prototypes)
+ {
+ if (proto.Abstract || pair.IsTestPrototype(proto))
+ continue;
+
+ if (!proto.TryGetComponent(out var protoSinkComp, compFact))
+ continue;
+
+ foreach (var port in protoSinkComp.Ports)
+ {
+ // Create a map for each entity/port combo so they can't interfere
+ mapSys.CreateMap(out var mapId);
+ var grid = mapMan.CreateGridEntity(mapId);
+ mapSys.SetTile(grid.Owner, grid.Comp, Vector2i.Zero, new Tile(1));
+ var coord = new EntityCoordinates(grid.Owner, 0, 0);
+
+ // Spawn the sink entity
+ var sinkEnt = server.EntMan.SpawnEntity(proto.ID, coord);
+ // Get the actual sink component, since the one we got from the prototype doesn't have its owner set up
+ Assert.That(server.EntMan.TryGetComponent(sinkEnt, out var sinkComp),
+ $"{proto.ID} does not have a DeviceLinkSinkComponent!");
+
+ // Spawn the tester
+ var sourceEnt = server.EntMan.SpawnEntity(PortTesterProtoId, coord);
+ Assert.That(server.EntMan.TryGetComponent(sourceEnt, out var sourceComp),
+ $"Tester prototype does not have a DeviceLinkSourceComponent!");
+
+ // Create a link from the tester's output to the target port on the sink
+ deviceLinkSys.SaveLinks(null,
+ sourceEnt,
+ sinkEnt,
+ [("Output", port.Id)],
+ sourceComp,
+ sinkComp);
+
+ // Send a signal to the port
+ Assert.DoesNotThrow(() => { deviceLinkSys.InvokePort(sourceEnt, "Output", null, sourceComp); },
+ $"Exception thrown while triggering port {port.Id} of sink device {proto.ID}");
+
+ mapSys.DeleteMap(mapId);
+ }
+ }
+ });
+ });
+
+ await pair.CleanReturnAsync();
+ }
+}
diff --git a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs
index 039c0c7b18..5790e414f3 100644
--- a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs
+++ b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs
@@ -152,10 +152,14 @@ public sealed class NukeOpsTest
Assert.That(roleSys.MindGetAllRoleInfo(mindCrew).Any(x => nukeroles.Contains(x.Prototype)), Is.False);
}
+ var ruleGridComps = entMan.AllComponents();
+ Assert.That(ruleGridComps, Has.Length.EqualTo(1),
+ $"Unexpected RuleGrid(s) detected! {string.Join(',', ruleGridComps.Select(e => server.EntMan.ToPrettyString(e.Uid)))}");
+
// The game rule exists, and all the stations/shuttles/maps are properly initialized
var rule = entMan.AllComponents().Single();
var ruleComp = rule.Component;
- var gridsRule = entMan.AllComponents().Single().Component;
+ var gridsRule = ruleGridComps.Single().Component;
foreach (var grid in gridsRule.MapGrids)
{
Assert.That(entMan.EntityExists(grid));
diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs
index d431c440a2..a61f7c3ec8 100644
--- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs
+++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs
@@ -62,7 +62,10 @@ public abstract partial class InteractionTest
// Please someone purge async construction code
Task task = default!;
- await Server.WaitPost(() => task = SConstruction.TryStartItemConstruction(prototype, SEntMan.GetEntity(Player)));
+ await Server.WaitPost(() =>
+ {
+ task = SConstruction.TryStartItemConstruction(prototype, SEntMan.GetEntity(Player));
+ });
Task? tickTask = null;
while (!task.IsCompleted)
diff --git a/Content.IntegrationTests/Tests/Localization/LocalizedDatasetPrototypeTest.cs b/Content.IntegrationTests/Tests/Localization/LocalizedDatasetPrototypeTest.cs
index b30c0a370e..8d272545ce 100644
--- a/Content.IntegrationTests/Tests/Localization/LocalizedDatasetPrototypeTest.cs
+++ b/Content.IntegrationTests/Tests/Localization/LocalizedDatasetPrototypeTest.cs
@@ -19,16 +19,19 @@ public sealed class LocalizedDatasetPrototypeTest
var protos = protoMan.EnumeratePrototypes().OrderBy(p => p.ID);
- // Check each prototype
- foreach (var proto in protos)
+ Assert.Multiple(() =>
{
- // Check each value in the prototype
- foreach (var locId in proto.Values)
+ // Check each prototype
+ foreach (var proto in protos)
{
- // Make sure the localization manager has a string for the LocId
- Assert.That(localizationMan.HasString(locId), $"LocalizedDataset {proto.ID} with prefix \"{proto.Values.Prefix}\" specifies {proto.Values.Count} entries, but no localized string was found matching {locId}!");
+ // Check each value in the prototype
+ foreach (var locId in proto.Values)
+ {
+ // Make sure the localization manager has a string for the LocId
+ Assert.That(localizationMan.HasString(locId), $"LocalizedDataset {proto.ID} with prefix \"{proto.Values.Prefix}\" specifies {proto.Values.Count} entries, but no localized string was found matching {locId}!");
+ }
}
- }
+ });
await pair.CleanReturnAsync();
}
diff --git a/Content.IntegrationTests/Tests/MagazineVisualsSpriteTest.cs b/Content.IntegrationTests/Tests/MagazineVisualsSpriteTest.cs
index f0f3b72d8d..fb0c67afd7 100644
--- a/Content.IntegrationTests/Tests/MagazineVisualsSpriteTest.cs
+++ b/Content.IntegrationTests/Tests/MagazineVisualsSpriteTest.cs
@@ -23,46 +23,49 @@ public sealed class MagazineVisualsSpriteTest
await client.WaitAssertion(() =>
{
- foreach (var proto in protoMan.EnumeratePrototypes())
+ Assert.Multiple(() =>
{
- if (proto.Abstract || pair.IsTestPrototype(proto))
- continue;
-
- if (!proto.TryGetComponent(out var visuals, componentFactory))
- continue;
-
- Assert.That(proto.TryGetComponent(out var sprite, componentFactory),
- @$"{proto.ID} has MagazineVisualsComponent but no SpriteComponent.");
- Assert.That(proto.HasComponent(componentFactory),
- @$"{proto.ID} has MagazineVisualsComponent but no AppearanceComponent.");
-
- var toTest = new List<(int, string)>();
- if (sprite.LayerMapTryGet(GunVisualLayers.Mag, out var magLayerId))
- toTest.Add((magLayerId, ""));
- if (sprite.LayerMapTryGet(GunVisualLayers.MagUnshaded, out var magUnshadedLayerId))
- toTest.Add((magUnshadedLayerId, "-unshaded"));
-
- Assert.That(toTest, Is.Not.Empty,
- @$"{proto.ID} has MagazineVisualsComponent but no Mag or MagUnshaded layer map.");
-
- var start = visuals.ZeroVisible ? 0 : 1;
- foreach (var (id, midfix) in toTest)
+ foreach (var proto in protoMan.EnumeratePrototypes())
{
- Assert.That(sprite.TryGetLayer(id, out var layer));
- var rsi = layer.ActualRsi;
- for (var i = start; i < visuals.MagSteps; i++)
- {
- var state = $"{visuals.MagState}{midfix}-{i}";
- Assert.That(rsi.TryGetState(state, out _),
- @$"{proto.ID} has MagazineVisualsComponent with MagSteps = {visuals.MagSteps}, but {rsi.Path} doesn't have state {state}!");
- }
+ if (proto.Abstract || pair.IsTestPrototype(proto))
+ continue;
- // MagSteps includes the 0th step, so sometimes people are off by one.
- var extraState = $"{visuals.MagState}{midfix}-{visuals.MagSteps}";
- Assert.That(rsi.TryGetState(extraState, out _), Is.False,
- @$"{proto.ID} has MagazineVisualsComponent with MagSteps = {visuals.MagSteps}, but more states exist!");
+ if (!proto.TryGetComponent(out var visuals, componentFactory))
+ continue;
+
+ Assert.That(proto.TryGetComponent(out var sprite, componentFactory),
+ @$"{proto.ID} has MagazineVisualsComponent but no SpriteComponent.");
+ Assert.That(proto.HasComponent(componentFactory),
+ @$"{proto.ID} has MagazineVisualsComponent but no AppearanceComponent.");
+
+ var toTest = new List<(int, string)>();
+ if (sprite.LayerMapTryGet(GunVisualLayers.Mag, out var magLayerId))
+ toTest.Add((magLayerId, ""));
+ if (sprite.LayerMapTryGet(GunVisualLayers.MagUnshaded, out var magUnshadedLayerId))
+ toTest.Add((magUnshadedLayerId, "-unshaded"));
+
+ Assert.That(toTest, Is.Not.Empty,
+ @$"{proto.ID} has MagazineVisualsComponent but no Mag or MagUnshaded layer map.");
+
+ var start = visuals.ZeroVisible ? 0 : 1;
+ foreach (var (id, midfix) in toTest)
+ {
+ Assert.That(sprite.TryGetLayer(id, out var layer));
+ var rsi = layer.ActualRsi;
+ for (var i = start; i < visuals.MagSteps; i++)
+ {
+ var state = $"{visuals.MagState}{midfix}-{i}";
+ Assert.That(rsi.TryGetState(state, out _),
+ @$"{proto.ID} has MagazineVisualsComponent with MagSteps = {visuals.MagSteps}, but {rsi.Path} doesn't have state {state}!");
+ }
+
+ // MagSteps includes the 0th step, so sometimes people are off by one.
+ var extraState = $"{visuals.MagState}{midfix}-{visuals.MagSteps}";
+ Assert.That(rsi.TryGetState(extraState, out _), Is.False,
+ @$"{proto.ID} has MagazineVisualsComponent with MagSteps = {visuals.MagSteps}, but more states exist!");
+ }
}
- }
+ });
});
await pair.CleanReturnAsync();
diff --git a/Content.IntegrationTests/Tests/PrototypeSaveTest.cs b/Content.IntegrationTests/Tests/PrototypeSaveTest.cs
index 1ef34365ea..9ff8ca2900 100644
--- a/Content.IntegrationTests/Tests/PrototypeSaveTest.cs
+++ b/Content.IntegrationTests/Tests/PrototypeSaveTest.cs
@@ -35,7 +35,6 @@ public sealed class PrototypeSaveTest
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
- var mapManager = server.ResolveDependency();
var entityMan = server.ResolveDependency();
var prototypeMan = server.ResolveDependency();
var seriMan = server.ResolveDependency();
diff --git a/Content.IntegrationTests/Tests/StoreTests.cs b/Content.IntegrationTests/Tests/StoreTests.cs
index 1f7d357bc0..e6aed78755 100644
--- a/Content.IntegrationTests/Tests/StoreTests.cs
+++ b/Content.IntegrationTests/Tests/StoreTests.cs
@@ -102,6 +102,12 @@ public sealed class StoreTests
+ $"flag as 'true'. This marks the fact that cost modifier of discount is not applied properly!"
);
+ // The storeComponent returns discounted items with conditions randomly, so we remove these to sanitize the data.
+ foreach (var discountedItem in discountedListingItems)
+ {
+ discountedItem.Conditions = null;
+ }
+
// Refund action requests re-generation of listing items so we will be re-acquiring items from component a lot of times.
var itemIds = discountedListingItems.Select(x => x.ID);
foreach (var itemId in itemIds)
@@ -140,6 +146,9 @@ public sealed class StoreTests
// get refreshed item after refund re-generated items
discountedListingItem = storeComponent.FullListingsCatalog.First(x => x.ID == itemId);
+ // The storeComponent can give a discounted item a condition at random, so we remove it to sanitize the data.
+ discountedListingItem.Conditions = null;
+
var afterRefundBalance = storeComponent.Balance[UplinkSystem.TelecrystalCurrencyPrototype];
Assert.That(afterRefundBalance.Value, Is.EqualTo(originalBalance.Value), "Expected refund to return all discounted cost value.");
Assert.That(
diff --git a/Content.IntegrationTests/Tests/Vending/VendingInteractionTest.cs b/Content.IntegrationTests/Tests/Vending/VendingInteractionTest.cs
new file mode 100644
index 0000000000..bcaf4343b6
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Vending/VendingInteractionTest.cs
@@ -0,0 +1,205 @@
+using System.Linq;
+using Content.IntegrationTests.Tests.Interaction;
+using Content.Server.VendingMachines;
+using Content.Shared.Damage;
+using Content.Shared.Damage.Prototypes;
+using Content.Shared.FixedPoint;
+using Content.Shared.VendingMachines;
+
+namespace Content.IntegrationTests.Tests.Vending;
+
+public sealed class VendingInteractionTest : InteractionTest
+{
+ private const string VendingMachineProtoId = "InteractionTestVendingMachine";
+
+ private const string VendedItemProtoId = "InteractionTestItem";
+
+ private const string RestockBoxProtoId = "InteractionTestRestockBox";
+
+ private const string RestockBoxOtherProtoId = "InteractionTestRestockBoxOther";
+
+ [TestPrototypes]
+ private const string TestPrototypes = $@"
+- type: entity
+ parent: BaseItem
+ id: {VendedItemProtoId}
+ name: {VendedItemProtoId}
+
+- type: vendingMachineInventory
+ id: InteractionTestVendingInventory
+ startingInventory:
+ {VendedItemProtoId}: 5
+
+- type: vendingMachineInventory
+ id: InteractionTestVendingInventoryOther
+ startingInventory:
+ {VendedItemProtoId}: 5
+
+- type: entity
+ parent: BaseVendingMachineRestock
+ id: {RestockBoxProtoId}
+ components:
+ - type: VendingMachineRestock
+ canRestock:
+ - InteractionTestVendingInventory
+
+- type: entity
+ parent: BaseVendingMachineRestock
+ id: {RestockBoxOtherProtoId}
+ components:
+ - type: VendingMachineRestock
+ canRestock:
+ - InteractionTestVendingInventoryOther
+
+- type: entity
+ id: {VendingMachineProtoId}
+ parent: VendingMachine
+ components:
+ - type: VendingMachine
+ pack: InteractionTestVendingInventory
+ ejectDelay: 0 # no delay to speed up tests
+ - type: Sprite
+ sprite: error.rsi
+";
+
+ [Test]
+ public async Task InteractUITest()
+ {
+ await SpawnTarget(VendingMachineProtoId);
+
+ // Should start with no BUI open
+ Assert.That(IsUiOpen(VendingMachineUiKey.Key), Is.False, "BUI was open unexpectedly.");
+
+ // Unpowered vending machine does not open BUI
+ await Activate();
+ Assert.That(IsUiOpen(VendingMachineUiKey.Key), Is.False, "BUI opened without power.");
+
+ // Power the vending machine
+ var apc = await SpawnEntity("APCBasic", SEntMan.GetCoordinates(TargetCoords));
+ await RunTicks(1);
+
+ // Interacting with powered vending machine opens BUI
+ await Activate();
+ Assert.That(IsUiOpen(VendingMachineUiKey.Key), "BUI failed to open.");
+
+ // Interacting with it again closes the BUI
+ await Activate();
+ Assert.That(IsUiOpen(VendingMachineUiKey.Key), Is.False, "BUI failed to close on interaction.");
+
+ // Reopen BUI for the next check
+ await Activate();
+ Assert.That(IsUiOpen(VendingMachineUiKey.Key), "BUI failed to reopen.");
+
+ // Remove power
+ await Delete(apc);
+ await RunTicks(1);
+
+ // The BUI should close when power is lost
+ Assert.That(IsUiOpen(VendingMachineUiKey.Key), Is.False, "BUI failed to close on power loss.");
+ }
+
+ [Test]
+ public async Task DispenseItemTest()
+ {
+ await SpawnTarget(VendingMachineProtoId);
+ var vendorEnt = SEntMan.GetEntity(Target.Value);
+
+ var vendingSystem = SEntMan.System();
+ var items = vendingSystem.GetAllInventory(vendorEnt);
+
+ // Verify initial item count
+ Assert.That(items, Is.Not.Empty, $"{VendingMachineProtoId} spawned with no items.");
+ Assert.That(items.First().Amount, Is.EqualTo(5), $"{VendingMachineProtoId} spawned with unexpected item count.");
+
+ // Power the vending machine
+ await SpawnEntity("APCBasic", SEntMan.GetCoordinates(TargetCoords));
+ await RunTicks(1);
+
+ // Open the BUI
+ await Activate();
+ Assert.That(IsUiOpen(VendingMachineUiKey.Key), "BUI failed to open.");
+
+ // Request an item be dispensed
+ var ev = new VendingMachineEjectMessage(InventoryType.Regular, VendedItemProtoId);
+ await SendBui(VendingMachineUiKey.Key, ev);
+
+ // Make sure the stock decreased
+ Assert.That(items.First().Amount, Is.EqualTo(4), "Stocked item count did not decrease.");
+ // Make sure the dispensed item was spawned in to the world
+ await AssertEntityLookup(
+ ("APCBasic", 1),
+ (VendedItemProtoId, 1)
+ );
+ }
+
+ [Test]
+ public async Task RestockTest()
+ {
+ var vendingSystem = SEntMan.System();
+
+ await SpawnTarget(VendingMachineProtoId);
+ var vendorEnt = SEntMan.GetEntity(Target.Value);
+
+ var items = vendingSystem.GetAllInventory(vendorEnt);
+
+ Assert.That(items, Is.Not.Empty, $"{VendingMachineProtoId} spawned with no items.");
+ Assert.That(items.First().Amount, Is.EqualTo(5), $"{VendingMachineProtoId} spawned with unexpected item count.");
+
+ // Try to restock with the maintenance panel closed (nothing happens)
+ await InteractUsing(RestockBoxProtoId);
+
+ Assert.That(items.First().Amount, Is.EqualTo(5), "Restocked without opening maintenance panel.");
+
+ // Open the maintenance panel
+ await InteractUsing(Screw);
+
+ // Try to restock using the wrong restock box (nothing happens)
+ await InteractUsing(RestockBoxOtherProtoId);
+
+ Assert.That(items.First().Amount, Is.EqualTo(5), "Restocked with wrong restock box.");
+
+ // Restock the machine
+ await InteractUsing(RestockBoxProtoId);
+
+ Assert.That(items.First().Amount, Is.EqualTo(10), "Restocking resulted in unexpected item count.");
+ }
+
+ [Test]
+ public async Task RepairTest()
+ {
+ await SpawnTarget(VendingMachineProtoId);
+
+ // Power the vending machine
+ await SpawnEntity("APCBasic", SEntMan.GetCoordinates(TargetCoords));
+ await RunTicks(1);
+
+ // Break it
+ await BreakVendor();
+ Assert.That(IsUiOpen(VendingMachineUiKey.Key), Is.False, "BUI did not close when vending machine broke.");
+
+ // Make sure we can't open the BUI while it's broken
+ await Activate();
+ Assert.That(IsUiOpen(VendingMachineUiKey.Key), Is.False, "Opened BUI of broken vending machine.");
+
+ // Repair the vending machine
+ await InteractUsing(Weld);
+
+ // Make sure the BUI can open now that the machine has been repaired
+ await Activate();
+ Assert.That(IsUiOpen(VendingMachineUiKey.Key), "Failed to open BUI after repair.");
+ }
+
+ private async Task BreakVendor()
+ {
+ var damageableSys = SEntMan.System();
+ Assert.That(TryComp(out var damageableComp), $"{VendingMachineProtoId} does not have DamageableComponent.");
+ Assert.That(damageableComp.Damage.GetTotal(), Is.EqualTo(FixedPoint2.Zero), $"{VendingMachineProtoId} started with unexpected damage.");
+
+ // Damage the vending machine to the point that it breaks
+ var damageType = ProtoMan.Index("Blunt");
+ var damage = new DamageSpecifier(damageType, FixedPoint2.New(100));
+ await Server.WaitPost(() => damageableSys.TryChangeDamage(SEntMan.GetEntity(Target), damage, ignoreResistances: true));
+ await RunTicks(5);
+ Assert.That(damageableComp.Damage.GetTotal(), Is.GreaterThan(FixedPoint2.Zero), $"{VendingMachineProtoId} did not take damage.");
+ }
+}
diff --git a/Content.Server/Abilities/Mime/MimePowersComponent.cs b/Content.Server/Abilities/Mime/MimePowersComponent.cs
index d56644ed19..24ffe5d023 100644
--- a/Content.Server/Abilities/Mime/MimePowersComponent.cs
+++ b/Content.Server/Abilities/Mime/MimePowersComponent.cs
@@ -55,5 +55,16 @@ namespace Content.Server.Abilities.Mime
[DataField]
public ProtoId VowBrokenAlert = "VowBroken";
+ ///
+ /// Does this component prevent the mime from writing on paper while their vow is active?
+ ///
+ [DataField]
+ public bool PreventWriting = false;
+
+ ///
+ /// What message is displayed when the mime fails to write?
+ ///
+ [DataField]
+ public LocId FailWriteMessage = "paper-component-illiterate-mime";
}
}
diff --git a/Content.Server/Abilities/Mime/MimePowersSystem.cs b/Content.Server/Abilities/Mime/MimePowersSystem.cs
index bd8cf7c176..3de356f608 100644
--- a/Content.Server/Abilities/Mime/MimePowersSystem.cs
+++ b/Content.Server/Abilities/Mime/MimePowersSystem.cs
@@ -5,6 +5,7 @@ using Content.Shared.Actions.Events;
using Content.Shared.Alert;
using Content.Shared.Coordinates.Helpers;
using Content.Shared.Maps;
+using Content.Shared.Paper;
using Content.Shared.Physics;
using Robust.Shared.Containers;
using Robust.Shared.Map;
@@ -55,6 +56,13 @@ namespace Content.Server.Abilities.Mime
private void OnComponentInit(EntityUid uid, MimePowersComponent component, ComponentInit args)
{
EnsureComp(uid);
+ if (component.PreventWriting)
+ {
+ EnsureComp(uid, out var illiterateComponent);
+ illiterateComponent.FailWriteMessage = component.FailWriteMessage;
+ Dirty(uid, illiterateComponent);
+ }
+
_alertsSystem.ShowAlert(uid, component.VowAlert);
_actionsSystem.AddAction(uid, ref component.InvisibleWallActionEntity, component.InvisibleWallAction, uid);
}
@@ -123,6 +131,8 @@ namespace Content.Server.Abilities.Mime
mimePowers.VowBroken = true;
mimePowers.VowRepentTime = _timing.CurTime + mimePowers.VowCooldown;
RemComp(uid);
+ if (mimePowers.PreventWriting)
+ RemComp(uid);
_alertsSystem.ClearAlert(uid, mimePowers.VowAlert);
_alertsSystem.ShowAlert(uid, mimePowers.VowBrokenAlert);
_actionsSystem.RemoveAction(uid, mimePowers.InvisibleWallActionEntity);
@@ -146,6 +156,13 @@ namespace Content.Server.Abilities.Mime
mimePowers.ReadyToRepent = false;
mimePowers.VowBroken = false;
AddComp(uid);
+ if (mimePowers.PreventWriting)
+ {
+ EnsureComp(uid, out var illiterateComponent);
+ illiterateComponent.FailWriteMessage = mimePowers.FailWriteMessage;
+ Dirty(uid, illiterateComponent);
+ }
+
_alertsSystem.ClearAlert(uid, mimePowers.VowBrokenAlert);
_alertsSystem.ShowAlert(uid, mimePowers.VowAlert);
_actionsSystem.AddAction(uid, ref mimePowers.InvisibleWallActionEntity, mimePowers.InvisibleWallAction, uid);
diff --git a/Content.Server/Access/Systems/AccessOverriderSystem.cs b/Content.Server/Access/Systems/AccessOverriderSystem.cs
index a0dbc96677..4bb42a30a3 100644
--- a/Content.Server/Access/Systems/AccessOverriderSystem.cs
+++ b/Content.Server/Access/Systems/AccessOverriderSystem.cs
@@ -241,7 +241,7 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
var addedTags = newAccessList.Except(oldTags).Select(tag => "+" + tag).ToList();
var removedTags = oldTags.Except(newAccessList).Select(tag => "-" + tag).ToList();
- _adminLogger.Add(LogType.Action, LogImpact.Medium,
+ _adminLogger.Add(LogType.Action, LogImpact.High,
$"{ToPrettyString(player):player} has modified {ToPrettyString(accessReaderEnt.Value):entity} with the following allowed access level holders: [{string.Join(", ", addedTags.Union(removedTags))}] [{string.Join(", ", newAccessList)}]");
accessReaderEnt.Value.Comp.AccessLists = ConvertAccessListToHashSet(newAccessList);
diff --git a/Content.Server/Access/Systems/IdCardConsoleSystem.cs b/Content.Server/Access/Systems/IdCardConsoleSystem.cs
index a9e5d9a6d3..c8fbe2ba9d 100644
--- a/Content.Server/Access/Systems/IdCardConsoleSystem.cs
+++ b/Content.Server/Access/Systems/IdCardConsoleSystem.cs
@@ -168,7 +168,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
/*TODO: ECS SharedIdCardConsoleComponent and then log on card ejection, together with the save.
This current implementation is pretty shit as it logs 27 entries (27 lines) if someone decides to give themselves AA*/
- _adminLogger.Add(LogType.Action, LogImpact.Medium,
+ _adminLogger.Add(LogType.Action, LogImpact.High,
$"{ToPrettyString(player):player} has modified {ToPrettyString(targetId):entity} with the following accesses: [{string.Join(", ", addedTags.Union(removedTags))}] [{string.Join(", ", newAccessList)}]");
}
diff --git a/Content.Server/Access/Systems/IdCardSystem.cs b/Content.Server/Access/Systems/IdCardSystem.cs
index aeb4cc163f..9057fade72 100644
--- a/Content.Server/Access/Systems/IdCardSystem.cs
+++ b/Content.Server/Access/Systems/IdCardSystem.cs
@@ -31,7 +31,7 @@ public sealed class IdCardSystem : SharedIdCardSystem
private void OnMicrowaved(EntityUid uid, IdCardComponent component, BeingMicrowavedEvent args)
{
if (!component.CanMicrowave || !TryComp(args.Microwave, out var micro) || micro.Broken)
- return;
+ return;
if (TryComp(uid, out var access))
{
@@ -78,12 +78,17 @@ public sealed class IdCardSystem : SharedIdCardSystem
}
// Give them a wonderful new access to compensate for everything
- var random = _random.Pick(_prototypeManager.EnumeratePrototypes().ToArray());
+ var ids = _prototypeManager.EnumeratePrototypes().Where(x => x.CanAddToIdCard).ToArray();
+
+ if (ids.Length == 0)
+ return;
+
+ var random = _random.Pick(ids);
access.Tags.Add(random.ID);
Dirty(uid, access);
- _adminLogger.Add(LogType.Action, LogImpact.Medium,
+ _adminLogger.Add(LogType.Action, LogImpact.High,
$"{ToPrettyString(args.Microwave)} added {random.ID} access to {ToPrettyString(uid):entity}");
}
diff --git a/Content.Server/Administration/Commands/ChangeCvarCommand.cs b/Content.Server/Administration/Commands/ChangeCvarCommand.cs
index 984c9c1077..66f93e4ebd 100644
--- a/Content.Server/Administration/Commands/ChangeCvarCommand.cs
+++ b/Content.Server/Administration/Commands/ChangeCvarCommand.cs
@@ -180,7 +180,7 @@ public sealed class ChangeCvarCommand : IConsoleCommand
var oldValue = _configurationManager.GetCVar