Merge remote-tracking branch 'space-wizards/master'

# Conflicts:
#	Content.Client/Weapons/Ranged/Systems/GunSystem.cs
#	Content.IntegrationTests/Tests/PostMapInitTest.cs
#	Content.Server/Entry/EntryPoint.cs
#	Content.Server/Ghost/GhostSystem.cs
#	Content.Server/IoC/ServerContentIoC.cs
#	Content.Server/Shuttles/Systems/ArrivalsSystem.cs
#	Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs
#	Content.Server/Storage/EntitySystems/SpawnItemsOnUseSystem.cs
#	Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs
#	Content.Server/Weapons/Ranged/Systems/GunSystem.Battery.cs
#	Content.Server/Weapons/Ranged/Systems/GunSystem.Cartridges.cs
#	Content.Server/Weapons/Ranged/Systems/GunSystem.cs
#	Content.Shared/Damage/Systems/DamageableSystem.cs
#	Content.Shared/Follower/FollowerSystem.cs
#	Resources/Locale/en-US/_strings/actions/actions/dna-scrambler.ftl
#	Resources/Locale/en-US/_strings/administration/commands/change-cvar-command.ftl
#	Resources/Locale/en-US/discord/vote-notifications.ftl
#	Resources/Prototypes/Catalog/Fills/Crates/armory.yml
#	Resources/Prototypes/Catalog/Fills/Lockers/security.yml
#	Resources/Prototypes/Entities/Clothing/OuterClothing/misc.yml
#	Resources/Prototypes/Entities/Objects/Specific/Medical/handheld_crew_monitor.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Guns/Rifles/rifles.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Guns/SMGs/smgs.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Guns/Shotguns/shotguns.yml
#	Resources/Prototypes/Voice/speech_emotes.yml
This commit is contained in:
Vigers Ray 2025-02-19 13:16:37 +03:00
commit ed0a30bb64
273 changed files with 59076 additions and 37668 deletions

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]
@ -52,17 +53,19 @@ public class MapLoadBenchmark
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

@ -141,6 +141,11 @@ public sealed class ChemistryGuideDataSystem : SharedChemistryGuideDataSystem
{
return _reagentSources.GetValueOrDefault(id) ?? new List<ReagentSourceData>();
}
// Is handled on server and updated on client via ReagentGuideRegistryChangedEvent
public override void ReloadAllReagentPrototypes()
{
}
}
/// <summary>

View file

@ -7,6 +7,7 @@ namespace Content.Client.Construction
[RegisterComponent]
public sealed partial class ConstructionGhostComponent : Component
{
public int GhostId { get; set; }
[ViewVariables] public ConstructionPrototype? Prototype { get; set; }
}
}

View file

@ -55,6 +55,12 @@ namespace Content.Client.Construction
.Register<ConstructionSystem>();
SubscribeLocalEvent<ConstructionGhostComponent, ExaminedEvent>(HandleConstructionGhostExamined);
SubscribeLocalEvent<ConstructionGhostComponent, ComponentShutdown>(HandleGhostComponentShutdown);
}
private void HandleGhostComponentShutdown(EntityUid uid, ConstructionGhostComponent component, ComponentShutdown args)
{
ClearGhost(component.GhostId);
}
private void OnConstructionGuideReceived(ResponseConstructionGuide ev)
@ -205,8 +211,9 @@ namespace Content.Client.Construction
ghost = EntityManager.SpawnEntity("constructionghost", loc);
var comp = EntityManager.GetComponent<ConstructionGhostComponent>(ghost.Value);
comp.Prototype = prototype;
comp.GhostId = ghost.GetHashCode();
EntityManager.GetComponent<TransformComponent>(ghost.Value).LocalRotation = dir.ToAngle();
_ghosts.Add(ghost.GetHashCode(), ghost.Value);
_ghosts.Add(comp.GhostId, ghost.Value);
var sprite = EntityManager.GetComponent<SpriteComponent>(ghost.Value);
sprite.Color = new Color(48, 255, 48, 128);

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

@ -0,0 +1,59 @@
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 lightScale = viewport.LightRenderTarget.Size / (Vector2) viewport.Size;
var newScale = viewport.RenderScale / (Vector2.One / lightScale);
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 = 2f;
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 * 5f);
}
}

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,122 @@
using System.Numerics;
using Content.Shared.Light.Components;
using Content.Shared.Maps;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
namespace Content.Client.Light;
public sealed class RoofOverlay : Overlay
{
private readonly IEntityManager _entManager;
[Dependency] private readonly IMapManager _mapManager = default!;
[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();
private List<Entity<MapGridComponent>> _grids = 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 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;
_grids.Clear();
_mapManager.FindGridsIntersecting(args.MapId, bounds, ref _grids);
for (var i = _grids.Count - 1; i >= 0; i--)
{
var grid = _grids[i];
if (_entManager.HasComponent<RoofComponent>(grid.Owner))
continue;
_grids.RemoveAt(i);
}
if (_grids.Count == 0)
return;
var lightScale = viewport.LightRenderTarget.Size / (Vector2) viewport.Size;
var scale = viewport.RenderScale / (Vector2.One / lightScale);
worldHandle.RenderInRenderTarget(target,
() =>
{
foreach (var grid in _grids)
{
if (!_entManager.TryGetComponent(grid.Owner, out RoofComponent? roof))
continue;
var invMatrix = target.GetWorldToLocalMatrix(eye, scale);
var gridMatrix = _xformSystem.GetWorldMatrix(grid.Owner);
var matty = Matrix3x2.Multiply(gridMatrix, invMatrix);
worldHandle.SetTransform(matty);
var tileEnumerator = _mapSystem.GetTilesEnumerator(grid.Owner, 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(grid.Owner, 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.Comp.TileSize);
worldHandle.DrawRect(local, roof.Color);
}
}
}, null);
worldHandle.SetTransform(Matrix3x2.Identity);
}
}

View file

@ -0,0 +1,96 @@
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;
_grids.Clear();
_mapManager.FindGridsIntersecting(mapId, bounds, ref _grids, approx: true);
if (_grids.Count == 0)
return;
var lightScale = viewport.LightRenderTarget.Size / (Vector2) viewport.Size;
var scale = viewport.RenderScale / (Vector2.One / lightScale);
args.WorldHandle.RenderInRenderTarget(target,
() =>
{
var invMatrix = target.GetWorldToLocalMatrix(viewport.Eye, scale);
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

@ -6,7 +6,7 @@
MinSize="800 128">
<BoxContainer Orientation="Vertical" VerticalExpand="True">
<BoxContainer Name="RoleNameBox" Orientation="Vertical" Margin="10">
<Label Name="LoadoutNameLabel" Text="{Loc 'loadout-name-edit-label'}"/>
<Label Name="LoadoutNameLabel"/>
<PanelContainer HorizontalExpand="True" SetHeight="24">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#1B1B1E" />

View file

@ -40,6 +40,10 @@ public sealed partial class LoadoutWindow : FancyWindow
{
var name = loadout.EntityName;
LoadoutNameLabel.Text = proto.NameDataset == null ?
Loc.GetString("loadout-name-edit-label") :
Loc.GetString("loadout-name-edit-label-dataset");
RoleNameEdit.ToolTip = Loc.GetString(
"loadout-name-edit-tooltip",
("max", HumanoidCharacterProfile.MaxLoadoutNameLength));

View file

@ -30,8 +30,7 @@ namespace Content.Client.PDA
private void CreateMenu()
{
_menu = this.CreateWindow<PdaMenu>();
_menu.OpenCenteredLeft();
_menu = this.CreateWindowCenteredLeft<PdaMenu>();
_menu.FlashLightToggleButton.OnToggled += _ =>
{

View file

@ -130,6 +130,12 @@ namespace Content.Client.Popups
PopupMessage(message, type, coordinates, null);
}
public override void PopupPredictedCoordinates(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small)
{
if (recipient != null && _timing.IsFirstTimePredicted)
PopupCoordinates(message, coordinates, recipient.Value, type);
}
private void PopupCursorInternal(string? message, PopupType type, bool recordReplay)
{
if (message == null)

View file

@ -1,8 +1,10 @@
using System.Numerics;
using Content.Client.UserInterface.Systems.Storage;
using Content.Client.UserInterface.Systems.Storage.Controls;
using Content.Shared.Storage;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.Storage;
@ -11,6 +13,8 @@ public sealed class StorageBoundUserInterface : BoundUserInterface
{
private StorageWindow? _window;
public Vector2? Position => _window?.Position;
public StorageBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
@ -21,7 +25,7 @@ public sealed class StorageBoundUserInterface : BoundUserInterface
_window = IoCManager.Resolve<IUserInterfaceManager>()
.GetUIController<StorageUIController>()
.CreateStorageWindow(Owner);
.CreateStorageWindow(this);
if (EntMan.TryGetComponent(Owner, out StorageComponent? storage))
{
@ -50,10 +54,20 @@ public sealed class StorageBoundUserInterface : BoundUserInterface
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
Reclaim();
}
public void CloseWindow(Vector2 position)
{
if (_window == null)
return;
// Update its position before potentially saving.
// Listen it makes sense okay.
LayoutContainer.SetPosition(_window, position);
_window?.Close();
}
public void Hide()
{
if (_window == null)
@ -70,6 +84,15 @@ public sealed class StorageBoundUserInterface : BoundUserInterface
_window.Visible = true;
}
public void Show(Vector2 position)
{
if (_window == null)
return;
Show();
LayoutContainer.SetPosition(_window, position);
}
public void ReOpen()
{
_window?.Orphan();

View file

@ -19,6 +19,8 @@ public sealed class StorageSystem : SharedStorageSystem
private Dictionary<EntityUid, ItemStorageLocation> _oldStoredItems = new();
private List<(StorageBoundUserInterface Bui, bool Value)> _queuedBuis = new();
public override void Initialize()
{
base.Initialize();
@ -72,7 +74,7 @@ public sealed class StorageSystem : SharedStorageSystem
if (NestedStorage && player != null && ContainerSystem.TryGetContainingContainer((uid, null, null), out var container) &&
UI.TryGetOpenUi<StorageBoundUserInterface>(container.Owner, StorageComponent.StorageUiKey.Key, out var containerBui))
{
containerBui.Hide();
_queuedBuis.Add((containerBui, false));
}
}
}
@ -89,7 +91,7 @@ public sealed class StorageSystem : SharedStorageSystem
{
if (UI.TryGetOpenUi<StorageBoundUserInterface>(uid, StorageComponent.StorageUiKey.Key, out var storageBui))
{
storageBui.Hide();
_queuedBuis.Add((storageBui, false));
}
}
@ -97,7 +99,7 @@ public sealed class StorageSystem : SharedStorageSystem
{
if (UI.TryGetOpenUi<StorageBoundUserInterface>(uid, StorageComponent.StorageUiKey.Key, out var storageBui))
{
storageBui.Show();
_queuedBuis.Add((storageBui, true));
}
}
@ -152,4 +154,30 @@ public sealed class StorageSystem : SharedStorageSystem
}
}
}
public override void Update(float frameTime)
{
base.Update(frameTime);
if (!_timing.IsFirstTimePredicted)
{
return;
}
// This update loop exists just to synchronize with UISystem and avoid 1-tick delays.
// If deferred opens / closes ever get removed you can dump this.
foreach (var (bui, open) in _queuedBuis)
{
if (open)
{
bui.Show();
}
else
{
bui.Hide();
}
}
_queuedBuis.Clear();
}
}

View file

@ -9,6 +9,7 @@ using Content.Shared.IdentityManagement;
using Content.Shared.Input;
using Content.Shared.Item;
using Content.Shared.Storage;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
@ -190,6 +191,26 @@ public sealed class StorageWindow : BaseWindow
BuildGridRepresentation();
}
private void CloseParent()
{
if (StorageEntity == null)
return;
var containerSystem = _entity.System<SharedContainerSystem>();
var uiSystem = _entity.System<UserInterfaceSystem>();
if (containerSystem.TryGetContainingContainer(StorageEntity.Value, out var container) &&
_entity.TryGetComponent(container.Owner, out StorageComponent? storage) &&
storage.Container.Contains(StorageEntity.Value) &&
uiSystem
.TryGetOpenUi<StorageBoundUserInterface>(container.Owner,
StorageComponent.StorageUiKey.Key,
out var parentBui))
{
parentBui.CloseWindow(Position);
}
}
private void BuildGridRepresentation()
{
if (!_entity.TryGetComponent<StorageComponent>(StorageEntity, out var comp) || comp.Grid.Count == 0)
@ -212,7 +233,9 @@ public sealed class StorageWindow : BaseWindow
};
exitButton.OnPressed += _ =>
{
// Close ourselves and all parent BUIs.
Close();
CloseParent();
};
exitButton.OnKeyBindDown += args =>
{
@ -220,6 +243,7 @@ public sealed class StorageWindow : BaseWindow
if (!args.Handled && args.Function == ContentKeyFunctions.ActivateItemInWorld)
{
Close();
CloseParent();
args.Handle();
}
};
@ -258,7 +282,8 @@ public sealed class StorageWindow : BaseWindow
var containerSystem = _entity.System<SharedContainerSystem>();
if (containerSystem.TryGetContainingContainer(StorageEntity.Value, out var container) &&
_entity.TryGetComponent(container.Owner, out StorageComponent? storage))
_entity.TryGetComponent(container.Owner, out StorageComponent? storage) &&
storage.Container.Contains(StorageEntity.Value))
{
Close();
@ -267,7 +292,7 @@ public sealed class StorageWindow : BaseWindow
StorageComponent.StorageUiKey.Key,
out var parentBui))
{
parentBui.Show();
parentBui.Show(Position);
}
}
};
@ -412,6 +437,8 @@ public sealed class StorageWindow : BaseWindow
{
if (storageComp.StoredItems.TryGetValue(ent, out var updated))
{
data.Control.Marked = IsMarked(ent);
if (data.Loc.Equals(updated))
{
DebugTools.Assert(data.Control.Location == updated);
@ -450,12 +477,7 @@ public sealed class StorageWindow : BaseWindow
var gridPiece = new ItemGridPiece((ent, itemEntComponent), loc, _entity)
{
MinSize = size,
Marked = _contained.IndexOf(ent) switch
{
0 => ItemGridPieceMarks.First,
1 => ItemGridPieceMarks.Second,
_ => null,
}
Marked = IsMarked(ent),
};
gridPiece.OnPiecePressed += OnPiecePressed;
gridPiece.OnPieceUnpressed += OnPieceUnpressed;
@ -467,6 +489,16 @@ public sealed class StorageWindow : BaseWindow
}
}
private ItemGridPieceMarks? IsMarked(EntityUid uid)
{
return _contained.IndexOf(uid) switch
{
0 => ItemGridPieceMarks.First,
1 => ItemGridPieceMarks.Second,
_ => null,
};
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
@ -486,8 +518,9 @@ public sealed class StorageWindow : BaseWindow
{
if (StorageEntity != null && _entity.System<StorageSystem>().NestedStorage)
{
// If parent container nests us then show back button
if (containerSystem.TryGetContainingContainer(StorageEntity.Value, out var container) &&
_entity.HasComponent<StorageComponent>(container.Owner))
_entity.TryGetComponent(container.Owner, out StorageComponent? storageComp) && storageComp.Container.Contains(StorageEntity.Value))
{
_backButton.Visible = true;
}

View file

@ -11,6 +11,7 @@ using Content.Shared.CCVar;
using Content.Shared.Input;
using Content.Shared.Interaction;
using Content.Shared.Storage;
using Robust.Client.GameObjects;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.UserInterface;
@ -37,6 +38,7 @@ public sealed class StorageUIController : UIController, IOnSystemChanged<Storage
[Dependency] private readonly IInputManager _input = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[UISystemDependency] private readonly StorageSystem _storage = default!;
[UISystemDependency] private readonly UserInterfaceSystem _ui = default!;
private readonly DragDropHelper<ItemGridPiece> _menuDragHelper;
@ -107,7 +109,7 @@ public sealed class StorageUIController : UIController, IOnSystemChanged<Storage
StaticStorageUIEnabled = obj;
}
public StorageWindow CreateStorageWindow(EntityUid uid)
public StorageWindow CreateStorageWindow(StorageBoundUserInterface sBui)
{
var window = new StorageWindow();
window.MouseFilter = Control.MouseFilterMode.Pass;
@ -127,9 +129,25 @@ public sealed class StorageUIController : UIController, IOnSystemChanged<Storage
}
else
{
window.OpenCenteredLeft();
// Open at parent position if it's open.
if (_ui.TryGetOpenUi<StorageBoundUserInterface>(EntityManager.GetComponent<TransformComponent>(sBui.Owner).ParentUid,
StorageComponent.StorageUiKey.Key, out var bui) && bui.Position != null)
{
window.Open(bui.Position.Value);
}
// Open at the saved position if it exists.
else if (_ui.TryGetPosition(sBui.Owner, StorageComponent.StorageUiKey.Key, out var pos))
{
window.Open(pos);
}
// Open at the default position.
else
{
window.OpenCenteredLeft();
}
}
_ui.RegisterControl(sBui, window);
return window;
}

View file

@ -40,6 +40,7 @@ public sealed partial class GunSystem : SharedGunSystem
[Dependency] private readonly InputSystem _inputSystem = default!;
[Dependency] private readonly SharedCameraRecoilSystem _recoil = default!;
[Dependency] private readonly SharedMapSystem _maps = default!;
[Dependency] private readonly SharedTransformSystem _xform = default!;
[ValidatePrototypeId<EntityPrototype>]
public const string HitscanProto = "HitscanEffect";
@ -117,6 +118,14 @@ public sealed partial class GunSystem : SharedGunSystem
private void OnHitscan(HitscanEvent ev)
{
// ALL I WANT IS AN ANIMATED EFFECT
// TODO EFFECTS
// This is very jank
// because the effect consists of three unrelatd entities, the hitscan beam can be split appart.
// E.g., if a grid rotates while part of the beam is parented to the grid, and part of it is parented to the map.
// Ideally, there should only be one entity, with one sprite that has multiple layers
// Or at the very least, have the other entities parented to the same entity to make sure they stick together.
const double tracerInterval = 0.01f;
foreach (var a in ev.Sprites)
@ -126,7 +135,7 @@ public sealed partial class GunSystem : SharedGunSystem
var startCoords = GetCoordinates(a.coordinates);
if (Deleted(startCoords.EntityId))
if (!TryComp(startCoords.EntityId, out TransformComponent? relativeXform))
continue;
if (a.effectType == EffectType.Tracer)
@ -142,7 +151,7 @@ public sealed partial class GunSystem : SharedGunSystem
Timer.Spawn(TimeSpan.FromSeconds(delay), () =>
{
CreateTracerEffect(stepCoords, a.angle, rsi);
CreateTracerEffect(stepCoords, a.angle, rsi, relativeXform);
});
stepIndex++;
@ -150,17 +159,21 @@ public sealed partial class GunSystem : SharedGunSystem
}
else if (a.effectType == EffectType.Static)
{
CreateStaticEffect(startCoords, a.angle, rsi, a.distance);
CreateStaticEffect(startCoords, a.angle, rsi, a.distance, relativeXform);
}
}
}
private EntityUid CreateTracerEffect(EntityCoordinates coords, Angle angle, SpriteSpecifier.Rsi rsi)
private EntityUid CreateTracerEffect(EntityCoordinates coords, Angle angle, SpriteSpecifier.Rsi rsi, TransformComponent relativeXform)
{
var ent = Spawn(HitscanTracerProto, coords);
var sprite = Comp<SpriteComponent>(ent);
var xform = Transform(ent);
xform.LocalRotation = angle;
var targetWorldRot = angle + _xform.GetWorldRotation(relativeXform);
var delta = targetWorldRot - _xform.GetWorldRotation(xform);
_xform.SetLocalRotationNoLerp(ent, xform.LocalRotation + delta, xform);
sprite[EffectLayers.Unshaded].AutoAnimated = false;
sprite.LayerSetSprite(EffectLayers.Unshaded, rsi);
sprite.LayerSetState(EffectLayers.Unshaded, rsi.RsiState);
@ -188,7 +201,7 @@ public sealed partial class GunSystem : SharedGunSystem
return ent;
}
private void CreateStaticEffect(EntityCoordinates coords, Angle angle, SpriteSpecifier.Rsi rsi, float distance)
private void CreateStaticEffect(EntityCoordinates coords, Angle angle, SpriteSpecifier.Rsi rsi, float distance, TransformComponent relativeXform)
{
var ent = Spawn(HitscanProto, coords);
var sprite = Comp<SpriteComponent>(ent);

View file

@ -26,11 +26,7 @@ public sealed partial class TestPair
instance.ProtoMan.LoadString(file, changed: changed);
}
await instance.WaitPost(() =>
{
instance.ProtoMan.ResolveResults();
instance.ProtoMan.ReloadPrototypes(changed);
});
await instance.WaitPost(() => instance.ProtoMan.ReloadPrototypes(changed));
foreach (var (kind, ids) in changed)
{
@ -65,4 +61,4 @@ public sealed partial class TestPair
{
return _loadedPrototypes.TryGetValue(kind, out var ids) && ids.Contains(id);
}
}
}

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

@ -0,0 +1,70 @@
using System.Collections.Generic;
using Content.Client.Weapons.Ranged.Components;
using Content.Shared.Prototypes;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests;
/// <summary>
/// Tests all entity prototypes with the MagazineVisualsComponent.
/// </summary>
[TestFixture]
public sealed class MagazineVisualsSpriteTest
{
[Test]
public async Task MagazineVisualsSpritesExist()
{
await using var pair = await PoolManager.GetServerClient();
var client = pair.Client;
var protoMan = client.ResolveDependency<IPrototypeManager>();
var componentFactory = client.ResolveDependency<IComponentFactory>();
await client.WaitAssertion(() =>
{
foreach (var proto in protoMan.EnumeratePrototypes<EntityPrototype>())
{
if (proto.Abstract || pair.IsTestPrototype(proto))
continue;
if (!proto.TryGetComponent<MagazineVisualsComponent>(out var visuals, componentFactory))
continue;
Assert.That(proto.TryGetComponent<SpriteComponent>(out var sprite, componentFactory),
@$"{proto.ID} has MagazineVisualsComponent but no SpriteComponent.");
Assert.That(proto.HasComponent<AppearanceComponent>(componentFactory),
@$"{proto.ID} has MagazineVisualsComponent but no AppearanceComponent.");
var toTest = new List<(int, string)>();
if (sprite.LayerMapTryGet(GunVisualLayers.Mag, out var magLayerId))
toTest.Add((magLayerId, ""));
if (sprite.LayerMapTryGet(GunVisualLayers.MagUnshaded, out var magUnshadedLayerId))
toTest.Add((magUnshadedLayerId, "-unshaded"));
Assert.That(toTest, Is.Not.Empty,
@$"{proto.ID} has MagazineVisualsComponent but no Mag or MagUnshaded layer map.");
var start = visuals.ZeroVisible ? 0 : 1;
foreach (var (id, midfix) in toTest)
{
Assert.That(sprite.TryGetLayer(id, out var layer));
var rsi = layer.ActualRsi;
for (var i = start; i < visuals.MagSteps; i++)
{
var state = $"{visuals.MagState}{midfix}-{i}";
Assert.That(rsi.TryGetState(state, out _),
@$"{proto.ID} has MagazineVisualsComponent with MagSteps = {visuals.MagSteps}, but {rsi.Path} doesn't have state {state}!");
}
// MagSteps includes the 0th step, so sometimes people are off by one.
var extraState = $"{visuals.MagState}{midfix}-{visuals.MagSteps}";
Assert.That(rsi.TryGetState(extraState, out _), Is.False,
@$"{proto.ID} has MagazineVisualsComponent with MagSteps = {visuals.MagSteps}, but more states exist!");
}
}
});
await pair.CleanReturnAsync();
}
}

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;
@ -39,8 +40,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
@ -56,7 +57,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;
@ -35,11 +37,21 @@ namespace Content.IntegrationTests.Tests
};
private static readonly string[] Grids =
{
"/Maps/centcomm.yml"
};
private static readonly string[] DoNotMapWhitelist =
{
"/Maps/centcomm.yml",
"/Maps/Shuttles/cargo.yml",
"/Maps/Shuttles/emergency.yml",
"/Maps/Shuttles/infiltrator.yml",
"/Maps/bagel.yml", // Contains mime's rubber stamp --> Either fix this, remove the category, or remove this comment if intentional.
"/Maps/gate.yml", // Contains positronic brain and LSE-1200c "Perforator"
"/Maps/meta.yml", // Contains warden's rubber stamp
"/Maps/reach.yml", // Contains handheld crew monitor
"/Maps/Shuttles/ShuttleEvent/cruiser.yml", // Contains LSE-1200c "Perforator"
"/Maps/Shuttles/ShuttleEvent/honki.yml", // Contains golden honker, clown's rubber stamp
"/Maps/Shuttles/ShuttleEvent/instigator.yml", // Contains EXP-320g "Friendship"
"/Maps/Shuttles/ShuttleEvent/syndie_evacpod.yml", // Contains syndicate rubber stamp
};
private static readonly string[] GameMaps =
@ -117,33 +129,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);
@ -157,12 +208,16 @@ namespace Content.IntegrationTests.Tests
var server = pair.Server;
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();
@ -185,13 +240,101 @@ namespace Content.IntegrationTests.Tests
var root = yamlStream.Documents[0].RootNode;
var meta = root["meta"];
var postMapInit = meta["postmapinit"].AsBool();
var version = meta["format"].AsInt();
// TODO MAP TESTS
// Move this to some separate test?
CheckDoNotMap(map, root, protoManager);
if (version >= 7)
{
v7Maps.Add(map);
continue;
}
var postMapInit = meta["postmapinit"].AsBool();
Assert.That(postMapInit, Is.False, $"Map {map.Filename} was saved postmapinit");
}
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();
}
/// <summary>
/// Check that maps do not have any entities that belong to the DoNotMap entity category
/// </summary>
private void CheckDoNotMap(ResPath map, YamlNode node, IPrototypeManager protoManager)
{
if (DoNotMapWhitelist.Contains(map.ToString()))
return;
var yamlEntities = node["entities"];
if (!protoManager.TryIndex<EntityCategoryPrototype>("DoNotMap", out var dnmCategory))
return;
Assert.Multiple(() =>
{
foreach (var yamlEntity in (YamlSequenceNode)yamlEntities)
{
var protoId = yamlEntity["proto"].AsString();
// This doesn't properly handle prototype migrations, but thats not a significant issue.
if (!protoManager.TryIndex(protoId, out var proto, false))
continue;
Assert.That(!proto.Categories.Contains(dnmCategory),
$"\nMap {map} contains entities in the DO NOT MAP category ({proto.Name})");
}
});
}
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(TotalMaps))] // Sunrise-Edit
public async Task GameMapsLoadableTest(string mapProto)
{
@ -208,16 +351,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)
{
@ -254,21 +397,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))
{
@ -306,7 +445,7 @@ namespace Content.IntegrationTests.Tests
try
{
mapManager.DeleteMap(mapId);
mapSystem.DeleteMap(mapId);
}
catch (Exception ex)
{
@ -370,11 +509,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();
@ -385,7 +522,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))
@ -396,32 +533,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

@ -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));

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

@ -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

@ -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

@ -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);

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

@ -19,11 +19,12 @@ public sealed partial class AntagSelectionSystem
/// Tries to get the next non-filled definition based on the current amount of selected minds and other factors.
/// </summary>
public bool TryGetNextAvailableDefinition(Entity<AntagSelectionComponent> ent,
[NotNullWhen(true)] out AntagSelectionDefinition? definition)
[NotNullWhen(true)] out AntagSelectionDefinition? definition,
int? players = null)
{
definition = null;
var totalTargetCount = GetTargetAntagCount(ent);
var totalTargetCount = GetTargetAntagCount(ent, players);
var mindCount = ent.Comp.SelectedMinds.Count;
if (mindCount >= totalTargetCount)
return false;

View file

@ -160,7 +160,10 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
DebugTools.AssertEqual(antag.SelectionTime, AntagSelectionTime.PostPlayerSpawn);
if (!TryGetNextAvailableDefinition((uid, antag), out var def))
// do not count players in the lobby for the antag ratio
var players = _playerManager.NetworkedSessions.Count(x => x.AttachedEntity != null);
if (!TryGetNextAvailableDefinition((uid, antag), out var def, players))
continue;
// Sunrise-Start

View file

@ -76,14 +76,13 @@ public sealed class GasAnalyzerSystem : EntitySystem
/// </summary>
private void OnUseInHand(Entity<GasAnalyzerComponent> entity, ref UseInHandEvent args)
{
// Not checking for Handled because ActivatableUISystem already marks it as such.
if (!entity.Comp.Enabled)
{
ActivateAnalyzer(entity, args.User);
}
else
{
DisableAnalyzer(entity, args.User);
}
args.Handled = true;
}

View file

@ -96,7 +96,7 @@ public sealed class BeamSystem : SharedBeamSystem
_physics.SetBodyType(ent, BodyType.Dynamic, manager: manager, body: physics);
_physics.SetCanCollide(ent, true, manager: manager, body: physics);
_broadphase.RegenerateContacts(ent, physics, manager);
_broadphase.RegenerateContacts((ent, physics, manager));
var distanceLength = distanceCorrection.Length();

View file

@ -216,17 +216,30 @@ public partial class ChatSystem
/// <returns></returns>
private bool AllowedToUseEmote(EntityUid source, EmotePrototype emote)
{
if ((_whitelistSystem.IsWhitelistFail(emote.Whitelist, source) || _whitelistSystem.IsBlacklistPass(emote.Blacklist, source)))
return false;
// If emote is in AllowedEmotes, it will bypass whitelist and blacklist
if (TryComp<SpeechComponent>(source, out var speech) &&
speech.AllowedEmotes.Contains(emote.ID))
{
return true;
}
if (!emote.Available &&
TryComp<SpeechComponent>(source, out var speech) &&
!speech.AllowedEmotes.Contains(emote.ID))
// Check the whitelist and blacklist
if (_whitelistSystem.IsWhitelistFail(emote.Whitelist, source) ||
_whitelistSystem.IsBlacklistPass(emote.Blacklist, source))
{
return false;
}
// Check if the emote is available for all
if (!emote.Available)
{
return false;
}
return true;
}
private void InvokeEmoteEvent(EntityUid uid, EmotePrototype proto)
{
var ev = new EmoteEvent(proto);

View file

@ -64,4 +64,9 @@ public sealed class ChemistryGuideDataSystem : SharedChemistryGuideDataSystem
var ev = new ReagentGuideRegistryChangedEvent(changeset);
RaiseNetworkEvent(ev);
}
public override void ReloadAllReagentPrototypes()
{
InitializeServerRegistry();
}
}

View file

@ -17,14 +17,14 @@ namespace Content.Server.Chemistry.TileReactions
[DataDefinition]
public sealed partial class SpillTileReaction : ITileReaction
{
[DataField("launchForwardsMultiplier")] private float _launchForwardsMultiplier = 1;
[DataField("requiredSlipSpeed")] private float _requiredSlipSpeed = 6;
[DataField("paralyzeTime")] private float _paralyzeTime = 1;
[DataField("launchForwardsMultiplier")] public float LaunchForwardsMultiplier = 1;
[DataField("requiredSlipSpeed")] public float RequiredSlipSpeed = 6;
[DataField("paralyzeTime")] public float ParalyzeTime = 1;
/// <summary>
/// <see cref="SlipperyComponent.SuperSlippery"/>
/// </summary>
[DataField("superSlippery")] private bool _superSlippery;
[DataField("superSlippery")] public bool SuperSlippery;
public FixedPoint2 TileReact(TileRef tile,
ReagentPrototype reagent,
@ -39,13 +39,13 @@ namespace Content.Server.Chemistry.TileReactions
.TrySpillAt(tile, new Solution(reagent.ID, reactVolume, data), out var puddleUid, false, false))
{
var slippery = entityManager.EnsureComponent<SlipperyComponent>(puddleUid);
slippery.LaunchForwardsMultiplier = _launchForwardsMultiplier;
slippery.ParalyzeTime = _paralyzeTime;
slippery.SuperSlippery = _superSlippery;
slippery.LaunchForwardsMultiplier = LaunchForwardsMultiplier;
slippery.ParalyzeTime = ParalyzeTime;
slippery.SuperSlippery = SuperSlippery;
entityManager.Dirty(puddleUid, slippery);
var step = entityManager.EnsureComponent<StepTriggerComponent>(puddleUid);
entityManager.EntitySysManager.GetEntitySystem<StepTriggerSystem>().SetRequiredTriggerSpeed(puddleUid, _requiredSlipSpeed, step);
entityManager.EntitySysManager.GetEntitySystem<StepTriggerSystem>().SetRequiredTriggerSpeed(puddleUid, RequiredSlipSpeed, step);
var slow = entityManager.EnsureComponent<SpeedModifierContactsComponent>(puddleUid);
var speedModifier = 1 - reagent.Viscosity;

View file

@ -4,6 +4,7 @@ using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Toolshed.Commands.Generic;
namespace Content.Server.Commands
{
@ -50,45 +51,5 @@ namespace Content.Server.Commands
attachedEntity = session.AttachedEntity.Value;
return true;
}
public static string SubstituteEntityDetails(IConsoleShell shell, EntityUid ent, string ruleString)
{
var entMan = IoCManager.Resolve<IEntityManager>();
var transform = entMan.GetComponent<TransformComponent>(ent);
var transformSystem = entMan.System<SharedTransformSystem>();
var worldPosition = transformSystem.GetWorldPosition(transform);
// gross, is there a better way to do this?
ruleString = ruleString.Replace("$ID", ent.ToString());
ruleString = ruleString.Replace("$WX",
worldPosition.X.ToString(CultureInfo.InvariantCulture));
ruleString = ruleString.Replace("$WY",
worldPosition.Y.ToString(CultureInfo.InvariantCulture));
ruleString = ruleString.Replace("$LX",
transform.LocalPosition.X.ToString(CultureInfo.InvariantCulture));
ruleString = ruleString.Replace("$LY",
transform.LocalPosition.Y.ToString(CultureInfo.InvariantCulture));
ruleString = ruleString.Replace("$NAME", entMan.GetComponent<MetaDataComponent>(ent).EntityName);
if (shell.Player is { } player)
{
if (player.AttachedEntity is {Valid: true} p)
{
var pTransform = entMan.GetComponent<TransformComponent>(p);
var pWorldPosition = transformSystem.GetWorldPosition(pTransform);
ruleString = ruleString.Replace("$PID", ent.ToString());
ruleString = ruleString.Replace("$PWX",
pWorldPosition.X.ToString(CultureInfo.InvariantCulture));
ruleString = ruleString.Replace("$PWY",
pWorldPosition.Y.ToString(CultureInfo.InvariantCulture));
ruleString = ruleString.Replace("$PLX",
pTransform.LocalPosition.X.ToString(CultureInfo.InvariantCulture));
ruleString = ruleString.Replace("$PLY",
pTransform.LocalPosition.Y.ToString(CultureInfo.InvariantCulture));
}
}
return ruleString;
}
}
}

View file

@ -37,7 +37,7 @@ namespace Content.Server.Damage.Systems
if (TerminatingOrDeleted(args.Target))
return;
var dmg = _damageable.TryChangeDamage(args.Target, component.Damage, component.IgnoreResistances, origin: args.Component.Thrower);
var dmg = _damageable.TryChangeDamage(args.Target, component.Damage * _damageable.UniversalThrownDamageModifier, component.IgnoreResistances, origin: args.Component.Thrower);
// Log damage only for mobs. Useful for when people throw spears at each other, but also avoids log-spam when explosions send glass shards flying.
if (dmg != null && HasComp<MobStateComponent>(args.Target))
@ -58,7 +58,7 @@ namespace Content.Server.Damage.Systems
private void OnDamageExamine(EntityUid uid, DamageOtherOnHitComponent component, ref DamageExamineEvent args)
{
_damageExamine.AddDamageExamine(args.Message, component.Damage, Loc.GetString("damage-throw"));
_damageExamine.AddDamageExamine(args.Message, _damageable.ApplyUniversalAllModifiers(component.Damage * _damageable.UniversalThrownDamageModifier), Loc.GetString("damage-throw"));
}
/// <summary>

View file

@ -20,7 +20,7 @@ public sealed class DeviceListSystem : SharedDeviceListSystem
SubscribeLocalEvent<DeviceListComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<DeviceListComponent, BeforeBroadcastAttemptEvent>(OnBeforeBroadcast);
SubscribeLocalEvent<DeviceListComponent, BeforePacketSentEvent>(OnBeforePacketSent);
SubscribeLocalEvent<BeforeSaveEvent>(OnMapSave);
SubscribeLocalEvent<BeforeSerializationEvent>(OnMapSave);
}
private void OnShutdown(EntityUid uid, DeviceListComponent component, ComponentShutdown args)
@ -124,14 +124,14 @@ public sealed class DeviceListSystem : SharedDeviceListSystem
Dirty(list);
}
private void OnMapSave(BeforeSaveEvent ev)
private void OnMapSave(BeforeSerializationEvent ev)
{
List<EntityUid> toRemove = new();
var query = GetEntityQuery<TransformComponent>();
var enumerator = AllEntityQuery<DeviceListComponent, TransformComponent>();
while (enumerator.MoveNext(out var uid, out var device, out var xform))
{
if (xform.MapUid != ev.Map)
if (!ev.MapIds.Contains(xform.MapID))
continue;
foreach (var ent in device.Devices)
@ -144,7 +144,10 @@ public sealed class DeviceListSystem : SharedDeviceListSystem
continue;
}
if (linkedXform.MapUid == ev.Map)
// This is assuming that **all** of the map is getting saved.
// Which is not necessarily true.
// AAAAAAAAAAAAAA
if (ev.MapIds.Contains(linkedXform.MapID))
continue;
toRemove.Add(ent);

View file

@ -67,15 +67,18 @@ public sealed class NetworkConfiguratorSystem : SharedNetworkConfiguratorSystem
SubscribeLocalEvent<DeviceListComponent, ComponentRemove>(OnComponentRemoved);
SubscribeLocalEvent<BeforeSaveEvent>(OnMapSave);
SubscribeLocalEvent<BeforeSerializationEvent>(OnMapSave);
}
private void OnMapSave(BeforeSaveEvent ev)
private void OnMapSave(BeforeSerializationEvent ev)
{
var enumerator = AllEntityQuery<NetworkConfiguratorComponent>();
while (enumerator.MoveNext(out var uid, out var conf))
{
if (CompOrNull<TransformComponent>(conf.ActiveDeviceList)?.MapUid != ev.Map)
if (!TryComp(conf.ActiveDeviceList, out TransformComponent? listXform))
continue;
if (!ev.MapIds.Contains(listXform.MapID))
continue;
// The linked device list is (probably) being saved. Make sure that the configurator is also being saved
@ -83,9 +86,10 @@ public sealed class NetworkConfiguratorSystem : SharedNetworkConfiguratorSystem
// containing a set of all entities that are about to be saved, which would make checking this much easier.
// This is a shitty bandaid, and will force close the UI during auto-saves.
// TODO Map serialization refactor
// I'm refactoring it now and I still dont know what to do
var xform = Transform(uid);
if (xform.MapUid == ev.Map && IsSaveable(uid))
if (ev.MapIds.Contains(xform.MapID) && IsSaveable(uid))
continue;
_uiSystem.CloseUi(uid, NetworkConfiguratorUiKey.Configure);

View file

@ -44,6 +44,26 @@ namespace Content.Server.EntityEffects.Effects
var damageSpec = new DamageSpecifier(Damage);
var universalReagentDamageModifier = entSys.GetEntitySystem<DamageableSystem>().UniversalReagentDamageModifier;
var universalReagentHealModifier = entSys.GetEntitySystem<DamageableSystem>().UniversalReagentHealModifier;
if (universalReagentDamageModifier != 1 || universalReagentHealModifier != 1)
{
foreach (var (type, val) in damageSpec.DamageDict)
{
if (val < 0f)
{
damageSpec.DamageDict[type] = val * universalReagentHealModifier;
}
if (val > 0f)
{
damageSpec.DamageDict[type] = val * universalReagentDamageModifier;
}
}
}
damageSpec = entSys.GetEntitySystem<DamageableSystem>().ApplyUniversalAllModifiers(damageSpec);
foreach (var group in prototype.EnumeratePrototypes<DamageGroupPrototype>())
{
if (!damageSpec.TryGetDamageInGroup(group, out var amount))
@ -114,17 +134,37 @@ namespace Content.Server.EntityEffects.Effects
public override void Effect(EntityEffectBaseArgs args)
{
var scale = FixedPoint2.New(1);
var damageSpec = new DamageSpecifier(Damage);
if (args is EntityEffectReagentArgs reagentArgs)
{
scale = ScaleByQuantity ? reagentArgs.Quantity * reagentArgs.Scale : reagentArgs.Scale;
}
args.EntityManager.System<DamageableSystem>().TryChangeDamage(
args.TargetEntity,
Damage * scale,
IgnoreResistances,
interruptsDoAfters: false);
var universalReagentDamageModifier = args.EntityManager.System<DamageableSystem>().UniversalReagentDamageModifier;
var universalReagentHealModifier = args.EntityManager.System<DamageableSystem>().UniversalReagentHealModifier;
if (universalReagentDamageModifier != 1 || universalReagentHealModifier != 1)
{
foreach (var (type, val) in damageSpec.DamageDict)
{
if (val < 0f)
{
damageSpec.DamageDict[type] = val * universalReagentHealModifier;
}
if (val > 0f)
{
damageSpec.DamageDict[type] = val * universalReagentDamageModifier;
}
}
}
args.EntityManager.System<DamageableSystem>()
.TryChangeDamage(
args.TargetEntity,
damageSpec * scale,
IgnoreResistances,
interruptsDoAfters: false);
}
}
}

View file

@ -181,6 +181,7 @@ namespace Content.Server.Entry
IoCManager.Resolve<IBanManager>().Initialize();
IoCManager.Resolve<IConnectionManager>().PostInit();
IoCManager.Resolve<MultiServerKickManager>().Initialize();
IoCManager.Resolve<CVarControlManager>().Initialize();
// Sunrise-Sponsors-Start
SunriseServerEntry.PostInit();
// Sunrise-Sponsors-End

View file

@ -464,7 +464,7 @@ public sealed partial class ExplosionSystem
}
// TODO EXPLOSIONS turn explosions into entities, and pass the the entity in as the damage origin.
_damageableSystem.TryChangeDamage(entity, damage, ignoreResistances: true);
_damageableSystem.TryChangeDamage(entity, damage * _damageableSystem.UniversalExplosionDamageModifier, ignoreResistances: true);
}
}

View file

@ -36,7 +36,7 @@ public sealed partial class TriggerSystem
// Re-check for contacts as we cleared them.
else if (TryComp<PhysicsComponent>(uid, out var body))
{
_broadphase.RegenerateContacts(uid, body);
_broadphase.RegenerateContacts((uid, body));
}
}

View file

@ -1,4 +1,6 @@
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.Chemistry.TileReactions;
using Content.Server.DoAfter;
using Content.Server.Fluids.Components;
using Content.Server.Spreader;
@ -391,23 +393,36 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
private void UpdateSlip(EntityUid entityUid, PuddleComponent component, Solution solution)
{
var isSlippery = false;
var isSuperSlippery = false;
// The base sprite is currently at 0.3 so we require at least 2nd tier to be slippery or else it's too hard to see.
var amountRequired = FixedPoint2.New(component.OverflowVolume.Float() * LowThreshold);
var slipperyAmount = FixedPoint2.Zero;
// Utilize the defaults from their relevant systems... this sucks, and is a bandaid
var launchForwardsMultiplier = SlipperyComponent.DefaultLaunchForwardsMultiplier;
var paralyzeTime = SlipperyComponent.DefaultParalyzeTime;
var requiredSlipSpeed = StepTriggerComponent.DefaultRequiredTriggeredSpeed;
foreach (var (reagent, quantity) in solution.Contents)
{
var reagentProto = _prototypeManager.Index<ReagentPrototype>(reagent.Prototype);
if (reagentProto.Slippery)
{
slipperyAmount += quantity;
if (!reagentProto.Slippery)
continue;
slipperyAmount += quantity;
if (slipperyAmount > amountRequired)
{
isSlippery = true;
break;
}
if (slipperyAmount <= amountRequired)
continue;
isSlippery = true;
foreach (var tileReaction in reagentProto.TileReactions)
{
if (tileReaction is not SpillTileReaction spillTileReaction)
continue;
isSuperSlippery = spillTileReaction.SuperSlippery;
launchForwardsMultiplier = launchForwardsMultiplier < spillTileReaction.LaunchForwardsMultiplier ? spillTileReaction.LaunchForwardsMultiplier : launchForwardsMultiplier;
requiredSlipSpeed = requiredSlipSpeed > spillTileReaction.RequiredSlipSpeed ? spillTileReaction.RequiredSlipSpeed : requiredSlipSpeed;
paralyzeTime = paralyzeTime < spillTileReaction.ParalyzeTime ? spillTileReaction.ParalyzeTime : paralyzeTime;
}
}
@ -417,6 +432,14 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
_stepTrigger.SetActive(entityUid, true, comp);
var friction = EnsureComp<TileFrictionModifierComponent>(entityUid);
_tile.SetModifier(entityUid, TileFrictionController.DefaultFriction * 0.5f, friction);
if (!TryComp<SlipperyComponent>(entityUid, out var slipperyComponent))
return;
slipperyComponent.SuperSlippery = isSuperSlippery;
_stepTrigger.SetRequiredTriggerSpeed(entityUid, requiredSlipSpeed);
slipperyComponent.LaunchForwardsMultiplier = launchForwardsMultiplier;
slipperyComponent.ParalyzeTime = paralyzeTime;
}
else if (TryComp<StepTriggerComponent>(entityUid, out var comp))
{

View file

@ -228,7 +228,7 @@ public sealed class SmokeSystem : EntitySystem
var xform = Transform(uid);
_physics.SetBodyType(uid, BodyType.Dynamic, fixtures, body, xform);
_physics.SetCanCollide(uid, true, manager: fixtures, body: body);
_broadphase.RegenerateContacts(uid, body, fixtures, xform);
_broadphase.RegenerateContacts((uid, body, fixtures, xform));
}
var timer = EnsureComp<TimedDespawnComponent>(uid);

View file

@ -1,4 +1,5 @@
using System.Linq;
using System.Numerics;
using Content.Server.Announcements;
using Content.Server.Discord;
using Content.Server.GameTicking.Events;
@ -14,10 +15,12 @@ using Content.Shared.Players;
using Content.Shared.Preferences;
using JetBrains.Annotations;
using Prometheus;
using Robust.Server.Maps;
using Robust.Shared.Asynchronous;
using Robust.Shared.Audio;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Random;
@ -96,9 +99,6 @@ namespace Content.Server.GameTicking
AddGamePresetRules();
DefaultMap = _mapManager.CreateMap();
_mapManager.AddUninitializedMap(DefaultMap);
var maps = new List<GameMapPrototype>();
// the map might have been force-set by something
@ -136,52 +136,202 @@ namespace Content.Server.GameTicking
// Let game rules dictate what maps we should load.
RaiseLocalEvent(new LoadingMapsEvent(maps));
foreach (var map in maps)
if (maps.Count == 0)
{
var toLoad = DefaultMap;
if (maps[0] != map)
{
// Create other maps for the others since we need to.
toLoad = _mapManager.CreateMap();
_mapManager.AddUninitializedMap(toLoad);
}
_map.CreateMap(out var mapId, runMapInit: false);
DefaultMap = mapId;
return;
}
LoadGameMap(map, toLoad, null);
for (var i = 0; i < maps.Count; i++)
{
LoadGameMap(maps[i], out var mapId);
DebugTools.Assert(!_map.IsInitialized(mapId));
if (i == 0)
DefaultMap = mapId;
}
}
public PreGameMapLoad RaisePreLoad(
GameMapPrototype proto,
DeserializationOptions? opts = null,
Vector2? offset = null,
Angle? rot = null)
{
offset ??= proto.MaxRandomOffset != 0f
? _robustRandom.NextVector2(proto.MaxRandomOffset)
: Vector2.Zero;
rot ??= proto.RandomRotation
? _robustRandom.NextAngle()
: Angle.Zero;
opts ??= DeserializationOptions.Default;
var ev = new PreGameMapLoad(proto, opts.Value, offset.Value, rot.Value);
RaiseLocalEvent(ev);
return ev;
}
/// <summary>
/// Loads a new map, allowing systems interested in it to handle loading events.
/// In the base game, this is required to be used if you want to load a station.
/// This does not initialze maps, unles specified via the <see cref="DeserializationOptions"/>.
/// </summary>
/// <param name="map">Game map prototype to load in.</param>
/// <param name="targetMapId">Map to load into.</param>
/// <param name="loadOptions">Map loading options, includes offset.</param>
/// <remarks>
/// This is basically a wrapper around a <see cref="MapLoaderSystem"/> method that auto generate
/// some <see cref="MapLoadOptions"/> using information in a prototype, and raise some events to allow content
/// to modify the options and react to the map creation.
/// </remarks>
/// <param name="proto">Game map prototype to load in.</param>
/// <param name="mapId">The id of the map that was loaded.</param>
/// <param name="options">Entity loading options, including whether the maps should be initialized.</param>
/// <param name="stationName">Name to assign to the loaded station.</param>
/// <returns>All loaded entities and grids.</returns>
public IReadOnlyList<EntityUid> LoadGameMap(GameMapPrototype map, MapId targetMapId, MapLoadOptions? loadOptions, string? stationName = null)
public IReadOnlyList<EntityUid> LoadGameMap(
GameMapPrototype proto,
out MapId mapId,
DeserializationOptions? options = null,
string? stationName = null,
Vector2? offset = null,
Angle? rot = null)
{
// Okay I specifically didn't set LoadMap here because this is typically called onto a new map.
// whereas the command can also be used on an existing map.
var loadOpts = loadOptions ?? new MapLoadOptions();
var ev = RaisePreLoad(proto, options, offset, rot);
if (map.MaxRandomOffset != 0f)
loadOpts.Offset = _robustRandom.NextVector2(map.MaxRandomOffset);
if (ev.GameMap.IsGrid)
{
var mapUid = _map.CreateMap(out mapId);
if (!_loader.TryLoadGrid(mapId,
ev.GameMap.MapPath,
out var grid,
ev.Options,
ev.Offset,
ev.Rotation))
{
throw new Exception($"Failed to load game-map grid {ev.GameMap.ID}");
}
if (map.RandomRotation)
loadOpts.Rotation = _robustRandom.NextAngle();
_metaData.SetEntityName(mapUid, proto.MapName);
var g = new List<EntityUid> {grid.Value.Owner};
RaiseLocalEvent(new PostGameMapLoad(proto, mapId, g, stationName));
return g;
}
var ev = new PreGameMapLoad(targetMapId, map, loadOpts);
RaiseLocalEvent(ev);
if (!_loader.TryLoadMap(ev.GameMap.MapPath,
out var map,
out var grids,
ev.Options,
ev.Offset,
ev.Rotation))
{
throw new Exception($"Failed to load game map {ev.GameMap.ID}");
}
var gridIds = _map.LoadMap(targetMapId, ev.GameMap.MapPath.ToString(), ev.Options);
mapId = map.Value.Comp.MapId;
_metaData.SetEntityName(map.Value.Owner, proto.MapName);
var gridUids = grids.Select(x => x.Owner).ToList();
RaiseLocalEvent(new PostGameMapLoad(proto, mapId, gridUids, stationName));
return gridUids;
}
_metaData.SetEntityName(_mapManager.GetMapEntityId(targetMapId), map.MapName);
/// <summary>
/// Variant of <see cref="LoadGameMap"/> that attempts to assign the provided <see cref="MapId"/> to the
/// loaded map.
/// </summary>
public IReadOnlyList<EntityUid> LoadGameMapWithId(
GameMapPrototype proto,
MapId mapId,
DeserializationOptions? opts = null,
string? stationName = null,
Vector2? offset = null,
Angle? rot = null)
{
var ev = RaisePreLoad(proto, opts, offset, rot);
var gridUids = gridIds.ToList();
RaiseLocalEvent(new PostGameMapLoad(map, targetMapId, gridUids, stationName));
if (ev.GameMap.IsGrid)
{
var mapUid = _map.CreateMap(mapId);
if (!_loader.TryLoadGrid(mapId,
ev.GameMap.MapPath,
out var grid,
ev.Options,
ev.Offset,
ev.Rotation))
{
throw new Exception($"Failed to load game-map grid {ev.GameMap.ID}");
}
_metaData.SetEntityName(mapUid, proto.MapName);
var g = new List<EntityUid> {grid.Value.Owner};
RaiseLocalEvent(new PostGameMapLoad(proto, mapId, g, stationName));
return g;
}
if (!_loader.TryLoadMapWithId(
mapId,
ev.GameMap.MapPath,
out var map,
out var grids,
ev.Options,
ev.Offset,
ev.Rotation))
{
throw new Exception($"Failed to load map");
}
_metaData.SetEntityName(map.Value.Owner, proto.MapName);
var gridUids = grids.Select(x => x.Owner).ToList();
RaiseLocalEvent(new PostGameMapLoad(proto, mapId, gridUids, stationName));
return gridUids;
}
/// <summary>
/// Variant of <see cref="LoadGameMap"/> that loads and then merges a game map onto an existing map.
/// </summary>
public IReadOnlyList<EntityUid> MergeGameMap(
GameMapPrototype proto,
MapId targetMap,
DeserializationOptions? opts = null,
string? stationName = null,
Vector2? offset = null,
Angle? rot = null)
{
// TODO MAP LOADING use a new event?
// This is quite different from the other methods, which will actually create a **new** map.
var ev = RaisePreLoad(proto, opts, offset, rot);
if (ev.GameMap.IsGrid)
{
if (!_loader.TryLoadGrid(targetMap,
ev.GameMap.MapPath,
out var grid,
ev.Options,
ev.Offset,
ev.Rotation))
{
throw new Exception($"Failed to load game-map grid {ev.GameMap.ID}");
}
var g = new List<EntityUid> {grid.Value.Owner};
// TODO MAP LOADING use a new event?
RaiseLocalEvent(new PostGameMapLoad(proto, targetMap, g, stationName));
return g;
}
if (!_loader.TryMergeMap(targetMap,
ev.GameMap.MapPath,
out var grids,
ev.Options,
ev.Offset,
ev.Rotation))
{
throw new Exception($"Failed to load map");
}
var gridUids = grids.Select(x => x.Owner).ToList();
// TODO MAP LOADING use a new event?
RaiseLocalEvent(new PostGameMapLoad(proto, targetMap, gridUids, stationName));
return gridUids;
}
@ -278,7 +428,7 @@ namespace Content.Server.GameTicking
}
// MapInitialize *before* spawning players, our codebase is too shit to do it afterwards...
_mapManager.DoMapInitialize(DefaultMap);
_map.InitializeMap(DefaultMap);
SpawnPlayers(readyPlayers, readyPlayerProfiles, force);
@ -736,21 +886,14 @@ namespace Content.Server.GameTicking
/// You likely want to subscribe to this after StationSystem.
/// </remarks>
[PublicAPI]
public sealed class PreGameMapLoad : EntityEventArgs
public sealed class PreGameMapLoad(GameMapPrototype gameMap, DeserializationOptions options, Vector2 offset, Angle rotation) : EntityEventArgs
{
public readonly MapId Map;
public GameMapPrototype GameMap;
public MapLoadOptions Options;
public PreGameMapLoad(MapId map, GameMapPrototype gameMap, MapLoadOptions options)
{
Map = map;
GameMap = gameMap;
Options = options;
}
public readonly GameMapPrototype GameMap = gameMap;
public DeserializationOptions Options = options;
public Vector2 Offset = offset;
public Angle Rotation = rotation;
}
/// <summary>
/// Event raised after the game loads a given map.
/// </summary>

View file

@ -390,6 +390,7 @@ namespace Content.Server.GameTicking
if (DummyTicker)
return;
var makeObserver = false;
Entity<MindComponent?>? mind = player.GetMind();
if (mind == null)
{
@ -397,10 +398,13 @@ namespace Content.Server.GameTicking
var (mindId, mindComp) = _mind.CreateMind(player.UserId, name);
mind = (mindId, mindComp);
_mind.SetUserId(mind.Value, player.UserId);
_roles.MindAddRole(mind.Value, "MindRoleObserver");
makeObserver = true;
}
var ghost = _ghost.SpawnGhost(mind.Value);
if (makeObserver)
_roles.MindAddRole(mind.Value, "MindRoleObserver");
_adminLogger.Add(LogType.LateJoin,
LogImpact.Low,
$"{player.Name} late joined the round as an Observer with {ToPrettyString(ghost):entity}.");

View file

@ -18,6 +18,7 @@ using Robust.Server.GameObjects;
using Robust.Server.GameStates;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Console;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
@ -48,7 +49,8 @@ namespace Content.Server.GameTicking
[Dependency] private readonly IServerPreferencesManager _prefsManager = default!;
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly ChatSystem _chatSystem = default!;
[Dependency] private readonly MapLoaderSystem _map = default!;
[Dependency] private readonly MapLoaderSystem _loader = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly GhostSystem _ghost = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly PlayTimeTrackingSystem _playTimeTrackings = default!;

View file

@ -20,11 +20,17 @@ public sealed partial class LoadMapRuleComponent : Component
public ProtoId<GameMapPrototype>? GameMap;
/// <summary>
/// A map path to load on a new map.
/// A map to load.
/// </summary>
[DataField]
public ResPath? MapPath;
/// <summary>
/// A grid to load on a new map.
/// </summary>
[DataField]
public ResPath? GridPath;
/// <summary>
/// A <see cref="PreloadedGridPrototype"/> to move to a new map.
/// If there are no instances left nothing is done.

View file

@ -1,10 +1,14 @@
using System.Linq;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.GridPreloader;
using Content.Server.StationEvents.Events;
using Content.Shared.GameTicking.Components;
using Robust.Server.GameObjects;
using Robust.Server.Maps;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Server.GameTicking.Rules;
@ -26,29 +30,50 @@ public sealed class LoadMapRuleSystem : StationEventSystem<LoadMapRuleComponent>
return;
}
// grid preloading needs map to init after moving it
var mapUid = _map.CreateMap(out var mapId, runMapInit: comp.PreloadedGrid == null);
Log.Info($"Created map {mapId} for {ToPrettyString(uid):rule}");
MapId mapId;
IReadOnlyList<EntityUid> grids;
if (comp.GameMap != null)
{
// Component has one of three modes, only one of the three fields should ever be populated.
DebugTools.AssertNull(comp.MapPath);
DebugTools.AssertNull(comp.GridPath);
DebugTools.AssertNull(comp.PreloadedGrid);
var gameMap = _prototypeManager.Index(comp.GameMap.Value);
grids = GameTicker.LoadGameMap(gameMap, mapId, new MapLoadOptions());
grids = GameTicker.LoadGameMap(gameMap, out mapId, null);
Log.Info($"Created map {mapId} for {ToPrettyString(uid):rule}");
}
else if (comp.MapPath is {} path)
{
var options = new MapLoadOptions { LoadMap = true };
if (!_mapLoader.TryLoad(mapId, path.ToString(), out var roots, options))
DebugTools.AssertNull(comp.GridPath);
DebugTools.AssertNull(comp.PreloadedGrid);
var opts = DeserializationOptions.Default with {InitializeMaps = true};
if (!_mapLoader.TryLoadMap(path, out var map, out var gridSet, opts))
{
Log.Error($"Failed to load map from {path}!");
Del(mapUid);
ForceEndSelf(uid, rule);
return;
}
grids = roots;
grids = gridSet.Select( x => x.Owner).ToList();
mapId = map.Value.Comp.MapId;
}
else if (comp.GridPath is { } gPath)
{
DebugTools.AssertNull(comp.PreloadedGrid);
// I fucking love it when "map paths" choses to ar
_map.CreateMap(out mapId);
var opts = DeserializationOptions.Default with {InitializeMaps = true};
if (!_mapLoader.TryLoadGrid(mapId, gPath, out var grid, opts))
{
Log.Error($"Failed to load grid from {gPath}!");
ForceEndSelf(uid, rule);
return;
}
grids = new List<EntityUid> {grid.Value.Owner};
}
else if (comp.PreloadedGrid is {} preloaded)
{
@ -56,11 +81,11 @@ public sealed class LoadMapRuleSystem : StationEventSystem<LoadMapRuleComponent>
if (!_gridPreloader.TryGetPreloadedGrid(preloaded, out var loadedShuttle))
{
Log.Error($"Failed to get a preloaded grid with {preloaded}!");
Del(mapUid);
ForceEndSelf(uid, rule);
return;
}
var mapUid = _map.CreateMap(out mapId, runMapInit: false);
_transform.SetParent(loadedShuttle.Value, mapUid);
grids = new List<EntityUid>() { loadedShuttle.Value };
_map.InitializeMap(mapUid);
@ -68,7 +93,6 @@ public sealed class LoadMapRuleSystem : StationEventSystem<LoadMapRuleComponent>
else
{
Log.Error($"No valid map prototype or map path associated with the rule {ToPrettyString(uid)}");
Del(mapUid);
ForceEndSelf(uid, rule);
return;
}

View file

@ -29,6 +29,7 @@ using Content.Shared.Movement.Events;
using Content.Shared.Movement.Systems;
using Content.Shared.Popups;
using Content.Shared.Storage.Components;
using Content.Shared.Tag;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Configuration;
@ -67,6 +68,7 @@ namespace Content.Server.Ghost
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly NewLifeSystem _newLifeSystem = default!;
[Dependency] private readonly EuiManager _euiManager = default!;
@ -435,8 +437,11 @@ namespace Content.Server.Ghost
public void MakeVisible(bool visible)
{
var entityQuery = EntityQueryEnumerator<GhostComponent, VisibilityComponent>();
while (entityQuery.MoveNext(out var uid, out _, out var vis))
while (entityQuery.MoveNext(out var uid, out var _, out var vis))
{
if (!_tag.HasTag(uid, "AllowGhostShownByEvent"))
continue;
if (visible)
{
_visibilitySystem.AddLayer((uid, vis), (int) VisibilityFlags.Normal, false);

View file

@ -3,7 +3,6 @@ using Content.Shared.CCVar;
using Content.Shared.GridPreloader.Prototypes;
using Content.Shared.GridPreloader.Systems;
using Robust.Server.GameObjects;
using Robust.Server.Maps;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
@ -13,6 +12,7 @@ using System.Numerics;
using Content.Server.GameTicking;
using Content.Shared.GameTicking;
using JetBrains.Annotations;
using Robust.Shared.EntitySerialization.Systems;
namespace Content.Server.GridPreloader;
public sealed class GridPreloaderSystem : SharedGridPreloaderSystem
@ -72,23 +72,13 @@ public sealed class GridPreloaderSystem : SharedGridPreloaderSystem
{
for (var i = 0; i < proto.Copies; i++)
{
var options = new MapLoadOptions
if (!_mapLoader.TryLoadGrid(mapId, proto.Path, out var grid))
{
LoadMap = false,
};
if (!_mapLoader.TryLoad(mapId, proto.Path.ToString(), out var roots, options))
Log.Error($"Failed to preload grid prototype {proto.ID}");
continue;
}
// only supports loading maps with one grid.
if (roots.Count != 1)
continue;
var gridUid = roots[0];
// gets grid + also confirms that the root we loaded is actually a grid
if (!TryComp<MapGridComponent>(gridUid, out var mapGrid))
continue;
var (gridUid, mapGrid) = grid.Value;
if (!TryComp<PhysicsComponent>(gridUid, out var physics))
continue;

View file

@ -6,7 +6,6 @@ using Content.Shared.Interaction.Events;
using Content.Shared.Item;
using Content.Shared.Whitelist;
using Robust.Server.Audio;
using Robust.Server.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes;
@ -26,6 +25,7 @@ public sealed class RandomGiftSystem : EntitySystem
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
private readonly List<string> _possibleGiftsSafe = new();
private readonly List<string> _possibleGiftsUnsafe = new();
@ -63,11 +63,16 @@ public sealed class RandomGiftSystem : EntitySystem
if (component.Wrapper is not null)
Spawn(component.Wrapper, coords);
args.Handled = true;
_audio.PlayPvs(component.Sound, args.User);
Del(uid);
// Don't delete the entity in the event bus, so we queue it for deletion.
// We need the free hand for the new item, so we send it to nullspace.
_transform.DetachEntity(uid, Transform(uid));
QueueDel(uid);
_hands.PickupOrDrop(args.User, handsEnt);
args.Handled = true;
}
private void OnGiftMapInit(EntityUid uid, RandomGiftComponent component, MapInitEvent args)

View file

@ -115,13 +115,13 @@ public sealed class HolopadSystem : SharedHolopadSystem
if (source != null)
{
// Close any AI request windows
if (_stationAiSystem.TryGetStationAiCore(args.Actor, out var stationAiCore) && stationAiCore != null)
if (_stationAiSystem.TryGetCore(args.Actor, out var stationAiCore))
_userInterfaceSystem.CloseUi(receiver.Owner, HolopadUiKey.AiRequestWindow, args.Actor);
// Try to warn the AI if the source of the call is out of its range
if (TryComp<TelephoneComponent>(stationAiCore, out var stationAiTelephone) &&
TryComp<TelephoneComponent>(source, out var sourceTelephone) &&
!_telephoneSystem.IsSourceInRangeOfReceiver((stationAiCore.Value.Owner, stationAiTelephone), (source.Value.Owner, sourceTelephone)))
!_telephoneSystem.IsSourceInRangeOfReceiver((stationAiCore.Owner, stationAiTelephone), (source.Value.Owner, sourceTelephone)))
{
_popupSystem.PopupEntity(Loc.GetString("holopad-ai-is-unable-to-reach-holopad"), receiver, args.Actor);
return;
@ -150,11 +150,11 @@ public sealed class HolopadSystem : SharedHolopadSystem
// If the user is an AI, end all calls originating from its
// associated core to ensure that any broadcasts will end
if (!TryComp<StationAiHeldComponent>(args.Actor, out var stationAiHeld) ||
!_stationAiSystem.TryGetStationAiCore((args.Actor, stationAiHeld), out var stationAiCore))
!_stationAiSystem.TryGetCore(args.Actor, out var stationAiCore))
return;
if (TryComp<TelephoneComponent>(stationAiCore, out var telephone))
_telephoneSystem.EndTelephoneCalls((stationAiCore.Value, telephone));
_telephoneSystem.EndTelephoneCalls((stationAiCore, telephone));
}
private void OnHolopadActivateProjector(Entity<HolopadComponent> entity, ref HolopadActivateProjectorMessage args)
@ -176,17 +176,17 @@ public sealed class HolopadSystem : SharedHolopadSystem
// Link the AI to the holopad they are broadcasting from
LinkHolopadToUser(source, args.Actor);
if (!_stationAiSystem.TryGetStationAiCore((args.Actor, stationAiHeld), out var stationAiCore) ||
stationAiCore.Value.Comp.RemoteEntity == null ||
if (!_stationAiSystem.TryGetCore(args.Actor, out var stationAiCore) ||
stationAiCore.Comp?.RemoteEntity == null ||
!TryComp<HolopadComponent>(stationAiCore, out var stationAiCoreHolopad))
return;
// Execute the broadcast, but have it originate from the AI core
ExecuteBroadcast((stationAiCore.Value, stationAiCoreHolopad), args.Actor);
ExecuteBroadcast((stationAiCore, stationAiCoreHolopad), args.Actor);
// Switch the AI's perspective from free roaming to the target holopad
_xformSystem.SetCoordinates(stationAiCore.Value.Comp.RemoteEntity.Value, Transform(source).Coordinates);
_stationAiSystem.SwitchRemoteEntityMode(stationAiCore.Value, false);
_xformSystem.SetCoordinates(stationAiCore.Comp.RemoteEntity.Value, Transform(source).Coordinates);
_stationAiSystem.SwitchRemoteEntityMode(stationAiCore, false);
return;
}
@ -220,10 +220,10 @@ public sealed class HolopadSystem : SharedHolopadSystem
reachableAiCores.Add((receiverUid, receiverTelephone));
if (!_stationAiSystem.TryGetInsertedAI((receiver, receiverStationAiCore), out var insertedAi))
if (!_stationAiSystem.TryGetHeld((receiver, receiverStationAiCore), out var insertedAi))
continue;
if (_userInterfaceSystem.TryOpenUi(receiverUid, HolopadUiKey.AiRequestWindow, insertedAi.Value.Owner))
if (_userInterfaceSystem.TryOpenUi(receiverUid, HolopadUiKey.AiRequestWindow, insertedAi))
LinkHolopadToUser(entity, args.Actor);
}
@ -274,8 +274,8 @@ public sealed class HolopadSystem : SharedHolopadSystem
return;
// Auto-close the AI request window
if (_stationAiSystem.TryGetInsertedAI((entity, stationAiCore), out var insertedAi))
_userInterfaceSystem.CloseUi(entity.Owner, HolopadUiKey.AiRequestWindow, insertedAi.Value.Owner);
if (_stationAiSystem.TryGetHeld((entity, stationAiCore), out var insertedAi))
_userInterfaceSystem.CloseUi(entity.Owner, HolopadUiKey.AiRequestWindow, insertedAi);
}
private void OnTelephoneMessageSent(Entity<HolopadComponent> holopad, ref TelephoneMessageSentEvent args)
@ -381,13 +381,13 @@ public sealed class HolopadSystem : SharedHolopadSystem
if (!TryComp<StationAiHeldComponent>(entity, out var entityStationAiHeld))
return;
if (!_stationAiSystem.TryGetStationAiCore((entity, entityStationAiHeld), out var stationAiCore))
if (!_stationAiSystem.TryGetCore(entity, out var stationAiCore))
return;
if (!TryComp<TelephoneComponent>(stationAiCore, out var stationAiCoreTelephone))
return;
_telephoneSystem.EndTelephoneCalls((stationAiCore.Value, stationAiCoreTelephone));
_telephoneSystem.EndTelephoneCalls((stationAiCore, stationAiCoreTelephone));
}
private void AddToggleProjectorVerb(Entity<HolopadComponent> entity, ref GetVerbsEvent<AlternativeVerb> args)
@ -407,8 +407,8 @@ public sealed class HolopadSystem : SharedHolopadSystem
if (!TryComp<StationAiHeldComponent>(user, out var userAiHeld))
return;
if (!_stationAiSystem.TryGetStationAiCore((user, userAiHeld), out var stationAiCore) ||
stationAiCore.Value.Comp.RemoteEntity == null)
if (!_stationAiSystem.TryGetCore(user, out var stationAiCore) ||
stationAiCore.Comp?.RemoteEntity == null)
return;
AlternativeVerb verb = new()
@ -595,17 +595,17 @@ public sealed class HolopadSystem : SharedHolopadSystem
{
// Check if the associated holopad user is an AI
if (TryComp<StationAiHeldComponent>(entity.Comp.User, out var stationAiHeld) &&
_stationAiSystem.TryGetStationAiCore((entity.Comp.User.Value, stationAiHeld), out var stationAiCore))
_stationAiSystem.TryGetCore(entity.Comp.User.Value, out var stationAiCore))
{
// Return the AI eye to free roaming
_stationAiSystem.SwitchRemoteEntityMode(stationAiCore.Value, true);
_stationAiSystem.SwitchRemoteEntityMode(stationAiCore, true);
// If the AI core is still broadcasting, end its calls
if (entity.Owner != stationAiCore.Value.Owner &&
if (entity.Owner != stationAiCore.Owner &&
TryComp<TelephoneComponent>(stationAiCore, out var stationAiCoreTelephone) &&
_telephoneSystem.IsTelephoneEngaged((stationAiCore.Value.Owner, stationAiCoreTelephone)))
_telephoneSystem.IsTelephoneEngaged((stationAiCore.Owner, stationAiCoreTelephone)))
{
_telephoneSystem.EndTelephoneCalls((stationAiCore.Value.Owner, stationAiCoreTelephone));
_telephoneSystem.EndTelephoneCalls((stationAiCore.Owner, stationAiCoreTelephone));
}
}
@ -625,8 +625,8 @@ public sealed class HolopadSystem : SharedHolopadSystem
if (!TryComp<StationAiHeldComponent>(user, out var userAiHeld))
return;
if (!_stationAiSystem.TryGetStationAiCore((user, userAiHeld), out var stationAiCore) ||
stationAiCore.Value.Comp.RemoteEntity == null)
if (!_stationAiSystem.TryGetCore(user, out var stationAiCore) ||
stationAiCore.Comp?.RemoteEntity == null)
return;
if (!TryComp<TelephoneComponent>(stationAiCore, out var stationAiTelephone))
@ -635,7 +635,7 @@ public sealed class HolopadSystem : SharedHolopadSystem
if (!TryComp<HolopadComponent>(stationAiCore, out var stationAiHolopad))
return;
var source = new Entity<TelephoneComponent>(stationAiCore.Value, stationAiTelephone);
var source = new Entity<TelephoneComponent>(stationAiCore, stationAiTelephone);
// Check if the AI is unable to activate the projector (unlikely this will ever pass; its just a safeguard)
if (!_telephoneSystem.IsSourceInRangeOfReceiver(source, receiver))
@ -658,11 +658,11 @@ public sealed class HolopadSystem : SharedHolopadSystem
if (!_telephoneSystem.IsSourceConnectedToReceiver(source, receiver))
return;
LinkHolopadToUser((stationAiCore.Value, stationAiHolopad), user);
LinkHolopadToUser((stationAiCore, stationAiHolopad), user);
// Switch the AI's perspective from free roaming to the target holopad
_xformSystem.SetCoordinates(stationAiCore.Value.Comp.RemoteEntity.Value, Transform(entity).Coordinates);
_stationAiSystem.SwitchRemoteEntityMode(stationAiCore.Value, false);
_xformSystem.SetCoordinates(stationAiCore.Comp.RemoteEntity.Value, Transform(entity).Coordinates);
_stationAiSystem.SwitchRemoteEntityMode(stationAiCore, false);
// Open the holopad UI if it hasn't been opened yet
if (TryComp<UserInterfaceComponent>(entity, out var entityUserInterfaceComponent))

View file

@ -80,6 +80,7 @@ namespace Content.Server.IoC
IoCManager.Register<IWatchlistWebhookManager, WatchlistWebhookManager>();
IoCManager.Register<ConnectionManager>();
IoCManager.Register<MultiServerKickManager>();
IoCManager.Register<CVarControlManager>();
// Sunrise-Start
IoCManager.Register<ServersHubManager>();

View file

@ -0,0 +1,11 @@
namespace Content.Server.Light.Components;
/// <summary>
/// Applies the roof flag to this tile and deletes the entity.
/// </summary>
[RegisterComponent]
public sealed partial class SetRoofComponent : Component
{
[DataField(required: true)]
public bool Value;
}

View file

@ -0,0 +1,22 @@
using Content.Shared;
using Content.Shared.Light.Components;
using Robust.Shared.Random;
namespace Content.Server.Light.EntitySystems;
/// <inheritdoc/>
public sealed class LightCycleSystem : SharedLightCycleSystem
{
[Dependency] private readonly IRobustRandom _random = default!;
protected override void OnCycleMapInit(Entity<LightCycleComponent> ent, ref MapInitEvent args)
{
base.OnCycleMapInit(ent, ref args);
if (ent.Comp.InitialOffset)
{
ent.Comp.Offset = _random.Next(ent.Comp.Duration);
Dirty(ent);
}
}
}

View file

@ -0,0 +1,33 @@
using Content.Server.Light.Components;
using Content.Shared.Light.EntitySystems;
using Robust.Shared.Map.Components;
namespace Content.Server.Light.EntitySystems;
/// <inheritdoc/>
public sealed class RoofSystem : SharedRoofSystem
{
[Dependency] private readonly SharedMapSystem _maps = default!;
private EntityQuery<MapGridComponent> _gridQuery;
public override void Initialize()
{
base.Initialize();
_gridQuery = GetEntityQuery<MapGridComponent>();
SubscribeLocalEvent<SetRoofComponent, ComponentStartup>(OnFlagStartup);
}
private void OnFlagStartup(Entity<SetRoofComponent> ent, ref ComponentStartup args)
{
var xform = Transform(ent.Owner);
if (_gridQuery.TryComp(xform.GridUid, out var grid))
{
var index = _maps.LocalToTile(xform.GridUid.Value, grid, xform.Coordinates);
SetRoof((xform.GridUid.Value, grid, null), index, ent.Comp.Value);
}
QueueDel(ent.Owner);
}
}

View file

@ -3,12 +3,14 @@ using Content.Server.Administration;
using Content.Server.GameTicking;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Robust.Server.GameObjects;
using Robust.Server.Maps;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.ContentPack;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Utility;
namespace Content.Server.Mapping
{
@ -34,6 +36,8 @@ namespace Content.Server.Mapping
var opts = CompletionHelper.UserFilePath(args[1], res.UserData)
.Concat(CompletionHelper.ContentFilePath(args[1], res));
return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-hint-mapping-path"));
case 3:
return CompletionResult.FromHintOptions(["false", "true"], Loc.GetString("cmd-mapping-hint-grid"));
}
return CompletionResult.Empty;
}
@ -46,7 +50,7 @@ namespace Content.Server.Mapping
return;
}
if (args.Length > 2)
if (args.Length > 3)
{
shell.WriteLine(Help);
return;
@ -56,12 +60,20 @@ namespace Content.Server.Mapping
shell.WriteLine(Loc.GetString("cmd-mapping-warning"));
#endif
// For backwards compatibility, isGrid is optional and we allow mappers to try load grids without explicitly
// specifying that they are loading a grid. Currently content is not allowed to override a map's MapId, so
// without engine changes this needs to be done by brute force by just trying to load it as a map first.
// This can result in errors being logged if the file is actually a grid, but the command should still work.
// yipeeee
bool? isGrid = args.Length < 3 ? null : bool.Parse(args[2]);
MapId mapId;
string? toLoad = null;
var mapSys = _entities.System<SharedMapSystem>();
Entity<MapGridComponent>? grid = null;
// Get the map ID to use
if (args.Length is 1 or 2)
if (args.Length > 0)
{
if (!int.TryParse(args[0], out var intMapId))
{
@ -78,7 +90,7 @@ namespace Content.Server.Mapping
return;
}
if (_map.MapExists(mapId))
if (mapSys.MapExists(mapId))
{
shell.WriteError(Loc.GetString("cmd-mapping-exists", ("mapId", mapId)));
return;
@ -91,12 +103,44 @@ namespace Content.Server.Mapping
}
else
{
var loadOptions = new MapLoadOptions {StoreMapUids = true};
_entities.System<MapLoaderSystem>().TryLoad(mapId, args[1], out _, loadOptions);
var path = new ResPath(args[1]);
toLoad = path.FilenameWithoutExtension;
var opts = new DeserializationOptions {StoreYamlUids = true};
var loader = _entities.System<MapLoaderSystem>();
if (isGrid == true)
{
mapSys.CreateMap(mapId, runMapInit: false);
if (!loader.TryLoadGrid(mapId, path, out grid, opts))
{
shell.WriteError(Loc.GetString("cmd-mapping-error"));
mapSys.DeleteMap(mapId);
return;
}
}
else if (!loader.TryLoadMapWithId(mapId, path, out _, out _, opts))
{
if (isGrid == false)
{
shell.WriteError(Loc.GetString("cmd-mapping-error"));
return;
}
// isGrid was not specified and loading it as a map failed, so we fall back to trying to load
// the file as a grid
shell.WriteLine(Loc.GetString("cmd-mapping-try-grid"));
mapSys.CreateMap(mapId, runMapInit: false);
if (!loader.TryLoadGrid(mapId, path, out grid, opts))
{
shell.WriteError(Loc.GetString("cmd-mapping-error"));
mapSys.DeleteMap(mapId);
return;
}
}
}
// was the map actually created or did it fail somehow?
if (!_map.MapExists(mapId))
if (!mapSys.MapExists(mapId))
{
shell.WriteError(Loc.GetString("cmd-mapping-error"));
return;
@ -115,19 +159,25 @@ namespace Content.Server.Mapping
}
// don't interrupt mapping with events or auto-shuttle
shell.ExecuteCommand("sudo cvar events.enabled false");
shell.ExecuteCommand("sudo cvar shuttle.auto_call_time 0");
shell.ExecuteCommand("changecvar events.enabled false");
shell.ExecuteCommand("changecvar shuttle.auto_call_time 0");
var auto = _entities.System<MappingSystem>();
if (grid != null)
auto.ToggleAutosave(grid.Value.Owner, toLoad ?? "NEWGRID");
else
auto.ToggleAutosave(mapId, toLoad ?? "NEWMAP");
if (_cfg.GetCVar(CCVars.AutosaveEnabled))
shell.ExecuteCommand($"toggleautosave {mapId} {toLoad ?? "NEWMAP"}");
shell.ExecuteCommand($"tp 0 0 {mapId}");
shell.RemoteExecuteCommand("mappingclientsidesetup");
_map.SetMapPaused(mapId, true);
DebugTools.Assert(mapSys.IsPaused(mapId));
if (args.Length == 2)
shell.WriteLine(Loc.GetString("cmd-mapping-success-load",("mapId",mapId),("path", args[1])));
else
if (args.Length != 2)
shell.WriteLine(Loc.GetString("cmd-mapping-success", ("mapId", mapId)));
else if (grid == null)
shell.WriteLine(Loc.GetString("cmd-mapping-success-load", ("mapId", mapId), ("path", args[1])));
else
shell.WriteLine(Loc.GetString("cmd-mapping-success-load-grid", ("mapId", mapId), ("path", args[1])));
}
}
}

View file

@ -4,6 +4,8 @@ using Content.Shared.Administration;
using Content.Shared.Mapping;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
@ -21,6 +23,7 @@ public sealed class MappingManager : IPostInjectInit
[Dependency] private readonly IServerNetManager _net = default!;
[Dependency] private readonly IPlayerManager _players = default!;
[Dependency] private readonly IEntitySystemManager _systems = default!;
[Dependency] private readonly IEntityManager _ent = default!;
private ISawmill _sawmill = default!;
private ZStdCompressionContext _zstd = default!;
@ -45,14 +48,14 @@ public sealed class MappingManager : IPostInjectInit
if (!_players.TryGetSessionByChannel(message.MsgChannel, out var session) ||
!_admin.IsAdmin(session, true) ||
!_admin.HasAdminFlag(session, AdminFlags.Host) ||
session.AttachedEntity is not { } player)
!_ent.TryGetComponent(session.AttachedEntity, out TransformComponent? xform) ||
xform.MapUid is not {} mapUid)
{
return;
}
var mapId = _systems.GetEntitySystem<TransformSystem>().GetMapCoordinates(player).MapId;
var mapEntity = _map.GetMapEntityIdOrThrow(mapId);
var data = _systems.GetEntitySystem<MapLoaderSystem>().GetSaveData(mapEntity);
var sys = _systems.GetEntitySystem<MapLoaderSystem>();
var data = sys.SerializeEntitiesRecursive([mapUid]).Node;
var document = new YamlDocument(data.ToYaml());
var stream = new YamlStream { document };
var writer = new StringWriter();

View file

@ -2,12 +2,12 @@ using System.IO;
using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Robust.Server.GameObjects;
using Robust.Server.Maps;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.ContentPack;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@ -21,16 +21,16 @@ public sealed class MappingSystem : EntitySystem
[Dependency] private readonly IConsoleHost _conHost = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly IResourceManager _resMan = default!;
[Dependency] private readonly MapLoaderSystem _map = default!;
[Dependency] private readonly MapLoaderSystem _loader = default!;
// Not a comp because I don't want to deal with this getting saved onto maps ever
/// <summary>
/// map id -> next autosave timespan & original filename.
/// </summary>
/// <returns></returns>
private Dictionary<MapId, (TimeSpan next, string fileName)> _currentlyAutosaving = new();
private Dictionary<EntityUid, (TimeSpan next, string fileName)> _currentlyAutosaving = new();
private bool _autosaveEnabled;
@ -60,25 +60,29 @@ public sealed class MappingSystem : EntitySystem
if (!_autosaveEnabled)
return;
foreach (var (map, (time, name))in _currentlyAutosaving.ToArray())
foreach (var (uid, (time, name))in _currentlyAutosaving)
{
if (_timing.RealTime <= time)
continue;
if (!_mapManager.MapExists(map) || _mapManager.IsMapInitialized(map))
if (LifeStage(uid) >= EntityLifeStage.MapInitialized)
{
Log.Warning($"Can't autosave map {map}; it doesn't exist, or is initialized. Removing from autosave.");
_currentlyAutosaving.Remove(map);
return;
Log.Warning($"Can't autosave entity {uid}; it doesn't exist, or is initialized. Removing from autosave.");
_currentlyAutosaving.Remove(uid);
continue;
}
_currentlyAutosaving[uid] = (CalculateNextTime(), name);
var saveDir = Path.Combine(_cfg.GetCVar(CCVars.AutosaveDirectory), name);
_resMan.UserData.CreateDir(new ResPath(saveDir).ToRootedPath());
var path = Path.Combine(saveDir, $"{DateTime.Now.ToString("yyyy-M-dd_HH.mm.ss")}-AUTO.yml");
_currentlyAutosaving[map] = (CalculateNextTime(), name);
Log.Info($"Autosaving map {name} ({map}) to {path}. Next save in {ReadableTimeLeft(map)} seconds.");
_map.SaveMap(map, path);
var path = new ResPath(Path.Combine(saveDir, $"{DateTime.Now:yyyy-M-dd_HH.mm.ss}-AUTO.yml"));
Log.Info($"Autosaving map {name} ({uid}) to {path}. Next save in {ReadableTimeLeft(uid)} seconds.");
if (HasComp<MapComponent>(uid))
_loader.TrySaveMap(uid, path);
else
_loader.TrySaveGrid(uid, path);
}
}
@ -87,34 +91,41 @@ public sealed class MappingSystem : EntitySystem
return _timing.RealTime + TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.AutosaveInterval));
}
private double ReadableTimeLeft(MapId map)
private double ReadableTimeLeft(EntityUid uid)
{
return Math.Round(_currentlyAutosaving[map].next.TotalSeconds - _timing.RealTime.TotalSeconds);
return Math.Round(_currentlyAutosaving[uid].next.TotalSeconds - _timing.RealTime.TotalSeconds);
}
#region Public API
public void ToggleAutosave(MapId map, string? path=null)
public void ToggleAutosave(MapId map, string? path = null)
{
if (_map.TryGetMap(map, out var uid))
ToggleAutosave(uid.Value, path);
}
public void ToggleAutosave(EntityUid uid, string? path=null)
{
if (!_autosaveEnabled)
return;
if (path != null && _currentlyAutosaving.TryAdd(map, (CalculateNextTime(), Path.GetFileName(path))))
{
if (!_mapManager.MapExists(map) || _mapManager.IsMapInitialized(map))
{
Log.Warning("Tried to enable autosaving on non-existant or already initialized map!");
_currentlyAutosaving.Remove(map);
return;
}
if (_currentlyAutosaving.Remove(uid) || path == null)
return;
Log.Info($"Started autosaving map {path} ({map}). Next save in {ReadableTimeLeft(map)} seconds.");
}
else
if (LifeStage(uid) >= EntityLifeStage.MapInitialized)
{
_currentlyAutosaving.Remove(map);
Log.Info($"Stopped autosaving on map {map}");
Log.Error("Tried to enable autosaving on a post map-init entity.");
return;
}
if (!HasComp<MapComponent>(uid) && !HasComp<MapGridComponent>(uid))
{
Log.Error($"{ToPrettyString(uid)} is neither a grid or map");
return;
}
_currentlyAutosaving[uid] = (CalculateNextTime(), Path.GetFileName(path));
Log.Info($"Started autosaving map {path} ({uid}). Next save in {ReadableTimeLeft(uid)} seconds.");
}
#endregion

View file

@ -25,6 +25,11 @@ public sealed partial class GameMapPrototype : IPrototype
[DataField]
public float MaxRandomOffset = 1000f;
/// <summary>
/// Turns out some of the map files are actually secretly grids. Excellent. I love map loading code.
/// </summary>
[DataField] public bool IsGrid;
[DataField]
public bool RandomRotation = true;

View file

@ -2,8 +2,8 @@
using System.IO;
using System.Linq;
using Robust.Server.GameObjects;
using Robust.Server.Maps;
using Robust.Shared.ContentPack;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.Map.Events;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Markdown;

View file

@ -1,11 +1,11 @@
using System.Linq;
using Content.Server.Administration;
using Content.Shared.Administration;
using Robust.Server.GameObjects;
using Robust.Server.Maps;
using Robust.Shared.Console;
using Robust.Shared.ContentPack;
using Robust.Shared.Map;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Components;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.Utility;
namespace Content.Server.Maps;
@ -17,8 +17,8 @@ namespace Content.Server.Maps;
public sealed class ResaveCommand : LocalizedCommands
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IResourceManager _res = default!;
[Dependency] private readonly ILogManager _log = default!;
public override string Command => "resave";
@ -26,32 +26,56 @@ public sealed class ResaveCommand : LocalizedCommands
{
var loader = _entManager.System<MapLoaderSystem>();
foreach (var fn in _res.ContentFindFiles(new ResPath("/Maps/")))
var opts = MapLoadOptions.Default with
{
var mapId = _mapManager.CreateMap();
_mapManager.AddUninitializedMap(mapId);
loader.Load(mapId, fn.ToString(), new MapLoadOptions()
DeserializationOptions = DeserializationOptions.Default with
{
StoreMapUids = true,
LoadMap = true,
});
StoreYamlUids = true,
LogOrphanedGrids = false
}
};
var log = _log.GetSawmill(Command);
var files = _res.ContentFindFiles(new ResPath("/Maps/")).ToList();
for (var i = 0; i < files.Count; i++)
{
var fn = files[i];
log.Info($"Re-saving file {i}/{files.Count} : {fn}");
if (!loader.TryLoadGeneric(fn, out var result, opts))
continue;
if (result.Maps.Count != 1)
{
shell.WriteError(
$"Multi-map or multi-grid files like {fn} are not yet supported by the {Command} command");
loader.Delete(result);
continue;
}
var map = result.Maps.First();
// Process deferred component removals.
_entManager.CullRemovedComponents();
var mapUid = _mapManager.GetMapEntityId(mapId);
var mapXform = _entManager.GetComponent<TransformComponent>(mapUid);
if (_entManager.HasComponent<LoadedMapComponent>(mapUid) || mapXform.ChildCount != 1)
if (_entManager.HasComponent<LoadedMapComponent>(map))
{
loader.SaveMap(mapId, fn.ToString());
loader.TrySaveMap(map.Comp.MapId, fn);
}
else if (mapXform.ChildEnumerator.MoveNext(out var child))
else if (result.Grids.Count == 1)
{
loader.Save(child, fn.ToString());
loader.TrySaveGrid(result.Grids.First(), fn);
}
else
{
shell.WriteError($"Failed to resave {fn}");
}
_mapManager.DeleteMap(mapId);
loader.Delete(result);
}
shell.WriteLine($"Resaved all maps");
}
}

View file

@ -83,7 +83,7 @@ public sealed class HealingSystem : EntitySystem
if (healing.ModifyBloodLevel != 0)
_bloodstreamSystem.TryModifyBloodLevel(entity.Owner, healing.ModifyBloodLevel);
var healed = _damageable.TryChangeDamage(entity.Owner, healing.Damage, true, origin: args.Args.User);
var healed = _damageable.TryChangeDamage(entity.Owner, healing.Damage * _damageable.UniversalTopicalsHealModifier, true, origin: args.Args.User);
if (healed == null && healing.BloodlossModifier != 0)
return;

View file

@ -27,6 +27,9 @@ public sealed class MousetrapSystem : EntitySystem
private void OnUseInHand(EntityUid uid, MousetrapComponent component, UseInHandEvent args)
{
if (args.Handled)
return;
component.IsActive = !component.IsActive;
_popupSystem.PopupEntity(component.IsActive
? Loc.GetString("mousetrap-on-activate")
@ -35,6 +38,8 @@ public sealed class MousetrapSystem : EntitySystem
args.User);
UpdateVisuals(uid);
args.Handled = true;
}
private void OnStepTriggerAttempt(EntityUid uid, MousetrapComponent component, ref StepTriggerAttemptEvent args)

View file

@ -37,6 +37,8 @@ public sealed class PAISystem : SharedPAISystem
private void OnUseInHand(EntityUid uid, PAIComponent component, UseInHandEvent args)
{
// Not checking for Handled because ToggleableGhostRoleSystem already marks it as such.
if (!TryComp<MindContainerComponent>(uid, out var mind) || !mind.HasMind)
component.LastUser = args.User;
}

View file

@ -13,6 +13,7 @@ using Content.Shared.Atmos;
using Content.Shared.Decals;
using Content.Shared.Ghost;
using Content.Shared.Gravity;
using Content.Shared.Light.Components;
using Content.Shared.Parallax.Biomes;
using Content.Shared.Parallax.Biomes.Layers;
using Content.Shared.Parallax.Biomes.Markers;
@ -331,6 +332,9 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
while (biomes.MoveNext(out var biome))
{
if (biome.LifeStage < ComponentLifeStage.Running)
continue;
_activeChunks.Add(biome, _tilePool.Get());
_markerChunks.GetOrNew(biome);
}
@ -380,6 +384,10 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
while (loadBiomes.MoveNext(out var gridUid, out var biome, out var grid))
{
// If not MapInit don't run it.
if (biome.LifeStage < ComponentLifeStage.Running)
continue;
if (!biome.Enabled)
continue;
@ -745,7 +753,10 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
}
if (modified.Count == 0)
{
component.ModifiedTiles.Remove(chunk);
_tilePool.Return(modified);
}
component.PendingMarkers.Remove(chunk);
}
@ -1014,11 +1025,14 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
// Midday: #E6CB8B
// Moonlight: #2b3143
// Lava: #A34931
var light = EnsureComp<MapLightComponent>(mapUid);
light.AmbientLightColor = mapLight ?? Color.FromHex("#D8B059");
Dirty(mapUid, light, metadata);
EnsureComp<RoofComponent>(mapUid);
EnsureComp<LightCycleComponent>(mapUid);
var moles = new float[Atmospherics.AdjustedNumberOfGases];
moles[(int) Gas.Oxygen] = 21.824779f;
moles[(int) Gas.Nitrogen] = 82.10312f;

View file

@ -109,7 +109,7 @@ public sealed class ConveyorController : SharedConveyorController
component.State = state;
if (TryComp<PhysicsComponent>(uid, out var physics))
_broadphase.RegenerateContacts(uid, physics);
_broadphase.RegenerateContacts((uid, physics));
UpdateAppearance(uid, component);
Dirty(uid, component);

View file

@ -11,7 +11,7 @@ namespace Content.Server.Popups
{
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly TransformSystem _xform = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
public override void PopupCursor(string? message, PopupType type = PopupType.Small)
{
@ -47,8 +47,7 @@ namespace Content.Server.Popups
{
if (message == null)
return;
var mapPos = coordinates.ToMap(EntityManager, _xform);
var mapPos = _transform.ToMapCoordinates(coordinates);
var filter = Filter.Empty().AddPlayersByPvs(mapPos, entManager: EntityManager, playerMan: _player, cfgMan: _cfg);
RaiseNetworkEvent(new PopupCoordinatesEvent(message, type, GetNetCoordinates(coordinates)), filter);
}
@ -70,6 +69,21 @@ namespace Content.Server.Popups
RaiseNetworkEvent(new PopupCoordinatesEvent(message, type, GetNetCoordinates(coordinates)), actor.PlayerSession);
}
public override void PopupPredictedCoordinates(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small)
{
if (message == null)
return;
var mapPos = _transform.ToMapCoordinates(coordinates);
var filter = Filter.Empty().AddPlayersByPvs(mapPos, entManager: EntityManager, playerMan: _player, cfgMan: _cfg);
if (recipient != null)
{
// Don't send to recipient, since they predicted it locally
filter = filter.RemovePlayerByAttachedEntity(recipient.Value);
}
RaiseNetworkEvent(new PopupCoordinatesEvent(message, type, GetNetCoordinates(coordinates)), filter);
}
public override void PopupEntity(string? message, EntityUid uid, PopupType type = PopupType.Small)
{
if (message == null)

View file

@ -15,10 +15,13 @@ using Robust.Server.GameObjects;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Procedural;
@ -173,14 +176,18 @@ public sealed partial class DungeonSystem : SharedDungeonSystem
return Transform(uid).MapID;
}
var mapId = _mapManager.CreateMap();
_mapManager.AddUninitializedMap(mapId);
_loader.Load(mapId, proto.AtlasPath.ToString());
var mapUid = _mapManager.GetMapEntityId(mapId);
_mapManager.SetMapPaused(mapId, true);
comp = AddComp<DungeonAtlasTemplateComponent>(mapUid);
var opts = new MapLoadOptions
{
DeserializationOptions = DeserializationOptions.Default with {PauseMaps = true},
ExpectedCategory = FileCategory.Map
};
if (!_loader.TryLoadGeneric(proto.AtlasPath, out var res, opts) || !res.Maps.TryFirstOrNull(out var map))
throw new Exception($"Failed to load dungeon template.");
comp = AddComp<DungeonAtlasTemplateComponent>(map.Value.Owner);
comp.Path = proto.AtlasPath;
return mapId;
return map.Value.Comp.MapId;
}
/// <summary>

View file

@ -44,7 +44,7 @@ public sealed class ProjectileSystem : SharedProjectileSystem
return;
}
var ev = new ProjectileHitEvent(component.Damage, target, component.Shooter);
var ev = new ProjectileHitEvent(component.Damage * _damageableSystem.UniversalProjectileDamageModifier, target, component.Shooter);
RaiseLocalEvent(uid, ref ev);
var otherName = ToPrettyString(target);

View file

@ -6,7 +6,6 @@ using Content.Shared.Mobs.Components;
using Content.Shared.Procedural;
using Content.Shared.Radio;
using Content.Shared.Salvage.Magnet;
using Robust.Server.Maps;
using Robust.Shared.Exceptions;
using Robust.Shared.Map;
@ -291,15 +290,10 @@ public sealed partial class SalvageSystem
case SalvageOffering wreck:
var salvageProto = wreck.SalvageMap;
var opts = new MapLoadOptions
{
Offset = new Vector2(0, 0)
};
if (!_map.TryLoad(salvMapXform.MapID, salvageProto.MapPath.ToString(), out _, opts))
if (!_loader.TryLoadGrid(salvMapXform.MapID, salvageProto.MapPath, out _))
{
Report(magnet, MagnetChannel, "salvage-system-announcement-spawn-debris-disintegrated");
_mapManager.DeleteMap(salvMapXform.MapID);
_mapSystem.DeleteMap(salvMapXform.MapID);
return;
}

View file

@ -1,38 +1,23 @@
using System.Linq;
using System.Numerics;
using Content.Server.Cargo.Systems;
using Content.Server.Construction;
using Content.Server.GameTicking;
using Content.Server.Radio.EntitySystems;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.Radio;
using Content.Shared.Salvage;
using Robust.Server.GameObjects;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
using Content.Server.Chat.Managers;
using Content.Server.Gravity;
using Content.Server.Parallax;
using Content.Server.Procedural;
using Content.Server.Shuttles.Systems;
using Content.Server.Station.Systems;
using Content.Shared.CCVar;
using Content.Shared.Construction.EntitySystems;
using Content.Shared.Random;
using Content.Shared.Random.Helpers;
using Content.Shared.Tools.Components;
using Robust.Server.Maps;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Map.Components;
using Robust.Shared.Timing;
using Content.Server.Labels;
using Robust.Shared.EntitySerialization.Systems;
namespace Content.Server.Salvage
{
@ -50,7 +35,7 @@ namespace Content.Server.Salvage
[Dependency] private readonly DungeonSystem _dungeon = default!;
[Dependency] private readonly GravitySystem _gravity = default!;
[Dependency] private readonly LabelSystem _labelSystem = default!;
[Dependency] private readonly MapLoaderSystem _map = default!;
[Dependency] private readonly MapLoaderSystem _loader = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly RadioSystem _radioSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;

View file

@ -30,11 +30,13 @@ using Robust.Server.GameObjects;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using TimedDespawnComponent = Robust.Shared.Spawners.TimedDespawnComponent;
namespace Content.Server.Shuttles.Systems;
@ -338,7 +340,7 @@ public sealed class ArrivalsSystem : EntitySystem
return;
// We use arrivals as the default spawn so don't check for job prio.
// Sunrise-Start
if (ev.DesiredSpawnPointType == SpawnPointType.Job)
return;
@ -510,55 +512,53 @@ public sealed class ArrivalsSystem : EntitySystem
}
}
// Sunrise-Edit
// private void OnRoundStarting(RoundStartingEvent ev)
// {
// // Setup arrivals station
// if (!Enabled)
// return;
//
// SetupArrivalsStation();
// }
//
// private void SetupArrivalsStation()
// {
// var mapUid = _mapSystem.CreateMap(out var mapId, false);
// _metaData.SetEntityName(mapUid, Loc.GetString("map-name-terminal"));
//
// if (!_loader.TryLoad(mapId, _cfgManager.GetCVar(CCVars.ArrivalsMap), out var uids))
// {
// return;
// }
//
// foreach (var id in uids)
// {
// EnsureComp<ArrivalsSourceComponent>(id);
// EnsureComp<ProtectedGridComponent>(id);
// EnsureComp<PreventPilotComponent>(id);
// }
//
// // Setup planet arrivals if relevant
// if (_cfgManager.GetCVar(CCVars.ArrivalsPlanet))
// {
// var template = _random.Pick(_arrivalsBiomeOptions);
// _biomes.EnsurePlanet(mapUid, _protoManager.Index(template));
// var restricted = new RestrictedRangeComponent
// {
// Range = 32f
// };
// AddComp(mapUid, restricted);
// }
//
// _mapSystem.InitializeMap(mapId);
//
// // Handle roundstart stations.
// var query = AllEntityQuery<StationArrivalsComponent>();
//
// while (query.MoveNext(out var uid, out var comp))
// {
// SetupShuttle(uid, comp);
// }
// }
/// Sunrise-Edit
private void OnRoundStarting(RoundStartingEvent ev)
{
// Setup arrivals station
if (!Enabled)
return;
SetupArrivalsStation();
}
private void SetupArrivalsStation()
{
var path = new ResPath(_cfgManager.GetCVar(CCVars.ArrivalsMap));
if (!_loader.TryLoadMap(path, out var map, out var grids))
return;
_metaData.SetEntityName(map.Value, Loc.GetString("map-name-terminal"));
foreach (var id in grids)
{
EnsureComp<ArrivalsSourceComponent>(id);
EnsureComp<ProtectedGridComponent>(id);
EnsureComp<PreventPilotComponent>(id);
}
// Setup planet arrivals if relevant
if (_cfgManager.GetCVar(CCVars.ArrivalsPlanet))
{
var template = _random.Pick(_arrivalsBiomeOptions);
_biomes.EnsurePlanet(map.Value, _protoManager.Index(template));
var restricted = new RestrictedRangeComponent
{
Range = 32f
};
AddComp(map.Value, restricted);
}
_mapSystem.InitializeMap(map.Value.Comp.MapId);
// Handle roundstart stations.
var query = AllEntityQuery<StationArrivalsComponent>();
while (query.MoveNext(out var uid, out var comp))
{
SetupShuttle(uid, comp);
}
}
private void SetArrivals(bool obj)
{
@ -611,9 +611,9 @@ public sealed class ArrivalsSystem : EntitySystem
var dummpMapEntity = _mapSystem.CreateMap(out var dummyMapId);
if (TryGetArrivals(out var arrivals) &&
_loader.TryLoad(dummyMapId, component.ShuttlePath.ToString(), out var shuttleUids))
_loader.TryLoadGrid(dummyMapId, component.ShuttlePath, out var shuttle))
{
component.Shuttle = shuttleUids[0];
component.Shuttle = shuttle.Value;
var shuttleComp = Comp<ShuttleComponent>(component.Shuttle);
var arrivalsComp = EnsureComp<ArrivalsShuttleComponent>(component.Shuttle);
arrivalsComp.Station = uid;

View file

@ -330,7 +330,9 @@ public sealed partial class DockingSystem
// If it's a map check no hard collidable anchored entities overlap
if (isMap)
{
foreach (var tile in _mapSystem.GetLocalTilesIntersecting(gridEntity.Owner, gridEntity.Comp, aabb))
var localTiles = _mapSystem.GetLocalTilesEnumerator(gridEntity.Owner, gridEntity.Comp, aabb);
while (localTiles.MoveNext(out var tile))
{
var anchoredEnumerator = _mapSystem.GetAnchoredEntitiesEnumerator(gridEntity.Owner, gridEntity.Comp, tile.GridIndices);

View file

@ -34,10 +34,9 @@ using Content.Shared.Shuttles.Events;
using Content.Shared.Tag;
using Content.Shared.Tiles;
using Robust.Server.GameObjects;
using Robust.Server.Maps;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.Map.Components;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
@ -79,6 +78,8 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem
[Dependency] private readonly DockingSystem _dock = default!;
[Dependency] private readonly IdCardSystem _idSystem = default!;
[Dependency] private readonly NavMapSystem _navMap = default!;
[Dependency] private readonly MapLoaderSystem _loader = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly RoundEndSystem _roundEnd = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
@ -575,7 +576,7 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem
// Sunrise-start
var mapUid = _mapSystem.CreateMap(out var mapId, runMapInit: false);
if (!_loader.TryLoad(mapId, component.Map.ToString(), out var uids) || uids.Count != 1)
if (!_loader.TryLoadGrid(mapId, component.Map.ToString(), out var uids) || uids.Count != 1)
{
Log.Error($"Failed to set up transit hub map!");
QueueDel(mapUid);
@ -678,7 +679,7 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem
var mapId = _mapManager.CreateMap();
var mapOptions = new MapLoadOptions { LoadMap = false,};
if (!_loader.TryLoad(mapId, shuttlePath.ToString(), out var uids, mapOptions) || uids.Count != 1)
if (!_loader.TryLoadGrid(mapId, shuttlePath.ToString(), out var uids, mapOptions) || uids.Count != 1)
{
Log.Error($"Unable to spawn emergency shuttle {shuttlePath} for {ToPrettyString(ent)}");
return;

View file

@ -72,17 +72,15 @@ public sealed partial class ShuttleSystem
_mapSystem.CreateMap(out var mapId);
if (_loader.TryLoad(mapId, component.Path.ToString(), out var ent) && ent.Count > 0)
if (_loader.TryLoadGrid(mapId, component.Path, out var ent))
{
if (HasComp<ShuttleComponent>(ent[0]))
{
TryFTLProximity(ent[0], targetGrid.Value);
}
if (HasComp<ShuttleComponent>(ent))
TryFTLProximity(ent.Value, targetGrid.Value);
_station.AddGridToStation(uid, ent[0]);
_station.AddGridToStation(uid, ent.Value);
}
_mapManager.DeleteMap(mapId);
_mapSystem.DeleteMap(mapId);
}
private bool TryDungeonSpawn(Entity<MapGridComponent?> targetGrid, DungeonSpawnGroup group, out EntityUid spawned)
@ -143,20 +141,18 @@ public sealed partial class ShuttleSystem
var path = paths[^1];
paths.RemoveAt(paths.Count - 1);
if (_loader.TryLoad(mapId, path.ToString(), out var ent) && ent.Count == 1)
if (_loader.TryLoadGrid(mapId, path, out var grid))
{
if (HasComp<ShuttleComponent>(ent[0]))
{
TryFTLProximity(ent[0], targetGrid);
}
if (HasComp<ShuttleComponent>(grid))
TryFTLProximity(grid.Value, targetGrid);
if (group.NameGrid)
{
var name = path.FilenameWithoutExtension;
_metadata.SetEntityName(ent[0], name);
_metadata.SetEntityName(grid.Value, name);
}
spawned = ent[0];
spawned = grid.Value;
return true;
}
@ -227,7 +223,7 @@ public sealed partial class ShuttleSystem
}
}
_mapManager.DeleteMap(mapId);
_mapSystem.DeleteMap(mapId);
}
private void OnGridFillMapInit(EntityUid uid, GridFillComponent component, MapInitEvent args)
@ -246,23 +242,22 @@ public sealed partial class ShuttleSystem
_mapSystem.CreateMap(out var mapId);
var valid = false;
if (_loader.TryLoad(mapId, component.Path.ToString(), out var ent) &&
ent.Count == 1 &&
TryComp(ent[0], out TransformComponent? shuttleXform))
if (_loader.TryLoadGrid(mapId, component.Path, out var grid))
{
var escape = GetSingleDock(ent[0]);
var escape = GetSingleDock(grid.Value);
if (escape != null)
{
var config = _dockSystem.GetDockingConfig(ent[0], xform.GridUid.Value, escape.Value.Entity, escape.Value.Component, uid, dock);
var config = _dockSystem.GetDockingConfig(grid.Value, xform.GridUid.Value, escape.Value.Entity, escape.Value.Component, uid, dock);
if (config != null)
{
FTLDock((ent[0], shuttleXform), config);
var shuttleXform = Transform(grid.Value);
FTLDock((grid.Value, shuttleXform), config);
if (TryComp<StationMemberComponent>(xform.GridUid, out var stationMember))
{
_station.AddGridToStation(stationMember.Station, ent[0]);
_station.AddGridToStation(stationMember.Station, grid.Value);
}
valid = true;
@ -273,11 +268,11 @@ public sealed partial class ShuttleSystem
{
var compType = compReg.Component.GetType();
if (HasComp(ent[0], compType))
if (HasComp(grid.Value, compType))
continue;
var comp = _factory.GetComponent(compType);
AddComp(ent[0], comp, true);
AddComp(grid.Value, comp, true);
}
}
@ -286,7 +281,7 @@ public sealed partial class ShuttleSystem
Log.Error($"Error loading gridfill dock for {ToPrettyString(uid)} / {component.Path}");
}
_mapManager.DeleteMap(mapId);
_mapSystem.DeleteMap(mapId);
}
private (EntityUid Entity, DockingComponent Component)? GetSingleDock(EntityUid uid)

View file

@ -17,6 +17,7 @@ using Robust.Server.GameStates;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;

View file

@ -40,12 +40,12 @@ public sealed class StationAiSystem : SharedStationAiSystem
var query = EntityManager.EntityQueryEnumerator<StationAiCoreComponent, TransformComponent>();
while (query.MoveNext(out var ent, out var entStationAiCore, out var entXform))
{
var stationAiCore = new Entity<StationAiCoreComponent>(ent, entStationAiCore);
var stationAiCore = new Entity<StationAiCoreComponent?>(ent, entStationAiCore);
if (!TryGetInsertedAI(stationAiCore, out var insertedAi) || !TryComp(insertedAi, out ActorComponent? actor))
if (!TryGetHeld(stationAiCore, out var insertedAi) || !TryComp(insertedAi, out ActorComponent? actor))
continue;
if (stationAiCore.Comp.RemoteEntity == null || stationAiCore.Comp.Remote)
if (stationAiCore.Comp?.RemoteEntity == null || stationAiCore.Comp.Remote)
continue;
var xform = Transform(stationAiCore.Comp.RemoteEntity.Value);

View file

@ -4,10 +4,8 @@ using Content.Server.Storage.Components;
using Content.Shared.Database;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction.Events;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Random;
using static Content.Shared.Storage.EntitySpawnCollection;
@ -20,6 +18,7 @@ namespace Content.Server.Storage.EntitySystems
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly PricingSystem _pricing = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
public override void Initialize()
{
@ -80,27 +79,27 @@ namespace Content.Server.Storage.EntitySystems
_adminLogger.Add(LogType.EntitySpawn, LogImpact.Low, $"{ToPrettyString(args.User)} used {ToPrettyString(uid)} which spawned {ToPrettyString(entityToPlaceInHands.Value)}");
}
// The entity is often deleted, so play the sound at its position rather than parenting
if (component.Sound != null)
{
// The entity is often deleted, so play the sound at its position rather than parenting
var coordinates = Transform(uid).Coordinates;
_audio.PlayPvs(component.Sound, coordinates);
}
_audio.PlayPvs(component.Sound, coords);
component.Uses--;
// Delete entity only if component was successfully used
if (component.Uses <= 0)
{
args.Handled = true;
EntityManager.DeleteEntity(uid);
// Don't delete the entity in the event bus, so we queue it for deletion.
// We need the free hand for the new item, so we send it to nullspace.
_transform.DetachEntity(uid, Transform(uid));
QueueDel(uid);
}
if (entityToPlaceInHands != null)
{
_hands.PickupOrDrop(args.User, entityToPlaceInHands.Value);
_audio.PlayPvs(component.Sound, entityToPlaceInHands.Value); // Sunrise-edit
}
args.Handled = true;
}
}
}

View file

@ -12,6 +12,7 @@ public sealed class SpawnTableOnUseSystem : EntitySystem
[Dependency] private readonly EntityTableSystem _entityTable = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
public override void Initialize()
{
@ -25,17 +26,21 @@ public sealed class SpawnTableOnUseSystem : EntitySystem
if (args.Handled)
return;
args.Handled = true;
var coords = Transform(ent).Coordinates;
var spawns = _entityTable.GetSpawns(ent.Comp.Table);
// Don't delete the entity in the event bus, so we queue it for deletion.
// We need the free hand for the new item, so we send it to nullspace.
_transform.DetachEntity(ent, Transform(ent));
QueueDel(ent);
foreach (var id in spawns)
{
var spawned = Spawn(id, coords);
_adminLogger.Add(LogType.EntitySpawn, LogImpact.Low, $"{ToPrettyString(args.User):user} used {ToPrettyString(ent):spawner} which spawned {ToPrettyString(spawned)}");
_hands.TryPickupAnyHand(args.User, spawned);
_hands.PickupOrDrop(args.User, spawned);
}
Del(ent);
args.Handled = true;
}
}

View file

@ -38,7 +38,6 @@ public sealed partial class StoreSystem
private void OnUseInHand(Entity<StoreRefundComponent> ent, ref UseInHandEvent args)
{
args.Handled = true;
CheckDisableRefund(ent);
}

View file

@ -41,6 +41,9 @@ public sealed class HandTeleporterSystem : EntitySystem
private void OnUseInHand(EntityUid uid, HandTeleporterComponent component, UseInHandEvent args)
{
if (args.Handled)
return;
if (Deleted(component.FirstPortal))
component.FirstPortal = null;
@ -67,6 +70,8 @@ public sealed class HandTeleporterSystem : EntitySystem
_doafter.TryStartDoAfter(doafterArgs);
}
args.Handled = true;
}

View file

@ -9,7 +9,7 @@ using Robust.Shared.Toolshed.Errors;
namespace Content.Server.Toolshed.Commands;
[ToolshedCommand, AdminCommand(AdminFlags.Admin)]
[ToolshedCommand, AdminCommand(AdminFlags.VarEdit)]
public sealed class VisualizeCommand : ToolshedCommand
{
[Dependency] private readonly EuiManager _euiManager = default!;

View file

@ -53,7 +53,7 @@ public sealed class MeleeWeaponSystem : SharedMeleeWeaponSystem
if (damageSpec.Empty)
return;
_damageExamine.AddDamageExamine(args.Message, damageSpec, Loc.GetString("damage-melee"));
_damageExamine.AddDamageExamine(args.Message, Damageable.ApplyUniversalAllModifiers(damageSpec), Loc.GetString("damage-melee"));
}
protected override bool ArcRaySuccessful(EntityUid targetUid,

View file

@ -101,7 +101,7 @@ public sealed partial class GunSystem
throw new ArgumentOutOfRangeException();
}
_damageExamine.AddDamageExamineWithModifier(args.Message, damageSpec, shotCount, shootModifier, damageType);
_damageExamine.AddDamageExamine(args.Message, Damageable.ApplyUniversalAllModifiers(damageSpec), shotCount, shootModifier, damageType);
}
private DamageSpecifier? GetDamage(BatteryAmmoProviderComponent component)
@ -115,7 +115,7 @@ public sealed partial class GunSystem
if (!p.Damage.Empty)
{
return p.Damage;
return p.Damage * Damageable.UniversalProjectileDamageModifier;
}
}
@ -124,7 +124,8 @@ public sealed partial class GunSystem
if (component is HitscanBatteryAmmoProviderComponent hitscan)
{
return ProtoManager.Index<HitscanPrototype>(hitscan.Prototype).Damage;
var dmg = ProtoManager.Index<HitscanPrototype>(hitscan.Prototype).Damage;
return dmg == null ? dmg : dmg * Damageable.UniversalHitscanDamageModifier;
}
return null;

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