This commit is contained in:
TemporalOroboros
2023-06-28 05:02:06 -07:00
committed by GitHub
parent 638026878e
commit d9de405859
35 changed files with 965 additions and 1005 deletions

View File

@@ -1,63 +0,0 @@
using Content.Client.AME.Components;
using Robust.Client.GameObjects;
using static Content.Shared.AME.SharedAMEControllerComponent;
namespace Content.Client.AME;
public sealed class AMEControllerVisualizerSystem : VisualizerSystem<AMEControllerVisualsComponent>
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AMEControllerVisualsComponent, ComponentInit>(OnComponentInit);
}
private void OnComponentInit(EntityUid uid, AMEControllerVisualsComponent component, ComponentInit args)
{
if (TryComp<SpriteComponent>(uid, out var sprite))
{
sprite.LayerMapSet(AMEControllerVisualLayers.Display, sprite.AddLayerState("control_on"));
sprite.LayerSetVisible(AMEControllerVisualLayers.Display, false);
}
}
protected override void OnAppearanceChange(EntityUid uid, AMEControllerVisualsComponent component, ref AppearanceChangeEvent args)
{
base.OnAppearanceChange(uid, component, ref args);
if (args.Sprite == null
|| !AppearanceSystem.TryGetData<string>(uid, AMEControllerVisuals.DisplayState, out var state, args.Component))
{
return;
}
switch (state)
{
case "on":
args.Sprite.LayerSetState(AMEControllerVisualLayers.Display, "control_on");
args.Sprite.LayerSetVisible(AMEControllerVisualLayers.Display, true);
break;
case "critical":
args.Sprite.LayerSetState(AMEControllerVisualLayers.Display, "control_critical");
args.Sprite.LayerSetVisible(AMEControllerVisualLayers.Display, true);
break;
case "fuck":
args.Sprite.LayerSetState(AMEControllerVisualLayers.Display, "control_fuck");
args.Sprite.LayerSetVisible(AMEControllerVisualLayers.Display, true);
break;
case "off":
args.Sprite.LayerSetVisible(AMEControllerVisualLayers.Display, false);
break;
default:
args.Sprite.LayerSetVisible(AMEControllerVisualLayers.Display, false);
break;
}
}
}
public enum AMEControllerVisualLayers : byte
{
Display
}

View File

@@ -1,70 +0,0 @@
using Content.Client.AME.Components;
using Robust.Client.GameObjects;
using static Content.Shared.AME.SharedAMEShieldComponent;
namespace Content.Client.AME;
public sealed class AMEShieldingVisualizerSystem : VisualizerSystem<AMEShieldingVisualsComponent>
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AMEShieldingVisualsComponent, ComponentInit>(OnComponentInit);
}
private void OnComponentInit(EntityUid uid, AMEShieldingVisualsComponent component, ComponentInit args)
{
if (TryComp<SpriteComponent>(uid, out var sprite))
{
sprite.LayerMapSet(AMEShieldingVisualsLayer.Core, sprite.AddLayerState("core"));
sprite.LayerSetVisible(AMEShieldingVisualsLayer.Core, false);
sprite.LayerMapSet(AMEShieldingVisualsLayer.CoreState, sprite.AddLayerState("core_weak"));
sprite.LayerSetVisible(AMEShieldingVisualsLayer.CoreState, false);
}
}
protected override void OnAppearanceChange(EntityUid uid, AMEShieldingVisualsComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
if (AppearanceSystem.TryGetData<string>(uid, AMEShieldVisuals.Core, out var core, args.Component))
{
if (core == "isCore")
{
args.Sprite.LayerSetState(AMEShieldingVisualsLayer.Core, "core");
args.Sprite.LayerSetVisible(AMEShieldingVisualsLayer.Core, true);
}
else
{
args.Sprite.LayerSetVisible(AMEShieldingVisualsLayer.Core, false);
}
}
if (AppearanceSystem.TryGetData<string>(uid, AMEShieldVisuals.CoreState, out var coreState, args.Component))
{
switch (coreState)
{
case "weak":
args.Sprite.LayerSetState(AMEShieldingVisualsLayer.CoreState, "core_weak");
args.Sprite.LayerSetVisible(AMEShieldingVisualsLayer.CoreState, true);
break;
case "strong":
args.Sprite.LayerSetState(AMEShieldingVisualsLayer.CoreState, "core_strong");
args.Sprite.LayerSetVisible(AMEShieldingVisualsLayer.CoreState, true);
break;
case "off":
args.Sprite.LayerSetVisible(AMEShieldingVisualsLayer.CoreState, false);
break;
}
}
}
}
public enum AMEShieldingVisualsLayer : byte
{
Core,
CoreState,
}

View File

@@ -1,9 +0,0 @@
using System.ComponentModel.DataAnnotations;
using Robust.Shared.GameObjects;
namespace Content.Client.AME.Components;
[RegisterComponent]
public sealed class AMEControllerVisualsComponent : Component
{
}

View File

@@ -1,9 +0,0 @@
using System.ComponentModel.DataAnnotations;
using Robust.Shared.GameObjects;
namespace Content.Client.AME.Components;
[RegisterComponent]
public sealed class AMEShieldingVisualsComponent : Component
{
}

View File

@@ -1,17 +1,15 @@
using Content.Shared.Chemistry.Dispenser; using Content.Shared.Ame;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using static Content.Shared.AME.SharedAMEControllerComponent;
namespace Content.Client.AME.UI namespace Content.Client.Ame.UI
{ {
[UsedImplicitly] [UsedImplicitly]
public sealed class AMEControllerBoundUserInterface : BoundUserInterface public sealed class AmeControllerBoundUserInterface : BoundUserInterface
{ {
private AMEWindow? _window; private AmeWindow? _window;
public AMEControllerBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) public AmeControllerBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{ {
} }
@@ -19,7 +17,7 @@ namespace Content.Client.AME.UI
{ {
base.Open(); base.Open();
_window = new AMEWindow(this); _window = new AmeWindow(this);
_window.OnClose += Close; _window.OnClose += Close;
_window.OpenCentered(); _window.OpenCentered();
} }
@@ -35,11 +33,11 @@ namespace Content.Client.AME.UI
{ {
base.UpdateState(state); base.UpdateState(state);
var castState = (AMEControllerBoundUserInterfaceState) state; var castState = (AmeControllerBoundUserInterfaceState) state;
_window?.UpdateState(castState); //Update window state _window?.UpdateState(castState); //Update window state
} }
public void ButtonPressed(UiButton button, int dispenseIndex = -1) public void ButtonPressed(UiButton button)
{ {
SendMessage(new UiButtonPressedMessage(button)); SendMessage(new UiButtonPressedMessage(button));
} }

View File

@@ -1,20 +1,15 @@
using Content.Client.UserInterface; using Content.Client.UserInterface;
using Content.Shared.Ame;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using static Content.Shared.AME.SharedAMEControllerComponent;
namespace Content.Client.AME.UI namespace Content.Client.Ame.UI
{ {
[GenerateTypedNameReferences] [GenerateTypedNameReferences]
public sealed partial class AMEWindow : DefaultWindow public sealed partial class AmeWindow : DefaultWindow
{ {
public AMEWindow(AMEControllerBoundUserInterface ui) public AmeWindow(AmeControllerBoundUserInterface ui)
{ {
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this); IoCManager.InjectDependencies(this);
@@ -31,7 +26,7 @@ namespace Content.Client.AME.UI
/// <param name="state">State data sent by the server.</param> /// <param name="state">State data sent by the server.</param>
public void UpdateState(BoundUserInterfaceState state) public void UpdateState(BoundUserInterfaceState state)
{ {
var castState = (AMEControllerBoundUserInterfaceState) state; var castState = (AmeControllerBoundUserInterfaceState) state;
// Disable all buttons if not powered // Disable all buttons if not powered
if (Contents.Children != null) if (Contents.Children != null)

View File

@@ -21,7 +21,7 @@ using Content.Client.Stylesheets;
using Content.Client.Viewport; using Content.Client.Viewport;
using Content.Client.Voting; using Content.Client.Voting;
using Content.Shared.Administration; using Content.Shared.Administration;
using Content.Shared.AME; using Content.Shared.Ame;
using Content.Shared.Gravity; using Content.Shared.Gravity;
using Content.Shared.Localizations; using Content.Shared.Localizations;
using Robust.Client; using Robust.Client;
@@ -91,7 +91,7 @@ namespace Content.Client.Entry
// Do not add to these, they are legacy. // Do not add to these, they are legacy.
_componentFactory.RegisterClass<SharedGravityGeneratorComponent>(); _componentFactory.RegisterClass<SharedGravityGeneratorComponent>();
_componentFactory.RegisterClass<SharedAMEControllerComponent>(); _componentFactory.RegisterClass<SharedAmeControllerComponent>();
// Do not add to the above, they are legacy // Do not add to the above, they are legacy
_prototypeManager.RegisterIgnore("utilityQuery"); _prototypeManager.RegisterIgnore("utilityQuery");

View File

@@ -1,198 +0,0 @@
using System.Linq;
using Content.Server.Administration.Systems;
using Content.Server.AME.Components;
using Content.Server.Chat.Managers;
using Content.Server.Explosion.EntitySystems;
using Content.Server.NodeContainer.NodeGroups;
using Content.Server.NodeContainer.Nodes;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Random;
namespace Content.Server.AME
{
/// <summary>
/// Node group class for handling the Antimatter Engine's console and parts.
/// </summary>
[NodeGroup(NodeGroupID.AMEngine)]
public sealed class AMENodeGroup : BaseNodeGroup
{
/// <summary>
/// The AME controller which is currently in control of this node group.
/// This could be tracked a few different ways, but this is most convenient,
/// since any part connected to the node group can easily find the master.
/// </summary>
[ViewVariables]
private AMEControllerComponent? _masterController;
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
public AMEControllerComponent? MasterController => _masterController;
private readonly List<AMEShieldComponent> _cores = new();
public int CoreCount => _cores.Count;
public override void LoadNodes(List<Node> groupNodes)
{
base.LoadNodes(groupNodes);
MapGridComponent? grid = null;
foreach (var node in groupNodes)
{
var nodeOwner = node.Owner;
if (_entMan.TryGetComponent(nodeOwner, out AMEShieldComponent? shield))
{
var xform = _entMan.GetComponent<TransformComponent>(nodeOwner);
if (xform.GridUid != grid?.Owner && !_mapManager.TryGetGrid(xform.GridUid, out grid))
continue;
if (grid == null)
continue;
var nodeNeighbors = grid.GetCellsInSquareArea(xform.Coordinates, 1)
.Where(entity => entity != nodeOwner && _entMan.HasComponent<AMEShieldComponent>(entity));
if (nodeNeighbors.Count() >= 8)
{
_cores.Add(shield);
shield.SetCore();
// Core visuals will be updated later.
}
else
{
shield.UnsetCore();
}
}
}
// Separate to ensure core count is correctly updated.
foreach (var node in groupNodes)
{
var nodeOwner = node.Owner;
if (_entMan.TryGetComponent(nodeOwner, out AMEControllerComponent? controller))
{
if (_masterController == null)
{
// Has to be the first one, as otherwise IsMasterController will return true on them all for this first update.
_masterController = controller;
}
controller.OnAMENodeGroupUpdate();
}
}
UpdateCoreVisuals();
}
public void UpdateCoreVisuals()
{
var injectionAmount = 0;
var injecting = false;
if (_masterController != null)
{
injectionAmount = _masterController.InjectionAmount;
injecting = _masterController.Injecting;
}
var injectionStrength = CoreCount > 0 ? injectionAmount / CoreCount : 0;
foreach (AMEShieldComponent core in _cores)
{
core.UpdateCoreVisuals(injectionStrength, injecting);
}
}
public float InjectFuel(int fuel, out bool overloading)
{
overloading = false;
if(fuel > 0 && CoreCount > 0)
{
var safeFuelLimit = CoreCount * 2;
if (fuel > safeFuelLimit)
{
// The AME is being overloaded.
// Note about these maths: I would assume the general idea here is to make larger engines less safe to overload.
// In other words, yes, those are supposed to be CoreCount, not safeFuelLimit.
var instability = 0;
var overloadVsSizeResult = fuel - CoreCount;
// fuel > safeFuelLimit: Slow damage. Can safely run at this level for burst periods if the engine is small and someone is keeping an eye on it.
if (_random.Prob(0.5f))
instability = 1;
// overloadVsSizeResult > 5:
if (overloadVsSizeResult > 5)
instability = 5;
// overloadVsSizeResult > 10: This will explode in at most 5 injections.
if (overloadVsSizeResult > 10)
instability = 20;
// Apply calculated instability
if (instability != 0)
{
overloading = true;
var integrityCheck = 100;
foreach(AMEShieldComponent core in _cores)
{
var oldIntegrity = core.CoreIntegrity;
core.CoreIntegrity -= instability;
if (oldIntegrity > 95
&& core.CoreIntegrity <= 95
&& core.CoreIntegrity < integrityCheck)
integrityCheck = core.CoreIntegrity;
}
// Admin alert
if (integrityCheck != 100 && _masterController != null)
_chat.SendAdminAlert($"AME overloading: {_entMan.ToPrettyString(_masterController.Owner)}");
}
}
// Note the float conversions. The maths will completely fail if not done using floats.
// Oh, and don't ever stuff the result of this in an int. Seriously.
return (((float) fuel) / CoreCount) * fuel * 20000;
}
return 0;
}
public int GetTotalStability()
{
if(CoreCount < 1) { return 100; }
var stability = 0;
foreach(AMEShieldComponent core in _cores)
{
stability += core.CoreIntegrity;
}
stability = stability / CoreCount;
return stability;
}
public void ExplodeCores()
{
if(_cores.Count < 1 || MasterController == null) { return; }
float radius = 0;
/*
* todo: add an exact to the shielding and make this find the core closest to the controller
* so they chain explode, after helpers have been added to make it not cancer
*/
foreach (AMEShieldComponent core in _cores)
{
radius += MasterController.InjectionAmount;
}
radius *= 2;
radius = Math.Min(radius, 8);
EntitySystem.Get<ExplosionSystem>().TriggerExplosive(MasterController.Owner, radius: radius, delete: false);
}
}
}

View File

@@ -1,120 +0,0 @@
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.AME.Components;
using Content.Server.Popups;
using Content.Server.Power.Components;
using Content.Server.Tools;
using Content.Shared.Database;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using JetBrains.Annotations;
using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Player;
namespace Content.Server.AME
{
[UsedImplicitly]
public sealed partial class AMESystem : EntitySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly ToolSystem _toolSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
private float _accumulatedFrameTime;
private const float UpdateCooldown = 10f;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AMEControllerComponent, PowerChangedEvent>(OnAMEPowerChange);
SubscribeLocalEvent<AMEControllerComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<AMEPartComponent, InteractUsingEvent>(OnPartInteractUsing);
InitializeFuel();
}
public override void Update(float frameTime)
{
base.Update(frameTime);
// TODO: Won't exactly work with replays I guess?
_accumulatedFrameTime += frameTime;
if (_accumulatedFrameTime >= UpdateCooldown)
{
foreach (var comp in EntityManager.EntityQuery<AMEControllerComponent>())
{
comp.OnUpdate(frameTime);
}
_accumulatedFrameTime -= UpdateCooldown;
}
}
private static void OnAMEPowerChange(EntityUid uid, AMEControllerComponent component, ref PowerChangedEvent args)
{
component.UpdateUserInterface();
}
private void OnInteractUsing(EntityUid uid, AMEControllerComponent component, InteractUsingEvent args)
{
if (!TryComp(args.User, out HandsComponent? hands))
{
_popupSystem.PopupEntity(Loc.GetString("ame-controller-component-interact-using-no-hands-text"), uid, args.User);
return;
}
if (HasComp<AMEFuelContainerComponent?>(args.Used))
{
if (component.HasJar)
{
_popupSystem.PopupEntity(Loc.GetString("ame-controller-component-interact-using-already-has-jar"), uid, args.User);
}
else
{
component.JarSlot.Insert(args.Used);
_popupSystem.PopupEntity(Loc.GetString("ame-controller-component-interact-using-success"), uid,
args.User, PopupType.Medium);
component.UpdateUserInterface();
}
}
else
{
_popupSystem.PopupEntity(Loc.GetString("ame-controller-component-interact-using-fail"), uid, args.User);
}
}
private void OnPartInteractUsing(EntityUid uid, AMEPartComponent component, InteractUsingEvent args)
{
if (!HasComp<HandsComponent>(args.User))
{
_popupSystem.PopupEntity(Loc.GetString("ame-part-component-interact-using-no-hands"), uid, args.User);
return;
}
if (!_toolSystem.HasQuality(args.Used, component.QualityNeeded))
return;
if (!_mapManager.TryGetGrid(args.ClickLocation.GetGridUid(EntityManager), out var mapGrid))
return; // No AME in space.
var snapPos = mapGrid.TileIndicesFor(args.ClickLocation);
if (mapGrid.GetAnchoredEntities(snapPos).Any(sc => HasComp<AMEShieldComponent>(sc)))
{
_popupSystem.PopupEntity(Loc.GetString("ame-part-component-shielding-already-present"), uid, args.User);
return;
}
var ent = EntityManager.SpawnEntity("AMEShielding", mapGrid.GridTileToLocal(snapPos));
_adminLogger.Add(LogType.Construction, LogImpact.Low, $"{ToPrettyString(args.User):player} unpacked {ToPrettyString(ent)} at {Transform(ent).Coordinates} from {ToPrettyString(uid)}");
SoundSystem.Play(component.UnwrapSound.GetSound(), Filter.Pvs(uid), uid);
EntityManager.QueueDeleteEntity(uid);
}
}
}

View File

@@ -1,298 +0,0 @@
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.Administration.Systems;
using Content.Server.Chat.Managers;
using Content.Server.Mind.Components;
using Content.Server.NodeContainer;
using Content.Server.Power.Components;
using Content.Server.UserInterface;
using Content.Shared.AME;
using Content.Shared.Database;
using Content.Shared.Hands.EntitySystems;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.Player;
namespace Content.Server.AME.Components
{
[RegisterComponent]
public sealed class AMEControllerComponent : SharedAMEControllerComponent
{
[Dependency] private readonly IEntityManager _entities = default!;
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IChatManager _chat = default!;
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(AMEControllerUiKey.Key);
private bool _injecting;
[ViewVariables] public bool Injecting => _injecting;
[ViewVariables] public int InjectionAmount;
private AppearanceComponent? _appearance;
private PowerSupplierComponent? _powerSupplier;
[DataField("clickSound")] private SoundSpecifier _clickSound = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg");
[DataField("injectSound")] private SoundSpecifier _injectSound = new SoundPathSpecifier("/Audio/Effects/bang.ogg");
private bool Powered => !_entities.TryGetComponent(Owner, out ApcPowerReceiverComponent? receiver) || receiver.Powered;
[ViewVariables]
private int _stability = 100;
public ContainerSlot JarSlot = default!;
[ViewVariables] public bool HasJar => JarSlot.ContainedEntity != null;
protected override void Initialize()
{
base.Initialize();
if (UserInterface != null)
{
UserInterface.OnReceiveMessage += OnUiReceiveMessage;
}
_entities.TryGetComponent(Owner, out _appearance);
_entities.TryGetComponent(Owner, out _powerSupplier);
_injecting = false;
InjectionAmount = 2;
// TODO: Fix this bad name. I'd update maps but then people get mad.
JarSlot = ContainerHelpers.EnsureContainer<ContainerSlot>(Owner, $"AMEController-fuelJarContainer");
}
internal void OnUpdate(float frameTime)
{
if (!_injecting)
{
return;
}
var group = GetAMENodeGroup();
if (group == null)
{
return;
}
if (JarSlot.ContainedEntity is not {Valid: true} jar)
return;
_entities.TryGetComponent<AMEFuelContainerComponent?>(jar, out var fuelJar);
if (fuelJar != null && _powerSupplier != null)
{
var availableInject = fuelJar.FuelAmount >= InjectionAmount ? InjectionAmount : fuelJar.FuelAmount;
_powerSupplier.MaxSupply = group.InjectFuel(availableInject, out var overloading);
fuelJar.FuelAmount -= availableInject;
InjectSound(overloading);
UpdateUserInterface();
}
_stability = group.GetTotalStability();
UpdateDisplay(_stability);
if (_stability <= 0) { group.ExplodeCores(); }
}
// Used to update core count
public void OnAMENodeGroupUpdate()
{
UpdateUserInterface();
}
private AMEControllerBoundUserInterfaceState GetUserInterfaceState()
{
if (JarSlot.ContainedEntity is not {Valid: true} jar)
{
return new AMEControllerBoundUserInterfaceState(Powered, IsMasterController(), false, HasJar, 0, InjectionAmount, GetCoreCount());
}
var jarComponent = _entities.GetComponent<AMEFuelContainerComponent>(jar);
return new AMEControllerBoundUserInterfaceState(Powered, IsMasterController(), _injecting, HasJar, jarComponent.FuelAmount, InjectionAmount, GetCoreCount());
}
/// <summary>
/// Checks whether the player entity is able to use the controller.
/// </summary>
/// <param name="playerEntity">The player entity.</param>
/// <returns>Returns true if the entity can use the controller, and false if it cannot.</returns>
private bool PlayerCanUseController(EntityUid playerEntity, bool needsPower = true)
{
//Need player entity to check if they are still able to use the dispenser
if (playerEntity == default)
return false;
//Check if device is powered
if (needsPower && !Powered)
return false;
return true;
}
public void UpdateUserInterface()
{
var state = GetUserInterfaceState();
UserInterface?.SetState(state);
}
/// <summary>
/// Handles ui messages from the client. For things such as button presses
/// which interact with the world and require server action.
/// </summary>
/// <param name="obj">A user interface message from the client.</param>
private void OnUiReceiveMessage(ServerBoundUserInterfaceMessage obj)
{
if (obj.Session.AttachedEntity is not {Valid: true} player)
{
return;
}
var msg = (UiButtonPressedMessage) obj.Message;
var needsPower = msg.Button switch
{
UiButton.Eject => false,
_ => true,
};
if (!PlayerCanUseController(player, needsPower))
return;
switch (msg.Button)
{
case UiButton.Eject:
TryEject(player);
break;
case UiButton.ToggleInjection:
ToggleInjection();
break;
case UiButton.IncreaseFuel:
InjectionAmount += 2;
break;
case UiButton.DecreaseFuel:
InjectionAmount = InjectionAmount > 0 ? InjectionAmount -= 2 : 0;
break;
}
// Logging
_entities.TryGetComponent(player, out MindContainerComponent? mindContainerComponent);
if (mindContainerComponent != null)
{
var humanReadableState = _injecting ? "Inject" : "Not inject";
if (msg.Button == UiButton.IncreaseFuel || msg.Button == UiButton.DecreaseFuel)
_adminLogger.Add(LogType.Action, LogImpact.Extreme, $"{_entities.ToPrettyString(mindContainerComponent.Owner):player} has set the AME to inject {InjectionAmount} while set to {humanReadableState}");
if (msg.Button == UiButton.ToggleInjection)
_adminLogger.Add(LogType.Action, LogImpact.Extreme, $"{_entities.ToPrettyString(mindContainerComponent.Owner):player} has set the AME to {humanReadableState}");
// Admin alert
if (GetCoreCount() * 2 == InjectionAmount - 2 && msg.Button == UiButton.IncreaseFuel)
_chat.SendAdminAlert(player, $"increased AME over safe limit to {InjectionAmount}", mindContainerComponent);
}
GetAMENodeGroup()?.UpdateCoreVisuals();
UpdateUserInterface();
ClickSound();
}
private void TryEject(EntityUid user)
{
if (!HasJar || _injecting)
return;
if (JarSlot.ContainedEntity is not {Valid: true} jar)
return;
JarSlot.Remove(jar);
UpdateUserInterface();
_sysMan.GetEntitySystem<SharedHandsSystem>().PickupOrDrop(user, jar);
}
private void ToggleInjection()
{
if (!_injecting)
{
_appearance?.SetData(AMEControllerVisuals.DisplayState, "on");
}
else
{
_appearance?.SetData(AMEControllerVisuals.DisplayState, "off");
if (_powerSupplier != null)
{
_powerSupplier.MaxSupply = 0;
}
}
_injecting = !_injecting;
UpdateUserInterface();
}
private void UpdateDisplay(int stability)
{
if (_appearance == null) { return; }
_appearance.TryGetData<string>(AMEControllerVisuals.DisplayState, out var state);
var newState = "on";
if (stability < 50) { newState = "critical"; }
if (stability < 10) { newState = "fuck"; }
if (state != newState)
{
_appearance?.SetData(AMEControllerVisuals.DisplayState, newState);
}
}
private AMENodeGroup? GetAMENodeGroup()
{
_entities.TryGetComponent(Owner, out NodeContainerComponent? nodeContainer);
var engineNodeGroup = nodeContainer?.Nodes.Values
.Select(node => node.NodeGroup)
.OfType<AMENodeGroup>()
.FirstOrDefault();
return engineNodeGroup;
}
private bool IsMasterController()
{
if (GetAMENodeGroup()?.MasterController == this)
{
return true;
}
return false;
}
private int GetCoreCount()
{
var coreCount = 0;
var group = GetAMENodeGroup();
if (group != null)
{
coreCount = group.CoreCount;
}
return coreCount;
}
private void ClickSound()
{
SoundSystem.Play(_clickSound.GetSound(), Filter.Pvs(Owner), Owner, AudioParams.Default.WithVolume(-2f));
}
private void InjectSound(bool overloading)
{
SoundSystem.Play(_injectSound.GetSound(), Filter.Pvs(Owner), Owner, AudioParams.Default.WithVolume(overloading ? 10f : 0f));
}
}
}

View File

@@ -1,18 +0,0 @@
namespace Content.Server.AME.Components;
// TODO: network and put in shared
[RegisterComponent]
public sealed class AMEFuelContainerComponent : Component
{
/// <summary>
/// The amount of fuel in the jar.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("fuelAmount")]
public int FuelAmount = 1000;
/// <summary>
/// The maximum fuel capacity of the jar.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("fuelCapacity")]
public int FuelCapacity = 1000;
}

View File

@@ -1,16 +0,0 @@
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.AME.Components
{
[RegisterComponent]
public sealed class AMEPartComponent : Component
{
[DataField("unwrapSound")]
public SoundSpecifier UnwrapSound = new SoundPathSpecifier("/Audio/Effects/unwrap.ogg");
[DataField("qualityNeeded", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
public string QualityNeeded = "Pulsing";
}
}

View File

@@ -1,64 +0,0 @@
using Content.Shared.AME;
using Robust.Server.GameObjects;
namespace Content.Server.AME.Components
{
[RegisterComponent]
public sealed class AMEShieldComponent : SharedAMEShieldComponent
{
private bool _isCore = false;
[ViewVariables]
public int CoreIntegrity = 100;
private AppearanceComponent? _appearance;
private PointLightComponent? _pointLight;
protected override void Initialize()
{
base.Initialize();
var entMan = IoCManager.Resolve<IEntityManager>();
entMan.TryGetComponent(Owner, out _appearance);
entMan.TryGetComponent(Owner, out _pointLight);
}
public void SetCore()
{
if(_isCore) { return; }
_isCore = true;
_appearance?.SetData(AMEShieldVisuals.Core, "isCore");
}
public void UnsetCore()
{
_isCore = false;
_appearance?.SetData(AMEShieldVisuals.Core, "isNotCore");
UpdateCoreVisuals(0, false);
}
public void UpdateCoreVisuals(int injectionStrength, bool injecting)
{
if (!injecting)
{
_appearance?.SetData(AMEShieldVisuals.CoreState, "off");
if (_pointLight != null) { _pointLight.Enabled = false; }
return;
}
if (_pointLight != null)
{
_pointLight.Radius = Math.Clamp(injectionStrength, 1, 12);
_pointLight.Enabled = true;
}
if (injectionStrength > 2)
{
_appearance?.SetData(AMEShieldVisuals.CoreState, "strong");
return;
}
_appearance?.SetData(AMEShieldVisuals.CoreState, "weak");
}
}
}

View File

@@ -0,0 +1,211 @@
using System.Linq;
using Content.Server.Ame.Components;
using Content.Server.Ame.EntitySystems;
using Content.Server.Chat.Managers;
using Content.Server.Explosion.EntitySystems;
using Content.Server.NodeContainer.NodeGroups;
using Content.Server.NodeContainer.Nodes;
using Robust.Shared.Map;
using Robust.Shared.Random;
namespace Content.Server.Ame;
/// <summary>
/// Node group class for handling the Antimatter Engine's console and parts.
/// </summary>
[NodeGroup(NodeGroupID.AMEngine)]
public sealed class AmeNodeGroup : BaseNodeGroup
{
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
/// <summary>
/// The AME controller which is currently in control of this node group.
/// This could be tracked a few different ways, but this is most convenient,
/// since any part connected to the node group can easily find the master.
/// </summary>
[ViewVariables]
private EntityUid? _masterController;
public EntityUid? MasterController => _masterController;
/// <summary>
/// The set of AME shielding units that currently count as cores for the AME.
/// </summary>
private readonly List<EntityUid> _cores = new();
public int CoreCount => _cores.Count;
public override void LoadNodes(List<Node> groupNodes)
{
base.LoadNodes(groupNodes);
EntityUid? gridEnt = null;
var ameControllerSystem = _entMan.System<AmeControllerSystem>();
var ameShieldingSystem = _entMan.System<AmeShieldingSystem>();
var shieldQuery = _entMan.GetEntityQuery<AmeShieldComponent>();
var controllerQuery = _entMan.GetEntityQuery<AmeControllerComponent>();
var xformQuery = _entMan.GetEntityQuery<TransformComponent>();
foreach (var node in groupNodes)
{
var nodeOwner = node.Owner;
if (!shieldQuery.TryGetComponent(nodeOwner, out var shield))
continue;
if (!xformQuery.TryGetComponent(nodeOwner, out var xform))
continue;
if (!_mapManager.TryGetGrid(xform.GridUid, out var grid))
continue;
if (gridEnt == null)
gridEnt = xform.GridUid;
else if (gridEnt != xform.GridUid)
continue;
var nodeNeighbors = grid.GetCellsInSquareArea(xform.Coordinates, 1)
.Where(entity => entity != nodeOwner && shieldQuery.HasComponent(entity));
if (nodeNeighbors.Count() >= 8)
{
_cores.Add(nodeOwner);
ameShieldingSystem.SetCore(nodeOwner, true, shield);
// Core visuals will be updated later.
}
else
{
ameShieldingSystem.SetCore(nodeOwner, false, shield);
}
}
// Separate to ensure core count is correctly updated.
foreach (var node in groupNodes)
{
var nodeOwner = node.Owner;
if (!controllerQuery.TryGetComponent(nodeOwner, out var controller))
continue;
if (_masterController == null)
_masterController = nodeOwner;
ameControllerSystem.UpdateUi(nodeOwner, controller);
}
UpdateCoreVisuals();
}
public void UpdateCoreVisuals()
{
var injectionAmount = 0;
var injecting = false;
if (_entMan.TryGetComponent<AmeControllerComponent>(_masterController, out var controller))
{
injectionAmount = controller.InjectionAmount;
injecting = controller.Injecting;
}
var injectionStrength = CoreCount > 0 ? injectionAmount / CoreCount : 0;
var coreSystem = _entMan.System<AmeShieldingSystem>();
foreach (var coreUid in _cores)
{
coreSystem.UpdateCoreVisuals(coreUid, injectionStrength, injecting);
}
}
public float InjectFuel(int fuel, out bool overloading)
{
overloading = false;
var shieldQuery = _entMan.GetEntityQuery<AmeShieldComponent>();
if (fuel <= 0 || CoreCount <= 0)
return 0;
var safeFuelLimit = CoreCount * 2;
// Note the float conversions. The maths will completely fail if not done using floats.
// Oh, and don't ever stuff the result of this in an int. Seriously.
var floatFuel = (float) fuel;
var floatCores = (float) CoreCount;
var powerOutput = 20000f * floatFuel * floatFuel / floatCores;
if (fuel <= safeFuelLimit)
return powerOutput;
// The AME is being overloaded.
// Note about these maths: I would assume the general idea here is to make larger engines less safe to overload.
// In other words, yes, those are supposed to be CoreCount, not safeFuelLimit.
var instability = 0;
var overloadVsSizeResult = fuel - CoreCount;
// fuel > safeFuelLimit: Slow damage. Can safely run at this level for burst periods if the engine is small and someone is keeping an eye on it.
if (_random.Prob(0.5f))
instability = 1;
// overloadVsSizeResult > 5:
if (overloadVsSizeResult > 5)
instability = 5;
// overloadVsSizeResult > 10: This will explode in at most 5 injections.
if (overloadVsSizeResult > 10)
instability = 20;
// Apply calculated instability
if (instability == 0)
return powerOutput;
overloading = true;
var integrityCheck = 100;
foreach (var coreUid in _cores)
{
if (!shieldQuery.TryGetComponent(coreUid, out var core))
continue;
var oldIntegrity = core.CoreIntegrity;
core.CoreIntegrity -= instability;
if (oldIntegrity > 95
&& core.CoreIntegrity <= 95
&& core.CoreIntegrity < integrityCheck)
integrityCheck = core.CoreIntegrity;
}
// Admin alert
if (integrityCheck != 100 && _masterController.HasValue)
_chat.SendAdminAlert($"AME overloading: {_entMan.ToPrettyString(_masterController.Value)}");
return powerOutput;
}
public int GetTotalStability()
{
if (CoreCount < 1)
return 100;
var stability = 0;
var coreQuery = _entMan.GetEntityQuery<AmeShieldComponent>();
foreach (var coreUid in _cores)
{
if (coreQuery.TryGetComponent(coreUid, out var core))
stability += core.CoreIntegrity;
}
stability /= CoreCount;
return stability;
}
public void ExplodeCores()
{
if (_cores.Count < 1
|| !_entMan.TryGetComponent<AmeControllerComponent>(MasterController, out var controller))
return;
/*
* todo: add an exact to the shielding and make this find the core closest to the controller
* so they chain explode, after helpers have been added to make it not cancer
*/
var radius = Math.Min(2 * CoreCount * controller.InjectionAmount, 8f);
_entMan.System<ExplosionSystem>().TriggerExplosive(MasterController.Value, radius: radius, delete: false);
}
}

View File

@@ -0,0 +1,81 @@
using Content.Server.Ame.EntitySystems;
using Content.Shared.Ame;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
namespace Content.Server.Ame.Components;
/// <summary>
/// The component used to make an entity the controller/fuel injector port of an AntiMatter Engine.
/// Connects to adjacent entities with this component or <see cref="AmeShieldComponent"/> to make an AME.
/// </summary>
[Access(typeof(AmeControllerSystem), typeof(AmeNodeGroup))]
[RegisterComponent]
public sealed class AmeControllerComponent : SharedAmeControllerComponent
{
/// <summary>
/// The id of the container used to store the current fuel container for the AME.
/// </summary>
public const string FuelContainerId = "AmeFuel";
/// <summary>
/// The container for the fuel canisters used by the AME.
/// </summary>
[ViewVariables]
public ContainerSlot JarSlot = default!;
/// <summary>
/// Whether or not the AME controller is currently injecting animatter into the reactor.
/// </summary>
[DataField("injecting")]
[ViewVariables(VVAccess.ReadWrite)]
public bool Injecting = false;
/// <summary>
/// How much antimatter the AME controller is set to inject into the reactor per update.
/// </summary>
[DataField("injectionAmount")]
[ViewVariables(VVAccess.ReadWrite)]
public int InjectionAmount = 2;
/// <summary>
/// How stable the reactor currently is.
/// When this falls to <= 0 the reactor explodes.
/// </summary>
[DataField("stability")]
[ViewVariables(VVAccess.ReadWrite)]
public int Stability = 100;
/// <summary>
/// The sound used when pressing buttons in the UI.
/// </summary>
[DataField("clickSound")]
[ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier ClickSound = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg");
/// <summary>
/// The sound used when injecting antimatter into the AME.
/// </summary>
[DataField("injectSound")]
[ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier InjectSound = new SoundPathSpecifier("/Audio/Effects/bang.ogg");
/// <summary>
/// The last time this could have injected fuel into the AME.
/// </summary>
[DataField("lastUpdate")]
public TimeSpan LastUpdate = default!;
/// <summary>
/// The next time this will try to inject fuel into the AME.
/// </summary>
[DataField("nextUpdate")]
public TimeSpan NextUpdate = default!;
/// <summary>
/// The the amount of time that passes between injection attempts.
/// </summary>
[DataField("updatePeriod")]
[ViewVariables(VVAccess.ReadWrite)]
public TimeSpan UpdatePeriod = TimeSpan.FromSeconds(10.0);
}

View File

@@ -0,0 +1,23 @@
namespace Content.Server.Ame.Components;
/// <summary>
/// An antimatter containment cell used to handle the fuel for the AME.
/// TODO: network and put in shared
/// </summary>
[RegisterComponent]
public sealed class AmeFuelContainerComponent : Component
{
/// <summary>
/// The amount of fuel in the jar.
/// </summary>
[DataField("fuelAmount")]
[ViewVariables(VVAccess.ReadWrite)]
public int FuelAmount = 1000;
/// <summary>
/// The maximum fuel capacity of the jar.
/// </summary>
[DataField("fuelCapacity")]
[ViewVariables(VVAccess.ReadWrite)]
public int FuelCapacity = 1000;
}

View File

@@ -0,0 +1,24 @@
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Ame.Components;
/// <summary>
/// Packaged AME machinery that can be deployed to construct an AME.
/// </summary>
[RegisterComponent]
public sealed class AmePartComponent : Component
{
/// <summary>
/// The sound played when the AME shielding is unpacked.
/// </summary>
[DataField("unwrapSound")]
public SoundSpecifier UnwrapSound = new SoundPathSpecifier("/Audio/Effects/unwrap.ogg");
/// <summary>
/// The tool quality required to deploy the packaged AME shielding.
/// </summary>
[DataField("qualityNeeded", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string QualityNeeded = "Pulsing";
}

View File

@@ -0,0 +1,26 @@
using Content.Server.Ame.EntitySystems;
using Content.Shared.Ame;
namespace Content.Server.Ame.Components;
/// <summary>
/// The component used to make an entity part of the bulk machinery of an AntiMatter Engine.
/// Connects to adjacent entities with this component or <see cref="AmeControllerComponent"/> to make an AME.
/// </summary>
[Access(typeof(AmeShieldingSystem), typeof(AmeNodeGroup))]
[RegisterComponent]
public sealed class AmeShieldComponent : SharedAmeShieldComponent
{
/// <summary>
/// Whether or not this AME shield counts as a core for the AME or not.
/// </summary>
[ViewVariables]
public bool IsCore = false;
/// <summary>
/// The current integrity of the AME shield.
/// </summary>
[DataField("integrity")]
[ViewVariables]
public int CoreIntegrity = 100;
}

View File

@@ -0,0 +1,326 @@
using Content.Server.Administration.Logs;
using Content.Server.Ame.Components;
using Content.Server.Chat.Managers;
using Content.Server.Mind.Components;
using Content.Server.NodeContainer;
using Content.Server.Power.Components;
using Content.Shared.Ame;
using Content.Shared.Database;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Robust.Server.Containers;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.Timing;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace Content.Server.Ame.EntitySystems;
public sealed class AmeControllerSystem : EntitySystem
{
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly ContainerSystem _containerSystem = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AmeControllerComponent, ComponentStartup>(OnComponentStartup);
SubscribeLocalEvent<AmeControllerComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<AmeControllerComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<AmeControllerComponent, UiButtonPressedMessage>(OnUiButtonPressed);
}
public override void Update(float frameTime)
{
var curTime = _gameTiming.CurTime;
var query = EntityQueryEnumerator<AmeControllerComponent, NodeContainerComponent>();
while (query.MoveNext(out var uid, out var controller, out var nodes))
{
if (controller.NextUpdate <= curTime)
UpdateController(uid, curTime, controller, nodes);
}
}
private void UpdateController(EntityUid uid, TimeSpan curTime, AmeControllerComponent? controller = null, NodeContainerComponent? nodes = null)
{
if (!Resolve(uid, ref controller))
return;
controller.LastUpdate = curTime;
controller.NextUpdate = curTime + controller.UpdatePeriod;
if (!controller.Injecting)
return;
if (!TryGetAMENodeGroup(uid, out var group, nodes))
return;
if (TryComp<AmeFuelContainerComponent>(controller.JarSlot.ContainedEntity, out var fuelJar))
{
var availableInject = Math.Min(controller.InjectionAmount, fuelJar.FuelAmount);
var powerOutput = group.InjectFuel(availableInject, out var overloading);
if (TryComp<PowerSupplierComponent>(uid, out var powerOutlet))
powerOutlet.MaxSupply = powerOutput;
fuelJar.FuelAmount -= availableInject;
_audioSystem.PlayPvs(controller.InjectSound, uid, AudioParams.Default.WithVolume(overloading ? 10f : 0f));
UpdateUi(uid, controller);
}
controller.Stability = group.GetTotalStability();
UpdateDisplay(uid, controller.Stability, controller);
if (controller.Stability <= 0)
group.ExplodeCores();
}
public void UpdateUi(EntityUid uid, AmeControllerComponent? controller = null)
{
if (!Resolve(uid, ref controller))
return;
if (!_userInterfaceSystem.TryGetUi(uid, AmeControllerUiKey.Key, out var bui))
return;
var state = GetUiState(uid, controller);
_userInterfaceSystem.SetUiState(bui, state);
}
private AmeControllerBoundUserInterfaceState GetUiState(EntityUid uid, AmeControllerComponent controller)
{
var powered = !TryComp<ApcPowerReceiverComponent>(uid, out var powerSource) || powerSource.Powered;
var coreCount = TryGetAMENodeGroup(uid, out var group) ? group.CoreCount : 0;
var hasJar = Exists(controller.JarSlot.ContainedEntity);
if (!hasJar || !TryComp<AmeFuelContainerComponent>(controller.JarSlot.ContainedEntity, out var jar))
return new AmeControllerBoundUserInterfaceState(powered, IsMasterController(uid), false, hasJar, 0, controller.InjectionAmount, coreCount);
return new AmeControllerBoundUserInterfaceState(powered, IsMasterController(uid), controller.Injecting, hasJar, jar.FuelAmount, controller.InjectionAmount, coreCount);
}
private bool IsMasterController(EntityUid uid)
{
return TryGetAMENodeGroup(uid, out var group) && group.MasterController == uid;
}
private bool TryGetAMENodeGroup(EntityUid uid, [MaybeNullWhen(false)] out AmeNodeGroup group, NodeContainerComponent? nodes = null)
{
if (!Resolve(uid, ref nodes))
{
group = null;
return false;
}
group = nodes.Nodes.Values
.Select(node => node.NodeGroup)
.OfType<AmeNodeGroup>()
.FirstOrDefault();
return group != null;
}
public void TryEject(EntityUid uid, EntityUid? user = null, AmeControllerComponent? controller = null)
{
if (!Resolve(uid, ref controller))
return;
if (controller.Injecting)
return;
var jar = controller.JarSlot.ContainedEntity;
if (!Exists(jar))
return;
controller.JarSlot.Remove(jar!.Value);
UpdateUi(uid, controller);
if (Exists(user))
_handsSystem.PickupOrDrop(user, jar!.Value);
}
public void SetInjecting(EntityUid uid, bool value, EntityUid? user = null, AmeControllerComponent? controller = null)
{
if (!Resolve(uid, ref controller))
return;
if (controller.Injecting == value)
return;
controller.Injecting = value;
_appearanceSystem.SetData(uid, AmeControllerVisuals.DisplayState, value ? AmeControllerState.On : AmeControllerState.Off);
if (!value && TryComp<PowerSupplierComponent>(uid, out var powerOut))
powerOut.MaxSupply = 0;
UpdateUi(uid, controller);
// Logging
if (!HasComp<MindContainerComponent>(user))
return;
var humanReadableState = value ? "Inject" : "Not inject";
_adminLogger.Add(LogType.Action, LogImpact.Extreme, $"{EntityManager.ToPrettyString(user.Value):player} has set the AME to {humanReadableState}");
}
public void ToggleInjecting(EntityUid uid, EntityUid? user = null, AmeControllerComponent? controller = null)
{
if (!Resolve(uid, ref controller))
return;
SetInjecting(uid, !controller.Injecting, user, controller);
}
public void SetInjectionAmount(EntityUid uid, int value, EntityUid? user = null, AmeControllerComponent? controller = null)
{
if (!Resolve(uid, ref controller))
return;
if (controller.InjectionAmount == value)
return;
var oldValue = controller.InjectionAmount;
controller.InjectionAmount = value;
UpdateUi(uid, controller);
// Logging
if (!TryComp<MindContainerComponent>(user, out var mindContainer))
return;
var humanReadableState = controller.Injecting ? "Inject" : "Not inject";
_adminLogger.Add(LogType.Action, LogImpact.Extreme, $"{EntityManager.ToPrettyString(user.Value):player} has set the AME to inject {controller.InjectionAmount} while set to {humanReadableState}");
// Admin alert
var safeLimit = 0;
if (TryGetAMENodeGroup(uid, out var group))
safeLimit = group.CoreCount * 2;
if (oldValue <= safeLimit && value > safeLimit)
_chatManager.SendAdminAlert(user.Value, $"increased AME over safe limit to {controller.InjectionAmount}", mindContainer);
}
public void AdjustInjectionAmount(EntityUid uid, int delta, int min = 0, int max = int.MaxValue, EntityUid? user = null, AmeControllerComponent? controller = null)
{
if (Resolve(uid, ref controller))
SetInjectionAmount(uid, MathHelper.Clamp(controller.InjectionAmount + delta, min, max), user, controller);
}
private void UpdateDisplay(EntityUid uid, int stability, AmeControllerComponent? controller = null, AppearanceComponent? appearance = null)
{
if (!Resolve(uid, ref controller, ref appearance))
return;
_appearanceSystem.SetData(
uid,
AmeControllerVisuals.DisplayState,
stability switch
{
< 10 => AmeControllerState.Fuck,
< 50 => AmeControllerState.Critical,
_ => AmeControllerState.On,
},
appearance
);
}
private void OnComponentStartup(EntityUid uid, AmeControllerComponent comp, ComponentStartup args)
{
// TODO: Fix this bad name. I'd update maps but then people get mad.
comp.JarSlot = _containerSystem.EnsureContainer<ContainerSlot>(uid, AmeControllerComponent.FuelContainerId);
}
private void OnInteractUsing(EntityUid uid, AmeControllerComponent comp, InteractUsingEvent args)
{
if (!HasComp<HandsComponent>(args.User))
{
_popupSystem.PopupEntity(Loc.GetString("ame-controller-component-interact-using-no-hands-text"), uid, args.User);
return;
}
if (!HasComp<AmeFuelContainerComponent?>(args.Used))
{
_popupSystem.PopupEntity(Loc.GetString("ame-controller-component-interact-using-fail"), uid, args.User);
return;
}
if (Exists(comp.JarSlot.ContainedEntity))
{
_popupSystem.PopupEntity(Loc.GetString("ame-controller-component-interact-using-already-has-jar"), uid, args.User);
return;
}
comp.JarSlot.Insert(args.Used);
_popupSystem.PopupEntity(Loc.GetString("ame-controller-component-interact-using-success"), uid, args.User, PopupType.Medium);
UpdateUi(uid, comp);
}
private void OnPowerChanged(EntityUid uid, AmeControllerComponent comp, ref PowerChangedEvent args)
{
UpdateUi(uid, comp);
}
private void OnUiButtonPressed(EntityUid uid, AmeControllerComponent comp, UiButtonPressedMessage msg)
{
var user = msg.Session.AttachedEntity;
if (!Exists(user))
return;
var needsPower = msg.Button switch
{
UiButton.Eject => false,
_ => true,
};
if (!PlayerCanUseController(uid, user!.Value, needsPower, comp))
return;
_audioSystem.PlayPvs(comp.ClickSound, uid, AudioParams.Default.WithVolume(-2f));
switch (msg.Button)
{
case UiButton.Eject:
TryEject(uid, user: user, controller: comp);
break;
case UiButton.ToggleInjection:
ToggleInjecting(uid, user: user, controller: comp);
break;
case UiButton.IncreaseFuel:
AdjustInjectionAmount(uid, +1, user: user, controller: comp);
break;
case UiButton.DecreaseFuel:
AdjustInjectionAmount(uid, -1, user: user, controller: comp);
break;
}
if (TryGetAMENodeGroup(uid, out var group))
group.UpdateCoreVisuals();
UpdateUi(uid, comp);
}
/// <summary>
/// Checks whether the player entity is able to use the controller.
/// </summary>
/// <param name="playerEntity">The player entity.</param>
/// <returns>Returns true if the entity can use the controller, and false if it cannot.</returns>
private bool PlayerCanUseController(EntityUid uid, EntityUid playerEntity, bool needsPower = true, AmeControllerComponent? controller = null)
{
if (!Resolve(uid, ref controller))
return false;
//Need player entity to check if they are still able to use the dispenser
if (!Exists(playerEntity))
return false;
//Check if device is powered
if (needsPower && TryComp<ApcPowerReceiverComponent>(uid, out var powerSource) && !powerSource.Powered)
return false;
return true;
}
}

View File

@@ -1,19 +1,21 @@
using Content.Server.AME.Components; using Content.Server.Ame.Components;
using Content.Shared.Examine; using Content.Shared.Examine;
namespace Content.Server.AME; namespace Content.Server.Ame.EntitySystems;
/// <summary> /// <summary>
/// Adds fuel level info to examine on fuel jars and handles network state. /// Adds fuel level info to examine on fuel jars and handles network state.
/// </summary> /// </summary>
public sealed partial class AMESystem public sealed class AmeFuelSystem : EntitySystem
{ {
private void InitializeFuel() public override void Initialize()
{ {
SubscribeLocalEvent<AMEFuelContainerComponent, ExaminedEvent>(OnFuelExamined); base.Initialize();
SubscribeLocalEvent<AmeFuelContainerComponent, ExaminedEvent>(OnFuelExamined);
} }
private void OnFuelExamined(EntityUid uid, AMEFuelContainerComponent comp, ExaminedEvent args) private void OnFuelExamined(EntityUid uid, AmeFuelContainerComponent comp, ExaminedEvent args)
{ {
if (!args.IsInDetailsRange) if (!args.IsInDetailsRange)
return; return;

View File

@@ -0,0 +1,51 @@
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.Ame.Components;
using Content.Server.Popups;
using Content.Server.Tools;
using Content.Shared.Database;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using Robust.Shared.Map;
namespace Content.Server.Ame.EntitySystems;
public sealed class AmePartSystem : EntitySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly ToolSystem _toolSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AmePartComponent, InteractUsingEvent>(OnPartInteractUsing);
}
private void OnPartInteractUsing(EntityUid uid, AmePartComponent component, InteractUsingEvent args)
{
if (!_toolSystem.HasQuality(args.Used, component.QualityNeeded))
return;
if (!_mapManager.TryGetGrid(args.ClickLocation.GetGridUid(EntityManager), out var mapGrid))
return; // No AME in space.
var snapPos = mapGrid.TileIndicesFor(args.ClickLocation);
if (mapGrid.GetAnchoredEntities(snapPos).Any(sc => HasComp<AmeShieldComponent>(sc)))
{
_popupSystem.PopupEntity(Loc.GetString("ame-part-component-shielding-already-present"), uid, args.User);
return;
}
var ent = Spawn("AmeShielding", mapGrid.GridTileToLocal(snapPos));
_adminLogger.Add(LogType.Construction, LogImpact.Low, $"{ToPrettyString(args.User):player} unpacked {ToPrettyString(ent)} at {Transform(ent).Coordinates} from {ToPrettyString(uid)}");
_audioSystem.PlayPvs(component.UnwrapSound, uid);
QueueDel(uid);
}
}

View File

@@ -0,0 +1,41 @@
using Content.Server.Ame.Components;
using Content.Shared.Ame;
using Robust.Server.GameObjects;
namespace Content.Server.Ame.EntitySystems;
public sealed class AmeShieldingSystem : EntitySystem
{
[Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly PointLightSystem _pointLightSystem = default!;
public void SetCore(EntityUid uid, bool value, AmeShieldComponent? shield = null)
{
if (!Resolve(uid, ref shield))
return;
if (value == shield.IsCore)
return;
shield.IsCore = value;
_appearanceSystem.SetData(uid, AmeShieldVisuals.Core, value);
if (!value)
UpdateCoreVisuals(uid, 0, false, shield);
}
public void UpdateCoreVisuals(EntityUid uid, int injectionStrength, bool injecting, AmeShieldComponent? shield = null)
{
if (!Resolve(uid, ref shield))
return;
if (!injecting)
{
_appearanceSystem.SetData(uid, AmeShieldVisuals.CoreState, AmeCoreState.Off);
_pointLightSystem.SetEnabled(uid, false);
return;
}
_pointLightSystem.SetRadius(uid, Math.Clamp(injectionStrength, 1, 12));
_pointLightSystem.SetEnabled(uid, true);
_appearanceSystem.SetData(uid, AmeShieldVisuals.CoreState, injectionStrength > 2 ? AmeCoreState.Strong : AmeCoreState.Weak);
}
}

View File

@@ -1,62 +0,0 @@
using Robust.Shared.Serialization;
namespace Content.Shared.AME
{
[Virtual]
public class SharedAMEControllerComponent : Component
{
[Serializable, NetSerializable]
public sealed class AMEControllerBoundUserInterfaceState : BoundUserInterfaceState
{
public readonly bool HasPower;
public readonly bool IsMaster;
public readonly bool Injecting;
public readonly bool HasFuelJar;
public readonly int FuelAmount;
public readonly int InjectionAmount;
public readonly int CoreCount;
public AMEControllerBoundUserInterfaceState(bool hasPower, bool isMaster, bool injecting, bool hasFuelJar, int fuelAmount, int injectionAmount, int coreCount)
{
HasPower = hasPower;
IsMaster = isMaster;
Injecting = injecting;
HasFuelJar = hasFuelJar;
FuelAmount = fuelAmount;
InjectionAmount = injectionAmount;
CoreCount = coreCount;
}
}
[Serializable, NetSerializable]
public sealed class UiButtonPressedMessage : BoundUserInterfaceMessage
{
public readonly UiButton Button;
public UiButtonPressedMessage(UiButton button)
{
Button = button;
}
}
[Serializable, NetSerializable]
public enum AMEControllerUiKey
{
Key
}
public enum UiButton
{
Eject,
ToggleInjection,
IncreaseFuel,
DecreaseFuel,
}
[Serializable, NetSerializable]
public enum AMEControllerVisuals
{
DisplayState,
}
}
}

View File

@@ -1,22 +0,0 @@
using Robust.Shared.Serialization;
namespace Content.Shared.AME
{
[Virtual]
public class SharedAMEShieldComponent : Component
{
[Serializable, NetSerializable]
public enum AMEShieldVisuals
{
Core,
CoreState
}
public enum AMECoreState
{
Off,
Weak,
Strong
}
}
}

View File

@@ -0,0 +1,71 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Ame;
[Virtual]
public class SharedAmeControllerComponent : Component
{
}
[Serializable, NetSerializable]
public sealed class AmeControllerBoundUserInterfaceState : BoundUserInterfaceState
{
public readonly bool HasPower;
public readonly bool IsMaster;
public readonly bool Injecting;
public readonly bool HasFuelJar;
public readonly int FuelAmount;
public readonly int InjectionAmount;
public readonly int CoreCount;
public AmeControllerBoundUserInterfaceState(bool hasPower, bool isMaster, bool injecting, bool hasFuelJar, int fuelAmount, int injectionAmount, int coreCount)
{
HasPower = hasPower;
IsMaster = isMaster;
Injecting = injecting;
HasFuelJar = hasFuelJar;
FuelAmount = fuelAmount;
InjectionAmount = injectionAmount;
CoreCount = coreCount;
}
}
[Serializable, NetSerializable]
public sealed class UiButtonPressedMessage : BoundUserInterfaceMessage
{
public readonly UiButton Button;
public UiButtonPressedMessage(UiButton button)
{
Button = button;
}
}
[Serializable, NetSerializable]
public enum AmeControllerUiKey
{
Key
}
public enum UiButton
{
Eject,
ToggleInjection,
IncreaseFuel,
DecreaseFuel,
}
[Serializable, NetSerializable]
public enum AmeControllerVisuals
{
DisplayState,
}
[Serializable, NetSerializable]
public enum AmeControllerState
{
On,
Critical,
Fuck,
Off,
}

View File

@@ -0,0 +1,23 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Ame;
[Virtual]
public class SharedAmeShieldComponent : Component
{
}
[Serializable, NetSerializable]
public enum AmeShieldVisuals
{
Core,
CoreState
}
[Serializable, NetSerializable]
public enum AmeCoreState
{
Off,
Weak,
Strong
}

View File

@@ -6,7 +6,7 @@
components: components:
- type: StorageFill - type: StorageFill
contents: contents:
- id: AMEPart - id: AmePart
amount: 9 amount: 9
- type: entity - type: entity
@@ -15,7 +15,7 @@
components: components:
- type: StorageFill - type: StorageFill
contents: contents:
- id: AMEJar - id: AmeJar
amount: 3 amount: 3
- type: entity - type: entity
@@ -24,7 +24,7 @@
components: components:
- type: StorageFill - type: StorageFill
contents: contents:
- id: AMEControllerUnanchored - id: AmeControllerUnanchored
# Singularity # Singularity

View File

@@ -23,9 +23,9 @@
- type: MachineBoard - type: MachineBoard
prototype: ParticleAcceleratorFuelChamberUnfinished prototype: ParticleAcceleratorFuelChamberUnfinished
componentRequirements: componentRequirements:
AMEFuelContainer: AmeFuelContainer:
Amount: 1 Amount: 1
DefaultPrototype: AMEJar DefaultPrototype: AmeJar
ExamineName: AME Fuel Jar ExamineName: AME Fuel Jar
materialRequirements: materialRequirements:
Glass: 10 Glass: 10

View File

@@ -1,6 +1,6 @@
- type: entity - type: entity
parent: BaseItem parent: BaseItem
id: AMEJar id: AmeJar
name: AME fuel jar name: AME fuel jar
description: A hermetically sealed jar containing antimatter for use in an antimatter reactor. description: A hermetically sealed jar containing antimatter for use in an antimatter reactor.
components: components:
@@ -10,7 +10,7 @@
- type: Sprite - type: Sprite
sprite: Objects/Power/AME/ame_jar.rsi sprite: Objects/Power/AME/ame_jar.rsi
state: jar state: jar
- type: AMEFuelContainer - type: AmeFuelContainer
- type: StaticPrice - type: StaticPrice
price: 500 price: 500
- type: GuideHelp - type: GuideHelp

View File

@@ -1,6 +1,6 @@
- type: entity - type: entity
parent: BaseItem parent: BaseItem
id: AMEPart id: AmePart
name: AME part name: AME part
description: A flatpack used for constructing an antimatter engine reactor. Use a multitool to unpack it. description: A flatpack used for constructing an antimatter engine reactor. Use a multitool to unpack it.
components: components:
@@ -10,7 +10,7 @@
- type: Sprite - type: Sprite
sprite: Objects/Power/AME/ame_part.rsi sprite: Objects/Power/AME/ame_part.rsi
state: box state: box
- type: AMEPart - type: AmePart
- type: StaticPrice - type: StaticPrice
price: 500 price: 500
- type: GuideHelp - type: GuideHelp

View File

@@ -1,5 +1,5 @@
- type: entity - type: entity
id: AMEController id: AmeController
name: AME controller name: AME controller
description: It's a controller for the antimatter engine. description: It's a controller for the antimatter engine.
placement: placement:
@@ -11,6 +11,11 @@
snapCardinals: true snapCardinals: true
sprite: Structures/Power/Generation/ame.rsi sprite: Structures/Power/Generation/ame.rsi
state: control state: control
layers:
- state: control
- state: control_on
map: ["display"]
visible: false
- type: Physics - type: Physics
- type: Fixtures - type: Fixtures
fixtures: fixtures:
@@ -44,19 +49,26 @@
noRot: true noRot: true
- type: Anchorable - type: Anchorable
- type: Pullable - type: Pullable
- type: AMEController - type: AmeController
- type: Explosive - type: Explosive
explosionType: Default explosionType: Default
intensitySlope: 5 intensitySlope: 5
maxIntensity: 60 maxIntensity: 60
- type: ActivatableUI - type: ActivatableUI
key: enum.AMEControllerUiKey.Key key: enum.AmeControllerUiKey.Key
- type: UserInterface - type: UserInterface
interfaces: interfaces:
- key: enum.AMEControllerUiKey.Key - key: enum.AmeControllerUiKey.Key
type: AMEControllerBoundUserInterface type: AmeControllerBoundUserInterface
- type: Appearance - type: Appearance
- type: AMEControllerVisuals - type: GenericVisualizer
visuals:
enum.AmeControllerVisuals.DisplayState:
display:
Off: { visible: false }
On: { state: control_on, visible: true }
Critical: { state: control_critical, visible: true }
Fuck: { state: control_fuck, visible: true }
- type: NodeContainer - type: NodeContainer
examinable: true examinable: true
nodes: nodes:
@@ -72,7 +84,7 @@
supplyRate: 0 supplyRate: 0
- type: ContainerContainer - type: ContainerContainer
containers: containers:
AMEController-fuelJarContainer: !type:ContainerSlot {} AmeFuel: !type:ContainerSlot {}
- type: GuideHelp - type: GuideHelp
guides: [ AME, Power ] guides: [ AME, Power ]
- type: Electrified - type: Electrified
@@ -85,8 +97,8 @@
- type: entity - type: entity
noSpawn: true noSpawn: true
parent: AMEController parent: AmeController
id: AMEControllerUnanchored id: AmeControllerUnanchored
suffix: Unanchored suffix: Unanchored
components: components:
- type: Transform - type: Transform
@@ -95,7 +107,7 @@
bodyType: Dynamic bodyType: Dynamic
- type: entity - type: entity
id: AMEShielding id: AmeShielding
name: AME shielding name: AME shielding
description: Keeps the antimatter in and the matter out. description: Keeps the antimatter in and the matter out.
placement: placement:
@@ -107,6 +119,15 @@
drawdepth: Walls drawdepth: Walls
sprite: Structures/Power/Generation/ame.rsi sprite: Structures/Power/Generation/ame.rsi
state: shield_0 state: shield_0
layers:
- state: shield_0
- state: core
map: ["core"]
visible: false
- state: core_weak
map: ["core_glow"]
shader: unshaded
visible: false
- type: Physics - type: Physics
- type: Fixtures - type: Fixtures
fixtures: fixtures:
@@ -141,7 +162,7 @@
mode: CardinalFlags mode: CardinalFlags
base: shield_ base: shield_
key: ame_shield key: ame_shield
- type: AMEShield - type: AmeShield
- type: NodeContainer - type: NodeContainer
nodes: nodes:
ame: ame:
@@ -153,9 +174,19 @@
energy: 0.5 energy: 0.5
color: "#00AAFF" color: "#00AAFF"
- type: Appearance - type: Appearance
- type: AMEShieldingVisuals - type: GenericVisualizer
visuals:
enum.AmeShieldVisuals.Core:
core:
True: { visible: true }
False: { visible: false }
enum.AmeShieldVisuals.CoreState:
core_glow:
Off: { visible: false }
Weak: { state: core_weak, visible: true }
Strong: { state: core_strong, visible: true }
- type: Construction - type: Construction
graph: AMEShielding graph: AmeShielding
node: ameShielding node: ameShielding
- type: GuideHelp - type: GuideHelp
guides: [ AME, Power ] guides: [ AME, Power ]

View File

@@ -1,18 +1,18 @@
- type: constructionGraph - type: constructionGraph
id: AMEShielding id: AmeShielding
start: start start: start
graph: graph:
- node: start - node: start
- node: ameShielding - node: ameShielding
entity: AMEShielding entity: AmeShielding
edges: edges:
- to: start - to: start
completed: completed:
- !type:AdminLog # I don't like logging it like this. The log should include the user, AMEShielding EntityID, and AMEPart EntityID, and there should also be a start of attempt log. - !type:AdminLog # I don't like logging it like this. The log should include the user, AMEShielding EntityID, and AMEPart EntityID, and there should also be a start of attempt log.
message: "An AME shielding was deconstructed" message: "An AME shielding was deconstructed"
- !type:SpawnPrototype - !type:SpawnPrototype
prototype: AMEPart prototype: AmePart
amount: 1 amount: 1
- !type:DeleteEntity - !type:DeleteEntity
steps: steps:

View File

@@ -6,9 +6,9 @@ The AME is one of the simplest engines available. You put together the multi-til
## Construction ## Construction
<Box>Required parts:</Box> <Box>Required parts:</Box>
<Box> <Box>
<GuideEntityEmbed Entity="AMEController"/> <GuideEntityEmbed Entity="AmeController"/>
<GuideEntityEmbed Entity="AMEPart"/> <GuideEntityEmbed Entity="AmePart"/>
<GuideEntityEmbed Entity="AMEJar"/> <GuideEntityEmbed Entity="AmeJar"/>
</Box> </Box>
To assemble an AME, start by wrenching down the controller on the far end of a HV wire. On most stations, there's catwalks to assist with this. From there, start putting down a 3x3 or larger square of AME parts in preparation for construction, making sure to maximize the number of "center" pieces that are surrounded on all 8 sides. To assemble an AME, start by wrenching down the controller on the far end of a HV wire. On most stations, there's catwalks to assist with this. From there, start putting down a 3x3 or larger square of AME parts in preparation for construction, making sure to maximize the number of "center" pieces that are surrounded on all 8 sides.

View File

@@ -53,6 +53,12 @@ FoodCondimentBottleSmallHotsauce: FoodCondimentBottleHotsauce
FoodBakedCookieFortune: FoodSnackCookieFortune FoodBakedCookieFortune: FoodSnackCookieFortune
GunSafeSubMachineGunVector: GunSafeSubMachineGunDrozd GunSafeSubMachineGunVector: GunSafeSubMachineGunDrozd
# 2023-05-24
AMEController: AmeController
AMEJar: AmeJar
AMEPart: AmePart
AMEShielding: AmeShielding
# 2023-05-29 # 2023-05-29
OrGate: null OrGate: null