Skip to content

Commit

Permalink
config: add config ui for trigger set options (#5357)
Browse files Browse the repository at this point in the history
This adds a new `config` entry to trigger sets that can add field
entries to the top of the trigger set under the raidboss section in the
config ui. Trigger sets must have an id to have any config loaded,
saved, or displayed.

Triggers can use `data.triggerSetConfig['optionId']` here to access
anything listed in the `config` section by id. The `Data` type for a
trigger set can be extended in a way that will type check ids and add
field names.

To avoid naming collisions, not all trigger set config values are
loaded. Trigger sets get their own config's values loaded implicitly,
but if they need to load some other trigger set's values, they can use
`loadConfigs` and specify other trigger sets by id.

For backwards compat, `data.triggerSetConfig` contains all saved values
(even ones that no longer have a `config` entry). Also, it is possible
to use `loadConfigs` to load trigger set ids that no longer exist.

To also support backwards compat, `ConfigEntry.default` can now be a
function that reads from options. See the e8s/TEA changes for how this
works.
  • Loading branch information
quisquous authored Apr 22, 2023
1 parent eeaabae commit f7d621d
Show file tree
Hide file tree
Showing 12 changed files with 342 additions and 116 deletions.
1 change: 1 addition & 0 deletions eslint/cactbot-triggerset-property-order.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ module.exports = {
const raidbossOrderList = [
'id',
'zoneId',
'config',
'overrideTimelineFile',
'timelineFile',
'timeline',
Expand Down
69 changes: 47 additions & 22 deletions resources/user_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,23 +61,33 @@ export type ConfigEntry = {
html?: LocaleText;
// This must be a valid option even if there is a setterFunc, as `_getOptionLeafHelper`
// for the config ui reads from the SavedConfig directly rather than post-setterFunc.
default: ConfigValue;
default: ConfigValue | ((options: BaseOptions) => ConfigValue);
debug?: boolean;
debugOnly?: boolean;
// For select.
options?: LocaleObject<{ [selectText: string]: string }>;
// An optional function to transform a saved/default value into the final value.
// `value` is the saved/default value. `isDefault` is true if `value` is implicitly the default.
// The data flow here is `default` -> ui -> `setterFunc` -> final data in one direction only.
// `setterFunc` should be used for one setting -> multiple options, or for select option
// renaming, or for data cleanup if needed.
setterFunc?: (
value: SavedConfigEntry,
options: BaseOptions,
isDefault: boolean,
) => ConfigValue | void;
) => ConfigValue | void | undefined;
};

export interface NamedConfigEntry<NameUnion> extends Omit<ConfigEntry, 'id'> {
id: NameUnion;
}

export type OptionsTemplate = {
buildExtraUI?: (base: CactbotConfigurator, container: HTMLElement) => void;
processExtraOptions?: (options: BaseOptions, savedConfig: SavedConfigEntry) => void;
processExtraOptions?: (
options: BaseOptions,
savedConfig: SavedConfigEntry,
) => void;
options: ConfigEntry[];
};

Expand Down Expand Up @@ -303,11 +313,22 @@ class UserConfig {
// processOptions needs to be called whether or not there are
// any userOptions saved, as it sets up the defaults.
this.savedConfig = (await readOptions)?.data ?? {};
this.processOptions(
options,
this.savedConfig[overlayName] ?? {},
this.optionTemplates[overlayName],
);

const template = this.optionTemplates[overlayName];
if (template !== undefined) {
const savedConfig = this.savedConfig[overlayName] ?? {};
this.processOptions(
options,
options,
savedConfig,
template.options,
);

// For things like raidboss that build extra UI, also give them a chance
// to handle anything that has been set on that UI.
if (template.processExtraOptions)
template.processExtraOptions(options, savedConfig);
}

// If the overlay has a "Debug" setting, set to true via the config tool,
// then also print out user files that have been loaded.
Expand Down Expand Up @@ -430,21 +451,30 @@ class UserConfig {
if (head)
head.appendChild(userCSS);
}
processOptions(options: BaseOptions, savedConfig: SavedConfigEntry, template?: OptionsTemplate) {
processOptions(
options: BaseOptions,
output: { [key: string]: unknown },
savedConfig: SavedConfigEntry,
templateOptions?: ConfigEntry[],
) {
// Take options from the template, find them in savedConfig,
// and apply them to options. This also handles setting
// defaults for anything in the template, even if it does not
// exist in savedConfig.

// Not all overlays have option templates.
if (!template)
if (templateOptions === undefined)
return;

const templateOptions = template.options;
for (const opt of templateOptions) {
// Grab the saved value or the default to set in options.

let value: SavedConfigEntry = opt.default;
let value: SavedConfigEntry;
if (typeof opt.default === 'function')
value = opt.default(options);
else
value = opt.default;

let isDefault = true;
if (typeof savedConfig === 'object' && !Array.isArray(savedConfig)) {
if (opt.id in savedConfig) {
Expand All @@ -462,26 +492,21 @@ class UserConfig {
if (opt.setterFunc) {
const setValue = opt.setterFunc(value, options, isDefault);
if (setValue !== undefined)
options[opt.id] = setValue;
output[opt.id] = setValue;
} else if (opt.type === 'integer') {
if (typeof value === 'number')
options[opt.id] = Math.floor(value);
output[opt.id] = Math.floor(value);
else if (typeof value === 'string')
options[opt.id] = parseInt(value);
output[opt.id] = parseInt(value);
} else if (opt.type === 'float') {
if (typeof value === 'number')
options[opt.id] = value;
output[opt.id] = value;
else if (typeof value === 'string')
options[opt.id] = parseFloat(value);
output[opt.id] = parseFloat(value);
} else {
options[opt.id] = value;
}
}

// For things like raidboss that build extra UI, also give them a chance
// to handle anything that has been set on that UI.
if (template.processExtraOptions)
template.processExtraOptions(options, savedConfig);
}
addUnlockText(lang: Lang) {
const unlockText = {
Expand Down
1 change: 1 addition & 0 deletions test/helper/test_trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const getFakeRaidbossData = (triggerSet?: LooseTriggerSet): RaidbossData => {
currentHP: 0,
options: raidbossOptions,
inCombat: true,
triggerSetConfig: {},
ShortName: (x: string | undefined) => x ?? '',
StopCombat: (): void => {/* noop */},
ParseLocaleFloat: () => 0,
Expand Down
2 changes: 2 additions & 0 deletions types/data.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Lang } from '../resources/languages';
import PartyTracker from '../resources/party';
import { ConfigValue } from '../resources/user_config';

import { SystemInfo } from './event';
import { Job, Role } from './job';
Expand Down Expand Up @@ -30,6 +31,7 @@ export interface RaidbossData {
currentHP: number;
options: BaseOptions;
inCombat: boolean;
triggerSetConfig: { [key: string]: ConfigValue };
ShortName: (x?: string) => string;
StopCombat: () => void;
/** @deprecated Use parseFloat instead */
Expand Down
4 changes: 4 additions & 0 deletions types/trigger.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Lang, NonEnLang } from '../resources/languages';
import { NamedConfigEntry } from '../resources/user_config';
import { TimelineReplacement, TimelineStyle } from '../ui/raidboss/timeline_parser';

import { RaidbossData } from './data';
Expand Down Expand Up @@ -177,6 +178,9 @@ export type BaseTriggerSet<Data extends RaidbossData> = {
zoneId: ZoneIdType | number[];
// useful if the zoneId is an array or zone name is otherwise non-descriptive
zoneLabel?: LocaleText;
// trigger set ids to load configs from (this trigger set is loaded implicitly).
loadConfigs?: string[];
config?: NamedConfigEntry<Extract<keyof Data['triggerSetConfig'], string>>[];
// If the timeline exists, but needs significant improvements and a rewrite.
timelineNeedsFixing?: boolean;
// If no timeline is possible for this zone, e.g. t3.
Expand Down
Loading

0 comments on commit f7d621d

Please sign in to comment.