Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
cbfd1a0
Save/Load game MapLoader methods
Roudenn Aug 24, 2025
acb1511
Improve map serialization error logging
ElectroJr Sep 9, 2025
3779201
Prevent remove children of erroring entities
ElectroJr Sep 9, 2025
c5b887a
better logging
ElectroJr Sep 9, 2025
e089da2
Improve error tolerance
ElectroJr Sep 9, 2025
2ce4b5b
Even more exception tolerance
ElectroJr Sep 9, 2025
d0884c9
missing !
ElectroJr Sep 9, 2025
016cf86
Add WriteYaml and WriteObject to IReplayFileWriter
ElectroJr Sep 9, 2025
ebfa4bd
Add MapLoaderSystem.TrySaveAllEntities()
ElectroJr Sep 9, 2025
ecba27b
On second thought, WriteObject will just be abused
ElectroJr Sep 9, 2025
6d4bc2c
I forgot to commit
ElectroJr Sep 10, 2025
b74b518
Add default implementation to avoid breaking changes
ElectroJr Sep 11, 2025
02866f1
release notes
ElectroJr Sep 11, 2025
b247741
Merge branch 'master' of https://github.com/space-wizards/RobustToolb…
ElectroJr Sep 23, 2025
973ebec
fix merge issues
ElectroJr Sep 23, 2025
2bc5fb5
Merge remote-tracking branch 'upstream/master' into game-saving
Roudenn Oct 26, 2025
bce8ec2
Add GameSavesSystem
Roudenn Oct 26, 2025
85499d0
Merge remote-tracking branch 'upstream/master' into game-saving
Roudenn Oct 27, 2025
01fdeb7
Temporarely implement #6189
Roudenn Nov 13, 2025
a92fc24
Merge remote-tracking branch 'upstream/master' into game-saving
Roudenn Nov 13, 2025
00c76d8
Try to implement ZSTD compression (and fail)
Roudenn Nov 13, 2025
6ef80a5
Revert "Temporarely implement #6189"
Roudenn Nov 13, 2025
b5eb550
Merge remote-tracking branch 'upstream/master' into game-saving
Roudenn Nov 13, 2025
c09a612
Improve savegame and loadgame commands
Roudenn Nov 13, 2025
e2c47a6
hmm...
HacksLua Nov 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Resources/Locale/en-US/commands.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,20 @@ cmd-hint-loadmap-y-position = [y-position]
cmd-hint-loadmap-rotation = [rotation]
cmd-hint-loadmap-uids = [float]

cmd-savegame-desc = Serializes all game entities to disk. Will save all entities, paused an unpaused.
cmd-savegame-help = savegame <Path>
cmd-savegame-attempt = Attempting to save full game state to {$path}.
cmd-savegame-success = Game state successfully saved.
cmd-savegame-error = Could not save the game state! See server log for details.
cmd-savegame-disabled = Game saves are disabled on this server.

cmd-loadgame-desc = Loads a full game state from disk into the game. Flushes all existing entities
cmd-loadgame-help = loadgame <Path>
cmd-loadgame-attempt = Attempting to load full game state from {$path}.
cmd-loadgame-success = Game state successfully loaded.
cmd-loadgame-error = Could not load the game state! See server log for details.
cmd-loadgame-disabled = Game saves are disabled on this server.

cmd-hint-savebp-id = <Grid EntityID>

## 'flushcookies' command
Expand Down
8 changes: 4 additions & 4 deletions Robust.Client/GameObjects/EntitySystems/ContainerSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ private void HandleComponentState(EntityUid uid, ContainerManagerComponent compo
if (!component.Containers.TryGetValue(id, out var container))
{
var type = _serializer.FindSerializedType(typeof(BaseContainer), data.ContainerType);
container = _dynFactory.CreateInstanceUnchecked<BaseContainer>(type!, inject:false);
container = _dynFactory.CreateInstanceUnchecked<BaseContainer>(type!, inject: false);
container.Init(this, id, (uid, component));
component.Containers.Add(id, container);
}
Expand Down Expand Up @@ -169,15 +169,15 @@ private void HandleComponentState(EntityUid uid, ContainerManagerComponent compo
{
var entity = stateEnts[i];
var netEnt = stateNetEnts[i];

if (!entity.IsValid())
{
DebugTools.Assert(netEnt.IsValid());
AddExpectedEntity(netEnt, container);
if (netEnt.IsValid())
AddExpectedEntity(netEnt, container);
continue;
}

var meta = MetaData(entity);
DebugTools.Assert(meta.NetEntity == netEnt);

// If an entity is currently in the shadow realm, it means we probably left PVS and are now getting
// back into range. We do not want to directly insert this entity, as IF the container and entity
Expand Down
111 changes: 111 additions & 0 deletions Robust.Server/Console/Commands/MapCommands.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
using System.Linq;
using System.Numerics;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.ContentPack;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.GameSaves;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map;
Expand Down Expand Up @@ -334,4 +337,112 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
shell.WriteLine(Loc.GetString("cmd-loadmap-error", ("path", args[1])));
}
}

public sealed class SaveGame : LocalizedCommands
{
[Dependency] private readonly IEntitySystemManager _system = default!;
[Dependency] private readonly IResourceManager _resource = default!;
[Dependency] private readonly IConfigurationManager _config = default!;

public override string Command => "savegame";

public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
switch (args.Length)
{
case 1:
var opts = CompletionHelper.UserFilePath(args[0], _resource.UserData);
return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-hint-savemap-path"));
case 2:
return CompletionResult.FromHint(Loc.GetString("cmd-hint-savemap-force"));
}
return CompletionResult.Empty;
}

public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (!_config.GetCVar(CVars.GameSavesEnabled))
{
shell.WriteLine(Loc.GetString("cmd-savegame-disabled"));
return;
}

if (args.Length < 1)
{
shell.WriteLine(Help);
return;
}

shell.WriteLine(Loc.GetString("cmd-savegame-attempt", ("path", args[0])));
bool saveSuccess = _system.GetEntitySystem<GameSavesSystem>().TrySaveGame(new ResPath(args[0]));
if(saveSuccess)
{
shell.WriteLine(Loc.GetString("cmd-savegame-success"));
}
else
{
shell.WriteError(Loc.GetString("cmd-savegame-error"));
}
}
}

public sealed class LoadGame : LocalizedCommands
{
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IEntitySystemManager _system = default!;
[Dependency] private readonly IResourceManager _resource = default!;
[Dependency] private readonly IConfigurationManager _config = default!;

public override string Command => "loadgame";

public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
switch (args.Length)
{
case 1:
var opts = CompletionHelper.UserFilePath(args[0], _resource.UserData);
return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-hint-savemap-path"));
case 2:
return CompletionResult.FromHint(Loc.GetString("cmd-hint-savemap-force"));
}
return CompletionResult.Empty;
}

public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (!_config.GetCVar(CVars.GameSavesEnabled))
{
shell.WriteLine(Loc.GetString("cmd-savegame-disabled"));
return;
}

if (args.Length < 1)
{
shell.WriteLine(Help);
return;
}

var flush = false;
if (args.Length == 2 && !bool.TryParse(args[1], out flush))
{
shell.WriteError(Loc.GetString("cmd-parse-failure-bool", ("arg", args[1])));
return;
}

shell.WriteLine(Loc.GetString("cmd-loadgame-attempt", ("path", args[0])));

if (flush)
_entMan.FlushEntities();

bool loadSuccess = _system.GetEntitySystem<GameSavesSystem>().TryLoadGame(new ResPath(args[0]));
if(loadSuccess)
{
shell.WriteLine(Loc.GetString("cmd-loadgame-success"));
}
else
{
shell.WriteError(Loc.GetString("cmd-loadgame-error"));
}
}
}
}
21 changes: 21 additions & 0 deletions Robust.Shared/CVars.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1962,5 +1962,26 @@ internal static readonly CVarDef<string>
/// </summary>
public static readonly CVarDef<bool> LoadingShowDebug =
CVarDef.Create("loading.show_debug", DefaultShowDebug, CVar.CLIENTONLY);

/*
* GAME SAVES
*/

/// <summary>
/// Whether to allow saving and loading all entities.
/// Should be enabled only after the repository is tested, and it's confirmed that
/// saving and loading in stable scenarios doesn't throw any errors.
/// </summary>
public static readonly CVarDef<bool> GameSavesEnabled =
CVarDef.Create("gamesaves.enabled", true, CVar.SERVER | CVar.REPLICATED);

/// <summary>
/// ZSTD compression level to use when compressing game saves.
/// </summary>
public static readonly CVarDef<int> GameSavesCompressLevel =
CVarDef.Create("gamesaves.compress_level", 3, CVar.ARCHIVE);

public static readonly CVarDef<string> GameSavesAutoloadName =
CVarDef.Create("gamesaves.autoload_name", "save", CVar.SERVERONLY);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public bool TryLoadGeneric(ResPath file, [NotNullWhen(true)] out LoadResult? res
return TryLoadGeneric(data, file.ToString(), out result, options);
}

private bool TryLoadGeneric(
public bool TryLoadGeneric(
MappingDataNode data,
string fileName,
[NotNullWhen(true)] out LoadResult? result,
Expand Down
6 changes: 5 additions & 1 deletion Robust.Shared/EntitySerialization/Systems/MapLoaderSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ public void Delete(LoadResult result)
{
Del(uid);
}
}

foreach (var uid in result.NullspaceEntities)
{
Del(uid);
}
}
}
2 changes: 1 addition & 1 deletion Robust.Shared/GameObjects/EntityManager.Network.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ public NetEntity GetNetEntity(EntityUid uid, MetaDataComponent? metadata = null)
if (uid == EntityUid.Invalid)
return NetEntity.Invalid;

if (!MetaQuery.Resolve(uid, ref metadata))
if (!MetaQuery.Resolve(uid, ref metadata, logMissing: false))
return NetEntity.Invalid;

return metadata.NetEntity;
Expand Down
16 changes: 16 additions & 0 deletions Robust.Shared/GameSaves/GameSaveEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Utility;

namespace Robust.Shared.GameSaves;

/// <summary>
/// Event that is raised before all entities from the current state are saved to file.
/// </summary>
[ByRefEvent]
public readonly record struct BeforeGameSaveEvent(ResPath SavePath);

/// <summary>
/// Event that is raised before loading all saved entities from the file.
/// </summary>
[ByRefEvent]
public readonly record struct BeforeGameLoadEvent(ResPath SavePath);
Loading