Converts the particle accelerator over to ECS + misc (#17075)

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
This commit is contained in:
TemporalOroboros
2023-06-07 23:25:59 -07:00
committed by GitHub
parent a3137cc0f0
commit d71b6c84e5
61 changed files with 1293 additions and 1169 deletions

View File

@@ -1,715 +1,170 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using Content.Server.Administration.Logs;
using Content.Server.Mind.Components;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.UserInterface;
using Content.Shared.Database;
using Content.Server.ParticleAccelerator.Wires;
using Content.Shared.Singularity.Components;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Utility;
using Timer = Robust.Shared.Timing.Timer;
// using Content.Server.WireHacking;
// using static Content.Shared.Wires.SharedWiresComponent;
namespace Content.Server.ParticleAccelerator.Components
namespace Content.Server.ParticleAccelerator.Components;
// This component is in control of the PA's logic because it's the one to contain the wires for hacking.
// And also it's the only PA component that meaningfully needs to work on its own.
/// <summary>
/// Is the computer thing people interact with to control the PA.
/// Also contains primary logic for actual PA behavior, part scanning, etc...
/// </summary>
[RegisterComponent]
public sealed class ParticleAcceleratorControlBoxComponent : Component
{
// This component is in control of the PA's logic because it's the one to contain the wires for hacking.
// And also it's the only PA component that meaningfully needs to work on its own.
/// <summary>
/// Is the computer thing people interact with to control the PA.
/// Also contains primary logic for actual PA behavior, part scanning, etc...
/// Whether the PA parts have been correctly arranged to make a functional device.
/// </summary>
[RegisterComponent]
public sealed class ParticleAcceleratorControlBoxComponent : ParticleAcceleratorPartComponent
{
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[ViewVariables]
private BoundUserInterface? UserInterface => Owner.GetUIOrNull(ParticleAcceleratorControlBoxUiKey.Key);
/// <summary>
/// Power receiver for the control console itself.
/// </summary>
[ViewVariables] private ApcPowerReceiverComponent _apcPowerReceiverComponent = default!;
[ViewVariables] private ParticleAcceleratorFuelChamberComponent? _partFuelChamber;
[ViewVariables] private ParticleAcceleratorEndCapComponent? _partEndCap;
[ViewVariables] private ParticleAcceleratorPowerBoxComponent? _partPowerBox;
[ViewVariables] private ParticleAcceleratorEmitterComponent? _partEmitterLeft;
[ViewVariables] private ParticleAcceleratorEmitterComponent? _partEmitterCenter;
[ViewVariables] private ParticleAcceleratorEmitterComponent? _partEmitterRight;
[ViewVariables] private ParticleAcceleratorPowerState _selectedStrength = ParticleAcceleratorPowerState.Standby;
[ViewVariables] private bool _isAssembled;
// Enabled: power switch is on
[ViewVariables] private bool _isEnabled;
// Powered: power switch is on AND the PA is actively firing (if not on standby)
[ViewVariables] private bool _isPowered;
[ViewVariables] private bool _wireInterfaceBlocked;
[ViewVariables] private bool _wirePowerBlocked;
[ViewVariables] private bool _wireLimiterCut;
[ViewVariables] private bool _wireStrengthCut;
[ViewVariables] private CancellationTokenSource? _fireCancelTokenSrc;
/// <summary>
/// Delay between consecutive PA shots.
/// </summary>
// Fun fact:
// On /vg/station (can't check TG because lol they removed singulo),
// the PA emitter parts have var/fire_delay = 50.
// For anybody from the future not BYOND-initiated, that's 5 seconds.
// However, /obj/machinery/particle_accelerator/control_box/process(),
// which calls emit_particle() on the emitters,
// only gets called every *2* seconds, because of CarnMC timing.
// So the *actual* effective firing delay of the PA is 6 seconds, not 5 as listed in the code.
// So...
// I have reflected that here to be authentic.
[ViewVariables(VVAccess.ReadWrite)] [DataField("fireDelay")] private TimeSpan _firingDelay = TimeSpan.FromSeconds(6);
[ViewVariables(VVAccess.ReadWrite)] [DataField("powerDrawBase")] private int _powerDrawBase = 500;
[ViewVariables(VVAccess.ReadWrite)] [DataField("powerDrawMult")] private int _powerDrawMult = 1500;
[ViewVariables] private bool ConsolePowered => _apcPowerReceiverComponent?.Powered ?? true;
public ParticleAcceleratorControlBoxComponent()
{
Master = this;
}
private ParticleAcceleratorPowerState MaxPower => _wireLimiterCut
? ParticleAcceleratorPowerState.Level3
: ParticleAcceleratorPowerState.Level2;
protected override void Initialize()
{
base.Initialize();
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
}
Owner.EnsureComponent(out _apcPowerReceiverComponent);
_apcPowerReceiverComponent.Load = 250;
}
// This is the power state for the PA control box itself.
// Keep in mind that the PA itself can keep firing as long as the HV cable under the power box has... power.
public void OnPowerStateChanged(PowerChangedEvent e)
{
UpdateAppearance();
if (!e.Powered)
{
UserInterface?.CloseAll();
}
}
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage obj)
{
if (!ConsolePowered)
{
return;
}
if (_wireInterfaceBlocked)
{
return;
}
switch (obj.Message)
{
case ParticleAcceleratorSetEnableMessage enableMessage:
if (enableMessage.Enabled)
{
SwitchOn(obj.Session);
}
else
{
SwitchOff(obj.Session);
}
break;
case ParticleAcceleratorSetPowerStateMessage stateMessage:
SetStrength(stateMessage.State, obj.Session);
break;
case ParticleAcceleratorRescanPartsMessage _:
RescanParts(obj.Session);
break;
}
UpdateUI();
}
public void UpdateUI()
{
var draw = 0f;
var receive = 0f;
if (_isEnabled)
{
draw = _partPowerBox!.PowerConsumerComponent!.DrawRate;
receive = _partPowerBox!.PowerConsumerComponent!.ReceivedPower;
}
var state = new ParticleAcceleratorUIState(
_isAssembled,
_isEnabled,
_selectedStrength,
(int) draw,
(int) receive,
_partEmitterLeft != null,
_partEmitterCenter != null,
_partEmitterRight != null,
_partPowerBox != null,
_partFuelChamber != null,
_partEndCap != null,
_wireInterfaceBlocked,
MaxPower,
_wirePowerBlocked);
UserInterface?.SetState(state);
}
protected override void OnRemove()
{
_fireCancelTokenSrc?.Cancel();
_fireCancelTokenSrc = null;
Master = null;
foreach (var part in AllParts())
{
if (_entMan.TryGetComponent(part.Owner, out ParticleAcceleratorPartComponent? paPart))
paPart.Master = null;
}
base.OnRemove();
}
/*
void IWires.RegisterWires(WiresComponent.WiresBuilder builder)
{
builder.CreateWire(ParticleAcceleratorControlBoxWires.Toggle);
builder.CreateWire(ParticleAcceleratorControlBoxWires.Strength);
builder.CreateWire(ParticleAcceleratorControlBoxWires.Interface);
builder.CreateWire(ParticleAcceleratorControlBoxWires.Limiter);
builder.CreateWire(ParticleAcceleratorControlBoxWires.Nothing);
}
public void WiresUpdate(WiresUpdateEventArgs args)
{
switch (args.Identifier)
{
case ParticleAcceleratorControlBoxWires.Toggle:
if (args.Action == WiresAction.Pulse)
{
if (_isEnabled)
{
SwitchOff();
}
else
{
SwitchOn();
}
}
else
{
_wirePowerBlocked = args.Action == WiresAction.Cut;
if (_isEnabled)
{
SwitchOff();
}
}
break;
case ParticleAcceleratorControlBoxWires.Strength:
if (args.Action == WiresAction.Pulse)
{
SetStrength(_selectedStrength + 1);
}
else
{
_wireStrengthCut = args.Action == WiresAction.Cut;
}
break;
case ParticleAcceleratorControlBoxWires.Interface:
if (args.Action == WiresAction.Pulse)
{
_wireInterfaceBlocked ^= true;
}
else
{
_wireInterfaceBlocked = args.Action == WiresAction.Cut;
}
break;
case ParticleAcceleratorControlBoxWires.Limiter:
if (args.Action == WiresAction.Pulse)
{
Owner.PopupMessageEveryone(Loc.GetString("particle-accelerator-control-box-component-wires-update-limiter-on-pulse"));
}
else
{
_wireLimiterCut = args.Action == WiresAction.Cut;
if (_selectedStrength == ParticleAcceleratorPowerState.Level3 && !_wireLimiterCut)
{
// Yes, it's a feature that mending this wire WON'T WORK if the strength wire is also cut.
// Since that blocks SetStrength().
SetStrength(ParticleAcceleratorPowerState.Level2);
}
}
break;
}
UpdateUI();
UpdateWireStatus();
}
private void UpdateWireStatus()
{
if (!_entMan.TryGetComponent(Owner, out WiresComponent? wires))
{
return;
}
var powerBlock = _wirePowerBlocked;
var keyboardLight = new StatusLightData(Color.LimeGreen,
_wireInterfaceBlocked
? StatusLightState.BlinkingFast
: StatusLightState.On,
"KEYB");
var powerLight = new StatusLightData(
Color.Yellow,
powerBlock ? StatusLightState.Off : StatusLightState.On,
"POWR");
var limiterLight = new StatusLightData(
_wireLimiterCut ? Color.Purple : Color.Teal,
StatusLightState.On,
"LIMT");
var strengthLight = new StatusLightData(
Color.Blue,
_wireStrengthCut ? StatusLightState.BlinkingSlow : StatusLightState.On,
"STRC");
wires.SetStatus(ParticleAcceleratorWireStatus.Keyboard, keyboardLight);
wires.SetStatus(ParticleAcceleratorWireStatus.Power, powerLight);
wires.SetStatus(ParticleAcceleratorWireStatus.Limiter, limiterLight);
wires.SetStatus(ParticleAcceleratorWireStatus.Strength, strengthLight);
}
*/
public void RescanParts(IPlayerSession? playerSession = null)
{
SwitchOff(playerSession, true);
foreach (var part in AllParts())
{
if (_entMan.TryGetComponent(part.Owner, out ParticleAcceleratorPartComponent? paPart))
paPart.Master = null;
}
_isAssembled = false;
_partFuelChamber = null;
_partEndCap = null;
_partPowerBox = null;
_partEmitterLeft = null;
_partEmitterCenter = null;
_partEmitterRight = null;
var xform = _entMan.GetComponent<TransformComponent>(Owner);
// Find fuel chamber first by scanning cardinals.
if (xform.Anchored && _entMan.TryGetComponent(xform.GridUid, out MapGridComponent? grid))
{
foreach (var maybeFuel in grid.GetCardinalNeighborCells(xform.Coordinates))
{
if (_entMan.TryGetComponent(maybeFuel, out _partFuelChamber))
{
break;
}
}
}
if (_partFuelChamber == null)
{
UpdateUI();
return;
}
// Align ourselves to match fuel chamber orientation.
// This means that if you mess up the orientation of the control box it's not a big deal,
// because the sprite is far from obvious about the orientation.
xform.LocalRotation = _entMan.GetComponent<TransformComponent>(_partFuelChamber.Owner).LocalRotation;
var offsetEndCap = RotateOffset((1, 1));
var offsetPowerBox = RotateOffset((1, -1));
var offsetEmitterLeft = RotateOffset((0, -2));
var offsetEmitterCenter = RotateOffset((1, -2));
var offsetEmitterRight = RotateOffset((2, -2));
ScanPart(offsetEndCap, out _partEndCap);
ScanPart(offsetPowerBox, out _partPowerBox);
if (!ScanPart(offsetEmitterCenter, out _partEmitterCenter) ||
_partEmitterCenter.Type != ParticleAcceleratorEmitterType.Center)
{
// if it's the wrong type we need to clear this to avoid shenanigans.
_partEmitterCenter = null;
}
if (ScanPart(offsetEmitterLeft, out _partEmitterLeft) &&
_partEmitterLeft.Type != ParticleAcceleratorEmitterType.Left)
{
_partEmitterLeft = null;
}
if (ScanPart(offsetEmitterRight, out _partEmitterRight) &&
_partEmitterRight.Type != ParticleAcceleratorEmitterType.Right)
{
_partEmitterRight = null;
}
_isAssembled = _partFuelChamber != null &&
_partPowerBox != null &&
_partEmitterCenter != null &&
_partEmitterLeft != null &&
_partEmitterRight != null &&
_partEndCap != null;
foreach (var part in AllParts())
{
if (_entMan.TryGetComponent(part.Owner, out ParticleAcceleratorPartComponent? paPart))
paPart.Master = this;
}
UpdateUI();
Vector2i RotateOffset(in Vector2i vec)
{
var rot = new Angle(_entMan.GetComponent<TransformComponent>(Owner).LocalRotation);
return (Vector2i) rot.RotateVec(vec);
}
}
private bool ScanPart<T>(Vector2i offset, [NotNullWhen(true)] out T? part)
where T : Component
{
var xform = _entMan.GetComponent<TransformComponent>(Owner);
if (!_mapManager.TryGetGrid(xform.GridUid, out var grid))
{
part = default;
return false;
}
var coords = xform.Coordinates;
foreach (var ent in grid.GetOffset(coords, offset))
{
if (_entMan.TryGetComponent(ent, out part) && !part.Deleted)
{
return true;
}
}
part = default;
return false;
}
private IEnumerable<Component> AllParts()
{
if (_partFuelChamber != null)
yield return _partFuelChamber;
if (_partEndCap != null)
yield return _partEndCap;
if (_partPowerBox != null)
yield return _partPowerBox;
if (_partEmitterLeft != null)
yield return _partEmitterLeft;
if (_partEmitterCenter != null)
yield return _partEmitterCenter;
if (_partEmitterRight != null)
yield return _partEmitterRight;
}
public void SwitchOn(IPlayerSession? playerSession = null)
{
DebugTools.Assert(_isAssembled);
if (_isEnabled)
{
return;
}
// Logging
_entMan.TryGetComponent(playerSession?.AttachedEntity, out MindComponent? mindComponent);
if(mindComponent != null)
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{_entMan.ToPrettyString(mindComponent.Owner):player} has set {_entMan.ToPrettyString(Owner)} to on");
_isEnabled = true;
UpdatePowerDraw();
// If we don't have power yet we'll turn on when we receive more power from the powernet.
// if we do we'll just go and turn on right now.
if (_partPowerBox!.PowerConsumerComponent!.ReceivedPower >= _partPowerBox.PowerConsumerComponent.DrawRate)
{
PowerOn();
}
UpdateUI();
}
private void UpdatePowerDraw()
{
_partPowerBox!.PowerConsumerComponent!.DrawRate = PowerDrawFor(_selectedStrength);
}
public void SwitchOff(IPlayerSession? playerSession = null, bool rescan = false)
{
// Logging
_entMan.TryGetComponent(playerSession?.AttachedEntity, out MindComponent? mindComponent);
if(mindComponent != null)
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{_entMan.ToPrettyString(mindComponent.Owner):player} has set {_entMan.ToPrettyString(Owner)} to off{(rescan ? " via rescan" : "")}");
_isEnabled = false;
PowerOff();
UpdateUI();
}
private void PowerOn()
{
DebugTools.Assert(_isEnabled);
DebugTools.Assert(_isAssembled);
if (_isPowered)
{
return;
}
_isPowered = true;
UpdateFiring();
UpdatePartVisualStates();
UpdateUI();
}
private void PowerOff()
{
if (!_isPowered)
{
return;
}
_isPowered = false;
UpdateFiring();
UpdateUI();
UpdatePartVisualStates();
}
public void SetStrength(ParticleAcceleratorPowerState state, IPlayerSession? playerSession = null)
{
if (_wireStrengthCut)
{
return;
}
state = (ParticleAcceleratorPowerState) MathHelper.Clamp(
(int) state,
(int) ParticleAcceleratorPowerState.Standby,
(int) MaxPower);
_selectedStrength = state;
UpdateAppearance();
UpdatePartVisualStates();
// Logging
_entMan.TryGetComponent(playerSession?.AttachedEntity, out MindComponent? mindComponent);
LogImpact impact;
switch (state)
{
default:
case ParticleAcceleratorPowerState.Standby:
case ParticleAcceleratorPowerState.Level0:
impact = LogImpact.Low;
break;
case ParticleAcceleratorPowerState.Level1:
impact = LogImpact.High;
break;
case ParticleAcceleratorPowerState.Level2:
case ParticleAcceleratorPowerState.Level3:
impact = LogImpact.Extreme;
break;
}
if(mindComponent != null)
_adminLogger.Add(LogType.Action, impact, $"{_entMan.ToPrettyString(mindComponent.Owner):player} has set the strength of {_entMan.ToPrettyString(Owner)} to {state}");
if (_isEnabled)
{
UpdatePowerDraw();
UpdateFiring();
}
}
private void UpdateAppearance()
{
if (_entMan.TryGetComponent(Owner, out AppearanceComponent? appearance))
{
appearance.SetData(ParticleAcceleratorVisuals.VisualState,
_apcPowerReceiverComponent!.Powered
? (ParticleAcceleratorVisualState) _selectedStrength
: ParticleAcceleratorVisualState.Unpowered);
}
}
private void UpdateFiring()
{
if (!_isPowered || _selectedStrength == ParticleAcceleratorPowerState.Standby)
{
StopFiring();
}
else
{
StartFiring();
}
}
private void StartFiring()
{
EverythingIsWellToFire();
_fireCancelTokenSrc?.Cancel();
_fireCancelTokenSrc = new CancellationTokenSource();
var cancelToken = _fireCancelTokenSrc.Token;
Timer.SpawnRepeating(_firingDelay, Fire, cancelToken);
}
private void Fire()
{
EverythingIsWellToFire();
_partEmitterCenter!.Fire(_selectedStrength);
_partEmitterLeft!.Fire(_selectedStrength);
_partEmitterRight!.Fire(_selectedStrength);
}
[Conditional("DEBUG")]
private void EverythingIsWellToFire()
{
DebugTools.Assert(!Deleted);
DebugTools.Assert(_isPowered);
DebugTools.Assert(_selectedStrength != ParticleAcceleratorPowerState.Standby);
DebugTools.Assert(_isAssembled);
DebugTools.Assert(_partEmitterCenter != null);
DebugTools.Assert(_partEmitterLeft != null);
DebugTools.Assert(_partEmitterRight != null);
}
private void StopFiring()
{
_fireCancelTokenSrc?.Cancel();
_fireCancelTokenSrc = null;
}
private int PowerDrawFor(ParticleAcceleratorPowerState strength)
{
return strength switch
{
ParticleAcceleratorPowerState.Standby => 0,
ParticleAcceleratorPowerState.Level0 => 1,
ParticleAcceleratorPowerState.Level1 => 3,
ParticleAcceleratorPowerState.Level2 => 4,
ParticleAcceleratorPowerState.Level3 => 5,
_ => 0
} * _powerDrawMult + _powerDrawBase;
}
public void PowerBoxReceivedChanged(PowerConsumerReceivedChanged eventArgs)
{
DebugTools.Assert(_isAssembled);
if (!_isEnabled)
{
return;
}
var isPowered = eventArgs.ReceivedPower >= eventArgs.DrawRate;
if (isPowered)
{
PowerOn();
}
else
{
PowerOff();
}
UpdateUI();
}
private void UpdatePartVisualStates()
{
// UpdatePartVisualState(ControlBox);
UpdatePartVisualState(_partFuelChamber);
UpdatePartVisualState(_partPowerBox);
UpdatePartVisualState(_partEmitterCenter);
UpdatePartVisualState(_partEmitterLeft);
UpdatePartVisualState(_partEmitterRight);
//no endcap because it has no powerlevel-sprites
}
private void UpdatePartVisualState(Component? component)
{
if (component == null || !_entMan.TryGetComponent<AppearanceComponent?>(component.Owner, out var appearanceComponent))
{
return;
}
var state = _isPowered
? (ParticleAcceleratorVisualState) _selectedStrength
: ParticleAcceleratorVisualState.Unpowered;
appearanceComponent.SetData(ParticleAcceleratorVisuals.VisualState, state);
}
public enum ParticleAcceleratorControlBoxWires
{
/// <summary>
/// Pulse toggles Power. Cut permanently turns off until Mend.
/// </summary>
Toggle,
/// <summary>
/// Pulsing increases level until at limit.
/// </summary>
Strength,
/// <summary>
/// Pulsing toggles Button-Disabled on UI. Cut disables, Mend enables.
/// </summary>
Interface,
/// <summary>
/// Pulsing will produce short message about whirring noise. Cutting increases the max level to 3. Mending reduces it back to 2.
/// </summary>
Limiter,
/// <summary>
/// Does Nothing
/// </summary>
Nothing
}
}
[ViewVariables]
public bool Assembled = false;
/// <summary>
/// Whether the PA is currently set to fire at the console.
/// Requires <see cref="Assembled"/> to be true.
/// </summary>
[ViewVariables]
public bool Enabled = false;
/// <summary>
/// Whether the PA actually has the power necessary to fire.
/// Requires <see cref="Enabled"/> to be true.
/// </summary>
[ViewVariables]
public bool Powered = false;
/// <summary>
/// Whether the PA is currently firing or charging to fire.
/// Requires <see cref="Powered"/> to be true.
/// </summary>
[ViewVariables]
public bool Firing = false;
/// <summary>
/// Whether the PA is currently firing or charging to fire.
/// Bounded by <see cref="ParticleAcceleratorPowerState.Standby"/> and <see cref="MaxStrength"/>.
/// Modified by <see cref="ParticleAcceleratorStrengthWireAction"/>.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public ParticleAcceleratorPowerState SelectedStrength = ParticleAcceleratorPowerState.Standby;
/// <summary>
/// The maximum strength level this particle accelerator can be set to operate at.
/// Modified by <see cref="ParticleAcceleratorLimiterWireAction"/>.
/// </summary>
[ViewVariables]
public ParticleAcceleratorPowerState MaxStrength = ParticleAcceleratorPowerState.Level2;
/// <summary>
/// The power supply unit of the assembled particle accelerator.
/// Implies the existance of a <see cref="ParticleAcceleratorPowerBoxComponent"/> attached to this entity.
/// </summary>
[ViewVariables]
public EntityUid? PowerBox;
/// <summary>
/// Whether the PA is currently firing or charging to fire.
/// Implies the existance of a <see cref="ParticleAcceleratorEndCapComponent"/> attached to this entity.
/// </summary>
[ViewVariables]
public EntityUid? EndCap;
/// <summary>
/// Whether the PA is currently firing or charging to fire.
/// Implies the existance of a <see cref="ParticleAcceleratorFuelChamberComponent"/> attached to this entity.
/// </summary>
[ViewVariables]
public EntityUid? FuelChamber;
/// <summary>
/// Whether the PA is currently firing or charging to fire.
/// Implies the existance of a <see cref="ParticleAcceleratorEmitterComponent"/> attached to this entity.
/// </summary>
[ViewVariables]
public EntityUid? PortEmitter;
/// <summary>
/// Whether the PA is currently firing or charging to fire.
/// Implies the existance of a <see cref="ParticleAcceleratorEmitterComponent"/> attached to this entity.
/// </summary>
[ViewVariables]
public EntityUid? ForeEmitter;
/// <summary>
/// Whether the PA is currently firing or charging to fire.
/// Implies the existance of a <see cref="ParticleAcceleratorEmitterComponent"/> attached to this entity.
/// </summary>
[ViewVariables]
public EntityUid? StarboardEmitter;
/// <summary>
/// The amount of power the particle accelerator must be provided with relative to the expected power draw to function.
/// </summary>
[ViewVariables]
public const float RequiredPowerRatio = 0.999f;
/// <summary>
/// The amount of power (in watts) the PA draws just by existing as a functional machine.
/// </summary>
[DataField("powerDrawBase")]
[ViewVariables(VVAccess.ReadWrite)]
public int BasePowerDraw = 500;
/// <summary>
/// The amount of power (in watts) the PA draws per level when turned on.
/// </summary>
[DataField("powerDrawMult")]
[ViewVariables(VVAccess.ReadWrite)]
public int LevelPowerDraw = 1500;
/// <summary>
/// The time at which the PA last fired a wave of particles.
/// </summary>
[DataField("lastFire")]
[ViewVariables(VVAccess.ReadWrite)]
public TimeSpan LastFire;
/// <summary>
/// The time at which the PA will next fire a wave of particles.
/// </summary>
[DataField("nextFire")]
[ViewVariables(VVAccess.ReadWrite)]
public TimeSpan NextFire;
/// <summary>
/// Delay between consecutive PA shots.
/// </summary>
// Fun fact:
// On /vg/station (can't check TG because lol they removed singulo),
// the PA emitter parts have var/fire_delay = 50.
// For anybody from the future not BYOND-initiated, that's 5 seconds.
// However, /obj/machinery/particle_accelerator/control_box/process(),
// which calls emit_particle() on the emitters,
// only gets called every *2* seconds, because of CarnMC timing.
// So the *actual* effective firing delay of the PA is 6 seconds, not 5 as listed in the code.
// So...
// I have reflected that here to be authentic.
[DataField("chargeTime")]
[ViewVariables(VVAccess.ReadWrite)]
public TimeSpan ChargeTime = TimeSpan.FromSeconds(6.0);
/// <summary>
/// Whether the interface has been disabled via a cut wire or not.
/// Modified by <see cref="ParticleAcceleratorKeyboardWireAction"/>.
/// </summary>
[ViewVariables]
public bool InterfaceDisabled = false;
/// <summary>
/// Whether the ability to change the strength of the PA has been disabled via a cut wire or not.
/// Modified by <see cref="ParticleAcceleratorStrengthWireAction"/>.
/// </summary>
[ViewVariables]
public bool StrengthLocked = false;
/// <summary>
/// Whether the PA can be turned on.
/// Modified by <see cref="ParticleAcceleratorPowerWireAction"/>.
/// </summary>
[ViewVariables]
public bool CanBeEnabled = true;
}

View File

@@ -1,36 +1,27 @@
using Content.Shared.Singularity.Components;
namespace Content.Server.ParticleAccelerator.Components
namespace Content.Server.ParticleAccelerator.Components;
[RegisterComponent]
public sealed class ParticleAcceleratorEmitterComponent : Component
{
[RegisterComponent]
public sealed class ParticleAcceleratorEmitterComponent : Component
[DataField("emittedPrototype")]
[ViewVariables(VVAccess.ReadWrite)]
public string EmittedPrototype = "ParticlesProjectile";
[DataField("emitterType")]
[ViewVariables(VVAccess.ReadWrite)]
public ParticleAcceleratorEmitterType Type = ParticleAcceleratorEmitterType.Fore;
public override string ToString()
{
[DataField("emitterType")]
public ParticleAcceleratorEmitterType Type = ParticleAcceleratorEmitterType.Center;
public void Fire(ParticleAcceleratorPowerState strength)
{
var entities = IoCManager.Resolve<IEntityManager>();
var projectile = entities.SpawnEntity("ParticlesProjectile", entities.GetComponent<TransformComponent>(Owner).Coordinates);
if (!entities.TryGetComponent<ParticleProjectileComponent?>(projectile, out var particleProjectileComponent))
{
Logger.Error("ParticleAcceleratorEmitter tried firing particles, but they was spawned without a ParticleProjectileComponent");
return;
}
particleProjectileComponent.Fire(strength, entities.GetComponent<TransformComponent>(Owner).WorldRotation, Owner);
}
public override string ToString()
{
return base.ToString() + $" EmitterType:{Type}";
}
}
public enum ParticleAcceleratorEmitterType
{
Left,
Center,
Right
return base.ToString() + $" EmitterType:{Type}";
}
}
public enum ParticleAcceleratorEmitterType
{
Port,
Fore,
Starboard
}

View File

@@ -1,39 +1,8 @@
namespace Content.Server.ParticleAccelerator.Components
namespace Content.Server.ParticleAccelerator.Components;
[RegisterComponent]
public sealed class ParticleAcceleratorPartComponent : Component
{
[RegisterComponent]
[Virtual]
public class ParticleAcceleratorPartComponent : Component
{
[ViewVariables] public ParticleAcceleratorControlBoxComponent? Master;
protected override void Initialize()
{
base.Initialize();
// FIXME: this has to be an entity system, full stop.
IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).Anchored = true;
}
public void OnAnchorChanged()
{
RescanIfPossible();
}
protected override void OnRemove()
{
base.OnRemove();
RescanIfPossible();
}
private void RescanIfPossible()
{
Master?.RescanParts();
}
public void Moved()
{
RescanIfPossible();
}
}
[ViewVariables]
public EntityUid? Master;
}

View File

@@ -1,16 +1,6 @@
using Content.Server.Power.Components;
namespace Content.Server.ParticleAccelerator.Components;
namespace Content.Server.ParticleAccelerator.Components;
[RegisterComponent]
public sealed class ParticleAcceleratorPowerBoxComponent : Component
{
[ViewVariables] public PowerConsumerComponent? PowerConsumerComponent;
protected override void Initialize()
{
base.Initialize();
PowerConsumerComponent = Owner.EnsureComponentWarn<PowerConsumerComponent>();
}
}

View File

@@ -1,66 +1,9 @@
using Content.Server.Projectiles;
using Content.Server.Singularity.Components;
using Content.Shared.Projectiles;
using Content.Shared.Singularity.Components;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Timing;
namespace Content.Server.ParticleAccelerator.Components
namespace Content.Server.ParticleAccelerator.Components;
[RegisterComponent]
public sealed class ParticleProjectileComponent : Component
{
[RegisterComponent]
public sealed class ParticleProjectileComponent : Component
{
[Dependency] private readonly IEntityManager _entMan = default!;
public ParticleAcceleratorPowerState State;
public void Fire(ParticleAcceleratorPowerState state, Angle angle, EntityUid firer)
{
State = state;
if (!_entMan.TryGetComponent<PhysicsComponent>(Owner, out var physicsComponent))
{
Logger.Error("ParticleProjectile tried firing, but it was spawned without a CollidableComponent");
return;
}
var physics = _entMan.System<SharedPhysicsSystem>();
physics.SetBodyStatus(physicsComponent, BodyStatus.InAir);
if (!_entMan.TryGetComponent<ProjectileComponent>(Owner, out var projectileComponent))
{
Logger.Error("ParticleProjectile tried firing, but it was spawned without a ProjectileComponent");
return;
}
_entMan.EntitySysManager.GetEntitySystem<ProjectileSystem>().SetShooter(projectileComponent, firer);
if (!_entMan.TryGetComponent<SinguloFoodComponent>(Owner, out var singuloFoodComponent))
{
Logger.Error("ParticleProjectile tried firing, but it was spawned without a SinguloFoodComponent");
return;
}
var multiplier = State switch
{
ParticleAcceleratorPowerState.Standby => 0,
ParticleAcceleratorPowerState.Level0 => 1,
ParticleAcceleratorPowerState.Level1 => 3,
ParticleAcceleratorPowerState.Level2 => 6,
ParticleAcceleratorPowerState.Level3 => 10,
_ => 0
};
singuloFoodComponent.Energy = 10 * multiplier;
if (_entMan.TryGetComponent(Owner, out AppearanceComponent? appearance))
{
appearance.SetData(ParticleAcceleratorVisuals.VisualState, state);
}
physics.SetLinearVelocity(Owner, angle.ToWorldVec() * 20f, body: physicsComponent);
_entMan.GetComponent<TransformComponent>(Owner).LocalRotation = angle;
Timer.Spawn(3000, () => _entMan.DeleteEntity(Owner));
}
}
public ParticleAcceleratorPowerState State;
}

View File

@@ -1,5 +1,11 @@
using Content.Server.Mind.Components;
using Content.Server.ParticleAccelerator.Components;
using Content.Server.Power.Components;
using Content.Shared.Database;
using Content.Shared.Singularity.Components;
using Robust.Server.Player;
using Robust.Shared.Utility;
using System.Diagnostics;
namespace Content.Server.ParticleAccelerator.EntitySystems;
@@ -7,11 +13,361 @@ public sealed partial class ParticleAcceleratorSystem
{
private void InitializeControlBoxSystem()
{
SubscribeLocalEvent<ParticleAcceleratorControlBoxComponent, ComponentStartup>(OnComponentStartup);
SubscribeLocalEvent<ParticleAcceleratorControlBoxComponent, ComponentShutdown>(OnComponentShutdown);
SubscribeLocalEvent<ParticleAcceleratorControlBoxComponent, PowerChangedEvent>(OnControlBoxPowerChange);
SubscribeLocalEvent<ParticleAcceleratorControlBoxComponent, ParticleAcceleratorSetEnableMessage>(OnUISetEnableMessage);
SubscribeLocalEvent<ParticleAcceleratorControlBoxComponent, ParticleAcceleratorSetPowerStateMessage>(OnUISetPowerMessage);
SubscribeLocalEvent<ParticleAcceleratorControlBoxComponent, ParticleAcceleratorRescanPartsMessage>(OnUIRescanMessage);
}
private static void OnControlBoxPowerChange(EntityUid uid, ParticleAcceleratorControlBoxComponent component, ref PowerChangedEvent args)
public override void Update(float frameTime)
{
component.OnPowerStateChanged(args);
var curTime = _gameTiming.CurTime;
var query = EntityQueryEnumerator<ParticleAcceleratorControlBoxComponent>();
while (query.MoveNext(out var uid, out var controller))
{
if (controller.Firing && curTime >= controller.NextFire)
Fire(uid, curTime, controller);
}
}
[Conditional("DEBUG")]
private void EverythingIsWellToFire(ParticleAcceleratorControlBoxComponent controller)
{
DebugTools.Assert(controller.Powered);
DebugTools.Assert(controller.SelectedStrength != ParticleAcceleratorPowerState.Standby);
DebugTools.Assert(controller.Assembled);
DebugTools.Assert(EntityManager.EntityExists(controller.PortEmitter));
DebugTools.Assert(EntityManager.EntityExists(controller.ForeEmitter));
DebugTools.Assert(EntityManager.EntityExists(controller.StarboardEmitter));
}
public void Fire(EntityUid uid, TimeSpan curTime, ParticleAcceleratorControlBoxComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return;
comp.LastFire = curTime;
comp.NextFire = curTime + comp.ChargeTime;
EverythingIsWellToFire(comp);
var strength = comp.SelectedStrength;
FireEmitter(comp.PortEmitter!.Value, strength);
FireEmitter(comp.ForeEmitter!.Value, strength);
FireEmitter(comp.StarboardEmitter!.Value, strength);
}
public void SwitchOn(EntityUid uid, IPlayerSession? user = null, ParticleAcceleratorControlBoxComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return;
DebugTools.Assert(comp.Assembled);
if (comp.Enabled || !comp.CanBeEnabled)
return;
if (HasComp<MindComponent>(user?.AttachedEntity))
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{EntityManager.ToPrettyString((EntityUid) user!.AttachedEntity):player} has turned {EntityManager.ToPrettyString(uid)} on");
comp.Enabled = true;
UpdatePowerDraw(uid, comp);
if (!TryComp<PowerConsumerComponent>(comp.PowerBox, out var powerConsumer)
|| powerConsumer.ReceivedPower >= powerConsumer.DrawRate * ParticleAcceleratorControlBoxComponent.RequiredPowerRatio)
PowerOn(uid, comp);
UpdateUI(uid, comp);
}
public void SwitchOff(EntityUid uid, IPlayerSession? user = null, ParticleAcceleratorControlBoxComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return;
if (!comp.Enabled)
return;
if (HasComp<MindComponent>(user?.AttachedEntity))
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{EntityManager.ToPrettyString((EntityUid) user!.AttachedEntity):player} has turned {EntityManager.ToPrettyString(uid)} off");
comp.Enabled = false;
UpdatePowerDraw(uid, comp);
PowerOff(uid, comp);
UpdateUI(uid, comp);
}
public void PowerOn(EntityUid uid, ParticleAcceleratorControlBoxComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return;
DebugTools.Assert(comp.Enabled);
DebugTools.Assert(comp.Assembled);
if (comp.Powered)
return;
comp.Powered = true;
UpdatePowerDraw(uid, comp);
UpdateFiring(uid, comp);
UpdatePartVisualStates(uid, comp);
UpdateUI(uid, comp);
}
public void PowerOff(EntityUid uid, ParticleAcceleratorControlBoxComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return;
if (!comp.Powered)
return;
comp.Powered = false;
UpdatePowerDraw(uid, comp);
UpdateFiring(uid, comp);
UpdatePartVisualStates(uid, comp);
UpdateUI(uid, comp);
}
public void SetStrength(EntityUid uid, ParticleAcceleratorPowerState strength, IPlayerSession? user = null, ParticleAcceleratorControlBoxComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return;
if (comp.StrengthLocked)
return;
strength = (ParticleAcceleratorPowerState) MathHelper.Clamp(
(int) strength,
(int) ParticleAcceleratorPowerState.Standby,
(int) comp.MaxStrength
);
if (strength == comp.SelectedStrength)
return;
if (HasComp<MindComponent>(user?.AttachedEntity))
{
var impact = strength switch
{
ParticleAcceleratorPowerState.Standby => LogImpact.Low,
ParticleAcceleratorPowerState.Level0 => LogImpact.Medium,
ParticleAcceleratorPowerState.Level1 => LogImpact.High,
ParticleAcceleratorPowerState.Level2
or ParticleAcceleratorPowerState.Level3
or _ => LogImpact.Extreme,
};
_adminLogger.Add(LogType.Action, impact, $"{EntityManager.ToPrettyString(user!.AttachedEntity!.Value):player} has set the strength of {EntityManager.ToPrettyString(uid)} to {strength}");
}
comp.SelectedStrength = strength;
UpdateAppearance(uid, comp);
UpdatePartVisualStates(uid, comp);
if (comp.Enabled)
{
UpdatePowerDraw(uid, comp);
UpdateFiring(uid, comp);
}
}
private void UpdateFiring(EntityUid uid, ParticleAcceleratorControlBoxComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return;
if (!comp.Powered || comp.SelectedStrength < ParticleAcceleratorPowerState.Level0)
{
comp.Firing = false;
return;
}
EverythingIsWellToFire(comp);
var curTime = _gameTiming.CurTime;
comp.LastFire = curTime;
comp.NextFire = curTime + comp.ChargeTime;
comp.Firing = true;
}
private void UpdatePowerDraw(EntityUid uid, ParticleAcceleratorControlBoxComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return;
if (!TryComp<PowerConsumerComponent>(comp.PowerBox, out var powerConsumer))
return;
var powerDraw = comp.BasePowerDraw;
if (comp.Enabled)
powerDraw += comp.LevelPowerDraw * (int) comp.SelectedStrength;
powerConsumer.DrawRate = powerDraw;
}
private void UpdateUI(EntityUid uid, ParticleAcceleratorControlBoxComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return;
if (!_uiSystem.TryGetUi(uid, ParticleAcceleratorControlBoxUiKey.Key, out var bui))
return;
var draw = 0f;
var receive = 0f;
if (TryComp<PowerConsumerComponent>(comp.PowerBox, out var powerConsumer))
{
draw = powerConsumer.DrawRate;
receive = powerConsumer.ReceivedPower;
}
_uiSystem.SetUiState(bui, new ParticleAcceleratorUIState(
comp.Assembled,
comp.Enabled,
comp.SelectedStrength,
(int) draw,
(int) receive,
comp.StarboardEmitter != null,
comp.ForeEmitter != null,
comp.PortEmitter != null,
comp.PowerBox != null,
comp.FuelChamber != null,
comp.EndCap != null,
comp.InterfaceDisabled,
comp.MaxStrength,
comp.StrengthLocked
));
}
private void UpdateAppearance(EntityUid uid, ParticleAcceleratorControlBoxComponent? comp = null, AppearanceComponent? appearance = null)
{
if (!Resolve(uid, ref comp))
return;
_appearanceSystem.SetData(
uid,
ParticleAcceleratorVisuals.VisualState,
TryComp<ApcPowerReceiverComponent>(uid, out var apcPower) && !apcPower.Powered
? ParticleAcceleratorVisualState.Unpowered
: (ParticleAcceleratorVisualState) comp.SelectedStrength,
appearance
);
}
private void UpdatePartVisualStates(EntityUid uid, ParticleAcceleratorControlBoxComponent? controller = null)
{
if (!Resolve(uid, ref controller))
return;
var state = controller.Powered ? (ParticleAcceleratorVisualState) controller.SelectedStrength : ParticleAcceleratorVisualState.Unpowered;
// UpdatePartVisualState(ControlBox); (We are the control box)
if (controller.FuelChamber.HasValue)
_appearanceSystem.SetData(controller.FuelChamber!.Value, ParticleAcceleratorVisuals.VisualState, state);
if (controller.PowerBox.HasValue)
_appearanceSystem.SetData(controller.PowerBox!.Value, ParticleAcceleratorVisuals.VisualState, state);
if (controller.PortEmitter.HasValue)
_appearanceSystem.SetData(controller.PortEmitter!.Value, ParticleAcceleratorVisuals.VisualState, state);
if (controller.ForeEmitter.HasValue)
_appearanceSystem.SetData(controller.ForeEmitter!.Value, ParticleAcceleratorVisuals.VisualState, state);
if (controller.StarboardEmitter.HasValue)
_appearanceSystem.SetData(controller.StarboardEmitter!.Value, ParticleAcceleratorVisuals.VisualState, state);
//no endcap because it has no powerlevel-sprites
}
private IEnumerable<EntityUid> AllParts(EntityUid uid, ParticleAcceleratorControlBoxComponent? comp = null)
{
if (Resolve(uid, ref comp))
{
if (comp.FuelChamber.HasValue)
yield return comp.FuelChamber.Value;
if (comp.EndCap.HasValue)
yield return comp.EndCap.Value;
if (comp.PowerBox.HasValue)
yield return comp.PowerBox.Value;
if (comp.PortEmitter.HasValue)
yield return comp.PortEmitter.Value;
if (comp.ForeEmitter.HasValue)
yield return comp.ForeEmitter.Value;
if (comp.StarboardEmitter.HasValue)
yield return comp.StarboardEmitter.Value;
}
}
private void OnComponentStartup(EntityUid uid, ParticleAcceleratorControlBoxComponent comp, ComponentStartup args)
{
if (TryComp<ParticleAcceleratorPartComponent>(uid, out var part))
part.Master = uid;
}
private void OnComponentShutdown(EntityUid uid, ParticleAcceleratorControlBoxComponent comp, ComponentShutdown args)
{
if (TryComp<ParticleAcceleratorPartComponent>(uid, out var partStatus))
partStatus.Master = null;
var partQuery = GetEntityQuery<ParticleAcceleratorPartComponent>();
foreach (var part in AllParts(uid, comp))
{
if (partQuery.TryGetComponent(part, out var partData))
partData.Master = null;
}
}
// This is the power state for the PA control box itself.
// Keep in mind that the PA itself can keep firing as long as the HV cable under the power box has... power.
private void OnControlBoxPowerChange(EntityUid uid, ParticleAcceleratorControlBoxComponent comp, ref PowerChangedEvent args)
{
UpdateAppearance(uid, comp);
if (!args.Powered)
_uiSystem.TryCloseAll(uid, ParticleAcceleratorControlBoxUiKey.Key);
}
private void OnUISetEnableMessage(EntityUid uid, ParticleAcceleratorControlBoxComponent comp, ParticleAcceleratorSetEnableMessage msg)
{
if (!ParticleAcceleratorControlBoxUiKey.Key.Equals(msg.UiKey))
return;
if (comp.InterfaceDisabled)
return;
if (TryComp<ApcPowerReceiverComponent>(uid, out var apcPower) && !apcPower.Powered)
return;
if (msg.Enabled)
{
if (comp.Assembled)
SwitchOn(uid, (IPlayerSession?) msg.Session, comp);
}
else
SwitchOff(uid, (IPlayerSession?) msg.Session, comp);
UpdateUI(uid, comp);
}
private void OnUISetPowerMessage(EntityUid uid, ParticleAcceleratorControlBoxComponent comp, ParticleAcceleratorSetPowerStateMessage msg)
{
if (!ParticleAcceleratorControlBoxUiKey.Key.Equals(msg.UiKey))
return;
if (comp.InterfaceDisabled)
return;
if (TryComp<ApcPowerReceiverComponent>(uid, out var apcPower) && !apcPower.Powered)
return;
SetStrength(uid, msg.State, (IPlayerSession?) msg.Session, comp);
UpdateUI(uid, comp);
}
private void OnUIRescanMessage(EntityUid uid, ParticleAcceleratorControlBoxComponent comp, ParticleAcceleratorRescanPartsMessage msg)
{
if (!ParticleAcceleratorControlBoxUiKey.Key.Equals(msg.UiKey))
return;
if (comp.InterfaceDisabled)
return;
if (TryComp<ApcPowerReceiverComponent>(uid, out var apcPower) && !apcPower.Powered)
return;
RescanParts(uid, (IPlayerSession?) msg.Session, comp);
UpdateUI(uid, comp);
}
}

View File

@@ -0,0 +1,62 @@
using Content.Server.ParticleAccelerator.Components;
using Content.Server.Singularity.Components;
using Content.Shared.Projectiles;
using Content.Shared.Singularity.Components;
using Robust.Shared.Physics.Components;
namespace Content.Server.ParticleAccelerator.EntitySystems;
public sealed partial class ParticleAcceleratorSystem
{
private void FireEmitter(EntityUid uid, ParticleAcceleratorPowerState strength, ParticleAcceleratorEmitterComponent? emitter = null)
{
if (!Resolve(uid, ref emitter))
return;
var xformQuery = GetEntityQuery<TransformComponent>();
if (!xformQuery.TryGetComponent(uid, out var xform))
{
Logger.Error("ParticleAccelerator attempted to emit a particle without (having) a transform from which to base its initial position and orientation.");
return;
}
var emitted = Spawn(emitter.EmittedPrototype, xform.Coordinates);
if (xformQuery.TryGetComponent(emitted, out var particleXform))
_transformSystem.SetLocalRotation(emitted, xform.LocalRotation, particleXform);
if (TryComp<PhysicsComponent>(emitted, out var particlePhys))
{
var angle = _transformSystem.GetWorldRotation(uid, xformQuery);
_physicsSystem.SetBodyStatus(particlePhys, BodyStatus.InAir);
var velocity = angle.ToWorldVec() * 20f;
if (TryComp<PhysicsComponent>(uid, out var phys))
velocity += phys.LinearVelocity; // Inherit velocity from parent so if the clown has strapped a dozen engines to departures we don't outpace the particles.
_physicsSystem.SetLinearVelocity(emitted, velocity, body: particlePhys);
}
if (TryComp<ProjectileComponent>(emitted, out var proj))
_projectileSystem.SetShooter(proj, uid);
if (TryComp<SinguloFoodComponent>(emitted, out var food))
{
// TODO: Unhardcode this.
food.Energy = strength switch
{
ParticleAcceleratorPowerState.Standby => 0,
ParticleAcceleratorPowerState.Level0 => 1,
ParticleAcceleratorPowerState.Level1 => 3,
ParticleAcceleratorPowerState.Level2 => 6,
ParticleAcceleratorPowerState.Level3 => 10,
_ => 0,
} * 10;
}
if (TryComp<ParticleProjectileComponent>(emitted, out var particle))
particle.State = strength;
_appearanceSystem.SetData(emitted, ParticleAcceleratorVisuals.VisualState, strength);
}
}

View File

@@ -1,30 +1,162 @@
using Content.Server.ParticleAccelerator.Components;
using JetBrains.Annotations;
using Robust.Shared.Physics.Components;
using Robust.Server.Player;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Events;
using System.Diagnostics.CodeAnalysis;
namespace Content.Server.ParticleAccelerator.EntitySystems
namespace Content.Server.ParticleAccelerator.EntitySystems;
[UsedImplicitly]
public sealed partial class ParticleAcceleratorSystem
{
[UsedImplicitly]
public sealed partial class ParticleAcceleratorSystem
private void InitializePartSystem()
{
private void InitializePartSystem()
SubscribeLocalEvent<ParticleAcceleratorPartComponent, ComponentShutdown>(OnComponentShutdown);
SubscribeLocalEvent<ParticleAcceleratorPartComponent, MoveEvent>(OnMoveEvent);
SubscribeLocalEvent<ParticleAcceleratorPartComponent, PhysicsBodyTypeChangedEvent>(BodyTypeChanged);
}
public void RescanParts(EntityUid uid, IPlayerSession? user = null, ParticleAcceleratorControlBoxComponent? controller = null)
{
if (!Resolve(uid, ref controller))
return;
SwitchOff(uid, user, controller);
var partQuery = GetEntityQuery<ParticleAcceleratorPartComponent>();
foreach (var part in AllParts(uid, controller))
{
SubscribeLocalEvent<ParticleAcceleratorPartComponent, MoveEvent>(OnMoveEvent);
SubscribeLocalEvent<ParticleAcceleratorPartComponent, PhysicsBodyTypeChangedEvent>(BodyTypeChanged);
if (partQuery.TryGetComponent(part, out var partState))
partState.Master = null;
}
private static void BodyTypeChanged(
EntityUid uid,
ParticleAcceleratorPartComponent component,
ref PhysicsBodyTypeChangedEvent args)
controller.Assembled = false;
controller.FuelChamber = null;
controller.EndCap = null;
controller.PowerBox = null;
controller.PortEmitter = null;
controller.ForeEmitter = null;
controller.StarboardEmitter = null;
var xformQuery = GetEntityQuery<TransformComponent>();
if (!xformQuery.TryGetComponent(uid, out var xform) || !xform.Anchored)
return;
var gridUid = xform.GridUid;
if (gridUid == null || gridUid != xform.ParentUid || !_mapManager.TryGetGrid(gridUid, out var grid))
return;
// Find fuel chamber first by scanning cardinals.
var fuelQuery = GetEntityQuery<ParticleAcceleratorFuelChamberComponent>();
foreach (var adjacent in grid.GetCardinalNeighborCells(xform.Coordinates))
{
component.OnAnchorChanged();
if (fuelQuery.HasComponent(adjacent)
&& partQuery.TryGetComponent(adjacent, out var partState)
&& partState.Master == null)
{
controller.FuelChamber = adjacent;
break;
}
}
private static void OnMoveEvent(EntityUid uid, ParticleAcceleratorPartComponent component, ref MoveEvent args)
if (controller.FuelChamber == null)
{
component.Moved();
UpdateUI(uid, controller);
return;
}
// Align ourselves to match fuel chamber orientation.
// This means that if you mess up the orientation of the control box it's not a big deal,
// because the sprite is far from obvious about the orientation.
var fuelXform = xformQuery.GetComponent(controller.FuelChamber!.Value);
var rotation = fuelXform.LocalRotation;
_transformSystem.SetLocalRotation(uid, rotation, xform);
// Calculate offsets for each of the parts of the PA.
// These are all done relative to the fuel chamber BC that is basically the center of the machine.
var positionFuelChamber = grid.TileIndicesFor(fuelXform.Coordinates); // //
var positionEndCap = positionFuelChamber + (Vector2i) rotation.RotateVec((0, 1)); // n // n: End Cap
var positionPowerBox = positionFuelChamber + (Vector2i) rotation.RotateVec((0, -1)); // CF // C: Control Box, F: Fuel Chamber
var positionPortEmitter = positionFuelChamber + (Vector2i) rotation.RotateVec((1, -2)); // P // P: Power Box
var positionForeEmitter = positionFuelChamber + (Vector2i) rotation.RotateVec((0, -2)); // EEE // E: Emitter (Starboard, Fore, Port)
var positionStarboardEmitter = positionFuelChamber + (Vector2i) rotation.RotateVec((-1, -2)); // //
ScanPart<ParticleAcceleratorEndCapComponent>(gridUid!.Value, positionEndCap, rotation, out controller.EndCap, out var _, grid);
ScanPart<ParticleAcceleratorPowerBoxComponent>(gridUid!.Value, positionPowerBox, rotation, out controller.PowerBox, out var _, grid);
if (!ScanPart<ParticleAcceleratorEmitterComponent>(gridUid!.Value, positionPortEmitter, rotation, out controller.PortEmitter, out var portEmitter, grid)
|| portEmitter!.Type != ParticleAcceleratorEmitterType.Port)
controller.PortEmitter = null;
if (!ScanPart<ParticleAcceleratorEmitterComponent>(gridUid!.Value, positionForeEmitter, rotation, out controller.ForeEmitter, out var foreEmitter, grid)
|| foreEmitter!.Type != ParticleAcceleratorEmitterType.Fore)
controller.ForeEmitter = null;
if (!ScanPart<ParticleAcceleratorEmitterComponent>(gridUid!.Value, positionStarboardEmitter, rotation, out controller.StarboardEmitter, out var starboardEmitter, grid)
|| starboardEmitter!.Type != ParticleAcceleratorEmitterType.Starboard)
controller.StarboardEmitter = null;
controller.Assembled =
controller.FuelChamber.HasValue
&& controller.EndCap.HasValue
&& controller.PowerBox.HasValue
&& controller.PortEmitter.HasValue
&& controller.ForeEmitter.HasValue
&& controller.StarboardEmitter.HasValue;
foreach (var part in AllParts(uid, controller))
{
if (partQuery.TryGetComponent(part, out var partState))
partState.Master = uid;
}
UpdatePowerDraw(uid, controller);
UpdateUI(uid, controller);
}
private bool ScanPart<T>(EntityUid uid, Vector2i coordinates, Angle? rotation, [NotNullWhen(true)] out EntityUid? part, [NotNullWhen(true)] out T? comp, MapGridComponent? grid = null)
where T : Component
{
if (!Resolve(uid, ref grid))
{
part = null;
comp = null;
return false;
}
var compQuery = GetEntityQuery<T>();
foreach (var entity in grid.GetAnchoredEntities(coordinates))
{
if (compQuery.TryGetComponent(entity, out comp)
&& TryComp<ParticleAcceleratorPartComponent>(entity, out var partState) && partState.Master == null
&& (rotation == null || MathHelper.CloseTo(Transform(entity).LocalRotation.Theta, rotation!.Value.Theta)))
{
part = entity;
return true;
}
}
part = null;
comp = null;
return false;
}
private void OnComponentShutdown(EntityUid uid, ParticleAcceleratorPartComponent comp, ComponentShutdown args)
{
if (EntityManager.EntityExists(comp.Master))
RescanParts(comp.Master!.Value);
}
private void BodyTypeChanged(EntityUid uid, ParticleAcceleratorPartComponent comp, ref PhysicsBodyTypeChangedEvent args)
{
if (EntityManager.EntityExists(comp.Master))
RescanParts(comp.Master!.Value);
}
private void OnMoveEvent(EntityUid uid, ParticleAcceleratorPartComponent comp, ref MoveEvent args)
{
if (EntityManager.EntityExists(comp.Master))
RescanParts(comp.Master!.Value);
}
}

View File

@@ -1,22 +1,28 @@
using Content.Server.ParticleAccelerator.Components;
using Content.Server.Power.EntitySystems;
namespace Content.Server.ParticleAccelerator.EntitySystems
{
public sealed partial class ParticleAcceleratorSystem
{
private void InitializePowerBoxSystem()
{
SubscribeLocalEvent<ParticleAcceleratorPowerBoxComponent, PowerConsumerReceivedChanged>(PowerBoxReceivedChanged);
}
namespace Content.Server.ParticleAccelerator.EntitySystems;
private void PowerBoxReceivedChanged(
EntityUid uid,
ParticleAcceleratorPowerBoxComponent component,
ref PowerConsumerReceivedChanged args)
{
if (TryComp(uid, out ParticleAcceleratorPartComponent? paPart))
paPart.Master?.PowerBoxReceivedChanged(args);
}
public sealed partial class ParticleAcceleratorSystem
{
private void InitializePowerBoxSystem()
{
SubscribeLocalEvent<ParticleAcceleratorPowerBoxComponent, PowerConsumerReceivedChanged>(PowerBoxReceivedChanged);
}
private void PowerBoxReceivedChanged(EntityUid uid, ParticleAcceleratorPowerBoxComponent component, ref PowerConsumerReceivedChanged args)
{
if (!TryComp<ParticleAcceleratorPartComponent>(uid, out var part))
return;
if (!TryComp<ParticleAcceleratorControlBoxComponent>(part.Master, out var controller))
return;
var master = part.Master!.Value;
if (controller.Enabled && args.ReceivedPower >= args.DrawRate * ParticleAcceleratorControlBoxComponent.RequiredPowerRatio)
PowerOn(master, comp: controller);
else
PowerOff(master, comp: controller);
UpdateUI(master, controller);
}
}

View File

@@ -1,7 +1,23 @@
using Content.Server.Administration.Logs;
using Content.Server.Projectiles;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Map;
using Robust.Shared.Timing;
using Robust.Server.GameObjects;
namespace Content.Server.ParticleAccelerator.EntitySystems;
public sealed partial class ParticleAcceleratorSystem : EntitySystem
{
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly ProjectileSystem _projectileSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly SharedPhysicsSystem _physicsSystem = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
public override void Initialize()
{
base.Initialize();

View File

@@ -0,0 +1,35 @@
using Content.Server.ParticleAccelerator.Components;
using Content.Server.Wires;
using Content.Shared.Singularity.Components;
using Content.Shared.Wires;
namespace Content.Server.ParticleAccelerator.Wires;
public sealed class ParticleAcceleratorKeyboardWireAction : ComponentWireAction<ParticleAcceleratorControlBoxComponent>
{
public override string Name { get; set; } = "wire-name-pa-keyboard";
public override Color Color { get; set; } = Color.LimeGreen;
public override object StatusKey { get; } = ParticleAcceleratorWireStatus.Keyboard;
public override StatusLightState? GetLightState(Wire wire, ParticleAcceleratorControlBoxComponent component)
{
return component.InterfaceDisabled ? StatusLightState.BlinkingFast : StatusLightState.On;
}
public override bool Cut(EntityUid user, Wire wire, ParticleAcceleratorControlBoxComponent controller)
{
controller.InterfaceDisabled = true;
return true;
}
public override bool Mend(EntityUid user, Wire wire, ParticleAcceleratorControlBoxComponent controller)
{
controller.InterfaceDisabled = false;
return true;
}
public override void Pulse(EntityUid user, Wire wire, ParticleAcceleratorControlBoxComponent controller)
{
controller.InterfaceDisabled = !controller.InterfaceDisabled;
}
}

View File

@@ -0,0 +1,69 @@
using Content.Server.ParticleAccelerator.Components;
using Content.Server.ParticleAccelerator.EntitySystems;
using Content.Server.Popups;
using Content.Server.Wires;
using Content.Shared.Popups;
using Content.Shared.Singularity.Components;
using Content.Shared.Wires;
using Robust.Server.GameObjects;
namespace Content.Server.ParticleAccelerator.Wires;
public sealed class ParticleAcceleratorLimiterWireAction : ComponentWireAction<ParticleAcceleratorControlBoxComponent>
{
public override string Name { get; set; } = "wire-name-pa-limiter";
public override Color Color { get; set; } = Color.Teal;
public override object StatusKey { get; } = ParticleAcceleratorWireStatus.Limiter;
public override StatusLightData? GetStatusLightData(Wire wire)
{
var result = base.GetStatusLightData(wire);
if (result.HasValue
&& EntityManager.TryGetComponent<ParticleAcceleratorControlBoxComponent>(wire.Owner, out var controller)
&& controller.MaxStrength >= ParticleAcceleratorPowerState.Level3)
result = new(Color.Purple, result.Value.State, result.Value.Text);
return result;
}
public override StatusLightState? GetLightState(Wire wire, ParticleAcceleratorControlBoxComponent component)
{
return StatusLightState.On;
}
public override bool Cut(EntityUid user, Wire wire, ParticleAcceleratorControlBoxComponent controller)
{
controller.MaxStrength = ParticleAcceleratorPowerState.Level3;
return true;
}
public override bool Mend(EntityUid user, Wire wire, ParticleAcceleratorControlBoxComponent controller)
{
controller.MaxStrength = ParticleAcceleratorPowerState.Level2;
if (controller.SelectedStrength <= controller.MaxStrength || controller.StrengthLocked)
return true;
// Yes, it's a feature that mending this wire WON'T WORK if the strength wire is also cut.
// Since that blocks SetStrength().
var paSystem = EntityManager.System<ParticleAcceleratorSystem>();
var userSession = EntityManager.TryGetComponent<ActorComponent>(user, out var actor) ? actor.PlayerSession : null;
paSystem.SetStrength(wire.Owner, controller.MaxStrength, userSession, controller);
return true;
}
public override void Pulse(EntityUid user, Wire wire, ParticleAcceleratorControlBoxComponent controller)
{
EntityManager.System<PopupSystem>().PopupEntity(
Loc.GetString("particle-accelerator-control-box-component-wires-update-limiter-on-pulse"),
user,
PopupType.SmallCaution
);
}
public override void Update(Wire wire)
{
}
}

View File

@@ -0,0 +1,40 @@
using Content.Server.ParticleAccelerator.Components;
using Content.Server.ParticleAccelerator.EntitySystems;
using Content.Server.Wires;
using Content.Shared.Singularity.Components;
using Content.Shared.Wires;
using Robust.Server.GameObjects;
using Robust.Shared.Random;
namespace Content.Server.ParticleAccelerator.Wires;
public sealed class ParticleAcceleratorStrengthWireAction : ComponentWireAction<ParticleAcceleratorControlBoxComponent>
{
public override string Name { get; set; } = "wire-name-pa-strength";
public override Color Color { get; set; } = Color.Blue;
public override object StatusKey { get; } = ParticleAcceleratorWireStatus.Strength;
public override StatusLightState? GetLightState(Wire wire, ParticleAcceleratorControlBoxComponent component)
{
return component.StrengthLocked ? StatusLightState.BlinkingSlow : StatusLightState.On;
}
public override bool Cut(EntityUid user, Wire wire, ParticleAcceleratorControlBoxComponent controller)
{
controller.StrengthLocked = true;
return true;
}
public override bool Mend(EntityUid user, Wire wire, ParticleAcceleratorControlBoxComponent controller)
{
controller.StrengthLocked = false;
return true;
}
public override void Pulse(EntityUid user, Wire wire, ParticleAcceleratorControlBoxComponent controller)
{
var paSystem = EntityManager.System<ParticleAcceleratorSystem>();
var userSession = EntityManager.TryGetComponent<ActorComponent>(user, out var actor) ? actor.PlayerSession : null;
paSystem.SetStrength(wire.Owner, (ParticleAcceleratorPowerState) ((int) controller.SelectedStrength + 1), userSession, controller);
}
}

View File

@@ -0,0 +1,49 @@
using Content.Server.ParticleAccelerator.Components;
using Content.Server.ParticleAccelerator.EntitySystems;
using Content.Server.Wires;
using Content.Shared.Singularity.Components;
using Content.Shared.Wires;
using Robust.Server.GameObjects;
namespace Content.Server.ParticleAccelerator.Wires;
public sealed class ParticleAcceleratorPowerWireAction : ComponentWireAction<ParticleAcceleratorControlBoxComponent>
{
public override string Name { get; set; } = "wire-name-pa-power";
public override Color Color { get; set; } = Color.Yellow;
public override object StatusKey { get; } = ParticleAcceleratorWireStatus.Power;
public override StatusLightState? GetLightState(Wire wire, ParticleAcceleratorControlBoxComponent component)
{
if (!component.CanBeEnabled)
return StatusLightState.Off;
return component.Enabled ? StatusLightState.On : StatusLightState.BlinkingSlow;
}
public override bool Cut(EntityUid user, Wire wire, ParticleAcceleratorControlBoxComponent controller)
{
var paSystem = EntityManager.System<ParticleAcceleratorSystem>();
var userSession = EntityManager.TryGetComponent<ActorComponent>(user, out var actor) ? actor.PlayerSession : null;
controller.CanBeEnabled = false;
paSystem.SwitchOff(wire.Owner, userSession, controller);
return true;
}
public override bool Mend(EntityUid user, Wire wire, ParticleAcceleratorControlBoxComponent controller)
{
controller.CanBeEnabled = true;
return true;
}
public override void Pulse(EntityUid user, Wire wire, ParticleAcceleratorControlBoxComponent controller)
{
var paSystem = EntityManager.System<ParticleAcceleratorSystem>();
var userSession = EntityManager.TryGetComponent<ActorComponent>(user, out var actor) ? actor.PlayerSession : null;
if (controller.Enabled)
paSystem.SwitchOff(wire.Owner, userSession, controller);
else if (controller.Assembled)
paSystem.SwitchOn(wire.Owner, userSession, controller);
}
}