diff --git a/CHANGELOG.md b/CHANGELOG.md index ac2e436..2c4f0ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## 1.12.3 +### Bugfixes +- Fixed a bug that could cause foundry to freeze indefinitely when trying to pathfind to an unreachalbe location (thanks to JDCalvert) +- Fixed a bug that caused the pathfinder to route through one-directional walls from the wrong direction (thanks to JDCalvert) +- Fixed a bug that could cause Drag Ruler to write errors into the JS console during regular usage + +### Compatibility +- Drag Ruler's generic speed provider is now aware of good defaults for DnD 4th Edition +- Drag Ruler's pathfinder should now be compatible with the Wall Height and Levels modules (thanks to JDCalvert) + + ## 1.12.2 ### Bugfixes - Fixed a bug where the pathfinder on gridless scenes sometimes wasn't able to find a way around corners with specific angles diff --git a/js/pathfinding.js b/js/pathfinding.js index 325d806..e2aa518 100644 --- a/js/pathfinding.js +++ b/js/pathfinding.js @@ -9,9 +9,59 @@ import {PriorityQueueSet} from "./data_structures.js"; import { buildCostFunction } from "./api.js"; import { measure } from "./foundry_imports.js"; -// TODO Account for changing terrain per token in the caches -let caches = new Map(); -let cacheElevation; +class Cache { + static maxCacheIds = 5; + + constructor() { + this.nodes = new Map(); + this.lastUsed = new Map(); + } + + clear() { + this.nodes.clear(); + this.lastUsed.clear(); + } + + /** + * Get the cache associated with the given cache ID, creating a new one + * if we don't already have one + */ + getCachedNodes(cacheId) { + // Track that we've last used this cache right now + this.lastUsed.set(cacheId, Date.now()); + + // Get the nodes for the cacheId. If we don't already have one, create one + let cachedNodes = this.nodes.get(cacheId); + if (!cachedNodes) { + cachedNodes = new Array(gridHeight); + for (let y = 0; y < gridHeight; y++) { + cachedNodes[y] = new Array(gridWidth); + for (let x = 0; x < gridWidth; x++) { + cachedNodes[y][x] = {x, y}; + } + } + this.nodes.set(cacheId, cachedNodes); + + // Since we're adding a new cache, check if we have too many and, + // if we do, get rid of the one that was last used longest ago + if (this.lastUsed.size > Cache.maxCacheIds) { + let oldest; + for (let entry of this.lastUsed) { + if (!oldest || oldest[1] > entry[1]) { + oldest = entry; + } + } + this.nodes.delete(oldest[0]); + this.lastUsed.delete(oldest[0]); + } + } + + return cachedNodes; + } +} + +const cache = new Cache(); + let use5105 = false; let gridlessPathfinders = new Map(); let gridWidth, gridHeight; @@ -27,8 +77,6 @@ export function isPathfindingEnabled() { } export function findPath(from, to, token, previousWaypoints) { - checkCacheValid(token); - if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) { let tokenSize = Math.max(token.data.width, token.data.height) * canvas.dimensions.size; let pathfinder = gridlessPathfinders.get(tokenSize); @@ -39,15 +87,16 @@ export function findPath(from, to, token, previousWaypoints) { paintGridlessPathfindingDebug(pathfinder); return GridlessPathfinding.findPath(pathfinder, from, to); } else { - const lastNode = calculatePath(from, to, token, previousWaypoints); + const cachedNodes = getCachedNodes(token); + const lastNode = calculatePath(from, to, cachedNodes, token, previousWaypoints); if (!lastNode) return null; paintGriddedPathfindingDebug(lastNode, token); const path = []; let currentNode = lastNode; while (currentNode) { - if (path.length >= 2 && !stepCollidesWithWall(path[path.length - 2], currentNode.node, token)) - // Replace last waypoint if the current waypoint leads to a valid path that isn't longer than the old path + if (path.length >= 2 && !stepCollidesWithWall(path[path.length - 2], currentNode.node, token)) { + // Replace last waypoint if the current waypoint leads to a valid path that isn't longer than the old path if (window.terrainRuler) { let startNode = getCenterFromGridPositionObj(path[path.length - 2]); let middleNode = getCenterFromGridPositionObj(path[path.length - 1]); @@ -83,24 +132,32 @@ export function findPath(from, to, token, previousWaypoints) { } } -function getNode(pos, token, initialize=true) { - let shapeId = getTokenShapeId(token); - let cache = caches.get(shapeId); - if (!cache) { - cache = new Array(gridHeight); - caches.set(shapeId, cache); - } - if (!cache[pos.y]) - cache[pos.y] = new Array(gridWidth); - if (!cache[pos.y][pos.x]) { - cache[pos.y][pos.x] = pos; +/** + * Build a cache ID based on the current token's data and then retrieve the cache to use from that + */ +function getCachedNodes(token) { + const cacheData = {}; + + // Different-sized tokens snap to different points on the grid, + // so they might follow a different path to other tokens + cacheData.tokenShape = getTokenShapeId(token); + + // If levels is enabled, the token's elevation can affect which walls + // they need to worry about + if (game.modules.get("levels")?.active) { + cacheData.elevation = token.data.elevation; } - const node = cache[pos.y][pos.x]; + const cacheId = JSON.stringify(cacheData); + return cache.getCachedNodes(cacheId); +} + +function getNode(pos, cachedNodes, token, initialize = true) { + const node = cachedNodes[pos.y][pos.x]; if (initialize && !node.edges) { node.edges = []; for (const neighborPos of canvas.grid.grid.getNeighbors(pos.y, pos.x).map(([y, x]) => {return {x, y};})) { - if (neighborPos.x < 0 || neighborPos.y < 0 || neighborPos.x > gridWidth || neighborPos.y > gridHeight) { + if (neighborPos.x < 0 || neighborPos.y < 0 || neighborPos.x >= gridWidth || neighborPos.y >= gridHeight) { continue; } @@ -125,7 +182,7 @@ function getNode(pos, token, initialize=true) { // TODO Account for difficult terrain edgeCost = isDiagonal ? (use5105 ? 1.5 : 1.0001) : 1; } - const neighbor = getNode(neighborPos, token, false); + const neighbor = getNode(neighborPos, cachedNodes, token, false); node.edges.push({target: neighbor, cost: edgeCost}); } } @@ -133,7 +190,7 @@ function getNode(pos, token, initialize=true) { return node; } -function calculatePath(from, to, token, previousWaypoints) { +function calculatePath(from, to, cachedNodes, token, previousWaypoints) { use5105 = game.system.id === "pf2e" || canvas.grid.diagonalRule === "5105"; let startCost = 0; if (use5105 && canvas.grid.type === CONST.GRID_TYPES.SQUARE) { @@ -146,7 +203,7 @@ function calculatePath(from, to, token, previousWaypoints) { nextNodes.pushWithPriority( { - node: getNode(to, token), + node: getNode(to, cachedNodes, token), cost: startCost, estimated: startCost + estimateCost(to, from), previous: null @@ -161,7 +218,7 @@ function calculatePath(from, to, token, previousWaypoints) { } previousNodes.add(currentNode.node); for (const edge of currentNode.node.edges) { - const neighborNode = getNode(edge.target, token); + const neighborNode = getNode(edge.target, cachedNodes, token); if (previousNodes.has(neighborNode)) { continue; } @@ -202,7 +259,7 @@ function stepCollidesWithWall(from, to, token) { } export function wipePathfindingCache() { - caches.clear(); + cache.clear(); for (const pathfinder of gridlessPathfinders.values()) { GridlessPathfinding.free(pathfinder); } @@ -211,20 +268,6 @@ export function wipePathfindingCache() { debugGraphics.removeChildren().forEach(c => c.destroy()); } -/** - * Check if the current cache is still suitable for the path we're about to find. If not, clear the cache - */ - function checkCacheValid(token) { - // If levels is enabled, the cache is invalid if it was made for a - if (game.modules.get("levels")?.active) { - const tokenElevation = token.data.elevation; - if (tokenElevation !== cacheElevation) { - cacheElevation = tokenElevation; - wipePathfindingCache(); - } - } -} - export function initializePathfinding() { gridWidth = Math.ceil(canvas.dimensions.width / canvas.grid.w); gridHeight = Math.ceil(canvas.dimensions.height / canvas.grid.h); diff --git a/js/socket.js b/js/socket.js index f610fb5..47fc178 100644 --- a/js/socket.js +++ b/js/socket.js @@ -39,5 +39,7 @@ export function recalculate(tokens) { } function _socketRecalculate(tokenIds) { - return canvas.controls.ruler.dragRulerRecalculate(tokenIds); + const ruler = canvas.controls.ruler; + if (ruler.isDragRuler) + ruler.dragRulerRecalculate(tokenIds); } diff --git a/module.json b/module.json index d123fda..6df1c76 100644 --- a/module.json +++ b/module.json @@ -2,7 +2,7 @@ "name": "drag-ruler", "title": "Drag Ruler", "description": "When dragging a token displays a ruler showing how far you've moved that token.", - "version": "1.12.2", + "version": "1.12.3", "minimumCoreVersion" : "9.245", "compatibleCoreVersion" : "9", "authors": [ @@ -65,7 +65,7 @@ ], "socket": true, "url": "https://github.com/manuelVo/foundryvtt-drag-ruler", - "download": "https://github.com/manuelVo/foundryvtt-drag-ruler/releases/download/v1.12.2/drag-ruler-1.12.2.zip", + "download": "https://github.com/manuelVo/foundryvtt-drag-ruler/releases/download/v1.12.3/drag-ruler-1.12.3.zip", "manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/master/module.json", "readme": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/README.md", "changelog": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/CHANGELOG.md", diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 2734585..ac1d6e9 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -26,7 +26,7 @@ dependencies = [ [[package]] name = "gridless-pathfinding" -version = "1.12.2" +version = "1.12.3" dependencies = [ "console_error_panic_hook", "js-sys", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 88914a5..cd0c9dd 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gridless-pathfinding" -version = "1.12.2" +version = "1.12.3" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html