This commit is contained in:
ElectroJr
2025-02-17 01:16:22 +13:00
664 changed files with 79784 additions and 224737 deletions

45
.github/workflows/publish-testing.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: Publish Testing
concurrency:
group: publish-testing
on:
workflow_dispatch:
schedule:
- cron: '0 10 * * *'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.6.0
with:
submodules: 'recursive'
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 9.0.x
- name: Get Engine Tag
run: |
cd RobustToolbox
git fetch --depth=1
- name: Install dependencies
run: dotnet restore
- name: Build Packaging
run: dotnet build Content.Packaging --configuration Release --no-restore /m
- name: Package server
run: dotnet run --project Content.Packaging server --platform win-x64 --platform linux-x64 --platform osx-x64 --platform linux-arm64
- name: Package client
run: dotnet run --project Content.Packaging client --no-wipe-release
- name: Publish version
run: Tools/publish_multi_request.py --fork-id wizards-testing
env:
PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
GITHUB_REPOSITORY: ${{ vars.GITHUB_REPOSITORY }}

View File

@@ -12,12 +12,12 @@ You want to handle the Build, Clean and Rebuild tasks to prevent missing task er
If you want to learn more about these kinds of things, check out Microsoft's official documentation about MSBuild:
https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild
-->
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Python>python3</Python>
<Python Condition="'$(OS)'=='Windows_NT' Or '$(OS)'=='Windows'">py -3</Python>
<ProjectGuid>{C899FCA4-7037-4E49-ABC2-44DE72487110}</ProjectGuid>
<TargetFrameworkMoniker>.NETFramework, Version=v4.7.2</TargetFrameworkMoniker>
<TargetFramework>net4.7.2</TargetFramework>
<RestorePackages>false</RestorePackages>
</PropertyGroup>
<PropertyGroup>

View File

@@ -9,13 +9,14 @@ using Content.IntegrationTests.Pair;
using Content.Shared.Clothing.Components;
using Content.Shared.Doors.Components;
using Content.Shared.Item;
using Robust.Server.GameObjects;
using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Benchmarks;
@@ -32,7 +33,6 @@ public class ComponentQueryBenchmark
private TestPair _pair = default!;
private IEntityManager _entMan = default!;
private MapId _mapId = new(10);
private EntityQuery<ItemComponent> _itemQuery;
private EntityQuery<ClothingComponent> _clothingQuery;
private EntityQuery<MapComponent> _mapQuery;
@@ -54,10 +54,10 @@ public class ComponentQueryBenchmark
_pair.Server.ResolveDependency<IRobustRandom>().SetSeed(42);
_pair.Server.WaitPost(() =>
{
var success = _entMan.System<MapLoaderSystem>().TryLoad(_mapId, Map, out _);
if (!success)
var map = new ResPath(Map);
var opts = DeserializationOptions.Default with {InitializeMaps = true};
if (!_entMan.System<MapLoaderSystem>().TryLoadMap(map, out _, out _, opts))
throw new Exception("Map load failed");
_pair.Server.MapMan.DoMapInitialize(_mapId);
}).GetAwaiter().GetResult();
_items = new EntityUid[_entMan.Count<ItemComponent>()];

View File

@@ -6,12 +6,13 @@ using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
using Content.Server.Maps;
using Robust.Server.GameObjects;
using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Benchmarks;
@@ -20,7 +21,7 @@ public class MapLoadBenchmark
{
private TestPair _pair = default!;
private MapLoaderSystem _mapLoader = default!;
private IMapManager _mapManager = default!;
private SharedMapSystem _mapSys = default!;
[GlobalSetup]
public void Setup()
@@ -36,7 +37,7 @@ public class MapLoadBenchmark
.ToDictionary(x => x.ID, x => x.MapPath.ToString());
_mapLoader = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<MapLoaderSystem>();
_mapManager = server.ResolveDependency<IMapManager>();
_mapSys = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<SharedMapSystem>();
}
[GlobalCleanup]
@@ -46,23 +47,25 @@ public class MapLoadBenchmark
PoolManager.Shutdown();
}
public static readonly string[] MapsSource = { "Empty", "Satlern", "Box", "Bagel", "Dev", "CentComm", "Core", "TestTeg", "Packed", "Omega", "Reach", "Meta", "Marathon", "MeteorArena", "Fland", "Oasis", "Cog", "Convex"};
public static readonly string[] MapsSource = { "Empty", "Satlern", "Box", "Bagel", "Dev", "CentComm", "Core", "TestTeg", "Packed", "Omega", "Reach", "Meta", "Marathon", "MeteorArena", "Fland", "Oasis", "Convex"};
[ParamsSource(nameof(MapsSource))]
public string Map;
public Dictionary<string, string> Paths;
private MapId _mapId;
[Benchmark]
public async Task LoadMap()
{
var mapPath = Paths[Map];
var mapPath = new ResPath(Paths[Map]);
var server = _pair.Server;
await server.WaitPost(() =>
{
var success = _mapLoader.TryLoad(new MapId(10), mapPath, out _);
var success = _mapLoader.TryLoadMap(mapPath, out var map, out _);
if (!success)
throw new Exception("Map load failed");
_mapId = map.Value.Comp.MapId;
});
}
@@ -70,9 +73,7 @@ public class MapLoadBenchmark
public void IterationCleanup()
{
var server = _pair.Server;
server.WaitPost(() =>
{
_mapManager.DeleteMap(new MapId(10));
}).Wait();
server.WaitPost(() => _mapSys.DeleteMap(_mapId))
.Wait();
}
}

View File

@@ -7,13 +7,15 @@ using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
using Content.Server.Mind;
using Content.Server.Warps;
using Robust.Server.GameObjects;
using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Benchmarks;
@@ -34,7 +36,6 @@ public class PvsBenchmark
private TestPair _pair = default!;
private IEntityManager _entMan = default!;
private MapId _mapId = new(10);
private ICommonSession[] _players = default!;
private EntityCoordinates[] _spawns = default!;
public int _cycleOffset = 0;
@@ -65,10 +66,10 @@ public class PvsBenchmark
_pair.Server.ResolveDependency<IRobustRandom>().SetSeed(42);
await _pair.Server.WaitPost(() =>
{
var success = _entMan.System<MapLoaderSystem>().TryLoad(_mapId, Map, out _);
if (!success)
var path = new ResPath(Map);
var opts = DeserializationOptions.Default with {InitializeMaps = true};
if (!_entMan.System<MapLoaderSystem>().TryLoadMap(path, out _, out _, opts))
throw new Exception("Map load failed");
_pair.Server.MapMan.DoMapInitialize(_mapId);
});
// Get list of ghost warp positions

View File

@@ -88,6 +88,7 @@ namespace Content.Client.Actions
return;
component.Whitelist = state.Whitelist;
component.Blacklist = state.Blacklist;
component.CanTargetSelf = state.CanTargetSelf;
BaseHandleState<EntityTargetActionComponent>(uid, component, state);
}
@@ -137,6 +138,7 @@ namespace Content.Client.Actions
component.Priority = state.Priority;
component.AttachedEntity = EnsureEntity<T>(state.AttachedEntity, uid);
component.RaiseOnUser = state.RaiseOnUser;
component.RaiseOnAction = state.RaiseOnAction;
component.AutoPopulate = state.AutoPopulate;
component.Temporary = state.Temporary;
component.ItemIconStyle = state.ItemIconStyle;

View File

@@ -11,6 +11,8 @@ public sealed class AtmosAlertsComputerBoundUserInterface : BoundUserInterface
protected override void Open()
{
base.Open();
_menu = new AtmosAlertsComputerWindow(this, Owner);
_menu.OpenCentered();
_menu.OnClose += Close;

View File

@@ -16,6 +16,7 @@ namespace Content.Client.Atmos.EntitySystems
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IOverlayManager _overlayMan = default!;
[Dependency] private readonly SpriteSystem _spriteSys = default!;
[Dependency] private readonly SharedTransformSystem _xformSys = default!;
private GasTileOverlay _overlay = default!;
@@ -25,7 +26,7 @@ namespace Content.Client.Atmos.EntitySystems
SubscribeNetworkEvent<GasOverlayUpdateEvent>(HandleGasOverlayUpdate);
SubscribeLocalEvent<GasTileOverlayComponent, ComponentHandleState>(OnHandleState);
_overlay = new GasTileOverlay(this, EntityManager, _resourceCache, ProtoMan, _spriteSys);
_overlay = new GasTileOverlay(this, EntityManager, _resourceCache, ProtoMan, _spriteSys, _xformSys);
_overlayMan.AddOverlay(_overlay);
}

View File

@@ -21,6 +21,7 @@ namespace Content.Client.Atmos.Overlays
{
private readonly IEntityManager _entManager;
private readonly IMapManager _mapManager;
private readonly SharedTransformSystem _xformSys;
public override OverlaySpace Space => OverlaySpace.WorldSpaceEntities | OverlaySpace.WorldSpaceBelowWorld;
private readonly ShaderInstance _shader;
@@ -46,10 +47,11 @@ namespace Content.Client.Atmos.Overlays
public const int GasOverlayZIndex = (int) Shared.DrawDepth.DrawDepth.Effects; // Under ghosts, above mostly everything else
public GasTileOverlay(GasTileOverlaySystem system, IEntityManager entManager, IResourceCache resourceCache, IPrototypeManager protoMan, SpriteSystem spriteSys)
public GasTileOverlay(GasTileOverlaySystem system, IEntityManager entManager, IResourceCache resourceCache, IPrototypeManager protoMan, SpriteSystem spriteSys, SharedTransformSystem xformSys)
{
_entManager = entManager;
_mapManager = IoCManager.Resolve<IMapManager>();
_xformSys = xformSys;
_shader = protoMan.Index<ShaderPrototype>("unshaded").Instance();
ZIndex = GasOverlayZIndex;
@@ -158,7 +160,8 @@ namespace Content.Client.Atmos.Overlays
_fireFrameCounter,
_shader,
overlayQuery,
xformQuery);
xformQuery,
_xformSys);
var mapUid = _mapManager.GetMapEntityId(args.MapId);
@@ -180,7 +183,8 @@ namespace Content.Client.Atmos.Overlays
int[] fireFrameCounter,
ShaderInstance shader,
EntityQuery<GasTileOverlayComponent> overlayQuery,
EntityQuery<TransformComponent> xformQuery) state) =>
EntityQuery<TransformComponent> xformQuery,
SharedTransformSystem xformSys) state) =>
{
if (!state.overlayQuery.TryGetComponent(uid, out var comp) ||
!state.xformQuery.TryGetComponent(uid, out var gridXform))
@@ -188,7 +192,7 @@ namespace Content.Client.Atmos.Overlays
return true;
}
var (_, _, worldMatrix, invMatrix) = gridXform.GetWorldPositionRotationMatrixWithInv();
var (_, _, worldMatrix, invMatrix) = state.xformSys.GetWorldPositionRotationMatrixWithInv(gridXform);
state.drawHandle.SetTransform(worldMatrix);
var floatBounds = invMatrix.TransformBox(state.WorldBounds).Enlarged(grid.TileSize);
var localBounds = new Box2i(

View File

@@ -1,6 +1,6 @@
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
MinSize="280 160" Title="Temperature Control Unit">
MinSize="280 160" Title="{Loc comp-space-heater-ui-title}">
<BoxContainer Name="VboxContainer" Orientation="Vertical" Margin="5 5 5 5" SeparationOverride="10">

View File

@@ -3,13 +3,15 @@ using Content.Shared.Buckle;
using Content.Shared.Buckle.Components;
using Content.Shared.Rotation;
using Robust.Client.GameObjects;
using Robust.Shared.GameStates;
using Robust.Client.Graphics;
namespace Content.Client.Buckle;
internal sealed class BuckleSystem : SharedBuckleSystem
{
[Dependency] private readonly RotationVisualizerSystem _rotationVisualizerSystem = default!;
[Dependency] private readonly IEyeManager _eye = default!;
[Dependency] private readonly SharedTransformSystem _xformSystem = default!;
public override void Initialize()
{
@@ -17,6 +19,8 @@ internal sealed class BuckleSystem : SharedBuckleSystem
SubscribeLocalEvent<BuckleComponent, AppearanceChangeEvent>(OnAppearanceChange);
SubscribeLocalEvent<StrapComponent, MoveEvent>(OnStrapMoveEvent);
SubscribeLocalEvent<BuckleComponent, BuckledEvent>(OnBuckledEvent);
SubscribeLocalEvent<BuckleComponent, UnbuckledEvent>(OnUnbuckledEvent);
}
private void OnStrapMoveEvent(EntityUid uid, StrapComponent component, ref MoveEvent args)
@@ -28,13 +32,21 @@ internal sealed class BuckleSystem : SharedBuckleSystem
// This code is garbage, it doesn't work with rotated viewports. I need to finally get around to reworking
// sprite rendering for entity layers & direction dependent sorting.
// Future notes:
// Right now this doesn't handle: other grids, other grids rotating, the camera rotation changing, and many other fun rotation specific things
// The entire thing should be a concern of the engine, or something engine helps to implement properly.
// Give some of the sprite rotations their own drawdepth, maybe as an offset within the rsi, or something like this
// And we won't ever need to set the draw depth manually
if (args.NewRotation == args.OldRotation)
return;
if (!TryComp<SpriteComponent>(uid, out var strapSprite))
return;
var isNorth = Transform(uid).LocalRotation.GetCardinalDir() == Direction.North;
var angle = _xformSystem.GetWorldRotation(uid) + _eye.CurrentEye.Rotation; // Get true screen position, or close enough
var isNorth = angle.GetCardinalDir() == Direction.North;
foreach (var buckledEntity in component.BuckledEntities)
{
if (!TryComp<BuckleComponent>(buckledEntity, out var buckle))
@@ -45,6 +57,7 @@ internal sealed class BuckleSystem : SharedBuckleSystem
if (isNorth)
{
// This will only assign if empty, it won't get overwritten by new depth on multiple calls, which do happen easily
buckle.OriginalDrawDepth ??= buckledSprite.DrawDepth;
buckledSprite.DrawDepth = strapSprite.DrawDepth - 1;
}
@@ -56,6 +69,42 @@ internal sealed class BuckleSystem : SharedBuckleSystem
}
}
/// <summary>
/// Lower the draw depth of the buckled entity without needing for the strap entity to rotate/move.
/// Only do so when the entity is facing screen-local north
/// </summary>
private void OnBuckledEvent(Entity<BuckleComponent> ent, ref BuckledEvent args)
{
if (!TryComp<SpriteComponent>(args.Strap, out var strapSprite))
return;
if (!TryComp<SpriteComponent>(ent.Owner, out var buckledSprite))
return;
var angle = _xformSystem.GetWorldRotation(args.Strap) + _eye.CurrentEye.Rotation; // Get true screen position, or close enough
if (angle.GetCardinalDir() != Direction.North)
return;
ent.Comp.OriginalDrawDepth ??= buckledSprite.DrawDepth;
buckledSprite.DrawDepth = strapSprite.DrawDepth - 1;
}
/// <summary>
/// Was the draw depth of the buckled entity lowered? Reset it upon unbuckling.
/// </summary>
private void OnUnbuckledEvent(Entity<BuckleComponent> ent, ref UnbuckledEvent args)
{
if (!TryComp<SpriteComponent>(ent.Owner, out var buckledSprite))
return;
if (!ent.Comp.OriginalDrawDepth.HasValue)
return;
buckledSprite.DrawDepth = ent.Comp.OriginalDrawDepth.Value;
ent.Comp.OriginalDrawDepth = null;
}
private void OnAppearanceChange(EntityUid uid, BuckleComponent component, ref AppearanceChangeEvent args)
{
if (!TryComp<RotationVisualsComponent>(uid, out var rotVisuals))

View File

@@ -20,7 +20,7 @@ namespace Content.Client.Clickable
"/Textures/Logo",
};
private const float Threshold = 0.25f;
private const float Threshold = 0.1f;
private const int ClickRadius = 2;
[Dependency] private readonly IResourceCache _resourceCache = default!;

View File

@@ -58,9 +58,7 @@
StyleClasses="LabelBig" />
<BoxContainer Orientation="Horizontal"
Margin="0 0 0 5">
<Label Text="{Loc 'crew-monitoring-user-interface-job'}:"
FontColorOverride="DarkGray" />
<Label Text=":"
<Label Text="{Loc 'crew-monitoring-user-interface-job'}"
FontColorOverride="DarkGray" />
<TextureRect Name="PersonJobIcon"
TextureScale="2 2"

View File

@@ -5,17 +5,24 @@ namespace Content.Client.Dice;
public sealed class DiceSystem : SharedDiceSystem
{
protected override void UpdateVisuals(EntityUid uid, DiceComponent? die = null)
public override void Initialize()
{
if (!Resolve(uid, ref die) || !TryComp(uid, out SpriteComponent? sprite))
base.Initialize();
SubscribeLocalEvent<DiceComponent, AfterAutoHandleStateEvent>(OnDiceAfterHandleState);
}
private void OnDiceAfterHandleState(Entity<DiceComponent> entity, ref AfterAutoHandleStateEvent args)
{
if (!TryComp<SpriteComponent>(entity, out var sprite))
return;
// TODO maybe just move each diue to its own RSI?
// TODO maybe just move each die to its own RSI?
var state = sprite.LayerGetState(0).Name;
if (state == null)
return;
var prefix = state.Substring(0, state.IndexOf('_'));
sprite.LayerSetState(0, $"{prefix}_{die.CurrentValue}");
sprite.LayerSetState(0, $"{prefix}_{entity.Comp.CurrentValue}");
}
}

View File

@@ -30,7 +30,9 @@ public sealed class DoAfterSystem : SharedDoAfterSystem
_overlay.RemoveOverlay<DoAfterOverlay>();
}
#pragma warning disable RA0028 // No base call in overriden function
public override void Update(float frameTime)
#pragma warning restore RA0028 // No base call in overriden function
{
// Currently this only predicts do afters initiated by the player.

View File

@@ -1,6 +1,7 @@
using Content.Client.Wires.Visualizers;
using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems;
using Content.Shared.Power;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
@@ -84,7 +85,8 @@ public sealed class AirlockSystem : SharedAirlockSystem
if (!_appearanceSystem.TryGetData<DoorState>(uid, DoorVisuals.State, out var state, args.Component))
state = DoorState.Closed;
if (_appearanceSystem.TryGetData<bool>(uid, DoorVisuals.Powered, out var powered, args.Component) && powered)
if (_appearanceSystem.TryGetData<bool>(uid, PowerDeviceVisuals.Powered, out var powered, args.Component)
&& powered)
{
boltedVisible = _appearanceSystem.TryGetData<bool>(uid, DoorVisuals.BoltLights, out var lights, args.Component)
&& lights && (state == DoorState.Closed || state == DoorState.Welded);

View File

@@ -43,6 +43,8 @@ namespace Content.Client.GameTicking.Managers
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<TickerJoinLobbyEvent>(JoinLobby);
SubscribeNetworkEvent<TickerJoinGameEvent>(JoinGame);
SubscribeNetworkEvent<TickerConnectionStatusEvent>(ConnectionStatus);

View File

@@ -13,6 +13,7 @@ namespace Content.Client.Ghost
[Dependency] private readonly IClientConsoleHost _console = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly PointLightSystem _pointLightSystem = default!;
[Dependency] private readonly ContentEyeSystem _contentEye = default!;
public int AvailableGhostRoleCount { get; private set; }
@@ -79,8 +80,27 @@ namespace Content.Client.Ghost
if (args.Handled)
return;
Popup.PopupEntity(Loc.GetString("ghost-gui-toggle-lighting-manager-popup"), args.Performer);
_contentEye.RequestToggleLight(uid, component);
TryComp<PointLightComponent>(uid, out var light);
if (!component.DrawLight)
{
// normal lighting
Popup.PopupEntity(Loc.GetString("ghost-gui-toggle-lighting-manager-popup-normal"), args.Performer);
_contentEye.RequestEye(component.DrawFov, true);
}
else if (!light?.Enabled ?? false) // skip this option if we have no PointLightComponent
{
// enable personal light
Popup.PopupEntity(Loc.GetString("ghost-gui-toggle-lighting-manager-popup-personal-light"), args.Performer);
_pointLightSystem.SetEnabled(uid, true, light);
}
else
{
// fullbright mode
Popup.PopupEntity(Loc.GetString("ghost-gui-toggle-lighting-manager-popup-fullbright"), args.Performer);
_contentEye.RequestEye(component.DrawFov, false);
_pointLightSystem.SetEnabled(uid, false, light);
}
args.Handled = true;
}

View File

@@ -2,7 +2,6 @@ using Content.Shared.ActionBlocker;
using Content.Shared.Instruments.UI;
using Content.Shared.Interaction;
using Robust.Client.Audio.Midi;
using Robust.Client.GameObjects;
using Robust.Client.Player;
using Robust.Client.UserInterface;
@@ -13,7 +12,6 @@ namespace Content.Client.Instruments.UI
public IEntityManager Entities => EntMan;
[Dependency] public readonly IMidiManager MidiManager = default!;
[Dependency] public readonly IFileDialogManager FileDialogManager = default!;
[Dependency] public readonly IPlayerManager PlayerManager = default!;
[Dependency] public readonly ILocalizationManager Loc = default!;
public readonly InstrumentSystem Instruments;
@@ -41,6 +39,8 @@ namespace Content.Client.Instruments.UI
protected override void Open()
{
base.Open();
_instrumentMenu = this.CreateWindow<InstrumentMenu>();
_instrumentMenu.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName;

View File

@@ -256,7 +256,7 @@ public sealed class DragDropSystem : SharedDragDropSystem
dragSprite.DrawDepth = (int) DrawDepth.Overlays;
if (!dragSprite.NoRotation)
{
Transform(_dragShadow.Value).WorldRotation = Transform(_draggedEntity.Value).WorldRotation;
_transformSystem.SetWorldRotationNoLerp(_dragShadow.Value, _transformSystem.GetWorldRotation(_draggedEntity.Value));
}
// drag initiated

View File

@@ -0,0 +1,11 @@
using Content.Shared.ItemRecall;
namespace Content.Client.ItemRecall;
/// <summary>
/// System for handling the ItemRecall ability for wizards.
/// </summary>
public sealed partial class ItemRecallSystem : SharedItemRecallSystem
{
}

View File

@@ -33,8 +33,13 @@ namespace Content.Client.Labels.UI
_focused = false;
LabelLineEdit.Text = _label;
};
}
// Give the editor keybard focus, since that's the only
protected override void Opened()
{
base.Opened();
// Give the editor keyboard focus, since that's the only
// thing the user will want to be doing with this UI
LabelLineEdit.GrabKeyboardFocus();
}

View File

@@ -64,7 +64,7 @@ public sealed partial class LatheMenu : DefaultWindow
if (_entityManager.TryGetComponent<LatheComponent>(Entity, out var latheComponent))
{
if (!latheComponent.DynamicRecipes.Any())
if (!latheComponent.DynamicPacks.Any())
{
ServerListButton.Visible = false;
}

View File

@@ -0,0 +1,58 @@
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
namespace Content.Client.Light;
/// <summary>
/// This exists just to copy <see cref="BeforeLightTargetOverlay"/> to the light render target
/// </summary>
public sealed class AfterLightTargetOverlay : Overlay
{
public override OverlaySpace Space => OverlaySpace.BeforeLighting;
[Dependency] private readonly IOverlayManager _overlay = default!;
public const int ContentZIndex = LightBlurOverlay.ContentZIndex + 1;
public AfterLightTargetOverlay()
{
IoCManager.InjectDependencies(this);
ZIndex = ContentZIndex;
}
protected override void Draw(in OverlayDrawArgs args)
{
var viewport = args.Viewport;
var worldHandle = args.WorldHandle;
if (viewport.Eye == null)
return;
var lightOverlay = _overlay.GetOverlay<BeforeLightTargetOverlay>();
var bounds = args.WorldBounds;
// at 1-1 render scale it's mostly fine but at 4x4 it's way too fkn big
var newScale = viewport.RenderScale / 2f;
var localMatrix =
viewport.LightRenderTarget.GetWorldToLocalMatrix(viewport.Eye, newScale);
var diff = (lightOverlay.EnlargedLightTarget.Size - viewport.LightRenderTarget.Size);
var halfDiff = diff / 2;
// Pixels -> Metres -> Half distance.
// If we're zoomed in need to enlarge the bounds further.
args.WorldHandle.RenderInRenderTarget(viewport.LightRenderTarget,
() =>
{
// We essentially need to draw the cropped version onto the lightrendertarget.
var subRegion = new UIBox2i(halfDiff.X,
halfDiff.Y,
viewport.LightRenderTarget.Size.X + halfDiff.X,
viewport.LightRenderTarget.Size.Y + halfDiff.Y);
worldHandle.SetTransform(localMatrix);
worldHandle.DrawTextureRectRegion(lightOverlay.EnlargedLightTarget.Texture, bounds, subRegion: subRegion);
}, null);
}
}

View File

@@ -0,0 +1,51 @@
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
namespace Content.Client.Light;
/// <summary>
/// Handles an enlarged lighting target so content can use large blur radii.
/// </summary>
public sealed class BeforeLightTargetOverlay : Overlay
{
public override OverlaySpace Space => OverlaySpace.BeforeLighting;
[Dependency] private readonly IClyde _clyde = default!;
public IRenderTexture EnlargedLightTarget = default!;
public Box2Rotated EnlargedBounds;
/// <summary>
/// In metres
/// </summary>
private float _skirting = 1.5f;
public const int ContentZIndex = -10;
public BeforeLightTargetOverlay()
{
IoCManager.InjectDependencies(this);
ZIndex = ContentZIndex;
}
protected override void Draw(in OverlayDrawArgs args)
{
// Code is weird but I don't think engine should be enlarging the lighting render target arbitrarily either, maybe via cvar?
// The problem is the blur has no knowledge of pixels outside the viewport so with a large enough blur radius you get sampling issues.
var size = args.Viewport.LightRenderTarget.Size + (int) (_skirting * EyeManager.PixelsPerMeter);
EnlargedBounds = args.WorldBounds.Enlarged(_skirting / 2f);
// This just exists to copy the lightrendertarget and write back to it.
if (EnlargedLightTarget?.Size != size)
{
EnlargedLightTarget = _clyde
.CreateRenderTarget(size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "enlarged-light-copy");
}
args.WorldHandle.RenderInRenderTarget(EnlargedLightTarget,
() =>
{
}, _clyde.GetClearColor(args.MapUid));
}
}

View File

@@ -0,0 +1,36 @@
using Robust.Client.Graphics;
namespace Content.Client.Light.EntitySystems;
public sealed class PlanetLightSystem : EntitySystem
{
[Dependency] private readonly IOverlayManager _overlayMan = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GetClearColorEvent>(OnClearColor);
_overlayMan.AddOverlay(new BeforeLightTargetOverlay());
_overlayMan.AddOverlay(new RoofOverlay(EntityManager));
_overlayMan.AddOverlay(new TileEmissionOverlay(EntityManager));
_overlayMan.AddOverlay(new LightBlurOverlay());
_overlayMan.AddOverlay(new AfterLightTargetOverlay());
}
private void OnClearColor(ref GetClearColorEvent ev)
{
ev.Color = Color.Transparent;
}
public override void Shutdown()
{
base.Shutdown();
_overlayMan.RemoveOverlay<BeforeLightTargetOverlay>();
_overlayMan.RemoveOverlay<RoofOverlay>();
_overlayMan.RemoveOverlay<TileEmissionOverlay>();
_overlayMan.RemoveOverlay<LightBlurOverlay>();
_overlayMan.RemoveOverlay<AfterLightTargetOverlay>();
}
}

View File

@@ -0,0 +1,9 @@
using Content.Shared.Light.EntitySystems;
namespace Content.Client.Light.EntitySystems;
/// <inheritdoc/>
public sealed class RoofSystem : SharedRoofSystem
{
}

View File

@@ -0,0 +1,44 @@
using Robust.Client.Graphics;
using Robust.Shared.Enums;
namespace Content.Client.Light;
/// <summary>
/// Essentially handles blurring for content-side light overlays.
/// </summary>
public sealed class LightBlurOverlay : Overlay
{
public override OverlaySpace Space => OverlaySpace.BeforeLighting;
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IOverlayManager _overlay = default!;
public const int ContentZIndex = TileEmissionOverlay.ContentZIndex + 1;
private IRenderTarget? _blurTarget;
public LightBlurOverlay()
{
IoCManager.InjectDependencies(this);
ZIndex = ContentZIndex;
}
protected override void Draw(in OverlayDrawArgs args)
{
if (args.Viewport.Eye == null)
return;
var beforeOverlay = _overlay.GetOverlay<BeforeLightTargetOverlay>();
var size = beforeOverlay.EnlargedLightTarget.Size;
if (_blurTarget?.Size != size)
{
_blurTarget = _clyde
.CreateRenderTarget(size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "enlarged-light-blur");
}
var target = beforeOverlay.EnlargedLightTarget;
// Yeah that's all this does keep walkin.
_clyde.BlurRenderTarget(args.Viewport, target, _blurTarget, args.Viewport.Eye, 14f * 2f);
}
}

View File

@@ -0,0 +1,33 @@
using Content.Client.GameTicking.Managers;
using Content.Shared;
using Content.Shared.Light.Components;
using Robust.Shared.Map.Components;
using Robust.Shared.Timing;
namespace Content.Client.Light;
/// <inheritdoc/>
public sealed class LightCycleSystem : SharedLightCycleSystem
{
[Dependency] private readonly ClientGameTicker _ticker = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Update(float frameTime)
{
base.Update(frameTime);
var mapQuery = AllEntityQuery<LightCycleComponent, MapLightComponent>();
while (mapQuery.MoveNext(out var uid, out var cycle, out var map))
{
if (!cycle.Running)
continue;
var time = (float) _timing.CurTime
.Add(cycle.Offset)
.Subtract(_ticker.RoundStartTimeSpan)
.TotalSeconds;
var color = GetColor((uid, cycle), cycle.OriginalColor, time);
map.AmbientLightColor = color;
}
}
}

View File

@@ -0,0 +1,100 @@
using System.Numerics;
using Content.Shared.Light.Components;
using Content.Shared.Maps;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.Map.Components;
namespace Content.Client.Light;
public sealed class RoofOverlay : Overlay
{
private readonly IEntityManager _entManager;
[Dependency] private readonly IOverlayManager _overlay = default!;
private readonly EntityLookupSystem _lookup;
private readonly SharedMapSystem _mapSystem;
private readonly SharedTransformSystem _xformSystem;
private readonly HashSet<Entity<OccluderComponent>> _occluders = new();
public override OverlaySpace Space => OverlaySpace.BeforeLighting;
public const int ContentZIndex = BeforeLightTargetOverlay.ContentZIndex + 1;
public RoofOverlay(IEntityManager entManager)
{
_entManager = entManager;
IoCManager.InjectDependencies(this);
_lookup = _entManager.System<EntityLookupSystem>();
_mapSystem = _entManager.System<SharedMapSystem>();
_xformSystem = _entManager.System<SharedTransformSystem>();
ZIndex = ContentZIndex;
}
protected override void Draw(in OverlayDrawArgs args)
{
if (args.Viewport.Eye == null)
return;
var mapEnt = _mapSystem.GetMap(args.MapId);
if (!_entManager.TryGetComponent(mapEnt, out RoofComponent? roofComp) ||
!_entManager.TryGetComponent(mapEnt, out MapGridComponent? grid))
{
return;
}
var viewport = args.Viewport;
var eye = args.Viewport.Eye;
var worldHandle = args.WorldHandle;
var lightoverlay = _overlay.GetOverlay<BeforeLightTargetOverlay>();
var bounds = lightoverlay.EnlargedBounds;
var target = lightoverlay.EnlargedLightTarget;
worldHandle.RenderInRenderTarget(target,
() =>
{
var invMatrix = target.GetWorldToLocalMatrix(eye, viewport.RenderScale / 2f);
var gridMatrix = _xformSystem.GetWorldMatrix(mapEnt);
var matty = Matrix3x2.Multiply(gridMatrix, invMatrix);
worldHandle.SetTransform(matty);
var tileEnumerator = _mapSystem.GetTilesEnumerator(mapEnt, grid, bounds);
// Due to stencilling we essentially draw on unrooved tiles
while (tileEnumerator.MoveNext(out var tileRef))
{
if ((tileRef.Tile.Flags & (byte) TileFlag.Roof) == 0x0)
{
// Check if the tile is occluded in which case hide it anyway.
// This is to avoid lit walls bleeding over to unlit tiles.
_occluders.Clear();
_lookup.GetLocalEntitiesIntersecting(mapEnt, tileRef.GridIndices, _occluders);
var found = false;
foreach (var occluder in _occluders)
{
if (!occluder.Comp.Enabled)
continue;
found = true;
break;
}
if (!found)
continue;
}
var local = _lookup.GetLocalBounds(tileRef, grid.TileSize);
worldHandle.DrawRect(local, roofComp.Color);
}
}, null);
}
}

View File

@@ -0,0 +1,90 @@
using System.Numerics;
using Content.Shared.Light.Components;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
namespace Content.Client.Light;
public sealed class TileEmissionOverlay : Overlay
{
public override OverlaySpace Space => OverlaySpace.BeforeLighting;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IOverlayManager _overlay = default!;
private SharedMapSystem _mapSystem;
private SharedTransformSystem _xformSystem;
private readonly EntityLookupSystem _lookup;
private readonly EntityQuery<TransformComponent> _xformQuery;
private readonly HashSet<Entity<TileEmissionComponent>> _entities = new();
private List<Entity<MapGridComponent>> _grids = new();
public const int ContentZIndex = RoofOverlay.ContentZIndex + 1;
public TileEmissionOverlay(IEntityManager entManager)
{
IoCManager.InjectDependencies(this);
_lookup = entManager.System<EntityLookupSystem>();
_mapSystem = entManager.System<SharedMapSystem>();
_xformSystem = entManager.System<SharedTransformSystem>();
_xformQuery = entManager.GetEntityQuery<TransformComponent>();
ZIndex = ContentZIndex;
}
protected override void Draw(in OverlayDrawArgs args)
{
if (args.Viewport.Eye == null)
return;
var mapId = args.MapId;
var worldHandle = args.WorldHandle;
var lightoverlay = _overlay.GetOverlay<BeforeLightTargetOverlay>();
var bounds = lightoverlay.EnlargedBounds;
var target = lightoverlay.EnlargedLightTarget;
var viewport = args.Viewport;
args.WorldHandle.RenderInRenderTarget(target,
() =>
{
var invMatrix = target.GetWorldToLocalMatrix(viewport.Eye, viewport.RenderScale / 2f);
_grids.Clear();
_mapManager.FindGridsIntersecting(mapId, bounds, ref _grids, approx: true);
foreach (var grid in _grids)
{
var gridInvMatrix = _xformSystem.GetInvWorldMatrix(grid);
var localBounds = gridInvMatrix.TransformBox(bounds);
_entities.Clear();
_lookup.GetLocalEntitiesIntersecting(grid.Owner, localBounds, _entities);
if (_entities.Count == 0)
continue;
var gridMatrix = _xformSystem.GetWorldMatrix(grid.Owner);
foreach (var ent in _entities)
{
var xform = _xformQuery.Comp(ent);
var tile = _mapSystem.LocalToTile(grid.Owner, grid, xform.Coordinates);
var matty = Matrix3x2.Multiply(gridMatrix, invMatrix);
worldHandle.SetTransform(matty);
// Yes I am fully aware this leads to overlap. If you really want to have alpha then you'll need
// to turn the squares into polys.
// Additionally no shadows so if you make it too big it's going to go through a 1x wall.
var local = _lookup.GetLocalBounds(tile, grid.Comp.TileSize).Enlarged(ent.Comp.Range);
worldHandle.DrawRect(local, ent.Comp.Color);
}
}
}, null);
}
}

View File

@@ -43,7 +43,6 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
[UISystemDependency] private readonly ClientInventorySystem _inventory = default!;
[UISystemDependency] private readonly StationSpawningSystem _spawn = default!;
[UISystemDependency] private readonly GuidebookSystem _guide = default!;
[UISystemDependency] private readonly LoadoutSystem _loadouts = default!;
private CharacterSetupGui? _characterSetup;
private HumanoidProfileEditor? _profileEditor;

View File

@@ -1015,6 +1015,13 @@ namespace Content.Client.Lobby.UI
_loadoutWindow.RefreshLoadouts(roleLoadout, session, collection);
_loadoutWindow.OpenCenteredLeft();
_loadoutWindow.OnNameChanged += name =>
{
roleLoadout.EntityName = name;
Profile = Profile.WithLoadout(roleLoadout);
SetDirty();
};
_loadoutWindow.OnLoadoutPressed += (loadoutGroup, loadoutProto) =>
{
roleLoadout.AddLoadout(loadoutGroup, loadoutProto, _prototypeManager);

View File

@@ -5,17 +5,15 @@
SetSize="800 800"
MinSize="800 128">
<BoxContainer Orientation="Vertical" VerticalExpand="True">
<!--
<BoxContainer Name="RoleNameBox" Orientation="Vertical" Margin="10">
<Label Name="LoadoutNameLabel" Text="{Loc 'loadout-name-edit-label'}"/>
<PanelContainer HorizontalExpand="True" SetHeight="24">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#1B1B1E" />
</PanelContainer.PanelOverride>
<LineEdit Name="RoleNameEdit" ToolTip="{Loc 'loadout-name-edit-tooltip'}" VerticalExpand="True" HorizontalExpand="True"/>
<LineEdit Name="RoleNameEdit" VerticalExpand="True" HorizontalExpand="True"/>
</PanelContainer>
</BoxContainer>
-->
<VerticalTabContainer Name="LoadoutGroupsContainer"
VerticalExpand="True"
HorizontalExpand="True">

View File

@@ -1,7 +1,9 @@
using System.Numerics;
using Content.Client.UserInterface.Controls;
using Content.Shared.Dataset;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Random.Helpers;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Player;
@@ -13,6 +15,7 @@ namespace Content.Client.Lobby.UI.Loadouts;
[GenerateTypedNameReferences]
public sealed partial class LoadoutWindow : FancyWindow
{
public event Action<string>? OnNameChanged;
public event Action<ProtoId<LoadoutGroupPrototype>, ProtoId<LoadoutPrototype>>? OnLoadoutPressed;
public event Action<ProtoId<LoadoutGroupPrototype>, ProtoId<LoadoutPrototype>>? OnLoadoutUnpressed;
@@ -25,6 +28,23 @@ public sealed partial class LoadoutWindow : FancyWindow
RobustXamlLoader.Load(this);
Profile = profile;
var protoManager = collection.Resolve<IPrototypeManager>();
RoleNameEdit.IsValid = text => text.Length <= HumanoidCharacterProfile.MaxLoadoutNameLength;
// Hide if we can't edit the name.
if (!proto.CanCustomizeName)
{
RoleNameBox.Visible = false;
}
else
{
var name = loadout.EntityName;
RoleNameEdit.ToolTip = Loc.GetString(
"loadout-name-edit-tooltip",
("max", HumanoidCharacterProfile.MaxLoadoutNameLength));
RoleNameEdit.Text = name ?? string.Empty;
RoleNameEdit.OnTextChanged += args => OnNameChanged?.Invoke(args.Text);
}
// Hide if no groups
if (proto.Groups.Count == 0)

View File

@@ -1,4 +1,4 @@
using Content.Shared.Magic;
using Content.Shared.Magic;
using Content.Shared.Magic.Events;
namespace Content.Client.Magic;

View File

@@ -102,7 +102,7 @@ public sealed class GridDraggingSystem : SharedGridDraggingSystem
if (!_mapManager.TryFindGridAt(mousePos, out var gridUid, out var grid))
return;
StartDragging(gridUid, Vector2.Transform(mousePos.Position, Transform(gridUid).InvWorldMatrix));
StartDragging(gridUid, Vector2.Transform(mousePos.Position, _transformSystem.GetInvWorldMatrix(gridUid)));
}
if (!TryComp(_dragging, out TransformComponent? xform))
@@ -117,11 +117,11 @@ public sealed class GridDraggingSystem : SharedGridDraggingSystem
return;
}
var localToWorld = Vector2.Transform(_localPosition, xform.WorldMatrix);
var localToWorld = Vector2.Transform(_localPosition, _transformSystem.GetWorldMatrix(xform));
if (localToWorld.EqualsApprox(mousePos.Position, 0.01f)) return;
var requestedGridOrigin = mousePos.Position - xform.WorldRotation.RotateVec(_localPosition);
var requestedGridOrigin = mousePos.Position - _transformSystem.GetWorldRotation(xform).RotateVec(_localPosition);
_lastMousePosition = new MapCoordinates(requestedGridOrigin, mousePos.MapId);
RaiseNetworkEvent(new GridDragRequestPosition()

View File

@@ -20,6 +20,8 @@ public sealed class NewsWriterBoundUserInterface : BoundUserInterface
protected override void Open()
{
base.Open();
_menu = this.CreateWindow<NewsWriterMenu>();
_menu.ArticleEditorPanel.PublishButtonPressed += OnPublishButtonPressed;

View File

@@ -14,6 +14,8 @@ public sealed class CrewMonitoringBoundUserInterface : BoundUserInterface
protected override void Open()
{
base.Open();
EntityUid? gridUid = null;
var stationName = string.Empty;

View File

@@ -18,6 +18,8 @@ namespace Content.Client.Nuke
protected override void Open()
{
base.Open();
_menu = this.CreateWindow<NukeMenu>();
_menu.OnKeypadButtonPressed += i =>

View File

@@ -32,6 +32,11 @@ public sealed class TargetOutlineSystem : EntitySystem
/// </summary>
public EntityWhitelist? Whitelist = null;
/// <summary>
/// Blacklist that the target must satisfy.
/// </summary>
public EntityWhitelist? Blacklist = null;
/// <summary>
/// Predicate the target must satisfy.
/// </summary>
@@ -93,15 +98,16 @@ public sealed class TargetOutlineSystem : EntitySystem
RemoveHighlights();
}
public void Enable(float range, bool checkObstructions, Func<EntityUid, bool>? predicate, EntityWhitelist? whitelist, CancellableEntityEventArgs? validationEvent)
public void Enable(float range, bool checkObstructions, Func<EntityUid, bool>? predicate, EntityWhitelist? whitelist, EntityWhitelist? blacklist, CancellableEntityEventArgs? validationEvent)
{
Range = range;
CheckObstruction = checkObstructions;
Predicate = predicate;
Whitelist = whitelist;
Blacklist = blacklist;
ValidationEvent = validationEvent;
_enabled = Predicate != null || Whitelist != null || ValidationEvent != null;
_enabled = Predicate != null || Whitelist != null || Blacklist != null || ValidationEvent != null;
}
public override void Update(float frameTime)

View File

@@ -10,8 +10,6 @@ namespace Content.Client.Pinpointer.UI;
[GenerateTypedNameReferences]
public sealed partial class StationMapBeaconControl : Control, IComparable<StationMapBeaconControl>
{
[Dependency] private readonly IEntityManager _entMan = default!;
public readonly EntityCoordinates BeaconPosition;
public Action<EntityCoordinates>? OnPressed;
public string? Label => BeaconNameLabel.Text;

View File

@@ -12,6 +12,8 @@ public sealed class PowerMonitoringConsoleBoundUserInterface : BoundUserInterfac
protected override void Open()
{
base.Open();
_menu = this.CreateWindow<PowerMonitoringWindow>();
_menu.SetEntity(Owner);
_menu.SendPowerMonitoringConsoleMessageAction += SendPowerMonitoringConsoleMessage;

View File

@@ -4,14 +4,18 @@ using Robust.Client.GameObjects;
namespace Content.Client.Power.Visualizers;
public sealed class CableVisualizerSystem : VisualizerSystem<CableVisualizerComponent>
public sealed class CableVisualizerSystem : EntitySystem
{
[Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CableVisualizerComponent, AppearanceChangeEvent>(OnAppearanceChange, after: new[] { typeof(SubFloorHideSystem) });
}
protected override void OnAppearanceChange(EntityUid uid, CableVisualizerComponent component, ref AppearanceChangeEvent args)
private void OnAppearanceChange(EntityUid uid, CableVisualizerComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
@@ -23,7 +27,7 @@ public sealed class CableVisualizerSystem : VisualizerSystem<CableVisualizerComp
return;
}
if (!AppearanceSystem.TryGetData<WireVisDirFlags>(uid, WireVisVisuals.ConnectedMask, out var mask, args.Component))
if (!_appearanceSystem.TryGetData<WireVisDirFlags>(uid, WireVisVisuals.ConnectedMask, out var mask, args.Component))
mask = WireVisDirFlags.None;
args.Sprite.LayerSetState(0, $"{component.StatePrefix}{(int) mask}");

View File

@@ -23,7 +23,7 @@ public sealed partial class ShuttleSystem : SharedShuttleSystem
if (_enableShuttlePosition)
{
_overlay = new EmergencyShuttleOverlay(EntityManager);
_overlay = new EmergencyShuttleOverlay(EntityManager.TransformQuery, XformSystem);
overlayManager.AddOverlay(_overlay);
RaiseNetworkEvent(new EmergencyShuttleRequestPositionMessage());
}
@@ -57,23 +57,26 @@ public sealed partial class ShuttleSystem : SharedShuttleSystem
/// </summary>
public sealed class EmergencyShuttleOverlay : Overlay
{
private IEntityManager _entManager;
private readonly EntityQuery<TransformComponent> _transformQuery;
private readonly SharedTransformSystem _transformSystem;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public EntityUid? StationUid;
public Box2? Position;
public EmergencyShuttleOverlay(IEntityManager entManager)
public EmergencyShuttleOverlay(EntityQuery<TransformComponent> transformQuery, SharedTransformSystem transformSystem)
{
_entManager = entManager;
_transformQuery = transformQuery;
_transformSystem = transformSystem;
}
protected override void Draw(in OverlayDrawArgs args)
{
if (Position == null || !_entManager.TryGetComponent<TransformComponent>(StationUid, out var xform)) return;
if (Position == null || !_transformQuery.TryGetComponent(StationUid, out var xform))
return;
args.WorldHandle.SetTransform(xform.WorldMatrix);
args.WorldHandle.SetTransform(_transformSystem.GetWorldMatrix(xform));
args.WorldHandle.DrawRect(Position.Value, Color.Red.WithAlpha(100));
args.WorldHandle.SetTransform(Matrix3x2.Identity);
}

View File

@@ -1,24 +1,14 @@
using System.Text;
using Content.Shared.Shuttles.BUIStates;
using Content.Shared.Shuttles.Systems;
using JetBrains.Annotations;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Map;
namespace Content.Client.Shuttles.UI;
[GenerateTypedNameReferences]
public sealed partial class DockObject : PanelContainer
{
[PublicAPI]
public event Action? UndockPressed;
[PublicAPI]
public event Action? ViewPressed;
public BoxContainer ContentsContainer => Contents;
public DockObject()

View File

@@ -163,16 +163,6 @@ public sealed partial class DockingScreen : BoxContainer
}
dockContainer.AddDock(dock, DockingControl);
dockContainer.ViewPressed += () =>
{
OnDockPress(dock);
};
dockContainer.UndockPressed += () =>
{
UndockRequest?.Invoke(dock.Entity);
};
}
}

View File

@@ -3,12 +3,12 @@ using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
using Content.Client.Administration.Managers;
using Content.Shared.Database;
using Content.Shared.Verbs;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Shared.ContentPack;
using Robust.Shared.Exceptions;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using SixLabors.ImageSharp;
@@ -24,6 +24,7 @@ public sealed class ContentSpriteSystem : EntitySystem
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IResourceManager _resManager = default!;
[Dependency] private readonly IUserInterfaceManager _ui = default!;
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
private ContentSpriteControl _control = new();
@@ -42,12 +43,12 @@ public sealed class ContentSpriteSystem : EntitySystem
{
base.Shutdown();
foreach (var queued in _control._queuedTextures)
foreach (var queued in _control.QueuedTextures)
{
queued.Tcs.SetCanceled();
}
_control._queuedTextures.Clear();
_control.QueuedTextures.Clear();
_ui.RootControl.RemoveChild(_control);
}
@@ -103,7 +104,7 @@ public sealed class ContentSpriteSystem : EntitySystem
var texture = _clyde.CreateRenderTarget(new Vector2i(size.X, size.Y), new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "export");
var tcs = new TaskCompletionSource(cancelToken);
_control._queuedTextures.Enqueue((texture, direction, entity, includeId, tcs));
_control.QueuedTextures.Enqueue((texture, direction, entity, includeId, tcs));
await tcs.Task;
}
@@ -113,13 +114,21 @@ public sealed class ContentSpriteSystem : EntitySystem
if (!_adminManager.IsAdmin())
return;
var target = ev.Target;
Verb verb = new()
{
Text = Loc.GetString("export-entity-verb-get-data-text"),
Category = VerbCategory.Debug,
Act = () =>
Act = async () =>
{
Export(ev.Target);
try
{
await Export(target);
}
catch (Exception e)
{
_runtimeLog.LogException(e, $"{nameof(ContentSpriteSystem)}.{nameof(Export)}");
}
},
};
@@ -141,7 +150,7 @@ public sealed class ContentSpriteSystem : EntitySystem
Direction Direction,
EntityUid Entity,
bool IncludeId,
TaskCompletionSource Tcs)> _queuedTextures = new();
TaskCompletionSource Tcs)> QueuedTextures = new();
private ISawmill _sawmill;
@@ -155,7 +164,7 @@ public sealed class ContentSpriteSystem : EntitySystem
{
base.Draw(handle);
while (_queuedTextures.TryDequeue(out var queued))
while (QueuedTextures.TryDequeue(out var queued))
{
if (queued.Tcs.Task.IsCanceled)
continue;

View File

@@ -4,6 +4,7 @@ using Content.Shared.StatusIcon;
using Content.Shared.StatusIcon.Components;
using Content.Shared.Stealth.Components;
using Content.Shared.Whitelist;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Configuration;
@@ -85,6 +86,9 @@ public sealed class StatusIconSystem : SharedStatusIconSystem
if (data.HideOnStealth && TryComp<StealthComponent>(ent, out var stealth) && stealth.Enabled)
return false;
if (TryComp<SpriteComponent>(ent, out var sprite) && !sprite.Visible)
return false;
if (data.ShowTo != null && !_entityWhitelist.IsValid(data.ShowTo, viewer))
return false;

View File

@@ -74,10 +74,6 @@ public sealed class StorageSystem : SharedStorageSystem
{
containerBui.Hide();
}
else
{
storageBui.Show();
}
}
}

View File

@@ -27,6 +27,8 @@ public sealed class StoreBoundUserInterface : BoundUserInterface
protected override void Open()
{
base.Open();
_menu = this.CreateWindow<StoreMenu>();
if (EntMan.TryGetComponent<StoreComponent>(Owner, out var store))
_menu.Title = Loc.GetString(store.Name);

View File

@@ -21,6 +21,8 @@ public sealed class SurveillanceCameraSetupBoundUi : BoundUserInterface
protected override void Open()
{
base.Open();
_window = new();
if (_type == SurveillanceCameraSetupUiKey.Router)

View File

@@ -87,9 +87,6 @@ public sealed partial class DialogWindow : FancyWindow
Prompts.AddChild(box);
}
// Grab keyboard focus for the first dialog entry
_promptLines[0].Item2.GrabKeyboardFocus();
OkButton.OnPressed += _ => Confirm();
CancelButton.OnPressed += _ =>
@@ -110,6 +107,14 @@ public sealed partial class DialogWindow : FancyWindow
OpenCentered();
}
protected override void Opened()
{
base.Opened();
// Grab keyboard focus for the first dialog entry
_promptLines[0].Item2.GrabKeyboardFocus();
}
private void Confirm()
{
var results = new Dictionary<string, string>();

View File

@@ -7,7 +7,6 @@ using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Graphics;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -96,7 +95,6 @@ public sealed partial class FancyTree : Control
private void LoadIcons()
{
IconColor = TryGetStyleProperty(StylePropertyIconColor, out Color color) ? color : Color.White;
string? path;
if (!TryGetStyleProperty(StylePropertyIconExpanded, out IconExpanded))
IconExpanded = _resCache.GetTexture(DefaultIconExpanded);

View File

@@ -952,7 +952,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
var range = entityAction.CheckCanAccess ? action.Range : -1;
_interactionOutline?.SetEnabled(false);
_targetOutline?.Enable(range, entityAction.CheckCanAccess, predicate, entityAction.Whitelist, null);
_targetOutline?.Enable(range, entityAction.CheckCanAccess, predicate, entityAction.Whitelist, entityAction.Blacklist, null);
}
/// <summary>

View File

@@ -56,19 +56,20 @@ public sealed class CharacterUIController : UIController, IOnStateEntered<Gamepl
_window = UIManager.CreateWindow<CharacterWindow>();
LayoutContainer.SetAnchorPreset(_window, LayoutContainer.LayoutPreset.CenterTop);
_window.OnClose += DeactivateButton;
_window.OnOpen += ActivateButton;
CommandBinds.Builder
.Bind(ContentKeyFunctions.OpenCharacterMenu,
InputCmdHandler.FromDelegate(_ => ToggleWindow()))
.Register<CharacterUIController>();
InputCmdHandler.FromDelegate(_ => ToggleWindow()))
.Register<CharacterUIController>();
}
public void OnStateExited(GameplayState state)
{
if (_window != null)
{
_window.Dispose();
_window.Close();
_window = null;
}
@@ -105,18 +106,27 @@ public sealed class CharacterUIController : UIController, IOnStateEntered<Gamepl
}
CharacterButton.OnPressed += CharacterButtonPressed;
}
if (_window == null)
private void DeactivateButton()
{
if (CharacterButton == null)
{
return;
}
_window.OnClose += DeactivateButton;
_window.OnOpen += ActivateButton;
CharacterButton.Pressed = false;
}
private void DeactivateButton() => CharacterButton!.Pressed = false;
private void ActivateButton() => CharacterButton!.Pressed = true;
private void ActivateButton()
{
if (CharacterButton == null)
{
return;
}
CharacterButton.Pressed = true;
}
private void CharacterUpdated(CharacterData data)
{
@@ -150,7 +160,7 @@ public sealed class CharacterUIController : UIController, IOnStateEntered<Gamepl
var objectiveLabel = new RichTextLabel
{
StyleClasses = {StyleNano.StyleClassTooltipActionTitle}
StyleClasses = { StyleNano.StyleClassTooltipActionTitle }
};
objectiveLabel.SetMessage(objectiveText);
@@ -245,10 +255,7 @@ public sealed class CharacterUIController : UIController, IOnStateEntered<Gamepl
if (_window == null)
return;
if (CharacterButton != null)
{
CharacterButton.SetClickPressed(!_window.IsOpen);
}
CharacterButton?.SetClickPressed(!_window.IsOpen);
if (_window.IsOpen)
{

View File

@@ -38,11 +38,6 @@ public sealed class StorageUIController : UIController, IOnSystemChanged<Storage
[Dependency] private readonly IPlayerManager _player = default!;
[UISystemDependency] private readonly StorageSystem _storage = default!;
/// <summary>
/// Cached positions for opening nested storage.
/// </summary>
private readonly Dictionary<EntityUid, Vector2> _reservedStorage = new();
private readonly DragDropHelper<ItemGridPiece> _menuDragHelper;
public ItemGridPiece? DraggingGhost => _menuDragHelper.Dragged;
@@ -133,11 +128,6 @@ public sealed class StorageUIController : UIController, IOnSystemChanged<Storage
else
{
window.OpenCenteredLeft();
if (_reservedStorage.Remove(uid, out var pos))
{
LayoutContainer.SetPosition(window, pos);
}
}
return window;

View File

@@ -3,8 +3,10 @@ using Content.Client.UserInterface.Systems.Gameplay;
using Content.Shared.CCVar;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.Timing;
namespace Content.Client.UserInterface.Systems.Viewport;
@@ -15,6 +17,7 @@ public sealed class ViewportUIController : UIController
[Dependency] private readonly IPlayerManager _playerMan = default!;
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[UISystemDependency] private readonly SharedTransformSystem? _transformSystem = default!;
public static readonly Vector2i ViewportSize = (EyeManager.PixelsPerMeter * 21, EyeManager.PixelsPerMeter * 15);
public const int ViewportHeight = 15;
private MainViewport? Viewport => UIManager.ActiveScreen?.GetWidget<MainViewport>();
@@ -93,8 +96,11 @@ public sealed class ViewportUIController : UIController
_entMan.TryGetComponent(ent, out EyeComponent? eye);
if (eye?.Eye == _eyeManager.CurrentEye
&& _entMan.GetComponent<TransformComponent>(ent.Value).WorldPosition == default)
return; // nothing to worry about, the player is just in null space... actually that is probably a problem?
&& _entMan.GetComponent<TransformComponent>(ent.Value).MapID == MapId.Nullspace)
{
// nothing to worry about, the player is just in null space... actually that is probably a problem?
return;
}
// Currently, this shouldn't happen. This likely happened because the main eye was set to null. When this
// does happen it can create hard to troubleshoot bugs, so lets print some helpful warnings:

View File

@@ -1,15 +1,11 @@
using Content.Client.Ghost;
using Content.Shared.Voting;
namespace Content.Client.Voting;
public sealed class VotingSystem : EntitySystem
{
public event Action<VotePlayerListResponseEvent>? VotePlayerListResponse; //Provides a list of players elligble for vote actions
[Dependency] private readonly GhostSystem _ghostSystem = default!;
public override void Initialize()
{
base.Initialize();

View File

@@ -170,7 +170,7 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
var targetCoordinates = xform.Coordinates;
var targetLocalAngle = xform.LocalRotation;
return Interaction.InRangeUnobstructed(user, target, targetCoordinates, targetLocalAngle, range);
return Interaction.InRangeUnobstructed(user, target, targetCoordinates, targetLocalAngle, range, overlapCheck: false);
}
protected override void DoDamageEffect(List<EntityUid> targets, EntityUid? user, TransformComponent targetXform)

View File

@@ -11,6 +11,8 @@ using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using System.Linq;
using System.Numerics;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.Utility;
namespace Content.IntegrationTests.Tests.Body
{
@@ -57,7 +59,6 @@ namespace Content.IntegrationTests.Tests.Body
await server.WaitIdleAsync();
var mapManager = server.ResolveDependency<IMapManager>();
var entityManager = server.ResolveDependency<IEntityManager>();
var mapLoader = entityManager.System<MapLoaderSystem>();
var mapSys = entityManager.System<SharedMapSystem>();
@@ -69,17 +70,13 @@ namespace Content.IntegrationTests.Tests.Body
GridAtmosphereComponent relevantAtmos = default;
var startingMoles = 0.0f;
var testMapName = "Maps/Test/Breathing/3by3-20oxy-80nit.yml";
var testMapName = new ResPath("Maps/Test/Breathing/3by3-20oxy-80nit.yml");
await server.WaitPost(() =>
{
mapSys.CreateMap(out var mapId);
Assert.That(mapLoader.TryLoad(mapId, testMapName, out var roots));
var query = entityManager.GetEntityQuery<MapGridComponent>();
var grids = roots.Where(x => query.HasComponent(x));
Assert.That(grids, Is.Not.Empty);
grid = grids.First();
Assert.That(mapLoader.TryLoadGrid(mapId, testMapName, out var gridEnt));
grid = gridEnt!.Value.Owner;
});
Assert.That(grid, Is.Not.Null, $"Test blueprint {testMapName} not found.");
@@ -148,18 +145,13 @@ namespace Content.IntegrationTests.Tests.Body
RespiratorComponent respirator = null;
EntityUid human = default;
var testMapName = "Maps/Test/Breathing/3by3-20oxy-80nit.yml";
var testMapName = new ResPath("Maps/Test/Breathing/3by3-20oxy-80nit.yml");
await server.WaitPost(() =>
{
mapSys.CreateMap(out var mapId);
Assert.That(mapLoader.TryLoad(mapId, testMapName, out var ents), Is.True);
var query = entityManager.GetEntityQuery<MapGridComponent>();
grid = ents
.Select<EntityUid, EntityUid?>(x => x)
.FirstOrDefault((uid) => uid.HasValue && query.HasComponent(uid.Value), null);
Assert.That(grid, Is.Not.Null);
Assert.That(mapLoader.TryLoadGrid(mapId, testMapName, out var gridEnt));
grid = gridEnt!.Value.Owner;
});
Assert.That(grid, Is.Not.Null, $"Test blueprint {testMapName} not found.");

View File

@@ -2,10 +2,11 @@
using System.Linq;
using Content.Shared.Body.Components;
using Content.Shared.Body.Systems;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Utility;
namespace Content.IntegrationTests.Tests.Body;
@@ -111,13 +112,12 @@ public sealed class SaveLoadReparentTest
Is.Not.Empty
);
const string mapPath = $"/{nameof(SaveLoadReparentTest)}{nameof(Test)}map.yml";
var mapPath = new ResPath($"/{nameof(SaveLoadReparentTest)}{nameof(Test)}map.yml");
mapLoader.SaveMap(mapId, mapPath);
maps.DeleteMap(mapId);
Assert.That(mapLoader.TrySaveMap(mapId, mapPath));
mapSys.DeleteMap(mapId);
mapSys.CreateMap(out mapId);
Assert.That(mapLoader.TryLoad(mapId, mapPath, out _), Is.True);
Assert.That(mapLoader.TryLoadMap(mapPath, out var map, out _), Is.True);
var query = EnumerateQueryEnumerator(
entities.EntityQueryEnumerator<BodyComponent>()
@@ -173,7 +173,7 @@ public sealed class SaveLoadReparentTest
});
}
maps.DeleteMap(mapId);
entities.DeleteEntity(map);
}
});

View File

@@ -7,6 +7,7 @@ using Content.Server.Nutrition.Components;
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.Cargo.Prototypes;
using Content.Shared.IdentityManagement;
using Content.Shared.Prototypes;
using Content.Shared.Stacks;
using Content.Shared.Tag;
using Content.Shared.Whitelist;
@@ -104,51 +105,34 @@ public sealed class CargoTest
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var testMap = await pair.CreateTestMap();
var entManager = server.ResolveDependency<IEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>();
var protoManager = server.ResolveDependency<IPrototypeManager>();
var protoManager = server.ProtoMan;
var compFact = server.ResolveDependency<IComponentFactory>();
await server.WaitAssertion(() =>
{
var mapId = testMap.MapId;
var grid = mapManager.CreateGridEntity(mapId);
var coord = new EntityCoordinates(grid.Owner, 0, 0);
var protoIds = protoManager.EnumeratePrototypes<EntityPrototype>()
.Where(p => !p.Abstract)
.Where(p => !pair.IsTestPrototype(p))
.Where(p => !p.Components.ContainsKey("MapGrid")) // Grids are not for sale.
.Select(p => p.ID)
.Where(p => p.Components.ContainsKey("StaticPrice"))
.ToList();
foreach (var proto in protoIds)
{
var ent = entManager.SpawnEntity(proto, coord);
// Sanity check
Assert.That(proto.TryGetComponent<StaticPriceComponent>(out var staticPriceComp, compFact), Is.True);
if (entManager.TryGetComponent<StackPriceComponent>(ent, out var stackpricecomp)
&& stackpricecomp.Price > 0)
if (proto.TryGetComponent<StackPriceComponent>(out var stackPriceComp, compFact) && stackPriceComp.Price > 0)
{
if (entManager.TryGetComponent<StaticPriceComponent>(ent, out var staticpricecomp))
{
Assert.That(staticpricecomp.Price, Is.EqualTo(0),
$"The prototype {proto} has a StackPriceComponent and StaticPriceComponent whose values are not compatible with each other.");
}
Assert.That(staticPriceComp.Price, Is.EqualTo(0),
$"The prototype {proto} has a StackPriceComponent and StaticPriceComponent whose values are not compatible with each other.");
}
if (entManager.HasComponent<StackComponent>(ent))
if (proto.HasComponent<StackComponent>(compFact))
{
if (entManager.TryGetComponent<StaticPriceComponent>(ent, out var staticpricecomp))
{
Assert.That(staticpricecomp.Price, Is.EqualTo(0),
$"The prototype {proto} has a StackComponent and StaticPriceComponent whose values are not compatible with each other.");
}
Assert.That(staticPriceComp.Price, Is.EqualTo(0),
$"The prototype {proto} has a StackComponent and StaticPriceComponent whose values are not compatible with each other.");
}
entManager.DeleteEntity(ent);
}
mapManager.DeleteMap(mapId);
});
await pair.CleanReturnAsync();

View File

@@ -39,6 +39,7 @@ namespace Content.IntegrationTests.Tests
.Where(p => !p.Abstract)
.Where(p => !pair.IsTestPrototype(p))
.Where(p => !p.Components.ContainsKey("MapGrid")) // This will smash stuff otherwise.
.Where(p => !p.Components.ContainsKey("RoomFill")) // This comp can delete all entities, and spawn others
.Select(p => p.ID)
.ToList();
@@ -101,6 +102,7 @@ namespace Content.IntegrationTests.Tests
.Where(p => !p.Abstract)
.Where(p => !pair.IsTestPrototype(p))
.Where(p => !p.Components.ContainsKey("MapGrid")) // This will smash stuff otherwise.
.Where(p => !p.Components.ContainsKey("RoomFill")) // This comp can delete all entities, and spawn others
.Select(p => p.ID)
.ToList();
foreach (var protoId in protoIds)
@@ -341,6 +343,7 @@ namespace Content.IntegrationTests.Tests
"DebugExceptionInitialize",
"DebugExceptionStartup",
"GridFill",
"RoomFill",
"Map", // We aren't testing a map entity in this test
"MapGrid",
"Broadphase",

View File

@@ -26,6 +26,7 @@ public sealed class LatheTest
var compFactory = server.ResolveDependency<IComponentFactory>();
var materialStorageSystem = server.System<SharedMaterialStorageSystem>();
var whitelistSystem = server.System<EntityWhitelistSystem>();
var latheSystem = server.System<SharedLatheSystem>();
await server.WaitAssertion(() =>
{
@@ -74,14 +75,14 @@ public sealed class LatheTest
}
}
// Collect all the recipes assigned to this lathe
var recipes = new List<ProtoId<LatheRecipePrototype>>();
recipes.AddRange(latheComp.StaticRecipes);
recipes.AddRange(latheComp.DynamicRecipes);
// Collect all possible recipes assigned to this lathe
var recipes = new HashSet<ProtoId<LatheRecipePrototype>>();
latheSystem.AddRecipesFromPacks(recipes, latheComp.StaticPacks);
latheSystem.AddRecipesFromPacks(recipes, latheComp.DynamicPacks);
if (latheProto.TryGetComponent<EmagLatheRecipesComponent>(out var emagRecipesComp, compFactory))
{
recipes.AddRange(emagRecipesComp.EmagStaticRecipes);
recipes.AddRange(emagRecipesComp.EmagDynamicRecipes);
latheSystem.AddRecipesFromPacks(recipes, emagRecipesComp.EmagStaticPacks);
latheSystem.AddRecipesFromPacks(recipes, emagRecipesComp.EmagDynamicPacks);
}
// Check each recipe assigned to this lathe

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using Content.Shared.Tag;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization.Manager.Attributes;
@@ -29,7 +30,9 @@ public sealed class StaticFieldValidationTest
Assert.That(protoMan.ValidateStaticFields(typeof(StringValid), protos), Is.Empty);
Assert.That(protoMan.ValidateStaticFields(typeof(StringArrayValid), protos), Is.Empty);
Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdValid), protos), Is.Empty);
Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdTValid), protos), Is.Empty);
Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdArrayValid), protos), Is.Empty);
Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdTArrayValid), protos), Is.Empty);
Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdTestValid), protos), Is.Empty);
Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdArrayValid), protos), Is.Empty);
Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdListValid), protos), Is.Empty);
@@ -39,7 +42,9 @@ public sealed class StaticFieldValidationTest
Assert.That(protoMan.ValidateStaticFields(typeof(StringInvalid), protos), Has.Count.EqualTo(1));
Assert.That(protoMan.ValidateStaticFields(typeof(StringArrayInvalid), protos), Has.Count.EqualTo(2));
Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdInvalid), protos), Has.Count.EqualTo(1));
Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdTInvalid), protos), Has.Count.EqualTo(1));
Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdArrayInvalid), protos), Has.Count.EqualTo(2));
Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdTArrayInvalid), protos), Has.Count.EqualTo(2));
Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdTestInvalid), protos), Has.Count.EqualTo(1));
Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdArrayInvalid), protos), Has.Count.EqualTo(2));
Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdListInvalid), protos), Has.Count.EqualTo(2));
@@ -88,24 +93,48 @@ public sealed class StaticFieldValidationTest
public static EntProtoId Tag = "StaticFieldTestEnt";
}
[Reflect(false)]
private sealed class EntProtoIdTValid
{
public static EntProtoId<TransformComponent> Tag = "StaticFieldTestEnt";
}
[Reflect(false)]
private sealed class EntProtoIdInvalid
{
public static EntProtoId Tag = string.Empty;
}
[Reflect(false)]
private sealed class EntProtoIdTInvalid
{
public static EntProtoId<TransformComponent> Tag = string.Empty;
}
[Reflect(false)]
private sealed class EntProtoIdArrayValid
{
public static EntProtoId[] Tag = ["StaticFieldTestEnt", "StaticFieldTestEnt"];
}
[Reflect(false)]
private sealed class EntProtoIdTArrayValid
{
public static EntProtoId<TransformComponent>[] Tag = ["StaticFieldTestEnt", "StaticFieldTestEnt"];
}
[Reflect(false)]
private sealed class EntProtoIdArrayInvalid
{
public static EntProtoId[] Tag = [string.Empty, "StaticFieldTestEnt", string.Empty];
}
[Reflect(false)]
private sealed class EntProtoIdTArrayInvalid
{
public static EntProtoId<TransformComponent>[] Tag = [string.Empty, "StaticFieldTestEnt", string.Empty];
}
[Reflect(false)]
private sealed class ProtoIdTestValid
{

View File

@@ -1,5 +1,6 @@
#nullable enable
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
namespace Content.IntegrationTests.Tests.Minds;
@@ -37,8 +38,8 @@ public sealed partial class MindTests
Assert.That(pair.Client.EntMan.EntityCount, Is.EqualTo(0));
// Create a new map.
int mapId = 1;
await pair.Server.WaitPost(() => conHost.ExecuteCommand($"addmap {mapId}"));
MapId mapId = default;
await pair.Server.WaitPost(() => pair.Server.System<SharedMapSystem>().CreateMap(out mapId));
await pair.RunTicksSync(5);
// Client is not attached to anything
@@ -54,7 +55,7 @@ public sealed partial class MindTests
Assert.That(pair.Client.EntMan.EntityExists(pair.Client.AttachedEntity));
Assert.That(pair.Server.EntMan.EntityExists(pair.PlayerData?.Mind));
var xform = pair.Client.Transform(pair.Client.AttachedEntity!.Value);
Assert.That(xform.MapID, Is.EqualTo(new MapId(mapId)));
Assert.That(xform.MapID, Is.EqualTo(mapId));
await pair.CleanReturnAsync();
}

View File

@@ -9,7 +9,6 @@ using Content.Server.Spawners.Components;
using Content.Server.Station.Components;
using Content.Shared.CCVar;
using Content.Shared.Roles;
using Robust.Server.GameObjects;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
@@ -17,6 +16,9 @@ using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Content.Shared.Station.Components;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.IoC;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
@@ -36,10 +38,7 @@ namespace Content.IntegrationTests.Tests
private static readonly string[] Grids =
{
"/Maps/centcomm.yml",
"/Maps/Shuttles/cargo.yml",
"/Maps/Shuttles/emergency.yml",
"/Maps/Shuttles/infiltrator.yml",
"/Maps/centcomm.yml"
};
private static readonly string[] DoNotMapWhitelist =
@@ -65,14 +64,13 @@ namespace Content.IntegrationTests.Tests
"Reach",
"Train",
"Oasis",
"Cog",
"Gate",
"Amber",
"Loop",
"Plasma",
"Elkridge",
"Convex"
"Convex",
"Relic"
};
/// <summary>
@@ -87,33 +85,72 @@ namespace Content.IntegrationTests.Tests
var entManager = server.ResolveDependency<IEntityManager>();
var mapLoader = entManager.System<MapLoaderSystem>();
var mapSystem = entManager.System<SharedMapSystem>();
var mapManager = server.ResolveDependency<IMapManager>();
var cfg = server.ResolveDependency<IConfigurationManager>();
Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False);
var path = new ResPath(mapFile);
await server.WaitPost(() =>
{
mapSystem.CreateMap(out var mapId);
try
{
#pragma warning disable NUnit2045
Assert.That(mapLoader.TryLoad(mapId, mapFile, out var roots));
Assert.That(roots.Where(uid => entManager.HasComponent<MapGridComponent>(uid)), Is.Not.Empty);
#pragma warning restore NUnit2045
Assert.That(mapLoader.TryLoadGrid(mapId, path, out var grid));
}
catch (Exception ex)
{
throw new Exception($"Failed to load map {mapFile}, was it saved as a map instead of a grid?", ex);
}
try
mapSystem.DeleteMap(mapId);
});
await server.WaitRunTicks(1);
await pair.CleanReturnAsync();
}
/// <summary>
/// Asserts that shuttles are loadable and have been saved as grids and not maps.
/// </summary>
[Test]
public async Task ShuttlesLoadableTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entManager = server.ResolveDependency<IEntityManager>();
var resMan = server.ResolveDependency<IResourceManager>();
var mapLoader = entManager.System<MapLoaderSystem>();
var mapSystem = entManager.System<SharedMapSystem>();
var cfg = server.ResolveDependency<IConfigurationManager>();
Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False);
var shuttleFolder = new ResPath("/Maps/Shuttles");
var shuttles = resMan
.ContentFindFiles(shuttleFolder)
.Where(filePath =>
filePath.Extension == "yml" && !filePath.Filename.StartsWith(".", StringComparison.Ordinal))
.ToArray();
await server.WaitPost(() =>
{
Assert.Multiple(() =>
{
mapManager.DeleteMap(mapId);
}
catch (Exception ex)
{
throw new Exception($"Failed to delete map {mapFile}", ex);
}
foreach (var path in shuttles)
{
mapSystem.CreateMap(out var mapId);
try
{
Assert.That(mapLoader.TryLoadGrid(mapId, path, out _),
$"Failed to load shuttle {path}, was it saved as a map instead of a grid?");
}
catch (Exception ex)
{
throw new Exception($"Failed to load shuttle {path}, was it saved as a map instead of a grid?",
ex);
}
mapSystem.DeleteMap(mapId);
}
});
});
await server.WaitRunTicks(1);
@@ -128,12 +165,15 @@ namespace Content.IntegrationTests.Tests
var resourceManager = server.ResolveDependency<IResourceManager>();
var protoManager = server.ResolveDependency<IPrototypeManager>();
var loader = server.System<MapLoaderSystem>();
var mapFolder = new ResPath("/Maps");
var maps = resourceManager
.ContentFindFiles(mapFolder)
.Where(filePath => filePath.Extension == "yml" && !filePath.Filename.StartsWith(".", StringComparison.Ordinal))
.ToArray();
var v7Maps = new List<ResPath>();
foreach (var map in maps)
{
var rootedPath = map.ToRootedPath();
@@ -156,8 +196,15 @@ namespace Content.IntegrationTests.Tests
var root = yamlStream.Documents[0].RootNode;
var meta = root["meta"];
var postMapInit = meta["postmapinit"].AsBool();
var version = meta["format"].AsInt();
if (version >= 7)
{
v7Maps.Add(map);
continue;
}
var postMapInit = meta["postmapinit"].AsBool();
Assert.That(postMapInit, Is.False, $"Map {map.Filename} was saved postmapinit");
// testing that maps have nothing with the DoNotMap entity category
@@ -177,9 +224,58 @@ namespace Content.IntegrationTests.Tests
}
}
}
var deps = server.ResolveDependency<IEntitySystemManager>().DependencyCollection;
foreach (var map in v7Maps)
{
Assert.That(IsPreInit(map, loader, deps));
}
// Check that the test actually does manage to catch post-init maps and isn't just blindly passing everything.
// To that end, create a new post-init map and try verify it.
var mapSys = server.System<SharedMapSystem>();
MapId id = default;
await server.WaitPost(() => mapSys.CreateMap(out id, runMapInit: false));
await server.WaitPost(() => server.EntMan.Spawn(null, new MapCoordinates(0, 0, id)));
// First check that a pre-init version passes
var path = new ResPath($"{nameof(NoSavedPostMapInitTest)}.yml");
Assert.That(loader.TrySaveMap(id, path));
Assert.That(IsPreInit(path, loader, deps));
// and the post-init version fails.
await server.WaitPost(() => mapSys.InitializeMap(id));
Assert.That(loader.TrySaveMap(id, path));
Assert.That(IsPreInit(path, loader, deps), Is.False);
await pair.CleanReturnAsync();
}
private bool IsPreInit(ResPath map, MapLoaderSystem loader, IDependencyCollection deps)
{
if (!loader.TryReadFile(map, out var data))
{
Assert.Fail($"Failed to read {map}");
return false;
}
var reader = new EntityDeserializer(deps, data, DeserializationOptions.Default);
if (!reader.TryProcessData())
{
Assert.Fail($"Failed to process {map}");
return false;
}
foreach (var mapId in reader.MapYamlIds)
{
var mapData = reader.YamlEntities[mapId];
if (mapData.PostInit)
return false;
}
return true;
}
[Test, TestCaseSource(nameof(GameMaps))]
public async Task GameMapsLoadableTest(string mapProto)
{
@@ -196,16 +292,16 @@ namespace Content.IntegrationTests.Tests
var protoManager = server.ResolveDependency<IPrototypeManager>();
var ticker = entManager.EntitySysManager.GetEntitySystem<GameTicker>();
var shuttleSystem = entManager.EntitySysManager.GetEntitySystem<ShuttleSystem>();
var xformQuery = entManager.GetEntityQuery<TransformComponent>();
var cfg = server.ResolveDependency<IConfigurationManager>();
Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False);
await server.WaitPost(() =>
{
mapSystem.CreateMap(out var mapId);
MapId mapId;
try
{
ticker.LoadGameMap(protoManager.Index<GameMapPrototype>(mapProto), mapId, null);
var opts = DeserializationOptions.Default with {InitializeMaps = true};
ticker.LoadGameMap(protoManager.Index<GameMapPrototype>(mapProto), out mapId, opts);
}
catch (Exception ex)
{
@@ -242,21 +338,17 @@ namespace Content.IntegrationTests.Tests
if (entManager.TryGetComponent<StationEmergencyShuttleComponent>(station, out var stationEvac))
{
var shuttlePath = stationEvac.EmergencyShuttlePath;
#pragma warning disable NUnit2045
Assert.That(mapLoader.TryLoad(shuttleMap, shuttlePath.ToString(), out var roots));
EntityUid shuttle = default!;
Assert.DoesNotThrow(() =>
{
shuttle = roots.First(uid => entManager.HasComponent<MapGridComponent>(uid));
}, $"Failed to load {shuttlePath}");
Assert.That(mapLoader.TryLoadGrid(shuttleMap, shuttlePath, out var shuttle),
$"Failed to load {shuttlePath}");
Assert.That(
shuttleSystem.TryFTLDock(shuttle,
entManager.GetComponent<ShuttleComponent>(shuttle), targetGrid.Value),
shuttleSystem.TryFTLDock(shuttle!.Value.Owner,
entManager.GetComponent<ShuttleComponent>(shuttle!.Value.Owner),
targetGrid.Value),
$"Unable to dock {shuttlePath} to {mapProto}");
#pragma warning restore NUnit2045
}
mapManager.DeleteMap(shuttleMap);
mapSystem.DeleteMap(shuttleMap);
if (entManager.HasComponent<StationJobsComponent>(station))
{
@@ -293,7 +385,7 @@ namespace Content.IntegrationTests.Tests
try
{
mapManager.DeleteMap(mapId);
mapSystem.DeleteMap(mapId);
}
catch (Exception ex)
{
@@ -357,11 +449,9 @@ namespace Content.IntegrationTests.Tests
var server = pair.Server;
var mapLoader = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<MapLoaderSystem>();
var mapManager = server.ResolveDependency<IMapManager>();
var resourceManager = server.ResolveDependency<IResourceManager>();
var protoManager = server.ResolveDependency<IPrototypeManager>();
var cfg = server.ResolveDependency<IConfigurationManager>();
var mapSystem = server.System<SharedMapSystem>();
Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False);
var gameMaps = protoManager.EnumeratePrototypes<GameMapPrototype>().Select(o => o.MapPath).ToHashSet();
@@ -372,7 +462,7 @@ namespace Content.IntegrationTests.Tests
.Where(filePath => filePath.Extension == "yml" && !filePath.Filename.StartsWith(".", StringComparison.Ordinal))
.ToArray();
var mapNames = new List<string>();
var mapPaths = new List<ResPath>();
foreach (var map in maps)
{
if (gameMaps.Contains(map))
@@ -383,32 +473,46 @@ namespace Content.IntegrationTests.Tests
{
continue;
}
mapNames.Add(rootedPath.ToString());
mapPaths.Add(rootedPath);
}
await server.WaitPost(() =>
{
Assert.Multiple(() =>
{
foreach (var mapName in mapNames)
// This bunch of files contains a random mixture of both map and grid files.
// TODO MAPPING organize files
var opts = MapLoadOptions.Default with
{
DeserializationOptions = DeserializationOptions.Default with
{
InitializeMaps = true,
LogOrphanedGrids = false
}
};
HashSet<Entity<MapComponent>> maps;
foreach (var path in mapPaths)
{
mapSystem.CreateMap(out var mapId);
try
{
Assert.That(mapLoader.TryLoad(mapId, mapName, out _));
Assert.That(mapLoader.TryLoadGeneric(path, out maps, out _, opts));
}
catch (Exception ex)
{
throw new Exception($"Failed to load map {mapName}", ex);
throw new Exception($"Failed to load map {path}", ex);
}
try
{
mapManager.DeleteMap(mapId);
foreach (var map in maps)
{
server.EntMan.DeleteEntity(map);
}
}
catch (Exception ex)
{
throw new Exception($"Failed to delete map {mapName}", ex);
throw new Exception($"Failed to delete map {path}", ex);
}
}
});

View File

@@ -52,13 +52,16 @@ public sealed class ResearchTest
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.ResolveDependency<IEntityManager>();
var protoManager = server.ResolveDependency<IPrototypeManager>();
var compFact = server.ResolveDependency<IComponentFactory>();
var latheSys = entMan.System<SharedLatheSystem>();
await server.WaitAssertion(() =>
{
var allEnts = protoManager.EnumeratePrototypes<EntityPrototype>();
var allLathes = new HashSet<LatheComponent>();
var latheTechs = new HashSet<ProtoId<LatheRecipePrototype>>();
foreach (var proto in allEnts)
{
if (proto.Abstract)
@@ -69,30 +72,31 @@ public sealed class ResearchTest
if (!proto.TryGetComponent<LatheComponent>(out var lathe, compFact))
continue;
allLathes.Add(lathe);
}
var latheTechs = new HashSet<string>();
foreach (var lathe in allLathes)
{
if (lathe.DynamicRecipes == null)
continue;
latheSys.AddRecipesFromPacks(latheTechs, lathe.DynamicPacks);
foreach (var recipe in lathe.DynamicRecipes)
{
latheTechs.Add(recipe);
}
if (proto.TryGetComponent<EmagLatheRecipesComponent>(out var emag, compFact))
latheSys.AddRecipesFromPacks(latheTechs, emag.EmagDynamicPacks);
}
Assert.Multiple(() =>
{
// check that every recipe a tech adds can be made on some lathe
var unlockedTechs = new HashSet<ProtoId<LatheRecipePrototype>>();
foreach (var tech in protoManager.EnumeratePrototypes<TechnologyPrototype>())
{
unlockedTechs.UnionWith(tech.RecipeUnlocks);
foreach (var recipe in tech.RecipeUnlocks)
{
Assert.That(latheTechs, Does.Contain(recipe), $"Recipe \"{recipe}\" cannot be unlocked on any lathes.");
Assert.That(latheTechs, Does.Contain(recipe), $"Recipe '{recipe}' from tech '{tech.ID}' cannot be unlocked on any lathes.");
}
}
// now check that every dynamic recipe a lathe lists can be unlocked
foreach (var recipe in latheTechs)
{
Assert.That(unlockedTechs, Does.Contain(recipe), $"Recipe '{recipe}' is dynamic on a lathe but cannot be unlocked by research.");
}
});
});

View File

@@ -1,11 +1,8 @@
using System.Linq;
using Content.Shared.CCVar;
using Content.Shared.CCVar;
using Content.Shared.Salvage;
using Robust.Server.GameObjects;
using Robust.Shared.Configuration;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests;
@@ -24,7 +21,6 @@ public sealed class SalvageTest
var entManager = server.ResolveDependency<IEntityManager>();
var mapLoader = entManager.System<MapLoaderSystem>();
var mapManager = server.ResolveDependency<IMapManager>();
var prototypeManager = server.ResolveDependency<IPrototypeManager>();
var cfg = server.ResolveDependency<IConfigurationManager>();
var mapSystem = entManager.System<SharedMapSystem>();
@@ -34,13 +30,10 @@ public sealed class SalvageTest
{
foreach (var salvage in prototypeManager.EnumeratePrototypes<SalvageMapPrototype>())
{
var mapFile = salvage.MapPath;
mapSystem.CreateMap(out var mapId);
try
{
Assert.That(mapLoader.TryLoad(mapId, mapFile.ToString(), out var roots));
Assert.That(roots.Where(uid => entManager.HasComponent<MapGridComponent>(uid)), Is.Not.Empty);
Assert.That(mapLoader.TryLoadGrid(mapId, salvage.MapPath, out var grid));
}
catch (Exception ex)
{
@@ -49,7 +42,7 @@ public sealed class SalvageTest
try
{
mapManager.DeleteMap(mapId);
mapSystem.DeleteMap(mapId);
}
catch (Exception ex)
{

View File

@@ -3,6 +3,7 @@ using Content.Shared.CCVar;
using Robust.Server.GameObjects;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
@@ -16,7 +17,7 @@ namespace Content.IntegrationTests.Tests
[Test]
public async Task SaveLoadMultiGridMap()
{
const string mapPath = @"/Maps/Test/TestMap.yml";
var mapPath = new ResPath("/Maps/Test/TestMap.yml");
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
@@ -31,7 +32,7 @@ namespace Content.IntegrationTests.Tests
await server.WaitAssertion(() =>
{
var dir = new ResPath(mapPath).Directory;
var dir = mapPath.Directory;
resManager.UserData.CreateDir(dir);
mapSystem.CreateMap(out var mapId);
@@ -39,23 +40,25 @@ namespace Content.IntegrationTests.Tests
{
var mapGrid = mapManager.CreateGridEntity(mapId);
xformSystem.SetWorldPosition(mapGrid, new Vector2(10, 10));
mapSystem.SetTile(mapGrid, new Vector2i(0, 0), new Tile(1, (TileRenderFlag) 1, 255));
mapSystem.SetTile(mapGrid, new Vector2i(0, 0), new Tile(typeId: 1, flags: 1, variant: 255));
}
{
var mapGrid = mapManager.CreateGridEntity(mapId);
xformSystem.SetWorldPosition(mapGrid, new Vector2(-8, -8));
mapSystem.SetTile(mapGrid, new Vector2i(0, 0), new Tile(2, (TileRenderFlag) 1, 254));
mapSystem.SetTile(mapGrid, new Vector2i(0, 0), new Tile(typeId: 2, flags: 1, variant: 254));
}
Assert.Multiple(() => mapLoader.SaveMap(mapId, mapPath));
Assert.Multiple(() => mapManager.DeleteMap(mapId));
Assert.That(mapLoader.TrySaveMap(mapId, mapPath));
mapSystem.DeleteMap(mapId);
});
await server.WaitIdleAsync();
MapId newMap = default;
await server.WaitAssertion(() =>
{
Assert.That(mapLoader.TryLoad(new MapId(10), mapPath, out _));
Assert.That(mapLoader.TryLoadMap(mapPath, out var map, out _));
newMap = map!.Value.Comp.MapId;
});
await server.WaitIdleAsync();
@@ -63,7 +66,7 @@ namespace Content.IntegrationTests.Tests
await server.WaitAssertion(() =>
{
{
if (!mapManager.TryFindGridAt(new MapId(10), new Vector2(10, 10), out var gridUid, out var mapGrid) ||
if (!mapManager.TryFindGridAt(newMap, new Vector2(10, 10), out var gridUid, out var mapGrid) ||
!sEntities.TryGetComponent<TransformComponent>(gridUid, out var gridXform))
{
Assert.Fail();
@@ -73,11 +76,11 @@ namespace Content.IntegrationTests.Tests
Assert.Multiple(() =>
{
Assert.That(xformSystem.GetWorldPosition(gridXform), Is.EqualTo(new Vector2(10, 10)));
Assert.That(mapSystem.GetTileRef(gridUid, mapGrid, new Vector2i(0, 0)).Tile, Is.EqualTo(new Tile(1, (TileRenderFlag) 1, 255)));
Assert.That(mapSystem.GetTileRef(gridUid, mapGrid, new Vector2i(0, 0)).Tile, Is.EqualTo(new Tile(typeId: 1, flags: 1, variant: 255)));
});
}
{
if (!mapManager.TryFindGridAt(new MapId(10), new Vector2(-8, -8), out var gridUid, out var mapGrid) ||
if (!mapManager.TryFindGridAt(newMap, new Vector2(-8, -8), out var gridUid, out var mapGrid) ||
!sEntities.TryGetComponent<TransformComponent>(gridUid, out var gridXform))
{
Assert.Fail();
@@ -87,7 +90,7 @@ namespace Content.IntegrationTests.Tests
Assert.Multiple(() =>
{
Assert.That(xformSystem.GetWorldPosition(gridXform), Is.EqualTo(new Vector2(-8, -8)));
Assert.That(mapSystem.GetTileRef(gridUid, mapGrid, new Vector2i(0, 0)).Tile, Is.EqualTo(new Tile(2, (TileRenderFlag) 1, 254)));
Assert.That(mapSystem.GetTileRef(gridUid, mapGrid, new Vector2i(0, 0)).Tile, Is.EqualTo(new Tile(typeId: 2, flags: 1, variant: 254)));
});
}
});

View File

@@ -1,25 +1,25 @@
using System.IO;
using System.Linq;
using Content.Shared.CCVar;
using Robust.Server.GameObjects;
using Robust.Server.Maps;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Map.Events;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Utility;
namespace Content.IntegrationTests.Tests
{
/// <summary>
/// Tests that a map's yaml does not change when saved consecutively.
/// Tests that a grid's yaml does not change when saved consecutively.
/// </summary>
[TestFixture]
public sealed class SaveLoadSaveTest
{
[Test]
public async Task SaveLoadSave()
public async Task CreateSaveLoadSaveGrid()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
@@ -30,22 +30,21 @@ namespace Content.IntegrationTests.Tests
var cfg = server.ResolveDependency<IConfigurationManager>();
Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False);
var testSystem = server.System<SaveLoadSaveTestSystem>();
testSystem.Enabled = true;
var rp1 = new ResPath("/save load save 1.yml");
var rp2 = new ResPath("/save load save 2.yml");
await server.WaitPost(() =>
{
mapSystem.CreateMap(out var mapId0);
// TODO: Properly find the "main" station grid.
var grid0 = mapManager.CreateGridEntity(mapId0);
mapLoader.Save(grid0.Owner, "save load save 1.yml");
entManager.RunMapInit(grid0.Owner, entManager.GetComponent<MetaDataComponent>(grid0));
Assert.That(mapLoader.TrySaveGrid(grid0.Owner, rp1));
mapSystem.CreateMap(out var mapId1);
EntityUid grid1 = default!;
#pragma warning disable NUnit2045
Assert.That(mapLoader.TryLoad(mapId1, "save load save 1.yml", out var roots, new MapLoadOptions() { LoadMap = false }), $"Failed to load test map {TestMap}");
Assert.DoesNotThrow(() =>
{
grid1 = roots.First(uid => entManager.HasComponent<MapGridComponent>(uid));
});
#pragma warning restore NUnit2045
mapLoader.Save(grid1, "save load save 2.yml");
Assert.That(mapLoader.TryLoadGrid(mapId1, rp1, out var grid1));
Assert.That(mapLoader.TrySaveGrid(grid1!.Value, rp2));
});
await server.WaitIdleAsync();
@@ -54,14 +53,12 @@ namespace Content.IntegrationTests.Tests
string one;
string two;
var rp1 = new ResPath("/save load save 1.yml");
await using (var stream = userData.Open(rp1, FileMode.Open))
using (var reader = new StreamReader(stream))
{
one = await reader.ReadToEndAsync();
}
var rp2 = new ResPath("/save load save 2.yml");
await using (var stream = userData.Open(rp2, FileMode.Open))
using (var reader = new StreamReader(stream))
{
@@ -87,6 +84,7 @@ namespace Content.IntegrationTests.Tests
TestContext.Error.WriteLine(twoTmp);
}
});
testSystem.Enabled = false;
await pair.CleanReturnAsync();
}
@@ -101,8 +99,12 @@ namespace Content.IntegrationTests.Tests
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var mapLoader = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<MapLoaderSystem>();
var mapManager = server.ResolveDependency<IMapManager>();
var mapSystem = server.System<SharedMapSystem>();
var mapSys = server.System<SharedMapSystem>();
var testSystem = server.System<SaveLoadSaveTestSystem>();
testSystem.Enabled = true;
var rp1 = new ResPath("/load save ticks save 1.yml");
var rp2 = new ResPath("/load save ticks save 2.yml");
MapId mapId = default;
var cfg = server.ResolveDependency<IConfigurationManager>();
@@ -111,10 +113,10 @@ namespace Content.IntegrationTests.Tests
// Load bagel.yml as uninitialized map, and save it to ensure it's up to date.
server.Post(() =>
{
mapSystem.CreateMap(out mapId, runMapInit: false);
mapManager.SetMapPaused(mapId, true);
Assert.That(mapLoader.TryLoad(mapId, TestMap, out _), $"Failed to load test map {TestMap}");
mapLoader.SaveMap(mapId, "load save ticks save 1.yml");
var path = new ResPath(TestMap);
Assert.That(mapLoader.TryLoadMap(path, out var map, out _), $"Failed to load test map {TestMap}");
mapId = map!.Value.Comp.MapId;
Assert.That(mapLoader.TrySaveMap(mapId, rp1));
});
// Run 5 ticks.
@@ -122,7 +124,7 @@ namespace Content.IntegrationTests.Tests
await server.WaitPost(() =>
{
mapLoader.SaveMap(mapId, "/load save ticks save 2.yml");
Assert.That(mapLoader.TrySaveMap(mapId, rp2));
});
await server.WaitIdleAsync();
@@ -131,13 +133,13 @@ namespace Content.IntegrationTests.Tests
string one;
string two;
await using (var stream = userData.Open(new ResPath("/load save ticks save 1.yml"), FileMode.Open))
await using (var stream = userData.Open(rp1, FileMode.Open))
using (var reader = new StreamReader(stream))
{
one = await reader.ReadToEndAsync();
}
await using (var stream = userData.Open(new ResPath("/load save ticks save 2.yml"), FileMode.Open))
await using (var stream = userData.Open(rp2, FileMode.Open))
using (var reader = new StreamReader(stream))
{
two = await reader.ReadToEndAsync();
@@ -163,7 +165,8 @@ namespace Content.IntegrationTests.Tests
}
});
await server.WaitPost(() => mapManager.DeleteMap(mapId));
testSystem.Enabled = false;
await server.WaitPost(() => mapSys.DeleteMap(mapId));
await pair.CleanReturnAsync();
}
@@ -184,29 +187,31 @@ namespace Content.IntegrationTests.Tests
var server = pair.Server;
var mapLoader = server.System<MapLoaderSystem>();
var mapSystem = server.System<SharedMapSystem>();
var mapManager = server.ResolveDependency<IMapManager>();
var mapSys = server.System<SharedMapSystem>();
var userData = server.ResolveDependency<IResourceManager>().UserData;
var cfg = server.ResolveDependency<IConfigurationManager>();
Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False);
var testSystem = server.System<SaveLoadSaveTestSystem>();
testSystem.Enabled = true;
MapId mapId = default;
const string fileA = "/load tick load a.yml";
const string fileB = "/load tick load b.yml";
MapId mapId1 = default;
MapId mapId2 = default;
var fileA = new ResPath("/load tick load a.yml");
var fileB = new ResPath("/load tick load b.yml");
string yamlA;
string yamlB;
// Load & save the first map
server.Post(() =>
{
mapSystem.CreateMap(out mapId, runMapInit: false);
mapManager.SetMapPaused(mapId, true);
Assert.That(mapLoader.TryLoad(mapId, TestMap, out _), $"Failed to load test map {TestMap}");
mapLoader.SaveMap(mapId, fileA);
var path = new ResPath(TestMap);
Assert.That(mapLoader.TryLoadMap(path, out var map, out _), $"Failed to load test map {TestMap}");
mapId1 = map!.Value.Comp.MapId;
Assert.That(mapLoader.TrySaveMap(mapId1, fileA));
});
await server.WaitIdleAsync();
await using (var stream = userData.Open(new ResPath(fileA), FileMode.Open))
await using (var stream = userData.Open(fileA, FileMode.Open))
using (var reader = new StreamReader(stream))
{
yamlA = await reader.ReadToEndAsync();
@@ -217,16 +222,15 @@ namespace Content.IntegrationTests.Tests
// Load & save the second map
server.Post(() =>
{
mapManager.DeleteMap(mapId);
mapSystem.CreateMap(out mapId, runMapInit: false);
mapManager.SetMapPaused(mapId, true);
Assert.That(mapLoader.TryLoad(mapId, TestMap, out _), $"Failed to load test map {TestMap}");
mapLoader.SaveMap(mapId, fileB);
var path = new ResPath(TestMap);
Assert.That(mapLoader.TryLoadMap(path, out var map, out _), $"Failed to load test map {TestMap}");
mapId2 = map!.Value.Comp.MapId;
Assert.That(mapLoader.TrySaveMap(mapId2, fileB));
});
await server.WaitIdleAsync();
await using (var stream = userData.Open(new ResPath(fileB), FileMode.Open))
await using (var stream = userData.Open(fileB, FileMode.Open))
using (var reader = new StreamReader(stream))
{
yamlB = await reader.ReadToEndAsync();
@@ -234,8 +238,32 @@ namespace Content.IntegrationTests.Tests
Assert.That(yamlA, Is.EqualTo(yamlB));
await server.WaitPost(() => mapManager.DeleteMap(mapId));
testSystem.Enabled = false;
await server.WaitPost(() => mapSys.DeleteMap(mapId1));
await server.WaitPost(() => mapSys.DeleteMap(mapId2));
await pair.CleanReturnAsync();
}
/// <summary>
/// Simple system that modifies the data saved to a yaml file by removing the timestamp.
/// Required by some tests that validate that re-saving a map does not modify it.
/// </summary>
private sealed class SaveLoadSaveTestSystem : EntitySystem
{
public bool Enabled;
public override void Initialize()
{
SubscribeLocalEvent<AfterSerializationEvent>(OnAfterSave);
}
private void OnAfterSave(AfterSerializationEvent ev)
{
if (!Enabled)
return;
// Remove timestamp.
((MappingDataNode)ev.Node["meta"]).Remove("time");
}
}
}
}

View File

@@ -4,10 +4,12 @@ using System.Numerics;
using Content.Server.Shuttles.Systems;
using Content.Tests;
using Robust.Server.GameObjects;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Content.IntegrationTests.Tests.Shuttle;
@@ -106,8 +108,9 @@ public sealed class DockTest : ContentUnitTest
{
mapGrid = entManager.AddComponent<MapGridComponent>(map.MapUid);
entManager.DeleteEntity(map.Grid);
Assert.That(entManager.System<MapLoaderSystem>().TryLoad(otherMap.MapId, "/Maps/Shuttles/emergency.yml", out var rootUids));
shuttle = rootUids[0];
var path = new ResPath("/Maps/Shuttles/emergency.yml");
Assert.That(entManager.System<MapLoaderSystem>().TryLoadGrid(otherMap.MapId, path, out var grid));
shuttle = grid!.Value.Owner;
var dockingConfig = dockingSystem.GetDockingConfig(shuttle, map.MapUid);
Assert.That(dockingConfig, Is.EqualTo(null));

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class LoadoutNames : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "entity_name",
table: "profile_role_loadout",
type: "character varying(256)",
maxLength: 256,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "entity_name",
table: "profile_role_loadout");
}
}
}

View File

@@ -975,6 +975,11 @@ namespace Content.Server.Database.Migrations.Postgres
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("EntityName")
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("entity_name");
b.Property<int>("ProfileId")
.HasColumnType("integer")
.HasColumnName("profile_id");

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class LoadoutNames : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "entity_name",
table: "profile_role_loadout",
type: "TEXT",
maxLength: 256,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "entity_name",
table: "profile_role_loadout");
}
}
}

View File

@@ -921,6 +921,11 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("INTEGER")
.HasColumnName("profile_role_loadout_id");
b.Property<string>("EntityName")
.HasMaxLength(256)
.HasColumnType("TEXT")
.HasColumnName("entity_name");
b.Property<int>("ProfileId")
.HasColumnType("INTEGER")
.HasColumnName("profile_id");

View File

@@ -480,6 +480,12 @@ namespace Content.Server.Database
/// </summary>
public string RoleName { get; set; } = string.Empty;
/// <summary>
/// Custom name of the role loadout if it supports it.
/// </summary>
[MaxLength(256)]
public string? EntityName { get; set; }
/// <summary>
/// Store the saved loadout groups. These may get validated and removed when loaded at runtime.
/// </summary>

View File

@@ -0,0 +1,215 @@
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
using Content.Shared.Administration;
using Content.Shared.Database;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
namespace Content.Server.Administration.Commands;
/// <summary>
/// Allows admins to change certain CVars. This is different than the "cvar" command which is host only and can change any CVar.
/// </summary>
/// <remarks>
/// Possible todo for future, store default values for cvars, and allow resetting to default.
/// </remarks>
[AnyCommand]
public sealed class ChangeCvarCommand : IConsoleCommand
{
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IAdminLogManager _adminLogManager = default!;
[Dependency] private readonly CVarControlManager _cVarControlManager = default!;
/// <summary>
/// Searches the list of cvars for a cvar that matches the search string.
/// </summary>
private void SearchCVars(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length < 2)
{
shell.WriteLine(Loc.GetString("cmd-changecvar-search-no-arguments"));
return;
}
var cvars = _cVarControlManager.GetAllRunnableCvars(shell);
var matches = cvars
.Where(c =>
c.Name.Contains(args[1], StringComparison.OrdinalIgnoreCase)
|| c.ShortHelp?.Contains(args[1], StringComparison.OrdinalIgnoreCase) == true
|| c.LongHelp?.Contains(args[1], StringComparison.OrdinalIgnoreCase) == true
) // Might be very slow and stupid, but eh.
.ToList();
if (matches.Count == 0)
{
shell.WriteLine(Loc.GetString("cmd-changecvar-search-no-matches"));
return;
}
shell.WriteLine(Loc.GetString("cmd-changecvar-search-matches", ("count", matches.Count)));
shell.WriteLine(string.Join("\n", matches.Select(FormatCVarFullHelp)));
}
/// <summary>
/// Formats a CVar into a string for display.
/// </summary>
private string FormatCVarFullHelp(ChangableCVar cvar)
{
if (cvar.LongHelp != null && cvar.ShortHelp != null)
{
return $"{cvar.Name} - {cvar.LongHelp}";
}
// There is no help, no one is coming. We are all doomed.
return cvar.Name;
}
public string Command => "changecvar";
public string Description { get; } = Loc.GetString("cmd-changecvar-desc");
public string Help { get; } = Loc.GetString("cmd-changecvar-help");
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length == 0)
{
shell.WriteLine(Loc.GetString("cmd-changecvar-no-arguments"));
return;
}
var cvars = _cVarControlManager.GetAllRunnableCvars(shell);
var cvar = args[0];
if (cvar == "?")
{
if (cvars.Count == 0)
{
shell.WriteLine(Loc.GetString("cmd-changecvar-no-cvars"));
return;
}
shell.WriteLine(Loc.GetString("cmd-changecvar-available-cvars"));
shell.WriteLine(string.Join("\n", cvars.Select(FormatCVarFullHelp)));
return;
}
if (cvar == "search")
{
SearchCVars(shell, argStr, args);
return;
}
if (!_configurationManager.IsCVarRegistered(cvar)) // Might be a redunat check with the if statement below.
{
shell.WriteLine(Loc.GetString("cmd-changecvar-cvar-not-registered", ("cvar", cvar)));
return;
}
if (cvars.All(c => c.Name != cvar))
{
shell.WriteLine(Loc.GetString("cmd-changecvar-cvar-not-allowed"));
return;
}
if (args.Length == 1)
{
var value = _configurationManager.GetCVar<object>(cvar);
shell.WriteLine(value.ToString()!);
}
else
{
var value = args[1];
var type = _configurationManager.GetCVarType(cvar);
try
{
var parsed = CVarCommandUtil.ParseObject(type, value);
// Value check, is it in the min/max range?
var control = _cVarControlManager.GetCVar(cvar)!.Control; // Null check is done above.
var allowed = true;
if (control is { Min: not null, Max: not null })
{
switch (parsed) // This looks bad, and im not sorry.
{
case int intVal:
{
if (intVal < (int)control.Min || intVal > (int)control.Max)
{
allowed = false;
}
break;
}
case float floatVal:
{
if (floatVal < (float)control.Min || floatVal > (float)control.Max)
{
allowed = false;
}
break;
}
case long longVal:
{
if (longVal < (long)control.Min || longVal > (long)control.Max)
{
allowed = false;
}
break;
}
case ushort ushortVal:
{
if (ushortVal < (ushort)control.Min || ushortVal > (ushort)control.Max)
{
allowed = false;
}
break;
}
}
}
if (!allowed)
{
shell.WriteError(Loc.GetString("cmd-changecvar-value-out-of-range",
("min", control.Min ?? "-∞"),
("max", control.Max ?? "∞")));
return;
}
var oldValue = _configurationManager.GetCVar<object>(cvar);
_configurationManager.SetCVar(cvar, parsed);
_adminLogManager.Add(LogType.AdminCommands,
LogImpact.High,
$"{shell.Player!.Name} ({shell.Player!.UserId}) changed CVAR {cvar} from {oldValue.ToString()} to {parsed.ToString()}"
);
shell.WriteLine(Loc.GetString("cmd-changecvar-success", ("cvar", cvar), ("old", oldValue), ("value", parsed)));
}
catch (FormatException)
{
shell.WriteError(Loc.GetString("cmd-cvar-parse-error", ("type", type)));
}
}
}
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
var cvars = _cVarControlManager.GetAllRunnableCvars(shell);
if (args.Length == 1)
{
return CompletionResult.FromHintOptions(
cvars
.Select(c => new CompletionOption(c.Name, c.ShortHelp ?? c.Name)),
Loc.GetString("cmd-changecvar-arg-name"));
}
var cvar = args[0];
if (!_configurationManager.IsCVarRegistered(cvar))
return CompletionResult.Empty;
var type = _configurationManager.GetCVarType(cvar);
return CompletionResult.FromHint($"<{type.Name}>");
}
}

View File

@@ -1,11 +1,9 @@
using System.Linq;
using System.Numerics;
using Content.Server.GameTicking;
using Content.Server.Maps;
using Content.Shared.Administration;
using Robust.Server.Maps;
using Robust.Shared.Console;
using Robust.Shared.ContentPack;
using Robust.Shared.EntitySerialization;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
@@ -25,6 +23,7 @@ namespace Content.Server.Administration.Commands
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var entityManager = IoCManager.Resolve<IEntityManager>();
var gameTicker = entityManager.EntitySysManager.GetEntitySystem<GameTicker>();
var mapSys = entityManager.EntitySysManager.GetEntitySystem<SharedMapSystem>();
if (args.Length is not (2 or 4 or 5))
{
@@ -32,29 +31,28 @@ namespace Content.Server.Administration.Commands
return;
}
if (prototypeManager.TryIndex<GameMapPrototype>(args[1], out var gameMap))
{
if (!int.TryParse(args[0], out var mapId))
return;
var loadOptions = new MapLoadOptions()
{
LoadMap = false,
};
var stationName = args.Length == 5 ? args[4] : null;
if (args.Length >= 4 && int.TryParse(args[2], out var x) && int.TryParse(args[3], out var y))
{
loadOptions.Offset = new Vector2(x, y);
}
var grids = gameTicker.LoadGameMap(gameMap, new MapId(mapId), loadOptions, stationName);
shell.WriteLine($"Loaded {grids.Count} grids.");
}
else
if (!prototypeManager.TryIndex<GameMapPrototype>(args[1], out var gameMap))
{
shell.WriteError($"The given map prototype {args[0]} is invalid.");
return;
}
if (!int.TryParse(args[0], out var mapId))
return;
var stationName = args.Length == 5 ? args[4] : null;
Vector2? offset = null;
if (args.Length >= 4)
offset = new Vector2(int.Parse(args[2]), int.Parse(args[3]));
var id = new MapId(mapId);
var grids = mapSys.MapExists(id)
? gameTicker.MergeGameMap(gameMap, id, stationName: stationName, offset: offset)
: gameTicker.LoadGameMapWithId(gameMap, id, stationName: stationName, offset: offset);
shell.WriteLine($"Loaded {grids.Count} grids.");
}
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)

View File

@@ -1,10 +1,10 @@
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Robust.Server.GameObjects;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Map;
using System.Linq;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.Utility;
namespace Content.Server.Administration.Commands;
@@ -48,7 +48,7 @@ public sealed class PersistenceSave : IConsoleCommand
}
var mapLoader = _system.GetEntitySystem<MapLoaderSystem>();
mapLoader.SaveMap(mapId, saveFilePath);
mapLoader.TrySaveMap(mapId, new ResPath(saveFilePath));
shell.WriteLine(Loc.GetString("cmd-savemap-success"));
}
}

View File

@@ -118,8 +118,9 @@ namespace Content.Server.Administration.Commands
}
var xform = _entManager.GetComponent<TransformComponent>(playerEntity);
var xformSystem = _entManager.System<SharedTransformSystem>();
xform.Coordinates = coords;
xform.AttachToGridOrMap();
xformSystem.AttachToGridOrMap(playerEntity, xform);
if (_entManager.TryGetComponent(playerEntity, out PhysicsComponent? physics))
{
_entManager.System<SharedPhysicsSystem>().SetLinearVelocity(playerEntity, Vector2.Zero, body: physics);

View File

@@ -0,0 +1,125 @@
using System.Linq;
using System.Reflection;
using Content.Shared.CCVar.CVarAccess;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Player;
using Robust.Shared.Reflection;
namespace Content.Server.Administration.Managers;
/// <summary>
/// Manages the control of CVars via the <see cref="Content.Shared.CCVar.CVarAccess.CVarControl"/> attribute.
/// </summary>
public sealed class CVarControlManager : IPostInjectInit
{
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
[Dependency] private readonly IAdminManager _adminManager = default!;
[Dependency] private readonly ILocalizationManager _localizationManager = default!;
[Dependency] private readonly ILogManager _logger = default!;
private readonly List<ChangableCVar> _changableCvars = new();
private ISawmill _sawmill = default!;
void IPostInjectInit.PostInject()
{
_sawmill = _logger.GetSawmill("cvarcontrol");
}
public void Initialize()
{
RegisterCVars();
}
private void RegisterCVars()
{
if (_changableCvars.Count != 0)
{
_sawmill.Warning("CVars already registered, overwriting.");
_changableCvars.Clear();
}
var validCvarsDefs = _reflectionManager.FindTypesWithAttribute<CVarDefsAttribute>();
foreach (var type in validCvarsDefs)
{
foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy))
{
var allowed = field.GetCustomAttribute<CVarControl>();
if (allowed == null)
{
continue;
}
var cvarDef = (CVarDef)field.GetValue(null)!;
_changableCvars.Add(new ChangableCVar(cvarDef.Name, allowed, _localizationManager));
}
}
_sawmill.Info($"Registered {_changableCvars.Count} CVars.");
}
/// <summary>
/// Gets all CVars that the player can change.
/// </summary>
public List<ChangableCVar> GetAllRunnableCvars(IConsoleShell shell)
{
// Not a player, running as server. We COULD return all cvars,
// but a check later down the line will prevent it from anyways. Use the "cvar" command instead.
if (shell.Player == null)
return [];
return GetAllRunnableCvars(shell.Player);
}
public List<ChangableCVar> GetAllRunnableCvars(ICommonSession session)
{
var adminData = _adminManager.GetAdminData(session);
if (adminData == null)
return []; // Not an admin
return _changableCvars
.Where(cvar => adminData.HasFlag(cvar.Control.AdminFlags))
.ToList();
}
public ChangableCVar? GetCVar(string name)
{
return _changableCvars.FirstOrDefault(cvar => cvar.Name == name);
}
}
public sealed class ChangableCVar
{
private const string LocPrefix = "changecvar";
public string Name { get; }
// Holding a reference to the attribute might be skrunkly? Not sure how much mem it eats up.
public CVarControl Control { get; }
public string? ShortHelp;
public string? LongHelp;
public ChangableCVar(string name, CVarControl control, ILocalizationManager loc)
{
Name = name;
Control = control;
if (loc.TryGetString($"{LocPrefix}-simple-{name.Replace('.', '_')}", out var simple))
{
ShortHelp = simple;
}
if (loc.TryGetString($"{LocPrefix}-full-{name.Replace('.', '_')}", out var longHelp))
{
LongHelp = longHelp;
}
// If one is set and the other is not, we throw
if (ShortHelp == null && LongHelp != null || ShortHelp != null && LongHelp == null)
{
throw new InvalidOperationException("Short and long help must both be set or both be null.");
}
}
}

View File

@@ -11,6 +11,7 @@ using Content.Server.StationRecords.Systems;
using Content.Shared.Administration;
using Content.Shared.Administration.Events;
using Content.Shared.CCVar;
using Content.Shared.Forensics.Components;
using Content.Shared.GameTicking;
using Content.Shared.Hands.Components;
using Content.Shared.IdentityManagement;

View File

@@ -1,8 +1,7 @@
using Robust.Server.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Utility;
namespace Content.Server.Administration.Systems;
@@ -11,8 +10,7 @@ namespace Content.Server.Administration.Systems;
/// </summary>
public sealed class AdminTestArenaSystem : EntitySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly MapLoaderSystem _map = default!;
[Dependency] private readonly MapLoaderSystem _loader = default!;
[Dependency] private readonly MetaDataSystem _metaDataSystem = default!;
public const string ArenaMapPath = "/Maps/Test/admin_test_arena.yml";
@@ -28,26 +26,24 @@ public sealed class AdminTestArenaSystem : EntitySystem
{
return (arenaMap, arenaGrid);
}
else
{
ArenaGrid[admin.UserId] = null;
return (arenaMap, null);
}
}
ArenaMap[admin.UserId] = _mapManager.GetMapEntityId(_mapManager.CreateMap());
_metaDataSystem.SetEntityName(ArenaMap[admin.UserId], $"ATAM-{admin.Name}");
var grids = _map.LoadMap(Comp<MapComponent>(ArenaMap[admin.UserId]).MapId, ArenaMapPath);
if (grids.Count != 0)
{
_metaDataSystem.SetEntityName(grids[0], $"ATAG-{admin.Name}");
ArenaGrid[admin.UserId] = grids[0];
}
else
{
ArenaGrid[admin.UserId] = null;
return (arenaMap, null);
}
return (ArenaMap[admin.UserId], ArenaGrid[admin.UserId]);
var path = new ResPath(ArenaMapPath);
if (!_loader.TryLoadMap(path, out var map, out var grids))
throw new Exception($"Failed to load admin arena");
ArenaMap[admin.UserId] = map.Value.Owner;
_metaDataSystem.SetEntityName(map.Value.Owner, $"ATAM-{admin.Name}");
var grid = grids.FirstOrNull();
ArenaGrid[admin.UserId] = grid?.Owner;
if (grid != null)
_metaDataSystem.SetEntityName(grid.Value.Owner, $"ATAG-{admin.Name}");
return (map.Value.Owner, grid?.Owner);
}
}

View File

@@ -137,7 +137,7 @@ public sealed partial class AdminVerbSystem
var board = Spawn("ChessBoard", xform.Coordinates);
var session = _tabletopSystem.EnsureSession(Comp<TabletopGameComponent>(board));
xform.Coordinates = EntityCoordinates.FromMap(_mapManager, session.Position);
xform.WorldRotation = Angle.Zero;
_transformSystem.SetWorldRotationNoLerp((args.Target, xform), Angle.Zero);
},
Impact = LogImpact.Extreme,
Message = string.Join(": ", chessName, Loc.GetString("admin-smite-chess-dimension-description"))

View File

@@ -105,7 +105,7 @@ namespace Content.Server.Administration.Systems
mark.Text = Loc.GetString("toolshed-verb-mark");
mark.Message = Loc.GetString("toolshed-verb-mark-description");
mark.Category = VerbCategory.Admin;
mark.Act = () => _toolshed.InvokeCommand(player, "=> $marked", Enumerable.Repeat(args.Target, 1), out _);
mark.Act = () => _toolshed.InvokeCommand(player, "=> $marked", new List<EntityUid> {args.Target}, out _);
mark.Impact = LogImpact.Low;
args.Verbs.Add(mark);
@@ -370,7 +370,7 @@ namespace Content.Server.Administration.Systems
}
if (lawBoundComponent != null && target != null)
if (lawBoundComponent != null && target != null && _adminManager.HasAdminFlag(player, AdminFlags.Moderator))
{
args.Verbs.Add(new Verb()
{

View File

@@ -9,8 +9,7 @@ public sealed class MarkedCommand : ToolshedCommand
[CommandImplementation]
public IEnumerable<EntityUid> Marked(IInvocationContext ctx)
{
var res = (IEnumerable<EntityUid>?)ctx.ReadVar("marked");
res ??= Array.Empty<EntityUid>();
return res;
var marked = ctx.ReadVar("marked") as IEnumerable<EntityUid>;
return marked ?? Array.Empty<EntityUid>();
}
}

View File

@@ -1,38 +1,79 @@
using Content.Server.Administration;
using Content.Server.Chat;
using Content.Server.Chat.Systems;
using Content.Shared.Administration;
using Robust.Shared.Audio;
using Robust.Shared.Console;
using Robust.Shared.ContentPack;
using Robust.Shared.Prototypes;
namespace Content.Server.Announcements
namespace Content.Server.Announcements;
[AdminCommand(AdminFlags.Moderator)]
public sealed class AnnounceCommand : LocalizedEntityCommands
{
[AdminCommand(AdminFlags.Moderator)]
public sealed class AnnounceCommand : IConsoleCommand
{
public string Command => "announce";
public string Description => "Send an in-game announcement.";
public string Help => $"{Command} <sender> <message> or {Command} <message> to send announcement as CentCom.";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var chat = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ChatSystem>();
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IResourceManager _res = default!;
if (args.Length == 0)
public override string Command => "announce";
public override string Description => Loc.GetString("cmd-announce-desc");
public override string Help => Loc.GetString("cmd-announce-help", ("command", Command));
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
switch (args.Length)
{
case 0:
shell.WriteError(Loc.GetString("shell-need-minimum-one-argument"));
return;
case > 4:
shell.WriteError(Loc.GetString("shell-wrong-arguments-number"));
return;
}
var message = args[0];
var sender = Loc.GetString("cmd-announce-sender");
var color = Color.Gold;
var sound = new SoundPathSpecifier("/Audio/Announcements/announce.ogg");
// Optional sender argument
if (args.Length >= 2)
sender = args[1];
// Optional color argument
if (args.Length >= 3)
{
try
{
shell.WriteError("Not enough arguments! Need at least 1.");
color = Color.FromHex(args[2]);
}
catch
{
shell.WriteError(Loc.GetString("shell-invalid-color-hex"));
return;
}
if (args.Length == 1)
{
chat.DispatchGlobalAnnouncement(args[0], colorOverride: Color.Gold);
}
else
{
// Explicit IEnumerable<string> due to overload ambiguity on .NET 9
var message = string.Join(' ', (IEnumerable<string>)new ArraySegment<string>(args, 1, args.Length-1));
chat.DispatchGlobalAnnouncement(message, args[0], colorOverride: Color.Gold);
}
shell.WriteLine("Sent!");
}
// Optional sound argument
if (args.Length >= 4)
sound = new SoundPathSpecifier(args[3]);
_chat.DispatchGlobalAnnouncement(message, sender, true, sound, color);
shell.WriteLine(Loc.GetString("shell-command-success"));
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
return args.Length switch
{
1 => CompletionResult.FromHint(Loc.GetString("cmd-announce-arg-message")),
2 => CompletionResult.FromHint(Loc.GetString("cmd-announce-arg-sender")),
3 => CompletionResult.FromHint(Loc.GetString("cmd-announce-arg-color")),
4 => CompletionResult.FromHintOptions(
CompletionHelper.AudioFilePath(args[3], _proto, _res),
Loc.GetString("cmd-announce-arg-sound")
),
_ => CompletionResult.Empty
};
}
}

View File

@@ -4,7 +4,6 @@ using Content.Server.Chat.Managers;
using Content.Server.Jittering;
using Content.Server.Mind;
using Content.Server.Stunnable;
using Content.Shared.Actions;
using Content.Shared.Anomaly;
using Content.Shared.Anomaly.Components;
using Content.Shared.Anomaly.Effects;
@@ -24,8 +23,6 @@ public sealed class InnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
{
[Dependency] private readonly IAdminLogManager _adminLog = default!;
[Dependency] private readonly AnomalySystem _anomaly = default!;
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly BodySystem _body = default!;
[Dependency] private readonly IChatManager _chat = default!;

View File

@@ -10,10 +10,8 @@ using Content.Shared.Atmos.Monitor;
using Content.Shared.Atmos.Monitor.Components;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.Pinpointer;
using Content.Shared.Tag;
using Robust.Server.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Timing;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
@@ -25,11 +23,9 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
[Dependency] private readonly AirAlarmSystem _airAlarmSystem = default!;
[Dependency] private readonly AtmosDeviceNetworkSystem _atmosDevNet = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly TagSystem _tagSystem = default!;
[Dependency] private readonly MapSystem _mapSystem = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
[Dependency] private readonly NavMapSystem _navMapSystem = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly DeviceListSystem _deviceListSystem = default!;
private const float UpdateTime = 1.0f;
@@ -54,7 +50,7 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
SubscribeLocalEvent<AtmosAlertsDeviceComponent, AnchorStateChangedEvent>(OnDeviceAnchorChanged);
}
#region Event handling
#region Event handling
private void OnConsoleInit(EntityUid uid, AtmosAlertsComputerComponent component, ComponentInit args)
{

View File

@@ -61,9 +61,9 @@ public sealed partial class AtmosphereSystem
mixtures[5].Temperature = 5000f;
// 6: (Walk-In) Freezer
mixtures[6].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesStandard);
mixtures[6].AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesStandard);
mixtures[6].Temperature = 235f; // Little colder than an actual freezer but gives a grace period to get e.g. themomachines set up, should keep warm for a few door openings
mixtures[6].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesFreezer);
mixtures[6].AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesFreezer);
mixtures[6].Temperature = Atmospherics.FreezerTemp; // Little colder than an actual freezer but gives a grace period to get e.g. themomachines set up, should keep warm for a few door openings
// 7: Nitrogen (101kpa) for vox rooms
mixtures[7].AdjustMoles(Gas.Nitrogen, Atmospherics.MolesCellStandard);

View File

@@ -118,7 +118,7 @@ namespace Content.Server.Atmos.EntitySystems
return;
// Used by ExperiencePressureDifference to correct push/throw directions from tile-relative to physics world.
var gridWorldRotation = xforms.GetComponent(gridAtmosphere).WorldRotation;
var gridWorldRotation = _transformSystem.GetWorldRotation(gridAtmosphere);
// If we're using monstermos, smooth out the yeet direction to follow the flow
if (MonstermosEqualization)

View File

@@ -178,7 +178,7 @@ namespace Content.Server.Atmos.EntitySystems
if (tile.Hotspot.Bypassing)
{
tile.Hotspot.Volume = tile.Air.ReactionResults[GasReaction.Fire] * Atmospherics.FireGrowthRate;
tile.Hotspot.Volume = tile.Air.ReactionResults[(byte)GasReaction.Fire] * Atmospherics.FireGrowthRate;
tile.Hotspot.Temperature = tile.Air.Temperature;
}
else
@@ -187,7 +187,7 @@ namespace Content.Server.Atmos.EntitySystems
affected.Temperature = tile.Hotspot.Temperature;
React(affected, tile);
tile.Hotspot.Temperature = affected.Temperature;
tile.Hotspot.Volume = affected.ReactionResults[GasReaction.Fire] * Atmospherics.FireGrowthRate;
tile.Hotspot.Volume = affected.ReactionResults[(byte)GasReaction.Fire] * Atmospherics.FireGrowthRate;
Merge(tile.Air, affected);
}

View File

@@ -9,10 +9,12 @@ using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Administration.Logs;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Monitor;
using Content.Shared.Atmos.Monitor.Components;
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.Database;
using Content.Shared.DeviceLinking;
using Content.Shared.DeviceNetwork;
using Content.Shared.DeviceNetwork.Systems;
@@ -37,6 +39,7 @@ namespace Content.Server.Atmos.Monitor.Systems;
public sealed class AirAlarmSystem : EntitySystem
{
[Dependency] private readonly AccessReaderSystem _access = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly AtmosAlarmableSystem _atmosAlarmable = default!;
[Dependency] private readonly AtmosDeviceNetworkSystem _atmosDevNet = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNet = default!;
@@ -296,6 +299,7 @@ public sealed class AirAlarmSystem : EntitySystem
addr = netConn.Address;
}
_adminLogger.Add(LogType.AtmosDeviceSetting, LogImpact.Medium, $"{ToPrettyString(args.Actor)} changed {ToPrettyString(uid)} mode to {args.Mode}");
SetMode(uid, addr, args.Mode, false);
}
else
@@ -307,15 +311,26 @@ public sealed class AirAlarmSystem : EntitySystem
private void OnUpdateAutoMode(EntityUid uid, AirAlarmComponent component, AirAlarmUpdateAutoModeMessage args)
{
component.AutoMode = args.Enabled;
_adminLogger.Add(LogType.AtmosDeviceSetting, LogImpact.Medium, $"{ToPrettyString(args.Actor)} changed {ToPrettyString(uid)} auto mode to {args.Enabled}");
UpdateUI(uid, component);
}
private void OnUpdateThreshold(EntityUid uid, AirAlarmComponent component, AirAlarmUpdateAlarmThresholdMessage args)
{
if (AccessCheck(uid, args.Actor, component))
{
if (args.Gas != null)
_adminLogger.Add(LogType.AtmosDeviceSetting, LogImpact.Medium, $"{ToPrettyString(args.Actor)} changed {args.Address} {args.Gas} {args.Type} threshold using {ToPrettyString(uid)}");
else
_adminLogger.Add(LogType.AtmosDeviceSetting, LogImpact.Medium, $"{ToPrettyString(args.Actor)} changed {args.Address} {args.Type} threshold using {ToPrettyString(uid)}");
SetThreshold(uid, args.Address, args.Type, args.Threshold, args.Gas);
}
else
{
UpdateUI(uid, component);
}
}
private void OnUpdateDeviceData(EntityUid uid, AirAlarmComponent component, AirAlarmUpdateDeviceDataMessage args)
@@ -323,6 +338,8 @@ public sealed class AirAlarmSystem : EntitySystem
if (AccessCheck(uid, args.Actor, component)
&& _deviceList.ExistsInDeviceList(uid, args.Address))
{
_adminLogger.Add(LogType.AtmosDeviceSetting, LogImpact.Medium, $"{ToPrettyString(args.Actor)} changed {args.Address} settings using {ToPrettyString(uid)}");
SetDeviceData(uid, args.Address, args.Data);
}
else
@@ -344,6 +361,7 @@ public sealed class AirAlarmSystem : EntitySystem
case GasVentPumpData ventData:
foreach (string addr in component.VentData.Keys)
{
_adminLogger.Add(LogType.AtmosDeviceSetting, LogImpact.Medium, $"{ToPrettyString(args.Actor)} copied settings to vent {addr}");
SetData(uid, addr, args.Data);
}
break;
@@ -351,6 +369,7 @@ public sealed class AirAlarmSystem : EntitySystem
case GasVentScrubberData scrubberData:
foreach (string addr in component.ScrubberData.Keys)
{
_adminLogger.Add(LogType.AtmosDeviceSetting, LogImpact.Medium, $"{ToPrettyString(args.Actor)} copied settings to scrubber {addr}");
SetData(uid, addr, args.Data);
}
break;
@@ -379,6 +398,7 @@ public sealed class AirAlarmSystem : EntitySystem
if (!_access.IsAllowed(user.Value, uid, reader))
{
_popup.PopupEntity(Loc.GetString("air-alarm-ui-access-denied"), user.Value, user.Value);
_adminLogger.Add(LogType.AtmosDeviceSetting, LogImpact.Low, $"{ToPrettyString(user)} attempted to access {ToPrettyString(uid)} without access");
return false;
}

View File

@@ -9,9 +9,11 @@ using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Administration.Logs;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Monitor;
using Content.Shared.Atmos.Piping.Components;
using Content.Shared.Database;
using Content.Shared.DeviceNetwork;
using Content.Shared.Power;
using Content.Shared.Tag;
@@ -25,6 +27,7 @@ namespace Content.Server.Atmos.Monitor.Systems;
// a danger), and atmos (which triggers based on set thresholds).
public sealed class AtmosMonitorSystem : EntitySystem
{
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly AtmosDeviceSystem _atmosDeviceSystem = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNetSystem = default!;
@@ -393,21 +396,74 @@ public sealed class AtmosMonitorSystem : EntitySystem
if (!Resolve(uid, ref monitor))
return;
// Used for logging after the switch statement
string logPrefix = "";
string logValueSuffix = "";
AtmosAlarmThreshold? logPreviousThreshold = null;
switch (type)
{
case AtmosMonitorThresholdType.Pressure:
logPrefix = "pressure";
logValueSuffix = "kPa";
logPreviousThreshold = monitor.PressureThreshold;
monitor.PressureThreshold = threshold;
break;
case AtmosMonitorThresholdType.Temperature:
logPrefix = "temperature";
logValueSuffix = "K";
logPreviousThreshold = monitor.TemperatureThreshold;
monitor.TemperatureThreshold = threshold;
break;
case AtmosMonitorThresholdType.Gas:
if (gas == null || monitor.GasThresholds == null)
return;
logPrefix = ((Gas) gas).ToString();
logValueSuffix = "kPa";
monitor.GasThresholds.TryGetValue((Gas) gas, out logPreviousThreshold);
monitor.GasThresholds[(Gas) gas] = threshold;
break;
}
// Admin log each change separately rather than logging the whole state
if (logPreviousThreshold != null)
{
if (threshold.Ignore != logPreviousThreshold.Ignore)
{
string enabled = threshold.Ignore ? "disabled" : "enabled";
_adminLogger.Add(
LogType.AtmosDeviceSetting,
LogImpact.Medium,
$"{ToPrettyString(uid)} {logPrefix} thresholds {enabled}"
);
}
foreach (var change in threshold.GetChanges(logPreviousThreshold))
{
if (change.Current.Enabled != change.Previous?.Enabled)
{
string enabled = change.Current.Enabled ? "enabled" : "disabled";
_adminLogger.Add(
LogType.AtmosDeviceSetting,
LogImpact.Medium,
$"{ToPrettyString(uid)} {logPrefix} {change.Type} {enabled}"
);
}
if (change.Current.Value != change.Previous?.Value)
{
_adminLogger.Add(
LogType.AtmosDeviceSetting,
LogImpact.Medium,
$"{ToPrettyString(uid)} {logPrefix} {change.Type} changed from {change.Previous?.Value} {logValueSuffix} to {change.Current.Value} {logValueSuffix}"
);
}
}
}
}
/// <summary>

View File

@@ -11,8 +11,12 @@ namespace Content.Server.Atmos.Piping.Unary.Components
[RegisterComponent]
public sealed partial class GasVentPumpComponent : Component
{
/// <summary>
/// Identifies if the device is enabled by an air alarm. Does not indicate if the device is powered.
/// By default, all air vents start enabled, whether linked to an alarm or not.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool Enabled { get; set; } = false;
public bool Enabled { get; set; } = true;
[ViewVariables]
public bool IsDirty { get; set; } = false;

View File

@@ -9,8 +9,12 @@ namespace Content.Server.Atmos.Piping.Unary.Components
[Access(typeof(GasVentScrubberSystem))]
public sealed partial class GasVentScrubberComponent : Component
{
/// <summary>
/// Identifies if the device is enabled by an air alarm. Does not indicate if the device is powered.
/// By default, all air scrubbers start enabled, whether linked to an alarm or not.
/// </summary>
[DataField]
public bool Enabled { get; set; } = false;
public bool Enabled { get; set; } = true;
[DataField]
public bool IsDirty { get; set; } = false;

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