diff --git a/package.json b/package.json
index d0a3873..37eca4e 100644
--- a/package.json
+++ b/package.json
@@ -99,7 +99,8 @@
 		],
 		"ignores": [
 			"snippets.js",
-			"mock/constants.ts"
+			"mock/constants.ts",
+			"src/utils/Profiler"
 		],
 		"settings": {
 			"import/resolver": {
diff --git a/src/main.ts b/src/main.ts
index 219498e..49d1a1a 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -10,8 +10,7 @@ import './prototype/room';
 import './prototype/structure';
 
 // Create kernel object.
-import {PROCESS_PRIORITY_ALWAYS, PROCESS_PRIORITY_LOW, PROCESS_PRIORITY_HIGH} from 'hivemind';
-import hivemind from 'hivemind';
+import hivemind, {PROCESS_PRIORITY_ALWAYS, PROCESS_PRIORITY_LOW, PROCESS_PRIORITY_HIGH} from 'hivemind';
 import SegmentedMemory from 'utils/segmented-memory';
 
 import container from 'utils/container';
@@ -58,7 +57,7 @@ import * as Profiler from 'utils/Profiler';
 
 declare global {
 	interface RawMemory {
-		_parsed: boolean;
+		_parsed: Memory;
 	}
 
 	namespace NodeJS {
@@ -98,12 +97,14 @@ balancer.init();
 
 // @todo make unarmed creeps run from hostiles.
 
-const main = {
+class BotKernel {
+	lastTime: number = 0;
+	lastMemory: Memory = null;
 
 	/**
 	 * Runs main game loop.
 	 */
-	loop() {
+	runTick() {
 		this.useMemoryFromHeap();
 
 		hivemind.segmentMemory.manage();
@@ -213,10 +214,8 @@ const main = {
 
 		this.showDebug();
 		this.recordStats();
-	},
+	}
 
-	lastTime: 0,
-	lastMemory: null,
 	useMemoryFromHeap() {
 		if (this.lastTime && this.lastMemory && Game.time === this.lastTime + 1) {
 			delete global.Memory;
@@ -233,7 +232,7 @@ const main = {
 		}
 
 		this.lastTime = Game.time;
-	},
+	}
 
 	/**
 	 * Saves CPU stats for the current tick to memory.
@@ -252,7 +251,7 @@ const main = {
 		stats.recordStat('cpu_total', time);
 		stats.recordStat('bucket', Game.cpu.bucket);
 		stats.recordStat('creeps', _.size(Game.creeps));
-	},
+	}
 
 	/**
 	 * Periodically deletes unused data from memory.
@@ -296,37 +295,12 @@ const main = {
 
 		// Periodically clean old room memory.
 		if (Game.time % 3738 === 2100 && hivemind.segmentMemory.isReady()) {
-			let count = 0;
-			_.each(Memory.rooms, (memory, roomName) => {
-				if (getRoomIntel(roomName).getAge() > 100_000) {
-					delete Memory.rooms[roomName];
-					count++;
-				}
-
-				if (memory.observeTargets && !Game.rooms[roomName]?.observer) {
-					delete memory.observeTargets;
-				}
-			});
-
-			if (count > 0) {
-				hivemind.log('main').debug('Pruned old memory for', count, 'rooms.');
-			}
+			this.cleanupRoomMemory();
 		}
 
 		// Periodically clean old squad memory.
 		if (Game.time % 548 === 3) {
-			_.each(Memory.squads, (memory, squadName) => {
-				// Only delete if squad can't be spawned.
-				if (memory.spawnRoom && Game.rooms[memory.spawnRoom]) return;
-
-				// Don't delete inter-shard squad that can't have a spawn room.
-				if (squadName === 'interShardExpansion') return;
-
-				// Only delete if there are no creeps belonging to this squad.
-				if (_.size(_.filter(Game.creeps, creep => creep.memory.squadName === squadName)) > 0) return;
-
-				delete Memory.squads[squadName];
-			});
+			this.cleanupSquadMemory();
 		}
 
 		// Periodically garbage collect in caches.
@@ -352,7 +326,40 @@ const main = {
 		if (Game.time % 3625 == 0 && hivemind.segmentMemory.isReady()) {
 			this.cleanupSegmentMemory();
 		}
-	},
+	}
+
+	cleanupRoomMemory() {
+		let count = 0;
+		_.each(Memory.rooms, (memory, roomName) => {
+			if (getRoomIntel(roomName).getAge() > 100_000) {
+				delete Memory.rooms[roomName];
+				count++;
+			}
+
+			if (memory.observeTargets && !Game.rooms[roomName]?.observer) {
+				delete memory.observeTargets;
+			}
+		});
+
+		if (count > 0) {
+			hivemind.log('main').debug('Pruned old memory for', count, 'rooms.');
+		}
+	}
+
+	cleanupSquadMemory() {
+		_.each(Memory.squads, (memory, squadName) => {
+			// Only delete if squad can't be spawned.
+			if (memory.spawnRoom && Game.rooms[memory.spawnRoom]) return;
+
+			// Don't delete inter-shard squad that can't have a spawn room.
+			if (squadName === 'interShardExpansion') return;
+
+			// Only delete if there are no creeps belonging to this squad.
+			if (_.size(_.filter(Game.creeps, creep => creep.memory.squadName === squadName)) > 0) return;
+
+			delete Memory.squads[squadName];
+		});
+	}
 
 	cleanupSegmentMemory() {
 		// Clean old entries from remote path manager from segment memory.
@@ -383,7 +390,7 @@ const main = {
 			const isMyRoom = Game.rooms[roomName] && Game.rooms[roomName].isMine();
 			if (!isMyRoom) hivemind.segmentMemory.delete(key);
 		});
-	},
+	}
 
 	/**
 	 *
@@ -396,10 +403,11 @@ const main = {
 			Memory.hivemind.showProcessDebug--;
 			hivemind.drawProcessDebug();
 		}
-	},
+	}
 
 };
 
+const kernel = new BotKernel();
 export const loop = ErrorMapper.wrapLoop(() => {
-	main.loop();
+	kernel.runTick();
 });
diff --git a/src/prototype/room.structures.ts b/src/prototype/room.structures.ts
index c2ab570..d70d249 100644
--- a/src/prototype/room.structures.ts
+++ b/src/prototype/room.structures.ts
@@ -16,6 +16,8 @@ declare global {
 			[STRUCTURE_LINK]: StructureLink[];
 			[STRUCTURE_NUKER]: StructureNuker[];
 			[STRUCTURE_OBSERVER]: StructureObserver[];
+			[STRUCTURE_PORTAL]: StructurePortal[];
+			[STRUCTURE_POWER_BANK]: StructurePowerBank[];
 			[STRUCTURE_POWER_SPAWN]: StructurePowerSpawn[];
 			[STRUCTURE_RAMPART]: StructureRampart[];
 			[STRUCTURE_SPAWN]: StructureSpawn[];
diff --git a/src/role/hauler.relay.ts b/src/role/hauler.relay.ts
index 8daf2ed..b82da34 100644
--- a/src/role/hauler.relay.ts
+++ b/src/role/hauler.relay.ts
@@ -463,7 +463,7 @@ export default class RelayHaulerRole extends Role {
 				filter: structure => structure.structureType === STRUCTURE_CONTAINER
 					&& structure.store.getFreeCapacity() < structure.store.getCapacity() * 0.1
 					&& structure.store.getUsedCapacity(RESOURCE_ENERGY) > 100,
-			});
+			}) as StructureContainer[];
 			if (container.length > 0) creep.heapMemory.energyPickupTarget = container[0].id;
 			return container[0];
 		}
diff --git a/src/room-intel.ts b/src/room-intel.ts
index 56d4901..19a99bf 100644
--- a/src/room-intel.ts
+++ b/src/room-intel.ts
@@ -1,5 +1,5 @@
 /* global PathFinder Room RoomPosition
-STRUCTURE_KEEPER_LAIR STRUCTURE_CONTROLLER CONTROLLER_DOWNGRADE FIND_SOURCES
+STRUCTURE_KEEPER_LAIR STRUCTURE_CONTROLLER FIND_SOURCES
 TERRAIN_MASK_WALL TERRAIN_MASK_SWAMP POWER_BANK_DECAY STRUCTURE_PORTAL
 STRUCTURE_POWER_BANK FIND_MY_CONSTRUCTION_SITES STRUCTURE_STORAGE
 STRUCTURE_TERMINAL FIND_RUINS STRUCTURE_INVADER_CORE EFFECT_COLLAPSE_TIMER */
@@ -36,6 +36,12 @@ declare global {
 	}
 }
 
+type AdjacentRoomEntry = {
+	range: number;
+	origin: string;
+	room: string;
+};
+
 export interface RoomIntelMemory {
 	lastScan: number;
 	exits: Partial<Record<ExitKey, string>>;
@@ -104,7 +110,7 @@ export default class RoomIntel {
 	otherUnsafeRooms: string[];
 	joinedDirs: Record<string, Record<string, boolean>>;
 
-	constructor(roomName) {
+	constructor(roomName: string) {
 		this.roomName = roomName;
 
 		const key = 'intel:' + roomName;
@@ -139,9 +145,9 @@ export default class RoomIntel {
 		this.gatherResourceIntel(room);
 
 		const structures = room.structuresByType;
-		this.gatherPowerIntel(structures[STRUCTURE_POWER_BANK] as StructurePowerBank[]);
+		this.gatherPowerIntel(structures[STRUCTURE_POWER_BANK]);
 		this.gatherDepositIntel();
-		this.gatherPortalIntel(structures[STRUCTURE_PORTAL] as StructurePortal[]);
+		this.gatherPortalIntel(structures[STRUCTURE_PORTAL]);
 		this.gatherInvaderIntel(structures);
 		this.gatherExitIntel(room.name);
 
@@ -180,7 +186,7 @@ export default class RoomIntel {
 		this.memory.rcl = 0;
 		this.memory.ticksToDowngrade = 0;
 		this.memory.hasController = typeof room.controller !== 'undefined';
-		if (room.controller && room.controller.owner) {
+		if (room.controller?.owner) {
 			this.memory.owner = room.controller.owner.username;
 			this.memory.rcl = room.controller.level;
 			this.memory.ticksToDowngrade = room.controller.ticksToDowngrade;
@@ -386,7 +392,7 @@ export default class RoomIntel {
 	gatherPortalIntel(portals: StructurePortal[]) {
 		delete this.memory.portals;
 
-		const targetRooms = [];
+		const targetRooms: string[] = [];
 		for (const portal of portals || []) {
 			// Ignore same-shard portals for now.
 			if ('shard' in portal.destination) {
@@ -456,13 +462,13 @@ export default class RoomIntel {
 		if (this.memory.owner) return;
 		if (!structures[STRUCTURE_STORAGE] && !structures[STRUCTURE_TERMINAL] && ruins.length === 0) return;
 
-		const resources = {};
+		const resources: Partial<Record<ResourceConstant, number>> = {};
 		const collections = [structures[STRUCTURE_STORAGE], structures[STRUCTURE_TERMINAL], ruins];
 		_.each(collections, objects => {
 			_.each(objects, object => {
 				if (!object.store) return;
 
-				_.each(object.store, (amount, resourceType) => {
+				_.each(object.store, (amount: number, resourceType: ResourceConstant) => {
 					resources[resourceType] = (resources[resourceType] || 0) + amount;
 				});
 			});
@@ -497,10 +503,10 @@ export default class RoomIntel {
 	 * @param {object} structures
 	 *   An object containing Arrays of structures, keyed by structure type.
 	 */
-	gatherInvaderIntel(structures) {
+	gatherInvaderIntel(structures: Record<string, Structure[]>) {
 		delete this.memory.invaderInfo;
 
-		const core: StructureInvaderCore = _.first(structures[STRUCTURE_INVADER_CORE]);
+		const core = _.first(structures[STRUCTURE_INVADER_CORE]) as StructureInvaderCore;
 		if (!core) return;
 
 		// Commit basic invader core info.
@@ -619,7 +625,7 @@ export default class RoomIntel {
 	 */
 	isClaimed(): boolean {
 		if (this.isOwned()) return true;
-		if (this.memory.reservation && this.memory.reservation.username && this.memory.reservation.username !== getUsername()) return true;
+		if (this.memory.reservation?.username && this.memory.reservation.username !== getUsername()) return true;
 
 		return false;
 	}
@@ -675,7 +681,7 @@ export default class RoomIntel {
 	 *   Type of this room's mineral source.
 	 */
 	getMineralTypes(): string[] {
-		const result = [];
+		const result: string[] = [];
 
 		for (const mineral of this.memory.minerals || []) {
 			result.push(mineral.type);
@@ -716,7 +722,7 @@ export default class RoomIntel {
 	 */
 	getCostMatrix(): CostMatrix {
 		// @todo For some reason, calling this in console gives a different version of the cost matrix. Verify!
-		let obstaclePositions;
+		let obstaclePositions: {obstacles: RoomPosition[]; roads: RoomPosition[]};
 		if (this.memory.costPositions) {
 			obstaclePositions = {
 				obstacles: unpackCoordListAsPosList(this.memory.costPositions[0], this.roomName),
@@ -817,7 +823,7 @@ export default class RoomIntel {
 	 * @return {number}
 	 *   Number of tiles of the given type in this room.
 	 */
-	countTiles(type: string) {
+	countTiles(type: 'plain' | 'swamp' | 'wall' | 'exit') {
 		if (!this.memory.terrain) return 0;
 
 		return this.memory.terrain[type] || 0;
@@ -871,7 +877,7 @@ export default class RoomIntel {
 				[RIGHT]: 'E',
 				[BOTTOM]: 'S',
 				[LEFT]: 'W',
-			};
+			} as const;
 
 			this.newStatus = {
 				N: true,
@@ -880,14 +886,14 @@ export default class RoomIntel {
 				W: true,
 			};
 
-			const openList = {};
-			const closedList = {};
+			const openList: Record<string, AdjacentRoomEntry> = {};
+			const closedList: Record<string, AdjacentRoomEntry> = {};
 			this.joinedDirs = {};
 			this.otherSafeRooms = options ? (options.safe || []) : [];
 			this.otherUnsafeRooms = options ? (options.unsafe || []) : [];
 			// Add initial directions to open list.
-			for (const moveDir of _.keys(this.memory.exits)) {
-				const dir = dirMap[moveDir];
+			for (const moveDir in this.memory.exits) {
+				const dir: string = dirMap[moveDir];
 				const roomName = this.memory.exits[moveDir];
 
 				this.addAdjacentRoomToCheck(roomName, openList, {dir, range: 0});
@@ -895,7 +901,7 @@ export default class RoomIntel {
 
 			// Process adjacent rooms until range has been reached.
 			while (_.size(openList) > 0) {
-				let minRange = null;
+				let minRange: AdjacentRoomEntry = null;
 				for (const roomName in openList) {
 					if (!minRange || minRange.range > openList[roomName].range) {
 						minRange = openList[roomName];
@@ -942,7 +948,7 @@ export default class RoomIntel {
 	 * @param {object} base
 	 *   Information about the room this operation is base on.
 	 */
-	addAdjacentRoomToCheck(roomName: string, openList: Record<string, {range: number; origin: string; room: string}>, base: {range: number; dir: string}) {
+	addAdjacentRoomToCheck(roomName: string, openList: Record<string, AdjacentRoomEntry>, base: {range: number; dir: string}) {
 		if (!this.isPotentiallyUnsafeRoom(roomName)) return;
 
 		openList[roomName] = {
@@ -975,7 +981,7 @@ export default class RoomIntel {
 	 * @param {object} closedList
 	 *   List of rooms that have been checked.
 	 */
-	handleAdjacentRoom(roomData: {range: number; origin: string; room: string}, openList: Record<string, {range: number; origin: string; room: string}>, closedList: Record<string, {range: number; origin: string; room: string}>) {
+	handleAdjacentRoom(roomData: AdjacentRoomEntry, openList: Record<string, AdjacentRoomEntry>, closedList: Record<string, AdjacentRoomEntry>) {
 		const roomIntel = getRoomIntel(roomData.room);
 		if (roomIntel.getAge() > 100_000) {
 			// Room has no intel, declare it as unsafe.
diff --git a/src/utils/nav-mesh.ts b/src/utils/nav-mesh.ts
index 4c37c65..7a8a9f8 100644
--- a/src/utils/nav-mesh.ts
+++ b/src/utils/nav-mesh.ts
@@ -3,7 +3,7 @@ TERRAIN_MASK_WALL STRUCTURE_KEEPER_LAIR */
 
 import cache from 'utils/cache';
 import hivemind from 'hivemind';
-import {encodePosition, serializePosition, deserializePosition, serializeCoords} from 'utils/serialization';
+import {encodePosition, deserializePosition, serializeCoords} from 'utils/serialization';
 import {getCostMatrix} from 'utils/cost-matrix';
 import {getRoomIntel} from 'room-intel';
 import {handleMapArea} from 'utils/map';
@@ -88,8 +88,7 @@ export default class NavMesh {
 		// Mesh doesn't need to be updated very often.
 		// @todo Allow forcing update for when we dismantle a structure.
 		if (
-			this.memory.rooms[roomName]
-			&& this.memory.rooms[roomName].paths
+			this.memory.rooms[roomName]?.paths
 			&& !hivemind.hasIntervalPassed(10_000, this.memory.rooms[roomName].gen)
 		) return;
 
@@ -147,17 +146,18 @@ export default class NavMesh {
 	 *   An array of exit information objects.
 	 */
 	getExitInfo(roomName: string): ExitInfo[] {
-		const exits: ExitInfo[] = [];
-
-		this.collectExitGroups(roomName, exits, LEFT, true, 0);
-		this.collectExitGroups(roomName, exits, RIGHT, true, 49);
-		this.collectExitGroups(roomName, exits, TOP, false, 0);
-		this.collectExitGroups(roomName, exits, BOTTOM, false, 49);
+		const exits: ExitInfo[] = [
+			...this.collectExitGroups(roomName, LEFT, true, 0),
+			...this.collectExitGroups(roomName, RIGHT, true, 49),
+			...this.collectExitGroups(roomName, TOP, false, 0),
+			...this.collectExitGroups(roomName, BOTTOM, false, 49),
+		];
 
 		return exits;
 	}
 
-	collectExitGroups(roomName: string, exits: ExitInfo[], dir: DirectionConstant, vertical: boolean, offset: number) {
+	collectExitGroups(roomName: string, dir: DirectionConstant, vertical: boolean, offset: number): ExitInfo[] {
+		const exits: ExitInfo[] = [];
 		const isAvailable = this.isAvailableExitDirection(roomName, dir);
 		let groupId = 1;
 		let currentStart: number = null;
@@ -187,6 +187,8 @@ export default class NavMesh {
 
 			this.costMatrix.set(x, y, isAvailable ? (nextId + 100) : 255);
 		}
+
+		return exits;
 	}
 
 	isAvailableExitDirection(roomName: string, dir: DirectionConstant): boolean {
@@ -311,17 +313,46 @@ export default class NavMesh {
 		const costMatrix = getCostMatrix(roomName, {ignoreMilitary: true});
 
 		for (const region of regions) {
-			const centerXR = region.center % 50;
-			const centerYR = Math.floor(region.center / 50);
+			this.addConnectingPathsForRegion(region, paths, roomName, costMatrix);
+		}
+
+		return paths;
+	}
+
+	addConnectingPathsForRegion(region: RegionInfo, paths: Record<number, Record<number, number>>, roomName: string, costMatrix: CostMatrix) {
+		const centerXR = region.center % 50;
+		const centerYR = Math.floor(region.center / 50);
+
+		for (const exitId of region.exits) {
+			const exit = this.exitLookup[exitId];
+			const centerX = exit.vertical ? exit.offset : exit.center;
+			const centerY = exit.vertical ? exit.center : exit.offset;
+
+			const result = PathFinder.search(
+				new RoomPosition(centerX, centerY, roomName),
+				new RoomPosition(centerXR, centerYR, roomName),
+				{
+					roomCallback: () => costMatrix,
+					maxRooms: 1,
+				},
+			);
+
+			if (!result.incomplete) {
+				if (!paths[exitId]) paths[exitId] = {};
+				paths[exitId][0] = result.path.length;
+			}
+
+			for (const exitId2 of region.exits) {
+				if (exitId === exitId2) continue;
+				if (paths[exitId2] && paths[exitId2][exitId]) continue;
 
-			for (const exitId of region.exits) {
-				const exit = this.exitLookup[exitId];
-				const centerX = exit.vertical ? exit.offset : exit.center;
-				const centerY = exit.vertical ? exit.center : exit.offset;
+				const exit2 = this.exitLookup[exitId2];
+				const centerX2 = exit2.vertical ? exit2.offset : exit2.center;
+				const centerY2 = exit2.vertical ? exit2.center : exit2.offset;
 
 				const result = PathFinder.search(
 					new RoomPosition(centerX, centerY, roomName),
-					new RoomPosition(centerXR, centerYR, roomName),
+					new RoomPosition(centerX2, centerY2, roomName),
 					{
 						roomCallback: () => costMatrix,
 						maxRooms: 1,
@@ -330,35 +361,10 @@ export default class NavMesh {
 
 				if (!result.incomplete) {
 					if (!paths[exitId]) paths[exitId] = {};
-					paths[exitId][0] = result.path.length;
-				}
-
-				for (const exitId2 of region.exits) {
-					if (exitId === exitId2) continue;
-					if (paths[exitId2] && paths[exitId2][exitId]) continue;
-
-					const exit2 = this.exitLookup[exitId2];
-					const centerX2 = exit2.vertical ? exit2.offset : exit2.center;
-					const centerY2 = exit2.vertical ? exit2.center : exit2.offset;
-
-					const result = PathFinder.search(
-						new RoomPosition(centerX, centerY, roomName),
-						new RoomPosition(centerX2, centerY2, roomName),
-						{
-							roomCallback: () => costMatrix,
-							maxRooms: 1,
-						},
-					);
-
-					if (!result.incomplete) {
-						if (!paths[exitId]) paths[exitId] = {};
-						paths[exitId][exitId2] = result.path.length;
-					}
+					paths[exitId][exitId2] = result.path.length;
 				}
 			}
 		}
-
-		return paths;
 	}
 
 	getPortals(roomName: string) {
@@ -437,10 +443,6 @@ export default class NavMesh {
 		const startTime = Game.cpu.getUsed();
 		const startRoom = startPos.roomName;
 		const endRoom = endPos.roomName;
-		let availableExits: Array<{
-			id: number;
-			center: number;
-		}> = [];
 		const openList: NavMeshPathfindingEntry[] = [];
 		const openListLookup: Record<string, boolean> = {};
 		const closedList: Record<string, boolean> = {};
@@ -452,60 +454,8 @@ export default class NavMesh {
 		}
 
 		const roomMemory = this.memory.rooms[startRoom];
-		if (roomMemory.regions) {
-			const costMatrix = getCostMatrix(startRoom, {ignoreMilitary: true});
-			for (const region of roomMemory.regions) {
-				// Check if we can reach region center.
-				const result = PathFinder.search(
-					startPos,
-					deserializePosition(region.center, startRoom),
-					{
-						roomCallback: () => costMatrix,
-						maxRooms: 1,
-					},
-				);
-
-				if (result.incomplete) continue;
-
-				// Exits for this region are available.
-				availableExits = _.filter(roomMemory.exits, exit => region.exits.includes(exit.id));
-			}
-		}
-		else {
-			availableExits = roomMemory.exits;
-		}
-
-		for (const exit of availableExits) {
-			const segmentLength = roomMemory.paths[exit.id] ? roomMemory.paths[exit.id][0] : 50;
-			const entry: NavMeshPathfindingEntry = {
-				exitId: exit.id,
-				pos: exit.center,
-				roomName: startRoom,
-				parent: null,
-				pathLength: segmentLength,
-				totalSteps: segmentLength,
-				heuristic: (Game.map.getRoomLinearDistance(startRoom, endRoom) - 1) * 50,
-				portal: false,
-			};
-			openList.push(entry);
-			openListLookup[startRoom + '/' + entry.pos] = true;
-		}
-
-		for (const portal of roomMemory.portals || []) {
-			const entry: NavMeshPathfindingEntry = {
-				exitId: null,
-				pos: portal.pos,
-				roomName: startRoom,
-				parent: null,
-				pathLength: 25,
-				totalSteps: 25,
-				heuristic: (Game.map.getRoomLinearDistance(startRoom, endRoom) - 1) * 50,
-				portal: true,
-				targetRoom: portal.room,
-			};
-			openList.push(entry);
-			openListLookup[startRoom + '/' + entry.pos] = true;
-		}
+		this.addExitsToOpenList(this.getAvailableExits(startPos, startRoom), openList, openListLookup, startRoom, endRoom);
+		this.addPortalsToOpenList(openList, openListLookup, startRoom, endRoom);
 
 		while (openList.length > 0) {
 			if (Game.cpu.getUsed() - startTime > (options.maxCpu || 5)) {
@@ -518,8 +468,7 @@ export default class NavMesh {
 			const current = this.popBestCandidate(openList);
 			const nextRoom = current.portal ? current.targetRoom : this.getAdjacentRoom(current.roomName, current.exitId);
 			const correspondingExit = current.portal ? null : this.getCorrespondingExitId(current.exitId);
-			let costMultiplier = 1;
-			closedList[current.roomName + '/' + current.pos] = true;
+			closedList[`${current.roomName}/${current.pos}`] = true;
 
 			if (nextRoom === endRoom) {
 				// @todo There might be shorter paths to the actual endPosition.
@@ -547,58 +496,29 @@ export default class NavMesh {
 			if (current.portal) {
 				const portalBack = _.find(roomMemory.portals, p => p.room === current.roomName);
 				const exitPos = portalBack ? portalBack.pos : (25 + (50 * 25));
-				if (closedList[nextRoom + '/' + exitPos]) continue;
+				if (closedList[`${nextRoom}/${exitPos}`]) continue;
 
-				closedList[nextRoom + '/' + exitPos] = true;
+				closedList[`${nextRoom}/${exitPos}`] = true;
 			}
 			else if (roomMemory.exits[correspondingExit]) {
 				const exitPos = roomMemory.exits[correspondingExit].center;
-				if (closedList[nextRoom + '/' + exitPos]) continue;
+				if (closedList[`${nextRoom}/${exitPos}`]) continue;
 
-				closedList[nextRoom + '/' + exitPos] = true;
+				closedList[`${nextRoom}/${exitPos}`] = true;
 			}
 
 			if (hivemind.segmentMemory.isReady()) {
 				const roomIntel = getRoomIntel(nextRoom);
 				if (roomIntel.isOwned()) {
 					if (!options.allowDanger && !hivemind.relations.isAlly(roomIntel.getOwner())) continue;
-
-					costMultiplier *= 5;
 				}
-				else if (roomIntel.isClaimed() && roomIntel.getReservationStatus().username !== 'Invader') {
-					costMultiplier *= 1.5;
-				}
-				else if (_.size(roomIntel.getStructures(STRUCTURE_KEEPER_LAIR)) > 0) {
-					// Allow pathing through source keeper rooms since we can safely avoid them.
-					costMultiplier *= 1.2;
-				}
-			}
-
-			if (Memory.rooms[nextRoom]?.enemies && !Memory.rooms[nextRoom]?.enemies?.safe && !options.allowDanger) {
-				// Avoid rooms with enemies in them if possible.
-				costMultiplier *= 2;
-			}
-
-			availableExits = [];
-			if (current.portal) {
-				availableExits = roomMemory.exits;
-			}
-
-			if (roomMemory.regions) {
-				// Find region containing corresponding exit.
-				const region = _.find(roomMemory.regions, (region: any) => region.exits.includes(correspondingExit));
-				if (!region) continue;
-
-				availableExits = _.filter(roomMemory.exits, exit => exit.id !== correspondingExit && region.exits.includes(exit.id));
 			}
-			else {
-				availableExits = _.filter(roomMemory.exits, exit => exit.id !== correspondingExit);
-			}
-
-			for (const exit of availableExits) {
+	
+			const costMultiplier = this.calculateCostMultiplier(nextRoom, options.allowDanger);
+			for (const exit of this.getAvailableExitsCorrespondingTo(nextRoom, correspondingExit, current.portal)) {
 				// Check if in closed list.
-				if (closedList[nextRoom + '/' + exit.center]) continue;
-				if (openListLookup[nextRoom + '/' + exit.center]) continue;
+				if (closedList[`${nextRoom}/${exit.center}`]) continue;
+				if (openListLookup[`${nextRoom}/${exit.center}`]) continue;
 
 				if (!current.portal) {
 					// If there's a weird path mismatch, skip.
@@ -627,14 +547,14 @@ export default class NavMesh {
 				}
 
 				openList.push(item);
-				openListLookup[nextRoom + '/' + exit.center] = true;
-				openListLookup[nextRoom + '/' + item.pos] = true;
+				openListLookup[`${nextRoom}/${exit.center}`] = true;
+				openListLookup[`${nextRoom}/${item.pos}`] = true;
 			}
 
 			for (const portal of roomMemory.portals || []) {
 				// Check if in closed list.
-				if (closedList[nextRoom + '/' + portal.pos]) continue;
-				if (openListLookup[nextRoom + '/' + portal.pos]) continue;
+				if (closedList[`${nextRoom}/${portal.pos}`]) continue;
+				if (openListLookup[`${nextRoom}/${portal.pos}`]) continue;
 
 				const item = {
 					exitId: null,
@@ -654,8 +574,7 @@ export default class NavMesh {
 				}
 
 				openList.push(item);
-				openListLookup[nextRoom + '/' + item.pos] = true;
-				openListLookup[nextRoom + '/' + item.pos] = true;
+				openListLookup[`${nextRoom}/${item.pos}`] = true;
 			}
 		}
 
@@ -666,6 +585,114 @@ export default class NavMesh {
 		};
 	}
 
+	getAvailableExits(startPos: RoomPosition, roomName: string): Array<{
+		id: number;
+		center: number;
+	}> {
+		const roomMemory = this.memory.rooms[roomName];
+		if (!roomMemory.regions) return roomMemory.exits;
+
+		const costMatrix = getCostMatrix(roomName, {ignoreMilitary: true});
+		for (const region of roomMemory.regions) {
+			// Check if we can reach region center.
+			const result = PathFinder.search(
+				startPos,
+				deserializePosition(region.center, roomName),
+				{
+					roomCallback: () => costMatrix,
+					maxRooms: 1,
+				},
+			);
+
+			if (result.incomplete) continue;
+
+			// Exits for this region are available.
+			return _.filter(roomMemory.exits, exit => region.exits.includes(exit.id));
+		}
+
+		return [];
+	}
+
+	getAvailableExitsCorrespondingTo(roomName: string, exitId: number | null, isPortal: boolean): Array<{
+		id: number;
+		center: number;
+	}> {
+		const roomMemory = this.memory.rooms[roomName];
+		if (isPortal) return roomMemory.exits;
+
+		if (!roomMemory.regions) return _.filter(roomMemory.exits, exit => exit.id !== exitId);
+
+		// Find region containing corresponding exit.
+		const region = _.find(roomMemory.regions, (region: any) => region.exits.includes(exitId));
+		if (!region) return [];
+
+		return _.filter(roomMemory.exits, exit => exit.id !== exitId && region.exits.includes(exit.id));
+	}
+
+	addExitsToOpenList(exits: Array<{id: number; center: number;}>, openList: NavMeshPathfindingEntry[], openListLookup: Record<string, boolean>, roomName: string, endRoom: string) {
+		const roomMemory = this.memory.rooms[roomName];
+		for (const exit of exits) {
+			const segmentLength = roomMemory.paths[exit.id] ? roomMemory.paths[exit.id][0] : 50;
+			const entry: NavMeshPathfindingEntry = {
+				exitId: exit.id,
+				pos: exit.center,
+				roomName,
+				parent: null,
+				pathLength: segmentLength,
+				totalSteps: segmentLength,
+				heuristic: (Game.map.getRoomLinearDistance(roomName, endRoom) - 1) * 50,
+				portal: false,
+			};
+			openList.push(entry);
+			openListLookup[`${roomName}/${entry.pos}`] = true;
+		}
+	}
+
+	addPortalsToOpenList(openList: NavMeshPathfindingEntry[], openListLookup: Record<string, boolean>, roomName: string, endRoom: string) {
+		const roomMemory = this.memory.rooms[roomName];
+		for (const portal of roomMemory.portals || []) {
+			const entry: NavMeshPathfindingEntry = {
+				exitId: null,
+				pos: portal.pos,
+				roomName,
+				parent: null,
+				pathLength: 25,
+				totalSteps: 25,
+				heuristic: (Game.map.getRoomLinearDistance(roomName, endRoom) - 1) * 50,
+				portal: true,
+				targetRoom: portal.room,
+			};
+			openList.push(entry);
+			openListLookup[`${roomName}/${entry.pos}`] = true;
+		}
+	}
+
+	calculateCostMultiplier(roomName: string, allowDanger: boolean): number {
+		let costMultiplier = 1;
+		if (hivemind.segmentMemory.isReady()) {
+			const roomIntel = getRoomIntel(roomName);
+			if (roomIntel.isOwned()) {
+				if (!hivemind.relations.isAlly(roomIntel.getOwner())) {
+					costMultiplier *= 5;
+				}
+			}
+			else if (roomIntel.isClaimed() && roomIntel.getReservationStatus().username !== 'Invader') {
+				costMultiplier *= 1.5;
+			}
+			else if (_.size(roomIntel.getStructures(STRUCTURE_KEEPER_LAIR)) > 0) {
+				// Allow pathing through source keeper rooms since we can safely avoid them.
+				costMultiplier *= 1.2;
+			}
+		}
+
+		if (Memory.rooms[roomName]?.enemies && !Memory.rooms[roomName]?.enemies?.safe && !allowDanger) {
+			// Avoid rooms with enemies in them if possible.
+			costMultiplier *= 2;
+		}
+
+		return costMultiplier;
+	}
+
 	popBestCandidate(openList: NavMeshPathfindingEntry[]): NavMeshPathfindingEntry {
 		// Find element id with lowest pathLength + heuristic.
 		let minId = null;