[Fix] Player Created Regions (#1855)

* Fixed so that creating regions without behaviors work for players. Fixed so that creating regions with behaviors works via GmEmit for players

* Updated previous uses of emitAsGM to emitGMUpdate

* Fixed linting

* Update module/documents/chatMessage.mjs

Co-authored-by: Carlos Fernandez <CarlosFdez@users.noreply.github.com>

---------

Co-authored-by: Carlos Fernandez <CarlosFdez@users.noreply.github.com>
This commit is contained in:
WBHarry 2026-05-02 22:34:53 +02:00 committed by GitHub
parent 54d1b2bdc0
commit edbf5aa55f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 108 additions and 69 deletions

View file

@ -3220,7 +3220,8 @@
"domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used", "domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used",
"knowTheTide": "Know The Tide gained a token", "knowTheTide": "Know The Tide gained a token",
"lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}", "lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}",
"noTokenTargeted": "No token is targeted" "noTokenTargeted": "No token is targeted",
"behaviorRegionRequiresGM": "Creating a Region with an attached Behavior requires an online GM"
}, },
"Progress": { "Progress": {
"migrationLabel": "Performing system migration. Please wait and do not close Foundry." "migrationLabel": "Performing system migration. Please wait and do not close Foundry."

View file

@ -1,5 +1,5 @@
import { ResourceUpdateMap } from '../../data/action/baseAction.mjs'; import { ResourceUpdateMap } from '../../data/action/baseAction.mjs';
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
import Party from '../sheets/actors/party.mjs'; import Party from '../sheets/actors/party.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -242,7 +242,7 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat
}); });
}; };
await emitAsGM( await emitGMUpdate(
GMUpdateEvent.UpdateDocument, GMUpdateEvent.UpdateDocument,
gmUpdate, gmUpdate,
update, update,

View file

@ -1,6 +1,6 @@
import { MemberData } from '../../data/tagTeamData.mjs'; import { MemberData } from '../../data/tagTeamData.mjs';
import { getCritDamageBonus } from '../../helpers/utils.mjs'; import { getCritDamageBonus } from '../../helpers/utils.mjs';
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
import Party from '../sheets/actors/party.mjs'; import Party from '../sheets/actors/party.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -259,7 +259,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio
}); });
}; };
await emitAsGM( await emitGMUpdate(
GMUpdateEvent.UpdateDocument, GMUpdateEvent.UpdateDocument,
gmUpdate, gmUpdate,
update, update,

View file

@ -1,6 +1,6 @@
import { DhCountdown } from '../../data/countdowns.mjs'; import { DhCountdown } from '../../data/countdowns.mjs';
import { waitForDiceSoNice } from '../../helpers/utils.mjs'; import { waitForDiceSoNice } from '../../helpers/utils.mjs';
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -114,7 +114,7 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio
} }
await this.data.updateSource(update); await this.data.updateSource(update);
await emitAsGM(GMUpdateEvent.UpdateCountdowns, this.gmSetSetting.bind(this.data), this.data, null, { await emitGMUpdate(GMUpdateEvent.UpdateCountdowns, this.gmSetSetting.bind(this.data), this.data, null, {
refreshType: RefreshType.Countdown refreshType: RefreshType.Countdown
}); });

View file

@ -1,5 +1,5 @@
import { waitForDiceSoNice } from '../../helpers/utils.mjs'; import { waitForDiceSoNice } from '../../helpers/utils.mjs';
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -204,7 +204,7 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
start: newMax start: newMax
} }
}); });
await emitAsGM(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, { await emitGMUpdate(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, {
refreshType: RefreshType.Countdown refreshType: RefreshType.Countdown
}); });
} }
@ -218,7 +218,7 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
? Math.min(countdown.progress.current + 1, countdown.progress.start) ? Math.min(countdown.progress.current + 1, countdown.progress.start)
: Math.max(countdown.progress.current - 1, 0); : Math.max(countdown.progress.current - 1, 0);
await settings.updateSource({ [`countdowns.${target.id}.progress.current`]: newCurrent }); await settings.updateSource({ [`countdowns.${target.id}.progress.current`]: newCurrent });
await emitAsGM(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, { await emitGMUpdate(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, {
refreshType: RefreshType.Countdown refreshType: RefreshType.Countdown
}); });
} }
@ -277,7 +277,7 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application
return acc; return acc;
}, {}) }, {})
}; };
await emitAsGM(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, { await emitGMUpdate(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, {
refreshType: RefreshType.Countdown refreshType: RefreshType.Countdown
}); });
} }

View file

@ -1,4 +1,4 @@
import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs'; import { emitGMUpdate, GMUpdateEvent } from '../../systemRegistration/socket.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
@ -104,7 +104,7 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV
} }
async updateFear(value) { async updateFear(value) {
return emitAsGM( return emitGMUpdate(
GMUpdateEvent.UpdateFear, GMUpdateEvent.UpdateFear,
game.settings.set.bind(game.settings, CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear), game.settings.set.bind(game.settings, CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear),
value value

View file

@ -1,4 +1,4 @@
import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs'; import { emitGMUpdate, GMUpdateEvent } from '../../systemRegistration/socket.mjs';
export default class DhSceneNavigation extends foundry.applications.ui.SceneNavigation { export default class DhSceneNavigation extends foundry.applications.ui.SceneNavigation {
/** @inheritdoc */ /** @inheritdoc */
@ -68,7 +68,7 @@ export default class DhSceneNavigation extends foundry.applications.ui.SceneNavi
1 1
)[0]; )[0];
newEnvironments.unshift(newFirst); newEnvironments.unshift(newFirst);
emitAsGM( emitGMUpdate(
GMUpdateEvent.UpdateDocument, GMUpdateEvent.UpdateDocument,
scene.update.bind(scene), scene.update.bind(scene),
{ 'flags.daggerheart.sceneEnvironments': newEnvironments }, { 'flags.daggerheart.sceneEnvironments': newEnvironments },

View file

@ -57,14 +57,14 @@ export default class DhRegionLayer extends foundry.canvas.layers.RegionLayer {
} }
async placeRegion(data, options = {}) { async placeRegion(data, options = {}) {
const preConfirm = ({ _event, document, _create, _options }) => { const preConfirm = data => {
const shape = document.shapes[0]; const shape = data.document.shapes[0];
const isEmanation = shape.type === 'emanation'; const isEmanation = shape.type === 'emanation';
if (isEmanation) { if (isEmanation) {
const token = this.#findTokenInBounds(shape.base.origin); const token = this.#findTokenInBounds(shape.base.origin);
if (!token) return options.preConfirm?.() ?? true; if (!token) return options.preConfirm?.(data) ?? true;
const shapeData = shape.toObject(); const shapeData = shape.toObject();
document.updateSource({ data.document.updateSource({
shapes: [ shapes: [
{ {
...shapeData, ...shapeData,
@ -80,10 +80,10 @@ export default class DhRegionLayer extends foundry.canvas.layers.RegionLayer {
}); });
} }
return options?.preConfirm?.() ?? true; return options?.preConfirm?.(data) ?? true;
}; };
super.placeRegion(data, { ...options, preConfirm }); return await super.placeRegion(data, { ...options, preConfirm });
} }
/** Searches for token at origin point, returning null if there are no tokens or multiple overlapping tokens */ /** Searches for token at origin point, returning null if there are no tokens or multiple overlapping tokens */

View file

@ -1,4 +1,4 @@
import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../../systemRegistration/socket.mjs'; import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../../systemRegistration/socket.mjs';
const fields = foundry.data.fields; const fields = foundry.data.fields;
@ -78,7 +78,7 @@ export default class CountdownField extends fields.ArrayField {
); );
} }
await emitAsGM( await emitGMUpdate(
GMUpdateEvent.UpdateCountdowns, GMUpdateEvent.UpdateCountdowns,
async () => { async () => {
const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns);

View file

@ -1,4 +1,4 @@
import { emitAsGM, GMUpdateEvent } from '../../../systemRegistration/socket.mjs'; import { emitGMUpdate, GMUpdateEvent } from '../../../systemRegistration/socket.mjs';
const fields = foundry.data.fields; const fields = foundry.data.fields;
@ -34,7 +34,7 @@ export default class EffectsField extends fields.ArrayField {
} }
if (EffectsField.getAutomation() || force) { if (EffectsField.getAutomation() || force) {
targets ??= (message.system?.targets ?? config.targets).filter(t => !config.hasRoll || t.hit); targets ??= (message.system?.targets ?? config.targets).filter(t => !config.hasRoll || t.hit);
await emitAsGM(GMUpdateEvent.UpdateEffect, EffectsField.applyEffects.bind(this), targets, this.uuid); await emitGMUpdate(GMUpdateEvent.UpdateEffect, EffectsField.applyEffects.bind(this), targets, this.uuid);
// EffectsField.applyEffects.call(this, config.targets.filter(t => !config.hasRoll || t.hit)); // EffectsField.applyEffects.call(this, config.targets.filter(t => !config.hasRoll || t.hit));
} }
} }

View file

@ -1,4 +1,4 @@
import { emitAsGM, GMUpdateEvent } from '../systemRegistration/socket.mjs'; import { emitGMUpdate, GMUpdateEvent } from '../systemRegistration/socket.mjs';
import { LevelOptionType } from '../data/levelTier.mjs'; import { LevelOptionType } from '../data/levelTier.mjs';
import DHFeature from '../data/item/feature.mjs'; import DHFeature from '../data/item/feature.mjs';
import { createScrollText, damageKeyToNumber, getDamageKey, createShallowProxy } from '../helpers/utils.mjs'; import { createScrollText, damageKeyToNumber, getDamageKey, createShallowProxy } from '../helpers/utils.mjs';
@ -827,7 +827,7 @@ export default class DhpActor extends Actor {
const u = updates[key]; const u = updates[key];
if (key === 'items') { if (key === 'items') {
Object.values(u).forEach(async item => { Object.values(u).forEach(async item => {
await emitAsGM( await emitGMUpdate(
GMUpdateEvent.UpdateDocument, GMUpdateEvent.UpdateDocument,
item.target.update.bind(item.target), item.target.update.bind(item.target),
item.resources, item.resources,
@ -836,7 +836,7 @@ export default class DhpActor extends Actor {
}); });
} else { } else {
if (Object.keys(u.resources).length > 0) { if (Object.keys(u.resources).length > 0) {
await emitAsGM( await emitGMUpdate(
GMUpdateEvent.UpdateDocument, GMUpdateEvent.UpdateDocument,
u.target.update.bind(u.target), u.target.update.bind(u.target),
u.resources, u.resources,

View file

@ -1,4 +1,4 @@
import { emitAsGM, GMUpdateEvent } from '../systemRegistration/socket.mjs'; import { emitGMUpdate, emitGMCreate, GMUpdateEvent } from '../systemRegistration/socket.mjs';
export default class DhpChatMessage extends foundry.documents.ChatMessage { export default class DhpChatMessage extends foundry.documents.ChatMessage {
targetHook = null; targetHook = null;
@ -214,7 +214,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
const action = this.system.action; const action = this.system.action;
if (!action || !action?.hasSave) return; if (!action || !action?.hasSave) return;
game.system.api.fields.ActionFields.SaveField.rollSave.call(action, token.actor, event).then(result => game.system.api.fields.ActionFields.SaveField.rollSave.call(action, token.actor, event).then(result =>
emitAsGM( emitGMUpdate(
GMUpdateEvent.UpdateSaveMessage, GMUpdateEvent.UpdateSaveMessage,
game.system.api.fields.ActionFields.SaveField.updateSaveMessage.bind( game.system.api.fields.ActionFields.SaveField.updateSaveMessage.bind(
action, action,
@ -259,12 +259,17 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
const { shape: type, size: range } = selectedArea; const { shape: type, size: range } = selectedArea;
const shapeData = CONFIG.Canvas.layers.regions.layerClass.getTemplateShape({ type, range }); const shapeData = CONFIG.Canvas.layers.regions.layerClass.getTemplateShape({ type, range });
await canvas.regions.placeRegion( const scene = game.scenes.get(game.user.viewedScene);
{ const level = scene.levels.find(x => x.isView);
const regionData = {
name: selectedArea.name, name: selectedArea.name,
levels: level ? [level.id] : [],
shapes: [shapeData], shapes: [shapeData],
restriction: { enabled: false, type: 'move', priority: 0 }, restriction: { enabled: false, type: 'move', priority: 0 },
behaviors: [ behaviors:
effects.length > 0
? [
{ {
name: game.i18n.localize('TYPES.RegionBehavior.applyActiveEffect'), name: game.i18n.localize('TYPES.RegionBehavior.applyActiveEffect'),
type: 'applyActiveEffect', type: 'applyActiveEffect',
@ -272,14 +277,29 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
effects: effects effects: effects
} }
} }
], ]
: [],
displayMeasurements: true, displayMeasurements: true,
locked: false, locked: false,
ownership: { default: CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE }, ownership: { default: CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE },
visibility: CONST.REGION_VISIBILITY.ALWAYS visibility: CONST.REGION_VISIBILITY.ALWAYS
}, };
{ create: true } const placeRegion = data => {
canvas.regions.placeRegion(data, { create: true });
};
// Regions with effects must be placed by the GM
if (effects.length > 0 && !game.user.isGM) {
if (!game.users.activeGM)
return ui.notifications.error(
game.i18n.localize('DAGGERHEART.UI.Notifications.behaviorRegionRequiresGM')
); );
const region = await canvas.regions.placeRegion(regionData, { create: false });
emitGMCreate('Region', placeRegion, region, scene.id);
} else {
placeRegion(regionData);
}
}; };
if (this.system.action.areas.length === 1) createArea(this.system.action.areas[0]); if (this.system.action.areas.length === 1) createArea(this.system.action.areas[0]);

View file

@ -6,6 +6,9 @@ export function handleSocketEvent({ action = null, data = {} } = {}) {
case socketEvent.GMUpdate: case socketEvent.GMUpdate:
Hooks.callAll(socketEvent.GMUpdate, data); Hooks.callAll(socketEvent.GMUpdate, data);
break; break;
case socketEvent.GMCreate:
Hooks.callAll(socketEvent.GMCreate, data);
break;
case socketEvent.DhpFearUpdate: case socketEvent.DhpFearUpdate:
Hooks.callAll(socketEvent.DhpFearUpdate); Hooks.callAll(socketEvent.DhpFearUpdate);
break; break;
@ -25,6 +28,7 @@ export function handleSocketEvent({ action = null, data = {} } = {}) {
export const socketEvent = { export const socketEvent = {
GMUpdate: 'DhGMUpdate', GMUpdate: 'DhGMUpdate',
GMCreate: 'DhGMCreate',
Refresh: 'DhRefresh', Refresh: 'DhRefresh',
DhpFearUpdate: 'DhFearUpdate', DhpFearUpdate: 'DhFearUpdate',
DowntimeTrigger: 'DowntimeTrigger', DowntimeTrigger: 'DowntimeTrigger',
@ -56,14 +60,14 @@ export const registerSocketHooks = () => {
const document = data.uuid ? await fromUuid(data.uuid) : null; const document = data.uuid ? await fromUuid(data.uuid) : null;
switch (data.action) { switch (data.action) {
case GMUpdateEvent.UpdateDocument: case GMUpdateEvent.UpdateDocument:
if (document && data.update) await document.update(data.update); if (document && data.data) await document.update(data.data);
break; break;
case GMUpdateEvent.UpdateEffect: case GMUpdateEvent.UpdateEffect:
if (document && data.update) if (document && data.data)
await game.system.api.fields.ActionFields.EffectsField.applyEffects.call(document, data.update); await game.system.api.fields.ActionFields.EffectsField.applyEffects.call(document, data.data);
break; break;
case GMUpdateEvent.UpdateSetting: case GMUpdateEvent.UpdateSetting:
await game.settings.set(CONFIG.DH.id, data.uuid, data.update); await game.settings.set(CONFIG.DH.id, data.uuid, data.data);
break; break;
case GMUpdateEvent.UpdateFear: case GMUpdateEvent.UpdateFear:
await game.settings.set( await game.settings.set(
@ -73,22 +77,22 @@ export const registerSocketHooks = () => {
0, 0,
Math.min( Math.min(
game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxFear, game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxFear,
data.update data.data
) )
) )
); );
break; break;
case GMUpdateEvent.UpdateCountdowns: case GMUpdateEvent.UpdateCountdowns:
await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data.update); await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data.data);
Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown }); Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown });
break; break;
case GMUpdateEvent.UpdateSaveMessage: case GMUpdateEvent.UpdateSaveMessage:
const message = game.messages.get(data.update.message); const message = game.messages.get(data.data.message);
if (!message) return; if (!message) return;
game.system.api.fields.ActionFields.SaveField.updateSaveMessage( game.system.api.fields.ActionFields.SaveField.updateSaveMessage(
data.update.result, data.data.result,
message, message,
data.update.token data.data.token
); );
break; break;
} }
@ -102,6 +106,17 @@ export const registerSocketHooks = () => {
} }
} }
}); });
Hooks.on(socketEvent.GMCreate, async ({ data, documentType, scene }) => {
if (!game.user.isGM) return;
switch (documentType) {
default:
const cls = getDocumentClass(documentType);
cls.create(data, { parent: game.scenes.get(scene) });
break;
}
});
}; };
export const registerUserQueries = () => { export const registerUserQueries = () => {
@ -109,18 +124,21 @@ export const registerUserQueries = () => {
CONFIG.queries.reactionRoll = game.system.api.fields.ActionFields.SaveField.rollSaveQuery; CONFIG.queries.reactionRoll = game.system.api.fields.ActionFields.SaveField.rollSaveQuery;
}; };
export const emitAsGM = async (eventName, callback, update, uuid = null, refresh = null) => { export const emitGMUpdate = async (eventName, callback, update, uuid = null, refresh = null) => {
return await emitAsGM(socketEvent.GMUpdate, { action: eventName, callback, data: update, uuid, refresh });
};
export const emitGMCreate = async (documentType, callback, data, scene) => {
return await emitAsGM(socketEvent.GMCreate, { documentType, callback, data, scene });
};
export const emitAsGM = async (event, data = { callback: () => {}, data: {} }) => {
if (!game.user.isGM) { if (!game.user.isGM) {
return await game.socket.emit(`system.${CONFIG.DH.id}`, { return await game.socket.emit(`system.${CONFIG.DH.id}`, {
action: socketEvent.GMUpdate, action: event,
data: { data: data
action: eventName,
uuid,
update,
refresh
}
}); });
} else return callback(update); } else return data.callback(data.data);
}; };
export const emitAsOwner = (eventName, userId, args) => { export const emitAsOwner = (eventName, userId, args) => {