diff --git a/Content.Server/Administration/Commands/PersistenceSaveCommand.cs b/Content.Server/Administration/Commands/PersistenceSaveCommand.cs new file mode 100644 index 0000000000..2684e85d5f --- /dev/null +++ b/Content.Server/Administration/Commands/PersistenceSaveCommand.cs @@ -0,0 +1,60 @@ +using Content.Server.GameTicking; +using Content.Server.Ghost.Components; +using Content.Server.Players; +using Content.Shared.Administration; +using Content.Shared.CCVar; +using Content.Shared.Ghost; +using Robust.Server.GameObjects; +using Robust.Server.Player; +using Robust.Shared.Configuration; +using Robust.Shared.Console; +using Robust.Shared.Map; +using System.Linq; + +namespace Content.Server.Administration.Commands; + +[AdminCommand(AdminFlags.Server)] +public sealed class PersistenceSave : IConsoleCommand +{ + [Dependency] private readonly IConfigurationManager _config = default!; + [Dependency] private readonly IEntityManager _entities = default!; + [Dependency] private readonly IEntitySystemManager _system = default!; + [Dependency] private readonly IMapManager _map = default!; + + public string Command => "persistencesave"; + public string Description => "Saves server data to a persistence file to be loaded later."; + public string Help => "persistencesave [mapId] [filePath - default: game.map (CCVar) ]"; + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length < 1 || args.Length > 2) + { + shell.WriteError(Loc.GetString("shell-wrong-arguments-number")); + return; + } + + if (!int.TryParse(args[0], out var intMapId)) + { + shell.WriteError(Loc.GetString("cmd-parse-failure-integer", ("arg", args[0]))); + return; + } + + var mapId = new MapId(intMapId); + if (!_map.MapExists(mapId)) + { + shell.WriteError(Loc.GetString("cmd-savemap-not-exist")); + return; + } + + var saveFilePath = (args.Length > 1 ? args[1] : null) ?? _config.GetCVar(CCVars.GameMap); + if (string.IsNullOrWhiteSpace(saveFilePath)) + { + shell.WriteError(Loc.GetString("cmd-persistencesave-no-path", ("cvar", nameof(CCVars.GameMap)))); + return; + } + + var mapLoader = _system.GetEntitySystem(); + mapLoader.SaveMap(mapId, saveFilePath); + shell.WriteLine(Loc.GetString("cmd-savemap-success")); + } +} diff --git a/Content.Server/Maps/GameMapManager.cs b/Content.Server/Maps/GameMapManager.cs index 69f732151f..9638dffaa7 100644 --- a/Content.Server/Maps/GameMapManager.cs +++ b/Content.Server/Maps/GameMapManager.cs @@ -5,8 +5,10 @@ using Content.Server.GameTicking; using Content.Shared.CCVar; using Robust.Server.Player; using Robust.Shared.Configuration; +using Robust.Shared.ContentPack; using Robust.Shared.Prototypes; using Robust.Shared.Random; +using Robust.Shared.Utility; namespace Content.Server.Maps; @@ -16,6 +18,7 @@ public sealed class GameMapManager : IGameMapManager [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IConfigurationManager _configurationManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IResourceManager _resMan = default!; [Dependency] private readonly IRobustRandom _random = default!; [ViewVariables(VVAccess.ReadOnly)] @@ -40,18 +43,34 @@ public sealed class GameMapManager : IGameMapManager if (TryLookupMap(value, out GameMapPrototype? map)) { _configSelectedMap = map; + return; } - else + + if (string.IsNullOrEmpty(value)) { - if (string.IsNullOrEmpty(value)) - { - _configSelectedMap = default!; - } - else - { - _log.Error($"Unknown map prototype {value} was selected!"); - } + _configSelectedMap = default!; + return; } + + if (_configurationManager.GetCVar(CCVars.UsePersistence)) + { + var startMap = _configurationManager.GetCVar(CCVars.PersistenceMap); + _configSelectedMap = _prototypeManager.Index(startMap); + + var mapPath = new ResPath(value); + if (_resMan.UserData.Exists(mapPath)) + { + _configSelectedMap = _configSelectedMap.Persistence(mapPath); + _log.Info($"Using persistence map from {value}"); + return; + } + + // persistence save path doesn't exist so we just use the start map + _log.Warning($"Using persistence start map {startMap} as {value} doesn't exist"); + return; + } + + _log.Error($"Unknown map prototype {value} was selected!"); }, true); _configurationManager.OnValueChanged(CCVars.GameMapRotation, value => _mapRotationEnabled = value, true); _configurationManager.OnValueChanged(CCVars.GameMapMemoryDepth, value => diff --git a/Content.Server/Maps/GameMapPrototype.cs b/Content.Server/Maps/GameMapPrototype.cs index 837e838290..bd15194495 100644 --- a/Content.Server/Maps/GameMapPrototype.cs +++ b/Content.Server/Maps/GameMapPrototype.cs @@ -40,4 +40,18 @@ public sealed partial class GameMapPrototype : IPrototype /// The stations this map contains. The names should match with the BecomesStation components. /// public IReadOnlyDictionary Stations => _stations; + + /// + /// Performs a shallow clone of this map prototype, replacing MapPath with the argument. + /// + public GameMapPrototype Persistence(ResPath mapPath) + { + return new() + { + ID = ID, + MapName = MapName, + MapPath = mapPath, + _stations = _stations + }; + } } diff --git a/Content.Server/Station/Components/StationMemberComponent.cs b/Content.Server/Station/Components/StationMemberComponent.cs index 7213d2549e..2100c20c55 100644 --- a/Content.Server/Station/Components/StationMemberComponent.cs +++ b/Content.Server/Station/Components/StationMemberComponent.cs @@ -11,6 +11,6 @@ public sealed partial class StationMemberComponent : Component /// /// Station that this grid is a part of. /// - [ViewVariables] + [DataField] public EntityUid Station = EntityUid.Invalid; } diff --git a/Content.Server/Station/Systems/StationSystem.cs b/Content.Server/Station/Systems/StationSystem.cs index 65eabd8190..8f9e634c09 100644 --- a/Content.Server/Station/Systems/StationSystem.cs +++ b/Content.Server/Station/Systems/StationSystem.cs @@ -331,7 +331,7 @@ public sealed class StationSystem : EntitySystem if (!string.IsNullOrEmpty(name)) _metaData.SetEntityName(mapGrid, name); - var stationMember = AddComp(mapGrid); + var stationMember = EnsureComp(mapGrid); stationMember.Station = station; stationData.Grids.Add(mapGrid); diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index b29e227350..2e1589a7e9 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -179,6 +179,19 @@ namespace Content.Shared.CCVar public static readonly CVarDef GameMap = CVarDef.Create("game.map", string.Empty, CVar.SERVERONLY); + /// + /// Controls whether to use world persistence or not. + /// + public static readonly CVarDef + UsePersistence = CVarDef.Create("game.usepersistence", false, CVar.ARCHIVE); + + /// + /// If world persistence is used, what map prototype should be initially loaded. + /// If the save file exists, it replaces MapPath but everything else stays the same (station name and such). + /// + public static readonly CVarDef + PersistenceMap = CVarDef.Create("game.persistencemap", "Empty", CVar.ARCHIVE); + /// /// Prototype to use for map pool. /// diff --git a/Resources/Locale/en-US/persistence/command.ftl b/Resources/Locale/en-US/persistence/command.ftl new file mode 100644 index 0000000000..b070aee115 --- /dev/null +++ b/Resources/Locale/en-US/persistence/command.ftl @@ -0,0 +1 @@ +cmd-persistencesave-no-path = filePath was not specified and CCVar {$cvar} is not set. Manually set the filePath param in order to save the map.