From cbc2691a0ecc52f96555482472ae13bf0e7bff91 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Thu, 12 Jun 2025 22:04:01 -0600 Subject: [PATCH 1/6] Update privacy detection to default to Self if it's not able to be otherwise determined --- module/utils/privacy.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/module/utils/privacy.mjs b/module/utils/privacy.mjs index f04a885..b437880 100644 --- a/module/utils/privacy.mjs +++ b/module/utils/privacy.mjs @@ -11,6 +11,8 @@ export const PrivacyMode = Object.freeze({ export function determinePrivacyFromRollMode(rollMode) { switch (rollMode) { + case CONST.DICE_ROLL_MODES.PUBLIC: + return PrivacyMode.PUBLIC; case CONST.DICE_ROLL_MODES.BLIND: return PrivacyMode.GM; case CONST.DICE_ROLL_MODES.PRIVATE: @@ -18,7 +20,7 @@ export function determinePrivacyFromRollMode(rollMode) { case CONST.DICE_ROLL_MODES.SELF: return PrivacyMode.SELF; } - return PrivacyMode.PUBLIC; + return PrivacyMode.SELF; }; /** From 8c42f1b240e1c8fc8c2f06f97b8bdba98ab0e8d1 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Fri, 13 Jun 2025 19:27:39 -0600 Subject: [PATCH 2/6] Add a utility to the API for inferring a chat message's roll mode (and update the docs) --- module/api.mjs | 2 ++ module/utils/inferRollMode.mjs | 36 +++++++++++++++++++ .../_source/English_pBOyeBDuTeowuDOE.json | 6 ++-- 3 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 module/utils/inferRollMode.mjs diff --git a/module/api.mjs b/module/api.mjs index 9faffaa..274f255 100644 --- a/module/api.mjs +++ b/module/api.mjs @@ -13,6 +13,7 @@ import { UserFlagDatabase } from "./utils/databases/UserFlag.mjs"; import { barGraphSchema, numberBucketSchema, rowSchema, stringBucketSchema, tableSchema } from "./utils/databases/model.mjs"; import { determinePrivacyFromRollMode, filterPrivateRows, PrivacyMode } from "./utils/privacy.mjs"; import { validateBucketConfig, validateValue } from "./utils/buckets.mjs"; +import { inferRollMode } from "./utils/inferRollMode.mjs"; const { deepFreeze } = foundry.utils; @@ -25,6 +26,7 @@ export const api = deepFreeze({ }, utils: { determinePrivacyFromRollMode, + inferRollMode, filterPrivateRows, validateValue, validateBucketConfig, diff --git a/module/utils/inferRollMode.mjs b/module/utils/inferRollMode.mjs new file mode 100644 index 0000000..4880c21 --- /dev/null +++ b/module/utils/inferRollMode.mjs @@ -0,0 +1,36 @@ +/** + * A helper function to try and infer what roll mode was used when creating a + * chat message in case the roll mode was not provided during the createChatMessage + * hook for whatever reason. + * + * **Disclaimer**: This inference is not totally correct. Particularly when inferring + * a GM's message, as it won't be able to distinguish between a self-roll and a + * private GM roll when it's + * + * @param {ChatMessage} message The ChatMessage document to infer from + * @returns The Foundry-specified roll mode + */ +export function inferRollMode(message) { + const whisperCount = message.whisper.length; + if (whisperCount === 0) { + return CONST.DICE_ROLL_MODES.PUBLIC; + }; + + if (whisperCount === 1 && message.whisper[0] === game.user.id) { + return CONST.DICE_ROLL_MODES.SELF; + }; + + let allGMs = true; + for (const userID of message.whisper) { + const user = game.users.get(userID); + if (!user) { continue }; + allGMs &&= user.isGM; + }; + + if (!allGMs) { + return CONST.DICE_ROLL_MODES.PUBLIC; + }; + return message.blind + ? CONST.DICE_ROLL_MODES.BLIND + : CONST.DICE_ROLL_MODES.PRIVATE; +}; diff --git a/packs/docs/_source/English_pBOyeBDuTeowuDOE.json b/packs/docs/_source/English_pBOyeBDuTeowuDOE.json index 91393f7..f6e18d1 100644 --- a/packs/docs/_source/English_pBOyeBDuTeowuDOE.json +++ b/packs/docs/_source/English_pBOyeBDuTeowuDOE.json @@ -250,7 +250,7 @@ "image": {}, "text": { "format": 1, - "content": "

The module provides a multitude of utility functions through it's API for usage however desired. This will go over them and describe their purpose.

filterPrivateRows

This method is intended to take @UUID[Compendium.stat-tracker.docs.JournalEntry.pBOyeBDuTeowuDOE.JournalEntryPage.S7Z6mZ0JablJVQJu]{rows} provided by the database and filter out any that the user would not be able to see normally. This is usually called by the database adapters so there's unlikely to be any reason to use it externally.

Available under <api>.utils.filterPrivateRows.

validateValue

Available under <api>.utils.validateValue.

validateBucketConfig

Available under <api>.utils.validateBucketConfig.

" + "content": "

The module provides a multitude of utility functions through it's API for usage however desired. This will go over them and describe their purpose.

filterPrivateRows

This method is intended to take @UUID[Compendium.stat-tracker.docs.JournalEntry.pBOyeBDuTeowuDOE.JournalEntryPage.S7Z6mZ0JablJVQJu]{rows} provided by the database and filter out any that the user would not be able to see normally. This is usually called by the database adapters so there's unlikely to be any reason to use it externally.

Available under <api>.utils.filterPrivateRows.

inferRollMode

This utility is intended to try and determine what roll mode was used to create a chat message. The inference is not entirely accurate because it struggles to differentiate between a GM rolling with a Private GM Roll and a Self Roll when there is only one GM present in the world.

Available under <api>.utils.inferRollMode

validateValue

Available under <api>.utils.validateValue.

validateBucketConfig

Available under <api>.utils.validateBucketConfig.

" }, "video": { "controls": true, @@ -266,11 +266,11 @@ "compendiumSource": null, "duplicateSource": null, "exportSource": null, - "coreVersion": "13.344", + "coreVersion": "13.345", "systemId": "empty-system", "systemVersion": "0.0.0", "createdTime": 1748330904988, - "modifiedTime": 1748394635911, + "modifiedTime": 1749864406851, "lastModifiedBy": "t2sWGWEYSMFrfBu3" }, "_key": "!journal.pages!pBOyeBDuTeowuDOE.TQzWrVTEz4oQhLPD" From 8905cb05bc365397cfde2c22dbad20ecce474b04 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Fri, 13 Jun 2025 19:28:08 -0600 Subject: [PATCH 3/6] Update the message listening to use createChatMessage instead of preCreateChatMessage --- module/hooks/createChatMessage.mjs | 41 +++++++++++++++++++++++++++ module/hooks/preCreateChatMessage.mjs | 30 -------------------- module/main.mjs | 2 +- 3 files changed, 42 insertions(+), 31 deletions(-) create mode 100644 module/hooks/createChatMessage.mjs delete mode 100644 module/hooks/preCreateChatMessage.mjs diff --git a/module/hooks/createChatMessage.mjs b/module/hooks/createChatMessage.mjs new file mode 100644 index 0000000..12d1bdf --- /dev/null +++ b/module/hooks/createChatMessage.mjs @@ -0,0 +1,41 @@ +import { determinePrivacyFromRollMode } from "../utils/privacy.mjs"; +import { inferRollMode } from "../utils/inferRollMode.mjs"; + +Hooks.on(`createChatMessage`, (message, options, author) => { + console.log({ message, options, author}); + const isSelf = author === game.user.id; + const isNew = options.action === `create`; + const hasRolls = message.rolls?.length > 0; + const autoTracking = game.settings.get(__ID__, `autoTrackRolls`); + if (!isSelf || !isNew || !hasRolls || !autoTracking) { return }; + + /** An object of dice denomination to database rows */ + const rows = {}; + + const privacy = determinePrivacyFromRollMode(options.rollMode ?? inferRollMode(message)); + + /* + Goes through all of the dice within the message and grabs their result in order + to batch-save them all to the database handler. + */ + for (const roll of message.rolls) { + for (const die of roll.dice) { + const size = die.denomination; + rows[size] ??= []; + for (const result of die.results) { + rows[size].push({ privacy, value: result.result }); + }; + }; + }; + + // save all the rows, then rerender once we're properly done + for (const denomination in rows) { + CONFIG.stats.db.createRows( + `Dice/${denomination}`, + author, + rows[denomination], + { rerender: false }, + ); + }; + CONFIG.stats.db.render({ userUpdated: author }); +}); diff --git a/module/hooks/preCreateChatMessage.mjs b/module/hooks/preCreateChatMessage.mjs deleted file mode 100644 index 05a4d41..0000000 --- a/module/hooks/preCreateChatMessage.mjs +++ /dev/null @@ -1,30 +0,0 @@ -import { determinePrivacyFromRollMode } from "../utils/privacy.mjs"; - -Hooks.on(`preCreateChatMessage`, (_message, context, options, author) => { - const isNew = options.action === `create`; - const hasRolls = context.rolls?.length > 0; - const autoTracking = game.settings.get(__ID__, `autoTrackRolls`); - if (!isNew || !hasRolls || !autoTracking) { return }; - - /** An object of dice denomination to rows to add */ - const rows = {}; - - const privacy = determinePrivacyFromRollMode(options.rollMode); - for (const roll of context.rolls) { - for (const die of roll.dice) { - const size = die.denomination; - rows[size] ??= []; - for (const result of die.results) { - rows[size].push({ privacy, value: result.result }); - }; - }; - }; - - for (const denomination in rows) { - CONFIG.stats.db.createRows( - `Dice/${denomination}`, - author, - rows[denomination], - ); - }; -}); diff --git a/module/main.mjs b/module/main.mjs index 12d5422..4a90d3f 100644 --- a/module/main.mjs +++ b/module/main.mjs @@ -5,7 +5,7 @@ import "./hooks/init.mjs"; import "./hooks/ready.mjs"; // Document Hooks -import "./hooks/preCreateChatMessage.mjs"; +import "./hooks/createChatMessage.mjs"; // Dev Only imports if (import.meta.env.DEV) { From 1a8fcf04ab8838d59ed97d6a0ff0d3c01ae73216 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Sun, 22 Jun 2025 10:30:23 -0600 Subject: [PATCH 4/6] Remove stray log --- module/hooks/createChatMessage.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/module/hooks/createChatMessage.mjs b/module/hooks/createChatMessage.mjs index 12d1bdf..e686c70 100644 --- a/module/hooks/createChatMessage.mjs +++ b/module/hooks/createChatMessage.mjs @@ -2,7 +2,6 @@ import { determinePrivacyFromRollMode } from "../utils/privacy.mjs"; import { inferRollMode } from "../utils/inferRollMode.mjs"; Hooks.on(`createChatMessage`, (message, options, author) => { - console.log({ message, options, author}); const isSelf = author === game.user.id; const isNew = options.action === `create`; const hasRolls = message.rolls?.length > 0; From e84e921bec8dfa97cce736ed24780b4f0b03dcae Mon Sep 17 00:00:00 2001 From: Eldritch-Oliver Date: Sun, 28 Sep 2025 00:45:48 -0600 Subject: [PATCH 5/6] Add scripts and infra required to get Foundry intellisense working --- .env.template | 2 ++ .gitignore | 2 ++ augments.d.ts | 14 ++++++++++++ jsconfig.json | 16 ++++++++++++-- scripts/linkFoundry.mjs | 47 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 .env.template create mode 100644 augments.d.ts create mode 100644 scripts/linkFoundry.mjs diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..180dbd6 --- /dev/null +++ b/.env.template @@ -0,0 +1,2 @@ +# The absolute path to the Foundry installation to create symlinks to +FOUNDRY_ROOT="" diff --git a/.gitignore b/.gitignore index 8878028..ff8974d 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ lerna-debug.log* node_modules /*.dist *.local +.env +/foundry # Editor directories and files .vscode/* diff --git a/augments.d.ts b/augments.d.ts new file mode 100644 index 0000000..a08bb60 --- /dev/null +++ b/augments.d.ts @@ -0,0 +1,14 @@ +declare global { + class Hooks extends foundry.helpers.Hooks {}; + const fromUuid = foundry.utils.fromUuid; +} + +interface Actor { + /** The system-specific data */ + system: any; +}; + +interface Item { + /** The system-specific data */ + system: any; +}; diff --git a/jsconfig.json b/jsconfig.json index 8b97154..27f6e7c 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,10 +1,22 @@ { "compilerOptions": { "module": "ES2020", - "target": "ES2020" + "target": "ES2020", + "types": [ + "./augments.d.ts" + ], + "paths": { + "@client/*": ["./foundry/client/*"], + "@common/*": ["./foundry/common/*"], + } }, "exclude": ["node_modules", "**/node_modules/*"], - "include": ["module/**/*"], + "include": [ + "module/**/*", + "foundry/client/client.mjs", + "foundry/client/global.d.mts", + "foundry/common/primitives/global.d.mts" + ], "typeAcquisition": { "include": ["joi"] } diff --git a/scripts/linkFoundry.mjs b/scripts/linkFoundry.mjs new file mode 100644 index 0000000..1cbb71a --- /dev/null +++ b/scripts/linkFoundry.mjs @@ -0,0 +1,47 @@ +import { existsSync } from "fs"; +import { symlink, unlink } from "fs/promises"; +import { join } from "path"; +import { config } from "dotenv"; + +config({ quiet: true }); + +const root = process.env.FOUNDRY_ROOT; + +// Early exit +if (!root) { + console.error(`Must provide a FOUNDRY_ROOT environment variable`); + process.exit(1); +}; + +// Assert Foundry exists +if (!existsSync(root)) { + console.error(`Foundry root not found.`); + process.exit(1); +}; + +// Removing existing symlink +if (existsSync(`foundry`)) { + console.log(`Attempting to unlink foundry instance`); + try { + await unlink(`foundry`); + } catch { + console.error(`Failed to unlink foundry folder.`); + process.exit(1); + }; +}; + +// Account for if the root is pointing at an Electron install +let targetRoot = root; +if (existsSync(join(root, `resources`, `app`))) { + console.log(`Switching to use the "${root}/resources/app" directory`); + targetRoot = join(root, `resources`, `app`); +}; + +// Create symlink +console.log(`Linking foundry source into folder`) +try { + await symlink(targetRoot, `foundry`); +} catch (e) { + console.error(e); + process.exit(1); +}; From 99559663753ed9d1df5366e4a3e6866670d5b4c3 Mon Sep 17 00:00:00 2001 From: Eldritch-Oliver Date: Sun, 28 Sep 2025 00:49:47 -0600 Subject: [PATCH 6/6] Add required package for the linkFoundry to package.json --- package-lock.json | 55 ++++++++++++++++++++++++++++++++--------------- package.json | 1 + 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7664dfa..6594a26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,10 +13,11 @@ "devDependencies": { "@foundryvtt/foundryvtt-cli": "^1.1.0", "@stylistic/eslint-plugin": "^4.2.0", + "dotenv": "^17.2.2", "eslint": "^9.25.0", "glob": "^11.0.1", "terser": "^5.39.0", - "vite": "^6.3.1" + "vite": "^6.3.4" } }, "node_modules/@esbuild/aix-ppc64": { @@ -1680,6 +1681,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dotenv": { + "version": "17.2.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz", + "integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2031,10 +2045,14 @@ } }, "node_modules/fdir": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", - "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -2952,10 +2970,11 @@ "dev": true }, "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -3374,13 +3393,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz", - "integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==", + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, + "license": "MIT", "dependencies": { - "fdir": "^6.4.3", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -3462,17 +3482,18 @@ } }, "node_modules/vite": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.2.tgz", - "integrity": "sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==", + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", + "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "^0.25.0", - "fdir": "^6.4.3", + "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", - "tinyglobby": "^0.2.12" + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" diff --git a/package.json b/package.json index a88df2c..ad4b381 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "devDependencies": { "@foundryvtt/foundryvtt-cli": "^1.1.0", "@stylistic/eslint-plugin": "^4.2.0", + "dotenv": "^17.2.2", "eslint": "^9.25.0", "glob": "^11.0.1", "terser": "^5.39.0",