Merge branch 'master' into #4219-emit-sound-system-add-single-sound-support

This commit is contained in:
Galactic Chimp
2021-07-10 10:21:40 +02:00
303 changed files with 14059 additions and 6891 deletions

View File

@@ -27,7 +27,6 @@ namespace Content.Client.AME.UI
_window.ToggleInjection.OnPressed += _ => ButtonPressed(UiButton.ToggleInjection);
_window.IncreaseFuelButton.OnPressed += _ => ButtonPressed(UiButton.IncreaseFuel);
_window.DecreaseFuelButton.OnPressed += _ => ButtonPressed(UiButton.DecreaseFuel);
_window.RefreshPartsButton.OnPressed += _ => ButtonPressed(UiButton.RefreshParts);
}
/// <summary>

View File

@@ -16,7 +16,6 @@ namespace Content.Client.AME.UI
public Button ToggleInjection { get; set; }
public Button IncreaseFuelButton { get; set; }
public Button DecreaseFuelButton { get; set; }
public Button RefreshPartsButton { get; set; }
public ProgressBar? FuelMeter { get; set; }
public Label FuelAmount { get; set; }
public Label InjectionAmount { get; set; }
@@ -85,7 +84,6 @@ namespace Content.Client.AME.UI
{
Children =
{
(RefreshPartsButton = new Button {Text = Loc.GetString("ame-window-refresh-parts-button"), StyleClasses = {StyleBase.ButtonOpenBoth }, Disabled = true }),
new Label { Text = Loc.GetString("ame-window-core-count-label")},
(CoreCount = new Label { Text = "0"}),
}
@@ -159,8 +157,6 @@ namespace Content.Client.AME.UI
InjectionStatus.Text = Loc.GetString("ame-window-engine-injection-status-injecting-label");
}
RefreshPartsButton.Disabled = castState.Injecting;
CoreCount.Text = $"{castState.CoreCount}";
InjectionAmount.Text = $"{castState.InjectionAmount}";
}

View File

@@ -13,7 +13,7 @@ using Robust.Shared.Maths;
namespace Content.Client.Atmos.EntitySystems
{
[UsedImplicitly]
internal sealed class AtmosDebugOverlaySystem : SharedAtmosDebugOverlaySystem, IResettingEntitySystem
internal sealed class AtmosDebugOverlaySystem : SharedAtmosDebugOverlaySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
@@ -37,6 +37,7 @@ namespace Content.Client.Atmos.EntitySystems
{
base.Initialize();
SubscribeNetworkEvent<RoundRestartCleanupEvent>(Reset);
SubscribeNetworkEvent<AtmosDebugOverlayMessage>(HandleAtmosDebugOverlayMessage);
SubscribeNetworkEvent<AtmosDebugOverlayDisableMessage>(HandleAtmosDebugOverlayDisableMessage);
@@ -66,7 +67,7 @@ namespace Content.Client.Atmos.EntitySystems
overlayManager.RemoveOverlay<GasTileOverlay>();
}
public void Reset()
public void Reset(RoundRestartCleanupEvent ev)
{
_tileData.Clear();
}

View File

@@ -585,6 +585,10 @@ namespace Content.Client.Chat.Managers
private void EnqueueSpeechBubble(IEntity entity, string contents, SpeechBubble.SpeechType speechType)
{
// Don't enqueue speech bubbles for other maps. TODO: Support multiple viewports/maps?
if (entity.Transform.MapID != _eyeManager.CurrentMap)
return;
if (!_queuedSpeechBubbles.TryGetValue(entity.Uid, out var queueData))
{
queueData = new SpeechBubbleQueueData();
@@ -600,8 +604,7 @@ namespace Content.Client.Chat.Managers
private void CreateSpeechBubble(IEntity entity, SpeechBubbleData speechData)
{
var bubble =
SpeechBubble.CreateSpeechBubble(speechData.Type, speechData.Message, entity, _eyeManager, this);
var bubble = SpeechBubble.CreateSpeechBubble(speechData.Type, speechData.Message, entity, _eyeManager, this);
if (_activeSpeechBubbles.TryGetValue(entity.Uid, out var existing))
{

View File

@@ -185,10 +185,10 @@ namespace Content.Client.Chat.UI
{
Children =
{
new Control{MinSize = (10,0)},
new Control{MinSize = (4,0)},
(_filterVBox = new VBoxContainer
{
SeparationOverride = 10
SeparationOverride = 4
})
}
}
@@ -203,7 +203,7 @@ namespace Content.Client.Chat.UI
{
(_channelSelectorHBox = new HBoxContainer
{
SeparationOverride = 4
SeparationOverride = 1
})
}
};

View File

@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=power_005Cvisualizers/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@@ -1,5 +1,6 @@
using System;
using Content.Client.Wires;
using Content.Client.Wires.Visualizers;
using Content.Shared.Audio;
using Content.Shared.Doors;
using JetBrains.Annotations;

View File

@@ -55,7 +55,7 @@ namespace Content.Client.Entry
"AccessReader",
"IdCardConsole",
"Airlock",
"WirePlacer",
"CablePlacer",
"Drink",
"Food",
"FoodContainer",
@@ -73,6 +73,7 @@ namespace Content.Client.Entry
"StorageFill",
"Mop",
"Bucket",
"CableVis",
"Puddle",
"CanSpill",
"SpeedLoader",
@@ -105,12 +106,11 @@ namespace Content.Client.Entry
"PowerSupplier",
"PowerConsumer",
"Battery",
"BatteryStorage",
"BatteryDischarger",
"Apc",
"PowerProvider",
"PowerReceiver",
"Wire",
"ApcPowerReceiver",
"Cable",
"StressTestMovement",
"Toys",
"SurgeryTool",
@@ -273,6 +273,8 @@ namespace Content.Client.Entry
"ExplosionLaunched",
"BeingCloned",
"Advertise",
"PowerNetworkBattery",
"BatteryCharger",
};
}
}

View File

@@ -13,7 +13,7 @@ using Robust.Shared.IoC;
namespace Content.Client.HealthOverlay
{
[UsedImplicitly]
public class HealthOverlaySystem : EntitySystem, IResettingEntitySystem
public class HealthOverlaySystem : EntitySystem
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
@@ -44,10 +44,11 @@ namespace Content.Client.HealthOverlay
{
base.Initialize();
SubscribeNetworkEvent<RoundRestartCleanupEvent>(Reset);
SubscribeLocalEvent<PlayerAttachSysMessage>(HandlePlayerAttached);
}
public void Reset()
public void Reset(RoundRestartCleanupEvent ev)
{
foreach (var gui in _guis.Values)
{

View File

@@ -0,0 +1,23 @@
using Content.Shared.Light.Component;
using Robust.Client.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Client.Light.Visualizers
{
[DataDefinition]
public sealed class EmergencyLightVisualizer : AppearanceVisualizer
{
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
if (!component.Owner.TryGetComponent(out SpriteComponent? sprite))
return;
if (!component.TryGetData(EmergencyLightVisuals.On, out bool on))
on = false;
sprite.LayerSetState(0, on ? "emergency_light_on" : "emergency_light_off");
}
}
}

View File

@@ -1,4 +1,5 @@
using Content.Shared.MobState;
using Content.Shared.MobState.Components;
using Content.Shared.MobState.State;
using Robust.Shared.GameObjects;

View File

@@ -0,0 +1,99 @@
using System.Collections.Generic;
using System.Linq;
using Content.Shared.NodeContainer;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.ResourceManagement;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
namespace Content.Client.NodeContainer
{
[UsedImplicitly]
public sealed class NodeGroupSystem : EntitySystem
{
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IEntityLookup _entityLookup = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
public bool VisEnabled { get; private set; }
public Dictionary<int, NodeVis.GroupData> Groups { get; } = new();
public HashSet<string> Filtered { get; } = new();
public Dictionary<EntityUid, (NodeVis.GroupData group, NodeVis.NodeDatum node)[]>
Entities { get; private set; } = new();
public Dictionary<(int group, int node), NodeVis.NodeDatum> NodeLookup { get; private set; } = new();
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<NodeVis.MsgData>(DataMsgHandler);
}
public override void Shutdown()
{
base.Shutdown();
_overlayManager.RemoveOverlay<NodeVisualizationOverlay>();
}
private void DataMsgHandler(NodeVis.MsgData ev)
{
if (!VisEnabled)
return;
foreach (var deletion in ev.GroupDeletions)
{
Groups.Remove(deletion);
}
foreach (var group in ev.Groups)
{
Groups.Add(group.NetId, group);
}
Entities = Groups.Values
.SelectMany(g => g.Nodes, (data, nodeData) => (data, nodeData))
.GroupBy(n => n.nodeData.Entity)
.ToDictionary(g => g.Key, g => g.ToArray());
NodeLookup = Groups.Values
.SelectMany(g => g.Nodes, (data, nodeData) => (data, nodeData))
.ToDictionary(n => (n.data.NetId, n.nodeData.NetId), n => n.nodeData);
}
public void SetVisEnabled(bool enabled)
{
VisEnabled = enabled;
RaiseNetworkEvent(new NodeVis.MsgEnable(enabled));
if (enabled)
{
var overlay = new NodeVisualizationOverlay(
this,
_entityLookup,
_mapManager,
_inputManager,
_eyeManager,
_resourceCache,
EntityManager);
_overlayManager.AddOverlay(overlay);
}
else
{
Groups.Clear();
Entities.Clear();
}
}
}
}

View File

@@ -0,0 +1,56 @@
using Content.Client.Administration.Managers;
using Content.Shared.Administration;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Content.Client.NodeContainer
{
public sealed class NodeVisCommand : IConsoleCommand
{
public string Command => "nodevis";
public string Description => "Toggles node group visualization";
public string Help => "";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var adminMan = IoCManager.Resolve<IClientAdminManager>();
if (!adminMan.HasFlag(AdminFlags.Debug))
{
shell.WriteError("You need +DEBUG for this command");
return;
}
var sys = EntitySystem.Get<NodeGroupSystem>();
sys.SetVisEnabled(!sys.VisEnabled);
}
}
public sealed class NodeVisFilterCommand : IConsoleCommand
{
public string Command => "nodevisfilter";
public string Description => "Toggles showing a specific group on nodevis";
public string Help => "Usage: nodevis [filter]\nOmit filter to list currently masked-off";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var sys = EntitySystem.Get<NodeGroupSystem>();
if (args.Length == 0)
{
foreach (var filtered in sys.Filtered)
{
shell.WriteLine(filtered);
}
}
else
{
var filter = args[0];
if (!sys.Filtered.Add(filter))
{
sys.Filtered.Remove(filter);
}
}
}
}
}

View File

@@ -0,0 +1,232 @@
using System;
using System.Collections.Generic;
using System.Text;
using Content.Client.Resources;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Enums;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using static Content.Shared.NodeContainer.NodeVis;
namespace Content.Client.NodeContainer
{
public sealed class NodeVisualizationOverlay : Overlay
{
private readonly NodeGroupSystem _system;
private readonly IEntityLookup _lookup;
private readonly IMapManager _mapManager;
private readonly IInputManager _inputManager;
private readonly IEyeManager _eyeManager;
private readonly IEntityManager _entityManager;
private readonly Dictionary<(int, int), NodeRenderData> _nodeIndex = new();
private readonly Dictionary<GridId, Dictionary<Vector2i, List<(GroupData, NodeDatum)>>> _gridIndex = new ();
private readonly Font _font;
private (int group, int node)? _hovered;
private float _time;
public override OverlaySpace Space => OverlaySpace.ScreenSpace | OverlaySpace.WorldSpace;
public NodeVisualizationOverlay(
NodeGroupSystem system,
IEntityLookup lookup,
IMapManager mapManager,
IInputManager inputManager,
IEyeManager eyeManager,
IResourceCache cache,
IEntityManager entityManager)
{
_system = system;
_lookup = lookup;
_mapManager = mapManager;
_inputManager = inputManager;
_eyeManager = eyeManager;
_entityManager = entityManager;
_font = cache.GetFont("/Fonts/NotoSans/NotoSans-Regular.ttf", 12);
}
protected override void Draw(in OverlayDrawArgs args)
{
if ((args.Space & OverlaySpace.WorldSpace) != 0)
{
DrawWorld(args);
}
else if ((args.Space & OverlaySpace.ScreenSpace) != 0)
{
DrawScreen(args);
}
}
private void DrawScreen(in OverlayDrawArgs args)
{
if (_hovered == null)
return;
var (groupId, nodeId) = _hovered.Value;
var group = _system.Groups[groupId];
var node = _system.NodeLookup[(groupId, nodeId)];
var mousePos = _inputManager.MouseScreenPosition.Position;
var entity = _entityManager.GetEntity(node.Entity);
var gridId = entity.Transform.GridID;
var grid = _mapManager.GetGrid(gridId);
var gridTile = grid.TileIndicesFor(entity.Transform.Coordinates);
var sb = new StringBuilder();
sb.Append($"entity: {entity}\n");
sb.Append($"group id: {group.GroupId}\n");
sb.Append($"node: {node.Name}\n");
sb.Append($"type: {node.Type}\n");
sb.Append($"grid pos: {gridTile}\n");
args.ScreenHandle.DrawString(_font, mousePos + (20, -20), sb.ToString());
}
private void DrawWorld(in OverlayDrawArgs overlayDrawArgs)
{
const float nodeSize = 8f / 32;
const float nodeOffset = 6f / 32;
var handle = overlayDrawArgs.WorldHandle;
var map = overlayDrawArgs.Viewport.Eye?.Position.MapId ?? default;
if (map == MapId.Nullspace)
return;
var mouseScreenPos = _inputManager.MouseScreenPosition;
var mouseWorldPos = _eyeManager.ScreenToMap(mouseScreenPos).Position;
_hovered = default;
var cursorBox = Box2.CenteredAround(mouseWorldPos, (nodeSize, nodeSize));
// Group visible nodes by grid tiles.
var worldBounds = overlayDrawArgs.WorldBounds;
_lookup.FastEntitiesIntersecting(map, ref worldBounds, entity =>
{
if (!_system.Entities.TryGetValue(entity.Uid, out var nodeData))
return;
var gridId = entity.Transform.GridID;
var grid = _mapManager.GetGrid(gridId);
var gridDict = _gridIndex.GetOrNew(gridId);
var coords = entity.Transform.Coordinates;
// TODO: This probably shouldn't be capable of returning NaN...
if (float.IsNaN(coords.Position.X) || float.IsNaN(coords.Position.Y))
return;
var tile = gridDict.GetOrNew(grid.TileIndicesFor(coords));
foreach (var (group, nodeDatum) in nodeData)
{
if (!_system.Filtered.Contains(group.GroupId))
{
tile.Add((group, nodeDatum));
}
}
});
foreach (var (gridId, gridDict) in _gridIndex)
{
var grid = _mapManager.GetGrid(gridId);
foreach (var (pos, list) in gridDict)
{
var centerPos = grid.GridTileToWorld(pos).Position;
list.Sort(NodeDisplayComparer.Instance);
var offset = -(list.Count - 1) * nodeOffset / 2;
foreach (var (group, node) in list)
{
var nodePos = centerPos + (offset, offset);
if (cursorBox.Contains(nodePos))
_hovered = (group.NetId, node.NetId);
_nodeIndex[(group.NetId, node.NetId)] = new NodeRenderData(group, node, nodePos);
offset += nodeOffset;
}
}
}
foreach (var nodeRenderData in _nodeIndex.Values)
{
var pos = nodeRenderData.WorldPos;
var bounds = Box2.CenteredAround(pos, (nodeSize, nodeSize));
var groupData = nodeRenderData.GroupData;
var color = groupData.Color;
if (!_hovered.HasValue)
color.A = 0.5f;
else if (_hovered.Value.group != groupData.NetId)
color.A = 0.2f;
else
color.A = 0.75f + MathF.Sin(_time * 4) * 0.25f;
handle.DrawRect(bounds, color);
foreach (var reachable in nodeRenderData.NodeDatum.Reachable)
{
if (_nodeIndex.TryGetValue((groupData.NetId, reachable), out var reachDat))
{
handle.DrawLine(pos, reachDat.WorldPos, color);
}
}
}
_nodeIndex.Clear();
_gridIndex.Clear();
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
_time += args.DeltaSeconds;
}
private sealed class NodeDisplayComparer : IComparer<(GroupData, NodeDatum)>
{
public static readonly NodeDisplayComparer Instance = new();
public int Compare((GroupData, NodeDatum) x, (GroupData, NodeDatum) y)
{
var (groupX, nodeX) = x;
var (groupY, nodeY) = y;
var cmp = groupX.NetId.CompareTo(groupY.NetId);
if (cmp != 0)
return cmp;
return nodeX.NetId.CompareTo(nodeY.NetId);
}
}
private sealed class NodeRenderData
{
public GroupData GroupData;
public NodeDatum NodeDatum;
public Vector2 WorldPos;
public NodeRenderData(GroupData groupData, NodeDatum nodeDatum, Vector2 worldPos)
{
GroupData = groupData;
NodeDatum = nodeDatum;
WorldPos = worldPos;
}
}
}
}

View File

@@ -9,7 +9,7 @@ using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
namespace Content.Client.APC
namespace Content.Client.Power.APC
{
[UsedImplicitly]
public class ApcBoundUserInterface : BoundUserInterface

View File

@@ -3,7 +3,7 @@ using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
namespace Content.Client.APC
namespace Content.Client.Power.APC
{
public class ApcVisualizer : AppearanceVisualizer
{

View File

@@ -4,7 +4,7 @@ using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
namespace Content.Client.SMES
namespace Content.Client.Power.SMES
{
[UsedImplicitly]
public class SmesVisualizer : AppearanceVisualizer

View File

@@ -0,0 +1,26 @@
using Content.Shared.Wires;
using Robust.Client.GameObjects;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Client.Power
{
[DataDefinition]
public sealed class CableVisualizer : AppearanceVisualizer
{
[DataField("base")]
public string? StateBase;
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
if (!component.Owner.TryGetComponent(out SpriteComponent? sprite))
return;
if (!component.TryGetData(WireVisVisuals.ConnectedMask, out WireVisDirFlags mask))
mask = WireVisDirFlags.None;
sprite.LayerSetState(0, $"{StateBase}{(int) mask}");
}
}
}

View File

@@ -66,7 +66,7 @@ namespace Content.Client.Stack
///
/// <list type="bullet">
/// <item>
/// <description>false: they are opaque and mutually exclusive (e.g. sprites in a wire coil). <b>Default value</b></description>
/// <description>false: they are opaque and mutually exclusive (e.g. sprites in a cable coil). <b>Default value</b></description>
/// </item>
/// <item>
/// <description>true: they are transparent and thus layered one over another in ascending order first</description>

View File

@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using System.Linq;
using Content.Client.Actions.UI;
using Content.Client.Examine;

View File

@@ -28,7 +28,7 @@ using Timer = Robust.Shared.Timing.Timer;
namespace Content.Client.Verbs
{
[UsedImplicitly]
public sealed class VerbSystem : SharedVerbSystem, IResettingEntitySystem
public sealed class VerbSystem : SharedVerbSystem
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
@@ -47,6 +47,7 @@ namespace Content.Client.Verbs
{
base.Initialize();
SubscribeNetworkEvent<RoundRestartCleanupEvent>(Reset);
SubscribeNetworkEvent<VerbSystemMessages.VerbsResponseMessage>(FillEntityPopup);
SubscribeNetworkEvent<PlayerContainerVisibilityMessage>(HandleContainerVisibilityMessage);
@@ -68,7 +69,7 @@ namespace Content.Client.Verbs
CommandBinds.Unregister<VerbSystem>();
}
public void Reset()
public void Reset(RoundRestartCleanupEvent ev)
{
ToggleContainerVisibility?.Invoke(this, false);
}

View File

@@ -1,7 +1,7 @@
using Robust.Client.GameObjects;
using static Content.Shared.Wires.SharedWiresComponent;
namespace Content.Client.Wires
namespace Content.Client.Wires.Visualizers
{
public class WiresVisualizer : AppearanceVisualizer
{

View File

@@ -7,6 +7,8 @@ using Content.Server.IoC;
using Content.Shared.CCVar;
using Moq;
using NUnit.Framework;
using Robust.Client;
using Robust.Server;
using Robust.Server.Maps;
using Robust.Shared;
using Robust.Shared.ContentPack;
@@ -31,6 +33,13 @@ namespace Content.IntegrationTests
FailureLogLevel = LogLevel.Warning
};
// Load content resources, but not config and user data.
options.Options = new GameControllerOptions()
{
LoadContentResources = true,
LoadConfigAndUserData = false,
};
options.ContentStart = true;
options.ContentAssemblies = new[]
@@ -57,13 +66,6 @@ namespace Content.IntegrationTests
});
};
// Connecting to Discord is a massive waste of time.
// Basically just makes the CI logs a mess.
options.CVarOverrides[CVars.DiscordEnabled.Name] = "false";
// Avoid preloading textures in tests.
options.CVarOverrides.TryAdd(CVars.TexturePreloadingEnabled.Name, "false");
return base.StartClient(options);
}
@@ -74,6 +76,13 @@ namespace Content.IntegrationTests
FailureLogLevel = LogLevel.Warning
};
// Load content resources, but not config and user data.
options.Options = new ServerOptions()
{
LoadConfigAndUserData = false,
LoadContentResources = true,
};
options.ContentStart = true;
options.ContentAssemblies = new[]
@@ -105,7 +114,8 @@ namespace Content.IntegrationTests
// Disable holidays as some of them might mess with the map at round start.
options.CVarOverrides[CCVars.HolidaysEnabled.Name] = "false";
// Avoid loading a large map by default for integration tests.
// Avoid loading a large map by default for integration tests if none has been specified.
if(!options.CVarOverrides.ContainsKey(CCVars.GameMap.Name))
options.CVarOverrides[CCVars.GameMap.Name] = "Maps/Test/empty.yml";
return base.StartServer(options);
@@ -113,7 +123,14 @@ namespace Content.IntegrationTests
protected ServerIntegrationInstance StartServerDummyTicker(ServerIntegrationOptions options = null)
{
options ??= new ServerIntegrationOptions();
options ??= new ServerContentIntegrationOption();
// Load content resources, but not config and user data.
options.Options = new ServerOptions()
{
LoadConfigAndUserData = false,
LoadContentResources = true,
};
// Dummy game ticker.
options.CVarOverrides[CCVars.GameDummyTicker.Name] = "true";
@@ -229,6 +246,12 @@ namespace Content.IntegrationTests
FailureLogLevel = LogLevel.Warning;
}
public override GameControllerOptions Options { get; set; } = new()
{
LoadContentResources = true,
LoadConfigAndUserData = false,
};
public Action ContentBeforeIoC { get; set; }
}
@@ -239,6 +262,12 @@ namespace Content.IntegrationTests
FailureLogLevel = LogLevel.Warning;
}
public override ServerOptions Options { get; set; } = new()
{
LoadContentResources = true,
LoadConfigAndUserData = false,
};
public Action ContentBeforeIoC { get; set; }
}
}

View File

@@ -20,7 +20,13 @@ namespace Content.IntegrationTests.Tests.Commands
[TestCase(false)]
public async Task RestartRoundAfterStart(bool lobbyEnabled)
{
var (_, server) = await StartConnectedServerClientPair();
var (_, server) = await StartConnectedServerClientPair(serverOptions: new ServerContentIntegrationOption
{
CVarOverrides =
{
[CCVars.GameMap.Name] = "Maps/saltern.yml"
}
});
await server.WaitIdleAsync();

View File

@@ -85,7 +85,7 @@ namespace Content.IntegrationTests.Tests.Disposal
components:
- type: DisposalUnit
- type: Anchorable
- type: PowerReceiver
- type: ApcPowerReceiver
- type: Physics
bodyType: Static
@@ -155,7 +155,7 @@ namespace Content.IntegrationTests.Tests.Disposal
Flush(unit, false, human, wrench);
// Remove power need
Assert.True(disposalUnit.TryGetComponent(out PowerReceiverComponent? power));
Assert.True(disposalUnit.TryGetComponent(out ApcPowerReceiverComponent? power));
power!.NeedsPower = false;
Assert.True(unit.Powered);

View File

@@ -39,7 +39,7 @@ namespace Content.IntegrationTests.Tests.DoAfter
var mob = entityManager.SpawnEntity("Dummy", MapCoordinates.Nullspace);
var cancelToken = new CancellationTokenSource();
var args = new DoAfterEventArgs(mob, tickTime / 2, cancelToken.Token);
task = EntitySystem.Get<DoAfterSystem>().DoAfter(args);
task = EntitySystem.Get<DoAfterSystem>().WaitDoAfter(args);
});
await server.WaitRunTicks(1);
@@ -62,7 +62,7 @@ namespace Content.IntegrationTests.Tests.DoAfter
var mob = entityManager.SpawnEntity("Dummy", MapCoordinates.Nullspace);
var cancelToken = new CancellationTokenSource();
var args = new DoAfterEventArgs(mob, tickTime * 2, cancelToken.Token);
task = EntitySystem.Get<DoAfterSystem>().DoAfter(args);
task = EntitySystem.Get<DoAfterSystem>().WaitDoAfter(args);
cancelToken.Cancel();
});

View File

@@ -2,8 +2,9 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Content.Server.Battery.Components;
using Content.Server.Power.Components;
using Content.Server.PowerCell.Components;
using Content.Shared.CCVar;
using Content.Shared.Coordinates;
using NUnit.Framework;
using Robust.Shared.GameObjects;
@@ -21,7 +22,12 @@ namespace Content.IntegrationTests.Tests
[Test]
public async Task SpawnTest()
{
var server = StartServerDummyTicker();
var options = new ServerContentIntegrationOption()
{
CVarOverrides = {{CCVars.AIMaxUpdates.Name, int.MaxValue.ToString()}}
};
var server = StartServerDummyTicker(options);
await server.WaitIdleAsync();
var mapManager = server.ResolveDependency<IMapManager>();

View File

@@ -22,7 +22,7 @@ namespace Content.IntegrationTests.Tests
id: GravityGeneratorDummy
components:
- type: GravityGenerator
- type: PowerReceiver
- type: ApcPowerReceiver
";
[Test]
public async Task Test()
@@ -48,9 +48,9 @@ namespace Content.IntegrationTests.Tests
generator = entityMan.SpawnEntity("GravityGeneratorDummy", grid2.ToCoordinates());
Assert.That(generator.HasComponent<GravityGeneratorComponent>());
Assert.That(generator.HasComponent<PowerReceiverComponent>());
Assert.That(generator.HasComponent<ApcPowerReceiverComponent>());
var generatorComponent = generator.GetComponent<GravityGeneratorComponent>();
var powerComponent = generator.GetComponent<PowerReceiverComponent>();
var powerComponent = generator.GetComponent<ApcPowerReceiverComponent>();
Assert.That(generatorComponent.Status, Is.EqualTo(GravityGeneratorStatus.Unpowered));
powerComponent.NeedsPower = false;
});

View File

@@ -0,0 +1,996 @@
#nullable enable
using System.Threading.Tasks;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Components;
using Content.Server.Power.Nodes;
using Content.Shared.Coordinates;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
namespace Content.IntegrationTests.Tests.Power
{
[Parallelizable(ParallelScope.Fixtures)]
[TestFixture]
public class PowerTest : ContentIntegrationTest
{
private const string Prototypes = @"
- type: entity
id: GeneratorDummy
components:
- type: NodeContainer
nodes:
output:
!type:CableDeviceNode
nodeGroupID: HVPower
- type: PowerSupplier
- type: Transform
anchored: true
- type: entity
id: ConsumerDummy
components:
- type: Transform
anchored: true
- type: NodeContainer
nodes:
input:
!type:CableDeviceNode
nodeGroupID: HVPower
- type: PowerConsumer
- type: entity
id: ChargingBatteryDummy
components:
- type: Transform
anchored: true
- type: NodeContainer
nodes:
output:
!type:CableDeviceNode
nodeGroupID: HVPower
- type: PowerNetworkBattery
- type: Battery
- type: BatteryCharger
- type: entity
id: DischargingBatteryDummy
components:
- type: Transform
anchored: true
- type: NodeContainer
nodes:
output:
!type:CableDeviceNode
nodeGroupID: HVPower
- type: PowerNetworkBattery
- type: Battery
- type: BatteryDischarger
- type: entity
id: FullBatteryDummy
components:
- type: Transform
anchored: true
- type: NodeContainer
nodes:
output:
!type:CableDeviceNode
nodeGroupID: HVPower
input:
!type:CableTerminalPortNode
nodeGroupID: HVPower
- type: PowerNetworkBattery
- type: Battery
- type: BatteryDischarger
node: output
- type: BatteryCharger
node: input
- type: entity
id: SubstationDummy
components:
- type: NodeContainer
nodes:
input:
!type:CableDeviceNode
nodeGroupID: HVPower
output:
!type:CableDeviceNode
nodeGroupID: MVPower
- type: BatteryCharger
voltage: High
- type: BatteryDischarger
voltage: Medium
- type: PowerNetworkBattery
maxChargeRate: 1000
maxSupply: 1000
supplyRampTolerance: 1000
- type: Battery
maxCharge: 1000
startingCharge: 1000
- type: Transform
anchored: true
- type: entity
id: ApcDummy
components:
- type: Battery
maxCharge: 10000
startingCharge: 10000
- type: PowerNetworkBattery
maxChargeRate: 1000
maxSupply: 1000
supplyRampTolerance: 1000
- type: BatteryCharger
voltage: Medium
- type: BatteryDischarger
voltage: Apc
- type: Apc
voltage: Apc
- type: NodeContainer
nodes:
input:
!type:CableDeviceNode
nodeGroupID: MVPower
output:
!type:CableDeviceNode
nodeGroupID: Apc
- type: Transform
anchored: true
- type: UserInterface
interfaces:
- key: enum.ApcUiKey.Key
type: ApcBoundUserInterface
- type: AccessReader
access: [['Engineering']]
- type: entity
id: ApcPowerReceiverDummy
components:
- type: ApcPowerReceiver
- type: Transform
anchored: true
";
private ServerIntegrationInstance _server = default!;
private IMapManager _mapManager = default!;
private IEntityManager _entityManager = default!;
private IGameTiming _gameTiming = default!;
[OneTimeSetUp]
public async Task Setup()
{
var options = new ServerIntegrationOptions {ExtraPrototypes = Prototypes};
_server = StartServerDummyTicker(options);
await _server.WaitIdleAsync();
_mapManager = _server.ResolveDependency<IMapManager>();
_entityManager = _server.ResolveDependency<IEntityManager>();
_gameTiming = _server.ResolveDependency<IGameTiming>();
}
/// <summary>
/// Test small power net with a simple surplus of power over the loads.
/// </summary>
[Test]
public async Task TestSimpleSurplus()
{
const float loadPower = 200;
PowerSupplierComponent supplier = default!;
PowerConsumerComponent consumer1 = default!;
PowerConsumerComponent consumer2 = default!;
_server.Assert(() =>
{
var map = _mapManager.CreateMap();
var grid = _mapManager.CreateGrid(map);
// Power only works when anchored
for (var i = 0; i < 3; i++)
{
grid.SetTile(new Vector2i(0, i), new Tile(1));
_entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i));
}
var generatorEnt = _entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates());
var consumerEnt1 = _entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 1));
var consumerEnt2 = _entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 2));
supplier = generatorEnt.GetComponent<PowerSupplierComponent>();
consumer1 = consumerEnt1.GetComponent<PowerConsumerComponent>();
consumer2 = consumerEnt2.GetComponent<PowerConsumerComponent>();
// Plenty of surplus and tolerance
supplier.MaxSupply = loadPower * 4;
supplier.SupplyRampTolerance = loadPower * 4;
consumer1.DrawRate = loadPower;
consumer2.DrawRate = loadPower;
});
_server.RunTicks(1); //let run a tick for PowerNet to process power
_server.Assert(() =>
{
// Assert both consumers fully powered
Assert.That(consumer1.ReceivedPower, Is.EqualTo(consumer1.DrawRate).Within(0.1));
Assert.That(consumer2.ReceivedPower, Is.EqualTo(consumer2.DrawRate).Within(0.1));
// Assert that load adds up on supply.
Assert.That(supplier.CurrentSupply, Is.EqualTo(loadPower * 2).Within(0.1));
});
await _server.WaitIdleAsync();
}
/// <summary>
/// Test small power net with a simple deficit of power over the loads.
/// </summary>
[Test]
public async Task TestSimpleDeficit()
{
const float loadPower = 200;
PowerSupplierComponent supplier = default!;
PowerConsumerComponent consumer1 = default!;
PowerConsumerComponent consumer2 = default!;
_server.Assert(() =>
{
var map = _mapManager.CreateMap();
var grid = _mapManager.CreateGrid(map);
// Power only works when anchored
for (var i = 0; i < 3; i++)
{
grid.SetTile(new Vector2i(0, i), new Tile(1));
_entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i));
}
var generatorEnt = _entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates());
var consumerEnt1 = _entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 1));
var consumerEnt2 = _entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 2));
supplier = generatorEnt.GetComponent<PowerSupplierComponent>();
consumer1 = consumerEnt1.GetComponent<PowerConsumerComponent>();
consumer2 = consumerEnt2.GetComponent<PowerConsumerComponent>();
// Too little supply, both consumers should get 33% power.
supplier.MaxSupply = loadPower;
supplier.SupplyRampTolerance = loadPower;
consumer1.DrawRate = loadPower;
consumer2.DrawRate = loadPower * 2;
});
_server.RunTicks(1); //let run a tick for PowerNet to process power
_server.Assert(() =>
{
// Assert both consumers get 33% power.
Assert.That(consumer1.ReceivedPower, Is.EqualTo(consumer1.DrawRate / 3).Within(0.1));
Assert.That(consumer2.ReceivedPower, Is.EqualTo(consumer2.DrawRate / 3).Within(0.1));
// Supply should be maxed out
Assert.That(supplier.CurrentSupply, Is.EqualTo(supplier.MaxSupply).Within(0.1));
});
await _server.WaitIdleAsync();
}
[Test]
public async Task TestSupplyRamp()
{
PowerSupplierComponent supplier = default!;
PowerConsumerComponent consumer = default!;
_server.Assert(() =>
{
var map = _mapManager.CreateMap();
var grid = _mapManager.CreateGrid(map);
// Power only works when anchored
for (var i = 0; i < 3; i++)
{
grid.SetTile(new Vector2i(0, i), new Tile(1));
_entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i));
}
var generatorEnt = _entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates());
var consumerEnt = _entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 2));
supplier = generatorEnt.GetComponent<PowerSupplierComponent>();
consumer = consumerEnt.GetComponent<PowerConsumerComponent>();
// Supply has enough total power but needs to ramp up to match.
supplier.MaxSupply = 400;
supplier.SupplyRampRate = 400;
supplier.SupplyRampTolerance = 100;
consumer.DrawRate = 400;
});
// Exact values can/will be off by a tick, add tolerance for that.
var tickRate = (float) _gameTiming.TickPeriod.TotalSeconds;
var tickDev = 400 * tickRate * 1.1f;
_server.RunTicks(1);
_server.Assert(() =>
{
// First tick, supply should be delivering 100 W (max tolerance) and start ramping up.
Assert.That(supplier.CurrentSupply, Is.EqualTo(100).Within(0.1));
Assert.That(consumer.ReceivedPower, Is.EqualTo(100).Within(0.1));
});
_server.RunTicks(14);
_server.Assert(() =>
{
// After 15 ticks (0.25 seconds), supply ramp pos should be at 100 W and supply at 100, approx.
Assert.That(supplier.CurrentSupply, Is.EqualTo(200).Within(tickDev));
Assert.That(supplier.SupplyRampPosition, Is.EqualTo(100).Within(tickDev));
Assert.That(consumer.ReceivedPower, Is.EqualTo(200).Within(tickDev));
});
_server.RunTicks(45);
_server.Assert(() =>
{
// After 1 second total, ramp should be at 400 and supply should be at 400, everybody happy.
Assert.That(supplier.CurrentSupply, Is.EqualTo(400).Within(tickDev));
Assert.That(supplier.SupplyRampPosition, Is.EqualTo(400).Within(tickDev));
Assert.That(consumer.ReceivedPower, Is.EqualTo(400).Within(tickDev));
});
await _server.WaitIdleAsync();
}
[Test]
public async Task TestBatteryRamp()
{
const float startingCharge = 100_000;
PowerNetworkBatteryComponent netBattery = default!;
BatteryComponent battery = default!;
PowerConsumerComponent consumer = default!;
_server.Assert(() =>
{
var map = _mapManager.CreateMap();
var grid = _mapManager.CreateGrid(map);
// Power only works when anchored
for (var i = 0; i < 3; i++)
{
grid.SetTile(new Vector2i(0, i), new Tile(1));
_entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i));
}
var generatorEnt = _entityManager.SpawnEntity("DischargingBatteryDummy", grid.ToCoordinates());
var consumerEnt = _entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 2));
netBattery = generatorEnt.GetComponent<PowerNetworkBatteryComponent>();
battery = generatorEnt.GetComponent<BatteryComponent>();
consumer = consumerEnt.GetComponent<PowerConsumerComponent>();
battery.MaxCharge = startingCharge;
battery.CurrentCharge = startingCharge;
netBattery.MaxSupply = 400;
netBattery.SupplyRampRate = 400;
netBattery.SupplyRampTolerance = 100;
consumer.DrawRate = 400;
});
// Exact values can/will be off by a tick, add tolerance for that.
var tickRate = (float) _gameTiming.TickPeriod.TotalSeconds;
var tickDev = 400 * tickRate * 1.1f;
_server.RunTicks(1);
_server.Assert(() =>
{
// First tick, supply should be delivering 100 W (max tolerance) and start ramping up.
Assert.That(netBattery.CurrentSupply, Is.EqualTo(100).Within(0.1));
Assert.That(consumer.ReceivedPower, Is.EqualTo(100).Within(0.1));
});
_server.RunTicks(14);
_server.Assert(() =>
{
// After 15 ticks (0.25 seconds), supply ramp pos should be at 100 W and supply at 100, approx.
Assert.That(netBattery.CurrentSupply, Is.EqualTo(200).Within(tickDev));
Assert.That(netBattery.SupplyRampPosition, Is.EqualTo(100).Within(tickDev));
Assert.That(consumer.ReceivedPower, Is.EqualTo(200).Within(tickDev));
// Trivial integral to calculate expected power spent.
const double spentExpected = (200 + 100) / 2.0 * 0.25;
Assert.That(battery.CurrentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev));
});
_server.RunTicks(45);
_server.Assert(() =>
{
// After 1 second total, ramp should be at 400 and supply should be at 400, everybody happy.
Assert.That(netBattery.CurrentSupply, Is.EqualTo(400).Within(tickDev));
Assert.That(netBattery.SupplyRampPosition, Is.EqualTo(400).Within(tickDev));
Assert.That(consumer.ReceivedPower, Is.EqualTo(400).Within(tickDev));
// Trivial integral to calculate expected power spent.
const double spentExpected = (400 + 100) / 2.0 * 0.75 + 400 * 0.25;
Assert.That(battery.CurrentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev));
});
await _server.WaitIdleAsync();
}
[Test]
public async Task TestSimpleBatteryChargeDeficit()
{
PowerSupplierComponent supplier = default!;
BatteryComponent battery = default!;
_server.Assert(() =>
{
var map = _mapManager.CreateMap();
var grid = _mapManager.CreateGrid(map);
// Power only works when anchored
for (var i = 0; i < 3; i++)
{
grid.SetTile(new Vector2i(0, i), new Tile(1));
_entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i));
}
var generatorEnt = _entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates());
var batteryEnt = _entityManager.SpawnEntity("ChargingBatteryDummy", grid.ToCoordinates(0, 2));
supplier = generatorEnt.GetComponent<PowerSupplierComponent>();
var netBattery = batteryEnt.GetComponent<PowerNetworkBatteryComponent>();
battery = batteryEnt.GetComponent<BatteryComponent>();
supplier.MaxSupply = 500;
supplier.SupplyRampTolerance = 500;
battery.MaxCharge = 100000;
netBattery.MaxChargeRate = 1000;
netBattery.Efficiency = 0.5f;
});
_server.RunTicks(30); // 60 TPS, 0.5 seconds
_server.Assert(() =>
{
// half a second @ 500 W = 250
// 50% efficiency, so 125 J stored total.
Assert.That(battery.CurrentCharge, Is.EqualTo(125).Within(0.1));
Assert.That(supplier.CurrentSupply, Is.EqualTo(500).Within(0.1));
});
await _server.WaitIdleAsync();
}
[Test]
public async Task TestFullBattery()
{
PowerConsumerComponent consumer = default!;
PowerSupplierComponent supplier = default!;
PowerNetworkBatteryComponent netBattery = default!;
BatteryComponent battery = default!;
_server.Assert(() =>
{
var map = _mapManager.CreateMap();
var grid = _mapManager.CreateGrid(map);
// Power only works when anchored
for (var i = 0; i < 4; i++)
{
grid.SetTile(new Vector2i(0, i), new Tile(1));
_entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i));
}
var terminal = _entityManager.SpawnEntity("CableTerminal", grid.ToCoordinates(0, 1));
terminal.Transform.LocalRotation = Angle.FromDegrees(180);
var batteryEnt = _entityManager.SpawnEntity("FullBatteryDummy", grid.ToCoordinates(0, 2));
var supplyEnt = _entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates(0, 0));
var consumerEnt = _entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 3));
consumer = consumerEnt.GetComponent<PowerConsumerComponent>();
supplier = supplyEnt.GetComponent<PowerSupplierComponent>();
netBattery = batteryEnt.GetComponent<PowerNetworkBatteryComponent>();
battery = batteryEnt.GetComponent<BatteryComponent>();
// Consumer needs 1000 W, supplier can only provide 800, battery fills in the remaining 200.
consumer.DrawRate = 1000;
supplier.MaxSupply = 800;
supplier.SupplyRampTolerance = 800;
netBattery.MaxSupply = 400;
netBattery.SupplyRampTolerance = 400;
netBattery.SupplyRampRate = 100_000;
battery.MaxCharge = 100_000;
battery.CurrentCharge = 100_000;
});
// Run some ticks so everything is stable.
_server.RunTicks(60);
// Exact values can/will be off by a tick, add tolerance for that.
var tickRate = (float) _gameTiming.TickPeriod.TotalSeconds;
var tickDev = 400 * tickRate * 1.1f;
_server.Assert(() =>
{
Assert.That(consumer.ReceivedPower, Is.EqualTo(consumer.DrawRate).Within(0.1));
Assert.That(supplier.CurrentSupply, Is.EqualTo(supplier.MaxSupply).Within(0.1));
// Battery's current supply includes passed-through power from the supply.
// Assert ramp position is correct to make sure it's only supplying 200 W for real.
Assert.That(netBattery.CurrentSupply, Is.EqualTo(1000).Within(0.1));
Assert.That(netBattery.SupplyRampPosition, Is.EqualTo(200).Within(0.1));
const int expectedSpent = 200;
Assert.That(battery.CurrentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev));
});
await _server.WaitIdleAsync();
}
[Test]
public async Task TestFullBatteryEfficiencyPassThrough()
{
PowerConsumerComponent consumer = default!;
PowerSupplierComponent supplier = default!;
PowerNetworkBatteryComponent netBattery = default!;
BatteryComponent battery = default!;
_server.Assert(() =>
{
var map = _mapManager.CreateMap();
var grid = _mapManager.CreateGrid(map);
// Power only works when anchored
for (var i = 0; i < 4; i++)
{
grid.SetTile(new Vector2i(0, i), new Tile(1));
_entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i));
}
var terminal = _entityManager.SpawnEntity("CableTerminal", grid.ToCoordinates(0, 1));
terminal.Transform.LocalRotation = Angle.FromDegrees(180);
var batteryEnt = _entityManager.SpawnEntity("FullBatteryDummy", grid.ToCoordinates(0, 2));
var supplyEnt = _entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates(0, 0));
var consumerEnt = _entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 3));
consumer = consumerEnt.GetComponent<PowerConsumerComponent>();
supplier = supplyEnt.GetComponent<PowerSupplierComponent>();
netBattery = batteryEnt.GetComponent<PowerNetworkBatteryComponent>();
battery = batteryEnt.GetComponent<BatteryComponent>();
// Consumer needs 1000 W, supply and battery can only provide 400 each.
// BUT the battery has 50% input efficiency, so 50% of the power of the supply gets lost.
consumer.DrawRate = 1000;
supplier.MaxSupply = 400;
supplier.SupplyRampTolerance = 400;
netBattery.MaxSupply = 400;
netBattery.SupplyRampTolerance = 400;
netBattery.SupplyRampRate = 100_000;
netBattery.Efficiency = 0.5f;
battery.MaxCharge = 1_000_000;
battery.CurrentCharge = 1_000_000;
});
// Run some ticks so everything is stable.
_server.RunTicks(60);
// Exact values can/will be off by a tick, add tolerance for that.
var tickRate = (float) _gameTiming.TickPeriod.TotalSeconds;
var tickDev = 400 * tickRate * 1.1f;
_server.Assert(() =>
{
Assert.That(consumer.ReceivedPower, Is.EqualTo(600).Within(0.1));
Assert.That(supplier.CurrentSupply, Is.EqualTo(supplier.MaxSupply).Within(0.1));
Assert.That(netBattery.CurrentSupply, Is.EqualTo(600).Within(0.1));
Assert.That(netBattery.SupplyRampPosition, Is.EqualTo(400).Within(0.1));
const int expectedSpent = 400;
Assert.That(battery.CurrentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev));
});
await _server.WaitIdleAsync();
}
[Test]
public async Task TestFullBatteryEfficiencyDemandPassThrough()
{
PowerConsumerComponent consumer1 = default!;
PowerConsumerComponent consumer2 = default!;
PowerSupplierComponent supplier = default!;
_server.Assert(() =>
{
var map = _mapManager.CreateMap();
var grid = _mapManager.CreateGrid(map);
// Map layout here is
// C - consumer
// B - battery
// G - generator
// B - battery
// C - consumer
// Connected in the only way that makes sense.
// Power only works when anchored
for (var i = 0; i < 5; i++)
{
grid.SetTile(new Vector2i(0, i), new Tile(1));
_entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i));
}
_entityManager.SpawnEntity("CableTerminal", grid.ToCoordinates(0, 2));
var terminal = _entityManager.SpawnEntity("CableTerminal", grid.ToCoordinates(0, 2));
terminal.Transform.LocalRotation = Angle.FromDegrees(180);
var batteryEnt1 = _entityManager.SpawnEntity("FullBatteryDummy", grid.ToCoordinates(0, 1));
var batteryEnt2 = _entityManager.SpawnEntity("FullBatteryDummy", grid.ToCoordinates(0, 3));
var supplyEnt = _entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates(0, 2));
var consumerEnt1 = _entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 0));
var consumerEnt2 = _entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 4));
consumer1 = consumerEnt1.GetComponent<PowerConsumerComponent>();
consumer2 = consumerEnt2.GetComponent<PowerConsumerComponent>();
supplier = supplyEnt.GetComponent<PowerSupplierComponent>();
var netBattery1 = batteryEnt1.GetComponent<PowerNetworkBatteryComponent>();
var netBattery2 = batteryEnt2.GetComponent<PowerNetworkBatteryComponent>();
var battery1 = batteryEnt1.GetComponent<BatteryComponent>();
var battery2 = batteryEnt2.GetComponent<BatteryComponent>();
// There are two loads, 500 W and 1000 W respectively.
// The 500 W load is behind a 50% efficient battery,
// so *effectively* it needs 2x as much power from the supply to run.
// Assert that both are getting 50% power.
// Batteries are empty and only a bridge.
consumer1.DrawRate = 500;
consumer2.DrawRate = 1000;
supplier.MaxSupply = 1000;
supplier.SupplyRampTolerance = 1000;
battery1.MaxCharge = 1_000_000;
battery2.MaxCharge = 1_000_000;
netBattery1.MaxChargeRate = 1_000;
netBattery2.MaxChargeRate = 1_000;
netBattery1.Efficiency = 0.5f;
netBattery1.MaxSupply = 1_000_000;
netBattery2.MaxSupply = 1_000_000;
netBattery1.SupplyRampTolerance = 1_000_000;
netBattery2.SupplyRampTolerance = 1_000_000;
});
// Run some ticks so everything is stable.
_server.RunTicks(10);
_server.Assert(() =>
{
Assert.That(consumer1.ReceivedPower, Is.EqualTo(250).Within(0.1));
Assert.That(consumer2.ReceivedPower, Is.EqualTo(500).Within(0.1));
Assert.That(supplier.CurrentSupply, Is.EqualTo(supplier.MaxSupply).Within(0.1));
});
await _server.WaitIdleAsync();
}
/// <summary>
/// Test that power is distributed proportionally, even through batteries.
/// </summary>
[Test]
public async Task TestBatteriesProportional()
{
PowerConsumerComponent consumer1 = default!;
PowerConsumerComponent consumer2 = default!;
PowerSupplierComponent supplier = default!;
_server.Assert(() =>
{
var map = _mapManager.CreateMap();
var grid = _mapManager.CreateGrid(map);
// Map layout here is
// C - consumer
// B - battery
// G - generator
// B - battery
// C - consumer
// Connected in the only way that makes sense.
// Power only works when anchored
for (var i = 0; i < 5; i++)
{
grid.SetTile(new Vector2i(0, i), new Tile(1));
_entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i));
}
_entityManager.SpawnEntity("CableTerminal", grid.ToCoordinates(0, 2));
var terminal = _entityManager.SpawnEntity("CableTerminal", grid.ToCoordinates(0, 2));
terminal.Transform.LocalRotation = Angle.FromDegrees(180);
var batteryEnt1 = _entityManager.SpawnEntity("FullBatteryDummy", grid.ToCoordinates(0, 1));
var batteryEnt2 = _entityManager.SpawnEntity("FullBatteryDummy", grid.ToCoordinates(0, 3));
var supplyEnt = _entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates(0, 2));
var consumerEnt1 = _entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 0));
var consumerEnt2 = _entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 4));
consumer1 = consumerEnt1.GetComponent<PowerConsumerComponent>();
consumer2 = consumerEnt2.GetComponent<PowerConsumerComponent>();
supplier = supplyEnt.GetComponent<PowerSupplierComponent>();
var netBattery1 = batteryEnt1.GetComponent<PowerNetworkBatteryComponent>();
var netBattery2 = batteryEnt2.GetComponent<PowerNetworkBatteryComponent>();
var battery1 = batteryEnt1.GetComponent<BatteryComponent>();
var battery2 = batteryEnt2.GetComponent<BatteryComponent>();
consumer1.DrawRate = 500;
consumer2.DrawRate = 1000;
supplier.MaxSupply = 1000;
supplier.SupplyRampTolerance = 1000;
battery1.MaxCharge = 1_000_000;
battery2.MaxCharge = 1_000_000;
netBattery1.MaxChargeRate = 20;
netBattery2.MaxChargeRate = 20;
netBattery1.MaxSupply = 1_000_000;
netBattery2.MaxSupply = 1_000_000;
netBattery1.SupplyRampTolerance = 1_000_000;
netBattery2.SupplyRampTolerance = 1_000_000;
});
// Run some ticks so everything is stable.
_server.RunTicks(60);
_server.Assert(() =>
{
// NOTE: MaxChargeRate on batteries actually skews the demand.
// So that's why the tolerance is so high, the charge rate is so *low*,
// and we run so many ticks to stabilize.
Assert.That(consumer1.ReceivedPower, Is.EqualTo(333.333).Within(10));
Assert.That(consumer2.ReceivedPower, Is.EqualTo(666.666).Within(10));
Assert.That(supplier.CurrentSupply, Is.EqualTo(supplier.MaxSupply).Within(0.1));
});
await _server.WaitIdleAsync();
}
[Test]
public async Task TestBatteryEngineCut()
{
PowerConsumerComponent consumer = default!;
PowerSupplierComponent supplier = default!;
PowerNetworkBatteryComponent netBattery = default!;
_server.Post(() =>
{
var map = _mapManager.CreateMap();
var grid = _mapManager.CreateGrid(map);
// Power only works when anchored
for (var i = 0; i < 4; i++)
{
grid.SetTile(new Vector2i(0, i), new Tile(1));
_entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, i));
}
var terminal = _entityManager.SpawnEntity("CableTerminal", grid.ToCoordinates(0, 1));
terminal.Transform.LocalRotation = Angle.FromDegrees(180);
var batteryEnt = _entityManager.SpawnEntity("FullBatteryDummy", grid.ToCoordinates(0, 2));
var supplyEnt = _entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates(0, 0));
var consumerEnt = _entityManager.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 3));
consumer = consumerEnt.GetComponent<PowerConsumerComponent>();
supplier = supplyEnt.GetComponent<PowerSupplierComponent>();
netBattery = batteryEnt.GetComponent<PowerNetworkBatteryComponent>();
var battery = batteryEnt.GetComponent<BatteryComponent>();
// Consumer needs 1000 W, supplier can only provide 800, battery fills in the remaining 200.
consumer.DrawRate = 1000;
supplier.MaxSupply = 1000;
supplier.SupplyRampTolerance = 1000;
netBattery.MaxSupply = 1000;
netBattery.SupplyRampTolerance = 200;
netBattery.SupplyRampRate = 10;
battery.MaxCharge = 100_000;
battery.CurrentCharge = 100_000;
});
// Run some ticks so everything is stable.
_server.RunTicks(5);
_server.Assert(() =>
{
// Supply and consumer are fully loaded/supplied.
Assert.That(consumer.ReceivedPower, Is.EqualTo(consumer.DrawRate).Within(0.5));
Assert.That(supplier.CurrentSupply, Is.EqualTo(supplier.MaxSupply).Within(0.5));
// Cut off the supplier
supplier.Enabled = false;
// Remove tolerance on battery too.
netBattery.SupplyRampTolerance = 5;
});
_server.RunTicks(3);
_server.Assert(() =>
{
// Assert that network drops to 0 power and starts ramping up
Assert.That(consumer.ReceivedPower, Is.LessThan(50).And.GreaterThan(0));
Assert.That(netBattery.CurrentReceiving, Is.EqualTo(0));
Assert.That(netBattery.CurrentSupply, Is.GreaterThan(0));
});
await _server.WaitIdleAsync();
}
/// <summary>
/// Test that <see cref="CableTerminalNode"/> correctly isolates two networks.
/// </summary>
[Test]
public async Task TestTerminalNodeGroups()
{
CableNode leftNode = default!;
CableNode rightNode = default!;
Node batteryInput = default!;
Node batteryOutput = default!;
_server.Assert(() =>
{
var map = _mapManager.CreateMap();
var grid = _mapManager.CreateGrid(map);
// Power only works when anchored
for (var i = 0; i < 4; i++)
{
grid.SetTile(new Vector2i(0, i), new Tile(1));
}
var leftEnt = _entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, 0));
_entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, 1));
_entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, 2));
var rightEnt = _entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, 3));
var terminal = _entityManager.SpawnEntity("CableTerminal", grid.ToCoordinates(0, 1));
terminal.Transform.LocalRotation = Angle.FromDegrees(180);
var battery = _entityManager.SpawnEntity("FullBatteryDummy", grid.ToCoordinates(0, 2));
var batteryNodeContainer = battery.GetComponent<NodeContainerComponent>();
leftNode = leftEnt.GetComponent<NodeContainerComponent>().GetNode<CableNode>("power");
rightNode = rightEnt.GetComponent<NodeContainerComponent>().GetNode<CableNode>("power");
batteryInput = batteryNodeContainer.GetNode<Node>("input");
batteryOutput = batteryNodeContainer.GetNode<Node>("output");
});
// Run ticks to allow node groups to update.
_server.RunTicks(1);
_server.Assert(() =>
{
Assert.That(batteryInput.NodeGroup, Is.EqualTo(leftNode.NodeGroup));
Assert.That(batteryOutput.NodeGroup, Is.EqualTo(rightNode.NodeGroup));
Assert.That(leftNode.NodeGroup, Is.Not.EqualTo(rightNode.NodeGroup));
});
await _server.WaitIdleAsync();
}
[Test]
public async Task ApcChargingTest()
{
PowerNetworkBatteryComponent substationNetBattery = default!;
BatteryComponent apcBattery = default!;
_server.Assert(() =>
{
var map = _mapManager.CreateMap();
var grid = _mapManager.CreateGrid(map);
// Power only works when anchored
for (var i = 0; i < 3; i++)
{
grid.SetTile(new Vector2i(0, i), new Tile(1));
}
_entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, 0));
_entityManager.SpawnEntity("CableHV", grid.ToCoordinates(0, 1));
_entityManager.SpawnEntity("CableMV", grid.ToCoordinates(0, 1));
_entityManager.SpawnEntity("CableMV", grid.ToCoordinates(0, 2));
var generatorEnt = _entityManager.SpawnEntity("GeneratorDummy", grid.ToCoordinates(0, 0));
var substationEnt = _entityManager.SpawnEntity("SubstationDummy", grid.ToCoordinates(0, 1));
var apcEnt = _entityManager.SpawnEntity("ApcDummy", grid.ToCoordinates(0, 2));
var generatorSupplier = generatorEnt.GetComponent<PowerSupplierComponent>();
substationNetBattery = substationEnt.GetComponent<PowerNetworkBatteryComponent>();
apcBattery = apcEnt.GetComponent<BatteryComponent>();
generatorSupplier.MaxSupply = 1000;
generatorSupplier.SupplyRampTolerance = 1000;
apcBattery.CurrentCharge = 0;
});
_server.RunTicks(5); //let run a few ticks for PowerNets to reevaluate and start charging apc
_server.Assert(() =>
{
Assert.That(substationNetBattery.CurrentSupply, Is.GreaterThan(0)); //substation should be providing power
Assert.That(apcBattery.CurrentCharge, Is.GreaterThan(0)); //apc battery should have gained charge
});
await _server.WaitIdleAsync();
}
[Test]
public async Task ApcNetTest()
{
PowerNetworkBatteryComponent apcNetBattery = default!;
ApcPowerReceiverComponent receiver = default!;
_server.Assert(() =>
{
var map = _mapManager.CreateMap();
var grid = _mapManager.CreateGrid(map);
// Power only works when anchored
for (var i = 0; i < 3; i++)
{
grid.SetTile(new Vector2i(0, i), new Tile(1));
}
var apcEnt = _entityManager.SpawnEntity("ApcDummy", grid.ToCoordinates(0, 0));
var apcExtensionEnt = _entityManager.SpawnEntity("CableApcExtension", grid.ToCoordinates(0, 0));
var powerReceiverEnt = _entityManager.SpawnEntity("ApcPowerReceiverDummy", grid.ToCoordinates(0, 2));
var provider = apcExtensionEnt.GetComponent<ApcPowerProviderComponent>();
receiver = powerReceiverEnt.GetComponent<ApcPowerReceiverComponent>();
var battery = apcEnt.GetComponent<BatteryComponent>();
apcNetBattery = apcEnt.GetComponent<PowerNetworkBatteryComponent>();
provider.PowerTransferRange = 5; //arbitrary range to reach receiver
receiver.PowerReceptionRange = 5; //arbitrary range to reach provider
battery.MaxCharge = 10000; //arbitrary nonzero amount of charge
battery.CurrentCharge = battery.MaxCharge; //fill battery
receiver.Load = 1; //arbitrary small amount of power
});
_server.RunTicks(1); //let run a tick for ApcNet to process power
_server.Assert(() =>
{
Assert.That(receiver.Powered);
Assert.That(apcNetBattery.CurrentSupply, Is.EqualTo(1).Within(0.1));
});
await _server.WaitIdleAsync();
}
}
}

View File

@@ -1,292 +0,0 @@
#nullable enable
using System.Threading.Tasks;
using Content.Server.APC.Components;
using Content.Server.Battery.Components;
using Content.Server.GameObjects.Components;
using Content.Server.Power.Components;
using Content.Shared.Coordinates;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
namespace Content.IntegrationTests.Tests
{
[TestFixture]
public class PowerTest : ContentIntegrationTest
{
private const string Prototypes = @"
- type: entity
name: GeneratorDummy
id: GeneratorDummy
components:
- type: NodeContainer
nodes:
output:
!type:AdjacentNode
nodeGroupID: HVPower
- type: PowerSupplier
supplyRate: 3000
- type: Anchorable
- type: Transform
anchored: true
- type: entity
name: ConsumerDummy
id: ConsumerDummy
components:
- type: Transform
anchored: true
- type: NodeContainer
nodes:
input:
!type:AdjacentNode
nodeGroupID: HVPower
- type: PowerConsumer
drawRate: 50
- type: entity
name: SubstationDummy
id: SubstationDummy
components:
- type: Battery
maxCharge: 1000
startingCharge: 1000
- type: NodeContainer
nodes:
input:
!type:AdjacentNode
nodeGroupID: HVPower
output:
!type:AdjacentNode
nodeGroupID: MVPower
- type: PowerConsumer
- type: BatteryStorage
activeDrawRate: 1500
- type: PowerSupplier
voltage: Medium
- type: BatteryDischarger
activeSupplyRate: 1000
- type: Transform
anchored: true
- type: entity
name: ApcDummy
id: ApcDummy
components:
- type: Battery
maxCharge: 10000
startingCharge: 10000
- type: BatteryStorage
activeDrawRate: 1000
- type: PowerProvider
voltage: Apc
- type: Apc
voltage: Apc
- type: PowerConsumer
voltage: Medium
- type: NodeContainer
nodes:
input:
!type:AdjacentNode
nodeGroupID: MVPower
output:
!type:AdjacentNode
nodeGroupID: Apc
- type: Transform
anchored: true
- type: UserInterface
interfaces:
- key: enum.ApcUiKey.Key
type: ApcBoundUserInterface
- type: AccessReader
access: [['Engineering']]
- type: entity
name: ApcExtensionCableDummy
id: ApcExtensionCableDummy
components:
- type: NodeContainer
nodes:
apc:
!type:AdjacentNode
nodeGroupID: Apc
wire:
!type:AdjacentNode
nodeGroupID: WireNet
- type: PowerProvider
voltage: Apc
- type: Wire
wireType: Apc
- type: Transform
anchored: true
- type: entity
name: PowerReceiverDummy
id: PowerReceiverDummy
components:
- type: PowerReceiver
- type: Transform
anchored: true
";
[Test]
public async Task PowerNetTest()
{
var options = new ServerIntegrationOptions{ExtraPrototypes = Prototypes};
var server = StartServerDummyTicker(options);
PowerSupplierComponent supplier = default!;
PowerConsumerComponent consumer1 = default!;
PowerConsumerComponent consumer2 = default!;
server.Assert(() =>
{
var mapMan = IoCManager.Resolve<IMapManager>();
var entityMan = IoCManager.Resolve<IEntityManager>();
mapMan.CreateMap(new MapId(1));
var grid = mapMan.CreateGrid(new MapId(1));
// Power only works when anchored
grid.SetTile(new Vector2i(0, 0), new Tile(1));
grid.SetTile(new Vector2i(0, 1), new Tile(1));
grid.SetTile(new Vector2i(0, 2), new Tile(1));
var generatorEnt = entityMan.SpawnEntity("GeneratorDummy", grid.ToCoordinates());
var consumerEnt1 = entityMan.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 1));
var consumerEnt2 = entityMan.SpawnEntity("ConsumerDummy", grid.ToCoordinates(0, 2));
if (generatorEnt.TryGetComponent(out PhysicsComponent? physics))
{
physics.BodyType = BodyType.Static;
}
supplier = generatorEnt.GetComponent<PowerSupplierComponent>();
consumer1 = consumerEnt1.GetComponent<PowerConsumerComponent>();
consumer2 = consumerEnt2.GetComponent<PowerConsumerComponent>();
var supplyRate = 1000; //arbitrary amount of power supply
supplier.SupplyRate = supplyRate;
consumer1.DrawRate = supplyRate / 2; //arbitrary draw less than supply
consumer2.DrawRate = supplyRate * 2; //arbitrary draw greater than supply
consumer1.Priority = Priority.First; //power goes to this consumer first
consumer2.Priority = Priority.Last; //any excess power should go to low priority consumer
});
server.RunTicks(1); //let run a tick for PowerNet to process power
server.Assert(() =>
{
Assert.That(consumer1.DrawRate, Is.EqualTo(consumer1.ReceivedPower)); //first should be fully powered
Assert.That(consumer2.ReceivedPower, Is.EqualTo(supplier.SupplyRate - consumer1.ReceivedPower)); //second should get remaining power
});
await server.WaitIdleAsync();
}
[Test]
public async Task ApcChargingTest()
{
var options = new ServerIntegrationOptions{ExtraPrototypes = Prototypes};
var server = StartServerDummyTicker(options);
BatteryComponent apcBattery = default!;
PowerSupplierComponent substationSupplier = default!;
server.Assert(() =>
{
var mapMan = IoCManager.Resolve<IMapManager>();
var entityMan = IoCManager.Resolve<IEntityManager>();
mapMan.CreateMap(new MapId(1));
var grid = mapMan.CreateGrid(new MapId(1));
// Power only works when anchored
grid.SetTile(new Vector2i(0, 0), new Tile(1));
grid.SetTile(new Vector2i(0, 1), new Tile(1));
grid.SetTile(new Vector2i(0, 2), new Tile(1));
var generatorEnt = entityMan.SpawnEntity("GeneratorDummy", grid.ToCoordinates());
var substationEnt = entityMan.SpawnEntity("SubstationDummy", grid.ToCoordinates(0, 1));
var apcEnt = entityMan.SpawnEntity("ApcDummy", grid.ToCoordinates(0, 2));
var generatorSupplier = generatorEnt.GetComponent<PowerSupplierComponent>();
substationSupplier = substationEnt.GetComponent<PowerSupplierComponent>();
var substationStorage = substationEnt.GetComponent<BatteryStorageComponent>();
var substationDischarger = substationEnt.GetComponent<BatteryDischargerComponent>();
apcBattery = apcEnt.GetComponent<BatteryComponent>();
var apcStorage = apcEnt.GetComponent<BatteryStorageComponent>();
generatorSupplier.SupplyRate = 1000; //arbitrary nonzero amount of power
substationStorage.ActiveDrawRate = 1000; //arbitrary nonzero power draw
substationDischarger.ActiveSupplyRate = 500; //arbitirary nonzero power supply less than substation storage draw
apcStorage.ActiveDrawRate = 500; //arbitrary nonzero power draw
apcBattery.MaxCharge = 100; //abbitrary nonzero amount of charge
apcBattery.CurrentCharge = 0; //no charge
});
server.RunTicks(5); //let run a few ticks for PowerNets to reevaluate and start charging apc
server.Assert(() =>
{
Assert.That(substationSupplier.SupplyRate, Is.Not.EqualTo(0)); //substation should be providing power
Assert.That(apcBattery.CurrentCharge, Is.Not.EqualTo(0)); //apc battery should have gained charge
});
await server.WaitIdleAsync();
}
[Test]
public async Task ApcNetTest()
{
var options = new ServerIntegrationOptions{ExtraPrototypes = Prototypes};
var server = StartServerDummyTicker(options);
PowerReceiverComponent receiver = default!;
server.Assert(() =>
{
var mapMan = IoCManager.Resolve<IMapManager>();
var entityMan = IoCManager.Resolve<IEntityManager>();
var mapId = new MapId(1);
mapMan.CreateMap(mapId);
var grid = mapMan.CreateGrid(mapId);
// Power only works when anchored
grid.SetTile(new Vector2i(0, 0), new Tile(1));
grid.SetTile(new Vector2i(0, 1), new Tile(1));
grid.SetTile(new Vector2i(0, 2), new Tile(1));
var apcEnt = entityMan.SpawnEntity("ApcDummy", grid.ToCoordinates(0, 0));
var apcExtensionEnt = entityMan.SpawnEntity("ApcExtensionCableDummy", grid.ToCoordinates(0, 1));
var powerReceiverEnt = entityMan.SpawnEntity("PowerReceiverDummy", grid.ToCoordinates(0, 2));
var apc = apcEnt.GetComponent<ApcComponent>();
var provider = apcExtensionEnt.GetComponent<PowerProviderComponent>();
receiver = powerReceiverEnt.GetComponent<PowerReceiverComponent>();
var battery = apcEnt.GetComponent<BatteryComponent>();
provider.PowerTransferRange = 5; //arbitrary range to reach receiver
receiver.PowerReceptionRange = 5; //arbitrary range to reach provider
battery.MaxCharge = 10000; //arbitrary nonzero amount of charge
battery.CurrentCharge = battery.MaxCharge; //fill battery
receiver.Load = 1; //arbitrary small amount of power
});
server.RunTicks(1); //let run a tick for ApcNet to process power
server.Assert(() =>
{
Assert.That(receiver.Powered);
});
await server.WaitIdleAsync();
}
}
}

View File

@@ -9,15 +9,22 @@ using Robust.Shared.Reflection;
namespace Content.IntegrationTests.Tests
{
[TestFixture]
[TestOf(typeof(IResettingEntitySystem))]
[TestOf(typeof(RoundRestartCleanupEvent))]
public class ResettingEntitySystemTests : ContentIntegrationTest
{
[Reflect(false)]
private class TestResettingEntitySystem : EntitySystem, IResettingEntitySystem
private class TestRoundRestartCleanupEvent : EntitySystem
{
public bool HasBeenReset { get; set; }
public void Reset()
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
}
public void Reset(RoundRestartCleanupEvent ev)
{
HasBeenReset = true;
}
@@ -30,7 +37,7 @@ namespace Content.IntegrationTests.Tests
{
ContentBeforeIoC = () =>
{
IoCManager.Resolve<IEntitySystemManager>().LoadExtraSystemType<TestResettingEntitySystem>();
IoCManager.Resolve<IEntitySystemManager>().LoadExtraSystemType<TestRoundRestartCleanupEvent>();
}
});
@@ -43,7 +50,7 @@ namespace Content.IntegrationTests.Tests
{
Assert.That(gameTicker.RunLevel, Is.EqualTo(GameRunLevel.InRound));
var system = entitySystemManager.GetEntitySystem<TestResettingEntitySystem>();
var system = entitySystemManager.GetEntitySystem<TestRoundRestartCleanupEvent>();
system.HasBeenReset = false;

View File

@@ -122,7 +122,24 @@ namespace Content.IntegrationTests.Tests
two = reader.ReadToEnd();
}
Assert.That(one, Is.EqualTo(two));
Assert.Multiple(() => {
Assert.That(two, Is.EqualTo(one));
var failed = TestContext.CurrentContext.Result.Assertions.FirstOrDefault();
if (failed != null)
{
var oneTmp = Path.GetTempFileName();
var twoTmp = Path.GetTempFileName();
File.WriteAllText(oneTmp, one);
File.WriteAllText(twoTmp, two);
TestContext.AddTestAttachment(oneTmp, "First save file");
TestContext.AddTestAttachment(twoTmp, "Second save file");
TestContext.Error.WriteLine("Complete output:");
TestContext.Error.WriteLine(oneTmp);
TestContext.Error.WriteLine(twoTmp);
}
});
}
}
}

View File

@@ -1,5 +1,6 @@
using System.Threading.Tasks;
using Content.Server.StationEvents;
using Content.Shared.GameTicking;
using NUnit.Framework;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -33,7 +34,7 @@ namespace Content.IntegrationTests.Tests.StationEvents
Assert.That(stationEvent.Occurrences > 0);
}
stationEventsSystem.Reset();
stationEventsSystem.Reset(new RoundRestartCleanupEvent());
foreach (var stationEvent in stationEventsSystem.StationEvents)
{

View File

@@ -21,7 +21,7 @@ namespace Content.Server.AI.Pathfinding.Accessible
/// </summary>
/// Long-term can be used to do hierarchical pathfinding
[UsedImplicitly]
public sealed class AiReachableSystem : EntitySystem, IResettingEntitySystem
public sealed class AiReachableSystem : EntitySystem
{
/*
* The purpose of this is to provide a higher-level / hierarchical abstraction of the actual pathfinding graph
@@ -81,6 +81,7 @@ namespace Content.Server.AI.Pathfinding.Accessible
public override void Initialize()
{
_pathfindingSystem = Get<PathfindingSystem>();
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
SubscribeLocalEvent<PathfindingChunkUpdateMessage>(RecalculateNodeRegions);
#if DEBUG
SubscribeNetworkEvent<SharedAiDebug.SubscribeReachableMessage>(HandleSubscription);
@@ -699,7 +700,7 @@ namespace Content.Server.AI.Pathfinding.Accessible
#endif
}
public void Reset()
public void Reset(RoundRestartCleanupEvent ev)
{
_queuedUpdates.Clear();
_regions.Clear();

View File

@@ -27,7 +27,7 @@ namespace Content.Server.AI.Pathfinding
/// This system handles pathfinding graph updates as well as dispatches to the pathfinder
/// (90% of what it's doing is graph updates so not much point splitting the 2 roles)
/// </summary>
public class PathfindingSystem : EntitySystem, IResettingEntitySystem
public class PathfindingSystem : EntitySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
@@ -201,6 +201,7 @@ namespace Content.Server.AI.Pathfinding
public override void Initialize()
{
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
SubscribeLocalEvent<CollisionChangeMessage>(QueueCollisionChangeMessage);
SubscribeLocalEvent<MoveEvent>(QueueMoveEvent);
SubscribeLocalEvent<AccessReaderChangeMessage>(QueueAccessChangeMessage);
@@ -385,7 +386,7 @@ namespace Content.Server.AI.Pathfinding
return true;
}
public void Reset()
public void Reset(RoundRestartCleanupEvent ev)
{
_graph.Clear();
_collidableUpdateQueue.Clear();

View File

@@ -8,6 +8,7 @@ using Content.Server.NodeContainer.NodeGroups;
using Content.Server.NodeContainer.Nodes;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Random;
using Robust.Shared.ViewVariables;
@@ -36,64 +37,42 @@ namespace Content.Server.AME
public int CoreCount => _cores.Count;
protected override void OnAddNode(Node node)
public override void LoadNodes(List<Node> groupNodes)
{
base.OnAddNode(node);
if (_masterController == null)
{
node.Owner.TryGetComponent<AMEControllerComponent>(out var controller);
_masterController = controller;
}
}
base.LoadNodes(groupNodes);
protected override void OnRemoveNode(Node node)
{
base.OnRemoveNode(node);
RefreshAMENodes(_masterController);
if (_masterController != null && _masterController?.Owner == node.Owner) { _masterController = null; }
}
var mapManager = IoCManager.Resolve<IMapManager>();
var grid = mapManager.GetGrid(GridId);
public void RefreshAMENodes(AMEControllerComponent? controller)
{
if(_masterController == null && controller != null)
{
_masterController = controller;
}
foreach (AMEShieldComponent core in _cores)
{
core.UnsetCore();
}
_cores.Clear();
//Check each shield node to see if it meets core criteria
foreach (Node node in Nodes)
foreach (var node in groupNodes)
{
var nodeOwner = node.Owner;
if (!nodeOwner.TryGetComponent<AMEShieldComponent>(out var shield)) { continue; }
if (nodeOwner.TryGetComponent(out AMEControllerComponent? controller))
{
_masterController = controller;
}
var grid = IoCManager.Resolve<IMapManager>().GetGrid(nodeOwner.Transform.GridID);
if (nodeOwner.TryGetComponent(out AMEShieldComponent? shield))
{
var nodeNeighbors = grid.GetCellsInSquareArea(nodeOwner.Transform.Coordinates, 1)
.Select(sgc => nodeOwner.EntityManager.GetEntity(sgc))
.Where(entity => entity != nodeOwner)
.Select(entity => entity.TryGetComponent<AMEShieldComponent>(out var adjshield) ? adjshield : null)
.Where(adjshield => adjshield != null);
.Where(entity => entity != nodeOwner && entity.HasComponent<AMEShieldComponent>());
if (nodeNeighbors.Count() >= 8)
{
_cores.Add(shield);
shield.SetCore();
}
}
foreach (AMEShieldComponent core in _cores)
else
{
core.SetCore();
shield.UnsetCore();
}
}
}
}
public void UpdateCoreVisuals(int injectionAmount, bool injecting)
{
var injectionStrength = CoreCount > 0 ? injectionAmount / CoreCount : 0;
foreach (AMEShieldComponent core in _cores)

View File

@@ -33,7 +33,7 @@ namespace Content.Server.AME.Components
private AppearanceComponent? _appearance;
private PowerSupplierComponent? _powerSupplier;
private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
private bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered;
[ViewVariables]
private int _stability = 100;
@@ -92,7 +92,7 @@ namespace Content.Server.AME.Components
if(fuelJar != null && _powerSupplier != null)
{
var availableInject = fuelJar.FuelAmount >= InjectionAmount ? InjectionAmount : fuelJar.FuelAmount;
_powerSupplier.SupplyRate = group.InjectFuel(availableInject, out var overloading);
_powerSupplier.MaxSupply = group.InjectFuel(availableInject, out var overloading);
fuelJar.FuelAmount -= availableInject;
InjectSound(overloading);
UpdateUserInterface();
@@ -212,9 +212,6 @@ namespace Content.Server.AME.Components
case UiButton.DecreaseFuel:
InjectionAmount = InjectionAmount > 0 ? InjectionAmount -= 2 : 0;
break;
case UiButton.RefreshParts:
RefreshParts();
break;
}
GetAMENodeGroup()?.UpdateCoreVisuals(InjectionAmount, _injecting);
@@ -252,7 +249,7 @@ namespace Content.Server.AME.Components
_appearance?.SetData(AMEControllerVisuals.DisplayState, "off");
if (_powerSupplier != null)
{
_powerSupplier.SupplyRate = 0;
_powerSupplier.MaxSupply = 0;
}
}
_injecting = !_injecting;
@@ -277,12 +274,6 @@ namespace Content.Server.AME.Components
}
private void RefreshParts()
{
GetAMENodeGroup()?.RefreshAMENodes(this);
UpdateUserInterface();
}
private AMENodeGroup? GetAMENodeGroup()
{
Owner.TryGetComponent(out NodeContainerComponent? nodeContainer);

View File

@@ -1,223 +0,0 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Content.Server.APC.Components;
using Content.Server.Battery.Components;
using Content.Server.NodeContainer.NodeGroups;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Content.Server.APC
{
public interface IApcNet
{
bool Powered { get; }
void AddApc(ApcComponent apc);
void RemoveApc(ApcComponent apc);
void AddPowerProvider(PowerProviderComponent provider);
void RemovePowerProvider(PowerProviderComponent provider);
void UpdatePowerProviderReceivers(PowerProviderComponent provider, int oldLoad, int newLoad);
void Update(float frameTime);
GridId? GridId { get; }
}
[NodeGroup(NodeGroupID.Apc)]
public class ApcNetNodeGroup : BaseNetConnectorNodeGroup<BaseApcNetComponent, IApcNet>, IApcNet
{
[ViewVariables]
private readonly Dictionary<ApcComponent, BatteryComponent> _apcBatteries = new();
[ViewVariables]
private readonly List<PowerProviderComponent> _providers = new();
[ViewVariables]
public bool Powered { get => _powered; private set => SetPowered(value); }
private bool _powered = false;
//Debug property
[ViewVariables]
private int TotalReceivers => _providers.SelectMany(provider => provider.LinkedReceivers).Count();
[ViewVariables]
private int TotalPowerReceiverLoad { get => _totalPowerReceiverLoad; set => SetTotalPowerReceiverLoad(value); }
GridId? IApcNet.GridId => GridId;
private int _totalPowerReceiverLoad = 0;
public static readonly IApcNet NullNet = new NullApcNet();
public override void Initialize(Node sourceNode)
{
base.Initialize(sourceNode);
EntitySystem.Get<ApcNetSystem>().AddApcNet(this);
}
protected override void AfterRemake(IEnumerable<INodeGroup> newGroups)
{
base.AfterRemake(newGroups);
foreach (var group in newGroups)
{
if (group is not ApcNetNodeGroup apcNet)
continue;
apcNet.Powered = Powered;
}
StopUpdates();
}
protected override void OnGivingNodesForCombine(INodeGroup newGroup)
{
base.OnGivingNodesForCombine(newGroup);
if (newGroup is ApcNetNodeGroup apcNet)
{
apcNet.Powered = Powered;
}
StopUpdates();
}
private void StopUpdates()
{
EntitySystem.Get<ApcNetSystem>().RemoveApcNet(this);
}
#region IApcNet Methods
protected override void SetNetConnectorNet(BaseApcNetComponent netConnectorComponent)
{
netConnectorComponent.Net = this;
}
public void AddApc(ApcComponent apc)
{
if (!apc.Owner.TryGetComponent(out BatteryComponent? battery))
{
return;
}
_apcBatteries.Add(apc, battery);
}
public void RemoveApc(ApcComponent apc)
{
_apcBatteries.Remove(apc);
}
public void AddPowerProvider(PowerProviderComponent provider)
{
_providers.Add(provider);
foreach (var receiver in provider.LinkedReceivers)
{
TotalPowerReceiverLoad += receiver.Load;
}
}
public void RemovePowerProvider(PowerProviderComponent provider)
{
_providers.Remove(provider);
foreach (var receiver in provider.LinkedReceivers)
{
TotalPowerReceiverLoad -= receiver.Load;
}
}
public void UpdatePowerProviderReceivers(PowerProviderComponent provider, int oldLoad, int newLoad)
{
DebugTools.Assert(_providers.Contains(provider));
TotalPowerReceiverLoad -= oldLoad;
TotalPowerReceiverLoad += newLoad;
}
public void Update(float frameTime)
{
var remainingPowerNeeded = TotalPowerReceiverLoad * frameTime;
foreach (var apcBatteryPair in _apcBatteries)
{
var apc = apcBatteryPair.Key;
if (!apc.MainBreakerEnabled)
continue;
var battery = apcBatteryPair.Value;
if (battery.CurrentCharge < remainingPowerNeeded)
{
remainingPowerNeeded -= battery.CurrentCharge;
battery.CurrentCharge = 0;
}
else
{
battery.UseCharge(remainingPowerNeeded);
remainingPowerNeeded = 0;
}
if (remainingPowerNeeded == 0)
break;
}
Powered = remainingPowerNeeded == 0;
}
private void SetPowered(bool powered)
{
if (powered != Powered)
{
_powered = powered;
PoweredChanged();
}
}
private void PoweredChanged()
{
foreach (var provider in _providers)
{
foreach (var receiver in provider.LinkedReceivers)
{
receiver.ApcPowerChanged();
}
}
}
private void SetTotalPowerReceiverLoad(int totalPowerReceiverLoad)
{
DebugTools.Assert(totalPowerReceiverLoad >= 0, $"Expected load equal to or greater than 0, was {totalPowerReceiverLoad}");
_totalPowerReceiverLoad = totalPowerReceiverLoad;
}
#endregion
private class NullApcNet : IApcNet
{
/// <summary>
/// It is important that this returns false, so <see cref="PowerProviderComponent"/>s with a <see cref="NullApcNet"/> have no power.
/// </summary>
public bool Powered => false;
public GridId? GridId => default;
public void AddApc(ApcComponent apc) { }
public void AddPowerProvider(PowerProviderComponent provider) { }
public void RemoveApc(ApcComponent apc) { }
public void RemovePowerProvider(PowerProviderComponent provider) { }
public void UpdatePowerProviderReceivers(PowerProviderComponent provider, int oldLoad, int newLoad) { }
public void Update(float frameTime) { }
}
}
}

View File

@@ -1,45 +0,0 @@
#nullable enable
using System.Collections.Generic;
using Content.Shared.GameTicking;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Timing;
namespace Content.Server.APC
{
[UsedImplicitly]
internal sealed class ApcNetSystem : EntitySystem, IResettingEntitySystem
{
[Dependency] private readonly IPauseManager _pauseManager = default!;
private HashSet<IApcNet> _apcNets = new();
public override void Update(float frameTime)
{
foreach (var apcNet in _apcNets)
{
var gridId = apcNet.GridId;
if (gridId != null && !_pauseManager.IsGridPaused(gridId.Value))
apcNet.Update(frameTime);
}
}
public void AddApcNet(ApcNetNodeGroup apcNet)
{
_apcNets.Add(apcNet);
}
public void RemoveApcNet(ApcNetNodeGroup apcNet)
{
_apcNets.Remove(apcNet);
}
public void Reset()
{
// NodeGroupSystem does not remake ApcNets affected during restarting until a frame later,
// when their grid is invalid. So, we are clearing them on round restart.
_apcNets.Clear();
}
}
}

View File

@@ -1,10 +0,0 @@
#nullable enable
using Content.Server.Power.Components;
namespace Content.Server.APC.Components
{
public abstract class BaseApcNetComponent : BaseNetConnectorComponent<IApcNet>
{
protected override IApcNet NullNet => ApcNetNodeGroup.NullNet;
}
}

View File

@@ -34,7 +34,7 @@ namespace Content.Server.Access.Components
private ContainerSlot _targetIdContainer = default!;
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(IdCardConsoleUiKey.Key);
[ViewVariables] private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
[ViewVariables] private bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered;
private bool PrivilegedIDEmpty => _privilegedIdContainer.ContainedEntities.Count < 1;
private bool TargetIDEmpty => _targetIdContainer.ContainedEntities.Count < 1;

View File

@@ -0,0 +1,75 @@
using Content.Server.Hands.Components;
using Content.Server.Items;
using Content.Server.Notification;
using Content.Shared.ActionBlocker;
using Content.Shared.Actions.Behaviors;
using Content.Shared.Actions.Components;
using Content.Shared.Cooldown;
using Content.Shared.Notification.Managers;
using JetBrains.Annotations;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
namespace Content.Server.Actions.Spells
{
[UsedImplicitly]
[DataDefinition]
public class GiveItemSpell : IInstantAction
{ //TODO: Needs to be an EntityPrototype for proper validation
[ViewVariables] [DataField("castMessage")] public string? CastMessage { get; set; } = default!;
[ViewVariables] [DataField("cooldown")] public float CoolDown { get; set; } = 1f;
[ViewVariables] [DataField("spellItem")] public string ItemProto { get; set; } = default!;
[ViewVariables] [DataField("castSound")] public string? CastSound { get; set; } = default!;
//Rubber-band snapping items into player's hands, originally was a workaround, later found it works quite well with stuns
//Not sure if needs fixing
public void DoInstantAction(InstantActionEventArgs args)
{
var caster = args.Performer;
if (!caster.TryGetComponent(out HandsComponent? handsComponent))
{
caster.PopupMessage(Loc.GetString("spell-fail-no-hands"));
return;
}
if (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(caster)) return;
// TODO: Nix when we get EntityPrototype serializers
if (!IoCManager.Resolve<IPrototypeManager>().HasIndex<EntityPrototype>(ItemProto))
{
Logger.Error($"Invalid prototype {ItemProto} supplied for {nameof(GiveItemSpell)}");
return;
}
// TODO: Look this is shitty and ideally a test would do it
var spawnedProto = caster.EntityManager.SpawnEntity(ItemProto, caster.Transform.MapPosition);
if (!spawnedProto.TryGetComponent(out ItemComponent? itemComponent))
{
Logger.Error($"Tried to use {nameof(GiveItemSpell)} but prototype has no {nameof(ItemComponent)}?");
spawnedProto.Delete();
return;
}
args.PerformerActions?.Cooldown(args.ActionType, Cooldowns.SecondsFromNow(CoolDown));
if (CastMessage != null)
caster.PopupMessageEveryone(CastMessage);
handsComponent.PutInHandOrDrop(itemComponent);
if (CastSound != null)
SoundSystem.Play(Filter.Pvs(caster), CastSound, caster);
}
}
}

View File

@@ -27,7 +27,7 @@ namespace Content.Server.Arcade.Components
public override string Name => "BlockGameArcade";
public override uint? NetID => ContentNetIDs.BLOCKGAME_ARCADE;
[ComponentDependency] private readonly PowerReceiverComponent? _powerReceiverComponent = default!;
[ComponentDependency] private readonly ApcPowerReceiverComponent? _powerReceiverComponent = default!;
private bool Powered => _powerReceiverComponent?.Powered ?? false;
private BoundUserInterface? UserInterface => Owner.GetUIOrNull(BlockGameUiKey.Key);

View File

@@ -3,7 +3,7 @@ using System.Collections.Generic;
using Content.Server.Power.Components;
using Content.Server.UserInterface;
using Content.Server.VendingMachines;
using Content.Server.Wires.Components;
using Content.Server.WireHacking;
using Content.Shared.ActionBlocker;
using Content.Shared.Arcade;
using Content.Shared.Interaction;
@@ -28,7 +28,7 @@ namespace Content.Server.Arcade.Components
{
[Dependency] private readonly IRobustRandom _random = null!;
[ComponentDependency] private readonly PowerReceiverComponent? _powerReceiverComponent = default!;
[ComponentDependency] private readonly ApcPowerReceiverComponent? _powerReceiverComponent = default!;
[ComponentDependency] private readonly WiresComponent? _wiresComponent = default!;
private bool Powered => _powerReceiverComponent != null && _powerReceiverComponent.Powered;

View File

@@ -20,7 +20,7 @@ namespace Content.Server.GameObjects.Components
protected readonly object UserInterfaceKey;
[ViewVariables] protected BoundUserInterface? UserInterface => Owner.GetUIOrNull(UserInterfaceKey);
[ViewVariables] public bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
[ViewVariables] public bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered;
public BaseComputerUserInterfaceComponent(object key)
{

View File

@@ -4,9 +4,9 @@ using System;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Respiratory;
using Content.Server.Explosion;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.Interfaces;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Server.UserInterface;
using Content.Shared.ActionBlocker;
using Content.Shared.Actions;

View File

@@ -1,6 +1,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Content.Server.Atmos.Components;
using Content.Server.NodeContainer.EntitySystems;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Maps;
using JetBrains.Annotations;
@@ -33,6 +34,8 @@ namespace Content.Server.Atmos.EntitySystems
{
base.Initialize();
UpdatesAfter.Add(typeof(NodeGroupSystem));
InitializeGases();
InitializeCVars();

View File

@@ -25,7 +25,7 @@ using Dependency = Robust.Shared.IoC.DependencyAttribute;
namespace Content.Server.Atmos.EntitySystems
{
[UsedImplicitly]
internal sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem, IResettingEntitySystem
internal sealed class GasTileOverlaySystem : SharedGasTileOverlaySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
@@ -67,6 +67,8 @@ namespace Content.Server.Atmos.EntitySystems
{
base.Initialize();
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
_atmosphereSystem = Get<AtmosphereSystem>();
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
_mapManager.OnGridRemoved += OnGridRemoved;
@@ -479,7 +481,7 @@ namespace Content.Server.Atmos.EntitySystems
}
}
public void Reset()
public void Reset(RoundRestartCleanupEvent ev)
{
_invalidTiles.Clear();
_overlay.Clear();

View File

@@ -1,5 +1,5 @@
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.ActionBlocker;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Helpers;

View File

@@ -1,8 +1,8 @@
using System;
using Content.Server.Atmos.Piping.Binary.Components;
using Content.Server.Atmos.Piping.Components;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;

View File

@@ -1,7 +1,7 @@
using Content.Server.Atmos.Piping.Binary.Components;
using Content.Server.Atmos.Piping.Components;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Piping;
using JetBrains.Annotations;

View File

@@ -1,7 +1,7 @@
using Content.Server.Atmos.Piping.Binary.Components;
using Content.Server.Atmos.Piping.Components;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;

View File

@@ -1,8 +1,8 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Construction.Components;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using Content.Shared.Notification.Managers;
using JetBrains.Annotations;

View File

@@ -1,7 +1,7 @@
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Trinary.Components;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;

View File

@@ -1,8 +1,8 @@
using System;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Trinary.Components;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;

View File

@@ -4,9 +4,9 @@ using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Binary.Components;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Unary.Components;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.Hands.Components;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Server.UserInterface;
using Content.Shared.ActionBlocker;
using Content.Shared.Atmos;
@@ -62,7 +62,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
return;
// Create a pipenet if we don't have one already.
portNode.TryAssignGroupIfNeeded();
portNode.CreateSingleNetImmediate();
Get<AtmosphereSystem>().Merge(portNode.Air, canister.InitialMixture);
portNode.Air.Temperature = canister.InitialMixture.Temperature;
portNode.Volume = canister.InitialMixture.Volume;
@@ -90,7 +90,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
}
ui.SetState(new GasCanisterBoundUserInterfaceState(metadata.EntityName, portNode.Air.Pressure,
portNode.NodeGroup.Nodes.Count > 1, tankLabel, tankPressure,
portNode.NodeGroup!.Nodes.Count > 1, tankLabel, tankPressure,
canister.ReleasePressure, canister.ReleaseValve,
canister.MinReleasePressure, canister.MaxReleasePressure));
}

View File

@@ -1,7 +1,7 @@
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Unary.Components;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;

View File

@@ -1,8 +1,8 @@
using System;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Unary.Components;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;

View File

@@ -2,8 +2,8 @@ using System.Diagnostics.CodeAnalysis;
using Content.Server.Atmos.Piping.Binary.Components;
using Content.Server.Atmos.Piping.Unary.Components;
using Content.Server.Construction.Components;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos.Piping.Unary.Components;
using JetBrains.Annotations;
using Robust.Server.GameObjects;

View File

@@ -1,8 +1,8 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Unary.Components;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos.Piping;
using JetBrains.Annotations;
using Robust.Server.GameObjects;

View File

@@ -1,8 +1,8 @@
using System;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Unary.Components;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Visuals;
using JetBrains.Annotations;

View File

@@ -2,8 +2,8 @@ using System;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.Unary.Components;
using Content.Server.GameObjects.Components.NodeContainer.Nodes;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Piping.Unary.Visuals;
using JetBrains.Annotations;

View File

@@ -35,7 +35,7 @@ namespace Content.Server.BarSign
}
}
private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
private bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered;
private void UpdateSignInfo()
{

View File

@@ -1,19 +0,0 @@
#nullable enable
using Content.Server.Power.Components;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.Battery.EntitySystems
{
[UsedImplicitly]
internal sealed class BatteryDischargerSystem : EntitySystem
{
public override void Update(float frameTime)
{
foreach (var comp in ComponentManager.EntityQuery<BatteryDischargerComponent>(false))
{
comp.Update(frameTime);
}
}
}
}

View File

@@ -1,19 +0,0 @@
#nullable enable
using Content.Server.Power.Components;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.Battery.EntitySystems
{
[UsedImplicitly]
internal sealed class BatteryStorageSystem : EntitySystem
{
public override void Update(float frameTime)
{
foreach (var comp in ComponentManager.EntityQuery<BatteryStorageComponent>(false))
{
comp.Update(frameTime);
}
}
}
}

View File

@@ -1,19 +0,0 @@
#nullable enable
using Content.Server.Battery.Components;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Content.Server.Battery.EntitySystems
{
[UsedImplicitly]
public class BatterySystem : EntitySystem
{
public override void Update(float frameTime)
{
foreach (var comp in ComponentManager.EntityQuery<BatteryComponent>(true))
{
comp.OnUpdate(frameTime);
}
}
}
}

View File

@@ -68,7 +68,7 @@ namespace Content.Server.Body.Surgery
BreakOnTargetMove = true
};
return await doAfterSystem.DoAfter(args) == DoAfterStatus.Finished;
return await doAfterSystem.WaitDoAfter(args) == DoAfterStatus.Finished;
}
private bool HasIncisionNotClamped()

View File

@@ -10,7 +10,7 @@ using Robust.Shared.GameObjects;
namespace Content.Server.Body.Surgery.Components
{
[UsedImplicitly]
public class SurgeryToolSystem : EntitySystem, IResettingEntitySystem
public class SurgeryToolSystem : EntitySystem
{
private readonly HashSet<SurgeryToolComponent> _openSurgeryUIs = new();
@@ -18,11 +18,12 @@ namespace Content.Server.Body.Surgery.Components
{
base.Initialize();
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
SubscribeLocalEvent<SurgeryWindowOpenMessage>(OnSurgeryWindowOpen);
SubscribeLocalEvent<SurgeryWindowCloseMessage>(OnSurgeryWindowClose);
}
public void Reset()
public void Reset(RoundRestartCleanupEvent ev)
{
_openSurgeryUIs.Clear();
}

View File

@@ -14,7 +14,7 @@ namespace Content.Server.Botany.Components
[RegisterComponent]
public class SeedExtractorComponent : Component, IInteractUsing
{
[ComponentDependency] private readonly PowerReceiverComponent? _powerReceiver = default!;
[ComponentDependency] private readonly ApcPowerReceiverComponent? _powerReceiver = default!;
[Dependency] private readonly IRobustRandom _random = default!;

View File

@@ -7,7 +7,7 @@ using Robust.Shared.GameObjects;
namespace Content.Server.Cargo
{
public class CargoConsoleSystem : EntitySystem, IResettingEntitySystem
public class CargoConsoleSystem : EntitySystem
{
/// <summary>
/// How much time to wait (in seconds) before increasing bank accounts balance.
@@ -45,6 +45,8 @@ namespace Content.Server.Cargo
public override void Initialize()
{
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
CreateBankAccount("Space Station 14", 1000);
CreateOrderDatabase(0);
}
@@ -64,7 +66,7 @@ namespace Content.Server.Cargo
}
}
public void Reset()
public void Reset(RoundRestartCleanupEvent ev)
{
_accountsDict.Clear();
_databasesDict.Clear();

View File

@@ -59,7 +59,7 @@ namespace Content.Server.Cargo.Components
[DataField("requestOnly")]
private bool _requestOnly = false;
private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
private bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered;
private CargoConsoleSystem _cargoConsoleSystem = default!;
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(CargoConsoleUiKey.Key);
@@ -171,7 +171,7 @@ namespace Content.Server.Cargo.Components
{
foreach (IEntity entity in enumerator)
{
if (entity.HasComponent<CargoTelepadComponent>() && entity.TryGetComponent<PowerReceiverComponent>(out var powerReceiver) && powerReceiver.Powered)
if (entity.HasComponent<CargoTelepadComponent>() && entity.TryGetComponent<ApcPowerReceiverComponent>(out var powerReceiver) && powerReceiver.Powered)
{
cargoTelepad = entity;
break;

View File

@@ -43,14 +43,14 @@ namespace Content.Server.Cargo.Components
{
if (args.Powered && _currentState == CargoTelepadState.Unpowered) {
_currentState = CargoTelepadState.Idle;
if(Owner.TryGetComponent<SpriteComponent>(out var spriteComponent))
if(Owner.TryGetComponent<SpriteComponent>(out var spriteComponent) && spriteComponent.LayerCount > 0)
spriteComponent.LayerSetState(0, "idle");
TeleportLoop();
}
else if (!args.Powered)
{
_currentState = CargoTelepadState.Unpowered;
if (Owner.TryGetComponent<SpriteComponent>(out var spriteComponent))
if (Owner.TryGetComponent<SpriteComponent>(out var spriteComponent) && spriteComponent.LayerCount > 0)
spriteComponent.LayerSetState(0, "offline");
}
}
@@ -59,14 +59,14 @@ namespace Content.Server.Cargo.Components
if (_currentState == CargoTelepadState.Idle && _teleportQueue.Count > 0)
{
_currentState = CargoTelepadState.Charging;
if (Owner.TryGetComponent<SpriteComponent>(out var spriteComponent))
if (Owner.TryGetComponent<SpriteComponent>(out var spriteComponent) && spriteComponent.LayerCount > 0)
spriteComponent.LayerSetState(0, "idle");
Owner.SpawnTimer((int) (TeleportDelay * 1000), () =>
{
if (!Deleted && !Owner.Deleted && _currentState == CargoTelepadState.Charging && _teleportQueue.Count > 0)
{
_currentState = CargoTelepadState.Teleporting;
if (Owner.TryGetComponent<SpriteComponent>(out var spriteComponent))
if (Owner.TryGetComponent<SpriteComponent>(out var spriteComponent) && spriteComponent.LayerCount > 0)
spriteComponent.LayerSetState(0, "beam");
Owner.SpawnTimer((int) (TeleportDuration * 1000), () =>
{
@@ -75,7 +75,7 @@ namespace Content.Server.Cargo.Components
SoundSystem.Play(Filter.Pvs(Owner), "/Audio/Machines/phasein.ogg", Owner, AudioParams.Default.WithVolume(-8f));
Owner.EntityManager.SpawnEntity(_teleportQueue[0].Product, Owner.Transform.Coordinates);
_teleportQueue.RemoveAt(0);
if (Owner.TryGetComponent<SpriteComponent>(out var spriteComponent))
if (Owner.TryGetComponent<SpriteComponent>(out var spriteComponent) && spriteComponent.LayerCount > 0)
spriteComponent.LayerSetState(0, "idle");
_currentState = CargoTelepadState.Idle;
TeleportLoop();

View File

@@ -40,7 +40,7 @@ namespace Content.Server.Chemistry.Components
[ViewVariables] private bool HasBeaker => _beakerContainer.ContainedEntity != null;
[ViewVariables] private bool _bufferModeTransfer = true;
[ViewVariables] private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
[ViewVariables] private bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered;
[ViewVariables] private readonly Solution BufferSolution = new();

View File

@@ -51,7 +51,7 @@ namespace Content.Server.Chemistry.Components
[ViewVariables] private ReagentUnit _dispenseAmount = ReagentUnit.New(10);
[UsedImplicitly] [ViewVariables] private SolutionContainerComponent? Solution => _beakerContainer.ContainedEntity?.GetComponent<SolutionContainerComponent>();
[ViewVariables] private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
[ViewVariables] private bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered;
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(ReagentDispenserUiKey.Key);

View File

@@ -8,10 +8,17 @@ using Robust.Shared.GameObjects;
namespace Content.Server.Climbing
{
[UsedImplicitly]
internal sealed class ClimbSystem : EntitySystem, IResettingEntitySystem
internal sealed class ClimbSystem : EntitySystem
{
private readonly HashSet<ClimbingComponent> _activeClimbers = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
}
public void AddActiveClimber(ClimbingComponent climbingComponent)
{
_activeClimbers.Add(climbingComponent);
@@ -30,7 +37,7 @@ namespace Content.Server.Climbing
}
}
public void Reset()
public void Reset(RoundRestartCleanupEvent ev)
{
_activeClimbers.Clear();
}

View File

@@ -157,7 +157,7 @@ namespace Content.Server.Climbing.Components
BreakOnStun = true
};
var result = await EntitySystem.Get<DoAfterSystem>().DoAfter(doAfterEventArgs);
var result = await EntitySystem.Get<DoAfterSystem>().WaitDoAfter(doAfterEventArgs);
if (result != DoAfterStatus.Cancelled && entityToMove.TryGetComponent(out PhysicsComponent? body) && body.Fixtures.Count >= 1)
{
@@ -204,7 +204,7 @@ namespace Content.Server.Climbing.Components
BreakOnStun = true
};
var result = await EntitySystem.Get<DoAfterSystem>().DoAfter(doAfterEventArgs);
var result = await EntitySystem.Get<DoAfterSystem>().WaitDoAfter(doAfterEventArgs);
if (result != DoAfterStatus.Cancelled && user.TryGetComponent(out PhysicsComponent? body) && body.Fixtures.Count >= 1)
{

View File

@@ -16,7 +16,7 @@ using static Content.Shared.Cloning.SharedCloningPodComponent;
namespace Content.Server.Cloning
{
internal sealed class CloningSystem : EntitySystem, IResettingEntitySystem
internal sealed class CloningSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
public readonly Dictionary<Mind.Mind, int> MindToId = new();
@@ -29,6 +29,7 @@ namespace Content.Server.Cloning
{
base.Initialize();
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
SubscribeLocalEvent<CloningPodComponent, ActivateInWorldEvent>(HandleActivate);
SubscribeLocalEvent<BeingClonedComponent, MindAddedMessage>(HandleMindAdded);
}
@@ -73,7 +74,7 @@ namespace Content.Server.Cloning
public override void Update(float frameTime)
{
foreach (var (cloning, power) in ComponentManager.EntityQuery<CloningPodComponent, PowerReceiverComponent>(true))
foreach (var (cloning, power) in ComponentManager.EntityQuery<CloningPodComponent, ApcPowerReceiverComponent>(true))
{
if (cloning.UiKnownPowerState != power.Powered)
{
@@ -142,7 +143,7 @@ namespace Content.Server.Cloning
return IdToDNA.ToDictionary(m => m.Key, m => m.Value.Mind.CharacterName);
}
public void Reset()
public void Reset(RoundRestartCleanupEvent ev)
{
MindToId.Clear();
IdToDNA.Clear();

View File

@@ -26,7 +26,7 @@ namespace Content.Server.Cloning.Components
[Dependency] private readonly EuiManager _euiManager = null!;
[ViewVariables]
public bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
public bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered;
[ViewVariables]
public BoundUserInterface? UserInterface =>

View File

@@ -25,7 +25,7 @@ namespace Content.Server.Communications
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
private bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered;
private RoundEndSystem RoundEndSystem => EntitySystem.Get<RoundEndSystem>();

View File

@@ -24,7 +24,7 @@ namespace Content.Server.Computer
// Let's ensure the container manager and container are here.
Owner.EnsureContainer<Container>("board", out var _);
if (Owner.TryGetComponent(out PowerReceiverComponent? powerReceiver) &&
if (Owner.TryGetComponent(out ApcPowerReceiverComponent? powerReceiver) &&
Owner.TryGetComponent(out AppearanceComponent? appearance))
{
appearance.SetData(ComputerVisuals.Powered, powerReceiver.Powered);

View File

@@ -258,7 +258,7 @@ namespace Content.Server.Construction.Components
{
case ArbitraryInsertConstructionGraphStep arbitraryStep:
if (arbitraryStep.EntityValid(eventArgs.Using)
&& await doAfterSystem.DoAfter(doAfterArgs) == DoAfterStatus.Finished)
&& await doAfterSystem.WaitDoAfter(doAfterArgs) == DoAfterStatus.Finished)
{
valid = true;
}
@@ -267,7 +267,7 @@ namespace Content.Server.Construction.Components
case MaterialConstructionGraphStep materialStep:
if (materialStep.EntityValid(eventArgs.Using, out var stack)
&& await doAfterSystem.DoAfter(doAfterArgs) == DoAfterStatus.Finished)
&& await doAfterSystem.WaitDoAfter(doAfterArgs) == DoAfterStatus.Finished)
{
var splitStack = EntitySystem.Get<StackSystem>().Split(eventArgs.Using.Uid, stack, materialStep.Amount, eventArgs.User.Transform.Coordinates);

View File

@@ -1,6 +1,6 @@
using System.Threading.Tasks;
using Content.Server.GameObjects.Components;
using Content.Server.Wires.Components;
using Content.Server.WireHacking;
using Content.Shared.Construction;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;

View File

@@ -1,6 +1,6 @@
using System.Threading.Tasks;
using Content.Server.GameObjects.Components;
using Content.Server.Wires.Components;
using Content.Server.WireHacking;
using Content.Shared.Construction;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;

View File

@@ -238,7 +238,7 @@ namespace Content.Server.Construction
NeedHand = false,
};
if (await doAfterSystem.DoAfter(doAfterArgs) == DoAfterStatus.Cancelled)
if (await doAfterSystem.WaitDoAfter(doAfterArgs) == DoAfterStatus.Cancelled)
{
FailCleanup();
return null;
@@ -457,9 +457,16 @@ namespace Content.Server.Construction
return;
}
// We do this to be able to move the construction to its proper position in case it's anchored...
// Oh wow transform anchoring is amazing wow I love it!!!!
var wasAnchored = structure.Transform.Anchored;
structure.Transform.Anchored = false;
structure.Transform.Coordinates = ev.Location;
structure.Transform.LocalRotation = constructionPrototype.CanRotate ? ev.Angle : Angle.Zero;
structure.Transform.Anchored = wasAnchored;
RaiseNetworkEvent(new AckStructureConstructionMessage(ev.Ack));
Cleanup();

View File

@@ -19,7 +19,7 @@ namespace Content.Server.Conveyor
{
public override string Name => "Conveyor";
[ViewVariables] private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
[ViewVariables] private bool Powered => !Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) || receiver.Powered;
/// <summary>
/// The angle to move entities by in relation to the owner's rotation.
@@ -105,7 +105,7 @@ namespace Content.Server.Conveyor
return false;
}
if (Owner.TryGetComponent(out PowerReceiverComponent? receiver) &&
if (Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) &&
!receiver.Powered)
{
return false;

View File

@@ -253,7 +253,7 @@ namespace Content.Server.Cuffs.Components
var doAfterSystem = EntitySystem.Get<DoAfterSystem>();
_uncuffing = true;
var result = await doAfterSystem.DoAfter(doAfterEventArgs);
var result = await doAfterSystem.WaitDoAfter(doAfterEventArgs);
_uncuffing = false;

View File

@@ -214,7 +214,7 @@ namespace Content.Server.Cuffs.Components
_cuffing = true;
var result = await EntitySystem.Get<DoAfterSystem>().DoAfter(doAfterEventArgs);
var result = await EntitySystem.Get<DoAfterSystem>().WaitDoAfter(doAfterEventArgs);
_cuffing = false;

View File

@@ -12,11 +12,18 @@ using Robust.Shared.GameObjects;
namespace Content.Server.Damage
{
[UsedImplicitly]
public class GodmodeSystem : EntitySystem, IResettingEntitySystem
public class GodmodeSystem : EntitySystem
{
private readonly Dictionary<IEntity, OldEntityInformation> _entities = new();
public void Reset()
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
}
public void Reset(RoundRestartCleanupEvent ev)
{
_entities.Clear();
}

View File

@@ -26,7 +26,7 @@ namespace Content.Server.DeviceNetwork.Connections
return false;
}
if (_owner.TryGetComponent<PowerReceiverComponent>(out var powerReceiver)
if (_owner.TryGetComponent<ApcPowerReceiverComponent>(out var powerReceiver)
&& TryGetWireNet(powerReceiver, out var ownNet)
&& metadata.TryParseMetadata<INodeGroup>(WIRENET, out var senderNet))
{
@@ -44,7 +44,7 @@ namespace Content.Server.DeviceNetwork.Connections
return new Metadata();
}
if (_owner.TryGetComponent<PowerReceiverComponent>(out var powerReceiver)
if (_owner.TryGetComponent<ApcPowerReceiverComponent>(out var powerReceiver)
&& TryGetWireNet(powerReceiver, out var net))
{
var metadata = new Metadata
@@ -63,16 +63,16 @@ namespace Content.Server.DeviceNetwork.Connections
return payload;
}
private bool TryGetWireNet(PowerReceiverComponent powerReceiver, [NotNullWhen(true)] out INodeGroup? net)
private bool TryGetWireNet(ApcPowerReceiverComponent apcPowerReceiver, [NotNullWhen(true)] out INodeGroup? net)
{
if (powerReceiver.Provider is PowerProviderComponent provider &&
provider.ProviderOwner.TryGetComponent<NodeContainerComponent>(out var nodeContainer))
var provider = apcPowerReceiver.Provider;
if (provider != null && provider.ProviderOwner.TryGetComponent<NodeContainerComponent>(out var nodeContainer))
{
var nodes = nodeContainer.Nodes;
foreach (var node in nodes.Values)
{
if (node.NodeGroupID == NodeGroupID.WireNet)
if (node.NodeGroupID == NodeGroupID.WireNet && node.NodeGroup != null)
{
net = node.NodeGroup;
return true;

View File

@@ -1,6 +1,7 @@
using Content.Shared.Audio;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Sound;
using Content.Shared.Throwing;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
@@ -24,15 +25,16 @@ namespace Content.Server.Dice
public override string Name => "Dice";
[DataField("step")]
private int _step = 1;
private int _sides = 20;
private int _currentSide = 20;
[ViewVariables]
[DataField("diceSoundCollection")]
public string _soundCollectionName = "dice";
[DataField("sound")]
private readonly SoundSpecifier _sound = new SoundCollectionSpecifier("Dice");
[ViewVariables]
public int Step => _step;
[DataField("step")]
public int Step { get; } = 1;
[ViewVariables]
[DataField("sides")]
public int Sides
@@ -41,29 +43,28 @@ namespace Content.Server.Dice
set
{
_sides = value;
_currentSide = value;
CurrentSide = value;
}
}
[ViewVariables]
public int CurrentSide => _currentSide;
public int CurrentSide { get; private set; } = 20;
public void Roll()
{
_currentSide = _random.Next(1, (_sides/_step)+1) * _step;
if (!Owner.TryGetComponent(out SpriteComponent? sprite)) return;
sprite.LayerSetState(0, $"d{_sides}{_currentSide}");
CurrentSide = _random.Next(1, (_sides/Step)+1) * Step;
PlayDiceEffect();
if (!Owner.TryGetComponent(out SpriteComponent? sprite))
return;
sprite.LayerSetState(0, $"d{_sides}{CurrentSide}");
}
public void PlayDiceEffect()
{
if (!string.IsNullOrWhiteSpace(_soundCollectionName))
{
var soundCollection = _prototypeManager.Index<SoundCollectionPrototype>(_soundCollectionName);
var file = _random.Pick(soundCollection.PickFiles);
SoundSystem.Play(Filter.Pvs(Owner), file, Owner, AudioParams.Default);
}
SoundSystem.Play(Filter.Pvs(Owner), _sound.GetSound(), Owner, AudioParams.Default);
}
void IActivate.Activate(ActivateEventArgs eventArgs)
@@ -89,7 +90,7 @@ namespace Content.Server.Dice
("sidesAmount", _sides))
+ "\n" +
Loc.GetString("dice-component-on-examine-message-part-2",
("currentSide", _currentSide)));
("currentSide", CurrentSide)));
}
}
}

View File

@@ -117,7 +117,7 @@ namespace Content.Server.Disposal.Mailing
[ViewVariables]
public bool Powered =>
!Owner.TryGetComponent(out PowerReceiverComponent? receiver) ||
!Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) ||
receiver.Powered;
[ViewVariables]
@@ -217,7 +217,7 @@ namespace Content.Server.Disposal.Mailing
NeedHand = false,
};
var result = await doAfterSystem.DoAfter(doAfterArgs);
var result = await doAfterSystem.WaitDoAfter(doAfterArgs);
if (result == DoAfterStatus.Cancelled)
return false;
@@ -372,7 +372,7 @@ namespace Content.Server.Disposal.Mailing
private void TogglePower()
{
if (!Owner.TryGetComponent(out PowerReceiverComponent? receiver))
if (!Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver))
{
return;
}

View File

@@ -107,7 +107,7 @@ namespace Content.Server.Disposal.Unit.Components
[ViewVariables]
public bool Powered =>
!Owner.TryGetComponent(out PowerReceiverComponent? receiver) ||
!Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver) ||
receiver.Powered;
[ViewVariables]
@@ -196,7 +196,7 @@ namespace Content.Server.Disposal.Unit.Components
NeedHand = false,
};
var result = await doAfterSystem.DoAfter(doAfterArgs);
var result = await doAfterSystem.WaitDoAfter(doAfterArgs);
if (result == DoAfterStatus.Cancelled)
return false;
@@ -314,7 +314,7 @@ namespace Content.Server.Disposal.Unit.Components
private void TogglePower()
{
if (!Owner.TryGetComponent(out PowerReceiverComponent? receiver))
if (!Owner.TryGetComponent(out ApcPowerReceiverComponent? receiver))
{
return;
}

View File

@@ -4,6 +4,7 @@ using System.Threading.Tasks;
using Content.Server.Hands.Components;
using Content.Server.Items;
using Content.Server.Stunnable.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Timing;
@@ -16,7 +17,7 @@ namespace Content.Server.DoAfter
private TaskCompletionSource<DoAfterStatus> Tcs { get; }
public DoAfterEventArgs EventArgs;
public readonly DoAfterEventArgs EventArgs;
public TimeSpan StartTime { get; }

View File

@@ -81,6 +81,36 @@ namespace Content.Server.DoAfter
/// </summary>
public Func<bool>? ExtraCheck { get; set; }
/// <summary>
/// Event to be raised directed to the <see cref="User"/> entity when the DoAfter is cancelled.
/// </summary>
public EntityEventArgs? UserCancelledEvent { get; set; }
/// <summary>
/// Event to be raised directed to the <see cref="User"/> entity when the DoAfter is finished successfully.
/// </summary>
public EntityEventArgs? UserFinishedEvent { get; set; }
/// <summary>
/// Event to be raised directed to the <see cref="User"/> entity when the DoAfter is cancelled.
/// </summary>
public EntityEventArgs? TargetCancelledEvent { get; set; }
/// <summary>
/// Event to be raised directed to the <see cref="User"/> entity when the DoAfter is finished successfully.
/// </summary>
public EntityEventArgs? TargetFinishedEvent { get; set; }
/// <summary>
/// Event to be broadcast when the DoAfter is cancelled.
/// </summary>
public object? BroadcastCancelledEvent { get; set; }
/// <summary>
/// Event to be broadcast when the DoAfter is finished successfully.
/// </summary>
public object? BroadcastFinishedEvent { get; set; }
public DoAfterEventArgs(
IEntity user,
float delay,

View File

@@ -11,15 +11,16 @@ namespace Content.Server.DoAfter
[UsedImplicitly]
public sealed class DoAfterSystem : EntitySystem
{
// We cache these lists as to not allocate them every update tick...
private readonly List<DoAfter> _cancelled = new();
private readonly List<DoAfter> _finished = new();
public override void Update(float frameTime)
{
base.Update(frameTime);
foreach (var comp in ComponentManager.EntityQuery<DoAfterComponent>(true))
{
var cancelled = new List<DoAfter>(0);
var finished = new List<DoAfter>(0);
foreach (var doAfter in comp.DoAfters.ToArray())
{
doAfter.Run(frameTime);
@@ -29,27 +30,47 @@ namespace Content.Server.DoAfter
case DoAfterStatus.Running:
break;
case DoAfterStatus.Cancelled:
cancelled.Add(doAfter);
_cancelled.Add(doAfter);
break;
case DoAfterStatus.Finished:
finished.Add(doAfter);
_finished.Add(doAfter);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
foreach (var doAfter in cancelled)
foreach (var doAfter in _cancelled)
{
comp.Cancelled(doAfter);
if(!doAfter.EventArgs.User.Deleted && doAfter.EventArgs.UserCancelledEvent != null)
RaiseLocalEvent(doAfter.EventArgs.User.Uid, doAfter.EventArgs.UserCancelledEvent, false);
if(doAfter.EventArgs.Target is { Deleted: false } && doAfter.EventArgs.TargetCancelledEvent != null)
RaiseLocalEvent(doAfter.EventArgs.Target.Uid, doAfter.EventArgs.TargetCancelledEvent, false);
if(doAfter.EventArgs.BroadcastCancelledEvent != null)
RaiseLocalEvent(doAfter.EventArgs.BroadcastCancelledEvent);
}
foreach (var doAfter in finished)
foreach (var doAfter in _finished)
{
comp.Finished(doAfter);
if(!doAfter.EventArgs.User.Deleted && doAfter.EventArgs.UserFinishedEvent != null)
RaiseLocalEvent(doAfter.EventArgs.User.Uid, doAfter.EventArgs.UserFinishedEvent, false);
if(doAfter.EventArgs.Target is { Deleted: false } && doAfter.EventArgs.TargetFinishedEvent != null)
RaiseLocalEvent(doAfter.EventArgs.Target.Uid, doAfter.EventArgs.TargetFinishedEvent, false);
if(doAfter.EventArgs.BroadcastFinishedEvent != null)
RaiseLocalEvent(doAfter.EventArgs.BroadcastFinishedEvent);
}
finished.Clear();
// Clean the shared lists at the end, ensuring they'll be clean for the next time we need them.
_cancelled.Clear();
_finished.Clear();
}
}
@@ -59,17 +80,33 @@ namespace Content.Server.DoAfter
/// </summary>
/// <param name="eventArgs"></param>
/// <returns></returns>
public async Task<DoAfterStatus> DoAfter(DoAfterEventArgs eventArgs)
public async Task<DoAfterStatus> WaitDoAfter(DoAfterEventArgs eventArgs)
{
var doAfter = CreateDoAfter(eventArgs);
await doAfter.AsTask;
return doAfter.Status;
}
/// <summary>
/// Creates a DoAfter without waiting for it to finish. You can use events with this.
/// These can be potentially cancelled by the user moving or when other things happen.
/// </summary>
/// <param name="eventArgs"></param>
public void DoAfter(DoAfterEventArgs eventArgs)
{
CreateDoAfter(eventArgs);
}
private DoAfter CreateDoAfter(DoAfterEventArgs eventArgs)
{
// Setup
var doAfter = new DoAfter(eventArgs);
// Caller's gonna be responsible for this I guess
var doAfterComponent = eventArgs.User.GetComponent<DoAfterComponent>();
doAfterComponent.Add(doAfter);
await doAfter.AsTask;
return doAfter.Status;
return doAfter;
}
}

View File

@@ -3,7 +3,7 @@ using System;
using System.Threading;
using Content.Server.Power.Components;
using Content.Server.VendingMachines;
using Content.Server.Wires.Components;
using Content.Server.WireHacking;
using Content.Shared.Doors;
using Content.Shared.Interaction;
using Content.Shared.Notification;
@@ -36,7 +36,7 @@ namespace Content.Server.Doors.Components
private readonly SharedAppearanceComponent? _appearanceComponent = null;
[ComponentDependency]
private readonly PowerReceiverComponent? _receiverComponent = null;
private readonly ApcPowerReceiverComponent? _receiverComponent = null;
[ComponentDependency]
private readonly WiresComponent? _wiresComponent = null;

View File

@@ -33,7 +33,7 @@ namespace Content.Server.Engineering.EntitySystems
BreakOnUserMove = true,
BreakOnStun = true,
};
var result = await doAfterSystem.DoAfter(doAfterArgs);
var result = await doAfterSystem.WaitDoAfter(doAfterArgs);
if (result != DoAfterStatus.Finished)
return;

View File

@@ -50,7 +50,7 @@ namespace Content.Server.Engineering.EntitySystems
BreakOnStun = true,
PostCheck = IsTileClear,
};
var result = await doAfterSystem.DoAfter(doAfterArgs);
var result = await doAfterSystem.WaitDoAfter(doAfterArgs);
if (result != DoAfterStatus.Finished)
return;

View File

@@ -82,7 +82,7 @@ namespace Content.Server.Fluids.Components
BreakOnStun = true,
BreakOnDamage = true,
};
var result = await EntitySystem.Get<DoAfterSystem>().DoAfter(doAfterArgs);
var result = await EntitySystem.Get<DoAfterSystem>().WaitDoAfter(doAfterArgs);
_currentlyUsing.Remove(eventArgs.Using.Uid);

Some files were not shown because too many files have changed in this diff Show More