diff --git a/1.3/Assemblies/DoorsExpanded.dll b/1.3/Assemblies/DoorsExpanded.dll index 359c99a..8ad571d 100644 Binary files a/1.3/Assemblies/DoorsExpanded.dll and b/1.3/Assemblies/DoorsExpanded.dll differ diff --git a/About/About.xml b/About/About.xml index 43ba2f1..1c3c564 100644 --- a/About/About.xml +++ b/About/About.xml @@ -18,7 +18,7 @@ https://github.com/pardeike/HarmonyRimWorld/releases/latest - 1.4.0.0 (08-08-2021) + 1.4.0.1 (09-10-2021) Adds new types and sizes of doors to RimWorld and an extensible framework for other mods to add such doors. @@ -42,6 +42,13 @@ Cade Perkinson, Jay Sacane, John Pahl, Tankok1998 also known as the Shermanlover ======================== Changelog ======================== +1.4.0.1 (09-10-2021) +======================== +Fix crash due to infinite recursion in RW 1.3.3116+ +Fix blueprints and frames of our doors not having proper labels +Fix CompProperties_PostProcessText not being removed after processing if not in dev mode (minor optimization) +Update PlaceWorker_OnTopOfWalls to consider smoothed buildings as walls (note: if JecsTools is loaded before Doors Expanded, this fix won't apply for the time being) + 1.4.0.0 (08-08-2021) ======================== Note: Starting from this version, changes only apply to RimWorld 1.3+. diff --git a/About/Changelog.txt b/About/Changelog.txt index c81905b..c8feaab 100644 --- a/About/Changelog.txt +++ b/About/Changelog.txt @@ -1,3 +1,10 @@ +1.4.0.1 (09-10-2021) +======================== +Fix crash due to infinite recursion in RW 1.3.3116+ +Fix blueprints and frames of our doors not having proper labels +Fix CompProperties_PostProcessText not being removed after processing if not in dev mode (minor optimization) +Update PlaceWorker_OnTopOfWalls to consider smoothed buildings as walls (note: if JecsTools is loaded before Doors Expanded, this fix won't apply for the time being) + 1.4.0.0 (08-08-2021) ======================== Note: Starting from this version, changes only apply to RimWorld 1.3+. diff --git a/About/Manifest.xml b/About/Manifest.xml index ad90f22..dd17c7e 100644 --- a/About/Manifest.xml +++ b/About/Manifest.xml @@ -1,7 +1,7 @@  DoorsExpanded - 1.4.0.0 + 1.4.0.1 https://raw.githubusercontent.com/jecrell/DoorsExpanded/master/About/Manifest.xml diff --git a/About/Version.txt b/About/Version.txt index 149bb3c..10be734 100644 --- a/About/Version.txt +++ b/About/Version.txt @@ -1 +1 @@ -1.4.0.0 +1.4.0.1 diff --git a/Source/Building_DoorRegionHandler.cs b/Source/Building_DoorRegionHandler.cs index 931587a..aa94917 100644 --- a/Source/Building_DoorRegionHandler.cs +++ b/Source/Building_DoorRegionHandler.cs @@ -84,19 +84,10 @@ public Building_DoorExpanded ParentDoor public override void SpawnSetup(Map map, bool respawningAfterLoad) { TLog.Log(this); - // Building_Door.SpawnSetup calls BlockedOpenMomentary, which will be delegating to Building_DoorExpanded. - // Since that Building_DoorExpanded may not be spawned yet, we want to avoid this. - // Building_Door.SpawnSetup also calls ClearReachabilityCache, which is redundant with Building_DoorExpanded - // (although not harmful). - // So we skip calling Building_Door.SpawnSetup (via base.SpawnSetup) and instead call Building.SpawnSetup. - var Building_SpawnSetup = (Action)Activator.CreateInstance(typeof(Action), this, - methodof_Building_SpawnSetup.MethodHandle.GetFunctionPointer()); - Building_SpawnSetup(map, respawningAfterLoad); + // See HarmonyPatches.InvisDoorSpawnSetupTranspiler. + base.SpawnSetup(map, respawningAfterLoad); } - private static readonly MethodInfo methodof_Building_SpawnSetup = - AccessTools.Method(typeof(Building), nameof(Building.SpawnSetup)); - public override void DeSpawn(DestroyMode mode = DestroyMode.Vanish) { TLog.Log(this); @@ -159,18 +150,10 @@ public override void Tick() ticksSinceOpen = parentDoor.TicksSinceOpen; ticksUntilClose = parentDoor.TicksUntilClose; - // We're delegating all the Tick logic to Building_DoorExpanded, which syncs its fields with its invis doors as needed. - // So we skip calling Building_Door.Tick (via base.Tick()) and instead call Building.Tick (actually ThingWithComps.Tick). - // Not replicating the logic in ThingWithComps.Tick, in case the logic changes or another mod patches that method. - Building_Tick ??= (Action)Activator.CreateInstance(typeof(Action), this, - methodof_Building_Tick.MethodHandle.GetFunctionPointer()); - Building_Tick(); + // See HarmonyPatches.InvisDoorTickTranspiler. + base.Tick(); } - private static readonly MethodInfo methodof_Building_Tick = - AccessTools.Method(typeof(Building), nameof(Building.Tick)); - private Action Building_Tick; - public override void PostApplyDamage(DamageInfo dinfo, float totalDamageDealt) { // Note: The invis door def has useHitPoints=true, so invis doors never take damage. diff --git a/Source/Building_Door_Reference/RW1.3_migration_notes.txt b/Source/Building_Door_Reference/RW1.3_migration_notes.txt index bdd1890..34eb016 100644 --- a/Source/Building_Door_Reference/RW1.3_migration_notes.txt +++ b/Source/Building_Door_Reference/RW1.3_migration_notes.txt @@ -280,6 +280,7 @@ Room members that are new: public bool ProperRoom private int OpenRoofCountStopAt(int threshold) Room.OpenRoofCountStopAt already exists, but its code is moved to District.OpenRoofCountStopAt + public IEnumerable ContainedThingsList(IEnumerable thingDefs) private string DebugRolesString() public override string ToString() Room.ToString already exists, but its code is moved to District.ToString diff --git a/Source/CompProperties_PostProcessText.cs b/Source/CompProperties_PostProcessText.cs index 1b9485e..34060e3 100644 --- a/Source/CompProperties_PostProcessText.cs +++ b/Source/CompProperties_PostProcessText.cs @@ -14,8 +14,16 @@ public class CompProperties_PostProcessText : CompProperties public ThingDef defaultLabelAndDescriptionFrom; public bool appendSizeToLabel; - [Unsaved(false)] - private string origLabel; + [Unsaved] + public string origLabel; + + [Unsaved] + public string baseLabel; + + [Unsaved] + private bool finalized = false; + + public static readonly Dictionary defToComp = new(); public CompProperties_PostProcessText() { @@ -24,30 +32,77 @@ public CompProperties_PostProcessText() public override void ResolveReferences(ThingDef parentDef) { - // Note: ResolveReferences can run more than once for a single def, so ensure it's idempotent. + // This method can be called multiple times per instance, so ensure idempotency. + if (finalized) + return; + + defToComp[parentDef] = this; + + origLabel = parentDef.label; + // This logic can't be done in PostLoadSpecial due to needing defaultLabelAndDescriptionFrom needing to be resolved. if (defaultLabelAndDescriptionFrom is not null) { + var otherPostProcessText = defaultLabelAndDescriptionFrom.GetCompProperties(); + otherPostProcessText?.ResolveReferences(defaultLabelAndDescriptionFrom); if (parentDef.label.NullOrEmpty()) - parentDef.label = - defaultLabelAndDescriptionFrom.GetCompProperties()?.origLabel ?? - defaultLabelAndDescriptionFrom.label; + { + var newLabel = otherPostProcessText?.baseLabel ?? defaultLabelAndDescriptionFrom.label; + if (TLog.Enabled) + TLog.Log(this, $"{parentDef}.label: {QuoteString(parentDef.label)} => {QuoteString(newLabel)}"); + parentDef.label = newLabel; + } if (parentDef.description.NullOrEmpty()) - parentDef.description = defaultLabelAndDescriptionFrom.description; + { + var newDescription = defaultLabelAndDescriptionFrom.description; + if (TLog.Enabled) + TLog.Log(this, $"{parentDef}.description: {QuoteString(parentDef.description)} => {QuoteString(newDescription)}"); + parentDef.description = newDescription; + } } - if (appendSizeToLabel && origLabel is null) + baseLabel = parentDef.label; + if (appendSizeToLabel) { - origLabel = parentDef.label; var size = parentDef.Size; - parentDef.label = $"{parentDef.label} ({size.x}x{size.z})"; + var newLabel = $"{parentDef.label} ({size.x}x{size.z})"; + if (TLog.Enabled) + TLog.Log(this, $"{parentDef}.label: {QuoteString(parentDef.label)} => {QuoteString(newLabel)}"); + parentDef.label = newLabel; } + finalized = true; } - // This serves as a useful hook for cleaning up after ourselves. - public override IEnumerable ConfigErrors(ThingDef parentDef) + internal static string QuoteString(string str) => str is null ? "null" : $"\"{str.Replace("\\", "\\\\").Replace("\"", "\\\"")}\""; + } + + [StaticConstructorOnStartup] + public static class PostProcessTextOnStartup + { + static PostProcessTextOnStartup() { - // Comps add overhead, so since we're done, remove ourselves. - parentDef.comps.Remove(this); - yield break; + // Update frames and blueprints that were implicitly generated for defs with CompProperties_PostProcessText. + // These are generated before ResolveReferences are ever called. + foreach (var def in DefDatabase.AllDefsListForReading) + { + if (def.entityDefToBuild is ThingDef entityDef && + CompProperties_PostProcessText.defToComp.TryGetValue(entityDef, out var postProcessTextComp)) + { + var newLabel = entityDef.label + + (postProcessTextComp.origLabel.NullOrEmpty() ? def.label : def.label.Substring(postProcessTextComp.origLabel.Length)); + if (TLog.Enabled) + TLog.Log(typeof(PostProcessTextOnStartup), string.Format("{0}.label: {1} => {2}", + def, CompProperties_PostProcessText.QuoteString(def.label), CompProperties_PostProcessText.QuoteString(newLabel))); + def.label = newLabel; + } + } + + // Comps add overhead, so since we're done, remove the comps now that they're no longer needed. + foreach (var (def, postProcessTextComp) in CompProperties_PostProcessText.defToComp) + { + if (TLog.Enabled) + TLog.Log(typeof(PostProcessTextOnStartup), $"Removing {postProcessTextComp} from {def}"); + def.comps.Remove(postProcessTextComp); + } + CompProperties_PostProcessText.defToComp.Clear(); } } } diff --git a/Source/HarmonyPatches.cs b/Source/HarmonyPatches.cs index 1f49a5f..8f2c1c9 100644 --- a/Source/HarmonyPatches.cs +++ b/Source/HarmonyPatches.cs @@ -332,6 +332,10 @@ public static void Patches() Patch(original: AccessTools.Method(typeof(Building_Door), nameof(Building_Door.StartManualCloseBy)), prefix: nameof(InvisDoorStartManualCloseByPrefix), priority: Priority.First); + Patch(original: AccessTools.Method(typeof(Building_Door), nameof(Building_Door.SpawnSetup)), + transpiler: nameof(InvisDoorSpawnSetupTranspiler)); + Patch(original: AccessTools.Method(typeof(Building_Door), nameof(Building_Door.Tick)), + transpiler: nameof(InvisDoorTickTranspiler)); // Patches to redirect access from invis door def to its parent door def. Patch(original: AccessTools.Method(typeof(GenStep_Terrain), nameof(GenStep_Terrain.Generate)), @@ -745,6 +749,56 @@ public static bool InvisDoorStartManualCloseByPrefix(Building_Door __instance, P return true; } + // Building_Door.SpawnSetup + public static IEnumerable InvisDoorSpawnSetupTranspiler(IEnumerable instructions, ILGenerator ilGen) + { + // Building_Door.SpawnSetup calls BlockedOpenMomentary, which will be delegating to Building_DoorExpanded. + // If this is a Building_DoorRegionHandler, that Building_DoorExpanded may not be spawned yet, so we want to avoid this. + // (It also calls ClearReachabilityCache, which is redundant with Building_DoorExpanded, although not harmful). + // So if this is a Building_DoorRegionHandler, exit early after the call to Building.SpawnSetup. + // Historical: this used to be done in Building_DoorRegionHandler.SpawnSetup override, but the way it was done + // no longer seems to work in RW 1.3.3116+ (due to the Unity update?); hence, this Harmony patch. + return InvisDoorBaseCallTranspiler(instructions, ilGen, AccessTools.Method(typeof(Building), nameof(Building.SpawnSetup))); + } + + // Building_Door.Tick + public static IEnumerable InvisDoorTickTranspiler(IEnumerable instructions, ILGenerator ilGen) + { + // If this is a Building_DoorRegionHandler, all Tick logic is delegated to Building_DoorExpanded, + // which syncs its fields with its invis doors as needed. So all the Tick logic in Building_Door itself should be skipped, + // other than that in Building.Tick, or rather ThingWithComps.Tick, since Building.Tick doesn't exist. + // Not replicating the logic in ThingWithComps.Tick, in case the logic changes or another mod patches that method. + // Historical: this used to be done in Building_DoorRegionHandler.Tick override, but the way it was done + // no longer seems to work in RW 1.3.3116+ (due to the Unity update?); hence, this Harmony patch. + return InvisDoorBaseCallTranspiler(instructions, ilGen, AccessTools.Method(typeof(Building), nameof(Building.Tick))); + } + + // Generic transpiler that transforms all following instances of code: + // base.(...) + // into: + // base.(...) + // if (this is Building_DoorRegionHandler) + // return; + private static IEnumerable InvisDoorBaseCallTranspiler(IEnumerable instructions, ILGenerator ilGen, + MethodInfo baseMethod) + { + var instructionsList = instructions.AsList(); + + var baseCallIndex = instructionsList.FindIndex(instr => instr.Calls(baseMethod)); + var afterBaseCallLabel = ilGen.DefineLabel(); + var afterBaseCallInstr = instructionsList[baseCallIndex + 1]; + instructionsList.SafeInsertRange(baseCallIndex + 1, new[] + { + new CodeInstruction(OpCodes.Ldarg_0), + new CodeInstruction(OpCodes.Isinst, typeof(Building_DoorRegionHandler)), + new CodeInstruction(OpCodes.Brfalse, afterBaseCallLabel), + new CodeInstruction(OpCodes.Ret), + }); + afterBaseCallInstr.labels.Add(afterBaseCallLabel); + + return instructionsList; + } + // GenStep_Terrain.Generate // GenGrid.CanBeSeenOver // GridsUtility.Filled diff --git a/Source/PlaceWorker_OnTopOfWalls.cs b/Source/PlaceWorker_OnTopOfWalls.cs index 8980860..a0f4ce5 100644 --- a/Source/PlaceWorker_OnTopOfWalls.cs +++ b/Source/PlaceWorker_OnTopOfWalls.cs @@ -17,8 +17,8 @@ public override AcceptanceReport AllowsPlacing(BuildableDef checkingDef, IntVec3 static bool IsWall(Thing thing) { - // In RW 1.3+, it seems like BuildingProperties.isPlaceOverableWall indicates whether something is a "wall". - if (thing.def.building?.isPlaceOverableWall ?? false) + // In RW 1.3+, it seems like BuildingProperties.isPlaceOverableWall or ThingDef.IsSmoothed indicates whether something is a "wall". + if (thing.def is { building: { isPlaceOverableWall: true } } or { IsSmoothed: true }) return true; // Legacy heuristic for mods that don't use isPlaceOverableWall. if (thing.def.defName.Contains("Wall")) diff --git a/Source/ProjectHeron.csproj b/Source/ProjectHeron.csproj index 8d4d0e0..7fd124c 100644 --- a/Source/ProjectHeron.csproj +++ b/Source/ProjectHeron.csproj @@ -8,7 +8,7 @@ true 9.0 false - 1.4.0.0 + 1.4.0.1 diff --git a/Source/TLog.cs b/Source/TLog.cs index 36ff648..1c89693 100644 --- a/Source/TLog.cs +++ b/Source/TLog.cs @@ -35,10 +35,11 @@ public static void Log(object obj, string message = null, [CallerMemberName] str return; } + var context = Current.Game is { } game ? $"Tick {game.tickManager.TicksGame}" : $"{Current.ProgramState}"; message ??= obj.ToString(); if (logLevel is TLogLevel.Debug) { - message = $"[Tick {Find.TickManager.TicksGame}] {message} called from {obj.GetType()}:{callerMemberName}"; + message = $"[{context}] {message} called from {obj as Type ?? obj.GetType()}:{callerMemberName}"; } else // if (logLevel is TLogLevel.StackTrace) { @@ -58,12 +59,12 @@ public static void Log(object obj, string message = null, [CallerMemberName] str updateValueFactory: (_, counter) => counter + 1); return $"{methodName} {{{counter}}}"; }); - message = $"[Tick {Find.TickManager.TicksGame}] {message} called from {id}"; + message = $"[{context}] {message} called from {id}"; if (newId) message += "\n" + stackTraceStr; } - // Workaround for RW 1.2 now monitoring all Unity Debug usage for the 1000 max message limit: + // Workaround for RW 1.2+ now monitoring all Unity Debug usage for the 1000 max message limit: if (!UnityEngine.Debug.unityLogger.logEnabled) Verse.Log.ResetMessageCount();