Skip to content

Commit 0abb5e8

Browse files
committed
feat(StateDivergence): add serialization and deserialization for state divergence supplements
- Introduced StateDivergenceSupplementPayload for capturing additional state divergence data. - Implemented serialization and deserialization patches to append and read divergence diagnostics in state messages. - Enhanced StateDivergenceDiagnosticReportBuilder to include protocol maps for better comparison reporting. - Updated patcher setup to register new serialization and deserialization patches.
1 parent 7907291 commit 0abb5e8

4 files changed

Lines changed: 323 additions & 0 deletions

File tree

Networking/StateDivergence/Patches/StateDivergenceDiagnosticsPatches.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using MegaCrit.Sts2.Core.Entities.Multiplayer;
44
using MegaCrit.Sts2.Core.Multiplayer.Game;
55
using MegaCrit.Sts2.Core.Multiplayer.Messages.Game.Checksums;
6+
using MegaCrit.Sts2.Core.Multiplayer.Serialization;
67
using MegaCrit.Sts2.Core.Nodes.CommonUi;
78
using STS2RitsuLib.Patching.Models;
89

@@ -25,6 +26,53 @@ public static bool TryGetLatest(out StateDivergenceDiagnosticReport report)
2526
}
2627
}
2728

29+
internal sealed class StateDivergenceSupplementSerializePatch : IPatchMethod
30+
{
31+
public static string PatchId => "state_divergence_supplement_serialize";
32+
public static bool IsCritical => false;
33+
34+
public static string Description =>
35+
"Append compressed RitsuLib divergence diagnostics to state divergence messages.";
36+
37+
public static ModPatchTarget[] GetTargets()
38+
{
39+
StateDivergenceSupplementPayloadCodec.EnsureRegistered();
40+
return
41+
[
42+
new(typeof(StateDivergenceMessage), nameof(StateDivergenceMessage.Serialize), [typeof(PacketWriter)]),
43+
];
44+
}
45+
46+
// ReSharper disable once InconsistentNaming
47+
public static void Postfix(StateDivergenceMessage __instance, PacketWriter writer)
48+
{
49+
StateDivergenceSupplementPayloadCodec.Write(writer, __instance);
50+
}
51+
}
52+
53+
internal sealed class StateDivergenceSupplementDeserializePatch : IPatchMethod
54+
{
55+
public static string PatchId => "state_divergence_supplement_deserialize";
56+
public static bool IsCritical => false;
57+
58+
public static string Description =>
59+
"Read compressed RitsuLib divergence diagnostics from state divergence messages.";
60+
61+
public static ModPatchTarget[] GetTargets()
62+
{
63+
StateDivergenceSupplementPayloadCodec.EnsureRegistered();
64+
return
65+
[
66+
new(typeof(StateDivergenceMessage), nameof(StateDivergenceMessage.Deserialize), [typeof(PacketReader)]),
67+
];
68+
}
69+
70+
public static void Postfix(PacketReader reader)
71+
{
72+
StateDivergenceSupplementPayloadCodec.Read(reader);
73+
}
74+
}
75+
2876
internal sealed class StateDivergenceDiagnosticsLogPatch : IPatchMethod
2977
{
3078
private const BindingFlags InstanceFieldFlags =

Networking/StateDivergence/StateDivergenceDiagnosticReportBuilder.cs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public static StateDivergenceDiagnosticReport Build(
2929
var sections = new List<StateDivergenceDiagnosticSection>
3030
{
3131
BuildOverview(local, remote, remotePeerId, role),
32+
BuildProtocolMaps(local.Checksum, remote.Checksum),
3233
BuildSynchronizers(local.FullState, remote.FullState),
3334
BuildCreatures(local.FullState, remote.FullState),
3435
BuildPlayers(local.FullState, remote.FullState),
@@ -57,6 +58,73 @@ public static StateDivergenceDiagnosticReport Build(
5758
remote.FullState.ToString());
5859
}
5960

61+
private static StateDivergenceDiagnosticSection BuildProtocolMaps(
62+
NetChecksumData localChecksum,
63+
NetChecksumData remoteChecksum)
64+
{
65+
var local = StateDivergenceSupplementPayloadCodec.CreateLocalSnapshot(localChecksum);
66+
var hasRemote = StateDivergenceSupplementStore.TryTake(remoteChecksum, out var remote);
67+
var rows = new List<StateDivergenceDiagnosticRow>();
68+
69+
if (!hasRemote)
70+
{
71+
rows.Add(new(
72+
"savedProperties.supplement",
73+
L("value.present", "Present"),
74+
L("value.missing", "Missing"),
75+
L("detail.missingSupplement",
76+
"The remote peer did not include a RitsuLib state-divergence supplement payload.")));
77+
}
78+
else
79+
{
80+
AddIfDifferent(rows, "savedProperties.netIdBitSize",
81+
local.SavedPropertyNetIdBitSize, remote.SavedPropertyNetIdBitSize);
82+
AddIfDifferent(rows, "savedProperties.count",
83+
local.SavedPropertyNames.Count, remote.SavedPropertyNames.Count);
84+
AddIfDifferent(rows, "savedProperties.mapHash",
85+
FormatHash(local.SavedPropertyMapHash), FormatHash(remote.SavedPropertyMapHash));
86+
AddSavedPropertyMapRows(rows, local.SavedPropertyNames, remote.SavedPropertyNames);
87+
}
88+
89+
return new(
90+
L("section.protocolMaps.title", "Protocol maps"),
91+
L("section.protocolMaps.description",
92+
"RitsuLib divergence supplement data that affects packet decoding, including SavedProperty net-id maps."),
93+
rows.Count == 0,
94+
rows);
95+
}
96+
97+
private static void AddSavedPropertyMapRows(
98+
ICollection<StateDivergenceDiagnosticRow> rows,
99+
IReadOnlyList<string> local,
100+
IReadOnlyList<string> remote)
101+
{
102+
var count = Math.Max(local.Count, remote.Count);
103+
var localLines = new List<string>();
104+
var remoteLines = new List<string>();
105+
106+
for (var i = 0; i < count; i++)
107+
{
108+
var l = i < local.Count ? local[i] : L("value.missing", "Missing");
109+
var r = i < remote.Count ? remote[i] : L("value.missing", "Missing");
110+
if (string.Equals(l, r, StringComparison.Ordinal))
111+
continue;
112+
113+
localLines.Add($"{i:0000} {l}");
114+
remoteLines.Add($"{i:0000} {r}");
115+
}
116+
117+
if (localLines.Count == 0)
118+
return;
119+
120+
rows.Add(new(
121+
"savedProperties.netIdMap",
122+
string.Join(Environment.NewLine, localLines),
123+
string.Join(Environment.NewLine, remoteLines),
124+
F("detail.savedPropertyMapMismatch", "{0} SavedProperty net-id slot(s) differ.",
125+
localLines.Count)));
126+
}
127+
60128
private static StateDivergenceDiagnosticSection BuildOverview(
61129
StateDivergenceTrackedState local,
62130
StateDivergenceTrackedState remote,
@@ -682,6 +750,11 @@ private static string FormatNullableInt(int? value)
682750
return value.HasValue ? value.Value.ToString() : L("value.none", "<none>");
683751
}
684752

753+
private static string FormatHash(uint value)
754+
{
755+
return "0x" + value.ToString("X8");
756+
}
757+
685758
private static string FormatIntArrayProperty(int[]? value)
686759
{
687760
return FormatIntArrayProperty("", value);
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
using System.IO.Compression;
2+
using System.Text;
3+
using System.Text.Json;
4+
using System.Text.Json.Serialization;
5+
using HarmonyLib;
6+
using MegaCrit.Sts2.Core.Entities.Multiplayer;
7+
using MegaCrit.Sts2.Core.Multiplayer.Messages.Game.Checksums;
8+
using MegaCrit.Sts2.Core.Multiplayer.Serialization;
9+
using MegaCrit.Sts2.Core.Saves.Runs;
10+
using STS2RitsuLib.Networking.MessageExtensions;
11+
12+
namespace STS2RitsuLib.Networking.StateDivergence
13+
{
14+
internal sealed record StateDivergenceSupplementPayload(
15+
uint ChecksumId,
16+
uint ChecksumValue,
17+
int SavedPropertyNetIdBitSize,
18+
uint SavedPropertyMapHash,
19+
IReadOnlyList<string> SavedPropertyNames);
20+
21+
internal static class StateDivergenceSupplementPayloadCodec
22+
{
23+
private const string ExtensionId = "ritsulib.stateDivergence";
24+
private const int PayloadVersion = 1;
25+
private static int _registered;
26+
27+
private static readonly JsonSerializerOptions JsonOptions = new()
28+
{
29+
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
30+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
31+
WriteIndented = false,
32+
};
33+
34+
public static void EnsureRegistered()
35+
{
36+
if (Interlocked.Exchange(ref _registered, 1) == 1)
37+
return;
38+
39+
RitsuNetMessageTailExtensions.Register<StateDivergenceMessage>(
40+
ExtensionId,
41+
PayloadVersion,
42+
SerializePayload,
43+
ReadPayload);
44+
}
45+
46+
public static void Write(PacketWriter writer, StateDivergenceMessage message)
47+
{
48+
EnsureRegistered();
49+
RitsuNetMessageTailExtensions.Write(writer, message);
50+
}
51+
52+
public static void Read(PacketReader reader)
53+
{
54+
EnsureRegistered();
55+
RitsuNetMessageTailExtensions.Read<StateDivergenceMessage>(reader);
56+
}
57+
58+
public static StateDivergenceSupplementPayload CreateLocalSnapshot(NetChecksumData checksum)
59+
{
60+
var propertyNames = GetSavedPropertyNames();
61+
return new(
62+
checksum.id,
63+
checksum.checksum,
64+
SavedPropertiesTypeCache.NetIdBitSize,
65+
StableHash(propertyNames),
66+
propertyNames);
67+
}
68+
69+
private static string? SerializePayload(StateDivergenceMessage message)
70+
{
71+
try
72+
{
73+
var payload = CreateLocalSnapshot(message.senderChecksum);
74+
var json = JsonSerializer.Serialize(payload, JsonOptions);
75+
return Convert.ToBase64String(Gzip(Encoding.UTF8.GetBytes(json)));
76+
}
77+
catch (Exception ex)
78+
{
79+
RitsuLibFramework.Logger.Warn(
80+
$"[State divergence diagnostics] Failed to create supplement payload: {ex.Message}");
81+
return null;
82+
}
83+
}
84+
85+
private static void ReadPayload(int version, string encoded)
86+
{
87+
try
88+
{
89+
if (version != PayloadVersion)
90+
{
91+
RitsuLibFramework.Logger.Warn(
92+
$"[State divergence diagnostics] Unsupported supplement payload version: {version}");
93+
return;
94+
}
95+
96+
var bytes = Gunzip(Convert.FromBase64String(encoded));
97+
var payload = JsonSerializer.Deserialize<StateDivergenceSupplementPayload>(
98+
Encoding.UTF8.GetString(bytes),
99+
JsonOptions);
100+
if (payload != null)
101+
StateDivergenceSupplementStore.Store(payload);
102+
}
103+
catch (Exception ex)
104+
{
105+
RitsuLibFramework.Logger.Warn(
106+
$"[State divergence diagnostics] Failed to read supplement payload: {ex.Message}");
107+
}
108+
}
109+
110+
private static IReadOnlyList<string> GetSavedPropertyNames()
111+
{
112+
return AccessTools.DeclaredField(typeof(SavedPropertiesTypeCache), "_netIdToPropertyNameMap")
113+
?.GetValue(null) is List<string> names
114+
? names.ToArray()
115+
: [];
116+
}
117+
118+
private static byte[] Gzip(byte[] data)
119+
{
120+
using var output = new MemoryStream();
121+
using (var gzip = new GZipStream(output, CompressionLevel.SmallestSize, true))
122+
{
123+
gzip.Write(data, 0, data.Length);
124+
}
125+
126+
return output.ToArray();
127+
}
128+
129+
private static byte[] Gunzip(byte[] data)
130+
{
131+
using var input = new MemoryStream(data, false);
132+
using var gzip = new GZipStream(input, CompressionMode.Decompress);
133+
using var output = new MemoryStream();
134+
gzip.CopyTo(output);
135+
return output.ToArray();
136+
}
137+
138+
private static uint StableHash(IEnumerable<string> values)
139+
{
140+
unchecked
141+
{
142+
var hash = 2166136261u;
143+
foreach (var value in values)
144+
{
145+
foreach (var ch in value)
146+
{
147+
hash ^= ch;
148+
hash *= 16777619u;
149+
}
150+
151+
hash ^= 0xffu;
152+
hash *= 16777619u;
153+
}
154+
155+
return hash;
156+
}
157+
}
158+
}
159+
160+
internal static class StateDivergenceSupplementStore
161+
{
162+
private static readonly Lock SyncRoot = new();
163+
164+
private static readonly Dictionary<(uint Id, uint Checksum), Queue<StateDivergenceSupplementPayload>> Payloads =
165+
new();
166+
167+
public static void Store(StateDivergenceSupplementPayload payload)
168+
{
169+
lock (SyncRoot)
170+
{
171+
var key = (payload.ChecksumId, payload.ChecksumValue);
172+
if (!Payloads.TryGetValue(key, out var queue))
173+
{
174+
queue = new();
175+
Payloads[key] = queue;
176+
}
177+
178+
queue.Enqueue(payload);
179+
}
180+
}
181+
182+
public static bool TryTake(NetChecksumData checksum, out StateDivergenceSupplementPayload payload)
183+
{
184+
lock (SyncRoot)
185+
{
186+
var key = (checksum.id, checksum.checksum);
187+
if (!Payloads.TryGetValue(key, out var queue) || queue.Count == 0)
188+
{
189+
payload = null!;
190+
return false;
191+
}
192+
193+
payload = queue.Dequeue();
194+
if (queue.Count == 0)
195+
Payloads.Remove(key);
196+
return true;
197+
}
198+
}
199+
}
200+
}

RitsuLibFramework.PatcherSetup.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,8 @@ private static void RegisterLifecyclePatches()
208208
patcher.RegisterPatch<JoinFailureDiagnosticsPopupCreatePatch>();
209209
patcher.RegisterPatch<JoinFailureDiagnosticsPopupReadyPatch>();
210210
patcher.RegisterPatch<StateDivergenceDiagnosticsLogPatch>();
211+
patcher.RegisterPatch<StateDivergenceSupplementSerializePatch>();
212+
patcher.RegisterPatch<StateDivergenceSupplementDeserializePatch>();
211213
patcher.RegisterPatch<StateDivergenceDiagnosticsPopupCreatePatch>();
212214
patcher.RegisterPatch<StateDivergenceDiagnosticsPopupReadyPatch>();
213215
patcher.RegisterPatch<RunEndedLifecyclePatch>();

0 commit comments

Comments
 (0)