Decompiled source of ChunkedSaveFix v0.2.0

plugins/FixSave.dll

Decompiled 3 days ago
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<bool> Enabled;

	internal static ConfigEntry<bool> Verbose;

	internal static ConfigEntry<bool> RemoveSizeChangedChunks;

	internal static ConfigEntry<bool> LogManifestOverlaps;

	internal static ConfigEntry<int> TopChunksToLog;

	internal static ConfigEntry<string> RepairMode;

	internal static ConfigEntry<bool> RepairIncludePortals;

	internal static ConfigEntry<int> RepairPositionPrecision;

	internal static ConfigEntry<int> RepairRotationPrecision;

	internal static ConfigEntry<int> RepairMaxRemovalsPerSave;

	internal static ConfigEntry<int> RepairTopGroupsToLog;

	internal static ConfigEntry<bool> ForceFullRepairSave;

	internal static ConfigEntry<bool> 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<bool>("General", "Enabled", true, "Enable chunked save diagnostics/fixes.");
		RemoveSizeChangedChunks = ((BaseUnityPlugin)this).Config.Bind<bool>("Fixes", "RemoveSizeChangedChunks", true, "Remove stale chunk mappings marked as sizeChanged before writing the .chunks manifest.");
		RepairMode = ((BaseUnityPlugin)this).Config.Bind<string>("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<bool>("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<bool>("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<bool>("Repair", "RepairIncludePortals", false, "Include portal prefabs in duplicate filtering. Recommended false.");
		RepairPositionPrecision = ((BaseUnityPlugin)this).Config.Bind<int>("Repair", "PositionPrecision", 100, "Position quantization multiplier. 100 = centimeters. Higher is stricter.");
		RepairRotationPrecision = ((BaseUnityPlugin)this).Config.Bind<int>("Repair", "RotationPrecision", 10000, "Rotation quaternion quantization multiplier. Higher is stricter.");
		RepairMaxRemovalsPerSave = ((BaseUnityPlugin)this).Config.Bind<int>("Repair", "MaxRemovalsPerSave", 0, "Safety cap for FilterSaveExact. 0 = no cap. If cap is reached, extra duplicates are kept.");
		RepairTopGroupsToLog = ((BaseUnityPlugin)this).Config.Bind<int>("Repair", "TopGroupsToLog", 10, "How many duplicate groups to log when repair diagnostics find duplicates.");
		LogManifestOverlaps = ((BaseUnityPlugin)this).Config.Bind<bool>("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<int>("Diagnostics", "TopChunksToLog", 0, "How many largest chunks to log during verbose save diagnostics.");
		Verbose = ((BaseUnityPlugin)this).Config.Bind<bool>("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<Tuple<ChunkIndex, List<ZDO>>> __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<ChunkIndex, List<ZDO>> 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<ChunkIndex, List<ZDO>> 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<ChunkIndex, List<ZDO>> item2 in __result.OrderByDescending((Tuple<ChunkIndex, List<ZDO>> 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<ChunkIndex, ChunkInfo> 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<ChunkIndex, ChunkInfo> 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<ChunkIndex, ChunkInfo> 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<ChunkIndex, ChunkInfo> item in chunks.OrderByDescending((KeyValuePair<ChunkIndex, ChunkInfo> 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<ChunkIndex, ChunkInfo> 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<ChunkIndex> 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<DedupeKey>
	{
		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<Tuple<ChunkIndex, List<ZDO>>> 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<DedupeKey, ZdoRef> dictionary = new Dictionary<DedupeKey, ZdoRef>();
		Dictionary<DedupeKey, DuplicateGroupStats> dictionary2 = new Dictionary<DedupeKey, DuplicateGroupStats>();
		List<ZdoRef> list = new List<ZdoRef>();
		for (int i = 0; i < objectsByChunk.Count; i++)
		{
			List<ZDO> 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<DuplicateGroupStats> 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<DuplicateGroupStats> 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<Tuple<ChunkIndex, List<ZDO>>> objectsByChunk, List<ZdoRef> refsToRemove)
	{
		Dictionary<int, HashSet<ZDO>> dictionary = new Dictionary<int, HashSet<ZDO>>();
		foreach (ZdoRef item2 in refsToRemove)
		{
			if (!dictionary.TryGetValue(item2.ChunkListIndex, out var value))
			{
				value = new HashSet<ZDO>();
				dictionary[item2.ChunkListIndex] = value;
			}
			value.Add(item2.Zdo);
		}
		int num = 0;
		foreach (KeyValuePair<int, HashSet<ZDO>> entry in dictionary)
		{
			int key = entry.Key;
			if (key < 0 || key >= objectsByChunk.Count)
			{
				continue;
			}
			List<ZDO> 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<ChunkIndex, ChunkInfo> 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<ChunkIndex> 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<int, int> 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<ChunkIndex> WrittenChunks = new HashSet<ChunkIndex>();

	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<Tuple<ChunkIndex, List<ZDO>>> objectsByChunk)
	{
		//IL_002c: Unknown result type (might be due to invalid IL or missing references)
		WrittenChunks.Clear();
		if (!Active || objectsByChunk == null)
		{
			return;
		}
		foreach (Tuple<ChunkIndex, List<ZDO>> 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<ChunkIndex, ChunkInfo> 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<ChunkIndex> 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<ChunkIndex> 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";
}