Merge remote-tracking branch 'upstream/master' into ed-2025-02-03-upstream-stable

# Conflicts:
#	Content.Client/Guidebook/Controls/GuideReagentReaction.xaml
#	Content.Client/Options/UI/OptionsMenu.xaml
#	Content.IntegrationTests/Tests/PostMapInitTest.cs
#	Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
This commit is contained in:
Ed
2025-03-02 22:03:30 +03:00
403 changed files with 11266 additions and 5590 deletions

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,5 @@
using Content.Shared.Advertise.Systems;
namespace Content.Client.Advertise.Systems;
public sealed class SpeakOnUIClosedSystem : SharedSpeakOnUIClosedSystem;

View File

@@ -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);
}
}

View File

@@ -18,4 +18,9 @@
<ScrollContainer VerticalExpand="True" HScrollEnabled="True">
<BoxContainer Orientation="Vertical" Name="ProbedDeviceContainer"/>
</ScrollContainer>
<BoxContainer Orientation="Horizontal" Margin="4 8">
<Button Name="PrintButton" HorizontalAlignment="Left" Text="{Loc 'log-probe-print-button'}" Disabled="True"/>
<BoxContainer HorizontalExpand="True"/>
<Label Name="EntityName" Align="Right"/>
</BoxContainer>
</cartridges:LogProbeUiFragment>

View File

@@ -8,17 +8,24 @@ namespace Content.Client.CartridgeLoader.Cartridges;
[GenerateTypedNameReferences]
public sealed partial class LogProbeUiFragment : BoxContainer
{
/// <summary>
/// Action invoked when the print button gets pressed.
/// </summary>
public Action? OnPrintPressed;
public LogProbeUiFragment()
{
RobustXamlLoader.Load(this);
PrintButton.OnPressed += _ => OnPrintPressed?.Invoke();
}
public void UpdateState(List<PulledAccessLog> logs)
public void UpdateState(string name, List<PulledAccessLog> 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)

View File

@@ -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
};

View File

@@ -94,7 +94,7 @@ public sealed class ChemistryGuideDataSystem : SharedChemistryGuideDataSystem
if (entProto.Abstract || usedNames.Contains(entProto.Name))
continue;
if (!entProto.TryGetComponent<ExtractableComponent>(out var extractableComponent))
if (!entProto.TryGetComponent<ExtractableComponent>(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<SolutionContainerManagerComponent>(out var manager) &&
entProto.TryGetComponent<SolutionContainerManagerComponent>(out var manager, EntityManager.ComponentFactory) &&
_solutionContainer.TryGetSolution(manager, grindableSolutionId, out var grindableSolution))
{
var data = new ReagentEntitySourceData(

View File

@@ -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++)
{

View File

@@ -34,6 +34,7 @@
<Label Text="{Loc 'chem-master-window-buffer-text'}" />
<Control HorizontalExpand="True" />
<Button MinSize="80 0" Name="BufferTransferButton" Access="Public" Text="{Loc 'chem-master-window-transfer-button'}" ToggleMode="True" StyleClasses="OpenRight" />
<Button MinSize="80 0" Name="BufferSortButton" Access="Public" Text="{Loc 'chem-master-window-sort-type-none'}" StyleClasses="OpenBoth" />
<Button MinSize="80 0" Name="BufferDiscardButton" Access="Public" Text="{Loc 'chem-master-window-discard-button'}" ToggleMode="True" StyleClasses="OpenLeft" />
</BoxContainer>

View File

@@ -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;

View File

@@ -27,7 +27,7 @@ public sealed class FlatpackSystem : SharedFlatpackSystem
if (!PrototypeManager.TryIndex<EntityPrototype>(machineBoardId, out var machineBoardPrototype))
return;
if (!machineBoardPrototype.TryGetComponent<SpriteComponent>(out var sprite))
if (!machineBoardPrototype.TryGetComponent<SpriteComponent>(out var sprite, EntityManager.ComponentFactory))
return;
Color? color = null;

View File

@@ -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

View File

@@ -155,7 +155,7 @@ namespace Content.Client.Ghost
private void OnGhostState(EntityUid uid, GhostComponent component, ref AfterAutoHandleStateEvent args)
{
if (TryComp<SpriteComponent>(uid, out var sprite))
sprite.LayerSetColor(0, component.color);
sprite.LayerSetColor(0, component.Color);
if (uid != _playerManager.LocalEntity)
return;

View File

@@ -1,11 +1,11 @@
<BoxContainer xmlns="https://spacestation14.io"
Orientation="Vertical"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Orientation="Vertical"
HorizontalAlignment="Stretch"
HorizontalExpand="True"
Margin="0 0 0 5">
<BoxContainer
Orientation="Horizontal">
<BoxContainer Orientation="Horizontal">
<BoxContainer Name="ReactantsContainer" Orientation="Vertical" HorizontalExpand="True"
VerticalAlignment="Center">
<RichTextLabel Name="ReactantsLabel"

View File

@@ -2,11 +2,15 @@
using Content.Client.Items;
using Content.Shared.Implants;
using Content.Shared.Implants.Components;
using Robust.Shared.Prototypes;
namespace Content.Client.Implants;
public sealed class ImplanterSystem : SharedImplanterSystem
{
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
public override void Initialize()
{
base.Initialize();
@@ -17,6 +21,18 @@ public sealed class ImplanterSystem : SharedImplanterSystem
private void OnHandleImplanterState(EntityUid uid, ImplanterComponent component, ref AfterAutoHandleStateEvent args)
{
if (_uiSystem.TryGetOpenUi<DeimplantBoundUserInterface>(uid, DeimplantUiKey.Key, out var bui))
{
Dictionary<string, string> 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;
}
}

View File

@@ -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<DeimplantChoiceWindow>();
_window.OnImplantChange += implant => SendMessage(new DeimplantChangeVerbMessage(implant));
}
public void UpdateState(Dictionary<string, string> implantList, string? implant)
{
if (_window != null)
{
_window.UpdateImplantList(implantList);
_window.UpdateState(implant);
}
}
}

View File

@@ -0,0 +1,12 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'implanter-set-draw-window'}"
MinSize="5 30">
<BoxContainer Orientation="Vertical" Margin="10 5">
<Label Text="{Loc 'implanter-set-draw-info'}" Margin="0 0 0 5"/>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'implanter-set-draw-type'}" Margin="0 0 5 0"/>
<OptionButton Name="ImplantSelector"/> <!-- Populated in LoadVerbs -->
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -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<string?>? OnImplantChange;
private Dictionary<string, string> _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<string, string> 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;
}
}
}
}

View File

@@ -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)));
}
}
}

View File

@@ -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<ProtoId<LatheCategoryPrototype>>();
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)))

View File

@@ -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!;

View File

@@ -9,6 +9,7 @@
<tabs:KeyRebindTab Name="KeyRebindTab" />
<tabs:AudioTab Name="AudioTab" />
<tabs:AccessibilityTab Name="AccessibilityTab" />
<tabs:AdminOptionsTab Name="AdminOptionsTab" />
<!-- CP14-options-menu-start -->
<options:CP14OptionsMenuMainTab Name="CP14OptionsMenuTab"/>
<!-- CP14-options-menu-end -->

View File

@@ -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();
}
}
}

View File

@@ -7,8 +7,11 @@
<CheckBox Name="ReducedMotionCheckBox" Text="{Loc 'ui-options-reduced-motion'}" />
<CheckBox Name="EnableColorNameCheckBox" Text="{Loc 'ui-options-enable-color-name'}" />
<CheckBox Name="ColorblindFriendlyCheckBox" Text="{Loc 'ui-options-colorblind-friendly'}" />
<ui:OptionSlider Name="ChatWindowOpacitySlider" Title="{Loc 'ui-options-chat-window-opacity'}" />
<ui:OptionSlider Name="ScreenShakeIntensitySlider" Title="{Loc 'ui-options-screen-shake-intensity'}" />
<ui:OptionSlider Name="ChatWindowOpacitySlider" Title="{Loc 'ui-options-chat-window-opacity'}" />
<ui:OptionSlider Name="SpeechBubbleTextOpacitySlider" Title="{Loc 'ui-options-speech-bubble-text-opacity'}" />
<ui:OptionSlider Name="SpeechBubbleSpeakerOpacitySlider" Title="{Loc 'ui-options-speech-bubble-speaker-opacity'}" />
<ui:OptionSlider Name="SpeechBubbleBackgroundOpacitySlider" Title="{Loc 'ui-options-speech-bubble-background-opacity'}" />
</BoxContainer>
</ScrollContainer>
<ui:OptionsTabControlRow Name="Control" Access="Public" />

View File

@@ -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();
}

View File

@@ -0,0 +1,12 @@
<Control xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="clr-namespace:Content.Client.Options.UI">
<BoxContainer Orientation="Vertical">
<ScrollContainer VerticalExpand="True" HScrollEnabled="False">
<BoxContainer Orientation="Vertical" Margin="8">
<CheckBox Name="EnableClassicOverlayCheckBox" Text="{Loc 'ui-options-enable-classic-overlay'}" />
</BoxContainer>
</ScrollContainer>
<ui:OptionsTabControlRow Name="Control" Access="Public" />
</BoxContainer>
</Control>

View File

@@ -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();
}
}

View File

@@ -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)
{

View File

@@ -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<MapGridComponent>(gridUid.Value);
var position = _transform.GetGridOrMapTilePosition(uid);
return HasTrayScanReveal(((EntityUid)gridUid, gridComp), position);
}
private bool HasTrayScanReveal(Entity<MapGridComponent> ent, Vector2i position)
{
var anchoredEnum = _map.GetAnchoredEntities(ent, position);
return anchoredEnum.Any(HasComp<TrayScanRevealComponent>);
}
}

View File

@@ -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<TrayRevealedComponent>(uid);
}
}

View File

@@ -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<ToggleableLi
|| !enabled)
return;
if (!component.ClothingVisuals.TryGetValue(args.Slot, out var layers))
if (!TryComp(args.Equipee, out InventoryComponent? inventory))
return;
List<PrototypeLayerData>? 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<Color>(uid, ToggleableLightVisuals.Color, out var color, appearance);

View File

@@ -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

View File

@@ -74,7 +74,7 @@ public sealed class CloseRecentWindowUIController : UIController
/// internal recentlyInteractedWindows tracking.
/// </summary>
/// <param name="window"></param>
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
}

View File

@@ -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<Storage
[Dependency] private readonly IConfigurationManager _configuration = default!;
[Dependency] private readonly IInputManager _input = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly CloseRecentWindowUIController _closeRecentWindowUIController = default!;
[UISystemDependency] private readonly StorageSystem _storage = default!;
[UISystemDependency] private readonly UserInterfaceSystem _ui = default!;
@@ -98,6 +100,7 @@ public sealed class StorageUIController : UIController, IOnSystemChanged<Storage
if (StaticStorageUIEnabled)
{
UIManager.GetActiveUIWidgetOrNull<HotbarGui>()?.StorageContainer.AddChild(window);
_closeRecentWindowUIController.SetMostRecentlyInteractedWindow(window);
}
else
{

View File

@@ -16,4 +16,9 @@ public sealed partial class VendingMachineItem : BoxContainer
NameLabel.Text = text;
}
public void SetText(string text)
{
NameLabel.Text = text;
}
}

View File

@@ -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<EntProtoId, EntityUid> _dummies = [];
private readonly Dictionary<EntProtoId, (ListContainerButton Button, VendingMachineItem Item)> _listItems = new();
private readonly Dictionary<EntProtoId, uint> _amounts = new();
/// <summary>
/// Whether the vending machine is able to be interacted with or not.
/// </summary>
private bool _enabled;
public event Action<GUIBoundKeyEventArgs, ListData>? 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;
}
/// <summary>
/// Populates the list of available items on the vending machine interface
/// and sets icons based on their prototypes
/// </summary>
public void Populate(List<VendingMachineInventoryEntry> inventory)
public void Populate(List<VendingMachineInventoryEntry> 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);
}
/// <summary>
/// Updates text entries for vending data in place without modifying the list controls.
/// </summary>
public void UpdateAmounts(List<VendingMachineInventoryEntry> 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;
}
}

View File

@@ -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<VendingMachineSystem>();
_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<VendingMachineSystem>();
_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)

View File

@@ -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<VendingMachineComponent, AppearanceChangeEvent>(OnAppearanceChange);
SubscribeLocalEvent<VendingMachineComponent, AnimationCompletedEvent>(OnAnimationCompleted);
SubscribeLocalEvent<VendingMachineComponent, AfterAutoHandleStateEvent>(OnVendingAfterState);
SubscribeLocalEvent<VendingMachineComponent, ComponentHandleState>(OnVendingHandleState);
}
private void OnVendingAfterState(EntityUid uid, VendingMachineComponent component, ref AfterAutoHandleStateEvent args)
private void OnVendingHandleState(Entity<VendingMachineComponent> entity, ref ComponentHandleState args)
{
if (_uiSystem.TryGetOpenUi<VendingMachineBoundUserInterface>(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<VendingMachineBoundUserInterface>(uid, VendingMachineUiKey.Key, out var bui))
{
if (fullUiUpdate)
{
bui.Refresh();
}
else
{
bui.UpdateAmounts();
}
}
}
protected override void UpdateUI(Entity<VendingMachineComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp))
return;
if (UISystem.TryGetOpenUi<VendingMachineBoundUserInterface>(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;

View File

@@ -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<IEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>();
var mapSystem = server.System<SharedMapSystem>();
var protoManager = server.ResolveDependency<IPrototypeManager>();
var cargo = entManager.System<CargoSystem>();
@@ -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<IEntityManager>();
var mapSystem = server.System<SharedMapSystem>();
var mapManager = server.ResolveDependency<IMapManager>();
var protoManager = server.ResolveDependency<IPrototypeManager>();
var componentFactory = server.ResolveDependency<IComponentFactory>();
@@ -207,7 +206,7 @@ public sealed class CargoTest
entManager.DeleteEntity(ent);
}
mapManager.DeleteMap(mapId);
mapSystem.DeleteMap(mapId);
});
await pair.CleanReturnAsync();

View File

@@ -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<IEntityManager>();
var playerMan = server.ResolveDependency<IPlayerManager>();
var mapMan = server.ResolveDependency<IMapManager>();
var mapSystem = server.System<SharedMapSystem>();
var sys = entMan.System<SharedHandsSystem>();
var tSys = entMan.System<TransformSystem>();
@@ -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<IEntityManager>();
var playerMan = server.ResolveDependency<IPlayerManager>();
var mapMan = server.ResolveDependency<IMapManager>();
var mapSystem = server.System<SharedMapSystem>();
var sys = entMan.System<SharedHandsSystem>();
var tSys = entMan.System<TransformSystem>();
var containerSystem = server.System<SharedContainerSystem>();
@@ -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();
}
}

View File

@@ -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<IMapManager>();
var mapSystem = server.System<SharedMapSystem>();
var entityMan = server.ResolveDependency<IEntityManager>();
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();

View File

@@ -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();
}

View File

@@ -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<double> GetSpawnedPrice(Dictionary<string, int> ents)

View File

@@ -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<IMapManager>();
var mapSystem = server.System<SharedMapSystem>();
var prototypeManager = server.ResolveDependency<IPrototypeManager>();
var entityManager = server.ResolveDependency<IEntityManager>();
@@ -59,7 +58,7 @@ namespace Content.IntegrationTests.Tests.Materials
}
});
mapManager.DeleteMap(testMap.MapId);
mapSystem.DeleteMap(testMap.MapId);
});
await pair.CleanReturnAsync();

View File

@@ -81,7 +81,7 @@ public sealed partial class MindTests
var testMap2 = await pair.CreateTestMap();
var entMan = server.ResolveDependency<IServerEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>();
var mapSystem = server.System<SharedMapSystem>();
var playerMan = server.ResolveDependency<IPlayerManager>();
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(() =>

View File

@@ -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 =

View File

@@ -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<IMapManager>();
var mapSystem = server.System<SharedMapSystem>();
var storageSystem = server.System<StorageSystem>();
var protos = server.ProtoMan
@@ -64,7 +63,7 @@ public sealed class StartingGearPrototypeStorageTest
}
}
mapManager.DeleteMap(testMap.MapId);
mapSystem.DeleteMap(testMap.MapId);
});
await pair.CleanReturnAsync();

View File

@@ -55,5 +55,16 @@ namespace Content.Server.Abilities.Mime
[DataField]
public ProtoId<AlertPrototype> VowBrokenAlert = "VowBroken";
/// <summary>
/// Does this component prevent the mime from writing on paper while their vow is active?
/// </summary>
[DataField]
public bool PreventWriting = false;
/// <summary>
/// What message is displayed when the mime fails to write?
/// </summary>
[DataField]
public LocId FailWriteMessage = "paper-component-illiterate-mime";
}
}

View File

@@ -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<MutedComponent>(uid);
if (component.PreventWriting)
{
EnsureComp<BlockWritingComponent>(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<MutedComponent>(uid);
if (mimePowers.PreventWriting)
RemComp<BlockWritingComponent>(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<MutedComponent>(uid);
if (mimePowers.PreventWriting)
{
EnsureComp<BlockWritingComponent>(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);

View File

@@ -31,7 +31,7 @@ public sealed class IdCardSystem : SharedIdCardSystem
private void OnMicrowaved(EntityUid uid, IdCardComponent component, BeingMicrowavedEvent args)
{
if (!component.CanMicrowave || !TryComp<MicrowaveComponent>(args.Microwave, out var micro) || micro.Broken)
return;
return;
if (TryComp<AccessComponent>(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<AccessLevelPrototype>().ToArray());
var ids = _prototypeManager.EnumeratePrototypes<AccessLevelPrototype>().Where(x => x.CanAddToIdCard).ToArray();
if (ids.Length == 0)
return;
var random = _random.Pick(ids);
access.Tags.Add(random.ID);
Dirty(uid, access);

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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<TraitorRuleComponent>(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<ZombieRuleComponent>(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<NukeopsRuleComponent>(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<RevolutionaryRuleComponent>(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<ThiefRuleComponent>(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);
*/

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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<SpeakOnUIClosedComponent?> entity, bool value = true)
{
if (!Resolve(entity, ref entity.Comp))
return false;
entity.Comp.Flag = value;
return true;
}
}

View File

@@ -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!;
/// <summary>
/// Dictionary of alert levels. Keyed by string - the string key is the most important

View File

@@ -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.
/// </summary>
[DataRecord]
public record struct AntagObjectiveSet()
public partial record struct AntagObjectiveSet()
{
/// <summary>
/// The grouping used by the objective system to pick random objectives.

View File

@@ -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;

View File

@@ -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<SpaceVillainArcadeComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<SpaceVillainArcadeComponent, AfterActivatableUIOpenEvent>(OnAfterUIOpenSV);
SubscribeLocalEvent<SpaceVillainArcadeComponent, SpaceVillainArcadePlayerActionMessage>(OnSVPlayerAction);
SubscribeLocalEvent<SpaceVillainArcadeComponent, SharedSpaceVillainArcadeComponent.SpaceVillainArcadePlayerActionMessage>(OnSVPlayerAction);
SubscribeLocalEvent<SpaceVillainArcadeComponent, PowerChangedEvent>(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<SpeakOnUIClosedComponent>(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<ApcPowerReceiverComponent>(uid, out var power) && power.Powered)
return;
_uiSystem.CloseUi(uid, SpaceVillainArcadeUiKey.Key);
_uiSystem.CloseUi(uid, SharedSpaceVillainArcadeComponent.SpaceVillainArcadeUiKey.Key);
}
}

View File

@@ -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;

View File

@@ -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
{
/// <summary>
/// Default sound when bible hits somebody.
/// </summary>
private static readonly ProtoId<SoundCollectionPrototype> DefaultBibleHit = new("BibleHit");
/// <summary>
/// Sound to play when bible hits somebody.
/// </summary>
[DataField]
public SoundSpecifier BibleHitSound = new SoundCollectionSpecifier(DefaultBibleHit, AudioParams.Default.WithVolume(-4f));
/// <summary>
/// Damage that will be healed on a success
/// </summary>

View File

@@ -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
{
/// <summary>
/// Default sound to play when entity is summoned.
/// </summary>
private static readonly ProtoId<SoundCollectionPrototype> DefaultSummonSound = new("Summon");
/// <summary>
/// Sound to play when entity is summoned.
/// </summary>
[DataField]
public SoundSpecifier SummonSound = new SoundCollectionSpecifier(DefaultSummonSound, AudioParams.Default.WithVolume(-4f));
/// <summary>
/// Used for a special item only the Chaplain can summon. Usually a mob, but supports regular items too.
/// </summary>

View File

@@ -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<DnaComponent>(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");
}
/// <summary>
@@ -502,13 +493,10 @@ public sealed class BloodstreamSystem : EntitySystem
var bloodData = new List<ReagentData>();
var dnaData = new DnaData();
if (TryComp<DnaComponent>(uid, out var donorComp))
{
if (TryComp<DnaComponent>(uid, out var donorComp) && donorComp.DNA != null)
dnaData.DNA = donorComp.DNA;
} else
{
else
dnaData.DNA = Loc.GetString("forensics-dna-unknown");
}
bloodData.Add(dnaData);

View File

@@ -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

View File

@@ -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;

View File

@@ -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
{
/// <summary>
/// The name of the scanned entity, sent to clients when they open the UI.
/// </summary>
[DataField]
public string EntityName = string.Empty;
/// <summary>
/// The list of pulled access logs
/// </summary>
@@ -18,4 +27,25 @@ public sealed partial class LogProbeCartridgeComponent : Component
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier SoundScan = new SoundPathSpecifier("/Audio/Machines/scan_finish.ogg");
/// <summary>
/// Paper to spawn when printing logs.
/// </summary>
[DataField]
public EntProtoId<PaperComponent> PaperPrototype = "PaperAccessLogs";
[DataField]
public SoundSpecifier PrintSound = new SoundPathSpecifier("/Audio/Machines/diagnoser_printing.ogg");
/// <summary>
/// How long you have to wait before printing logs again.
/// </summary>
[DataField]
public TimeSpan PrintCooldown = TimeSpan.FromSeconds(5);
/// <summary>
/// When anyone is allowed to spawn another printout.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
public TimeSpan NextPrintAllowed = TimeSpan.Zero;
}

View File

@@ -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<LogProbeCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady);
SubscribeLocalEvent<LogProbeCartridgeComponent, CartridgeAfterInteractEvent>(AfterInteract);
SubscribeLocalEvent<LogProbeCartridgeComponent, CartridgeMessageEvent>(OnMessage);
}
/// <summary>
@@ -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<LogProbeCartridgeComponent> ent, ref CartridgeMessageEvent args)
{
if (args is LogProbePrintMessage cast)
PrintLogs(ent, cast.User);
}
private void PrintLogs(Entity<LogProbeCartridgeComponent> 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<PaperComponent>(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<LogProbeCartridgeComponent> 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);
}
}

View File

@@ -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;

View File

@@ -53,6 +53,7 @@ namespace Content.Server.Chemistry.EntitySystems
SubscribeLocalEvent<ChemMasterComponent, BoundUIOpenedEvent>(SubscribeUpdateUiState);
SubscribeLocalEvent<ChemMasterComponent, ChemMasterSetModeMessage>(OnSetModeMessage);
SubscribeLocalEvent<ChemMasterComponent, ChemMasterSortingTypeCycleMessage>(OnCycleSortingTypeMessage);
SubscribeLocalEvent<ChemMasterComponent, ChemMasterSetPillTypeMessage>(OnSetPillTypeMessage);
SubscribeLocalEvent<ChemMasterComponent, ChemMasterReagentAmountButtonMessage>(OnReagentButtonMessage);
SubscribeLocalEvent<ChemMasterComponent, ChemMasterCreatePillsMessage>(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<ChemMasterComponent> 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<ChemMasterComponent> chemMaster, ref ChemMasterSetPillTypeMessage message)
{
// Ensure valid pill type. There are 20 pills selectable, 0-19.

View File

@@ -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();
}
}

View File

@@ -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)}.");
}

View File

@@ -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<MindComponent, EntityUid> ClonesWaitingForMind = new();
public readonly ProtoId<CloningSettingsPrototype> SettingsId = "CloningPod";
public const float EasyModeCloningCost = 0.7f;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
SubscribeLocalEvent<BeingClonedComponent, MindAddedMessage>(HandleMindAdded);
SubscribeLocalEvent<CloningPodComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<CloningPodComponent, PortDisconnectedEvent>(OnPortDisconnected);
SubscribeLocalEvent<CloningPodComponent, AnchorStateChangedEvent>(OnAnchor);
SubscribeLocalEvent<CloningPodComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<CloningPodComponent, GotEmaggedEvent>(OnEmagged);
}
private void OnComponentInit(Entity<CloningPodComponent> ent, ref ComponentInit args)
{
ent.Comp.BodyContainer = _containerSystem.EnsureContainer<ContainerSlot>(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<MindContainerComponent>(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<CloningPodComponent>(clonedComponent.Parent, out var cloningPodComponent) ||
uid != cloningPodComponent.BodyContainer.ContainedEntity)
{
EntityManager.RemoveComponent<BeingClonedComponent>(uid);
return;
}
UpdateStatus(clonedComponent.Parent, CloningPodStatus.Cloning, cloningPodComponent);
}
private void OnPortDisconnected(Entity<CloningPodComponent> ent, ref PortDisconnectedEvent args)
{
ent.Comp.ConnectedConsole = null;
}
private void OnAnchor(Entity<CloningPodComponent> ent, ref AnchorStateChangedEvent args)
{
if (ent.Comp.ConnectedConsole == null || !TryComp<CloningConsoleComponent>(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<CloningPodComponent> 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<MindComponent> mindEnt, CloningPodComponent? clonePod, float failChanceModifier = 1)
{
if (!Resolve(uid, ref clonePod))
return false;
if (HasComp<ActiveCloningPodComponent>(uid))
return false;
var mind = mindEnt.Comp;
if (ClonesWaitingForMind.TryGetValue(mind, out var clone))
{
if (EntityManager.EntityExists(clone) &&
!_mobStateSystem.IsDead(clone) &&
TryComp<MindContainerComponent>(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<PhysicsComponent>(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<DamageableComponent>(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<ActiveCloningPodComponent>(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<BeingClonedComponent>(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<ActiveCloningPodComponent>(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<ActiveCloningPodComponent, CloningPodComponent>();
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);
}
}
/// <summary>
/// On emag, spawns a failed clone when cloning process fails which attacks nearby crew.
/// </summary>
private void OnEmagged(Entity<CloningPodComponent> 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<BeingClonedComponent>(entity);
_containerSystem.Remove(entity, clonePod.BodyContainer);
clonePod.CloningProgress = 0f;
clonePod.UsedBiomass = 0;
UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
RemCompDeferred<ActiveCloningPodComponent>(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<EmaggedComponent>(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<EmaggedComponent>(uid))
{
_material.SpawnMultipleFromMaterial(_robustRandom.Next(1, (int)(clonePod.UsedBiomass / 2.5)), clonePod.RequiredMaterial, Transform(uid).Coordinates);
}
clonePod.UsedBiomass = 0;
RemCompDeferred<ActiveCloningPodComponent>(uid);
}
public void Reset(RoundRestartCleanupEvent ev)
{
ClonesWaitingForMind.Clear();
}
}

View File

@@ -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;
/// <summary>
/// System responsible for making a copy of a humanoid's body.
/// For the cloning machines themselves look at CloningPodSystem, CloningConsoleSystem and MedicalScannerSystem instead.
/// </summary>
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!;
/// <summary>
/// Spawns a clone of the given humanoid mob at the specified location or in nullspace.
/// </summary>
public bool TryCloning(EntityUid original, MapCoordinates? coords, ProtoId<CloningSettingsPrototype> 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<MindComponent, EntityUid> ClonesWaitingForMind = new();
public const float EasyModeCloningCost = 0.7f;
if (!TryComp<HumanoidAppearanceComponent>(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<StatusEffectsComponent>(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<CloningPodComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
SubscribeLocalEvent<BeingClonedComponent, MindAddedMessage>(HandleMindAdded);
SubscribeLocalEvent<CloningPodComponent, PortDisconnectedEvent>(OnPortDisconnected);
SubscribeLocalEvent<CloningPodComponent, AnchorStateChangedEvent>(OnAnchor);
SubscribeLocalEvent<CloningPodComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<CloningPodComponent, GotEmaggedEvent>(OnEmagged);
}
private void OnComponentInit(EntityUid uid, CloningPodComponent clonePod, ComponentInit args)
{
clonePod.BodyContainer = _containerSystem.EnsureContainer<ContainerSlot>(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<MindContainerComponent>(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<CloningPodComponent>(clonedComponent.Parent, out var cloningPodComponent) ||
uid != cloningPodComponent.BodyContainer.ContainedEntity)
if (!_componentFactory.TryGetRegistration(componentName, out var componentRegistration))
{
EntityManager.RemoveComponent<BeingClonedComponent>(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<CloningConsoleComponent>(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<MindComponent> mindEnt, CloningPodComponent? clonePod, float failChanceModifier = 1)
{
if (!Resolve(uid, ref clonePod))
return false;
if (HasComp<ActiveCloningPodComponent>(uid))
return false;
var mind = mindEnt.Comp;
if (ClonesWaitingForMind.TryGetValue(mind, out var clone))
{
if (EntityManager.EntityExists(clone) &&
!_mobStateSystem.IsDead(clone) &&
TryComp<MindContainerComponent>(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<HumanoidAppearanceComponent>(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<PhysicsComponent>(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<DamageableComponent>(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<ActiveCloningPodComponent>(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<BeingClonedComponent>(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<ActiveCloningPodComponent>(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<ActiveCloningPodComponent, CloningPodComponent>();
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);
}
}
/// <summary>
/// On emag, spawns a failed clone when cloning process fails which attacks nearby crew.
/// </summary>
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<NameModifierComponent>(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<NameModifierComponent>(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;
}
/// <summary>
/// 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!
/// </summary>
public void CopyEquipment(EntityUid original, EntityUid clone, SlotFlags slotFlags, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
{
if (!TryComp<InventoryComponent>(original, out var originalInventory) || !TryComp<InventoryComponent>(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<BeingClonedComponent>(entity);
_containerSystem.Remove(entity, clonePod.BodyContainer);
clonePod.CloningProgress = 0f;
clonePod.UsedBiomass = 0;
UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
RemCompDeferred<ActiveCloningPodComponent>(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<ActiveCloningPodComponent>(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);
}
}
}

View File

@@ -0,0 +1,17 @@
using Content.Shared.Cloning;
using Robust.Shared.Prototypes;
namespace Content.Server.Cloning.Components;
/// <summary>
/// This is added to a marker entity in order to spawn a clone of a random player.
/// </summary>
[RegisterComponent, EntityCategory("Spawner")]
public sealed partial class RandomCloneSpawnerComponent : Component
{
/// <summary>
/// Cloning settings to be used.
/// </summary>
[DataField]
public ProtoId<CloningSettingsPrototype> Settings = "BaseClone";
}

View File

@@ -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;
/// <summary>
/// This deals with spawning and setting up a clone of a random crew member.
/// </summary>
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<RandomCloneSpawnerComponent, MapInitEvent>(OnMapInit);
}
private void OnMapInit(Entity<RandomCloneSpawnerComponent> 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 _);
}
}

View File

@@ -18,25 +18,25 @@ namespace Content.Server.Connection.Whitelist;
/// If the condition doesn't match, the next condition is checked.
/// </summary>
[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!;
/// <summary>
/// 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.
/// </summary>
[DataField]
public int MinimumPlayers { get; } = 0;
public int MinimumPlayers = 0;
/// <summary>
/// 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.
/// </summary>
[DataField]
public int MaximumPlayers { get; } = int.MaxValue;
public int MaximumPlayers = int.MaxValue;
[DataField]
public WhitelistCondition[] Conditions { get; } = default!;
public WhitelistCondition[] Conditions = default!;
}

View File

@@ -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)}");
}
}
/// <summary>
/// Sent before the insertion is made.
/// Allows preventing the insertion if any system on the entity should need to.
/// </summary>
[ByRefEvent]
public record struct BeforeThrowInsertEvent(EntityUid ThrownEntity, bool Cancelled = false);

View File

@@ -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<DisposalUnitComponent, DisposalDoAfterEvent>(OnDoAfter);
SubscribeLocalEvent<DisposalUnitComponent, BeforeThrowInsertEvent>(OnThrowInsert);
SubscribeLocalEvent<DisposalUnitComponent, SharedDisposalUnitComponent.UiButtonPressedMessage>(OnUiButtonPressed);
}
@@ -195,6 +198,12 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
args.Handled = true;
}
private void OnThrowInsert(Entity<DisposalUnitComponent> 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))

View File

@@ -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
}
}
}
/// <summary>
/// Raised when a voice trigger is activated, containing the message that triggered it.
/// </summary>
/// <param name="Source"> The EntityUid of the entity sending the message</param>
/// <param name="Message"> The contents of the message</param>
[ByRefEvent]
public readonly record struct VoiceTriggeredEvent(EntityUid Source, string? Message);

View File

@@ -1,12 +0,0 @@
namespace Content.Server.Forensics
{
/// <summary>
/// This component is for mobs that leave fingerprints.
/// </summary>
[RegisterComponent]
public sealed partial class FingerprintComponent : Component
{
[DataField("fingerprint"), ViewVariables(VVAccess.ReadWrite)]
public string? Fingerprint;
}
}

View File

@@ -1,10 +0,0 @@
namespace Content.Server.Forensics
{
/// <summary>
/// This component stops the entity from leaving finger prints,
/// usually so fibres can be left instead.
/// </summary>
[RegisterComponent]
public sealed partial class FingerprintMaskComponent : Component
{}
}

View File

@@ -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;

View File

@@ -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<FingerprintComponent, ContactInteractionEvent>(OnInteract);
SubscribeLocalEvent<FingerprintComponent, MapInitEvent>(OnFingerprintInit);
SubscribeLocalEvent<DnaComponent, MapInitEvent>(OnDNAInit);
SubscribeLocalEvent<FingerprintComponent, MapInitEvent>(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<DnaComponent, MapInitEvent>(OnDNAInit, after: new[] { typeof(BloodstreamSystem) });
SubscribeLocalEvent<ForensicsComponent, BeingGibbedEvent>(OnBeingGibbed);
SubscribeLocalEvent<ForensicsComponent, MeleeHitEvent>(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<FingerprintComponent> 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<DnaComponent> 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<DnaComponent>(hitEntity, out var hitEntityComp))
if (TryComp<DnaComponent>(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<ForensicsComponent>(args.Recipient);
recipientComp.DNAs.Add(component.DNA);
recipientComp.CanDnaBeCleaned = args.CanDnaBeCleaned;
@@ -307,6 +315,36 @@ namespace Content.Server.Forensics
#region Public API
/// <summary>
/// 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.
/// </summary>
public void RandomizeDNA(Entity<DnaComponent?> 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);
}
/// <summary>
/// Give the entity a new, random fingerprint string.
/// Does nothing if it does not have the FingerprintComponent.
/// </summary>
public void RandomizeFingerprint(Entity<FingerprintComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))
return;
ent.Comp.Fingerprint = GenerateFingerprint();
Dirty(ent);
}
/// <summary>
/// Transfer DNA from one entity onto the forensics of another
/// </summary>
@@ -315,7 +353,7 @@ namespace Content.Server.Forensics
/// <param name="canDnaBeCleaned">If this DNA be cleaned off of the recipient. e.g. cleaning a knife vs cleaning a puddle of blood</param>
public void TransferDna(EntityUid recipient, EntityUid donor, bool canDnaBeCleaned = true)
{
if (TryComp<DnaComponent>(donor, out var donorComp))
if (TryComp<DnaComponent>(donor, out var donorComp) && donorComp.DNA != null)
{
EnsureComp<ForensicsComponent>(recipient, out var recipientComp);
recipientComp.DNAs.Add(donorComp.DNA);

View File

@@ -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;

View File

@@ -6,7 +6,7 @@ namespace Content.Server.Ghost.Roles.Raffles;
/// Allows getting a <see cref="IGhostRoleRaffleDecider"/> as prototype.
/// </summary>
[Prototype("ghostRoleRaffleDecider")]
public sealed class GhostRoleRaffleDeciderPrototype : IPrototype
public sealed partial class GhostRoleRaffleDeciderPrototype : IPrototype
{
/// <inheritdoc />
[IdDataField]

View File

@@ -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);

View File

@@ -68,8 +68,6 @@ public sealed partial class ImplanterSystem : SharedImplanterSystem
args.Handled = true;
}
/// <summary>
/// Attempt to implant someone else.
/// </summary>

View File

@@ -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<DnaComponent>(ent, out var dna))
{
dna.DNA = _forensicsSystem.GenerateDNA();
var ev = new GenerateDnaEvent { Owner = ent, DNA = dna.DNA };
RaiseLocalEvent(ent, ref ev);
}
if (TryComp<FingerprintComponent>(ent, out var fingerprint))
{
fingerprint.Fingerprint = _forensicsSystem.GenerateFingerprint();
}
RemComp<DetailExaminableComponent>(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<DetailExaminableComponent>(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);
}

View File

@@ -1,5 +1,3 @@
using Content.Shared.Kitchen;
namespace Content.Server.Kitchen.Components;
/// <summary>
@@ -8,4 +6,9 @@ namespace Content.Server.Kitchen.Components;
[RegisterComponent]
public sealed partial class ActivelyMicrowavedComponent : Component
{
/// <summary>
/// The microwave this entity is actively being microwaved by.
/// </summary>
[DataField]
public EntityUid? Microwave;
}

View File

@@ -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<ActiveMicrowaveComponent, EntRemovedFromContainerMessage>(OnActiveMicrowaveRemove);
SubscribeLocalEvent<ActivelyMicrowavedComponent, OnConstructionTemperatureEvent>(OnConstructionTemp);
SubscribeLocalEvent<ActivelyMicrowavedComponent, SolutionRelayEvent<ReactionAttemptEvent>>(OnReactionAttempt);
SubscribeLocalEvent<FoodRecipeProviderComponent, GetSecretRecipesEvent>(OnGetSecretRecipes);
}
@@ -126,7 +128,8 @@ namespace Content.Server.Kitchen.EntitySystems
private void OnActiveMicrowaveInsert(Entity<ActiveMicrowaveComponent> ent, ref EntInsertedIntoContainerMessage args)
{
AddComp<ActivelyMicrowavedComponent>(args.Entity);
var microwavedComp = AddComp<ActivelyMicrowavedComponent>(args.Entity);
microwavedComp.Microwave = ent.Owner;
}
private void OnActiveMicrowaveRemove(Entity<ActiveMicrowaveComponent> ent, ref EntRemovedFromContainerMessage args)
@@ -134,10 +137,33 @@ namespace Content.Server.Kitchen.EntitySystems
EntityManager.RemoveComponentDeferred<ActivelyMicrowavedComponent>(args.Entity);
}
// Stop items from transforming through constructiongraphs while being microwaved.
// They might be reserved for a microwave recipe.
private void OnConstructionTemp(Entity<ActivelyMicrowavedComponent> 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<ActivelyMicrowavedComponent> ent, ref SolutionRelayEvent<ReactionAttemptEvent> args)
{
if (!TryComp<ActiveMicrowaveComponent>(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;
}
}
}
/// <summary>
@@ -176,33 +202,29 @@ namespace Content.Server.Kitchen.EntitySystems
// this is spaghetti ngl
foreach (var item in component.Storage.ContainedEntities)
{
if (!TryComp<SolutionContainerManagerComponent>(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<ActivelyMicrowavedComponent>(item);
var microwavedComp = AddComp<ActivelyMicrowavedComponent>(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<SolutionContainerManagerComponent>(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;
}
}

View File

@@ -69,7 +69,7 @@ public sealed class MaterialStorageSystem : SharedMaterialStorageSystem
if (material.StackEntity != null)
{
if (!_prototypeManager.Index<EntityPrototype>(material.StackEntity).TryGetComponent<PhysicalCompositionComponent>(out var composition))
if (!_prototypeManager.Index<EntityPrototype>(material.StackEntity).TryGetComponent<PhysicalCompositionComponent>(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<EntityUid>();
var entProto = _prototypeManager.Index<EntityPrototype>(materialProto.StackEntity);
if (!entProto.TryGetComponent<PhysicalCompositionComponent>(out var composition))
if (!entProto.TryGetComponent<PhysicalCompositionComponent>(out var composition, EntityManager.ComponentFactory))
return new List<EntityUid>();
var materialPerStack = composition.MaterialComposition[materialProto.ID];

View File

@@ -31,6 +31,12 @@ namespace Content.Server.Medical
[Dependency] private readonly ForensicsSystem _forensics = default!;
[Dependency] private readonly BloodstreamSystem _bloodstream = default!;
[ValidatePrototypeId<SoundCollectionPrototype>]
private const string VomitCollection = "Vomit";
private readonly SoundSpecifier _vomitSound = new SoundCollectionSpecifier(VomitCollection,
AudioParams.Default.WithVariation(0.2f).WithVolume(-4f));
/// <summary>
/// Make an entity vomit, if they have a stomach.
/// </summary>
@@ -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);
}
}

View File

@@ -40,6 +40,13 @@ public sealed partial class NPCRangedCombatComponent : Component
[ViewVariables(VVAccess.ReadWrite)]
public bool TargetInLOS = false;
/// <summary>
/// If true, only opaque objects will block line of sight.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
// ReSharper disable once InconsistentNaming
public bool UseOpaqueForLOSChecks = false;
/// <summary>
/// Delay after target is in LOS before we start shooting.
/// </summary>

View File

@@ -10,7 +10,7 @@ public sealed partial class HTNComponent : NPCComponent
/// The base task to use for planning
/// </summary>
[ViewVariables(VVAccess.ReadWrite),
DataField("rootTask", required: true)]
DataField("rootTask", required: true)]
public HTNCompoundTask RootTask = default!;
/// <summary>
@@ -47,4 +47,10 @@ public sealed partial class HTNComponent : NPCComponent
/// Is this NPC currently planning?
/// </summary>
[ViewVariables] public bool Planning => PlanningJob != null;
/// <summary>
/// Determines whether plans should be made / updated for this entity
/// </summary>
[DataField]
public bool Enabled = true;
}

View File

@@ -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<HTNBranch> Branches = new();

View File

@@ -133,6 +133,39 @@ public sealed class HTNSystem : EntitySystem
component.PlanningJob = null;
}
/// <summary>
/// Enable / disable the hierarchical task network of an entity
/// </summary>
/// <param name="ent">The entity and its <see cref="HTNComponent"/></param>
/// <param name="state">Set 'true' to enable, or 'false' to disable, the HTN</param>
/// <param name="planCooldown">Specifies a time in seconds before the entity can start planning a new action (only takes effect when the HTN is enabled)</param>
// ReSharper disable once InconsistentNaming
[PublicAPI]
public void SetHTNEnabled(Entity<HTNComponent> 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);
}
/// <summary>
/// Forces the NPC to replan.
/// </summary>
@@ -147,12 +180,15 @@ public sealed class HTNSystem : EntitySystem
_planQueue.Process();
var query = EntityQueryEnumerator<ActiveNPCComponent, HTNComponent>();
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)

View File

@@ -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<float>(RangeKey, _entManager);
var collisionGroup = UseOpaqueForLOSChecksKey ? CollisionGroup.Opaque : (CollisionGroup.Impassable | CollisionGroup.InteractImpassable);
return _interaction.InRangeUnobstructed(owner, target, range);
return _interaction.InRangeUnobstructed(owner, target, range, collisionGroup);
}
}

View File

@@ -33,6 +33,12 @@ public sealed partial class GunOperator : HTNOperator, IHtnConditionalShutdown
[DataField("requireLOS")]
public bool RequireLOS = false;
/// <summary>
/// If true, only opaque objects will block line of sight.
/// </summary>
[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<string, object>? 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<NPCRangedCombatComponent>(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner));
ranged.Target = blackboard.GetValue<EntityUid>(TargetKey);
ranged.UseOpaqueForLOSChecks = UseOpaqueForLOSChecks;
if (blackboard.TryGetValue<float>(NPCBlackboard.RotateSpeed, out var rotSpeed, _entManager))
{

View File

@@ -0,0 +1,12 @@
namespace Content.Server.NPC.Queries.Considerations;
/// <summary>
/// Returns 0f if the NPC has a <see cref="TurretTargetSettingsComponent"/> and the
/// target entity is exempt from being targeted, otherwise it returns 1f.
/// See <see cref="TurretTargetSettingsSystem.EntityIsTargetForTurret"/>
/// for further details on turret target validation.
/// </summary>
public sealed partial class TurretTargetingCon : UtilityConsideration
{
}

Some files were not shown because too many files have changed in this diff Show More