From 5794ecd28ffe91fa1e3971784ba6f43f6bb8bf1b Mon Sep 17 00:00:00 2001 From: SlamBamActionman Date: Mon, 16 Sep 2024 11:05:57 +0200 Subject: [PATCH 001/529] Initial commit --- Resources/Prototypes/Reagents/medicine.yml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Resources/Prototypes/Reagents/medicine.yml b/Resources/Prototypes/Reagents/medicine.yml index 4dcef67f77..962e2d09f1 100644 --- a/Resources/Prototypes/Reagents/medicine.yml +++ b/Resources/Prototypes/Reagents/medicine.yml @@ -125,6 +125,8 @@ Radiation: -3 groups: Brute: 0.5 + - !type:ChemVomit + probability: 0.1 - type: reagent id: Bicaridine @@ -556,14 +558,20 @@ min: 4 metabolisms: Medicine: + metabolismRate: 0.1 effects: - !type:HealthChange damage: types: - Cellular: -1 - Radiation: 1 - - !type:ChemVomit - probability: 0.05 + Cellular: -0.3 + Radiation: 0.3 + - !type:HealthChange + conditions: + - !type:ReagentThreshold + min: 10 + damage: + types: + Radiation: 0.2 - type: reagent id: PolypyryliumOligomers From fe69de942f7aa13ed70bf1a774dd4ab5768eec7d Mon Sep 17 00:00:00 2001 From: SlamBamActionman Date: Fri, 1 Nov 2024 12:52:12 +0100 Subject: [PATCH 002/529] Updated values --- Resources/Prototypes/Reagents/medicine.yml | 34 ++++++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/Resources/Prototypes/Reagents/medicine.yml b/Resources/Prototypes/Reagents/medicine.yml index 962e2d09f1..c05d31b2f4 100644 --- a/Resources/Prototypes/Reagents/medicine.yml +++ b/Resources/Prototypes/Reagents/medicine.yml @@ -124,9 +124,7 @@ types: Radiation: -3 groups: - Brute: 0.5 - - !type:ChemVomit - probability: 0.1 + Brute: 1.5 - type: reagent id: Bicaridine @@ -564,14 +562,23 @@ damage: types: Cellular: -0.3 - Radiation: 0.3 + Radiation: 0.15 + Caustic: 0.15 - !type:HealthChange conditions: - - !type:ReagentThreshold - min: 10 + - !type:ReagentThreshold + min: 11 damage: types: Radiation: 0.2 + - !type:HealthChange + conditions: + - !type:ReagentThreshold + reagent: Arithrazine + min: 1 + damage: + types: + Caustic: 0.3 - type: reagent id: PolypyryliumOligomers @@ -977,6 +984,21 @@ - !type:ReagentThreshold min: 30 probability: 0.02 + - !type:ChemVomit + conditions: + - !type:ReagentThreshold + reagent: Arithrazine + min: 1 + probability: 0.1 + - !type:PopupMessage + conditions: + - !type:ReagentThreshold + reagent: Arithrazine + min: 1 + type: Local + visualType: Medium + messages: [ "generic-reagent-effect-nauseous" ] + probability: 0.2 - type: reagent id: Lacerinol From 6242567aff9cf3c1281ac7cf785df30d135800a2 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Sun, 22 Dec 2024 15:13:10 +1300 Subject: [PATCH 003/529] Refactor map loading & saving --- .../Tests/Body/LungTest.cs | 24 +- .../Tests/Body/SaveLoadReparentTest.cs | 11 +- .../Minds/MindTest.DeleteAllThenGhost.cs | 7 +- .../Tests/PostMapInitTest.cs | 80 +++--- Content.IntegrationTests/Tests/SalvageTest.cs | 15 +- .../Tests/SaveLoadMapTest.cs | 15 +- .../Tests/SaveLoadSaveTest.cs | 110 +++++---- .../Tests/Shuttle/DockTest.cs | 7 +- .../Commands/LoadGameMapCommand.cs | 44 ++-- .../Commands/PersistenceSaveCommand.cs | 1 + .../Systems/AdminTestArenaSystem.cs | 40 ++- .../GameTicking/GameTicker.RoundFlow.cs | 230 ++++++++++++++---- Content.Server/GameTicking/GameTicker.cs | 4 +- .../Rules/Components/LoadMapRuleComponent.cs | 8 +- .../GameTicking/Rules/LoadMapRuleSystem.cs | 50 +++- .../GridPreloader/GridPreloaderSystem.cs | 20 +- Content.Server/Mapping/MappingCommand.cs | 10 +- Content.Server/Mapping/MappingManager.cs | 10 +- Content.Server/Mapping/MappingSystem.cs | 6 +- Content.Server/Maps/GameMapPrototype.cs | 5 + Content.Server/Maps/MapMigrationSystem.cs | 2 +- Content.Server/Maps/ResaveCommand.cs | 62 +++-- Content.Server/Procedural/DungeonSystem.cs | 21 +- .../Salvage/SalvageSystem.Magnet.cs | 12 +- Content.Server/Salvage/SalvageSystem.cs | 19 +- .../Shuttles/Systems/ArrivalsSystem.cs | 24 +- .../Systems/EmergencyShuttleSystem.cs | 24 +- .../Systems/ShuttleSystem.GridFill.cs | 45 ++-- .../Shuttles/Systems/ShuttleSystem.cs | 1 + .../Systems/SharedMoverController.Input.cs | 3 +- Resources/Prototypes/GameRules/events.yml | 2 +- Resources/Prototypes/Maps/centcomm.yml | 1 + RobustToolbox | 2 +- 33 files changed, 553 insertions(+), 362 deletions(-) diff --git a/Content.IntegrationTests/Tests/Body/LungTest.cs b/Content.IntegrationTests/Tests/Body/LungTest.cs index 9b5ee431f1..8ac3a3021f 100644 --- a/Content.IntegrationTests/Tests/Body/LungTest.cs +++ b/Content.IntegrationTests/Tests/Body/LungTest.cs @@ -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(); var entityManager = server.ResolveDependency(); var mapLoader = entityManager.System(); var mapSys = entityManager.System(); @@ -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(); - 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(); - grid = ents - .Select(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."); diff --git a/Content.IntegrationTests/Tests/Body/SaveLoadReparentTest.cs b/Content.IntegrationTests/Tests/Body/SaveLoadReparentTest.cs index 01482ba8ee..f56b5c2850 100644 --- a/Content.IntegrationTests/Tests/Body/SaveLoadReparentTest.cs +++ b/Content.IntegrationTests/Tests/Body/SaveLoadReparentTest.cs @@ -4,8 +4,10 @@ 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 +113,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); + 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() @@ -173,7 +174,7 @@ public sealed class SaveLoadReparentTest }); } - maps.DeleteMap(mapId); + entities.DeleteEntity(map); } }); diff --git a/Content.IntegrationTests/Tests/Minds/MindTest.DeleteAllThenGhost.cs b/Content.IntegrationTests/Tests/Minds/MindTest.DeleteAllThenGhost.cs index 7bc62dfe2b..ad4ddc2612 100644 --- a/Content.IntegrationTests/Tests/Minds/MindTest.DeleteAllThenGhost.cs +++ b/Content.IntegrationTests/Tests/Minds/MindTest.DeleteAllThenGhost.cs @@ -1,5 +1,6 @@ #nullable enable using Robust.Shared.Console; +using Robust.Shared.GameObjects; using Robust.Shared.Map; namespace Content.IntegrationTests.Tests.Minds; @@ -37,8 +38,8 @@ public sealed partial class MindTests Assert.That(pair.Client.EntMan.EntityCount, Is.EqualTo(0)); // Create a new map. - int mapId = 1; - await pair.Server.WaitPost(() => conHost.ExecuteCommand($"addmap {mapId}")); + MapId mapId = default; + await pair.Server.WaitPost(() => pair.Server.System().CreateMap(out mapId)); await pair.RunTicksSync(5); // Client is not attached to anything @@ -54,7 +55,7 @@ public sealed partial class MindTests Assert.That(pair.Client.EntMan.EntityExists(pair.Client.AttachedEntity)); Assert.That(pair.Server.EntMan.EntityExists(pair.PlayerData?.Mind)); var xform = pair.Client.Transform(pair.Client.AttachedEntity!.Value); - Assert.That(xform.MapID, Is.EqualTo(new MapId(mapId))); + Assert.That(xform.MapID, Is.EqualTo(mapId)); await pair.CleanReturnAsync(); } diff --git a/Content.IntegrationTests/Tests/PostMapInitTest.cs b/Content.IntegrationTests/Tests/PostMapInitTest.cs index 635a9dbe0f..f7562d934d 100644 --- a/Content.IntegrationTests/Tests/PostMapInitTest.cs +++ b/Content.IntegrationTests/Tests/PostMapInitTest.cs @@ -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,7 +16,8 @@ using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Prototypes; using Content.Shared.Station.Components; -using FastAccessors; +using Robust.Shared.EntitySerialization; +using Robust.Shared.EntitySerialization.Systems; using Robust.Shared.Utility; using YamlDotNet.RepresentationModel; @@ -66,7 +66,7 @@ namespace Content.IntegrationTests.Tests "Amber", "Loop" - + }; /// @@ -81,33 +81,23 @@ namespace Content.IntegrationTests.Tests var entManager = server.ResolveDependency(); var mapLoader = entManager.System(); var mapSystem = entManager.System(); - var mapManager = server.ResolveDependency(); var cfg = server.ResolveDependency(); 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(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 - { - mapManager.DeleteMap(mapId); - } - catch (Exception ex) - { - throw new Exception($"Failed to delete map {mapFile}", ex); - } + mapSystem.DeleteMap(mapId); }); await server.WaitRunTicks(1); @@ -172,16 +162,16 @@ namespace Content.IntegrationTests.Tests var protoManager = server.ResolveDependency(); var ticker = entManager.EntitySysManager.GetEntitySystem(); var shuttleSystem = entManager.EntitySysManager.GetEntitySystem(); - var xformQuery = entManager.GetEntityQuery(); var cfg = server.ResolveDependency(); Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False); await server.WaitPost(() => { - mapSystem.CreateMap(out var mapId); + MapId mapId; try { - ticker.LoadGameMap(protoManager.Index(mapProto), mapId, null); + var opts = DeserializationOptions.Default with {InitializeMaps = true}; + ticker.LoadGameMap(protoManager.Index(mapProto), out mapId, opts); } catch (Exception ex) { @@ -218,21 +208,17 @@ namespace Content.IntegrationTests.Tests if (entManager.TryGetComponent(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(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(shuttle), targetGrid.Value), + shuttleSystem.TryFTLDock(shuttle!.Value.Owner, + entManager.GetComponent(shuttle!.Value.Owner), + targetGrid.Value), $"Unable to dock {shuttlePath} to {mapProto}"); -#pragma warning restore NUnit2045 } - mapManager.DeleteMap(shuttleMap); + mapSystem.DeleteMap(shuttleMap); if (entManager.HasComponent(station)) { @@ -269,7 +255,7 @@ namespace Content.IntegrationTests.Tests try { - mapManager.DeleteMap(mapId); + mapSystem.DeleteMap(mapId); } catch (Exception ex) { @@ -333,11 +319,9 @@ namespace Content.IntegrationTests.Tests var server = pair.Server; var mapLoader = server.ResolveDependency().GetEntitySystem(); - var mapManager = server.ResolveDependency(); var resourceManager = server.ResolveDependency(); var protoManager = server.ResolveDependency(); var cfg = server.ResolveDependency(); - var mapSystem = server.System(); Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False); var gameMaps = protoManager.EnumeratePrototypes().Select(o => o.MapPath).ToHashSet(); @@ -348,7 +332,7 @@ namespace Content.IntegrationTests.Tests .Where(filePath => filePath.Extension == "yml" && !filePath.Filename.StartsWith(".", StringComparison.Ordinal)) .ToArray(); - var mapNames = new List(); + var mapPaths = new List(); foreach (var map in maps) { if (gameMaps.Contains(map)) @@ -359,32 +343,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> maps; + foreach (var path in mapPaths) { - mapSystem.CreateMap(out var mapId); try { - Assert.That(mapLoader.TryLoad(mapId, mapName, out _)); + Assert.That(mapLoader.TryLoadEntities(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); } } }); diff --git a/Content.IntegrationTests/Tests/SalvageTest.cs b/Content.IntegrationTests/Tests/SalvageTest.cs index 5dfba82308..0059db6292 100644 --- a/Content.IntegrationTests/Tests/SalvageTest.cs +++ b/Content.IntegrationTests/Tests/SalvageTest.cs @@ -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(); var mapLoader = entManager.System(); - var mapManager = server.ResolveDependency(); var prototypeManager = server.ResolveDependency(); var cfg = server.ResolveDependency(); var mapSystem = entManager.System(); @@ -34,13 +30,10 @@ public sealed class SalvageTest { foreach (var salvage in prototypeManager.EnumeratePrototypes()) { - 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(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) { diff --git a/Content.IntegrationTests/Tests/SaveLoadMapTest.cs b/Content.IntegrationTests/Tests/SaveLoadMapTest.cs index 213da5d786..b1f6dd8433 100644 --- a/Content.IntegrationTests/Tests/SaveLoadMapTest.cs +++ b/Content.IntegrationTests/Tests/SaveLoadMapTest.cs @@ -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); @@ -48,14 +49,16 @@ namespace Content.IntegrationTests.Tests } Assert.Multiple(() => mapLoader.SaveMap(mapId, mapPath)); - Assert.Multiple(() => mapManager.DeleteMap(mapId)); + Assert.Multiple(() => 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(gridUid, out var gridXform)) { Assert.Fail(); @@ -77,7 +80,7 @@ namespace Content.IntegrationTests.Tests }); } { - 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(gridUid, out var gridXform)) { Assert.Fail(); diff --git a/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs b/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs index 4facd5ee40..a398b55919 100644 --- a/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs +++ b/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs @@ -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 { /// - /// Tests that a map's yaml does not change when saved consecutively. + /// Tests that a grid's yaml does not change when saved consecutively. /// [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(); Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False); + var testSystem = server.System(); + 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(grid0)); + mapLoader.SaveGrid(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(uid)); - }); -#pragma warning restore NUnit2045 - mapLoader.Save(grid1, "save load save 2.yml"); + Assert.That(mapLoader.TryLoadGrid(mapId1, rp1, out var grid1)); + mapLoader.SaveGrid(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().GetEntitySystem(); - var mapManager = server.ResolveDependency(); - var mapSystem = server.System(); + var mapSys = server.System(); + var testSystem = server.System(); + 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(); @@ -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; + mapLoader.SaveMap(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"); + mapLoader.SaveMap(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,13 +187,15 @@ namespace Content.IntegrationTests.Tests var server = pair.Server; var mapLoader = server.System(); - var mapSystem = server.System(); - var mapManager = server.ResolveDependency(); + var mapSys = server.System(); var userData = server.ResolveDependency().UserData; var cfg = server.ResolveDependency(); Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False); + var testSystem = server.System(); + testSystem.Enabled = true; - MapId mapId = default; + MapId mapId1 = default; + MapId mapId2 = default; const string fileA = "/load tick load a.yml"; const string fileB = "/load tick load b.yml"; string yamlA; @@ -199,10 +204,10 @@ namespace Content.IntegrationTests.Tests // 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; + mapLoader.SaveMap(mapId1, fileA); }); await server.WaitIdleAsync(); @@ -217,11 +222,10 @@ 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; + mapLoader.SaveMap(mapId2, fileB); }); await server.WaitIdleAsync(); @@ -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(); } + + /// + /// 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. + /// + private sealed class SaveLoadSaveTestSystem : EntitySystem + { + public bool Enabled; + public override void Initialize() + { + SubscribeLocalEvent(OnAfterSave); + } + + private void OnAfterSave(AfterSaveEvent ev) + { + if (!Enabled) + return; + + // Remove timestamp. + ((MappingDataNode)ev.Node["meta"]).Remove("time"); + } + } } } diff --git a/Content.IntegrationTests/Tests/Shuttle/DockTest.cs b/Content.IntegrationTests/Tests/Shuttle/DockTest.cs index f6e99596e9..ab82a3d2f9 100644 --- a/Content.IntegrationTests/Tests/Shuttle/DockTest.cs +++ b/Content.IntegrationTests/Tests/Shuttle/DockTest.cs @@ -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(map.MapUid); entManager.DeleteEntity(map.Grid); - Assert.That(entManager.System().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().TryLoadGrid(otherMap.MapId, path, out var grid)); + shuttle = grid!.Value.Owner; var dockingConfig = dockingSystem.GetDockingConfig(shuttle, map.MapUid); Assert.That(dockingConfig, Is.EqualTo(null)); diff --git a/Content.Server/Administration/Commands/LoadGameMapCommand.cs b/Content.Server/Administration/Commands/LoadGameMapCommand.cs index ea31c1ecb1..034b50a9c5 100644 --- a/Content.Server/Administration/Commands/LoadGameMapCommand.cs +++ b/Content.Server/Administration/Commands/LoadGameMapCommand.cs @@ -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(); var entityManager = IoCManager.Resolve(); var gameTicker = entityManager.EntitySysManager.GetEntitySystem(); + var mapSys = entityManager.EntitySysManager.GetEntitySystem(); if (args.Length is not (2 or 4 or 5)) { @@ -32,29 +31,28 @@ namespace Content.Server.Administration.Commands return; } - if (prototypeManager.TryIndex(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(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) diff --git a/Content.Server/Administration/Commands/PersistenceSaveCommand.cs b/Content.Server/Administration/Commands/PersistenceSaveCommand.cs index 7ef1932c56..dffe1eff7f 100644 --- a/Content.Server/Administration/Commands/PersistenceSaveCommand.cs +++ b/Content.Server/Administration/Commands/PersistenceSaveCommand.cs @@ -5,6 +5,7 @@ using Robust.Shared.Configuration; using Robust.Shared.Console; using Robust.Shared.Map; using System.Linq; +using Robust.Shared.EntitySerialization.Systems; namespace Content.Server.Administration.Commands; diff --git a/Content.Server/Administration/Systems/AdminTestArenaSystem.cs b/Content.Server/Administration/Systems/AdminTestArenaSystem.cs index e3671bcdfb..12bf0eba64 100644 --- a/Content.Server/Administration/Systems/AdminTestArenaSystem.cs +++ b/Content.Server/Administration/Systems/AdminTestArenaSystem.cs @@ -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; /// 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(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); } } diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs index a7dd5d6ab6..37e7a4e1ce 100644 --- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs +++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs @@ -1,4 +1,5 @@ using System.Linq; +using System.Numerics; using Content.Server.Announcements; using Content.Server.Discord; using Content.Server.GameTicking.Events; @@ -13,10 +14,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; @@ -92,9 +95,6 @@ namespace Content.Server.GameTicking AddGamePresetRules(); - DefaultMap = _mapManager.CreateMap(); - _mapManager.AddUninitializedMap(DefaultMap); - var maps = new List(); // the map might have been force-set by something @@ -132,52 +132,203 @@ 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; + } /// /// 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 . /// - /// Game map prototype to load in. - /// Map to load into. - /// Map loading options, includes offset. + /// + /// This is basically a wrapper around a method that auto generate + /// some using information in a prototype, and raise some events to allow content + /// to modify the options and react to the map creation. + /// + /// Game map prototype to load in. + /// The id of the map that was loaded. + /// Entity loading options, including whether the maps should be initialized. /// Name to assign to the loaded station. /// All loaded entities and grids. - public IReadOnlyList LoadGameMap(GameMapPrototype map, MapId targetMapId, MapLoadOptions? loadOptions, string? stationName = null) + public IReadOnlyList 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 {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); + /// + /// Variant of that attempts to assign the provided to the + /// loaded map. + /// + public IReadOnlyList 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 {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; + } + + /// + /// Variant of that loads and then merges a game map onto an existing map. + /// + public IReadOnlyList 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 {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 map, + 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; } @@ -274,7 +425,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); @@ -714,21 +865,14 @@ namespace Content.Server.GameTicking /// You likely want to subscribe to this after StationSystem. /// [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; } - /// /// Event raised after the game loads a given map. /// diff --git a/Content.Server/GameTicking/GameTicker.cs b/Content.Server/GameTicking/GameTicker.cs index f8ba7e1d7d..d286fd641e 100644 --- a/Content.Server/GameTicking/GameTicker.cs +++ b/Content.Server/GameTicking/GameTicker.cs @@ -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!; diff --git a/Content.Server/GameTicking/Rules/Components/LoadMapRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/LoadMapRuleComponent.cs index 1f0505c60f..f09fff5eaf 100644 --- a/Content.Server/GameTicking/Rules/Components/LoadMapRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/LoadMapRuleComponent.cs @@ -20,11 +20,17 @@ public sealed partial class LoadMapRuleComponent : Component public ProtoId? GameMap; /// - /// A map path to load on a new map. + /// A map to load. /// [DataField] public ResPath? MapPath; + /// + /// A grid to load on a new map. + /// + [DataField] + public ResPath? GridPath; + /// /// A to move to a new map. /// If there are no instances left nothing is done. diff --git a/Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs b/Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs index f18115d3cf..003a74ef4b 100644 --- a/Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs @@ -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 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 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 {grid.Value.Owner}; } else if (comp.PreloadedGrid is {} preloaded) { @@ -56,11 +81,11 @@ public sealed class LoadMapRuleSystem : StationEventSystem 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() { loadedShuttle.Value }; _map.InitializeMap(mapUid); @@ -68,7 +93,6 @@ public sealed class LoadMapRuleSystem : StationEventSystem else { Log.Error($"No valid map prototype or map path associated with the rule {ToPrettyString(uid)}"); - Del(mapUid); ForceEndSelf(uid, rule); return; } diff --git a/Content.Server/GridPreloader/GridPreloaderSystem.cs b/Content.Server/GridPreloader/GridPreloaderSystem.cs index e12ce41a31..d648acbb06 100644 --- a/Content.Server/GridPreloader/GridPreloaderSystem.cs +++ b/Content.Server/GridPreloader/GridPreloaderSystem.cs @@ -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(gridUid, out var mapGrid)) - continue; + var (gridUid, mapGrid) = grid.Value; if (!TryComp(gridUid, out var physics)) continue; diff --git a/Content.Server/Mapping/MappingCommand.cs b/Content.Server/Mapping/MappingCommand.cs index 70647d3281..b85b0953dd 100644 --- a/Content.Server/Mapping/MappingCommand.cs +++ b/Content.Server/Mapping/MappingCommand.cs @@ -3,12 +3,13 @@ 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.Utility; namespace Content.Server.Mapping { @@ -91,8 +92,9 @@ namespace Content.Server.Mapping } else { - var loadOptions = new MapLoadOptions {StoreMapUids = true}; - _entities.System().TryLoad(mapId, args[1], out _, loadOptions); + var path = new ResPath(args[1]); + var opts = new DeserializationOptions {StoreYamlUids = true}; + _entities.System().TryLoadMapWithId(mapId, path, out _, out _, opts); } // was the map actually created or did it fail somehow? diff --git a/Content.Server/Mapping/MappingManager.cs b/Content.Server/Mapping/MappingManager.cs index e8c6eca204..be6f503bf0 100644 --- a/Content.Server/Mapping/MappingManager.cs +++ b/Content.Server/Mapping/MappingManager.cs @@ -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,13 @@ 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().GetMapCoordinates(player).MapId; - var mapEntity = _map.GetMapEntityIdOrThrow(mapId); - var data = _systems.GetEntitySystem().GetSaveData(mapEntity); + var data = _systems.GetEntitySystem().SerializeEntityRecursive(mapUid).Node; var document = new YamlDocument(data.ToYaml()); var stream = new YamlStream { document }; var writer = new StringWriter(); diff --git a/Content.Server/Mapping/MappingSystem.cs b/Content.Server/Mapping/MappingSystem.cs index 28bb3afbe1..ecdbbe081c 100644 --- a/Content.Server/Mapping/MappingSystem.cs +++ b/Content.Server/Mapping/MappingSystem.cs @@ -3,10 +3,10 @@ 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.Timing; using Robust.Shared.Utility; @@ -23,7 +23,7 @@ public sealed class MappingSystem : EntitySystem [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IMapManager _mapManager = 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 /// @@ -78,7 +78,7 @@ public sealed class MappingSystem : EntitySystem 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); + _loader.SaveMap(map, path); } } diff --git a/Content.Server/Maps/GameMapPrototype.cs b/Content.Server/Maps/GameMapPrototype.cs index 5942a9930e..c39ca348da 100644 --- a/Content.Server/Maps/GameMapPrototype.cs +++ b/Content.Server/Maps/GameMapPrototype.cs @@ -25,6 +25,11 @@ public sealed partial class GameMapPrototype : IPrototype [DataField] public float MaxRandomOffset = 1000f; + /// + /// Turns out some of the map files are actually secretly grids. Excellent. I love map loading code. + /// + [DataField] public bool IsGrid; + [DataField] public bool RandomRotation = true; diff --git a/Content.Server/Maps/MapMigrationSystem.cs b/Content.Server/Maps/MapMigrationSystem.cs index 83c2abc5e3..3341034a35 100644 --- a/Content.Server/Maps/MapMigrationSystem.cs +++ b/Content.Server/Maps/MapMigrationSystem.cs @@ -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; diff --git a/Content.Server/Maps/ResaveCommand.cs b/Content.Server/Maps/ResaveCommand.cs index cc4d13dded..0d48f946be 100644 --- a/Content.Server/Maps/ResaveCommand.cs +++ b/Content.Server/Maps/ResaveCommand.cs @@ -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(); - 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.TryLoadEntities(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(mapUid); - - if (_entManager.HasComponent(mapUid) || mapXform.ChildCount != 1) + if (_entManager.HasComponent(map)) { - loader.SaveMap(mapId, fn.ToString()); + loader.SaveMap(map.Comp.MapId, fn); } - else if (mapXform.ChildEnumerator.MoveNext(out var child)) + else if (result.Grids.Count == 1) { - loader.Save(child, fn.ToString()); + loader.SaveGrid(result.Grids.First(), fn); + } + else + { + shell.WriteError($"Failed to resave {fn}"); } - _mapManager.DeleteMap(mapId); + loader.Delete(result); } + + shell.WriteLine($"Resaved all maps"); } } diff --git a/Content.Server/Procedural/DungeonSystem.cs b/Content.Server/Procedural/DungeonSystem.cs index 706f63ffd7..75cdb69130 100644 --- a/Content.Server/Procedural/DungeonSystem.cs +++ b/Content.Server/Procedural/DungeonSystem.cs @@ -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(mapUid); + var opts = new MapLoadOptions + { + DeserializationOptions = DeserializationOptions.Default with {PauseMaps = true}, + ExpectedCategory = FileCategory.Map + }; + + if (!_loader.TryLoadEntities(proto.AtlasPath, out var res, opts) || !res.Maps.TryFirstOrNull(out var map)) + throw new Exception($"Failed to load dungeon template."); + + comp = AddComp(map.Value.Owner); comp.Path = proto.AtlasPath; - return mapId; + return map.Value.Comp.MapId; } /// diff --git a/Content.Server/Salvage/SalvageSystem.Magnet.cs b/Content.Server/Salvage/SalvageSystem.Magnet.cs index 81db78fb20..f0520b6dc6 100644 --- a/Content.Server/Salvage/SalvageSystem.Magnet.cs +++ b/Content.Server/Salvage/SalvageSystem.Magnet.cs @@ -2,14 +2,11 @@ using System.Linq; using System.Numerics; using System.Threading.Tasks; using Content.Server.Salvage.Magnet; -using Content.Shared.Humanoid; 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.Map; -using Robust.Shared.Map.Components; namespace Content.Server.Salvage; @@ -278,15 +275,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; } diff --git a/Content.Server/Salvage/SalvageSystem.cs b/Content.Server/Salvage/SalvageSystem.cs index eb5719c892..9115c60536 100644 --- a/Content.Server/Salvage/SalvageSystem.cs +++ b/Content.Server/Salvage/SalvageSystem.cs @@ -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!; diff --git a/Content.Server/Shuttles/Systems/ArrivalsSystem.cs b/Content.Server/Shuttles/Systems/ArrivalsSystem.cs index 1f972d9675..708f5a7a1c 100644 --- a/Content.Server/Shuttles/Systems/ArrivalsSystem.cs +++ b/Content.Server/Shuttles/Systems/ArrivalsSystem.cs @@ -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; @@ -512,15 +514,13 @@ public sealed class ArrivalsSystem : EntitySystem 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)) - { + var path = new ResPath(_cfgManager.GetCVar(CCVars.ArrivalsMap)); + if (!_loader.TryLoadMap(path, out var map, out var grids)) return; - } - foreach (var id in uids) + _metaData.SetEntityName(map.Value, Loc.GetString("map-name-terminal")); + + foreach (var id in grids) { EnsureComp(id); EnsureComp(id); @@ -531,15 +531,15 @@ public sealed class ArrivalsSystem : EntitySystem if (_cfgManager.GetCVar(CCVars.ArrivalsPlanet)) { var template = _random.Pick(_arrivalsBiomeOptions); - _biomes.EnsurePlanet(mapUid, _protoManager.Index(template)); + _biomes.EnsurePlanet(map.Value, _protoManager.Index(template)); var restricted = new RestrictedRangeComponent { Range = 32f }; - AddComp(mapUid, restricted); + AddComp(map.Value, restricted); } - _mapSystem.InitializeMap(mapId); + _mapSystem.InitializeMap(map.Value.Comp.MapId); // Handle roundstart stations. var query = AllEntityQuery(); @@ -600,9 +600,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(component.Shuttle); var arrivalsComp = EnsureComp(component.Shuttle); arrivalsComp.Station = uid; diff --git a/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs b/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs index 6c4bdc0814..afa77421bd 100644 --- a/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs +++ b/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs @@ -29,10 +29,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.Random; @@ -60,7 +59,7 @@ 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 _map = 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!; @@ -531,10 +530,11 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem } var map = _mapSystem.CreateMap(out var mapId); - var grid = _map.LoadGrid(mapId, component.Map.ToString(), new MapLoadOptions() + if (!_loader.TryLoadGrid(mapId, component.Map, out var grid)) { - LoadMap = false, - }); + Log.Error($"Failed to set up centcomm grid!"); + return; + } if (!Exists(map)) { @@ -608,15 +608,11 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem // Load escape shuttle var shuttlePath = ent.Comp1.EmergencyShuttlePath; - var shuttle = _map.LoadGrid(map.MapId, shuttlePath.ToString(), new MapLoadOptions() - { + if (!_loader.TryLoadGrid(map.MapId, + shuttlePath, + out var shuttle, // Should be far enough... right? I'm too lazy to bounds check CentCom rn. - Offset = new Vector2(500f + ent.Comp2.ShuttleIndex, 0f), - // fun fact: if you just fucking yeet centcomm into nullspace anytime you try to spawn the shuttle, then any distance is far enough. so lets not do that - LoadMap = false, - }); - - if (shuttle == null) + offset: new Vector2(500f + ent.Comp2.ShuttleIndex, 0f))) { Log.Error($"Unable to spawn emergency shuttle {shuttlePath} for {ToPrettyString(ent)}"); return; diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.GridFill.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.GridFill.cs index de0593b26f..6123f348ec 100644 --- a/Content.Server/Shuttles/Systems/ShuttleSystem.GridFill.cs +++ b/Content.Server/Shuttles/Systems/ShuttleSystem.GridFill.cs @@ -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(ent[0])) - { - TryFTLProximity(ent[0], targetGrid.Value); - } + if (HasComp(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 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(ent[0])) - { - TryFTLProximity(ent[0], targetGrid); - } + if (HasComp(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(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) diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.cs index 6e8c1a9e20..f2c58d103c 100644 --- a/Content.Server/Shuttles/Systems/ShuttleSystem.cs +++ b/Content.Server/Shuttles/Systems/ShuttleSystem.cs @@ -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; diff --git a/Content.Shared/Movement/Systems/SharedMoverController.Input.cs b/Content.Shared/Movement/Systems/SharedMoverController.Input.cs index 6f508d9038..14c8036287 100644 --- a/Content.Shared/Movement/Systems/SharedMoverController.Input.cs +++ b/Content.Shared/Movement/Systems/SharedMoverController.Input.cs @@ -8,6 +8,7 @@ using Content.Shared.Movement.Events; using Robust.Shared.GameStates; using Robust.Shared.Input; using Robust.Shared.Input.Binding; +using Robust.Shared.Map.Components; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; @@ -207,7 +208,7 @@ namespace Content.Shared.Movement.Systems } // If we went from grid -> map we'll preserve our worldrotation - if (relative != null && _mapManager.IsMap(relative.Value)) + if (relative != null && HasComp(relative.Value)) { targetRotation = currentRotation.FlipPositive().Reduced(); } diff --git a/Resources/Prototypes/GameRules/events.yml b/Resources/Prototypes/GameRules/events.yml index 2051c24be8..9a4ca97210 100644 --- a/Resources/Prototypes/GameRules/events.yml +++ b/Resources/Prototypes/GameRules/events.yml @@ -454,7 +454,7 @@ duration: 1 - type: RuleGrids - type: LoadMapRule - mapPath: /Maps/Shuttles/ShuttleEvent/striker.yml + gridPath: /Maps/Shuttles/ShuttleEvent/striker.yml - type: NukeopsRule roundEndBehavior: Nothing - type: AntagSelection diff --git a/Resources/Prototypes/Maps/centcomm.yml b/Resources/Prototypes/Maps/centcomm.yml index 47dc5ca1f3..007da851d0 100644 --- a/Resources/Prototypes/Maps/centcomm.yml +++ b/Resources/Prototypes/Maps/centcomm.yml @@ -1,5 +1,6 @@ - type: gameMap id: CentComm + isGrid: true # Did you know that centcomm is the only "game map" that isn't actually a map? Send help. mapName: 'Central Command' mapPath: /Maps/centcomm.yml minPlayers: 10 diff --git a/RobustToolbox b/RobustToolbox index 5e97db435c..3cccf5be02 160000 --- a/RobustToolbox +++ b/RobustToolbox @@ -1 +1 @@ -Subproject commit 5e97db435c05b4c188184ef90e5d77b0500403d0 +Subproject commit 3cccf5be028be75242ffc86877b4e78a72b8cafe From 37c843328f964233a5e6690468729928eb05c0b4 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Mon, 23 Dec 2024 14:22:27 +1300 Subject: [PATCH 004/529] Fix NoSavedPostMapInitTest --- .../Tests/PostMapInitTest.cs | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/Content.IntegrationTests/Tests/PostMapInitTest.cs b/Content.IntegrationTests/Tests/PostMapInitTest.cs index f7562d934d..0a48730d54 100644 --- a/Content.IntegrationTests/Tests/PostMapInitTest.cs +++ b/Content.IntegrationTests/Tests/PostMapInitTest.cs @@ -111,12 +111,15 @@ namespace Content.IntegrationTests.Tests var server = pair.Server; var resourceManager = server.ResolveDependency(); + var loader = server.System(); + 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(); foreach (var map in maps) { var rootedPath = map.ToRootedPath(); @@ -139,10 +142,41 @@ namespace Content.IntegrationTests.Tests var root = yamlStream.Documents[0].RootNode; var meta = root["meta"]; - var postMapInit = meta["postmapinit"].AsBool(); + var version = meta["format"].AsInt(); + if (version >= 7) + { + v7Maps.Add(map); + continue; + } + + var postMapInit = meta["postmapinit"].AsBool(); Assert.That(postMapInit, Is.False, $"Map {map.Filename} was saved postmapinit"); } + + var deps = server.ResolveDependency().DependencyCollection; + foreach (var map in v7Maps) + { + if (!loader.TryReadFile(map, out var data)) + { + Assert.Fail($"Failed to read {map}"); + continue; + } + + var reader = new EntityDeserializer(deps, data, DeserializationOptions.Default); + if (!reader.TryProcessData()) + { + Assert.Fail($"Failed to process {map}"); + continue; + } + + foreach (var mapId in reader.MapYamlIds) + { + var mapData = reader.YamlEntities[mapId]; + Assert.That(!mapData.PostInit, $"Map {map.Filename} contains a postmapinit map with yaml id: {mapId}"); + } + } + await pair.CleanReturnAsync(); } From d47e72d7c2c6290563bf8519b2f6bb3a3c82f995 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Mon, 23 Dec 2024 16:38:05 +1300 Subject: [PATCH 005/529] oops --- RobustToolbox | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RobustToolbox b/RobustToolbox index 3cccf5be02..5e97db435c 160000 --- a/RobustToolbox +++ b/RobustToolbox @@ -1 +1 @@ -Subproject commit 3cccf5be028be75242ffc86877b4e78a72b8cafe +Subproject commit 5e97db435c05b4c188184ef90e5d77b0500403d0 From 43e6fd57d4e7136747bd372e5af1d1f3dfed57a6 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Mon, 23 Dec 2024 18:02:46 +1300 Subject: [PATCH 006/529] Fix MapLoadBenchmark --- Content.Benchmarks/MapLoadBenchmark.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Content.Benchmarks/MapLoadBenchmark.cs b/Content.Benchmarks/MapLoadBenchmark.cs index 8c04d9a40d..abf99f9836 100644 --- a/Content.Benchmarks/MapLoadBenchmark.cs +++ b/Content.Benchmarks/MapLoadBenchmark.cs @@ -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().GetEntitySystem(); - _mapManager = server.ResolveDependency(); + _mapSys = server.ResolveDependency().GetEntitySystem(); } [GlobalCleanup] @@ -52,17 +53,19 @@ public class MapLoadBenchmark public string Map; public Dictionary 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(); } } From ef3d7396d496374b639c949da4d9a5bb8153e057 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Mon, 23 Dec 2024 18:13:49 +1300 Subject: [PATCH 007/529] fix other benchmarks --- Content.Benchmarks/ComponentQueryBenchmark.cs | 12 ++++++------ Content.Benchmarks/PvsBenchmark.cs | 11 ++++++----- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Content.Benchmarks/ComponentQueryBenchmark.cs b/Content.Benchmarks/ComponentQueryBenchmark.cs index 11c7ab9d5f..49e9984c09 100644 --- a/Content.Benchmarks/ComponentQueryBenchmark.cs +++ b/Content.Benchmarks/ComponentQueryBenchmark.cs @@ -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 _itemQuery; private EntityQuery _clothingQuery; private EntityQuery _mapQuery; @@ -54,10 +54,10 @@ public class ComponentQueryBenchmark _pair.Server.ResolveDependency().SetSeed(42); _pair.Server.WaitPost(() => { - var success = _entMan.System().TryLoad(_mapId, Map, out _); - if (!success) + var map = new ResPath(Map); + var opts = DeserializationOptions.Default with {InitializeMaps = true}; + if (!_entMan.System().TryLoadMap(map, out _, out _, opts)) throw new Exception("Map load failed"); - _pair.Server.MapMan.DoMapInitialize(_mapId); }).GetAwaiter().GetResult(); _items = new EntityUid[_entMan.Count()]; diff --git a/Content.Benchmarks/PvsBenchmark.cs b/Content.Benchmarks/PvsBenchmark.cs index fa7f9d4542..2f87545426 100644 --- a/Content.Benchmarks/PvsBenchmark.cs +++ b/Content.Benchmarks/PvsBenchmark.cs @@ -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().SetSeed(42); await _pair.Server.WaitPost(() => { - var success = _entMan.System().TryLoad(_mapId, Map, out _); - if (!success) + var path = new ResPath(Map); + var opts = DeserializationOptions.Default with {InitializeMaps = true}; + if (!_entMan.System().TryLoadMap(path, out _, out _, opts)) throw new Exception("Map load failed"); - _pair.Server.MapMan.DoMapInitialize(_mapId); }); // Get list of ghost warp positions From f58baf042850680f9a03714976a3b33c48ca5ddf Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Tue, 24 Dec 2024 03:37:31 +1300 Subject: [PATCH 008/529] Update permissions for engine toolshed PR --- Resources/toolshedEngineCommandPerms.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Resources/toolshedEngineCommandPerms.yml b/Resources/toolshedEngineCommandPerms.yml index b9911e9468..ae02339c1a 100644 --- a/Resources/toolshedEngineCommandPerms.yml +++ b/Resources/toolshedEngineCommandPerms.yml @@ -74,7 +74,7 @@ - '%' - '%/' - '&~' - - '|~' + - bitornot - '^~' - '~' - '<' @@ -88,7 +88,7 @@ - '*/' - '//' - '&' - - '|' + - bitor - '^' - neg - abs From 9d05ea9f57dcb0931f820dab19648d6e6c7c30b9 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Tue, 24 Dec 2024 04:01:17 +1300 Subject: [PATCH 009/529] Poke tests From eef7d02e11c8bcf88fbee50ad87717d239ec1c48 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Tue, 24 Dec 2024 12:27:33 +1300 Subject: [PATCH 010/529] Improve NoSavedPostMapInitTest --- .../Tests/PostMapInitTest.cs | 62 +++++++++++++------ 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/Content.IntegrationTests/Tests/PostMapInitTest.cs b/Content.IntegrationTests/Tests/PostMapInitTest.cs index 0a48730d54..80e8e7b318 100644 --- a/Content.IntegrationTests/Tests/PostMapInitTest.cs +++ b/Content.IntegrationTests/Tests/PostMapInitTest.cs @@ -18,6 +18,7 @@ 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; @@ -157,29 +158,54 @@ namespace Content.IntegrationTests.Tests var deps = server.ResolveDependency().DependencyCollection; foreach (var map in v7Maps) { - if (!loader.TryReadFile(map, out var data)) - { - Assert.Fail($"Failed to read {map}"); - continue; - } - - var reader = new EntityDeserializer(deps, data, DeserializationOptions.Default); - if (!reader.TryProcessData()) - { - Assert.Fail($"Failed to process {map}"); - continue; - } - - foreach (var mapId in reader.MapYamlIds) - { - var mapData = reader.YamlEntities[mapId]; - Assert.That(!mapData.PostInit, $"Map {map.Filename} contains a postmapinit map with yaml id: {mapId}"); - } + 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(); + 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"); + loader.SaveMap(id, path); + Assert.That(IsPreInit(path, loader, deps)); + + // and the post-init version fails. + await server.WaitPost(() => mapSys.InitializeMap(id)); + loader.SaveMap(id, path); + Assert.That(IsPreInit(path, loader, deps), Is.False); + await pair.CleanReturnAsync(); } + private bool IsPreInit(ResPath map, MapLoaderSystem loader, IDependencyCollection deps) + { + if (!loader.TryReadFile(map, out var data)) + { + Assert.Fail($"Failed to read {map}"); + return false; + } + + var reader = new EntityDeserializer(deps, data, DeserializationOptions.Default); + if (!reader.TryProcessData()) + { + Assert.Fail($"Failed to process {map}"); + return false; + } + + foreach (var mapId in reader.MapYamlIds) + { + var mapData = reader.YamlEntities[mapId]; + if (mapData.PostInit) + return false; + } + + return true; + } + [Test, TestCaseSource(nameof(GameMaps))] public async Task GameMapsLoadableTest(string mapProto) { From a8cc0397c2634a085fbb424c7bdc2c951bb4cd72 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Tue, 24 Dec 2024 18:57:52 +1300 Subject: [PATCH 011/529] Moar engine changes --- .../Tests/Body/SaveLoadReparentTest.cs | 3 +-- .../Tests/PostMapInitTest.cs | 4 ++-- .../Tests/SaveLoadMapTest.cs | 4 ++-- .../Tests/SaveLoadSaveTest.cs | 24 +++++++++---------- .../Commands/PersistenceSaveCommand.cs | 5 ++-- .../DeviceNetwork/Systems/DeviceListSystem.cs | 4 ++-- .../Systems/NetworkConfiguratorSystem.cs | 4 ++-- Content.Server/Mapping/MappingSystem.cs | 2 +- Content.Server/Maps/ResaveCommand.cs | 4 ++-- Content.Shared/Follower/FollowerSystem.cs | 4 ++-- 10 files changed, 28 insertions(+), 30 deletions(-) diff --git a/Content.IntegrationTests/Tests/Body/SaveLoadReparentTest.cs b/Content.IntegrationTests/Tests/Body/SaveLoadReparentTest.cs index f56b5c2850..67163d0796 100644 --- a/Content.IntegrationTests/Tests/Body/SaveLoadReparentTest.cs +++ b/Content.IntegrationTests/Tests/Body/SaveLoadReparentTest.cs @@ -2,7 +2,6 @@ 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; @@ -115,7 +114,7 @@ public sealed class SaveLoadReparentTest var mapPath = new ResPath($"/{nameof(SaveLoadReparentTest)}{nameof(Test)}map.yml"); - mapLoader.SaveMap(mapId, mapPath); + Assert.That(mapLoader.TrySaveMap(mapId, mapPath)); mapSys.DeleteMap(mapId); Assert.That(mapLoader.TryLoadMap(mapPath, out var map, out _), Is.True); diff --git a/Content.IntegrationTests/Tests/PostMapInitTest.cs b/Content.IntegrationTests/Tests/PostMapInitTest.cs index 80e8e7b318..9e908371df 100644 --- a/Content.IntegrationTests/Tests/PostMapInitTest.cs +++ b/Content.IntegrationTests/Tests/PostMapInitTest.cs @@ -170,12 +170,12 @@ namespace Content.IntegrationTests.Tests // First check that a pre-init version passes var path = new ResPath($"{nameof(NoSavedPostMapInitTest)}.yml"); - loader.SaveMap(id, path); + Assert.That(loader.TrySaveMap(id, path)); Assert.That(IsPreInit(path, loader, deps)); // and the post-init version fails. await server.WaitPost(() => mapSys.InitializeMap(id)); - loader.SaveMap(id, path); + Assert.That(loader.TrySaveMap(id, path)); Assert.That(IsPreInit(path, loader, deps), Is.False); await pair.CleanReturnAsync(); diff --git a/Content.IntegrationTests/Tests/SaveLoadMapTest.cs b/Content.IntegrationTests/Tests/SaveLoadMapTest.cs index b1f6dd8433..b96fbe5767 100644 --- a/Content.IntegrationTests/Tests/SaveLoadMapTest.cs +++ b/Content.IntegrationTests/Tests/SaveLoadMapTest.cs @@ -48,8 +48,8 @@ namespace Content.IntegrationTests.Tests mapSystem.SetTile(mapGrid, new Vector2i(0, 0), new Tile(2, (TileRenderFlag) 1, 254)); } - Assert.Multiple(() => mapLoader.SaveMap(mapId, mapPath)); - Assert.Multiple(() => mapSystem.DeleteMap(mapId)); + Assert.That(mapLoader.TrySaveMap(mapId, mapPath)); + mapSystem.DeleteMap(mapId); }); await server.WaitIdleAsync(); diff --git a/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs b/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs index a398b55919..b41aa0bf2f 100644 --- a/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs +++ b/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs @@ -41,10 +41,10 @@ namespace Content.IntegrationTests.Tests mapSystem.CreateMap(out var mapId0); var grid0 = mapManager.CreateGridEntity(mapId0); entManager.RunMapInit(grid0.Owner, entManager.GetComponent(grid0)); - mapLoader.SaveGrid(grid0.Owner, rp1); + Assert.That(mapLoader.TrySaveGrid(grid0.Owner, rp1)); mapSystem.CreateMap(out var mapId1); Assert.That(mapLoader.TryLoadGrid(mapId1, rp1, out var grid1)); - mapLoader.SaveGrid(grid1!.Value, rp2); + Assert.That(mapLoader.TrySaveGrid(grid1!.Value, rp2)); }); await server.WaitIdleAsync(); @@ -116,7 +116,7 @@ namespace Content.IntegrationTests.Tests 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; - mapLoader.SaveMap(mapId, rp1); + Assert.That(mapLoader.TrySaveMap(mapId, rp1)); }); // Run 5 ticks. @@ -124,7 +124,7 @@ namespace Content.IntegrationTests.Tests await server.WaitPost(() => { - mapLoader.SaveMap(mapId, rp2); + Assert.That(mapLoader.TrySaveMap(mapId, rp2)); }); await server.WaitIdleAsync(); @@ -196,8 +196,8 @@ namespace Content.IntegrationTests.Tests MapId mapId1 = default; MapId mapId2 = default; - const string fileA = "/load tick load a.yml"; - const string fileB = "/load tick load b.yml"; + var fileA = new ResPath("/load tick load a.yml"); + var fileB = new ResPath("/load tick load b.yml"); string yamlA; string yamlB; @@ -207,11 +207,11 @@ namespace Content.IntegrationTests.Tests 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; - mapLoader.SaveMap(mapId1, fileA); + 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(); @@ -225,12 +225,12 @@ namespace Content.IntegrationTests.Tests 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; - mapLoader.SaveMap(mapId2, fileB); + 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(); @@ -253,10 +253,10 @@ namespace Content.IntegrationTests.Tests public bool Enabled; public override void Initialize() { - SubscribeLocalEvent(OnAfterSave); + SubscribeLocalEvent(OnAfterSave); } - private void OnAfterSave(AfterSaveEvent ev) + private void OnAfterSave(AfterSerializationEvent ev) { if (!Enabled) return; diff --git a/Content.Server/Administration/Commands/PersistenceSaveCommand.cs b/Content.Server/Administration/Commands/PersistenceSaveCommand.cs index dffe1eff7f..cae507f6d8 100644 --- a/Content.Server/Administration/Commands/PersistenceSaveCommand.cs +++ b/Content.Server/Administration/Commands/PersistenceSaveCommand.cs @@ -1,11 +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; @@ -49,7 +48,7 @@ public sealed class PersistenceSave : IConsoleCommand } var mapLoader = _system.GetEntitySystem(); - mapLoader.SaveMap(mapId, saveFilePath); + mapLoader.TrySaveMap(mapId, new ResPath(saveFilePath)); shell.WriteLine(Loc.GetString("cmd-savemap-success")); } } diff --git a/Content.Server/DeviceNetwork/Systems/DeviceListSystem.cs b/Content.Server/DeviceNetwork/Systems/DeviceListSystem.cs index a8d40882c4..4defec0aab 100644 --- a/Content.Server/DeviceNetwork/Systems/DeviceListSystem.cs +++ b/Content.Server/DeviceNetwork/Systems/DeviceListSystem.cs @@ -20,7 +20,7 @@ public sealed class DeviceListSystem : SharedDeviceListSystem SubscribeLocalEvent(OnShutdown); SubscribeLocalEvent(OnBeforeBroadcast); SubscribeLocalEvent(OnBeforePacketSent); - SubscribeLocalEvent(OnMapSave); + SubscribeLocalEvent(OnMapSave); } private void OnShutdown(EntityUid uid, DeviceListComponent component, ComponentShutdown args) @@ -124,7 +124,7 @@ public sealed class DeviceListSystem : SharedDeviceListSystem Dirty(list); } - private void OnMapSave(BeforeSaveEvent ev) + private void OnMapSave(BeforeSerializationEvent ev) { List toRemove = new(); var query = GetEntityQuery(); diff --git a/Content.Server/DeviceNetwork/Systems/NetworkConfiguratorSystem.cs b/Content.Server/DeviceNetwork/Systems/NetworkConfiguratorSystem.cs index efaf568094..7e6452d955 100644 --- a/Content.Server/DeviceNetwork/Systems/NetworkConfiguratorSystem.cs +++ b/Content.Server/DeviceNetwork/Systems/NetworkConfiguratorSystem.cs @@ -67,10 +67,10 @@ public sealed class NetworkConfiguratorSystem : SharedNetworkConfiguratorSystem SubscribeLocalEvent(OnComponentRemoved); - SubscribeLocalEvent(OnMapSave); + SubscribeLocalEvent(OnMapSave); } - private void OnMapSave(BeforeSaveEvent ev) + private void OnMapSave(BeforeSerializationEvent ev) { var enumerator = AllEntityQuery(); while (enumerator.MoveNext(out var uid, out var conf)) diff --git a/Content.Server/Mapping/MappingSystem.cs b/Content.Server/Mapping/MappingSystem.cs index ecdbbe081c..1ef6944924 100644 --- a/Content.Server/Mapping/MappingSystem.cs +++ b/Content.Server/Mapping/MappingSystem.cs @@ -78,7 +78,7 @@ public sealed class MappingSystem : EntitySystem 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."); - _loader.SaveMap(map, path); + _loader.TrySaveMap(map, new ResPath(path)); } } diff --git a/Content.Server/Maps/ResaveCommand.cs b/Content.Server/Maps/ResaveCommand.cs index 0d48f946be..39507c850f 100644 --- a/Content.Server/Maps/ResaveCommand.cs +++ b/Content.Server/Maps/ResaveCommand.cs @@ -62,11 +62,11 @@ public sealed class ResaveCommand : LocalizedCommands if (_entManager.HasComponent(map)) { - loader.SaveMap(map.Comp.MapId, fn); + loader.TrySaveMap(map.Comp.MapId, fn); } else if (result.Grids.Count == 1) { - loader.SaveGrid(result.Grids.First(), fn); + loader.TrySaveGrid(result.Grids.First(), fn); } else { diff --git a/Content.Shared/Follower/FollowerSystem.cs b/Content.Shared/Follower/FollowerSystem.cs index 0c7bc99c46..ec13a4f7c2 100644 --- a/Content.Shared/Follower/FollowerSystem.cs +++ b/Content.Shared/Follower/FollowerSystem.cs @@ -42,7 +42,7 @@ public sealed class FollowerSystem : EntitySystem SubscribeLocalEvent(OnFollowedAttempt); SubscribeLocalEvent(OnGotEquippedHand); SubscribeLocalEvent(OnFollowedTerminating); - SubscribeLocalEvent(OnBeforeSave); + SubscribeLocalEvent(OnBeforeSave); } private void OnFollowedAttempt(Entity ent, ref ComponentGetStateAttemptEvent args) @@ -60,7 +60,7 @@ public sealed class FollowerSystem : EntitySystem } } - private void OnBeforeSave(BeforeSaveEvent ev) + private void OnBeforeSave(BeforeSerializationEvent ev) { // Some followers will not be map savable. This ensures that maps don't get saved with empty/invalid // followers, but just stopping any following on the map being saved. From b555a6049ee6faa15ca03759f16ab32b7eaff6a3 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Tue, 24 Dec 2024 21:00:53 +1300 Subject: [PATCH 012/529] Even more engine changes --- Content.IntegrationTests/Tests/PostMapInitTest.cs | 2 +- .../DeviceNetwork/Systems/DeviceListSystem.cs | 7 +++++-- .../Systems/NetworkConfiguratorSystem.cs | 8 ++++++-- Content.Server/Mapping/MappingManager.cs | 3 ++- Content.Server/Maps/ResaveCommand.cs | 2 +- Content.Server/Procedural/DungeonSystem.cs | 2 +- Content.Shared/Follower/FollowerSystem.cs | 13 ++++++++++--- 7 files changed, 26 insertions(+), 11 deletions(-) diff --git a/Content.IntegrationTests/Tests/PostMapInitTest.cs b/Content.IntegrationTests/Tests/PostMapInitTest.cs index 9e908371df..f07d1e4996 100644 --- a/Content.IntegrationTests/Tests/PostMapInitTest.cs +++ b/Content.IntegrationTests/Tests/PostMapInitTest.cs @@ -426,7 +426,7 @@ namespace Content.IntegrationTests.Tests { try { - Assert.That(mapLoader.TryLoadEntities(path, out maps, out _, opts)); + Assert.That(mapLoader.TryLoadGeneric(path, out maps, out _, opts)); } catch (Exception ex) { diff --git a/Content.Server/DeviceNetwork/Systems/DeviceListSystem.cs b/Content.Server/DeviceNetwork/Systems/DeviceListSystem.cs index 4defec0aab..1988a07cea 100644 --- a/Content.Server/DeviceNetwork/Systems/DeviceListSystem.cs +++ b/Content.Server/DeviceNetwork/Systems/DeviceListSystem.cs @@ -131,7 +131,7 @@ public sealed class DeviceListSystem : SharedDeviceListSystem var enumerator = AllEntityQuery(); 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); diff --git a/Content.Server/DeviceNetwork/Systems/NetworkConfiguratorSystem.cs b/Content.Server/DeviceNetwork/Systems/NetworkConfiguratorSystem.cs index 7e6452d955..645d28f6d2 100644 --- a/Content.Server/DeviceNetwork/Systems/NetworkConfiguratorSystem.cs +++ b/Content.Server/DeviceNetwork/Systems/NetworkConfiguratorSystem.cs @@ -75,7 +75,10 @@ public sealed class NetworkConfiguratorSystem : SharedNetworkConfiguratorSystem var enumerator = AllEntityQuery(); while (enumerator.MoveNext(out var uid, out var conf)) { - if (CompOrNull(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); diff --git a/Content.Server/Mapping/MappingManager.cs b/Content.Server/Mapping/MappingManager.cs index be6f503bf0..0097df2e55 100644 --- a/Content.Server/Mapping/MappingManager.cs +++ b/Content.Server/Mapping/MappingManager.cs @@ -54,7 +54,8 @@ public sealed class MappingManager : IPostInjectInit return; } - var data = _systems.GetEntitySystem().SerializeEntityRecursive(mapUid).Node; + var sys = _systems.GetEntitySystem(); + var data = sys.SerializeEntitiesRecursive([mapUid]).Node; var document = new YamlDocument(data.ToYaml()); var stream = new YamlStream { document }; var writer = new StringWriter(); diff --git a/Content.Server/Maps/ResaveCommand.cs b/Content.Server/Maps/ResaveCommand.cs index 39507c850f..c48c02c1f7 100644 --- a/Content.Server/Maps/ResaveCommand.cs +++ b/Content.Server/Maps/ResaveCommand.cs @@ -44,7 +44,7 @@ public sealed class ResaveCommand : LocalizedCommands var fn = files[i]; log.Info($"Re-saving file {i}/{files.Count} : {fn}"); - if (!loader.TryLoadEntities(fn, out var result, opts)) + if (!loader.TryLoadGeneric(fn, out var result, opts)) continue; if (result.Maps.Count != 1) diff --git a/Content.Server/Procedural/DungeonSystem.cs b/Content.Server/Procedural/DungeonSystem.cs index 75cdb69130..521ebed7ec 100644 --- a/Content.Server/Procedural/DungeonSystem.cs +++ b/Content.Server/Procedural/DungeonSystem.cs @@ -182,7 +182,7 @@ public sealed partial class DungeonSystem : SharedDungeonSystem ExpectedCategory = FileCategory.Map }; - if (!_loader.TryLoadEntities(proto.AtlasPath, out var res, opts) || !res.Maps.TryFirstOrNull(out var 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(map.Value.Owner); diff --git a/Content.Shared/Follower/FollowerSystem.cs b/Content.Shared/Follower/FollowerSystem.cs index ec13a4f7c2..feea40fb2f 100644 --- a/Content.Shared/Follower/FollowerSystem.cs +++ b/Content.Shared/Follower/FollowerSystem.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Numerics; using Content.Shared.Administration.Managers; using Content.Shared.Database; @@ -62,8 +63,14 @@ public sealed class FollowerSystem : EntitySystem private void OnBeforeSave(BeforeSerializationEvent ev) { - // Some followers will not be map savable. This ensures that maps don't get saved with empty/invalid - // followers, but just stopping any following on the map being saved. + // Some followers will not be map savable. This ensures that maps don't get saved with some entities that have + // empty/invalid followers, by just stopping any following happening on the map being saved. + // I hate this so much. + // TODO WeakEntityReference + // We need some way to store entity references in a way that doesn't imply that the entity still exists. + // Then we wouldn't have to deal with this shit. + + var maps = ev.Entities.Select(x => Transform(x).MapUid).ToHashSet(); var query = AllEntityQuery(); while (query.MoveNext(out var uid, out var follower, out var xform, out var meta)) @@ -71,7 +78,7 @@ public sealed class FollowerSystem : EntitySystem if (meta.EntityPrototype == null || meta.EntityPrototype.MapSavable) continue; - if (xform.MapUid != ev.Map) + if (!maps.Contains(xform.MapUid)) continue; StopFollowingEntity(uid, follower.Following); From ad317f79b184e5122d91c478cb2d1b23e5a15fdc Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Wed, 25 Dec 2024 16:26:38 +1300 Subject: [PATCH 013/529] Poke engine tests From 74deed794e196e4a0c609b4c512450755c01ba98 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Fri, 27 Dec 2024 15:47:32 +1300 Subject: [PATCH 014/529] Eng changes --- Content.Server/GameTicking/GameTicker.RoundFlow.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs index 37e7a4e1ce..40e3303c2d 100644 --- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs +++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs @@ -316,7 +316,6 @@ namespace Content.Server.GameTicking if (!_loader.TryMergeMap(targetMap, ev.GameMap.MapPath, - out var map, out var grids, ev.Options, ev.Offset, From f95fb450fd936a4d5e73a34f4fd641c9a4dc4fcc Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Thu, 2 Jan 2025 17:00:13 +1300 Subject: [PATCH 015/529] obsolete --- Content.Server/Commands/CommandUtils.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Content.Server/Commands/CommandUtils.cs b/Content.Server/Commands/CommandUtils.cs index 30ea41b7c9..fde52d18e1 100644 --- a/Content.Server/Commands/CommandUtils.cs +++ b/Content.Server/Commands/CommandUtils.cs @@ -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 { @@ -51,6 +52,7 @@ namespace Content.Server.Commands return true; } + [Obsolete($"Use toolshed's {nameof(EmplaceCommand)}")] public static string SubstituteEntityDetails(IConsoleShell shell, EntityUid ent, string ruleString) { var entMan = IoCManager.Resolve(); From 48c13b32f43be057775acb8a87aa950dbd92adf5 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Thu, 2 Jan 2025 17:00:45 +1300 Subject: [PATCH 016/529] Remove unused helper. Use EmplaceCommand instead --- Content.Server/Commands/CommandUtils.cs | 41 ------------------------- 1 file changed, 41 deletions(-) diff --git a/Content.Server/Commands/CommandUtils.cs b/Content.Server/Commands/CommandUtils.cs index fde52d18e1..502c3ae024 100644 --- a/Content.Server/Commands/CommandUtils.cs +++ b/Content.Server/Commands/CommandUtils.cs @@ -51,46 +51,5 @@ namespace Content.Server.Commands attachedEntity = session.AttachedEntity.Value; return true; } - - [Obsolete($"Use toolshed's {nameof(EmplaceCommand)}")] - public static string SubstituteEntityDetails(IConsoleShell shell, EntityUid ent, string ruleString) - { - var entMan = IoCManager.Resolve(); - var transform = entMan.GetComponent(ent); - var transformSystem = entMan.System(); - 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(ent).EntityName); - - if (shell.Player is { } player) - { - if (player.AttachedEntity is {Valid: true} p) - { - var pTransform = entMan.GetComponent(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; - } } } From e5468982f5806dbf0066be3dd671f5da949414b6 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Fri, 3 Jan 2025 18:45:02 +1300 Subject: [PATCH 017/529] Fix mark verb --- Content.Server/Administration/Systems/AdminVerbSystem.cs | 2 +- Content.Server/Administration/Toolshed/MarkedCommand.cs | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.cs b/Content.Server/Administration/Systems/AdminVerbSystem.cs index 0640537f57..43b6007583 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.cs @@ -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 {args.Target}, out _); mark.Impact = LogImpact.Low; args.Verbs.Add(mark); diff --git a/Content.Server/Administration/Toolshed/MarkedCommand.cs b/Content.Server/Administration/Toolshed/MarkedCommand.cs index 54d45a352d..c2b9ecf79a 100644 --- a/Content.Server/Administration/Toolshed/MarkedCommand.cs +++ b/Content.Server/Administration/Toolshed/MarkedCommand.cs @@ -9,8 +9,7 @@ public sealed class MarkedCommand : ToolshedCommand [CommandImplementation] public IEnumerable Marked(IInvocationContext ctx) { - var res = (IEnumerable?)ctx.ReadVar("marked"); - res ??= Array.Empty(); - return res; + var marked = ctx.ReadVar("marked") as IEnumerable; + return marked ?? Array.Empty(); } } From 0d8d50de921adc9d821ec89ceb7801505a3eb7de Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Sat, 18 Jan 2025 16:29:21 +1300 Subject: [PATCH 018/529] Fix elk and add new test --- .../Tests/PostMapInitTest.cs | 54 +++++++++++++++++-- .../Maps/Shuttles/emergency_elkridge.yml | 14 +---- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/Content.IntegrationTests/Tests/PostMapInitTest.cs b/Content.IntegrationTests/Tests/PostMapInitTest.cs index b47416ec82..fac5569970 100644 --- a/Content.IntegrationTests/Tests/PostMapInitTest.cs +++ b/Content.IntegrationTests/Tests/PostMapInitTest.cs @@ -38,10 +38,7 @@ namespace Content.IntegrationTests.Tests private static readonly string[] Grids = { - "/Maps/centcomm.yml", - "/Maps/Shuttles/cargo.yml", - "/Maps/Shuttles/emergency.yml", - "/Maps/Shuttles/infiltrator.yml", + "/Maps/centcomm.yml" }; private static readonly string[] GameMaps = @@ -105,6 +102,55 @@ namespace Content.IntegrationTests.Tests await pair.CleanReturnAsync(); } + /// + /// Asserts that shuttles are loadable and have been saved as grids and not maps. + /// + [Test] + public async Task ShuttlesLoadableTest() + { + await using var pair = await PoolManager.GetServerClient(); + var server = pair.Server; + + var entManager = server.ResolveDependency(); + var resMan = server.ResolveDependency(); + var mapLoader = entManager.System(); + var mapSystem = entManager.System(); + var cfg = server.ResolveDependency(); + 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(() => + { + 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); + + await pair.CleanReturnAsync(); + } + [Test] public async Task NoSavedPostMapInitTest() { diff --git a/Resources/Maps/Shuttles/emergency_elkridge.yml b/Resources/Maps/Shuttles/emergency_elkridge.yml index efeba6844b..4dafdee84a 100644 --- a/Resources/Maps/Shuttles/emergency_elkridge.yml +++ b/Resources/Maps/Shuttles/emergency_elkridge.yml @@ -18,7 +18,7 @@ entities: name: NT Evac Log - type: Transform pos: -0.42093527,-0.86894274 - parent: 637 + parent: invalid - type: MapGrid chunks: 0,0: @@ -804,18 +804,6 @@ entities: chunkSize: 4 - type: GasTileOverlay - type: RadiationGridResistance - - uid: 637 - components: - - type: MetaData - name: Map Entity - - type: Transform - - type: Map - mapPaused: True - - type: PhysicsMap - - type: GridTree - - type: MovedGrids - - type: Broadphase - - type: OccluderTree - proto: AirAlarm entities: - uid: 577 From b13ccfefd6fd42005928280249a81b4fde9b599a Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Sat, 18 Jan 2025 16:50:50 +1300 Subject: [PATCH 019/529] poke tests From 26fe0292c47b6ec712b046998b0263bae3f7f681 Mon Sep 17 00:00:00 2001 From: juliangiebel Date: Thu, 23 Jan 2025 01:16:07 +0100 Subject: [PATCH 020/529] Update da packages --- Directory.Packages.props | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index eee8c65f9a..69b5ad1945 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,14 +6,16 @@ --> - - + - - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + - + \ No newline at end of file From 02f7dad6f3ce7c4008e3c98d28718d9ed2cbcaf2 Mon Sep 17 00:00:00 2001 From: Killerqu00 Date: Tue, 28 Jan 2025 17:42:17 +0100 Subject: [PATCH 021/529] Thou shall not map items with "do not map" suffix --- .../Tests/PostMapInitTest.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Content.IntegrationTests/Tests/PostMapInitTest.cs b/Content.IntegrationTests/Tests/PostMapInitTest.cs index 9ebd32a40a..d1420bc492 100644 --- a/Content.IntegrationTests/Tests/PostMapInitTest.cs +++ b/Content.IntegrationTests/Tests/PostMapInitTest.cs @@ -42,6 +42,11 @@ namespace Content.IntegrationTests.Tests "/Maps/Shuttles/infiltrator.yml", }; + private static readonly string[] DoNotMapWhitelist = + { + "/Maps/centcomm.yml", + }; + private static readonly string[] GameMaps = { "Dev", @@ -120,6 +125,7 @@ namespace Content.IntegrationTests.Tests var server = pair.Server; var resourceManager = server.ResolveDependency(); + var protoManager = server.ResolveDependency(); var mapFolder = new ResPath("/Maps"); var maps = resourceManager .ContentFindFiles(mapFolder) @@ -151,6 +157,21 @@ namespace Content.IntegrationTests.Tests var postMapInit = meta["postmapinit"].AsBool(); Assert.That(postMapInit, Is.False, $"Map {map.Filename} was saved postmapinit"); + + // testing that maps have nothing with the "DO NOT MAP" suffix + // I do it here because it's basically copy-paste code for the most part + var yamlEntities = root["entities"]; + foreach (var yamlEntity in (YamlSequenceNode)yamlEntities) + { + var protoId = yamlEntity["proto"].AsString(); + protoManager.TryIndex(protoId, out var proto, false); + if (proto is null || proto.EditorSuffix is null) + continue; + if (proto.EditorSuffix.ToUpper().Contains("DO NOT MAP") && !DoNotMapWhitelist.Contains(map.ToString())) + { + Assert.Fail($"\nMap {map} has the DO NOT MAP prototype {proto}"); + } + } } await pair.CleanReturnAsync(); } From d15a770078af35858857078199042eeb84c835ae Mon Sep 17 00:00:00 2001 From: Killerqu00 Date: Wed, 29 Jan 2025 12:22:38 +0100 Subject: [PATCH 022/529] convert it to entity category --- .../Tests/PostMapInitTest.cs | 6 ++-- Resources/Locale/en-US/entity-categories.ftl | 3 ++ .../Catalog/Fills/Lockers/service.yml | 1 + .../Clothing/Head/hardsuit-helmets.yml | 1 + .../Entities/Clothing/Head/helmets.yml | 2 +- .../Entities/Clothing/Head/misc.yml | 5 +-- .../Clothing/OuterClothing/hardsuits.yml | 1 + .../Entities/Clothing/OuterClothing/suits.yml | 3 +- .../Entities/Mobs/Player/silicon.yml | 9 ++--- .../Devices/Circuitboards/Machine/cannons.yml | 5 +++ .../Entities/Objects/Fun/bike_horn.yml | 2 +- .../Prototypes/Entities/Objects/Fun/toys.yml | 2 +- .../Entities/Objects/Misc/paper.yml | 4 +-- .../Entities/Objects/Misc/rubber_stamp.yml | 36 +++++++++---------- .../Medical/handheld_crew_monitor.yml | 2 +- .../Objects/Specific/Medical/healing.yml | 2 +- .../Objects/Specific/Research/disk.yml | 1 + .../Objects/Weapons/Melee/baseball_bat.yml | 2 +- .../Objects/Weapons/Melee/weapon_toolbox.yml | 2 +- .../Machines/Computers/computers.yml | 1 + .../Entities/Structures/Machines/holopad.yml | 3 +- .../Entities/Structures/Shuttles/cannons.yml | 10 +++--- .../Structures/Storage/Closets/closets.yml | 2 +- Resources/Prototypes/Entities/categories.yml | 5 +++ 24 files changed, 64 insertions(+), 46 deletions(-) diff --git a/Content.IntegrationTests/Tests/PostMapInitTest.cs b/Content.IntegrationTests/Tests/PostMapInitTest.cs index d1420bc492..906b96ebf0 100644 --- a/Content.IntegrationTests/Tests/PostMapInitTest.cs +++ b/Content.IntegrationTests/Tests/PostMapInitTest.cs @@ -161,15 +161,17 @@ namespace Content.IntegrationTests.Tests // testing that maps have nothing with the "DO NOT MAP" suffix // I do it here because it's basically copy-paste code for the most part var yamlEntities = root["entities"]; + if (!protoManager.TryIndex("DoNotMap", out var dnmCategory)) + return; foreach (var yamlEntity in (YamlSequenceNode)yamlEntities) { var protoId = yamlEntity["proto"].AsString(); protoManager.TryIndex(protoId, out var proto, false); if (proto is null || proto.EditorSuffix is null) continue; - if (proto.EditorSuffix.ToUpper().Contains("DO NOT MAP") && !DoNotMapWhitelist.Contains(map.ToString())) + if (proto.Categories.Contains(dnmCategory) && !DoNotMapWhitelist.Contains(map.ToString())) { - Assert.Fail($"\nMap {map} has the DO NOT MAP prototype {proto}"); + Assert.Fail($"\nMap {map} has the DO NOT MAP prototype {proto.Name}"); } } } diff --git a/Resources/Locale/en-US/entity-categories.ftl b/Resources/Locale/en-US/entity-categories.ftl index 4b6cf87942..a5ed66dd01 100644 --- a/Resources/Locale/en-US/entity-categories.ftl +++ b/Resources/Locale/en-US/entity-categories.ftl @@ -3,3 +3,6 @@ entity-category-name-game-rules = Game Rules entity-category-name-objectives = Objectives entity-category-name-roles = Mind Roles entity-category-name-mapping = Mapping +entity-category-name-donotmap = Do not map + +entity-category-suffix-donotmap = DO NOT MAP diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/service.yml b/Resources/Prototypes/Catalog/Fills/Lockers/service.yml index 2d564e0eb0..55b2165dff 100644 --- a/Resources/Prototypes/Catalog/Fills/Lockers/service.yml +++ b/Resources/Prototypes/Catalog/Fills/Lockers/service.yml @@ -133,6 +133,7 @@ id: ClosetJanitorBombFilled parent: ClosetJanitorBomb suffix: DO NOT MAP, Filled + categories: [ DoNotMap ] components: - type: StorageFill contents: diff --git a/Resources/Prototypes/Entities/Clothing/Head/hardsuit-helmets.yml b/Resources/Prototypes/Entities/Clothing/Head/hardsuit-helmets.yml index b97df840a7..0c2ffa423d 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/hardsuit-helmets.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/hardsuit-helmets.yml @@ -330,6 +330,7 @@ id: ClothingHeadHelmetHardsuitLuxury #DO NOT MAP - https://github.com/space-wizards/space-station-14/pull/19738#issuecomment-1703486738 name: luxury mining hardsuit helmet description: A refurbished mining hardsuit helmet, fitted with satin cushioning and an extra (non-functioning) antenna, because you're that extra. + categories: [ DoNotMap ] components: - type: Sprite sprite: Clothing/Head/Hardsuits/luxury.rsi diff --git a/Resources/Prototypes/Entities/Clothing/Head/helmets.yml b/Resources/Prototypes/Entities/Clothing/Head/helmets.yml index 65ad2d523e..5fc6dd5a56 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/helmets.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/helmets.yml @@ -137,7 +137,7 @@ id: ClothingHeadHelmetJanitorBombSuit name: janitorial bombsuit helmet description: A heavy helmet designed to withstand explosions formed from reactions between chemicals. - suffix: DO NOT MAP + categories: [ DoNotMap ] components: - type: Sprite sprite: Clothing/Head/Helmets/janitor_bombsuit.rsi diff --git a/Resources/Prototypes/Entities/Clothing/Head/misc.yml b/Resources/Prototypes/Entities/Clothing/Head/misc.yml index 254eaf37c8..28fe46e0fa 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/misc.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/misc.yml @@ -176,7 +176,7 @@ id: ClothingHeadHatCatEars name: cat ears description: "NYAH!" - suffix: DO NOT MAP + categories: [ DoNotMap ] components: - type: Tag tags: [] # ignore "WhitelistChameleon" tag @@ -191,6 +191,7 @@ parent: [ClothingHeadHatCatEars, BaseToggleClothing] id: ClothingHeadHatCatEarsValid suffix: Valid, DO NOT MAP + categories: [ DoNotMap ] components: - type: ToggleClothing action: ActionBecomeValid @@ -222,7 +223,7 @@ id: ClothingHeadHatDogEars name: doggy ears description: Only for good boys. - suffix: DO NOT MAP + categories: [ DoNotMap ] components: - type: Sprite sprite: Clothing/Head/Hats/dogears.rsi diff --git a/Resources/Prototypes/Entities/Clothing/OuterClothing/hardsuits.yml b/Resources/Prototypes/Entities/Clothing/OuterClothing/hardsuits.yml index 0e84fca06d..dd51be6be9 100644 --- a/Resources/Prototypes/Entities/Clothing/OuterClothing/hardsuits.yml +++ b/Resources/Prototypes/Entities/Clothing/OuterClothing/hardsuits.yml @@ -447,6 +447,7 @@ id: ClothingOuterHardsuitLuxury #DO NOT MAP - https://github.com/space-wizards/space-station-14/pull/19738#issuecomment-1703486738 name: luxury mining hardsuit description: A refurbished mining hardsuit, fashioned after the Quartermaster's colors. Graphene lining provides less protection, but is much easier to move. + categories: [ DoNotMap ] components: - type: Sprite sprite: Clothing/OuterClothing/Hardsuits/luxury.rsi diff --git a/Resources/Prototypes/Entities/Clothing/OuterClothing/suits.yml b/Resources/Prototypes/Entities/Clothing/OuterClothing/suits.yml index f78694a0fa..e6550dc01b 100644 --- a/Resources/Prototypes/Entities/Clothing/OuterClothing/suits.yml +++ b/Resources/Prototypes/Entities/Clothing/OuterClothing/suits.yml @@ -33,7 +33,7 @@ id: ClothingOuterSuitJanitorBomb name: janitorial bomb suit description: A heavy helmet designed to withstand explosions formed from reactions between chemicals. - suffix: DO NOT MAP + categories: [ DoNotMap ] components: - type: Sprite sprite: Clothing/OuterClothing/Suits/janitor_bombsuit.rsi @@ -289,6 +289,7 @@ parent: ClothingOuterSuitCarp id: ClothingOuterHardsuitCarp suffix: Hardsuit, DO NOT MAP + categories: [ DoNotMap ] components: - type: PressureProtection highPressureMultiplier: 0.6 diff --git a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml index 9bb793e47a..5a32e57c42 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml @@ -406,8 +406,7 @@ - type: entity id: StationAiBrain parent: PositronicBrain - categories: [ HideSpawnMenu ] - suffix: DO NOT MAP + categories: [ HideSpawnMenu, DoNotMap ] components: - type: Sprite # Once it's in a core it's pretty much an abstract entity at that point. @@ -454,8 +453,7 @@ id: StationAiHolo name: AI eye description: The AI's viewer. - categories: [ HideSpawnMenu ] - suffix: DO NOT MAP + categories: [ HideSpawnMenu, DoNotMap ] components: - type: NoFTL - type: WarpPoint @@ -476,8 +474,7 @@ id: StationAiHoloLocal name: AI hologram description: A holographic representation of an AI. - categories: [ HideSpawnMenu ] - suffix: DO NOT MAP + categories: [ HideSpawnMenu, DoNotMap ] components: - type: Transform anchored: true diff --git a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/cannons.yml b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/cannons.yml index 4b7a0ee55a..6c6190c01a 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/cannons.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/cannons.yml @@ -6,6 +6,7 @@ name: LSE-400c "Svalinn machine gun" machine board description: A machine printed circuit board for an LSE-400c "Svalinn machine gun". suffix: DO NOT MAP, Machine Board + categories: [ DoNotMap ] components: - type: Sprite state: security @@ -23,6 +24,7 @@ name: LSE-1200c "Perforator" machine board description: A machine printed circuit board for an LSE-1200c "Perforator". suffix: DO NOT MAP, Machine Board + categories: [ DoNotMap ] components: - type: Sprite state: security @@ -40,6 +42,7 @@ name: EXP-320g "Friendship" machine board description: A machine printed circuit board for an EXP-320g "Friendship". suffix: DO NOT MAP, Machine Board + categories: [ DoNotMap ] components: - type: Sprite state: security @@ -57,6 +60,7 @@ name: EXP-2100g "Duster" machine board description: A machine printed circuit board for an EXP-2100g "Duster". suffix: DO NOT MAP, Machine Board + categories: [ DoNotMap ] components: - type: Sprite state: security @@ -75,6 +79,7 @@ name: PTK-800 "Matter Dematerializer" machine board description: A machine printed circuit board for an PTK-800 "Matter Dematerializer". suffix: DO NOT MAP, Machine Board + categories: [ DoNotMap ] components: - type: Sprite state: security diff --git a/Resources/Prototypes/Entities/Objects/Fun/bike_horn.yml b/Resources/Prototypes/Entities/Objects/Fun/bike_horn.yml index 7c69aa0901..3e05c0e8ff 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/bike_horn.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/bike_horn.yml @@ -91,7 +91,7 @@ parent: BikeHorn id: GoldenBikeHorn name: golden honker - suffix: No mapping + categories: [ DoNotMap ] description: A happy honk prize, pray to the gods for your reward. components: - type: Sprite diff --git a/Resources/Prototypes/Entities/Objects/Fun/toys.yml b/Resources/Prototypes/Entities/Objects/Fun/toys.yml index c5e17c54d3..818cad7774 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/toys.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/toys.yml @@ -131,7 +131,7 @@ parent: PlushieGhost id: PlushieGhostRevenant name: revenant soft toy - suffix: DO NOT MAP + categories: [ DoNotMap ] description: So soft it almost makes you want to take a nap... components: - type: Item diff --git a/Resources/Prototypes/Entities/Objects/Misc/paper.yml b/Resources/Prototypes/Entities/Objects/Misc/paper.yml index e0862b8842..a1b287b8a7 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/paper.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/paper.yml @@ -447,7 +447,7 @@ id: BoxFolderCentCom name: CentComm folder parent: BoxFolderBase - suffix: DO NOT MAP + categories: [ DoNotMap ] description: CentComm's miserable little pile of secrets! components: - type: Sprite @@ -609,7 +609,7 @@ id: TraitorCodePaper name: syndicate codeword description: A leaked codeword to possibly get in touch with the Syndicate. - suffix: DO NOT MAP + categories: [ DoNotMap ] components: - type: TraitorCodePaper diff --git a/Resources/Prototypes/Entities/Objects/Misc/rubber_stamp.yml b/Resources/Prototypes/Entities/Objects/Misc/rubber_stamp.yml index 8707e6fca8..df038d3e2e 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/rubber_stamp.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/rubber_stamp.yml @@ -41,7 +41,7 @@ name: captain's rubber stamp parent: [RubberStampBase, BaseCommandContraband] id: RubberStampCaptain - suffix: DO NOT MAP + categories: [ DoNotMap ] components: - type: Stamp stampedName: stamp-component-stamped-name-captain @@ -54,7 +54,7 @@ name: CentComm rubber stamp parent: RubberStampBase id: RubberStampCentcom - suffix: DO NOT MAP + categories: [ DoNotMap ] components: - type: Stamp stampedName: stamp-component-stamped-name-centcom @@ -67,7 +67,7 @@ name: chaplain's rubber stamp parent: RubberStampBase id: RubberStampChaplain - suffix: DO NOT MAP + categories: [ DoNotMap ] components: - type: Stamp stampedName: stamp-component-stamped-name-chaplain @@ -80,7 +80,7 @@ name: lawyer's rubber stamp parent: RubberStampBase id: RubberStampLawyer - suffix: DO NOT MAP + categories: [ DoNotMap ] components: - type: Stamp stampedName: stamp-component-stamped-name-lawyer @@ -93,7 +93,7 @@ name: clown's rubber stamp parent: RubberStampBase id: RubberStampClown - suffix: DO NOT MAP + categories: [ DoNotMap ] components: - type: Stamp stampedName: stamp-component-stamped-name-clown @@ -109,7 +109,7 @@ name: chief engineer's rubber stamp parent: [RubberStampBase, BaseCommandContraband] id: RubberStampCE - suffix: DO NOT MAP + categories: [ DoNotMap ] components: - type: Stamp stampedName: stamp-component-stamped-name-ce @@ -122,7 +122,7 @@ name: chief medical officer's rubber stamp parent: [RubberStampBase, BaseCommandContraband] id: RubberStampCMO - suffix: DO NOT MAP + categories: [ DoNotMap ] components: - type: Stamp stampedName: stamp-component-stamped-name-cmo @@ -135,7 +135,7 @@ name: head of personnel's rubber stamp parent: [RubberStampBase, BaseCommandContraband] id: RubberStampHop - suffix: DO NOT MAP + categories: [ DoNotMap ] components: - type: Stamp stampedName: stamp-component-stamped-name-hop @@ -148,7 +148,7 @@ name: head of security's rubber stamp parent: [RubberStampBase, BaseCommandContraband] id: RubberStampHos - suffix: DO NOT MAP + categories: [ DoNotMap ] components: - type: Stamp stampedName: stamp-component-stamped-name-hos @@ -161,7 +161,7 @@ name: mime's rubber stamp parent: RubberStampBase id: RubberStampMime - suffix: DO NOT MAP + categories: [ DoNotMap ] components: - type: Stamp stampedName: stamp-component-stamped-name-mime @@ -175,7 +175,7 @@ name: quartermaster's rubber stamp parent: [RubberStampBase, BaseCommandContraband] id: RubberStampQm - suffix: DO NOT MAP + categories: [ DoNotMap ] components: - type: Stamp stampedName: stamp-component-stamped-name-qm @@ -188,7 +188,7 @@ name: research director's rubber stamp parent: [RubberStampBase, BaseCommandContraband] id: RubberStampRd - suffix: DO NOT MAP + categories: [ DoNotMap ] components: - type: Stamp stampedName: stamp-component-stamped-name-rd @@ -213,7 +213,7 @@ name: syndicate rubber stamp parent: [RubberStampBase, BaseSyndicateContraband] id: RubberStampSyndicate - suffix: DO NOT MAP + categories: [ DoNotMap ] components: - type: Stamp stampedName: stamp-component-stamped-name-syndicate @@ -226,7 +226,7 @@ name: warden's rubber stamp parent: [RubberStampBase, BaseRestrictedContraband] id: RubberStampWarden - suffix: DO NOT MAP + categories: [ DoNotMap ] components: - type: Stamp stampedName: stamp-component-stamped-name-warden @@ -263,7 +263,7 @@ name: detective's rubber stamp parent: [RubberStampBase, BaseRestrictedContraband] id: RubberStampDetective - suffix: DO NOT MAP + categories: [ DoNotMap ] components: - type: Stamp stampedName: stamp-component-stamped-name-detective @@ -289,12 +289,12 @@ name: psychologist's rubber stamp parent: RubberStampBase id: RubberStampPsychologist - suffix: DO NOT MAP + categories: [ DoNotMap ] description: A rubber stamp for stamping important documents. Prescribe those treatments! components: - type: Stamp - stampedName: stamp-component-stamped-name-psychologist + stampedName: stamp-component-stamped-name-psychologist stampedColor: "#5B97BC" stampState: "paper_stamp-psychologist" - type: Sprite - state: stamp-psychologist \ No newline at end of file + state: stamp-psychologist diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/handheld_crew_monitor.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/handheld_crew_monitor.yml index d7a362d725..fc252fd9f5 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Medical/handheld_crew_monitor.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/handheld_crew_monitor.yml @@ -1,6 +1,6 @@ - type: entity name: handheld crew monitor - suffix: DO NOT MAP + categories: [ DoNotMap ] parent: [ BaseHandheldComputer, BaseGrandTheftContraband ] # CMO-only bud, don't add more. id: HandheldCrewMonitor diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml index 46410a0164..a69d2a1587 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/healing.yml @@ -324,7 +324,7 @@ id: HealingToolbox name: healing toolbox description: A powerful toolbox imbued with robust energy. It can heal your wounds and fill you with murderous intent. - suffix: DO NOT MAP + categories: [ DoNotMap ] components: - type: Sprite sprite: Objects/Specific/Medical/healing_toolbox.rsi diff --git a/Resources/Prototypes/Entities/Objects/Specific/Research/disk.yml b/Resources/Prototypes/Entities/Objects/Specific/Research/disk.yml index 0cb605cee6..dd556d75f2 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Research/disk.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Research/disk.yml @@ -35,6 +35,7 @@ id: ResearchDiskDebug name: research point disk suffix: DEBUG, DO NOT MAP + categories: [ DoNotMap ] description: A disk for the R&D server containing all the points you could ever need. components: - type: ResearchDisk diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/baseball_bat.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/baseball_bat.yml index 551fedfd90..7aee3e5876 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/baseball_bat.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/baseball_bat.yml @@ -63,7 +63,7 @@ parent: BaseBallBat id: WeaponMeleeKnockbackStick description: And then he spleefed all over. - suffix: Do not map + categories: [ DoNotMap ] components: - type: MeleeThrowOnHit - type: MeleeWeapon diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/weapon_toolbox.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/weapon_toolbox.yml index 240a17a0a4..003967d33d 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/weapon_toolbox.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/weapon_toolbox.yml @@ -3,7 +3,7 @@ id: WeaponMeleeToolboxRobust name: robust toolbox description: A tider's weapon. - suffix: DO NOT MAP + categories: [ DoNotMap ] components: - type: Sprite sprite: Objects/Tools/Toolboxes/toolbox_red.rsi diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml index 1180b37096..fd8090a369 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml @@ -1192,6 +1192,7 @@ # Putting this as "DO NOT MAP" until the performance issues are fixed. # And it's more fleshed out. suffix: TESTING, DO NOT MAP + categories: [ DoNotMap ] components: - type: Sprite layers: diff --git a/Resources/Prototypes/Entities/Structures/Machines/holopad.yml b/Resources/Prototypes/Entities/Structures/Machines/holopad.yml index c95609c55a..de7cfe3e27 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/holopad.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/holopad.yml @@ -150,8 +150,7 @@ - type: entity id: HolopadHologram name: hologram - categories: [ HideSpawnMenu ] - suffix: DO NOT MAP + categories: [ HideSpawnMenu, DoNotMap ] components: - type: Transform anchored: true diff --git a/Resources/Prototypes/Entities/Structures/Shuttles/cannons.yml b/Resources/Prototypes/Entities/Structures/Shuttles/cannons.yml index 47ac2ae7c9..030be38a91 100644 --- a/Resources/Prototypes/Entities/Structures/Shuttles/cannons.yml +++ b/Resources/Prototypes/Entities/Structures/Shuttles/cannons.yml @@ -61,7 +61,7 @@ parent: [ ShuttleGunBase, ConstructibleMachine] name: LSE-400c "Svalinn machine gun" description: Basic stationary laser unit. Effective against live targets and electronics. Uses regular power cells to fire, and has an extremely high rate of fire. - suffix: DO NOT MAP + categories: [ DoNotMap ] components: - type: Sprite sprite: Objects/Weapons/Guns/Shuttles/laser.rsi @@ -115,7 +115,7 @@ parent: [ ShuttleGunBase, ConstructibleMachine] name: LSE-1200c "Perforator" description: Advanced stationary laser unit. Annihilates electronics and is extremely dangerous to health! Uses the power cage to fire. - suffix: DO NOT MAP + categories: [ DoNotMap ] components: - type: Sprite sprite: Objects/Weapons/Guns/Shuttles/laser.rsi @@ -171,7 +171,7 @@ parent: [ShuttleGunBase, ConstructibleMachine] name: EXP-320g "Friendship" description: A small stationary grenade launcher that holds 2 grenades. - suffix: DO NOT MAP + categories: [ DoNotMap ] components: - type: Sprite sprite: Objects/Weapons/Guns/Shuttles/launcher.rsi @@ -225,7 +225,7 @@ parent: [ShuttleGunBase, ConstructibleMachine] name: EXP-2100g "Duster" description: A powerful stationary grenade launcher. A cartridge is required for use. - suffix: DO NOT MAP + categories: [ DoNotMap ] components: - type: Sprite sprite: Objects/Weapons/Guns/Shuttles/launcher.rsi @@ -323,7 +323,7 @@ parent: [ ShuttleGunBase, ConstructibleMachine] name: PTK-800 "Matter Dematerializer" description: Salvage stationary mining turret. Gradually accumulates charges on its own, extremely effective for asteroid excavation. - suffix: DO NOT MAP + categories: [ DoNotMap ] components: - type: Sprite sprite: Objects/Weapons/Guns/Shuttles/kinetic.rsi diff --git a/Resources/Prototypes/Entities/Structures/Storage/Closets/closets.yml b/Resources/Prototypes/Entities/Structures/Storage/Closets/closets.yml index 8c8c493aa3..778c80e6fb 100644 --- a/Resources/Prototypes/Entities/Structures/Storage/Closets/closets.yml +++ b/Resources/Prototypes/Entities/Structures/Storage/Closets/closets.yml @@ -82,7 +82,7 @@ name: janitorial bomb suit closet parent: ClosetSteelBase description: It's a storage unit for janitorial explosion-protective suits. - suffix: DO NOT MAP + categories: [ DoNotMap ] components: - type: Appearance - type: EntityStorageVisuals diff --git a/Resources/Prototypes/Entities/categories.yml b/Resources/Prototypes/Entities/categories.yml index dffc6b6aaf..5b8e794309 100644 --- a/Resources/Prototypes/Entities/categories.yml +++ b/Resources/Prototypes/Entities/categories.yml @@ -22,3 +22,8 @@ - type: entityCategory id: Mapping name: entity-category-name-mapping + +- type: entityCategory + id: DoNotMap + name: entity-category-name-donotmap + suffix: entity-category-suffix-donotmap From 32f7ea6c29228a65a07341537c4ec7df3eeb299a Mon Sep 17 00:00:00 2001 From: Killerqu00 Date: Wed, 29 Jan 2025 13:24:14 +0100 Subject: [PATCH 023/529] slight text adjustment --- Content.IntegrationTests/Tests/PostMapInitTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Content.IntegrationTests/Tests/PostMapInitTest.cs b/Content.IntegrationTests/Tests/PostMapInitTest.cs index 906b96ebf0..e18271af53 100644 --- a/Content.IntegrationTests/Tests/PostMapInitTest.cs +++ b/Content.IntegrationTests/Tests/PostMapInitTest.cs @@ -158,7 +158,7 @@ namespace Content.IntegrationTests.Tests Assert.That(postMapInit, Is.False, $"Map {map.Filename} was saved postmapinit"); - // testing that maps have nothing with the "DO NOT MAP" suffix + // testing that maps have nothing with the DoNotMap entity category // I do it here because it's basically copy-paste code for the most part var yamlEntities = root["entities"]; if (!protoManager.TryIndex("DoNotMap", out var dnmCategory)) @@ -171,7 +171,7 @@ namespace Content.IntegrationTests.Tests continue; if (proto.Categories.Contains(dnmCategory) && !DoNotMapWhitelist.Contains(map.ToString())) { - Assert.Fail($"\nMap {map} has the DO NOT MAP prototype {proto.Name}"); + Assert.Fail($"\nMap {map} has the DO NOT MAP category in prototype {proto.Name}"); } } } From 7056c6051ba59fe4ffa57433fd8b6fae8c29308f Mon Sep 17 00:00:00 2001 From: MossyGreySlope Date: Fri, 14 Feb 2025 09:31:38 +1300 Subject: [PATCH 024/529] Fix dev server crashes caused by putting pills in drinks, depositing spesos, making burgers (#33431) * combine TrySpike into OnInteractUsing * mark spike drink event as handled * mark speso insertion event as handled * mark food sequence event as handled --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> --- Content.Server/Nutrition/EntitySystems/FoodSequenceSystem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Server/Nutrition/EntitySystems/FoodSequenceSystem.cs b/Content.Server/Nutrition/EntitySystems/FoodSequenceSystem.cs index ae8215ac6a..d5b4a0e2b3 100644 --- a/Content.Server/Nutrition/EntitySystems/FoodSequenceSystem.cs +++ b/Content.Server/Nutrition/EntitySystems/FoodSequenceSystem.cs @@ -39,7 +39,7 @@ public sealed class FoodSequenceSystem : SharedFoodSequenceSystem private void OnInteractUsing(Entity ent, ref InteractUsingEvent args) { if (TryComp(args.Used, out var sequenceElement)) - TryAddFoodElement(ent, (args.Used, sequenceElement), args.User); + args.Handled = TryAddFoodElement(ent, (args.Used, sequenceElement), args.User); } private void OnIngredientAdded(Entity ent, ref FoodSequenceIngredientAddedEvent args) From 562a41c00e43cd30c46dddf805bc4e4268f086a5 Mon Sep 17 00:00:00 2001 From: nikthechampiongr <32041239+nikthechampiongr@users.noreply.github.com> Date: Thu, 13 Feb 2025 14:03:55 -0800 Subject: [PATCH 025/529] Vote kicks now ban the target's ip (#35131) * Make vote kicks ban the target's ip address * Make it stop crashing my game --- .../Voting/Managers/VoteManager.DefaultVotes.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs b/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs index 187295a073..89f4acdef1 100644 --- a/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs +++ b/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs @@ -1,4 +1,6 @@ using System.Linq; +using System.Net; +using System.Net.Sockets; using Content.Server.Administration; using Content.Server.Administration.Managers; using Content.Server.Discord.WebhookMessages; @@ -48,6 +50,8 @@ namespace Content.Server.Voting.Managers else _adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Initiated a {voteType.ToString()} vote"); + _gameTicker = _entityManager.EntitySysManager.GetEntitySystem(); + bool timeoutVote = true; switch (voteType) @@ -68,7 +72,6 @@ namespace Content.Server.Voting.Managers default: throw new ArgumentOutOfRangeException(nameof(voteType), voteType, null); } - _gameTicker = _entityManager.EntitySysManager.GetEntitySystem(); _gameTicker.UpdateInfoText(); if (timeoutVote) TimeoutStandardVote(voteType); @@ -368,6 +371,15 @@ namespace Content.Server.Voting.Managers } var targetUid = located.UserId; var targetHWid = located.LastHWId; + (IPAddress, int)? targetIP = null; + + if (located.LastAddress is not null) + { + targetIP = located.LastAddress.AddressFamily is AddressFamily.InterNetwork + ? (located.LastAddress, 32) // People with ipv4 addresses get a /32 address so we ban that + : (located.LastAddress, 64); // This can only be an ipv6 address. People with ipv6 address should get /64 addresses so we ban that. + } + if (!_playerManager.TryGetSessionById(located.UserId, out ICommonSession? targetSession)) { _logManager.GetSawmill("admin.votekick") @@ -532,7 +544,7 @@ namespace Content.Server.Voting.Managers uint minutes = (uint)_cfg.GetCVar(CCVars.VotekickBanDuration); - _bans.CreateServerBan(targetUid, target, null, null, targetHWid, minutes, severity, reason); + _bans.CreateServerBan(targetUid, target, null, targetIP, targetHWid, minutes, severity, reason); } } else From dcfcd8916c248d29361c67f67f48108e1c957d43 Mon Sep 17 00:00:00 2001 From: PJBot Date: Thu, 13 Feb 2025 22:05:03 +0000 Subject: [PATCH 026/529] Automatic changelog update --- Resources/Changelog/Admin.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Resources/Changelog/Admin.yml b/Resources/Changelog/Admin.yml index 6b380572b0..180d6c03ae 100644 --- a/Resources/Changelog/Admin.yml +++ b/Resources/Changelog/Admin.yml @@ -752,5 +752,12 @@ Entries: id: 93 time: '2025-02-06T05:10:21.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/33426 +- author: nikthechampiongr + changes: + - message: Vote kicks now also ban the ip of the person that gets votekicked. + type: Tweak + id: 94 + time: '2025-02-13T22:03:55.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/35131 Name: Admin Order: 1 From 12105234fec23ca5c0bb3ca6263bc7cd3ef2c504 Mon Sep 17 00:00:00 2001 From: Centronias Date: Thu, 13 Feb 2025 14:35:59 -0800 Subject: [PATCH 027/529] High Heel Boots do the Clicky Clacky (#35083) * click clack * fix quote marks Co-authored-by: Hannah Giovanna Dawson --------- Co-authored-by: Errant <35878406+Errant-4@users.noreply.github.com> Co-authored-by: Hannah Giovanna Dawson --- .../Audio/Effects/Footsteps/attributions.yml | 14 ++++++++++++-- .../Audio/Effects/Footsteps/heelsclack1.ogg | Bin 0 -> 9232 bytes .../Audio/Effects/Footsteps/heelsclack2.ogg | Bin 0 -> 8247 bytes .../Audio/Effects/Footsteps/heelsclack3.ogg | Bin 0 -> 8410 bytes .../Audio/Effects/Footsteps/heelsclack4.ogg | Bin 0 -> 8530 bytes .../Audio/Effects/Footsteps/heelsclack5.ogg | Bin 0 -> 8998 bytes .../Prototypes/Entities/Clothing/Shoes/boots.yml | 5 +++++ .../Prototypes/SoundCollections/footsteps.yml | 13 +++++++++++-- 8 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 Resources/Audio/Effects/Footsteps/heelsclack1.ogg create mode 100644 Resources/Audio/Effects/Footsteps/heelsclack2.ogg create mode 100644 Resources/Audio/Effects/Footsteps/heelsclack3.ogg create mode 100644 Resources/Audio/Effects/Footsteps/heelsclack4.ogg create mode 100644 Resources/Audio/Effects/Footsteps/heelsclack5.ogg diff --git a/Resources/Audio/Effects/Footsteps/attributions.yml b/Resources/Audio/Effects/Footsteps/attributions.yml index 7a56beec38..7af75169b2 100644 --- a/Resources/Audio/Effects/Footsteps/attributions.yml +++ b/Resources/Audio/Effects/Footsteps/attributions.yml @@ -12,7 +12,7 @@ license: "CC-BY-SA-4.0" copyright: "Made by JustInvoke freesound.org" source: "https://freesound.org/people/JustInvoke/sounds/446100/" - + - files: - jesterstep1.ogg - jesterstep2.ogg @@ -63,7 +63,7 @@ license: "CC-BY-SA-3.0" copyright: "Taken from https://github.com/tgstation/tgstation" source: "https://github.com/tgstation/tgstation/blob/34d5ab2e46e3fb4dd9d7475f587d33441df9651c/sound/effects" - + - files: - spurs1.ogg - spurs2.ogg @@ -78,3 +78,13 @@ license: "CC-BY-SA-4.0" copyright: "Taken from IENBA freesound.org and modified by https://github.com/MilenVolf. borgwalk2 clipped my metalgearsloth." source: "https://freesound.org/people/IENBA/sounds/697379/" + +- files: + - heelsclack1.ogg + - heelsclack2.ogg + - heelsclack3.ogg + - heelsclack4.ogg + - heelsclack5.ogg + license: "CC0-1.0" + copyright: "Taken from NachtmahrTV on freesound.org, clipped by Centronias" + source: "https://freesound.org/people/NachtmahrTV/sounds/571801/" diff --git a/Resources/Audio/Effects/Footsteps/heelsclack1.ogg b/Resources/Audio/Effects/Footsteps/heelsclack1.ogg new file mode 100644 index 0000000000000000000000000000000000000000..903f4c19ea34ae237303b30e0d5e8f0d7dcf6cde GIT binary patch literal 9232 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 JmRXyh`9F<{D53xW literal 0 HcmV?d00001 diff --git a/Resources/Audio/Effects/Footsteps/heelsclack2.ogg b/Resources/Audio/Effects/Footsteps/heelsclack2.ogg new file mode 100644 index 0000000000000000000000000000000000000000..c81c36100f54a3ee5975a2a7be1b1d2754fcd06c GIT binary patch literal 8247 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 literal 0 HcmV?d00001 diff --git a/Resources/Audio/Effects/Footsteps/heelsclack5.ogg b/Resources/Audio/Effects/Footsteps/heelsclack5.ogg new file mode 100644 index 0000000000000000000000000000000000000000..6b9f0763fbb59dbcaaffe5a187ad7fb8fda79a12 GIT binary patch literal 8998 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 b?#Y$p+ot&exlca$ Date: Thu, 13 Feb 2025 22:37:06 +0000 Subject: [PATCH 028/529] Automatic changelog update --- Resources/Changelog/Changelog.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 021b37f5b5..f99dca0583 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: ElectroJr - changes: - - message: Fixed a store currency duplication bug/exploit. - type: Fix - id: 7453 - time: '2024-09-29T12:13:22.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/32524 - author: ElectroJr changes: - message: There is now a rate limit for most interactions. It should not be noticeable @@ -3887,3 +3880,10 @@ id: 7952 time: '2025-02-13T20:12:57.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/35135 +- author: Centronias + changes: + - message: High-heeled Boots now click and clack when walking + type: Tweak + id: 7953 + time: '2025-02-13T22:35:59.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/35083 From 3b142890cd904456376c84f7fb87ba16d7648f03 Mon Sep 17 00:00:00 2001 From: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Date: Fri, 14 Feb 2025 00:44:27 +0100 Subject: [PATCH 029/529] minor thruster system cleanup (#35143) --- Content.Server/Shuttles/Systems/ThrusterSystem.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Content.Server/Shuttles/Systems/ThrusterSystem.cs b/Content.Server/Shuttles/Systems/ThrusterSystem.cs index a75ddc911a..74008a6af7 100644 --- a/Content.Server/Shuttles/Systems/ThrusterSystem.cs +++ b/Content.Server/Shuttles/Systems/ThrusterSystem.cs @@ -44,6 +44,7 @@ public sealed class ThrusterSystem : EntitySystem base.Initialize(); SubscribeLocalEvent(OnActivateThruster); SubscribeLocalEvent(OnThrusterInit); + SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnThrusterShutdown); SubscribeLocalEvent(OnPowerChange); SubscribeLocalEvent(OnAnchorChange); @@ -232,8 +233,6 @@ public sealed class ThrusterSystem : EntitySystem private void OnThrusterInit(EntityUid uid, ThrusterComponent component, ComponentInit args) { - component.NextFire = _timing.CurTime + component.FireCooldown; - _ambient.SetAmbience(uid, false); if (!component.Enabled) @@ -247,6 +246,11 @@ public sealed class ThrusterSystem : EntitySystem } } + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + ent.Comp.NextFire = _timing.CurTime + ent.Comp.FireCooldown; + } + private void OnThrusterShutdown(EntityUid uid, ThrusterComponent component, ComponentShutdown args) { DisableThruster(uid, component); From 26deb4cdf9909b19425bb771bbb8ed7ceff238b6 Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Fri, 14 Feb 2025 14:14:43 +1100 Subject: [PATCH 030/529] Update engine to v245.0.0 (#35146) --- RobustToolbox | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RobustToolbox b/RobustToolbox index fea592e1d5..55571ef5b1 160000 --- a/RobustToolbox +++ b/RobustToolbox @@ -1 +1 @@ -Subproject commit fea592e1d53b043bda73a66248576caa6e3f65dc +Subproject commit 55571ef5b131dfa6f00a38e71a356ded34ca5911 From 0227afec4cd9061c17336db37c858d2cb09bc35b Mon Sep 17 00:00:00 2001 From: Winkarst <74284083+Winkarst-cpu@users.noreply.github.com> Date: Fri, 14 Feb 2025 15:23:27 +0300 Subject: [PATCH 031/529] Fix: showfluids command activates for everyone (#35155) Fix --- .../Fluids/EntitySystems/PuddleDebugDebugOverlaySystem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Server/Fluids/EntitySystems/PuddleDebugDebugOverlaySystem.cs b/Content.Server/Fluids/EntitySystems/PuddleDebugDebugOverlaySystem.cs index 859f2d80a6..88acf4b703 100644 --- a/Content.Server/Fluids/EntitySystems/PuddleDebugDebugOverlaySystem.cs +++ b/Content.Server/Fluids/EntitySystems/PuddleDebugDebugOverlaySystem.cs @@ -84,7 +84,7 @@ public sealed class PuddleDebugDebugOverlaySystem : SharedPuddleDebugOverlaySyst data.Add(new PuddleDebugOverlayData(pos, vol)); } - RaiseNetworkEvent(new PuddleOverlayDebugMessage(GetNetEntity(gridUid), data.ToArray())); + RaiseNetworkEvent(new PuddleOverlayDebugMessage(GetNetEntity(gridUid), data.ToArray()), session); } } From 244d7a629e05a1397d9c2254a77001d2a50cc8fd Mon Sep 17 00:00:00 2001 From: Ed <96445749+TheShuEd@users.noreply.github.com> Date: Fri, 14 Feb 2025 17:23:35 +0300 Subject: [PATCH 032/529] Fix embeddable projectiles dissapearing (reopening) (#35153) --- .../ItemRecall/SharedItemRecallSystem.cs | 2 +- .../Projectiles/EmbeddedContainerComponent.cs | 13 +++ .../Projectiles/SharedProjectileSystem.cs | 92 +++++++++++++------ 3 files changed, 78 insertions(+), 29 deletions(-) create mode 100644 Content.Shared/Projectiles/EmbeddedContainerComponent.cs diff --git a/Content.Shared/ItemRecall/SharedItemRecallSystem.cs b/Content.Shared/ItemRecall/SharedItemRecallSystem.cs index 63d38203c6..a4a49e9708 100644 --- a/Content.Shared/ItemRecall/SharedItemRecallSystem.cs +++ b/Content.Shared/ItemRecall/SharedItemRecallSystem.cs @@ -81,7 +81,7 @@ public abstract partial class SharedItemRecallSystem : EntitySystem return; if (TryComp(ent, out var projectile)) - _proj.UnEmbed(ent, projectile, actionOwner.Value); + _proj.EmbedDetach(ent, projectile, actionOwner.Value); _popups.PopupPredicted(Loc.GetString("item-recall-item-summon", ("item", ent)), actionOwner.Value, actionOwner.Value); diff --git a/Content.Shared/Projectiles/EmbeddedContainerComponent.cs b/Content.Shared/Projectiles/EmbeddedContainerComponent.cs new file mode 100644 index 0000000000..19dd93bfbd --- /dev/null +++ b/Content.Shared/Projectiles/EmbeddedContainerComponent.cs @@ -0,0 +1,13 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Projectiles; + +/// +/// Stores a list of all stuck entities to release when this entity is deleted. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class EmbeddedContainerComponent : Component +{ + [DataField, AutoNetworkedField] + public HashSet EmbeddedObjects = new(); +} diff --git a/Content.Shared/Projectiles/SharedProjectileSystem.cs b/Content.Shared/Projectiles/SharedProjectileSystem.cs index 1d0fc16cbd..be86fd1af2 100644 --- a/Content.Shared/Projectiles/SharedProjectileSystem.cs +++ b/Content.Shared/Projectiles/SharedProjectileSystem.cs @@ -15,6 +15,7 @@ using Robust.Shared.Physics.Dynamics; using Robust.Shared.Physics.Events; using Robust.Shared.Physics.Systems; using Robust.Shared.Serialization; +using Robust.Shared.Utility; namespace Content.Shared.Projectiles; @@ -22,7 +23,7 @@ public abstract partial class SharedProjectileSystem : EntitySystem { public const string ProjectileFixture = "projectile"; - [Dependency] private readonly INetManager _netManager = default!; + [Dependency] private readonly INetManager _net = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedHandsSystem _hands = default!; @@ -38,62 +39,63 @@ public abstract partial class SharedProjectileSystem : EntitySystem SubscribeLocalEvent(OnEmbedThrowDoHit); SubscribeLocalEvent(OnEmbedActivate); SubscribeLocalEvent(OnEmbedRemove); + + SubscribeLocalEvent(OnEmbeddableTermination); } - private void OnEmbedActivate(EntityUid uid, EmbeddableProjectileComponent component, ActivateInWorldEvent args) + private void OnEmbedActivate(Entity embeddable, ref ActivateInWorldEvent args) { - // Nuh uh - if (component.RemovalTime == null) + // Unremovable embeddables moment + if (embeddable.Comp.RemovalTime == null) return; - if (args.Handled || !args.Complex || !TryComp(uid, out var physics) || physics.BodyType != BodyType.Static) + if (args.Handled || !args.Complex || !TryComp(embeddable, out var physics) || + physics.BodyType != BodyType.Static) return; args.Handled = true; - _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, component.RemovalTime.Value, - new RemoveEmbeddedProjectileEvent(), eventTarget: uid, target: uid)); + _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, + args.User, + embeddable.Comp.RemovalTime.Value, + new RemoveEmbeddedProjectileEvent(), + eventTarget: embeddable, + target: embeddable)); } - private void OnEmbedRemove(EntityUid uid, EmbeddableProjectileComponent component, RemoveEmbeddedProjectileEvent args) + private void OnEmbedRemove(Entity embeddable, ref RemoveEmbeddedProjectileEvent args) { // Whacky prediction issues. - if (args.Cancelled || _netManager.IsClient) + if (args.Cancelled || _net.IsClient) return; - if (component.DeleteOnRemove) - { - QueueDel(uid); - return; - } - - UnEmbed(uid, component, args.User); + EmbedDetach(embeddable, embeddable.Comp, args.User); // try place it in the user's hand - _hands.TryPickupAnyHand(args.User, uid); + _hands.TryPickupAnyHand(args.User, embeddable); } - private void OnEmbedThrowDoHit(EntityUid uid, EmbeddableProjectileComponent component, ThrowDoHitEvent args) + private void OnEmbedThrowDoHit(Entity embeddable, ref ThrowDoHitEvent args) { - if (!component.EmbedOnThrow) + if (!embeddable.Comp.EmbedOnThrow) return; - Embed(uid, args.Target, null, component); + EmbedAttach(embeddable, args.Target, null, embeddable.Comp); } - private void OnEmbedProjectileHit(EntityUid uid, EmbeddableProjectileComponent component, ref ProjectileHitEvent args) + private void OnEmbedProjectileHit(Entity embeddable, ref ProjectileHitEvent args) { - Embed(uid, args.Target, args.Shooter, component); + EmbedAttach(embeddable, args.Target, args.Shooter, embeddable.Comp); // Raise a specific event for projectiles. - if (TryComp(uid, out ProjectileComponent? projectile)) + if (TryComp(embeddable, out ProjectileComponent? projectile)) { var ev = new ProjectileEmbedEvent(projectile.Shooter!.Value, projectile.Weapon!.Value, args.Target); - RaiseLocalEvent(uid, ref ev); + RaiseLocalEvent(embeddable, ref ev); } } - private void Embed(EntityUid uid, EntityUid target, EntityUid? user, EmbeddableProjectileComponent component) + private void EmbedAttach(EntityUid uid, EntityUid target, EntityUid? user, EmbeddableProjectileComponent component) { TryComp(uid, out var physics); _physics.SetLinearVelocity(uid, Vector2.Zero, body: physics); @@ -106,8 +108,7 @@ public abstract partial class SharedProjectileSystem : EntitySystem var rotation = xform.LocalRotation; if (TryComp(uid, out var throwingAngleComp)) rotation += throwingAngleComp.Angle; - _transform.SetLocalPosition(uid, xform.LocalPosition + rotation.RotateVec(component.Offset), - xform); + _transform.SetLocalPosition(uid, xform.LocalPosition + rotation.RotateVec(component.Offset), xform); } _audio.PlayPredicted(component.Sound, uid, null); @@ -115,13 +116,32 @@ public abstract partial class SharedProjectileSystem : EntitySystem var ev = new EmbedEvent(user, target); RaiseLocalEvent(uid, ref ev); Dirty(uid, component); + + EnsureComp(target, out var embeddedContainer); + + //Assert that this entity not embed + DebugTools.AssertEqual(embeddedContainer.EmbeddedObjects.Contains(uid), false); + + embeddedContainer.EmbeddedObjects.Add(uid); } - public void UnEmbed(EntityUid uid, EmbeddableProjectileComponent? component, EntityUid? user = null) + public void EmbedDetach(EntityUid uid, EmbeddableProjectileComponent? component, EntityUid? user = null) { if (!Resolve(uid, ref component)) return; + if (component.DeleteOnRemove) + { + QueueDel(uid); + return; + } + + if (component.EmbeddedIntoUid is not null) + { + if (TryComp(component.EmbeddedIntoUid.Value, out var embeddedContainer)) + embeddedContainer.EmbeddedObjects.Remove(uid); + } + var xform = Transform(uid); TryComp(uid, out var physics); _physics.SetBodyType(uid, BodyType.Dynamic, body: physics, xform: xform); @@ -149,6 +169,22 @@ public abstract partial class SharedProjectileSystem : EntitySystem _physics.WakeBody(uid, body: physics); } + private void OnEmbeddableTermination(Entity container, ref EntityTerminatingEvent args) + { + DetachAllEmbedded(container); + } + + public void DetachAllEmbedded(Entity container) + { + foreach (var embedded in container.Comp.EmbeddedObjects) + { + if (!TryComp(embedded, out var embeddedComp)) + continue; + + EmbedDetach(embedded, embeddedComp); + } + } + private void PreventCollision(EntityUid uid, ProjectileComponent component, ref PreventCollideEvent args) { if (component.IgnoreShooter && (args.OtherEntity == component.Shooter || args.OtherEntity == component.Weapon)) From 498440d369480bd5c75293813b13cd8414bbb4cd Mon Sep 17 00:00:00 2001 From: Milon Date: Fri, 14 Feb 2025 15:35:27 +0100 Subject: [PATCH 033/529] make chameleon verb predicted (#35156) * ok but what if we just predicted EVERYTHING * cleanup --- .../Systems/ChameleonClothingSystem.cs | 30 +------------------ .../SharedChameleonClothingSystem.cs | 22 ++++++++++++-- 2 files changed, 21 insertions(+), 31 deletions(-) diff --git a/Content.Server/Clothing/Systems/ChameleonClothingSystem.cs b/Content.Server/Clothing/Systems/ChameleonClothingSystem.cs index 3700aeb549..5c8954cc69 100644 --- a/Content.Server/Clothing/Systems/ChameleonClothingSystem.cs +++ b/Content.Server/Clothing/Systems/ChameleonClothingSystem.cs @@ -3,18 +3,13 @@ using Content.Shared.Clothing.Components; using Content.Shared.Clothing.EntitySystems; using Content.Shared.IdentityManagement.Components; using Content.Shared.Prototypes; -using Content.Shared.Verbs; -using Robust.Server.GameObjects; -using Robust.Shared.Player; using Robust.Shared.Prototypes; -using Robust.Shared.Utility; namespace Content.Server.Clothing.Systems; public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem { [Dependency] private readonly IPrototypeManager _proto = default!; - [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; [Dependency] private readonly IComponentFactory _factory = default!; [Dependency] private readonly IdentitySystem _identity = default!; @@ -22,7 +17,6 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem { base.Initialize(); SubscribeLocalEvent(OnMapInit); - SubscribeLocalEvent>(OnVerb); SubscribeLocalEvent(OnSelected); } @@ -31,40 +25,18 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem SetSelectedPrototype(uid, component.Default, true, component); } - private void OnVerb(EntityUid uid, ChameleonClothingComponent component, GetVerbsEvent args) - { - if (!args.CanAccess || !args.CanInteract || component.User != args.User) - return; - - args.Verbs.Add(new InteractionVerb() - { - Text = Loc.GetString("chameleon-component-verb-text"), - Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/settings.svg.192dpi.png")), - Act = () => TryOpenUi(uid, args.User, component) - }); - } - private void OnSelected(EntityUid uid, ChameleonClothingComponent component, ChameleonPrototypeSelectedMessage args) { SetSelectedPrototype(uid, args.SelectedId, component: component); } - private void TryOpenUi(EntityUid uid, EntityUid user, ChameleonClothingComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - if (!TryComp(user, out ActorComponent? actor)) - return; - _uiSystem.TryToggleUi(uid, ChameleonUiKey.Key, actor.PlayerSession); - } - private void UpdateUi(EntityUid uid, ChameleonClothingComponent? component = null) { if (!Resolve(uid, ref component)) return; var state = new ChameleonBoundUserInterfaceState(component.Slot, component.Default, component.RequireTag); - _uiSystem.SetUiState(uid, ChameleonUiKey.Key, state); + UI.SetUiState(uid, ChameleonUiKey.Key, state); } /// diff --git a/Content.Shared/Clothing/EntitySystems/SharedChameleonClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/SharedChameleonClothingSystem.cs index 488b7a5b64..725b034766 100644 --- a/Content.Shared/Clothing/EntitySystems/SharedChameleonClothingSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/SharedChameleonClothingSystem.cs @@ -5,8 +5,9 @@ using Content.Shared.Inventory; using Content.Shared.Inventory.Events; using Content.Shared.Item; using Content.Shared.Tag; +using Content.Shared.Verbs; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.Manager; +using Robust.Shared.Utility; namespace Content.Shared.Clothing.EntitySystems; @@ -14,19 +15,20 @@ public abstract class SharedChameleonClothingSystem : EntitySystem { [Dependency] private readonly IComponentFactory _factory = default!; [Dependency] private readonly IPrototypeManager _proto = default!; - [Dependency] private readonly ISerializationManager _serialization = default!; [Dependency] private readonly ClothingSystem _clothingSystem = default!; [Dependency] private readonly ContrabandSystem _contraband = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly SharedItemSystem _itemSystem = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly TagSystem _tag = default!; + [Dependency] protected readonly SharedUserInterfaceSystem UI = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnGotEquipped); SubscribeLocalEvent(OnGotUnequipped); + SubscribeLocalEvent>(OnVerb); } private void OnGotEquipped(EntityUid uid, ChameleonClothingComponent component, GotEquippedEvent args) @@ -94,6 +96,22 @@ public abstract class SharedChameleonClothingSystem : EntitySystem } } + private void OnVerb(Entity ent, ref GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract || ent.Comp.User != args.User) + return; + + // Can't pass args from a ref event inside of lambdas + var user = args.User; + + args.Verbs.Add(new InteractionVerb() + { + Text = Loc.GetString("chameleon-component-verb-text"), + Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/settings.svg.192dpi.png")), + Act = () => UI.TryToggleUi(ent.Owner, ChameleonUiKey.Key, user) + }); + } + protected virtual void UpdateSprite(EntityUid uid, EntityPrototype proto) { } /// From db732a25810a2d96cdf5d51f5d95d906695bb21a Mon Sep 17 00:00:00 2001 From: keronshb <54602815+keronshb@users.noreply.github.com> Date: Fri, 14 Feb 2025 10:01:40 -0500 Subject: [PATCH 034/529] Supermatter Grenade Rework (#35122) * Renames Supermatter grenade to singularity grenade. Removes explosion from Singularity Grenade. * Adjusts Singularity Grenade to account for engine changes, small buff to max range * Adjusts further * fix order --------- Co-authored-by: Milon --- .../Locale/en-US/store/uplink-catalog.ftl | 4 +-- .../Prototypes/Catalog/uplink_catalog.yml | 8 +++--- .../Objects/Weapons/Throwable/grenades.yml | 27 +++++++----------- .../icon.png | Bin .../meta.json | 0 .../primed.png | Bin 6 files changed, 17 insertions(+), 22 deletions(-) rename Resources/Textures/Objects/Weapons/Grenades/{supermattergrenade.rsi => singularitygrenade.rsi}/icon.png (100%) rename Resources/Textures/Objects/Weapons/Grenades/{supermattergrenade.rsi => singularitygrenade.rsi}/meta.json (100%) rename Resources/Textures/Objects/Weapons/Grenades/{supermattergrenade.rsi => singularitygrenade.rsi}/primed.png (100%) diff --git a/Resources/Locale/en-US/store/uplink-catalog.ftl b/Resources/Locale/en-US/store/uplink-catalog.ftl index 907e47a126..2ff6c16f5a 100644 --- a/Resources/Locale/en-US/store/uplink-catalog.ftl +++ b/Resources/Locale/en-US/store/uplink-catalog.ftl @@ -42,8 +42,8 @@ uplink-smoke-grenade-desc = A grenade that releases a huge cloud of smoke, perfe uplink-mini-bomb-name = Minibomb uplink-mini-bomb-desc = A low-yield, high-impact precision sabotage explosive with a 5 second long fuse. Perfect for quickly destroying a machine, dead body, or whatever else needs to go. -uplink-supermatter-grenade-name = Supermatter Grenade -uplink-supermatter-grenade-desc = Grenade that simulates delamination of a suppermatter engine, generates powerful gravity well. Explosion comparable to a Mini Bomb. +uplink-singularity-grenade-name = Singularity Grenade +uplink-singularity-grenade-desc = Grenade that simulates the power of a singularity, generates powerful gravity well. uplink-whitehole-grenade-name = Whitehole Grenade uplink-whitehole-grenade-desc = Grenade that repulses everything around for about 10 seconds. Very useful in small rooms and for chasing someone. diff --git a/Resources/Prototypes/Catalog/uplink_catalog.yml b/Resources/Prototypes/Catalog/uplink_catalog.yml index 7e3b939781..7f0f668cb7 100644 --- a/Resources/Prototypes/Catalog/uplink_catalog.yml +++ b/Resources/Prototypes/Catalog/uplink_catalog.yml @@ -263,10 +263,10 @@ - UplinkExplosives - type: listing - id: UplinkSupermatterGrenade - name: uplink-supermatter-grenade-name - description: uplink-supermatter-grenade-desc - productEntity: SupermatterGrenade + id: UplinkSingularityGrenade + name: uplink-singularity-grenade-name + description: uplink-singularity-grenade-desc + productEntity: SingularityGrenade discountCategory: usualDiscounts discountDownTo: Telecrystal: 3 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Throwable/grenades.yml b/Resources/Prototypes/Entities/Objects/Weapons/Throwable/grenades.yml index 08d6d855a8..d3a714445e 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Throwable/grenades.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Throwable/grenades.yml @@ -165,13 +165,13 @@ - type: entity - name: supermatter grenade - description: Grenade that simulates delamination of the supermatter engine, pulling things in a heap and exploding after some time. - parent: [GrenadeBase, BaseSyndicateContraband] - id: SupermatterGrenade + parent: [ GrenadeBase, BaseSyndicateContraband ] + id: SingularityGrenade + name: singularity grenade + description: Grenade that simulates the power of a singularity, pulling things in a heap. components: - type: Sprite - sprite: Objects/Weapons/Grenades/supermattergrenade.rsi + sprite: Objects/Weapons/Grenades/singularitygrenade.rsi - type: OnUseTimerTrigger delay: 3 beepInterval: 0.46 @@ -184,11 +184,6 @@ path: /Audio/Effects/Grenades/Supermatter/smbeep.ogg params: volume: -5 - - type: Explosive - explosionType: Default - totalIntensity: 200 - intensitySlope: 30 - maxIntensity: 120 - type: SoundOnTrigger removeOnTrigger: true sound: @@ -206,10 +201,10 @@ sound: path: /Audio/Effects/Grenades/Supermatter/supermatter_loop.ogg - type: GravityWell - maxRange: 8 - baseRadialAcceleration: 145 - baseTangentialAcceleration: 5 - gravPulsePeriod: 0.01 + maxRange: 10 + baseRadialAcceleration: 5 + baseTangentialAcceleration: .5 + gravPulsePeriod: 0.03 - type: SingularityDistortion intensity: 150 falloffPower: 1.5 @@ -225,12 +220,12 @@ path: /Audio/Effects/Grenades/Supermatter/supermatter_end.ogg params: volume: 5 - - type: ExplodeOnTrigger + - type: DeleteOnTrigger - type: entity name: whitehole grenade description: Grenade that repulses everything around for some time. - parent: SupermatterGrenade + parent: SingularityGrenade id: WhiteholeGrenade components: - type: Sprite diff --git a/Resources/Textures/Objects/Weapons/Grenades/supermattergrenade.rsi/icon.png b/Resources/Textures/Objects/Weapons/Grenades/singularitygrenade.rsi/icon.png similarity index 100% rename from Resources/Textures/Objects/Weapons/Grenades/supermattergrenade.rsi/icon.png rename to Resources/Textures/Objects/Weapons/Grenades/singularitygrenade.rsi/icon.png diff --git a/Resources/Textures/Objects/Weapons/Grenades/supermattergrenade.rsi/meta.json b/Resources/Textures/Objects/Weapons/Grenades/singularitygrenade.rsi/meta.json similarity index 100% rename from Resources/Textures/Objects/Weapons/Grenades/supermattergrenade.rsi/meta.json rename to Resources/Textures/Objects/Weapons/Grenades/singularitygrenade.rsi/meta.json diff --git a/Resources/Textures/Objects/Weapons/Grenades/supermattergrenade.rsi/primed.png b/Resources/Textures/Objects/Weapons/Grenades/singularitygrenade.rsi/primed.png similarity index 100% rename from Resources/Textures/Objects/Weapons/Grenades/supermattergrenade.rsi/primed.png rename to Resources/Textures/Objects/Weapons/Grenades/singularitygrenade.rsi/primed.png From 63b9255e71bb82b9b34286f33d4c41ac69beeac2 Mon Sep 17 00:00:00 2001 From: PJBot Date: Fri, 14 Feb 2025 15:02:50 +0000 Subject: [PATCH 035/529] Automatic changelog update --- Resources/Changelog/Changelog.yml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index f99dca0583..87f34230f6 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,12 +1,4 @@ Entries: -- author: ElectroJr - changes: - - message: There is now a rate limit for most interactions. It should not be noticeable - most of the time, but may lead to mispredicts when spam-clicking. - type: Tweak - id: 7454 - time: '2024-09-29T12:19:00.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/32527 - author: ArtisticRoomba changes: - message: HoS's energy shotgun is now correctly marked as grand theft contraband. @@ -3887,3 +3879,12 @@ id: 7953 time: '2025-02-13T22:35:59.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/35083 +- author: keronshb + changes: + - message: The Supermatter Grenade has been renamed to Singularity Grenade. + type: Tweak + - message: The Singularity Grenade no longer has an explosion. + type: Remove + id: 7954 + time: '2025-02-14T15:01:41.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/35122 From 862a2a744e1a8cb2643851b0f85c1372b1feadb6 Mon Sep 17 00:00:00 2001 From: Plykiya <58439124+Plykiya@users.noreply.github.com> Date: Fri, 14 Feb 2025 07:46:25 -0800 Subject: [PATCH 036/529] Predicted dice rolls (#34863) * Predicted dice rolls * Removed server-side dice system, make Shared no longer abstract, move visual code to client-side system * cleanup --------- Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com> --- Content.Client/Dice/DiceSystem.cs | 15 ++++-- Content.Server/Dice/DiceSystem.cs | 25 +-------- Content.Shared/Dice/DiceComponent.cs | 1 - Content.Shared/Dice/SharedDiceSystem.cs | 69 ++++++++++++------------- 4 files changed, 45 insertions(+), 65 deletions(-) diff --git a/Content.Client/Dice/DiceSystem.cs b/Content.Client/Dice/DiceSystem.cs index 2d36488257..2890de5f2f 100644 --- a/Content.Client/Dice/DiceSystem.cs +++ b/Content.Client/Dice/DiceSystem.cs @@ -5,17 +5,24 @@ namespace Content.Client.Dice; public sealed class DiceSystem : SharedDiceSystem { - protected override void UpdateVisuals(EntityUid uid, DiceComponent? die = null) + public override void Initialize() { - if (!Resolve(uid, ref die) || !TryComp(uid, out SpriteComponent? sprite)) + base.Initialize(); + + SubscribeLocalEvent(OnDiceAfterHandleState); + } + + private void OnDiceAfterHandleState(Entity entity, ref AfterAutoHandleStateEvent args) + { + if (!TryComp(entity, out var sprite)) return; - // TODO maybe just move each diue to its own RSI? + // TODO maybe just move each die to its own RSI? var state = sprite.LayerGetState(0).Name; if (state == null) return; var prefix = state.Substring(0, state.IndexOf('_')); - sprite.LayerSetState(0, $"{prefix}_{die.CurrentValue}"); + sprite.LayerSetState(0, $"{prefix}_{entity.Comp.CurrentValue}"); } } diff --git a/Content.Server/Dice/DiceSystem.cs b/Content.Server/Dice/DiceSystem.cs index 2d13679bd0..c2cb62a250 100644 --- a/Content.Server/Dice/DiceSystem.cs +++ b/Content.Server/Dice/DiceSystem.cs @@ -1,28 +1,5 @@ using Content.Shared.Dice; -using Content.Shared.Popups; -using JetBrains.Annotations; -using Robust.Shared.Audio; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Random; namespace Content.Server.Dice; -[UsedImplicitly] -public sealed class DiceSystem : SharedDiceSystem -{ - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly SharedPopupSystem _popup = default!; - - public override void Roll(EntityUid uid, DiceComponent? die = null) - { - if (!Resolve(uid, ref die)) - return; - - var roll = _random.Next(1, die.Sides + 1); - SetCurrentSide(uid, roll, die); - - _popup.PopupEntity(Loc.GetString("dice-component-on-roll-land", ("die", uid), ("currentSide", die.CurrentValue)), uid); - _audio.PlayPvs(die.Sound, uid); - } -} +public sealed class DiceSystem : SharedDiceSystem; diff --git a/Content.Shared/Dice/DiceComponent.cs b/Content.Shared/Dice/DiceComponent.cs index c01ad3c451..27f7bd70e0 100644 --- a/Content.Shared/Dice/DiceComponent.cs +++ b/Content.Shared/Dice/DiceComponent.cs @@ -1,6 +1,5 @@ using Robust.Shared.Audio; using Robust.Shared.GameStates; -using Robust.Shared.Serialization; namespace Content.Shared.Dice; diff --git a/Content.Shared/Dice/SharedDiceSystem.cs b/Content.Shared/Dice/SharedDiceSystem.cs index 8e2868e791..71a51584d3 100644 --- a/Content.Shared/Dice/SharedDiceSystem.cs +++ b/Content.Shared/Dice/SharedDiceSystem.cs @@ -1,12 +1,18 @@ using Content.Shared.Examine; using Content.Shared.Interaction.Events; +using Content.Shared.Popups; using Content.Shared.Throwing; -using Robust.Shared.GameStates; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Timing; namespace Content.Shared.Dice; public abstract class SharedDiceSystem : EntitySystem { + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + public override void Initialize() { base.Initialize(); @@ -14,76 +20,67 @@ public abstract class SharedDiceSystem : EntitySystem SubscribeLocalEvent(OnUseInHand); SubscribeLocalEvent(OnLand); SubscribeLocalEvent(OnExamined); - SubscribeLocalEvent(OnDiceAfterHandleState); } - private void OnDiceAfterHandleState(EntityUid uid, DiceComponent component, ref AfterAutoHandleStateEvent args) - { - UpdateVisuals(uid, component); - } - - private void OnUseInHand(EntityUid uid, DiceComponent component, UseInHandEvent args) + private void OnUseInHand(Entity entity, ref UseInHandEvent args) { if (args.Handled) return; + Roll(entity, args.User); args.Handled = true; - Roll(uid, component); } - private void OnLand(EntityUid uid, DiceComponent component, ref LandEvent args) + private void OnLand(Entity entity, ref LandEvent args) { - Roll(uid, component); + Roll(entity); } - private void OnExamined(EntityUid uid, DiceComponent dice, ExaminedEvent args) + private void OnExamined(Entity entity, ref ExaminedEvent args) { //No details check, since the sprite updates to show the side. using (args.PushGroup(nameof(DiceComponent))) { - args.PushMarkup(Loc.GetString("dice-component-on-examine-message-part-1", ("sidesAmount", dice.Sides))); + args.PushMarkup(Loc.GetString("dice-component-on-examine-message-part-1", ("sidesAmount", entity.Comp.Sides))); args.PushMarkup(Loc.GetString("dice-component-on-examine-message-part-2", - ("currentSide", dice.CurrentValue))); + ("currentSide", entity.Comp.CurrentValue))); } } - public void SetCurrentSide(EntityUid uid, int side, DiceComponent? die = null) + private void SetCurrentSide(Entity entity, int side) { - if (!Resolve(uid, ref die)) - return; - - if (side < 1 || side > die.Sides) + if (side < 1 || side > entity.Comp.Sides) { - Log.Error($"Attempted to set die {ToPrettyString(uid)} to an invalid side ({side})."); + Log.Error($"Attempted to set die {ToPrettyString(entity)} to an invalid side ({side})."); return; } - die.CurrentValue = (side - die.Offset) * die.Multiplier; - Dirty(uid, die); - UpdateVisuals(uid, die); + entity.Comp.CurrentValue = (side - entity.Comp.Offset) * entity.Comp.Multiplier; + Dirty(entity); } - public void SetCurrentValue(EntityUid uid, int value, DiceComponent? die = null) + public void SetCurrentValue(Entity entity, int value) { - if (!Resolve(uid, ref die)) - return; - - if (value % die.Multiplier != 0 || value/ die.Multiplier + die.Offset < 1) + if (value % entity.Comp.Multiplier != 0 || value / entity.Comp.Multiplier + entity.Comp.Offset < 1) { - Log.Error($"Attempted to set die {ToPrettyString(uid)} to an invalid value ({value})."); + Log.Error($"Attempted to set die {ToPrettyString(entity)} to an invalid value ({value})."); return; } - SetCurrentSide(uid, value / die.Multiplier + die.Offset, die); + SetCurrentSide(entity, value / entity.Comp.Multiplier + entity.Comp.Offset); } - protected virtual void UpdateVisuals(EntityUid uid, DiceComponent? die = null) + private void Roll(Entity entity, EntityUid? user = null) { - // See client system. - } + var rand = new System.Random((int)_timing.CurTick.Value); - public virtual void Roll(EntityUid uid, DiceComponent? die = null) - { - // See the server system, client cannot predict rolling. + var roll = rand.Next(1, entity.Comp.Sides + 1); + SetCurrentSide(entity, roll); + + var popupString = Loc.GetString("dice-component-on-roll-land", + ("die", entity), + ("currentSide", entity.Comp.CurrentValue)); + _popup.PopupPredicted(popupString, entity, user); + _audio.PlayPredicted(entity.Comp.Sound, entity, user); } } From ef4f5cf0a1acb338e101ac1a99a585c9c738bde4 Mon Sep 17 00:00:00 2001 From: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> Date: Fri, 14 Feb 2025 19:59:19 -0800 Subject: [PATCH 037/529] Engineering guidebook improvements (#34695) * engineering guidebook improvements * whoops * Changes * this game is en-US, so it shall be gasses, not gases * make changes to AccessConfigurator.xml, SolarPanels.xml, TeslaEngine.xml --------- Co-authored-by: ScarKy0 --- .../Solar/Components/SolarPanelComponent.cs | 2 + .../Entities/Objects/Devices/flatpack.yml | 4 +- .../Power/Generation/PA/particles.yml | 4 +- .../Structures/Power/Generation/ame.yml | 4 +- .../Structures/Power/Generation/teg.yml | 8 ++-- .../Engineering/AccessConfigurator.xml | 22 ++++++++-- .../Guidebook/Engineering/AirInjector.xml | 3 +- .../Guidebook/Engineering/AirVent.xml | 4 +- .../Guidebook/Engineering/Fires.xml | 2 +- .../Engineering/MixingAndFiltering.xml | 12 +++--- .../Engineering/PortableGenerator.xml | 4 +- .../Engineering/PortableScrubber.xml | 8 ++-- .../Guidebook/Engineering/PowerStorage.xml | 8 ++-- .../Guidebook/Engineering/Pumps.xml | 4 +- .../ServerInfo/Guidebook/Engineering/RTG.xml | 2 +- .../Guidebook/Engineering/Radiators.xml | 2 + .../Guidebook/Engineering/Ramping.xml | 4 +- .../Guidebook/Engineering/Shuttlecraft.xml | 4 +- .../Guidebook/Engineering/SignalValve.xml | 4 +- .../Engineering/SingularityEngine.xml | 10 ++--- .../Guidebook/Engineering/SolarPanels.xml | 19 +++++++- .../Guidebook/Engineering/Spacing.xml | 4 +- .../ServerInfo/Guidebook/Engineering/TEG.xml | 22 +++++----- .../Guidebook/Engineering/TeslaEngine.xml | 43 ++++++++++--------- .../Guidebook/Engineering/Thermomachines.xml | 14 +++--- .../Guidebook/Engineering/WirePanels.xml | 6 +-- 26 files changed, 129 insertions(+), 94 deletions(-) diff --git a/Content.Server/Solar/Components/SolarPanelComponent.cs b/Content.Server/Solar/Components/SolarPanelComponent.cs index 870b4c78ef..edf622d06f 100644 --- a/Content.Server/Solar/Components/SolarPanelComponent.cs +++ b/Content.Server/Solar/Components/SolarPanelComponent.cs @@ -1,4 +1,5 @@ using Content.Server.Solar.EntitySystems; +using Content.Shared.Guidebook; namespace Content.Server.Solar.Components { @@ -15,6 +16,7 @@ namespace Content.Server.Solar.Components /// Maximum supply output by this panel (coverage = 1) /// [DataField("maxSupply")] + [GuidebookData] public int MaxSupply = 750; /// diff --git a/Resources/Prototypes/Entities/Objects/Devices/flatpack.yml b/Resources/Prototypes/Entities/Objects/Devices/flatpack.yml index 4facfd91b3..fa23947c5c 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/flatpack.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/flatpack.yml @@ -83,8 +83,8 @@ - state: singularity-generator - type: GuideHelp guides: - - SingularityEngine - - Power + - SingularityEngine + - Power - type: entity parent: BaseFlatpack diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/PA/particles.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/PA/particles.yml index 0a36d91e38..6e55bfec5e 100644 --- a/Resources/Prototypes/Entities/Structures/Power/Generation/PA/particles.yml +++ b/Resources/Prototypes/Entities/Structures/Power/Generation/PA/particles.yml @@ -48,8 +48,8 @@ Level3: {state: particle3} - type: GuideHelp # why does this even have a guidebook link in the first place guides: - - SingularityTeslaEngine - - Power + - SingularityTeslaEngine + - Power - type: entity name: anti particles diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/ame.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/ame.yml index 550dddc847..b3723252d3 100644 --- a/Resources/Prototypes/Entities/Structures/Power/Generation/ame.yml +++ b/Resources/Prototypes/Entities/Structures/Power/Generation/ame.yml @@ -204,8 +204,8 @@ node: ameShielding - type: GuideHelp guides: - - AME - - Power + - AME + - Power - type: Electrified onHandInteract: false onInteractUsing: false diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/teg.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/teg.yml index 860462788b..e6e4b6b72b 100644 --- a/Resources/Prototypes/Entities/Structures/Power/Generation/teg.yml +++ b/Resources/Prototypes/Entities/Structures/Power/Generation/teg.yml @@ -98,8 +98,8 @@ path: /Audio/Ambience/Objects/vending_machine_hum.ogg - type: GuideHelp guides: - - TEG - - Power + - TEG + - Power - type: StealTarget stealGroup: Teg @@ -159,8 +159,8 @@ - type: Pullable - type: GuideHelp guides: - - TEG - - Power + - TEG + - Power # functionality - type: NodeContainer diff --git a/Resources/ServerInfo/Guidebook/Engineering/AccessConfigurator.xml b/Resources/ServerInfo/Guidebook/Engineering/AccessConfigurator.xml index fea8104a5b..a73bdde959 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/AccessConfigurator.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/AccessConfigurator.xml @@ -27,8 +27,24 @@ A device with no access requirements set, like a public access airlock, can be modified using any valid station ID card. - ## Repairing damaged ID card readers - Syndicate agents may attempt to hack access-restricted devices through the use of a [color=#a4885c]Cryptographic Sequencer (EMAG)[/color]. This nefarious tool will completely short out any ID card readers that are attached to the device. + ## Repairing access-broken ID card readers + Syndicate agents may attempt to hack access-restricted devices through the use of an [color=#a4885c]Authentication Disruptor[/color]. + This nefarious tool will completely short out any ID card readers that are attached to the device, making it all-access. + + + + + + To repair the damage, you'll commonly need to partially deconstruct the device and reconstruct it. + This will reset the access requirements to defaults, allowing you to reconfigure the device as needed. + + ## Repairing access-broken Airlocks + Airlocks can be repaired multiple ways if their access requirements have been tampered with. + + If you have an Access Configurator, you can use a [color=#a4885c]Screwdriver[/color] to remove the airlock's maintenance panel, and then use the Access Configurator to reconfigure the airlock's access requirements. + No partial deconstruction is needed. + + If you don't have an Access Configurator, you can still fix the airlock by partially deconstructing it until you remove the door electronics, and then reconstructing it. + This will reset the airlock to the default access requirements it had at the start of the shift. - Engineers will need to partially de/reconstruct affected devices, and then set appropriate access permissions afterwards using the access configurator (or network configurator, for airlocks), to re-establish access restrictions. diff --git a/Resources/ServerInfo/Guidebook/Engineering/AirInjector.xml b/Resources/ServerInfo/Guidebook/Engineering/AirInjector.xml index 3c4027dcb3..54b9cccba8 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/AirInjector.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/AirInjector.xml @@ -4,12 +4,11 @@ - It is primarily used to force gasses into high-pressure rooms like the station's [textlink="gas storage rooms" link="GasMiningAndStorage"], or a burn chamber. + It is primarily used to force gasses into high-pressure rooms like the station's [textlink="gas storage rooms" link="GasMiningAndStorage"] or a burn chamber. The air injector does not require [textlink="power" link="Power"] to function. The air injector will inject gasses into the atmosphere it's exposed to until the atmosphere reaches [color=orange][protodata="GasOutletInjector" comp="GasOutletInjector" member="MaxPressure"/] kPa[/color]. The air injector's speed is proportional to the amount of gas in the injector. - The more gas in the injector, the faster it will inject gas into the exposed atmosphere. diff --git a/Resources/ServerInfo/Guidebook/Engineering/AirVent.xml b/Resources/ServerInfo/Guidebook/Engineering/AirVent.xml index d6b81fa19b..bbdd1a530a 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/AirVent.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/AirVent.xml @@ -53,8 +53,8 @@ - If the Internal bound pressure is set to 50 kPa, the air vent will not draw gas from the connected pipe if its pressure is below 50 kPa. When the vent is in siphoning mode: - - If the External bound pressure is set to 101.3 kPa, the air vent will siphon gases until the atmosphere reaches 101.3 kPa. - - If the Internal bound pressure is set to 50 kPa, the air vent will not push gases into the pipenet if its pressure is above 50 kPa. + - If the External bound pressure is set to 101.3 kPa, the air vent will siphon gasses until the atmosphere reaches 101.3 kPa. + - If the Internal bound pressure is set to 50 kPa, the air vent will not push gasses into the pipenet if its pressure is above 50 kPa. If you're still confused about PressureBounds, here's a simple way to think about it: - You can think of the External bound as the upper limit for the exposed atmosphere. "I will not pressurize the exposed atmosphere past this pressure, or draw from the atmosphere below this pressure." diff --git a/Resources/ServerInfo/Guidebook/Engineering/Fires.xml b/Resources/ServerInfo/Guidebook/Engineering/Fires.xml index ca728f1e8d..1afcca89f7 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/Fires.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/Fires.xml @@ -7,7 +7,7 @@ - They often make high-pressure and high-temperature gases, which can quickly spread throughout the station if firelocks or doors are opened carelessly, even if only for a moment. + They often make high-pressure and high-temperature gasses, which can quickly spread throughout the station if firelocks or doors are opened carelessly, even if only for a moment. If people are caught in a fire, they can quickly become incapacitated, die, and even ash, rendering them unrevivable. diff --git a/Resources/ServerInfo/Guidebook/Engineering/MixingAndFiltering.xml b/Resources/ServerInfo/Guidebook/Engineering/MixingAndFiltering.xml index 5814dd4b08..d38dd1bd99 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/MixingAndFiltering.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/MixingAndFiltering.xml @@ -1,13 +1,13 @@ # Mixing and Filtering - Gas mixers and filters are essential tools for manipulating the composition of gases within a [textlink="pipe network" link="PipeNetworks"]. + Gas mixers and filters are essential tools for manipulating the composition of gasses within a [textlink="pipe network" link="PipeNetworks"]. ## Gas Mixer - Gas mixers are used to combine gases in specific ratios within a [textlink="pipe network." link="PipeNetworks"] + Gas mixers are used to combine gasses in specific ratios within a [textlink="pipe network." link="PipeNetworks"] They are essential for creating controlled gas mixtures for various applications. Gas mixers have 3 connections: 2 inputs and 1 output, as shown below: @@ -20,7 +20,7 @@ - Gas mixers will still respect the requested gas mixture even if one of the input gases is not available. For example: + Gas mixers will still respect the requested gas mixture even if one of the input gasses is not available. For example: - If the requested mixture is 22% oxygen and 78% nitrogen, but there is no available oxygen, the mixer will not work until oxygen is available. - If oxygen is available, but at a pressure lower than required to create the proper mixture at the requested pressure, the mixer will still create the mixture, but the output will be at a lower pressure than requested. @@ -36,7 +36,7 @@ - Mixing oxygen and plasma for plasma burning to create Tritium ## Gas Filter - Gas filters are used to separate gases from a mixture within a [textlink="pipe network." link="PipeNetworks"] + Gas filters are used to separate gasses from a mixture within a [textlink="pipe network." link="PipeNetworks"] @@ -50,6 +50,6 @@ Gas filters will become blocked and will not filter gas if either output is blocked. Gas filters can be used in a variety of applications, for example: - - Filtering out unwanted gases from a [textlink="pipe network" link="PipeNetworks"] - - Separating specific gases for storage in a station's recyclernet + - Filtering out unwanted gasses from a [textlink="pipe network" link="PipeNetworks"] + - Separating specific gasses for storage in a station's recyclernet diff --git a/Resources/ServerInfo/Guidebook/Engineering/PortableGenerator.xml b/Resources/ServerInfo/Guidebook/Engineering/PortableGenerator.xml index 55a1e7fde5..d4eee0f597 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/PortableGenerator.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/PortableGenerator.xml @@ -22,7 +22,7 @@ Setup is incredibly easy: wrench it down above an [color=green]LV[/color] power cable, give it some welding fuel, and start it up. - Welding fuel is the only fuel source the J.R.P.A.C.M.A.N. can use, and it can be found in welding fuel tanks across the station, commonly in maintenance areas. + Welding fuel is the only fuel source the J.R.P.A.C.M.A.N. can use and it can be found in welding fuel tanks across the station, commonly in maintenance areas. # The Big Ones @@ -41,7 +41,7 @@ The S.U.P.E.R.P.A.C.M.A.N. boasts a larger power output (up to [color=orange][protodata="PortableGeneratorSuperPacman" comp="FuelGenerator" member="MaxTargetPower" format="N0"/] W[/color]) and longer runtime at maximum output, but scales down to lower outputs less efficiently. - They connect directly to [color=yellow]MV[/color] or [color=orange]HV[/color] [textlink="power cables" link="VoltageNetworks"], and are able to switch between them for flexibility. + They connect directly to [color=yellow]MV[/color] or [color=orange]HV[/color] [textlink="power cables" link="VoltageNetworks"] and are able to switch between them for flexibility. The S.U.P.E.R.P.A.C.M.A.N and P.A.C.M.A.N require uranium sheets and plasma sheets as fuel, respectively. diff --git a/Resources/ServerInfo/Guidebook/Engineering/PortableScrubber.xml b/Resources/ServerInfo/Guidebook/Engineering/PortableScrubber.xml index 287e591ff3..cfc82b9d72 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/PortableScrubber.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/PortableScrubber.xml @@ -5,16 +5,16 @@ - It is invaluable for quickly scrubbing unwanted gasses from a room when regular [textlink="scrubbers" link="AirScrubber"] are working too slow, or when a [textlink="scrubber" link="AirScrubber"] is not present. + It is invaluable for quickly removing unwanted gasses from a room when regular [textlink="scrubbers" link="AirScrubber"] are working too slow, or when a [textlink="scrubber" link="AirScrubber"] is not present. The portable scrubber requires [textlink="power" link="Power"] through a nearby [textlink="LV cable" link="VoltageNetworks"] to function. - The portable scrubber automatically starts scrubbing all non-breathable gasses from the room when it is bolted (wrenched) to the floor. + The portable scrubber automatically starts removing all non-breathable gasses from the room when it is bolted (wrenched) to the floor. - It will stop scrubbing when unbolted (unwrenched) from the floor, or when its internal volume is full. The scrubber has an internal capacity of [color=orange][protodata="PortableScrubber" comp="PortableScrubber" member="Volume"/] liters[/color]. + It will stop scrubbing when unbolted (unwrenched) from the floor or when its internal volume is full. The scrubber has an internal capacity of [color=orange][protodata="PortableScrubber" comp="PortableScrubber" member="Volume"/] liters[/color]. ## Dumping Waste and Storage - The scrubber's internal volume can be emptied by bolting (wrenching it) onto a [textlink="connector" link="GasCanisters"]. + The scrubber's internal volume can be emptied by anchoring (wrenching) onto a [textlink="connector" link="GasCanisters"]. Stations commonly have a dedicated emptying point to quickly transfer the waste from the scrubber to the station's wastenet. diff --git a/Resources/ServerInfo/Guidebook/Engineering/PowerStorage.xml b/Resources/ServerInfo/Guidebook/Engineering/PowerStorage.xml index efd0167eb1..729e1720e1 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/PowerStorage.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/PowerStorage.xml @@ -29,7 +29,7 @@ - SMESes can store [color=orange][protodata="SMESBasic" comp="Battery" member="MaxCharge" format="N0"/] J[/color] of energy, and can output a maximum [color=orange][protodata="SMESBasic" comp="PowerNetworkBattery" member="MaxSupply" format="N0"/] W[/color] of power. + SMESes can store [color=orange][protodata="SMESBasic" comp="Battery" member="MaxCharge" format="N0"/] J[/color] of energy and can output a maximum [color=orange][protodata="SMESBasic" comp="PowerNetworkBattery" member="MaxSupply" format="N0"/] W[/color] of power. If the battery is full, the SMES will pass through the power it receives from the input cable to the output cable. In the event of a power deficit, the SMES will ramp up to supplement the power draw. @@ -41,10 +41,10 @@ They're primarily used in station SMES arrays to store large amounts of power for the station's power grid. - They help to buy engineers time to setup power at roundstart, or to provide power in the event of a power deficit for extended periods of time. + They help to buy engineers time to setup power at roundstart or to provide power in the event of a power deficit for extended periods of time. - Advanced SMESes can store [color=orange][protodata="SMESAdvanced" comp="Battery" member="MaxCharge" format="N0"/] J[/color] of energy, and can output a maximum [color=orange][protodata="SMESAdvanced" comp="PowerNetworkBattery" member="MaxSupply" format="N0"/] W[/color] of power. + Advanced SMESes can store [color=orange][protodata="SMESAdvanced" comp="Battery" member="MaxCharge" format="N0"/] J[/color] of energy and can output a maximum [color=orange][protodata="SMESAdvanced" comp="PowerNetworkBattery" member="MaxSupply" format="N0"/] W[/color] of power. - Keep in mind that these aren't a magic solution to power deficits, and they can't store infinite energy. + Keep in mind that these aren't a magic solution to power deficits and they can't store infinite energy. A station load will drain these battries quickly if there is no power source partially supporting them. diff --git a/Resources/ServerInfo/Guidebook/Engineering/Pumps.xml b/Resources/ServerInfo/Guidebook/Engineering/Pumps.xml index 7b399b0e5a..3574e34a82 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/Pumps.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/Pumps.xml @@ -1,7 +1,7 @@  # Pumps Pumps are the primary way of actively moving gasses through a [textlink="pipenet." link="PipeNetworks"] - They take gas from one side, and push it to the other. + They take gas from one side and push it to the other. There are two different types of pumps: @@ -16,7 +16,7 @@ - Pumps cannot move gasses into pipes with pressures or volumes exceeding their [color=#a4885c]limit[/color]. This causes them to be [color=red]blocked[/color]. Pumps will show a colorful animation when they are doing work. - If they have no gas to pump, or they are blocked, they will show a blinking [color=red]red[/color] animation. + If they have no gas to pump or they are blocked, they will show a blinking [color=red]red[/color] animation. Pumps that are off, have no power, or are unanchored will show no animation. ## Pressure Pumps diff --git a/Resources/ServerInfo/Guidebook/Engineering/RTG.xml b/Resources/ServerInfo/Guidebook/Engineering/RTG.xml index 06280d5eef..d412251e24 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/RTG.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/RTG.xml @@ -9,7 +9,7 @@ They require no maintenance and are a reliable source of power, making them ideal for powering essential systems that need to be online at all times, like Telecoms, the AI, or the Crew Monitoring Server. - RTGs always generate [color=orange][protodata="GeneratorRTG" comp="PowerSupplier" member="MaxSupply" format="N0"/] W[/color] of power, and must be connected to an [color=orange]HV power[/color] [textlink="network" link="VoltageNetworks"] to function. + RTGs always generate [color=orange][protodata="GeneratorRTG" comp="PowerSupplier" member="MaxSupply" format="N0"/] W[/color] of power and must be connected to an [color=orange]HV power[/color] [textlink="network" link="VoltageNetworks"] to function. However, they're only accessible through salvage finding one on an expedition. Should they bring some in, make sure to thank them! diff --git a/Resources/ServerInfo/Guidebook/Engineering/Radiators.xml b/Resources/ServerInfo/Guidebook/Engineering/Radiators.xml index 0d3ec5f8f6..5471142270 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/Radiators.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/Radiators.xml @@ -21,6 +21,8 @@ If you're exchanging heat with space, you can only get as cold as space. To increase the efficiency of radiation, you can build radiators on lattice, which will allow the radiator to radiate more heat, compared to being directly attached to hull tile. + Gas will flow naturally through the radiator via differences in pressure, but you can use a gas pump to increase the flow rate. + Increasing the flow rate of gas through the radiator will increase the rate of heat exchange. diff --git a/Resources/ServerInfo/Guidebook/Engineering/Ramping.xml b/Resources/ServerInfo/Guidebook/Engineering/Ramping.xml index 6799353143..b7ef39c2a4 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/Ramping.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/Ramping.xml @@ -12,9 +12,9 @@ - After some seconds have passed, the generator will have ramped up to 100 kW of power, and the brownout will end. All devices are now satisfied with the power they are receiving. During a shift, this is most commonly observed when a generator runs out of fuel and suddenly stops producing power. - Suddenly, the grid is hit with a large deficit of power (as supply has fallen below demand), and all devices will experience a brownout until SMESes or other generators can ramp up to match the new demand. + Suddenly the grid is hit with a large deficit of power (as supply has fallen below demand), and all devices will experience a brownout until SMESes or other generators can ramp up to match the new demand. - This can also happen when a large power consuming device, or department, is reconnected to the grid. + This can also happen when a large power consuming device or department is reconnected to the grid. The sudden increase in power draw will cause a brownout until the generators can ramp up to match the new demand. diff --git a/Resources/ServerInfo/Guidebook/Engineering/Shuttlecraft.xml b/Resources/ServerInfo/Guidebook/Engineering/Shuttlecraft.xml index 8dc7cdf57d..27151a7565 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/Shuttlecraft.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/Shuttlecraft.xml @@ -46,7 +46,7 @@ - Head out into space with steel sheets and metal rods in hand, and click on the edge of the station to place lattice. + Head out into space with steel sheets and metal rods in hand and click on the edge of the station to place lattice. Place a line of lattice about 3-4 tiles away from the station, then start building a platform with lattice. @@ -54,7 +54,7 @@ Once you're finished constructing the base of your shuttle, you can use wirecutters to snip the connecting lattice that joins your new ship and the station. - This platform is considered a different grid from the station, and thus will not have any gravity or be held in place by a station anchor — it can move around freely. + This platform is considered a different grid from the station and thus will not have any gravity or be held in place by a station anchor — it can move around freely. You can expand your lattice platform further by clicking just off the edge with some rods in hand. diff --git a/Resources/ServerInfo/Guidebook/Engineering/SignalValve.xml b/Resources/ServerInfo/Guidebook/Engineering/SignalValve.xml index dd2991e56d..2b6b11dd58 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/SignalValve.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/SignalValve.xml @@ -5,13 +5,13 @@ - The signal valve is similar to the manual valve. Gas can flow unrestricted in both directions, and it can be operated manually. + The signal valve is similar to the manual valve. Gas can flow unrestricted in both directions and it can be operated manually. The signal valve has 3 [textlink="signal" link="Networking"] inputs, which can open, close, or toggle the valve. Signal valves can be used in a variety of applications, for example: - Remote control of valves in hazardous areas or areas inaccessible to crew - Convenient control over a valve in a hard-to-reach area - - Automation with other [textlink="signal-enabled" link="Networking"] machines and equipment such as [textlink="air alarms" link="AirAlarms"] and remote signallers + - Automation with other [textlink="signal-enabled" link="Networking"] machines and equipment such as [textlink="air alarms" link="AirAlarms"] and remote signalers diff --git a/Resources/ServerInfo/Guidebook/Engineering/SingularityEngine.xml b/Resources/ServerInfo/Guidebook/Engineering/SingularityEngine.xml index 6b4bb1780a..cca2bd2479 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/SingularityEngine.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/SingularityEngine.xml @@ -28,7 +28,7 @@ It is suggested to use a max size containment field for the singularity. Any smaller and the singularity may outgrow its field and escape. - Containment pylons should be arranged in a square, with 7 tiles of spacing between each pylon. + Containment field generators should be arranged in a square, with 7 tiles of spacing between each field generator. @@ -62,11 +62,11 @@ From here, you can refill the tank with plasma using a plasma canister and reinsert it into the collector. The maximum power the radiation collector can produce is determined by: - - The amount of radiation it is capturing (which is effectively the Singularity's power level), - - and the amount of plasma it has in its connected tank. + - The amount of radiation it is capturing (which is effectively the Singularity's power level) + - The amount of plasma it has in its connected tank - Over time, the collector will drain the tank of plasma, which reduces it's effective power output. - Eventually, the tank will be empty, and the collector will stop producing power. Be sure to refill the tank often! + Over time the collector will drain the tank of plasma, which reduces it's effective power output. + Eventually the tank will be empty, and the collector will stop producing power. Be sure to refill the tank often! ## Radiation Protection The singularity emits a massive amount of radiation, which can kill crew members who are not wearing proper protection. diff --git a/Resources/ServerInfo/Guidebook/Engineering/SolarPanels.xml b/Resources/ServerInfo/Guidebook/Engineering/SolarPanels.xml index d97d2a78d7..2873e0ae10 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/SolarPanels.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/SolarPanels.xml @@ -18,6 +18,8 @@ This is because the station is occluding some panels, so don't worry about it too much. Most stations have solar arrays placed at all sides of the station, so there's always some power being generated. + When directly facing the sun with no object occlusion, regular solar panels can generate [color=orange][protodata="SolarPanel" comp="SolarPanel" member="MaxSupply" format="N0"] W[/color] of power. + Solar panels generate [color=orange]HV power[/color], and as such require an [color=orange]HV wire[/color] running underneath them to connect to the station's power grid. ## Power Bridge @@ -81,17 +83,30 @@ ## Setting Up Solar Panel Arrays Solar panel arrays are commonly found on the exterior of the station, and are used to generate large amounts of free power for the station. - You can either spacewalk to them from the outside, or you can run a loop through the station's maintenance tunnels to reach them. + You can either spacewalk to them from the outside or you can run a loop through the station's maintenance tunnels to reach them. Solar array machine rooms are often marked with signs, and locked behind engineering access. - At the start of the shift, solar panels are misaligned and disconnected from the grid. + At the start of the shift solar panels are misaligned and disconnected from the grid. You will need to align them and connect them to the station's power grid to start generating power. This usually involves running a line of [color=orange]HV wire[/color] to the pannels from the solar array machine room, and then using a Solar Control Computer to align the panels. Solar array machine rooms frequently have a Solar Control Computer nearby, as well as an [textlink="SMES" link="PowerStorage"] to store the power generated by the panels for later use. + + ## Upgrading Solar Panels + Solar panels can be upgraded to increase their power output. + + + + + + + This can be done by replacing the glass sheets in the solar assembly with plasma or uranium glass sheets. + + Plasma and Uranium solar panels generate [color=orange][protodata="SolarPanelPlasma" comp="SolarPanel" member="MaxSupply" format="N0"] W[/color] and [color=orange][protodata="SolarPanelUranium" comp="SolarPanel" member="MaxSupply" format="N0"] W[/color] of power respectively. + They also are much tougher than regular solar panels. diff --git a/Resources/ServerInfo/Guidebook/Engineering/Spacing.xml b/Resources/ServerInfo/Guidebook/Engineering/Spacing.xml index 01097c10e2..6fb9565ad3 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/Spacing.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/Spacing.xml @@ -6,14 +6,14 @@ Fixing spacing generally follows two simple steps: - 1. Identify the area that has been spaced, and [textlink="seal the hole." link="ExpandingRepairingStation"] + 1. Identify the area that has been spaced and [textlink="seal the hole." link="ExpandingRepairingStation"] - If you're having trouble finding the hole, you can carefully listen for the flow of air rushing by you, if air is currently leaking to space. - Look for any holes underneath girders that may be hard to see. 2. Repressurize the area. - - [textlink="Air vents" link="AirVent"] enter pressure lockout when a room is spaced, so you'll need to override the vents to repressurize the area. You can do this by setting the connected [textlink="air alarm" link="AirAlarms"] to fill, or by using a screwdriver on a vent to manually override it temporarily. + - [textlink="Air vents" link="AirVent"] enter pressure lockout in order not to lose more air to spacing, so you'll need to override the vents to repressurize the area. You can do this by setting the connected [textlink="air alarm" link="AirAlarms"] to fill, or by using a screwdriver on a vent to manually override it temporarily. ## Things to Avoid - Keep in mind that while you have an infinite supply of [textlink="mined gas" link="GasMiningAndStorage"], it is not quick enough to fill up multiple rooms at once. Setting [textlink="air alarms" link="AirAlarms"] to fill first [italic]before[/italic] fixing the root problem will often lead to wasted time and gas. diff --git a/Resources/ServerInfo/Guidebook/Engineering/TEG.xml b/Resources/ServerInfo/Guidebook/Engineering/TEG.xml index f79ad044ce..04268e2db3 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/TEG.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/TEG.xml @@ -1,8 +1,8 @@ # Thermo-electric Engine (TEG) - The TEG generates power by exchanging heat between hot and cold gases. - On the station, hot gas is usually created by burning plasma, and an array of [textlink="heat-exchanging" link="Radiators"] pipes in space radiates away heat to cool down circulated gases. + The TEG generates power by exchanging heat between hot and cold gasses. + On the station, hot gas is usually created by burning plasma, and an array of [textlink="heat-exchanging" link="Radiators"] pipes in space radiates away heat to cool down circulated gasses. The TEG relies heavily on [textlink="atmospherics" link="Atmospherics"] [textlink="piping." link="Pipes"] The only truly special component about it is the generator core and circulators; the rest is all off-the-shelf atmospherics equipment. @@ -26,7 +26,7 @@ A pressure difference is required across the input and output, so pumps are generally provided and must be turned on. There is no preference for which side must be hot or cold, there need only be a difference in temperature between them. - The gases in the two "loops" are never mixed, [color=#a4885c]only energy is exchanged between them[/color]. + The gasses in the two "loops" are never mixed, [color=#a4885c]only energy is exchanged between them[/color]. The hot side will cool down, the cold side will heat up. ## The Pipes @@ -39,14 +39,14 @@ As I'm sure a wise person once said: the best way to make something hot is to light it on fire. Well, depending on context, that may not be very wise, but luckily your engineering department has just what's needed to do it wisely after all. - As stated above, there are many different layouts one can follow to heat up (or cool down) gases; this part of the guide will cover some common methods one will often see for the hot loop, involving [color=red]the Burn Chamber[/color]. + As stated above, there are many different layouts one can follow to heat up (or cool down) gasses; this part of the guide will cover some common methods one will often see for the hot loop, involving [color=red]the Burn Chamber[/color]. Side note: Plasma fires burn relatively cool compared to, for example, Tritium fires. It may be viable to extract Tritium from an extraction setup and react it with Oxygen to get truly hellish temperatures for power. ## The Burn Chamber - The burn chamber is the preferred method for heating up gases, and it is commonly used for other purposes too. (see: Tritium production) + The burn chamber is the preferred method for heating up gasses, and it is commonly used for other purposes too. (see: Tritium production) Most (if not all) stations have the burn chamber separated from the main atmospherics block by a 1-wide spaced grid, to prevent the flow of scalding hot gas to Atmos if there was a breach. The chambers consist of 3 important parts: - The [textlink="Air Injector" link="AirInjector"]/[textlink="Passive Vent" link="PassiveVent"] @@ -89,7 +89,7 @@ There is a notable difference between the [textlink="passive vent" link="PassiveVent"] and the [textlink="air injector" link="AirInjector"]; the [textlink="air injector" link="AirInjector"] can only keep injecting air up to [color=#a4885c]9MPa[/color], which can be reached very easily with a good burn. Ideally, switch out the [textlink="air injector" link="AirInjector"] for a [textlink="passive vent" link="PassiveVent"] connected to a volume pump. - The space vent (designated as a blast door to space on one side of the burn chamber) allows waste gases to be expelled and destroyed. + The space vent (designated as a blast door to space on one side of the burn chamber) allows waste gasses to be expelled and destroyed. Open this occasionally to keep the pressure under control, or to space excess input gas. You even might find the pneumatic valve useful for occasionally spacing the gas. @@ -221,14 +221,14 @@ ## In the Pursuit of Greater Efficiency Remember, Atmospherics is a science, and as such, it is always evolving. - The setups above are just the tip of the iceberg; there are many ways to setup the TEG, and many ways to improve upon the setups above. + The setups above are just the tip of the iceberg; there are many ways to setup the TEG and many ways to improve upon the setups above. [color=#a4885c]Experiment![/color] - Always seek to improve upon the designs you see, and always seek to improve upon the designs you make. - The TEG is a powerful tool, and with great power comes great responsibility. - Make sure to use it wisely, and make sure to use it well. + Always seek to improve upon the designs you see and always seek to improve upon the designs you make. + The TEG is a powerful tool and with great power comes great responsibility. + Make sure to use it wisely and make sure to use it well. - Space Station 14 atmospherics is a complex system, and the TEG is just one part of it. + Space Station 14 atmospherics is a complex system and the TEG is just one part of it. [bold]It's like a giant puzzle, so go out and solve it![/bold] diff --git a/Resources/ServerInfo/Guidebook/Engineering/TeslaEngine.xml b/Resources/ServerInfo/Guidebook/Engineering/TeslaEngine.xml index 859ec8317e..168f6253a8 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/TeslaEngine.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/TeslaEngine.xml @@ -37,30 +37,13 @@ The Tesla prefers to strike some objects more than others, such as Tesla Coils and Grounding Rods. - - - - - - - - - - - Because of this, strategically placing Tesla Coils and Grounding Rods around the lightning ball can help protect sensitive equipment from being struck, and prevent a loosed tesla (tesloose). - If the tesla can't find any Tesla Coils or Grounding Rods to strike first, it will strike almost any station object capable of being powered, such as Substations, APCs, and general machinery. Certain objects aren't struck by the tesla, such as batteries, lights, PDAs, and other handheld items. It will also strike mobs and crew members, shocking them. Make sure to wear insulated gloves before approaching it. - Note that only placing Tesla Coils won't be enough to prevent the tesla from striking sensitive equipment. - Grounding Rods should also be placed to help protect nearby Emitters from being struck. - - Engineers can also use grounding rods to protect sensitive equipment from lightning strikes, such as the Emitters powering the containment field generators. - - ## Power Generation + ## Tesla Coils Lightning strikes can be harnessed using Tesla Coils, which convert the lightning strikes into power for the station. @@ -69,16 +52,34 @@ - Tesla Coils should be placed around the lightning ball to capture the lightning strikes, as well as to prevent the lightning from striking sensitive equipment further away. + Tesla Coils should be placed around the lightning ball to capture the energy from lightning strikes, as well as to prevent the lightning from striking sensitive equipment further away. Tesla Coils take damage every time they are struck by lightning, and will eventually break if not repaired. Be sure to monitor the condition of the Tesla Coils and repair them as needed. - Grounding rods, in contrast, do not take damage from lightning strikes. - When lightning strikes Tesla Coils, they fill an internal battery, which is rapidly discharged to the grid. It will discharge this power even if there is no consumer to take it, so it's a good idea to have an SMES nearby to store the power and discharge it smoothly. + ## Grounding Rods + Grounding Rods help protect sensitive equipment from being struck and prevent a loosed tesla (tesloose). + + + + + + + + + + + + + + Grounding rods do not take damage from lightning strikes. + This makes them beneficial for forming a saftey net of grounding rods to rely on in case the tesla coils are damaged or destroyed. + + Engineers should use grounding rods to protect sensitive equipment from lightning strikes, such as the Emitters powering the containment field generators. + ## Loosed Tesla (Tesloose) If the lightning ball escapes the containment field, it is referred to as a loosed tesla, or tesloose. diff --git a/Resources/ServerInfo/Guidebook/Engineering/Thermomachines.xml b/Resources/ServerInfo/Guidebook/Engineering/Thermomachines.xml index 64035cde19..7d449f33ed 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/Thermomachines.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/Thermomachines.xml @@ -1,6 +1,6 @@  # Thermomachines - Thermomachines are devices that manipulate the temperature of gases within a [textlink="pipe network" link="PipeNetworks"] or exposed atmosphere. + Thermomachines are devices that manipulate the temperature of gasses within a [textlink="pipe network" link="PipeNetworks"] or exposed atmosphere. @@ -8,10 +8,10 @@ They are essential for maintaining the temperature of gasses for various applications. - All thermomachines work by using [textlink="electrical power" link="Power"] to preform work on the atmosphere to either heat or cool it. - The amount of work they do is directly related to the amount of power they consume. + All thermomachines work by using [textlink="electrical power" link="Power"] to heat or cool the atmosphere. + How much they heat/cool the atmosphere is directly related to the amount of power they consume. - Thermomachines also have an efficiency coefficient, which determines how much work they can do per unit of power consumed. + Thermomachines also have an efficiency coefficient, which determines how much they can heat or cool the atmosphere per unit of power consumed. To prevent overshooting their target value, thermomachines will scale back their heating/cooling power as they approach the target temperature. However, they will still consume the same amount of electrical power, even when idle. @@ -19,7 +19,7 @@ All thermomachines have a target temperature tolerance of [color=orange][protodata="GasThermoMachineFreezer" comp="GasThermoMachine" member="TemperatureTolerance"/] K[/color], meaning they will stop heating or cooling when the temperature is within [color=orange][protodata="GasThermoMachineFreezer" comp="GasThermoMachine" member="TemperatureTolerance"/] K[/color] of the target temperature. ## Space Heater - The space heater is a portable temperature control unit that preforms work to heat or cool gas in the atmosphere it's exposed to. + The space heater is a portable temperature control unit that heats or cools gas in the atmosphere it's exposed to. It's a simple and effective way to maintain the temperature of a room, without having to build a pipenet or other system. @@ -30,7 +30,7 @@ The space heater can cool to as low as [color=orange][protodata="SpaceHeater" comp="SpaceHeater" member="MinTemperature"/] K[/color] and heat to as high as [color=orange][protodata="SpaceHeater" comp="SpaceHeater" member="MaxTemperature"/] K[/color]. - It also has three power settings, which determine how much power it consumes and how much work it does. + It also has three power settings which determine how fast it heats or cools the atmosphere. Botany or science will often request these to maintain the temperature of their plants or department. @@ -44,7 +44,7 @@ They draw [color=orange][protodata="GasThermoMachineFreezer" comp="GasThermoMachine" member="HeatCapacity" format="N0"/] W[/color] of power and can heat or cool gas in a pipenet to as high as [color=orange][protodata="GasThermoMachineFreezer" comp="GasThermoMachine" member="MaxTemperature"/] K[/color] or as low as [color=orange][protodata="GasThermoMachineFreezer" comp="GasThermoMachine" member="MinTemperature"/] K[/color]. - You can swap the mode of the thermomachine by deconstructing it and using a screwdriver on its board. + You can swap the mode of the thermomachine by deconstructing it and using a screwdriver on its circuit board. The board can be printed at a circuit imprinter, commonly found in Science. diff --git a/Resources/ServerInfo/Guidebook/Engineering/WirePanels.xml b/Resources/ServerInfo/Guidebook/Engineering/WirePanels.xml index 6ac749120b..aa86e6e9b3 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/WirePanels.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/WirePanels.xml @@ -10,16 +10,16 @@ - From here, you can use wirecutters and a multitool to interact with the wiring. Note that interacting with the wiring often requires insulated gloves, as if the wire is live, you can get shocked. + From here you can use wirecutters and a multitool to interact with the wiring. Note that interacting with the wiring often requires insulated gloves, as if the wire is live, you can get shocked. - You can cut and mend wires using the wirecutters, and pulse wires using the multitool. + You can cut and mend wires using the wirecutters and pulse wires using the multitool. Cutting wires often completely disables or restores functionality to a device. - It may also trigger unintended functionality, like shocking people, dropping door bolts, or exploding. + It may also trigger unintended functionality like shocking people, dropping door bolts, or exploding. Pulsing wires can have a variety of effects, but oftentimes it either temporarily disables or enables functionality. From b45613ad3395e3e6c1ee079fc0c3ba3037f6d8ee Mon Sep 17 00:00:00 2001 From: PJBot Date: Sat, 15 Feb 2025 04:00:27 +0000 Subject: [PATCH 038/529] Automatic changelog update --- Resources/Changelog/Changelog.yml | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 87f34230f6..a8f0fd717a 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: ArtisticRoomba - changes: - - message: HoS's energy shotgun is now correctly marked as grand theft contraband. - type: Tweak - id: 7455 - time: '2024-09-29T12:22:57.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/32521 - author: Ilya246 changes: - message: Steel cost of conveyor belt assemblies halved. @@ -3888,3 +3881,20 @@ id: 7954 time: '2025-02-14T15:01:41.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/35122 +- author: ArtisticRoomba + changes: + - message: The Solar Panels section of the Engineering Guidebook has been updated + to include information on upgrading solar panels, as well as how much each variant + produces. + type: Add + - message: The Tesla Engine section of the Engineering Guidebook has been slightly + reorganized to better outline the differences between Grounding Rods and Tesla + Coils, as well as tips on how to use them. + type: Tweak + - message: The Access Configurator section of the Engineering Guidebook has been + updated to reflect the recent addition of the Authentication Disruptor, as well + as how to fix access-broken doors and equipment the easy way. + type: Add + id: 7955 + time: '2025-02-15T03:59:19.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/34695 From 51a56e013c1a1a14b44edde50bc0ce579085419e Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Fri, 14 Feb 2025 23:29:40 -0500 Subject: [PATCH 039/529] Separate Udder examine into ExamineableHunger (#35164) * Separate udder hunger examine into ExamineableHunger * Fluent grammar improvements * Add ExamineableHunger to chickens and ducks. * Use starving message as "dead" message --- Content.Shared/Animals/UdderSystem.cs | 48 ------------------- .../Components/ExamineableHungerComponent.cs | 31 ++++++++++++ .../EntitySystems/ExamineableHungerSystem.cs | 41 ++++++++++++++++ .../en-US/animals/udder/udder-system.ftl | 6 --- .../examineable-hunger-component.ftl | 5 ++ .../Prototypes/Entities/Mobs/NPCs/animals.yml | 4 ++ 6 files changed, 81 insertions(+), 54 deletions(-) create mode 100644 Content.Shared/Nutrition/Components/ExamineableHungerComponent.cs create mode 100644 Content.Shared/Nutrition/EntitySystems/ExamineableHungerSystem.cs create mode 100644 Resources/Locale/en-US/nutrition/components/examineable-hunger-component.ftl diff --git a/Content.Shared/Animals/UdderSystem.cs b/Content.Shared/Animals/UdderSystem.cs index cb6e5b307f..177fbab2f2 100644 --- a/Content.Shared/Animals/UdderSystem.cs +++ b/Content.Shared/Animals/UdderSystem.cs @@ -1,7 +1,6 @@ using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.DoAfter; -using Content.Shared.Examine; using Content.Shared.IdentityManagement; using Content.Shared.Mobs.Systems; using Content.Shared.Nutrition.Components; @@ -32,7 +31,6 @@ public sealed class UdderSystem : EntitySystem SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent>(AddMilkVerb); SubscribeLocalEvent(OnDoAfter); - SubscribeLocalEvent(OnExamine); } private void OnMapInit(EntityUid uid, UdderComponent component, MapInitEvent args) @@ -140,50 +138,4 @@ public sealed class UdderSystem : EntitySystem }; args.Verbs.Add(verb); } - - /// - /// Defines the text provided on examine. - /// Changes depending on the amount of hunger the target has. - /// - private void OnExamine(Entity entity, ref ExaminedEvent args) - { - - var entityIdentity = Identity.Entity(args.Examined, EntityManager); - - string message; - - // Check if the target has hunger, otherwise return not hungry. - if (!TryComp(entity, out var hunger)) - { - message = Loc.GetString("udder-system-examine-none", ("entity", entityIdentity)); - args.PushMarkup(message); - return; - } - - // Choose the correct examine string based on HungerThreshold. - switch (_hunger.GetHungerThreshold(hunger)) - { - case >= HungerThreshold.Overfed: - message = Loc.GetString("udder-system-examine-overfed", ("entity", entityIdentity)); - break; - - case HungerThreshold.Okay: - message = Loc.GetString("udder-system-examine-okay", ("entity", entityIdentity)); - break; - - case HungerThreshold.Peckish: - message = Loc.GetString("udder-system-examine-hungry", ("entity", entityIdentity)); - break; - - // There's a final hunger threshold called "dead" but animals don't actually die so we'll re-use this. - case <= HungerThreshold.Starving: - message = Loc.GetString("udder-system-examine-starved", ("entity", entityIdentity)); - break; - - default: - return; - } - - args.PushMarkup(message); - } } diff --git a/Content.Shared/Nutrition/Components/ExamineableHungerComponent.cs b/Content.Shared/Nutrition/Components/ExamineableHungerComponent.cs new file mode 100644 index 0000000000..00aba82e58 --- /dev/null +++ b/Content.Shared/Nutrition/Components/ExamineableHungerComponent.cs @@ -0,0 +1,31 @@ +using Content.Shared.Nutrition.EntitySystems; +using Robust.Shared.GameStates; + +namespace Content.Shared.Nutrition.Components; + +/// +/// Adds text to the entity's description box based on its current hunger threshold. +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(ExamineableHungerSystem))] +public sealed partial class ExamineableHungerComponent : Component +{ + /// + /// Dictionary of hunger thresholds to LocIds of the messages to display. + /// + [DataField] + public Dictionary Descriptions = new() + { + { HungerThreshold.Overfed, "examineable-hunger-component-examine-overfed"}, + { HungerThreshold.Okay, "examineable-hunger-component-examine-okay"}, + { HungerThreshold.Peckish, "examineable-hunger-component-examine-peckish"}, + { HungerThreshold.Starving, "examineable-hunger-component-examine-starving"}, + { HungerThreshold.Dead, "examineable-hunger-component-examine-starving"} + }; + + /// + /// LocId of a fallback message to display if the entity has no + /// or does not have a value in for the current threshold. + /// + public LocId NoHungerDescription = "examineable-hunger-component-examine-none"; +} diff --git a/Content.Shared/Nutrition/EntitySystems/ExamineableHungerSystem.cs b/Content.Shared/Nutrition/EntitySystems/ExamineableHungerSystem.cs new file mode 100644 index 0000000000..e0ac767bcf --- /dev/null +++ b/Content.Shared/Nutrition/EntitySystems/ExamineableHungerSystem.cs @@ -0,0 +1,41 @@ +using Content.Shared.Examine; +using Content.Shared.IdentityManagement; +using Content.Shared.Nutrition.Components; + +namespace Content.Shared.Nutrition.EntitySystems; + +/// +public sealed class ExamineableHungerSystem : EntitySystem +{ + [Dependency] private readonly HungerSystem _hunger = default!; + private EntityQuery _hungerQuery; + + public override void Initialize() + { + base.Initialize(); + + _hungerQuery = GetEntityQuery(); + + SubscribeLocalEvent(OnExamine); + } + + /// + /// Defines the text provided on examine. + /// Changes depending on the amount of hunger the target has. + /// + private void OnExamine(Entity entity, ref ExaminedEvent args) + { + var identity = Identity.Entity(entity, EntityManager); + + if (!_hungerQuery.TryComp(entity, out var hungerComp) + || !entity.Comp.Descriptions.TryGetValue(_hunger.GetHungerThreshold(hungerComp), out var locId)) + { + // Use a fallback message if the entity has no HungerComponent + // or is missing a description for the current threshold + locId = entity.Comp.NoHungerDescription; + } + + var msg = Loc.GetString(locId, ("entity", identity)); + args.PushMarkup(msg); + } +} diff --git a/Resources/Locale/en-US/animals/udder/udder-system.ftl b/Resources/Locale/en-US/animals/udder/udder-system.ftl index 959a4fef59..8479ae08bf 100644 --- a/Resources/Locale/en-US/animals/udder/udder-system.ftl +++ b/Resources/Locale/en-US/animals/udder/udder-system.ftl @@ -5,9 +5,3 @@ udder-system-success = You fill {THE($target)} with {$amount}u from the udder. udder-system-dry = The udder is dry. udder-system-verb-milk = Milk - -udder-system-examine-overfed = {CAPITALIZE(SUBJECT($entity))} looks stuffed! -udder-system-examine-okay = {CAPITALIZE(SUBJECT($entity))} looks content. -udder-system-examine-hungry = {CAPITALIZE(SUBJECT($entity))} looks hungry. -udder-system-examine-starved = {CAPITALIZE(SUBJECT($entity))} looks starved! -udder-system-examine-none = {CAPITALIZE(SUBJECT($entity))} seems not to get hungry. diff --git a/Resources/Locale/en-US/nutrition/components/examineable-hunger-component.ftl b/Resources/Locale/en-US/nutrition/components/examineable-hunger-component.ftl new file mode 100644 index 0000000000..d8d9963908 --- /dev/null +++ b/Resources/Locale/en-US/nutrition/components/examineable-hunger-component.ftl @@ -0,0 +1,5 @@ +examineable-hunger-component-examine-overfed = {CAPITALIZE(SUBJECT($entity))} {CONJUGATE-BASIC($entity, "look", "looks")} stuffed! +examineable-hunger-component-examine-okay = {CAPITALIZE(SUBJECT($entity))} {CONJUGATE-BASIC($entity, "look", "looks")} content. +examineable-hunger-component-examine-peckish = {CAPITALIZE(SUBJECT($entity))} {CONJUGATE-BASIC($entity, "look", "looks")} hungry. +examineable-hunger-component-examine-starving = {CAPITALIZE(SUBJECT($entity))} {CONJUGATE-BASIC($entity, "look", "looks")} starved! +examineable-hunger-component-examine-none = {CAPITALIZE(SUBJECT($entity))} {CONJUGATE-BASIC($entity, "seem", "seems")} not to get hungry. diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index e71902db6b..a33529cc48 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -229,6 +229,7 @@ - type: EggLayer eggSpawn: - id: FoodEgg + - type: ExamineableHunger - type: ReplacementAccent accent: chicken - type: SentienceTarget @@ -663,6 +664,7 @@ - type: EggLayer eggSpawn: - id: FoodEgg + - type: ExamineableHunger - type: ReplacementAccent accent: duck - type: SentienceTarget @@ -828,6 +830,7 @@ reagentId: Milk quantityPerUpdate: 25 growthDelay: 30 + - type: ExamineableHunger - type: Butcherable spawned: - id: FoodMeat @@ -984,6 +987,7 @@ reagentId: MilkGoat quantityPerUpdate: 25 growthDelay: 20 + - type: ExamineableHunger - type: Wooly - type: Food solution: wool From 9042827d45ac9255ea9797957f6f3b6f79856651 Mon Sep 17 00:00:00 2001 From: PJBot Date: Sat, 15 Feb 2025 04:30:47 +0000 Subject: [PATCH 040/529] Automatic changelog update --- Resources/Changelog/Changelog.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index a8f0fd717a..1c74b7f64c 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: Ilya246 - changes: - - message: Steel cost of conveyor belt assemblies halved. - type: Tweak - id: 7456 - time: '2024-09-29T15:18:09.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/32444 - author: CuteMoonGod changes: - message: Fixed execution system showing character name instead of their identity @@ -3898,3 +3891,10 @@ id: 7955 time: '2025-02-15T03:59:19.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/34695 +- author: Tayrtahn + changes: + - message: Chickens and ducks now report hunger levels when examined. + type: Add + id: 7956 + time: '2025-02-15T04:29:41.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/35164 From 44747278869ef8b084ee5c15e05f43a62d9f6fc0 Mon Sep 17 00:00:00 2001 From: Kyle Tyo <36606155+VerinSenpai@users.noreply.github.com> Date: Sat, 15 Feb 2025 01:07:15 -0500 Subject: [PATCH 041/529] Fix air devices ignoring settings after power cycle (#34887) * add powered variable to vent components * add checks for powered to vent systems also corrected onpowerchanged methods to update powered arg. * removed powered from components * Use ApcPowerReceieverComponent for power state. * removed unneeded code from OnPowerChanged * document what enabled is used for in components * only you can prevent oopsie daisies. * add check for powered in OnGasVentPumpUpdated * apcPowerReceiverComponent BEGONE * CODE RED EVERYTHINGS ON FIRE wait we're fine now. --- .../Piping/Unary/Components/GasVentPumpComponent.cs | 6 +++++- .../Unary/Components/GasVentScrubberComponent.cs | 6 +++++- .../Piping/Unary/EntitySystems/GasVentPumpSystem.cs | 11 +++++++---- .../Unary/EntitySystems/GasVentScrubberSystem.cs | 8 ++++++-- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/Content.Server/Atmos/Piping/Unary/Components/GasVentPumpComponent.cs b/Content.Server/Atmos/Piping/Unary/Components/GasVentPumpComponent.cs index 25b15f0ed5..a9aa40611d 100644 --- a/Content.Server/Atmos/Piping/Unary/Components/GasVentPumpComponent.cs +++ b/Content.Server/Atmos/Piping/Unary/Components/GasVentPumpComponent.cs @@ -11,8 +11,12 @@ namespace Content.Server.Atmos.Piping.Unary.Components [RegisterComponent] public sealed partial class GasVentPumpComponent : Component { + /// + /// Identifies if the device is enabled by an air alarm. Does not indicate if the device is powered. + /// By default, all air vents start enabled, whether linked to an alarm or not. + /// [ViewVariables(VVAccess.ReadWrite)] - public bool Enabled { get; set; } = false; + public bool Enabled { get; set; } = true; [ViewVariables] public bool IsDirty { get; set; } = false; diff --git a/Content.Server/Atmos/Piping/Unary/Components/GasVentScrubberComponent.cs b/Content.Server/Atmos/Piping/Unary/Components/GasVentScrubberComponent.cs index b2143283f7..4a9437bc1f 100644 --- a/Content.Server/Atmos/Piping/Unary/Components/GasVentScrubberComponent.cs +++ b/Content.Server/Atmos/Piping/Unary/Components/GasVentScrubberComponent.cs @@ -9,8 +9,12 @@ namespace Content.Server.Atmos.Piping.Unary.Components [Access(typeof(GasVentScrubberSystem))] public sealed partial class GasVentScrubberComponent : Component { + /// + /// Identifies if the device is enabled by an air alarm. Does not indicate if the device is powered. + /// By default, all air scrubbers start enabled, whether linked to an alarm or not. + /// [DataField] - public bool Enabled { get; set; } = false; + public bool Enabled { get; set; } = true; [DataField] public bool IsDirty { get; set; } = false; diff --git a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentPumpSystem.cs b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentPumpSystem.cs index c58d6eb14b..93f7dcf111 100644 --- a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentPumpSystem.cs +++ b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentPumpSystem.cs @@ -9,6 +9,8 @@ using Content.Server.DeviceNetwork.Components; using Content.Server.DeviceNetwork.Systems; using Content.Server.NodeContainer.EntitySystems; using Content.Server.NodeContainer.Nodes; +using Content.Server.Power.Components; +using Content.Server.Power.EntitySystems; using Content.Shared.Administration.Logs; using Content.Shared.Atmos; using Content.Shared.Atmos.Monitor; @@ -43,6 +45,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems [Dependency] private readonly SharedToolSystem _toolSystem = default!; [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!; public override void Initialize() { base.Initialize(); @@ -66,9 +69,10 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems { //Bingo waz here if (_weldable.IsWelded(uid)) - { return; - } + + if (!_powerReceiverSystem.IsPowered(uid)) + return; var nodeName = vent.PumpDirection switch { @@ -210,7 +214,6 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems private void OnPowerChanged(EntityUid uid, GasVentPumpComponent component, ref PowerChangedEvent args) { - component.Enabled = args.Powered; UpdateState(uid, component); } @@ -318,7 +321,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems _ambientSoundSystem.SetAmbience(uid, false); _appearance.SetData(uid, VentPumpVisuals.State, VentPumpState.Welded, appearance); } - else if (!vent.Enabled) + else if (!_powerReceiverSystem.IsPowered(uid) || !vent.Enabled) { _ambientSoundSystem.SetAmbience(uid, false); _appearance.SetData(uid, VentPumpVisuals.State, VentPumpState.Off, appearance); diff --git a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentScrubberSystem.cs b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentScrubberSystem.cs index 0207535398..38d75701d7 100644 --- a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentScrubberSystem.cs +++ b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasVentScrubberSystem.cs @@ -10,6 +10,7 @@ using Content.Server.NodeContainer; using Content.Server.NodeContainer.EntitySystems; using Content.Server.NodeContainer.Nodes; using Content.Server.Power.Components; +using Content.Server.Power.EntitySystems; using Content.Shared.Administration.Logs; using Content.Shared.Atmos; using Content.Shared.Atmos.Piping.Unary.Visuals; @@ -37,6 +38,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems [Dependency] private readonly TransformSystem _transformSystem = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly WeldableSystem _weldable = default!; + [Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!; public override void Initialize() { @@ -58,6 +60,9 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems var timeDelta = args.dt; + if (!_powerReceiverSystem.IsPowered(uid)) + return; + if (!scrubber.Enabled || !_nodeContainer.TryGetNode(uid, scrubber.OutletName, out PipeNode? outlet)) return; @@ -141,7 +146,6 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems private void OnPowerChanged(EntityUid uid, GasVentScrubberComponent component, ref PowerChangedEvent args) { - component.Enabled = args.Powered; UpdateState(uid, component); } @@ -225,7 +229,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems _ambientSoundSystem.SetAmbience(uid, false); _appearance.SetData(uid, ScrubberVisuals.State, ScrubberState.Welded, appearance); } - else if (!scrubber.Enabled) + else if (!_powerReceiverSystem.IsPowered(uid) || !scrubber.Enabled) { _ambientSoundSystem.SetAmbience(uid, false); _appearance.SetData(uid, ScrubberVisuals.State, ScrubberState.Off, appearance); From 97d6111614582f8e86352cc85ab8cdd2d888bdda Mon Sep 17 00:00:00 2001 From: PJBot Date: Sat, 15 Feb 2025 06:08:21 +0000 Subject: [PATCH 042/529] Automatic changelog update --- Resources/Changelog/Changelog.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 1c74b7f64c..7423194135 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: CuteMoonGod - changes: - - message: Fixed execution system showing character name instead of their identity - type: Fix - id: 7457 - time: '2024-09-29T22:36:47.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/32536 - author: drakewill-CRL changes: - message: Fix error in gas exchange processing order. @@ -3898,3 +3891,11 @@ id: 7956 time: '2025-02-15T04:29:41.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/35164 +- author: VerinSenpai + changes: + - message: Air alarm devices now return to their previous settings after a power + cycle. + type: Fix + id: 7957 + time: '2025-02-15T06:07:15.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/34887 From 71b5133a5395b3ebe0d2af31055d6dcaa65a1b78 Mon Sep 17 00:00:00 2001 From: Simon <63975668+Simyon264@users.noreply.github.com> Date: Sat, 15 Feb 2025 10:01:46 +0100 Subject: [PATCH 043/529] Fix colornetwork command not checking for correct permissions (#35180) Fix colornetwork command not checking for correct permissions. What is shell.IsClient even?? --- Content.Server/Sandbox/Commands/ColorNetworkCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Server/Sandbox/Commands/ColorNetworkCommand.cs b/Content.Server/Sandbox/Commands/ColorNetworkCommand.cs index 1fc207058d..0a778f5880 100644 --- a/Content.Server/Sandbox/Commands/ColorNetworkCommand.cs +++ b/Content.Server/Sandbox/Commands/ColorNetworkCommand.cs @@ -19,7 +19,7 @@ namespace Content.Server.Sandbox.Commands { var sandboxManager = _entManager.System(); var adminManager = IoCManager.Resolve(); - if (shell.IsClient && (!sandboxManager.IsSandboxEnabled && !adminManager.HasAdminFlag(shell.Player!, AdminFlags.Mapping))) + if (shell.IsClient || (!sandboxManager.IsSandboxEnabled && !adminManager.HasAdminFlag(shell.Player!, AdminFlags.Mapping))) { shell.WriteError(Loc.GetString("cmd-colornetwork-no-access")); } From 0cb11241d79c8e51f5a1391c1fc49bc063144ccc Mon Sep 17 00:00:00 2001 From: keronshb <54602815+keronshb@users.noreply.github.com> Date: Sat, 15 Feb 2025 11:17:29 -0500 Subject: [PATCH 044/529] Store Refund - Add more disable scenerios & time to disable refund. (#34671) * Puts disable refund logic into a helper method. Removes action check. Disables refund on action use. * Adds check if refund is disabled for the store already * Adds a way to disable refunds based on time * Checks if the ent is on the starting map and the end time is below the current time before disabling refund * Replaces instances of component.RefundAllowed = false with DisableRefund for more consistency and easier tracking * Adds methods to handle inhand and shooting events * Removes gamestates --------- Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> --- Content.Server/Store/StoreRefundComponent.cs | 21 +++++++- .../Store/Systems/StoreSystem.Refund.cs | 51 ++++++++++++++++--- .../Store/Systems/StoreSystem.Ui.cs | 5 +- Content.Server/Store/Systems/StoreSystem.cs | 2 + 4 files changed, 68 insertions(+), 11 deletions(-) diff --git a/Content.Server/Store/StoreRefundComponent.cs b/Content.Server/Store/StoreRefundComponent.cs index 1a6b17c5ea..df35afdf53 100644 --- a/Content.Server/Store/StoreRefundComponent.cs +++ b/Content.Server/Store/StoreRefundComponent.cs @@ -2,12 +2,31 @@ namespace Content.Server.Store.Components; +// TODO: Refund on a per-item/action level. +// Requires a refund button next to each purchase (disabled/invis by default) +// Interactions with ActionUpgrades would need to be modified to reset all upgrade progress and return the original action purchase to the store. + /// /// Keeps track of entities bought from stores for refunds, especially useful if entities get deleted before they can be refunded. /// [RegisterComponent, Access(typeof(StoreSystem))] public sealed partial class StoreRefundComponent : Component { - [ViewVariables, DataField] + /// + /// The store this entity was bought from + /// + [DataField] public EntityUid? StoreEntity; + + /// + /// The time this entity was bought + /// + [DataField] + public TimeSpan? BoughtTime; + + /// + /// How long until this entity disables refund purchase? + /// + [DataField] + public TimeSpan DisableTime = TimeSpan.FromSeconds(300); } diff --git a/Content.Server/Store/Systems/StoreSystem.Refund.cs b/Content.Server/Store/Systems/StoreSystem.Refund.cs index 04bd585ffc..e9d801f9e1 100644 --- a/Content.Server/Store/Systems/StoreSystem.Refund.cs +++ b/Content.Server/Store/Systems/StoreSystem.Refund.cs @@ -1,5 +1,8 @@ using Content.Server.Store.Components; +using Content.Shared.Actions.Events; +using Content.Shared.Interaction.Events; using Content.Shared.Store.Components; +using Content.Shared.Weapons.Ranged.Systems; using Robust.Shared.Containers; namespace Content.Server.Store.Systems; @@ -12,22 +15,39 @@ public sealed partial class StoreSystem SubscribeLocalEvent(OnRefundTerminating); SubscribeLocalEvent(OnEntityRemoved); SubscribeLocalEvent(OnEntityInserted); + SubscribeLocalEvent(OnActionPerformed); + SubscribeLocalEvent(OnUseInHand); + SubscribeLocalEvent(OnShootAttempt); + // TODO: Handle guardian refund disabling when guardians support refunds. } - private void OnEntityRemoved(EntityUid uid, StoreRefundComponent component, EntRemovedFromContainerMessage args) + private void OnEntityRemoved(Entity ent, ref EntRemovedFromContainerMessage args) { - if (component.StoreEntity == null || _actions.TryGetActionData(uid, out _, false) || !TryComp(component.StoreEntity.Value, out var storeComp)) - return; - - DisableRefund(component.StoreEntity.Value, storeComp); + CheckDisableRefund(ent); } - private void OnEntityInserted(EntityUid uid, StoreRefundComponent component, EntInsertedIntoContainerMessage args) + private void OnEntityInserted(Entity ent, ref EntInsertedIntoContainerMessage args) { - if (component.StoreEntity == null || _actions.TryGetActionData(uid, out _) || !TryComp(component.StoreEntity.Value, out var storeComp)) + CheckDisableRefund(ent); + } + + private void OnActionPerformed(Entity ent, ref ActionPerformedEvent args) + { + CheckDisableRefund(ent); + } + + private void OnUseInHand(Entity ent, ref UseInHandEvent args) + { + args.Handled = true; + CheckDisableRefund(ent); + } + + private void OnShootAttempt(Entity ent, ref AttemptShootEvent args) + { + if (args.Cancelled) return; - DisableRefund(component.StoreEntity.Value, storeComp); + CheckDisableRefund(ent); } private void OnStoreTerminating(Entity ent, ref EntityTerminatingEvent args) @@ -52,4 +72,19 @@ public sealed partial class StoreSystem var ev = new RefundEntityDeletedEvent(ent); RaiseLocalEvent(ent.Comp.StoreEntity.Value, ref ev); } + + private void CheckDisableRefund(Entity ent) + { + var component = ent.Comp; + + if (component.StoreEntity == null || !TryComp(component.StoreEntity.Value, out var storeComp) || !storeComp.RefundAllowed) + return; + + var endTime = component.BoughtTime + component.DisableTime; + + if (IsOnStartingMap(component.StoreEntity.Value, storeComp) && _timing.CurTime < endTime) + return; + + DisableRefund(component.StoreEntity.Value, storeComp); + } } diff --git a/Content.Server/Store/Systems/StoreSystem.Ui.cs b/Content.Server/Store/Systems/StoreSystem.Ui.cs index 5af6ce1c97..3f4ccf696d 100644 --- a/Content.Server/Store/Systems/StoreSystem.Ui.cs +++ b/Content.Server/Store/Systems/StoreSystem.Ui.cs @@ -164,7 +164,7 @@ public sealed partial class StoreSystem } if (!IsOnStartingMap(uid, component)) - component.RefundAllowed = false; + DisableRefund(uid, component); //subtract the cash foreach (var (currency, amount) in cost) @@ -332,7 +332,7 @@ public sealed partial class StoreSystem if (!IsOnStartingMap(uid, component)) { - component.RefundAllowed = false; + DisableRefund(uid, component); UpdateUserInterface(buyer, uid, component); } @@ -376,6 +376,7 @@ public sealed partial class StoreSystem component.BoughtEntities.Add(purchase); var refundComp = EnsureComp(purchase); refundComp.StoreEntity = uid; + refundComp.BoughtTime = _timing.CurTime; } private bool IsOnStartingMap(EntityUid store, StoreComponent component) diff --git a/Content.Server/Store/Systems/StoreSystem.cs b/Content.Server/Store/Systems/StoreSystem.cs index a57895364d..0625ced087 100644 --- a/Content.Server/Store/Systems/StoreSystem.cs +++ b/Content.Server/Store/Systems/StoreSystem.cs @@ -10,6 +10,7 @@ using JetBrains.Annotations; using Robust.Shared.Prototypes; using Robust.Shared.Utility; using System.Linq; +using Robust.Shared.Timing; using Content.Shared.Mind; namespace Content.Server.Store.Systems; @@ -22,6 +23,7 @@ public sealed partial class StoreSystem : EntitySystem { [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly IGameTiming _timing = default!; public override void Initialize() { From d798d4d3f567ee5311105950f2cd5a59d6c9fcf2 Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Sun, 16 Feb 2025 06:52:47 +1100 Subject: [PATCH 045/529] Update StaticFieldValidationTest (#34287) --- .../Tests/Linter/StaticFieldValidationTest.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/Content.IntegrationTests/Tests/Linter/StaticFieldValidationTest.cs b/Content.IntegrationTests/Tests/Linter/StaticFieldValidationTest.cs index 0632fe1347..90bf82e8f1 100644 --- a/Content.IntegrationTests/Tests/Linter/StaticFieldValidationTest.cs +++ b/Content.IntegrationTests/Tests/Linter/StaticFieldValidationTest.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using Content.Shared.Tag; +using Robust.Shared.GameObjects; using Robust.Shared.Prototypes; using Robust.Shared.Reflection; using Robust.Shared.Serialization.Manager.Attributes; @@ -29,7 +30,9 @@ public sealed class StaticFieldValidationTest Assert.That(protoMan.ValidateStaticFields(typeof(StringValid), protos), Is.Empty); Assert.That(protoMan.ValidateStaticFields(typeof(StringArrayValid), protos), Is.Empty); Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdValid), protos), Is.Empty); + Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdTValid), protos), Is.Empty); Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdArrayValid), protos), Is.Empty); + Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdTArrayValid), protos), Is.Empty); Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdTestValid), protos), Is.Empty); Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdArrayValid), protos), Is.Empty); Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdListValid), protos), Is.Empty); @@ -39,7 +42,9 @@ public sealed class StaticFieldValidationTest Assert.That(protoMan.ValidateStaticFields(typeof(StringInvalid), protos), Has.Count.EqualTo(1)); Assert.That(protoMan.ValidateStaticFields(typeof(StringArrayInvalid), protos), Has.Count.EqualTo(2)); Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdInvalid), protos), Has.Count.EqualTo(1)); + Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdTInvalid), protos), Has.Count.EqualTo(1)); Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdArrayInvalid), protos), Has.Count.EqualTo(2)); + Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdTArrayInvalid), protos), Has.Count.EqualTo(2)); Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdTestInvalid), protos), Has.Count.EqualTo(1)); Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdArrayInvalid), protos), Has.Count.EqualTo(2)); Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdListInvalid), protos), Has.Count.EqualTo(2)); @@ -88,24 +93,48 @@ public sealed class StaticFieldValidationTest public static EntProtoId Tag = "StaticFieldTestEnt"; } + [Reflect(false)] + private sealed class EntProtoIdTValid + { + public static EntProtoId Tag = "StaticFieldTestEnt"; + } + [Reflect(false)] private sealed class EntProtoIdInvalid { public static EntProtoId Tag = string.Empty; } + [Reflect(false)] + private sealed class EntProtoIdTInvalid + { + public static EntProtoId Tag = string.Empty; + } + [Reflect(false)] private sealed class EntProtoIdArrayValid { public static EntProtoId[] Tag = ["StaticFieldTestEnt", "StaticFieldTestEnt"]; } + [Reflect(false)] + private sealed class EntProtoIdTArrayValid + { + public static EntProtoId[] Tag = ["StaticFieldTestEnt", "StaticFieldTestEnt"]; + } + [Reflect(false)] private sealed class EntProtoIdArrayInvalid { public static EntProtoId[] Tag = [string.Empty, "StaticFieldTestEnt", string.Empty]; } + [Reflect(false)] + private sealed class EntProtoIdTArrayInvalid + { + public static EntProtoId[] Tag = [string.Empty, "StaticFieldTestEnt", string.Empty]; + } + [Reflect(false)] private sealed class ProtoIdTestValid { From 831dbef591d45f606f1e0f063304622306ebb5e8 Mon Sep 17 00:00:00 2001 From: Kyle Tyo <36606155+VerinSenpai@users.noreply.github.com> Date: Sat, 15 Feb 2025 21:01:58 -0500 Subject: [PATCH 046/529] move a colon to the localization string (#35192) * move the colon to the localization string * remove a redundancy * beck suggested this per how its done elsewhere. * comply with requested changes. --- .../CriminalRecords/CriminalRecordsConsoleWindow.xaml | 4 +--- .../en-US/medical/components/crew-monitoring-component.ftl | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Content.Client/CriminalRecords/CriminalRecordsConsoleWindow.xaml b/Content.Client/CriminalRecords/CriminalRecordsConsoleWindow.xaml index d36718cf08..179304a978 100644 --- a/Content.Client/CriminalRecords/CriminalRecordsConsoleWindow.xaml +++ b/Content.Client/CriminalRecords/CriminalRecordsConsoleWindow.xaml @@ -58,9 +58,7 @@ StyleClasses="LabelBig" /> -