Skip to content

Commit d0f7da3

Browse files
committed
World saver
1 parent 4dedf81 commit d0f7da3

14 files changed

Lines changed: 370 additions & 27 deletions

File tree

Resources/Locale/en-US/commands.ftl

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
### Localization for engine console commands
1+
### Localization for engine console commands
22

33
cmd-hint-float = [float]
44
@@ -177,6 +177,22 @@ cmd-hint-loadmap-uids = [float]
177177
178178
cmd-hint-savebp-id = <Grid EntityID>
179179
180+
## Full game save/load
181+
cmd-savegame-desc = Saves the full game state (all entities) to a .rtsave file.
182+
cmd-savegame-help = Usage: {$command} <Path>
183+
cmd-savegame-disabled = Game saves are disabled (gamesaves.enabled = false).
184+
cmd-savegame-attempt = Attempting to save game to {$path}.
185+
cmd-savegame-success = Game saved successfully.
186+
cmd-savegame-error = Could not save game. See server log for details.
187+
188+
cmd-loadgame-desc = Loads the full game state from a .rtsave file.
189+
cmd-loadgame-help = Usage: {$command} <Path> [flush]
190+
cmd-loadgame-disabled = Game saves are disabled (gamesaves.enabled = false).
191+
cmd-loadgame-attempt = Attempting to load game from {$path}.
192+
cmd-loadgame-success = Game loaded successfully.
193+
cmd-loadgame-error = Could not load game. See server log for details.
194+
cmd-hint-loadgame-flush = [flush: clear existing entities before load]
195+
180196
## 'flushcookies' command
181197
# Note: the flushcookies command is from Robust.Client.WebView, it's not in the main engine code.
182198

Robust.Client/GameObjects/EntitySystems/ContainerSystem.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,8 @@ private void HandleComponentState(EntityUid uid, ContainerManagerComponent compo
171171
var netEnt = stateNetEnts[i];
172172
if (!entity.IsValid())
173173
{
174-
DebugTools.Assert(netEnt.IsValid());
175-
AddExpectedEntity(netEnt, container);
174+
if (netEnt.IsValid())
175+
AddExpectedEntity(netEnt, container); // Данное изменение не попадет в релизную версию, важно учесть
176176
continue;
177177
}
178178

Robust.Serialization.Generator/ComponentPauseGenerator.cs

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,10 @@ public override void Initialize()
167167
}
168168
169169
private void OnEntityUnpaused(EntityUid uid, {{info.PartialTypeInfo.Name}} component, ref EntityUnpausedEvent args)
170-
{
171170
""");
171+
builder.AppendLine(" {");
172+
builder.AppendLine(" try");
173+
builder.AppendLine(" {");
172174

173175
var anyValidField = false;
174176
foreach (var field in info.Fields)
@@ -183,19 +185,20 @@ private void OnEntityUnpaused(EntityUid uid, {{info.PartialTypeInfo.Name}} compo
183185
{
184186
builder.AppendLine($"""
185187
if (component.{field.Name}.HasValue)
186-
component.{field.Name} = component.{field.Name}.Value + args.PausedTime;
188+
component.{field.Name} = AddTimeSpanClamped(component.{field.Name}.Value, args.PausedTime);
187189
""");
188190
}
189191
else if (field.Dictionary)
190192
{
191-
builder.AppendLine($"""
192-
foreach (var key in component.{field.Name}.Keys)
193-
component.{field.Name}[key] += args.PausedTime;
194-
""");
193+
builder.AppendLine(" foreach (var key in component." + field.Name + ".Keys)");
194+
builder.AppendLine(" {");
195+
builder.AppendLine(" try { component." + field.Name + "[key] += args.PausedTime; }");
196+
builder.AppendLine(" catch (System.Exception) { component." + field.Name + "[key] = global::System.TimeSpan.MaxValue; }");
197+
builder.AppendLine(" }");
195198
}
196199
else
197200
{
198-
builder.AppendLine($" component.{field.Name} += args.PausedTime;");
201+
builder.AppendLine($" component.{field.Name} = AddTimeSpanClamped(component.{field.Name}, args.PausedTime);");
199202
}
200203

201204
anyValidField = true;
@@ -207,10 +210,20 @@ private void OnEntityUnpaused(EntityUid uid, {{info.PartialTypeInfo.Name}} compo
207210
if (info.Dirty)
208211
builder.AppendLine(" Dirty(uid, component);");
209212

210-
builder.AppendLine("""
211-
}
212-
}
213-
""");
213+
builder.AppendLine(" }");
214+
builder.AppendLine(" catch (System.Exception)");
215+
builder.AppendLine(" {");
216+
builder.AppendLine(" // Map was paused for very long (e.g. StarGate world freeze); clamp to avoid crash on unpause during delete.");
217+
builder.AppendLine(" }");
218+
builder.AppendLine(" }");
219+
builder.AppendLine("");
220+
builder.AppendLine(" private static global::System.TimeSpan AddTimeSpanClamped(global::System.TimeSpan a, global::System.TimeSpan b)");
221+
builder.AppendLine(" {");
222+
builder.AppendLine(" try { return a + b; }");
223+
builder.AppendLine(" catch (System.Exception) { return b >= global::System.TimeSpan.Zero ? global::System.TimeSpan.MaxValue : global::System.TimeSpan.MinValue; }");
224+
builder.AppendLine(" }");
225+
builder.AppendLine(" }");
226+
builder.AppendLine(" ");
214227

215228
builder.AppendLine("}");
216229

Robust.Server/Console/Commands/MapCommands.cs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
using System.Linq;
22
using System.Numerics;
3+
using Robust.Shared;
4+
using Robust.Shared.Configuration;
35
using Robust.Shared.Console;
46
using Robust.Shared.ContentPack;
57
using Robust.Shared.EntitySerialization;
68
using Robust.Shared.EntitySerialization.Systems;
79
using Robust.Shared.GameObjects;
10+
using Robust.Shared.GameSaves;
811
using Robust.Shared.IoC;
912
using Robust.Shared.Localization;
1013
using Robust.Shared.Map;
@@ -334,4 +337,94 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
334337
shell.WriteLine(Loc.GetString("cmd-loadmap-error", ("path", args[1])));
335338
}
336339
}
340+
341+
public sealed class SaveGame : LocalizedCommands
342+
{
343+
[Dependency] private readonly IEntitySystemManager _system = default!;
344+
[Dependency] private readonly IResourceManager _resource = default!;
345+
[Dependency] private readonly IConfigurationManager _config = default!;
346+
347+
public override string Command => "savegame";
348+
349+
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
350+
{
351+
if (args.Length == 1)
352+
{
353+
var opts = CompletionHelper.UserFilePath(args[0], _resource.UserData);
354+
return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-hint-savemap-path"));
355+
}
356+
return CompletionResult.Empty;
357+
}
358+
359+
public override void Execute(IConsoleShell shell, string argStr, string[] args)
360+
{
361+
if (!_config.GetCVar(CVars.GameSavesEnabled))
362+
{
363+
shell.WriteLine(Loc.GetString("cmd-savegame-disabled"));
364+
return;
365+
}
366+
if (args.Length < 1)
367+
{
368+
shell.WriteLine(Help);
369+
return;
370+
}
371+
shell.WriteLine(Loc.GetString("cmd-savegame-attempt", ("path", args[0])));
372+
var success = _system.GetEntitySystem<GameSavesSystem>().TrySaveGame(new ResPath(args[0]));
373+
if (success)
374+
shell.WriteLine(Loc.GetString("cmd-savegame-success"));
375+
else
376+
shell.WriteError(Loc.GetString("cmd-savegame-error"));
377+
}
378+
}
379+
380+
public sealed class LoadGame : LocalizedCommands
381+
{
382+
[Dependency] private readonly IEntityManager _entMan = default!;
383+
[Dependency] private readonly IEntitySystemManager _system = default!;
384+
[Dependency] private readonly IResourceManager _resource = default!;
385+
[Dependency] private readonly IConfigurationManager _config = default!;
386+
387+
public override string Command => "loadgame";
388+
389+
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
390+
{
391+
switch (args.Length)
392+
{
393+
case 1:
394+
var opts = CompletionHelper.UserFilePath(args[0], _resource.UserData);
395+
return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-hint-savemap-path"));
396+
case 2:
397+
return CompletionResult.FromHint(Loc.GetString("cmd-hint-loadgame-flush"));
398+
}
399+
return CompletionResult.Empty;
400+
}
401+
402+
public override void Execute(IConsoleShell shell, string argStr, string[] args)
403+
{
404+
if (!_config.GetCVar(CVars.GameSavesEnabled))
405+
{
406+
shell.WriteLine(Loc.GetString("cmd-loadgame-disabled"));
407+
return;
408+
}
409+
if (args.Length < 1)
410+
{
411+
shell.WriteLine(Help);
412+
return;
413+
}
414+
var flush = false;
415+
if (args.Length == 2 && !bool.TryParse(args[1], out flush))
416+
{
417+
shell.WriteError(Loc.GetString("cmd-parse-failure-bool", ("arg", args[1])));
418+
return;
419+
}
420+
shell.WriteLine(Loc.GetString("cmd-loadgame-attempt", ("path", args[0])));
421+
if (flush)
422+
_entMan.FlushEntities();
423+
var success = _system.GetEntitySystem<GameSavesSystem>().TryLoadGame(new ResPath(args[0]));
424+
if (success)
425+
shell.WriteLine(Loc.GetString("cmd-loadgame-success"));
426+
else
427+
shell.WriteError(Loc.GetString("cmd-loadgame-error"));
428+
}
429+
}
337430
}

Robust.Shared/CVars.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2017,5 +2017,27 @@ internal static readonly CVarDef<string>
20172017
/// </summary>
20182018
public static readonly CVarDef<bool> LoadingShowDebug =
20192019
CVarDef.Create("loading.show_debug", DefaultShowDebug, CVar.CLIENTONLY);
2020+
2021+
/*
2022+
* GAME SAVES
2023+
*/
2024+
2025+
/// <summary>
2026+
/// Whether to allow saving and loading all entities (full game save/load).
2027+
/// </summary>
2028+
public static readonly CVarDef<bool> GameSavesEnabled =
2029+
CVarDef.Create("gamesaves.enabled", false, CVar.SERVER | CVar.REPLICATED);
2030+
2031+
/// <summary>
2032+
/// ZSTD compression level to use when compressing game saves.
2033+
/// </summary>
2034+
public static readonly CVarDef<int> GameSavesCompressLevel =
2035+
CVarDef.Create("gamesaves.compress_level", 3, CVar.ARCHIVE);
2036+
2037+
/// <summary>
2038+
/// Default save name for autoload (e.g. on server start).
2039+
/// </summary>
2040+
public static readonly CVarDef<string> GameSavesAutoloadName =
2041+
CVarDef.Create("gamesaves.autoload_name", "save", CVar.SERVERONLY);
20202042
}
20212043
}

Robust.Shared/Containers/SharedContainerSystem.Remove.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,10 @@ public bool CanRemove(EntityUid toRemove, BaseContainer container)
126126
RaiseLocalEvent(toRemove, gettingRemovedAttemptEvent, true);
127127
return !gettingRemovedAttemptEvent.Cancelled;
128128
}
129+
130+
public void RemoveDeletedEntity(EntityUid staleUid, BaseContainer container)
131+
{
132+
container.InternalRemove(staleUid, EntityManager);
133+
Dirty(container.Owner, container.Manager);
134+
}
129135
}

Robust.Shared/EntitySerialization/EntityDeserializer.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -965,7 +965,9 @@ private void StartEntitiesInternal()
965965
_stopwatch.Restart();
966966
foreach (var uid in SortedEntities)
967967
{
968-
StartupEntity(uid, _metaQuery.GetComponent(uid));
968+
if (!_metaQuery.TryComp(uid, out var meta))
969+
continue;
970+
StartupEntity(uid, meta);
969971
}
970972
_log.Debug($"Started up {Result.Entities.Count} entities in {_stopwatch.Elapsed}");
971973
}
@@ -1025,7 +1027,10 @@ private void SetMapInitLifestage()
10251027
continue;
10261028

10271029
DebugTools.Assert(meta.EntityLifeStage == EntityLifeStage.Initialized);
1028-
EntMan.SetLifeStage(meta, EntityLifeStage.MapInitialized);
1030+
if (_mapQuery.HasComp(uid))
1031+
EntMan.SetLifeStage(meta, EntityLifeStage.MapInitialized);
1032+
else
1033+
EntMan.RunMapInit(uid, meta);
10291034
}
10301035

10311036
_log.Debug($"Finished flagging mapinit in {_stopwatch.Elapsed}");

Robust.Shared/EntitySerialization/EntitySerializer.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,8 @@ public bool IsSerializable(Entity<MetaDataComponent?> ent)
190190
if (ent.Comp == null && !EntMan.TryGetComponent(ent.Owner, out ent.Comp))
191191
return false;
192192

193-
if (ent.Comp.EntityPrototype?.MapSavable == false)
194-
return false;
195193

196-
bool serializable = true;
194+
bool serializable = ent.Comp.EntityPrototype?.MapSavable != false;
197195
OnIsSerializeable?.Invoke(ent!, ref serializable);
198196
return serializable;
199197
}
@@ -1005,7 +1003,8 @@ public DataNode Write(
10051003
if (value == EntityUid.Invalid)
10061004
{
10071005
if (Options.MissingEntityBehaviour != MissingEntityBehaviour.Ignore)
1008-
_log.Error($"Encountered an invalid entityUid reference.");
1006+
_log.Error("Encountered an invalid entityUid reference while serializing {Entity}, component: {Component}.",
1007+
CurrentEntity != null ? EntMan.ToPrettyString(CurrentEntity) : "?", CurrentComponent ?? "?");
10091008

10101009
return InvalidNode;
10111010
}
@@ -1020,8 +1019,9 @@ public DataNode Write(
10201019
{
10211020
case MissingEntityBehaviour.Error:
10221021
_log.Error(EntMan.Deleted(value)
1023-
? $"Encountered a reference to a deleted entity {value} while serializing {EntMan.ToPrettyString(CurrentEntity)}."
1024-
: $"Encountered a reference to a missing entity: {value} while serializing {EntMan.ToPrettyString(CurrentEntity)}.");
1022+
? "Encountered a reference to a deleted entity {Value} while serializing {Entity}, component: {Component}."
1023+
: "Encountered a reference to a missing entity: {Value} while serializing {Entity}, component: {Component}.",
1024+
value, CurrentEntity != null ? EntMan.ToPrettyString(CurrentEntity) : "?", CurrentComponent ?? "?");
10251025
return InvalidNode;
10261026
case MissingEntityBehaviour.Ignore:
10271027
return InvalidNode;

Robust.Shared/EntitySerialization/Systems/MapLoaderSystem.Load.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public bool TryLoadGeneric(ResPath file, [NotNullWhen(true)] out LoadResult? res
8080
return TryLoadGeneric(data, file.ToString(), out result, options);
8181
}
8282

83-
private bool TryLoadGeneric(
83+
public bool TryLoadGeneric(
8484
MappingDataNode data,
8585
string fileName,
8686
[NotNullWhen(true)] out LoadResult? result,

Robust.Shared/EntitySerialization/Systems/MapLoaderSystem.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ public void Delete(LoadResult result)
135135
{
136136
Del(uid);
137137
}
138+
139+
foreach (var uid in result.NullspaceEntities)
140+
{
141+
Del(uid);
142+
}
138143
}
139144

140145
}

0 commit comments

Comments
 (0)