├── .gitignore ├── 1.3 └── Assemblies │ └── CleanPathfinding.dll ├── 1.4 ├── Assemblies │ └── CleanPathfinding.dll └── Patches │ └── patch.owlchemist.cleanpathfinding.xml ├── About ├── About.xml ├── PublishedFileId.txt └── preview.png ├── LICENSE ├── Languages └── English │ └── Keyed │ └── owlchemist.cleanpathfinding.xml ├── README.md ├── Source ├── CleanPathfinding.csproj ├── CleanPathfindingUtility.cs ├── DoorPathingUtility.cs ├── Mod_CleanPathfinding.cs ├── Patch_Collider.cs ├── Patch_Compat.cs ├── Patch_MapExit.cs ├── Patch_Wander.cs ├── ResourceBank.cs └── Source.code-workspace └── Textures └── UI ├── Owl_DoorPriority.dds ├── Owl_DoorPriority.png └── Owl_Priority.png /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | Source/obj/ 3 | -------------------------------------------------------------------------------- /1.3/Assemblies/CleanPathfinding.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Owlchemist/clean-pathfinding/1fdbd93ae097edc2388a53d7037a463fc04aa7ba/1.3/Assemblies/CleanPathfinding.dll -------------------------------------------------------------------------------- /1.4/Assemblies/CleanPathfinding.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Owlchemist/clean-pathfinding/1fdbd93ae097edc2388a53d7037a463fc04aa7ba/1.4/Assemblies/CleanPathfinding.dll -------------------------------------------------------------------------------- /1.4/Patches/patch.owlchemist.cleanpathfinding.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
  • 6 | Defs/TerrainDef[tags]/tags[li/text()="Road" or li/text()="FineFloor"] 7 | 8 |
  • CleanPath
  • 9 | 10 | 11 |
  • 12 | Always 13 | Defs/TerrainTemplateDef[tags]/tags[li/text()="FineFloor"] 14 | 15 |
  • CleanPath
  • 16 | 17 | 18 |
  • 19 | Defs/TerrainDef[defName="PavedTile" or defName="Concrete" or @Name="TileStoneBase"][not(tags)] 20 | 21 | 22 | 23 |
  • 24 |
  • 25 | Defs/TerrainDef[defName="PavedTile" or defName="Concrete" or @Name="TileStoneBase"]/tags 26 | 27 |
  • CleanPath
  • 28 | 29 | 30 |
    31 |
    32 |
    -------------------------------------------------------------------------------- /About/About.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | [Temp beta] Clean Pathfinding 2 4 | Owlchemist 5 | Owlchemist.CleanPathfinding.tmp 6 | 7 | 8 |
  • 1.4
  • 9 |
    10 | When pathfinding costs are calculated, any terrain tiles that generate filth are taken into account and given some degree of avoidance. Also, roads can be given a bonus attraction, and the overall pathfinding range can be adjusted. Other features such as light attraction and extra range can also be enabled. 11 | 12 |
  • 13 | brrainz.harmony 14 | Harmony 15 | steam://url/CommunityFilePage/2009463077 16 |
  • 17 |
    18 | 19 |
  • Owlchemist.CleanPathfinding
  • 20 |
    21 |
    -------------------------------------------------------------------------------- /About/PublishedFileId.txt: -------------------------------------------------------------------------------- 1 | 2887176193 -------------------------------------------------------------------------------- /About/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Owlchemist/clean-pathfinding/1fdbd93ae097edc2388a53d7037a463fc04aa7ba/About/preview.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 jptrrs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Languages/English/Keyed/owlchemist.cleanpathfinding.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Tuning 4 | Doorpathing 5 | Rules 6 | Misc 7 | 8 | Dirty terrain avoidance (Mod default: {0}, Min: {1}, Max: {2}): 9 | Extra perceived pathcost to apply to filth-generating terrain tiles. 10 | 11 | Clean stone terrain avoidance (Mod default: {0}, Min: {1}, Max: {2}): 12 | Since stone terrain does not generate filth, the above setting may cause pawns be overly attracted to walking upon it, depending on the map layout. You can tinker with this setting if you're trying to avoid this behavior. 13 | 14 | Road attraction (Mod default: {0}, Min: {1}, Max: {2}): 15 | A reduction of perceived pathcost for any tiles with the "Road" tag. 16 | 17 | Enable region pathing 18 | When enabled, 19 | Region pathing threshold (Mod default: {0}, Min: {1}, Max: {2}): 20 | if a pawn's pathfinder calculates a number of cells beyond this threshold, they switch to "regional pathfinding mode", which causes their calculations to chunk out into 12x12 blocks. This is more expensive, but can broaden their search to allow them to discover things such as roads to aid calculating the best path. This option may be ideal for players whom are dillgent about building out pathing infrastructure.\n\nIf you decrease this threshold, consider lowering heuristic tuning, or turning it off entirely 21 | 22 | Enable tuning 23 | Turning this off returns pathfinding to normal vanilla behavior. 24 | 25 | Darkness penalty (Mod default: {0}, Min: {1}, Max: {2}): 26 | Sets the extra cost to add to dark tiles. 27 | 28 | Pathfinding should factor light 29 | Pawns will factor and bias lit paths. This extra calculation has a small performance impact. 30 | Heuristic tuning (Mod default: {0}, Min: {1}, Max: {2}): {3} 31 | A higher value will cause the calculations to "bloom out" like a cone based on how far away the destination is. When maxed out, a brute-force degree of cells are analyzed for maximum precision at the cost of performance. 32 | 33 | Pawns ignore rules if carrying another pawn 34 | Pawns will ignore rules if they're carrying another pawn. 35 | 36 | Pawns ignore rules if bleeding 37 | Pawns will ignore rules if they're bleeding more than 10%/d. 38 | 39 | Enable exitfinding 40 | Only use exit tuning if you play on an unusual map and notice pawns getting stuck trying to find the map exit. Enabling/disabling this requires a restart. 41 | Exit range (Mod default: {0}, Min: {1}, Max: {2}): 42 | Pawns look around them for an exit, each time looking +4 tiles farther and farther out. In vanilla they give up after 30 iterations. This slider increases the number of iterations to make their exit finding more aggressive. 43 | 44 | Enable wander tuning 45 | Allows adjustments to pawns and animals that wander around aimlessly. 46 | Wander delay (Mod default: {0}, Min: {1}, Max: {2}) {3} 47 | Adds a number of extra seconds before a pawn (including penned animals and wildlife) wanders to a new nearby spot. Increase this for a minor aid to performance at the cost of a less lively world. 48 | 49 | Enable doorpathing 50 | Disabling this will remove the pathing priority gizmos given to doors. 51 | (Doorpathing cannot be disabled while a game is currently loaded.) 52 | 53 | Side door pathcost (Mod default: {0}, Min: {1}, Max: {2}): 54 | Pawns will consider this extra pathing cost when going through doors marked as side doors. For consideration, by default a normal door is considered +45 cost. 55 | Emergency door pathcost (Mod default: {0}, Min: {1}, Max: {2}): 56 | Pawns will consider this extra pathing cost when going through doors marked as emergency doors. For consideration, by default a normal door is considered +45 cost. 57 | 58 | Optimize collider check 59 | Replace the code that checks if pawns should collide with other pawns with an optimized version. No logic change. This optimization is automatically disabled if it detects another mod needs to modify this code and it would conflict. Changing this requires a restart. 60 | 61 | Change the door type to alter routing behavior.\n\nColonist pawns resist going through side doors, and even more so emergency doors.\n\nExclusive doors are meant for rooms with no other way in, providing a routing hint. 62 | Normal 63 | Side 64 | Emergency 65 | Exclusive 66 | 67 | Door prioritization cannot be set when a room only has one door. 68 | 69 | Reload required 70 | Reload your save to enable door pathing features. You do not need to restart the game. 71 | 72 | [Clean Pathfinding] Avoid area registered. 73 | [Clean Pathfinding] Avoid area removed. 74 | 75 | seconds 76 | OFF]]> 77 | MAX]]> 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Clean Pathfinding 2 | 3 | https://steamcommunity.com/sharedfiles/filedetails/?id=2603765747&tscn=1632192869 4 | -------------------------------------------------------------------------------- /Source/CleanPathfinding.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Clean Pathfinding 5 | 2.0.41 6 | 1.4 7 | $([System.DateTime]::Now.ToString('yyyy')) 8 | Owlchemist 9 | net48 10 | preview 11 | false 12 | 13 | 14 | ..\..\..\Mods\$(Product)\$(Version)\Assemblies 15 | TRACE;NDEBUG 16 | 4 17 | false 18 | None 19 | 20 | 21 | ..\..\..\Mods\$(Product)\$(Version)\Assemblies 22 | TRACE;DEBUG;NETFRAMEWORK;NET48; 23 | 24 | 25 | 26 | runtime; build; native; contentfiles; analyzers; buildtransitive 27 | all 28 | 29 | 30 | 31 | runtime; build; native; contentfiles; analyzers; buildtransitive 32 | all 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Source/CleanPathfindingUtility.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using RimWorld; 3 | using Verse; 4 | using Verse.AI; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Reflection.Emit; 8 | using System.Linq; 9 | using static CleanPathfinding.ModSettings_CleanPathfinding; 10 | 11 | namespace CleanPathfinding 12 | { 13 | #region Harmony 14 | [HarmonyPatch(typeof(PathFinder), nameof(PathFinder.FindPath), new Type[] { 15 | typeof(IntVec3), 16 | typeof(LocalTargetInfo), 17 | typeof(TraverseParms), 18 | typeof(PathEndMode), 19 | typeof(PathFinderCostTuning) })] 20 | static class Patch_PathFinder 21 | { 22 | static IEnumerable Transpiler(IEnumerable instructions) 23 | { 24 | int offset = -1, objectsFound = 0; 25 | bool ran = false, searchForObjects = false, thresholdReplaced = false; 26 | 27 | var method_regionModeThreshold = AccessTools.Field(typeof(ModSettings_CleanPathfinding), nameof(ModSettings_CleanPathfinding.regionModeThreshold)); 28 | var field_extraDraftedPerceivedPathCost = AccessTools.Field(typeof(TerrainDef), nameof(TerrainDef.extraDraftedPerceivedPathCost)); 29 | var field_extraNonDraftedPerceivedPathCost = AccessTools.Field(typeof(TerrainDef), nameof(TerrainDef.extraNonDraftedPerceivedPathCost)); 30 | 31 | object[] objects = new object[3]; 32 | foreach (var code in instructions) 33 | { 34 | //Replace region-mode pathing threshold 35 | if (!thresholdReplaced && code.opcode == OpCodes.Ldc_I4 && code.OperandIs(100000)) 36 | { 37 | code.opcode = OpCodes.Ldsfld; 38 | code.operand = method_regionModeThreshold; 39 | } 40 | yield return code; 41 | if (!searchForObjects && code.opcode == OpCodes.Ldfld && code.OperandIs(field_extraDraftedPerceivedPathCost)) 42 | { 43 | searchForObjects = true; 44 | continue; 45 | } 46 | 47 | //Record which local variables extraNonDraftedPerceivedPathCost is using, instead of blindly pulling from the local array ourselves which may jumble 48 | if (searchForObjects && objectsFound < 3 && code.opcode == OpCodes.Ldloc_S) 49 | { 50 | objects[objectsFound++] = code.operand; 51 | //As of 12/5, object 0 should be 48, object 1 should be 12, and object 2 should be 45 52 | } 53 | 54 | if (offset == -1 && code.opcode == OpCodes.Ldfld && code.OperandIs(field_extraNonDraftedPerceivedPathCost)) 55 | { 56 | offset = 0; 57 | continue; 58 | } 59 | if (offset > -1 && ++offset == 2) 60 | { 61 | yield return new CodeInstruction(OpCodes.Ldloc_0); 62 | yield return new CodeInstruction(OpCodes.Ldloc_S, objects[1]); //topGrid 63 | yield return new CodeInstruction(OpCodes.Ldloc_S, objects[2]); //TerrainDef within the grid 64 | yield return new CodeInstruction(OpCodes.Ldelem_Ref); 65 | yield return new CodeInstruction(OpCodes.Ldloc_S, objects[0]); //Pathcost total 66 | yield return new CodeInstruction(OpCodes.Ldarg_0); 67 | yield return new CodeInstruction(OpCodes.Ldfld, AccessTools.Field(typeof(PathFinder), nameof(PathFinder.map))); 68 | yield return new CodeInstruction(OpCodes.Ldloc_S, objects[2]); //cell location 69 | yield return new CodeInstruction(OpCodes.Call, typeof(CleanPathfindingUtility).GetMethod(nameof(CleanPathfindingUtility.AdjustCosts))); 70 | yield return new CodeInstruction(OpCodes.Stloc_S, objects[0]); 71 | 72 | ran = true; 73 | } 74 | } 75 | 76 | if (!ran) Log.Warning("[Clean Pathfinding] Transpiler could not find target. There may be a mod conflict, or RimWorld updated?"); 77 | } 78 | } 79 | 80 | #endregion 81 | 82 | public static class CleanPathfindingUtility 83 | { 84 | public static Dictionary terrainCache = new Dictionary(), terrainCacheOriginalValues = new Dictionary(); 85 | public static SimpleCurve Custom_DistanceCurve; 86 | public static MapComponent_DoorPathing cachedComp; //the last map component used by the door cost adjustments. It will be reused if the map hash is the same 87 | static bool lastFactionHostileCache, lastPawnReversionCache; 88 | public static int cachedMapID = -1, lastFactionID = -1; 89 | static int loggedOnTick, calls, lastTerrainCacheCost, lastPawnID; 90 | static ushort lastTerrainDefID; 91 | 92 | public static void UpdatePathCosts() 93 | { 94 | try 95 | { 96 | //Reset the cache 97 | List report = new List(); 98 | foreach (ushort key in terrainCache.Keys.ToList()) 99 | { 100 | if (terrainCacheOriginalValues.TryGetValue(key, out int originalValue)) terrainCache[key] = originalValue; 101 | else terrainCache[key] = 0; 102 | } 103 | 104 | var list = DefDatabase.AllDefsListForReading; 105 | var length = list.Count; 106 | for (int i = 0; i < length; i++) 107 | { 108 | TerrainDef terrainDef = list[i]; 109 | 110 | ushort index = terrainDef.shortHash; 111 | //Reset to original value 112 | if (terrainCache.ContainsKey(index)) terrainDef.extraNonDraftedPerceivedPathCost = terrainCacheOriginalValues[index]; 113 | else continue; 114 | 115 | //Attraction to roads 116 | if (!Setup.safetyNeeded && roadBias > 0 && (terrainDef.tags?.Contains("CleanPath") ?? false)) 117 | { 118 | terrainDef.extraNonDraftedPerceivedPathCost -= roadBias; 119 | terrainCache[index] += roadBias; 120 | } 121 | else 122 | { 123 | //Avoid filth 124 | if (bias != 0 && terrainDef.generatedFilth != null) 125 | { 126 | terrainDef.extraNonDraftedPerceivedPathCost += bias; 127 | terrainCache[index] -= bias; 128 | } 129 | 130 | //Clean but natural terrain bias 131 | if (naturalBias > 0 && terrainDef.generatedFilth == null && (terrainDef.defName.Contains("_Rough"))) 132 | { 133 | terrainDef.extraNonDraftedPerceivedPathCost += naturalBias; 134 | terrainCache[index] -= naturalBias; 135 | } 136 | } 137 | 138 | //Debug 139 | if (logging && Prefs.DevMode) 140 | { 141 | report.Add(terrainDef.defName + ": " + terrainDef.extraNonDraftedPerceivedPathCost); 142 | } 143 | } 144 | 145 | //Debug print 146 | if (logging && Prefs.DevMode) 147 | { 148 | report.Sort(); 149 | Log.Message("[Clean Pathfinding] Terrain report:\n" + string.Join("\n - ", report)); 150 | } 151 | 152 | //Reset the extra pathfinding range curve 153 | Custom_DistanceCurve = new SimpleCurve 154 | { 155 | { new CurvePoint(40f + heuristicAdjuster, 1f), true }, 156 | { new CurvePoint(120f + (heuristicAdjuster * 3), 3f), true } 157 | }; 158 | 159 | //If playing, update the pathfinders now 160 | if (Current.ProgramState == ProgramState.Playing) foreach (Map map in Find.Maps) map.pathing.RecalculateAllPerceivedPathCosts(); 161 | } 162 | catch (System.Exception ex) 163 | { 164 | Log.Error("[Clean Pathfinding] Error processing settings, skipping...\n" + ex); 165 | } 166 | } 167 | static public int AdjustCosts(Pawn pawn, TerrainDef def, int cost, Map map, int index) 168 | { 169 | if (pawn == null) goto skipAdjustment; 170 | 171 | //Do not do cost adjustments if... 172 | bool revert = lastPawnReversionCache; 173 | 174 | //Is not this the last pawn we checked? 175 | if (pawn.thingIDNumber != lastPawnID) 176 | { 177 | lastPawnID = pawn.thingIDNumber; 178 | var faction = pawn.Faction; 179 | 180 | revert = ((faction == null || pawn.def.race.intelligence == Intelligence.Animal) || // Animal or other entity? 181 | (!faction.def.isPlayer && IsHostileFast(faction)) || //They are hostile 182 | (factorCarryingPawn && pawn.carryTracker != null && pawn.carryTracker.CarriedThing != null && pawn.carryTracker.CarriedThing.def.category == ThingCategory.Pawn) || //They are carrying someone 183 | (factorBleeding && pawn.health.hediffSet.cachedBleedRate > 0.1f)); //They are bleeding 184 | 185 | lastPawnReversionCache = revert; 186 | } 187 | 188 | if (!revert) 189 | { 190 | //Factor in door pathing 191 | if (doorPathing) 192 | { 193 | int doorCost = 0; 194 | if (cachedMapID == map.uniqueID) doorCost = cachedComp.doorCostGrid[index]; 195 | else if (DoorPathingUtility.compCache.TryGetValue(map.uniqueID, out cachedComp)) 196 | { 197 | cachedMapID = map.uniqueID; 198 | doorCost = cachedComp.doorCostGrid[index]; 199 | } 200 | if (doorCost < 0) goto skipAdjustment; 201 | cost += doorCost; 202 | } 203 | //...And then light pathing 204 | if (factorLight && GameGlowAtFast(map, index) < 0.3f) cost += darknessPenalty; 205 | } 206 | //Revert if needed, check if cache is available 207 | else if (def.shortHash == lastTerrainDefID) cost += lastTerrainCacheCost; 208 | //If not, use and set... 209 | else 210 | { 211 | lastTerrainDefID = def.shortHash; 212 | if (terrainCache.TryGetValue(def.shortHash, out lastTerrainCacheCost)) 213 | { 214 | //Double-revert back to 0 if this is a clean path (value returned as greater than 0) and this is our faction, meaning it's probably a hauling animal 215 | if (lastTerrainCacheCost > 0 && pawn.factionInt != null && pawn.factionInt.def.isPlayer) lastTerrainCacheCost = 0; 216 | cost += lastTerrainCacheCost; 217 | } 218 | else lastTerrainCacheCost = 0; //Record any terrain defs that are not modified to avoid looking them up again 219 | } 220 | 221 | //Logging and debugging stuff 222 | if (logging && Verse.Prefs.DevMode) 223 | { 224 | ++calls; 225 | if (Current.gameInt.tickManager.ticksGameInt != loggedOnTick) 226 | { 227 | loggedOnTick = Current.gameInt.tickManager.ticksGameInt; 228 | if (calls != 0) Log.Message("[Clean Pathfinding] Calls last pathfinding: " + calls); 229 | calls = 0; 230 | } 231 | if (cost < 0) cost = 0; 232 | var cell = map.cellIndices.IndexToCell(index); 233 | if (!map.debugDrawer.debugCells.Any(x => x.c == cell)) map.debugDrawer.FlashCell(cell, cost, cost.ToString()); 234 | } 235 | skipAdjustment: 236 | if (cost < 0) return 0; 237 | return cost; 238 | 239 | #region embedded methods 240 | bool IsHostileFast(Faction faction) 241 | { 242 | //Check and set cache 243 | if (Current.gameInt.tickManager.ticksGameInt % 600 == 0) lastFactionID = -1; // Reset every 10th second 244 | if (faction.loadID == lastFactionID) return lastFactionHostileCache; 245 | else lastFactionID = faction.loadID; 246 | 247 | //Look through their relationships table and look up the player faction, then record to cache 248 | var relations = faction.relations; 249 | for (int i = relations.Count; i-- > 0;) 250 | { 251 | var tmp = relations[i]; 252 | if (tmp.other == Current.gameInt.worldInt.factionManager.ofPlayer) 253 | { 254 | lastFactionHostileCache = tmp.kind == FactionRelationKind.Hostile; 255 | break; 256 | } 257 | } 258 | return lastFactionHostileCache; 259 | } 260 | 261 | float GameGlowAtFast(Map map, int index) 262 | { 263 | float daylight = 0f; 264 | //If there's no roof, they're outside, so factory the daylight 265 | if (map.roofGrid.roofGrid[index] == null) 266 | { 267 | daylight = map.skyManager.curSkyGlowInt; 268 | if (daylight == 1f) return 1f; 269 | } 270 | ColorInt color = map.glowGrid.glowGrid[index]; 271 | if (color.a == 1) return 1; 272 | 273 | return (float)(color.r + color.g + color.b) * 0.0047058823529412f; //n / 3f / 255f * 3.6f pre-computed, since I guess the assembler doesn't optimize this 274 | } 275 | 276 | #endregion 277 | } 278 | } 279 | } -------------------------------------------------------------------------------- /Source/DoorPathingUtility.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Verse; 3 | using Verse.AI; 4 | using System; 5 | using System.Collections.Generic; 6 | using RimWorld; 7 | using RimWorld.Planet; 8 | using Verse.Sound; 9 | using System.Reflection.Emit; 10 | using UnityEngine; 11 | using static CleanPathfinding.DoorPathingUtility; 12 | using static CleanPathfinding.ModSettings_CleanPathfinding; 13 | 14 | namespace CleanPathfinding 15 | { 16 | //Add gizmos to the doors 17 | [HarmonyPatch(typeof(Building_Door), nameof(Building_Door.GetGizmos))] 18 | static class Patch_Building_Door_GetGizmos 19 | { 20 | static bool Prepare() 21 | { 22 | if (!Mod_CleanPathfinding.patchLedger.ContainsKey(nameof(Patch_Building_Door_GetGizmos))) Mod_CleanPathfinding.patchLedger.Add(nameof(Patch_Building_Door_GetGizmos), doorPathing); 23 | return doorPathing; 24 | } 25 | public static IEnumerable Postfix(IEnumerable values, Building_Door __instance) 26 | { 27 | return GetGizmos(values, __instance); 28 | } 29 | } 30 | 31 | //If a door is despawned, remove the offset 32 | [HarmonyPatch(typeof(Building_Door), nameof(Building_Door.DeSpawn))] 33 | static class Patch_Building_DoorDeSpawn 34 | { 35 | static bool Prepare() { return doorPathing; } 36 | public static void Prefix(Building_Door __instance) 37 | { 38 | if (compCache.TryGetValue(__instance.Map.uniqueID, out MapComponent_DoorPathing comp)) comp.doorCostGrid[__instance.Map.cellIndices.CellToIndex(__instance.Position)] = 0; 39 | } 40 | } 41 | 42 | //If a door is despawned, remove the offset 43 | [HarmonyPatch(typeof(Room), nameof(Room.Notify_RoomShapeChanged))] 44 | static class Patch_Notify_RoomShapeChanged 45 | { 46 | static bool Prepare() { return doorPathing; } 47 | public static void Postfix(Room __instance) 48 | { 49 | if (Current.ProgramState != ProgramState.MapInitializing && compCache.TryGetValue(__instance.Map?.uniqueID ?? -1, out MapComponent_DoorPathing mapComp)) 50 | { 51 | foreach (var cell in __instance.Cells) 52 | { 53 | mapComp.ValidateRoomDoors(cell, true); 54 | break; 55 | } 56 | } 57 | } 58 | } 59 | 60 | //Update avoid area 61 | [HarmonyPatch(typeof(Area), nameof(Area.Set))] 62 | static class Patch_Area_Set 63 | { 64 | static bool Prepare() { return doorPathing; } 65 | public static void Postfix(Area __instance, IntVec3 c, bool val) 66 | { 67 | if (__instance.Label == "Avoid" && compCache.TryGetValue(__instance.Map.uniqueID, out MapComponent_DoorPathing mapComp)) 68 | { 69 | mapComp.UpdateAvoidArea(c, val); 70 | } 71 | } 72 | } 73 | 74 | [HarmonyPatch(typeof(Area), nameof(Area.Delete))] 75 | static class Patch_Area_Delete 76 | { 77 | static bool Prepare() { return doorPathing; } 78 | public static void Prefix(Area __instance) 79 | { 80 | if (__instance.Label == "Avoid" && compCache.TryGetValue(__instance.Map.uniqueID, out MapComponent_DoorPathing mapComp)) mapComp.DeregisterAvoidArea(__instance); 81 | } 82 | } 83 | 84 | [HarmonyPatch(typeof(Area_Allowed), nameof(Area_Allowed.SetLabel))] 85 | static class Patch_Area_Allowed_SetLabel 86 | { 87 | static bool Prepare() { return doorPathing; } 88 | 89 | static string originalName; 90 | public static void Prefix(Area_Allowed __instance) 91 | { 92 | originalName = __instance.Label; 93 | } 94 | public static void Postfix(Area_Allowed __instance) 95 | { 96 | if (originalName == "Avoid") 97 | { 98 | if (__instance.Label != "Avoid" && compCache.TryGetValue(__instance.Map.uniqueID, out MapComponent_DoorPathing mapComp)) mapComp.DeregisterAvoidArea(__instance); 99 | } 100 | else if (__instance.Label == "Avoid" && compCache.TryGetValue(__instance.Map.uniqueID, out MapComponent_DoorPathing mapComp)) mapComp.RegisterAvoidArea(__instance); 101 | } 102 | } 103 | 104 | [HarmonyPatch(typeof(World), nameof(World.FinalizeInit))] 105 | static class Patch_FinalizeInit 106 | { 107 | static void Postfix() 108 | { 109 | DoorPathingUtility.compCache.Clear(); 110 | CleanPathfindingUtility.cachedMapID = -1; 111 | CleanPathfindingUtility.cachedComp = null; 112 | CleanPathfindingUtility.lastFactionID = -1; 113 | } 114 | } 115 | 116 | [HarmonyPatch(typeof(PathFinder), nameof(PathFinder.GetBuildingCost))] 117 | public static class Patch_GetBuildingCost 118 | { 119 | static bool Prepare() 120 | { 121 | return doorPathing; 122 | } 123 | static int Postfix(int __result, Building b, Pawn pawn) 124 | { 125 | return ( 126 | (pawn?.factionInt?.def.isPlayer ?? false) && //Is pawn the player's? 127 | ((!usingDoorsExpanded && b is Building_Door) || usingDoorsExpanded) && //Is a door? 128 | compCache.TryGetValue(b.Map.uniqueID, out MapComponent_DoorPathing mapComp) && //Map component found? 129 | mapComp.doorRegistry.TryGetValue(b.thingIDNumber, out DoorType doorType) && //Door registered? 130 | doorType == DoorType.Exclusive //Door is exclusive? 131 | ) ? 0 : __result; //Cancel the cost if exclusive, otherwise pass thru 132 | } 133 | } 134 | 135 | [HarmonyPatch(typeof(SelectionDrawer), nameof(SelectionDrawer.DrawSelectionBracketFor))] 136 | static class Patch_DrawSelectionBracketFor 137 | { 138 | static bool Prepare() 139 | { 140 | return doorPathing; 141 | } 142 | static IEnumerable Transpiler(IEnumerable instructions) 143 | { 144 | int offset = 0; 145 | bool ran = false; 146 | foreach (var code in instructions) 147 | { 148 | yield return code; 149 | if (offset != 4 && code.opcode == OpCodes.Stloc_3) 150 | { 151 | ++offset; 152 | continue; 153 | } 154 | if (!ran && offset == 3) 155 | { 156 | yield return new CodeInstruction(OpCodes.Ldloc_1); 157 | yield return new CodeInstruction(OpCodes.Call, typeof(DoorPathingUtility).GetMethod(nameof(DoorPathingUtility.DrawDoorField))); 158 | 159 | ran = true; 160 | } 161 | } 162 | if (!ran) Log.Warning("[Clean Pathfinding] Transpiler could not find target for door edge drawer. There may be a mod conflict, or RimWorld updated?"); 163 | } 164 | } 165 | 166 | static public class DoorPathingUtility 167 | { 168 | public enum DoorType { Normal = 1, Side, Emergency, Exclusive} 169 | public static Dictionary compCache = new Dictionary(); 170 | public static bool usingDoorsExpanded; 171 | 172 | public static IEnumerable GetGizmos(IEnumerable values, Building thing) 173 | { 174 | foreach (var value in values) yield return value; 175 | if (thing.def.passability != Traversability.Impassable && Find.Selector.NumSelected == 1) 176 | { 177 | if (compCache.TryGetValue(thing.Map.uniqueID, out MapComponent_DoorPathing doorPathingComp)) 178 | { 179 | //Add a new registry entry if needed 180 | if (!doorPathingComp.doorRegistry.TryGetValue(thing.thingIDNumber, out DoorType doorType)) 181 | { 182 | doorPathingComp.doorRegistry.Add(thing.thingIDNumber, DoorType.Normal); 183 | doorPathingComp.doorCostGrid[thing.Map.cellIndices.CellToIndex(thing.Position)] = GetDoorCost(DoorType.Normal, thing); 184 | doorType = DoorType.Normal; 185 | } 186 | 187 | yield return new Command_Action() 188 | { 189 | icon = ResourceBank.iconPriority, 190 | defaultDesc = "CleanPathfinding.Icon.DoorType.Desc".Translate(), 191 | defaultLabel = ("CleanPathfinding.Icon." + doorType.ToString()).Translate(), 192 | action = () => doorPathingComp.SwitchDoorType(thing, doorType) 193 | }; 194 | } 195 | } 196 | } 197 | 198 | //Converts the doorType enum to the mod setting config 199 | public static int GetDoorCost(DoorType doorType, Building door) 200 | { 201 | switch (doorType) 202 | { 203 | case DoorType.Normal: return 0; 204 | case DoorType.Side: return doorPathingSide; 205 | case DoorType.Exclusive: return -1; 206 | default: return doorPathingEmergency; 207 | } 208 | } 209 | 210 | public static void UpdateAllDoorsOnAllMaps() 211 | { 212 | Dialog_MessageBox reloadGameMessage = new Dialog_MessageBox("CleanPathfinding.ReloadRequired".Translate(), null, null, null, null, "CleanPathfinding.ReloadHeader".Translate(), true, null, null, WindowLayer.Dialog); 213 | 214 | if (Current.ProgramState == ProgramState.Playing) 215 | { 216 | foreach (var map in Find.Maps) 217 | { 218 | if (compCache.TryGetValue(map.uniqueID, out MapComponent_DoorPathing mapComp)) mapComp.RecalculateAllDoors(); 219 | else 220 | { 221 | if (!Find.WindowStack.IsOpen(reloadGameMessage)) Find.WindowStack.Add(reloadGameMessage); 222 | } 223 | } 224 | } 225 | } 226 | 227 | public static void DrawDoorField(Thing thing) 228 | { 229 | Map map = thing.Map; 230 | if ( map != null && ((!usingDoorsExpanded && thing is Building_Door) || usingDoorsExpanded) && //Filter on doors normally, or check all buildings if using Doors Expanded 231 | compCache.TryGetValue(map.uniqueID, out MapComponent_DoorPathing mapComp) && 232 | mapComp.doorRegistry.TryGetValue(thing.thingIDNumber, out DoorType doorType)) 233 | { 234 | Color color; 235 | switch (doorType) 236 | { 237 | case DoorType.Exclusive: { 238 | color = ResourceBank.blue; break; 239 | } 240 | case DoorType.Side: { 241 | color = ResourceBank.yellow; break; 242 | } 243 | case DoorType.Emergency: { 244 | color = ResourceBank.red; break; 245 | } 246 | default: { 247 | color = ResourceBank.white; break; 248 | } 249 | } 250 | 251 | GenDraw.DrawFieldEdges(new List(thing.OccupiedRect()), color); 252 | } 253 | } 254 | } 255 | 256 | public class MapComponent_DoorPathing : MapComponent 257 | { 258 | public Dictionary doorRegistry = new Dictionary(); //Int is the door thingID 259 | public int[] doorCostGrid; 260 | public bool usingAvoidArea = false; 261 | public Area avoidArea; 262 | 263 | public MapComponent_DoorPathing(Map map) : base(map) 264 | { 265 | if (!doorPathing) map.components.Remove(this); 266 | } 267 | 268 | public override void ExposeData() 269 | { 270 | if (doorPathing) Scribe_Collections.Look(ref this.doorRegistry, "doorRegistry"); 271 | } 272 | 273 | public override void FinalizeInit() 274 | { 275 | if (!compCache.ContainsKey(map.uniqueID)) compCache.AddDistinct(map.uniqueID, this); 276 | else Log.Warning("[Clean Pathfinding] Tried to register a doorpathing component to a map that already has one. Did the cache not flush?"); 277 | 278 | //Validate registry. May potentially be null if doorpathing feature was disabled initially 279 | if (doorRegistry == null) doorRegistry = new Dictionary(); 280 | 281 | //Setup 282 | doorCostGrid = new int[map.info.NumCells]; 283 | RecalculateAllDoors(); 284 | CheckForAvoidArea(); 285 | } 286 | 287 | //Is there an avoid area? 288 | public void CheckForAvoidArea() 289 | { 290 | foreach (var area in map.areaManager.AllAreas) 291 | { 292 | if (area.Label == "Avoid") 293 | { 294 | if (logging) Log.Message("[Clean Pathfinding] Registering avoid zone."); 295 | RegisterAvoidArea(area, true); 296 | break; 297 | } 298 | } 299 | } 300 | 301 | public void RegisterAvoidArea(Area area, bool quiet = false) 302 | { 303 | usingAvoidArea = true; 304 | avoidArea = area; 305 | if (!quiet) Messages.Message("CleanPathfinding.NewAvoidArea".Translate(), MessageTypeDefOf.PositiveEvent, false); 306 | for (int i = 0; i < avoidArea.innerGrid.arr.Length; i++) 307 | { 308 | if (avoidArea.innerGrid.arr[i] && doorCostGrid[i] == 0) doorCostGrid[i] = 45; 309 | } 310 | } 311 | 312 | public void DeregisterAvoidArea(Area area) 313 | { 314 | usingAvoidArea = false; 315 | avoidArea = null; 316 | Messages.Message("CleanPathfinding.DeletedAvoidArea".Translate(), MessageTypeDefOf.PositiveEvent, false); 317 | 318 | //Reset and rebuild the cost grids 319 | Array.Clear(doorCostGrid, 0, map.info.NumCells); 320 | RecalculateAllDoors(); 321 | } 322 | 323 | public void UpdateAvoidArea(IntVec3 c, bool val) 324 | { 325 | var index = map.cellIndices.CellToIndex(c); 326 | //Turning on avoid for this cell? 327 | if (val) 328 | { 329 | //Only apply to normal cells without cost adjustment 330 | if (doorCostGrid[index] == 0) doorCostGrid[index] = 45; 331 | } 332 | //Turning off avoid? 333 | else 334 | { 335 | var edifice = c.GetEdifice(map); 336 | if (edifice != null && doorRegistry.TryGetValue(edifice.thingIDNumber, out DoorType doorValue)) 337 | { 338 | doorCostGrid[index] = DoorPathingUtility.GetDoorCost(doorValue, edifice); 339 | } 340 | else doorCostGrid[index] = 0; 341 | } 342 | } 343 | 344 | //All the doors' cost is written to cache. Called on map init and changing mod settings 345 | public void RecalculateAllDoors() 346 | { 347 | try 348 | { 349 | List list = map.listerBuildings.allBuildingsColonist; 350 | var length = list.Count; 351 | for (int i = 0; i < length; i++) 352 | { 353 | Building building = list[i]; 354 | if (doorRegistry.ContainsKey(building.thingIDNumber)) WriteToDoorGrid(building, doorRegistry[building.thingIDNumber]); 355 | } 356 | } 357 | catch (System.Exception ex) 358 | { 359 | Log.Error("[Clean Pathfinding] Error processing door recalculation, skipping...\n" + ex); 360 | } 361 | } 362 | 363 | //Handles the gizmo switching door types 364 | public void SwitchDoorType(Building thing, DoorType doorType) 365 | { 366 | if (ValidateRoomDoors(thing.Position) == 1) 367 | { 368 | Messages.Message("CleanPathfinding.InvalidDoor".Translate(), MessageTypeDefOf.RejectInput, false); 369 | return; 370 | } 371 | SoundDefOf.Click.PlayOneShotOnCamera(null); 372 | 373 | int length = Enum.GetValues(typeof(DoorType)).Length; 374 | doorType = doorType != DoorType.Exclusive ? ++doorType : DoorType.Normal; 375 | doorRegistry[thing.thingIDNumber] = doorType; 376 | WriteToDoorGrid(thing, doorType); 377 | } 378 | 379 | //Going through all cells a door occupies and writing the cost 380 | void WriteToDoorGrid(Building thing, DoorType doorType) 381 | { 382 | foreach (var c in thing.OccupiedRect().Cells) 383 | { 384 | if (c.InBounds(map)) doorCostGrid[map.cellIndices.CellToIndex(c)] = GetDoorCost(doorType, thing); 385 | } 386 | } 387 | 388 | //A door is invalid for priority if it's the only door leading into a room. This method checks this. 389 | public int ValidateRoomDoors(IntVec3 c, bool roomUpdate = false) 390 | { 391 | try 392 | { 393 | //First, find the region the door is on 394 | if (roomUpdate) 395 | { 396 | var tmp = c.GetRegion(map); 397 | if (tmp?.door != null) c = tmp.door.positionInt; 398 | else return 0; 399 | } 400 | 401 | if (logging && Prefs.DevMode) map.debugDrawer.FlashCell(c, text: "REF"); 402 | Region doorRegion = c.GetRegion(map); 403 | if (doorRegion == null) return 0; 404 | 405 | //Prepare list of doors to update 406 | List doorsInRoom = new List(); 407 | 408 | //Now iterate through this region's links to look at adjacent rooms 409 | foreach (RegionLink regionLink in doorRegion.links) 410 | { 411 | //Fetch the room adjacent to the door 412 | Room room = regionLink.GetOtherRegion(doorRegion)?.Room; 413 | 414 | //Is it a room? This filters out the "wall regions" left and right of the door 415 | if (room != null && !room.TouchesMapEdge) 416 | { 417 | if (logging && Verse.Prefs.DevMode) map.debugDrawer.FlashCell(room.FirstRegion.AnyCell, text: "\nROOM"); 418 | //Go through all the regions that make up the adjaent room 419 | foreach (var adjacentRegion in room.Regions) 420 | { 421 | //Try to look around the other walls of the room to find other doors 422 | foreach (RegionLink roomLink in adjacentRegion.links) 423 | { 424 | //Is this a door? 425 | Region otherDoorRegion = roomLink.GetOtherRegion(adjacentRegion); 426 | if (otherDoorRegion.type == RegionType.Portal && otherDoorRegion.IsDoorway && otherDoorRegion.door.def.passability != Traversability.Impassable) 427 | { 428 | if (logging && Prefs.DevMode) map.debugDrawer.FlashCell(otherDoorRegion.door.positionInt, text: "\n\nDOOR"); 429 | if (!doorsInRoom.Contains(otherDoorRegion.door)) doorsInRoom.Add(otherDoorRegion.door); 430 | } 431 | } 432 | } 433 | } 434 | } 435 | //Sanity check doors 436 | if (logging && Prefs.DevMode) Log.Message("[Clean Pathfinding] Doors in new room layout: " + doorsInRoom.Count); 437 | if (doorsInRoom.Count > 0) 438 | { 439 | foreach (Building item in doorsInRoom) 440 | { 441 | //Register door if missing 442 | if (!doorRegistry.ContainsKey(item.thingIDNumber)) doorRegistry.Add(item.thingIDNumber, DoorType.Normal); 443 | 444 | if (doorsInRoom.Count == 1) 445 | { 446 | doorRegistry[item.thingIDNumber] = DoorType.Exclusive; 447 | WriteToDoorGrid(item, DoorType.Exclusive); 448 | } 449 | } 450 | } 451 | return doorsInRoom.Count; 452 | } 453 | catch (System.Exception ex) 454 | { 455 | Log.Warning("[Clean Pathfinding] Could not validate doors at " + c + " (update: " + roomUpdate + ") for some-odd reason: " + ex); 456 | return 0; 457 | } 458 | } 459 | } 460 | } -------------------------------------------------------------------------------- /Source/Mod_CleanPathfinding.cs: -------------------------------------------------------------------------------- 1 | 2 | using Verse; 3 | using Verse.AI; 4 | using RimWorld; 5 | using HarmonyLib; 6 | using UnityEngine; 7 | using System.Collections.Generic; 8 | using static CleanPathfinding.ModSettings_CleanPathfinding; 9 | 10 | namespace CleanPathfinding 11 | { 12 | [StaticConstructorOnStartup] 13 | public static class Setup 14 | { 15 | static public bool safetyNeeded = true; 16 | static Setup() 17 | { 18 | var report = new List(); 19 | var list = DefDatabase.AllDefsListForReading; 20 | for (int i = list.Count; i-- > 0;) 21 | { 22 | TerrainDef terrainDef = list[i]; 23 | 24 | if (terrainDef.destroyEffectWater != null) 25 | { 26 | if (terrainDef.tags == null) terrainDef.tags = new List(); 27 | terrainDef.tags.Add("CleanPath"); 28 | } 29 | 30 | bool isClean = terrainDef.tags?.Contains("CleanPath") ?? false; 31 | 32 | if 33 | ( 34 | terrainDef.generatedFilth != null || //Generates filth? 35 | isClean || //Is a road? 36 | (terrainDef.generatedFilth == null && terrainDef.defName.Contains("_Rough") ) //Is clean but avoided regardless? 37 | ) 38 | { 39 | CleanPathfindingUtility.terrainCacheOriginalValues.Add(terrainDef.shortHash, terrainDef.extraNonDraftedPerceivedPathCost); 40 | CleanPathfindingUtility.terrainCache.Add(terrainDef.shortHash, terrainDef.extraNonDraftedPerceivedPathCost); 41 | 42 | if (isClean) report.Add(terrainDef.label); 43 | } 44 | } 45 | 46 | SafetyCheck(); 47 | 48 | CleanPathfindingUtility.UpdatePathCosts(); 49 | if (Prefs.DevMode) Log.Message("[Clean Pathfinding] The following terrains apply to road attraction:\n - " + string.Join("\n - ", report)); 50 | } 51 | 52 | static void SafetyCheck() 53 | { 54 | var list = LoadedModManager.RunningModsListForReading; 55 | for (int i = list.Count; i-- > 0;) 56 | { 57 | string name = list[i].packageIdPlayerFacingInt; 58 | if (name == "Haplo.Miscellaneous.Robots") 59 | { 60 | Print(name); 61 | return; 62 | } 63 | if (name == "BiomesTeam.BiomesIslands") 64 | { 65 | Print(name); 66 | return; 67 | } 68 | if (name == "RH2.Faction.VOID") 69 | { 70 | Print(name); 71 | return; 72 | } 73 | } 74 | safetyNeeded = false; 75 | 76 | static void Print(string mod) 77 | { 78 | Log.Warning($"[Clean Pathfinding] the mod {mod} is partially incompatible. The 'road attraction' calculations will be skipped."); 79 | } 80 | } 81 | } 82 | public class Mod_CleanPathfinding : Mod 83 | { 84 | public static Dictionary patchLedger = new Dictionary(); 85 | 86 | public Mod_CleanPathfinding(ModContentPack content) : base(content) 87 | { 88 | base.GetSettings(); 89 | new Harmony(this.Content.PackageIdPlayerFacing).PatchAll(); 90 | } 91 | 92 | public override void DoSettingsWindowContents(Rect inRect) 93 | { 94 | //========Setup tabs========= 95 | GUI.BeginGroup(inRect); 96 | var tabs = new List(); 97 | tabs.Add(new TabRecord("CleanPathfinding.Settings.Header.Tuning".Translate(), delegate { selectedTab = Tab.tuning; }, selectedTab == Tab.tuning)); 98 | tabs.Add(new TabRecord("CleanPathfinding.Settings.Header.Doorpathing".Translate(), delegate { selectedTab = Tab.doorPathing; }, selectedTab == Tab.doorPathing)); 99 | tabs.Add(new TabRecord("CleanPathfinding.Settings.Header.Rules".Translate(), delegate { selectedTab = Tab.rules; }, selectedTab == Tab.rules)); 100 | tabs.Add(new TabRecord("CleanPathfinding.Settings.Header.Misc".Translate(), delegate { selectedTab = Tab.misc; }, selectedTab == Tab.misc)); 101 | 102 | Rect rect = new Rect(0f, 32f, inRect.width, inRect.height - 32f); 103 | Widgets.DrawMenuSection(rect); 104 | TabDrawer.DrawTabs(new Rect(0f, 32f, inRect.width, Text.LineHeight), tabs); 105 | 106 | if (selectedTab == Tab.tuning) DrawTuning(); 107 | else if (selectedTab == Tab.doorPathing) DrawDoorpathing(); 108 | else if (selectedTab == Tab.rules) DrawRules(); 109 | else DrawMisc(); 110 | GUI.EndGroup(); 111 | 112 | void DrawTuning() 113 | { 114 | Listing_Standard options = new Listing_Standard(); 115 | options.Begin(inRect.ContractedBy(15f)); 116 | var before = enableTuning; 117 | options.CheckboxLabeled("CleanPathfinding.Settings.EnableTuning".Translate(), ref enableTuning, "CleanPathfinding.Settings.EnableTuning.Desc".Translate()); 118 | if (before != enableTuning) 119 | { 120 | //TODO: Need some sorta default resetter method 121 | bias = 5; 122 | naturalBias = 0; 123 | roadBias = 9; 124 | heuristicAdjuster = 90; 125 | regionPathing = true; 126 | regionModeThreshold = 1000; 127 | } 128 | options.GapLine(); 129 | options.End(); 130 | options.Begin(new Rect(inRect.x + 15, inRect.y + 55, inRect.width - 30, inRect.height - 30)); 131 | 132 | if (enableTuning) 133 | { 134 | options.Label("CleanPathfinding.Settings.Bias".Translate("5", "0", "12") + bias, -1f, "CleanPathfinding.Settings.Bias.Desc".Translate()); 135 | bias = (int)options.Slider((float)bias, 0f, 12f); 136 | 137 | options.Label("CleanPathfinding.Settings.NaturalBias".Translate("0", "0", "12") + naturalBias, -1f, "CleanPathfinding.Settings.NaturalBias.Desc".Translate()); 138 | naturalBias = (int)options.Slider((float)naturalBias, 0f, 12f); 139 | 140 | options.Label("CleanPathfinding.Settings.RoadBias".Translate("9", "0", "12") + roadBias, -1f, "CleanPathfinding.Settings.RoadBias.Desc".Translate()); 141 | roadBias = (int)options.Slider((float)roadBias, 0f, 12f); 142 | 143 | options.Label("CleanPathfinding.Settings.HeuristicAdjuster".Translate("90", "0", "200", heuristicAdjuster == 200 ? "Max".Translate() : heuristicAdjuster), -1f, "CleanPathfinding.Settings.HeuristicAdjuster.Desc".Translate()); 144 | heuristicAdjuster = (int)options.Slider((float)heuristicAdjuster, 0f, 200f); 145 | 146 | options.CheckboxLabeled("CleanPathfinding.Settings.EnableRegionPathing".Translate(), ref regionPathing, "CleanPathfinding.Settings.EnableRegionPathing.Desc".Translate() + "CleanPathfinding.Settings.RegionModeThreshold.Desc".Translate()); 147 | if (regionPathing) 148 | { 149 | if (regionModeThreshold == 100000) regionModeThreshold = 1000; //Reset to default if just enabled 150 | options.Label("CleanPathfinding.Settings.RegionModeThreshold".Translate("1000", "500", "2000") + regionModeThreshold, -1f, "CleanPathfinding.Settings.RegionModeThreshold.Desc".Translate().CapitalizeFirst()); 151 | regionModeThreshold = (int)options.Slider((float)regionModeThreshold, 500f, 2000f); 152 | } 153 | else regionModeThreshold = 100000; 154 | } 155 | else 156 | { 157 | bias = naturalBias = roadBias = 0; 158 | regionModeThreshold = 100000; 159 | regionPathing = false; 160 | } 161 | options.End(); 162 | } 163 | 164 | void DrawDoorpathing() 165 | { 166 | Listing_Standard options = new Listing_Standard(); 167 | options.Begin(inRect.ContractedBy(15f)); 168 | if (Current.ProgramState != ProgramState.Playing) 169 | { 170 | options.CheckboxLabeled("CleanPathfinding.Settings.DoorPathing".Translate(), ref doorPathing, "CleanPathfinding.Settings.DoorPathing.Desc".Translate()); 171 | } 172 | else 173 | { 174 | options.Label("CleanPathfinding.Settings.DoorPathing.Notice".Translate()); 175 | } 176 | options.GapLine(); 177 | options.End(); 178 | options.Begin(new Rect(inRect.x + 15, inRect.y + 55, inRect.width - 30, inRect.height - 30)); 179 | 180 | if (doorPathing) 181 | { 182 | options.Label("CleanPathfinding.Settings.DoorPathingSide".Translate("250", "50", "500") + doorPathingSide, -1f, "CleanPathfinding.Settings.DoorPathingSide.Desc".Translate()); 183 | doorPathingSide = (int)options.Slider((float)doorPathingSide, 50f, 500f); 184 | options.Label("CleanPathfinding.Settings.DoorPathingEmergency".Translate("500", "500", "1000") + doorPathingEmergency, -1f, "CleanPathfinding.Settings.DoorPathingEmergency.Desc".Translate()); 185 | doorPathingEmergency = (int)options.Slider((float)doorPathingEmergency, 500f, 1000f); 186 | } 187 | 188 | options.End(); 189 | } 190 | 191 | void DrawRules() 192 | { 193 | Listing_Standard options = new Listing_Standard(); 194 | options.Begin(new Rect(inRect.x + 15, inRect.y + 55, inRect.width - 30, inRect.height - 30)); 195 | 196 | options.CheckboxLabeled("CleanPathfinding.Settings.FactorCarryingPawn".Translate(), ref factorCarryingPawn, "CleanPathfinding.Settings.FactorCarryingPawn.Desc".Translate()); 197 | options.CheckboxLabeled("CleanPathfinding.Settings.FactorBleeding".Translate(), ref factorBleeding, "CleanPathfinding.Settings.FactorBleeding.Desc".Translate()); 198 | 199 | options.Label("CleanPathfinding.Settings.DarknessPenalty".Translate("2", "0", "6") + (darknessPenalty == 0f ? "Off".Translate() : darknessPenalty), -1f, "CleanPathfinding.Settings.DarknessPenalty.Desc".Translate()); 200 | darknessPenalty = (int)options.Slider((float)darknessPenalty, 0, 6); 201 | factorLight = darknessPenalty != 0f; 202 | 203 | options.End(); 204 | } 205 | 206 | void DrawMisc() 207 | { 208 | Listing_Standard options = new Listing_Standard(); 209 | options.Begin(new Rect(inRect.x + 15, inRect.y + 55, inRect.width - 30, inRect.height - 30)); 210 | 211 | 212 | options.Label("CleanPathfinding.Settings.ExitRange".Translate("0", "0", "200") + (exitRange == 0f ? "Off".Translate() : exitRange), -1f, "CleanPathfinding.Settings.ExitRange.Desc".Translate()); 213 | exitRange = (int)options.Slider((float)exitRange, 0f, 200f); 214 | exitTuning = exitRange > 0f; 215 | 216 | float wanderDelayRounded = (float)System.Math.Round((wanderDelay / 60f), 1); 217 | options.Label("CleanPathfinding.Settings.WanderDelay".Translate("0", "-2", "10", 218 | wanderTuning ? (wanderDelayRounded.ToString() + "Seconds".Translate()) : "Off".Translate() ), -1f, "CleanPathfinding.Settings.WanderDelay.Desc".Translate()); 219 | wanderDelay = (int)(options.Slider((float)wanderDelay, -118f, 600f)); 220 | wanderTuning = wanderDelay < -20f || wanderDelay > 20f; 221 | 222 | options.CheckboxLabeled("CleanPathfinding.Settings.OptimizeCollider".Translate(), ref optimizeCollider, "CleanPathfinding.Settings.OptimizeCollider.Desc".Translate()); 223 | if (Prefs.DevMode) options.CheckboxLabeled("DevMode: Enable logging", ref logging, null); 224 | 225 | options.End(); 226 | } 227 | } 228 | 229 | public override string SettingsCategory() 230 | { 231 | return "Clean Pathfinding"; 232 | } 233 | 234 | public override void WriteSettings() 235 | { 236 | base.WriteSettings(); 237 | var harmony = new Harmony(this.Content.PackageIdPlayerFacing); 238 | 239 | try 240 | { 241 | CleanPathfindingUtility.UpdatePathCosts(); 242 | DoorPathingUtility.UpdateAllDoorsOnAllMaps(); 243 | } 244 | catch (System.Exception ex) 245 | { 246 | Log.Message("[Clean Pathfinding] Error processing settings: " + ex); 247 | } 248 | 249 | 250 | //Wander tuning patcher/unpatcher 251 | //To-do: wrap all this up into a nice clean universal method 252 | try 253 | { 254 | //Wander tuning 255 | if (!wanderTuning && patchLedger[nameof(Patch_JobGiver_Wander)]) 256 | { 257 | patchLedger[nameof(Patch_JobGiver_Wander)] = false; 258 | harmony.Unpatch(AccessTools.Method(typeof(JobGiver_Wander), nameof(JobGiver_Wander.TryGiveJob) ), HarmonyPatchType.Postfix, this.Content.PackageIdPlayerFacing); 259 | } 260 | else if (wanderTuning && !patchLedger[nameof(Patch_JobGiver_Wander)]) 261 | { 262 | patchLedger[nameof(Patch_JobGiver_Wander)] = true; 263 | harmony.Patch(AccessTools.Method(typeof(JobGiver_Wander), nameof(JobGiver_Wander.TryGiveJob) ), 264 | postfix: new HarmonyMethod(typeof(Patch_JobGiver_Wander), nameof(Patch_JobGiver_Wander.Postfix))); 265 | } 266 | 267 | //Doorpathing 268 | if (!doorPathing && patchLedger[nameof(Patch_Building_Door_GetGizmos)]) 269 | { 270 | patchLedger[nameof(Patch_Building_Door_GetGizmos)] = false; 271 | harmony.Unpatch(AccessTools.Method(typeof(Building_Door), nameof(Building_Door.GetGizmos) ), HarmonyPatchType.Postfix, this.Content.PackageIdPlayerFacing); 272 | harmony.Unpatch(AccessTools.Method(typeof(Building_Door), nameof(Building_Door.DeSpawn) ), HarmonyPatchType.Prefix, this.Content.PackageIdPlayerFacing); 273 | harmony.Unpatch(AccessTools.Method(typeof(Room), nameof(Room.Notify_RoomShapeChanged) ), HarmonyPatchType.Postfix, this.Content.PackageIdPlayerFacing); 274 | } 275 | else if (doorPathing && !patchLedger[nameof(Patch_Building_Door_GetGizmos)]) 276 | { 277 | patchLedger[nameof(Patch_Building_Door_GetGizmos)] = true; 278 | harmony.Patch(AccessTools.Method(typeof(Building_Door), nameof(Building_Door.GetGizmos) ), 279 | postfix: new HarmonyMethod(typeof(Patch_Building_Door_GetGizmos), nameof(Patch_Building_Door_GetGizmos.Postfix))); 280 | harmony.Patch(AccessTools.Method(typeof(Building_Door), nameof(Building_Door.DeSpawn) ), 281 | postfix: new HarmonyMethod(typeof(Patch_Building_DoorDeSpawn), nameof(Patch_Building_DoorDeSpawn.Prefix))); 282 | harmony.Patch(AccessTools.Method(typeof(Room), nameof(Room.Notify_RoomShapeChanged) ), 283 | postfix: new HarmonyMethod(typeof(Patch_Notify_RoomShapeChanged), nameof(Patch_Notify_RoomShapeChanged.Postfix))); 284 | } 285 | } 286 | catch (System.Exception ex) 287 | { 288 | Log.Error("[Clean Pathfinding] Error processing patching or unpatching, skipping...\n" + ex); 289 | } 290 | } 291 | } 292 | 293 | public class ModSettings_CleanPathfinding : ModSettings 294 | { 295 | public override void ExposeData() 296 | { 297 | Scribe_Values.Look(ref bias, "bias", 5); 298 | Scribe_Values.Look(ref naturalBias, "naturalBias", 0); 299 | Scribe_Values.Look(ref roadBias, "roadBias", 9); 300 | Scribe_Values.Look(ref regionModeThreshold, "regionModeThreshold", 100000); 301 | Scribe_Values.Look(ref heuristicAdjuster, "heuristicAdjuster", 90); 302 | Scribe_Values.Look(ref darknessPenalty, "darknessPenalty", 2); 303 | Scribe_Values.Look(ref factorLight, "factorLight", true); 304 | Scribe_Values.Look(ref factorCarryingPawn, "factorCarryingPawn", true); 305 | Scribe_Values.Look(ref factorBleeding, "factorBleeding", true); 306 | Scribe_Values.Look(ref exitRange, "exitRange"); 307 | Scribe_Values.Look(ref doorPathing, "doorPathing", true); 308 | Scribe_Values.Look(ref doorPathingSide, "doorPathingSide", 250); 309 | Scribe_Values.Look(ref doorPathingEmergency, "doorPathingEmergency", 500); 310 | Scribe_Values.Look(ref wanderDelay, "wanderDelay"); 311 | Scribe_Values.Look(ref optimizeCollider, "optimizeCollider", true); 312 | Scribe_Values.Look(ref exitTuning, "exitTuning"); 313 | Scribe_Values.Look(ref wanderTuning, "wanderTuning"); 314 | Scribe_Values.Look(ref regionPathing, "regionPathing", true); 315 | Scribe_Values.Look(ref enableTuning, "enableTuning", true); 316 | base.ExposeData(); 317 | } 318 | 319 | static public int bias = 8, 320 | naturalBias, 321 | roadBias = 9, 322 | exitRange, 323 | doorPathingSide = 250, 324 | doorPathingEmergency = 500, 325 | wanderDelay = 0, 326 | regionModeThreshold = 1000, 327 | heuristicAdjuster = 90, 328 | darknessPenalty = 2; 329 | static public bool factorLight = true, 330 | factorCarryingPawn = true, 331 | factorBleeding = true, 332 | logging, 333 | doorPathing = true, 334 | optimizeCollider = true, 335 | exitTuning, 336 | wanderTuning, 337 | regionPathing = true, 338 | enableTuning = true; 339 | public static Vector2 scrollPos = Vector2.zero; 340 | 341 | public static Tab selectedTab = Tab.tuning; 342 | public enum Tab { tuning, doorPathing, rules, misc }; 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /Source/Patch_Collider.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Verse; 3 | using Verse.AI; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Reflection.Emit; 7 | using RimWorld; 8 | 9 | namespace CleanPathfinding 10 | { 11 | [HarmonyPatch (typeof(PawnUtility), nameof(PawnUtility.ShouldCollideWithPawns))] 12 | static class Patch_ShouldCollideWithPawns 13 | { 14 | static IEnumerable Transpiler(IEnumerable instructions, ILGenerator generator) 15 | { 16 | if (!ModSettings_CleanPathfinding.optimizeCollider || instructions.Count() != 16) { 17 | foreach (var code in instructions) yield return code; 18 | if (ModSettings_CleanPathfinding.optimizeCollider) Log.Warning("[Clean Pathfinding] Could not apply collider optimization patch. There may be a mod conflict, or RimWorld updated?"); 19 | yield break; 20 | } 21 | 22 | var label = generator.DefineLabel(); 23 | var endLabel = new CodeInstruction(OpCodes.Ldc_I4_0); 24 | endLabel.labels.Add(label); 25 | 26 | yield return new CodeInstruction(OpCodes.Ldarg_0); 27 | yield return new CodeInstruction(OpCodes.Ldfld, AccessTools.Field(typeof(Pawn), nameof(Pawn.mindState))); 28 | yield return new CodeInstruction(OpCodes.Ldfld, AccessTools.Field(typeof(Pawn_MindState), nameof(Pawn_MindState.anyCloseHostilesRecently))); 29 | yield return new CodeInstruction(OpCodes.Brfalse_S, label); 30 | 31 | yield return new CodeInstruction(OpCodes.Ldarg_0); 32 | yield return new CodeInstruction(OpCodes.Ldfld, AccessTools.Field(typeof(Pawn), nameof(Pawn.health))); 33 | yield return new CodeInstruction(OpCodes.Ldfld, AccessTools.Field(typeof(Pawn_HealthTracker), nameof(Pawn_HealthTracker.healthState))); 34 | yield return new CodeInstruction(OpCodes.Ldc_I4_1); 35 | yield return new CodeInstruction(OpCodes.Beq_S, label); 36 | 37 | yield return new CodeInstruction(OpCodes.Ldarg_0); 38 | yield return new CodeInstruction(OpCodes.Ldfld, AccessTools.Field(typeof(Pawn), nameof(Pawn.health))); 39 | yield return new CodeInstruction(OpCodes.Ldfld, AccessTools.Field(typeof(Pawn_HealthTracker), nameof(Pawn_HealthTracker.healthState))); 40 | yield return new CodeInstruction(OpCodes.Ldc_I4_0); 41 | yield return new CodeInstruction(OpCodes.Cgt_Un); 42 | yield return new CodeInstruction(OpCodes.Ret); 43 | 44 | yield return endLabel; 45 | yield return new CodeInstruction(OpCodes.Ret); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /Source/Patch_Compat.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | using System.Collections.Generic; 5 | using HarmonyLib; 6 | using Verse; 7 | 8 | namespace CleanPathfinding 9 | { 10 | [HarmonyPatch] 11 | static class Patch_DoorsExpanded 12 | { 13 | static MethodBase target; 14 | 15 | static bool Prepare() 16 | { 17 | target = AccessTools.DeclaredMethod(AccessTools.TypeByName("DoorsExpanded.Building_DoorExpanded"), "GetGizmos"); 18 | return target != null; 19 | } 20 | 21 | static MethodBase TargetMethod() 22 | { 23 | DoorPathingUtility.usingDoorsExpanded = true; 24 | return target; 25 | } 26 | 27 | static IEnumerable Postfix(IEnumerable values, Building __instance) 28 | { 29 | if (!ModSettings_CleanPathfinding.doorPathing) foreach (var value in values) yield return value; 30 | else foreach (var item in DoorPathingUtility.GetGizmos(values, __instance)) yield return item; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /Source/Patch_MapExit.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Verse; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection.Emit; 6 | using RimWorld; 7 | 8 | namespace CleanPathfinding 9 | { 10 | [HarmonyPatch (typeof(RCellFinder), nameof(RCellFinder.TryFindBestExitSpot))] 11 | static class Patch_TryFindBestExitSpot 12 | { 13 | static IEnumerable Transpiler(IEnumerable instructions) 14 | { 15 | if (!ModSettings_CleanPathfinding.exitTuning) 16 | { 17 | foreach (var code in instructions) yield return code; 18 | yield break; 19 | } 20 | 21 | bool ran = false; 22 | foreach (var code in instructions) 23 | { 24 | yield return code; 25 | if (code.opcode == OpCodes.Ldc_I4_S) 26 | { 27 | yield return new CodeInstruction(OpCodes.Ldsfld, AccessTools.Field(typeof(ModSettings_CleanPathfinding), nameof(ModSettings_CleanPathfinding.exitRange))); 28 | yield return new CodeInstruction(OpCodes.Add); 29 | ran = true; 30 | } 31 | } 32 | 33 | if (!ran) Log.Warning("[Clean Pathfinding] Transpiler could not find target for exit finding patch. There may be a mod conflict, or RimWorld updated?"); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /Source/Patch_Wander.cs: -------------------------------------------------------------------------------- 1 | using HarmonyLib; 2 | using Verse.AI; 3 | using static CleanPathfinding.ModSettings_CleanPathfinding; 4 | using static CleanPathfinding.Mod_CleanPathfinding; 5 | 6 | namespace CleanPathfinding 7 | { 8 | [HarmonyPatch (typeof(JobGiver_Wander), nameof(JobGiver_Wander.TryGiveJob))] 9 | static class Patch_JobGiver_Wander 10 | { 11 | static bool Prepare() 12 | { 13 | if (!patchLedger.ContainsKey(nameof(Patch_JobGiver_Wander))) patchLedger.Add(nameof(Patch_JobGiver_Wander), wanderTuning); 14 | return wanderTuning; 15 | } 16 | public static void Postfix(Job __result) 17 | { 18 | if (__result?.def.shortHash == RimWorld.JobDefOf.Wait_Wander.shortHash) __result.expiryInterval += wanderDelay; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /Source/ResourceBank.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using Verse; 3 | 4 | namespace CleanPathfinding 5 | { 6 | [StaticConstructorOnStartup] 7 | internal static class ResourceBank 8 | { 9 | public static readonly Texture2D iconPriority = ContentFinder.Get("UI/Owl_DoorPriority", true); 10 | public static Color red = Color.red, blue = Color.blue, yellow = Color.yellow, white = Color.white; 11 | } 12 | } -------------------------------------------------------------------------------- /Source/Source.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /Textures/UI/Owl_DoorPriority.dds: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Owlchemist/clean-pathfinding/1fdbd93ae097edc2388a53d7037a463fc04aa7ba/Textures/UI/Owl_DoorPriority.dds -------------------------------------------------------------------------------- /Textures/UI/Owl_DoorPriority.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Owlchemist/clean-pathfinding/1fdbd93ae097edc2388a53d7037a463fc04aa7ba/Textures/UI/Owl_DoorPriority.png -------------------------------------------------------------------------------- /Textures/UI/Owl_Priority.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Owlchemist/clean-pathfinding/1fdbd93ae097edc2388a53d7037a463fc04aa7ba/Textures/UI/Owl_Priority.png --------------------------------------------------------------------------------