├── .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
--------------------------------------------------------------------------------