mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-10-09 18:31:17 +03:00
Stable release (#41759)
This commit is contained in:
commit
db366bc20d
491 changed files with 139984 additions and 177663 deletions
8
.github/CODEOWNERS
vendored
8
.github/CODEOWNERS
vendored
|
|
@ -36,12 +36,4 @@
|
|||
/Pow3r/ @PJB3005
|
||||
/Content.Server/Power/Pow3r/ @PJB3005
|
||||
|
||||
|
||||
# Sloth
|
||||
/Content.*/Audio @metalgearsloth
|
||||
/Content.*/Movement @metalgearsloth
|
||||
/Content.*/NPC @metalgearsloth
|
||||
/Content.*/Shuttles @metalgearsloth
|
||||
/Content.*/Weapons @metalgearsloth
|
||||
|
||||
/Content.Server/Discord/ @Simyon264
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
using Content.Shared.Administration.Components;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Client.Administration.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class KillSignComponent : SharedKillSignComponent;
|
||||
7
Content.Client/Administration/Systems/BufferingSystem.cs
Normal file
7
Content.Client/Administration/Systems/BufferingSystem.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
using Content.Shared.Administration.Systems;
|
||||
|
||||
namespace Content.Client.Administration.Systems;
|
||||
|
||||
public sealed class BufferingSystem : SharedBufferingSystem
|
||||
{
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
using System.Numerics;
|
||||
using Content.Client.Administration.Components;
|
||||
using Content.Shared.Administration.Components;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using Content.Client.Atmos.Components;
|
|||
using Content.Client.Atmos.EntitySystems;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Atmos.EntitySystems;
|
||||
using Content.Shared.Atmos.Prototypes;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
|
|
@ -23,6 +24,7 @@ namespace Content.Client.Atmos.Overlays
|
|||
|
||||
private readonly IEntityManager _entManager;
|
||||
private readonly IMapManager _mapManager;
|
||||
private readonly SharedAtmosphereSystem _atmosphereSystem;
|
||||
private readonly SharedMapSystem _mapSystem;
|
||||
private readonly SharedTransformSystem _xformSys;
|
||||
|
||||
|
|
@ -54,6 +56,7 @@ namespace Content.Client.Atmos.Overlays
|
|||
{
|
||||
_entManager = entManager;
|
||||
_mapManager = IoCManager.Resolve<IMapManager>();
|
||||
_atmosphereSystem = entManager.System<SharedAtmosphereSystem>();
|
||||
_mapSystem = entManager.System<SharedMapSystem>();
|
||||
_xformSys = xformSys;
|
||||
_shader = protoMan.Index(UnshadedShader).Instance();
|
||||
|
|
@ -67,7 +70,7 @@ namespace Content.Client.Atmos.Overlays
|
|||
|
||||
for (var i = 0; i < _gasCount; i++)
|
||||
{
|
||||
var gasPrototype = protoMan.Index<GasPrototype>(system.VisibleGasId[i].ToString());
|
||||
var gasPrototype = _atmosphereSystem.GetGas(system.VisibleGasId[i]);
|
||||
|
||||
SpriteSpecifier overlay;
|
||||
|
||||
|
|
|
|||
|
|
@ -52,14 +52,18 @@ namespace Content.Client.Atmos.UI
|
|||
|
||||
private void OnSelectGasPressed()
|
||||
{
|
||||
if (_window is null) return;
|
||||
if (_window is null)
|
||||
return;
|
||||
|
||||
if (_window.SelectedGas is null)
|
||||
{
|
||||
SendMessage(new GasFilterSelectGasMessage(null));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!int.TryParse(_window.SelectedGas, out var gas)) return;
|
||||
if (!Enum.TryParse<Gas>(_window.SelectedGas, out var gas))
|
||||
return;
|
||||
|
||||
SendMessage(new GasFilterSelectGasMessage(gas));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
using Content.Shared.Bed;
|
||||
|
||||
namespace Content.Client.Bed;
|
||||
|
||||
public sealed class BedSystem : SharedBedSystem
|
||||
{
|
||||
|
||||
}
|
||||
|
|
@ -112,7 +112,6 @@ namespace Content.Client.Entry
|
|||
_prototypeManager.RegisterIgnore("htnPrimitive");
|
||||
_prototypeManager.RegisterIgnore("gameMap");
|
||||
_prototypeManager.RegisterIgnore("gameMapPool");
|
||||
_prototypeManager.RegisterIgnore("lobbyBackground");
|
||||
_prototypeManager.RegisterIgnore("gamePreset");
|
||||
_prototypeManager.RegisterIgnore("noiseChannel");
|
||||
_prototypeManager.RegisterIgnore("playerConnectionWhitelist");
|
||||
|
|
|
|||
7
Content.Client/Fluids/SpraySystem.cs
Normal file
7
Content.Client/Fluids/SpraySystem.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
using Content.Shared.Fluids.Components;
|
||||
using Content.Shared.Fluids.EntitySystems;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Client.Fluids;
|
||||
|
||||
public sealed class SpraySystem : SharedSpraySystem;
|
||||
|
|
@ -11,6 +11,7 @@ using Robust.Client.State;
|
|||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Audio;
|
||||
using Content.Shared.GameTicking.Prototypes;
|
||||
|
||||
namespace Content.Client.GameTicking.Managers
|
||||
{
|
||||
|
|
@ -28,7 +29,7 @@ namespace Content.Client.GameTicking.Managers
|
|||
[ViewVariables] public bool AreWeReady { get; private set; }
|
||||
[ViewVariables] public bool IsGameStarted { get; private set; }
|
||||
[ViewVariables] public ResolvedSoundSpecifier? RestartSound { get; private set; }
|
||||
[ViewVariables] public string? LobbyBackground { get; private set; }
|
||||
[ViewVariables] public ProtoId<LobbyBackgroundPrototype>? LobbyBackground { get; private set; }
|
||||
[ViewVariables] public bool DisallowedLateJoin { get; private set; }
|
||||
[ViewVariables] public string? ServerInfoBlob { get; private set; }
|
||||
[ViewVariables] public TimeSpan StartTime { get; private set; }
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ using Robust.Client.ResourceManagement;
|
|||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Lobby
|
||||
|
|
@ -28,6 +29,7 @@ namespace Content.Client.Lobby
|
|||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IVoteManager _voteManager = default!;
|
||||
[Dependency] private readonly ClientsidePlaytimeTrackingManager _playtimeTracking = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoMan = default!;
|
||||
|
||||
private ClientGameTicker _gameTicker = default!;
|
||||
private ContentAudioSystem _contentAudioSystem = default!;
|
||||
|
|
@ -250,15 +252,22 @@ namespace Content.Client.Lobby
|
|||
|
||||
private void UpdateLobbyBackground()
|
||||
{
|
||||
if (_gameTicker.LobbyBackground != null)
|
||||
if (_protoMan.TryIndex(_gameTicker.LobbyBackground, out var proto))
|
||||
{
|
||||
Lobby!.Background.Texture = _resourceCache.GetResource<TextureResource>(_gameTicker.LobbyBackground );
|
||||
Lobby!.Background.Texture = _resourceCache.GetResource<TextureResource>(proto.Background);
|
||||
|
||||
var markup = Loc.GetString("lobby-state-background-text",
|
||||
("backgroundTitle", Loc.GetString(proto.Title)),
|
||||
("backgroundArtist", Loc.GetString(proto.Artist)));
|
||||
|
||||
Lobby!.LobbyBackground.SetMarkup(markup);
|
||||
}
|
||||
else
|
||||
{
|
||||
Lobby!.Background.Texture = null;
|
||||
}
|
||||
|
||||
Lobby!.LobbyBackground.SetMarkup(Loc.GetString("lobby-state-background-no-background-text"));
|
||||
}
|
||||
}
|
||||
|
||||
private void SetReady(bool newReady)
|
||||
|
|
|
|||
|
|
@ -51,10 +51,13 @@
|
|||
<!-- Vertical Padding-->
|
||||
<Control VerticalExpand="True" />
|
||||
<!-- Left Bot Panel -->
|
||||
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Bottom">
|
||||
<BoxContainer Orientation="Vertical" HorizontalAlignment="Left" VerticalAlignment="Bottom">
|
||||
<info:DevInfoBanner Name="DevInfoBanner" VerticalExpand="false" Margin="3 3 3 3" />
|
||||
<PanelContainer StyleClasses="BackgroundPanel">
|
||||
<RichTextLabel Name="LobbySong" Access="Public" HorizontalAlignment="Center" />
|
||||
<PanelContainer StyleClasses="BackgroundPanel" Margin="3 3 3 3">
|
||||
<BoxContainer Orientation="Vertical" HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="3 3 3 3">
|
||||
<RichTextLabel Name="LobbySong" Access="Public" HorizontalAlignment="Left" />
|
||||
<RichTextLabel Name="LobbyBackground" Access="Public" HorizontalAlignment="Left" />
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
|
|
|
|||
5
Content.Client/NameIdentifier/NameIdentifierSystem.cs
Normal file
5
Content.Client/NameIdentifier/NameIdentifierSystem.cs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
using Content.Shared.NameIdentifier;
|
||||
|
||||
namespace Content.Client.NameIdentifier;
|
||||
|
||||
public sealed class NameIdentifierSystem : SharedNameIdentifierSystem;
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
<!-- Power On/Off -->
|
||||
<Label Text="{Loc 'apc-menu-breaker-label'}" HorizontalExpand="True"
|
||||
StyleClasses="highlight" MinWidth="120"/>
|
||||
<BoxContainer Orientation="Horizontal" MinWidth="90">
|
||||
<BoxContainer Orientation="Horizontal" MinWidth="150">
|
||||
<Button Name="BreakerButton" Text="{Loc 'apc-menu-breaker-button'}" HorizontalExpand="True" ToggleMode="True"/>
|
||||
</BoxContainer>
|
||||
<!--Charging Status-->
|
||||
|
|
|
|||
|
|
@ -40,7 +40,14 @@ namespace Content.Client.Power.APC.UI
|
|||
|
||||
if (PowerLabel != null)
|
||||
{
|
||||
PowerLabel.Text = Loc.GetString("apc-menu-power-state-label-text", ("power", castState.Power));
|
||||
if (castState.Tripped)
|
||||
{
|
||||
PowerLabel.Text = Loc.GetString("apc-menu-power-state-label-tripped");
|
||||
}
|
||||
else
|
||||
{
|
||||
PowerLabel.Text = Loc.GetString("apc-menu-power-state-label-text", ("power", castState.Power), ("maxLoad", castState.MaxLoad));
|
||||
}
|
||||
}
|
||||
|
||||
if (ExternalPowerStateLabel != null)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ public sealed class ActivatableUIRequiresPowerSystem : SharedActivatableUIRequir
|
|||
return;
|
||||
}
|
||||
|
||||
_popup.PopupClient(Loc.GetString("base-computer-ui-component-not-powered", ("machine", ent.Owner)), args.User, args.User);
|
||||
if (!args.Silent)
|
||||
_popup.PopupClient(Loc.GetString("base-computer-ui-component-not-powered", ("machine", ent.Owner)), args.User, args.User);
|
||||
|
||||
args.Cancel();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
using Content.Shared.Power.EntitySystems;
|
||||
|
||||
namespace Content.Client.Power.EntitySystems;
|
||||
|
||||
public sealed class ChargerSystem : SharedChargerSystem;
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
using Content.Shared.PowerCell;
|
||||
using Content.Shared.PowerCell.Components;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.PowerCell;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class PowerCellSystem : SharedPowerCellSystem
|
||||
{
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<PowerCellVisualsComponent, AppearanceChangeEvent>(OnPowerCellVisualsChange);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool HasActivatableCharge(EntityUid uid, PowerCellDrawComponent? battery = null, PowerCellSlotComponent? cell = null,
|
||||
EntityUid? user = null)
|
||||
{
|
||||
if (!Resolve(uid, ref battery, ref cell, false))
|
||||
return true;
|
||||
|
||||
return battery.CanUse;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool HasDrawCharge(
|
||||
EntityUid uid,
|
||||
PowerCellDrawComponent? battery = null,
|
||||
PowerCellSlotComponent? cell = null,
|
||||
EntityUid? user = null)
|
||||
{
|
||||
if (!Resolve(uid, ref battery, ref cell, false))
|
||||
return true;
|
||||
|
||||
return battery.CanDraw;
|
||||
}
|
||||
|
||||
private void OnPowerCellVisualsChange(EntityUid uid, PowerCellVisualsComponent component, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (args.Sprite == null)
|
||||
return;
|
||||
|
||||
if (!_sprite.LayerExists((uid, args.Sprite), PowerCellVisualLayers.Unshaded))
|
||||
return;
|
||||
|
||||
// If no appearance data is set, rely on whatever existing sprite state is set being correct.
|
||||
if (!_appearance.TryGetData<byte>(uid, PowerCellVisuals.ChargeLevel, out var level, args.Component))
|
||||
return;
|
||||
|
||||
var positiveCharge = level > 0;
|
||||
_sprite.LayerSetVisible((uid, args.Sprite), PowerCellVisualLayers.Unshaded, positiveCharge);
|
||||
|
||||
if (positiveCharge)
|
||||
_sprite.LayerSetRsiState((uid, args.Sprite), PowerCellVisualLayers.Unshaded, $"o{level}");
|
||||
}
|
||||
|
||||
private enum PowerCellVisualLayers : byte
|
||||
{
|
||||
Base,
|
||||
Unshaded,
|
||||
}
|
||||
}
|
||||
11
Content.Client/PowerCell/PowerCellVisualLayers.cs
Normal file
11
Content.Client/PowerCell/PowerCellVisualLayers.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
namespace Content.Client.PowerCell;
|
||||
|
||||
/// <summary>
|
||||
/// Sprite layers for power cells.
|
||||
/// For use with the generic visualizer.
|
||||
/// </summary>
|
||||
public enum PowerCellVisualLayers : byte
|
||||
{
|
||||
Base,
|
||||
Unshaded,
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
namespace Content.Client.PowerCell;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class PowerCellVisualsComponent : Component {}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
using Content.Shared.Power;
|
||||
using Content.Shared.Power.Components;
|
||||
|
||||
namespace Content.Client.PowerCell;
|
||||
|
||||
|
|
@ -9,15 +9,13 @@ public sealed partial class PowerChargerVisualsComponent : Component
|
|||
/// <summary>
|
||||
/// The base sprite state used if the power cell charger does not contain a power cell.
|
||||
/// </summary>
|
||||
[DataField("emptyState")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public string EmptyState = "empty";
|
||||
|
||||
/// <summary>
|
||||
/// The base sprite state used if the power cell charger contains a power cell.
|
||||
/// </summary>
|
||||
[DataField("occupiedState")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public string OccupiedState = "full";
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -27,8 +25,7 @@ public sealed partial class PowerChargerVisualsComponent : Component
|
|||
/// <see cref="CellChargerStatus.Charging"/> Maps to the state used when the charger is charging a power cell.
|
||||
/// <see cref="CellChargerStatus.Charged"/> Maps to the state used when the charger contains a fully charged power cell.
|
||||
/// </summary>
|
||||
[DataField("lightStates")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public Dictionary<CellChargerStatus, string> LightStates = new()
|
||||
{
|
||||
[CellChargerStatus.Off] = "light-off",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using Content.Shared.Power;
|
||||
using Content.Shared.Power.Components;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.PowerCell;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ public sealed class RCDConstructionGhostSystem : EntitySystem
|
|||
[Dependency] private readonly IPlacementManager _placementManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
||||
[Dependency] private readonly HandsSystem _hands = default!;
|
||||
|
||||
|
||||
private Direction _placementDirection = default;
|
||||
|
||||
public override void Update(float frameTime)
|
||||
|
|
@ -42,6 +42,11 @@ public sealed class RCDConstructionGhostSystem : EntitySystem
|
|||
|
||||
var heldEntity = _hands.GetActiveItem(player);
|
||||
|
||||
// Don't open the placement overlay for client-side RCDs.
|
||||
// This may happen when predictively spawning one in your hands.
|
||||
if (heldEntity != null && IsClientSide(heldEntity.Value))
|
||||
return;
|
||||
|
||||
if (!TryComp<RCDComponent>(heldEntity, out var rcd))
|
||||
{
|
||||
// If the player was holding an RCD, but is no longer, cancel placement
|
||||
|
|
@ -69,7 +74,7 @@ public sealed class RCDConstructionGhostSystem : EntitySystem
|
|||
MobUid = heldEntity.Value,
|
||||
PlacementOption = PlacementMode,
|
||||
EntityType = prototype.Prototype,
|
||||
Range = (int) Math.Ceiling(SharedInteractionSystem.InteractionRange),
|
||||
Range = (int)Math.Ceiling(SharedInteractionSystem.InteractionRange),
|
||||
IsTile = (prototype.Mode == RcdMode.ConstructTile),
|
||||
UseEditorContext = false,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using Content.Shared.Silicons.Borgs;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.Silicons.Borgs;
|
||||
|
|
@ -24,31 +23,29 @@ public sealed class BorgBoundUserInterface : BoundUserInterface
|
|||
|
||||
_menu.BrainButtonPressed += () =>
|
||||
{
|
||||
SendMessage(new BorgEjectBrainBuiMessage());
|
||||
SendPredictedMessage(new BorgEjectBrainBuiMessage());
|
||||
};
|
||||
|
||||
_menu.EjectBatteryButtonPressed += () =>
|
||||
{
|
||||
SendMessage(new BorgEjectBatteryBuiMessage());
|
||||
SendPredictedMessage(new BorgEjectBatteryBuiMessage());
|
||||
};
|
||||
|
||||
_menu.NameChanged += name =>
|
||||
{
|
||||
SendMessage(new BorgSetNameBuiMessage(name));
|
||||
SendPredictedMessage(new BorgSetNameBuiMessage(name));
|
||||
};
|
||||
|
||||
_menu.RemoveModuleButtonPressed += module =>
|
||||
{
|
||||
SendMessage(new BorgRemoveModuleBuiMessage(EntMan.GetNetEntity(module)));
|
||||
SendPredictedMessage(new BorgRemoveModuleBuiMessage(EntMan.GetNetEntity(module)));
|
||||
};
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
public override void Update()
|
||||
{
|
||||
base.UpdateState(state);
|
||||
|
||||
if (state is not BorgBuiState msg)
|
||||
return;
|
||||
_menu?.UpdateState(msg);
|
||||
_menu?.UpdateBatteryButton();
|
||||
_menu?.UpdateBrainButton();
|
||||
_menu?.UpdateModulePanel();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ using Content.Client.UserInterface.Controls;
|
|||
using Content.Shared.CCVar;
|
||||
using Content.Shared.NameIdentifier;
|
||||
using Content.Shared.NameModifier.EntitySystems;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Silicons.Borgs;
|
||||
using Content.Shared.Power.EntitySystems;
|
||||
using Content.Shared.PowerCell;
|
||||
using Content.Shared.Silicons.Borgs.Components;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
|
@ -20,6 +20,8 @@ public sealed partial class BorgMenu : FancyWindow
|
|||
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entity = default!;
|
||||
private readonly NameModifierSystem _nameModifier;
|
||||
private readonly PowerCellSystem _powerCell;
|
||||
private readonly PredictedBatterySystem _battery;
|
||||
|
||||
public Action? BrainButtonPressed;
|
||||
public Action? EjectBatteryButtonPressed;
|
||||
|
|
@ -41,6 +43,8 @@ public sealed partial class BorgMenu : FancyWindow
|
|||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_nameModifier = _entity.System<NameModifierSystem>();
|
||||
_powerCell = _entity.System<PowerCellSystem>();
|
||||
_battery = _entity.System<PredictedBatterySystem>();
|
||||
|
||||
_maxNameLength = _cfgManager.GetCVar(CCVars.MaxNameLength);
|
||||
|
||||
|
|
@ -52,8 +56,6 @@ public sealed partial class BorgMenu : FancyWindow
|
|||
NameLineEdit.OnTextChanged += OnNameChanged;
|
||||
NameLineEdit.OnTextEntered += OnNameEntered;
|
||||
NameLineEdit.OnFocusExit += OnNameFocusExit;
|
||||
|
||||
UpdateBrainButton();
|
||||
}
|
||||
|
||||
public void SetEntity(EntityUid entity)
|
||||
|
|
@ -73,6 +75,10 @@ public sealed partial class BorgMenu : FancyWindow
|
|||
NameIdentifierLabel.Visible = false;
|
||||
NameLineEdit.Text = _entity.GetComponent<MetaDataComponent>(Entity).EntityName;
|
||||
}
|
||||
|
||||
UpdateBatteryButton();
|
||||
UpdateBrainButton();
|
||||
UpdateModulePanel();
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
|
|
@ -80,21 +86,24 @@ public sealed partial class BorgMenu : FancyWindow
|
|||
base.FrameUpdate(args);
|
||||
|
||||
AccumulatedTime += args.DeltaSeconds;
|
||||
BorgSprite.OverrideDirection = (Direction) ((int) AccumulatedTime % 4 * 2);
|
||||
}
|
||||
BorgSprite.OverrideDirection = (Direction)((int)AccumulatedTime % 4 * 2);
|
||||
|
||||
public void UpdateState(BorgBuiState state)
|
||||
{
|
||||
EjectBatteryButton.Disabled = !state.HasBattery;
|
||||
ChargeBar.Value = state.ChargePercent;
|
||||
var chargeFraction = 0f;
|
||||
|
||||
if (_powerCell.TryGetBatteryFromSlot(Entity, out var battery))
|
||||
chargeFraction = _battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge;
|
||||
|
||||
ChargeBar.Value = chargeFraction;
|
||||
ChargeLabel.Text = Loc.GetString("borg-ui-charge-label",
|
||||
("charge", (int) MathF.Round(state.ChargePercent * 100)));
|
||||
|
||||
UpdateBrainButton();
|
||||
UpdateModulePanel();
|
||||
("charge", (int)MathF.Round(chargeFraction * 100)));
|
||||
}
|
||||
|
||||
private void UpdateBrainButton()
|
||||
public void UpdateBatteryButton()
|
||||
{
|
||||
EjectBatteryButton.Disabled = !_powerCell.HasBattery(Entity);
|
||||
}
|
||||
|
||||
public void UpdateBrainButton()
|
||||
{
|
||||
if (_entity.TryGetComponent(Entity, out BorgChassisComponent? chassis) && chassis.BrainEntity is { } brain)
|
||||
{
|
||||
|
|
@ -113,7 +122,7 @@ public sealed partial class BorgMenu : FancyWindow
|
|||
}
|
||||
}
|
||||
|
||||
private void UpdateModulePanel()
|
||||
public void UpdateModulePanel()
|
||||
{
|
||||
if (!_entity.TryGetComponent(Entity, out BorgChassisComponent? chassis))
|
||||
return;
|
||||
|
|
|
|||
83
Content.Client/Silicons/Borgs/BorgSystem.Battery.cs
Normal file
83
Content.Client/Silicons/Borgs/BorgSystem.Battery.cs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
using Content.Shared.PowerCell.Components;
|
||||
using Content.Shared.Silicons.Borgs.Components;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Client.Silicons.Borgs;
|
||||
|
||||
public sealed partial class BorgSystem
|
||||
{
|
||||
// How often to update the battery alert.
|
||||
// Also gets updated instantly when switching bodies or a battery is inserted or removed.
|
||||
private static readonly TimeSpan AlertUpdateDelay = TimeSpan.FromSeconds(0.5f);
|
||||
|
||||
// Don't put this on the component because we only need to track the time for a single entity
|
||||
// and we don't want to TryComp it every single tick.
|
||||
private TimeSpan _nextAlertUpdate = TimeSpan.Zero;
|
||||
private EntityQuery<BorgChassisComponent> _chassisQuery;
|
||||
private EntityQuery<PowerCellSlotComponent> _slotQuery;
|
||||
|
||||
public void InitializeBattery()
|
||||
{
|
||||
SubscribeLocalEvent<BorgChassisComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
|
||||
SubscribeLocalEvent<BorgChassisComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
|
||||
|
||||
_chassisQuery = GetEntityQuery<BorgChassisComponent>();
|
||||
_slotQuery = GetEntityQuery<PowerCellSlotComponent>();
|
||||
}
|
||||
|
||||
private void OnPlayerAttached(Entity<BorgChassisComponent> ent, ref LocalPlayerAttachedEvent args)
|
||||
{
|
||||
UpdateBatteryAlert((ent.Owner, ent.Comp, null));
|
||||
}
|
||||
|
||||
private void OnPlayerDetached(Entity<BorgChassisComponent> ent, ref LocalPlayerDetachedEvent args)
|
||||
{
|
||||
// Remove all borg related alerts.
|
||||
_alerts.ClearAlert(ent.Owner, ent.Comp.BatteryAlert);
|
||||
_alerts.ClearAlert(ent.Owner, ent.Comp.NoBatteryAlert);
|
||||
}
|
||||
|
||||
private void UpdateBatteryAlert(Entity<BorgChassisComponent, PowerCellSlotComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp2, false))
|
||||
return;
|
||||
|
||||
if (!_powerCell.TryGetBatteryFromSlot((ent.Owner, ent.Comp2), out var battery))
|
||||
{
|
||||
_alerts.ShowAlert(ent.Owner, ent.Comp1.NoBatteryAlert);
|
||||
return;
|
||||
}
|
||||
|
||||
// Alert levels from 0 to 10.
|
||||
var chargeLevel = (short)MathF.Round(_battery.GetChargeLevel(battery.Value.AsNullable()) * 10f);
|
||||
|
||||
// we make sure 0 only shows if they have absolutely no battery.
|
||||
// also account for floating point imprecision
|
||||
if (chargeLevel == 0 && _powerCell.HasDrawCharge((ent.Owner, null, ent.Comp2)))
|
||||
{
|
||||
chargeLevel = 1;
|
||||
}
|
||||
|
||||
_alerts.ShowAlert(ent.Owner, ent.Comp1.BatteryAlert, chargeLevel);
|
||||
}
|
||||
|
||||
// Periodically update the charge indicator.
|
||||
// We do this with a client-side alert so that we don't have to network the charge level.
|
||||
public void UpdateBattery(float frameTime)
|
||||
{
|
||||
if (_player.LocalEntity is not { } localPlayer)
|
||||
return;
|
||||
|
||||
var curTime = _timing.CurTime;
|
||||
|
||||
if (curTime < _nextAlertUpdate)
|
||||
return;
|
||||
|
||||
_nextAlertUpdate = curTime + AlertUpdateDelay;
|
||||
|
||||
if (!_chassisQuery.TryComp(localPlayer, out var chassis) || !_slotQuery.TryComp(localPlayer, out var slot))
|
||||
return;
|
||||
|
||||
UpdateBatteryAlert((localPlayer, chassis, slot));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,72 +1,93 @@
|
|||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Power.EntitySystems;
|
||||
using Content.Shared.PowerCell;
|
||||
using Content.Shared.Silicons.Borgs;
|
||||
using Content.Shared.Silicons.Borgs.Components;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Silicons.Borgs;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed class BorgSystem : SharedBorgSystem
|
||||
public sealed partial class BorgSystem : SharedBorgSystem
|
||||
{
|
||||
[Dependency] private readonly AppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
||||
[Dependency] private readonly PowerCellSystem _powerCell = default!;
|
||||
[Dependency] private readonly PredictedBatterySystem _battery = default!;
|
||||
[Dependency] private readonly AlertsSystem _alerts = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
InitializeBattery();
|
||||
|
||||
SubscribeLocalEvent<BorgChassisComponent, AppearanceChangeEvent>(OnBorgAppearanceChanged);
|
||||
SubscribeLocalEvent<MMIComponent, AppearanceChangeEvent>(OnMMIAppearanceChanged);
|
||||
}
|
||||
|
||||
private void OnBorgAppearanceChanged(EntityUid uid, BorgChassisComponent component, ref AppearanceChangeEvent args)
|
||||
public override void UpdateUI(Entity<BorgChassisComponent?> chassis)
|
||||
{
|
||||
if (_ui.TryGetOpenUi(chassis.Owner, BorgUiKey.Key, out var bui))
|
||||
bui.Update();
|
||||
}
|
||||
|
||||
private void OnBorgAppearanceChanged(Entity<BorgChassisComponent> chassis, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (args.Sprite == null)
|
||||
return;
|
||||
UpdateBorgAppearance(uid, component, args.Component, args.Sprite);
|
||||
|
||||
UpdateBorgAppearance((chassis.Owner, chassis.Comp, args.Component, args.Sprite));
|
||||
}
|
||||
|
||||
protected override void OnInserted(EntityUid uid, BorgChassisComponent component, EntInsertedIntoContainerMessage args)
|
||||
protected override void OnInserted(Entity<BorgChassisComponent> chassis, ref EntInsertedIntoContainerMessage args)
|
||||
{
|
||||
if (!component.Initialized)
|
||||
if (!chassis.Comp.Initialized)
|
||||
return;
|
||||
|
||||
base.OnInserted(uid, component, args);
|
||||
UpdateBorgAppearance(uid, component);
|
||||
base.OnInserted(chassis, ref args);
|
||||
UpdateUI(chassis.AsNullable());
|
||||
UpdateBorgAppearance((chassis, chassis.Comp));
|
||||
UpdateBatteryAlert((chassis.Owner, chassis.Comp, null));
|
||||
}
|
||||
|
||||
protected override void OnRemoved(EntityUid uid, BorgChassisComponent component, EntRemovedFromContainerMessage args)
|
||||
protected override void OnRemoved(Entity<BorgChassisComponent> chassis, ref EntRemovedFromContainerMessage args)
|
||||
{
|
||||
if (!component.Initialized)
|
||||
if (!chassis.Comp.Initialized)
|
||||
return;
|
||||
|
||||
base.OnRemoved(uid, component, args);
|
||||
UpdateBorgAppearance(uid, component);
|
||||
base.OnRemoved(chassis, ref args);
|
||||
UpdateUI(chassis.AsNullable());
|
||||
UpdateBorgAppearance((chassis, chassis.Comp));
|
||||
UpdateBatteryAlert((chassis.Owner, chassis.Comp, null));
|
||||
}
|
||||
|
||||
private void UpdateBorgAppearance(EntityUid uid,
|
||||
BorgChassisComponent? component = null,
|
||||
AppearanceComponent? appearance = null,
|
||||
SpriteComponent? sprite = null)
|
||||
private void UpdateBorgAppearance(Entity<BorgChassisComponent?, AppearanceComponent?, SpriteComponent?> ent)
|
||||
{
|
||||
if (!Resolve(uid, ref component, ref appearance, ref sprite))
|
||||
if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2, ref ent.Comp3))
|
||||
return;
|
||||
|
||||
if (_appearance.TryGetData<MobState>(uid, MobStateVisuals.State, out var state, appearance))
|
||||
if (_appearance.TryGetData<MobState>(ent.Owner, MobStateVisuals.State, out var state, ent.Comp2))
|
||||
{
|
||||
if (state != MobState.Alive)
|
||||
{
|
||||
_sprite.LayerSetVisible((uid, sprite), BorgVisualLayers.Light, false);
|
||||
_sprite.LayerSetVisible((ent.Owner, ent.Comp3), BorgVisualLayers.Light, false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_appearance.TryGetData<bool>(uid, BorgVisuals.HasPlayer, out var hasPlayer, appearance))
|
||||
if (!_appearance.TryGetData<bool>(ent.Owner, BorgVisuals.HasPlayer, out var hasPlayer, ent.Comp2))
|
||||
hasPlayer = false;
|
||||
|
||||
_sprite.LayerSetVisible((uid, sprite), BorgVisualLayers.Light, component.BrainEntity != null || hasPlayer);
|
||||
_sprite.LayerSetRsiState((uid, sprite), BorgVisualLayers.Light, hasPlayer ? component.HasMindState : component.NoMindState);
|
||||
_sprite.LayerSetVisible((ent.Owner, ent.Comp3), BorgVisualLayers.Light, ent.Comp1.BrainEntity != null || hasPlayer);
|
||||
_sprite.LayerSetRsiState((ent.Owner, ent.Comp3), BorgVisualLayers.Light, hasPlayer ? ent.Comp1.HasMindState : ent.Comp1.NoMindState);
|
||||
}
|
||||
|
||||
private void OnMMIAppearanceChanged(EntityUid uid, MMIComponent component, ref AppearanceChangeEvent args)
|
||||
|
|
@ -107,4 +128,10 @@ public sealed class BorgSystem : SharedBorgSystem
|
|||
borg.Comp.HasMindState = hasMindState;
|
||||
borg.Comp.NoMindState = noMindState;
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
UpdateBattery(frameTime);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,43 +1,5 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Client.Storage.Components;
|
||||
using Content.Shared.Destructible;
|
||||
using Content.Shared.Foldable;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Lock;
|
||||
using Content.Shared.Movement.Events;
|
||||
using Content.Shared.Storage.Components;
|
||||
using Content.Shared.Storage.EntitySystems;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.GameStates;
|
||||
using Content.Shared.Storage.EntitySystems;
|
||||
|
||||
namespace Content.Client.Storage.Systems;
|
||||
|
||||
public sealed class EntityStorageSystem : SharedEntityStorageSystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<EntityStorageComponent, EntityUnpausedEvent>(OnEntityUnpausedEvent);
|
||||
SubscribeLocalEvent<EntityStorageComponent, ComponentInit>(OnComponentInit);
|
||||
SubscribeLocalEvent<EntityStorageComponent, ComponentStartup>(OnComponentStartup);
|
||||
SubscribeLocalEvent<EntityStorageComponent, ActivateInWorldEvent>(OnInteract, after: new[] { typeof(LockSystem) });
|
||||
SubscribeLocalEvent<EntityStorageComponent, LockToggleAttemptEvent>(OnLockToggleAttempt);
|
||||
SubscribeLocalEvent<EntityStorageComponent, DestructionEventArgs>(OnDestruction);
|
||||
SubscribeLocalEvent<EntityStorageComponent, GetVerbsEvent<InteractionVerb>>(AddToggleOpenVerb);
|
||||
SubscribeLocalEvent<EntityStorageComponent, ContainerRelayMovementEntityEvent>(OnRelayMovement);
|
||||
SubscribeLocalEvent<EntityStorageComponent, FoldAttemptEvent>(OnFoldAttempt);
|
||||
|
||||
SubscribeLocalEvent<EntityStorageComponent, ComponentGetState>(OnGetState);
|
||||
SubscribeLocalEvent<EntityStorageComponent, ComponentHandleState>(OnHandleState);
|
||||
}
|
||||
|
||||
public override bool ResolveStorage(EntityUid uid, [NotNullWhen(true)] ref EntityStorageComponent? component)
|
||||
{
|
||||
if (component != null)
|
||||
return true;
|
||||
|
||||
TryComp<EntityStorageComponent>(uid, out var storage);
|
||||
component = storage;
|
||||
return component != null;
|
||||
}
|
||||
}
|
||||
public sealed class EntityStorageSystem : SharedEntityStorageSystem;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
Title="{Loc 'thief-backpack-window-title'}"
|
||||
MinSize="700 700">
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
|
||||
<!-- First Informational panel -->
|
||||
|
|
@ -25,7 +24,6 @@
|
|||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
</PanelContainer>
|
||||
|
||||
<!-- Third approve button panel -->
|
||||
<PanelContainer Margin="10">
|
||||
<Button Name="ApproveButton"
|
||||
|
|
|
|||
|
|
@ -46,7 +46,8 @@ public sealed partial class ThiefBackpackMenu : FancyWindow
|
|||
selectedNumber++;
|
||||
}
|
||||
|
||||
Description.Text = Loc.GetString("thief-backpack-window-description", ("maxCount", state.MaxSelectedSets));
|
||||
Title = Loc.GetString(state.ToolName);
|
||||
Description.Text = Loc.GetString(state.ToolDesc, ("maxCount", state.MaxSelectedSets));
|
||||
SelectedSets.Text = Loc.GetString("thief-backpack-window-selected", ("selectedCount", selectedNumber), ("maxCount", state.MaxSelectedSets));
|
||||
ApproveButton.Disabled = selectedNumber != state.MaxSelectedSets;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using Content.Shared.FixedPoint;
|
|||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.StatusEffectNew;
|
||||
using Content.Shared.Traits.Assorted;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
|
|
@ -20,6 +21,7 @@ public sealed class DamageOverlayUiController : UIController
|
|||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
|
||||
[UISystemDependency] private readonly MobThresholdSystem _mobThresholdSystem = default!;
|
||||
[UISystemDependency] private readonly StatusEffectsSystem _statusEffects = default!;
|
||||
private Overlays.DamageOverlay _overlay = default!;
|
||||
|
||||
public override void Initialize()
|
||||
|
|
@ -98,7 +100,7 @@ public sealed class DamageOverlayUiController : UIController
|
|||
FixedPoint2 painLevel = 0;
|
||||
_overlay.PainLevel = 0;
|
||||
|
||||
if (!EntityManager.HasComponent<PainNumbnessComponent>(entity))
|
||||
if (!_statusEffects.TryEffectsWithComp<PainNumbnessStatusEffectComponent>(entity, out _))
|
||||
{
|
||||
foreach (var painDamageType in damageable.PainDamageGroups)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ public sealed class EmotesUIController : UIController, IOnStateChanged<GameplayS
|
|||
if (emote.Category == EmoteCategory.Invalid
|
||||
|| emote.ChatTriggers.Count == 0
|
||||
|| !(player.HasValue && whitelistSystem.IsWhitelistPassOrNull(emote.Whitelist, player.Value))
|
||||
|| whitelistSystem.IsBlacklistPass(emote.Blacklist, player.Value))
|
||||
|| whitelistSystem.IsWhitelistPass(emote.Blacklist, player.Value))
|
||||
continue;
|
||||
|
||||
if (!emote.Available
|
||||
|
|
|
|||
|
|
@ -7,23 +7,20 @@ public sealed partial class GunSystem
|
|||
protected override void InitializeBattery()
|
||||
{
|
||||
base.InitializeBattery();
|
||||
// Hitscan
|
||||
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, AmmoCounterControlEvent>(OnControl);
|
||||
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, UpdateAmmoCounterEvent>(OnAmmoCountUpdate);
|
||||
|
||||
// Projectile
|
||||
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, AmmoCounterControlEvent>(OnControl);
|
||||
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, UpdateAmmoCounterEvent>(OnAmmoCountUpdate);
|
||||
SubscribeLocalEvent<BatteryAmmoProviderComponent, UpdateAmmoCounterEvent>(OnAmmoCountUpdate);
|
||||
SubscribeLocalEvent<BatteryAmmoProviderComponent, AmmoCounterControlEvent>(OnControl);
|
||||
}
|
||||
|
||||
private void OnAmmoCountUpdate(EntityUid uid, BatteryAmmoProviderComponent component, UpdateAmmoCounterEvent args)
|
||||
private void OnAmmoCountUpdate(Entity<BatteryAmmoProviderComponent> ent, ref UpdateAmmoCounterEvent args)
|
||||
{
|
||||
if (args.Control is not BoxesStatusControl boxes) return;
|
||||
if (args.Control is not BoxesStatusControl boxes)
|
||||
return;
|
||||
|
||||
boxes.Update(component.Shots, component.Capacity);
|
||||
boxes.Update(ent.Comp.Shots, ent.Comp.Capacity);
|
||||
}
|
||||
|
||||
private void OnControl(EntityUid uid, BatteryAmmoProviderComponent component, AmmoCounterControlEvent args)
|
||||
private void OnControl(Entity<BatteryAmmoProviderComponent> ent, ref AmmoCounterControlEvent args)
|
||||
{
|
||||
args.Control = new BoxesStatusControl();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,6 @@ public sealed partial class GunSystem : SharedGunSystem
|
|||
base.Initialize();
|
||||
UpdatesOutsidePrediction = true;
|
||||
SubscribeLocalEvent<AmmoCounterComponent, ItemStatusCollectMessage>(OnAmmoCounterCollect);
|
||||
SubscribeLocalEvent<AmmoCounterComponent, UpdateClientAmmoEvent>(OnUpdateClientAmmo);
|
||||
SubscribeAllEvent<MuzzleFlashEvent>(OnMuzzleFlash);
|
||||
|
||||
// Plays animated effects on the client.
|
||||
|
|
@ -90,10 +89,6 @@ public sealed partial class GunSystem : SharedGunSystem
|
|||
InitializeSpentAmmo();
|
||||
}
|
||||
|
||||
private void OnUpdateClientAmmo(EntityUid uid, AmmoCounterComponent ammoComp, ref UpdateClientAmmoEvent args)
|
||||
{
|
||||
UpdateAmmoCount(uid, ammoComp);
|
||||
}
|
||||
|
||||
private void OnMuzzleFlash(MuzzleFlashEvent args)
|
||||
{
|
||||
|
|
@ -158,6 +153,8 @@ public sealed partial class GunSystem : SharedGunSystem
|
|||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
if (!Timing.IsFirstTimePredicted)
|
||||
return;
|
||||
|
||||
|
|
|
|||
|
|
@ -38,12 +38,10 @@ public sealed class ConstantsTest
|
|||
Assert.That(Atmospherics.GasAbbreviations, Has.Count.EqualTo(Atmospherics.TotalNumberOfGases),
|
||||
$"GasAbbreviations size is not equal to TotalNumberOfGases.");
|
||||
|
||||
// the ID for each gas has to be a number from 0 to TotalNumberOfGases-1
|
||||
// the ID for each gas has to correspond to a value in the Gas enum (converted to a string)
|
||||
foreach (var gas in gasProtos)
|
||||
{
|
||||
var validInteger = int.TryParse(gas.ID, out var number);
|
||||
Assert.That(validInteger, Is.True, $"GasPrototype {gas.ID} has an invalid ID. It has to be an integer between 0 and TotalNumberOfGases - 1.");
|
||||
Assert.That(number, Is.InRange(0, Atmospherics.TotalNumberOfGases - 1), $"GasPrototype {gas.ID} has an invalid ID. It has to be an integer between 0 and TotalNumberOfGases - 1.");
|
||||
Assert.That(Enum.TryParse<Gas>(gas.ID, out _), $"GasPrototype {gas.ID} has an invalid ID. It must correspond to a value in the {nameof(Gas)} enum.");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Interaction;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Reflection;
|
||||
|
|
@ -64,17 +66,16 @@ namespace Content.IntegrationTests.Tests.DoAfter
|
|||
var server = pair.Server;
|
||||
await server.WaitIdleAsync();
|
||||
|
||||
var entityManager = server.ResolveDependency<IEntityManager>();
|
||||
var entityManager = server.EntMan;
|
||||
var timing = server.ResolveDependency<IGameTiming>();
|
||||
var doAfterSystem = entityManager.EntitySysManager.GetEntitySystem<SharedDoAfterSystem>();
|
||||
var doAfterSystem = entityManager.System<SharedDoAfterSystem>();
|
||||
var ev = new TestDoAfterEvent();
|
||||
|
||||
// That it finishes successfully
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var tickTime = 1.0f / timing.TickRate;
|
||||
var mob = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
|
||||
var args = new DoAfterArgs(entityManager, mob, tickTime / 2, ev, null) { Broadcast = true };
|
||||
var args = new DoAfterArgs(entityManager, mob, timing.TickPeriod / 2, ev, null) { Broadcast = true };
|
||||
#pragma warning disable NUnit2045 // Interdependent assertions.
|
||||
Assert.That(doAfterSystem.TryStartDoAfter(args));
|
||||
Assert.That(ev.Cancelled, Is.False);
|
||||
|
|
@ -92,23 +93,17 @@ namespace Content.IntegrationTests.Tests.DoAfter
|
|||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
var entityManager = server.ResolveDependency<IEntityManager>();
|
||||
var entityManager = server.EntMan;
|
||||
var timing = server.ResolveDependency<IGameTiming>();
|
||||
var doAfterSystem = entityManager.EntitySysManager.GetEntitySystem<SharedDoAfterSystem>();
|
||||
var doAfterSystem = entityManager.System<SharedDoAfterSystem>();
|
||||
var ev = new TestDoAfterEvent();
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var tickTime = 1.0f / timing.TickRate;
|
||||
|
||||
var mob = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
|
||||
var args = new DoAfterArgs(entityManager, mob, tickTime * 2, ev, null) { Broadcast = true };
|
||||
var args = new DoAfterArgs(entityManager, mob, timing.TickPeriod * 2, ev, null) { Broadcast = true };
|
||||
|
||||
if (!doAfterSystem.TryStartDoAfter(args, out var id))
|
||||
{
|
||||
Assert.Fail();
|
||||
return;
|
||||
}
|
||||
Assert.That(doAfterSystem.TryStartDoAfter(args, out var id));
|
||||
|
||||
Assert.That(!ev.Cancelled);
|
||||
doAfterSystem.Cancel(id);
|
||||
|
|
@ -121,5 +116,67 @@ namespace Content.IntegrationTests.Tests.DoAfter
|
|||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns two sets of mobs with a targeted DoAfter to check that the GetEntitiesInteractingWithTarget result
|
||||
/// includes the correct interacting entities.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task TestGetInteractingEntities()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
var entityManager = server.EntMan;
|
||||
var timing = server.ResolveDependency<IGameTiming>();
|
||||
var doAfterSystem = entityManager.System<SharedDoAfterSystem>();
|
||||
var interactionSystem = entityManager.System<SharedInteractionSystem>();
|
||||
var ev = new TestDoAfterEvent();
|
||||
|
||||
EntityUid mob = default;
|
||||
EntityUid target = default;
|
||||
|
||||
EntityUid mob2 = default;
|
||||
EntityUid mob3 = default;
|
||||
EntityUid target2 = default;
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
// Spawn two targets to interact with
|
||||
target = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
|
||||
target2 = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
|
||||
|
||||
// Spawn a mob which is interacting with the first target
|
||||
mob = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
|
||||
var args = new DoAfterArgs(entityManager, mob, timing.TickPeriod * 5, ev, null, target) { Broadcast = true };
|
||||
Assert.That(doAfterSystem.TryStartDoAfter(args));
|
||||
|
||||
// Spawn two more mobs which are interacting with the second target
|
||||
mob2 = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
|
||||
var args2 = new DoAfterArgs(entityManager, mob2, timing.TickPeriod * 5, ev, null, target2) { Broadcast = true };
|
||||
Assert.That(doAfterSystem.TryStartDoAfter(args2));
|
||||
|
||||
mob3 = entityManager.SpawnEntity("DoAfterDummy", MapCoordinates.Nullspace);
|
||||
var args3 = new DoAfterArgs(entityManager, mob3, timing.TickPeriod * 5, ev, null, target2) { Broadcast = true };
|
||||
Assert.That(doAfterSystem.TryStartDoAfter(args3));
|
||||
});
|
||||
|
||||
var list = new HashSet<EntityUid>();
|
||||
interactionSystem.GetEntitiesInteractingWithTarget(target, list);
|
||||
Assert.That(list, Is.EquivalentTo([mob]), $"{mob} was not considered to be interacting with {target}");
|
||||
|
||||
interactionSystem.GetEntitiesInteractingWithTarget(target2, list);
|
||||
Assert.That(list, Is.EquivalentTo([mob2, mob3]), $"{mob2} and {mob3} were not considered to be interacting with {target2}");
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
entityManager.DeleteEntity(mob);
|
||||
entityManager.DeleteEntity(mob2);
|
||||
entityManager.DeleteEntity(mob3);
|
||||
entityManager.DeleteEntity(target);
|
||||
entityManager.DeleteEntity(target2);
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,9 @@ public abstract partial class InteractionTest
|
|||
public static implicit operator EntitySpecifier(string prototype)
|
||||
=> new(prototype, 1);
|
||||
|
||||
public static implicit operator EntitySpecifier(EntProtoId prototype)
|
||||
=> new(prototype.Id, 1);
|
||||
|
||||
public static implicit operator EntitySpecifier((string, int) tuple)
|
||||
=> new(tuple.Item1, tuple.Item2);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
using Content.Server.Administration.Systems;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Maps;
|
||||
|
|
@ -11,20 +12,18 @@ using Content.Server.Spawners.Components;
|
|||
using Content.Server.Station.Components;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Station.Components;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Content.Shared.Station.Components;
|
||||
using Robust.Shared.EntitySerialization;
|
||||
using Robust.Shared.EntitySerialization.Systems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Utility;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Map.Events;
|
||||
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
namespace Content.IntegrationTests.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
|
|
@ -97,12 +96,12 @@ namespace Content.IntegrationTests.Tests
|
|||
"Saltern",
|
||||
"Reach",
|
||||
"Oasis",
|
||||
"Amber",
|
||||
"Plasma",
|
||||
"Elkridge",
|
||||
"Relic",
|
||||
"dm01-entryway",
|
||||
"Exo",
|
||||
"Snowball",
|
||||
};
|
||||
|
||||
private static readonly ProtoId<EntityCategoryPrototype> DoNotMapCategory = "DoNotMap";
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ using Content.Server.Power.NodeGroups;
|
|||
using Content.Server.Power.Pow3r;
|
||||
using Content.Shared.Power.Components;
|
||||
using Content.Shared.NodeContainer;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.EntitySerialization;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Power;
|
||||
|
||||
[Explicit]
|
||||
public sealed class StationPowerTests
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -21,27 +21,21 @@ public sealed class StationPowerTests
|
|||
|
||||
private static readonly string[] GameMaps =
|
||||
[
|
||||
"Fland",
|
||||
"Meta",
|
||||
"Packed",
|
||||
"Omega",
|
||||
"Bagel",
|
||||
"Box",
|
||||
"Core",
|
||||
"Marathon",
|
||||
"Saltern",
|
||||
"Reach",
|
||||
"Train",
|
||||
"Oasis",
|
||||
"Gate",
|
||||
"Amber",
|
||||
"Loop",
|
||||
"Plasma",
|
||||
"Elkridge",
|
||||
"Convex",
|
||||
"Fland",
|
||||
"Marathon",
|
||||
"Oasis",
|
||||
"Packed",
|
||||
"Plasma",
|
||||
"Relic",
|
||||
"Snowball",
|
||||
"Reach",
|
||||
"Exo",
|
||||
];
|
||||
|
||||
[Explicit]
|
||||
[Test, TestCaseSource(nameof(GameMaps))]
|
||||
public async Task TestStationStartingPowerWindow(string mapProtoId)
|
||||
{
|
||||
|
|
@ -100,6 +94,54 @@ public sealed class StationPowerTests
|
|||
$"Needs at least {requiredStoredPower - totalStartingCharge} more stored power!");
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test, TestCaseSource(nameof(GameMaps))]
|
||||
public async Task TestApcLoad(string mapProtoId)
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings
|
||||
{
|
||||
Dirty = true,
|
||||
});
|
||||
var server = pair.Server;
|
||||
|
||||
var entMan = server.EntMan;
|
||||
var protoMan = server.ProtoMan;
|
||||
var ticker = entMan.System<GameTicker>();
|
||||
var xform = entMan.System<TransformSystem>();
|
||||
|
||||
// Load the map
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
Assert.That(protoMan.TryIndex<GameMapPrototype>(mapProtoId, out var mapProto));
|
||||
var opts = DeserializationOptions.Default with { InitializeMaps = true };
|
||||
ticker.LoadGameMap(mapProto, out var mapId, opts);
|
||||
});
|
||||
|
||||
// Wait long enough for power to ramp up, but before anything can trip
|
||||
await pair.RunSeconds(2);
|
||||
|
||||
// Check that no APCs start overloaded
|
||||
var apcQuery = entMan.EntityQueryEnumerator<ApcComponent, PowerNetworkBatteryComponent>();
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
while (apcQuery.MoveNext(out var uid, out var apc, out var battery))
|
||||
{
|
||||
// Uncomment the following line to log starting APC load to the console
|
||||
//Console.WriteLine($"ApcLoad:{mapProtoId}:{uid}:{battery.CurrentSupply}");
|
||||
if (xform.TryGetMapOrGridCoordinates(uid, out var coord))
|
||||
{
|
||||
Assert.That(apc.MaxLoad, Is.GreaterThanOrEqualTo(battery.CurrentSupply),
|
||||
$"APC {uid} on {mapProtoId} ({coord.Value.X}, {coord.Value.Y}) is overloaded {battery.CurrentSupply} / {apc.MaxLoad}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.That(apc.MaxLoad, Is.GreaterThanOrEqualTo(battery.CurrentSupply),
|
||||
$"APC {uid} on {mapProtoId} is overloaded {battery.CurrentSupply} / {apc.MaxLoad}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
using System.Collections.Generic;
|
||||
using Content.IntegrationTests.Tests.Interaction;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Movement.Pulling.Systems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Puller;
|
||||
|
||||
#nullable enable
|
||||
|
||||
public sealed class InteractingEntitiesTest : InteractionTest
|
||||
{
|
||||
private static readonly EntProtoId MobHuman = "MobHuman";
|
||||
|
||||
/// <summary>
|
||||
/// Spawns a Target mob, and a second mob which drags it,
|
||||
/// and checks that the dragger is considered to be interacting with the dragged mob.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task PullerIsConsideredInteractingTest()
|
||||
{
|
||||
await SpawnTarget(MobHuman);
|
||||
var puller = await SpawnEntity(MobHuman, ToServer(TargetCoords));
|
||||
|
||||
var pullSys = SEntMan.System<PullingSystem>();
|
||||
await Server.WaitAssertion(() =>
|
||||
{
|
||||
Assert.That(pullSys.TryStartPull(puller, ToServer(Target.Value)),
|
||||
$"{puller} failed to start pulling {Target}");
|
||||
});
|
||||
|
||||
var list = new HashSet<EntityUid>();
|
||||
Server.System<SharedInteractionSystem>()
|
||||
.GetEntitiesInteractingWithTarget(ToServer(Target.Value), list);
|
||||
Assert.That(list, Is.EquivalentTo([puller]), $"{puller} was not considered to be interacting with {Target}");
|
||||
}
|
||||
}
|
||||
|
|
@ -17,19 +17,21 @@ public sealed class SmartFridgeInteractionTest : InteractionTest
|
|||
parent: PillCanister
|
||||
id: {SampleDumpableAndInsertableId}
|
||||
components:
|
||||
- type: StorageFill
|
||||
contents:
|
||||
- id: PillCopper
|
||||
amount: 5
|
||||
- type: EntityTableContainerFill
|
||||
containers:
|
||||
storagebase:
|
||||
id: PillCopper
|
||||
amount: 5
|
||||
|
||||
- type: entity
|
||||
parent: ChemBag
|
||||
id: {SampleDumpableId}
|
||||
components:
|
||||
- type: StorageFill
|
||||
contents:
|
||||
- id: PillCopper
|
||||
amount: 5
|
||||
- type: EntityTableContainerFill
|
||||
containers:
|
||||
storagebase:
|
||||
id: PillCopper
|
||||
amount: 5
|
||||
";
|
||||
|
||||
[Test]
|
||||
|
|
|
|||
263
Content.IntegrationTests/Tests/Storage/StorageTest.cs
Normal file
263
Content.IntegrationTests/Tests/Storage/StorageTest.cs
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Shared.Containers;
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Prototypes;
|
||||
using Content.Shared.Storage;
|
||||
using Content.Shared.Storage.Components;
|
||||
using Content.Shared.Storage.EntitySystems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Storage;
|
||||
|
||||
public sealed class StorageTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Can an item store more than itself weighs.
|
||||
/// In an ideal world this test wouldn't need to exist because sizes would be recursive.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task StorageSizeArbitrageTest()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var protoManager = server.ResolveDependency<IPrototypeManager>();
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
|
||||
var itemSys = entMan.System<SharedItemSystem>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (!proto.TryGetComponent<StorageComponent>("Storage", out var storage) ||
|
||||
storage.Whitelist != null ||
|
||||
storage.MaxItemSize == null ||
|
||||
!proto.TryGetComponent<ItemComponent>("Item", out var item))
|
||||
continue;
|
||||
|
||||
Assert.That(itemSys.GetSizePrototype(storage.MaxItemSize.Value).Weight,
|
||||
Is.LessThanOrEqualTo(itemSys.GetSizePrototype(item.Size).Weight),
|
||||
$"Found storage arbitrage on {proto.ID}");
|
||||
}
|
||||
});
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestStorageFillPrototypes()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var protoManager = server.ResolveDependency<IPrototypeManager>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (!proto.TryGetComponent<StorageFillComponent>("StorageFill", out var storage))
|
||||
continue;
|
||||
|
||||
foreach (var entry in storage.Contents)
|
||||
{
|
||||
Assert.That(entry.Amount, Is.GreaterThan(0), $"Specified invalid amount of {entry.Amount} for prototype {proto.ID}");
|
||||
Assert.That(entry.SpawnProbability, Is.GreaterThan(0), $"Specified invalid probability of {entry.SpawnProbability} for prototype {proto.ID}");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestSufficientSpaceForFill()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var compFact = server.ResolveDependency<IComponentFactory>();
|
||||
var id = compFact.GetComponentName<StorageFillComponent>();
|
||||
|
||||
var itemSys = entMan.System<SharedItemSystem>();
|
||||
|
||||
var allSizes = protoMan.EnumeratePrototypes<ItemSizePrototype>().ToList();
|
||||
allSizes.Sort();
|
||||
|
||||
await Assert.MultipleAsync(async () =>
|
||||
{
|
||||
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<StorageFillComponent>())
|
||||
{
|
||||
if (proto.HasComponent<EntityStorageComponent>(compFact))
|
||||
continue;
|
||||
|
||||
StorageComponent? storage = null;
|
||||
ItemComponent? item = null;
|
||||
var size = 0;
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
if (!proto.TryGetComponent("Storage", out storage))
|
||||
{
|
||||
Assert.Fail($"Entity {proto.ID} has storage-fill without a storage component!");
|
||||
return;
|
||||
}
|
||||
|
||||
proto.TryGetComponent("Item", out item);
|
||||
size = GetFillSize(fill, false, protoMan, itemSys);
|
||||
});
|
||||
|
||||
if (storage == null)
|
||||
continue;
|
||||
|
||||
var maxSize = storage.MaxItemSize;
|
||||
if (storage.MaxItemSize == null)
|
||||
{
|
||||
if (item?.Size == null)
|
||||
{
|
||||
maxSize = SharedStorageSystem.DefaultStorageMaxItemSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
var curIndex = allSizes.IndexOf(protoMan.Index(item.Size));
|
||||
var index = Math.Max(0, curIndex - 1);
|
||||
maxSize = allSizes[index].ID;
|
||||
}
|
||||
}
|
||||
|
||||
if (maxSize == null)
|
||||
continue;
|
||||
|
||||
Assert.That(size, Is.LessThanOrEqualTo(storage.Grid.GetArea()), $"{proto.ID} storage fill is too large.");
|
||||
|
||||
foreach (var entry in fill.Contents)
|
||||
{
|
||||
if (entry.PrototypeId == null)
|
||||
continue;
|
||||
|
||||
if (!protoMan.TryIndex<EntityPrototype>(entry.PrototypeId, out var fillItem))
|
||||
continue;
|
||||
|
||||
ItemComponent? entryItem = null;
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
fillItem.TryGetComponent("Item", out entryItem);
|
||||
});
|
||||
|
||||
if (entryItem == null)
|
||||
continue;
|
||||
|
||||
Assert.That(protoMan.Index(entryItem.Size).Weight,
|
||||
Is.LessThanOrEqualTo(protoMan.Index(maxSize.Value).Weight),
|
||||
$"Entity {proto.ID} has storage-fill item, {entry.PrototypeId}, that is too large");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestSufficientSpaceForEntityStorageFill()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var compFact = server.ResolveDependency<IComponentFactory>();
|
||||
var id = compFact.GetComponentName<StorageFillComponent>();
|
||||
|
||||
var itemSys = entMan.System<SharedItemSystem>();
|
||||
|
||||
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<StorageFillComponent>())
|
||||
{
|
||||
if (proto.HasComponent<StorageComponent>(compFact))
|
||||
continue;
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
if (!proto.TryGetComponent("EntityStorage", out EntityStorageComponent? entStorage))
|
||||
Assert.Fail($"Entity {proto.ID} has storage-fill without a storage component!");
|
||||
|
||||
if (entStorage == null)
|
||||
return;
|
||||
|
||||
var size = GetFillSize(fill, true, protoMan, itemSys);
|
||||
Assert.That(size, Is.LessThanOrEqualTo(entStorage.Capacity),
|
||||
$"{proto.ID} storage fill is too large.");
|
||||
});
|
||||
}
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
private int GetEntrySize(EntitySpawnEntry entry, bool getCount, IPrototypeManager protoMan, SharedItemSystem itemSystem)
|
||||
{
|
||||
if (entry.PrototypeId == null)
|
||||
return 0;
|
||||
|
||||
if (!protoMan.TryIndex<EntityPrototype>(entry.PrototypeId, out var proto))
|
||||
{
|
||||
Assert.Fail($"Unknown prototype: {entry.PrototypeId}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (getCount)
|
||||
return entry.Amount;
|
||||
|
||||
|
||||
if (proto.TryGetComponent<ItemComponent>("Item", out var item))
|
||||
return itemSystem.GetItemShape(item).GetArea() * entry.Amount;
|
||||
|
||||
Assert.Fail($"Prototype is missing item comp: {entry.PrototypeId}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int GetFillSize(StorageFillComponent fill, bool getCount, IPrototypeManager protoMan, SharedItemSystem itemSystem)
|
||||
{
|
||||
var totalSize = 0;
|
||||
var groups = new Dictionary<string, int>();
|
||||
foreach (var entry in fill.Contents)
|
||||
{
|
||||
var size = GetEntrySize(entry, getCount, protoMan, itemSystem);
|
||||
|
||||
if (entry.GroupId == null)
|
||||
totalSize += size;
|
||||
else
|
||||
groups[entry.GroupId] = Math.Max(size, groups.GetValueOrDefault(entry.GroupId));
|
||||
}
|
||||
|
||||
return totalSize + groups.Values.Sum();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that prototypes are not using multiple container fill components at the same time.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task NoMultipleContainerFillsTest()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var compFact = pair.Server.ResolveDependency<IComponentFactory>();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<EntityTableContainerFillComponent>())
|
||||
{
|
||||
Assert.That(!proto.HasComponent<StorageFillComponent>(compFact), $"Prototype {proto.ID} has both {nameof(EntityTableContainerFillComponent)} and {nameof(StorageFillComponent)}.");
|
||||
Assert.That(!proto.HasComponent<ContainerFillComponent>(compFact), $"Prototype {proto.ID} has both {nameof(EntityTableContainerFillComponent)} and {nameof(ContainerFillComponent)}.");
|
||||
}
|
||||
|
||||
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<ContainerFillComponent>())
|
||||
{
|
||||
Assert.That(!proto.HasComponent<StorageFillComponent>(compFact), $"Prototype {proto.ID} has both {nameof(ContainerFillComponent)} and {nameof(StorageFillComponent)}.");
|
||||
}
|
||||
});
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,240 +0,0 @@
|
|||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Server.Storage.Components;
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Prototypes;
|
||||
using Content.Shared.Storage;
|
||||
using Content.Shared.Storage.Components;
|
||||
using Content.Shared.Storage.EntitySystems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public sealed class StorageTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Can an item store more than itself weighs.
|
||||
/// In an ideal world this test wouldn't need to exist because sizes would be recursive.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task StorageSizeArbitrageTest()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var protoManager = server.ResolveDependency<IPrototypeManager>();
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
|
||||
var itemSys = entMan.System<SharedItemSystem>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (!proto.TryGetComponent<StorageComponent>("Storage", out var storage) ||
|
||||
storage.Whitelist != null ||
|
||||
storage.MaxItemSize == null ||
|
||||
!proto.TryGetComponent<ItemComponent>("Item", out var item))
|
||||
continue;
|
||||
|
||||
Assert.That(itemSys.GetSizePrototype(storage.MaxItemSize.Value).Weight,
|
||||
Is.LessThanOrEqualTo(itemSys.GetSizePrototype(item.Size).Weight),
|
||||
$"Found storage arbitrage on {proto.ID}");
|
||||
}
|
||||
});
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestStorageFillPrototypes()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var protoManager = server.ResolveDependency<IPrototypeManager>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (!proto.TryGetComponent<StorageFillComponent>("StorageFill", out var storage))
|
||||
continue;
|
||||
|
||||
foreach (var entry in storage.Contents)
|
||||
{
|
||||
Assert.That(entry.Amount, Is.GreaterThan(0), $"Specified invalid amount of {entry.Amount} for prototype {proto.ID}");
|
||||
Assert.That(entry.SpawnProbability, Is.GreaterThan(0), $"Specified invalid probability of {entry.SpawnProbability} for prototype {proto.ID}");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestSufficientSpaceForFill()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var compFact = server.ResolveDependency<IComponentFactory>();
|
||||
var id = compFact.GetComponentName<StorageFillComponent>();
|
||||
|
||||
var itemSys = entMan.System<SharedItemSystem>();
|
||||
|
||||
var allSizes = protoMan.EnumeratePrototypes<ItemSizePrototype>().ToList();
|
||||
allSizes.Sort();
|
||||
|
||||
await Assert.MultipleAsync(async () =>
|
||||
{
|
||||
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<StorageFillComponent>())
|
||||
{
|
||||
if (proto.HasComponent<EntityStorageComponent>(compFact))
|
||||
continue;
|
||||
|
||||
StorageComponent? storage = null;
|
||||
ItemComponent? item = null;
|
||||
var size = 0;
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
if (!proto.TryGetComponent("Storage", out storage))
|
||||
{
|
||||
Assert.Fail($"Entity {proto.ID} has storage-fill without a storage component!");
|
||||
return;
|
||||
}
|
||||
|
||||
proto.TryGetComponent("Item", out item);
|
||||
size = GetFillSize(fill, false, protoMan, itemSys);
|
||||
});
|
||||
|
||||
if (storage == null)
|
||||
continue;
|
||||
|
||||
var maxSize = storage.MaxItemSize;
|
||||
if (storage.MaxItemSize == null)
|
||||
{
|
||||
if (item?.Size == null)
|
||||
{
|
||||
maxSize = SharedStorageSystem.DefaultStorageMaxItemSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
var curIndex = allSizes.IndexOf(protoMan.Index(item.Size));
|
||||
var index = Math.Max(0, curIndex - 1);
|
||||
maxSize = allSizes[index].ID;
|
||||
}
|
||||
}
|
||||
|
||||
if (maxSize == null)
|
||||
continue;
|
||||
|
||||
Assert.That(size, Is.LessThanOrEqualTo(storage.Grid.GetArea()), $"{proto.ID} storage fill is too large.");
|
||||
|
||||
foreach (var entry in fill.Contents)
|
||||
{
|
||||
if (entry.PrototypeId == null)
|
||||
continue;
|
||||
|
||||
if (!protoMan.TryIndex<EntityPrototype>(entry.PrototypeId, out var fillItem))
|
||||
continue;
|
||||
|
||||
ItemComponent? entryItem = null;
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
fillItem.TryGetComponent("Item", out entryItem);
|
||||
});
|
||||
|
||||
if (entryItem == null)
|
||||
continue;
|
||||
|
||||
Assert.That(protoMan.Index(entryItem.Size).Weight,
|
||||
Is.LessThanOrEqualTo(protoMan.Index(maxSize.Value).Weight),
|
||||
$"Entity {proto.ID} has storage-fill item, {entry.PrototypeId}, that is too large");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestSufficientSpaceForEntityStorageFill()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var compFact = server.ResolveDependency<IComponentFactory>();
|
||||
var id = compFact.GetComponentName<StorageFillComponent>();
|
||||
|
||||
var itemSys = entMan.System<SharedItemSystem>();
|
||||
|
||||
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<StorageFillComponent>())
|
||||
{
|
||||
if (proto.HasComponent<StorageComponent>(compFact))
|
||||
continue;
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
if (!proto.TryGetComponent("EntityStorage", out EntityStorageComponent? entStorage))
|
||||
Assert.Fail($"Entity {proto.ID} has storage-fill without a storage component!");
|
||||
|
||||
if (entStorage == null)
|
||||
return;
|
||||
|
||||
var size = GetFillSize(fill, true, protoMan, itemSys);
|
||||
Assert.That(size, Is.LessThanOrEqualTo(entStorage.Capacity),
|
||||
$"{proto.ID} storage fill is too large.");
|
||||
});
|
||||
}
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
private int GetEntrySize(EntitySpawnEntry entry, bool getCount, IPrototypeManager protoMan, SharedItemSystem itemSystem)
|
||||
{
|
||||
if (entry.PrototypeId == null)
|
||||
return 0;
|
||||
|
||||
if (!protoMan.TryIndex<EntityPrototype>(entry.PrototypeId, out var proto))
|
||||
{
|
||||
Assert.Fail($"Unknown prototype: {entry.PrototypeId}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (getCount)
|
||||
return entry.Amount;
|
||||
|
||||
|
||||
if (proto.TryGetComponent<ItemComponent>("Item", out var item))
|
||||
return itemSystem.GetItemShape(item).GetArea() * entry.Amount;
|
||||
|
||||
Assert.Fail($"Prototype is missing item comp: {entry.PrototypeId}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int GetFillSize(StorageFillComponent fill, bool getCount, IPrototypeManager protoMan, SharedItemSystem itemSystem)
|
||||
{
|
||||
var totalSize = 0;
|
||||
var groups = new Dictionary<string, int>();
|
||||
foreach (var entry in fill.Contents)
|
||||
{
|
||||
var size = GetEntrySize(entry, getCount, protoMan, itemSystem);
|
||||
|
||||
if (entry.GroupId == null)
|
||||
totalSize += size;
|
||||
else
|
||||
groups[entry.GroupId] = Math.Max(size, groups.GetValueOrDefault(entry.GroupId));
|
||||
}
|
||||
|
||||
return totalSize + groups.Values.Sum();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,11 +3,13 @@ using System.Collections.Generic;
|
|||
using Content.Server.VendingMachines;
|
||||
using Content.Server.Wires;
|
||||
using Content.Shared.Cargo.Prototypes;
|
||||
using Content.Shared.Containers;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.EntityTable;
|
||||
using Content.Shared.Prototypes;
|
||||
using Content.Shared.Storage.Components;
|
||||
using Content.Shared.Storage.EntitySystems;
|
||||
using Content.Shared.VendingMachines;
|
||||
using Content.Shared.Wires;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
|
@ -115,6 +117,7 @@ namespace Content.IntegrationTests.Tests
|
|||
|
||||
var prototypeManager = server.ResolveDependency<IPrototypeManager>();
|
||||
var compFact = server.ResolveDependency<IComponentFactory>();
|
||||
var entityTable = server.EntMan.System<EntityTableSystem>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
|
|
@ -134,17 +137,23 @@ namespace Content.IntegrationTests.Tests
|
|||
restocks.Add(proto.ID);
|
||||
}
|
||||
|
||||
// Collect all the prototypes with StorageFills referencing those entities.
|
||||
// Collect all the prototypes with EntityTableContainerFills referencing those entities.
|
||||
foreach (var proto in prototypeManager.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (!proto.TryGetComponent<StorageFillComponent>(out var storage, compFact))
|
||||
if (!proto.TryGetComponent<EntityTableContainerFillComponent>(out var storage, compFact))
|
||||
continue;
|
||||
|
||||
var containers = storage.Containers;
|
||||
|
||||
if (!containers.TryGetValue(SharedEntityStorageSystem.ContainerName, out var container)) // We only care about this container type.
|
||||
continue;
|
||||
|
||||
List<string> restockStore = new();
|
||||
foreach (var spawnEntry in storage.Contents)
|
||||
|
||||
foreach (var spawnEntry in entityTable.GetSpawns(container))
|
||||
{
|
||||
if (spawnEntry.PrototypeId != null && restocks.Contains(spawnEntry.PrototypeId))
|
||||
restockStore.Add(spawnEntry.PrototypeId);
|
||||
if (restocks.Contains(spawnEntry))
|
||||
restockStore.Add(spawnEntry);
|
||||
}
|
||||
|
||||
if (restockStore.Count > 0)
|
||||
|
|
@ -153,7 +162,7 @@ namespace Content.IntegrationTests.Tests
|
|||
|
||||
// Iterate through every CargoProduct and make sure each
|
||||
// prototype with a restock component is referenced in a
|
||||
// purchaseable entity with a StorageFill.
|
||||
// purchaseable entity with an EntityTableContianerFill.
|
||||
foreach (var proto in prototypeManager.EnumeratePrototypes<CargoProductPrototype>())
|
||||
{
|
||||
if (restockStores.ContainsKey(proto.Product))
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using Content.Shared.Access.Components;
|
|||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Kitchen;
|
||||
using Content.Shared.Popups;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ namespace Content.Server.Administration.Commands
|
|||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
_roundEndSystem.CancelRoundEndCountdown(shell.Player?.AttachedEntity, false);
|
||||
_roundEndSystem.CancelRoundEndCountdown(shell.Player?.AttachedEntity, forceRecall: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
namespace Content.Server.Administration.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class AdminMinigunComponent : Component
|
||||
{
|
||||
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
using Content.Shared.Administration.Components;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Server.Administration.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class KillSignComponent : SharedKillSignComponent;
|
||||
|
|
@ -6,7 +6,8 @@ namespace Content.Server.Administration.Components;
|
|||
/// <summary>
|
||||
/// Component to track the timer for the SuperBonk smite.
|
||||
/// </summary>
|
||||
[RegisterComponent, AutoGenerateComponentPause, Access(typeof(SuperBonkSystem))]
|
||||
[RegisterComponent, AutoGenerateComponentPause]
|
||||
[Access(typeof(SuperBonkSystem))]
|
||||
public sealed partial class SuperBonkComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ using Content.Shared.Verbs;
|
|||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using Content.Shared.Roles.Components;
|
||||
|
||||
namespace Content.Server.Administration.Systems;
|
||||
|
||||
|
|
@ -29,6 +30,7 @@ public sealed partial class AdminVerbSystem
|
|||
private static readonly EntProtoId DefaultThiefRule = "Thief";
|
||||
private static readonly EntProtoId DefaultChangelingRule = "Changeling";
|
||||
private static readonly EntProtoId ParadoxCloneRuleId = "ParadoxCloneSpawn";
|
||||
private static readonly EntProtoId DefaultWizardRule = "Wizard";
|
||||
private static readonly ProtoId<StartingGearPrototype> PirateGearId = "PirateGear";
|
||||
|
||||
// All antag verbs have names so invokeverb works.
|
||||
|
|
@ -189,6 +191,22 @@ public sealed partial class AdminVerbSystem
|
|||
Message = string.Join(": ", paradoxCloneName, Loc.GetString("admin-verb-make-paradox-clone")),
|
||||
};
|
||||
|
||||
var wizardName = Loc.GetString("admin-verb-text-make-wizard");
|
||||
Verb wizard = new()
|
||||
{
|
||||
Text = wizardName,
|
||||
Category = VerbCategory.Antag,
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "Wizard"),
|
||||
Act = () =>
|
||||
{
|
||||
// Wizard has no rule components as of writing, but I gotta put something here to satisfy the machine so just make it wizard mind rule :)
|
||||
_antag.ForceMakeAntag<WizardRoleComponent>(targetPlayer, DefaultWizardRule);
|
||||
},
|
||||
Impact = LogImpact.High,
|
||||
Message = string.Join(": ", wizardName, Loc.GetString("admin-verb-make-wizard")),
|
||||
};
|
||||
args.Verbs.Add(wizard);
|
||||
|
||||
if (HasComp<HumanoidAppearanceComponent>(args.Target)) // only humanoids can be cloned
|
||||
args.Verbs.Add(paradox);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Content.Server.Administration.Components;
|
||||
using System.Numerics;
|
||||
using System.Threading;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.Electrocution;
|
||||
|
|
@ -17,12 +18,14 @@ using Content.Server.Tabletop.Components;
|
|||
using Content.Shared.Actions;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Administration.Components;
|
||||
using Content.Shared.Administration.Systems;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Body.Part;
|
||||
using Content.Shared.Clothing.Components;
|
||||
using Content.Shared.Clumsy;
|
||||
using Content.Shared.Cluwne;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Electrocution;
|
||||
|
|
@ -54,9 +57,6 @@ using Robust.Shared.Prototypes;
|
|||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Spawners;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Numerics;
|
||||
using System.Threading;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Timer = Robust.Shared.Timing.Timer;
|
||||
|
||||
namespace Content.Server.Administration.Systems;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Server.Administration.Components;
|
||||
using Content.Server.Cargo.Components;
|
||||
using Content.Server.Doors.Systems;
|
||||
using Content.Server.Hands.Systems;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Server.Stack;
|
||||
using Content.Server.Station.Systems;
|
||||
|
|
@ -14,10 +12,11 @@ using Content.Shared.Access;
|
|||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Administration.Components;
|
||||
using Content.Shared.Administration.Systems;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Construction.Components;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Doors.Components;
|
||||
|
|
@ -25,6 +24,7 @@ using Content.Shared.Hands.Components;
|
|||
using Content.Shared.Inventory;
|
||||
using Content.Shared.PDA;
|
||||
using Content.Shared.Power.Components;
|
||||
using Content.Shared.Power.EntitySystems;
|
||||
using Content.Shared.Stacks;
|
||||
using Content.Shared.Station.Components;
|
||||
using Content.Shared.Verbs;
|
||||
|
|
@ -52,6 +52,7 @@ public sealed partial class AdminVerbSystem
|
|||
[Dependency] private readonly StationJobsSystem _stationJobsSystem = default!;
|
||||
[Dependency] private readonly JointSystem _jointSystem = default!;
|
||||
[Dependency] private readonly BatterySystem _batterySystem = default!;
|
||||
[Dependency] private readonly PredictedBatterySystem _predictedBatterySystem = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
|
||||
[Dependency] private readonly GunSystem _gun = default!;
|
||||
|
||||
|
|
@ -160,6 +161,57 @@ public sealed partial class AdminVerbSystem
|
|||
args.Verbs.Add(makeVulnerable);
|
||||
}
|
||||
|
||||
if (TryComp<PredictedBatteryComponent>(args.Target, out var pBattery))
|
||||
{
|
||||
Verb refillBattery = new()
|
||||
{
|
||||
Text = Loc.GetString("admin-verbs-refill-battery"),
|
||||
Category = VerbCategory.Tricks,
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/fill_battery.png")),
|
||||
Act = () =>
|
||||
{
|
||||
_predictedBatterySystem.SetCharge((args.Target, pBattery), pBattery.MaxCharge);
|
||||
},
|
||||
Impact = LogImpact.Medium,
|
||||
Message = Loc.GetString("admin-trick-refill-battery-description"),
|
||||
Priority = (int)TricksVerbPriorities.RefillBattery,
|
||||
};
|
||||
args.Verbs.Add(refillBattery);
|
||||
|
||||
Verb drainBattery = new()
|
||||
{
|
||||
Text = Loc.GetString("admin-verbs-drain-battery"),
|
||||
Category = VerbCategory.Tricks,
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/drain_battery.png")),
|
||||
Act = () =>
|
||||
{
|
||||
_predictedBatterySystem.SetCharge((args.Target, pBattery), 0);
|
||||
},
|
||||
Impact = LogImpact.Medium,
|
||||
Priority = (int)TricksVerbPriorities.DrainBattery,
|
||||
};
|
||||
args.Verbs.Add(drainBattery);
|
||||
|
||||
Verb infiniteBattery = new()
|
||||
{
|
||||
Text = Loc.GetString("admin-verbs-infinite-battery"),
|
||||
Category = VerbCategory.Tricks,
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/infinite_battery.png")),
|
||||
Act = () =>
|
||||
{
|
||||
var recharger = EnsureComp<PredictedBatterySelfRechargerComponent>(args.Target);
|
||||
recharger.AutoRechargeRate = pBattery.MaxCharge; // Instant refill.
|
||||
recharger.AutoRechargePauseTime = TimeSpan.Zero; // No delay.
|
||||
Dirty(args.Target, recharger);
|
||||
_predictedBatterySystem.RefreshChargeRate((args.Target, pBattery));
|
||||
},
|
||||
Impact = LogImpact.Medium,
|
||||
Message = Loc.GetString("admin-trick-infinite-battery-object-description"),
|
||||
Priority = (int)TricksVerbPriorities.InfiniteBattery,
|
||||
};
|
||||
args.Verbs.Add(infiniteBattery);
|
||||
}
|
||||
|
||||
if (TryComp<BatteryComponent>(args.Target, out var battery))
|
||||
{
|
||||
Verb refillBattery = new()
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
using System.Numerics;
|
||||
using Content.Server.Administration.Components;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Administration.Components;
|
||||
using Content.Shared.Administration.Systems;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Administration.Systems;
|
||||
|
||||
public sealed class BufferingSystem : EntitySystem
|
||||
public sealed class BufferingSystem : SharedBufferingSystem
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
|
||||
|
|
|
|||
|
|
@ -52,12 +52,12 @@ public sealed class BlockGameArcadeSystem : EntitySystem
|
|||
private void OnAfterUIOpen(EntityUid uid, BlockGameArcadeComponent component, AfterActivatableUIOpenEvent args)
|
||||
{
|
||||
if (component.Player == null)
|
||||
component.Player = args.Actor;
|
||||
component.Player = args.User;
|
||||
else
|
||||
component.Spectators.Add(args.Actor);
|
||||
component.Spectators.Add(args.User);
|
||||
|
||||
UpdatePlayerStatus(uid, args.Actor, component);
|
||||
component.Game?.UpdateNewPlayerUI(args.Actor);
|
||||
UpdatePlayerStatus(uid, args.User, component);
|
||||
component.Game?.UpdateNewPlayerUI(args.User);
|
||||
}
|
||||
|
||||
private void OnAfterUiClose(EntityUid uid, BlockGameArcadeComponent component, BoundUIClosedEvent args)
|
||||
|
|
|
|||
|
|
@ -225,20 +225,14 @@ namespace Content.Server.Atmos.EntitySystems
|
|||
mass2 = otherPhys.Mass;
|
||||
}
|
||||
|
||||
// when the thing on fire is more massive than the other, the following happens:
|
||||
// - the thing on fire loses a small number of firestacks
|
||||
// - the other thing gains a large number of firestacks
|
||||
// so a person on fire engulfs a mouse, but an engulfed mouse barely does anything to a person
|
||||
var total = mass1 + mass2;
|
||||
var avg = (flammable.FireStacks + otherFlammable.FireStacks) / total;
|
||||
// Get the average of both entity's firestacks * mass
|
||||
// Then for each entity, we divide the average by their mass and set their firestacks to that value
|
||||
// An entity with a higher mass will lose some fire and transfer it to the one with lower mass.
|
||||
var avg = (flammable.FireStacks * mass1 + otherFlammable.FireStacks * mass2) / 2f;
|
||||
|
||||
// swap the entity losing stacks depending on whichever has the most firestack kilos
|
||||
var (src, dest) = flammable.FireStacks * mass1 > otherFlammable.FireStacks * mass2
|
||||
? (-1f, 1f)
|
||||
: (1f, -1f);
|
||||
// bring each entity to the same firestack mass, firestacks being scaled by the other's mass
|
||||
AdjustFireStacks(uid, src * avg * mass2, flammable, ignite: true);
|
||||
AdjustFireStacks(otherUid, dest * avg * mass1, otherFlammable, ignite: true);
|
||||
// bring each entity to the same firestack mass, firestack amount is scaled by the inverse of the entity's mass
|
||||
SetFireStacks(uid, avg / mass1, flammable, ignite: true);
|
||||
SetFireStacks(otherUid, avg / mass2, otherFlammable, ignite: true);
|
||||
}
|
||||
|
||||
private void OnIsHot(EntityUid uid, FlammableComponent flammable, IsHotEvent args)
|
||||
|
|
|
|||
|
|
@ -156,18 +156,18 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
|
|||
|
||||
private void OnSelectGasMessage(EntityUid uid, GasFilterComponent filter, GasFilterSelectGasMessage args)
|
||||
{
|
||||
if (args.ID.HasValue)
|
||||
if (args.Gas.HasValue)
|
||||
{
|
||||
if (Enum.TryParse<Gas>(args.ID.ToString(), true, out var parsedGas))
|
||||
if (Enum.IsDefined(typeof(Gas), args.Gas))
|
||||
{
|
||||
filter.FilteredGas = parsedGas;
|
||||
filter.FilteredGas = args.Gas;
|
||||
_adminLogger.Add(LogType.AtmosFilterChanged, LogImpact.Medium,
|
||||
$"{ToPrettyString(args.Actor):player} set the filter on {ToPrettyString(uid):device} to {parsedGas.ToString()}");
|
||||
$"{ToPrettyString(args.Actor):player} set the filter on {ToPrettyString(uid):device} to {args.Gas.ToString()}");
|
||||
DirtyUI(uid, filter);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning($"{ToPrettyString(uid)} received GasFilterSelectGasMessage with an invalid ID: {args.ID}");
|
||||
Log.Warning($"{ToPrettyString(uid)} received GasFilterSelectGasMessage with an invalid ID: {args.Gas}");
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
|
|||
|
|
@ -54,11 +54,13 @@ public sealed class SpaceHeaterSystem : EntitySystem
|
|||
|
||||
private void OnUIActivationAttempt(EntityUid uid, SpaceHeaterComponent spaceHeater, ActivatableUIOpenAttemptEvent args)
|
||||
{
|
||||
if (!Comp<TransformComponent>(uid).Anchored)
|
||||
{
|
||||
if (Comp<TransformComponent>(uid).Anchored)
|
||||
return;
|
||||
|
||||
if (!args.Silent)
|
||||
_popup.PopupEntity(Loc.GetString("comp-space-heater-unanchored", ("device", Loc.GetString("comp-space-heater-device-name"))), uid, args.User);
|
||||
args.Cancel();
|
||||
}
|
||||
|
||||
args.Cancel();
|
||||
}
|
||||
|
||||
private void OnDeviceUpdated(EntityUid uid, SpaceHeaterComponent spaceHeater, ref AtmosDeviceUpdateEvent args)
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
using Content.Shared.Bed;
|
||||
using Content.Shared.Bed.Components;
|
||||
using Content.Shared.Bed.Sleep;
|
||||
using Content.Shared.Buckle.Components;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
|
||||
namespace Content.Server.Bed
|
||||
{
|
||||
public sealed class BedSystem : SharedBedSystem
|
||||
{
|
||||
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
|
||||
|
||||
private EntityQuery<SleepingComponent> _sleepingQuery;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_sleepingQuery = GetEntityQuery<SleepingComponent>();
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var query = EntityQueryEnumerator<HealOnBuckleHealingComponent, HealOnBuckleComponent, StrapComponent>();
|
||||
while (query.MoveNext(out var uid, out _, out var bedComponent, out var strapComponent))
|
||||
{
|
||||
if (Timing.CurTime < bedComponent.NextHealTime)
|
||||
continue;
|
||||
|
||||
bedComponent.NextHealTime += TimeSpan.FromSeconds(bedComponent.HealTime);
|
||||
|
||||
if (strapComponent.BuckledEntities.Count == 0)
|
||||
continue;
|
||||
|
||||
foreach (var healedEntity in strapComponent.BuckledEntities)
|
||||
{
|
||||
if (_mobStateSystem.IsDead(healedEntity))
|
||||
continue;
|
||||
|
||||
var damage = bedComponent.Damage;
|
||||
|
||||
if (_sleepingQuery.HasComp(healedEntity))
|
||||
damage *= bedComponent.SleepMultiplier;
|
||||
|
||||
_damageableSystem.TryChangeDamage(healedEntity, damage, true, origin: uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -37,7 +37,10 @@ public sealed class BloodstreamSystem : SharedBloodstreamSystem
|
|||
|
||||
// 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);
|
||||
var solution = entity.Comp.BloodReagents.Clone();
|
||||
solution.ScaleTo(entity.Comp.BloodMaxVolume - bloodSolution.Volume);
|
||||
solution.SetReagentData(GetEntityBloodData(entity.Owner));
|
||||
bloodSolution.AddSolution(solution, PrototypeManager);
|
||||
}
|
||||
|
||||
// forensics is not predicted yet
|
||||
|
|
|
|||
|
|
@ -367,7 +367,7 @@ public sealed class RespiratorSystem : EntitySystem
|
|||
if (ent.Comp.SuffocationCycles == 2)
|
||||
_adminLogger.Add(LogType.Asphyxiation, $"{ToPrettyString(ent):entity} started suffocating");
|
||||
|
||||
_damageableSys.ChangeDamage(ent.Owner, ent.Comp.Damage, interruptsDoAfters: false);
|
||||
_damageableSys.ChangeDamage(ent.Owner, ent.Comp.Damage, interruptsDoAfters: false, ignoreResistances: true);
|
||||
|
||||
if (ent.Comp.SuffocationCycles < ent.Comp.SuffocationCycleThreshold)
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -270,6 +270,12 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||
if (!_critLoocEnabled && _mobStateSystem.IsCritical(source))
|
||||
return;
|
||||
|
||||
// Systems can differentiate Looc and DeadChat by type, and cancel the speak attempt if necessary.
|
||||
var ev = new InGameOocMessageAttemptEvent(player, sendType);
|
||||
RaiseLocalEvent(source, ref ev, true);
|
||||
if (ev.Cancelled)
|
||||
return;
|
||||
|
||||
switch (sendType)
|
||||
{
|
||||
case InGameOOCChatType.Dead:
|
||||
|
|
@ -419,18 +425,18 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||
if (originalMessage == message)
|
||||
{
|
||||
if (name != Name(source))
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {ToPrettyString(source):user} as {name}: {originalMessage}.");
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {source} as {name}: {originalMessage}.");
|
||||
else
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {ToPrettyString(source):user}: {originalMessage}.");
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {source}: {originalMessage}.");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (name != Name(source))
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low,
|
||||
$"Say from {ToPrettyString(source):user} as {name}, original: {originalMessage}, transformed: {message}.");
|
||||
$"Say from {source} as {name}, original: {originalMessage}, transformed: {message}.");
|
||||
else
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low,
|
||||
$"Say from {ToPrettyString(source):user}, original: {originalMessage}, transformed: {message}.");
|
||||
$"Say from {source}, original: {originalMessage}, transformed: {message}.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -508,18 +514,18 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||
if (originalMessage == message)
|
||||
{
|
||||
if (name != Name(source))
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {ToPrettyString(source):user} as {name}: {originalMessage}.");
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {source} as {name}: {originalMessage}.");
|
||||
else
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {ToPrettyString(source):user}: {originalMessage}.");
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {source}: {originalMessage}.");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (name != Name(source))
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low,
|
||||
$"Whisper from {ToPrettyString(source):user} as {name}, original: {originalMessage}, transformed: {message}.");
|
||||
$"Whisper from {source} as {name}, original: {originalMessage}, transformed: {message}.");
|
||||
else
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low,
|
||||
$"Whisper from {ToPrettyString(source):user}, original: {originalMessage}, transformed: {message}.");
|
||||
$"Whisper from {source}, original: {originalMessage}, transformed: {message}.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -554,9 +560,9 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||
SendInVoiceRange(ChatChannel.Emotes, action, wrappedMessage, source, range, author);
|
||||
if (!hideLog)
|
||||
if (name != Name(source))
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {ToPrettyString(source):user} as {name}: {action}");
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {source} as {name}: {action}");
|
||||
else
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {ToPrettyString(source):user}: {action}");
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {source}: {action}");
|
||||
}
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
|
|
@ -579,7 +585,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||
("message", FormattedMessage.EscapeText(message)));
|
||||
|
||||
SendInVoiceRange(ChatChannel.LOOC, message, wrappedMessage, source, hideChat ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal, player.UserId);
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"LOOC from {player:Player}: {message}");
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"LOOC from {source}: {message}");
|
||||
}
|
||||
|
||||
private void SendDeadChat(EntityUid source, ICommonSession player, string message, bool hideChat)
|
||||
|
|
@ -593,7 +599,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")),
|
||||
("userName", player.Channel.UserName),
|
||||
("message", FormattedMessage.EscapeText(message)));
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Admin dead chat from {player:Player}: {message}");
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Admin dead chat from {source}: {message}");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -601,7 +607,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||
("deadChannelName", Loc.GetString("chat-manager-dead-channel-name")),
|
||||
("playerName", (playerName)),
|
||||
("message", FormattedMessage.EscapeText(message)));
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Dead chat from {player:Player}: {message}");
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Dead chat from {source}: {message}");
|
||||
}
|
||||
|
||||
_chatManager.ChatMessageToMany(ChatChannel.Dead, message, wrappedMessage, source, hideChat, true, clients.ToList(), author: player.UserId);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ using Content.Shared.Implants;
|
|||
using Content.Shared.Implants.Components;
|
||||
using Content.Shared.NameModifier.EntitySystems;
|
||||
using Content.Shared.StatusEffect;
|
||||
using Content.Shared.StatusEffectNew.Components;
|
||||
using Content.Shared.Storage;
|
||||
using Content.Shared.Storage.EntitySystems;
|
||||
using Content.Shared.Whitelist;
|
||||
|
|
@ -36,6 +37,7 @@ public sealed partial class CloningSystem : SharedCloningSystem
|
|||
[Dependency] private readonly SharedStorageSystem _storage = default!;
|
||||
[Dependency] private readonly SharedSubdermalImplantSystem _subdermalImplant = default!;
|
||||
[Dependency] private readonly NameModifierSystem _nameMod = default!;
|
||||
[Dependency] private readonly Shared.StatusEffectNew.StatusEffectsSystem _statusEffects = default!; //TODO: This system has to support both the old and new status effect systems, until the old is able to be fully removed.
|
||||
|
||||
/// <summary>
|
||||
/// Spawns a clone of the given humanoid mob at the specified location or in nullspace.
|
||||
|
|
@ -75,6 +77,10 @@ public sealed partial class CloningSystem : SharedCloningSystem
|
|||
if (settings.CopyImplants)
|
||||
CopyImplants(original, clone.Value, settings.CopyInternalStorage, settings.Whitelist, settings.Blacklist);
|
||||
|
||||
// Copy permanent status effects
|
||||
if (settings.CopyStatusEffects)
|
||||
CopyStatusEffects(original, clone.Value);
|
||||
|
||||
var originalName = _nameMod.GetBaseName(original);
|
||||
|
||||
// Set the clone's name. The raised events will also adjust their PDA and ID card names.
|
||||
|
|
@ -267,4 +273,33 @@ public sealed partial class CloningSystem : SharedCloningSystem
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scans all permanent status effects applied to the original entity and transfers them to the clone.
|
||||
/// </summary>
|
||||
public void CopyStatusEffects(Entity<StatusEffectContainerComponent?> original, Entity<StatusEffectContainerComponent?> target)
|
||||
{
|
||||
if (!Resolve(original, ref original.Comp, false))
|
||||
return;
|
||||
|
||||
if (original.Comp.ActiveStatusEffects is null)
|
||||
return;
|
||||
|
||||
foreach (var effect in original.Comp.ActiveStatusEffects.ContainedEntities)
|
||||
{
|
||||
if (!TryComp<StatusEffectComponent>(effect, out var effectComp))
|
||||
continue;
|
||||
|
||||
//We are not interested in temporary effects, only permanent ones.
|
||||
if (effectComp.EndEffectTime is not null)
|
||||
continue;
|
||||
|
||||
var effectProto = Prototype(effect);
|
||||
|
||||
if (effectProto is null)
|
||||
continue;
|
||||
|
||||
_statusEffects.TrySetStatusEffectDuration(target, effectProto);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ public sealed partial class BuildMech : IGraphAction
|
|||
|
||||
var cell = container.ContainedEntities[0];
|
||||
|
||||
if (!entityManager.TryGetComponent<BatteryComponent>(cell, out var batteryComponent))
|
||||
if (!entityManager.TryGetComponent<PredictedBatteryComponent>(cell, out var batteryComponent))
|
||||
{
|
||||
Logger.Warning($"Mech construct entity {uid} had an invalid entity in container \"{Container}\"! Aborting build mech action.");
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -839,11 +839,5 @@ public sealed class NetworkConfiguratorSystem : SharedNetworkConfiguratorSystem
|
|||
|
||||
UpdateListUiState(conf, conf.Comp);
|
||||
}
|
||||
|
||||
private void OnUiOpenAttempt(EntityUid uid, NetworkConfiguratorComponent configurator, ActivatableUIOpenAttemptEvent args)
|
||||
{
|
||||
if (configurator.LinkModeActive)
|
||||
args.Cancel();
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
using Content.Server.Explosion.EntitySystems;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Explosion.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Stores data for airtight explosion traversal on a <see cref="MapGridComponent"/> entity.
|
||||
/// </summary>
|
||||
/// <seealso cref="ExplosionSystem"/>
|
||||
[RegisterComponent]
|
||||
[Access(typeof(ExplosionSystem), Other = AccessPermissions.None)]
|
||||
public sealed partial class ExplosionAirtightGridComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Data for every tile on the current grid.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Intentionally not saved.
|
||||
/// </remarks>
|
||||
[ViewVariables]
|
||||
public readonly Dictionary<Vector2i, TileData> Tiles = new();
|
||||
|
||||
/// <summary>
|
||||
/// Data struct that describes the explosion-blocking airtight entities on a tile.
|
||||
/// </summary>
|
||||
public struct TileData
|
||||
{
|
||||
/// <summary>
|
||||
/// Which index into the tolerance cache of <see cref="ExplosionSystem"/> this tile is using.
|
||||
/// </summary>
|
||||
public required int ToleranceCacheIndex;
|
||||
|
||||
/// <summary>
|
||||
/// Which directions this tile is blocking explosions in. Bitflag field.
|
||||
/// </summary>
|
||||
public required AtmosDirection BlockedDirections;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A set of tolerance values
|
||||
/// </summary>
|
||||
public struct ToleranceValues : IEquatable<ToleranceValues>
|
||||
{
|
||||
/// <summary>
|
||||
/// Special value that indicates the entity is "invulnerable" against a specific explosion type.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Here to deal with the limited range of <see cref="FixedPoint2"/> over typical floats.
|
||||
/// </remarks>
|
||||
public static readonly FixedPoint2 Invulnerable = FixedPoint2.MaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// The intensities at which explosions of each type can instantly break through an entity.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This is an array, with the index of each value corresponding to the "explosion type ID" cached by
|
||||
/// <see cref="ExplosionSystem"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Values are stored as <see cref="FixedPoint2"/> to avoid possible precision issues resulting in
|
||||
/// different-but-almost-identical tolerance values wasting memory.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If a value is <see cref="Invulnerable"/>, that indicates the tile is invulnerable.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public required FixedPoint2[] Values;
|
||||
|
||||
public bool Equals(ToleranceValues other)
|
||||
{
|
||||
return Values.AsSpan().SequenceEqual(other.Values);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is ToleranceValues other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hc = new HashCode();
|
||||
hc.AddArray(Values);
|
||||
return hc.ToHashCode();
|
||||
}
|
||||
|
||||
public static bool operator ==(ToleranceValues left, ToleranceValues right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(ToleranceValues left, ToleranceValues right)
|
||||
{
|
||||
return !left.Equals(right);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
using System.Numerics;
|
||||
using Content.Shared.Atmos;
|
||||
using Robust.Shared.Map;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.Map.Components;
|
||||
using static Content.Server.Explosion.Components.ExplosionAirtightGridComponent;
|
||||
using static Content.Server.Explosion.EntitySystems.ExplosionSystem;
|
||||
|
||||
namespace Content.Server.Explosion.EntitySystems;
|
||||
|
|
@ -11,6 +12,8 @@ namespace Content.Server.Explosion.EntitySystems;
|
|||
/// </summary>
|
||||
public sealed class ExplosionGridTileFlood : ExplosionTileFlood
|
||||
{
|
||||
private readonly ExplosionSystem _explosionSystem;
|
||||
|
||||
public Entity<MapGridComponent> Grid;
|
||||
private bool _needToTransform = false;
|
||||
|
||||
|
|
@ -45,7 +48,8 @@ public sealed class ExplosionGridTileFlood : ExplosionTileFlood
|
|||
Dictionary<Vector2i, NeighborFlag> edgeTiles,
|
||||
EntityUid? referenceGrid,
|
||||
Matrix3x2 spaceMatrix,
|
||||
Angle spaceAngle)
|
||||
Angle spaceAngle,
|
||||
ExplosionSystem explosionSystem)
|
||||
{
|
||||
Grid = grid;
|
||||
_airtightMap = airtightMap;
|
||||
|
|
@ -53,6 +57,7 @@ public sealed class ExplosionGridTileFlood : ExplosionTileFlood
|
|||
_intensityStepSize = intensityStepSize;
|
||||
_typeIndex = typeIndex;
|
||||
_edgeTiles = edgeTiles;
|
||||
_explosionSystem = explosionSystem;
|
||||
|
||||
// initialise SpaceTiles
|
||||
foreach (var (tile, spaceNeighbors) in _edgeTiles)
|
||||
|
|
@ -193,11 +198,11 @@ public sealed class ExplosionGridTileFlood : ExplosionTileFlood
|
|||
NewBlockedTiles.Add(tile);
|
||||
|
||||
// At what explosion iteration would this blocker be destroyed?
|
||||
var required = tileData.ExplosionTolerance[_typeIndex];
|
||||
var required = _explosionSystem.GetToleranceValues(tileData.ToleranceCacheIndex).Values[_typeIndex];
|
||||
if (required > _maxIntensity)
|
||||
return; // blocker is never destroyed.
|
||||
|
||||
var clearIteration = iteration + (int) MathF.Ceiling(required / _intensityStepSize);
|
||||
var clearIteration = iteration + (int) MathF.Ceiling((float)required / _intensityStepSize);
|
||||
if (FreedTileLists.TryGetValue(clearIteration, out var list))
|
||||
list.Add(tile);
|
||||
else
|
||||
|
|
@ -261,13 +266,13 @@ public sealed class ExplosionGridTileFlood : ExplosionTileFlood
|
|||
foreach (var tile in tiles)
|
||||
{
|
||||
var blockedDirections = AtmosDirection.Invalid;
|
||||
float sealIntegrity = 0;
|
||||
FixedPoint2 sealIntegrity = 0;
|
||||
|
||||
// Note that if (grid, tile) is not a valid key, then airtight.BlockedDirections will default to 0 (no blocked directions)
|
||||
if (_airtightMap.TryGetValue(tile, out var tileData))
|
||||
{
|
||||
blockedDirections = tileData.BlockedDirections;
|
||||
sealIntegrity = tileData.ExplosionTolerance[_typeIndex];
|
||||
sealIntegrity = _explosionSystem.GetToleranceValues(tileData.ToleranceCacheIndex).Values[_typeIndex];
|
||||
}
|
||||
|
||||
// First, yield any neighboring tiles that are not blocked by airtight entities on this tile
|
||||
|
|
@ -290,7 +295,7 @@ public sealed class ExplosionGridTileFlood : ExplosionTileFlood
|
|||
continue;
|
||||
|
||||
// At what explosion iteration would this blocker be destroyed?
|
||||
var clearIteration = iteration + (int) MathF.Ceiling(sealIntegrity / _intensityStepSize);
|
||||
var clearIteration = iteration + (int) MathF.Ceiling((float) sealIntegrity / _intensityStepSize);
|
||||
|
||||
// Get the delayed neighbours list
|
||||
if (!_delayedNeighbors.TryGetValue(clearIteration, out var list))
|
||||
|
|
|
|||
|
|
@ -1,44 +1,59 @@
|
|||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Server.Explosion.Components;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.Explosion;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using static Content.Server.Explosion.Components.ExplosionAirtightGridComponent;
|
||||
|
||||
namespace Content.Server.Explosion.EntitySystems;
|
||||
|
||||
public sealed partial class ExplosionSystem
|
||||
{
|
||||
private readonly Dictionary<string, int> _explosionTypes = new();
|
||||
// We keep track of which tiles are airtight, and how much damage from explosions those airtight blockers can take.
|
||||
// This is quite complicated, as the data effectively needs to be tracked *per tile*, *per explosion type*.
|
||||
// To avoid wasting significant memory, we calculate the values and share the actual backing storage of it.
|
||||
// Stored values are reference counted so they can be evicted when no longer needed.
|
||||
// At the time of writing, this compacts the storage for Box Station from ~5500 tolerance value sets to 13,
|
||||
// at round start.
|
||||
|
||||
// Use integers instead of prototype IDs for storage of explosion data.
|
||||
// This allows us to replace a Dictionary<string, FixedPoint2> with just a FixedPoint2[].
|
||||
private readonly Dictionary<ProtoId<ExplosionPrototype>, int> _explosionTypes = new();
|
||||
// Index to look up if we already have an existing set of tolerance values stored, so the data can be shared.
|
||||
private readonly Dictionary<ToleranceValues, int> _toleranceIndex = new();
|
||||
// Storage for tolerance values. Entries form a free linked list when not occupied by a set of real values.
|
||||
private ValueList<CacheEntry> _toleranceData;
|
||||
// First free position in _toleranceData.
|
||||
// -1 indicates there are no free slots left and the storage must be expanded.
|
||||
private int _freeListHead = -1;
|
||||
|
||||
private void InitAirtightMap()
|
||||
{
|
||||
// Currently explosion prototype hot-reload isn't supported, as it would involve completely re-computing the
|
||||
// airtight map. Could be done, just not yet implemented.
|
||||
_explosionTypes.Clear();
|
||||
|
||||
// for storing airtight entity damage thresholds for all anchored airtight entities, we will use integers in
|
||||
// place of id-strings. This initializes the string <--> id association.
|
||||
// This allows us to replace a Dictionary<string, float> with just a float[].
|
||||
int index = 0;
|
||||
foreach (var prototype in _prototypeManager.EnumeratePrototypes<ExplosionPrototype>())
|
||||
{
|
||||
// TODO EXPLOSION
|
||||
// just make this a field on the prototype
|
||||
_explosionTypes.Add(prototype.ID, index);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
// The explosion intensity required to break an entity depends on the explosion type. So it is stored in a
|
||||
// Dictionary<string, float>
|
||||
//
|
||||
// Hence, each tile has a tuple (Dictionary<string, float>, AtmosDirection). This specifies what directions are
|
||||
// blocked, and how intense a given explosion type needs to be in order to destroy ALL airtight entities on that
|
||||
// tile. This is the TileData struct.
|
||||
//
|
||||
// We then need this data for every tile on a grid. So this mess of a variable maps the Grid ID and Vector2i grid
|
||||
// indices to this tile-data struct.
|
||||
private Dictionary<EntityUid, Dictionary<Vector2i, TileData>> _airtightMap = new();
|
||||
private void ReloadExplosionPrototypes(PrototypesReloadedEventArgs prototypesReloadedEventArgs)
|
||||
{
|
||||
if (!prototypesReloadedEventArgs.Modified.Contains(typeof(ExplosionPrototype)))
|
||||
return;
|
||||
|
||||
InitAirtightMap();
|
||||
ReloadMap();
|
||||
}
|
||||
|
||||
public void UpdateAirtightMap(EntityUid gridId, Vector2i tile, MapGridComponent? grid = null)
|
||||
{
|
||||
|
|
@ -46,6 +61,12 @@ public sealed partial class ExplosionSystem
|
|||
UpdateAirtightMap(gridId, grid, tile);
|
||||
}
|
||||
|
||||
[Access(typeof(ExplosionGridTileFlood))]
|
||||
public ToleranceValues GetToleranceValues(int idx)
|
||||
{
|
||||
return _toleranceData[idx].Values;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the map of explosion blockers.
|
||||
/// </summary>
|
||||
|
|
@ -58,11 +79,12 @@ public sealed partial class ExplosionSystem
|
|||
/// </remarks>
|
||||
public void UpdateAirtightMap(EntityUid gridId, MapGridComponent grid, Vector2i tile)
|
||||
{
|
||||
var tolerance = new float[_explosionTypes.Count];
|
||||
var blockedDirections = AtmosDirection.Invalid;
|
||||
var airtightGrid = EnsureComp<ExplosionAirtightGridComponent>(gridId);
|
||||
|
||||
if (!_airtightMap.ContainsKey(gridId))
|
||||
_airtightMap[gridId] = new();
|
||||
// Calculate tile new airtight state.
|
||||
|
||||
var tolerance = new FixedPoint2[_explosionTypes.Count];
|
||||
var blockedDirections = AtmosDirection.Invalid;
|
||||
|
||||
var anchoredEnumerator = _map.GetAnchoredEntitiesEnumerator(gridId, grid, tile);
|
||||
|
||||
|
|
@ -72,17 +94,97 @@ public sealed partial class ExplosionSystem
|
|||
continue;
|
||||
|
||||
blockedDirections |= airtight.AirBlockedDirection;
|
||||
var entityTolerances = GetExplosionTolerance(uid.Value);
|
||||
for (var i = 0; i < tolerance.Length; i++)
|
||||
{
|
||||
tolerance[i] = Math.Max(tolerance[i], entityTolerances[i]);
|
||||
}
|
||||
GetExplosionTolerance(uid.Value, tolerance);
|
||||
}
|
||||
|
||||
if (blockedDirections != AtmosDirection.Invalid)
|
||||
_airtightMap[gridId][tile] = new(tolerance, blockedDirections);
|
||||
// Log.Info($"UPDATE {gridId}/{tile}: {blockedDirections}");
|
||||
|
||||
if (blockedDirections == AtmosDirection.Invalid)
|
||||
{
|
||||
// No longer airtight
|
||||
|
||||
if (!airtightGrid.Tiles.Remove(tile, out var tileData))
|
||||
{
|
||||
// Did not have this tile before and after, nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
// Removing tile data.
|
||||
DecrementRefCount(tileData.ToleranceCacheIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
ref var tileEntry = ref CollectionsMarshal.GetValueRefOrAddDefault(airtightGrid.Tiles, tile, out var existed);
|
||||
var cacheKey = new ToleranceValues { Values = tolerance };
|
||||
|
||||
// Remove previous tolerance reference if necessary.
|
||||
if (existed)
|
||||
{
|
||||
ref var prevEntry = ref _toleranceData[tileEntry.ToleranceCacheIndex];
|
||||
if (prevEntry.Values == cacheKey)
|
||||
{
|
||||
// No change.
|
||||
return;
|
||||
}
|
||||
|
||||
DecrementRefCount(tileEntry.ToleranceCacheIndex);
|
||||
}
|
||||
|
||||
ref var newCacheIndex = ref CollectionsMarshal.GetValueRefOrAddDefault(_toleranceIndex, cacheKey, out existed);
|
||||
if (existed)
|
||||
{
|
||||
_toleranceData[newCacheIndex].RefCount += 1;
|
||||
}
|
||||
else
|
||||
_airtightMap[gridId].Remove(tile);
|
||||
{
|
||||
if (_freeListHead < 0)
|
||||
ExpandCache();
|
||||
|
||||
newCacheIndex = _freeListHead;
|
||||
ref var newCacheEntry = ref _toleranceData[newCacheIndex];
|
||||
_freeListHead = newCacheEntry.RefCount;
|
||||
|
||||
newCacheEntry.Values = cacheKey;
|
||||
newCacheEntry.RefCount = 1;
|
||||
}
|
||||
|
||||
tileEntry = new TileData
|
||||
{
|
||||
BlockedDirections = blockedDirections,
|
||||
ToleranceCacheIndex = newCacheIndex,
|
||||
};
|
||||
}
|
||||
|
||||
private void ExpandCache()
|
||||
{
|
||||
var newCacheSize = Math.Max(8, _toleranceData.Count * 2);
|
||||
var curSize = _toleranceData.Count;
|
||||
|
||||
_toleranceData.EnsureLength(newCacheSize);
|
||||
for (var i = curSize; i < newCacheSize; i++)
|
||||
{
|
||||
_toleranceData[i].RefCount = _freeListHead;
|
||||
_freeListHead = i;
|
||||
}
|
||||
}
|
||||
|
||||
private void DecrementRefCount(int index)
|
||||
{
|
||||
ref var cacheEntry = ref _toleranceData[index];
|
||||
|
||||
DebugTools.Assert(cacheEntry.RefCount > 0);
|
||||
cacheEntry.RefCount -= 1;
|
||||
|
||||
if (cacheEntry.RefCount == 0)
|
||||
{
|
||||
var prevValue = cacheEntry.Values;
|
||||
cacheEntry.Values = default;
|
||||
cacheEntry.RefCount = _freeListHead;
|
||||
_freeListHead = index;
|
||||
|
||||
var result = _toleranceIndex.Remove(prevValue);
|
||||
DebugTools.Assert(result, "Failed to removed 0 refcounted index!");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -106,7 +208,7 @@ public sealed partial class ExplosionSystem
|
|||
/// <summary>
|
||||
/// Return a dictionary that specifies how intense a given explosion type needs to be in order to destroy an entity.
|
||||
/// </summary>
|
||||
public float[] GetExplosionTolerance(EntityUid uid)
|
||||
private void GetExplosionTolerance(EntityUid uid, Span<FixedPoint2> explosionTolerance)
|
||||
{
|
||||
// How much total damage is needed to destroy this entity? This also includes "break" behaviors. This ASSUMES
|
||||
// that this will result in a non-airtight entity.Entities that ONLY break via construction graph node changes
|
||||
|
|
@ -117,14 +219,14 @@ public sealed partial class ExplosionSystem
|
|||
totalDamageTarget = _destructibleSystem.DestroyedAt(uid, destructible);
|
||||
}
|
||||
|
||||
var explosionTolerance = new float[_explosionTypes.Count];
|
||||
if (totalDamageTarget == FixedPoint2.MaxValue || !_damageableQuery.TryGetComponent(uid, out var damageable))
|
||||
{
|
||||
for (var i = 0; i < explosionTolerance.Length; i++)
|
||||
{
|
||||
explosionTolerance[i] = float.MaxValue;
|
||||
explosionTolerance[i] = ToleranceValues.Invulnerable;
|
||||
}
|
||||
return explosionTolerance;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// What multiple of each explosion type damage set will result in the damage exceeding the required amount? This
|
||||
|
|
@ -157,38 +259,43 @@ public sealed partial class ExplosionSystem
|
|||
damagePerIntensity += value * mod * Math.Max(0, ev.DamageCoefficient);
|
||||
}
|
||||
|
||||
explosionTolerance[index] = damagePerIntensity > 0
|
||||
var toleranceValue = damagePerIntensity > 0
|
||||
? (float) ((totalDamageTarget - damageable.TotalDamage) / damagePerIntensity)
|
||||
: float.MaxValue;
|
||||
}
|
||||
: ToleranceValues.Invulnerable;
|
||||
|
||||
return explosionTolerance;
|
||||
explosionTolerance[index] = toleranceValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Data struct that describes the explosion-blocking airtight entities on a tile.
|
||||
/// </summary>
|
||||
public struct TileData
|
||||
private void OnAirtightGridRemoved(EntityUid entity)
|
||||
{
|
||||
public TileData(float[] explosionTolerance, AtmosDirection blockedDirections)
|
||||
if (!TryComp(entity, out ExplosionAirtightGridComponent? airtightGrid))
|
||||
return;
|
||||
|
||||
foreach (var tile in airtightGrid.Tiles.Values)
|
||||
{
|
||||
ExplosionTolerance = explosionTolerance;
|
||||
BlockedDirections = blockedDirections;
|
||||
DecrementRefCount(tile.ToleranceCacheIndex);
|
||||
}
|
||||
|
||||
public float[] ExplosionTolerance;
|
||||
public AtmosDirection BlockedDirections = AtmosDirection.Invalid;
|
||||
RemComp<ExplosionAirtightGridComponent>(entity);
|
||||
}
|
||||
|
||||
public override void ReloadMap()
|
||||
{
|
||||
foreach (var(grid, dict) in _airtightMap)
|
||||
var enumerator = EntityQueryEnumerator<ExplosionAirtightGridComponent, MapGridComponent>();
|
||||
while (enumerator.MoveNext(out var uid, out var airtightComp, out var mapGrid))
|
||||
{
|
||||
var comp = Comp<MapGridComponent>(grid);
|
||||
foreach (var index in dict.Keys)
|
||||
foreach (var pos in airtightComp.Tiles.Keys)
|
||||
{
|
||||
UpdateAirtightMap(grid, comp, index);
|
||||
UpdateAirtightMap(uid, pos, mapGrid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct CacheEntry
|
||||
{
|
||||
public ToleranceValues Values;
|
||||
public int RefCount; // Doubles as freelist chain
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ public sealed partial class ExplosionSystem
|
|||
|
||||
private void OnGridRemoved(GridRemovalEvent ev)
|
||||
{
|
||||
_airtightMap.Remove(ev.EntityUid);
|
||||
OnAirtightGridRemoved(ev.EntityUid);
|
||||
_gridEdges.Remove(ev.EntityUid);
|
||||
|
||||
// this should be a small enough set that iterating all of them is fine
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Server.Explosion.Components;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Explosion.Components;
|
||||
using Robust.Shared.Map;
|
||||
|
|
@ -40,11 +41,7 @@ public sealed partial class ExplosionSystem
|
|||
if (totalIntensity <= 0 || slope <= 0)
|
||||
return null;
|
||||
|
||||
if (!_explosionTypes.TryGetValue(typeID, out var typeIndex))
|
||||
{
|
||||
Log.Error("Attempted to spawn explosion using a prototype that was not defined during initialization. Explosion prototype hot-reload is not currently supported.");
|
||||
return null;
|
||||
}
|
||||
var typeIndex = _explosionTypes[typeID];
|
||||
|
||||
Vector2i initialTile;
|
||||
EntityUid? epicentreGrid = null;
|
||||
|
|
@ -103,8 +100,7 @@ public sealed partial class ExplosionSystem
|
|||
// set up the initial `gridData` instance
|
||||
encounteredGrids.Add(epicentreGrid.Value);
|
||||
|
||||
if (!_airtightMap.TryGetValue(epicentreGrid.Value, out var airtightMap))
|
||||
airtightMap = new();
|
||||
var airtightMap = CompOrNull<ExplosionAirtightGridComponent>(epicentreGrid)?.Tiles ?? new();
|
||||
|
||||
var initialGridData = new ExplosionGridTileFlood(
|
||||
(epicentreGrid.Value, Comp<MapGridComponent>(epicentreGrid.Value)),
|
||||
|
|
@ -115,7 +111,8 @@ public sealed partial class ExplosionSystem
|
|||
_gridEdges[epicentreGrid.Value],
|
||||
referenceGrid,
|
||||
spaceMatrix,
|
||||
spaceAngle);
|
||||
spaceAngle,
|
||||
this);
|
||||
|
||||
gridData[epicentreGrid.Value] = initialGridData;
|
||||
|
||||
|
|
@ -192,8 +189,7 @@ public sealed partial class ExplosionSystem
|
|||
// is this a new grid, for which we must create a new explosion data set
|
||||
if (!gridData.TryGetValue(grid, out var data))
|
||||
{
|
||||
if (!_airtightMap.TryGetValue(grid, out var airtightMap))
|
||||
airtightMap = new();
|
||||
var airtightMap = CompOrNull<ExplosionAirtightGridComponent>(grid)?.Tiles ?? new();
|
||||
|
||||
data = new ExplosionGridTileFlood(
|
||||
(grid, Comp<MapGridComponent>(grid)),
|
||||
|
|
@ -204,7 +200,8 @@ public sealed partial class ExplosionSystem
|
|||
_gridEdges[grid],
|
||||
referenceGrid,
|
||||
spaceMatrix,
|
||||
spaceAngle);
|
||||
spaceAngle,
|
||||
this);
|
||||
|
||||
gridData[grid] = data;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,6 +104,8 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
|
|||
_destructibleQuery = GetEntityQuery<DestructibleComponent>();
|
||||
_damageableQuery = GetEntityQuery<DamageableComponent>();
|
||||
_airtightQuery = GetEntityQuery<AirtightComponent>();
|
||||
|
||||
_prototypeManager.PrototypesReloaded += ReloadExplosionPrototypes;
|
||||
}
|
||||
|
||||
private void OnReset(RoundRestartCleanupEvent ev)
|
||||
|
|
@ -122,6 +124,7 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
|
|||
base.Shutdown();
|
||||
_nodeGroupSystem.PauseUpdating = false;
|
||||
_pathfindingSystem.PauseUpdating = false;
|
||||
_prototypeManager.PrototypesReloaded -= ReloadExplosionPrototypes;
|
||||
}
|
||||
|
||||
private void RelayedResistance(EntityUid uid, ExplosionResistanceComponent component,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using Content.Server.Chemistry.Components;
|
||||
using Content.Server.Chemistry.EntitySystems;
|
||||
using Content.Server.Fluids.Components;
|
||||
using Content.Server.Gravity;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.CCVar;
|
||||
|
|
@ -16,11 +15,14 @@ using Robust.Shared.Configuration;
|
|||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
using System.Numerics;
|
||||
using Content.Shared.Fluids.EntitySystems;
|
||||
using Content.Shared.Fluids.Components;
|
||||
using Robust.Server.Containers;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Server.Fluids.EntitySystems;
|
||||
|
||||
public sealed class SpraySystem : EntitySystem
|
||||
public sealed class SpraySystem : SharedSpraySystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly GravitySystem _gravity = default!;
|
||||
|
|
@ -33,6 +35,7 @@ public sealed class SpraySystem : EntitySystem
|
|||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly ContainerSystem _container = default!;
|
||||
|
||||
private float _gridImpulseMultiplier;
|
||||
|
||||
|
|
@ -54,7 +57,7 @@ public sealed class SpraySystem : EntitySystem
|
|||
|
||||
var targetMapPos = _transform.GetMapCoordinates(GetEntityQuery<TransformComponent>().GetComponent(args.Target));
|
||||
|
||||
Spray(entity, args.User, targetMapPos);
|
||||
Spray(entity, targetMapPos, args.User);
|
||||
}
|
||||
|
||||
private void UpdateGridMassMultiplier(float value)
|
||||
|
|
@ -71,10 +74,19 @@ public sealed class SpraySystem : EntitySystem
|
|||
|
||||
var clickPos = _transform.ToMapCoordinates(args.ClickLocation);
|
||||
|
||||
Spray(entity, args.User, clickPos);
|
||||
Spray(entity, clickPos, args.User);
|
||||
}
|
||||
|
||||
public void Spray(Entity<SprayComponent> entity, EntityUid user, MapCoordinates mapcoord)
|
||||
public override void Spray(Entity<SprayComponent> entity, EntityUid? user = null)
|
||||
{
|
||||
var xform = Transform(entity);
|
||||
var throwing = xform.LocalRotation.ToWorldVec() * entity.Comp.SprayDistance;
|
||||
var direction = xform.Coordinates.Offset(throwing);
|
||||
|
||||
Spray(entity, _transform.ToMapCoordinates(direction), user);
|
||||
}
|
||||
|
||||
public override void Spray(Entity<SprayComponent> entity, MapCoordinates mapcoord, EntityUid? user = null)
|
||||
{
|
||||
if (!_solutionContainer.TryGetSolution(entity.Owner, SprayComponent.SolutionName, out var soln, out var solution))
|
||||
return;
|
||||
|
|
@ -82,25 +94,29 @@ public sealed class SpraySystem : EntitySystem
|
|||
var ev = new SprayAttemptEvent(user);
|
||||
RaiseLocalEvent(entity, ref ev);
|
||||
if (ev.Cancelled)
|
||||
{
|
||||
if (ev.CancelPopupMessage != null && user != null)
|
||||
_popupSystem.PopupEntity(Loc.GetString(ev.CancelPopupMessage), entity.Owner, user.Value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryComp<UseDelayComponent>(entity, out var useDelay)
|
||||
&& _useDelay.IsDelayed((entity, useDelay)))
|
||||
if (_useDelay.IsDelayed((entity, null)))
|
||||
return;
|
||||
|
||||
if (solution.Volume <= 0)
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("spray-component-is-empty-message"), entity.Owner, user);
|
||||
if (user != null)
|
||||
_popupSystem.PopupEntity(Loc.GetString(entity.Comp.SprayEmptyPopupMessage, ("entity", entity)), entity.Owner, user.Value);
|
||||
return;
|
||||
}
|
||||
|
||||
var xformQuery = GetEntityQuery<TransformComponent>();
|
||||
var userXform = xformQuery.GetComponent(user);
|
||||
var sprayerXform = xformQuery.GetComponent(entity);
|
||||
|
||||
var userMapPos = _transform.GetMapCoordinates(userXform);
|
||||
var sprayerMapPos = _transform.GetMapCoordinates(sprayerXform);
|
||||
var clickMapPos = mapcoord;
|
||||
|
||||
var diffPos = clickMapPos.Position - userMapPos.Position;
|
||||
var diffPos = clickMapPos.Position - sprayerMapPos.Position;
|
||||
if (diffPos == Vector2.Zero || diffPos == Vector2Helpers.NaN)
|
||||
return;
|
||||
|
||||
|
|
@ -127,12 +143,12 @@ public sealed class SpraySystem : EntitySystem
|
|||
Angle.FromDegrees(spread * (amount - 1) / 2));
|
||||
|
||||
// Calculate the destination for the vapor cloud. Limit to the maximum spray distance.
|
||||
var target = userMapPos
|
||||
var target = sprayerMapPos
|
||||
.Offset((diffNorm + rotation.ToVec()).Normalized() * diffLength + quarter);
|
||||
|
||||
var distance = (target.Position - userMapPos.Position).Length();
|
||||
var distance = (target.Position - sprayerMapPos.Position).Length();
|
||||
if (distance > entity.Comp.SprayDistance)
|
||||
target = userMapPos.Offset(diffNorm * entity.Comp.SprayDistance);
|
||||
target = sprayerMapPos.Offset(diffNorm * entity.Comp.SprayDistance);
|
||||
|
||||
var adjustedSolutionAmount = entity.Comp.TransferAmount / entity.Comp.VaporAmount;
|
||||
var newSolution = _solutionContainer.SplitSolution(soln.Value, adjustedSolutionAmount);
|
||||
|
|
@ -141,7 +157,7 @@ public sealed class SpraySystem : EntitySystem
|
|||
break;
|
||||
|
||||
// Spawn the vapor cloud onto the grid/map the user is present on. Offset the start position based on how far the target destination is.
|
||||
var vaporPos = userMapPos.Offset(distance < 1 ? quarter : threeQuarters);
|
||||
var vaporPos = sprayerMapPos.Offset(distance < 1 ? quarter : threeQuarters);
|
||||
var vapor = Spawn(entity.Comp.SprayedPrototype, vaporPos);
|
||||
var vaporXform = xformQuery.GetComponent(vapor);
|
||||
|
||||
|
|
@ -164,17 +180,21 @@ public sealed class SpraySystem : EntitySystem
|
|||
|
||||
_vapor.Start(ent, vaporXform, impulseDirection * diffLength, entity.Comp.SprayVelocity, target, time, user);
|
||||
|
||||
if (TryComp<PhysicsComponent>(user, out var body))
|
||||
var thingGettingPushed = entity.Owner;
|
||||
if (_container.TryGetOuterContainer(entity, sprayerXform, out var container))
|
||||
thingGettingPushed = container.Owner;
|
||||
|
||||
if (TryComp<PhysicsComponent>(thingGettingPushed, out var body))
|
||||
{
|
||||
if (_gravity.IsWeightless(user))
|
||||
if (_gravity.IsWeightless(thingGettingPushed))
|
||||
{
|
||||
// push back the player
|
||||
_physics.ApplyLinearImpulse(user, -impulseDirection * entity.Comp.PushbackAmount, body: body);
|
||||
_physics.ApplyLinearImpulse(thingGettingPushed, -impulseDirection * entity.Comp.PushbackAmount, body: body);
|
||||
}
|
||||
else
|
||||
{
|
||||
// push back the grid the player is standing on
|
||||
var userTransform = Transform(user);
|
||||
var userTransform = Transform(thingGettingPushed);
|
||||
if (userTransform.GridUid == userTransform.ParentUid)
|
||||
{
|
||||
// apply both linear and angular momentum depending on the player position
|
||||
|
|
@ -187,7 +207,6 @@ public sealed class SpraySystem : EntitySystem
|
|||
|
||||
_audio.PlayPvs(entity.Comp.SpraySound, entity, entity.Comp.SpraySound.Params.WithVariation(0.125f));
|
||||
|
||||
if (useDelay != null)
|
||||
_useDelay.TryResetDelay((entity, useDelay));
|
||||
_useDelay.TryResetDelay(entity);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Content.Server.GameTicking.Prototypes;
|
||||
using Content.Shared.GameTicking.Prototypes;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Linq;
|
||||
|
|
@ -8,24 +9,37 @@ namespace Content.Server.GameTicking;
|
|||
public sealed partial class GameTicker
|
||||
{
|
||||
[ViewVariables]
|
||||
public string? LobbyBackground { get; private set; }
|
||||
public ProtoId<LobbyBackgroundPrototype>? LobbyBackground { get; private set; }
|
||||
|
||||
[ViewVariables]
|
||||
private List<ResPath>? _lobbyBackgrounds;
|
||||
private List<ProtoId<LobbyBackgroundPrototype>>? _lobbyBackgrounds;
|
||||
|
||||
private static readonly string[] WhitelistedBackgroundExtensions = new string[] {"png", "jpg", "jpeg", "webp"};
|
||||
|
||||
private void InitializeLobbyBackground()
|
||||
{
|
||||
_lobbyBackgrounds = _prototypeManager.EnumeratePrototypes<LobbyBackgroundPrototype>()
|
||||
.Select(x => x.Background)
|
||||
.Where(x => WhitelistedBackgroundExtensions.Contains(x.Extension))
|
||||
.ToList();
|
||||
var allprotos = _prototypeManager.EnumeratePrototypes<LobbyBackgroundPrototype>().ToList();
|
||||
_lobbyBackgrounds ??= new List<ProtoId<LobbyBackgroundPrototype>>();
|
||||
|
||||
//create protoids from them
|
||||
foreach (var proto in allprotos)
|
||||
{
|
||||
var ext = proto.Background.Extension;
|
||||
if (!WhitelistedBackgroundExtensions.Contains(ext))
|
||||
continue;
|
||||
|
||||
//create a protoid and add it to the list
|
||||
_lobbyBackgrounds.Add(new ProtoId<LobbyBackgroundPrototype>(proto.ID));
|
||||
}
|
||||
|
||||
RandomizeLobbyBackground();
|
||||
}
|
||||
|
||||
private void RandomizeLobbyBackground() {
|
||||
LobbyBackground = _lobbyBackgrounds!.Any() ? _robustRandom.Pick(_lobbyBackgrounds!).ToString() : null;
|
||||
private void RandomizeLobbyBackground()
|
||||
{
|
||||
if (_lobbyBackgrounds != null && _lobbyBackgrounds.Count != 0)
|
||||
LobbyBackground = _robustRandom.Pick(_lobbyBackgrounds);
|
||||
else
|
||||
LobbyBackground = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -254,28 +254,7 @@ namespace Content.Server.GameTicking
|
|||
return;
|
||||
}
|
||||
|
||||
PlayerJoinGame(player, silent);
|
||||
|
||||
var data = player.ContentData();
|
||||
|
||||
DebugTools.AssertNotNull(data);
|
||||
|
||||
var newMind = _mind.CreateMind(data!.UserId, character.Name);
|
||||
_mind.SetUserId(newMind, data.UserId);
|
||||
|
||||
var jobPrototype = _prototypeManager.Index<JobPrototype>(jobId);
|
||||
|
||||
_playTimeTrackings.PlayerRolesChanged(player);
|
||||
|
||||
var mobMaybe = _stationSpawning.SpawnPlayerCharacterOnStation(station, jobId, character);
|
||||
DebugTools.AssertNotNull(mobMaybe);
|
||||
var mob = mobMaybe!.Value;
|
||||
|
||||
_mind.TransferTo(newMind, mob);
|
||||
|
||||
_roles.MindAddJobRole(newMind, silent: silent, jobPrototype: jobId);
|
||||
var jobName = _jobs.MindTryGetJobName(newMind);
|
||||
_admin.UpdatePlayerList(player);
|
||||
DoSpawn(player, character, station, jobId, silent, out var mob, out var jobPrototype, out var jobName);
|
||||
|
||||
if (lateJoin && !silent)
|
||||
{
|
||||
|
|
@ -348,6 +327,43 @@ namespace Content.Server.GameTicking
|
|||
RaiseLocalEvent(mob, aev, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a mob on the specified station, creates the new mind, equips job-specific starting gear and loadout
|
||||
/// </summary>
|
||||
public void DoSpawn(
|
||||
ICommonSession player,
|
||||
HumanoidCharacterProfile character,
|
||||
EntityUid station,
|
||||
string jobId,
|
||||
bool silent,
|
||||
out EntityUid mob,
|
||||
out JobPrototype jobPrototype,
|
||||
out string jobName)
|
||||
{
|
||||
PlayerJoinGame(player, silent);
|
||||
|
||||
var data = player.ContentData();
|
||||
|
||||
DebugTools.AssertNotNull(data);
|
||||
|
||||
var newMind = _mind.CreateMind(data!.UserId, character.Name);
|
||||
_mind.SetUserId(newMind, data.UserId);
|
||||
|
||||
jobPrototype = _prototypeManager.Index<JobPrototype>(jobId);
|
||||
|
||||
_playTimeTrackings.PlayerRolesChanged(player);
|
||||
|
||||
var mobMaybe = _stationSpawning.SpawnPlayerCharacterOnStation(station, jobId, character);
|
||||
DebugTools.AssertNotNull(mobMaybe);
|
||||
mob = mobMaybe!.Value;
|
||||
|
||||
_mind.TransferTo(newMind, mob);
|
||||
|
||||
_roles.MindAddJobRole(newMind, silent: silent, jobPrototype: jobId);
|
||||
jobName = _jobs.MindTryGetJobName(newMind);
|
||||
_admin.UpdatePlayerList(player);
|
||||
}
|
||||
|
||||
public void Respawn(ICommonSession player)
|
||||
{
|
||||
_mind.WipeMind(player);
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.GameTicking.Prototypes;
|
||||
|
||||
/// <summary>
|
||||
/// Prototype for a lobby background the game can choose.
|
||||
/// </summary>
|
||||
[Prototype]
|
||||
public sealed partial class LobbyBackgroundPrototype : IPrototype
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
[IdDataField]
|
||||
public string ID { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The sprite to use as the background. This should ideally be 1920x1080.
|
||||
/// </summary>
|
||||
[DataField("background", required: true)]
|
||||
public ResPath Background = default!;
|
||||
}
|
||||
|
|
@ -25,9 +25,22 @@ public sealed partial class XenoborgsRuleComponent : Component
|
|||
[DataField]
|
||||
public float XenoborgShuttleCallPercentage = 0.7f;
|
||||
|
||||
/// <summary>
|
||||
/// The most xenoborgs that existed at one point.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int MaxNumberXenoborgs = 0;
|
||||
|
||||
/// <summary>
|
||||
/// If the announcment of the death of the mothership core was sent
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool MothershipCoreDeathAnnouncmentSent = false;
|
||||
|
||||
/// <summary>
|
||||
/// If the emergency shuttle trigged by <see cref="XenoborgShuttleCallPercentage"> was already called.
|
||||
/// Will only call once. if a admin recalls it. it won't call again unless this is set to false by a admin
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool XenoborgShuttleCalled = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ public sealed class CutWireVariationPassSystem : VariationPassSystem<CutWireVari
|
|||
continue;
|
||||
|
||||
// Check against blacklist
|
||||
if (_whitelistSystem.IsBlacklistPass(ent.Comp.Blacklist, uid))
|
||||
if (_whitelistSystem.IsWhitelistPass(ent.Comp.Blacklist, uid))
|
||||
continue;
|
||||
|
||||
if (Random.Prob(ent.Comp.WireCutChance))
|
||||
|
|
|
|||
|
|
@ -75,7 +75,12 @@ public sealed class XenoborgsRuleSystem : GameRuleSystem<XenoborgsRuleComponent>
|
|||
else if (numXenoborgs == 0)
|
||||
args.AddLine(Loc.GetString("xenoborgs-cond-all-xenoborgs-dead-core-alive"));
|
||||
else
|
||||
args.AddLine(Loc.GetString("xenoborgs-cond-xenoborgs-alive", ("count", numXenoborgs)));
|
||||
{
|
||||
args.AddLine(Loc.GetString("xenoborg-number-xenoborg-alive-end", ("count", numXenoborgs)));
|
||||
args.AddLine(Loc.GetString("xenoborg-number-crew-alive-end", ("count", numHumans)));
|
||||
}
|
||||
|
||||
args.AddLine(Loc.GetString("xenoborg-max-number", ("count", component.MaxNumberXenoborgs)));
|
||||
|
||||
args.AddLine(Loc.GetString("xenoborgs-list-start"));
|
||||
|
||||
|
|
@ -93,14 +98,19 @@ public sealed class XenoborgsRuleSystem : GameRuleSystem<XenoborgsRuleComponent>
|
|||
var numXenoborgs = GetNumberXenoborgs();
|
||||
var numHumans = _mindSystem.GetAliveHumans().Count;
|
||||
|
||||
if ((float)numXenoborgs / (numHumans + numXenoborgs) > xenoborgsRuleComponent.XenoborgShuttleCallPercentage)
|
||||
xenoborgsRuleComponent.MaxNumberXenoborgs = Math.Max(xenoborgsRuleComponent.MaxNumberXenoborgs, numXenoborgs);
|
||||
|
||||
if (xenoborgsRuleComponent.XenoborgShuttleCalled
|
||||
|| (float)numXenoborgs / (numHumans + numXenoborgs) <= xenoborgsRuleComponent.XenoborgShuttleCallPercentage
|
||||
|| _roundEnd.IsRoundEndRequested())
|
||||
return;
|
||||
|
||||
foreach (var station in _station.GetStations())
|
||||
{
|
||||
foreach (var station in _station.GetStations())
|
||||
{
|
||||
_chatSystem.DispatchStationAnnouncement(station, Loc.GetString("xenoborg-shuttle-call"), colorOverride: Color.BlueViolet);
|
||||
}
|
||||
_roundEnd.RequestRoundEnd(null, false);
|
||||
_chatSystem.DispatchStationAnnouncement(station, Loc.GetString("xenoborg-shuttle-call"), colorOverride: Color.BlueViolet);
|
||||
}
|
||||
_roundEnd.RequestRoundEnd(null, false, cantRecall: true);
|
||||
xenoborgsRuleComponent.XenoborgShuttleCalled = true;
|
||||
}
|
||||
|
||||
protected override void Started(EntityUid uid, XenoborgsRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
using Content.Shared.Examine;
|
||||
using Content.Shared.Coordinates.Helpers;
|
||||
using Content.Server.PowerCell;
|
||||
using Content.Shared.PowerCell;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Power.Components;
|
||||
using Content.Shared.Storage;
|
||||
|
||||
namespace Content.Server.Holosign;
|
||||
|
|
@ -23,9 +22,8 @@ public sealed class HolosignSystem : EntitySystem
|
|||
{
|
||||
// TODO: This should probably be using an itemstatus
|
||||
// TODO: I'm too lazy to do this rn but it's literally copy-paste from emag.
|
||||
_powerCell.TryGetBatteryFromSlot(uid, out var battery);
|
||||
var charges = UsesRemaining(component, battery);
|
||||
var maxCharges = MaxUses(component, battery);
|
||||
var charges = _powerCell.GetRemainingUses(uid, component.ChargeUse);
|
||||
var maxCharges = _powerCell.GetMaxUses(uid, component.ChargeUse);
|
||||
|
||||
using (args.PushGroup(nameof(HolosignProjectorComponent)))
|
||||
{
|
||||
|
|
@ -52,25 +50,10 @@ public sealed class HolosignSystem : EntitySystem
|
|||
// overlapping of the same holo on one tile remains allowed to allow holofan refreshes
|
||||
var holoUid = Spawn(component.SignProto, args.ClickLocation.SnapToGrid(EntityManager));
|
||||
var xform = Transform(holoUid);
|
||||
// TODO: Just make the prototype anchored
|
||||
if (!xform.Anchored)
|
||||
_transform.AnchorEntity(holoUid, xform); // anchor to prevent any tempering with (don't know what could even interact with it)
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private int UsesRemaining(HolosignProjectorComponent component, BatteryComponent? battery = null)
|
||||
{
|
||||
if (battery == null ||
|
||||
component.ChargeUse == 0f) return 0;
|
||||
|
||||
return (int) (battery.CurrentCharge / component.ChargeUse);
|
||||
}
|
||||
|
||||
private int MaxUses(HolosignProjectorComponent component, BatteryComponent? battery = null)
|
||||
{
|
||||
if (battery == null ||
|
||||
component.ChargeUse == 0f) return 0;
|
||||
|
||||
return (int) (battery.MaxCharge / component.ChargeUse);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
27
Content.Server/Jobs/ApplyStatusEffectSpecial.cs
Normal file
27
Content.Server/Jobs/ApplyStatusEffectSpecial.cs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
using Content.Shared.Roles;
|
||||
using Content.Shared.StatusEffectNew;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// Adds permanent status effects to the entity.
|
||||
/// TODO: Move this, and other JobSpecials, from Server to Shared.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public sealed partial class ApplyStatusEffectSpecial : JobSpecial
|
||||
{
|
||||
[DataField(required: true)]
|
||||
public HashSet<EntProtoId> StatusEffects { get; private set; } = new();
|
||||
|
||||
public override void AfterEquip(EntityUid mob)
|
||||
{
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
var statusSystem = entMan.System<StatusEffectsSystem>();
|
||||
foreach (var effect in StatusEffects)
|
||||
{
|
||||
statusSystem.TrySetStatusEffectDuration(mob, effect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -111,16 +111,4 @@ namespace Content.Server.Kitchen.Components
|
|||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool CanMicrowaveIdsSafely = true;
|
||||
}
|
||||
|
||||
public sealed class BeingMicrowavedEvent : HandledEntityEventArgs
|
||||
{
|
||||
public EntityUid Microwave;
|
||||
public EntityUid? User;
|
||||
|
||||
public BeingMicrowavedEvent(EntityUid microwave, EntityUid? user)
|
||||
{
|
||||
Microwave = microwave;
|
||||
User = user;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
using Content.Server.Actions;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Server.PowerCell;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Light;
|
||||
using Content.Shared.Light.Components;
|
||||
using Content.Shared.Power.EntitySystems;
|
||||
using Content.Shared.PowerCell;
|
||||
using Content.Shared.Rounding;
|
||||
using Content.Shared.Toggleable;
|
||||
using JetBrains.Annotations;
|
||||
|
|
@ -25,7 +24,7 @@ namespace Content.Server.Light.EntitySystems
|
|||
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly PowerCellSystem _powerCell = default!;
|
||||
[Dependency] private readonly BatterySystem _battery = default!;
|
||||
[Dependency] private readonly PredictedBatterySystem _battery = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedPointLightSystem _lights = default!;
|
||||
|
|
@ -44,7 +43,6 @@ namespace Content.Server.Light.EntitySystems
|
|||
SubscribeLocalEvent<HandheldLightComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<HandheldLightComponent, ComponentShutdown>(OnShutdown);
|
||||
|
||||
SubscribeLocalEvent<HandheldLightComponent, ExaminedEvent>(OnExamine);
|
||||
|
||||
SubscribeLocalEvent<HandheldLightComponent, ActivateInWorldEvent>(OnActivate);
|
||||
|
||||
|
|
@ -108,13 +106,15 @@ namespace Content.Server.Light.EntitySystems
|
|||
// Curently every single flashlight has the same number of levels for status and that's all it uses the charge for
|
||||
// Thus we'll just check if the level changes.
|
||||
|
||||
if (!_powerCell.TryGetBatteryFromSlot(ent, out var battery))
|
||||
if (!_powerCell.TryGetBatteryFromSlotOrEntity(ent.Owner, out var battery))
|
||||
return null;
|
||||
|
||||
if (MathHelper.CloseToPercent(battery.CurrentCharge, 0) || ent.Comp.Wattage > battery.CurrentCharge)
|
||||
var currentCharge = _battery.GetCharge(battery.Value.AsNullable());
|
||||
|
||||
if (MathHelper.CloseToPercent(currentCharge, 0) || ent.Comp.Wattage > currentCharge)
|
||||
return 0;
|
||||
|
||||
return (byte?) ContentHelpers.RoundToNearestLevels(battery.CurrentCharge / battery.MaxCharge * 255, 255, HandheldLightComponent.StatusLevels);
|
||||
return (byte?)ContentHelpers.RoundToNearestLevels(currentCharge / battery.Value.Comp.MaxCharge * 255, 255, HandheldLightComponent.StatusLevels);
|
||||
}
|
||||
|
||||
private void OnRemove(Entity<HandheldLightComponent> ent, ref ComponentRemove args)
|
||||
|
|
@ -140,19 +140,14 @@ namespace Content.Server.Light.EntitySystems
|
|||
return ent.Comp.Activated ? TurnOff(ent) : TurnOn(user, ent);
|
||||
}
|
||||
|
||||
private void OnExamine(EntityUid uid, HandheldLightComponent component, ExaminedEvent args)
|
||||
{
|
||||
args.PushMarkup(component.Activated
|
||||
? Loc.GetString("handheld-light-component-on-examine-is-on-message")
|
||||
: Loc.GetString("handheld-light-component-on-examine-is-off-message"));
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
_activeLights.Clear();
|
||||
}
|
||||
|
||||
// TODO: Very important: Make this charge rate based instead of instantly removing charge each update step.
|
||||
// See PredictedBatteryComponent
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
var toRemove = new RemQueue<Entity<HandheldLightComponent>>();
|
||||
|
|
@ -199,8 +194,7 @@ namespace Content.Server.Light.EntitySystems
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!_powerCell.TryGetBatteryFromSlot(uid, out var battery) &&
|
||||
!TryComp(uid, out battery))
|
||||
if (!_powerCell.TryGetBatteryFromSlotOrEntity(uid.Owner, out var battery))
|
||||
{
|
||||
_audio.PlayPvs(_audio.ResolveSound(component.TurnOnFailSound), uid);
|
||||
_popup.PopupEntity(Loc.GetString("handheld-light-component-cell-missing-message"), uid, user);
|
||||
|
|
@ -210,7 +204,7 @@ namespace Content.Server.Light.EntitySystems
|
|||
// To prevent having to worry about frame time in here.
|
||||
// Let's just say you need a whole second of charge before you can turn it on.
|
||||
// Simple enough.
|
||||
if (component.Wattage > battery.CurrentCharge)
|
||||
if (component.Wattage > _battery.GetCharge(battery.Value.AsNullable()))
|
||||
{
|
||||
_audio.PlayPvs(_audio.ResolveSound(component.TurnOnFailSound), uid);
|
||||
_popup.PopupEntity(Loc.GetString("handheld-light-component-cell-dead-message"), uid, user);
|
||||
|
|
@ -227,24 +221,20 @@ namespace Content.Server.Light.EntitySystems
|
|||
public void TryUpdate(Entity<HandheldLightComponent> uid, float frameTime)
|
||||
{
|
||||
var component = uid.Comp;
|
||||
if (!_powerCell.TryGetBatteryFromSlot(uid, out var batteryUid, out var battery, null) &&
|
||||
!TryComp(uid, out battery))
|
||||
if (!_powerCell.TryGetBatteryFromSlotOrEntity(uid.Owner, out var battery))
|
||||
{
|
||||
TurnOff(uid, false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (batteryUid == null)
|
||||
return;
|
||||
|
||||
var appearanceComponent = EntityManager.GetComponentOrNull<AppearanceComponent>(uid);
|
||||
|
||||
var fraction = battery.CurrentCharge / battery.MaxCharge;
|
||||
if (fraction >= 0.30)
|
||||
var chargeFraction = _battery.GetChargeLevel(battery.Value.AsNullable());
|
||||
if (chargeFraction >= 0.30)
|
||||
{
|
||||
_appearance.SetData(uid, HandheldLightVisuals.Power, HandheldLightPowerStates.FullPower, appearanceComponent);
|
||||
}
|
||||
else if (fraction >= 0.10)
|
||||
else if (chargeFraction >= 0.10)
|
||||
{
|
||||
_appearance.SetData(uid, HandheldLightVisuals.Power, HandheldLightPowerStates.LowPower, appearanceComponent);
|
||||
}
|
||||
|
|
@ -253,7 +243,7 @@ namespace Content.Server.Light.EntitySystems
|
|||
_appearance.SetData(uid, HandheldLightVisuals.Power, HandheldLightPowerStates.Dying, appearanceComponent);
|
||||
}
|
||||
|
||||
if (component.Activated && !_battery.TryUseCharge((batteryUid.Value, battery), component.Wattage * frameTime))
|
||||
if (component.Activated && !_battery.TryUseCharge(battery.Value.AsNullable(), component.Wattage * frameTime))
|
||||
TurnOff(uid, false);
|
||||
|
||||
UpdateLevel(uid);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ using System.Linq;
|
|||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.Mech.Components;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.DoAfter;
|
||||
|
|
@ -14,6 +13,7 @@ using Content.Shared.Mech.EntitySystems;
|
|||
using Content.Shared.Movement.Events;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Power.Components;
|
||||
using Content.Shared.Power.EntitySystems;
|
||||
using Content.Shared.Tools;
|
||||
using Content.Shared.Tools.Components;
|
||||
using Content.Shared.Tools.Systems;
|
||||
|
|
@ -33,7 +33,7 @@ public sealed partial class MechSystem : SharedMechSystem
|
|||
{
|
||||
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
|
||||
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
|
||||
[Dependency] private readonly BatterySystem _battery = default!;
|
||||
[Dependency] private readonly PredictedBatterySystem _battery = default!;
|
||||
[Dependency] private readonly ContainerSystem _container = default!;
|
||||
[Dependency] private readonly DamageableSystem _damageable = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
|
||||
|
|
@ -88,7 +88,7 @@ public sealed partial class MechSystem : SharedMechSystem
|
|||
if (TryComp<WiresPanelComponent>(uid, out var panel) && !panel.Open)
|
||||
return;
|
||||
|
||||
if (component.BatterySlot.ContainedEntity == null && TryComp<BatteryComponent>(args.Used, out var battery))
|
||||
if (component.BatterySlot.ContainedEntity == null && TryComp<PredictedBatteryComponent>(args.Used, out var battery))
|
||||
{
|
||||
InsertBattery(uid, args.Used, component, battery);
|
||||
_actionBlocker.UpdateCanMove(uid);
|
||||
|
|
@ -109,10 +109,10 @@ public sealed partial class MechSystem : SharedMechSystem
|
|||
|
||||
private void OnInsertBattery(EntityUid uid, MechComponent component, EntInsertedIntoContainerMessage args)
|
||||
{
|
||||
if (args.Container != component.BatterySlot || !TryComp<BatteryComponent>(args.Entity, out var battery))
|
||||
if (args.Container != component.BatterySlot || !TryComp<PredictedBatteryComponent>(args.Entity, out var battery))
|
||||
return;
|
||||
|
||||
component.Energy = battery.CurrentCharge;
|
||||
component.Energy = _battery.GetCharge((args.Entity, battery));
|
||||
component.MaxEnergy = battery.MaxCharge;
|
||||
|
||||
Dirty(uid, component);
|
||||
|
|
@ -337,21 +337,23 @@ public sealed partial class MechSystem : SharedMechSystem
|
|||
if (battery == null)
|
||||
return false;
|
||||
|
||||
if (!TryComp<BatteryComponent>(battery, out var batteryComp))
|
||||
if (!TryComp<PredictedBatteryComponent>(battery, out var batteryComp))
|
||||
return false;
|
||||
|
||||
_battery.SetCharge((battery.Value, batteryComp), batteryComp.CurrentCharge + delta.Float());
|
||||
if (batteryComp.CurrentCharge != component.Energy) //if there's a discrepency, we have to resync them
|
||||
_battery.SetCharge((battery.Value, batteryComp), _battery.GetCharge((battery.Value, batteryComp)) + delta.Float());
|
||||
// TODO: Power cells are predicted now, so no need to duplicate the charge level
|
||||
var charge = _battery.GetCharge((battery.Value, batteryComp));
|
||||
if (charge != component.Energy) //if there's a discrepency, we have to resync them
|
||||
{
|
||||
Log.Debug($"Battery charge was not equal to mech charge. Battery {batteryComp.CurrentCharge}. Mech {component.Energy}");
|
||||
component.Energy = batteryComp.CurrentCharge;
|
||||
Log.Debug($"Battery charge was not equal to mech charge. Battery {charge}. Mech {component.Energy}");
|
||||
component.Energy = charge;
|
||||
Dirty(uid, component);
|
||||
}
|
||||
_actionBlocker.UpdateCanMove(uid);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void InsertBattery(EntityUid uid, EntityUid toInsert, MechComponent? component = null, BatteryComponent? battery = null)
|
||||
public void InsertBattery(EntityUid uid, EntityUid toInsert, MechComponent? component = null, PredictedBatteryComponent? battery = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component, false))
|
||||
return;
|
||||
|
|
@ -360,7 +362,7 @@ public sealed partial class MechSystem : SharedMechSystem
|
|||
return;
|
||||
|
||||
_container.Insert(toInsert, component.BatterySlot);
|
||||
component.Energy = battery.CurrentCharge;
|
||||
component.Energy = _battery.GetCharge((toInsert, battery));
|
||||
component.MaxEnergy = battery.MaxCharge;
|
||||
|
||||
_actionBlocker.UpdateCanMove(uid);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Storage;
|
||||
|
||||
namespace Content.Server.Medical.BiomassReclaimer
|
||||
|
|
@ -34,10 +35,10 @@ namespace Content.Server.Medical.BiomassReclaimer
|
|||
public float CurrentExpectedYield = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// The reagent that will be spilled while processing a mob.
|
||||
/// The reagents that will be spilled while processing a mob.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public string? BloodReagent;
|
||||
public Solution? BloodReagents = null;
|
||||
|
||||
/// <summary>
|
||||
/// Entities that can be randomly spawned while processing a mob.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ using Content.Shared.Administration.Logs;
|
|||
using Content.Shared.Audio;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Climbing.Events;
|
||||
using Content.Shared.Construction.Components;
|
||||
using Content.Shared.Database;
|
||||
|
|
@ -45,6 +45,7 @@ namespace Content.Server.Medical.BiomassReclaimer
|
|||
[Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly PuddleSystem _puddleSystem = default!;
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solution = default!;
|
||||
[Dependency] private readonly ThrowingSystem _throwing = default!;
|
||||
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
|
|
@ -68,10 +69,8 @@ namespace Content.Server.Medical.BiomassReclaimer
|
|||
|
||||
if (reclaimer.RandomMessTimer <= 0)
|
||||
{
|
||||
if (_robustRandom.Prob(0.2f) && reclaimer.BloodReagent is not null)
|
||||
if (_robustRandom.Prob(0.2f) && reclaimer.BloodReagents is { } blood)
|
||||
{
|
||||
Solution blood = new();
|
||||
blood.AddReagent(reclaimer.BloodReagent, 50);
|
||||
_puddleSystem.TrySpillAt(uid, blood, out _);
|
||||
}
|
||||
if (_robustRandom.Prob(0.03f) && reclaimer.SpawnedEntities.Count > 0)
|
||||
|
|
@ -92,7 +91,7 @@ namespace Content.Server.Medical.BiomassReclaimer
|
|||
reclaimer.CurrentExpectedYield = reclaimer.CurrentExpectedYield - actualYield; // store non-integer leftovers
|
||||
_material.SpawnMultipleFromMaterial(actualYield, BiomassPrototype, Transform(uid).Coordinates);
|
||||
|
||||
reclaimer.BloodReagent = null;
|
||||
reclaimer.BloodReagents = null;
|
||||
reclaimer.SpawnedEntities.Clear();
|
||||
RemCompDeferred<ActiveBiomassReclaimerComponent>(uid);
|
||||
}
|
||||
|
|
@ -208,9 +207,11 @@ namespace Content.Server.Medical.BiomassReclaimer
|
|||
var component = ent.Comp;
|
||||
AddComp<ActiveBiomassReclaimerComponent>(ent);
|
||||
|
||||
if (TryComp<BloodstreamComponent>(toProcess, out var stream))
|
||||
if (TryComp<BloodstreamComponent>(toProcess, out var stream) &&
|
||||
_solution.ResolveSolution(toProcess, stream.BloodSolutionName, ref stream.BloodSolution, out var solution))
|
||||
{
|
||||
component.BloodReagent = stream.BloodReagent;
|
||||
component.BloodReagents = solution.Clone();
|
||||
component.BloodReagents.ScaleSolution(50 / component.BloodReagents.Volume);
|
||||
}
|
||||
if (TryComp<ButcherableComponent>(toProcess, out var butcherableComponent))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using System.Linq;
|
||||
using Content.Server.DeviceNetwork;
|
||||
using Content.Server.DeviceNetwork.Systems;
|
||||
using Content.Server.PowerCell;
|
||||
using Content.Shared.PowerCell;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.DeviceNetwork.Events;
|
||||
using Content.Shared.Medical.CrewMonitoring;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ using Content.Server.Electrocution;
|
|||
using Content.Server.EUI;
|
||||
using Content.Server.Ghost;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.PowerCell;
|
||||
using Content.Shared.PowerCell;
|
||||
using Content.Shared.Traits.Assorted;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Damage.Components;
|
||||
|
|
@ -18,6 +18,8 @@ using Content.Shared.Mind;
|
|||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Movement.Pulling.Components;
|
||||
using Content.Shared.PowerCell;
|
||||
using Content.Shared.Timing;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Player;
|
||||
|
|
@ -44,6 +46,7 @@ public sealed class DefibrillatorSystem : EntitySystem
|
|||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||
[Dependency] private readonly UseDelaySystem _useDelay = default!;
|
||||
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
|
|
@ -179,6 +182,18 @@ public sealed class DefibrillatorSystem : EntitySystem
|
|||
|
||||
_audio.PlayPvs(component.ZapSound, uid);
|
||||
_electrocution.TryDoElectrocution(target, null, component.ZapDamage, component.WritheDuration, true, ignoreInsulation: true);
|
||||
|
||||
var interacters = new HashSet<EntityUid>();
|
||||
_interactionSystem.GetEntitiesInteractingWithTarget(target, interacters);
|
||||
foreach (var other in interacters)
|
||||
{
|
||||
if (other == user)
|
||||
continue;
|
||||
|
||||
// Anyone else still operating on the target gets zapped too
|
||||
_electrocution.TryDoElectrocution(other, null, component.ZapDamage, component.WritheDuration, true);
|
||||
}
|
||||
|
||||
if (!TryComp<UseDelayComponent>(uid, out var useDelay))
|
||||
return;
|
||||
_useDelay.SetLength((uid, useDelay), component.ZapDelay, component.DelayId);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using Content.Server.Medical.Components;
|
||||
using Content.Server.PowerCell;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Damage.Components;
|
||||
|
|
@ -12,6 +11,7 @@ using Content.Shared.Item.ItemToggle.Components;
|
|||
using Content.Shared.MedicalScanner;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.PowerCell;
|
||||
using Content.Shared.Temperature.Components;
|
||||
using Content.Shared.Traits.Assorted;
|
||||
using Robust.Server.GameObjects;
|
||||
|
|
@ -81,7 +81,7 @@ public sealed class HealthAnalyzerSystem : EntitySystem
|
|||
/// </summary>
|
||||
private void OnAfterInteract(Entity<HealthAnalyzerComponent> uid, ref AfterInteractEvent args)
|
||||
{
|
||||
if (args.Target == null || !args.CanReach || !HasComp<MobStateComponent>(args.Target) || !_cell.HasDrawCharge(uid, user: args.User))
|
||||
if (args.Target == null || !args.CanReach || !HasComp<MobStateComponent>(args.Target) || !_cell.HasDrawCharge(uid.Owner, user: args.User))
|
||||
return;
|
||||
|
||||
_audio.PlayPvs(uid.Comp.ScanningBeginSound, uid);
|
||||
|
|
@ -101,7 +101,7 @@ public sealed class HealthAnalyzerSystem : EntitySystem
|
|||
|
||||
private void OnDoAfter(Entity<HealthAnalyzerComponent> uid, ref HealthAnalyzerDoAfterEvent args)
|
||||
{
|
||||
if (args.Handled || args.Cancelled || args.Target == null || !_cell.HasDrawCharge(uid, user: args.User))
|
||||
if (args.Handled || args.Cancelled || args.Target == null || !_cell.HasDrawCharge(uid.Owner, user: args.User))
|
||||
return;
|
||||
|
||||
if (!uid.Comp.Silent)
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ public sealed partial class TargetObjectiveMindFilter : MindFilter
|
|||
if (entMan.TryGetComponent<TargetObjectiveComponent>(objective, out var kill) && kill.Target == mind.Owner)
|
||||
{
|
||||
// remove the mind if this objective is blacklisted
|
||||
if (whitelistSys.IsBlacklistPassOrNull(Blacklist, objective))
|
||||
if (whitelistSys.IsWhitelistPassOrNull(Blacklist, objective))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,7 @@ using Robust.Shared.Random;
|
|||
|
||||
namespace Content.Server.NameIdentifier;
|
||||
|
||||
/// <summary>
|
||||
/// Handles unique name identifiers for entities e.g. `monkey (MK-912)`
|
||||
/// </summary>
|
||||
public sealed class NameIdentifierSystem : EntitySystem
|
||||
public sealed class NameIdentifierSystem : SharedNameIdentifierSystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
||||
|
|
@ -28,7 +25,6 @@ public sealed class NameIdentifierSystem : EntitySystem
|
|||
|
||||
SubscribeLocalEvent<NameIdentifierComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<NameIdentifierComponent, ComponentShutdown>(OnComponentShutdown);
|
||||
SubscribeLocalEvent<NameIdentifierComponent, RefreshNameModifiersEvent>(OnRefreshNameModifiers);
|
||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(CleanupIds);
|
||||
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnReloadPrototypes);
|
||||
|
||||
|
|
@ -122,24 +118,6 @@ public sealed class NameIdentifierSystem : EntitySystem
|
|||
_nameModifier.RefreshNameModifiers(ent.Owner);
|
||||
}
|
||||
|
||||
private void OnRefreshNameModifiers(Entity<NameIdentifierComponent> ent, ref RefreshNameModifiersEvent args)
|
||||
{
|
||||
if (ent.Comp.Group is null)
|
||||
return;
|
||||
|
||||
// Don't apply the modifier if the component is being removed
|
||||
if (ent.Comp.LifeStage > ComponentLifeStage.Running)
|
||||
return;
|
||||
|
||||
if (!_prototypeManager.Resolve(ent.Comp.Group, out var group))
|
||||
return;
|
||||
|
||||
var format = group.FullName ? "name-identifier-format-full" : "name-identifier-format-append";
|
||||
// We apply the modifier with a low priority to keep it near the base name
|
||||
// "Beep (Si-4562) the zombie" instead of "Beep the zombie (Si-4562)"
|
||||
args.AddModifier(format, -10, ("identifier", ent.Comp.FullIdentifier));
|
||||
}
|
||||
|
||||
private void InitialSetupPrototypes()
|
||||
{
|
||||
EnsureIds();
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using Content.Shared.Ninja.Components;
|
|||
using Content.Shared.Ninja.Systems;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Power.Components;
|
||||
using Content.Shared.Power.EntitySystems;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
|
||||
namespace Content.Server.Ninja.Systems;
|
||||
|
|
@ -17,6 +18,7 @@ namespace Content.Server.Ninja.Systems;
|
|||
public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
|
||||
{
|
||||
[Dependency] private readonly BatterySystem _battery = default!;
|
||||
[Dependency] private readonly PredictedBatterySystem _predictedBattery = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
|
|
@ -37,7 +39,7 @@ public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
|
|||
{
|
||||
var (uid, comp) = ent;
|
||||
var target = args.Target;
|
||||
if (args.Handled || comp.BatteryUid is not {} battery || !HasComp<PowerNetworkBatteryComponent>(target))
|
||||
if (args.Handled || comp.BatteryUid is not { } battery || !HasComp<PowerNetworkBatteryComponent>(target))
|
||||
return;
|
||||
|
||||
// handles even if battery is full so you can actually see the poup
|
||||
|
|
@ -70,7 +72,7 @@ public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
|
|||
{
|
||||
base.OnDoAfterAttempt(ent, ref args);
|
||||
|
||||
if (ent.Comp.BatteryUid is not {} battery || _battery.IsFull(battery))
|
||||
if (ent.Comp.BatteryUid is not { } battery || _battery.IsFull(battery))
|
||||
args.Cancel();
|
||||
}
|
||||
|
||||
|
|
@ -78,7 +80,7 @@ public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
|
|||
protected override bool TryDrainPower(Entity<BatteryDrainerComponent> ent, EntityUid target)
|
||||
{
|
||||
var (uid, comp) = ent;
|
||||
if (comp.BatteryUid == null || !TryComp<BatteryComponent>(comp.BatteryUid.Value, out var battery))
|
||||
if (comp.BatteryUid == null || !TryComp<PredictedBatteryComponent>(comp.BatteryUid.Value, out var battery))
|
||||
return false;
|
||||
|
||||
if (!TryComp<BatteryComponent>(target, out var targetBattery) || !TryComp<PowerNetworkBatteryComponent>(target, out var pnb))
|
||||
|
|
@ -91,7 +93,7 @@ public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
|
|||
}
|
||||
|
||||
var available = targetBattery.CurrentCharge;
|
||||
var required = battery.MaxCharge - battery.CurrentCharge;
|
||||
var required = battery.MaxCharge - _predictedBattery.GetCharge((comp.BatteryUid.Value, battery));
|
||||
// higher tier storages can charge more
|
||||
var maxDrained = pnb.MaxSupply * comp.DrainTime;
|
||||
var input = Math.Min(Math.Min(available, required / comp.DrainEfficiency), maxDrained);
|
||||
|
|
@ -99,13 +101,15 @@ public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
|
|||
return false;
|
||||
|
||||
var output = input * comp.DrainEfficiency;
|
||||
_battery.SetCharge((comp.BatteryUid.Value, battery), battery.CurrentCharge + output);
|
||||
// PowerCells use PredictedBatteryComponent
|
||||
// SMES, substations and APCs use BatteryComponent
|
||||
_predictedBattery.ChangeCharge((comp.BatteryUid.Value, battery), output);
|
||||
// TODO: create effect message or something
|
||||
Spawn("EffectSparks", Transform(target).Coordinates);
|
||||
_audio.PlayPvs(comp.SparkSound, target);
|
||||
_popup.PopupEntity(Loc.GetString("battery-drainer-success", ("battery", target)), uid, uid);
|
||||
|
||||
// repeat the doafter until battery is full
|
||||
return !_battery.IsFull((comp.BatteryUid.Value, battery));
|
||||
return !_predictedBattery.IsFull((comp.BatteryUid.Value, battery));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
using Content.Server.Ninja.Events;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Ninja.Components;
|
||||
using Content.Shared.Ninja.Systems;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Power.EntitySystems;
|
||||
|
||||
namespace Content.Server.Ninja.Systems;
|
||||
|
||||
public sealed class ItemCreatorSystem : SharedItemCreatorSystem
|
||||
{
|
||||
[Dependency] private readonly BatterySystem _battery = default!;
|
||||
[Dependency] private readonly PredictedBatterySystem _battery = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
|
||||
|
|
@ -24,7 +24,7 @@ public sealed class ItemCreatorSystem : SharedItemCreatorSystem
|
|||
private void OnCreateItem(Entity<ItemCreatorComponent> ent, ref CreateItemEvent args)
|
||||
{
|
||||
var (uid, comp) = ent;
|
||||
if (comp.Battery is not {} battery)
|
||||
if (comp.Battery is not { } battery)
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
using Content.Server.Ninja.Events;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.PowerCell;
|
||||
using Content.Shared.Emp;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Ninja.Components;
|
||||
using Content.Shared.Ninja.Systems;
|
||||
using Content.Shared.Power.Components;
|
||||
using Content.Shared.PowerCell;
|
||||
using Content.Shared.PowerCell.Components;
|
||||
using Robust.Shared.Containers;
|
||||
|
||||
|
|
@ -13,6 +12,7 @@ namespace Content.Server.Ninja.Systems;
|
|||
|
||||
/// <summary>
|
||||
/// Handles power cell upgrading and actions.
|
||||
/// TODO: Move all of this to shared and predict it
|
||||
/// </summary>
|
||||
public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
|
||||
{
|
||||
|
|
@ -51,8 +51,6 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
|
|||
RaiseLocalEvent(user, ref ev);
|
||||
}
|
||||
|
||||
// TODO: if/when battery is in shared, put this there too
|
||||
// TODO: or put MaxCharge in shared along with powercellslot
|
||||
private void OnSuitInsertAttempt(EntityUid uid, NinjaSuitComponent comp, ContainerIsInsertingAttemptEvent args)
|
||||
{
|
||||
// this is for handling battery upgrading, not stopping actions from being added
|
||||
|
|
@ -61,10 +59,10 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
|
|||
return;
|
||||
|
||||
// no power cell for some reason??? allow it
|
||||
if (!_powerCell.TryGetBatteryFromSlot(uid, out var batteryUid, out var battery))
|
||||
if (!_powerCell.TryGetBatteryFromSlot(uid, out var battery))
|
||||
return;
|
||||
|
||||
if (!TryComp<BatteryComponent>(args.EntityUid, out var inserting))
|
||||
if (!TryComp<PredictedBatteryComponent>(args.EntityUid, out var inserting))
|
||||
{
|
||||
args.Cancel();
|
||||
return;
|
||||
|
|
@ -73,7 +71,7 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
|
|||
var user = Transform(uid).ParentUid;
|
||||
|
||||
// can only upgrade power cell, not swap to recharge instantly otherwise ninja could just swap batteries with flashlights in maints for easy power
|
||||
if (GetCellScore(args.EntityUid, inserting) <= GetCellScore(batteryUid.Value, battery))
|
||||
if (GetCellScore(args.EntityUid, inserting) <= GetCellScore(battery.Value, battery.Value))
|
||||
{
|
||||
args.Cancel();
|
||||
Popup.PopupEntity(Loc.GetString("ninja-cell-downgrade"), user, user);
|
||||
|
|
@ -90,11 +88,11 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
|
|||
}
|
||||
|
||||
// this function assigns a score to a power cell depending on the capacity, to be used when comparing which cell is better.
|
||||
private float GetCellScore(EntityUid uid, BatteryComponent battcomp)
|
||||
private float GetCellScore(EntityUid uid, PredictedBatteryComponent battcomp)
|
||||
{
|
||||
// if a cell is able to automatically recharge, boost the score drastically depending on the recharge rate,
|
||||
// this is to ensure a ninja can still upgrade to a micro reactor cell even if they already have a medium or high.
|
||||
if (TryComp<BatterySelfRechargerComponent>(uid, out var selfcomp) && selfcomp.AutoRecharge)
|
||||
if (TryComp<PredictedBatterySelfRechargerComponent>(uid, out var selfcomp))
|
||||
return battcomp.MaxCharge + selfcomp.AutoRechargeRate * AutoRechargeValue;
|
||||
return battcomp.MaxCharge;
|
||||
}
|
||||
|
|
@ -136,7 +134,6 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
|
|||
Popup.PopupEntity(Loc.GetString(message), user, user);
|
||||
}
|
||||
|
||||
// TODO: Move this to shared when power cells are predicted.
|
||||
private void OnEmp(Entity<NinjaSuitComponent> ent, ref NinjaEmpEvent args)
|
||||
{
|
||||
var (uid, comp) = ent;
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ using Content.Server.Communications;
|
|||
using Content.Server.CriminalRecords.Systems;
|
||||
using Content.Server.Objectives.Components;
|
||||
using Content.Server.Objectives.Systems;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Server.PowerCell;
|
||||
using Content.Server.Research.Systems;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Doors.Components;
|
||||
|
|
@ -12,6 +10,8 @@ using Content.Shared.Mind;
|
|||
using Content.Shared.Ninja.Components;
|
||||
using Content.Shared.Ninja.Systems;
|
||||
using Content.Shared.Power.Components;
|
||||
using Content.Shared.Power.EntitySystems;
|
||||
using Content.Shared.PowerCell;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Rounding;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
|
@ -24,7 +24,7 @@ namespace Content.Server.Ninja.Systems;
|
|||
public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
|
||||
{
|
||||
[Dependency] private readonly AlertsSystem _alerts = default!;
|
||||
[Dependency] private readonly BatterySystem _battery = default!;
|
||||
[Dependency] private readonly PredictedBatterySystem _battery = default!;
|
||||
[Dependency] private readonly CodeConditionSystem _codeCondition = default!;
|
||||
[Dependency] private readonly PowerCellSystem _powerCell = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||
|
|
@ -39,6 +39,8 @@ public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
|
|||
SubscribeLocalEvent<SpaceNinjaComponent, CriminalRecordsHackedEvent>(OnCriminalRecordsHacked);
|
||||
}
|
||||
|
||||
// TODO: Make this charge rate based instead of updating it every single tick.
|
||||
// Or make it client side, since power cells are predicted.
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
var query = EntityQueryEnumerator<SpaceNinjaComponent>();
|
||||
|
|
@ -62,7 +64,7 @@ public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
|
|||
return newCount - oldCount;
|
||||
}
|
||||
|
||||
// TODO: can probably copy paste borg code here
|
||||
// TODO: Generic charge indicator that is combined with borg code.
|
||||
/// <summary>
|
||||
/// Update the alert for the ninja's suit power indicator.
|
||||
/// </summary>
|
||||
|
|
@ -75,10 +77,10 @@ public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
|
|||
return;
|
||||
}
|
||||
|
||||
if (GetNinjaBattery(uid, out _, out var battery))
|
||||
if (GetNinjaBattery(uid, out var batteryUid, out var batteryComp))
|
||||
{
|
||||
var severity = ContentHelpers.RoundToLevels(MathF.Max(0f, battery.CurrentCharge), battery.MaxCharge, 8);
|
||||
_alerts.ShowAlert(uid, comp.SuitPowerAlert, (short) severity);
|
||||
var severity = ContentHelpers.RoundToLevels(MathF.Max(0f, _battery.GetCharge((batteryUid.Value, batteryComp))), batteryComp.MaxCharge, 8);
|
||||
_alerts.ShowAlert(uid, comp.SuitPowerAlert, (short)severity);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -89,17 +91,19 @@ public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
|
|||
/// <summary>
|
||||
/// Get the battery component in a ninja's suit, if it's worn.
|
||||
/// </summary>
|
||||
public bool GetNinjaBattery(EntityUid user, [NotNullWhen(true)] out EntityUid? uid, [NotNullWhen(true)] out BatteryComponent? battery)
|
||||
public bool GetNinjaBattery(EntityUid user, [NotNullWhen(true)] out EntityUid? batteryUid, [NotNullWhen(true)] out PredictedBatteryComponent? batteryComp)
|
||||
{
|
||||
if (TryComp<SpaceNinjaComponent>(user, out var ninja)
|
||||
&& ninja.Suit != null
|
||||
&& _powerCell.TryGetBatteryFromSlot(ninja.Suit.Value, out uid, out battery))
|
||||
&& _powerCell.TryGetBatteryFromSlot(ninja.Suit.Value, out var battery))
|
||||
{
|
||||
batteryUid = battery.Value.Owner;
|
||||
batteryComp = battery.Value.Comp;
|
||||
return true;
|
||||
}
|
||||
|
||||
uid = null;
|
||||
battery = null;
|
||||
batteryUid = null;
|
||||
batteryComp = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
using Content.Server.Ninja.Events;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Ninja.Components;
|
||||
using Content.Shared.Ninja.Systems;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Power.EntitySystems;
|
||||
using Content.Shared.Stunnable;
|
||||
using Content.Shared.Timing;
|
||||
using Content.Shared.Whitelist;
|
||||
|
|
@ -17,7 +17,7 @@ namespace Content.Server.Ninja.Systems;
|
|||
/// </summary>
|
||||
public sealed class StunProviderSystem : SharedStunProviderSystem
|
||||
{
|
||||
[Dependency] private readonly BatterySystem _battery = default!;
|
||||
[Dependency] private readonly PredictedBatterySystem _battery = default!;
|
||||
[Dependency] private readonly DamageableSystem _damageable = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ using Content.Server.AlertLevel;
|
|||
using Content.Server.Audio;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.Explosion.EntitySystems;
|
||||
using Content.Server.Kitchen.Components;
|
||||
using Content.Server.Pinpointer;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Station.Systems;
|
||||
|
|
@ -11,6 +10,7 @@ using Content.Shared.Containers.ItemSlots;
|
|||
using Content.Shared.Coordinates.Helpers;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Kitchen;
|
||||
using Content.Shared.Maps;
|
||||
using Content.Shared.Nuke;
|
||||
using Content.Shared.Popups;
|
||||
|
|
|
|||
|
|
@ -44,8 +44,11 @@ public sealed class WarDeclaratorSystem : EntitySystem
|
|||
{
|
||||
if (!_accessReaderSystem.IsAllowed(args.User, ent))
|
||||
{
|
||||
var msg = Loc.GetString("war-declarator-not-working");
|
||||
_popupSystem.PopupEntity(msg, ent);
|
||||
if (!args.Silent)
|
||||
{
|
||||
var msg = Loc.GetString("war-declarator-not-working");
|
||||
_popupSystem.PopupEntity(msg, ent);
|
||||
}
|
||||
args.Cancel();
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ public sealed class NinjaConditionsSystem : EntitySystem
|
|||
|
||||
while (allEnts.MoveNext(out var warpUid, out var warp))
|
||||
{
|
||||
if (_whitelist.IsBlacklistFail(bombingBlacklist, warpUid)
|
||||
if (_whitelist.IsWhitelistFail(bombingBlacklist, warpUid)
|
||||
&& !string.IsNullOrWhiteSpace(warp.Location))
|
||||
{
|
||||
warps.Add(warpUid);
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ public sealed class ObjectiveBlacklistRequirementSystem : EntitySystem
|
|||
|
||||
foreach (var objective in args.Mind.Objectives)
|
||||
{
|
||||
if (_whitelistSystem.IsBlacklistPass(comp.Blacklist, objective))
|
||||
if (_whitelistSystem.IsWhitelistPass(comp.Blacklist, objective))
|
||||
{
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
using Content.Server.Ghost.Roles;
|
||||
using Content.Server.Ghost.Roles.Components;
|
||||
using Content.Server.Instruments;
|
||||
using Content.Server.Kitchen.Components;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Kitchen;
|
||||
using Content.Shared.PAI;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Instruments;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using Content.Server.PowerCell;
|
||||
using Content.Shared.PowerCell;
|
||||
using Content.Shared.Pinpointer;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Player;
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue