using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyCompany("FixSave")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.2.0.0")] [assembly: AssemblyInformationalVersion("0.2.0+20d46d95f64c44821203859faa8c8449a634cfc9")] [assembly: AssemblyProduct("FixSave")] [assembly: AssemblyTitle("FixSave")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.2.0.0")] [module: UnverifiableCode] namespace Balguito.Valheim.FixSave; [BepInPlugin("balguito.valheim.fixsave", "Balguito Fix Save", "0.2.0")] public class BalguitoFixSave : BaseUnityPlugin { internal static ManualLogSource Log; internal static ConfigEntry Enabled; internal static ConfigEntry Verbose; internal static ConfigEntry RemoveSizeChangedChunks; internal static ConfigEntry LogManifestOverlaps; internal static ConfigEntry TopChunksToLog; internal static ConfigEntry RepairMode; internal static ConfigEntry RepairIncludePortals; internal static ConfigEntry RepairPositionPrecision; internal static ConfigEntry RepairRotationPrecision; internal static ConfigEntry RepairMaxRemovalsPerSave; internal static ConfigEntry RepairTopGroupsToLog; internal static ConfigEntry ForceFullRepairSave; internal static ConfigEntry PruneManifestToFullRepairChunks; private Harmony _harmony; private void Awake() { //IL_01bb: Unknown result type (might be due to invalid IL or missing references) //IL_01c5: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; Enabled = ((BaseUnityPlugin)this).Config.Bind("General", "Enabled", true, "Enable chunked save diagnostics/fixes."); RemoveSizeChangedChunks = ((BaseUnityPlugin)this).Config.Bind("Fixes", "RemoveSizeChangedChunks", true, "Remove stale chunk mappings marked as sizeChanged before writing the .chunks manifest."); RepairMode = ((BaseUnityPlugin)this).Config.Bind("Repair", "RepairMode", "Disabled", "Repair mode: Disabled, ScanOnly, FilterSaveExact. Disabled is the public-safe default. ScanOnly logs duplicate save clones. FilterSaveExact removes duplicate clones from the save output before chunks are written."); ForceFullRepairSave = ((BaseUnityPlugin)this).Config.Bind("Repair", "ForceFullRepairSave", false, "Force one full-world repair save by marking all possible chunks as dirty before GetSaveClonePerChunk. Use with RepairMode=FilterSaveExact, then turn it off after verifying the next load."); PruneManifestToFullRepairChunks = ((BaseUnityPlugin)this).Config.Bind("Repair", "PruneManifestToFullRepairChunks", true, "During ForceFullRepairSave, remove non-portal chunk mappings from the manifest if they were not part of the rewritten full repair output."); RepairIncludePortals = ((BaseUnityPlugin)this).Config.Bind("Repair", "RepairIncludePortals", false, "Include portal prefabs in duplicate filtering. Recommended false."); RepairPositionPrecision = ((BaseUnityPlugin)this).Config.Bind("Repair", "PositionPrecision", 100, "Position quantization multiplier. 100 = centimeters. Higher is stricter."); RepairRotationPrecision = ((BaseUnityPlugin)this).Config.Bind("Repair", "RotationPrecision", 10000, "Rotation quaternion quantization multiplier. Higher is stricter."); RepairMaxRemovalsPerSave = ((BaseUnityPlugin)this).Config.Bind("Repair", "MaxRemovalsPerSave", 0, "Safety cap for FilterSaveExact. 0 = no cap. If cap is reached, extra duplicates are kept."); RepairTopGroupsToLog = ((BaseUnityPlugin)this).Config.Bind("Repair", "TopGroupsToLog", 10, "How many duplicate groups to log when repair diagnostics find duplicates."); LogManifestOverlaps = ((BaseUnityPlugin)this).Config.Bind("Diagnostics", "LogManifestOverlaps", false, "Detect and log overlapping chunk mappings before writing the .chunks manifest. Disabled by default because the geometric detector is noisy."); TopChunksToLog = ((BaseUnityPlugin)this).Config.Bind("Diagnostics", "TopChunksToLog", 0, "How many largest chunks to log during verbose save diagnostics."); Verbose = ((BaseUnityPlugin)this).Config.Bind("Diagnostics", "Verbose", false, "Enable verbose diagnostics. Public default is false to avoid log spam."); _harmony = new Harmony("balguito.valheim.fixsave"); _harmony.PatchAll(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Balguito Fix Save 0.2.0 loaded - build 20260629-211115"); ((BaseUnityPlugin)this).Logger.LogWarning((object)"Balguito Fix Save is intended for Valheim public-test 0.221.13 chunked saves. Back up the full world folder before enabling RepairMode or ForceFullRepairSave."); } private void OnDestroy() { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } } [HarmonyPatch(typeof(ZDOMan), "PrepareSave")] internal static class Patch_ZDOMan_PrepareSave_Diagnostics { private static void Prefix(ZDOMan __instance) { if (BalguitoFixSave.Enabled.Value && BalguitoFixSave.Verbose.Value) { BalguitoFixSave.Log.LogInfo((object)$"[ChunkedSaveFix] PrepareSave START. MemoryZDOs={__instance.NrOfObjects():N0}"); } } private static void Postfix(ZDOMan __instance) { if (BalguitoFixSave.Enabled.Value && BalguitoFixSave.Verbose.Value) { BalguitoFixSave.Log.LogInfo((object)$"[ChunkedSaveFix] PrepareSave END. MemoryZDOs={__instance.NrOfObjects():N0}"); } } } [HarmonyPatch(typeof(ZDOMan), "GetSaveClonePerChunk")] internal static class Patch_ZDOMan_GetSaveClonePerChunk_DiagnosticsAndRepair { private static void Prefix(ZDOMan __instance) { FullRepairState.BeforeGetSaveClonePerChunk(__instance); } private static void Postfix(List>> __result) { //IL_017c: Unknown result type (might be due to invalid IL or missing references) //IL_0181: Unknown result type (might be due to invalid IL or missing references) //IL_01a0: Unknown result type (might be due to invalid IL or missing references) if (!BalguitoFixSave.Enabled.Value) { return; } int num = __result?.Count ?? 0; int num2 = __result?.Sum((Tuple> x) => x.Item2?.Count ?? 0) ?? 0; if (BalguitoFixSave.Verbose.Value || FullRepairState.Active) { BalguitoFixSave.Log.LogInfo((object)$"[ChunkedSaveFix] GetSaveClonePerChunk BEFORE repair. DirtyChunksToWrite={num:N0}, ZDOsToWrite={num2:N0}, FullRepair={FullRepairState.Active}"); } if (__result == null) { return; } SaveCloneRepair.Run(__result); FullRepairState.AfterGetSaveClonePerChunk(__result); int count = __result.Count; int num3 = __result.Sum((Tuple> x) => x.Item2?.Count ?? 0); int num4 = num2 - num3; if (num4 > 0 || BalguitoFixSave.Verbose.Value || FullRepairState.Active) { BalguitoFixSave.Log.LogInfo((object)$"[ChunkedSaveFix] GetSaveClonePerChunk AFTER repair. DirtyChunksToWrite={count:N0}, ZDOsToWrite={num3:N0}, Removed={num4:N0}, FullRepair={FullRepairState.Active}"); } if (!BalguitoFixSave.Verbose.Value) { return; } int count2 = Math.Max(0, BalguitoFixSave.TopChunksToLog.Value); foreach (Tuple> item2 in __result.OrderByDescending((Tuple> x) => x.Item2?.Count ?? 0).Take(count2)) { ChunkIndex item = item2.Item1; int num5 = item2.Item2?.Count ?? 0; BalguitoFixSave.Log.LogInfo((object)$"[ChunkedSaveFix] write chunk={ChunkDebug.DescribeChunkIndex(item)}, zdos={num5:N0}"); } } } [HarmonyPatch(typeof(ChunkSaveMapping), "Save")] internal static class Patch_ChunkSaveMapping_Save { private static void Prefix(ChunkSaveMapping __instance) { if (!BalguitoFixSave.Enabled.Value) { return; } Dictionary chunks = __instance.Chunks; if (chunks == null) { BalguitoFixSave.Log.LogWarning((object)"[ChunkedSaveFix] ChunkSaveMapping.Save START. chunks=null"); return; } LogManifest("BEFORE", chunks); if (BalguitoFixSave.LogManifestOverlaps.Value) { ChunkDebug.LogOverlaps(chunks, "BEFORE"); } if (BalguitoFixSave.RemoveSizeChangedChunks.Value) { RemoveSizeChanged(chunks); } FullRepairState.PruneManifest(chunks); LogManifest("AFTER", chunks); if (BalguitoFixSave.LogManifestOverlaps.Value) { ChunkDebug.LogOverlaps(chunks, "AFTER"); } } private static void LogManifest(string phase, Dictionary chunks) { //IL_0111: Unknown result type (might be due to invalid IL or missing references) int num = chunks.Values.Sum((ChunkInfo x) => x?.m_numZDOs ?? 0); int num2 = chunks.Count((KeyValuePair x) => x.Value != null && x.Value.m_sizeChanged); if (BalguitoFixSave.Verbose.Value || num2 > 0 || FullRepairState.Active) { BalguitoFixSave.Log.LogInfo((object)$"[ChunkedSaveFix] ChunkSaveMapping.Save {phase}. ManifestChunks={chunks.Count:N0}, ManifestZDOs={num:N0}, SizeChanged={num2:N0}"); } if (!BalguitoFixSave.Verbose.Value) { return; } int count = Math.Max(0, BalguitoFixSave.TopChunksToLog.Value); foreach (KeyValuePair item in chunks.OrderByDescending((KeyValuePair x) => x.Value?.m_numZDOs ?? 0).Take(count)) { BalguitoFixSave.Log.LogInfo((object)("[ChunkedSaveFix] manifest " + phase + " chunk=" + ChunkDebug.DescribeChunk(item.Key, item.Value))); } } private static void RemoveSizeChanged(Dictionary chunks) { //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) List list = (from kv in chunks where kv.Value != null && kv.Value.m_sizeChanged select kv.Key).ToList(); if (list.Count == 0) { return; } foreach (ChunkIndex item in list) { if (chunks.TryGetValue(item, out var value)) { chunks.Remove(item); BalguitoFixSave.Log.LogWarning((object)("[ChunkedSaveFix] Removed stale resized chunk from manifest: " + ChunkDebug.DescribeChunk(item, value))); } } BalguitoFixSave.Log.LogWarning((object)$"[ChunkedSaveFix] Removed {list.Count:N0} stale resized chunk mapping(s) before writing .chunks manifest"); } } internal static class SaveCloneRepair { private struct ZdoRef { public readonly int ChunkListIndex; public readonly int ZdoIndex; public readonly ZDO Zdo; public ZdoRef(int chunkListIndex, int zdoIndex, ZDO zdo) { ChunkListIndex = chunkListIndex; ZdoIndex = zdoIndex; Zdo = zdo; } } private class DuplicateGroupStats { public readonly DedupeKey Key; public int Count; public ZdoRef Keeper; public DuplicateGroupStats(DedupeKey key, ZdoRef keeper) { Key = key; Keeper = keeper; Count = 1; } } private struct DedupeKey : IEquatable { public int Prefab; public int X; public int Y; public int Z; public int RX; public int RY; public int RZ; public int RW; public static DedupeKey FromZdo(ZDO zdo, int positionPrecision, int rotationPrecision) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) Vector3 position = zdo.GetPosition(); Quaternion rotation = zdo.GetRotation(); return new DedupeKey { Prefab = zdo.GetPrefab(), X = Quantize(position.x, positionPrecision), Y = Quantize(position.y, positionPrecision), Z = Quantize(position.z, positionPrecision), RX = Quantize(rotation.x, rotationPrecision), RY = Quantize(rotation.y, rotationPrecision), RZ = Quantize(rotation.z, rotationPrecision), RW = Quantize(rotation.w, rotationPrecision) }; } public Vector3 ToApproxPosition(int positionPrecision) { //IL_0026: Unknown result type (might be due to invalid IL or missing references) int num = Math.Max(1, positionPrecision); return new Vector3((float)X / (float)num, (float)Y / (float)num, (float)Z / (float)num); } private static int Quantize(float value, int precision) { return Mathf.RoundToInt(value * (float)precision); } public bool Equals(DedupeKey other) { if (Prefab == other.Prefab && X == other.X && Y == other.Y && Z == other.Z && RX == other.RX && RY == other.RY && RZ == other.RZ) { return RW == other.RW; } return false; } public override bool Equals(object obj) { if (obj is DedupeKey other) { return Equals(other); } return false; } public override int GetHashCode() { return (((((((((((((Prefab * 397) ^ X) * 397) ^ Y) * 397) ^ Z) * 397) ^ RX) * 397) ^ RY) * 397) ^ RZ) * 397) ^ RW; } } public static void Run(List>> objectsByChunk) { string text = NormalizeMode(BalguitoFixSave.RepairMode.Value); if (text == "disabled") { if (BalguitoFixSave.Verbose.Value) { BalguitoFixSave.Log.LogInfo((object)"[DedupeSave] RepairMode=Disabled"); } return; } bool flag = text == "filtersaveexact"; bool flag2 = text == "scanonly"; if (!flag && !flag2) { BalguitoFixSave.Log.LogError((object)("[DedupeSave] Unknown RepairMode='" + BalguitoFixSave.RepairMode.Value + "'. Valid: Disabled, ScanOnly, FilterSaveExact. Running as ScanOnly.")); flag2 = true; flag = false; } int positionPrecision = Math.Max(1, BalguitoFixSave.RepairPositionPrecision.Value); int rotationPrecision = Math.Max(1, BalguitoFixSave.RepairRotationPrecision.Value); int num = Math.Max(0, BalguitoFixSave.RepairMaxRemovalsPerSave.Value); bool value = BalguitoFixSave.RepairIncludePortals.Value; Dictionary dictionary = new Dictionary(); Dictionary dictionary2 = new Dictionary(); List list = new List(); for (int i = 0; i < objectsByChunk.Count; i++) { List item = objectsByChunk[i].Item2; if (item == null) { continue; } for (int j = 0; j < item.Count; j++) { ZDO val = item[j]; if (val == null || (!value && IsPortal(val))) { continue; } DedupeKey key = DedupeKey.FromZdo(val, positionPrecision, rotationPrecision); ZdoRef zdoRef = new ZdoRef(i, j, val); if (!dictionary.TryGetValue(key, out var value2)) { dictionary[key] = zdoRef; dictionary2[key] = new DuplicateGroupStats(key, zdoRef); continue; } DuplicateGroupStats duplicateGroupStats = dictionary2[key]; duplicateGroupStats.Count++; if (IsBetterKeeper(zdoRef.Zdo, value2.Zdo)) { list.Add(value2); dictionary[key] = zdoRef; duplicateGroupStats.Keeper = zdoRef; } else { list.Add(zdoRef); } } } List list2 = (from x in dictionary2.Values where x.Count > 1 orderby x.Count descending, x.Key.Prefab select x).ToList(); int num2 = list2.Sum((DuplicateGroupStats x) => x.Count - 1); if (num2 > 0 || BalguitoFixSave.Verbose.Value || FullRepairState.Active) { BalguitoFixSave.Log.LogInfo((object)string.Format("[DedupeSave] Mode={0}, DuplicateGroups={1:N0}, DuplicateZDOs={2:N0}, IncludePortals={3}", flag ? "FilterSaveExact" : "ScanOnly", list2.Count, num2, value)); } LogDuplicateGroups(list2); if (!flag) { return; } if (list.Count == 0) { if (BalguitoFixSave.Verbose.Value || FullRepairState.Active) { BalguitoFixSave.Log.LogInfo((object)"[DedupeSave] No duplicate save clones to remove."); } } else if (num > 0 && list.Count > num) { BalguitoFixSave.Log.LogError((object)$"[DedupeSave] Refusing to remove {list.Count:N0} duplicates because MaxRemovalsPerSave={num:N0}. Increase cap or set 0 for no cap."); } else { int num3 = RemoveFromSaveCloneLists(objectsByChunk, list); BalguitoFixSave.Log.LogWarning((object)$"[DedupeSave] Removed {num3:N0} duplicate ZDO save clone(s). This affects the saved .chunk output, not live memory."); } } private static string NormalizeMode(string mode) { return (mode ?? string.Empty).Trim().Replace("_", string.Empty).Replace("-", string.Empty) .ToLowerInvariant(); } private static bool IsPortal(ZDO zdo) { try { return (Object)(object)Game.instance != (Object)null && Game.instance.PortalPrefabHash != null && Game.instance.PortalPrefabHash.Contains(zdo.GetPrefab()); } catch { return false; } } private static bool IsBetterKeeper(ZDO candidate, ZDO current) { //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Unknown result type (might be due to invalid IL or missing references) if (candidate == null) { return false; } if (current == null) { return true; } if (candidate.DataRevision != current.DataRevision) { return candidate.DataRevision > current.DataRevision; } if (candidate.OwnerRevision != current.OwnerRevision) { return candidate.OwnerRevision > current.OwnerRevision; } return CompareZdoId(candidate.m_uid, current.m_uid) > 0; } private static int CompareZdoId(ZDOID a, ZDOID b) { int num = ((ZDOID)(ref a)).UserID.CompareTo(((ZDOID)(ref b)).UserID); if (num != 0) { return num; } return ((ZDOID)(ref a)).ID.CompareTo(((ZDOID)(ref b)).ID); } private static void LogDuplicateGroups(List groups) { //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) if (groups.Count == 0) { return; } int num = Math.Max(0, BalguitoFixSave.RepairTopGroupsToLog.Value); foreach (DuplicateGroupStats item in groups.Take(num)) { Vector3 val = item.Key.ToApproxPosition(BalguitoFixSave.RepairPositionPrecision.Value); BalguitoFixSave.Log.LogWarning((object)$"[DedupeSave] group prefabHash={item.Key.Prefab}, count={item.Count:N0}, approxPos=({val.x:F2},{val.y:F2},{val.z:F2}), keeper={DescribeZdo(item.Keeper.Zdo)}"); } if (groups.Count > num) { BalguitoFixSave.Log.LogWarning((object)$"[DedupeSave] ... {groups.Count - num:N0} more duplicate group(s) not logged."); } } private static int RemoveFromSaveCloneLists(List>> objectsByChunk, List refsToRemove) { Dictionary> dictionary = new Dictionary>(); foreach (ZdoRef item2 in refsToRemove) { if (!dictionary.TryGetValue(item2.ChunkListIndex, out var value)) { value = new HashSet(); dictionary[item2.ChunkListIndex] = value; } value.Add(item2.Zdo); } int num = 0; foreach (KeyValuePair> entry in dictionary) { int key = entry.Key; if (key < 0 || key >= objectsByChunk.Count) { continue; } List item = objectsByChunk[key].Item2; if (item != null && item.Count != 0) { int count = item.Count; item.RemoveAll((ZDO zdo) => entry.Value.Contains(zdo)); num += count - item.Count; } } return num; } private static string DescribeZdo(ZDO zdo) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) if (zdo == null) { return "null"; } Vector3 position = zdo.GetPosition(); return $"uid={zdo.m_uid}, prefabHash={zdo.GetPrefab()}, pos=({position.x:F2},{position.y:F2},{position.z:F2}), dataRev={zdo.DataRevision}, ownerRev={zdo.OwnerRevision}"; } } internal static class ChunkDebug { public static void LogOverlaps(Dictionary chunks, string phase) { //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) List list = chunks.Keys.ToList(); int num = 0; for (int i = 0; i < list.Count; i++) { for (int j = i + 1; j < list.Count; j++) { ChunkIndex val = list[i]; ChunkIndex val2 = list[j]; if (Overlaps(val, val2)) { num++; BalguitoFixSave.Log.LogError((object)$"[ChunkedSaveFix] OVERLAP {phase} #{num}: A={DescribeChunk(val, chunks[val])} | B={DescribeChunk(val2, chunks[val2])}"); } } } if (num == 0) { BalguitoFixSave.Log.LogInfo((object)("[ChunkedSaveFix] No chunk overlaps detected in manifest " + phase + ".")); } else { BalguitoFixSave.Log.LogError((object)$"[ChunkedSaveFix] Detected {num:N0} chunk overlap(s) in manifest {phase}."); } } public static bool Overlaps(ChunkIndex a, ChunkIndex b) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) ValueTuple zoneFromChunk = ZoneSystem.GetZoneFromChunk(a); (int, int) zoneFromChunk2 = ZoneSystem.GetZoneFromChunk(b); int num = ChunkWidth(a); int num2 = ChunkWidth(b); int item = zoneFromChunk.Item1; int item2 = zoneFromChunk.Item2; int num3 = item + num; int num4 = item2 + num; int item3 = zoneFromChunk2.Item1; int item4 = zoneFromChunk2.Item2; int num5 = item3 + num2; int num6 = item4 + num2; if (item < num5 && num3 > item3 && item2 < num6) { return num4 > item4; } return false; } public static int ChunkWidth(ChunkIndex chunkIndex) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) return 64 >> (int)chunkIndex.m_chunkSize; } public static string DescribeChunkIndex(ChunkIndex chunkIndex) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_008c: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) (int, int) zoneFromChunk = ZoneSystem.GetZoneFromChunk(chunkIndex); return $"chunk=0x{chunkIndex.Chunk:x4}, " + $"size={chunkIndex.m_chunkSize}, " + $"zone=({zoneFromChunk.Item1},{zoneFromChunk.Item2}), " + $"width={ChunkWidth(chunkIndex)}, " + $"filePrefix={chunkIndex.Chunk >> 8:x2}_{chunkIndex.Chunk & 0xFF:x2}__{chunkIndex.m_chunkSize}"; } public static string DescribeChunk(ChunkIndex chunkIndex, ChunkInfo info) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Unknown result type (might be due to invalid IL or missing references) if (info == null) { return DescribeChunkIndex(chunkIndex) + ", info=null"; } return DescribeChunkIndex(chunkIndex) + ", " + $"version={info.m_version}, " + $"zdos={info.m_numZDOs:N0}, " + $"sizeChanged={info.m_sizeChanged}, " + "file=" + ChunkFilename(chunkIndex, info); } public static string ChunkFilename(ChunkIndex chunkIndex, ChunkInfo info) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) return $"{chunkIndex.Chunk >> 8:x2}_" + $"{chunkIndex.Chunk & 0xFF:x2}__" + $"{chunkIndex.m_chunkSize}_" + $"{info.m_version}.chunk"; } } internal static class FullRepairState { public static bool Active; public static HashSet WrittenChunks = new HashSet(); public static void BeforeGetSaveClonePerChunk(ZDOMan zdoMan) { Active = false; WrittenChunks.Clear(); if (BalguitoFixSave.Enabled.Value && BalguitoFixSave.ForceFullRepairSave.Value) { if (NormalizeMode(BalguitoFixSave.RepairMode.Value) == "disabled") { BalguitoFixSave.Log.LogWarning((object)"[FullRepair] ForceFullRepairSave=true but RepairMode=Disabled; skipping full repair."); return; } int num = MarkAllPossibleChunksDirty(zdoMan); Active = true; BalguitoFixSave.Log.LogWarning((object)$"[FullRepair] ForceFullRepairSave active. Marked all possible non-portal chunk indexes as dirty. AddedToDirtySet={num:N0}"); } } public static void AfterGetSaveClonePerChunk(List>> objectsByChunk) { //IL_002c: Unknown result type (might be due to invalid IL or missing references) WrittenChunks.Clear(); if (!Active || objectsByChunk == null) { return; } foreach (Tuple> item in objectsByChunk) { WrittenChunks.Add(item.Item1); } BalguitoFixSave.Log.LogWarning((object)$"[FullRepair] Full repair save output contains {WrittenChunks.Count:N0} chunk(s) to keep in manifest."); } public static int PruneManifest(Dictionary chunks) { //IL_009a: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Unknown result type (might be due to invalid IL or missing references) if (!Active) { return 0; } if (!BalguitoFixSave.PruneManifestToFullRepairChunks.Value) { return 0; } if (WrittenChunks == null || WrittenChunks.Count == 0) { BalguitoFixSave.Log.LogError((object)"[FullRepair] Refusing to prune manifest: WrittenChunks is empty."); return 0; } List list = (from chunkIndex in chunks.Keys where !IsPortalChunk(chunkIndex) where !WrittenChunks.Contains(chunkIndex) select chunkIndex).ToList(); foreach (ChunkIndex item in list) { ChunkInfo info = chunks[item]; chunks.Remove(item); BalguitoFixSave.Log.LogWarning((object)("[FullRepair] Pruned stale non-rewritten manifest chunk: " + ChunkDebug.DescribeChunk(item, info))); } if (list.Count > 0) { BalguitoFixSave.Log.LogWarning((object)$"[FullRepair] Pruned {list.Count:N0} stale manifest chunk(s) not present in full repair output."); } return list.Count; } private static int MarkAllPossibleChunksDirty(ZDOMan zdoMan) { //IL_0055: Unknown result type (might be due to invalid IL or missing references) if (zdoMan == null || zdoMan.m_dirtyChunks == null || zdoMan.m_dirtyChunks.Length == 0) { BalguitoFixSave.Log.LogError((object)"[FullRepair] Cannot mark dirty chunks: ZDOMan.m_dirtyChunks unavailable."); return 0; } HashSet hashSet = zdoMan.m_dirtyChunks[0]; if (hashSet == null) { BalguitoFixSave.Log.LogError((object)"[FullRepair] Cannot mark dirty chunks: m_dirtyChunks[0] is null."); return 0; } int count = hashSet.Count; for (byte b = 0; b <= 3; b++) { for (int i = 0; i <= 65535; i++) { hashSet.Add(new ChunkIndex((ushort)i, b)); } } return hashSet.Count - count; } private static bool IsPortalChunk(ChunkIndex chunkIndex) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) return ((ChunkIndex)(ref chunkIndex)).Equals(ZoneSystem.ChunkPortal); } private static string NormalizeMode(string mode) { return (mode ?? string.Empty).Trim().Replace("_", string.Empty).Replace("-", string.Empty) .ToLowerInvariant(); } } internal static class PluginInfo { public const string PLUGIN_GUID = "balguito.valheim.fixsave"; public const string PLUGIN_NAME = "Balguito Fix Save"; public const string PLUGIN_VERSION = "0.2.0"; public const string BUILD_ID = "20260629-211115"; }