Skip to content

Commit

Permalink
raidboss: handle player object arrays in output params (#5895)
Browse files Browse the repository at this point in the history
Followup to #5861 and discussions on #5891.

As the comment in the code specifies, if a trigger passes in `players:
[player1, player2, player3]`, and a user specifies `${players.job}`,
then return: `${players[0].job}, ${players[1].job}, ${players[2].job}`.

In general, this means that all array elements must either be simple
strings/numbers or all share the same prop, or there will be errors
below about non-existent properties. In practice, this likely will never
happen.

This will also handle simpler cases like: `output.safeSpots!({ dirs:
[output.front!(), output.back!(), output.left!()] })`

This also allows for `data.party.member(undefined)` to return '???',
just to reduce boilerplate.
  • Loading branch information
quisquous committed Nov 3, 2023
1 parent ebe47e9 commit c3214ca
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 49 deletions.
12 changes: 11 additions & 1 deletion resources/party.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,17 @@ export default class PartyTracker {
return this.idToName_[id];
}

member(name: string): PartyMemberParamObject | undefined {
member(name?: string): PartyMemberParamObject {
// For boilerplate convenience in triggers, handle undefined names.
if (name === undefined) {
const unknown = '???';
return {
name: unknown,
nick: unknown,
toString: () => unknown,
};
}

const partyMember = this.details.find((member) => member.name === name);
let ret: PartyMemberParamObject;
const nick = Util.shortName(name, this.options.PlayerNicks);
Expand Down
3 changes: 2 additions & 1 deletion types/trigger.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ export type OutputStringsParamObject = {
toString: () => string;
};

export type OutputStringsParam = string | number | OutputStringsParamObject;
export type OutputStringsParams = {
[param: string]: string | number | OutputStringsParamObject | undefined;
[param: string]: OutputStringsParam | OutputStringsParam[] | undefined;
};

// TODO: is it awkward to have Outputs the static class and Output the unrelated type?
Expand Down
121 changes: 74 additions & 47 deletions ui/raidboss/popup-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,79 @@ class TriggerOutputProxy {
});
}

evaluateOutputParam(
id: string,
key: string,
val: unknown,
prop?: string,
isNestedArray?: boolean,
): string {
if (typeof val === 'string' || typeof val === 'number')
return val.toString();
if (typeof val !== 'object') {
console.error(`Trigger ${id} has non-string param value ${key}.`);
return this.unknownValue;
}

if (Array.isArray(val)) {
// Don't allow nesting arrays here, e.g. [player1, [player2, player3]].
if (isNestedArray) {
console.error(`Trigger ${id} passed nested arrays to param value ${key}.`);
return this.unknownValue;
}

// If a trigger passes in [player1, player2, player3] as a value,
// and a user specifies `${players.job}`, then return:
// `${players[0].job}, ${players[1].job}, ${players[2].job}`.
// In general, this means that all array elements must either be simple
// strings/numbers or all share the same prop, or there will be errors below
// about non-existent properties. In practice, this likely will never happen.
//
// Also, this assumes that all locales are ok with ", " as a separator.
// This seems to be true in practice.
return val.map((p) => this.evaluateOutputParam(id, key, p, prop, true)).join(', ');
}

// Appease TypeScript, this shouldn't happen.
if (!isObject(val))
return this.unknownValue;

if (prop !== undefined) {
const retVal = val[prop];
if (typeof retVal === 'string' || typeof retVal === 'number')
return retVal.toString();

if (retVal === undefined || retVal === null) {
console.error(
`Trigger ${id} is referencing non-existent object property ${key}.${prop}.`,
);
} else {
console.error(
`Trigger ${id} is referencing object property ${key}.${prop} with incorrect type ${typeof retVal}.`,
);
}
}

// At this point, we're going to try to return a default value if we can,
// either from an error or because `prop` was unspecified.
const toStringFunc = val['toString'];
if (typeof toStringFunc !== 'function') {
console.error(
`Trigger ${id} has non-func ${key}.toString property.`,
);
return this.unknownValue;
}

const toStringVal: unknown = toStringFunc();
if (typeof toStringVal !== 'string' && typeof toStringVal !== 'number') {
console.error(
`Trigger ${id} returned non-string ${typeof toStringVal} from ${key}.toString().`,
);
return this.unknownValue;
}
return toStringVal.toString();
}

getReplacement(
// Can't use optional modifier for this arg since the others aren't optional
template: { [lang: string]: unknown } | string | undefined,
Expand Down Expand Up @@ -400,53 +473,7 @@ class TriggerOutputProxy {

if (key in params) {
const val = params[key];
switch (typeof val) {
case 'string':
return val;
case 'number':
return val.toString();
case 'object': {
if (!isObject(val))
break;

if (prop !== undefined) {
const retVal = val[prop];
if (typeof retVal === 'string' || typeof retVal === 'number')
return retVal.toString();

if (retVal === undefined || retVal === null) {
console.error(
`Trigger ${id} is referencing non-existent object property ${key}.${prop}.`,
);
} else {
console.error(
`Trigger ${id} is referencing object property ${key}.${prop} with incorrect type ${typeof retVal}.`,
);
}
}

// At this point, we're going to try to return a default value if we can,
// either from an error or because `prop` was unspecified.
const toStringFunc = val['toString'];
if (typeof toStringFunc !== 'function') {
console.error(
`Trigger ${id} has non-func ${key}.toString property.`,
);
return this.unknownValue;
}

const toStringVal: unknown = toStringFunc();
if (typeof toStringVal !== 'string') {
console.error(
`Trigger ${id} returned non-string ${typeof toStringVal} from ${key}.toString().`,
);
return this.unknownValue;
}
return toStringVal;
}
}
console.error(`Trigger ${id} has non-string param value ${key}.`);
return this.unknownValue;
return this.evaluateOutputParam(id, key, val, prop);
}
}

Expand Down

0 comments on commit c3214ca

Please sign in to comment.