diff --git a/Content.Client/Administration/AdminNameOverlay.cs b/Content.Client/Administration/AdminNameOverlay.cs
index 47d251910c..c6e6a75293 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.Username, uiScale, playerInfo.Connected ? Color.Yellow : Color.White);
+ currentOffset += lineoffset;
+
+ args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.CharacterName, uiScale, playerInfo.Connected ? Color.Aquamarine : 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.ID))
+ 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/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/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/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/EntitySystems/ChemistryGuideDataSystem.cs b/Content.Client/Chemistry/EntitySystems/ChemistryGuideDataSystem.cs
index 4c3a583b2a..374b00aa6e 100644
--- a/Content.Client/Chemistry/EntitySystems/ChemistryGuideDataSystem.cs
+++ b/Content.Client/Chemistry/EntitySystems/ChemistryGuideDataSystem.cs
@@ -94,7 +94,7 @@ public sealed class ChemistryGuideDataSystem : SharedChemistryGuideDataSystem
if (entProto.Abstract || usedNames.Contains(entProto.Name))
continue;
- if (!entProto.TryGetComponent(out var extractableComponent))
+ if (!entProto.TryGetComponent(out var extractableComponent, EntityManager.ComponentFactory))
continue;
//these bloat the hell out of blood/fat
@@ -121,7 +121,7 @@ public sealed class ChemistryGuideDataSystem : SharedChemistryGuideDataSystem
if (extractableComponent.GrindableSolution is { } grindableSolutionId &&
- entProto.TryGetComponent(out var manager) &&
+ entProto.TryGetComponent(out var manager, EntityManager.ComponentFactory) &&
_solutionContainer.TryGetSolution(manager, grindableSolutionId, out var grindableSolution))
{
var data = new ReagentEntitySourceData(
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/Construction/FlatpackSystem.cs b/Content.Client/Construction/FlatpackSystem.cs
index 911ff1279c..f39c2327ca 100644
--- a/Content.Client/Construction/FlatpackSystem.cs
+++ b/Content.Client/Construction/FlatpackSystem.cs
@@ -27,7 +27,7 @@ public sealed class FlatpackSystem : SharedFlatpackSystem
if (!PrototypeManager.TryIndex(machineBoardId, out var machineBoardPrototype))
return;
- if (!machineBoardPrototype.TryGetComponent(out var sprite))
+ if (!machineBoardPrototype.TryGetComponent(out var sprite, EntityManager.ComponentFactory))
return;
Color? color = null;
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/Ghost/GhostSystem.cs b/Content.Client/Ghost/GhostSystem.cs
index 0a6046cefa..dd2e51d15d 100644
--- a/Content.Client/Ghost/GhostSystem.cs
+++ b/Content.Client/Ghost/GhostSystem.cs
@@ -155,7 +155,7 @@ namespace Content.Client.Ghost
private void OnGhostState(EntityUid uid, GhostComponent component, ref AfterAutoHandleStateEvent args)
{
if (TryComp(uid, out var sprite))
- sprite.LayerSetColor(0, component.color);
+ sprite.LayerSetColor(0, component.Color);
if (uid != _playerManager.LocalEntity)
return;
diff --git a/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml b/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml
index 63e80de2ae..92da9109d8 100644
--- a/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml
+++ b/Content.Client/Guidebook/Controls/GuideReagentReaction.xaml
@@ -1,11 +1,11 @@
-
+
(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.cs b/Content.Client/Lathe/UI/LatheMenu.xaml.cs
index fa99a9dd8e..2b18ce28de 100644
--- a/Content.Client/Lathe/UI/LatheMenu.xaml.cs
+++ b/Content.Client/Lathe/UI/LatheMenu.xaml.cs
@@ -94,8 +94,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)
{
@@ -179,18 +188,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/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 3c398d8590..22be5aeeab 100644
--- a/Content.Client/Options/UI/OptionsMenu.xaml
+++ b/Content.Client/Options/UI/OptionsMenu.xaml
@@ -9,6 +9,7 @@
+
diff --git a/Content.Client/Options/UI/OptionsMenu.xaml.cs b/Content.Client/Options/UI/OptionsMenu.xaml.cs
index df9b1b2ca5..430b86a436 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,6 +20,7 @@ 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"));
// CP14-options-menu-start
Tabs.SetTabTitle(5, Loc.GetString("cp14-ui-options-tab-main"));
@@ -28,10 +31,14 @@ namespace Content.Client.Options.UI
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..763fd995ca 100644
--- a/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml
+++ b/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml
@@ -7,8 +7,11 @@
-
+
+
+
+
diff --git a/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml.cs b/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml.cs
index 15182fbf12..e1dead0b0e 100644
--- a/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml.cs
+++ b/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml.cs
@@ -15,8 +15,11 @@ 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.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/SubFloor/SubFloorHideSystem.cs b/Content.Client/SubFloor/SubFloorHideSystem.cs
index 64d3afc640..5e5f2993b4 100644
--- a/Content.Client/SubFloor/SubFloorHideSystem.cs
+++ b/Content.Client/SubFloor/SubFloorHideSystem.cs
@@ -67,8 +67,11 @@ public sealed class SubFloorHideSystem : SharedSubFloorHideSystem
// allows a t-ray to show wires/pipes above carpets/puddles
if (scannerRevealed)
{
- component.OriginalDrawDepth ??= args.Sprite.DrawDepth;
- args.Sprite.DrawDepth = (int) Shared.DrawDepth.DrawDepth.FloorObjects + 1;
+ 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/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/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.IntegrationTests/Tests/CargoTest.cs b/Content.IntegrationTests/Tests/CargoTest.cs
index 8cb51e36c2..63015eb429 100644
--- a/Content.IntegrationTests/Tests/CargoTest.cs
+++ b/Content.IntegrationTests/Tests/CargoTest.cs
@@ -6,10 +6,8 @@ using Content.Server.Cargo.Systems;
using Content.Server.Nutrition.Components;
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.Cargo.Prototypes;
-using Content.Shared.IdentityManagement;
using Content.Shared.Prototypes;
using Content.Shared.Stacks;
-using Content.Shared.Tag;
using Content.Shared.Whitelist;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
@@ -67,7 +65,7 @@ public sealed class CargoTest
var testMap = await pair.CreateTestMap();
var entManager = server.ResolveDependency();
- var mapManager = server.ResolveDependency();
+ var mapSystem = server.System();
var protoManager = server.ResolveDependency();
var cargo = entManager.System();
@@ -93,7 +91,7 @@ public sealed class CargoTest
}
});
- mapManager.DeleteMap(mapId);
+ mapSystem.DeleteMap(mapId);
});
await pair.CleanReturnAsync();
@@ -151,6 +149,7 @@ public sealed class CargoTest
var testMap = await pair.CreateTestMap();
var entManager = server.ResolveDependency();
+ var mapSystem = server.System();
var mapManager = server.ResolveDependency();
var protoManager = server.ResolveDependency();
var componentFactory = server.ResolveDependency();
@@ -207,7 +206,7 @@ public sealed class CargoTest
entManager.DeleteEntity(ent);
}
- mapManager.DeleteMap(mapId);
+ mapSystem.DeleteMap(mapId);
});
await pair.CleanReturnAsync();
diff --git a/Content.IntegrationTests/Tests/Hands/HandTests.cs b/Content.IntegrationTests/Tests/Hands/HandTests.cs
index 5e96015feb..b88f2dc9eb 100644
--- a/Content.IntegrationTests/Tests/Hands/HandTests.cs
+++ b/Content.IntegrationTests/Tests/Hands/HandTests.cs
@@ -6,7 +6,6 @@ using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
-using Robust.Shared.Map;
namespace Content.IntegrationTests.Tests.Hands;
@@ -38,7 +37,7 @@ public sealed class HandTests
var entMan = server.ResolveDependency();
var playerMan = server.ResolveDependency();
- var mapMan = server.ResolveDependency();
+ var mapSystem = server.System();
var sys = entMan.System();
var tSys = entMan.System();
@@ -69,7 +68,7 @@ public sealed class HandTests
await pair.RunTicksSync(5);
Assert.That(hands.ActiveHandEntity, Is.Null);
- await server.WaitPost(() => mapMan.DeleteMap(data.MapId));
+ await server.WaitPost(() => mapSystem.DeleteMap(data.MapId));
await pair.CleanReturnAsync();
}
@@ -87,7 +86,7 @@ public sealed class HandTests
var entMan = server.ResolveDependency();
var playerMan = server.ResolveDependency();
- var mapMan = server.ResolveDependency();
+ var mapSystem = server.System();
var sys = entMan.System();
var tSys = entMan.System();
var containerSystem = server.System();
@@ -134,7 +133,7 @@ public sealed class HandTests
Assert.That(hands.ActiveHandEntity, Is.Not.EqualTo(item));
Assert.That(containerSystem.IsInSameOrNoContainer((player, xform), (item, itemXform)));
- await server.WaitPost(() => mapMan.DeleteMap(map.MapId));
+ await server.WaitPost(() => mapSystem.DeleteMap(map.MapId));
await pair.CleanReturnAsync();
}
}
diff --git a/Content.IntegrationTests/Tests/HumanInventoryUniformSlotsTest.cs b/Content.IntegrationTests/Tests/HumanInventoryUniformSlotsTest.cs
index 4bba605f0a..929a231159 100644
--- a/Content.IntegrationTests/Tests/HumanInventoryUniformSlotsTest.cs
+++ b/Content.IntegrationTests/Tests/HumanInventoryUniformSlotsTest.cs
@@ -1,6 +1,5 @@
using Content.Shared.Inventory;
using Robust.Shared.GameObjects;
-using Robust.Shared.Map;
namespace Content.IntegrationTests.Tests
{
@@ -67,7 +66,7 @@ namespace Content.IntegrationTests.Tests
EntityUid pocketItem = default;
InventorySystem invSystem = default!;
- var mapMan = server.ResolveDependency();
+ var mapSystem = server.System();
var entityMan = server.ResolveDependency();
await server.WaitAssertion(() =>
@@ -129,7 +128,7 @@ namespace Content.IntegrationTests.Tests
Assert.That(!invSystem.TryGetSlotEntity(human, "pocket1", out _));
});
- mapMan.DeleteMap(testMap.MapId);
+ mapSystem.DeleteMap(testMap.MapId);
});
await pair.CleanReturnAsync();
diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
index b3d684e01a..47a1f748eb 100644
--- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
+++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
@@ -260,7 +260,7 @@ public abstract partial class InteractionTest
[TearDown]
public async Task TearDownInternal()
{
- await Server.WaitPost(() => MapMan.DeleteMap(MapId));
+ await Server.WaitPost(() => MapSystem.DeleteMap(MapId));
await Pair.CleanReturnAsync();
await TearDown();
}
diff --git a/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs b/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs
index ae09405220..e6422f0ec4 100644
--- a/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs
+++ b/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs
@@ -346,7 +346,7 @@ public sealed class MaterialArbitrageTest
}
});
- await server.WaitPost(() => mapManager.DeleteMap(testMap.MapId));
+ await server.WaitPost(() => mapSystem.DeleteMap(testMap.MapId));
await pair.CleanReturnAsync();
async Task GetSpawnedPrice(Dictionary ents)
diff --git a/Content.IntegrationTests/Tests/Materials/MaterialTests.cs b/Content.IntegrationTests/Tests/Materials/MaterialTests.cs
index 1caaf8a3ff..30800f358e 100644
--- a/Content.IntegrationTests/Tests/Materials/MaterialTests.cs
+++ b/Content.IntegrationTests/Tests/Materials/MaterialTests.cs
@@ -3,7 +3,6 @@ using Content.Server.Stack;
using Content.Shared.Stacks;
using Content.Shared.Materials;
using Robust.Shared.GameObjects;
-using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Materials
@@ -24,7 +23,7 @@ namespace Content.IntegrationTests.Tests.Materials
var server = pair.Server;
await server.WaitIdleAsync();
- var mapManager = server.ResolveDependency();
+ var mapSystem = server.System();
var prototypeManager = server.ResolveDependency();
var entityManager = server.ResolveDependency();
@@ -59,7 +58,7 @@ namespace Content.IntegrationTests.Tests.Materials
}
});
- mapManager.DeleteMap(testMap.MapId);
+ mapSystem.DeleteMap(testMap.MapId);
});
await pair.CleanReturnAsync();
diff --git a/Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs b/Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs
index de7739b2ad..6f33188813 100644
--- a/Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs
+++ b/Content.IntegrationTests/Tests/Minds/MindTests.EntityDeletion.cs
@@ -81,7 +81,7 @@ public sealed partial class MindTests
var testMap2 = await pair.CreateTestMap();
var entMan = server.ResolveDependency();
- var mapManager = server.ResolveDependency();
+ var mapSystem = server.System();
var playerMan = server.ResolveDependency();
var player = playerMan.Sessions.Single();
@@ -101,7 +101,7 @@ public sealed partial class MindTests
});
await pair.RunTicksSync(5);
- await server.WaitAssertion(() => mapManager.DeleteMap(testMap.MapId));
+ await server.WaitAssertion(() => mapSystem.DeleteMap(testMap.MapId));
await pair.RunTicksSync(5);
await server.WaitAssertion(() =>
diff --git a/Content.IntegrationTests/Tests/PostMapInitTest.cs b/Content.IntegrationTests/Tests/PostMapInitTest.cs
index 3cb064c8f0..1e7c173263 100644
--- a/Content.IntegrationTests/Tests/PostMapInitTest.cs
+++ b/Content.IntegrationTests/Tests/PostMapInitTest.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using Content.Server.Administration.Systems;
using Content.Server.GameTicking;
using Content.Server.Maps;
using Content.Server.Shuttles.Components;
@@ -42,6 +43,7 @@ namespace Content.IntegrationTests.Tests
//CrystallEdge Map replacement
//CrystallEdge Map replacement end
+ AdminTestArenaSystem.ArenaMapPath
};
private static readonly string[] DoNotMapWhitelist =
diff --git a/Content.IntegrationTests/Tests/Roles/StartingGearStorageTests.cs b/Content.IntegrationTests/Tests/Roles/StartingGearStorageTests.cs
index 3b2935258a..de89f16be7 100644
--- a/Content.IntegrationTests/Tests/Roles/StartingGearStorageTests.cs
+++ b/Content.IntegrationTests/Tests/Roles/StartingGearStorageTests.cs
@@ -2,7 +2,6 @@ using System.Linq;
using Content.Shared.Roles;
using Content.Server.Storage.EntitySystems;
using Robust.Shared.GameObjects;
-using Robust.Shared.Map;
using Robust.Shared.Collections;
namespace Content.IntegrationTests.Tests.Roles;
@@ -19,7 +18,7 @@ public sealed class StartingGearPrototypeStorageTest
var settings = new PoolSettings { Connected = true, Dirty = true };
await using var pair = await PoolManager.GetServerClient(settings);
var server = pair.Server;
- var mapManager = server.ResolveDependency();
+ var mapSystem = server.System();
var storageSystem = server.System();
var protos = server.ProtoMan
@@ -64,7 +63,7 @@ public sealed class StartingGearPrototypeStorageTest
}
}
- mapManager.DeleteMap(testMap.MapId);
+ mapSystem.DeleteMap(testMap.MapId);
});
await pair.CleanReturnAsync();
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/IdCardSystem.cs b/Content.Server/Access/Systems/IdCardSystem.cs
index aeb4cc163f..a9b08aac8b 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,7 +78,12 @@ 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);
diff --git a/Content.Server/Administration/Commands/SetMindCommand.cs b/Content.Server/Administration/Commands/SetMindCommand.cs
index a7b6849423..423c30ae37 100644
--- a/Content.Server/Administration/Commands/SetMindCommand.cs
+++ b/Content.Server/Administration/Commands/SetMindCommand.cs
@@ -74,5 +74,15 @@ namespace Content.Server.Administration.Commands
mindSystem.TransferTo(mind, eUid, ghostOverride);
}
+
+ public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
+ {
+ if (args.Length == 2)
+ {
+ return CompletionResult.FromHintOptions(CompletionHelper.SessionNames(), Loc.GetString("cmd-mind-command-hint"));
+ }
+
+ return CompletionResult.Empty;
+ }
}
}
diff --git a/Content.Server/Administration/Systems/AdminTestArenaSystem.cs b/Content.Server/Administration/Systems/AdminTestArenaSystem.cs
index 12bf0eba64..f45af60f6d 100644
--- a/Content.Server/Administration/Systems/AdminTestArenaSystem.cs
+++ b/Content.Server/Administration/Systems/AdminTestArenaSystem.cs
@@ -12,6 +12,7 @@ public sealed class AdminTestArenaSystem : EntitySystem
{
[Dependency] private readonly MapLoaderSystem _loader = default!;
[Dependency] private readonly MetaDataSystem _metaDataSystem = default!;
+ [Dependency] private readonly SharedMapSystem _maps = default!;
public const string ArenaMapPath = "/Maps/Test/admin_test_arena.yml";
@@ -33,17 +34,20 @@ public sealed class AdminTestArenaSystem : EntitySystem
}
var path = new ResPath(ArenaMapPath);
- if (!_loader.TryLoadMap(path, out var map, out var grids))
+ var mapUid = _maps.CreateMap(out var mapId);
+
+ if (!_loader.TryLoadGrid(mapId, path, out var grid))
+ {
+ QueueDel(mapUid);
throw new Exception($"Failed to load admin arena");
+ }
- ArenaMap[admin.UserId] = map.Value.Owner;
- _metaDataSystem.SetEntityName(map.Value.Owner, $"ATAM-{admin.Name}");
+ ArenaMap[admin.UserId] = mapUid;
+ _metaDataSystem.SetEntityName(mapUid, $"ATAM-{admin.Name}");
- var grid = grids.FirstOrNull();
- ArenaGrid[admin.UserId] = grid?.Owner;
- if (grid != null)
- _metaDataSystem.SetEntityName(grid.Value.Owner, $"ATAG-{admin.Name}");
+ ArenaGrid[admin.UserId] = grid.Value.Owner;
+ _metaDataSystem.SetEntityName(grid.Value.Owner, $"ATAG-{admin.Name}");
- return (map.Value.Owner, grid?.Owner);
+ return (mapUid, grid.Value.Owner);
}
}
diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
index c69c865d84..43a09e471b 100644
--- a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
+++ b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
@@ -53,23 +53,25 @@ public sealed partial class AdminVerbSystem
var targetPlayer = targetActor.PlayerSession;
/* CP14 disable default antags
+ var traitorName = Loc.GetString("admin-verb-text-make-traitor");
Verb traitor = new()
{
- Text = Loc.GetString("admin-verb-text-make-traitor"),
+ Text = traitorName,
Category = VerbCategory.Antag,
- Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Structures/Wallmounts/posters.rsi"), "poster5_contraband"),
+ Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Interface/Misc/job_icons.rsi"), "Syndicate"),
Act = () =>
{
_antag.ForceMakeAntag(targetPlayer, DefaultTraitorRule);
},
Impact = LogImpact.High,
- Message = Loc.GetString("admin-verb-make-traitor"),
+ Message = string.Join(": ", traitorName, Loc.GetString("admin-verb-make-traitor")),
};
args.Verbs.Add(traitor);
+ var initialInfectedName = Loc.GetString("admin-verb-text-make-initial-infected");
Verb initialInfected = new()
{
- Text = Loc.GetString("admin-verb-text-make-initial-infected"),
+ Text = initialInfectedName,
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "InitialInfected"),
Act = () =>
@@ -77,42 +79,44 @@ public sealed partial class AdminVerbSystem
_antag.ForceMakeAntag(targetPlayer, DefaultInitialInfectedRule);
},
Impact = LogImpact.High,
- Message = Loc.GetString("admin-verb-make-initial-infected"),
+ Message = string.Join(": ", initialInfectedName, Loc.GetString("admin-verb-make-initial-infected")),
};
args.Verbs.Add(initialInfected);
+ var zombieName = Loc.GetString("admin-verb-text-make-zombie");
Verb zombie = new()
{
- Text = Loc.GetString("admin-verb-text-make-zombie"),
+ Text = zombieName,
Category = VerbCategory.Antag,
- Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/Actions/zombie-turn.png")),
+ Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "Zombie"),
Act = () =>
{
_zombie.ZombifyEntity(args.Target);
},
Impact = LogImpact.High,
- Message = Loc.GetString("admin-verb-make-zombie"),
+ Message = string.Join(": ", zombieName, Loc.GetString("admin-verb-make-zombie")),
};
args.Verbs.Add(zombie);
-
+ var nukeOpName = Loc.GetString("admin-verb-text-make-nuclear-operative");
Verb nukeOp = new()
{
- Text = Loc.GetString("admin-verb-text-make-nuclear-operative"),
+ Text = nukeOpName,
Category = VerbCategory.Antag,
- Icon = new SpriteSpecifier.Rsi(new("/Textures/Structures/Wallmounts/signs.rsi"), "radiation"),
+ Icon = new SpriteSpecifier.Rsi(new("/Textures/Clothing/Head/Hardsuits/syndicate.rsi"), "icon"),
Act = () =>
{
_antag.ForceMakeAntag(targetPlayer, DefaultNukeOpRule);
},
Impact = LogImpact.High,
- Message = Loc.GetString("admin-verb-make-nuclear-operative"),
+ Message = string.Join(": ", nukeOpName, Loc.GetString("admin-verb-make-nuclear-operative")),
};
args.Verbs.Add(nukeOp);
+ var pirateName = Loc.GetString("admin-verb-text-make-pirate");
Verb pirate = new()
{
- Text = Loc.GetString("admin-verb-text-make-pirate"),
+ Text = pirateName,
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Rsi(new("/Textures/Clothing/Head/Hats/pirate.rsi"), "icon"),
Act = () =>
@@ -121,13 +125,14 @@ public sealed partial class AdminVerbSystem
SetOutfitCommand.SetOutfit(args.Target, PirateGearId, EntityManager);
},
Impact = LogImpact.High,
- Message = Loc.GetString("admin-verb-make-pirate"),
+ Message = string.Join(": ", pirateName, Loc.GetString("admin-verb-make-pirate")),
};
args.Verbs.Add(pirate);
+ var headRevName = Loc.GetString("admin-verb-text-make-head-rev");
Verb headRev = new()
{
- Text = Loc.GetString("admin-verb-text-make-head-rev"),
+ Text = headRevName,
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "HeadRevolutionary"),
Act = () =>
@@ -135,13 +140,14 @@ public sealed partial class AdminVerbSystem
_antag.ForceMakeAntag(targetPlayer, DefaultRevsRule);
},
Impact = LogImpact.High,
- Message = Loc.GetString("admin-verb-make-head-rev"),
+ Message = string.Join(": ", headRevName, Loc.GetString("admin-verb-make-head-rev")),
};
args.Verbs.Add(headRev);
+ var thiefName = Loc.GetString("admin-verb-text-make-thief");
Verb thief = new()
{
- Text = Loc.GetString("admin-verb-text-make-thief"),
+ Text = thiefName,
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Clothing/Hands/Gloves/Color/black.rsi"), "icon"),
Act = () =>
@@ -149,7 +155,7 @@ public sealed partial class AdminVerbSystem
_antag.ForceMakeAntag(targetPlayer, DefaultThiefRule);
},
Impact = LogImpact.High,
- Message = Loc.GetString("admin-verb-make-thief"),
+ Message = string.Join(": ", thiefName, Loc.GetString("admin-verb-make-thief")),
};
args.Verbs.Add(thief);
*/
diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs
index 0459b4bdd6..8d56ebf0ff 100644
--- a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs
+++ b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs
@@ -149,7 +149,7 @@ public sealed partial class AdminVerbSystem
var flamesName = Loc.GetString("admin-smite-set-alight-name").ToLowerInvariant();
Verb flames = new()
{
- Text = "admin-smite-set-alight-name",
+ Text = flamesName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/Alerts/Fire/fire.png")),
Act = () =>
@@ -481,7 +481,7 @@ public sealed partial class AdminVerbSystem
var breadName = Loc.GetString("admin-smite-become-bread-name").ToLowerInvariant(); // Will I get cancelled for breadName-ing you?
Verb bread = new()
{
- Text = "admin-smite-kill-sign-name",
+ Text = breadName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Consumable/Food/Baked/bread.rsi"), "plain"),
Act = () =>
@@ -496,7 +496,7 @@ public sealed partial class AdminVerbSystem
var mouseName = Loc.GetString("admin-smite-become-mouse-name").ToLowerInvariant();
Verb mouse = new()
{
- Text = "admin-smite-cluwne-name",
+ Text = mouseName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Mobs/Animals/mouse.rsi"), "icon-0"),
Act = () =>
@@ -650,7 +650,7 @@ public sealed partial class AdminVerbSystem
var instrumentationName = Loc.GetString("admin-smite-become-instrument-name").ToLowerInvariant();
Verb instrumentation = new()
{
- Text = "admin-smite-become-mouse-name",
+ Text = instrumentationName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/Instruments/h_synthesizer.rsi"), "icon"),
Act = () =>
@@ -721,7 +721,7 @@ public sealed partial class AdminVerbSystem
var headstandName = Loc.GetString("admin-smite-headstand-name").ToLowerInvariant();
Verb headstand = new()
{
- Text = "admin-smite-run-walk-swap-name",
+ Text = headstandName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/refresh.svg.192dpi.png")),
Act = () =>
@@ -819,7 +819,7 @@ public sealed partial class AdminVerbSystem
var superSpeedName = Loc.GetString("admin-smite-super-speed-name").ToLowerInvariant();
Verb superSpeed = new()
{
- Text = "admin-smite-garbage-can-name",
+ Text = superSpeedName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/super_speed.png")),
Act = () =>
@@ -852,7 +852,7 @@ public sealed partial class AdminVerbSystem
args.Verbs.Add(superBonkLite);
var superBonkName = Loc.GetString("admin-smite-super-bonk-name").ToLowerInvariant();
- Verb superBonk= new()
+ Verb superBonk = new()
{
Text = superBonkName,
Category = VerbCategory.Smite,
diff --git a/Content.Server/Administration/Systems/BwoinkSystem.cs b/Content.Server/Administration/Systems/BwoinkSystem.cs
index b3039e92ff..2df9aa9fcc 100644
--- a/Content.Server/Administration/Systems/BwoinkSystem.cs
+++ b/Content.Server/Administration/Systems/BwoinkSystem.cs
@@ -759,6 +759,7 @@ namespace Content.Server.Administration.Systems
_gameTicker.RoundDuration().ToString("hh\\:mm\\:ss"),
_gameTicker.RunLevel,
playedSound: playSound,
+ adminOnly: message.AdminOnly,
noReceivers: nonAfkAdmins.Count == 0
);
_messageQueues[msg.UserId].Enqueue(GenerateAHelpMessage(messageParams));
@@ -790,7 +791,7 @@ namespace Content.Server.Administration.Systems
.ToList();
}
- private static DiscordRelayedData GenerateAHelpMessage(AHelpMessageParams parameters)
+ private DiscordRelayedData GenerateAHelpMessage(AHelpMessageParams parameters)
{
var stringbuilder = new StringBuilder();
@@ -806,7 +807,7 @@ namespace Content.Server.Administration.Systems
if (parameters.RoundTime != string.Empty && parameters.RoundState == GameRunLevel.InRound)
stringbuilder.Append($" **{parameters.RoundTime}**");
if (!parameters.PlayedSound)
- stringbuilder.Append(" **(S)**");
+ stringbuilder.Append($" **{(parameters.AdminOnly ? Loc.GetString("bwoink-message-admin-only") : Loc.GetString("bwoink-message-silent"))}**");
if (parameters.Icon == null)
stringbuilder.Append($" **{parameters.Username}:** ");
else
@@ -869,6 +870,7 @@ namespace Content.Server.Administration.Systems
public string RoundTime { get; set; }
public GameRunLevel RoundState { get; set; }
public bool PlayedSound { get; set; }
+ public readonly bool AdminOnly;
public bool NoReceivers { get; set; }
public string? Icon { get; set; }
@@ -879,6 +881,7 @@ namespace Content.Server.Administration.Systems
string roundTime,
GameRunLevel roundState,
bool playedSound,
+ bool adminOnly = false,
bool noReceivers = false,
string? icon = null)
{
@@ -888,6 +891,7 @@ namespace Content.Server.Administration.Systems
RoundTime = roundTime;
RoundState = roundState;
PlayedSound = playedSound;
+ AdminOnly = adminOnly;
NoReceivers = noReceivers;
Icon = icon;
}
diff --git a/Content.Server/Advertise/EntitySystems/SpeakOnUIClosedSystem.cs b/Content.Server/Advertise/EntitySystems/SpeakOnUIClosedSystem.cs
index a0a709e5fa..3fca640d4a 100644
--- a/Content.Server/Advertise/EntitySystems/SpeakOnUIClosedSystem.cs
+++ b/Content.Server/Advertise/EntitySystems/SpeakOnUIClosedSystem.cs
@@ -1,13 +1,13 @@
-using Content.Server.Advertise.Components;
using Content.Server.Chat.Systems;
-using Content.Shared.Dataset;
+using Content.Shared.Advertise.Components;
+using Content.Shared.Advertise.Systems;
+using Content.Shared.UserInterface;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
-using ActivatableUIComponent = Content.Shared.UserInterface.ActivatableUIComponent;
-namespace Content.Server.Advertise;
+namespace Content.Server.Advertise.EntitySystems;
-public sealed partial class SpeakOnUIClosedSystem : EntitySystem
+public sealed partial class SpeakOnUIClosedSystem : SharedSpeakOnUIClosedSystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
@@ -46,13 +46,4 @@ public sealed partial class SpeakOnUIClosedSystem : EntitySystem
entity.Comp.Flag = false;
return true;
}
-
- public bool TrySetFlag(Entity entity, bool value = true)
- {
- if (!Resolve(entity, ref entity.Comp))
- return false;
-
- entity.Comp.Flag = value;
- return true;
- }
}
diff --git a/Content.Server/AlertLevel/AlertLevelPrototype.cs b/Content.Server/AlertLevel/AlertLevelPrototype.cs
index c6740c16cc..ccf6ff912c 100644
--- a/Content.Server/AlertLevel/AlertLevelPrototype.cs
+++ b/Content.Server/AlertLevel/AlertLevelPrototype.cs
@@ -6,7 +6,7 @@ namespace Content.Server.AlertLevel;
[Prototype("alertLevels")]
public sealed partial class AlertLevelPrototype : IPrototype
{
- [IdDataField] public string ID { get; } = default!;
+ [IdDataField] public string ID { get; private set; } = default!;
///
/// Dictionary of alert levels. Keyed by string - the string key is the most important
diff --git a/Content.Server/Antag/Components/AntagRandomObjectivesComponent.cs b/Content.Server/Antag/Components/AntagRandomObjectivesComponent.cs
index 9a551acc49..4b5e0679f5 100644
--- a/Content.Server/Antag/Components/AntagRandomObjectivesComponent.cs
+++ b/Content.Server/Antag/Components/AntagRandomObjectivesComponent.cs
@@ -28,7 +28,7 @@ public sealed partial class AntagRandomObjectivesComponent : Component
/// Difficulty is checked over all sets, but each set has its own probability and pick count.
///
[DataRecord]
-public record struct AntagObjectiveSet()
+public partial record struct AntagObjectiveSet()
{
///
/// The grouping used by the objective system to pick random objectives.
diff --git a/Content.Server/Arcade/BlockGame/BlockGameArcadeSystem.cs b/Content.Server/Arcade/BlockGame/BlockGameArcadeSystem.cs
index b0bf389509..a0e52e9b48 100644
--- a/Content.Server/Arcade/BlockGame/BlockGameArcadeSystem.cs
+++ b/Content.Server/Arcade/BlockGame/BlockGameArcadeSystem.cs
@@ -1,11 +1,9 @@
-using Content.Server.Power.Components;
using Content.Shared.UserInterface;
-using Content.Server.Advertise;
-using Content.Server.Advertise.Components;
+using Content.Server.Advertise.EntitySystems;
+using Content.Shared.Advertise.Components;
using Content.Shared.Arcade;
using Content.Shared.Power;
using Robust.Server.GameObjects;
-using Robust.Shared.Player;
namespace Content.Server.Arcade.BlockGame;
diff --git a/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeSystem.cs b/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeSystem.cs
index b359a13bd1..2070ab8bfe 100644
--- a/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeSystem.cs
+++ b/Content.Server/Arcade/SpaceVillainGame/SpaceVillainArcadeSystem.cs
@@ -1,9 +1,9 @@
using Content.Server.Power.Components;
using Content.Shared.UserInterface;
-using Content.Server.Advertise;
-using Content.Server.Advertise.Components;
+using Content.Server.Advertise.EntitySystems;
+using Content.Shared.Advertise.Components;
+using Content.Shared.Arcade;
using Content.Shared.Power;
-using static Content.Shared.Arcade.SharedSpaceVillainArcadeComponent;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
@@ -24,7 +24,7 @@ public sealed partial class SpaceVillainArcadeSystem : EntitySystem
SubscribeLocalEvent(OnComponentInit);
SubscribeLocalEvent(OnAfterUIOpenSV);
- SubscribeLocalEvent(OnSVPlayerAction);
+ SubscribeLocalEvent(OnSVPlayerAction);
SubscribeLocalEvent(OnSVillainPower);
}
@@ -70,7 +70,7 @@ public sealed partial class SpaceVillainArcadeSystem : EntitySystem
component.RewardAmount = new Random().Next(component.RewardMinAmount, component.RewardMaxAmount + 1);
}
- private void OnSVPlayerAction(EntityUid uid, SpaceVillainArcadeComponent component, SpaceVillainArcadePlayerActionMessage msg)
+ private void OnSVPlayerAction(EntityUid uid, SpaceVillainArcadeComponent component, SharedSpaceVillainArcadeComponent.SpaceVillainArcadePlayerActionMessage msg)
{
if (component.Game == null)
return;
@@ -79,22 +79,22 @@ public sealed partial class SpaceVillainArcadeSystem : EntitySystem
switch (msg.PlayerAction)
{
- case PlayerAction.Attack:
- case PlayerAction.Heal:
- case PlayerAction.Recharge:
+ case SharedSpaceVillainArcadeComponent.PlayerAction.Attack:
+ case SharedSpaceVillainArcadeComponent.PlayerAction.Heal:
+ case SharedSpaceVillainArcadeComponent.PlayerAction.Recharge:
component.Game.ExecutePlayerAction(uid, msg.PlayerAction, component);
// Any sort of gameplay action counts
if (TryComp(uid, out var speakComponent))
_speakOnUIClosed.TrySetFlag((uid, speakComponent));
break;
- case PlayerAction.NewGame:
+ case SharedSpaceVillainArcadeComponent.PlayerAction.NewGame:
_audioSystem.PlayPvs(component.NewGameSound, uid, AudioParams.Default.WithVolume(-4f));
component.Game = new SpaceVillainGame(uid, component, this);
- _uiSystem.ServerSendUiMessage(uid, SpaceVillainArcadeUiKey.Key, component.Game.GenerateMetaDataMessage());
+ _uiSystem.ServerSendUiMessage(uid, SharedSpaceVillainArcadeComponent.SpaceVillainArcadeUiKey.Key, component.Game.GenerateMetaDataMessage());
break;
- case PlayerAction.RequestData:
- _uiSystem.ServerSendUiMessage(uid, SpaceVillainArcadeUiKey.Key, component.Game.GenerateMetaDataMessage());
+ case SharedSpaceVillainArcadeComponent.PlayerAction.RequestData:
+ _uiSystem.ServerSendUiMessage(uid, SharedSpaceVillainArcadeComponent.SpaceVillainArcadeUiKey.Key, component.Game.GenerateMetaDataMessage());
break;
}
}
@@ -109,6 +109,6 @@ public sealed partial class SpaceVillainArcadeSystem : EntitySystem
if (TryComp(uid, out var power) && power.Powered)
return;
- _uiSystem.CloseUi(uid, SpaceVillainArcadeUiKey.Key);
+ _uiSystem.CloseUi(uid, SharedSpaceVillainArcadeComponent.SpaceVillainArcadeUiKey.Key);
}
}
diff --git a/Content.Server/Bible/BibleSystem.cs b/Content.Server/Bible/BibleSystem.cs
index 76efe3290b..d9033a0f94 100644
--- a/Content.Server/Bible/BibleSystem.cs
+++ b/Content.Server/Bible/BibleSystem.cs
@@ -84,7 +84,7 @@ namespace Content.Server.Bible
}
summonableComp.AlreadySummoned = false;
_popupSystem.PopupEntity(Loc.GetString("bible-summon-respawn-ready", ("book", uid)), uid, PopupType.Medium);
- _audio.PlayPvs("/Audio/Effects/radpulse9.ogg", uid, AudioParams.Default.WithVolume(-4f));
+ _audio.PlayPvs(summonableComp.SummonSound, uid);
// Clean up the accumulator and respawn tracking component
summonableComp.Accumulator = 0;
_remQueue.Enqueue(uid);
@@ -126,7 +126,7 @@ namespace Content.Server.Bible
var selfFailMessage = Loc.GetString(component.LocPrefix + "-heal-fail-self", ("target", Identity.Entity(args.Target.Value, EntityManager)), ("bible", uid));
_popupSystem.PopupEntity(selfFailMessage, args.User, args.User, PopupType.MediumCaution);
- _audio.PlayPvs("/Audio/Effects/hit_kick.ogg", args.User);
+ _audio.PlayPvs(component.BibleHitSound, args.User);
_damageableSystem.TryChangeDamage(args.Target.Value, component.DamageOnFail, true, origin: uid);
_delay.TryResetDelay((uid, useDelay));
return;
diff --git a/Content.Server/Bible/Components/BibleComponent.cs b/Content.Server/Bible/Components/BibleComponent.cs
index b7dc3db8e3..7f4af569e5 100644
--- a/Content.Server/Bible/Components/BibleComponent.cs
+++ b/Content.Server/Bible/Components/BibleComponent.cs
@@ -1,11 +1,23 @@
using Content.Shared.Damage;
using Robust.Shared.Audio;
+using Robust.Shared.Prototypes;
namespace Content.Server.Bible.Components
{
[RegisterComponent]
public sealed partial class BibleComponent : Component
{
+ ///
+ /// Default sound when bible hits somebody.
+ ///
+ private static readonly ProtoId DefaultBibleHit = new("BibleHit");
+
+ ///
+ /// Sound to play when bible hits somebody.
+ ///
+ [DataField]
+ public SoundSpecifier BibleHitSound = new SoundCollectionSpecifier(DefaultBibleHit, AudioParams.Default.WithVolume(-4f));
+
///
/// Damage that will be healed on a success
///
diff --git a/Content.Server/Bible/Components/SummonableComponent.cs b/Content.Server/Bible/Components/SummonableComponent.cs
index 62f536d5d6..fa180c932e 100644
--- a/Content.Server/Bible/Components/SummonableComponent.cs
+++ b/Content.Server/Bible/Components/SummonableComponent.cs
@@ -1,3 +1,4 @@
+using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
@@ -9,6 +10,17 @@ namespace Content.Server.Bible.Components
[RegisterComponent]
public sealed partial class SummonableComponent : Component
{
+ ///
+ /// Default sound to play when entity is summoned.
+ ///
+ private static readonly ProtoId DefaultSummonSound = new("Summon");
+
+ ///
+ /// Sound to play when entity is summoned.
+ ///
+ [DataField]
+ public SoundSpecifier SummonSound = new SoundCollectionSpecifier(DefaultSummonSound, AudioParams.Default.WithVolume(-4f));
+
///
/// Used for a special item only the Chaplain can summon. Usually a mob, but supports regular items too.
///
diff --git a/Content.Server/Body/Systems/BloodstreamSystem.cs b/Content.Server/Body/Systems/BloodstreamSystem.cs
index d04a993226..6dc03fed74 100644
--- a/Content.Server/Body/Systems/BloodstreamSystem.cs
+++ b/Content.Server/Body/Systems/BloodstreamSystem.cs
@@ -1,7 +1,6 @@
using Content.Server.Body.Components;
using Content.Server.EntityEffects.Effects;
using Content.Server.Fluids.EntitySystems;
-using Content.Server.Forensics;
using Content.Server.Popups;
using Content.Shared.Alert;
using Content.Shared.Chemistry.Components;
@@ -40,7 +39,6 @@ public sealed class BloodstreamSystem : EntitySystem
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly SharedStutteringSystem _stutteringSystem = default!;
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
- [Dependency] private readonly ForensicsSystem _forensicsSystem = default!;
public override void Initialize()
{
@@ -193,17 +191,8 @@ public sealed class BloodstreamSystem : EntitySystem
bloodSolution.MaxVolume = entity.Comp.BloodMaxVolume;
tempSolution.MaxVolume = entity.Comp.BleedPuddleThreshold * 4; // give some leeway, for chemstream as well
- // Ensure blood that should have DNA has it; must be run here, in case DnaComponent has not yet been initialized
-
- if (TryComp(entity.Owner, out var donorComp) && donorComp.DNA == String.Empty)
- {
- donorComp.DNA = _forensicsSystem.GenerateDNA();
-
- var ev = new GenerateDnaEvent { Owner = entity.Owner, DNA = donorComp.DNA };
- RaiseLocalEvent(entity.Owner, ref ev);
- }
-
// Fill blood solution with BLOOD
+ // The DNA string might not be initialized yet, but the reagent data gets updated in the GenerateDnaEvent subscription
bloodSolution.AddReagent(new ReagentId(entity.Comp.BloodReagent, GetEntityBloodData(entity.Owner)), entity.Comp.BloodMaxVolume - bloodSolution.Volume);
}
@@ -492,6 +481,8 @@ public sealed class BloodstreamSystem : EntitySystem
reagentData.AddRange(GetEntityBloodData(entity.Owner));
}
}
+ else
+ Log.Error("Unable to set bloodstream DNA, solution entity could not be resolved");
}
///
@@ -502,13 +493,10 @@ public sealed class BloodstreamSystem : EntitySystem
var bloodData = new List();
var dnaData = new DnaData();
- if (TryComp(uid, out var donorComp))
- {
+ if (TryComp(uid, out var donorComp) && donorComp.DNA != null)
dnaData.DNA = donorComp.DNA;
- } else
- {
+ else
dnaData.DNA = Loc.GetString("forensics-dna-unknown");
- }
bloodData.Add(dnaData);
diff --git a/Content.Server/Botany/SeedPrototype.cs b/Content.Server/Botany/SeedPrototype.cs
index 5608338f22..1eb22241cd 100644
--- a/Content.Server/Botany/SeedPrototype.cs
+++ b/Content.Server/Botany/SeedPrototype.cs
@@ -14,7 +14,7 @@ namespace Content.Server.Botany;
[Prototype("seed")]
public sealed partial class SeedPrototype : SeedData, IPrototype
{
- [IdDataField] public string ID { get; private init; } = default!;
+ [IdDataField] public string ID { get; private set; } = default!;
}
public enum HarvestType : byte
diff --git a/Content.Server/CartridgeLoader/CartridgeLoaderSystem.cs b/Content.Server/CartridgeLoader/CartridgeLoaderSystem.cs
index 98df7e2c50..8637e44e62 100644
--- a/Content.Server/CartridgeLoader/CartridgeLoaderSystem.cs
+++ b/Content.Server/CartridgeLoader/CartridgeLoaderSystem.cs
@@ -427,6 +427,7 @@ public sealed class CartridgeLoaderSystem : SharedCartridgeLoaderSystem
private void OnUiMessage(EntityUid uid, CartridgeLoaderComponent component, CartridgeUiMessage args)
{
var cartridgeEvent = args.MessageEvent;
+ cartridgeEvent.User = args.Actor;
cartridgeEvent.LoaderUid = GetNetEntity(uid);
cartridgeEvent.Actor = args.Actor;
diff --git a/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeComponent.cs b/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeComponent.cs
index cfa92dd67f..659433de36 100644
--- a/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeComponent.cs
+++ b/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeComponent.cs
@@ -1,12 +1,21 @@
using Content.Shared.CartridgeLoader.Cartridges;
+using Content.Shared.Paper;
using Robust.Shared.Audio;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.CartridgeLoader.Cartridges;
-[RegisterComponent]
-[Access(typeof(LogProbeCartridgeSystem))]
+[RegisterComponent, Access(typeof(LogProbeCartridgeSystem))]
+[AutoGenerateComponentPause]
public sealed partial class LogProbeCartridgeComponent : Component
{
+ ///
+ /// The name of the scanned entity, sent to clients when they open the UI.
+ ///
+ [DataField]
+ public string EntityName = string.Empty;
+
///
/// The list of pulled access logs
///
@@ -18,4 +27,25 @@ public sealed partial class LogProbeCartridgeComponent : Component
///
[DataField, ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier SoundScan = new SoundPathSpecifier("/Audio/Machines/scan_finish.ogg");
+
+ ///
+ /// Paper to spawn when printing logs.
+ ///
+ [DataField]
+ public EntProtoId PaperPrototype = "PaperAccessLogs";
+
+ [DataField]
+ public SoundSpecifier PrintSound = new SoundPathSpecifier("/Audio/Machines/diagnoser_printing.ogg");
+
+ ///
+ /// How long you have to wait before printing logs again.
+ ///
+ [DataField]
+ public TimeSpan PrintCooldown = TimeSpan.FromSeconds(5);
+
+ ///
+ /// When anyone is allowed to spawn another printout.
+ ///
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
+ public TimeSpan NextPrintAllowed = TimeSpan.Zero;
}
diff --git a/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeSystem.cs b/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeSystem.cs
index f5ccea9590..ac5c0baa54 100644
--- a/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeSystem.cs
+++ b/Content.Server/CartridgeLoader/Cartridges/LogProbeCartridgeSystem.cs
@@ -1,25 +1,40 @@
using Content.Shared.Access.Components;
+using Content.Shared.Administration.Logs;
using Content.Shared.Audio;
using Content.Shared.CartridgeLoader;
using Content.Shared.CartridgeLoader.Cartridges;
+using Content.Shared.Database;
+using Content.Shared.Hands.EntitySystems;
+using Content.Shared.Labels.EntitySystems;
+using Content.Shared.Paper;
using Content.Shared.Popups;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Random;
+using Robust.Shared.Timing;
+using System.Text;
namespace Content.Server.CartridgeLoader.Cartridges;
public sealed class LogProbeCartridgeSystem : EntitySystem
{
+ [Dependency] private readonly CartridgeLoaderSystem _cartridge = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly CartridgeLoaderSystem? _cartridgeLoaderSystem = default!;
- [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
- [Dependency] private readonly SharedAudioSystem _audioSystem = default!;
+ [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedHandsSystem _hands = default!;
+ [Dependency] private readonly SharedLabelSystem _label = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly PaperSystem _paper = default!;
public override void Initialize()
{
base.Initialize();
+
SubscribeLocalEvent(OnUiReady);
SubscribeLocalEvent(AfterInteract);
+ SubscribeLocalEvent(OnMessage);
}
///
@@ -37,9 +52,10 @@ public sealed class LogProbeCartridgeSystem : EntitySystem
return;
//Play scanning sound with slightly randomized pitch
- _audioSystem.PlayEntity(ent.Comp.SoundScan, args.InteractEvent.User, target, AudioHelpers.WithVariation(0.25f, _random));
- _popupSystem.PopupCursor(Loc.GetString("log-probe-scan", ("device", target)), args.InteractEvent.User);
+ _audio.PlayEntity(ent.Comp.SoundScan, args.InteractEvent.User, target, AudioHelpers.WithVariation(0.25f, _random));
+ _popup.PopupCursor(Loc.GetString("log-probe-scan", ("device", target)), args.InteractEvent.User);
+ ent.Comp.EntityName = Name(target);
ent.Comp.PulledAccessLogs.Clear();
foreach (var accessRecord in accessReaderComponent.AccessLog)
@@ -52,6 +68,9 @@ public sealed class LogProbeCartridgeSystem : EntitySystem
ent.Comp.PulledAccessLogs.Add(log);
}
+ // Reverse the list so the oldest is at the bottom
+ ent.Comp.PulledAccessLogs.Reverse();
+
UpdateUiState(ent, args.Loader);
}
@@ -63,9 +82,49 @@ public sealed class LogProbeCartridgeSystem : EntitySystem
UpdateUiState(ent, args.Loader);
}
+ private void OnMessage(Entity ent, ref CartridgeMessageEvent args)
+ {
+ if (args is LogProbePrintMessage cast)
+ PrintLogs(ent, cast.User);
+ }
+
+ private void PrintLogs(Entity ent, EntityUid user)
+ {
+ if (string.IsNullOrEmpty(ent.Comp.EntityName))
+ return;
+
+ if (_timing.CurTime < ent.Comp.NextPrintAllowed)
+ return;
+
+ ent.Comp.NextPrintAllowed = _timing.CurTime + ent.Comp.PrintCooldown;
+
+ var paper = Spawn(ent.Comp.PaperPrototype, _transform.GetMapCoordinates(user));
+ _label.Label(paper, ent.Comp.EntityName); // label it for easy identification
+
+ _audio.PlayEntity(ent.Comp.PrintSound, user, paper);
+ _hands.PickupOrDrop(user, paper, checkActionBlocker: false);
+
+ // generate the actual printout text
+ var builder = new StringBuilder();
+ builder.AppendLine(Loc.GetString("log-probe-printout-device", ("name", ent.Comp.EntityName)));
+ builder.AppendLine(Loc.GetString("log-probe-printout-header"));
+ var number = 1;
+ foreach (var log in ent.Comp.PulledAccessLogs)
+ {
+ var time = TimeSpan.FromSeconds(Math.Truncate(log.Time.TotalSeconds)).ToString();
+ builder.AppendLine(Loc.GetString("log-probe-printout-entry", ("number", number), ("time", time), ("accessor", log.Accessor)));
+ number++;
+ }
+
+ var paperComp = Comp(paper);
+ _paper.SetContent((paper, paperComp), builder.ToString());
+
+ _adminLogger.Add(LogType.EntitySpawn, LogImpact.Low, $"{ToPrettyString(user):user} printed out LogProbe logs ({paper}) of {ent.Comp.EntityName}");
+ }
+
private void UpdateUiState(Entity ent, EntityUid loaderUid)
{
- var state = new LogProbeUiState(ent.Comp.PulledAccessLogs);
- _cartridgeLoaderSystem?.UpdateCartridgeUiState(loaderUid, state);
+ var state = new LogProbeUiState(ent.Comp.EntityName, ent.Comp.PulledAccessLogs);
+ _cartridge.UpdateCartridgeUiState(loaderUid, state);
}
}
diff --git a/Content.Server/Chemistry/Components/ChemMasterComponent.cs b/Content.Server/Chemistry/Components/ChemMasterComponent.cs
index 9578755ba7..0309d07ed9 100644
--- a/Content.Server/Chemistry/Components/ChemMasterComponent.cs
+++ b/Content.Server/Chemistry/Components/ChemMasterComponent.cs
@@ -18,6 +18,9 @@ namespace Content.Server.Chemistry.Components
[DataField("mode"), ViewVariables(VVAccess.ReadWrite)]
public ChemMasterMode Mode = ChemMasterMode.Transfer;
+ [DataField]
+ public ChemMasterSortingType SortingType = ChemMasterSortingType.None;
+
[DataField("pillDosageLimit", required: true), ViewVariables(VVAccess.ReadWrite)]
public uint PillDosageLimit;
diff --git a/Content.Server/Chemistry/EntitySystems/ChemMasterSystem.cs b/Content.Server/Chemistry/EntitySystems/ChemMasterSystem.cs
index c733c7215e..dd97bfa8f6 100644
--- a/Content.Server/Chemistry/EntitySystems/ChemMasterSystem.cs
+++ b/Content.Server/Chemistry/EntitySystems/ChemMasterSystem.cs
@@ -53,6 +53,7 @@ namespace Content.Server.Chemistry.EntitySystems
SubscribeLocalEvent(SubscribeUpdateUiState);
SubscribeLocalEvent(OnSetModeMessage);
+ SubscribeLocalEvent(OnCycleSortingTypeMessage);
SubscribeLocalEvent(OnSetPillTypeMessage);
SubscribeLocalEvent(OnReagentButtonMessage);
SubscribeLocalEvent(OnCreatePillsMessage);
@@ -76,7 +77,7 @@ namespace Content.Server.Chemistry.EntitySystems
var bufferCurrentVolume = bufferSolution.Volume;
var state = new ChemMasterBoundUserInterfaceState(
- chemMaster.Mode, BuildInputContainerInfo(inputContainer), BuildOutputContainerInfo(outputContainer),
+ chemMaster.Mode, chemMaster.SortingType, BuildInputContainerInfo(inputContainer), BuildOutputContainerInfo(outputContainer),
bufferReagents, bufferCurrentVolume, chemMaster.PillType, chemMaster.PillDosageLimit, updateLabel);
_userInterfaceSystem.SetUiState(owner, ChemMasterUiKey.Key, state);
@@ -93,6 +94,15 @@ namespace Content.Server.Chemistry.EntitySystems
ClickSound(chemMaster);
}
+ private void OnCycleSortingTypeMessage(Entity chemMaster, ref ChemMasterSortingTypeCycleMessage message)
+ {
+ chemMaster.Comp.SortingType++;
+ if (chemMaster.Comp.SortingType > ChemMasterSortingType.Latest)
+ chemMaster.Comp.SortingType = ChemMasterSortingType.None;
+ UpdateUiState(chemMaster);
+ ClickSound(chemMaster);
+ }
+
private void OnSetPillTypeMessage(Entity chemMaster, ref ChemMasterSetPillTypeMessage message)
{
// Ensure valid pill type. There are 20 pills selectable, 0-19.
diff --git a/Content.Server/Cloning/AcceptCloningEui.cs b/Content.Server/Cloning/AcceptCloningEui.cs
index 3d4356f8ca..2d1ea93fdb 100644
--- a/Content.Server/Cloning/AcceptCloningEui.cs
+++ b/Content.Server/Cloning/AcceptCloningEui.cs
@@ -9,13 +9,13 @@ namespace Content.Server.Cloning
{
private readonly EntityUid _mindId;
private readonly MindComponent _mind;
- private readonly CloningSystem _cloningSystem;
+ private readonly CloningPodSystem _cloningPodSystem;
- public AcceptCloningEui(EntityUid mindId, MindComponent mind, CloningSystem cloningSys)
+ public AcceptCloningEui(EntityUid mindId, MindComponent mind, CloningPodSystem cloningPodSys)
{
_mindId = mindId;
_mind = mind;
- _cloningSystem = cloningSys;
+ _cloningPodSystem = cloningPodSys;
}
public override void HandleMessage(EuiMessageBase msg)
@@ -29,7 +29,7 @@ namespace Content.Server.Cloning
return;
}
- _cloningSystem.TransferMindToClone(_mindId, _mind);
+ _cloningPodSystem.TransferMindToClone(_mindId, _mind);
Close();
}
}
diff --git a/Content.Server/Cloning/CloningConsoleSystem.cs b/Content.Server/Cloning/CloningConsoleSystem.cs
index 050e2b7f06..39eac842f0 100644
--- a/Content.Server/Cloning/CloningConsoleSystem.cs
+++ b/Content.Server/Cloning/CloningConsoleSystem.cs
@@ -3,7 +3,6 @@ using Content.Server.Administration.Logs;
using Content.Server.Cloning.Components;
using Content.Server.DeviceLinking.Systems;
using Content.Server.Medical.Components;
-using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.UserInterface;
using Content.Shared.Cloning;
@@ -16,19 +15,17 @@ using Content.Shared.Mind;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Power;
-using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Server.Player;
namespace Content.Server.Cloning
{
- [UsedImplicitly]
public sealed class CloningConsoleSystem : EntitySystem
{
[Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
- [Dependency] private readonly CloningSystem _cloningSystem = default!;
+ [Dependency] private readonly CloningPodSystem _cloningPodSystem = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
@@ -171,7 +168,7 @@ namespace Content.Server.Cloning
if (mind.UserId.HasValue == false || mind.Session == null)
return;
- if (_cloningSystem.TryCloning(cloningPodUid, body.Value, (mindId, mind), cloningPod, scannerComp.CloningFailChanceMultiplier))
+ if (_cloningPodSystem.TryCloning(cloningPodUid, body.Value, (mindId, mind), cloningPod, scannerComp.CloningFailChanceMultiplier))
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(uid)} successfully cloned {ToPrettyString(body.Value)}.");
}
diff --git a/Content.Server/Cloning/CloningPodSystem.cs b/Content.Server/Cloning/CloningPodSystem.cs
new file mode 100644
index 0000000000..594c5ebbb6
--- /dev/null
+++ b/Content.Server/Cloning/CloningPodSystem.cs
@@ -0,0 +1,323 @@
+using Content.Server.Atmos.EntitySystems;
+using Content.Server.Chat.Systems;
+using Content.Server.Cloning.Components;
+using Content.Server.DeviceLinking.Systems;
+using Content.Server.EUI;
+using Content.Server.Fluids.EntitySystems;
+using Content.Server.Materials;
+using Content.Server.Popups;
+using Content.Server.Power.EntitySystems;
+using Content.Shared.Atmos;
+using Content.Shared.CCVar;
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Cloning;
+using Content.Shared.Damage;
+using Content.Shared.DeviceLinking.Events;
+using Content.Shared.Emag.Components;
+using Content.Shared.Emag.Systems;
+using Content.Shared.Examine;
+using Content.Shared.GameTicking;
+using Content.Shared.Mind;
+using Content.Shared.Mind.Components;
+using Content.Shared.Mobs.Systems;
+using Robust.Server.Containers;
+using Robust.Server.Player;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Configuration;
+using Robust.Shared.Containers;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.Cloning;
+
+public sealed class CloningPodSystem : EntitySystem
+{
+ [Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = null!;
+ [Dependency] private readonly EuiManager _euiManager = null!;
+ [Dependency] private readonly CloningConsoleSystem _cloningConsoleSystem = default!;
+ [Dependency] private readonly ContainerSystem _containerSystem = default!;
+ [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
+ [Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
+ [Dependency] private readonly IRobustRandom _robustRandom = default!;
+ [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
+ [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+ [Dependency] private readonly PuddleSystem _puddleSystem = default!;
+ [Dependency] private readonly ChatSystem _chatSystem = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly IConfigurationManager _configManager = default!;
+ [Dependency] private readonly MaterialStorageSystem _material = default!;
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
+ [Dependency] private readonly SharedMindSystem _mindSystem = default!;
+ [Dependency] private readonly CloningSystem _cloning = default!;
+ [Dependency] private readonly EmagSystem _emag = default!;
+
+ public readonly Dictionary ClonesWaitingForMind = new();
+ public readonly ProtoId SettingsId = "CloningPod";
+ public const float EasyModeCloningCost = 0.7f;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(Reset);
+ SubscribeLocalEvent(HandleMindAdded);
+ SubscribeLocalEvent(OnComponentInit);
+ SubscribeLocalEvent(OnPortDisconnected);
+ SubscribeLocalEvent(OnAnchor);
+ SubscribeLocalEvent(OnExamined);
+ SubscribeLocalEvent(OnEmagged);
+ }
+
+ private void OnComponentInit(Entity ent, ref ComponentInit args)
+ {
+ ent.Comp.BodyContainer = _containerSystem.EnsureContainer(ent.Owner, "clonepod-bodyContainer");
+ _signalSystem.EnsureSinkPorts(ent.Owner, ent.Comp.PodPort);
+ }
+
+ internal void TransferMindToClone(EntityUid mindId, MindComponent mind)
+ {
+ if (!ClonesWaitingForMind.TryGetValue(mind, out var entity) ||
+ !EntityManager.EntityExists(entity) ||
+ !TryComp(entity, out var mindComp) ||
+ mindComp.Mind != null)
+ return;
+
+ _mindSystem.TransferTo(mindId, entity, ghostCheckOverride: true, mind: mind);
+ _mindSystem.UnVisit(mindId, mind);
+ ClonesWaitingForMind.Remove(mind);
+ }
+
+ private void HandleMindAdded(EntityUid uid, BeingClonedComponent clonedComponent, MindAddedMessage message)
+ {
+ if (clonedComponent.Parent == EntityUid.Invalid ||
+ !EntityManager.EntityExists(clonedComponent.Parent) ||
+ !TryComp(clonedComponent.Parent, out var cloningPodComponent) ||
+ uid != cloningPodComponent.BodyContainer.ContainedEntity)
+ {
+ EntityManager.RemoveComponent(uid);
+ return;
+ }
+ UpdateStatus(clonedComponent.Parent, CloningPodStatus.Cloning, cloningPodComponent);
+ }
+ private void OnPortDisconnected(Entity ent, ref PortDisconnectedEvent args)
+ {
+ ent.Comp.ConnectedConsole = null;
+ }
+
+ private void OnAnchor(Entity ent, ref AnchorStateChangedEvent args)
+ {
+ if (ent.Comp.ConnectedConsole == null || !TryComp(ent.Comp.ConnectedConsole, out var console))
+ return;
+
+ if (args.Anchored)
+ {
+ _cloningConsoleSystem.RecheckConnections(ent.Comp.ConnectedConsole.Value, ent.Owner, console.GeneticScanner, console);
+ return;
+ }
+ _cloningConsoleSystem.UpdateUserInterface(ent.Comp.ConnectedConsole.Value, console);
+ }
+
+ private void OnExamined(Entity ent, ref ExaminedEvent args)
+ {
+ if (!args.IsInDetailsRange || !_powerReceiverSystem.IsPowered(ent.Owner))
+ return;
+
+ args.PushMarkup(Loc.GetString("cloning-pod-biomass", ("number", _material.GetMaterialAmount(ent.Owner, ent.Comp.RequiredMaterial))));
+ }
+
+ public bool TryCloning(EntityUid uid, EntityUid bodyToClone, Entity mindEnt, CloningPodComponent? clonePod, float failChanceModifier = 1)
+ {
+ if (!Resolve(uid, ref clonePod))
+ return false;
+
+ if (HasComp(uid))
+ return false;
+
+ var mind = mindEnt.Comp;
+ if (ClonesWaitingForMind.TryGetValue(mind, out var clone))
+ {
+ if (EntityManager.EntityExists(clone) &&
+ !_mobStateSystem.IsDead(clone) &&
+ TryComp(clone, out var cloneMindComp) &&
+ (cloneMindComp.Mind == null || cloneMindComp.Mind == mindEnt))
+ return false; // Mind already has clone
+
+ ClonesWaitingForMind.Remove(mind);
+ }
+
+ if (mind.OwnedEntity != null && !_mobStateSystem.IsDead(mind.OwnedEntity.Value))
+ return false; // Body controlled by mind is not dead
+
+ // Yes, we still need to track down the client because we need to open the Eui
+ if (mind.UserId == null || !_playerManager.TryGetSessionById(mind.UserId.Value, out var client))
+ return false; // If we can't track down the client, we can't offer transfer. That'd be quite bad.
+
+ if (!TryComp(bodyToClone, out var physics))
+ return false;
+
+ var cloningCost = (int)Math.Round(physics.FixturesMass);
+
+ if (_configManager.GetCVar(CCVars.BiomassEasyMode))
+ cloningCost = (int)Math.Round(cloningCost * EasyModeCloningCost);
+
+ // biomass checks
+ var biomassAmount = _material.GetMaterialAmount(uid, clonePod.RequiredMaterial);
+
+ if (biomassAmount < cloningCost)
+ {
+ if (clonePod.ConnectedConsole != null)
+ _chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-chat-error", ("units", cloningCost)), InGameICChatType.Speak, false);
+ return false;
+ }
+
+ // end of biomass checks
+
+ // genetic damage checks
+ if (TryComp(bodyToClone, out var damageable) &&
+ damageable.Damage.DamageDict.TryGetValue("Cellular", out var cellularDmg))
+ {
+ var chance = Math.Clamp((float)(cellularDmg / 100), 0, 1);
+ chance *= failChanceModifier;
+
+ if (cellularDmg > 0 && clonePod.ConnectedConsole != null)
+ _chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-cellular-warning", ("percent", Math.Round(100 - chance * 100))), InGameICChatType.Speak, false);
+
+ if (_robustRandom.Prob(chance))
+ {
+ clonePod.FailedClone = true;
+ UpdateStatus(uid, CloningPodStatus.Gore, clonePod);
+ AddComp(uid);
+ _material.TryChangeMaterialAmount(uid, clonePod.RequiredMaterial, -cloningCost);
+ clonePod.UsedBiomass = cloningCost;
+ return true;
+ }
+ }
+ // end of genetic damage checks
+
+ if (!_cloning.TryCloning(bodyToClone, _transformSystem.GetMapCoordinates(bodyToClone), SettingsId, out var mob)) // spawn a new body
+ {
+ if (clonePod.ConnectedConsole != null)
+ _chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-uncloneable-trait-error"), InGameICChatType.Speak, false);
+ return false;
+ }
+
+ var cloneMindReturn = EntityManager.AddComponent(mob.Value);
+ cloneMindReturn.Mind = mind;
+ cloneMindReturn.Parent = uid;
+ _containerSystem.Insert(mob.Value, clonePod.BodyContainer);
+ ClonesWaitingForMind.Add(mind, mob.Value);
+ _euiManager.OpenEui(new AcceptCloningEui(mindEnt, mind, this), client);
+
+ UpdateStatus(uid, CloningPodStatus.NoMind, clonePod);
+ AddComp(uid);
+ _material.TryChangeMaterialAmount(uid, clonePod.RequiredMaterial, -cloningCost);
+ clonePod.UsedBiomass = cloningCost;
+ return true;
+ }
+
+ public void UpdateStatus(EntityUid podUid, CloningPodStatus status, CloningPodComponent cloningPod)
+ {
+ cloningPod.Status = status;
+ _appearance.SetData(podUid, CloningPodVisuals.Status, cloningPod.Status);
+ }
+
+ public override void Update(float frameTime)
+ {
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var _, out var cloning))
+ {
+ if (!_powerReceiverSystem.IsPowered(uid))
+ continue;
+
+ if (cloning.BodyContainer.ContainedEntity == null && !cloning.FailedClone)
+ continue;
+
+ cloning.CloningProgress += frameTime;
+ if (cloning.CloningProgress < cloning.CloningTime)
+ continue;
+
+ if (cloning.FailedClone)
+ EndFailedCloning(uid, cloning);
+ else
+ Eject(uid, cloning);
+ }
+ }
+
+ ///
+ /// On emag, spawns a failed clone when cloning process fails which attacks nearby crew.
+ ///
+ private void OnEmagged(Entity ent, ref GotEmaggedEvent args)
+ {
+ if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
+ return;
+
+ if (_emag.CheckFlag(ent.Owner, EmagType.Interaction))
+ return;
+
+ if (!this.IsPowered(ent.Owner, EntityManager))
+ return;
+
+ _popupSystem.PopupEntity(Loc.GetString("cloning-pod-component-upgrade-emag-requirement"), ent.Owner);
+ args.Handled = true;
+ }
+
+ public void Eject(EntityUid uid, CloningPodComponent? clonePod)
+ {
+ if (!Resolve(uid, ref clonePod))
+ return;
+
+ if (clonePod.BodyContainer.ContainedEntity is not { Valid: true } entity || clonePod.CloningProgress < clonePod.CloningTime)
+ return;
+
+ EntityManager.RemoveComponent(entity);
+ _containerSystem.Remove(entity, clonePod.BodyContainer);
+ clonePod.CloningProgress = 0f;
+ clonePod.UsedBiomass = 0;
+ UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
+ RemCompDeferred(uid);
+ }
+
+ private void EndFailedCloning(EntityUid uid, CloningPodComponent clonePod)
+ {
+ clonePod.FailedClone = false;
+ clonePod.CloningProgress = 0f;
+ UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
+ var transform = Transform(uid);
+ var indices = _transformSystem.GetGridTilePositionOrDefault((uid, transform));
+ var tileMix = _atmosphereSystem.GetTileMixture(transform.GridUid, null, indices, true);
+
+ if (HasComp(uid))
+ {
+ _audio.PlayPvs(clonePod.ScreamSound, uid);
+ Spawn(clonePod.MobSpawnId, transform.Coordinates);
+ }
+
+ Solution bloodSolution = new();
+
+ var i = 0;
+ while (i < 1)
+ {
+ tileMix?.AdjustMoles(Gas.Ammonia, 6f);
+ bloodSolution.AddReagent("Blood", 50);
+ if (_robustRandom.Prob(0.2f))
+ i++;
+ }
+ _puddleSystem.TrySpillAt(uid, bloodSolution, out _);
+
+ if (!HasComp(uid))
+ {
+ _material.SpawnMultipleFromMaterial(_robustRandom.Next(1, (int)(clonePod.UsedBiomass / 2.5)), clonePod.RequiredMaterial, Transform(uid).Coordinates);
+ }
+
+ clonePod.UsedBiomass = 0;
+ RemCompDeferred(uid);
+ }
+
+ public void Reset(RoundRestartCleanupEvent ev)
+ {
+ ClonesWaitingForMind.Clear();
+ }
+}
diff --git a/Content.Server/Cloning/CloningSystem.cs b/Content.Server/Cloning/CloningSystem.cs
index d8aac56515..937b311a59 100644
--- a/Content.Server/Cloning/CloningSystem.cs
+++ b/Content.Server/Cloning/CloningSystem.cs
@@ -1,350 +1,123 @@
-using Content.Server.Atmos.EntitySystems;
-using Content.Server.Chat.Systems;
-using Content.Server.Cloning.Components;
-using Content.Server.DeviceLinking.Systems;
-using Content.Server.EUI;
-using Content.Server.Fluids.EntitySystems;
using Content.Server.Humanoid;
-using Content.Server.Jobs;
-using Content.Server.Materials;
-using Content.Server.Popups;
-using Content.Server.Power.EntitySystems;
-using Content.Shared.Atmos;
-using Content.Shared.CCVar;
-using Content.Shared.Chemistry.Components;
+using Content.Shared.Administration.Logs;
using Content.Shared.Cloning;
-using Content.Shared.Damage;
-using Content.Shared.DeviceLinking.Events;
-using Content.Shared.Emag.Components;
-using Content.Shared.Emag.Systems;
-using Content.Shared.Examine;
-using Content.Shared.GameTicking;
+using Content.Shared.Cloning.Events;
+using Content.Shared.Database;
using Content.Shared.Humanoid;
-using Content.Shared.Mind;
-using Content.Shared.Mind.Components;
-using Content.Shared.Mobs.Systems;
-using Content.Shared.Roles.Jobs;
-using Robust.Server.Containers;
-using Robust.Server.GameObjects;
-using Robust.Server.Player;
-using Robust.Shared.Audio.Systems;
-using Robust.Shared.Configuration;
-using Robust.Shared.Containers;
-using Robust.Shared.Physics.Components;
+using Content.Shared.Inventory;
+using Content.Shared.NameModifier.Components;
+using Content.Shared.StatusEffect;
+using Content.Shared.Whitelist;
+using Robust.Shared.Map;
using Robust.Shared.Prototypes;
-using Robust.Shared.Random;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
-namespace Content.Server.Cloning
+namespace Content.Server.Cloning;
+
+///
+/// System responsible for making a copy of a humanoid's body.
+/// For the cloning machines themselves look at CloningPodSystem, CloningConsoleSystem and MedicalScannerSystem instead.
+///
+public sealed class CloningSystem : EntitySystem
{
- public sealed class CloningSystem : EntitySystem
+ [Dependency] private readonly IComponentFactory _componentFactory = default!;
+ [Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
+ [Dependency] private readonly InventorySystem _inventory = default!;
+ [Dependency] private readonly MetaDataSystem _metaData = default!;
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+ [Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
+ [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+
+ ///
+ /// Spawns a clone of the given humanoid mob at the specified location or in nullspace.
+ ///
+ public bool TryCloning(EntityUid original, MapCoordinates? coords, ProtoId settingsId, [NotNullWhen(true)] out EntityUid? clone)
{
- [Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
- [Dependency] private readonly IPlayerManager _playerManager = null!;
- [Dependency] private readonly IPrototypeManager _prototype = default!;
- [Dependency] private readonly EuiManager _euiManager = null!;
- [Dependency] private readonly CloningConsoleSystem _cloningConsoleSystem = default!;
- [Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
- [Dependency] private readonly ContainerSystem _containerSystem = default!;
- [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
- [Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
- [Dependency] private readonly IRobustRandom _robustRandom = default!;
- [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
- [Dependency] private readonly TransformSystem _transformSystem = default!;
- [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
- [Dependency] private readonly PuddleSystem _puddleSystem = default!;
- [Dependency] private readonly ChatSystem _chatSystem = default!;
- [Dependency] private readonly SharedAudioSystem _audio = default!;
- [Dependency] private readonly IConfigurationManager _configManager = default!;
- [Dependency] private readonly MaterialStorageSystem _material = default!;
- [Dependency] private readonly PopupSystem _popupSystem = default!;
- [Dependency] private readonly SharedMindSystem _mindSystem = default!;
- [Dependency] private readonly MetaDataSystem _metaSystem = default!;
- [Dependency] private readonly SharedJobSystem _jobs = default!;
- [Dependency] private readonly EmagSystem _emag = default!;
+ clone = null;
+ if (!_prototype.TryIndex(settingsId, out var settings))
+ return false; // invalid settings
- public readonly Dictionary ClonesWaitingForMind = new();
- public const float EasyModeCloningCost = 0.7f;
+ if (!TryComp(original, out var humanoid))
+ return false; // whatever body was to be cloned, was not a humanoid
- public override void Initialize()
+ if (!_prototype.TryIndex(humanoid.Species, out var speciesPrototype))
+ return false; // invalid species
+
+ var attemptEv = new CloningAttemptEvent(settings);
+ RaiseLocalEvent(original, ref attemptEv);
+ if (attemptEv.Cancelled && !settings.ForceCloning)
+ return false; // cannot clone, for example due to the unrevivable trait
+
+ clone = coords == null ? Spawn(speciesPrototype.Prototype) : Spawn(speciesPrototype.Prototype, coords.Value);
+ _humanoidSystem.CloneAppearance(original, clone.Value);
+
+ var componentsToCopy = settings.Components;
+
+ // don't make status effects permanent
+ if (TryComp(original, out var statusComp))
+ componentsToCopy.ExceptWith(statusComp.ActiveEffects.Values.Select(s => s.RelevantComponent).Where(s => s != null)!);
+
+ foreach (var componentName in componentsToCopy)
{
- base.Initialize();
-
- SubscribeLocalEvent(OnComponentInit);
- SubscribeLocalEvent(Reset);
- SubscribeLocalEvent(HandleMindAdded);
- SubscribeLocalEvent(OnPortDisconnected);
- SubscribeLocalEvent(OnAnchor);
- SubscribeLocalEvent(OnExamined);
- SubscribeLocalEvent(OnEmagged);
- }
-
- private void OnComponentInit(EntityUid uid, CloningPodComponent clonePod, ComponentInit args)
- {
- clonePod.BodyContainer = _containerSystem.EnsureContainer(uid, "clonepod-bodyContainer");
- _signalSystem.EnsureSinkPorts(uid, CloningPodComponent.PodPort);
- }
-
- internal void TransferMindToClone(EntityUid mindId, MindComponent mind)
- {
- if (!ClonesWaitingForMind.TryGetValue(mind, out var entity) ||
- !EntityManager.EntityExists(entity) ||
- !TryComp(entity, out var mindComp) ||
- mindComp.Mind != null)
- return;
-
- _mindSystem.TransferTo(mindId, entity, ghostCheckOverride: true, mind: mind);
- _mindSystem.UnVisit(mindId, mind);
- ClonesWaitingForMind.Remove(mind);
- }
-
- private void HandleMindAdded(EntityUid uid, BeingClonedComponent clonedComponent, MindAddedMessage message)
- {
- if (clonedComponent.Parent == EntityUid.Invalid ||
- !EntityManager.EntityExists(clonedComponent.Parent) ||
- !TryComp(clonedComponent.Parent, out var cloningPodComponent) ||
- uid != cloningPodComponent.BodyContainer.ContainedEntity)
+ if (!_componentFactory.TryGetRegistration(componentName, out var componentRegistration))
{
- EntityManager.RemoveComponent(uid);
- return;
- }
- UpdateStatus(clonedComponent.Parent, CloningPodStatus.Cloning, cloningPodComponent);
- }
-
- private void OnPortDisconnected(EntityUid uid, CloningPodComponent pod, PortDisconnectedEvent args)
- {
- pod.ConnectedConsole = null;
- }
-
- private void OnAnchor(EntityUid uid, CloningPodComponent component, ref AnchorStateChangedEvent args)
- {
- if (component.ConnectedConsole == null || !TryComp(component.ConnectedConsole, out var console))
- return;
-
- if (args.Anchored)
- {
- _cloningConsoleSystem.RecheckConnections(component.ConnectedConsole.Value, uid, console.GeneticScanner, console);
- return;
- }
- _cloningConsoleSystem.UpdateUserInterface(component.ConnectedConsole.Value, console);
- }
-
- private void OnExamined(EntityUid uid, CloningPodComponent component, ExaminedEvent args)
- {
- if (!args.IsInDetailsRange || !_powerReceiverSystem.IsPowered(uid))
- return;
-
- args.PushMarkup(Loc.GetString("cloning-pod-biomass", ("number", _material.GetMaterialAmount(uid, component.RequiredMaterial))));
- }
-
- public bool TryCloning(EntityUid uid, EntityUid bodyToClone, Entity mindEnt, CloningPodComponent? clonePod, float failChanceModifier = 1)
- {
- if (!Resolve(uid, ref clonePod))
- return false;
-
- if (HasComp(uid))
- return false;
-
- var mind = mindEnt.Comp;
- if (ClonesWaitingForMind.TryGetValue(mind, out var clone))
- {
- if (EntityManager.EntityExists(clone) &&
- !_mobStateSystem.IsDead(clone) &&
- TryComp(clone, out var cloneMindComp) &&
- (cloneMindComp.Mind == null || cloneMindComp.Mind == mindEnt))
- return false; // Mind already has clone
-
- ClonesWaitingForMind.Remove(mind);
+ Log.Error($"Tried to use invalid component registration for cloning: {componentName}");
+ continue;
}
- if (mind.OwnedEntity != null && !_mobStateSystem.IsDead(mind.OwnedEntity.Value))
- return false; // Body controlled by mind is not dead
-
- // Yes, we still need to track down the client because we need to open the Eui
- if (mind.UserId == null || !_playerManager.TryGetSessionById(mind.UserId.Value, out var client))
- return false; // If we can't track down the client, we can't offer transfer. That'd be quite bad.
-
- if (!TryComp(bodyToClone, out var humanoid))
- return false; // whatever body was to be cloned, was not a humanoid
-
- if (!_prototype.TryIndex(humanoid.Species, out var speciesPrototype))
- return false;
-
- if (!TryComp(bodyToClone, out var physics))
- return false;
-
- var cloningCost = (int) Math.Round(physics.FixturesMass);
-
- if (_configManager.GetCVar(CCVars.BiomassEasyMode))
- cloningCost = (int) Math.Round(cloningCost * EasyModeCloningCost);
-
- // biomass checks
- var biomassAmount = _material.GetMaterialAmount(uid, clonePod.RequiredMaterial);
-
- if (biomassAmount < cloningCost)
+ if (EntityManager.TryGetComponent(original, componentRegistration.Type, out var sourceComp)) // Does the original have this component?
{
- if (clonePod.ConnectedConsole != null)
- _chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-chat-error", ("units", cloningCost)), InGameICChatType.Speak, false);
- return false;
- }
-
- _material.TryChangeMaterialAmount(uid, clonePod.RequiredMaterial, -cloningCost);
- clonePod.UsedBiomass = cloningCost;
- // end of biomass checks
-
- // genetic damage checks
- if (TryComp(bodyToClone, out var damageable) &&
- damageable.Damage.DamageDict.TryGetValue("Cellular", out var cellularDmg))
- {
- var chance = Math.Clamp((float) (cellularDmg / 100), 0, 1);
- chance *= failChanceModifier;
-
- if (cellularDmg > 0 && clonePod.ConnectedConsole != null)
- _chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-cellular-warning", ("percent", Math.Round(100 - chance * 100))), InGameICChatType.Speak, false);
-
- if (_robustRandom.Prob(chance))
- {
- UpdateStatus(uid, CloningPodStatus.Gore, clonePod);
- clonePod.FailedClone = true;
- AddComp(uid);
- return true;
- }
- }
- // end of genetic damage checks
-
- var mob = Spawn(speciesPrototype.Prototype, _transformSystem.GetMapCoordinates(uid));
- _humanoidSystem.CloneAppearance(bodyToClone, mob);
-
- var ev = new CloningEvent(bodyToClone, mob);
- RaiseLocalEvent(bodyToClone, ref ev);
-
- if (!ev.NameHandled)
- _metaSystem.SetEntityName(mob, MetaData(bodyToClone).EntityName);
-
- var cloneMindReturn = EntityManager.AddComponent(mob);
- cloneMindReturn.Mind = mind;
- cloneMindReturn.Parent = uid;
- _containerSystem.Insert(mob, clonePod.BodyContainer);
- ClonesWaitingForMind.Add(mind, mob);
- UpdateStatus(uid, CloningPodStatus.NoMind, clonePod);
- _euiManager.OpenEui(new AcceptCloningEui(mindEnt, mind, this), client);
-
- AddComp(uid);
-
- // TODO: Ideally, components like this should be components on the mind entity so this isn't necessary.
- // Add on special job components to the mob.
- if (_jobs.MindTryGetJob(mindEnt, out var prototype))
- {
- foreach (var special in prototype.Special)
- {
- if (special is AddComponentSpecial)
- special.AfterEquip(mob);
- }
- }
-
- return true;
- }
-
- public void UpdateStatus(EntityUid podUid, CloningPodStatus status, CloningPodComponent cloningPod)
- {
- cloningPod.Status = status;
- _appearance.SetData(podUid, CloningPodVisuals.Status, cloningPod.Status);
- }
-
- public override void Update(float frameTime)
- {
- var query = EntityQueryEnumerator();
- while (query.MoveNext(out var uid, out var _, out var cloning))
- {
- if (!_powerReceiverSystem.IsPowered(uid))
- continue;
-
- if (cloning.BodyContainer.ContainedEntity == null && !cloning.FailedClone)
- continue;
-
- cloning.CloningProgress += frameTime;
- if (cloning.CloningProgress < cloning.CloningTime)
- continue;
-
- if (cloning.FailedClone)
- EndFailedCloning(uid, cloning);
- else
- Eject(uid, cloning);
+ if (HasComp(clone.Value, componentRegistration.Type)) // CopyComp cannot overwrite existing components
+ RemComp(clone.Value, componentRegistration.Type);
+ CopyComp(original, clone.Value, sourceComp);
}
}
- ///
- /// On emag, spawns a failed clone when cloning process fails which attacks nearby crew.
- ///
- private void OnEmagged(EntityUid uid, CloningPodComponent clonePod, ref GotEmaggedEvent args)
+ var cloningEv = new CloningEvent(settings, clone.Value);
+ RaiseLocalEvent(original, ref cloningEv); // used for datafields that cannot be directly copied
+
+ // Add equipment first so that SetEntityName also renames the ID card.
+ if (settings.CopyEquipment != null)
+ CopyEquipment(original, clone.Value, settings.CopyEquipment.Value, settings.Whitelist, settings.Blacklist);
+
+ var originalName = Name(original);
+ if (TryComp(original, out var nameModComp)) // if the originals name was modified, use the unmodified name
+ originalName = nameModComp.BaseName;
+
+ // This will properly set the BaseName and EntityName for the clone.
+ // Adding the component first before renaming will make sure RefreshNameModifers is called.
+ // Without this the name would get reverted to Urist.
+ // If the clone has no name modifiers, NameModifierComponent will be removed again.
+ EnsureComp(clone.Value);
+ _metaData.SetEntityName(clone.Value, originalName);
+
+ _adminLogger.Add(LogType.Chat, LogImpact.Medium, $"The body of {original:player} was cloned as {clone.Value:player}");
+ return true;
+ }
+
+ ///
+ /// Copies the equipment the original has to the clone.
+ /// This uses the original prototype of the items, so any changes to components that are done after spawning are lost!
+ ///
+ public void CopyEquipment(EntityUid original, EntityUid clone, SlotFlags slotFlags, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
+ {
+ if (!TryComp(original, out var originalInventory) || !TryComp(clone, out var cloneInventory))
+ return;
+ // Iterate over all inventory slots
+ var slotEnumerator = _inventory.GetSlotEnumerator((original, originalInventory), slotFlags);
+ while (slotEnumerator.NextItem(out var item, out var slot))
{
- if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
- return;
+ // Spawn a copy of the item using the original prototype.
+ // This means any changes done to the item after spawning will be reset, but that should not be a problem for simple items like clothing etc.
+ // we use a whitelist and blacklist to be sure to exclude any problematic entities
- if (_emag.CheckFlag(uid, EmagType.Interaction))
- return;
+ if (_whitelist.IsWhitelistFail(whitelist, item) || _whitelist.IsBlacklistPass(blacklist, item))
+ continue;
- if (!this.IsPowered(uid, EntityManager))
- return;
-
- _popupSystem.PopupEntity(Loc.GetString("cloning-pod-component-upgrade-emag-requirement"), uid);
- args.Handled = true;
- }
-
- public void Eject(EntityUid uid, CloningPodComponent? clonePod)
- {
- if (!Resolve(uid, ref clonePod))
- return;
-
- if (clonePod.BodyContainer.ContainedEntity is not { Valid: true } entity || clonePod.CloningProgress < clonePod.CloningTime)
- return;
-
- EntityManager.RemoveComponent(entity);
- _containerSystem.Remove(entity, clonePod.BodyContainer);
- clonePod.CloningProgress = 0f;
- clonePod.UsedBiomass = 0;
- UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
- RemCompDeferred(uid);
- }
-
- private void EndFailedCloning(EntityUid uid, CloningPodComponent clonePod)
- {
- clonePod.FailedClone = false;
- clonePod.CloningProgress = 0f;
- UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
- var transform = Transform(uid);
- var indices = _transformSystem.GetGridTilePositionOrDefault((uid, transform));
- var tileMix = _atmosphereSystem.GetTileMixture(transform.GridUid, null, indices, true);
-
- if (_emag.CheckFlag(uid, EmagType.Interaction))
- {
- _audio.PlayPvs(clonePod.ScreamSound, uid);
- Spawn(clonePod.MobSpawnId, transform.Coordinates);
- }
-
- Solution bloodSolution = new();
-
- var i = 0;
- while (i < 1)
- {
- tileMix?.AdjustMoles(Gas.Ammonia, 6f);
- bloodSolution.AddReagent("Blood", 50);
- if (_robustRandom.Prob(0.2f))
- i++;
- }
- _puddleSystem.TrySpillAt(uid, bloodSolution, out _);
-
- if (!_emag.CheckFlag(uid, EmagType.Interaction))
- {
- _material.SpawnMultipleFromMaterial(_robustRandom.Next(1, (int) (clonePod.UsedBiomass / 2.5)), clonePod.RequiredMaterial, Transform(uid).Coordinates);
- }
-
- clonePod.UsedBiomass = 0;
- RemCompDeferred(uid);
- }
-
- public void Reset(RoundRestartCleanupEvent ev)
- {
- ClonesWaitingForMind.Clear();
+ var prototype = MetaData(item).EntityPrototype;
+ if (prototype != null)
+ _inventory.SpawnItemInSlot(clone, slot.Name, prototype.ID, silent: true, inventory: cloneInventory);
}
}
}
diff --git a/Content.Server/Cloning/Components/RandomCloneSpawnerComponent.cs b/Content.Server/Cloning/Components/RandomCloneSpawnerComponent.cs
new file mode 100644
index 0000000000..ee06a532ef
--- /dev/null
+++ b/Content.Server/Cloning/Components/RandomCloneSpawnerComponent.cs
@@ -0,0 +1,17 @@
+using Content.Shared.Cloning;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Cloning.Components;
+
+///
+/// This is added to a marker entity in order to spawn a clone of a random player.
+///
+[RegisterComponent, EntityCategory("Spawner")]
+public sealed partial class RandomCloneSpawnerComponent : Component
+{
+ ///
+ /// Cloning settings to be used.
+ ///
+ [DataField]
+ public ProtoId Settings = "BaseClone";
+}
diff --git a/Content.Server/Cloning/RandomCloneSpawnerSystem.cs b/Content.Server/Cloning/RandomCloneSpawnerSystem.cs
new file mode 100644
index 0000000000..a645a10890
--- /dev/null
+++ b/Content.Server/Cloning/RandomCloneSpawnerSystem.cs
@@ -0,0 +1,47 @@
+using Content.Server.Cloning.Components;
+using Content.Shared.Mind;
+using Content.Shared.Mobs.Systems;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.Cloning;
+
+///
+/// This deals with spawning and setting up a clone of a random crew member.
+///
+public sealed class RandomCloneSpawnerSystem : EntitySystem
+{
+ [Dependency] private readonly CloningSystem _cloning = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
+ [Dependency] private readonly SharedMindSystem _mind = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnMapInit);
+ }
+
+ private void OnMapInit(Entity ent, ref MapInitEvent args)
+ {
+ QueueDel(ent.Owner);
+
+ if (!_prototypeManager.TryIndex(ent.Comp.Settings, out var settings))
+ {
+ Log.Error($"Used invalid cloning settings {ent.Comp.Settings} for RandomCloneSpawner");
+ return;
+ }
+
+ var allHumans = _mind.GetAliveHumans();
+
+ if (allHumans.Count == 0)
+ return;
+
+ var bodyToClone = _random.Pick(allHumans).Comp.OwnedEntity;
+
+ if (bodyToClone != null)
+ _cloning.TryCloning(bodyToClone.Value, _transformSystem.GetMapCoordinates(ent.Owner), settings, out _);
+ }
+}
diff --git a/Content.Server/Connection/Whitelist/WhitelistPrototype.cs b/Content.Server/Connection/Whitelist/WhitelistPrototype.cs
index 2b8b9babbc..eccf364a28 100644
--- a/Content.Server/Connection/Whitelist/WhitelistPrototype.cs
+++ b/Content.Server/Connection/Whitelist/WhitelistPrototype.cs
@@ -18,25 +18,25 @@ namespace Content.Server.Connection.Whitelist;
/// If the condition doesn't match, the next condition is checked.
///
[Prototype("playerConnectionWhitelist")]
-public sealed class PlayerConnectionWhitelistPrototype : IPrototype
+public sealed partial class PlayerConnectionWhitelistPrototype : IPrototype
{
[IdDataField]
- public string ID { get; } = default!;
+ public string ID { get; private set; } = default!;
///
/// Minimum number of players required for this whitelist to be active.
/// If there are less players than this, the whitelist will be ignored and the next one in the list will be used.
///
[DataField]
- public int MinimumPlayers { get; } = 0;
+ public int MinimumPlayers = 0;
///
/// Maximum number of players allowed for this whitelist to be active.
/// If there are more players than this, the whitelist will be ignored and the next one in the list will be used.
///
[DataField]
- public int MaximumPlayers { get; } = int.MaxValue;
+ public int MaximumPlayers = int.MaxValue;
[DataField]
- public WhitelistCondition[] Conditions { get; } = default!;
+ public WhitelistCondition[] Conditions = default!;
}
diff --git a/Content.Server/Containers/ThrowInsertContainerSystem.cs b/Content.Server/Containers/ThrowInsertContainerSystem.cs
index cbaec7f5c6..09176c423c 100644
--- a/Content.Server/Containers/ThrowInsertContainerSystem.cs
+++ b/Content.Server/Containers/ThrowInsertContainerSystem.cs
@@ -30,6 +30,12 @@ public sealed class ThrowInsertContainerSystem : EntitySystem
if (!_containerSystem.CanInsert(args.Thrown, container))
return;
+ var beforeThrowArgs = new BeforeThrowInsertEvent(args.Thrown);
+ RaiseLocalEvent(ent, ref beforeThrowArgs);
+
+ if (beforeThrowArgs.Cancelled)
+ return;
+
if (_random.Prob(ent.Comp.Probability))
{
_audio.PlayPvs(ent.Comp.MissSound, ent);
@@ -46,3 +52,10 @@ public sealed class ThrowInsertContainerSystem : EntitySystem
_adminLogger.Add(LogType.Landed, LogImpact.Low, $"{ToPrettyString(args.Thrown)} thrown by {ToPrettyString(args.Component.Thrower.Value):player} landed in {ToPrettyString(ent)}");
}
}
+
+///
+/// Sent before the insertion is made.
+/// Allows preventing the insertion if any system on the entity should need to.
+///
+[ByRefEvent]
+public record struct BeforeThrowInsertEvent(EntityUid ThrownEntity, bool Cancelled = false);
diff --git a/Content.Server/Disposal/Unit/EntitySystems/DisposalUnitSystem.cs b/Content.Server/Disposal/Unit/EntitySystems/DisposalUnitSystem.cs
index 2cf88efc6e..136ac2c440 100644
--- a/Content.Server/Disposal/Unit/EntitySystems/DisposalUnitSystem.cs
+++ b/Content.Server/Disposal/Unit/EntitySystems/DisposalUnitSystem.cs
@@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.Atmos.EntitySystems;
+using Content.Server.Containers;
using Content.Server.Disposal.Tube;
using Content.Server.Disposal.Tube.Components;
using Content.Server.Disposal.Unit.Components;
@@ -85,6 +86,8 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
SubscribeLocalEvent(OnDoAfter);
+ SubscribeLocalEvent(OnThrowInsert);
+
SubscribeLocalEvent(OnUiButtonPressed);
}
@@ -195,6 +198,12 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
args.Handled = true;
}
+ private void OnThrowInsert(Entity ent, ref BeforeThrowInsertEvent args)
+ {
+ if (!CanInsert(ent, ent, args.ThrownEntity))
+ args.Cancelled = true;
+ }
+
public override void DoInsertDisposalUnit(EntityUid uid, EntityUid toInsert, EntityUid user, SharedDisposalUnitComponent? disposal = null)
{
if (!ResolveDisposals(uid, ref disposal))
diff --git a/Content.Server/Explosion/EntitySystems/TriggerSystem.Voice.cs b/Content.Server/Explosion/EntitySystems/TriggerSystem.Voice.cs
index c78a8923cd..8dd170e667 100644
--- a/Content.Server/Explosion/EntitySystems/TriggerSystem.Voice.cs
+++ b/Content.Server/Explosion/EntitySystems/TriggerSystem.Voice.cs
@@ -53,6 +53,9 @@ namespace Content.Server.Explosion.EntitySystems
_adminLogger.Add(LogType.Trigger, LogImpact.High,
$"A voice-trigger on {ToPrettyString(ent):entity} was triggered by {ToPrettyString(args.Source):speaker} speaking the key-phrase {component.KeyPhrase}.");
Trigger(ent, args.Source);
+
+ var voice = new VoiceTriggeredEvent(args.Source, message);
+ RaiseLocalEvent(ent, ref voice);
}
}
@@ -137,3 +140,12 @@ namespace Content.Server.Explosion.EntitySystems
}
}
}
+
+
+///
+/// Raised when a voice trigger is activated, containing the message that triggered it.
+///
+/// The EntityUid of the entity sending the message
+/// The contents of the message
+[ByRefEvent]
+public readonly record struct VoiceTriggeredEvent(EntityUid Source, string? Message);
diff --git a/Content.Server/Forensics/Components/FingerprintComponent.cs b/Content.Server/Forensics/Components/FingerprintComponent.cs
deleted file mode 100644
index 97d138119e..0000000000
--- a/Content.Server/Forensics/Components/FingerprintComponent.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace Content.Server.Forensics
-{
- ///
- /// This component is for mobs that leave fingerprints.
- ///
- [RegisterComponent]
- public sealed partial class FingerprintComponent : Component
- {
- [DataField("fingerprint"), ViewVariables(VVAccess.ReadWrite)]
- public string? Fingerprint;
- }
-}
diff --git a/Content.Server/Forensics/Components/FingerprintMaskComponent.cs b/Content.Server/Forensics/Components/FingerprintMaskComponent.cs
deleted file mode 100644
index 7371097761..0000000000
--- a/Content.Server/Forensics/Components/FingerprintMaskComponent.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace Content.Server.Forensics
-{
- ///
- /// This component stops the entity from leaving finger prints,
- /// usually so fibres can be left instead.
- ///
- [RegisterComponent]
- public sealed partial class FingerprintMaskComponent : Component
- {}
-}
diff --git a/Content.Server/Forensics/Systems/ForensicPadSystem.cs b/Content.Server/Forensics/Systems/ForensicPadSystem.cs
index 71bbcfb44a..846d72a9fa 100644
--- a/Content.Server/Forensics/Systems/ForensicPadSystem.cs
+++ b/Content.Server/Forensics/Systems/ForensicPadSystem.cs
@@ -3,6 +3,7 @@ using Content.Server.Popups;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.Forensics;
+using Content.Shared.Forensics.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Inventory;
diff --git a/Content.Server/Forensics/Systems/ForensicsSystem.cs b/Content.Server/Forensics/Systems/ForensicsSystem.cs
index c1595dd0c1..a52b060391 100644
--- a/Content.Server/Forensics/Systems/ForensicsSystem.cs
+++ b/Content.Server/Forensics/Systems/ForensicsSystem.cs
@@ -1,4 +1,5 @@
using Content.Server.Body.Components;
+using Content.Server.Body.Systems;
using Content.Server.DoAfter;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Forensics.Components;
@@ -32,8 +33,9 @@ namespace Content.Server.Forensics
public override void Initialize()
{
SubscribeLocalEvent(OnInteract);
- SubscribeLocalEvent(OnFingerprintInit);
- SubscribeLocalEvent(OnDNAInit);
+ SubscribeLocalEvent(OnFingerprintInit, after: new[] { typeof(BloodstreamSystem) });
+ // The solution entities are spawned on MapInit as well, so we have to wait for that to be able to set the DNA in the bloodstream correctly without ResolveSolution failing
+ SubscribeLocalEvent(OnDNAInit, after: new[] { typeof(BloodstreamSystem) });
SubscribeLocalEvent(OnBeingGibbed);
SubscribeLocalEvent(OnMeleeHit);
@@ -63,19 +65,22 @@ namespace Content.Server.Forensics
ApplyEvidence(uid, args.Other);
}
- private void OnFingerprintInit(EntityUid uid, FingerprintComponent component, MapInitEvent args)
+ private void OnFingerprintInit(Entity ent, ref MapInitEvent args)
{
- component.Fingerprint = GenerateFingerprint();
+ if (ent.Comp.Fingerprint == null)
+ RandomizeFingerprint((ent.Owner, ent.Comp));
}
- private void OnDNAInit(EntityUid uid, DnaComponent component, MapInitEvent args)
+ private void OnDNAInit(Entity ent, ref MapInitEvent args)
{
- if (component.DNA == String.Empty)
+ Log.Debug($"Init DNA {Name(ent.Owner)} {ent.Comp.DNA}");
+ if (ent.Comp.DNA == null)
+ RandomizeDNA((ent.Owner, ent.Comp));
+ else
{
- component.DNA = GenerateDNA();
-
- var ev = new GenerateDnaEvent { Owner = uid, DNA = component.DNA };
- RaiseLocalEvent(uid, ref ev);
+ // If set manually (for example by cloning) we also need to inform the bloodstream of the correct DNA string so it can be updated
+ var ev = new GenerateDnaEvent { Owner = ent.Owner, DNA = ent.Comp.DNA };
+ RaiseLocalEvent(ent.Owner, ref ev);
}
}
@@ -83,7 +88,7 @@ namespace Content.Server.Forensics
{
string dna = Loc.GetString("forensics-dna-unknown");
- if (TryComp(uid, out DnaComponent? dnaComp))
+ if (TryComp(uid, out DnaComponent? dnaComp) && dnaComp.DNA != null)
dna = dnaComp.DNA;
foreach (EntityUid part in args.GibbedParts)
@@ -102,7 +107,7 @@ namespace Content.Server.Forensics
{
foreach (EntityUid hitEntity in args.HitEntities)
{
- if (TryComp(hitEntity, out var hitEntityComp))
+ if (TryComp(hitEntity, out var hitEntityComp) && hitEntityComp.DNA != null)
component.DNAs.Add(hitEntityComp.DNA);
}
}
@@ -300,6 +305,9 @@ namespace Content.Server.Forensics
private void OnTransferDnaEvent(EntityUid uid, DnaComponent component, ref TransferDnaEvent args)
{
+ if (component.DNA == null)
+ return;
+
var recipientComp = EnsureComp(args.Recipient);
recipientComp.DNAs.Add(component.DNA);
recipientComp.CanDnaBeCleaned = args.CanDnaBeCleaned;
@@ -307,6 +315,36 @@ namespace Content.Server.Forensics
#region Public API
+ ///
+ /// Give the entity a new, random DNA string and call an event to notify other systems like the bloodstream that it has been changed.
+ /// Does nothing if it does not have the DnaComponent.
+ ///
+ public void RandomizeDNA(Entity ent)
+ {
+ if (!Resolve(ent, ref ent.Comp, false))
+ return;
+
+ ent.Comp.DNA = GenerateDNA();
+ Dirty(ent);
+
+ Log.Debug($"Randomize DNA {Name(ent.Owner)} {ent.Comp.DNA}");
+ var ev = new GenerateDnaEvent { Owner = ent.Owner, DNA = ent.Comp.DNA };
+ RaiseLocalEvent(ent.Owner, ref ev);
+ }
+
+ ///
+ /// Give the entity a new, random fingerprint string.
+ /// Does nothing if it does not have the FingerprintComponent.
+ ///
+ public void RandomizeFingerprint(Entity ent)
+ {
+ if (!Resolve(ent, ref ent.Comp, false))
+ return;
+
+ ent.Comp.Fingerprint = GenerateFingerprint();
+ Dirty(ent);
+ }
+
///
/// Transfer DNA from one entity onto the forensics of another
///
@@ -315,7 +353,7 @@ namespace Content.Server.Forensics
/// If this DNA be cleaned off of the recipient. e.g. cleaning a knife vs cleaning a puddle of blood
public void TransferDna(EntityUid recipient, EntityUid donor, bool canDnaBeCleaned = true)
{
- if (TryComp(donor, out var donorComp))
+ if (TryComp(donor, out var donorComp) && donorComp.DNA != null)
{
EnsureComp(recipient, out var recipientComp);
recipientComp.DNAs.Add(donorComp.DNA);
diff --git a/Content.Server/GameTicking/Rules/SurvivorRuleSystem.cs b/Content.Server/GameTicking/Rules/SurvivorRuleSystem.cs
index 00f652b6c6..81ad2b1be7 100644
--- a/Content.Server/GameTicking/Rules/SurvivorRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/SurvivorRuleSystem.cs
@@ -1,4 +1,4 @@
-using Content.Server.Antag;
+using Content.Server.Antag;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Mind;
using Content.Server.Roles;
diff --git a/Content.Server/Ghost/Roles/Raffles/GhostRoleRaffleDeciderPrototype.cs b/Content.Server/Ghost/Roles/Raffles/GhostRoleRaffleDeciderPrototype.cs
index b2ebf6ca5f..e7b0c8326a 100644
--- a/Content.Server/Ghost/Roles/Raffles/GhostRoleRaffleDeciderPrototype.cs
+++ b/Content.Server/Ghost/Roles/Raffles/GhostRoleRaffleDeciderPrototype.cs
@@ -6,7 +6,7 @@ namespace Content.Server.Ghost.Roles.Raffles;
/// Allows getting a as prototype.
///
[Prototype("ghostRoleRaffleDecider")]
-public sealed class GhostRoleRaffleDeciderPrototype : IPrototype
+public sealed partial class GhostRoleRaffleDeciderPrototype : IPrototype
{
///
[IdDataField]
diff --git a/Content.Server/Guardian/GuardianSystem.cs b/Content.Server/Guardian/GuardianSystem.cs
index 7a1b875756..4f80e2cd6c 100644
--- a/Content.Server/Guardian/GuardianSystem.cs
+++ b/Content.Server/Guardian/GuardianSystem.cs
@@ -269,6 +269,7 @@ namespace Content.Server.Guardian
component.Host,
args.DamageDelta * component.DamageShare,
origin: args.Origin,
+ ignoreResistances: true,
interruptsDoAfters: false);
_popupSystem.PopupEntity(Loc.GetString("guardian-entity-taking-damage"), component.Host.Value, component.Host.Value);
diff --git a/Content.Server/Implants/ImplanterSystem.cs b/Content.Server/Implants/ImplanterSystem.cs
index 5efd5dc6fb..5023b1b3e4 100644
--- a/Content.Server/Implants/ImplanterSystem.cs
+++ b/Content.Server/Implants/ImplanterSystem.cs
@@ -68,8 +68,6 @@ public sealed partial class ImplanterSystem : SharedImplanterSystem
args.Handled = true;
}
-
-
///
/// Attempt to implant someone else.
///
diff --git a/Content.Server/Implants/SubdermalImplantSystem.cs b/Content.Server/Implants/SubdermalImplantSystem.cs
index c306e406a1..bd6ffe375c 100644
--- a/Content.Server/Implants/SubdermalImplantSystem.cs
+++ b/Content.Server/Implants/SubdermalImplantSystem.cs
@@ -216,18 +216,12 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
var newProfile = HumanoidCharacterProfile.RandomWithSpecies(humanoid.Species);
_humanoidAppearance.LoadProfile(ent, newProfile, humanoid);
_metaData.SetEntityName(ent, newProfile.Name, raiseEvents: false); // raising events would update ID card, station record, etc.
- if (TryComp(ent, out var dna))
- {
- dna.DNA = _forensicsSystem.GenerateDNA();
- var ev = new GenerateDnaEvent { Owner = ent, DNA = dna.DNA };
- RaiseLocalEvent(ent, ref ev);
- }
- if (TryComp(ent, out var fingerprint))
- {
- fingerprint.Fingerprint = _forensicsSystem.GenerateFingerprint();
- }
- RemComp(ent); // remove MRP+ custom description if one exists
+ // If the entity has the respecive components, then scramble the dna and fingerprint strings
+ _forensicsSystem.RandomizeDNA(ent);
+ _forensicsSystem.RandomizeFingerprint(ent);
+
+ RemComp(ent); // remove MRP+ custom description if one exists
_identity.QueueIdentityUpdate(ent); // manually queue identity update since we don't raise the event
_popup.PopupEntity(Loc.GetString("scramble-implant-activated-popup"), ent, ent);
}
diff --git a/Content.Server/Kitchen/Components/ActivelyMicrowavedComponent.cs b/Content.Server/Kitchen/Components/ActivelyMicrowavedComponent.cs
index 54a7edd745..c7fca5a47e 100644
--- a/Content.Server/Kitchen/Components/ActivelyMicrowavedComponent.cs
+++ b/Content.Server/Kitchen/Components/ActivelyMicrowavedComponent.cs
@@ -1,5 +1,3 @@
-using Content.Shared.Kitchen;
-
namespace Content.Server.Kitchen.Components;
///
@@ -8,4 +6,9 @@ namespace Content.Server.Kitchen.Components;
[RegisterComponent]
public sealed partial class ActivelyMicrowavedComponent : Component
{
+ ///
+ /// The microwave this entity is actively being microwaved by.
+ ///
+ [DataField]
+ public EntityUid? Microwave;
}
diff --git a/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs b/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs
index 1ac02ac882..7f0778cbdd 100644
--- a/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs
+++ b/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs
@@ -14,6 +14,7 @@ using Content.Shared.Body.Components;
using Content.Shared.Body.Part;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.Chemistry.Reaction;
using Content.Shared.Construction.EntitySystems;
using Content.Shared.Database;
using Content.Shared.Destructible;
@@ -101,6 +102,7 @@ namespace Content.Server.Kitchen.EntitySystems
SubscribeLocalEvent(OnActiveMicrowaveRemove);
SubscribeLocalEvent(OnConstructionTemp);
+ SubscribeLocalEvent>(OnReactionAttempt);
SubscribeLocalEvent(OnGetSecretRecipes);
}
@@ -126,7 +128,8 @@ namespace Content.Server.Kitchen.EntitySystems
private void OnActiveMicrowaveInsert(Entity ent, ref EntInsertedIntoContainerMessage args)
{
- AddComp(args.Entity);
+ var microwavedComp = AddComp(args.Entity);
+ microwavedComp.Microwave = ent.Owner;
}
private void OnActiveMicrowaveRemove(Entity ent, ref EntRemovedFromContainerMessage args)
@@ -134,10 +137,33 @@ namespace Content.Server.Kitchen.EntitySystems
EntityManager.RemoveComponentDeferred(args.Entity);
}
+ // Stop items from transforming through constructiongraphs while being microwaved.
+ // They might be reserved for a microwave recipe.
private void OnConstructionTemp(Entity ent, ref OnConstructionTemperatureEvent args)
{
args.Result = HandleResult.False;
- return;
+ }
+
+ // Stop reagents from reacting if they are currently reserved for a microwave recipe.
+ // For example Egg would cook into EggCooked, causing it to not being removed once we are done microwaving.
+ private void OnReactionAttempt(Entity ent, ref SolutionRelayEvent args)
+ {
+ if (!TryComp(ent.Comp.Microwave, out var activeMicrowaveComp))
+ return;
+
+ if (activeMicrowaveComp.PortionedRecipe.Item1 == null) // no recipe selected
+ return;
+
+ var recipeReagents = activeMicrowaveComp.PortionedRecipe.Item1.IngredientsReagents.Keys;
+
+ foreach (var reagent in recipeReagents)
+ {
+ if (args.Event.Reaction.Reactants.ContainsKey(reagent))
+ {
+ args.Event.Cancelled = true;
+ return;
+ }
+ }
}
///
@@ -176,33 +202,29 @@ namespace Content.Server.Kitchen.EntitySystems
// this is spaghetti ngl
foreach (var item in component.Storage.ContainedEntities)
{
- if (!TryComp(item, out var solMan))
+ // use the same reagents as when we selected the recipe
+ if (!_solutionContainer.TryGetDrainableSolution(item, out var solutionEntity, out var solution))
continue;
- // go over every solution
- foreach (var (_, soln) in _solutionContainer.EnumerateSolutions((item, solMan)))
+ foreach (var (reagent, _) in recipe.IngredientsReagents)
{
- var solution = soln.Comp.Solution;
- foreach (var (reagent, _) in recipe.IngredientsReagents)
+ // removed everything
+ if (!totalReagentsToRemove.ContainsKey(reagent))
+ continue;
+
+ var quant = solution.GetTotalPrototypeQuantity(reagent);
+
+ if (quant >= totalReagentsToRemove[reagent])
{
- // removed everything
- if (!totalReagentsToRemove.ContainsKey(reagent))
- continue;
-
- var quant = solution.GetTotalPrototypeQuantity(reagent);
-
- if (quant >= totalReagentsToRemove[reagent])
- {
- quant = totalReagentsToRemove[reagent];
- totalReagentsToRemove.Remove(reagent);
- }
- else
- {
- totalReagentsToRemove[reagent] -= quant;
- }
-
- _solutionContainer.RemoveReagent(soln, reagent, quant);
+ quant = totalReagentsToRemove[reagent];
+ totalReagentsToRemove.Remove(reagent);
}
+ else
+ {
+ totalReagentsToRemove[reagent] -= quant;
+ }
+
+ _solutionContainer.RemoveReagent(solutionEntity.Value, reagent, quant);
}
}
@@ -541,7 +563,8 @@ namespace Content.Server.Kitchen.EntitySystems
continue;
}
- AddComp(item);
+ var microwavedComp = AddComp(item);
+ microwavedComp.Microwave = uid;
string? solidID = null;
int amountToAdd = 1;
@@ -560,33 +583,20 @@ namespace Content.Server.Kitchen.EntitySystems
}
if (solidID is null)
- {
continue;
- }
-
- if (solidsDict.ContainsKey(solidID))
- {
+ if (!solidsDict.TryAdd(solidID, amountToAdd))
solidsDict[solidID] += amountToAdd;
- }
- else
- {
- solidsDict.Add(solidID, amountToAdd);
- }
- if (!TryComp(item, out var solMan))
+ // only use reagents we have access to
+ // you have to break the eggs before we can use them!
+ if (!_solutionContainer.TryGetDrainableSolution(item, out var _, out var solution))
continue;
- foreach (var (_, soln) in _solutionContainer.EnumerateSolutions((item, solMan)))
+ foreach (var (reagent, quantity) in solution.Contents)
{
- var solution = soln.Comp.Solution;
- foreach (var (reagent, quantity) in solution.Contents)
- {
- if (reagentDict.ContainsKey(reagent.Prototype))
- reagentDict[reagent.Prototype] += quantity;
- else
- reagentDict.Add(reagent.Prototype, quantity);
- }
+ if (!reagentDict.TryAdd(reagent.Prototype, quantity))
+ reagentDict[reagent.Prototype] += quantity;
}
}
diff --git a/Content.Server/Materials/MaterialStorageSystem.cs b/Content.Server/Materials/MaterialStorageSystem.cs
index 9f43a22049..8c50d5fe9a 100644
--- a/Content.Server/Materials/MaterialStorageSystem.cs
+++ b/Content.Server/Materials/MaterialStorageSystem.cs
@@ -69,7 +69,7 @@ public sealed class MaterialStorageSystem : SharedMaterialStorageSystem
if (material.StackEntity != null)
{
- if (!_prototypeManager.Index(material.StackEntity).TryGetComponent(out var composition))
+ if (!_prototypeManager.Index(material.StackEntity).TryGetComponent(out var composition, EntityManager.ComponentFactory))
return;
var volumePerSheet = composition.MaterialComposition.FirstOrDefault(kvp => kvp.Key == msg.Material).Value;
@@ -169,7 +169,7 @@ public sealed class MaterialStorageSystem : SharedMaterialStorageSystem
return new List();
var entProto = _prototypeManager.Index(materialProto.StackEntity);
- if (!entProto.TryGetComponent(out var composition))
+ if (!entProto.TryGetComponent(out var composition, EntityManager.ComponentFactory))
return new List();
var materialPerStack = composition.MaterialComposition[materialProto.ID];
diff --git a/Content.Server/Medical/VomitSystem.cs b/Content.Server/Medical/VomitSystem.cs
index 5cff161e0e..a6982cea4f 100644
--- a/Content.Server/Medical/VomitSystem.cs
+++ b/Content.Server/Medical/VomitSystem.cs
@@ -31,6 +31,12 @@ namespace Content.Server.Medical
[Dependency] private readonly ForensicsSystem _forensics = default!;
[Dependency] private readonly BloodstreamSystem _bloodstream = default!;
+ [ValidatePrototypeId]
+ private const string VomitCollection = "Vomit";
+
+ private readonly SoundSpecifier _vomitSound = new SoundCollectionSpecifier(VomitCollection,
+ AudioParams.Default.WithVariation(0.2f).WithVolume(-4f));
+
///
/// Make an entity vomit, if they have a stomach.
///
@@ -94,7 +100,7 @@ namespace Content.Server.Medical
}
// Force sound to play as spill doesn't work if solution is empty.
- _audio.PlayPvs("/Audio/Effects/Fluids/splat.ogg", uid, AudioParams.Default.WithVariation(0.2f).WithVolume(-4f));
+ _audio.PlayPvs(_vomitSound, uid);
_popup.PopupEntity(Loc.GetString("disease-vomit", ("person", Identity.Entity(uid, EntityManager))), uid);
}
}
diff --git a/Content.Server/NPC/Components/NPCRangedCombatComponent.cs b/Content.Server/NPC/Components/NPCRangedCombatComponent.cs
index 2e4fcf5298..21cdfba801 100644
--- a/Content.Server/NPC/Components/NPCRangedCombatComponent.cs
+++ b/Content.Server/NPC/Components/NPCRangedCombatComponent.cs
@@ -40,6 +40,13 @@ public sealed partial class NPCRangedCombatComponent : Component
[ViewVariables(VVAccess.ReadWrite)]
public bool TargetInLOS = false;
+ ///
+ /// If true, only opaque objects will block line of sight.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ // ReSharper disable once InconsistentNaming
+ public bool UseOpaqueForLOSChecks = false;
+
///
/// Delay after target is in LOS before we start shooting.
///
diff --git a/Content.Server/NPC/HTN/HTNComponent.cs b/Content.Server/NPC/HTN/HTNComponent.cs
index f482e0808c..788a347638 100644
--- a/Content.Server/NPC/HTN/HTNComponent.cs
+++ b/Content.Server/NPC/HTN/HTNComponent.cs
@@ -10,7 +10,7 @@ public sealed partial class HTNComponent : NPCComponent
/// The base task to use for planning
///
[ViewVariables(VVAccess.ReadWrite),
- DataField("rootTask", required: true)]
+ DataField("rootTask", required: true)]
public HTNCompoundTask RootTask = default!;
///
@@ -47,4 +47,10 @@ public sealed partial class HTNComponent : NPCComponent
/// Is this NPC currently planning?
///
[ViewVariables] public bool Planning => PlanningJob != null;
+
+ ///
+ /// Determines whether plans should be made / updated for this entity
+ ///
+ [DataField]
+ public bool Enabled = true;
}
diff --git a/Content.Server/NPC/HTN/HTNCompoundPrototype.cs b/Content.Server/NPC/HTN/HTNCompoundPrototype.cs
index 69f8441973..b99ea09826 100644
--- a/Content.Server/NPC/HTN/HTNCompoundPrototype.cs
+++ b/Content.Server/NPC/HTN/HTNCompoundPrototype.cs
@@ -8,7 +8,7 @@ namespace Content.Server.NPC.HTN;
[Prototype("htnCompound")]
public sealed partial class HTNCompoundPrototype : IPrototype
{
- [IdDataField] public string ID { get; } = string.Empty;
+ [IdDataField] public string ID { get; private set; } = string.Empty;
[DataField("branches", required: true)]
public List Branches = new();
diff --git a/Content.Server/NPC/HTN/HTNSystem.cs b/Content.Server/NPC/HTN/HTNSystem.cs
index cfa670e144..ce4b248a0d 100644
--- a/Content.Server/NPC/HTN/HTNSystem.cs
+++ b/Content.Server/NPC/HTN/HTNSystem.cs
@@ -133,6 +133,39 @@ public sealed class HTNSystem : EntitySystem
component.PlanningJob = null;
}
+ ///
+ /// Enable / disable the hierarchical task network of an entity
+ ///
+ /// The entity and its
+ /// Set 'true' to enable, or 'false' to disable, the HTN
+ /// Specifies a time in seconds before the entity can start planning a new action (only takes effect when the HTN is enabled)
+ // ReSharper disable once InconsistentNaming
+ [PublicAPI]
+ public void SetHTNEnabled(Entity ent, bool state, float planCooldown = 0f)
+ {
+ if (ent.Comp.Enabled == state)
+ return;
+
+ ent.Comp.Enabled = state;
+ ent.Comp.PlanAccumulator = planCooldown;
+
+ ent.Comp.PlanningToken?.Cancel();
+ ent.Comp.PlanningToken = null;
+
+ if (ent.Comp.Plan != null)
+ {
+ var currentOperator = ent.Comp.Plan.CurrentOperator;
+
+ ShutdownTask(currentOperator, ent.Comp.Blackboard, HTNOperatorStatus.Failed);
+ ShutdownPlan(ent.Comp);
+
+ ent.Comp.Plan = null;
+ }
+
+ if (ent.Comp.Enabled && ent.Comp.PlanAccumulator <= 0)
+ RequestPlan(ent.Comp);
+ }
+
///
/// Forces the NPC to replan.
///
@@ -147,12 +180,15 @@ public sealed class HTNSystem : EntitySystem
_planQueue.Process();
var query = EntityQueryEnumerator();
- while(query.MoveNext(out var uid, out _, out var comp))
+ while (query.MoveNext(out var uid, out _, out var comp))
{
// If we're over our max count or it's not MapInit then ignore the NPC.
if (count >= maxUpdates)
break;
+ if (!comp.Enabled)
+ continue;
+
if (comp.PlanningJob != null)
{
if (comp.PlanningJob.Exception != null)
diff --git a/Content.Server/NPC/HTN/Preconditions/TargetInLOSPrecondition.cs b/Content.Server/NPC/HTN/Preconditions/TargetInLOSPrecondition.cs
index 0b233292a4..bb27ae8868 100644
--- a/Content.Server/NPC/HTN/Preconditions/TargetInLOSPrecondition.cs
+++ b/Content.Server/NPC/HTN/Preconditions/TargetInLOSPrecondition.cs
@@ -1,4 +1,5 @@
using Content.Server.Interaction;
+using Content.Shared.Physics;
namespace Content.Server.NPC.HTN.Preconditions;
@@ -13,6 +14,9 @@ public sealed partial class TargetInLOSPrecondition : HTNPrecondition
[DataField("rangeKey")]
public string RangeKey = "RangeKey";
+ [DataField("opaqueKey")]
+ public bool UseOpaqueForLOSChecksKey = true;
+
public override void Initialize(IEntitySystemManager sysManager)
{
base.Initialize(sysManager);
@@ -27,7 +31,8 @@ public sealed partial class TargetInLOSPrecondition : HTNPrecondition
return false;
var range = blackboard.GetValueOrDefault(RangeKey, _entManager);
+ var collisionGroup = UseOpaqueForLOSChecksKey ? CollisionGroup.Opaque : (CollisionGroup.Impassable | CollisionGroup.InteractImpassable);
- return _interaction.InRangeUnobstructed(owner, target, range);
+ return _interaction.InRangeUnobstructed(owner, target, range, collisionGroup);
}
}
diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/Ranged/GunOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/Ranged/GunOperator.cs
index 53c5ed1952..65620e73aa 100644
--- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/Ranged/GunOperator.cs
+++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/Ranged/GunOperator.cs
@@ -33,6 +33,12 @@ public sealed partial class GunOperator : HTNOperator, IHtnConditionalShutdown
[DataField("requireLOS")]
public bool RequireLOS = false;
+ ///
+ /// If true, only opaque objects will block line of sight.
+ ///
+ [DataField("opaqueKey")]
+ public bool UseOpaqueForLOSChecks = false;
+
// Like movement we add a component and pass it off to the dedicated system.
public override async Task<(bool Valid, Dictionary? Effects)> Plan(NPCBlackboard blackboard,
@@ -56,8 +62,10 @@ public sealed partial class GunOperator : HTNOperator, IHtnConditionalShutdown
public override void Startup(NPCBlackboard blackboard)
{
base.Startup(blackboard);
+
var ranged = _entManager.EnsureComponent(blackboard.GetValue(NPCBlackboard.Owner));
ranged.Target = blackboard.GetValue(TargetKey);
+ ranged.UseOpaqueForLOSChecks = UseOpaqueForLOSChecks;
if (blackboard.TryGetValue(NPCBlackboard.RotateSpeed, out var rotSpeed, _entManager))
{
diff --git a/Content.Server/NPC/Queries/Considerations/TargetTargetingCon.cs b/Content.Server/NPC/Queries/Considerations/TargetTargetingCon.cs
new file mode 100644
index 0000000000..600eb20517
--- /dev/null
+++ b/Content.Server/NPC/Queries/Considerations/TargetTargetingCon.cs
@@ -0,0 +1,12 @@
+namespace Content.Server.NPC.Queries.Considerations;
+
+///
+/// Returns 0f if the NPC has a and the
+/// target entity is exempt from being targeted, otherwise it returns 1f.
+/// See
+/// for further details on turret target validation.
+///
+public sealed partial class TurretTargetingCon : UtilityConsideration
+{
+
+}
diff --git a/Content.Server/NPC/Queries/Curves/UtilityCurvePresetPrototype.cs b/Content.Server/NPC/Queries/Curves/UtilityCurvePresetPrototype.cs
index 7a6e9152c6..3116dede93 100644
--- a/Content.Server/NPC/Queries/Curves/UtilityCurvePresetPrototype.cs
+++ b/Content.Server/NPC/Queries/Curves/UtilityCurvePresetPrototype.cs
@@ -5,7 +5,7 @@ namespace Content.Server.NPC.Queries.Curves;
[Prototype("utilityCurvePreset")]
public sealed partial class UtilityCurvePresetPrototype : IPrototype
{
- [IdDataField] public string ID { get; } = string.Empty;
+ [IdDataField] public string ID { get; private set; } = string.Empty;
[DataField("curve", required: true)] public IUtilityCurve Curve = default!;
}
diff --git a/Content.Server/NPC/Systems/NPCCombatSystem.Ranged.cs b/Content.Server/NPC/Systems/NPCCombatSystem.Ranged.cs
index d7196ea73c..f4e312fbd4 100644
--- a/Content.Server/NPC/Systems/NPCCombatSystem.Ranged.cs
+++ b/Content.Server/NPC/Systems/NPCCombatSystem.Ranged.cs
@@ -1,6 +1,7 @@
using Content.Server.NPC.Components;
using Content.Shared.CombatMode;
using Content.Shared.Interaction;
+using Content.Shared.Physics;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
using Robust.Shared.Map;
@@ -132,8 +133,10 @@ public sealed partial class NPCCombatSystem
if (comp.LOSAccumulator < 0f)
{
comp.LOSAccumulator += UnoccludedCooldown;
+
// For consistency with NPC steering.
- comp.TargetInLOS = _interaction.InRangeUnobstructed(uid, comp.Target, distance + 0.1f);
+ var collisionGroup = comp.UseOpaqueForLOSChecks ? CollisionGroup.Opaque : (CollisionGroup.Impassable | CollisionGroup.InteractImpassable);
+ comp.TargetInLOS = _interaction.InRangeUnobstructed(uid, comp.Target, distance + 0.1f, collisionGroup);
}
if (!comp.TargetInLOS)
diff --git a/Content.Server/NPC/Systems/NPCUtilitySystem.cs b/Content.Server/NPC/Systems/NPCUtilitySystem.cs
index 60bc5cdfd8..b5d3ac3cbd 100644
--- a/Content.Server/NPC/Systems/NPCUtilitySystem.cs
+++ b/Content.Server/NPC/Systems/NPCUtilitySystem.cs
@@ -20,6 +20,7 @@ using Content.Shared.NPC.Systems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Tools.Systems;
+using Content.Shared.Turrets;
using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
@@ -53,6 +54,7 @@ public sealed class NPCUtilitySystem : EntitySystem
[Dependency] private readonly ExamineSystemShared _examine = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly MobThresholdSystem _thresholdSystem = default!;
+ [Dependency] private readonly TurretTargetSettingsSystem _turretTargetSettings = default!;
private EntityQuery _puddleQuery;
private EntityQuery _xformQuery;
@@ -358,6 +360,14 @@ public sealed class NPCUtilitySystem : EntitySystem
return 1f;
return 0f;
}
+ case TurretTargetingCon:
+ {
+ if (!TryComp(owner, out var turretTargetSettings) ||
+ _turretTargetSettings.EntityIsTargetForTurret((owner, turretTargetSettings), targetUid))
+ return 1f;
+
+ return 0f;
+ }
default:
throw new NotImplementedException();
}
diff --git a/Content.Server/Objectives/Components/FreeObjectiveComponent.cs b/Content.Server/Objectives/Components/FreeObjectiveComponent.cs
new file mode 100644
index 0000000000..ae5070ddf7
--- /dev/null
+++ b/Content.Server/Objectives/Components/FreeObjectiveComponent.cs
@@ -0,0 +1,7 @@
+namespace Content.Server.Objectives.Components;
+
+///
+/// A free greentext, that's it.
+///
+[RegisterComponent]
+public sealed partial class FreeObjectiveComponent : Component;
diff --git a/Content.Server/Objectives/Systems/FreeObjectiveSystem.cs b/Content.Server/Objectives/Systems/FreeObjectiveSystem.cs
new file mode 100644
index 0000000000..ae1753b5c4
--- /dev/null
+++ b/Content.Server/Objectives/Systems/FreeObjectiveSystem.cs
@@ -0,0 +1,20 @@
+using Content.Server.Objectives.Components;
+using Content.Shared.Objectives.Components;
+
+namespace Content.Server.Objectives.Systems;
+
+public sealed class FreeObjectiveSystem : EntitySystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnGetProgress);
+ }
+
+ // You automatically greentext, there's not much else to it
+ private void OnGetProgress(Entity ent, ref ObjectiveGetProgressEvent args)
+ {
+ args.Progress = 1f;
+ }
+}
diff --git a/Content.Server/Roles/WizardRoleComponent.cs b/Content.Server/Roles/WizardRoleComponent.cs
new file mode 100644
index 0000000000..72a89ee2ca
--- /dev/null
+++ b/Content.Server/Roles/WizardRoleComponent.cs
@@ -0,0 +1,7 @@
+namespace Content.Server.Roles;
+
+///
+/// Mind role to tag entities that they're a Wizard
+///
+[RegisterComponent]
+public sealed partial class WizardRoleComponent : Component;
diff --git a/Content.Server/Salvage/SalvageSystem.Magnet.cs b/Content.Server/Salvage/SalvageSystem.Magnet.cs
index 523ac06e0b..988c5da797 100644
--- a/Content.Server/Salvage/SalvageSystem.Magnet.cs
+++ b/Content.Server/Salvage/SalvageSystem.Magnet.cs
@@ -354,7 +354,7 @@ public sealed partial class SalvageSystem
if (!TryGetSalvagePlacementLocation(magnet, mapId, attachedBounds, bounds!.Value, worldAngle, out var spawnLocation, out var spawnAngle))
{
Report(magnet.Owner, MagnetChannel, "salvage-system-announcement-spawn-no-debris-available");
- _mapManager.DeleteMap(salvMapXform.MapID);
+ _mapSystem.DeleteMap(salvMapXform.MapID);
return;
}
@@ -391,7 +391,7 @@ public sealed partial class SalvageSystem
}
Report(magnet.Owner, MagnetChannel, "salvage-system-announcement-arrived", ("timeLeft", data.Comp.ActiveTime.TotalSeconds));
- _mapManager.DeleteMap(salvMapXform.MapID);
+ _mapSystem.DeleteMap(salvMapXform.MapID);
data.Comp.Announced = false;
diff --git a/Content.Server/Shuttles/Components/GridSpawnComponent.cs b/Content.Server/Shuttles/Components/GridSpawnComponent.cs
index 18959dd7f3..c74613f5eb 100644
--- a/Content.Server/Shuttles/Components/GridSpawnComponent.cs
+++ b/Content.Server/Shuttles/Components/GridSpawnComponent.cs
@@ -62,7 +62,7 @@ public interface IGridSpawnGroup
}
[DataRecord]
-public sealed class DungeonSpawnGroup : IGridSpawnGroup
+public sealed partial class DungeonSpawnGroup : IGridSpawnGroup
{
///
/// Prototypes we can choose from to spawn.
@@ -97,7 +97,7 @@ public sealed class DungeonSpawnGroup : IGridSpawnGroup
}
[DataRecord]
-public sealed class GridSpawnGroup : IGridSpawnGroup
+public sealed partial class GridSpawnGroup : IGridSpawnGroup
{
public List Paths = new();
diff --git a/Content.Server/Shuttles/Systems/ArrivalsSystem.cs b/Content.Server/Shuttles/Systems/ArrivalsSystem.cs
index 708f5a7a1c..896570fb34 100644
--- a/Content.Server/Shuttles/Systems/ArrivalsSystem.cs
+++ b/Content.Server/Shuttles/Systems/ArrivalsSystem.cs
@@ -30,8 +30,10 @@ using Robust.Server.GameObjects;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
+using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
@@ -46,23 +48,23 @@ namespace Content.Server.Shuttles.Systems;
///
public sealed class ArrivalsSystem : EntitySystem
{
+ [Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
[Dependency] private readonly IConsoleHost _console = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly IChatManager _chat = default!;
- [Dependency] private readonly SharedMapSystem _mapSystem = default!;
+ [Dependency] private readonly ActorSystem _actor = default!;
[Dependency] private readonly BiomeSystem _biomes = default!;
+ [Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!;
[Dependency] private readonly GameTicker _ticker = default!;
[Dependency] private readonly MapLoaderSystem _loader = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
- [Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!;
+ [Dependency] private readonly SharedMapSystem _mapSystem = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly ShuttleSystem _shuttles = default!;
[Dependency] private readonly StationSpawningSystem _stationSpawning = default!;
[Dependency] private readonly StationSystem _station = default!;
- [Dependency] private readonly ActorSystem _actor = default!;
private EntityQuery _pendingQuery;
private EntityQuery _blacklistQuery;
@@ -515,31 +517,31 @@ public sealed class ArrivalsSystem : EntitySystem
private void SetupArrivalsStation()
{
var path = new ResPath(_cfgManager.GetCVar(CCVars.ArrivalsMap));
- if (!_loader.TryLoadMap(path, out var map, out var grids))
+ _mapSystem.CreateMap(out var mapId, runMapInit: false);
+ var mapUid = _mapSystem.GetMap(mapId);
+
+ if (!_loader.TryLoadGrid(mapId, path, out var grid))
return;
- _metaData.SetEntityName(map.Value, Loc.GetString("map-name-terminal"));
+ _metaData.SetEntityName(mapUid, Loc.GetString("map-name-terminal"));
- foreach (var id in grids)
- {
- EnsureComp(id);
- EnsureComp(id);
- EnsureComp(id);
- }
+ EnsureComp(grid.Value);
+ EnsureComp(grid.Value);
+ EnsureComp(grid.Value);
// Setup planet arrivals if relevant
if (_cfgManager.GetCVar(CCVars.ArrivalsPlanet))
{
var template = _random.Pick(_arrivalsBiomeOptions);
- _biomes.EnsurePlanet(map.Value, _protoManager.Index(template));
+ _biomes.EnsurePlanet(mapUid, _protoManager.Index(template));
var restricted = new RestrictedRangeComponent
{
Range = 32f
};
- AddComp(map.Value, restricted);
+ AddComp(mapUid, restricted);
}
- _mapSystem.InitializeMap(map.Value.Comp.MapId);
+ _mapSystem.InitializeMap(mapId);
// Handle roundstart stations.
var query = AllEntityQuery();
diff --git a/Content.Server/Silicons/Laws/IonStormSystem.cs b/Content.Server/Silicons/Laws/IonStormSystem.cs
index 6017a36fc0..c47d7e99ea 100644
--- a/Content.Server/Silicons/Laws/IonStormSystem.cs
+++ b/Content.Server/Silicons/Laws/IonStormSystem.cs
@@ -200,11 +200,11 @@ public sealed class IonStormSystem : EntitySystem
// i dont think theres a way to do this in fluent
var (who, plural) = _robustRandom.Next(0, 5) switch
{
- 0 => (Loc.GetString("ion-storm-you"), false),
- 1 => (Loc.GetString("ion-storm-the-station"), true),
- 2 => (Loc.GetString("ion-storm-the-crew"), true),
- 3 => (Loc.GetString("ion-storm-the-job", ("job", crew2)), false),
- _ => (area, true) // THE SINGULARITY REQUIRES THE HAPPY CLOWNS
+ 0 => (Loc.GetString("ion-storm-you"), true),
+ 1 => (Loc.GetString("ion-storm-the-station"), false),
+ 2 => (Loc.GetString("ion-storm-the-crew"), false),
+ 3 => (Loc.GetString("ion-storm-the-job", ("job", crew2)), true),
+ _ => (area, false) // THE SINGULARITY REQUIRES THE HAPPY CLOWNS
};
var jobChange = _robustRandom.Next(0, 3) switch
{
diff --git a/Content.Server/Speech/EntitySystems/AddAccentClothingSystem.cs b/Content.Server/Speech/EntitySystems/AddAccentClothingSystem.cs
index 897cd061f4..d55c6e6764 100644
--- a/Content.Server/Speech/EntitySystems/AddAccentClothingSystem.cs
+++ b/Content.Server/Speech/EntitySystems/AddAccentClothingSystem.cs
@@ -14,6 +14,8 @@ public sealed class AddAccentClothingSystem : EntitySystem
SubscribeLocalEvent(OnGotUnequipped);
}
+
+// TODO: Turn this into a relay event.
private void OnGotEquipped(EntityUid uid, AddAccentClothingComponent component, ref ClothingGotEquippedEvent args)
{
// does the user already has this accent?
diff --git a/Content.Server/StationEvents/Components/PowerGridCheckRuleComponent.cs b/Content.Server/StationEvents/Components/PowerGridCheckRuleComponent.cs
index 98c60346c7..757a4f18b5 100644
--- a/Content.Server/StationEvents/Components/PowerGridCheckRuleComponent.cs
+++ b/Content.Server/StationEvents/Components/PowerGridCheckRuleComponent.cs
@@ -1,11 +1,24 @@
using System.Threading;
using Content.Server.StationEvents.Events;
+using Robust.Shared.Audio;
+using Robust.Shared.Prototypes;
namespace Content.Server.StationEvents.Components;
[RegisterComponent, Access(typeof(PowerGridCheckRule))]
public sealed partial class PowerGridCheckRuleComponent : Component
{
+ ///
+ /// Default sound of the announcement when power is back on.
+ ///
+ private static readonly ProtoId DefaultPowerOn = new("PowerOn");
+
+ ///
+ /// Sound of the announcement to play when power is back on.
+ ///
+ [DataField]
+ public SoundSpecifier PowerOnSound = new SoundCollectionSpecifier(DefaultPowerOn, AudioParams.Default.WithVolume(-4f));
+
public CancellationTokenSource? AnnounceCancelToken;
public EntityUid AffectedStation;
diff --git a/Content.Server/StationEvents/EventManagerSystem.cs b/Content.Server/StationEvents/EventManagerSystem.cs
index 86c98a8e30..a66499d0aa 100644
--- a/Content.Server/StationEvents/EventManagerSystem.cs
+++ b/Content.Server/StationEvents/EventManagerSystem.cs
@@ -210,7 +210,7 @@ public sealed class EventManagerSystem : EntitySystem
if (prototype.Abstract)
continue;
- if (!prototype.TryGetComponent(out var stationEvent))
+ if (!prototype.TryGetComponent(out var stationEvent, EntityManager.ComponentFactory))
continue;
allEvents.Add(prototype, stationEvent);
diff --git a/Content.Server/StationEvents/Events/ImmovableRodRule.cs b/Content.Server/StationEvents/Events/ImmovableRodRule.cs
index 17a92e1b3d..5e324a529b 100644
--- a/Content.Server/StationEvents/Events/ImmovableRodRule.cs
+++ b/Content.Server/StationEvents/Events/ImmovableRodRule.cs
@@ -26,7 +26,8 @@ public sealed class ImmovableRodRule : StationEventSystem(protoName);
- if (proto.TryGetComponent(out var rod) && proto.TryGetComponent(out var despawn))
+ if (proto.TryGetComponent(out var rod, EntityManager.ComponentFactory) &&
+ proto.TryGetComponent(out var despawn, EntityManager.ComponentFactory))
{
if (!TryFindRandomTile(out _, out _, out _, out var targetCoords))
return;
diff --git a/Content.Server/StationEvents/Events/PowerGridCheckRule.cs b/Content.Server/StationEvents/Events/PowerGridCheckRule.cs
index a4594e1387..3a86f694ab 100644
--- a/Content.Server/StationEvents/Events/PowerGridCheckRule.cs
+++ b/Content.Server/StationEvents/Events/PowerGridCheckRule.cs
@@ -59,7 +59,7 @@ namespace Content.Server.StationEvents.Events
component.AnnounceCancelToken = new CancellationTokenSource();
Timer.Spawn(3000, () =>
{
- Audio.PlayGlobal("/Audio/Announcements/power_on.ogg", Filter.Broadcast(), true, AudioParams.Default.WithVolume(-4f));
+ Audio.PlayGlobal(component.PowerOnSound, Filter.Broadcast(), true);
}, component.AnnounceCancelToken.Token);
component.Unpowered.Clear();
}
diff --git a/Content.Server/StationRecords/Systems/StationRecordsSystem.cs b/Content.Server/StationRecords/Systems/StationRecordsSystem.cs
index 87947ddf79..6a7584e65c 100644
--- a/Content.Server/StationRecords/Systems/StationRecordsSystem.cs
+++ b/Content.Server/StationRecords/Systems/StationRecordsSystem.cs
@@ -11,6 +11,7 @@ using Content.Shared.Roles;
using Content.Shared.StationRecords;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
namespace Content.Server.StationRecords.Systems;
@@ -39,6 +40,7 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem
[Dependency] private readonly StationRecordKeyStorageSystem _keyStorage = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IdCardSystem _idCard = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
public override void Initialize()
{
@@ -232,6 +234,28 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem
return records.Records.TryGetRecordEntry(key.Id, out entry);
}
+ ///
+ /// Gets a random record from the station's record entries.
+ ///
+ /// The EntityId of the station from which you want to get the record.
+ /// The resulting entry.
+ /// Type to get from the record set.
+ /// True if a record was obtained. False otherwise.
+ public bool TryGetRandomRecord(Entity ent, [NotNullWhen(true)] out T? entry)
+ {
+ entry = default;
+
+ if (!Resolve(ent.Owner, ref ent.Comp))
+ return false;
+
+ if (ent.Comp.Records.Keys.Count == 0)
+ return false;
+
+ var key = _random.Pick(ent.Comp.Records.Keys);
+
+ return ent.Comp.Records.TryGetRecordEntry(key, out entry);
+ }
+
///
/// Returns an id if a record with the same name exists.
///
diff --git a/Content.Server/Traits/Assorted/UnrevivableSystem.cs b/Content.Server/Traits/Assorted/UnrevivableSystem.cs
new file mode 100644
index 0000000000..c2c8ee9a50
--- /dev/null
+++ b/Content.Server/Traits/Assorted/UnrevivableSystem.cs
@@ -0,0 +1,20 @@
+using Content.Shared.Cloning.Events;
+using Content.Shared.Traits.Assorted;
+
+namespace Content.Server.Traits.Assorted;
+
+public sealed class UnrevivableSystem : EntitySystem
+{
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnCloningAttempt);
+ }
+
+ private void OnCloningAttempt(Entity ent, ref CloningAttemptEvent args)
+ {
+ if (!ent.Comp.Cloneable)
+ args.Cancelled = true;
+ }
+}
diff --git a/Content.Server/VendingMachines/VendingMachineSystem.cs b/Content.Server/VendingMachines/VendingMachineSystem.cs
index c2ea6dd1ac..e061398114 100644
--- a/Content.Server/VendingMachines/VendingMachineSystem.cs
+++ b/Content.Server/VendingMachines/VendingMachineSystem.cs
@@ -1,19 +1,12 @@
using System.Linq;
using System.Numerics;
-using Content.Server.Advertise;
-using Content.Server.Advertise.Components;
using Content.Server.Cargo.Systems;
using Content.Server.Emp;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
-using Content.Shared.Access.Components;
-using Content.Shared.Access.Systems;
-using Content.Shared.Actions;
using Content.Shared.Damage;
using Content.Shared.Destructible;
using Content.Shared.DoAfter;
-using Content.Shared.Emag.Components;
-using Content.Shared.Emag.Systems;
using Content.Shared.Emp;
using Content.Shared.Popups;
using Content.Shared.Power;
@@ -21,7 +14,6 @@ using Content.Shared.Throwing;
using Content.Shared.UserInterface;
using Content.Shared.VendingMachines;
using Content.Shared.Wall;
-using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
@@ -32,14 +24,9 @@ namespace Content.Server.VendingMachines
public sealed class VendingMachineSystem : SharedVendingMachineSystem
{
[Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly AccessReaderSystem _accessReader = default!;
- [Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly PricingSystem _pricing = default!;
[Dependency] private readonly ThrowingSystem _throwingSystem = default!;
[Dependency] private readonly IGameTiming _timing = default!;
- [Dependency] private readonly SpeakOnUIClosedSystem _speakOnUIClosed = default!;
- [Dependency] private readonly SharedPointLightSystem _light = default!;
- [Dependency] private readonly EmagSystem _emag = default!;
private const float WallVendEjectDistanceFromWall = 1f;
@@ -55,11 +42,6 @@ namespace Content.Server.VendingMachines
SubscribeLocalEvent(OnActivatableUIOpenAttempt);
- Subs.BuiEvents(VendingMachineUiKey.Key, subs =>
- {
- subs.Event(OnInventoryEjectMessage);
- });
-
SubscribeLocalEvent(OnSelfDispense);
SubscribeLocalEvent(OnDoAfter);
@@ -91,7 +73,7 @@ namespace Content.Server.VendingMachines
if (HasComp(uid))
{
- TryUpdateVisualState(uid, component);
+ TryUpdateVisualState((uid, component));
}
}
@@ -101,26 +83,15 @@ namespace Content.Server.VendingMachines
args.Cancel();
}
- private void OnInventoryEjectMessage(EntityUid uid, VendingMachineComponent component, VendingMachineEjectMessage args)
- {
- if (!this.IsPowered(uid, EntityManager))
- return;
-
- if (args.Actor is not { Valid: true } entity || Deleted(entity))
- return;
-
- AuthorizedVend(uid, entity, args.Type, args.ID, component);
- }
-
private void OnPowerChanged(EntityUid uid, VendingMachineComponent component, ref PowerChangedEvent args)
{
- TryUpdateVisualState(uid, component);
+ TryUpdateVisualState((uid, component));
}
private void OnBreak(EntityUid uid, VendingMachineComponent vendComponent, BreakageEventArgs eventArgs)
{
vendComponent.Broken = true;
- TryUpdateVisualState(uid, vendComponent);
+ TryUpdateVisualState((uid, vendComponent));
}
private void OnDamageChanged(EntityUid uid, VendingMachineComponent component, DamageChangedEvent args)
@@ -128,7 +99,7 @@ namespace Content.Server.VendingMachines
if (!args.DamageIncreased && component.Broken)
{
component.Broken = false;
- TryUpdateVisualState(uid, component);
+ TryUpdateVisualState((uid, component));
return;
}
@@ -139,8 +110,11 @@ namespace Content.Server.VendingMachines
if (args.DamageIncreased && args.DamageDelta.GetTotal() >= component.DispenseOnHitThreshold &&
_random.Prob(component.DispenseOnHitChance.Value))
{
- if (component.DispenseOnHitCooldown > 0f)
- component.DispenseOnHitCoolingDown = true;
+ if (component.DispenseOnHitCooldown != null)
+ {
+ component.DispenseOnHitEnd = Timing.CurTime + component.DispenseOnHitCooldown.Value;
+ }
+
EjectRandom(uid, throwItem: true, forceEject: true, component);
}
}
@@ -199,145 +173,6 @@ namespace Content.Server.VendingMachines
Dirty(uid, component);
}
- public void Deny(EntityUid uid, VendingMachineComponent? vendComponent = null)
- {
- if (!Resolve(uid, ref vendComponent))
- return;
-
- if (vendComponent.Denying)
- return;
-
- vendComponent.Denying = true;
- Audio.PlayPvs(vendComponent.SoundDeny, uid, AudioParams.Default.WithVolume(-2f));
- TryUpdateVisualState(uid, vendComponent);
- }
-
- ///
- /// Checks if the user is authorized to use this vending machine
- ///
- ///
- /// Entity trying to use the vending machine
- ///
- public bool IsAuthorized(EntityUid uid, EntityUid sender, VendingMachineComponent? vendComponent = null)
- {
- if (!Resolve(uid, ref vendComponent))
- return false;
-
- if (!TryComp(uid, out var accessReader))
- return true;
-
- if (_accessReader.IsAllowed(sender, uid, accessReader))
- return true;
-
- Popup.PopupEntity(Loc.GetString("vending-machine-component-try-eject-access-denied"), uid);
- Deny(uid, vendComponent);
- return false;
- }
-
- ///
- /// Tries to eject the provided item. Will do nothing if the vending machine is incapable of ejecting, already ejecting
- /// or the item doesn't exist in its inventory.
- ///
- ///
- /// The type of inventory the item is from
- /// The prototype ID of the item
- /// Whether the item should be thrown in a random direction after ejection
- ///
- public void TryEjectVendorItem(EntityUid uid, InventoryType type, string itemId, bool throwItem, VendingMachineComponent? vendComponent = null)
- {
- if (!Resolve(uid, ref vendComponent))
- return;
-
- if (vendComponent.Ejecting || vendComponent.Broken || !this.IsPowered(uid, EntityManager))
- {
- return;
- }
-
- var entry = GetEntry(uid, itemId, type, vendComponent);
-
- if (entry == null)
- {
- Popup.PopupEntity(Loc.GetString("vending-machine-component-try-eject-invalid-item"), uid);
- Deny(uid, vendComponent);
- return;
- }
-
- if (entry.Amount <= 0)
- {
- Popup.PopupEntity(Loc.GetString("vending-machine-component-try-eject-out-of-stock"), uid);
- Deny(uid, vendComponent);
- return;
- }
-
- if (string.IsNullOrEmpty(entry.ID))
- return;
-
-
- // Start Ejecting, and prevent users from ordering while anim playing
- vendComponent.Ejecting = true;
- vendComponent.NextItemToEject = entry.ID;
- vendComponent.ThrowNextItem = throwItem;
-
- if (TryComp(uid, out SpeakOnUIClosedComponent? speakComponent))
- _speakOnUIClosed.TrySetFlag((uid, speakComponent));
-
- entry.Amount--;
- Dirty(uid, vendComponent);
- TryUpdateVisualState(uid, vendComponent);
- Audio.PlayPvs(vendComponent.SoundVend, uid);
- }
-
- ///
- /// Checks whether the user is authorized to use the vending machine, then ejects the provided item if true
- ///
- ///
- /// Entity that is trying to use the vending machine
- /// The type of inventory the item is from
- /// The prototype ID of the item
- ///
- public void AuthorizedVend(EntityUid uid, EntityUid sender, InventoryType type, string itemId, VendingMachineComponent component)
- {
- if (IsAuthorized(uid, sender, component))
- {
- TryEjectVendorItem(uid, type, itemId, component.CanShoot, component);
- }
- }
-
- ///
- /// Tries to update the visuals of the component based on its current state.
- ///
- public void TryUpdateVisualState(EntityUid uid, VendingMachineComponent? vendComponent = null)
- {
- if (!Resolve(uid, ref vendComponent))
- return;
-
- var finalState = VendingMachineVisualState.Normal;
- if (vendComponent.Broken)
- {
- finalState = VendingMachineVisualState.Broken;
- }
- else if (vendComponent.Ejecting)
- {
- finalState = VendingMachineVisualState.Eject;
- }
- else if (vendComponent.Denying)
- {
- finalState = VendingMachineVisualState.Deny;
- }
- else if (!this.IsPowered(uid, EntityManager))
- {
- finalState = VendingMachineVisualState.Off;
- }
-
- if (_light.TryGetLight(uid, out var pointlight))
- {
- var lightState = finalState != VendingMachineVisualState.Broken && finalState != VendingMachineVisualState.Off;
- _light.SetEnabled(uid, lightState, pointlight);
- }
-
- _appearanceSystem.SetData(uid, VendingMachineVisuals.VisualState, finalState);
- }
-
///
/// Ejects a random item from the available stock. Will do nothing if the vending machine is empty.
///
@@ -367,18 +202,18 @@ namespace Content.Server.VendingMachines
}
else
{
- TryEjectVendorItem(uid, item.Type, item.ID, throwItem, vendComponent);
+ TryEjectVendorItem(uid, item.Type, item.ID, throwItem, user: null, vendComponent: vendComponent);
}
}
- private void EjectItem(EntityUid uid, VendingMachineComponent? vendComponent = null, bool forceEject = false)
+ protected override void EjectItem(EntityUid uid, VendingMachineComponent? vendComponent = null, bool forceEject = false)
{
if (!Resolve(uid, ref vendComponent))
return;
// No need to update the visual state because we never changed it during a forced eject
if (!forceEject)
- TryUpdateVisualState(uid, vendComponent);
+ TryUpdateVisualState((uid, vendComponent));
if (string.IsNullOrEmpty(vendComponent.NextItemToEject))
{
@@ -411,68 +246,17 @@ namespace Content.Server.VendingMachines
vendComponent.ThrowNextItem = false;
}
- private VendingMachineInventoryEntry? GetEntry(EntityUid uid, string entryId, InventoryType type, VendingMachineComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return null;
-
- if (type == InventoryType.Emagged && _emag.CheckFlag(uid, EmagType.Interaction))
- return component.EmaggedInventory.GetValueOrDefault(entryId);
-
- if (type == InventoryType.Contraband && component.Contraband)
- return component.ContrabandInventory.GetValueOrDefault(entryId);
-
- return component.Inventory.GetValueOrDefault(entryId);
- }
-
public override void Update(float frameTime)
{
base.Update(frameTime);
- var query = EntityQueryEnumerator();
- while (query.MoveNext(out var uid, out var comp))
- {
- if (comp.Ejecting)
- {
- comp.EjectAccumulator += frameTime;
- if (comp.EjectAccumulator >= comp.EjectDelay)
- {
- comp.EjectAccumulator = 0f;
- comp.Ejecting = false;
-
- EjectItem(uid, comp);
- }
- }
-
- if (comp.Denying)
- {
- comp.DenyAccumulator += frameTime;
- if (comp.DenyAccumulator >= comp.DenyDelay)
- {
- comp.DenyAccumulator = 0f;
- comp.Denying = false;
-
- TryUpdateVisualState(uid, comp);
- }
- }
-
- if (comp.DispenseOnHitCoolingDown)
- {
- comp.DispenseOnHitAccumulator += frameTime;
- if (comp.DispenseOnHitAccumulator >= comp.DispenseOnHitCooldown)
- {
- comp.DispenseOnHitAccumulator = 0f;
- comp.DispenseOnHitCoolingDown = false;
- }
- }
- }
var disabled = EntityQueryEnumerator();
while (disabled.MoveNext(out var uid, out _, out var comp))
{
if (comp.NextEmpEject < _timing.CurTime)
{
EjectRandom(uid, true, false, comp);
- comp.NextEmpEject += TimeSpan.FromSeconds(5 * comp.EjectDelay);
+ comp.NextEmpEject += (5 * comp.EjectDelay);
}
}
}
@@ -485,7 +269,7 @@ namespace Content.Server.VendingMachines
RestockInventoryFromPrototype(uid, vendComponent);
Dirty(uid, vendComponent);
- TryUpdateVisualState(uid, vendComponent);
+ TryUpdateVisualState((uid, vendComponent));
}
private void OnPriceCalculation(EntityUid uid, VendingMachineRestockComponent component, ref PriceCalculationEvent args)
diff --git a/Content.Server/VoiceTrigger/StorageVoiceControlComponent.cs b/Content.Server/VoiceTrigger/StorageVoiceControlComponent.cs
new file mode 100644
index 0000000000..1689979d82
--- /dev/null
+++ b/Content.Server/VoiceTrigger/StorageVoiceControlComponent.cs
@@ -0,0 +1,19 @@
+using Content.Shared.Inventory;
+
+namespace Content.Server.VoiceTrigger;
+
+///
+/// Entities with this component, Containers, and TriggerOnVoiceComponent will insert any item or extract the spoken item after the TriggerOnVoiceComponent has been activated
+///
+[RegisterComponent]
+public sealed partial class StorageVoiceControlComponent : Component
+{
+ ///
+ /// Used to determine which slots the component can be used in.
+ ///
+ /// If not set, the component can be used anywhere, even while inside other containers.
+ ///
+ ///
+ [DataField]
+ public SlotFlags? AllowedSlots;
+}
diff --git a/Content.Server/VoiceTrigger/StorageVoiceControlSystem.cs b/Content.Server/VoiceTrigger/StorageVoiceControlSystem.cs
new file mode 100644
index 0000000000..72e361bc58
--- /dev/null
+++ b/Content.Server/VoiceTrigger/StorageVoiceControlSystem.cs
@@ -0,0 +1,98 @@
+using Content.Server.Hands.Systems;
+using Content.Server.Storage.EntitySystems;
+using Content.Shared.Administration.Logs;
+using Content.Shared.Database;
+using Content.Shared.Hands.Components;
+using Content.Shared.Inventory;
+using Content.Shared.Popups;
+using Content.Shared.Storage;
+using Robust.Server.Containers;
+
+namespace Content.Server.VoiceTrigger;
+
+///
+/// Allows storages to be manipulated using voice commands.
+///
+public sealed class StorageVoiceControlSystem : EntitySystem
+{
+ [Dependency] private readonly ContainerSystem _container = default!;
+ [Dependency] private readonly HandsSystem _hands = default!;
+ [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+ [Dependency] private readonly InventorySystem _inventory = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly StorageSystem _storage = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(VoiceTriggered);
+ }
+
+ private void VoiceTriggered(Entity ent, ref VoiceTriggeredEvent args)
+ {
+ // Check if the component has any slot restrictions via AllowedSlots
+ // If it has slot restrictions, check if the item is in a slot that is allowed
+ if (ent.Comp.AllowedSlots != null && _inventory.TryGetContainingSlot(ent.Owner, out var itemSlot) &&
+ (itemSlot.SlotFlags & ent.Comp.AllowedSlots) == 0)
+ return;
+
+ // Don't do anything if there is no message
+ if (args.Message == null)
+ return;
+
+ // Get the storage component
+ if (!TryComp(ent, out var storage))
+ return;
+
+ // Get the hands component
+ if (!TryComp(args.Source, out var hands))
+ return;
+
+ // If the player has something in their hands, try to insert it into the storage
+ if (hands.ActiveHand != null && hands.ActiveHand.HeldEntity.HasValue)
+ {
+ // Disallow insertion and provide a reason why if the person decides to insert the item into itself
+ if (ent.Owner.Equals(hands.ActiveHand.HeldEntity.Value))
+ {
+ _popup.PopupEntity(Loc.GetString("comp-storagevoicecontrol-self-insert", ("entity", hands.ActiveHand.HeldEntity.Value)), ent, args.Source);
+ return;
+ }
+ if (_storage.CanInsert(ent, hands.ActiveHand.HeldEntity.Value, out var failedReason))
+ {
+ // We adminlog before insertion, otherwise the logger will attempt to pull info on an entity that no longer is present and throw an exception
+ _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.Source)} inserted {ToPrettyString(hands.ActiveHand.HeldEntity.Value)} into {ToPrettyString(ent)} via voice control");
+ _storage.Insert(ent, hands.ActiveHand.HeldEntity.Value, out _);
+ return;
+ }
+ {
+ // Tell the player the reason why the item couldn't be inserted
+ if (failedReason == null)
+ return;
+ _popup.PopupEntity(Loc.GetString(failedReason), ent, args.Source);
+ _adminLogger.Add(LogType.Action,
+ LogImpact.Low,
+ $"{ToPrettyString(args.Source)} failed to insert {ToPrettyString(hands.ActiveHand.HeldEntity.Value)} into {ToPrettyString(ent)} via voice control");
+ }
+ return;
+ }
+
+ // If otherwise, we're retrieving an item, so check all the items currently in the attached storage
+ foreach (var item in storage.Container.ContainedEntities)
+ {
+ // Get the item's name
+ var itemName = MetaData(item).EntityName;
+ // The message doesn't match the item name the requestor requested, skip and move on to the next item
+ if (!args.Message.Contains(itemName, StringComparison.InvariantCultureIgnoreCase))
+ continue;
+
+ // We found the item we want, so draw it from storage and place it into the player's hands
+ if (storage.Container.ContainedEntities.Count != 0)
+ {
+ _container.RemoveEntity(ent, item);
+ _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.Source)} retrieved {ToPrettyString(item)} from {ToPrettyString(ent)} via voice control");
+ _hands.TryPickup(args.Source, item, handsComp: hands);
+ break;
+ }
+ }
+ }
+}
diff --git a/Content.Server/Wires/WireLayout.cs b/Content.Server/Wires/WireLayout.cs
index 621992c915..7ca4faaeca 100644
--- a/Content.Server/Wires/WireLayout.cs
+++ b/Content.Server/Wires/WireLayout.cs
@@ -20,7 +20,7 @@ public sealed partial class WireLayoutPrototype : IPrototype, IInheritingPrototy
public string[]? Parents { get; private set; }
[AbstractDataField]
- public bool Abstract { get; }
+ public bool Abstract { get; private set; }
///
/// How many wires in this layout will do
diff --git a/Content.Server/Worldgen/Prototypes/BiomePrototype.cs b/Content.Server/Worldgen/Prototypes/BiomePrototype.cs
index 1cecf87a5e..1c3633fb5f 100644
--- a/Content.Server/Worldgen/Prototypes/BiomePrototype.cs
+++ b/Content.Server/Worldgen/Prototypes/BiomePrototype.cs
@@ -14,12 +14,12 @@ public sealed partial class BiomePrototype : IPrototype, IInheritingPrototype
{
///
[ParentDataField(typeof(AbstractPrototypeIdArraySerializer))]
- public string[]? Parents { get; }
+ public string[]? Parents { get; private set; }
///
[NeverPushInheritance]
[AbstractDataField]
- public bool Abstract { get; }
+ public bool Abstract { get; private set; }
///
[IdDataField]
@@ -42,7 +42,7 @@ public sealed partial class BiomePrototype : IPrototype, IInheritingPrototype
///
[DataField("chunkComponents")]
[AlwaysPushInheritance]
- public ComponentRegistry ChunkComponents { get; } = new();
+ public ComponentRegistry ChunkComponents = new();
//TODO: Get someone to make this a method on componentregistry that does it Correctly.
///
diff --git a/Content.Server/Worldgen/Prototypes/NoiseChannelPrototype.cs b/Content.Server/Worldgen/Prototypes/NoiseChannelPrototype.cs
index 02ca383d30..deb39de99e 100644
--- a/Content.Server/Worldgen/Prototypes/NoiseChannelPrototype.cs
+++ b/Content.Server/Worldgen/Prototypes/NoiseChannelPrototype.cs
@@ -84,12 +84,12 @@ public sealed partial class NoiseChannelPrototype : NoiseChannelConfig, IPrototy
{
///
[ParentDataField(typeof(AbstractPrototypeIdArraySerializer))]
- public string[]? Parents { get; }
+ public string[]? Parents { get; private set; }
///
[NeverPushInheritance]
[AbstractDataField]
- public bool Abstract { get; }
+ public bool Abstract { get; private set; }
///
[IdDataField]
diff --git a/Content.Server/Zombies/ZombieSystem.Transform.cs b/Content.Server/Zombies/ZombieSystem.Transform.cs
index 82c9e2dacc..b393850497 100644
--- a/Content.Server/Zombies/ZombieSystem.Transform.cs
+++ b/Content.Server/Zombies/ZombieSystem.Transform.cs
@@ -24,11 +24,11 @@ using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Movement.Systems;
+using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.NPC.Systems;
using Content.Shared.Nutrition.AnimalHusbandry;
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
-using Content.Shared.Roles;
using Content.Shared.Weapons.Melee;
using Content.Shared.Zombies;
using Content.Shared.Prying.Components;
@@ -58,8 +58,8 @@ public sealed partial class ZombieSystem
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
[Dependency] private readonly NPCSystem _npc = default!;
- [Dependency] private readonly SharedRoleSystem _roles = default!;
[Dependency] private readonly TagSystem _tag = default!;
+ [Dependency] private readonly NameModifierSystem _nameMod = default!;
///
/// Handles an entity turning into a zombie when they die or go into crit
@@ -235,7 +235,7 @@ public sealed partial class ZombieSystem
if (hasMind && _mind.TryGetSession(mindId, out var session))
{
//Zombie role for player manifest
- _roles.MindAddRole(mindId, "MindRoleZombie", mind: null, silent: true);
+ _role.MindAddRole(mindId, "MindRoleZombie", mind: null, silent: true);
//Greeting message for new bebe zombers
_chatMan.DispatchServerMessage(session, Loc.GetString("zombie-infection-greeting"));
diff --git a/Content.Server/Zombies/ZombieSystem.cs b/Content.Server/Zombies/ZombieSystem.cs
index ec50e11a0c..aa5c2682bc 100644
--- a/Content.Server/Zombies/ZombieSystem.cs
+++ b/Content.Server/Zombies/ZombieSystem.cs
@@ -5,18 +5,20 @@ using Content.Server.Chat;
using Content.Server.Chat.Systems;
using Content.Server.Emoting.Systems;
using Content.Server.Speech.EntitySystems;
+using Content.Server.Roles;
using Content.Shared.Anomaly.Components;
using Content.Shared.Bed.Sleep;
-using Content.Shared.Cloning;
+using Content.Shared.Cloning.Events;
using Content.Shared.Damage;
using Content.Shared.Humanoid;
using Content.Shared.Inventory;
using Content.Shared.Mind;
+using Content.Shared.Mind.Components;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
-using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.Popups;
+using Content.Shared.Roles;
using Content.Shared.Weapons.Melee.Events;
using Content.Shared.Zombies;
using Robust.Shared.Prototypes;
@@ -38,7 +40,7 @@ namespace Content.Server.Zombies
[Dependency] private readonly EmoteOnDamageSystem _emoteOnDamage = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
- [Dependency] private readonly NameModifierSystem _nameMod = default!;
+ [Dependency] private readonly SharedRoleSystem _role = default!;
public const SlotFlags ProtectiveSlots =
SlotFlags.FEET |
@@ -63,6 +65,8 @@ namespace Content.Server.Zombies
SubscribeLocalEvent(OnZombieCloning);
SubscribeLocalEvent(OnSleepAttempt);
SubscribeLocalEvent(OnGetCharacterDeadIC);
+ SubscribeLocalEvent(OnMindAdded);
+ SubscribeLocalEvent(OnMindRemoved);
SubscribeLocalEvent(OnPendingMapInit);
SubscribeLocalEvent(OnBeforeRemoveAnomalyOnDeath);
@@ -272,7 +276,7 @@ namespace Content.Server.Zombies
/// the entity you want to unzombify (different from source in case of cloning, for example)
///
///
- /// this currently only restore the name and skin/eye color from before zombified
+ /// this currently only restore the skin/eye color from before zombified
/// TODO: completely rethink how zombies are done to allow reversal.
///
public bool UnZombify(EntityUid source, EntityUid target, ZombieComponent? zombiecomp)
@@ -292,14 +296,25 @@ namespace Content.Server.Zombies
_humanoidAppearance.SetSkinColor(target, zombiecomp.BeforeZombifiedSkinColor, false);
_bloodstream.ChangeBloodReagent(target, zombiecomp.BeforeZombifiedBloodReagent);
- _nameMod.RefreshNameModifiers(target);
return true;
}
- private void OnZombieCloning(EntityUid uid, ZombieComponent zombiecomp, ref CloningEvent args)
+ private void OnZombieCloning(Entity ent, ref CloningEvent args)
{
- if (UnZombify(args.Source, args.Target, zombiecomp))
- args.NameHandled = true;
+ UnZombify(ent.Owner, args.CloneUid, ent.Comp);
+ }
+
+ // Make sure players that enter a zombie (for example via a ghost role or the mind swap spell) count as an antagonist.
+ private void OnMindAdded(Entity ent, ref MindAddedMessage args)
+ {
+ if (!_role.MindHasRole(args.Mind))
+ _role.MindAddRole(args.Mind, "MindRoleZombie", mind: args.Mind.Comp);
+ }
+
+ // Remove the role when getting cloned, getting gibbed and borged, or leaving the body via any other method.
+ private void OnMindRemoved(Entity ent, ref MindRemovedMessage args)
+ {
+ _role.MindTryRemoveRole(args.Mind);
}
}
}
diff --git a/Content.Shared/Access/AccessLevelPrototype.cs b/Content.Shared/Access/AccessLevelPrototype.cs
index e3a3b426b0..f44ad64228 100644
--- a/Content.Shared/Access/AccessLevelPrototype.cs
+++ b/Content.Shared/Access/AccessLevelPrototype.cs
@@ -15,9 +15,15 @@ namespace Content.Shared.Access
///
/// The player-visible name of the access level, in the ID card console and such.
///
- [DataField("name")]
+ [DataField]
public string? Name { get; set; }
+ ///
+ /// Denotes whether this access level is intended to be assignable to a crew ID card.
+ ///
+ [DataField]
+ public bool CanAddToIdCard = true;
+
public string GetAccessLevelName()
{
if (Name is { } name)
diff --git a/Content.Shared/ActionBlocker/ActionBlockerSystem.cs b/Content.Shared/ActionBlocker/ActionBlockerSystem.cs
index 6152699ccb..08eac657c0 100644
--- a/Content.Shared/ActionBlocker/ActionBlockerSystem.cs
+++ b/Content.Shared/ActionBlocker/ActionBlockerSystem.cs
@@ -109,10 +109,16 @@ namespace Content.Shared.ActionBlocker
///
public bool CanUseHeldEntity(EntityUid user, EntityUid used)
{
- var ev = new UseAttemptEvent(user, used);
- RaiseLocalEvent(user, ev);
+ var useEv = new UseAttemptEvent(user, used);
+ RaiseLocalEvent(user, useEv);
- return !ev.Cancelled;
+ if (useEv.Cancelled)
+ return false;
+
+ var usedEv = new GettingUsedAttemptEvent(user);
+ RaiseLocalEvent(used, usedEv);
+
+ return !usedEv.Cancelled;
}
diff --git a/Content.Server/Advertise/Components/SpeakOnUIClosedComponent.cs b/Content.Shared/Advertise/Components/SpeakOnUIClosedComponent.cs
similarity index 80%
rename from Content.Server/Advertise/Components/SpeakOnUIClosedComponent.cs
rename to Content.Shared/Advertise/Components/SpeakOnUIClosedComponent.cs
index 99d0080d7f..1f9672dc7f 100644
--- a/Content.Server/Advertise/Components/SpeakOnUIClosedComponent.cs
+++ b/Content.Shared/Advertise/Components/SpeakOnUIClosedComponent.cs
@@ -1,13 +1,15 @@
+using Content.Shared.Advertise.Systems;
using Content.Shared.Dataset;
+using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
-namespace Content.Server.Advertise.Components;
+namespace Content.Shared.Advertise.Components;
///
/// Causes the entity to speak using the Chat system when its ActivatableUI is closed, optionally
/// requiring that a Flag be set as well.
///
-[RegisterComponent, Access(typeof(SpeakOnUIClosedSystem))]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedSpeakOnUIClosedSystem))]
public sealed partial class SpeakOnUIClosedComponent : Component
{
///
@@ -31,6 +33,6 @@ public sealed partial class SpeakOnUIClosedComponent : Component
///
/// State variable only used if is true. Set with .
///
- [DataField]
+ [DataField, AutoNetworkedField]
public bool Flag;
}
diff --git a/Content.Shared/Advertise/Systems/SharedSpeakOnUIClosedSystem.cs b/Content.Shared/Advertise/Systems/SharedSpeakOnUIClosedSystem.cs
new file mode 100644
index 0000000000..2574e9d499
--- /dev/null
+++ b/Content.Shared/Advertise/Systems/SharedSpeakOnUIClosedSystem.cs
@@ -0,0 +1,16 @@
+using SpeakOnUIClosedComponent = Content.Shared.Advertise.Components.SpeakOnUIClosedComponent;
+
+namespace Content.Shared.Advertise.Systems;
+
+public abstract class SharedSpeakOnUIClosedSystem : EntitySystem
+{
+ public bool TrySetFlag(Entity entity, bool value = true)
+ {
+ if (!Resolve(entity, ref entity.Comp))
+ return false;
+
+ entity.Comp.Flag = value;
+ Dirty(entity);
+ return true;
+ }
+}
diff --git a/Content.Shared/Alert/AlertCategoryPrototype.cs b/Content.Shared/Alert/AlertCategoryPrototype.cs
index 7c7d047521..6685151142 100644
--- a/Content.Shared/Alert/AlertCategoryPrototype.cs
+++ b/Content.Shared/Alert/AlertCategoryPrototype.cs
@@ -10,5 +10,5 @@ public sealed partial class AlertCategoryPrototype : IPrototype
{
///
[IdDataField]
- public string ID { get; } = default!;
+ public string ID { get; private set; } = default!;
}
diff --git a/Content.Shared/Audio/AmbientMusicPrototype.cs b/Content.Shared/Audio/AmbientMusicPrototype.cs
index 219c41527d..e98f324a06 100644
--- a/Content.Shared/Audio/AmbientMusicPrototype.cs
+++ b/Content.Shared/Audio/AmbientMusicPrototype.cs
@@ -12,7 +12,7 @@ namespace Content.Shared.Audio;
[Prototype("ambientMusic")]
public sealed partial class AmbientMusicPrototype : IPrototype
{
- [IdDataField] public string ID { get; } = string.Empty;
+ [IdDataField] public string ID { get; private set; } = string.Empty;
///
/// Traditionally you'd prioritise most rules to least as priority but in our case we'll just be explicit.
diff --git a/Content.Shared/Audio/Jukebox/JukeboxPrototype.cs b/Content.Shared/Audio/Jukebox/JukeboxPrototype.cs
index ad690ef497..df90d42428 100644
--- a/Content.Shared/Audio/Jukebox/JukeboxPrototype.cs
+++ b/Content.Shared/Audio/Jukebox/JukeboxPrototype.cs
@@ -10,7 +10,7 @@ namespace Content.Shared.Audio.Jukebox;
public sealed partial class JukeboxPrototype : IPrototype
{
[IdDataField]
- public string ID { get; } = string.Empty;
+ public string ID { get; private set; } = string.Empty;
///
/// User friendly name to use in UI.
diff --git a/Content.Shared/Body/Prototypes/BodyPrototype.cs b/Content.Shared/Body/Prototypes/BodyPrototype.cs
index 123638f099..646acaaf6b 100644
--- a/Content.Shared/Body/Prototypes/BodyPrototype.cs
+++ b/Content.Shared/Body/Prototypes/BodyPrototype.cs
@@ -5,7 +5,7 @@ namespace Content.Shared.Body.Prototypes;
[Prototype("body")]
public sealed partial class BodyPrototype : IPrototype
{
- [IdDataField] public string ID { get; } = default!;
+ [IdDataField] public string ID { get; private set; } = default!;
[DataField("name")]
public string Name { get; private set; } = "";
@@ -26,4 +26,4 @@ public sealed partial class BodyPrototype : IPrototype
}
[DataRecord]
-public sealed record BodyPrototypeSlot(EntProtoId? Part, HashSet Connections, Dictionary Organs);
+public sealed partial record BodyPrototypeSlot(EntProtoId? Part, HashSet Connections, Dictionary Organs);
diff --git a/Content.Shared/CCVar/CCVars.Accessibility.cs b/Content.Shared/CCVar/CCVars.Accessibility.cs
index 8eb61f0806..14312363a1 100644
--- a/Content.Shared/CCVar/CCVars.Accessibility.cs
+++ b/Content.Shared/CCVar/CCVars.Accessibility.cs
@@ -38,4 +38,27 @@ public sealed partial class CCVars
///
public static readonly CVarDef AccessibilityColorblindFriendly =
CVarDef.Create("accessibility.colorblind_friendly", false, CVar.CLIENTONLY | CVar.ARCHIVE);
+
+ ///
+ /// Speech bubble text opacity slider, controlling the alpha of speech bubble's text.
+ /// Goes from to 0 (completely transparent) to 1 (completely opaque)
+ ///
+ public static readonly CVarDef SpeechBubbleTextOpacity =
+ CVarDef.Create("accessibility.speech_bubble_text_opacity", 1f, CVar.CLIENTONLY | CVar.ARCHIVE);
+
+ ///
+ /// Speech bubble speaker opacity slider, controlling the alpha of the speaker's name in a speech bubble.
+ /// Goes from to 0 (completely transparent) to 1 (completely opaque)
+ ///
+ public static readonly CVarDef SpeechBubbleSpeakerOpacity =
+ CVarDef.Create("accessibility.speech_bubble_speaker_opacity", 1f, CVar.CLIENTONLY | CVar.ARCHIVE);
+
+ ///
+ /// Speech bubble background opacity slider, controlling the alpha of the speech bubble's background.
+ /// Goes from to 0 (completely transparent) to 1 (completely opaque)
+ ///
+ public static readonly CVarDef SpeechBubbleBackgroundOpacity =
+ CVarDef.Create("accessibility.speech_bubble_background_opacity", 0.75f, CVar.CLIENTONLY | CVar.ARCHIVE);
+
+
}
diff --git a/Content.Shared/CCVar/CCVars.Game.cs b/Content.Shared/CCVar/CCVars.Game.cs
index 4a6f1118a1..13e72b9e3c 100644
--- a/Content.Shared/CCVar/CCVars.Game.cs
+++ b/Content.Shared/CCVar/CCVars.Game.cs
@@ -5,391 +5,390 @@ namespace Content.Shared.CCVar;
public sealed partial class CCVars
{
- ///
- /// Disables most functionality in the GameTicker.
- ///
- public static readonly CVarDef
- GameDummyTicker = CVarDef.Create("game.dummyticker", false, CVar.ARCHIVE | CVar.SERVERONLY);
+ ///
+ /// Disables most functionality in the GameTicker.
+ ///
+ public static readonly CVarDef
+ GameDummyTicker = CVarDef.Create("game.dummyticker", false, CVar.ARCHIVE | CVar.SERVERONLY);
- ///
- /// Controls if the lobby is enabled. If it is not, and there are no available jobs, you may get stuck on a black screen.
- ///
- public static readonly CVarDef
- GameLobbyEnabled = CVarDef.Create("game.lobbyenabled", true, CVar.ARCHIVE);
+ ///
+ /// Controls if the lobby is enabled. If it is not, and there are no available jobs, you may get stuck on a black screen.
+ ///
+ public static readonly CVarDef
+ GameLobbyEnabled = CVarDef.Create("game.lobbyenabled", true, CVar.ARCHIVE);
- ///
- /// Controls the duration of the lobby timer in seconds. Defaults to 2 minutes and 30 seconds.
- ///
- public static readonly CVarDef
- GameLobbyDuration = CVarDef.Create("game.lobbyduration", 150, CVar.ARCHIVE);
+ ///
+ /// Controls the duration of the lobby timer in seconds. Defaults to 2 minutes and 30 seconds.
+ ///
+ public static readonly CVarDef
+ GameLobbyDuration = CVarDef.Create("game.lobbyduration", 150, CVar.ARCHIVE);
- ///
- /// Controls if players can latejoin at all.
- ///
- public static readonly CVarDef
- GameDisallowLateJoins = CVarDef.Create("game.disallowlatejoins", false, CVar.ARCHIVE | CVar.SERVERONLY);
+ ///
+ /// Controls if players can latejoin at all.
+ ///
+ public static readonly CVarDef
+ GameDisallowLateJoins = CVarDef.Create("game.disallowlatejoins", false, CVar.ARCHIVE | CVar.SERVERONLY);
- ///
- /// Controls the default game preset.
- ///
- public static readonly CVarDef
- GameLobbyDefaultPreset = CVarDef.Create("game.defaultpreset", "secret", CVar.ARCHIVE);
+ ///
+ /// Controls the default game preset.
+ ///
+ public static readonly CVarDef
+ GameLobbyDefaultPreset = CVarDef.Create("game.defaultpreset", "secret", CVar.ARCHIVE);
- ///
- /// Controls if the game can force a different preset if the current preset's criteria are not met.
- ///
- public static readonly CVarDef
- GameLobbyFallbackEnabled = CVarDef.Create("game.fallbackenabled", true, CVar.ARCHIVE);
+ ///
+ /// Controls if the game can force a different preset if the current preset's criteria are not met.
+ ///
+ public static readonly CVarDef
+ GameLobbyFallbackEnabled = CVarDef.Create("game.fallbackenabled", true, CVar.ARCHIVE);
- ///
- /// The preset for the game to fall back to if the selected preset could not be used, and fallback is enabled.
- ///
- public static readonly CVarDef
- GameLobbyFallbackPreset = CVarDef.Create("game.fallbackpreset", "Traitor,Extended", CVar.ARCHIVE);
+ ///
+ /// The preset for the game to fall back to if the selected preset could not be used, and fallback is enabled.
+ ///
+ public static readonly CVarDef
+ GameLobbyFallbackPreset = CVarDef.Create("game.fallbackpreset", "Traitor,Extended", CVar.ARCHIVE);
- ///
- /// Controls if people can win the game in Suspicion or Deathmatch.
- ///
- public static readonly CVarDef
- GameLobbyEnableWin = CVarDef.Create("game.enablewin", true, CVar.ARCHIVE);
+ ///
+ /// Controls if people can win the game in Suspicion or Deathmatch.
+ ///
+ public static readonly CVarDef
+ GameLobbyEnableWin = CVarDef.Create("game.enablewin", true, CVar.ARCHIVE);
- ///
- /// Controls the maximum number of character slots a player is allowed to have.
- ///
- public static readonly CVarDef
- GameMaxCharacterSlots = CVarDef.Create("game.maxcharacterslots", 30, CVar.ARCHIVE | CVar.SERVERONLY);
+ ///
+ /// Controls the maximum number of character slots a player is allowed to have.
+ ///
+ public static readonly CVarDef
+ GameMaxCharacterSlots = CVarDef.Create("game.maxcharacterslots", 30, CVar.ARCHIVE | CVar.SERVERONLY);
- ///
- /// Controls the game map prototype to load. SS14 stores these prototypes in Prototypes/Maps.
- ///
- public static readonly CVarDef
- GameMap = CVarDef.Create("game.map", string.Empty, CVar.SERVERONLY);
+ ///
+ /// Controls the game map prototype to load. SS14 stores these prototypes in Prototypes/Maps.
+ ///
+ public static readonly CVarDef
+ GameMap = CVarDef.Create("game.map", string.Empty, CVar.SERVERONLY);
- ///
- /// Controls whether to use world persistence or not.
- ///
- public static readonly CVarDef
- UsePersistence = CVarDef.Create("game.usepersistence", false, CVar.ARCHIVE);
+ ///
+ /// Controls whether to use world persistence or not.
+ ///
+ public static readonly CVarDef
+ UsePersistence = CVarDef.Create("game.usepersistence", false, CVar.ARCHIVE);
- ///
- /// If world persistence is used, what map prototype should be initially loaded.
- /// If the save file exists, it replaces MapPath but everything else stays the same (station name and such).
- ///
- public static readonly CVarDef
- PersistenceMap = CVarDef.Create("game.persistencemap", "Empty", CVar.ARCHIVE);
+ ///
+ /// If world persistence is used, what map prototype should be initially loaded.
+ /// If the save file exists, it replaces MapPath but everything else stays the same (station name and such).
+ ///
+ public static readonly CVarDef
+ PersistenceMap = CVarDef.Create("game.persistencemap", "Empty", CVar.ARCHIVE);
- ///
- /// Prototype to use for map pool.
- ///
- public static readonly CVarDef
- GameMapPool = CVarDef.Create("game.map_pool", "DefaultMapPool", CVar.SERVERONLY);
+ ///
+ /// Prototype to use for map pool.
+ ///
+ public static readonly CVarDef
+ GameMapPool = CVarDef.Create("game.map_pool", "DefaultMapPool", CVar.SERVERONLY);
- ///
- /// The depth of the queue used to calculate which map is next in rotation.
- /// This is how long the game "remembers" that some map was put in play. Default is 16 rounds.
- ///
- public static readonly CVarDef
- GameMapMemoryDepth = CVarDef.Create("game.map_memory_depth", 16, CVar.SERVERONLY);
+ ///
+ /// The depth of the queue used to calculate which map is next in rotation.
+ /// This is how long the game "remembers" that some map was put in play. Default is 16 rounds.
+ ///
+ public static readonly CVarDef
+ GameMapMemoryDepth = CVarDef.Create("game.map_memory_depth", 16, CVar.SERVERONLY);
- ///
- /// Is map rotation enabled?
- ///
- public static readonly CVarDef
- GameMapRotation = CVarDef.Create("game.map_rotation", true, CVar.SERVERONLY);
+ ///
+ /// Is map rotation enabled?
+ ///
+ public static readonly CVarDef
+ GameMapRotation = CVarDef.Create("game.map_rotation", true, CVar.SERVERONLY);
- ///
- /// If roles should be restricted based on time.
- ///
- public static readonly CVarDef
- GameRoleTimers = CVarDef.Create("game.role_timers", true, CVar.SERVER | CVar.REPLICATED);
+ ///
+ /// If roles should be restricted based on time.
+ ///
+ public static readonly CVarDef
+ GameRoleTimers = CVarDef.Create("game.role_timers", true, CVar.SERVER | CVar.REPLICATED);
- ///
- /// Override default role requirements using a
- ///
- public static readonly CVarDef
- GameRoleTimerOverride = CVarDef.Create("game.role_timer_override", "", CVar.SERVER | CVar.REPLICATED);
+ ///
+ /// Override default role requirements using a
+ ///
+ public static readonly CVarDef
+ GameRoleTimerOverride = CVarDef.Create("game.role_timer_override", "", CVar.SERVER | CVar.REPLICATED);
- ///
- /// If roles should be restricted based on whether or not they are whitelisted.
- ///
- public static readonly CVarDef
- GameRoleWhitelist = CVarDef.Create("game.role_whitelist", true, CVar.SERVER | CVar.REPLICATED);
+ ///
+ /// If roles should be restricted based on whether or not they are whitelisted.
+ ///
+ public static readonly CVarDef
+ GameRoleWhitelist = CVarDef.Create("game.role_whitelist", true, CVar.SERVER | CVar.REPLICATED);
- ///
- /// Whether or not disconnecting inside of a cryopod should remove the character or just store them until they reconnect.
- ///
- public static readonly CVarDef
- GameCryoSleepRejoining = CVarDef.Create("game.cryo_sleep_rejoining", false, CVar.SERVER | CVar.REPLICATED);
+ ///
+ /// Whether or not disconnecting inside of a cryopod should remove the character or just store them until they reconnect.
+ ///
+ public static readonly CVarDef
+ GameCryoSleepRejoining = CVarDef.Create("game.cryo_sleep_rejoining", false, CVar.SERVER | CVar.REPLICATED);
- ///
- /// When enabled, guests will be assigned permanent UIDs and will have their preferences stored.
- ///
- public static readonly CVarDef GamePersistGuests =
- CVarDef.Create("game.persistguests", true, CVar.ARCHIVE | CVar.SERVERONLY);
+ ///
+ /// When enabled, guests will be assigned permanent UIDs and will have their preferences stored.
+ ///
+ public static readonly CVarDef GamePersistGuests =
+ CVarDef.Create("game.persistguests", true, CVar.ARCHIVE | CVar.SERVERONLY);
- public static readonly CVarDef GameDiagonalMovement =
- CVarDef.Create("game.diagonalmovement", true, CVar.ARCHIVE);
+ public static readonly CVarDef GameDiagonalMovement =
+ CVarDef.Create("game.diagonalmovement", true, CVar.ARCHIVE);
- public static readonly CVarDef SoftMaxPlayers =
- CVarDef.Create("game.soft_max_players", 30, CVar.SERVERONLY | CVar.ARCHIVE);
+ public static readonly CVarDef SoftMaxPlayers =
+ CVarDef.Create("game.soft_max_players", 30, CVar.SERVERONLY | CVar.ARCHIVE);
- ///
- /// If a player gets denied connection to the server,
- /// how long they are forced to wait before attempting to reconnect.
- ///
- public static readonly CVarDef GameServerFullReconnectDelay =
- CVarDef.Create("game.server_full_reconnect_delay", 30, CVar.SERVERONLY);
+ ///
+ /// If a player gets denied connection to the server,
+ /// how long they are forced to wait before attempting to reconnect.
+ ///
+ public static readonly CVarDef GameServerFullReconnectDelay =
+ CVarDef.Create("game.server_full_reconnect_delay", 30, CVar.SERVERONLY);
- ///
- /// Whether or not panic bunker is currently enabled.
- ///
- public static readonly CVarDef PanicBunkerEnabled =
- CVarDef.Create("game.panic_bunker.enabled", false, CVar.NOTIFY | CVar.REPLICATED | CVar.SERVER);
+ ///
+ /// Whether or not panic bunker is currently enabled.
+ ///
+ public static readonly CVarDef PanicBunkerEnabled =
+ CVarDef.Create("game.panic_bunker.enabled", false, CVar.NOTIFY | CVar.REPLICATED | CVar.SERVER);
- ///
- /// Whether or not the panic bunker will disable when an admin comes online.
- ///
- public static readonly CVarDef PanicBunkerDisableWithAdmins =
- CVarDef.Create("game.panic_bunker.disable_with_admins", false, CVar.SERVERONLY);
+ ///
+ /// Whether or not the panic bunker will disable when an admin comes online.
+ ///
+ public static readonly CVarDef PanicBunkerDisableWithAdmins =
+ CVarDef.Create("game.panic_bunker.disable_with_admins", false, CVar.SERVERONLY);
- ///
- /// Whether or not the panic bunker will enable when no admins are online.
- /// This counts everyone with the 'Admin' AdminFlag.
- ///
- public static readonly CVarDef PanicBunkerEnableWithoutAdmins =
- CVarDef.Create("game.panic_bunker.enable_without_admins", false, CVar.SERVERONLY);
+ ///
+ /// Whether or not the panic bunker will enable when no admins are online.
+ /// This counts everyone with the 'Admin' AdminFlag.
+ ///
+ public static readonly CVarDef PanicBunkerEnableWithoutAdmins =
+ CVarDef.Create("game.panic_bunker.enable_without_admins", false, CVar.SERVERONLY);
- ///
- /// Whether or not the panic bunker will count deadminned admins for
- /// and
- ///
- ///
- public static readonly CVarDef PanicBunkerCountDeadminnedAdmins =
- CVarDef.Create("game.panic_bunker.count_deadminned_admins", false, CVar.SERVERONLY);
+ ///
+ /// Whether or not the panic bunker will count deadminned admins for
+ /// and
+ ///
+ ///
+ public static readonly CVarDef PanicBunkerCountDeadminnedAdmins =
+ CVarDef.Create("game.panic_bunker.count_deadminned_admins", false, CVar.SERVERONLY);
- ///
- /// Show reason of disconnect for user or not.
- ///
- public static readonly CVarDef PanicBunkerShowReason =
- CVarDef.Create("game.panic_bunker.show_reason", false, CVar.SERVERONLY);
+ ///
+ /// Show reason of disconnect for user or not.
+ ///
+ public static readonly CVarDef PanicBunkerShowReason =
+ CVarDef.Create("game.panic_bunker.show_reason", false, CVar.SERVERONLY);
- ///
- /// Minimum age of the account (from server's PoV, so from first-seen date) in minutes.
- ///
- public static readonly CVarDef PanicBunkerMinAccountAge =
- CVarDef.Create("game.panic_bunker.min_account_age", 1440, CVar.SERVERONLY);
+ ///
+ /// Minimum age of the account (from server's PoV, so from first-seen date) in minutes.
+ ///
+ public static readonly CVarDef PanicBunkerMinAccountAge =
+ CVarDef.Create("game.panic_bunker.min_account_age", 1440, CVar.SERVERONLY);
- ///
- /// Minimal overall played time.
- ///
- public static readonly CVarDef PanicBunkerMinOverallMinutes =
- CVarDef.Create("game.panic_bunker.min_overall_minutes", 600, CVar.SERVERONLY);
+ ///
+ /// Minimal overall played time.
+ ///
+ public static readonly CVarDef PanicBunkerMinOverallMinutes =
+ CVarDef.Create("game.panic_bunker.min_overall_minutes", 600, CVar.SERVERONLY);
- ///
- /// A custom message that will be used for connections denied to the panic bunker
- /// If not empty, then will overwrite
- ///
- public static readonly CVarDef PanicBunkerCustomReason =
- CVarDef.Create("game.panic_bunker.custom_reason", string.Empty, CVar.SERVERONLY);
+ ///
+ /// A custom message that will be used for connections denied to the panic bunker
+ /// If not empty, then will overwrite
+ ///
+ public static readonly CVarDef PanicBunkerCustomReason =
+ CVarDef.Create("game.panic_bunker.custom_reason", string.Empty, CVar.SERVERONLY);
- ///
- /// Allow bypassing the panic bunker if the user is whitelisted.
- ///
- public static readonly CVarDef BypassBunkerWhitelist =
- CVarDef.Create("game.panic_bunker.whitelisted_can_bypass", true, CVar.SERVERONLY);
+ ///
+ /// Allow bypassing the panic bunker if the user is whitelisted.
+ ///
+ public static readonly CVarDef BypassBunkerWhitelist =
+ CVarDef.Create("game.panic_bunker.whitelisted_can_bypass", true, CVar.SERVERONLY);
- ///
- /// Enable IPIntel for blocking VPN connections from new players.
- ///
- public static readonly CVarDef GameIPIntelEnabled =
- CVarDef.Create("game.ipintel_enabled", false, CVar.SERVERONLY);
+ ///
+ /// Enable IPIntel for blocking VPN connections from new players.
+ ///
+ public static readonly CVarDef GameIPIntelEnabled =
+ CVarDef.Create("game.ipintel_enabled", false, CVar.SERVERONLY);
- ///
- /// Whether clients which are flagged as a VPN will be denied
- ///
- public static readonly CVarDef GameIPIntelRejectBad =
- CVarDef.Create("game.ipintel_reject_bad", true, CVar.SERVERONLY);
+ ///
+ /// Whether clients which are flagged as a VPN will be denied
+ ///
+ public static readonly CVarDef GameIPIntelRejectBad =
+ CVarDef.Create("game.ipintel_reject_bad", true, CVar.SERVERONLY);
- ///
- /// Whether clients which cannot be checked due to a rate limit will be denied
- ///
- public static readonly CVarDef GameIPIntelRejectRateLimited =
- CVarDef.Create("game.ipintel_reject_ratelimited", false, CVar.SERVERONLY);
+ ///
+ /// Whether clients which cannot be checked due to a rate limit will be denied
+ ///
+ public static readonly CVarDef GameIPIntelRejectRateLimited =
+ CVarDef.Create("game.ipintel_reject_ratelimited", false, CVar.SERVERONLY);
- ///
- /// Whether clients which cannot be checked due to an error of some form will be denied
- ///
- public static readonly CVarDef GameIPIntelRejectUnknown =
- CVarDef.Create("game.ipintel_reject_unknown", false, CVar.SERVERONLY);
+ ///