Skip to content

Commit

Permalink
feat(Microsoft): add support for Worlds 5.00
Browse files Browse the repository at this point in the history
  • Loading branch information
cengelha committed Jul 25, 2024
1 parent 18ecc0e commit 2fea567
Show file tree
Hide file tree
Showing 21 changed files with 217 additions and 84 deletions.
4 changes: 4 additions & 0 deletions .exclusion.dic
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,7 @@ Hebino
Treffpunkt
Batannam
Sph�re
Reisende
Lehave
Yamak
Innerhalb
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ into the selected directory. This must be in or one level below the selected one
* SteamDeck: **~/.local/share/Steam/steamapps/compatdata/275850/pfx/drive_c/users/steamuser/Application Data/HelloGames/NMS/st\_\<SteamID\>**
* macOS: **~/Library/Application Support/HelloGames/NMS/st\_\<SteamID\>**
* File Patterns: **save\*.hg**
* Notes: If you use a cloud gaming service like GeForce NOW you can still use
it by starting the game to trigger synchronization from/to the cloud.
* [Microsoft Store](https://www.microsoft.com/p/no-mans-sky/bqvqtl3pch05) (Windows PC)
* Location: **%LocalAppData%\Packages\HelloGames.NoMansSky_bs190hzg1sesy\SystemAppData\wgs\\<XboxID\>_29070100B936489ABCE8B9AF3980429C**
* File Patterns: **containers.index**
Expand All @@ -67,7 +69,7 @@ into the selected directory. This must be in or one level below the selected one
* [Xbox One/Series X\|S](https://www.microsoft.com/p/no-mans-sky/bqvqtl3pch05)
* Notes: Not directly supported but can easily achieved with cloud sync via
the Microsoft Store. The synchronization is triggered short after you close
the game (no need to load a save).
the game (no need to load a save). This also works for Xbox Cloud Gaming.

### Usage

Expand Down
2 changes: 1 addition & 1 deletion libNOM.cli/libNOM.cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@

<!-- Package -->
<PropertyGroup Label="General">
<Version>1.0.1</Version>
<Version>1.0.3</Version>
<Authors>cengelha</Authors>
<Description>CLI for libNOM.io to analyze single files or whole directories and print information about it, convert between JSON and actual save formats and perform file operations.</Description>
<Copyright>Copyright (c) Christian Engelhardt 2024</Copyright>
Expand Down
20 changes: 12 additions & 8 deletions libNOM.io/Global/Analyze.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using libNOM.io.Interfaces;
using libNOM.io.Settings;
using libNOM.io.Settings;

namespace libNOM.io.Global;

Expand All @@ -8,7 +7,7 @@ public static class Analyze
{
#region Field

private static uint _headerInteger = uint.MaxValue;
private static byte[] _headerByte = [];
private static string? _headerString0x08;
private static string? _headerString0x20;
private static string? _headerString0xA0;
Expand All @@ -35,20 +34,21 @@ public static class Analyze
return null;

FileInfo data = new(path);
UpdateHeader(data);
if (!UpdateHeader(data))
return null;

// Define variables and fill them while generating a platform.
FileInfo? meta;
int metaIndex;
Platform? platform;

// Select a platform below to convert the file with, based on the content.
if (_headerString0x08 == PlatformPlaystation.SAVEWIZARD_HEADER || (_headerInteger == Constants.SAVE_STREAMING_HEADER && _headerString0xA0!.Contains("PS4|Final")))
if (_headerString0x08 == PlatformPlaystation.SAVEWIZARD_HEADER || (_headerByte.SequenceEqual(Constants.SAVE_STREAMING_HEADER) && _headerString0xA0!.Contains("PS4|Final")))
{
platform = GenerateCommonPlatform<PlatformPlaystation>(data, platformSettings, out metaIndex, out meta);
}
// StartsWith for uncompressed saves and plaintext JSON.
else if (_headerInteger == Constants.SAVE_STREAMING_HEADER || _headerString0x20!.StartsWith("{\"F2P\":") || _headerString0x20.StartsWith("{\"Version\":"))
else if (_headerByte.SequenceEqual(Constants.SAVE_STREAMING_HEADER) || _headerString0x20!.StartsWith("{\"F2P\":") || _headerString0x20.StartsWith("{\"Version\":"))
{
if (_headerString0xA0!.Contains("NX1|Final"))
platform = GenerateCommonPlatform<PlatformSwitch>(data, platformSettings, out metaIndex, out meta);
Expand All @@ -69,15 +69,19 @@ public static class Analyze

// private //

private static void UpdateHeader(FileInfo data)
private static bool UpdateHeader(FileInfo data)
{
ReadOnlySpan<byte> bytes = data.ReadAllBytes();
if (bytes.Length < 0xA0)
return false;

// Convert header with different lengths.
_headerInteger = bytes.Cast<uint>(0);
_headerByte = bytes[..0x4].ToArray();
_headerString0x08 = bytes[..0x08].GetString();
_headerString0x20 = bytes[..0x20].GetString();
_headerString0xA0 = bytes[..0xA0].GetString();

return true;
}

#endregion
Expand Down
6 changes: 3 additions & 3 deletions libNOM.io/Global/Common.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace libNOM.io.Global;


internal static partial class Common
public static partial class Common
{
// No real DeepCopy but good enough to swap and that case is only using this.
internal static Container DeepCopy(Container original)
Expand All @@ -28,9 +28,9 @@ internal static Container DeepCopy(Container original)
MicrosoftBlobMetaFile = original.MicrosoftBlobMetaFile,
};

internal static Span<T> DeepCopy<T>(Span<T> original) => DeepCopy(original.ToArray());
public static Span<T> DeepCopy<T>(Span<T> original) => DeepCopy(original.ToArray());

internal static T DeepCopy<T>(T original)
public static T DeepCopy<T>(T original)
{
var serialized = JsonConvert.SerializeObject(original);
return JsonConvert.DeserializeObject<T>(serialized)!;
Expand Down
10 changes: 4 additions & 6 deletions libNOM.io/Global/Constants.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using libNOM.io.Interfaces;

namespace libNOM.io.Global;
namespace libNOM.io.Global;


public static class Constants
Expand Down Expand Up @@ -181,9 +179,9 @@ public static class Constants
internal const int SAVE_RENAMING_LENGTH_MANIFEST = 0x80; // 128
internal const int SAVE_RENAMING_LENGTH_INGAME = 0x2A; // 42

internal const uint SAVE_STREAMING_HEADER = 0xFEEDA1E5; // 4,276,986,341
internal const int SAVE_STREAMING_HEADER_TOTAL_LENGTH = 0x10; // 16
internal const int SAVE_STREAMING_CHUNK_MAX_LENGTH = 0x80000; // 524,288
internal static readonly byte[] SAVE_STREAMING_HEADER = [0xE5, 0xA1, 0xED, 0xFE];
internal const int SAVE_STREAMING_HEADER_LENGTH = 0x10; // 16
internal const int SAVE_STREAMING_CHUNK_LENGTH_MAX = 0x80000; // 524,288

internal const int THRESHOLD_GAMEMODE_NORMAL = THRESHOLD_VANILLA + ((int)(PresetGameModeEnum.Normal) * OFFSET_GAMEMODE);
internal const int THRESHOLD_GAMEMODE_CREATIVE = THRESHOLD_VANILLA + ((int)(PresetGameModeEnum.Creative) * OFFSET_GAMEMODE);
Expand Down
8 changes: 4 additions & 4 deletions libNOM.io/PlatformMicrosoft.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public partial class PlatformMicrosoft : Platform
protected override int META_LENGTH_KNOWN_VANILLA => 0x14; // 20
internal override int META_LENGTH_TOTAL_VANILLA => 0x18; // 24
internal override int META_LENGTH_TOTAL_WAYPOINT => 0x118; // 280
internal override int META_LENGTH_TOTAL_WORLDS => META_LENGTH_TOTAL_WAYPOINT; // no changes for this platform
internal override int META_LENGTH_TOTAL_WORLDS => 0x128; // 296

private const int BLOBCONTAINER_HEADER = 0x4; // 4
private const int BLOBCONTAINER_COUNT = 0x2; // 2
Expand All @@ -33,9 +33,9 @@ public partial class PlatformMicrosoft : Platform
private const long CONTAINERSINDEX_FOOTER = 0x10000000; // 268,435,456
private const int CONTAINERSINDEX_OFFSET_BLOBCONTAINER_LIST = 0xC8; // 200

internal static readonly byte[] SAVE_V2_HEADER = [.. Encoding.ASCII.GetBytes("HGSAVEV2"), 0x00];
internal const int SAVE_V2_HEADER_PARTIAL_LENGTH = 0x8; // 8
internal const int SAVE_V2_CHUNK_MAX_LENGTH = 0x100000; // 1,048,576
internal static readonly byte[] HGSAVEV2_HEADER = [.. Encoding.ASCII.GetBytes("HGSAVEV2"), 0x00];
internal const int HGSAVEV2_HEADER_LENGTH = 0x8; // 8
internal const int HGSAVEV2_CHUNK_LENGTH_MAX = 0x100000; // 1,048,576

#endregion

Expand Down
22 changes: 21 additions & 1 deletion libNOM.io/PlatformMicrosoft_Initialize.cs
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,11 @@ 37. SAVE SUMMARY (128) // may contain additional junk data aft
69. DIFFICULTY PRESET ( 1)
69. EMPTY ( 3) // may contain additional junk data
(280)
70. SLOT IDENTIFIER ( 8)
72. TIMESTAMP ( 4)
73. META FORMAT ( 4)
(296)
*/
if (disk.IsEmpty())
return;
Expand Down Expand Up @@ -328,12 +333,27 @@ 69. EMPTY ( 3) // may contain additional junk data
};

// Extended metadata since Waypoint 4.00.
UpdateContainerWithWaypointMetaInformation(container, disk);
if (disk.Length == META_LENGTH_TOTAL_WAYPOINT)
UpdateContainerWithWaypointMetaInformation(container, disk);

// Extended metadata since Worlds 5.00.
if (disk.Length == META_LENGTH_TOTAL_WORLDS)
UpdateContainerWithWorldsMetaInformation(container, disk, decompressed);

// GameVersion with BaseVersion only is not 100% accurate but good enough to calculate SaveVersion.
container.SaveVersion = Meta.SaveVersion.Calculate(container, Meta.GameVersion.Get(container.Extra.BaseVersion));
}
}

protected override void UpdateContainerWithWorldsMetaInformation(Container container, ReadOnlySpan<byte> disk, ReadOnlySpan<uint> decompressed)
{
base.UpdateContainerWithWorldsMetaInformation(container, disk, decompressed);

container.Extra = container.Extra with
{
LastWriteTime = DateTimeOffset.FromUnixTimeSeconds(decompressed[72]).ToLocalTime(),
};
}

#endregion
}
15 changes: 10 additions & 5 deletions libNOM.io/PlatformMicrosoft_Read.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,27 @@ protected override ReadOnlySpan<byte> LoadContainer(Container container)

protected override ReadOnlySpan<byte> DecompressData(Container container, ReadOnlySpan<byte> data)
{
if (container.IsAccount || !data.StartsWith(SAVE_V2_HEADER)) // single chunk compression for Account and before Omega 4.52
// Single chunk compression for Account and before Omega 4.52.
if (container.IsAccount || (!data.StartsWith(HGSAVEV2_HEADER) && !data.StartsWith(Constants.SAVE_STREAMING_HEADER)))
{
_ = LZ4.Decode(data, out var target, (int)(container.Extra.SizeDecompressed));
return target;
}

// New format is similar to the save streaming introduced with Frontiers.
var offset = SAVE_V2_HEADER.Length;
// Since Worlds 5.00, the standard save streaming is used.
if (data.StartsWith(Constants.SAVE_STREAMING_HEADER))
return base.DecompressData(container, data);

// Special format (similar to the standard streaming) used between Omega 4.52 and Worlds 5.00.
var offset = HGSAVEV2_HEADER.Length;
ReadOnlySpan<byte> result = [];

while (offset < data.Length)
{
var chunkHeader = data.Slice(offset, SAVE_V2_HEADER_PARTIAL_LENGTH).Cast<byte, uint>();
var chunkHeader = data.Slice(offset, HGSAVEV2_HEADER_LENGTH).Cast<byte, uint>();
var sizeCompressed = (int)(chunkHeader[1]);

offset += SAVE_V2_HEADER_PARTIAL_LENGTH;
offset += HGSAVEV2_HEADER_LENGTH;
_ = LZ4.Decode(data.Slice(offset, sizeCompressed), out var target, (int)(chunkHeader[0]));
offset += sizeCompressed;

Expand Down
26 changes: 18 additions & 8 deletions libNOM.io/PlatformMicrosoft_Write.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ protected override void WritePlatformSpecific(Container container, DateTimeOffse
// Writing all Microsoft Store files at once in the same way as the game itself does.
if (Settings.WriteAlways || !container.IsSynced || Settings.SetLastWriteTime)
{
// Timestamp must be set before creating meta.
if (Settings.SetLastWriteTime)
{
_lastWriteTime = writeTime; // global timestamp has full accuracy
container.LastWriteTime = _lastWriteTime.NullifyTicks(4);
}

if (Settings.WriteAlways || !container.IsSynced)
{
container.Exists = true;
Expand All @@ -31,11 +38,9 @@ protected override void WritePlatformSpecific(Container container, DateTimeOffse
WriteBlobContainer(container, blob, copy);
}

// Must be set after files have been created.
if (Settings.SetLastWriteTime)
{
_lastWriteTime = writeTime; // global timestamp has full accuracy
container.LastWriteTime = _lastWriteTime.NullifyTicks(4);

container.DataFile?.SetFileTime(container.LastWriteTime);
container.MetaFile?.SetFileTime(container.LastWriteTime);
}
Expand All @@ -49,22 +54,26 @@ protected override void WritePlatformSpecific(Container container, DateTimeOffse

protected override ReadOnlySpan<byte> CompressData(Container container, ReadOnlySpan<byte> data)
{
if (!container.IsSave || !container.IsVersion452OmegaWithMicrosoftV2)
if (!container.IsSave || !container.IsVersion452OmegaWithMicrosoftV2) // if not Omega 4.52, also not Worlds 5.00
{
_ = LZ4.Encode(data, out var target);
return target;
}

// New format is similar to the save streaming introduced with Frontiers.
// Since Worlds 5.00, the standard save streaming is used.
if (container.IsVersion500Worlds)
return base.CompressData(container, data);

// Special format (similar to the standard streaming) used between Omega 4.52 and Worlds 5.00.
var position = 0;
ReadOnlySpan<byte> result = SAVE_V2_HEADER;
ReadOnlySpan<byte> result = HGSAVEV2_HEADER;

while (position < data.Length)
{
var maxLength = data.Length - position;

// The tailing \0 needs to compressed separately and must not be part of the actual JSON chunks.
var source = data.Slice(position, Math.Min(SAVE_V2_CHUNK_MAX_LENGTH, maxLength == 1 ? 1 : maxLength - 1));
var source = data.Slice(position, Math.Min(HGSAVEV2_CHUNK_LENGTH_MAX, maxLength == 1 ? 1 : maxLength - 1));
_ = LZ4.Encode(source, out var target);
position += source.Length;

Expand Down Expand Up @@ -120,9 +129,10 @@ protected override Span<uint> CreateMeta(Container container, ReadOnlySpan<byte>
writer.Write(container.IsVersion452OmegaWithMicrosoftV2 ? container.Extra.SizeDisk : container.Extra.SizeDecompressed); // 4

// Append buffered bytes that follow META_LENGTH_KNOWN_VANILLA.
writer.Write(container.Extra.Bytes ?? []); // Extra.Bytes is 4 or 260
writer.Write(container.Extra.Bytes ?? []); // Extra.Bytes is 4 or 260 or 276

OverwriteWaypointMeta(writer, container);
OverwriteWorldsMeta(writer, container);
}

return buffer.AsSpan().Cast<byte, uint>();
Expand Down
39 changes: 18 additions & 21 deletions libNOM.io/PlatformSteam.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ 86. EMPTY ( 15) // META_FORMAT_3 // may contain additional junk d
(360)
86. DIFFICULTY PRESET ( 4) // META_FORMAT_4
87. ??? ( 8) // META_FORMAT_4 // maybe a slot identifier
87. SLOT IDENTIFIER ( 8) // META_FORMAT_4
89. TIMESTAMP ( 4) // META_FORMAT_4
90. META FORMAT ( 4) // META_FORMAT_4
91. EMPTY ( 20) // META_FORMAT_4
Expand Down Expand Up @@ -221,10 +221,12 @@ 91. EMPTY ( 20) // META_FORMAT_4
if (container.IsSave)
{
// Extended metadata since Waypoint 4.00.
UpdateContainerWithWaypointMetaInformation(container, disk);
if (disk.Length == META_LENGTH_TOTAL_WAYPOINT)
UpdateContainerWithWaypointMetaInformation(container, disk);

// Extended metadata including a new META_FORMAT since Worlds 5.00.
UpdateContainerWithWorldsMetaInformation(container, disk, decompressed);
// Extended metadata since Worlds 5.00.
if (disk.Length == META_LENGTH_TOTAL_WORLDS)
UpdateContainerWithWorldsMetaInformation(container, disk, decompressed);

// GameVersion with BaseVersion only is not 100% accurate but good enough to calculate SaveVersion.
container.SaveVersion = Meta.SaveVersion.Calculate(container, Meta.GameVersion.Get(container.Extra.BaseVersion));
Expand All @@ -235,16 +237,14 @@ 91. EMPTY ( 20) // META_FORMAT_4
container.Extra = container.Extra with { MetaLength = (uint)(disk.Length) };
}

protected void UpdateContainerWithWorldsMetaInformation(Container container, ReadOnlySpan<byte> disk, ReadOnlySpan<uint> decompressed)
protected override void UpdateContainerWithWorldsMetaInformation(Container container, ReadOnlySpan<byte> disk, ReadOnlySpan<uint> decompressed)
{
if (disk.Length == META_LENGTH_TOTAL_WORLDS)
container.Extra = container.Extra with
{
SaveName = disk.Slice(META_LENGTH_KNOWN_VANILLA, Constants.SAVE_RENAMING_LENGTH_MANIFEST).GetStringUntilTerminator(),
SaveSummary = disk.Slice(META_LENGTH_KNOWN_NAME, Constants.SAVE_RENAMING_LENGTH_MANIFEST).GetStringUntilTerminator(),
DifficultyPreset = disk[META_LENGTH_KNOWN_SUMMARY], // keep it a single byte to get the correct value if migrated but not updated
LastWriteTime = DateTimeOffset.FromUnixTimeSeconds(decompressed[89]).ToLocalTime(),
};
base.UpdateContainerWithWorldsMetaInformation(container, disk, decompressed);

container.Extra = container.Extra with
{
LastWriteTime = DateTimeOffset.FromUnixTimeSeconds(decompressed[89]).ToLocalTime(),
};
}

#endregion
Expand Down Expand Up @@ -377,20 +377,17 @@ protected override Span<uint> CreateMeta(Container container, ReadOnlySpan<byte>
return buffer.AsSpan().Cast<byte, uint>();
}

private void OverwriteWorldsMeta(BinaryWriter writer, Container container)
protected override void OverwriteWorldsMeta(BinaryWriter writer, Container container)
{
// Write appended.
base.OverwriteWorldsMeta(writer, container);

// Overwrite changed.
if (container.IsVersion500Worlds)
{
// COMPRESSED SIZE is used again.
writer.Seek(0x3C, SeekOrigin.Begin); // 4 + 4 + 16 + 32 = 48
writer.Write(container.Extra.SizeDisk); // 4

writer.Seek(META_LENGTH_KNOWN_SUMMARY, SeekOrigin.Begin);
writer.Write((uint)(container.Difficulty)); // 4

// Skip next 8 bytes that is maybe a slot identifier.
writer.Seek(0x8, SeekOrigin.Current);
writer.Write((uint)(container.LastWriteTime!.Value.ToUniversalTime().ToUnixTimeSeconds())); // 4
}
}

Expand Down
3 changes: 2 additions & 1 deletion libNOM.io/PlatformSwitch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,8 @@ 75. EMPTY ( 56)
};

// Extended data since Waypoint.
UpdateContainerWithWaypointMetaInformation(container, disk);
if (disk.Length == META_LENGTH_TOTAL_WAYPOINT)
UpdateContainerWithWaypointMetaInformation(container, disk);

// GameVersion with BaseVersion only is not 100% accurate but good enough to calculate SaveVersion.
container.SaveVersion = Meta.SaveVersion.Calculate(container, Meta.GameVersion.Get(container.Extra.BaseVersion));
Expand Down
Loading

0 comments on commit 2fea567

Please sign in to comment.