Skip to content

Commit 2019c2c

Browse files
committed
v5.8.2
1 parent 6f9722e commit 2019c2c

3 files changed

Lines changed: 48 additions & 15 deletions

File tree

module.json

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"id": "investigation-board",
33
"title": "Investigation Board",
44
"description": "<p>A Foundry VTT module that lets everyone create, edit, and move sticky and photo notes on the scene as collaborative investigation tools.</p>",
5-
"version": "5.8.1",
5+
"version": "5.8.2",
66
"socket": true,
77
"authors": [
88
{
@@ -15,7 +15,9 @@
1515
"minimum": "14",
1616
"verified": "14"
1717
},
18-
"esmodules": ["scripts/main.js"],
18+
"esmodules": [
19+
"scripts/main.js"
20+
],
1921
"styles": [
2022
{
2123
"src": "styles/style.css"
@@ -30,19 +32,23 @@
3032
"ownership": {
3133
"PLAYER": "OBSERVER",
3234
"ASSISTANT": "OWNER"
33-
}
35+
},
36+
"flags": {}
3437
}
3538
],
3639
"packFolders": [
3740
{
3841
"name": "Investigation Board",
3942
"sorting": "m",
4043
"color": "#061f6a",
41-
"packs": ["ib-scenes"]
44+
"packs": [
45+
"ib-scenes"
46+
],
47+
"folders": []
4248
}
4349
],
4450
"url": "https://github.com/mordachai/investigation-board",
45-
"manifest": "https://github.com/mordachai/investigation-board/releases/latest/download/module.json",
46-
"download": "https://github.com/mordachai/investigation-board/releases/latest/download/module.zip",
51+
"manifest": "https://github.com/mordachai/investigation-board/releases/download/v5.8.1/module.json",
52+
"download": "https://github.com/mordachai/investigation-board/releases/download/v5.8.1/module.zip",
4753
"flags": {}
48-
}
54+
}

scripts/canvas/custom-drawing.js

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { MODULE_ID, SOCKET_NAME, VIDEO_EXTENSIONS, DOC_BACKGROUNDS } from "../config.js";
22
import { InvestigationBoardState } from "../state.js";
33
import { collaborativeUpdate, collaborativeDelete, socket, activeGlobalSounds, activeVideoBroadcasts } from "../utils/socket-handler.js";
4-
import { truncateText, resolvePinImage, getAvailablePinFiles, resolveStampImage, getAvailableStampFiles } from "../utils/helpers.js";
4+
import { truncateText, resolvePinImage, getAvailablePinFiles, pickPinFileForDrawing, resolveStampImage, getAvailableStampFiles } from "../utils/helpers.js";
55
import { NotePreviewer } from "../apps/note-previewer.js";
66
import { VideoPlayer } from "../apps/video-player.js";
77
import { drawAllConnectionLines, beginConnectionFrom, resetPinConnectionState } from "./connection-manager.js";
@@ -940,9 +940,10 @@ export class CustomDrawing extends Drawing {
940940
*
941941
* The global "pinColor" setting controls visibility:
942942
* "none" → destroy any existing sprite and bail out.
943-
* "random" → pick a random image from the pin folder on first render,
944-
* persist the choice to the note's flags so every client
945-
* shows the same pin.
943+
* "random" → deterministically pick an image from the pin folder based
944+
* on the drawing ID (same result on every client); the GM
945+
* client persists the choice to the note's flags so it stays
946+
* stable even if the folder contents later change.
946947
*
947948
* The per-note flag `noteData.pinColor` stores the bare filename
948949
* (e.g. "redPin.webp"). resolvePinImage() prepends the configured
@@ -971,9 +972,16 @@ export class CustomDrawing extends Drawing {
971972

972973
let pinFilename = noteData.pinColor;
973974
if (!pinFilename) {
975+
// Deterministic "random": hash the drawing ID into the sorted file list
976+
// so every client resolves the same pin without waiting on a flag write.
974977
const files = await getAvailablePinFiles();
975-
pinFilename = files[Math.floor(Math.random() * files.length)];
976-
await collaborativeUpdate(this.document.id, { [`flags.${MODULE_ID}.pinColor`]: pinFilename });
978+
pinFilename = pickPinFileForDrawing(this.document.id, files);
979+
if (!pinFilename) return;
980+
// Persist from the GM client only — avoids N clients racing to write
981+
// different values through the socket on first render.
982+
if (game.user.isGM) {
983+
await collaborativeUpdate(this.document.id, { [`flags.${MODULE_ID}.pinColor`]: pinFilename });
984+
}
977985
}
978986

979987
const pinImage = resolvePinImage(pinFilename);

scripts/utils/helpers.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,36 @@ export async function getAvailablePinFiles() {
4848
const result = await FilePicker.browse("data", folder);
4949
_pinFilesCache = result.files
5050
.filter(f => /\.(webp|png)$/i.test(f))
51-
.map(f => f.split("/").pop());
51+
.map(f => f.split("/").pop())
52+
.sort();
5253
_pinFolderCache = folder;
5354
} catch {
5455
// No FILES_BROWSE permission or folder doesn't exist — use built-in list
55-
_pinFilesCache = [...PIN_COLORS];
56+
_pinFilesCache = [...PIN_COLORS].sort();
5657
_pinFolderCache = folder;
5758
}
5859

5960
return _pinFilesCache;
6061
}
6162

63+
/**
64+
* Deterministically pick a pin file for a drawing so every client resolves
65+
* the same "random" pin without needing a flag write to land first.
66+
* Hashes the drawing ID into an index over the sorted file list.
67+
*
68+
* @param {string} drawingId
69+
* @param {string[]} files Sorted list of bare filenames
70+
* @returns {string|undefined}
71+
*/
72+
export function pickPinFileForDrawing(drawingId, files) {
73+
if (!files?.length) return undefined;
74+
let hash = 0;
75+
for (let i = 0; i < drawingId.length; i++) {
76+
hash = (hash * 31 + drawingId.charCodeAt(i)) >>> 0;
77+
}
78+
return files[hash % files.length];
79+
}
80+
6281
// ---------------------------------------------------------------------------
6382
// Stamp image helpers
6483
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)