Extend ActiveEffect.changes

How do I extend the ActiveEffect changes array. I want to include additional details for my systems AE changes. I updated the UI to allow editing, but they don't save into database, likely because of a schema issue. My defineSchema knowledge is pretty much non-existent, so some pointers there may be helpful. Where I'm going with this: I currently have several AE to increase BODY (for example) that expires after 12 seconds. A second increase before the 12 seconds could be a separate AE. But there are crazy people who will increase their BODY every second, making for lots of AEs, which eventually make the token hard to see. I'm trying to consolidate all the increases into a single AE, then expire each change as appropriate. This means I need to store some extra info in the change array.
No description
58 Replies
Ethaks
Ethaks3d ago
You cannot store any data within core Foundry's changes since the schema is enforced not only client-side, but also on the server. You would have to either track the added data in a separate system/flags object, or replace core's changes completely and e.g. replace the core data with your own system's (whose schema you are in control of) during preparation.
Aaron
AaronOP3d ago
OK, nice to know what road NOT to travel. I tried adding an array to flags to augment changes, merge them in the UI. In the UI I "flags.changes.0.seconds"
No description
Aaron
AaronOP3d ago
the foundry changes is treated like an array, which is great when making updates/changes. The flags.changes is treated like a json object with numeric keys. Is there a way to make flags.changes treated like an array? If not I can kluge something together.
Ethaks
Ethaks3d ago
Small note: that flag path is missing the namespace, i.e. it should be something like flags.my-system.changes Or, since you're the system author, it could use system.changes or something like that if you want to use that instead of flags.
Aaron
AaronOP3d ago
Nice catch on the namespace.
Ethaks
Ethaks3d ago
Re: array, you'd have to adjust the format yourself for flags, or use a schema when using system.
Aaron
AaronOP3d ago
I didn't think system existed on an active effect.
Ethaks
Ethaks3d ago
Likely depends on the Foundry version in use since some documents gained their type data over time.
Aaron
AaronOP3d ago
it does exist in v12, surprising. should I use system instead of flags? Will it be v13 compatible?
Ethaks
Ethaks3d ago
For flags, assuming you're extending the AE config, you could override e.g. _getSubmitData to transform the object into an array.
Aaron
AaronOP3d ago
that was my plan, nice confirmation I was on the track there.
Ethaks
Ethaks3d ago
Depends on your specifics. Implementing a TypeDataModel for AE's base type and handling such things in can be quite a nice approach. Anything that has a system field will continue to have it, and the schema application should remain just as well.
Aaron
AaronOP3d ago
I'm going to explore the TypeDataModel + system approach and see if I can get that to work. Thanks for your words of wisdom.
Leo The League Lion
@Aaron gave :vote: LeaguePoints™ to @Ethaks (#7 • 395)
Ethaks
Ethaks3d ago
Going that route would likely also remove the need for form handling, since the schema should press the data into the provided shape. When going the merging route of maintaining a parallel set of values belonging to changes, you'll also have to handle core changes being removed.
Aaron
AaronOP3d ago
Which I was going to handle in _getSubmitData... So the system route doesn't really "fix" the need for that.
Ethaks
Ethaks3d ago
Hmm, not sure getSubmitData helps with that tbh. Oh, and whatever you do in your AE sheet: it'll break with v13.
Aaron
AaronOP3d ago
Ya, I basically copied active-effect-config.html and made my own .hbs file. So if v13 changes the UI (app2?) I'll have to rework my hbs file.
Ethaks
Ethaks3d ago
Yeah, v13 already has the AppV2 AE config
Aaron
AaronOP3d ago
My coding parter has poked around v13, I've pretty much ignored it so far.
Ethaks
Ethaks3d ago
Ordinarily I'd recommend overriding onDeleteChange, but since that's private you'll probably have to change the action callback to your own implementation that also deletes the matching value from your extra change data. So v13 uses this, which you cannot touch in any way whatsoever
/**
* Delete a change from the effect's changes array.
* @this {ActiveEffectConfig}
* @type {ApplicationClickAction}
*/
static async #onDeleteChange(event) {
const row = event.target.closest("li");
const index = Number(row.dataset.index) || 0;
const changes = foundry.utils.deepClone(this.document._source.changes);
changes.splice(index, 1);
return this.submit({updateData: {changes}});
}
/**
* Delete a change from the effect's changes array.
* @this {ActiveEffectConfig}
* @type {ApplicationClickAction}
*/
static async #onDeleteChange(event) {
const row = event.target.closest("li");
const index = Number(row.dataset.index) || 0;
const changes = foundry.utils.deepClone(this.document._source.changes);
changes.splice(index, 1);
return this.submit({updateData: {changes}});
}
But you could change
actions: {
deleteChange: MyAEConfig._onDeleteChange
}
actions: {
deleteChange: MyAEConfig._onDeleteChange
}
Aaron
AaronOP3d ago
That is COMPLETELY different than in v12, where they delete the html.
Ethaks
Ethaks3d ago
And then copy paste above method while also cloning your extra data, removing the index value there as well, and add that to the update call Welcome to AppV2, where everything's new and shiny. Or at least different.
Aaron
AaronOP3d ago
Ugg, so now I'm thinking about punting on this effort, delaying until v13.
Ethaks
Ethaks3d ago
That's a very valid consideration 😅
Aaron
AaronOP3d ago
I don't mind a few tweaks for v13, but not looking to overhaul something I just built.
Ethaks
Ethaks3d ago
Yep, anything related to core Foundry apps belongs into that category.
Aaron
AaronOP3d ago
I have a custom combat tracker that may be a challenge in v13. Is that one of the appv2 expected in v13? Shame they use private methods I can't override.
Ethaks
Ethaks3d ago
IIRC Foundry aims to replace every app with v2 in v13
Aaron
AaronOP3d ago
OK, I thought I read they intended to, but may not get to them all before v13 releases.
Ethaks
Ethaks3d ago
GitHub
All core Application instances migrated to ApplicationV2 and ThemeV...
Requirements Create a tracking spreadsheet for every application in our codebase where we can survey its migration status. These become individual issues which we can track as a sub-project or proj...
Aaron
AaronOP3d ago
Combat tracker is in that list, looks like it's mostly done already. I'm going to toy around with system and schema, if nothing else to "learn" how the schema stuff works.
Ethaks
Ethaks3d ago
I will say that I'm generally a fan of DataModels, and I overall like AppV2 and consider quite a few of its parts a step up.
Ethaks
Ethaks3d ago
Foundry VTT Community Wiki
Data Model
The abstract base class which defines the data schema contained within a Document.
Foundry VTT Community Wiki
ApplicationV2
The Application class is responsible for rendering an HTMLElement into the Foundry Virtual Tabletop user interface.
Aaron
AaronOP3d ago
Made schema, where AE has a system.changes array. The _updateObject formData returns an array for changes, but a json object for system.changes. How do I make foundry treat system.changes as an array so the native update routies work properly?
No description
Aaron
AaronOP3d ago
No description
Aaron
AaronOP3d ago
Ah, _getSubmitData cheats a bit by coercing changes into an array. I need to do the same with system.changes.
Ethaks
Ethaks3d ago
Your system there is not an instance of a data model, so there's no schema applied
Aaron
AaronOP3d ago
Did I miss an instancing step?
export class HeroSystem6eActorActiveEffectsSystemData extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
changes: new fields.ArrayField(
new fields.SchemaField({
seconds: new fields.NumberField({ integer: true }),
activePoints: new fields.NumberField({ integer: false }),
source: new fields.StringField(),
startTime: new fields.NumberField({ integer: true }),
}),
),
};
}
}
export class HeroSystem6eActorActiveEffectsSystemData extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
changes: new fields.ArrayField(
new fields.SchemaField({
seconds: new fields.NumberField({ integer: true }),
activePoints: new fields.NumberField({ integer: false }),
source: new fields.StringField(),
startTime: new fields.NumberField({ integer: true }),
}),
),
};
}
}
Object.assign(CONFIG.ActiveEffect.dataModels, {
// REF: https://foundryvtt.wiki/en/development/api/DataModel
base: HeroSystem6eActorActiveEffectsSystemData,
});
Object.assign(CONFIG.ActiveEffect.dataModels, {
// REF: https://foundryvtt.wiki/en/development/api/DataModel
base: HeroSystem6eActorActiveEffectsSystemData,
});
Ethaks
Ethaks3d ago
What exactly is shown in the first screenshot? Raw data or document?
Aaron
AaronOP3d ago
debugger stuff. The one with red circles is the formData. I did get it working:
_getSubmitData(updateData = {}) {
const fd = new FormDataExtended(this.form, { editors: this.editors, disabled: true });
let data = foundry.utils.expandObject(fd.object);
if (updateData) foundry.utils.mergeObject(data, updateData);
data.changes = Array.from(Object.values(data.changes || {}));
data.statuses ??= [];

data.system.changes = Array.from(Object.values(data.system.changes || {}));
return data;
}
_getSubmitData(updateData = {}) {
const fd = new FormDataExtended(this.form, { editors: this.editors, disabled: true });
let data = foundry.utils.expandObject(fd.object);
if (updateData) foundry.utils.mergeObject(data, updateData);
data.changes = Array.from(Object.values(data.changes || {}));
data.statuses ??= [];

data.system.changes = Array.from(Object.values(data.system.changes || {}));
return data;
}
Foundry cheated by coercing data.changes into an array. I just did the same for data.system.changes. Looks like a dataModel isn't really neceessary in this specific case.
Ethaks
Ethaks3d ago
The DataModel should make that very form handling obsolete by transforming update data So check the AE's system. If it's an instance of your data model, the schema is applied. Which should cast its contents into the array shape even if the form data does not contain it like that.
Aaron
AaronOP3d ago
Appears to be an instance
No description
Aaron
AaronOP3d ago
No description
Ethaks
Ethaks3d ago
Then run an update where your changes are in the object form instead of an array
Aaron
AaronOP3d ago
taking out my "custom" fix and see if it coerces like you said is should.
Ethaks
Ethaks3d ago
The schema should cast them into the desired array
Aaron
AaronOP3d ago
Hey it works, mostly. If I have 4 changes and delete one in the middle I get
No description
Aaron
AaronOP3d ago
the coercion into an array bypasses this issue. If I delete the last change and the array indexes 1-X are sequential, it coerces into an array just fine. BTW: I'm learning alot in this thread. Thanks for your time! Like I said I have it working. I'm willing to try more suggestions to make it work better or more inline with foundry/dev recommendations.
Ethaks
Ethaks3d ago
You're now at the point where you have to adjust your schema. Your changes element SchemaField is required by default, meaning the array cannot be sparse. So you could either make the element not required, allowing an array of [ empty, { … } ] Or you could give the whole thing empty initial values so that the data is there, just, well, empty
Aaron
AaronOP3d ago
Yuck, when I make the ArrayField not required the merge of changes and system.changes no longer is 1-1 and it makes a mess of things.
export class HeroSystem6eActorActiveEffectsSystemData extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
changes: new fields.ArrayField(
new fields.SchemaField(
{
seconds: new fields.NumberField({ integer: true }),
activePoints: new fields.NumberField({ integer: false }),
source: new fields.StringField(),
startTime: new fields.NumberField({ integer: true }),
},
{ required: false },
),
),
};
}
}
export class HeroSystem6eActorActiveEffectsSystemData extends foundry.abstract.TypeDataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
changes: new fields.ArrayField(
new fields.SchemaField(
{
seconds: new fields.NumberField({ integer: true }),
activePoints: new fields.NumberField({ integer: false }),
source: new fields.StringField(),
startTime: new fields.NumberField({ integer: true }),
},
{ required: false },
),
),
};
}
}
I supposed I can perform a filter during the merge...
Ethaks
Ethaks3d ago
How are you merging the sparse array?
Aaron
AaronOP3d ago
No description
Ethaks
Ethaks3d ago
(I would probably recommend the empty data route btw, it feels better than a sparse array)
Aaron
AaronOP3d ago
async getData() {
const context = await super.getData();
for (let i = 0; i < context.data.changes.length; i++) {
context.data.changes[i] = { ...context.data.changes[i], ...context.data.system.changes?.[i] };
}
async getData() {
const context = await super.getData();
for (let i = 0; i < context.data.changes.length; i++) {
context.data.changes[i] = { ...context.data.changes[i], ...context.data.system.changes?.[i] };
}
Ethaks
Ethaks3d ago
It could also be interesting to merge the system changes into the changes array at data preparation That'd depend on how core uses them though
Aaron
AaronOP3d ago
I sorta need a 1-1 match so I can do some backend calculations & change.value fading (custom system rules). Given there is no unique key, I'm left with an index to do the matching. There are a couple of data prep functions, which one should I look at? Is my use of getData to merge not ideal? Not sure I understand your "empty" ArrayField comment. I found the {required: false}, which allows for spare arrays. Is there some sort of {empty: ?} option for ArrayFields that removes the spares property?
Ethaks
Ethaks3d ago
I'd have to try this out in v12 since the add/delete handling differs there. Just spitballing though: giving the elements and empty object as initial could work to avoid a sparse array, meaning every value in your changes is an object with your properties. For the data prep, base should be good enough. Would have to check a) what Foundry does for its own prep, and b) whether the derived data is used for effect application etc. as well.
Want results from more Discord servers?
Add your server