Portable Generator Rework (#19302)

This commit is contained in:
Pieter-Jan Briers
2023-08-25 20:40:42 +02:00
committed by GitHub
parent 50828363fe
commit bf16698efa
73 changed files with 1933 additions and 473 deletions

View File

@@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.DoAfter; using Content.Shared.DoAfter;
using Content.Shared.Hands.Components; using Content.Shared.Hands.Components;
using Robust.Client.Graphics; using Robust.Client.Graphics;
@@ -51,4 +52,58 @@ public sealed class DoAfterSystem : SharedDoAfterSystem
var handsQuery = GetEntityQuery<HandsComponent>(); var handsQuery = GetEntityQuery<HandsComponent>();
Update(playerEntity.Value, active, comp, time, xformQuery, handsQuery); Update(playerEntity.Value, active, comp, time, xformQuery, handsQuery);
} }
/// <summary>
/// Try to find an active do-after being executed by the local player.
/// </summary>
/// <param name="entity">The entity the do after must be targeting (<see cref="DoAfterArgs.Target"/>)</param>
/// <param name="doAfter">The found do-after.</param>
/// <param name="event">The event to be raised on the found do-after when it completes.</param>
/// <param name="progress">The progress of the found do-after, from 0 to 1.</param>
/// <typeparam name="T">The type of event that must be raised by the found do-after.</typeparam>
/// <returns>True if a do-after was found.</returns>
public bool TryFindActiveDoAfter<T>(
EntityUid entity,
[NotNullWhen(true)] out Shared.DoAfter.DoAfter? doAfter,
[NotNullWhen(true)] out T? @event,
out float progress)
where T : DoAfterEvent
{
var playerEntity = _player.LocalPlayer?.ControlledEntity;
doAfter = null;
@event = null;
progress = default;
if (!TryComp(playerEntity, out ActiveDoAfterComponent? active))
return false;
if (_metadata.EntityPaused(playerEntity.Value))
return false;
var comp = Comp<DoAfterComponent>(playerEntity.Value);
var time = GameTiming.CurTime;
foreach (var candidate in comp.DoAfters.Values)
{
if (candidate.Cancelled)
continue;
if (candidate.Args.Target != entity)
continue;
if (candidate.Args.Event is not T candidateEvent)
continue;
@event = candidateEvent;
doAfter = candidate;
var elapsed = time - doAfter.StartTime;
progress = (float) Math.Min(1, elapsed.TotalSeconds / doAfter.Args.Delay.TotalSeconds);
return true;
}
return false;
}
} }

View File

@@ -1,22 +1,39 @@
<controls:FancyWindow xmlns="https://spacestation14.io" <controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls" xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls" xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
MinSize="350 130" MinSize="450 235"
SetSize="360 180" SetSize="450 235"
Title="{Loc 'generator-ui-title'}"> Resizable="False"
Title="{Loc 'portable-generator-ui-title'}">
<BoxContainer Margin="4 0" Orientation="Horizontal"> <BoxContainer Margin="4 0" Orientation="Horizontal">
<BoxContainer Orientation="Vertical" HorizontalExpand="True" SizeFlagsStretchRatio="2" VerticalAlignment="Center" Margin="5"> <BoxContainer Orientation="Vertical" HorizontalExpand="True" SizeFlagsStretchRatio="2" VerticalAlignment="Top" Margin="5">
<GridContainer Margin="2 0 0 0" Columns="2" HorizontalExpand="True"> <GridContainer Margin="2 0 0 0" Columns="2" HorizontalExpand="True">
<Label Name="StatusLabel" Text="{Loc 'portable-generator-ui-power-switch'}" HorizontalExpand="True" />
<Control MinWidth="120">
<Button Name="StartButton" Text="{Loc 'portable-generator-ui-start'}" />
<Button Name="StopButton" Text="{Loc 'portable-generator-ui-stop'}" />
<ProgressBar Name="StartProgress" MaxValue="1" />
<Label Name="LabelUnanchored" Text="{Loc 'portable-generator-ui-unanchored'}" />
</Control>
<!-- Power --> <!-- Power -->
<Label Text="{Loc 'generator-ui-target-power-label'}"/> <Label Text="{Loc 'portable-generator-ui-target-power-label'}"/>
<SpinBox Name="TargetPower" HorizontalExpand="True"/> <SpinBox Name="TargetPower" HorizontalExpand="True"/>
<Label Text="{Loc 'generator-ui-efficiency-label'}"/> <Label Text="{Loc 'portable-generator-ui-efficiency-label'}"/>
<Label Name="Efficiency" Text="???%" HorizontalExpand="True"/> <BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Label Text="{Loc 'generator-ui-fuel-use-label'}"/> <Label Name="Efficiency" Text="???%" />
<Label Name="Eta" HorizontalExpand="True" Margin="4 0 0 0" />
</BoxContainer>
<Label Text="{Loc 'portable-generator-ui-fuel-use-label'}"/>
<ProgressBar Name="FuelFraction" MinValue="0" MaxValue="1" HorizontalExpand="True"/> <ProgressBar Name="FuelFraction" MinValue="0" MaxValue="1" HorizontalExpand="True"/>
<Label Text="{Loc 'generator-ui-fuel-left-label'}"/> <Label Text="{Loc 'portable-generator-ui-fuel-left-label'}"/>
<Label Name="FuelLeft" Text="0" HorizontalExpand="True"/> <BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Label Name="FuelLeft" Text="0" HorizontalExpand="True"/>
<Button Name="FuelEject" Text="{Loc 'portable-generator-ui-eject'}" />
</BoxContainer>
<Label Name="OutputSwitchLabel" Text="{Loc 'portable-generator-ui-switch'}" Visible="False" />
<Button Name="OutputSwitchButton" Visible="False" />
</GridContainer> </GridContainer>
<Label Margin="2 0 0 0" Name="CloggedLabel" FontColorOverride="Red" Text="{Loc 'portable-generator-ui-clogged'}" />
</BoxContainer> </BoxContainer>
<cc:VSeparator StyleClasses="LowDivider"/> <cc:VSeparator StyleClasses="LowDivider"/>
<PanelContainer Margin="12 0 0 0" StyleClasses="Inset" VerticalAlignment="Center"> <PanelContainer Margin="12 0 0 0" StyleClasses="Inset" VerticalAlignment="Center">

View File

@@ -1,4 +1,5 @@
using Content.Client.UserInterface.Controls; using Content.Client.DoAfter;
using Content.Client.UserInterface.Controls;
using Content.Shared.Power.Generator; using Content.Shared.Power.Generator;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
@@ -8,24 +9,33 @@ namespace Content.Client.Power.Generator;
[GenerateTypedNameReferences] [GenerateTypedNameReferences]
public sealed partial class GeneratorWindow : FancyWindow public sealed partial class GeneratorWindow : FancyWindow
{ {
private readonly EntityUid _entity;
[Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly ILocalizationManager _loc = default!;
private readonly FuelGeneratorComponent? _component; private readonly FuelGeneratorComponent? _component;
private SolidFuelGeneratorComponentBuiState? _lastState; private PortableGeneratorComponentBuiState? _lastState;
public GeneratorWindow(SolidFuelGeneratorBoundUserInterface bui, EntityUid vis) public GeneratorWindow(PortableGeneratorBoundUserInterface bui, EntityUid entity)
{ {
_entity = entity;
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this); IoCManager.InjectDependencies(this);
_entityManager.TryGetComponent(vis, out _component); _entityManager.TryGetComponent(entity, out _component);
EntityView.SetEntity(vis); EntityView.SetEntity(entity);
TargetPower.IsValid += IsValid; TargetPower.IsValid += IsValid;
TargetPower.ValueChanged += (args) => TargetPower.ValueChanged += (args) =>
{ {
bui.SetTargetPower(args.Value); bui.SetTargetPower(args.Value);
}; };
StartButton.OnPressed += _ => bui.Start();
StopButton.OnPressed += _ => bui.Stop();
OutputSwitchButton.OnPressed += _ => bui.SwitchOutput();
FuelEject.OnPressed += _ => bui.EjectFuel();
} }
private bool IsValid(int arg) private bool IsValid(int arg)
@@ -39,19 +49,76 @@ public sealed partial class GeneratorWindow : FancyWindow
return true; return true;
} }
public void Update(SolidFuelGeneratorComponentBuiState state) public void Update(PortableGeneratorComponentBuiState state)
{ {
if (_component == null) if (_component == null)
return; return;
var oldState = _lastState;
_lastState = state; _lastState = state;
// ReSharper disable once CompareOfFloatsByEqualityOperator if (!TargetPower.LineEditControl.HasKeyboardFocus())
if (oldState?.TargetPower != state.TargetPower)
TargetPower.OverrideValue((int)(state.TargetPower / 1000.0f)); TargetPower.OverrideValue((int)(state.TargetPower / 1000.0f));
Efficiency.Text = SharedGeneratorSystem.CalcFuelEfficiency(state.TargetPower, state.OptimalPower, _component).ToString("P1"); var efficiency = SharedGeneratorSystem.CalcFuelEfficiency(state.TargetPower, state.OptimalPower, _component);
Efficiency.Text = efficiency.ToString("P1");
var burnRate = _component.OptimalBurnRate / efficiency;
var left = state.RemainingFuel / burnRate;
Eta.Text = Loc.GetString(
"portable-generator-ui-eta",
("minutes", Math.Ceiling(left / 60.0)));
FuelFraction.Value = state.RemainingFuel - (int) state.RemainingFuel; FuelFraction.Value = state.RemainingFuel - (int) state.RemainingFuel;
FuelLeft.Text = ((int) MathF.Floor(state.RemainingFuel)).ToString(); FuelLeft.Text = ((int) MathF.Floor(state.RemainingFuel)).ToString();
var progress = 0f;
var unanchored = !_entityManager.GetComponent<TransformComponent>(_entity).Anchored;
var starting = !unanchored && TryGetStartProgress(out progress);
var on = !unanchored && !starting && state.On;
var off = !unanchored && !starting && !state.On;
LabelUnanchored.Visible = unanchored;
StartProgress.Visible = starting;
StopButton.Visible = on;
StartButton.Visible = off;
if (starting)
{
StatusLabel.Text = _loc.GetString("portable-generator-ui-status-starting");
StatusLabel.SetOnlyStyleClass("Caution");
StartProgress.Value = progress;
}
else if (on)
{
StatusLabel.Text = _loc.GetString("portable-generator-ui-status-running");
StatusLabel.SetOnlyStyleClass("Good");
}
else
{
StatusLabel.Text = _loc.GetString("portable-generator-ui-status-stopped");
StatusLabel.SetOnlyStyleClass("Danger");
}
var canSwitch = _entityManager.TryGetComponent(_entity, out PowerSwitchableGeneratorComponent? switchable);
OutputSwitchLabel.Visible = canSwitch;
OutputSwitchButton.Visible = canSwitch;
if (canSwitch)
{
var isHV = switchable!.ActiveOutput == PowerSwitchableGeneratorOutput.HV;
OutputSwitchLabel.Text =
Loc.GetString(isHV ? "portable-generator-ui-switch-hv" : "portable-generator-ui-switch-mv");
OutputSwitchButton.Text =
Loc.GetString(isHV ? "portable-generator-ui-switch-to-mv" : "portable-generator-ui-switch-to-hv");
OutputSwitchButton.Disabled = state.On;
}
CloggedLabel.Visible = state.Clogged;
}
private bool TryGetStartProgress(out float progress)
{
var doAfterSystem = _entityManager.EntitySysManager.GetEntitySystem<DoAfterSystem>();
return doAfterSystem.TryFindActiveDoAfter<GeneratorStartedEvent>(_entity, out _, out _, out progress);
} }
} }

View File

@@ -0,0 +1,62 @@
using Content.Shared.Power.Generator;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
namespace Content.Client.Power.Generator;
[UsedImplicitly]
public sealed class PortableGeneratorBoundUserInterface : BoundUserInterface
{
private GeneratorWindow? _window;
public PortableGeneratorBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_window = new GeneratorWindow(this, Owner);
_window.OpenCenteredLeft();
_window.OnClose += Close;
}
protected override void UpdateState(BoundUserInterfaceState state)
{
if (state is not PortableGeneratorComponentBuiState msg)
return;
_window?.Update(msg);
}
protected override void Dispose(bool disposing)
{
_window?.Dispose();
}
public void SetTargetPower(int target)
{
SendMessage(new PortableGeneratorSetTargetPowerMessage(target));
}
public void Start()
{
SendMessage(new PortableGeneratorStartMessage());
}
public void Stop()
{
SendMessage(new PortableGeneratorStopMessage());
}
public void SwitchOutput()
{
SendMessage(new PortableGeneratorSwitchOutputMessage());
}
public void EjectFuel()
{
SendMessage(new PortableGeneratorEjectFuelMessage());
}
}

View File

@@ -1,42 +0,0 @@
using Content.Shared.Power.Generator;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
namespace Content.Client.Power.Generator;
[UsedImplicitly]
public sealed class SolidFuelGeneratorBoundUserInterface : BoundUserInterface
{
private GeneratorWindow? _window;
public SolidFuelGeneratorBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_window = new GeneratorWindow(this, Owner);
_window.OpenCenteredLeft();
_window.OnClose += Close;
}
protected override void UpdateState(BoundUserInterfaceState state)
{
if (state is not SolidFuelGeneratorComponentBuiState msg)
return;
_window?.Update(msg);
}
protected override void Dispose(bool disposing)
{
_window?.Dispose();
}
public void SetTargetPower(int target)
{
SendMessage(new SetTargetPowerMessage(target));
}
}

View File

@@ -369,13 +369,13 @@ namespace Content.Client.Stylesheets
{ {
BackgroundColor = new Color(0.25f, 0.25f, 0.25f) BackgroundColor = new Color(0.25f, 0.25f, 0.25f)
}; };
progressBarBackground.SetContentMarginOverride(StyleBox.Margin.Vertical, 5); progressBarBackground.SetContentMarginOverride(StyleBox.Margin.Vertical, 14.5f);
var progressBarForeground = new StyleBoxFlat var progressBarForeground = new StyleBoxFlat
{ {
BackgroundColor = new Color(0.25f, 0.50f, 0.25f) BackgroundColor = new Color(0.25f, 0.50f, 0.25f)
}; };
progressBarForeground.SetContentMarginOverride(StyleBox.Margin.Vertical, 5); progressBarForeground.SetContentMarginOverride(StyleBox.Margin.Vertical, 14.5f);
// CheckBox // CheckBox
var checkBoxTextureChecked = resCache.GetTexture("/Textures/Interface/Nano/checkbox_checked.svg.96dpi.png"); var checkBoxTextureChecked = resCache.GetTexture("/Textures/Interface/Nano/checkbox_checked.svg.96dpi.png");

View File

@@ -52,13 +52,13 @@ namespace Content.Client.Stylesheets
{ {
BackgroundColor = new Color(0.25f, 0.25f, 0.25f) BackgroundColor = new Color(0.25f, 0.25f, 0.25f)
}; };
progressBarBackground.SetContentMarginOverride(StyleBox.Margin.Vertical, 5); progressBarBackground.SetContentMarginOverride(StyleBox.Margin.Vertical, 14.5f);
var progressBarForeground = new StyleBoxFlat var progressBarForeground = new StyleBoxFlat
{ {
BackgroundColor = new Color(0.25f, 0.50f, 0.25f) BackgroundColor = new Color(0.25f, 0.50f, 0.25f)
}; };
progressBarForeground.SetContentMarginOverride(StyleBox.Margin.Vertical, 5); progressBarForeground.SetContentMarginOverride(StyleBox.Margin.Vertical, 14.5f);
var textureInvertedTriangle = resCache.GetTexture("/Textures/Interface/Nano/inverted_triangle.svg.png"); var textureInvertedTriangle = resCache.GetTexture("/Textures/Interface/Nano/inverted_triangle.svg.png");

View File

@@ -1,4 +1,5 @@
using Content.Server.Administration.Logs; using System.Linq;
using Content.Server.Administration.Logs;
using Content.Shared.Materials; using Content.Shared.Materials;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Stacks; using Content.Shared.Stacks;
@@ -129,4 +130,63 @@ public sealed class MaterialStorageSystem : SharedMaterialStorageSystem
overflowMaterial = amount - amountToSpawn * materialPerStack; overflowMaterial = amount - amountToSpawn * materialPerStack;
return _stackSystem.SpawnMultiple(materialProto.StackEntity, amountToSpawn, coordinates); return _stackSystem.SpawnMultiple(materialProto.StackEntity, amountToSpawn, coordinates);
} }
/// <summary>
/// Eject a material out of this storage. The internal counts are updated.
/// Material that cannot be ejected stays in storage. (e.g. only have 50 but a sheet needs 100).
/// </summary>
/// <param name="entity">The entity with storage to eject from.</param>
/// <param name="material">The material prototype to eject.</param>
/// <param name="maxAmount">The maximum amount to eject. If not given, as much as possible is ejected.</param>
/// <param name="coordinates">The position where to spawn the created sheets. If not given, they're spawned next to the entity.</param>
/// <param name="component">The storage component on <paramref name="entity"/>. Resolved automatically if not given.</param>
/// <returns>The stack entities that were spawned.</returns>
public List<EntityUid> EjectMaterial(
EntityUid entity,
string material,
int? maxAmount = null,
EntityCoordinates? coordinates = null,
MaterialStorageComponent? component = null)
{
if (!Resolve(entity, ref component))
return new List<EntityUid>();
coordinates ??= Transform(entity).Coordinates;
var amount = GetMaterialAmount(entity, material, component);
if (maxAmount != null)
amount = Math.Min(maxAmount.Value, amount);
var spawned = SpawnMultipleFromMaterial(amount, material, coordinates.Value, out var overflow);
TryChangeMaterialAmount(entity, material, -(amount - overflow), component);
return spawned;
}
/// <summary>
/// Eject all material stored in an entity, with the same mechanics as <see cref="EjectMaterial"/>.
/// </summary>
/// <param name="entity">The entity with storage to eject from.</param>
/// <param name="coordinates">The position where to spawn the created sheets. If not given, they're spawned next to the entity.</param>
/// <param name="component">The storage component on <paramref name="entity"/>. Resolved automatically if not given.</param>
/// <returns>The stack entities that were spawned.</returns>
public List<EntityUid> EjectAllMaterial(
EntityUid entity,
EntityCoordinates? coordinates = null,
MaterialStorageComponent? component = null)
{
if (!Resolve(entity, ref component))
return new List<EntityUid>();
coordinates ??= Transform(entity).Coordinates;
var allSpawned = new List<EntityUid>();
foreach (var material in component.Storage.Keys.ToArray())
{
var spawned = EjectMaterial(entity, material, null, coordinates, component);
allSpawned.AddRange(spawned);
}
return allSpawned;
}
} }

View File

@@ -13,6 +13,7 @@ namespace Content.Server.Power.Components
} }
public abstract partial class BaseNetConnectorComponent<TNetType> : Component, IBaseNetConnectorComponent<TNetType> public abstract partial class BaseNetConnectorComponent<TNetType> : Component, IBaseNetConnectorComponent<TNetType>
where TNetType : class
{ {
[Dependency] private readonly IEntityManager _entMan = default!; [Dependency] private readonly IEntityManager _entMan = default!;
@@ -46,7 +47,10 @@ namespace Content.Server.Power.Components
public void ClearNet() public void ClearNet()
{ {
if (_net != null) if (_net != null)
{
RemoveSelfFromNet(_net); RemoveSelfFromNet(_net);
_net = null;
}
} }
protected abstract void AddSelfToNet(TNetType net); protected abstract void AddSelfToNet(TNetType net);

View File

@@ -4,7 +4,7 @@ using Content.Server.Power.Pow3r;
namespace Content.Server.Power.Components namespace Content.Server.Power.Components
{ {
[RegisterComponent] [RegisterComponent]
public sealed partial class PowerSupplierComponent : BasePowerNetComponent public sealed partial class PowerSupplierComponent : BaseNetConnectorComponent<IBasePowerNet>
{ {
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
[DataField("supplyRate")] [DataField("supplyRate")]
@@ -47,12 +47,12 @@ namespace Content.Server.Power.Components
[ViewVariables] [ViewVariables]
public PowerState.Supply NetworkSupply { get; } = new(); public PowerState.Supply NetworkSupply { get; } = new();
protected override void AddSelfToNet(IPowerNet powerNet) protected override void AddSelfToNet(IBasePowerNet powerNet)
{ {
powerNet.AddSupplier(this); powerNet.AddSupplier(this);
} }
protected override void RemoveSelfFromNet(IPowerNet powerNet) protected override void RemoveSelfFromNet(IBasePowerNet powerNet)
{ {
powerNet.RemoveSupplier(this); powerNet.RemoveSupplier(this);
} }

View File

@@ -39,7 +39,7 @@ public sealed class PowerNetConnectorSystem : EntitySystem
BaseNetConnectorInit(component); BaseNetConnectorInit(component);
} }
public void BaseNetConnectorInit<T>(BaseNetConnectorComponent<T> component) public void BaseNetConnectorInit<T>(BaseNetConnectorComponent<T> component) where T : class
{ {
if (component.NeedsNet) if (component.NeedsNet)
{ {

View File

@@ -395,11 +395,7 @@ namespace Content.Server.Power.EntitySystems
} }
} }
foreach (var consumer in net.Consumers) DoReconnectBasePowerNet(net, netNode);
{
netNode.Loads.Add(consumer.NetworkLoad.Id);
consumer.NetworkLoad.LinkedNetwork = netNode.Id;
}
var batteryQuery = GetEntityQuery<PowerNetworkBatteryComponent>(); var batteryQuery = GetEntityQuery<PowerNetworkBatteryComponent>();
@@ -420,17 +416,7 @@ namespace Content.Server.Power.EntitySystems
netNode.BatteryLoads.Clear(); netNode.BatteryLoads.Clear();
netNode.BatterySupplies.Clear(); netNode.BatterySupplies.Clear();
foreach (var consumer in net.Consumers) DoReconnectBasePowerNet(net, netNode);
{
netNode.Loads.Add(consumer.NetworkLoad.Id);
consumer.NetworkLoad.LinkedNetwork = netNode.Id;
}
foreach (var supplier in net.Suppliers)
{
netNode.Supplies.Add(supplier.NetworkSupply.Id);
supplier.NetworkSupply.LinkedNetwork = netNode.Id;
}
var batteryQuery = GetEntityQuery<PowerNetworkBatteryComponent>(); var batteryQuery = GetEntityQuery<PowerNetworkBatteryComponent>();
@@ -448,6 +434,22 @@ namespace Content.Server.Power.EntitySystems
battery.NetworkBattery.LinkedNetworkDischarging = netNode.Id; battery.NetworkBattery.LinkedNetworkDischarging = netNode.Id;
} }
} }
private void DoReconnectBasePowerNet<TNetType>(BasePowerNet<TNetType> net, PowerState.Network netNode)
where TNetType : IBasePowerNet
{
foreach (var consumer in net.Consumers)
{
netNode.Loads.Add(consumer.NetworkLoad.Id);
consumer.NetworkLoad.LinkedNetwork = netNode.Id;
}
foreach (var supplier in net.Suppliers)
{
netNode.Supplies.Add(supplier.NetworkSupply.Id);
supplier.NetworkSupply.LinkedNetwork = netNode.Id;
}
}
} }
/// <summary> /// <summary>

View File

@@ -1,6 +1,7 @@
using Content.Shared.Chemistry.Reagent; using Content.Server.Chemistry.Components.SolutionManager;
using Content.Shared.Whitelist; using Content.Shared.Chemistry.Reagent;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; using Content.Shared.FixedPoint;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Power.Generator; namespace Content.Server.Power.Generator;
@@ -11,14 +12,29 @@ namespace Content.Server.Power.Generator;
public sealed partial class ChemicalFuelGeneratorAdapterComponent : Component public sealed partial class ChemicalFuelGeneratorAdapterComponent : Component
{ {
/// <summary> /// <summary>
/// The acceptable list of input entities. /// The reagent to accept as fuel.
/// </summary> /// </summary>
[DataField("whitelist")] [DataField("reagent", customTypeSerializer: typeof(PrototypeIdSerializer<ReagentPrototype>))]
public EntityWhitelist? Whitelist; [ViewVariables(VVAccess.ReadWrite)]
public string Reagent = "WeldingFuel";
/// <summary> /// <summary>
/// The conversion factor for different chems you can put in. /// The solution on the <see cref="SolutionContainerManagerComponent"/> to use.
/// </summary> /// </summary>
[DataField("chemConversionFactors", required: true, customTypeSerializer:typeof(PrototypeIdDictionarySerializer<float, ReagentPrototype>))] [DataField("solution")]
public Dictionary<string, float> ChemConversionFactors = default!; [ViewVariables(VVAccess.ReadWrite)]
public string Solution = "tank";
/// <summary>
/// Value to multiply reagent amount by to get fuel amount.
/// </summary>
[DataField("multiplier"), ViewVariables(VVAccess.ReadWrite)]
public float Multiplier = 1f;
/// <summary>
/// How much reagent (can be fractional) is left in the generator.
/// Stored in units of <see cref="FixedPoint2.Epsilon"/>.
/// </summary>
[DataField("fractionalReagent"), ViewVariables(VVAccess.ReadWrite)]
public float FractionalReagent;
} }

View File

@@ -0,0 +1,28 @@
using Content.Server.Atmos;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Power.Generator;
namespace Content.Server.Power.Generator;
/// <seealso cref="GeneratorSystem"/>
/// <seealso cref="GeneratorExhaustGasComponent"/>
public sealed class GeneratorExhaustGasSystem : EntitySystem
{
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
public override void Initialize()
{
SubscribeLocalEvent<GeneratorExhaustGasComponent, GeneratorUseFuel>(FuelUsed);
}
private void FuelUsed(EntityUid uid, GeneratorExhaustGasComponent component, GeneratorUseFuel args)
{
var exhaustMixture = new GasMixture();
exhaustMixture.SetMoles(component.GasType, args.FuelUsed * component.MoleRatio);
exhaustMixture.Temperature = component.Temperature;
var environment = _atmosphere.GetContainingMixture(uid, false, true);
if (environment != null)
_atmosphere.Merge(environment, exhaustMixture);
}
}

View File

@@ -1,22 +1,29 @@
using Content.Server.Chemistry.Components.SolutionManager; using Content.Server.Audio;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Materials;
using Content.Server.Popups; using Content.Server.Popups;
using Content.Server.Power.Components; using Content.Server.Power.Components;
using Content.Shared.Chemistry.Components; using Content.Server.Power.EntitySystems;
using Content.Shared.Interaction; using Content.Shared.FixedPoint;
using Content.Shared.Materials; using Content.Shared.Popups;
using Content.Shared.Power.Generator; using Content.Shared.Power.Generator;
using Content.Shared.Stacks;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
namespace Content.Server.Power.Generator; namespace Content.Server.Power.Generator;
/// <inheritdoc/> /// <inheritdoc/>
/// <seealso cref="FuelGeneratorComponent"/>
/// <seealso cref="ChemicalFuelGeneratorAdapterComponent"/>
/// <seealso cref="SolidFuelGeneratorAdapterComponent"/>
public sealed class GeneratorSystem : SharedGeneratorSystem public sealed class GeneratorSystem : SharedGeneratorSystem
{ {
[Dependency] private readonly AppearanceSystem _appearance = default!;
[Dependency] private readonly AmbientSoundSystem _ambientSound = default!;
[Dependency] private readonly MaterialStorageSystem _materialStorage = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly SharedStackSystem _stack = default!; [Dependency] private readonly PuddleSystem _puddle = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
private EntityQuery<UpgradePowerSupplierComponent> _upgradeQuery; private EntityQuery<UpgradePowerSupplierComponent> _upgradeQuery;
@@ -24,100 +31,239 @@ public sealed class GeneratorSystem : SharedGeneratorSystem
{ {
_upgradeQuery = GetEntityQuery<UpgradePowerSupplierComponent>(); _upgradeQuery = GetEntityQuery<UpgradePowerSupplierComponent>();
SubscribeLocalEvent<SolidFuelGeneratorAdapterComponent, InteractUsingEvent>(OnSolidFuelAdapterInteractUsing); UpdatesBefore.Add(typeof(PowerNetSystem));
SubscribeLocalEvent<ChemicalFuelGeneratorAdapterComponent, InteractUsingEvent>(OnChemicalFuelAdapterInteractUsing);
SubscribeLocalEvent<FuelGeneratorComponent, SetTargetPowerMessage>(OnTargetPowerSet); SubscribeLocalEvent<FuelGeneratorComponent, PortableGeneratorSetTargetPowerMessage>(OnTargetPowerSet);
SubscribeLocalEvent<FuelGeneratorComponent, PortableGeneratorEjectFuelMessage>(OnEjectFuel);
SubscribeLocalEvent<FuelGeneratorComponent, AnchorStateChangedEvent>(OnAnchorStateChanged);
SubscribeLocalEvent<SolidFuelGeneratorAdapterComponent, GeneratorGetFuelEvent>(SolidGetFuel);
SubscribeLocalEvent<SolidFuelGeneratorAdapterComponent, GeneratorUseFuel>(SolidUseFuel);
SubscribeLocalEvent<SolidFuelGeneratorAdapterComponent, GeneratorEmpty>(SolidEmpty);
SubscribeLocalEvent<ChemicalFuelGeneratorAdapterComponent, GeneratorGetFuelEvent>(ChemicalGetFuel);
SubscribeLocalEvent<ChemicalFuelGeneratorAdapterComponent, GeneratorUseFuel>(ChemicalUseFuel);
SubscribeLocalEvent<ChemicalFuelGeneratorAdapterComponent, GeneratorGetCloggedEvent>(ChemicalGetClogged);
SubscribeLocalEvent<ChemicalFuelGeneratorAdapterComponent, GeneratorEmpty>(ChemicalEmpty);
} }
private void OnChemicalFuelAdapterInteractUsing(EntityUid uid, ChemicalFuelGeneratorAdapterComponent component, InteractUsingEvent args) private void OnAnchorStateChanged(EntityUid uid, FuelGeneratorComponent component, ref AnchorStateChangedEvent args)
{ {
if (args.Handled) // Turn off generator if unanchored while running.
if (!component.On)
return; return;
if (!TryComp<SolutionContainerManagerComponent>(args.Used, out var solutions) || SetFuelGeneratorOn(uid, false, component);
!TryComp<FuelGeneratorComponent>(uid, out var generator)) }
private void OnEjectFuel(EntityUid uid, FuelGeneratorComponent component, PortableGeneratorEjectFuelMessage args)
{
EmptyGenerator(uid);
}
private void SolidEmpty(EntityUid uid, SolidFuelGeneratorAdapterComponent component, GeneratorEmpty args)
{
_materialStorage.EjectAllMaterial(uid);
}
private void ChemicalEmpty(EntityUid uid, ChemicalFuelGeneratorAdapterComponent component, GeneratorEmpty args)
{
if (!_solutionContainer.TryGetSolution(uid, component.Solution, out var solution))
return; return;
if (!(component.Whitelist?.IsValid(args.Used) ?? true)) var spillSolution = _solutionContainer.SplitSolution(uid, solution, solution.Volume);
_puddle.TrySpillAt(uid, spillSolution, out _);
}
private void ChemicalGetClogged(EntityUid uid, ChemicalFuelGeneratorAdapterComponent component, ref GeneratorGetCloggedEvent args)
{
if (!_solutionContainer.TryGetSolution(uid, component.Solution, out var solution))
return; return;
if (TryComp<ChemicalFuelGeneratorDirectSourceComponent>(args.Used, out var source)) foreach (var reagentQuantity in solution)
{ {
if (!solutions.Solutions.ContainsKey(source.Solution)) if (reagentQuantity.ReagentId != component.Reagent)
{ {
Log.Error($"Couldn't get solution {source.Solution} on {ToPrettyString(args.Used)}"); args.Clogged = true;
return; return;
} }
var solution = solutions.Solutions[source.Solution];
generator.RemainingFuel += ReagentsToFuel(component, solution);
solution.RemoveAllSolution();
QueueDel(args.Used);
} }
} }
private float ReagentsToFuel(ChemicalFuelGeneratorAdapterComponent component, Solution solution) private void ChemicalUseFuel(EntityUid uid, ChemicalFuelGeneratorAdapterComponent component, GeneratorUseFuel args)
{ {
var total = 0.0f; if (!_solutionContainer.TryGetSolution(uid, component.Solution, out var solution))
foreach (var reagent in solution.Contents) return;
var availableReagent = solution.GetReagentQuantity(component.Reagent).Value;
var toRemove = RemoveFractionalFuel(
ref component.FractionalReagent,
args.FuelUsed,
component.Multiplier * FixedPoint2.Epsilon.Float(),
availableReagent);
solution.RemoveReagent(component.Reagent, FixedPoint2.FromCents(toRemove));
}
private void ChemicalGetFuel(
EntityUid uid,
ChemicalFuelGeneratorAdapterComponent component,
ref GeneratorGetFuelEvent args)
{
if (!_solutionContainer.TryGetSolution(uid, component.Solution, out var solution))
return;
var reagent = component.FractionalReagent * FixedPoint2.Epsilon.Float()
+ solution.GetReagentQuantity(component.Reagent).Float();
args.Fuel = reagent * component.Multiplier;
}
private void SolidUseFuel(EntityUid uid, SolidFuelGeneratorAdapterComponent component, GeneratorUseFuel args)
{
var availableMaterial = _materialStorage.GetMaterialAmount(uid, component.FuelMaterial);
var toRemove = RemoveFractionalFuel(
ref component.FractionalMaterial,
args.FuelUsed,
component.Multiplier,
availableMaterial);
_materialStorage.TryChangeMaterialAmount(uid, component.FuelMaterial, -toRemove);
}
private int RemoveFractionalFuel(ref float fractional, float fuelUsed, float multiplier, int availableQuantity)
{
fractional -= fuelUsed / multiplier;
if (fractional >= 0)
return 0;
// worst (unrealistic) case: -5.5 -> -6.0 -> 6
var toRemove = -(int) MathF.Floor(fractional);
toRemove = Math.Min(availableQuantity, toRemove);
fractional = Math.Max(0, fractional + toRemove);
return toRemove;
}
private void SolidGetFuel(
EntityUid uid,
SolidFuelGeneratorAdapterComponent component,
ref GeneratorGetFuelEvent args)
{
var material = component.FractionalMaterial + _materialStorage.GetMaterialAmount(uid, component.FuelMaterial);
args.Fuel = material * component.Multiplier;
}
private void OnTargetPowerSet(EntityUid uid, FuelGeneratorComponent component,
PortableGeneratorSetTargetPowerMessage args)
{
component.TargetPower = Math.Clamp(
args.TargetPower,
component.MinTargetPower / 1000,
component.MaxTargetPower / 1000) * 1000;
}
public void SetFuelGeneratorOn(EntityUid uid, bool on, FuelGeneratorComponent? generator = null)
{
if (!Resolve(uid, ref generator))
return;
if (on && !Transform(uid).Anchored)
{ {
if (!component.ChemConversionFactors.ContainsKey(reagent.ReagentId)) // Generator must be anchored to start.
continue; return;
total += reagent.Quantity.Float() * component.ChemConversionFactors[reagent.ReagentId];
} }
return total; generator.On = on;
} UpdateState(uid, generator);
private void OnTargetPowerSet(EntityUid uid, FuelGeneratorComponent component, SetTargetPowerMessage args)
{
component.TargetPower = Math.Clamp(args.TargetPower, 0, component.MaxTargetPower / 1000) * 1000;
}
private void OnSolidFuelAdapterInteractUsing(EntityUid uid, SolidFuelGeneratorAdapterComponent component, InteractUsingEvent args)
{
if (args.Handled)
return;
if (!TryComp<PhysicalCompositionComponent>(args.Used, out var mat) ||
!HasComp<MaterialComponent>(args.Used) ||
!TryComp<FuelGeneratorComponent>(uid, out var generator))
return;
if (!mat.MaterialComposition.ContainsKey(component.FuelMaterial))
return;
_popup.PopupEntity(Loc.GetString("generator-insert-material", ("item", args.Used), ("generator", uid)), uid);
generator.RemainingFuel += _stack.GetCount(args.Used) * component.Multiplier;
QueueDel(args.Used);
args.Handled = true;
} }
public override void Update(float frameTime) public override void Update(float frameTime)
{ {
var query = EntityQueryEnumerator<FuelGeneratorComponent, PowerSupplierComponent, TransformComponent>(); var query = EntityQueryEnumerator<FuelGeneratorComponent, PowerSupplierComponent>();
while (query.MoveNext(out var uid, out var gen, out var supplier, out var xform)) while (query.MoveNext(out var uid, out var gen, out var supplier))
{ {
supplier.Enabled = gen.RemainingFuel > 0.0f && xform.Anchored; if (!gen.On)
continue;
var fuel = GetFuel(uid);
if (fuel <= 0)
{
SetFuelGeneratorOn(uid, false, gen);
continue;
}
if (GetIsClogged(uid))
{
_popup.PopupEntity(Loc.GetString("generator-clogged", ("generator", uid)), uid, PopupType.SmallCaution);
SetFuelGeneratorOn(uid, false, gen);
continue;
}
supplier.Enabled = true;
var upgradeMultiplier = _upgradeQuery.CompOrNull(uid)?.ActualScalar ?? 1f; var upgradeMultiplier = _upgradeQuery.CompOrNull(uid)?.ActualScalar ?? 1f;
supplier.MaxSupply = gen.TargetPower * upgradeMultiplier; supplier.MaxSupply = gen.TargetPower * upgradeMultiplier;
var eff = 1 / CalcFuelEfficiency(gen.TargetPower, gen.OptimalPower, gen); var eff = 1 / CalcFuelEfficiency(gen.TargetPower, gen.OptimalPower, gen);
var consumption = gen.OptimalBurnRate * frameTime * eff;
gen.RemainingFuel = MathF.Max(gen.RemainingFuel - (gen.OptimalBurnRate * frameTime * eff), 0.0f); RaiseLocalEvent(uid, new GeneratorUseFuel(consumption));
UpdateUi(uid, gen);
} }
} }
private void UpdateUi(EntityUid uid, FuelGeneratorComponent comp) public float GetFuel(EntityUid generator)
{ {
if (!_uiSystem.IsUiOpen(uid, GeneratorComponentUiKey.Key)) GeneratorGetFuelEvent getFuelEvent = default;
return; RaiseLocalEvent(generator, ref getFuelEvent);
return getFuelEvent.Fuel;
}
_uiSystem.TrySetUiState(uid, GeneratorComponentUiKey.Key, new SolidFuelGeneratorComponentBuiState(comp)); public bool GetIsClogged(EntityUid generator)
{
GeneratorGetCloggedEvent getCloggedEvent = default;
RaiseLocalEvent(generator, ref getCloggedEvent);
return getCloggedEvent.Clogged;
}
public void EmptyGenerator(EntityUid generator)
{
RaiseLocalEvent(generator, GeneratorEmpty.Instance);
}
private void UpdateState(EntityUid generator, FuelGeneratorComponent component)
{
_appearance.SetData(generator, GeneratorVisuals.Running, component.On);
_ambientSound.SetAmbience(generator, component.On);
if (!component.On)
Comp<PowerSupplierComponent>(generator).Enabled = false;
} }
} }
/// <summary>
/// Raised by <see cref="GeneratorSystem"/> to calculate the amount of remaining fuel in the generator.
/// </summary>
[ByRefEvent]
public record struct GeneratorGetFuelEvent(float Fuel);
/// <summary>
/// Raised by <see cref="GeneratorSystem"/> to check if a generator is "clogged".
/// For example there's bad chemicals in the fuel tank that prevent starting it.
/// </summary>
[ByRefEvent]
public record struct GeneratorGetCloggedEvent(bool Clogged);
/// <summary>
/// Raised by <see cref="GeneratorSystem"/> to draw fuel from its adapters.
/// </summary>
/// <remarks>
/// Implementations are expected to round fuel consumption up if the used fuel value is too small (e.g. reagent units).
/// </remarks>
public record struct GeneratorUseFuel(float FuelUsed);
/// <summary>
/// Raised by <see cref="GeneratorSystem"/> to empty a generator of its fuel contents.
/// </summary>
public sealed class GeneratorEmpty
{
public static readonly GeneratorEmpty Instance = new();
}

View File

@@ -0,0 +1,190 @@
using Content.Server.DoAfter;
using Content.Server.Popups;
using Content.Shared.DoAfter;
using Content.Shared.Power.Generator;
using Content.Shared.Verbs;
using Robust.Server.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Power.Generator;
/// <summary>
/// Implements logic for portable generators (the PACMAN). Primarily UI & power switching behavior.
/// </summary>
/// <seealso cref="PortableGeneratorComponent"/>
public sealed class PortableGeneratorSystem : SharedPortableGeneratorSystem
{
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly DoAfterSystem _doAfter = default!;
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly GeneratorSystem _generator = default!;
[Dependency] private readonly PowerSwitchableGeneratorSystem _switchableGenerator = default!;
public override void Initialize()
{
base.Initialize();
// Update UI after main system runs.
UpdatesAfter.Add(typeof(GeneratorSystem));
SubscribeLocalEvent<PortableGeneratorComponent, GetVerbsEvent<AlternativeVerb>>(GetAlternativeVerb);
SubscribeLocalEvent<PortableGeneratorComponent, GeneratorStartedEvent>(GeneratorTugged);
SubscribeLocalEvent<PortableGeneratorComponent, PortableGeneratorStartMessage>(GeneratorStartMessage);
SubscribeLocalEvent<PortableGeneratorComponent, PortableGeneratorStopMessage>(GeneratorStopMessage);
SubscribeLocalEvent<PortableGeneratorComponent, PortableGeneratorSwitchOutputMessage>(GeneratorSwitchOutputMessage);
}
private void GeneratorSwitchOutputMessage(EntityUid uid, PortableGeneratorComponent component, PortableGeneratorSwitchOutputMessage args)
{
if (args.Session.AttachedEntity == null)
return;
var fuelGenerator = Comp<FuelGeneratorComponent>(uid);
if (fuelGenerator.On)
return;
_switchableGenerator.ToggleActiveOutput(uid, args.Session.AttachedEntity.Value);
}
private void GeneratorStopMessage(EntityUid uid, PortableGeneratorComponent component, PortableGeneratorStopMessage args)
{
if (args.Session.AttachedEntity == null)
return;
StopGenerator(uid, component, args.Session.AttachedEntity.Value);
}
private void GeneratorStartMessage(EntityUid uid, PortableGeneratorComponent component, PortableGeneratorStartMessage args)
{
if (args.Session.AttachedEntity == null)
return;
StartGenerator(uid, component, args.Session.AttachedEntity.Value);
}
private void StartGenerator(EntityUid uid, PortableGeneratorComponent component, EntityUid user)
{
var fuelGenerator = Comp<FuelGeneratorComponent>(uid);
if (fuelGenerator.On || !Transform(uid).Anchored)
return;
_doAfter.TryStartDoAfter(new DoAfterArgs(user, component.StartTime, new GeneratorStartedEvent(), uid, uid)
{
BreakOnDamage = true, BreakOnTargetMove = true, BreakOnUserMove = true, RequireCanInteract = true,
NeedHand = true
});
}
private void StopGenerator(EntityUid uid, PortableGeneratorComponent component, EntityUid user)
{
_generator.SetFuelGeneratorOn(uid, false);
}
private void GeneratorTugged(EntityUid uid, PortableGeneratorComponent component, GeneratorStartedEvent args)
{
if (args.Cancelled || !Transform(uid).Anchored)
return;
var fuelGenerator = Comp<FuelGeneratorComponent>(uid);
var empty = _generator.GetFuel(uid) == 0;
var clogged = _generator.GetIsClogged(uid);
var sound = empty ? component.StartSoundEmpty : component.StartSound;
_audio.Play(sound, Filter.Pvs(uid), uid, true);
if (!clogged && !empty && _random.Prob(component.StartChance))
{
_popup.PopupEntity(Loc.GetString("portable-generator-start-success"), uid, args.User);
_generator.SetFuelGeneratorOn(uid, true, fuelGenerator);
}
else
{
_popup.PopupEntity(Loc.GetString("portable-generator-start-fail"), uid, args.User);
// Try again bozo
args.Repeat = true;
}
}
private void GetAlternativeVerb(EntityUid uid, PortableGeneratorComponent component,
GetVerbsEvent<AlternativeVerb> args)
{
if (!args.CanAccess || !args.CanInteract)
return;
var fuelGenerator = Comp<FuelGeneratorComponent>(uid);
if (fuelGenerator.On)
{
AlternativeVerb verb = new()
{
Act = () =>
{
StopGenerator(uid, component, args.User);
},
Disabled = false,
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/zap.svg.192dpi.png")),
Text = Loc.GetString("portable-generator-verb-stop"),
};
args.Verbs.Add(verb);
}
else
{
// ReSharper disable once CompareOfFloatsByEqualityOperator
var reliable = component.StartChance == 1;
AlternativeVerb verb = new()
{
Act = () =>
{
StartGenerator(uid, component, args.User);
},
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/zap.svg.192dpi.png")),
Text = Loc.GetString("portable-generator-verb-start"),
};
if (!Transform(uid).Anchored)
{
verb.Disabled = true;
verb.Message = Loc.GetString("portable-generator-verb-start-msg-unanchored");
}
else
{
verb.Message = Loc.GetString(reliable
? "portable-generator-verb-start-msg-reliable"
: "portable-generator-verb-start-msg-unreliable");
}
args.Verbs.Add(verb);
}
}
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<PortableGeneratorComponent, FuelGeneratorComponent, AppearanceComponent>();
while (query.MoveNext(out var uid, out var portGen, out var fuelGen, out var xform))
{
UpdateUI(uid, portGen, fuelGen);
}
}
private void UpdateUI(EntityUid uid, PortableGeneratorComponent comp, FuelGeneratorComponent fuelComp)
{
if (!_uiSystem.IsUiOpen(uid, GeneratorComponentUiKey.Key))
return;
var fuel = _generator.GetFuel(uid);
var clogged = _generator.GetIsClogged(uid);
_uiSystem.TrySetUiState(
uid,
GeneratorComponentUiKey.Key,
new PortableGeneratorComponentBuiState(fuelComp, fuel, clogged));
}
}

View File

@@ -0,0 +1,111 @@
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.Popups;
using Content.Server.Power.Components;
using Content.Server.Power.Nodes;
using Content.Shared.Power.Generator;
using Content.Shared.Verbs;
using Robust.Server.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Utility;
namespace Content.Server.Power.Generator;
/// <summary>
/// Implements power-switchable generators.
/// </summary>
/// <seealso cref="PowerSwitchableGeneratorComponent"/>
/// <seealso cref="PortableGeneratorSystem"/>
/// <seealso cref="GeneratorSystem"/>
public sealed class PowerSwitchableGeneratorSystem : SharedPowerSwitchableGeneratorSystem
{
[Dependency] private readonly NodeGroupSystem _nodeGroup = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly AudioSystem _audio = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PowerSwitchableGeneratorComponent, GetVerbsEvent<InteractionVerb>>(GetInteractionVerbs);
}
private void GetInteractionVerbs(
EntityUid uid,
PowerSwitchableGeneratorComponent component,
GetVerbsEvent<InteractionVerb> args)
{
if (!args.CanAccess || !args.CanInteract)
return;
var isCurrentlyHV = component.ActiveOutput == PowerSwitchableGeneratorOutput.HV;
var msg = isCurrentlyHV ? "power-switchable-generator-verb-mv" : "power-switchable-generator-verb-hv";
var isOn = TryComp(uid, out FuelGeneratorComponent? fuelGenerator) && fuelGenerator.On;
InteractionVerb verb = new()
{
Act = () =>
{
var verbIsOn = TryComp(uid, out FuelGeneratorComponent? verbFuelGenerator) && verbFuelGenerator.On;
if (verbIsOn)
return;
ToggleActiveOutput(uid, args.User, component);
},
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/zap.svg.192dpi.png")),
Text = Loc.GetString(msg),
};
if (isOn)
{
verb.Message = Loc.GetString("power-switchable-generator-verb-disable-on");
verb.Disabled = true;
}
args.Verbs.Add(verb);
}
public void ToggleActiveOutput(EntityUid uid, EntityUid user, PowerSwitchableGeneratorComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
var supplier = Comp<PowerSupplierComponent>(uid);
var nodeContainer = Comp<NodeContainerComponent>(uid);
var outputMV = (CableDeviceNode) nodeContainer.Nodes[component.NodeOutputMV];
var outputHV = (CableDeviceNode) nodeContainer.Nodes[component.NodeOutputHV];
if (component.ActiveOutput == PowerSwitchableGeneratorOutput.HV)
{
component.ActiveOutput = PowerSwitchableGeneratorOutput.MV;
supplier.Voltage = Voltage.Medium;
// Switching around the voltage on the power supplier is "enough",
// but we also want to disconnect the cable nodes so it doesn't show up in power monitors etc.
outputMV.Enabled = true;
outputHV.Enabled = false;
}
else
{
component.ActiveOutput = PowerSwitchableGeneratorOutput.HV;
supplier.Voltage = Voltage.High;
outputMV.Enabled = false;
outputHV.Enabled = true;
}
_popup.PopupEntity(
Loc.GetString("power-switchable-generator-switched-output"),
uid,
user);
_audio.Play(component.SwitchSound, Filter.Pvs(uid), uid, true);
Dirty(uid, component);
_nodeGroup.QueueReflood(outputMV);
_nodeGroup.QueueReflood(outputHV);
}
}

View File

@@ -1,23 +1,40 @@
using Content.Shared.Materials; using Content.Shared.Materials;
using Content.Shared.Power.Generator;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Power.Generator; namespace Content.Server.Power.Generator;
/// <summary> /// <summary>
/// This is used for allowing you to insert fuel into gens. /// Fuels a <see cref="FuelGeneratorComponent"/> through solid materials.
/// </summary> /// </summary>
/// <remarks>
/// <para>
/// Must be accompanied with a <see cref="MaterialStorageComponent"/> to store the actual material and handle insertion logic.
/// You should set a whitelist there for the fuel material.
/// </para>
/// <para>
/// The component itself stores a "fractional" fuel value to allow stack materials to be gradually consumed.
/// </para>
/// </remarks>
[RegisterComponent, Access(typeof(GeneratorSystem))] [RegisterComponent, Access(typeof(GeneratorSystem))]
public sealed partial class SolidFuelGeneratorAdapterComponent : Component public sealed partial class SolidFuelGeneratorAdapterComponent : Component
{ {
/// <summary> /// <summary>
/// The material to accept as fuel. /// The material to accept as fuel.
/// </summary> /// </summary>
[DataField("fuelMaterial", customTypeSerializer: typeof(PrototypeIdSerializer<MaterialPrototype>)), ViewVariables(VVAccess.ReadWrite)] [DataField("fuelMaterial", customTypeSerializer: typeof(PrototypeIdSerializer<MaterialPrototype>))]
[ViewVariables(VVAccess.ReadWrite)]
public string FuelMaterial = "Plasma"; public string FuelMaterial = "Plasma";
/// <summary> /// <summary>
/// How much fuel that material should count for. /// How much material (can be fractional) is left in the generator.
/// </summary>
[DataField("fractionalMaterial"), ViewVariables(VVAccess.ReadWrite)]
public float FractionalMaterial;
/// <summary>
/// Value to multiply material amount by to get fuel amount.
/// </summary> /// </summary>
[DataField("multiplier"), ViewVariables(VVAccess.ReadWrite)] [DataField("multiplier"), ViewVariables(VVAccess.ReadWrite)]
public float Multiplier = 1.0f; public float Multiplier;
} }

View File

@@ -3,9 +3,7 @@ using Content.Server.NodeContainer.NodeGroups;
using Content.Server.NodeContainer.Nodes; using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Components; using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems; using Content.Server.Power.EntitySystems;
using Content.Server.Power.Pow3r;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.Map;
namespace Content.Server.Power.NodeGroups namespace Content.Server.Power.NodeGroups
{ {
@@ -24,13 +22,12 @@ namespace Content.Server.Power.NodeGroups
[NodeGroup(NodeGroupID.Apc)] [NodeGroup(NodeGroupID.Apc)]
[UsedImplicitly] [UsedImplicitly]
public sealed partial class ApcNet : BaseNetConnectorNodeGroup<IApcNet>, IApcNet public sealed partial class ApcNet : BasePowerNet<IApcNet>, IApcNet
{ {
private PowerNetSystem? _powerNetSystem; private PowerNetSystem? _powerNetSystem;
[ViewVariables] public readonly List<ApcComponent> Apcs = new(); [ViewVariables] public readonly List<ApcComponent> Apcs = new();
[ViewVariables] public readonly List<ApcPowerProviderComponent> Providers = new(); [ViewVariables] public readonly List<ApcPowerProviderComponent> Providers = new();
[ViewVariables] public readonly List<PowerConsumerComponent> Consumers = new();
//Debug property //Debug property
[ViewVariables] private int TotalReceivers => Providers.Sum(provider => provider.LinkedReceivers.Count); [ViewVariables] private int TotalReceivers => Providers.Sum(provider => provider.LinkedReceivers.Count);
@@ -39,9 +36,6 @@ namespace Content.Server.Power.NodeGroups
private IEnumerable<ApcPowerReceiverComponent> AllReceivers => private IEnumerable<ApcPowerReceiverComponent> AllReceivers =>
Providers.SelectMany(provider => provider.LinkedReceivers); Providers.SelectMany(provider => provider.LinkedReceivers);
[ViewVariables]
public PowerState.Network NetworkNode { get; } = new();
public override void Initialize(Node sourceNode, IEntityManager entMan) public override void Initialize(Node sourceNode, IEntityManager entMan)
{ {
base.Initialize(sourceNode, entMan); base.Initialize(sourceNode, entMan);
@@ -89,21 +83,7 @@ namespace Content.Server.Power.NodeGroups
QueueNetworkReconnect(); QueueNetworkReconnect();
} }
public void AddConsumer(PowerConsumerComponent consumer) public override void QueueNetworkReconnect()
{
consumer.NetworkLoad.LinkedNetwork = default;
Consumers.Add(consumer);
QueueNetworkReconnect();
}
public void RemoveConsumer(PowerConsumerComponent consumer)
{
consumer.NetworkLoad.LinkedNetwork = default;
Consumers.Remove(consumer);
QueueNetworkReconnect();
}
public void QueueNetworkReconnect()
{ {
_powerNetSystem?.QueueReconnectApcNet(this); _powerNetSystem?.QueueReconnectApcNet(this);
} }

View File

@@ -0,0 +1,51 @@
using Content.Server.Power.Components;
using Content.Server.Power.Pow3r;
using Robust.Shared.Utility;
namespace Content.Server.Power.NodeGroups;
public abstract class BasePowerNet<TNetType> : BaseNetConnectorNodeGroup<TNetType>, IBasePowerNet
where TNetType : IBasePowerNet
{
[ViewVariables] public readonly List<PowerConsumerComponent> Consumers = new();
[ViewVariables] public readonly List<PowerSupplierComponent> Suppliers = new();
[ViewVariables]
public PowerState.Network NetworkNode { get; } = new();
public void AddConsumer(PowerConsumerComponent consumer)
{
DebugTools.Assert(consumer.NetworkLoad.LinkedNetwork == default);
consumer.NetworkLoad.LinkedNetwork = default;
Consumers.Add(consumer);
QueueNetworkReconnect();
}
public void RemoveConsumer(PowerConsumerComponent consumer)
{
// Linked network can be default if it was re-connected twice in one tick.
DebugTools.Assert(consumer.NetworkLoad.LinkedNetwork == default || consumer.NetworkLoad.LinkedNetwork == NetworkNode.Id);
consumer.NetworkLoad.LinkedNetwork = default;
Consumers.Remove(consumer);
QueueNetworkReconnect();
}
public void AddSupplier(PowerSupplierComponent supplier)
{
DebugTools.Assert(supplier.NetworkSupply.LinkedNetwork == default);
supplier.NetworkSupply.LinkedNetwork = default;
Suppliers.Add(supplier);
QueueNetworkReconnect();
}
public void RemoveSupplier(PowerSupplierComponent supplier)
{
// Linked network can be default if it was re-connected twice in one tick.
DebugTools.Assert(supplier.NetworkSupply.LinkedNetwork == default || supplier.NetworkSupply.LinkedNetwork == NetworkNode.Id);
supplier.NetworkSupply.LinkedNetwork = default;
Suppliers.Remove(supplier);
QueueNetworkReconnect();
}
public abstract void QueueNetworkReconnect();
}

View File

@@ -9,6 +9,10 @@ namespace Content.Server.Power.NodeGroups
void RemoveConsumer(PowerConsumerComponent consumer); void RemoveConsumer(PowerConsumerComponent consumer);
void AddSupplier(PowerSupplierComponent supplier);
void RemoveSupplier(PowerSupplierComponent supplier);
PowerState.Network NetworkNode { get; } PowerState.Network NetworkNode { get; }
} }
} }

View File

@@ -2,7 +2,6 @@ using Content.Server.NodeContainer.NodeGroups;
using Content.Server.NodeContainer.Nodes; using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Components; using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems; using Content.Server.Power.EntitySystems;
using Content.Server.Power.Pow3r;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.Utility; using Robust.Shared.Utility;
using System.Linq; using System.Linq;
@@ -11,10 +10,6 @@ namespace Content.Server.Power.NodeGroups
{ {
public interface IPowerNet : IBasePowerNet public interface IPowerNet : IBasePowerNet
{ {
void AddSupplier(PowerSupplierComponent supplier);
void RemoveSupplier(PowerSupplierComponent supplier);
void AddDischarger(BatteryDischargerComponent discharger); void AddDischarger(BatteryDischargerComponent discharger);
void RemoveDischarger(BatteryDischargerComponent discharger); void RemoveDischarger(BatteryDischargerComponent discharger);
@@ -26,19 +21,14 @@ namespace Content.Server.Power.NodeGroups
[NodeGroup(NodeGroupID.HVPower, NodeGroupID.MVPower)] [NodeGroup(NodeGroupID.HVPower, NodeGroupID.MVPower)]
[UsedImplicitly] [UsedImplicitly]
public sealed partial class PowerNet : BaseNetConnectorNodeGroup<IPowerNet>, IPowerNet public sealed partial class PowerNet : BasePowerNet<IPowerNet>, IPowerNet
{ {
private PowerNetSystem? _powerNetSystem; private PowerNetSystem? _powerNetSystem;
private IEntityManager? _entMan; private IEntityManager? _entMan;
[ViewVariables] public readonly List<PowerSupplierComponent> Suppliers = new();
[ViewVariables] public readonly List<PowerConsumerComponent> Consumers = new();
[ViewVariables] public readonly List<BatteryChargerComponent> Chargers = new(); [ViewVariables] public readonly List<BatteryChargerComponent> Chargers = new();
[ViewVariables] public readonly List<BatteryDischargerComponent> Dischargers = new(); [ViewVariables] public readonly List<BatteryDischargerComponent> Dischargers = new();
[ViewVariables]
public PowerState.Network NetworkNode { get; } = new();
public override void Initialize(Node sourceNode, IEntityManager entMan) public override void Initialize(Node sourceNode, IEntityManager entMan)
{ {
base.Initialize(sourceNode, entMan); base.Initialize(sourceNode, entMan);
@@ -61,38 +51,6 @@ namespace Content.Server.Power.NodeGroups
netConnectorComponent.Net = this; netConnectorComponent.Net = this;
} }
public void AddSupplier(PowerSupplierComponent supplier)
{
DebugTools.Assert(supplier.NetworkSupply.LinkedNetwork == default);
supplier.NetworkSupply.LinkedNetwork = default;
Suppliers.Add(supplier);
_powerNetSystem?.QueueReconnectPowerNet(this);
}
public void RemoveSupplier(PowerSupplierComponent supplier)
{
DebugTools.Assert(supplier.NetworkSupply.LinkedNetwork == NetworkNode.Id);
supplier.NetworkSupply.LinkedNetwork = default;
Suppliers.Remove(supplier);
_powerNetSystem?.QueueReconnectPowerNet(this);
}
public void AddConsumer(PowerConsumerComponent consumer)
{
DebugTools.Assert(consumer.NetworkLoad.LinkedNetwork == default);
consumer.NetworkLoad.LinkedNetwork = default;
Consumers.Add(consumer);
_powerNetSystem?.QueueReconnectPowerNet(this);
}
public void RemoveConsumer(PowerConsumerComponent consumer)
{
DebugTools.Assert(consumer.NetworkLoad.LinkedNetwork == NetworkNode.Id);
consumer.NetworkLoad.LinkedNetwork = default;
Consumers.Remove(consumer);
_powerNetSystem?.QueueReconnectPowerNet(this);
}
public void AddDischarger(BatteryDischargerComponent discharger) public void AddDischarger(BatteryDischargerComponent discharger)
{ {
if (_entMan == null) if (_entMan == null)
@@ -102,7 +60,7 @@ namespace Content.Server.Power.NodeGroups
DebugTools.Assert(battery.NetworkBattery.LinkedNetworkDischarging == default); DebugTools.Assert(battery.NetworkBattery.LinkedNetworkDischarging == default);
battery.NetworkBattery.LinkedNetworkDischarging = default; battery.NetworkBattery.LinkedNetworkDischarging = default;
Dischargers.Add(discharger); Dischargers.Add(discharger);
_powerNetSystem?.QueueReconnectPowerNet(this); QueueNetworkReconnect();
} }
public void RemoveDischarger(BatteryDischargerComponent discharger) public void RemoveDischarger(BatteryDischargerComponent discharger)
@@ -113,12 +71,13 @@ namespace Content.Server.Power.NodeGroups
// Can be missing if the entity is being deleted, not a big deal. // Can be missing if the entity is being deleted, not a big deal.
if (_entMan.TryGetComponent(discharger.Owner, out PowerNetworkBatteryComponent? battery)) if (_entMan.TryGetComponent(discharger.Owner, out PowerNetworkBatteryComponent? battery))
{ {
DebugTools.Assert(battery.NetworkBattery.LinkedNetworkDischarging == NetworkNode.Id); // Linked network can be default if it was re-connected twice in one tick.
DebugTools.Assert(battery.NetworkBattery.LinkedNetworkDischarging == default || battery.NetworkBattery.LinkedNetworkDischarging == NetworkNode.Id);
battery.NetworkBattery.LinkedNetworkDischarging = default; battery.NetworkBattery.LinkedNetworkDischarging = default;
} }
Dischargers.Remove(discharger); Dischargers.Remove(discharger);
_powerNetSystem?.QueueReconnectPowerNet(this); QueueNetworkReconnect();
} }
public void AddCharger(BatteryChargerComponent charger) public void AddCharger(BatteryChargerComponent charger)
@@ -130,7 +89,7 @@ namespace Content.Server.Power.NodeGroups
DebugTools.Assert(battery.NetworkBattery.LinkedNetworkCharging == default); DebugTools.Assert(battery.NetworkBattery.LinkedNetworkCharging == default);
battery.NetworkBattery.LinkedNetworkCharging = default; battery.NetworkBattery.LinkedNetworkCharging = default;
Chargers.Add(charger); Chargers.Add(charger);
_powerNetSystem?.QueueReconnectPowerNet(this); QueueNetworkReconnect();
} }
public void RemoveCharger(BatteryChargerComponent charger) public void RemoveCharger(BatteryChargerComponent charger)
@@ -141,11 +100,17 @@ namespace Content.Server.Power.NodeGroups
// Can be missing if the entity is being deleted, not a big deal. // Can be missing if the entity is being deleted, not a big deal.
if (_entMan.TryGetComponent(charger.Owner, out PowerNetworkBatteryComponent? battery)) if (_entMan.TryGetComponent(charger.Owner, out PowerNetworkBatteryComponent? battery))
{ {
DebugTools.Assert(battery.NetworkBattery.LinkedNetworkCharging == NetworkNode.Id); // Linked network can be default if it was re-connected twice in one tick.
DebugTools.Assert(battery.NetworkBattery.LinkedNetworkCharging == default || battery.NetworkBattery.LinkedNetworkCharging == NetworkNode.Id);
battery.NetworkBattery.LinkedNetworkCharging = default; battery.NetworkBattery.LinkedNetworkCharging = default;
} }
Chargers.Remove(charger); Chargers.Remove(charger);
QueueNetworkReconnect();
}
public override void QueueNetworkReconnect()
{
_powerNetSystem?.QueueReconnectPowerNet(this); _powerNetSystem?.QueueReconnectPowerNet(this);
} }

View File

@@ -1,6 +1,6 @@
using Content.Server.NodeContainer; using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes; using Content.Server.NodeContainer.Nodes;
using Robust.Shared.Map;
using Robust.Shared.Map.Components; using Robust.Shared.Map.Components;
namespace Content.Server.Power.Nodes namespace Content.Server.Power.Nodes
@@ -12,6 +12,24 @@ namespace Content.Server.Power.Nodes
[Virtual] [Virtual]
public partial class CableDeviceNode : Node public partial class CableDeviceNode : Node
{ {
/// <summary>
/// If disabled, this cable device will never connect.
/// </summary>
/// <remarks>
/// If you change this,
/// you must manually call <see cref="NodeGroupSystem.QueueReflood"/> to update the node connections.
/// </remarks>
[DataField("enabled")]
public bool Enabled { get; set; } = true;
public override bool Connectable(IEntityManager entMan, TransformComponent? xform = null)
{
if (!Enabled)
return false;
return base.Connectable(entMan, xform);
}
public override IEnumerable<Node> GetReachableNodes(TransformComponent xform, public override IEnumerable<Node> GetReachableNodes(TransformComponent xform,
EntityQuery<NodeContainerComponent> nodeQuery, EntityQuery<NodeContainerComponent> nodeQuery,
EntityQuery<TransformComponent> xformQuery, EntityQuery<TransformComponent> xformQuery,

View File

@@ -1,6 +1,7 @@
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.FixedPoint namespace Content.Shared.FixedPoint
{ {
@@ -13,14 +14,23 @@ namespace Content.Shared.FixedPoint
{ {
public int Value { get; private set; } public int Value { get; private set; }
private const int Shift = 2; private const int Shift = 2;
private const int ShiftConstant = 100; // Must be equal to pow(10, Shift)
public static FixedPoint2 MaxValue { get; } = new(int.MaxValue); public static FixedPoint2 MaxValue { get; } = new(int.MaxValue);
public static FixedPoint2 Epsilon { get; } = new(1); public static FixedPoint2 Epsilon { get; } = new(1);
public static FixedPoint2 Zero { get; } = new(0); public static FixedPoint2 Zero { get; } = new(0);
#if DEBUG
static FixedPoint2()
{
// ReSharper disable once CompareOfFloatsByEqualityOperator
DebugTools.Assert(Math.Pow(10, Shift) == ShiftConstant, "ShiftConstant must be equal to pow(10, Shift)");
}
#endif
private readonly double ShiftDown() private readonly double ShiftDown()
{ {
return Value / Math.Pow(10, Shift); return Value / (double) ShiftConstant;
} }
private FixedPoint2(int value) private FixedPoint2(int value)
@@ -30,24 +40,27 @@ namespace Content.Shared.FixedPoint
public static FixedPoint2 New(int value) public static FixedPoint2 New(int value)
{ {
return new(value * (int) Math.Pow(10, Shift)); return new(value * ShiftConstant);
} }
public static FixedPoint2 FromCents(int value) => new(value); public static FixedPoint2 FromCents(int value) => new(value);
public static FixedPoint2 New(float value) public static FixedPoint2 New(float value)
{ {
return new(FromFloat(value)); return new((int) MathF.Round(value * ShiftConstant, MidpointRounding.AwayFromZero));
} }
private static int FromFloat(float value) /// <summary>
/// Create the closest <see cref="FixedPoint2"/> for a float value, always rounding up.
/// </summary>
public static FixedPoint2 NewCeiling(float value)
{ {
return (int) MathF.Round(value * MathF.Pow(10, Shift), MidpointRounding.AwayFromZero); return new((int) MathF.Ceiling(value * ShiftConstant));
} }
public static FixedPoint2 New(double value) public static FixedPoint2 New(double value)
{ {
return new((int) Math.Round(value * Math.Pow(10, Shift), MidpointRounding.AwayFromZero)); return new((int) Math.Round(value * ShiftConstant, MidpointRounding.AwayFromZero));
} }
public static FixedPoint2 New(string value) public static FixedPoint2 New(string value)
@@ -72,7 +85,7 @@ namespace Content.Shared.FixedPoint
public static FixedPoint2 operator *(FixedPoint2 a, FixedPoint2 b) public static FixedPoint2 operator *(FixedPoint2 a, FixedPoint2 b)
{ {
return new((int) MathF.Round(b.Value * a.Value / MathF.Pow(10, Shift), MidpointRounding.AwayFromZero)); return new((int) MathF.Round(b.Value * a.Value / (float) ShiftConstant, MidpointRounding.AwayFromZero));
} }
public static FixedPoint2 operator *(FixedPoint2 a, float b) public static FixedPoint2 operator *(FixedPoint2 a, float b)
@@ -92,7 +105,7 @@ namespace Content.Shared.FixedPoint
public static FixedPoint2 operator /(FixedPoint2 a, FixedPoint2 b) public static FixedPoint2 operator /(FixedPoint2 a, FixedPoint2 b)
{ {
return new((int) MathF.Round((MathF.Pow(10, Shift) * a.Value) / b.Value, MidpointRounding.AwayFromZero)); return new((int) MathF.Round((ShiftConstant * a.Value) / (float) b.Value, MidpointRounding.AwayFromZero));
} }
public static FixedPoint2 operator /(FixedPoint2 a, float b) public static FixedPoint2 operator /(FixedPoint2 a, float b)
@@ -253,7 +266,7 @@ namespace Content.Shared.FixedPoint
if (value == "MaxValue") if (value == "MaxValue")
Value = int.MaxValue; Value = int.MaxValue;
else else
Value = FromFloat(FloatFromString(value)); this = New(FloatFromString(value));
} }
public override readonly string ToString() => $"{ShiftDown().ToString(CultureInfo.InvariantCulture)}"; public override readonly string ToString() => $"{ShiftDown().ToString(CultureInfo.InvariantCulture)}";

View File

@@ -183,6 +183,29 @@ public abstract class SharedMaterialStorageSystem : EntitySystem
return true; return true;
} }
/// <summary>
/// Tries to set the amount of material in the storage to a specific value.
/// Still respects the filters in place.
/// </summary>
/// <param name="uid">The entity to change the material storage on.</param>
/// <param name="materialId">The ID of the material to change.</param>
/// <param name="volume">The stored material volume to set the storage to.</param>
/// <param name="component">The storage component on <paramref name="uid"/>. Resolved automatically if not given.</param>
/// <returns>True if it was successful (enough space etc).</returns>
public bool TrySetMaterialAmount(
EntityUid uid,
string materialId,
int volume,
MaterialStorageComponent? component = null)
{
if (!Resolve(uid, ref component))
return false;
var curAmount = GetMaterialAmount(uid, materialId, component);
var delta = volume - curAmount;
return TryChangeMaterialAmount(uid, materialId, delta, component);
}
/// <summary> /// <summary>
/// Tries to insert an entity into the material storage. /// Tries to insert an entity into the material storage.
/// </summary> /// </summary>

View File

@@ -1,19 +1,24 @@
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Power.Generator; namespace Content.Shared.Power.Generator;
/// <summary> /// <summary>
/// This is used for generators that run off some kind of fuel. /// This is used for generators that run off some kind of fuel.
/// </summary> /// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedGeneratorSystem))] /// <remarks>
/// <para>
/// Generators must be anchored to be able to run.
/// </para>
/// </remarks>
/// <seealso cref="SharedGeneratorSystem"/>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedGeneratorSystem))]
public sealed partial class FuelGeneratorComponent : Component public sealed partial class FuelGeneratorComponent : Component
{ {
/// <summary> /// <summary>
/// The amount of fuel left in the generator. /// Is the generator currently running?
/// </summary> /// </summary>
[DataField("remainingFuel"), ViewVariables(VVAccess.ReadWrite)] [DataField("on"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
public float RemainingFuel; public bool On;
/// <summary> /// <summary>
/// The generator's target power. /// The generator's target power.
@@ -27,6 +32,15 @@ public sealed partial class FuelGeneratorComponent : Component
[DataField("maxTargetPower"), ViewVariables(VVAccess.ReadWrite)] [DataField("maxTargetPower"), ViewVariables(VVAccess.ReadWrite)]
public float MaxTargetPower = 30_000.0f; public float MaxTargetPower = 30_000.0f;
/// <summary>
/// The minimum target power.
/// </summary>
/// <remarks>
/// Setting this to any value above 0 means that the generator can't idle without consuming some amount of fuel.
/// </remarks>
[DataField("minTargetPower"), ViewVariables(VVAccess.ReadWrite)]
public float MinTargetPower = 1_000;
/// <summary> /// <summary>
/// The "optimal" power at which the generator is considered to be at 100% efficiency. /// The "optimal" power at which the generator is considered to be at 100% efficiency.
/// </summary> /// </summary>
@@ -45,43 +59,3 @@ public sealed partial class FuelGeneratorComponent : Component
[DataField("fuelEfficiencyConstant")] [DataField("fuelEfficiencyConstant")]
public float FuelEfficiencyConstant = 1.3f; public float FuelEfficiencyConstant = 1.3f;
} }
/// <summary>
/// Sent to the server to adjust the targeted power level.
/// </summary>
[Serializable, NetSerializable]
public sealed class SetTargetPowerMessage : BoundUserInterfaceMessage
{
public int TargetPower;
public SetTargetPowerMessage(int targetPower)
{
TargetPower = targetPower;
}
}
/// <summary>
/// Contains network state for FuelGeneratorComponent.
/// </summary>
[Serializable, NetSerializable]
public sealed class SolidFuelGeneratorComponentBuiState : BoundUserInterfaceState
{
public float RemainingFuel;
public float TargetPower;
public float MaximumPower;
public float OptimalPower;
public SolidFuelGeneratorComponentBuiState(FuelGeneratorComponent component)
{
RemainingFuel = component.RemainingFuel;
TargetPower = component.TargetPower;
MaximumPower = component.MaxTargetPower;
OptimalPower = component.OptimalPower;
}
}
[Serializable, NetSerializable]
public enum GeneratorComponentUiKey
{
Key
}

View File

@@ -0,0 +1,33 @@
using Content.Shared.Atmos;
namespace Content.Shared.Power.Generator;
/// <summary>
/// Makes a generator emit a gas into the atmosphere when running.
/// </summary>
/// <remarks>
/// The amount of gas produced is linear with the amount of fuel used.
/// </remarks>
/// <seealso cref="SharedGeneratorSystem"/>
/// <seealso cref="FuelGeneratorComponent"/>
[RegisterComponent]
public sealed partial class GeneratorExhaustGasComponent : Component
{
/// <summary>
/// The type of gas that will be emitted by the generator.
/// </summary>
[DataField("gasType"), ViewVariables(VVAccess.ReadWrite)]
public Gas GasType = Gas.CarbonDioxide;
/// <summary>
/// The amount of moles of gas that should be produced when one unit of fuel is burned.
/// </summary>
[DataField("moleRatio"), ViewVariables(VVAccess.ReadWrite)]
public float MoleRatio = 1;
/// <summary>
/// The temperature of created gas.
/// </summary>
[DataField("temperature"), ViewVariables(VVAccess.ReadWrite)]
public float Temperature = Atmospherics.T0C + 100;
}

View File

@@ -0,0 +1,61 @@
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Power.Generator;
/// <summary>
/// Enables a generator to switch between HV and MV output.
/// </summary>
/// <remarks>
/// Must have <c>CableDeviceNode</c>s for both <see cref="NodeOutputMV"/> and <see cref="NodeOutputHV"/>, and also a <c>PowerSupplierComponent</c>.
/// </remarks>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(SharedPowerSwitchableGeneratorSystem))]
public sealed partial class PowerSwitchableGeneratorComponent : Component
{
/// <summary>
/// Which output the portable generator is currently connected to.
/// </summary>
[DataField("activeOutput")]
[AutoNetworkedField]
public PowerSwitchableGeneratorOutput ActiveOutput { get; set; }
/// <summary>
/// Sound that plays when the output is switched.
/// </summary>
/// <returns></returns>
[DataField("switchSound")]
public SoundSpecifier? SwitchSound { get; set; }
/// <summary>
/// Which node is the MV output?
/// </summary>
[DataField("nodeOutputMV")]
public string NodeOutputMV { get; set; } = "output_mv";
/// <summary>
/// Which node is the HV output?
/// </summary>
[DataField("nodeOutputHV")]
public string NodeOutputHV { get; set; } = "output_hv";
}
/// <summary>
/// Possible power output for power-switchable generators.
/// </summary>
/// <seealso cref="PowerSwitchableGeneratorComponent"/>
[Serializable, NetSerializable]
public enum PowerSwitchableGeneratorOutput : byte
{
/// <summary>
/// The generator is set to connect to a high-voltage power network.
/// </summary>
HV,
/// <summary>
/// The generator is set to connect to a medium-voltage power network.
/// </summary>
MV
}

View File

@@ -3,6 +3,7 @@
/// <summary> /// <summary>
/// This handles small, portable generators that run off a material fuel. /// This handles small, portable generators that run off a material fuel.
/// </summary> /// </summary>
/// <seealso cref="FuelGeneratorComponent"/>
public abstract class SharedGeneratorSystem : EntitySystem public abstract class SharedGeneratorSystem : EntitySystem
{ {
/// <summary> /// <summary>

View File

@@ -0,0 +1,143 @@
using Robust.Shared.Audio;
using Robust.Shared.Serialization;
namespace Content.Shared.Power.Generator;
/// <summary>
/// Responsible for power output switching &amp; UI logic on portable generators.
/// </summary>
/// <remarks>
/// A portable generator is expected to have the following components: <c>SolidFuelGeneratorAdapterComponent</c> <see cref="FuelGeneratorComponent"/>.
/// </remarks>
/// <seealso cref="SharedPortableGeneratorSystem"/>
[RegisterComponent]
[Access(typeof(SharedPortableGeneratorSystem))]
public sealed partial class PortableGeneratorComponent : Component
{
/// <summary>
/// Chance that this generator will start. If it fails, the user has to try again.
/// </summary>
[DataField("startChance")]
[ViewVariables(VVAccess.ReadWrite)]
public float StartChance { get; set; } = 1f;
/// <summary>
/// Amount of time it takes to attempt to start the generator.
/// </summary>
[DataField("startTime")]
[ViewVariables(VVAccess.ReadWrite)]
public TimeSpan StartTime { get; set; } = TimeSpan.FromSeconds(2);
/// <summary>
/// Sound that plays when attempting to start this generator.
/// </summary>
[DataField("startSound")]
[ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier? StartSound { get; set; }
/// <summary>
/// Sound that plays when attempting to start this generator.
/// Plays instead of <see cref="StartSound"/> if the generator has no fuel (dumbass).
/// </summary>
[DataField("startSoundEmpty")]
[ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier? StartSoundEmpty { get; set; }
}
/// <summary>
/// Sent to the server to adjust the targeted power level of a portable generator.
/// </summary>
[Serializable, NetSerializable]
public sealed class PortableGeneratorSetTargetPowerMessage : BoundUserInterfaceMessage
{
public int TargetPower;
public PortableGeneratorSetTargetPowerMessage(int targetPower)
{
TargetPower = targetPower;
}
}
/// <summary>
/// Sent to the server to try to start a portable generator.
/// </summary>
[Serializable, NetSerializable]
public sealed class PortableGeneratorStartMessage : BoundUserInterfaceMessage
{
}
/// <summary>
/// Sent to the server to try to stop a portable generator.
/// </summary>
[Serializable, NetSerializable]
public sealed class PortableGeneratorStopMessage : BoundUserInterfaceMessage
{
}
/// <summary>
/// Sent to the server to try to change the power output of a power-switchable portable generator.
/// </summary>
[Serializable, NetSerializable]
public sealed class PortableGeneratorSwitchOutputMessage : BoundUserInterfaceMessage
{
}
/// <summary>
/// Sent to the server to try to eject all fuel stored in a portable generator.
/// </summary>
[Serializable, NetSerializable]
public sealed class PortableGeneratorEjectFuelMessage : BoundUserInterfaceMessage
{
}
/// <summary>
/// Contains network state for the portable generator.
/// </summary>
[Serializable, NetSerializable]
public sealed class PortableGeneratorComponentBuiState : BoundUserInterfaceState
{
public float RemainingFuel;
public bool Clogged;
public float TargetPower;
public float MaximumPower;
public float OptimalPower;
public bool On;
public PortableGeneratorComponentBuiState(FuelGeneratorComponent component, float remainingFuel, bool clogged)
{
RemainingFuel = remainingFuel;
Clogged = clogged;
TargetPower = component.TargetPower;
MaximumPower = component.MaxTargetPower;
OptimalPower = component.OptimalPower;
On = component.On;
}
}
[Serializable, NetSerializable]
public enum GeneratorComponentUiKey
{
Key
}
/// <summary>
/// Sprite layers for generator prototypes.
/// </summary>
[Serializable, NetSerializable]
public enum GeneratorVisualLayers : byte
{
Body,
Unlit
}
/// <summary>
/// Appearance keys for generators.
/// </summary>
[Serializable, NetSerializable]
public enum GeneratorVisuals : byte
{
/// <summary>
/// Boolean: is the generator running?
/// </summary>
Running,
}

View File

@@ -0,0 +1,25 @@
using Content.Shared.DoAfter;
using Robust.Shared.Serialization;
namespace Content.Shared.Power.Generator;
/// <summary>
/// Shared logic for portable generators.
/// </summary>
/// <seealso cref="PortableGeneratorComponent"/>
public abstract class SharedPortableGeneratorSystem : EntitySystem
{
}
/// <summary>
/// Used to start a portable generator.
/// </summary>
/// <seealso cref="SharedPortableGeneratorSystem"/>
[Serializable, NetSerializable]
public sealed partial class GeneratorStartedEvent : DoAfterEvent
{
public override DoAfterEvent Clone()
{
return this;
}
}

View File

@@ -0,0 +1,23 @@
using Content.Shared.Examine;
namespace Content.Shared.Power.Generator;
/// <summary>
/// Shared logic for power-switchable generators.
/// </summary>
/// <seealso cref="PowerSwitchableGeneratorComponent"/>
public abstract class SharedPowerSwitchableGeneratorSystem : EntitySystem
{
public override void Initialize()
{
SubscribeLocalEvent<PowerSwitchableGeneratorComponent, ExaminedEvent>(GeneratorExamined);
}
private void GeneratorExamined(EntityUid uid, PowerSwitchableGeneratorComponent component, ExaminedEvent args)
{
// Show which output is currently selected.
args.PushMarkup(Loc.GetString(
"power-switchable-generator-examine",
("output", component.ActiveOutput.ToString())));
}
}

View File

@@ -175,5 +175,15 @@ namespace Content.Tests.Shared.Chemistry
Assert.That(parameter.Equals(comparison), Is.EqualTo(comparison.Equals(parameter))); Assert.That(parameter.Equals(comparison), Is.EqualTo(comparison.Equals(parameter)));
Assert.That(comparison.Equals(parameter), Is.EqualTo(expected)); Assert.That(comparison.Equals(parameter), Is.EqualTo(expected));
} }
[Test]
[TestCase(1.001f, "1.01")]
[TestCase(2f, "2")]
[TestCase(2.5f, "2.5")]
public void NewCeilingTest(float value, string expected)
{
var result = FixedPoint2.NewCeiling(value);
Assert.That($"{result}", Is.EqualTo(expected));
}
} }
} }

View File

@@ -32,3 +32,8 @@
license: "CC0-1.0" license: "CC0-1.0"
copyright: "Created by BasedUser#2215 on discord" copyright: "Created by BasedUser#2215 on discord"
source: "https://discord.com/channels/310555209753690112/536955542913024015/1066824680188690452" source: "https://discord.com/channels/310555209753690112/536955542913024015/1066824680188690452"
- files: ["generator-tug-1.ogg", "generator-tug-1-empty.ogg", "generator-tug-2.ogg", "generator-tug-2-empty.ogg", "generator-tug-3.ogg", "generator-tug-3-empty.ogg"]
license: "CC0-1.0"
copyright: "Modified from https://freesound.org/people/Pagey1969/sounds/566048/"
source: "https://github.com/space-wizards/ss14-raw-assets/tree/99fcffb02d9953f031991fc8f9980f0c3abd3803/Audio"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -9,6 +9,7 @@ guide-entry-networking = Networking
guide-entry-network-configurator = Network Configurator guide-entry-network-configurator = Network Configurator
guide-entry-access-configurator = Access Configurator guide-entry-access-configurator = Access Configurator
guide-entry-power = Power guide-entry-power = Power
guide-entry-portable-generator = Portable Generators
guide-entry-ame = Antimatter Engine (AME) guide-entry-ame = Antimatter Engine (AME)
guide-entry-singularity = Singularity guide-entry-singularity = Singularity
guide-entry-teg = Thermo-electric Generator (TEG) guide-entry-teg = Thermo-electric Generator (TEG)

View File

@@ -1,3 +1,3 @@
node-container-component-on-examine-details-hvpower = It has a connector for [color=orange]HV cables[/color]. node-container-component-on-examine-details-hvpower = It has a connector for [color=orange]HV cables[/color].
node-container-component-on-examine-details-mvpower = It has a connector for [color=yellow]MV cables[/color]. node-container-component-on-examine-details-mvpower = It has a connector for [color=yellow]MV cables[/color].
node-container-component-on-examine-details-apc = It has a connector for [color=green]APC cables[/color]. node-container-component-on-examine-details-apc = It has a connector for [color=green]LV cables[/color].

View File

@@ -1,7 +1,40 @@
generator-ui-title = Generator generator-clogged = {THE($generator)} shuts off abruptly!
generator-ui-target-power-label = Target Power (KW):
generator-ui-efficiency-label = Efficiency:
generator-ui-fuel-use-label = Fuel use:
generator-ui-fuel-left-label = Fuel left:
generator-insert-material = Inserted {THE($item)} into {THE($generator)}... portable-generator-verb-start = Start generator
portable-generator-verb-start-msg-unreliable = Start the generator. This may take a few tries.
portable-generator-verb-start-msg-reliable = Start the generator.
portable-generator-verb-start-msg-unanchored = The generator must be anchored first!
portable-generator-verb-stop = Stop generator
portable-generator-start-fail = You tug the cord, but it didn't start.
portable-generator-start-success = You tug the cord, and it whirrs to life.
portable-generator-ui-title = Portable Generator
portable-generator-ui-status-stopped = Stopped:
portable-generator-ui-status-starting = Starting:
portable-generator-ui-status-running = Running:
portable-generator-ui-start = Start
portable-generator-ui-stop = Stop
portable-generator-ui-target-power-label = Target Power (kW):
portable-generator-ui-efficiency-label = Efficiency:
portable-generator-ui-fuel-use-label = Fuel use:
portable-generator-ui-fuel-left-label = Fuel left:
portable-generator-ui-clogged = Contaminants detected in fuel tank!
portable-generator-ui-eject = Eject
portable-generator-ui-eta = (~{ $minutes } min)
portable-generator-ui-unanchored = Unanchored
power-switchable-generator-examine = The power output is set to { $output ->
[HV] [color=orange]HV[/color]
*[MV] [color=yellow]MV[/color]
}.
portable-generator-ui-switch-hv = Current output: HV
portable-generator-ui-switch-mv = Current output: MV
portable-generator-ui-switch-to-hv = Switch to HV
portable-generator-ui-switch-to-mv = Switch to MV
power-switchable-generator-verb-hv = Switch output to HV
power-switchable-generator-verb-mv = Switch output to MV
power-switchable-generator-verb-disable-on = Turn the generator off first!
power-switchable-generator-switched-output = Output switched!

View File

@@ -599,14 +599,14 @@
ExamineName: Woodwind Instrument ExamineName: Woodwind Instrument
- type: entity - type: entity
id: GeneratorPlasmaMachineCircuitboard id: PortableGeneratorPacmanMachineCircuitboard
parent: BaseMachineCircuitboard parent: BaseMachineCircuitboard
name: generator (plasma) machine board name: P.A.C.M.A.N.-type portable generator machine board
components: components:
- type: Sprite - type: Sprite
state: engineering state: engineering
- type: MachineBoard - type: MachineBoard
prototype: GeneratorPlasma prototype: PortableGeneratorPacman
requirements: requirements:
Capacitor: 1 Capacitor: 1
materialRequirements: materialRequirements:
@@ -643,14 +643,14 @@
Glass: 2 Glass: 2
- type: entity - type: entity
id: GeneratorUraniumMachineCircuitboard id: PortableGeneratorSuperPacmanMachineCircuitboard
parent: BaseMachineCircuitboard parent: BaseMachineCircuitboard
name: generator (uranium) machine board name: S.U.P.E.R.P.A.C.M.A.N.-type portable generator machine board
components: components:
- type: Sprite - type: Sprite
state: engineering state: engineering
- type: MachineBoard - type: MachineBoard
prototype: GeneratorUranium prototype: PortableGeneratorSuperPacman
requirements: requirements:
Capacitor: 2 Capacitor: 2
materialRequirements: materialRequirements:
@@ -661,6 +661,25 @@
chemicalComposition: chemicalComposition:
Silicon: 20 Silicon: 20
- type: entity
id: PortableGeneratorJrPacmanMachineCircuitboard
parent: BaseMachineCircuitboard
name: J.R.P.A.C.M.A.N.-type portable generator machine board
components:
- type: Sprite
state: engineering
- type: MachineBoard
prototype: PortableGeneratorJrPacman
requirements:
Capacitor: 1
materialRequirements:
Cable: 10
- type: PhysicalComposition
materialComposition:
Glass: 200
chemicalComposition:
Silicon: 20
- type: entity - type: entity
id: ReagentGrinderMachineCircuitboard id: ReagentGrinderMachineCircuitboard
parent: BaseMachineCircuitboard parent: BaseMachineCircuitboard

View File

@@ -340,8 +340,9 @@
- HonkerTargetingElectronics - HonkerTargetingElectronics
- HamtrCentralElectronics - HamtrCentralElectronics
- HamtrPeripheralsElectronics - HamtrPeripheralsElectronics
- GeneratorPlasmaMachineCircuitboard - PortableGeneratorPacmanMachineCircuitboard
- GeneratorUraniumMachineCircuitboard - PortableGeneratorSuperPacmanMachineCircuitboard
- PortableGeneratorJrPacmanMachineCircuitboard
- WallmountGeneratorElectronics - WallmountGeneratorElectronics
- WallmountGeneratorAPUElectronics - WallmountGeneratorAPUElectronics
- WallmountSubstationElectronics - WallmountSubstationElectronics

View File

@@ -175,67 +175,6 @@
- type: PowerSupplier - type: PowerSupplier
supplyRate: 15000 supplyRate: 15000
- type: entity
parent: [ BaseGenerator, ConstructibleMachine ]
id: GeneratorPlasma
suffix: Plasma, 20kW
components:
- type: PowerSupplier
- type: Sprite
sprite: Structures/Power/Generation/portable_generator.rsi
state: portgen0_1
- type: WiresPanel
- type: Wires
BoardName: "GeneratorPlasma"
LayoutId: GeneratorPlasma
- type: Machine
board: GeneratorPlasmaMachineCircuitboard
- type: UpgradePowerSupplier
powerSupplyMultiplier: 1.25
scaling: Exponential
- type: FuelGenerator
targetPower: 20000
optimalPower: 20000
- type: SolidFuelGeneratorAdapter
fuelMaterial: Plasma
- type: ActivatableUI
key: enum.GeneratorComponentUiKey.Key
- type: UserInterface
interfaces:
- key: enum.GeneratorComponentUiKey.Key
type: SolidFuelGeneratorBoundUserInterface
- type: entity
parent: [ BaseGenerator, ConstructibleMachine ]
id: GeneratorUranium
suffix: Uranium, 30kW
components:
- type: PowerSupplier
- type: Sprite
sprite: Structures/Power/Generation/portable_generator.rsi
state: portgen1_1
- type: WiresPanel
- type: Wires
BoardName: "GeneratorUranium"
LayoutId: GeneratorUranium
- type: Machine
board: GeneratorUraniumMachineCircuitboard
- type: UpgradePowerSupplier
powerSupplyMultiplier: 1.25
scaling: Exponential
- type: FuelGenerator
targetPower: 30000
optimalPower: 30000
optimalBurnRate: 0.00416666666
- type: SolidFuelGeneratorAdapter
fuelMaterial: Uranium
- type: ActivatableUI
key: enum.GeneratorComponentUiKey.Key
- type: UserInterface
interfaces:
- key: enum.GeneratorComponentUiKey.Key
type: SolidFuelGeneratorBoundUserInterface
- type: entity - type: entity
parent: BaseGeneratorWallmount parent: BaseGeneratorWallmount
id: GeneratorWallmountBasic id: GeneratorWallmountBasic

View File

@@ -0,0 +1,327 @@
#
# You can use this Desmos sheet to calculate fuel burn rate values:
# https://www.desmos.com/calculator/qcektq5dqs
#
- type: entity
abstract: true
id: PortableGeneratorBase
parent: [ BaseMachine, ConstructibleMachine ]
components:
# Basic properties
- type: Transform
anchored: False
- type: Physics
bodyType: Dynamic
- type: StaticPrice
price: 500
- type: AmbientSound
range: 5
volume: -5
sound:
path: /Audio/Ambience/Objects/engine_hum.ogg
enabled: false
- type: Fixtures
fixtures:
fix1:
shape:
!type:PhysShapeAabb
bounds: "-0.40,-0.40,0.40,0.40"
# It has wheels
density: 45
mask:
- MachineMask
layer:
- MachineLayer
# Visuals
- type: Appearance
- type: Sprite
sprite: Structures/Power/Generation/portable_generator.rsi
# Construction, interaction
- type: WiresPanel
- type: Wires
BoardName: "Generator"
LayoutId: Generator
- type: UserInterface
interfaces:
- key: enum.GeneratorComponentUiKey.Key
type: PortableGeneratorBoundUserInterface
- type: ActivatableUI
key: enum.GeneratorComponentUiKey.Key
- type: Electrified
onHandInteract: false
onInteractUsing: false
onBump: false
requirePower: true
highVoltageNode: output
- type: Destructible
thresholds:
- trigger:
!type:DamageTrigger
damage: 200
behaviors:
- !type:DoActsBehavior
acts: [ "Destruction" ]
- trigger:
!type:DamageTrigger
damage: 100
behaviors:
- !type:PlaySoundBehavior
sound:
path: /Audio/Effects/metalbreak.ogg
- !type:ChangeConstructionNodeBehavior
node: machineFrame
- !type:DoActsBehavior
acts: ["Destruction"]
- type: GuideHelp
guides: [ PortableGenerator, Power ]
# Core functionality
- type: PortableGenerator
startSoundEmpty: { collection: GeneratorTugEmpty }
startSound: { collection: GeneratorTug }
- type: FuelGenerator
- type: PowerSupplier
supplyRate: 3000
supplyRampRate: 500
supplyRampTolerance: 500
enabled: false
- type: entity
abstract: true
parent: PortableGeneratorBase
id: PortableGeneratorSwitchableBase
components:
- type: PowerSwitchableGenerator
switchSound:
path: /Audio/Machines/button.ogg
- type: NodeContainer
examinable: true
nodes:
output_hv:
!type:CableDeviceNode
nodeGroupID: HVPower
output_mv:
!type:CableDeviceNode
nodeGroupID: MVPower
enabled: false
- type: entity
name: P.A.C.M.A.N.-type portable generator
description: |-
A flexible backup generator for powering a variety of equipment.
Runs off solid plasma sheets and is rated for up to 30 kW.
parent: PortableGeneratorSwitchableBase
id: PortableGeneratorPacman
suffix: Plasma, 30 kW
components:
- type: Sprite
layers:
- state: portgen0
map: [ "enum.GeneratorVisualLayers.Body" ]
- state: portgen_on_unlit
map: [ "enum.GeneratorVisualLayers.Unlit" ]
shader: unshaded
visible: false
- type: GenericVisualizer
visuals:
enum.GeneratorVisuals.Running:
enum.GeneratorVisualLayers.Body:
True: { state: portgen0on }
False: { state: portgen0 }
enum.GeneratorVisualLayers.Unlit:
True: { visible: true }
False: { visible: false }
- type: Machine
board: PortableGeneratorPacmanMachineCircuitboard
- type: FuelGenerator
minTargetPower: 5000
maxTargetPower: 30000
targetPower: 30000
optimalPower: 30000
# 15 minutes at max output
optimalBurnRate: 0.0333333
# a decent curve that goes up to about an hour at 5 kW.
fuelEfficiencyConstant: 0.75
- type: SolidFuelGeneratorAdapter
fuelMaterial: Plasma
multiplier: 0.01
- type: MaterialStorage
storageLimit: 3000
materialWhiteList: [Plasma]
- type: PortableGenerator
startChance: 0.8
- type: UpgradePowerSupplier
powerSupplyMultiplier: 1.25
scaling: Exponential
- type: GeneratorExhaustGas
gasType: CarbonDioxide
# 2 moles of gas for every sheet of plasma.
moleRatio: 2
- type: entity
name: S.U.P.E.R.P.A.C.M.A.N.-type portable generator
description: |-
An advanced generator for powering departments.
Runs off uranium sheets and is rated for up to 50 kW.
parent: PortableGeneratorSwitchableBase
id: PortableGeneratorSuperPacman
suffix: Uranium, 50 kW
components:
- type: Sprite
layers:
- state: portgen1
map: [ "enum.GeneratorVisualLayers.Body" ]
- state: portgen_on_unlit
map: [ "enum.GeneratorVisualLayers.Unlit" ]
shader: unshaded
visible: false
- type: GenericVisualizer
visuals:
enum.GeneratorVisuals.Running:
enum.GeneratorVisualLayers.Body:
True: { state: portgen1on }
False: { state: portgen1 }
enum.GeneratorVisualLayers.Unlit:
True: { visible: true }
False: { visible: false }
- type: Machine
board: PortableGeneratorSuperPacmanMachineCircuitboard
- type: FuelGenerator
minTargetPower: 10000
maxTargetPower: 50000
targetPower: 50000
optimalPower: 50000
# 30 minutes at full power
optimalBurnRate: 0.016666666
# Barely save any fuel from reducing power output
fuelEfficiencyConstant: 0.1
- type: SolidFuelGeneratorAdapter
fuelMaterial: Uranium
multiplier: 0.01
- type: MaterialStorage
storageLimit: 3000
materialWhiteList: [Uranium]
- type: PortableGenerator
- type: UpgradePowerSupplier
powerSupplyMultiplier: 1.25
scaling: Exponential
- type: entity
name: J.R.P.A.C.M.A.N.-type portable generator
description: |-
A small generator capable of powering individual rooms, in case of emergencies.
Runs off welding fuel and is rated for up to 5 kW.
Rated ages 3 and up.
parent: PortableGeneratorBase
id: PortableGeneratorJrPacman
suffix: Welding Fuel, 5 kW
components:
- type: AmbientSound
range: 4
volume: -8
- type: Fixtures
fixtures:
fix1:
shape:
!type:PhysShapeAabb
bounds: "-0.30,-0.30,0.30,0.30"
# It has wheels
density: 30
mask:
- MachineMask
layer:
- MachineLayer
- type: Sprite
layers:
- state: portgen3
map: [ "enum.GeneratorVisualLayers.Body" ]
- state: portgen3on_unlit
map: [ "enum.GeneratorVisualLayers.Unlit" ]
shader: unshaded
visible: false
- type: GenericVisualizer
visuals:
enum.GeneratorVisuals.Running:
enum.GeneratorVisualLayers.Body:
True: { state: portgen3on }
False: { state: portgen3 }
enum.GeneratorVisualLayers.Unlit:
True: { visible: true }
False: { visible: false }
- type: Machine
board: PortableGeneratorJrPacmanMachineCircuitboard
- type: FuelGenerator
targetPower: 2000
minTargetPower: 1000
optimalPower: 5000
maxTargetPower: 5000
# 7.5 minutes at full tank.
optimalBurnRate: 0.11111111
# Shallow curve that allows you to just barely eek out 12 minutes at lowest.
fuelEfficiencyConstant: 0.3
- type: ChemicalFuelGeneratorAdapter
solution: tank
reagent: WeldingFuel
- type: SolutionContainerManager
solutions:
tank:
maxVol: 50
- type: RefillableSolution
solution: tank
- type: PortableGenerator
# Unreliable bugger
startChance: 0.5
- type: NodeContainer
examinable: true
nodes:
output:
!type:CableDeviceNode
nodeGroupID: Apc
- type: PowerSupplier
# No ramping needed on this bugger.
voltage: Apc
supplyRampTolerance: 2000
- type: GeneratorExhaustGas
gasType: CarbonDioxide
# Full tank is 25 moles of gas
moleRatio: 0.5
- type: Explosive
explosionType: Default
tileBreakScale: 0
- type: Destructible
thresholds:
- trigger:
!type:DamageTrigger
damage: 200
behaviors:
- !type:DoActsBehavior
acts: [ "Destruction" ]
- trigger:
!type:DamageTrigger
damage: 100
behaviors:
- !type:SpillBehavior
solution: tank
- !type:PlaySoundBehavior
sound:
path: /Audio/Effects/metalbreak.ogg
- !type:ChangeConstructionNodeBehavior
node: machineFrame
- !type:DoActsBehavior
acts: ["Destruction"]
- trigger:
!type:DamageTypeTrigger
damageType: Piercing
damage: 75
behaviors:
- !type:SolutionExplosionBehavior
solution: tank

View File

@@ -61,6 +61,7 @@
name: guide-entry-power name: guide-entry-power
text: "/ServerInfo/Guidebook/Engineering/Power.xml" text: "/ServerInfo/Guidebook/Engineering/Power.xml"
children: children:
- PortableGenerator
- AME - AME
- Singularity - Singularity
- TEG - TEG
@@ -79,3 +80,8 @@
id: TEG id: TEG
name: guide-entry-teg name: guide-entry-teg
text: "/ServerInfo/Guidebook/Engineering/TEG.xml" text: "/ServerInfo/Guidebook/Engineering/TEG.xml"
- type: guideEntry
id: PortableGenerator
name: guide-entry-portable-generator
text: "/ServerInfo/Guidebook/Engineering/PortableGenerator.xml"

View File

@@ -438,21 +438,29 @@
Glass: 900 Glass: 900
- type: latheRecipe - type: latheRecipe
id: GeneratorPlasmaMachineCircuitboard id: PortableGeneratorPacmanMachineCircuitboard
result: GeneratorPlasmaMachineCircuitboard result: PortableGeneratorPacmanMachineCircuitboard
completetime: 4 completetime: 4
materials: materials:
Steel: 50 Steel: 50
Glass: 350 Glass: 350
- type: latheRecipe - type: latheRecipe
id: GeneratorUraniumMachineCircuitboard id: PortableGeneratorSuperPacmanMachineCircuitboard
result: GeneratorUraniumMachineCircuitboard result: PortableGeneratorSuperPacmanMachineCircuitboard
completetime: 4 completetime: 4
materials: materials:
Steel: 50 Steel: 50
Glass: 350 Glass: 350
- type: latheRecipe
id: PortableGeneratorJrPacmanMachineCircuitboard
result: PortableGeneratorJrPacmanMachineCircuitboard
completetime: 4
materials:
Steel: 50
Glass: 350
- type: latheRecipe - type: latheRecipe
id: WallmountGeneratorElectronics id: WallmountGeneratorElectronics
result: WallmountGeneratorElectronics result: WallmountGeneratorElectronics

View File

@@ -60,13 +60,14 @@
name: research-technology-power-generation name: research-technology-power-generation
icon: icon:
sprite: Structures/Power/Generation/portable_generator.rsi sprite: Structures/Power/Generation/portable_generator.rsi
state: portgen0_1 state: portgen0
discipline: Industrial discipline: Industrial
tier: 1 tier: 1
cost: 7500 cost: 7500
recipeUnlocks: recipeUnlocks:
- GeneratorPlasmaMachineCircuitboard - PortableGeneratorPacmanMachineCircuitboard
- GeneratorUraniumMachineCircuitboard - PortableGeneratorSuperPacmanMachineCircuitboard
- PortableGeneratorJrPacmanMachineCircuitboard
- PowerComputerCircuitboard #the actual solar panel itself should be in here - PowerComputerCircuitboard #the actual solar panel itself should be in here
- SolarControlComputerCircuitboard - SolarControlComputerCircuitboard
- SolarTrackerElectronics - SolarTrackerElectronics

View File

@@ -0,0 +1,15 @@
- type: soundCollection
# Plays when you try to start a generator.
# You know, lawnmower cord pull sound.
id: GeneratorTug
files:
- /Audio/Machines/generator-tug-1.ogg
- /Audio/Machines/generator-tug-2.ogg
- /Audio/Machines/generator-tug-3.ogg
- type: soundCollection
id: GeneratorTugEmpty
files:
- /Audio/Machines/generator-tug-1-empty.ogg
- /Audio/Machines/generator-tug-2-empty.ogg
- /Audio/Machines/generator-tug-3-empty.ogg

View File

@@ -0,0 +1,42 @@
<Document>
# Portable Generators
Need power? No engine running? The "P.A.C.M.A.N." line of portable generators has you covered.
<Box>
<GuideEntityEmbed Entity="PortableGeneratorJrPacman" Caption="J.R.P.A.C.M.A.N." />
<GuideEntityEmbed Entity="PortableGeneratorPacman" Caption="P.A.C.M.A.N." />
<GuideEntityEmbed Entity="PortableGeneratorSuperPacman" Caption="S.U.P.E.R.P.A.C.M.A.N." />
</Box>
# The Junior
<Box>
<GuideEntityEmbed Entity="PortableGeneratorJrPacman" Caption="J.R.P.A.C.M.A.N." />
<GuideEntityEmbed Entity="WeldingFuelTank" />
</Box>
The J.R.P.A.C.M.A.N. can be found across the station in maintenance areas, and is ideal for crew to set up themselves whenever there are power issues.
Setup is incredibly easy: wrench it down above an [color=green]LV[/color] power cable, give it some welding fuel, and start it up.
Welding fuel should be plentiful to find around the station. In a pinch, you can even transfer some from the big tanks with a soda can. Just remember to empty the soda can first, I don't think it likes soda as fuel.
# The Big Ones
<Box>
<GuideEntityEmbed Entity="PortableGeneratorPacman" Caption="P.A.C.M.A.N." />
<GuideEntityEmbed Entity="SheetPlasma" />
</Box>
<Box>
<GuideEntityEmbed Entity="PortableGeneratorSuperPacman" Caption="S.U.P.E.R.P.A.C.M.A.N." />
<GuideEntityEmbed Entity="SheetUranium" />
</Box>
The (S.U.P.E.R.)P.A.C.M.A.N. is intended for usage by engineering for advanced power scenarios. Bootstrapping the engine, powering departments, and so on.
The S.U.P.E.R.P.A.C.M.A.N. boasts larger power output and longer runtime at maximum output, but scales down to lower outputs less efficiently.
They connect directly to [color=yellow]MV[/color] or [color=orange]HV[/color] power cables, able to switch between them for flexibility.
</Document>

View File

@@ -10,7 +10,7 @@ Shuttle construction is simple and easy, albeit rather expensive and hard to pul
<GuideEntityEmbed Entity="Gyroscope"/> <GuideEntityEmbed Entity="Gyroscope"/>
<GuideEntityEmbed Entity="ComputerShuttle"/> <GuideEntityEmbed Entity="ComputerShuttle"/>
<GuideEntityEmbed Entity="SubstationBasic"/> <GuideEntityEmbed Entity="SubstationBasic"/>
<GuideEntityEmbed Entity="GeneratorPlasma"/> <GuideEntityEmbed Entity="PortableGeneratorPacman" Caption="P.A.C.M.A.N." />
</Box> </Box>
<Box> <Box>
<GuideEntityEmbed Entity="CableHVStack"/> <GuideEntityEmbed Entity="CableHVStack"/>
@@ -37,4 +37,4 @@ From there, once you have the shape you want, bring out and install thrusters at
Finally, install the shuttle computer wherever is convenient and ensure all your thrusters and gyroscopes are receiving power. If they are, congratulations, you should have a functional shuttle! Making it livable and good looking is left as an exercise to the reader. Finally, install the shuttle computer wherever is convenient and ensure all your thrusters and gyroscopes are receiving power. If they are, congratulations, you should have a functional shuttle! Making it livable and good looking is left as an exercise to the reader.
</Document> </Document>

View File

@@ -1,100 +1,98 @@
{ {
"version": 1, "version": 1,
"license": "CC-BY-SA-3.0", "license": "CC-BY-SA-3.0",
"copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/99ed48ce8d3ba7e2d26d68519e05eae277c94685, animations modified by Peptide90", "copyright": "Taken from https://github.com/Baystation12/Baystation12/blob/caa635edb97c58301ccdc64757eba323b6673cf3/icons/obj/structures/portgen.dmi. Adapted to SS14 and portgen3 sprites made by PJB3005.",
"size": { "size": {
"x": 32, "x": 32,
"y": 32 "y": 32
},
"states": [
{
"name": "portgen0_0"
}, },
{ "states": [
"name": "portgen1_0" {
}, "name": "portgen0"
{ },
"name": "portgen2_0" {
}, "name": "portgen0on",
{ "delays": [
"name": "portgen0_1", [
"delays": [ 0.1,
[ 0.1,
0.05, 0.1,
0.1, 0.1
0.1, ]
0.1, ]
0.1, },
0.1, {
0.1, "name": "portgen1"
0.1, },
0.1, {
0.1, "name": "portgen1on",
0.1, "delays": [
0.1, [
0.1, 0.1,
0.1, 0.1,
0.1, 0.1,
0.1, 0.1
0.1, ]
0.1, ]
0.05 },
] {
] "name": "portgen1rad",
}, "delays": [
{ [
"name": "portgen1_1", 0.05,
"delays": [ 0.05,
[ 0.05,
0.05, 0.05
0.1, ]
0.1, ]
0.1, },
0.1, {
0.1, "name": "portgen2"
0.1, },
0.1, {
0.1, "name": "portgen2on",
0.1, "delays": [
0.1, [
0.1, 0.1,
0.1, 0.1,
0.1, 0.1,
0.1, 0.1
0.1, ]
0.1, ]
0.1, },
0.05 {
] "name": "portgen3"
] },
}, {
{ "name": "portgen3on",
"name": "portgen2_1", "delays": [
"delays": [ [
[ 0.1,
0.1, 0.1,
0.1, 0.1
0.1, ]
0.1, ]
0.1, },
0.1, {
0.1, "name": "portgen3on_unlit",
0.1, "delays": [
0.1, [
0.1, 0.1,
0.1, 0.1,
0.1, 0.1
0.1, ]
0.1, ]
0.1, },
0.1, {
0.1, "name": "portgen_on_unlit",
0.1, "delays": [
0.1, [
0.1, 0.1,
0.1 0.1,
] 0.1,
] 0.1
} ]
] ]
}
]
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 522 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 773 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 522 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 760 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 515 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 886 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 B

View File

@@ -81,3 +81,9 @@ SyringeSpaceacillin: null
# 2023-08-13 # 2023-08-13
AirlockPainter: SprayPainter AirlockPainter: SprayPainter
# 2023-08-19
GeneratorPlasma: PortableGeneratorPacman
GeneratorUranium: PortableGeneratorSuperPacman
GeneratorPlasmaMachineCircuitboard: PortableGeneratorPacmanMachineCircuitboard
GeneratorUraniumMachineCircuitboard: PortableGeneratorSuperPacmanMachineCircuitboard

View File

@@ -56,6 +56,7 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GC/@EntryIndexedValue">GC</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GC/@EntryIndexedValue">GC</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GD/@EntryIndexedValue">GD</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GD/@EntryIndexedValue">GD</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GL/@EntryIndexedValue">GL</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GL/@EntryIndexedValue">GL</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HV/@EntryIndexedValue">HV</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HW/@EntryIndexedValue">HW</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HW/@EntryIndexedValue">HW</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IC/@EntryIndexedValue">IC</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IC/@EntryIndexedValue">IC</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IL/@EntryIndexedValue">IL</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IL/@EntryIndexedValue">IL</s:String>
@@ -63,6 +64,7 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=KHR/@EntryIndexedValue">KHR</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=KHR/@EntryIndexedValue">KHR</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MMI/@EntryIndexedValue">MMI</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MMI/@EntryIndexedValue">MMI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MS/@EntryIndexedValue">MS</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MS/@EntryIndexedValue">MS</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MV/@EntryIndexedValue">MV</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=OGL/@EntryIndexedValue">OGL</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=OGL/@EntryIndexedValue">OGL</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=OOC/@EntryIndexedValue">OOC</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=OOC/@EntryIndexedValue">OOC</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=OS/@EntryIndexedValue">OS</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=OS/@EntryIndexedValue">OS</s:String>