Stable release (#41759)

This commit is contained in:
Myra 2025-12-07 16:13:23 +01:00 • committed by GitHub
commit db366bc20d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
491 changed files with 139984 additions and 177663 deletions

8
.github/CODEOWNERS vendored
View file

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

View file

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

View file

@ -0,0 +1,7 @@
using Content.Shared.Administration.Systems;
namespace Content.Client.Administration.Systems;
public sealed class BufferingSystem : SharedBufferingSystem
{
}

View file

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

View file

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

View file

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

View file

@ -1,8 +0,0 @@
using Content.Shared.Bed;
namespace Content.Client.Bed;
public sealed class BedSystem : SharedBedSystem
{
}

View file

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

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,5 @@
using Content.Shared.NameIdentifier;
namespace Content.Client.NameIdentifier;
public sealed class NameIdentifierSystem : SharedNameIdentifierSystem;

View file

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

View file

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

View file

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

View file

@ -1,5 +0,0 @@
using Content.Shared.Power.EntitySystems;
namespace Content.Client.Power.EntitySystems;
public sealed class ChargerSystem : SharedChargerSystem;

View file

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

View 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,
}

View file

@ -1,4 +0,0 @@
namespace Content.Client.PowerCell;
[RegisterComponent]
public sealed partial class PowerCellVisualsComponent : Component {}

View file

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

View file

@ -1,4 +1,4 @@
using Content.Shared.Power;
using Content.Shared.Power.Components;
using Robust.Client.GameObjects;
namespace Content.Client.PowerCell;

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +0,0 @@
namespace Content.Server.Administration.Components;
[RegisterComponent]
public sealed partial class AdminMinigunComponent : Component
{
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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