diff --git a/Source/Client/Patches/Determinism.cs b/Source/Client/Patches/Determinism.cs index 388db51f..6f3b6658 100644 --- a/Source/Client/Patches/Determinism.cs +++ b/Source/Client/Patches/Determinism.cs @@ -587,4 +587,71 @@ static void Postfix(UndercaveMapComponent __instance) } } + [HarmonyPatch(typeof(MoteMaker), nameof(MoteMaker.MakeStaticMote))] + [HarmonyPatch([typeof(Vector3), typeof(Map), typeof(ThingDef), typeof(float), typeof(bool), typeof(float)])] + static class FixNullMotes + { + // Make sure that motes will (almost) always spawn. We skip based to player-specific + // data, and instead only allow the only fully deterministic checks (location is in map bounds). + // We skip checks to current map and current mote counter saturation, as those may vary between players. + + static void Prefix(ref bool makeOffscreen) + { + // Skip a call to GenView.ShouldSpawnMotesAt, forcing + // each call to check GenGrid.InBounds instead. + // ShouldSpawnMotesAt would check for bounds, but also + // include a current map check as well, which we don't want. + makeOffscreen = true; + } + + static IEnumerable Transpiler(IEnumerable instr) + { + var targetCall = AccessTools.DeclaredPropertyGetter(typeof(MoteCounter), nameof(MoteCounter.Saturated)); + + foreach (var ci in instr) + { + yield return ci; + + // Add "& false" to any call to MoteCounter.Saturated to get a deterministic result. + // Not a perfect solution performance-wise, but will prevent desyncs due to + // MoteMaker.MakeStaticMote being non-deterministic without this change. + if (ci.Calls(targetCall)) + { + yield return new CodeInstruction(OpCodes.Ldc_I4_0); + yield return new CodeInstruction(OpCodes.And); + } + } + } + } + + [HarmonyPatch(typeof(Building_BioferriteHarvester), nameof(Building_BioferriteHarvester.SpawnSetup))] + static class AlwaysRebuildBioferriteHarvesterCables + { + static void Postfix(Building_BioferriteHarvester __instance) + { + // After reloading the game the initialize method will generally be called + // during rendering, ensure it's called on respawning as well. + if (!__instance.initalized) + { + // We need to call ExecuteWhenFinished, as it will crash otherwise. + LongEventHandler.ExecuteWhenFinished(__instance.Initialize); + } + } + } + + [HarmonyPatch(typeof(Building_Electroharvester), nameof(Building_Electroharvester.SpawnSetup))] + static class AlwaysRebuildElectroharvesterCables + { + static void Postfix(Building_Electroharvester __instance) + { + // After reloading the game the initialize method will generally be called + // during rendering, ensure it's called on respawning as well. + if (!__instance.initalized) + { + // We need to call ExecuteWhenFinished, as it will crash otherwise. + LongEventHandler.ExecuteWhenFinished(__instance.Initialize); + } + } + } + } diff --git a/Source/Client/Patches/Patches.cs b/Source/Client/Patches/Patches.cs index 37e4e25c..72362ab4 100644 --- a/Source/Client/Patches/Patches.cs +++ b/Source/Client/Patches/Patches.cs @@ -426,31 +426,6 @@ internal static void Choose(QuestPart_Choice part, int index) } } - [HarmonyPatch(typeof(MoteMaker), nameof(MoteMaker.MakeStaticMote))] - [HarmonyPatch(new[] {typeof(Vector3), typeof(Map), typeof(ThingDef), typeof(float), typeof(bool), typeof(float)})] - static class FixNullMotes - { - static Dictionary cache = new(); - - static void Postfix(ThingDef moteDef, ref Mote __result) { - if (__result != null) return; - - if (moteDef.mote.needsMaintenance) return; - - var thingClass = moteDef.thingClass; - - if (cache.TryGetValue(thingClass, out Mote value)) { - __result = value; - } else { - __result = (Mote) Activator.CreateInstance(thingClass); - - cache.Add(thingClass, __result); - } - - __result.def = moteDef; - } - } - [HarmonyPatch(typeof(DiaOption), nameof(DiaOption.Activate))] static class NodeTreeDialogSync {