Compare commits
138 Commits
ed-07-03-2
...
ed-2025-02
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f20828b1bb | ||
|
|
469bb1a9ec | ||
|
|
ceff2bea00 | ||
|
|
02d3595faa | ||
|
|
ce7bb813d2 | ||
|
|
51754f09ce | ||
|
|
98cca7b0f8 | ||
|
|
7ea586cc1c | ||
|
|
a8ebcac5c9 | ||
|
|
ba1504d0d6 | ||
|
|
d9e86b3e81 | ||
|
|
66e926843f | ||
|
|
0563e0d67e | ||
|
|
a926c53979 | ||
|
|
bb0c4c66fa | ||
|
|
9c970d203d | ||
|
|
a54960eb81 | ||
|
|
5bdc93b102 | ||
|
|
deea33a36a | ||
|
|
10c868011e | ||
|
|
e8c812f90f | ||
|
|
212e942d21 | ||
|
|
5169ad4e8f | ||
|
|
e540f9cd50 | ||
|
|
aa05cbb49b | ||
|
|
1b20121114 | ||
|
|
6b84315928 | ||
|
|
30a6ebdb26 | ||
|
|
01e4029a11 | ||
|
|
8ea888d821 | ||
|
|
0c6db4db18 | ||
|
|
4442d5e277 | ||
|
|
d8838e31d5 | ||
|
|
38d72434fb | ||
|
|
6b6ac9ac53 | ||
|
|
1047e32944 | ||
|
|
cec05d697e | ||
|
|
3c20f63292 | ||
|
|
439e1c6dc0 | ||
|
|
41c51e2905 | ||
|
|
5fdf702e3c | ||
|
|
c7b9a76342 | ||
|
|
19c23682b0 | ||
|
|
2db57f11ac | ||
|
|
3f014d2b77 | ||
|
|
7520d8a2c8 | ||
|
|
4d0e63caeb | ||
|
|
f80f305d1d | ||
|
|
e705d04a12 | ||
|
|
a5aab8b8a1 | ||
|
|
0c6081fe10 | ||
|
|
3127f73c48 | ||
|
|
53dc27cb1e | ||
|
|
c20fb21ac1 | ||
|
|
7b0b401312 | ||
|
|
9a12bfd4e8 | ||
|
|
1c62e335b9 | ||
|
|
7351a9d1bd | ||
|
|
f850b69e89 | ||
|
|
16c377a05b | ||
|
|
9d62e8c0e7 | ||
|
|
3557be1cff | ||
|
|
7bd98b9075 | ||
|
|
6c3dbbccfe | ||
|
|
c698b163b6 | ||
|
|
8f164cffe4 | ||
|
|
058d9fec09 | ||
|
|
7c6028bc80 | ||
|
|
7283f9b6dc | ||
|
|
e86770f5a0 | ||
|
|
08a274dc28 | ||
|
|
68de58eb66 | ||
|
|
6ea742d4b4 | ||
|
|
11dd26e08e | ||
|
|
6e269c65d8 | ||
|
|
f65ff0bd37 | ||
|
|
d949feeacf | ||
|
|
9615bc64f8 | ||
|
|
9f4a4b81ac | ||
|
|
f165223a5e | ||
|
|
ab9c78b066 | ||
|
|
92006deede | ||
|
|
263f915671 | ||
|
|
183ea1043b | ||
|
|
7ddad07118 | ||
|
|
594811a686 | ||
|
|
afe83e1231 | ||
|
|
8265fb215b | ||
|
|
7f67ff4b26 | ||
|
|
e761ab5815 | ||
|
|
920df98f76 | ||
|
|
1e435822c7 | ||
|
|
309d21bb4c | ||
|
|
285decd734 | ||
|
|
5eeba30211 | ||
|
|
237df1c9a1 | ||
|
|
02f5015830 | ||
|
|
52df2dbe15 | ||
|
|
1a76e4fd52 | ||
|
|
2958706e04 | ||
|
|
004e54af51 | ||
|
|
ebc1bff4cb | ||
|
|
c899ae7649 | ||
|
|
08bc8436a5 | ||
|
|
4d72a2d5f3 | ||
|
|
16787a0281 | ||
|
|
e22c3b1eeb | ||
|
|
5fbe217db3 | ||
|
|
059c64a75f | ||
|
|
363eec1465 | ||
|
|
d1415d9dcb | ||
|
|
7fc8dcb811 | ||
|
|
670791ac49 | ||
|
|
51104a7316 | ||
|
|
bb110b376e | ||
|
|
02f0190c35 | ||
|
|
45e7891706 | ||
|
|
22398ea342 | ||
|
|
0148c441e6 | ||
|
|
615d548021 | ||
|
|
969e7bdd39 | ||
|
|
c71e6e67aa | ||
|
|
88308356db | ||
|
|
c3784a3005 | ||
|
|
f4fab85e34 | ||
|
|
6fa4767b4c | ||
|
|
ac9c8b8275 | ||
|
|
5385683b7e | ||
|
|
05de5bd3eb | ||
|
|
6f925dd610 | ||
|
|
0d84d25067 | ||
|
|
b7c86cae71 | ||
|
|
fa73217b52 | ||
|
|
91f2c46f56 | ||
|
|
1404095f27 | ||
|
|
9fb5517afa | ||
|
|
fe69de942f | ||
|
|
5794ecd28f |
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
using Content.Shared.Advertise.Systems;
|
||||
|
||||
namespace Content.Client.Advertise.Systems;
|
||||
|
||||
public sealed class SpeakOnUIClosedSystem : SharedSpeakOnUIClosedSystem;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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++)
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
35
Content.Client/Implants/UI/DeimplantBoundUserInterface.cs
Normal file
35
Content.Client/Implants/UI/DeimplantBoundUserInterface.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
12
Content.Client/Implants/UI/DeimplantChoiceWindow.xaml
Normal file
12
Content.Client/Implants/UI/DeimplantChoiceWindow.xaml
Normal 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>
|
||||
53
Content.Client/Implants/UI/DeimplantChoiceWindow.xaml.cs
Normal file
53
Content.Client/Implants/UI/DeimplantChoiceWindow.xaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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!;
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
12
Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml
Normal file
12
Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml
Normal 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>
|
||||
20
Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml.cs
Normal file
20
Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
29
Content.Client/SubFloor/TrayScanRevealSystem.cs
Normal file
29
Content.Client/SubFloor/TrayScanRevealSystem.cs
Normal 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>);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -16,4 +16,9 @@ public sealed partial class VendingMachineItem : BoxContainer
|
||||
|
||||
NameLabel.Text = text;
|
||||
}
|
||||
|
||||
public void SetText(string text)
|
||||
{
|
||||
NameLabel.Text = text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(() =>
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)}.");
|
||||
}
|
||||
|
||||
|
||||
323
Content.Server/Cloning/CloningPodSystem.cs
Normal file
323
Content.Server/Cloning/CloningPodSystem.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
47
Content.Server/Cloning/RandomCloneSpawnerSystem.cs
Normal file
47
Content.Server/Cloning/RandomCloneSpawnerSystem.cs
Normal 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 _);
|
||||
}
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -68,8 +68,6 @@ public sealed partial class ImplanterSystem : SharedImplanterSystem
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to implant someone else.
|
||||
/// </summary>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user