From 033afa10880597542bd8aaee85b34a8bfaee90c0 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Wed, 2 Nov 2022 07:26:06 +0800 Subject: [PATCH 01/19] merge #5038 --- types/trigger.d.ts | 32 +++++++++++++------------------- ui/raidboss/popup-text.ts | 2 +- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/types/trigger.d.ts b/types/trigger.d.ts index 686c4f0b7b..b33d95535c 100644 --- a/types/trigger.d.ts +++ b/types/trigger.d.ts @@ -106,8 +106,15 @@ export type TriggerField = { +export type BaseTrigger< + Data extends RaidbossData, + Type extends TriggerTypes, +> = Omit, 'type' | 'netRegex'>; + +type BaseNetTrigger = { id: string; + type: Type; + netRegex: NetParams[Type] | CactbotBaseRegExp; disabled?: boolean; condition?: TriggerField; preRun?: TriggerField; @@ -128,20 +135,13 @@ export type BaseTrigger = outputStrings?: OutputStrings; }; -// new trigger type, regex is build by core. -type PartialNetRegexTrigger = { - type?: T; - netRegex: NetParams[T] | CactbotBaseRegExp; -}; - export type NetRegexTrigger = TriggerTypes extends infer T - ? T extends TriggerTypes ? (BaseTrigger & PartialNetRegexTrigger) + ? T extends TriggerTypes ? BaseNetTrigger : never : never; export type GeneralNetRegexTrigger = - & BaseTrigger - & PartialNetRegexTrigger; + BaseNetTrigger; type PartialRegexTrigger = { regex: RegExp; @@ -162,8 +162,6 @@ export type TimelineField = string | TimelineFunc | undefined | TimelineField[]; export type DataInitializeFunc = () => Omit; -export type DisabledTrigger = { id: string; disabled: true }; - // This helper takes all of the properties in Type and checks to see if they can be assigned to a // blank object, and if so excludes them from the returned union. The `-?` syntax removes the // optional modifier from the attribute which prevents `undefined` from being included in the union @@ -183,8 +181,8 @@ export type BaseTriggerSet = { overrideTimelineFile?: boolean; timelineFile?: string; timeline?: TimelineField; - triggers?: (NetRegexTrigger | DisabledTrigger)[]; - timelineTriggers?: (TimelineTrigger | DisabledTrigger)[]; + triggers?: NetRegexTrigger[]; + timelineTriggers?: TimelineTrigger[]; timelineReplace?: TimelineReplacement[]; timelineStyles?: TimelineStyle[]; }; @@ -202,11 +200,7 @@ export type TriggerSet = // Less strict type for user triggers + built-in triggers, including deprecated fields. export type LooseTimelineTrigger = Partial>; -export type LooseTrigger = Partial< - & BaseTrigger - & PartialRegexTrigger - & PartialNetRegexTrigger<'None'> ->; +export type LooseTrigger = Partial & PartialRegexTrigger>; export type LooseTriggerSet = & Exclude>, 'triggers' | 'timelineTriggers'> diff --git a/ui/raidboss/popup-text.ts b/ui/raidboss/popup-text.ts index af30b01168..ee32aba71e 100644 --- a/ui/raidboss/popup-text.ts +++ b/ui/raidboss/popup-text.ts @@ -762,7 +762,7 @@ export class PopupText { trigger.localNetRegex = Regexes.parse(localeNetRegex); orderedTriggers.push(trigger); found = true; - } else if (defaultNetRegex) { + } else if (defaultNetRegex !== undefined) { // simple netRegex trigger, need to build netRegex and translate if (defaultNetRegex instanceof RegExp) { const trans = translateRegex(defaultNetRegex, this.parserLang, set.timelineReplace); From c89701ee7d7a55affda00777a66c3239243bca99 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Wed, 2 Nov 2022 07:26:38 +0800 Subject: [PATCH 02/19] merge #5045 --- test/helper/test_trigger.ts | 99 ++++++++++++++++++++++--------------- types/trigger.d.ts | 4 +- ui/raidboss/popup-text.ts | 8 +-- 3 files changed, 65 insertions(+), 46 deletions(-) diff --git a/test/helper/test_trigger.ts b/test/helper/test_trigger.ts index 7aa8a8d4c0..bb1735acc8 100644 --- a/test/helper/test_trigger.ts +++ b/test/helper/test_trigger.ts @@ -72,9 +72,11 @@ const testTriggerFile = (file: string) => { it('should not use non-network triggers', () => { const regexesProps = ['regex', 'regexCn', 'regexDe', 'regexFr', 'regexKo', 'regexJa']; - for (const trigger of triggerSet.triggers ?? []) { + for (const [index, trigger] of triggerSet.triggers?.entries() ?? []) { + const id = trigger.id ?? `triggers[${index}]`; + for (const prop of regexesProps) - assert.isFalse(prop in trigger, `trigger ${trigger.id} has prop ${prop}`); + assert.isFalse(prop in trigger, `trigger ${id} has prop ${prop}`); } }); @@ -113,7 +115,9 @@ const testTriggerFile = (file: string) => { }; it('has valid matches and output parameters', () => { - for (const currentTrigger of triggerSet.triggers ?? []) { + for (const [index, currentTrigger] of triggerSet.triggers?.entries() ?? []) { + const id = currentTrigger.id ?? `triggers[${index}]`; + let containsMatches = false; let containsMatchesParam = false; @@ -136,7 +140,7 @@ const testTriggerFile = (file: string) => { const containsOutputParam = paramNames.includes('output'); // TODO: should we error when there is an unused output param? that seems a bit much. if (containsOutput && !containsOutputParam) - assert.fail(`Missing 'output' param for '${currentTrigger.id}'.`); + assert.fail(`Missing 'output' param for '${id}'.`); containsMatches = containsMatches || /(? { if (funcStr.includes(builtInResponse)) { if (typeof currentTriggerFunction !== 'function') { assert.fail( - `${currentTrigger.id} field '${func}' has ${builtInResponse} but is not a function.`, + `${id} field '${func}' has ${builtInResponse} but is not a function.`, ); continue; } if (func !== 'response') { assert.fail( - `${currentTrigger.id} field '${func}' has ${builtInResponse} but is not a response.`, + `${id} field '${func}' has ${builtInResponse} but is not a response.`, ); continue; } @@ -182,7 +186,7 @@ const testTriggerFile = (file: string) => { } else { if (currentTrigger.type === undefined) { assert.fail( - `netTrigger "${currentTrigger.id}" without type and non-regex netRegex property`, + `netTrigger "${id}" without type and non-regex netRegex property`, ); continue; } @@ -199,15 +203,15 @@ const testTriggerFile = (file: string) => { if (captures > 0) { if (!containsMatches) { assert.fail( - `Found unnecessary regex capturing group for trigger id '${currentTrigger.id}'.`, + `Found unnecessary regex capturing group for trigger id '${id}'.`, ); } else if (!containsMatchesParam) { - assert.fail(`Missing matches param for '${currentTrigger.id}'.`); + assert.fail(`Missing matches param for '${id}'.`); } } else { if (containsMatches) { assert.fail( - `Found 'matches' as a function parameter without regex capturing group for trigger id '${currentTrigger.id}'.`, + `Found 'matches' as a function parameter without regex capturing group for trigger id '${id}'.`, ); } } @@ -282,15 +286,21 @@ const testTriggerFile = (file: string) => { 'tts', ]; - for (const set of [triggerSet.triggers, triggerSet.timelineTriggers]) { + for ( + const { name, set } of [ + { name: 'triggers', set: triggerSet.triggers }, + { name: 'timelineTriggers', set: triggerSet.timelineTriggers }, + ] + ) { if (!set) continue; - for (const trigger of set) { + for (const [index, trigger] of set.entries()) { + const id = trigger.id ?? `${name}[${index}]`; if (!trigger.response) continue; for (const item of bannedItems) { if (item in trigger) - assert.fail(`${trigger.id} cannot have both 'response' and '${item}'`); + assert.fail(`${id} cannot have both 'response' and '${item}'`); } } } @@ -326,10 +336,17 @@ const testTriggerFile = (file: string) => { 'outputStrings', ]; - for (const set of [triggerSet.triggers, triggerSet.timelineTriggers]) { + for ( + const { name, set } of [ + { name: 'triggers', set: triggerSet.triggers }, + { name: 'timelineTriggers', set: triggerSet.timelineTriggers }, + ] + ) { if (!set) continue; - for (const trigger of set) { + for (const [index, trigger] of set.entries()) { + const id = trigger.id ?? `${name}[${index}]`; + let lastIdx = -1; const keys = Object.keys(trigger); @@ -343,9 +360,8 @@ const testTriggerFile = (file: string) => { continue; if (thisIdx <= lastIdx) { assert.fail( - `in ${trigger.id}, field '${keys[lastIdx] ?? '???'}' must precede '${ - keys[thisIdx] ?? - '???' + `in ${id}, field '${keys[lastIdx] ?? '???'}' must precede '${ + keys[thisIdx] ?? '???' }'`, ); } @@ -360,13 +376,14 @@ const testTriggerFile = (file: string) => { if (!triggerSet.timelineTriggers) return; - for (const trigger of triggerSet.timelineTriggers) { + for (const [index, trigger] of triggerSet.timelineTriggers.entries()) { + const id = trigger.id ?? `timelineTriggers[${index}]`; for (const key in trigger) { // regex is the only valid regular expression field on a timeline trigger. if (key === 'regex') continue; if (key === 'netRegex') - assert.fail(`in ${trigger.id}, invalid field '${key}' in timelineTrigger`); + assert.fail(`in ${id}, invalid field '${key}' in timelineTrigger`); } } }); @@ -411,14 +428,16 @@ const testTriggerFile = (file: string) => { for (const set of [triggerSet.triggers, triggerSet.timelineTriggers]) { if (!set) continue; - for (const trigger of set) { + for (const [index, trigger] of set.entries()) { + const id = trigger.id ?? `triggers[${index}]`; + let outputStrings: OutputStrings = {}; let response = {}; if (trigger.response) { // Triggers using responses should include the outputStrings in the // response func itself, via `output.responseOutputStrings = {};` if (trigger.outputStrings) { - assert.fail(`found both 'response' and 'outputStrings in '${trigger.id}'.`); + assert.fail(`found both 'response' and 'outputStrings in '${id}'.`); continue; } if (typeof trigger.response !== 'function') @@ -426,7 +445,7 @@ const testTriggerFile = (file: string) => { const funcStr = trigger.response.toString(); if (!funcStr.includes(builtInResponseStr)) { assert.fail( - `'${trigger.id}' built-in response does not include "${builtInResponseStr}".`, + `'${id}' built-in response does not include "${builtInResponseStr}".`, ); continue; } @@ -439,19 +458,19 @@ const testTriggerFile = (file: string) => { response = responseFunc(data, {}, output) ?? {}; if (typeof outputStrings !== 'object') { - assert.fail(`'${trigger.id}' built-in response did not set outputStrings.`); + assert.fail(`'${id}' built-in response did not set outputStrings.`); continue; } } } else { if (trigger.outputStrings && typeof outputStrings !== 'object') { - assert.fail(`'${trigger.id}' outputStrings must be an object.`); + assert.fail(`'${id}' outputStrings must be an object.`); continue; } if (typeof trigger.outputStrings !== 'object') { for (const func of triggerTextOutputFunctions) { if (func in trigger) { - assert.fail(`'${trigger.id}' missing field outputStrings.`); + assert.fail(`'${id}' missing field outputStrings.`); break; } } @@ -480,7 +499,7 @@ const testTriggerFile = (file: string) => { templateObj = { en: templateObj }; } if (typeof templateObj !== 'object') { - assert.fail(`'${key}' in '${trigger.id}' outputStrings is not a translatable object`); + assert.fail(`'${key}' in '${id}' outputStrings is not a translatable object`); continue; } @@ -488,7 +507,7 @@ const testTriggerFile = (file: string) => { for (const [lang, template] of Object.entries(templateObj)) { if (typeof template !== 'string') { assert.fail( - `'${key}' in '${trigger.id}' outputStrings for lang ${lang} is not a string`, + `'${key}' in '${id}' outputStrings for lang ${lang} is not a string`, ); continue; } @@ -510,7 +529,7 @@ const testTriggerFile = (file: string) => { if (!ok) { assert.fail( - `'${key}' in '${trigger.id}' outputStrings has inconsistent params among languages`, + `'${key}' in '${id}' outputStrings has inconsistent params among languages`, ); continue; } @@ -520,9 +539,8 @@ const testTriggerFile = (file: string) => { // Verify that there's no dangling ${ if (/\${/.test(template.replace(paramRegex, ''))) { assert.fail( - `'${key}' in '${trigger.id}' outputStrings has an open \${ without a closing }`, + `'${key}' in '${id}' outputStrings has an open \${ without a closing }`, ); - continue; } } } @@ -549,7 +567,7 @@ const testTriggerFile = (file: string) => { // Validate that any calls to output.word() have a corresponding outputStrings entry. funcStr.replace(/\boutput\.(\w*)\(/g, (fullMatch: string, key: string) => { if (outputStrings[key] === undefined) { - assert.fail(`missing key '${key}' in '${trigger.id}' outputStrings`); + assert.fail(`missing key '${key}' in '${id}' outputStrings`); return fullMatch; } usedOutputStringEntries.add(key); @@ -561,7 +579,7 @@ const testTriggerFile = (file: string) => { for (const param of outputStringsParams[key] ?? []) { if (!Regexes.parse(`\\b${param}\\s*:`).exec(funcStr)) { assert.fail( - `'${trigger.id}' does not define param '${param}' for outputStrings entry '${key}'`, + `'${id}' does not define param '${param}' for outputStrings entry '${key}'`, ); } } @@ -573,7 +591,7 @@ const testTriggerFile = (file: string) => { if (!dynamicOutputStringAccess) { for (const key in outputStrings) { if (!usedOutputStringEntries.has(key)) - assert.fail(`'${trigger.id}' has unused outputStrings entry '${key}'`); + assert.fail(`'${id}' has unused outputStrings entry '${key}'`); } } } @@ -603,15 +621,16 @@ const testTriggerFile = (file: string) => { if (trans.missingTranslations) continue; - const triggers = triggerSet.triggers; - for (const trigger of triggers ?? []) { + for (const [index, trigger] of triggerSet.triggers?.entries() ?? []) { + const id = trigger.id ?? `triggers[${index}]`; + if (trigger.netRegex === undefined) continue; if (trigger.type === undefined) { if (!(trigger.netRegex instanceof RegExp)) { assert.fail( - `${trigger.id} doesn't have 'type' property and doesn't have a RegExp netRegex`, + `${id} doesn't have 'type' property and doesn't have a RegExp netRegex`, ); } continue; @@ -635,13 +654,13 @@ const testTriggerFile = (file: string) => { if (typeof field === 'string') { assert.isTrue( textHasTranslation(field), - `${trigger.id}:locale ${locale}:missing timelineReplace replaceSync for ${fieldName} '${field}'`, + `${id}:locale ${locale}:missing timelineReplace replaceSync for ${fieldName} '${field}'`, ); } else { for (const s of field) { assert.isTrue( textHasTranslation(s), - `${trigger.id}:locale ${locale}:missing timelineReplace replaceSync for ${fieldName} '${s}'`, + `${id}:locale ${locale}:missing timelineReplace replaceSync for ${fieldName} '${s}'`, ); } } @@ -674,7 +693,7 @@ const testTriggerFile = (file: string) => { assert.isTrue( wasTranslated, - `${trigger.id}:locale ${locale}:missing timelineReplace replaceSync for regex '${origRegex}'`, + `${id}:locale ${locale}:missing timelineReplace replaceSync for regex '${origRegex}'`, ); } } diff --git a/types/trigger.d.ts b/types/trigger.d.ts index b33d95535c..ef89857b34 100644 --- a/types/trigger.d.ts +++ b/types/trigger.d.ts @@ -188,7 +188,7 @@ export type BaseTriggerSet = { }; // If Data contains required properties that are not on RaidbossData, require initData -export type TriggerSet = +export type TriggerSet = & BaseTriggerSet & (RequiredFieldsAsUnion extends RequiredFieldsAsUnion ? { initData?: DataInitializeFunc; @@ -203,7 +203,7 @@ export type LooseTimelineTrigger = Partial>; export type LooseTrigger = Partial & PartialRegexTrigger>; export type LooseTriggerSet = - & Exclude>, 'triggers' | 'timelineTriggers'> + & Omit, 'triggers' | 'timelineTriggers'> & { /** @deprecated Use zoneId instead */ zoneRegex?: diff --git a/ui/raidboss/popup-text.ts b/ui/raidboss/popup-text.ts index ee32aba71e..520c36d2e9 100644 --- a/ui/raidboss/popup-text.ts +++ b/ui/raidboss/popup-text.ts @@ -713,12 +713,13 @@ export class PopupText { // Adjust triggers for the parser language. if (set.triggers && this.options.AlertsEnabled) { - for (const trigger of set.triggers) { + for (const [index, tr] of set.triggers.entries()) { + const trigger: ProcessedTrigger = tr; // Add an additional resolved regex here to save // time later. This will clobber each time we // load this, but that's ok. trigger.filename = setFilename; - const id = trigger.id; + const id = trigger.id ?? `${setFilename} trigger[${index}]`; if (!isRegexTrigger(trigger) && !isNetRegexTrigger(trigger)) { console.error(`Trigger ${id}: has no regex property specified`); @@ -787,8 +788,7 @@ export class PopupText { } if (!found) { - console.error('Trigger ' + trigger.id + ': missing regex and netRegex'); - continue; + console.error(`Trigger ${id}: missing regex and netRegex`); } } } From eec49c34fdd68c2540f5b44485c74ed5a57885fc Mon Sep 17 00:00:00 2001 From: Trim21 Date: Wed, 2 Nov 2022 07:27:54 +0800 Subject: [PATCH 03/19] real change of this PR --- ui/config/config.ts | 2 +- ui/raidboss/popup-text.ts | 474 ++++++++++++++++++-------------- ui/raidboss/raidboss_config.ts | 39 ++- ui/raidboss/raidboss_options.ts | 7 + 4 files changed, 295 insertions(+), 227 deletions(-) diff --git a/ui/config/config.ts b/ui/config/config.ts index 3a94f977c0..15bd29c753 100644 --- a/ui/config/config.ts +++ b/ui/config/config.ts @@ -219,7 +219,7 @@ export type ConfigLooseTrigger = LooseTrigger & LooseTimelineTrigger & { export type ConfigLooseTriggerSet = LooseTriggerSet & { filename?: string; - isUserTriggerSet?: boolean; + loaded?: boolean; }; export type ConfigLooseOopsyTrigger = LooseOopsyTrigger; diff --git a/ui/raidboss/popup-text.ts b/ui/raidboss/popup-text.ts index 520c36d2e9..8a10230072 100644 --- a/ui/raidboss/popup-text.ts +++ b/ui/raidboss/popup-text.ts @@ -18,6 +18,7 @@ import { Matches } from '../../types/net_matches'; import { DataInitializeFunc, GeneralNetRegexTrigger, + LooseTimelineTrigger, LooseTrigger, LooseTriggerSet, Output, @@ -40,16 +41,10 @@ import { PerTriggerAutoConfig, PerTriggerOption, RaidbossOptions } from './raidb import { TimelineLoader } from './timeline'; import { TimelineReplacement } from './timeline_parser'; -const isRaidbossLooseTimelineTrigger = ( - trigger: LooseTrigger, -): trigger is ProcessedTimelineTrigger => { - return 'isTimelineTrigger' in trigger; -}; - export const isNetRegexTrigger = ( trigger?: LooseTrigger, ): trigger is Partial> => { - if (trigger && !isRaidbossLooseTimelineTrigger(trigger)) + if (trigger) return 'netRegex' in trigger; return false; }; @@ -57,7 +52,7 @@ export const isNetRegexTrigger = ( export const isRegexTrigger = ( trigger?: LooseTrigger, ): trigger is Partial> => { - if (trigger && !isRaidbossLooseTimelineTrigger(trigger)) + if (trigger) return 'regex' in trigger; return false; }; @@ -69,14 +64,16 @@ export type ProcessedTrigger = LooseTrigger & { output?: Output; }; -type ProcessedTimelineTrigger = ProcessedTrigger & { - isTimelineTrigger?: true; -}; +// a loaded and localized trigger +export type LocalizedTrigger = + & LooseTrigger + & { id: string; filename: string; output?: Output } + & ({ localRegex: RegExp } | { localNetRegex: RegExp }); -type ProcessedTriggerSet = LooseTriggerSet & { - filename?: string; - timelineTriggers?: ProcessedTimelineTrigger[]; - triggers?: ProcessedTrigger[]; +export type LoadedTriggerSet = LooseTriggerSet & { + filename: string; + timelineTriggers?: Omit[]; + triggers?: LooseTrigger[]; }; // There should be (at most) six lines of instructions. @@ -222,10 +219,10 @@ const textMap: TextMap = { // JavaScript dictionaries are *almost* ordered automatically as we would want, // but want to handle missing ids and integer ids (you shouldn't, but just in case). class OrderedTriggerList { - triggers: ProcessedTrigger[] = []; + triggers: LocalizedTrigger[] = []; idToIndex: { [id: string]: number } = {}; - push(trigger: ProcessedTrigger) { + push(trigger: LocalizedTrigger) { const idx = trigger.id !== undefined ? this.idToIndex[trigger.id] : undefined; if (idx !== undefined && trigger.id !== undefined) { const oldTrigger = this.triggers[idx]; @@ -267,7 +264,7 @@ class TriggerOutputProxy { public unknownValue = '???'; private constructor( - public trigger: ProcessedTrigger, + public trigger: LooseTrigger, public displayLang: Lang, public perTriggerAutoConfig?: PerTriggerAutoConfig, ) { @@ -394,7 +391,7 @@ class TriggerOutputProxy { } static makeOutput( - trigger: ProcessedTrigger, + trigger: LooseTrigger, displayLang: Lang, perTriggerAutoConfig?: PerTriggerAutoConfig, ): Output { @@ -452,13 +449,9 @@ const wipeEndEcho = commonNetRegex.userWipeEcho; const wipeFadeIn = commonNetRegex.wipe; const isWipe = (line: string): boolean => { - if ( - wipeCactbotEcho.test(line) || + return wipeCactbotEcho.test(line) || wipeEndEcho.test(line) || - wipeFadeIn.test(line) - ) - return true; - return false; + wipeFadeIn.test(line); }; export class PopupText { @@ -483,7 +476,10 @@ export class PopupText { protected me = ''; protected job: Job = 'NONE'; protected role: Role = 'none'; - protected triggerSets: ProcessedTriggerSet[] = []; + + protected collectedTriggerSets: LoadedTriggerSet[] = []; + // protected processedTriggerSets: ProcessedTriggerSet[] = []; + protected zoneName = ''; protected zoneId = -1; protected dataInitializers: { @@ -498,7 +494,8 @@ export class PopupText { ) { this.options = options; this.timelineLoader = timelineLoader; - this.ProcessDataFiles(raidbossDataFiles); + + this.collectedTriggerSets = this.ProcessDataFiles(raidbossDataFiles); this.infoText = document.getElementById('popup-text-info'); this.alertText = document.getElementById('popup-text-alert'); @@ -570,33 +567,38 @@ export class PopupText { this.data.currentHP = e.detail.currentHP; } - ProcessDataFiles(files: RaidbossFileData): void { - this.triggerSets = []; - for (const [filename, json] of Object.entries(files)) { + ProcessDataFiles(files: RaidbossFileData): LoadedTriggerSet[] { + const triggerSets: LoadedTriggerSet[] = []; + + for (const [filename, set] of Object.entries(files)) { if (!filename.endsWith('.js') && !filename.endsWith('.ts')) continue; - if (typeof json !== 'object') { - console.log('Unexpected JSON from ' + filename + ', expected an array'); + if (typeof set !== 'object') { + console.log('Unexpected TriggerSet from ' + filename + ', expected an array'); continue; } - if (!json.triggers) { - console.log('Unexpected JSON from ' + filename + ', expected a triggers'); + if (!set.triggers) { + console.log('Unexpected TriggerSet from ' + filename + ', expected a triggers'); continue; } - if (typeof json.triggers !== 'object' || !(json.triggers.length >= 0)) { - console.log('Unexpected JSON from ' + filename + ', expected triggers to be an array'); + if (typeof set.triggers !== 'object' || !(set.triggers.length >= 0)) { + console.log( + 'Unexpected TriggerSet from ' + filename + ', expected triggers to be an array', + ); continue; } - const processedSet = { - filename: filename, - ...json, - }; - this.triggerSets.push(processedSet); + + triggerSets.push({ + filename, + ...set, + }); } // User triggers must come last so that they override built-in files. - this.triggerSets.push(...this.options.Triggers); + triggerSets.push(...this.options.LoadedTriggers); + + return triggerSets; } OnChangeZone(e: EventResponses['ChangeZone']): void { @@ -607,6 +609,7 @@ export class PopupText { } } + // this is expected to be called at job change or zone change. ReloadTimelines(): void { if (!this.me || !this.zoneName || !this.timelineLoader.IsReady()) return; @@ -640,160 +643,27 @@ export class PopupText { } }.bind(this); - // construct something like regexDe or regexFr. - const langSuffix = this.parserLang.charAt(0).toUpperCase() + this.parserLang.slice(1); - const regexParserLang = 'regex' + langSuffix; - const netRegexParserLang = 'netRegex' + langSuffix; - - for (const set of this.triggerSets) { - // zoneRegex can be undefined, a regex, or translatable object of regex. - const haveZoneRegex = 'zoneRegex' in set; - const haveZoneId = 'zoneId' in set; - if (!haveZoneRegex && !haveZoneId || haveZoneRegex && haveZoneId) { - console.error(`Trigger set must include exactly one of zoneRegex or zoneId property`); - continue; - } - if (haveZoneId && set.zoneId === undefined) { - const filename = set.filename ? `'${set.filename}'` : '(user file)'; - console.error( - `Trigger set has zoneId, but with nothing specified in ${filename}. ` + - `Did you misspell the ZoneId.ZoneName?`, - ); - continue; - } - - if (set.zoneId !== undefined) { - if ( - set.zoneId !== ZoneId.MatchAll && set.zoneId !== this.zoneId && - !(typeof set.zoneId === 'object' && set.zoneId.includes(this.zoneId)) - ) - continue; - } else if (set.zoneRegex) { - let zoneRegex = set.zoneRegex; - if (typeof zoneRegex !== 'object') { - console.error( - 'zoneRegex must be translatable object or regexp: ' + JSON.stringify(set.zoneRegex), - ); - continue; - } else if (!(zoneRegex instanceof RegExp)) { - const parserLangRegex = zoneRegex[this.parserLang]; - if (parserLangRegex) { - zoneRegex = parserLangRegex; - } else if (zoneRegex['en']) { - zoneRegex = zoneRegex['en']; - } else { - console.error('unknown zoneRegex parser language: ' + JSON.stringify(set.zoneRegex)); - continue; - } - - if (!(zoneRegex instanceof RegExp)) { - console.error('zoneRegex must be regexp: ' + JSON.stringify(set.zoneRegex)); - continue; - } - } - if (this.zoneName.search(Regexes.parse(zoneRegex)) < 0) - continue; - } + const timelineTriggers: ProcessedTrigger[] = []; - if (this.options.Debug) { - if (set.filename) - console.log('Loading ' + set.filename); - else - console.log('Loading user triggers for zone'); + for (const set of this.collectedTriggerSets) { + if (!this.TriggerSetEnabled(set)) { + continue; } - const setFilename = set.filename ?? 'Unknown'; - - if (set.initData) { - this.dataInitializers.push({ - file: setFilename, - func: set.initData, - }); + const processed = this.LocalizeTriggerSet(set); + if (processed) { + processed.forEach((x) => orderedTriggers.push(x)); } - // Adjust triggers for the parser language. - if (set.triggers && this.options.AlertsEnabled) { - for (const [index, tr] of set.triggers.entries()) { - const trigger: ProcessedTrigger = tr; - // Add an additional resolved regex here to save - // time later. This will clobber each time we - // load this, but that's ok. - trigger.filename = setFilename; - const id = trigger.id ?? `${setFilename} trigger[${index}]`; - - if (!isRegexTrigger(trigger) && !isNetRegexTrigger(trigger)) { - console.error(`Trigger ${id}: has no regex property specified`); - continue; - } - - this.ProcessTrigger(trigger); - - let found = false; - - const triggerObject: { [key: string]: unknown } = trigger; - - // `regex` and `regexDe` (etc) are deprecated, however they may still be used - // by user triggers, and so are still checked here. `regexDe` and friends - // will never be auto-translated and are assumed to be correct. - // TODO: maybe we could consider removing these once timelines don't need parsed lines? - if (isRegexTrigger(trigger)) { - const defaultRegex = trigger.regex; - const localeRegex = triggerObject[regexParserLang]; - if (localeRegex instanceof RegExp) { - trigger.localRegex = Regexes.parse(localeRegex); - orderedTriggers.push(trigger); - found = true; - } else if (defaultRegex) { - const trans = translateRegex(defaultRegex, this.parserLang, set.timelineReplace); - trigger.localRegex = Regexes.parse(trans); - orderedTriggers.push(trigger); - found = true; - } - } - - // `netRegexDe` (etc) is also deprecated, but they also may still be used by - // user triggers. If they exist, they will take precedence over `netRegex`. - // `netRegex` will be auto-translated into the parser language. `netRegexDe` - // and friends will never be auto-translated and are assumed to be correct. - if (isNetRegexTrigger(trigger)) { - const defaultNetRegex = trigger.netRegex; - const localeNetRegex = triggerObject[netRegexParserLang]; - if (localeNetRegex instanceof RegExp) { - // localized regex don't need to handle net-regex auto build - trigger.localNetRegex = Regexes.parse(localeNetRegex); - orderedTriggers.push(trigger); - found = true; - } else if (defaultNetRegex !== undefined) { - // simple netRegex trigger, need to build netRegex and translate - if (defaultNetRegex instanceof RegExp) { - const trans = translateRegex(defaultNetRegex, this.parserLang, set.timelineReplace); - trigger.localNetRegex = Regexes.parse(trans); - orderedTriggers.push(trigger); - found = true; - } else { - if (trigger.type === undefined) { - console.error(`Trigger ${id}: without type property need RegExp as netRegex`); - continue; - } - - const re = buildNetRegexForTrigger( - trigger.type, - translateRegexBuildParam(defaultNetRegex, this.parserLang, set.timelineReplace), - ); - trigger.localNetRegex = Regexes.parse(re); - orderedTriggers.push(trigger); - found = true; - } - } - } - - if (!found) { - console.error(`Trigger ${id}: missing regex and netRegex`); - } + if (set.timelineTriggers) { + for (const trigger of set.timelineTriggers) { + // Timeline triggers are never translated. + timelineTriggers.push(this.ProcessTimelineTrigger(trigger)); } } if (set.overrideTimelineFile) { + // TODO: set.filename won't be undefined const filename = set.filename ? `'${set.filename}'` : '(user file)'; console.log(`Overriding timeline from ${filename}.`); @@ -817,16 +687,10 @@ export class PopupText { if (set.timeline !== undefined) addTimeline(set.timeline); + if (set.timelineReplace) replacements.push(...set.timelineReplace); - if (set.timelineTriggers) { - for (const trigger of set.timelineTriggers) { - // Timeline triggers are never translated. - this.ProcessTrigger(trigger); - trigger.isTimelineTrigger = true; - orderedTriggers.push(trigger); - } - } + if (set.timelineStyles) timelineStyles.push(...set.timelineStyles); if (set.resetWhenOutOfCombat !== undefined) @@ -834,12 +698,13 @@ export class PopupText { } // Store all the collected triggers in order, and filter out disabled triggers. - const filterEnabled = (trigger: LooseTrigger) => !('disabled' in trigger && trigger.disabled); + const filterEnabled = (trigger: { disabled?: boolean }) => + !('disabled' in trigger && trigger.disabled); + const allTriggers = orderedTriggers.asList().filter(filterEnabled); this.triggers = allTriggers.filter(isRegexTrigger); this.netTriggers = allTriggers.filter(isNetRegexTrigger); - const timelineTriggers = allTriggers.filter(isRaidbossLooseTimelineTrigger); this.Reset(); @@ -847,28 +712,209 @@ export class PopupText { timelineFiles, timelines, replacements, - timelineTriggers, + timelineTriggers.filter(filterEnabled), timelineStyles, this.zoneId, ); } - ProcessTrigger(trigger: ProcessedTrigger | ProcessedTimelineTrigger): void { - // These properties are used internally by ReloadTimelines only and should - // not exist on user triggers. However, the trigger objects themselves are - // reused when reloading pages, and so it is impossible to verify that - // these properties don't exist. Therefore, just delete them silently. - if (isRaidbossLooseTimelineTrigger(trigger)) - delete trigger.isTimelineTrigger; - - delete trigger.localRegex; - delete trigger.localNetRegex; - - trigger.output = TriggerOutputProxy.makeOutput( - trigger, - this.options.DisplayLanguage, - this.options.PerTriggerAutoConfig, - ); + TriggerSetEnabled(set: LoadedTriggerSet): boolean { + // zoneRegex can be undefined, a regex, or translatable object of regex. + const haveZoneRegex = 'zoneRegex' in set; + const haveZoneId = 'zoneId' in set; + if (!haveZoneRegex && !haveZoneId || haveZoneRegex && haveZoneId) { + console.error(`Trigger set must include exactly one of zoneRegex or zoneId property`); + return false; + } + + if (haveZoneId && set.zoneId === undefined) { + // TODO: set.filename will never be undefined or null + const filename = set.filename ? `'${set.filename}'` : '(user file)'; + console.error( + `Trigger set has zoneId, but with nothing specified in ${filename}. Did you misspell the ZoneId.ZoneName?`, + ); + return false; + } + + if (set.zoneId !== undefined) { + if ( + set.zoneId !== ZoneId.MatchAll && set.zoneId !== this.zoneId && + !(Array.isArray(set.zoneId) && set.zoneId.includes(this.zoneId)) + ) + return false; + } else if (set.zoneRegex) { + let zoneRegex = set.zoneRegex; + if (typeof zoneRegex !== 'object') { + console.error( + 'zoneRegex must be translatable object or regexp: ' + JSON.stringify(set.zoneRegex), + ); + return false; + } + + if (!(zoneRegex instanceof RegExp)) { + const parserLangRegex = zoneRegex[this.parserLang]; + + if (parserLangRegex) { + zoneRegex = parserLangRegex; + } else if (zoneRegex['en']) { + zoneRegex = zoneRegex['en']; + } else { + console.error('unknown zoneRegex parser language: ' + JSON.stringify(set.zoneRegex)); + return false; + } + + if (!(zoneRegex instanceof RegExp)) { + console.error('zoneRegex must be regexp: ' + JSON.stringify(set.zoneRegex)); + return false; + } + } + + if (this.zoneName.search(Regexes.parse(zoneRegex)) < 0) + return false; + } + + return true; + } + + // process a zone-enabled trigger set + // return undefined for bad trigger Set + // process trigger based on current job, options and zone. + LocalizeTriggerSet(set: LoadedTriggerSet): undefined | LocalizedTrigger[] { + // construct something like regexDe or regexFr. + const langSuffix = this.parserLang.charAt(0).toUpperCase() + this.parserLang.slice(1); + + const processedTriggers = []; + + if (this.options.Debug) { + if (set.filename) + console.log('Loading ' + set.filename); + else + console.log('Loading user triggers for zone'); + } + + const setFilename = set.filename ?? 'Unknown'; + + if (set.initData) { + this.dataInitializers.push({ + file: setFilename, + func: set.initData, + }); + } + + // Adjust triggers for the parser language. + if (set.triggers && this.options.AlertsEnabled) { + for (const [index, trigger] of set.triggers.entries()) { + const localized = this.LocalizeTrigger(trigger, set, index, langSuffix); + if (localized) { + processedTriggers.push(localized); + } + } + } + + return processedTriggers; + } + + LocalizeTrigger( + trigger: LooseTrigger, + set: LoadedTriggerSet, + triggerIndex: number, + langSuffix: string, + ): LocalizedTrigger | undefined { + const id: string = trigger.id ?? `${set.filename}.triggers[${triggerIndex}]`; + + const processedTrigger = { + id: id, + filename: set.filename, + output: TriggerOutputProxy.makeOutput( + trigger, + this.options.DisplayLanguage, + this.options.PerTriggerAutoConfig, + ), + }; + + const triggerObject: { [key: string]: unknown } = trigger; + + // `regex` and `regexDe` (etc) are deprecated, however they may still be used + // by user triggers, and so are still checked here. `regexDe` and friends + // will never be auto-translated and are assumed to be correct. + // TODO: maybe we could consider removing these once timelines don't need parsed lines? + if (isRegexTrigger(trigger)) { + const defaultRegex = trigger.regex; + const localeRegex = triggerObject['regex' + langSuffix]; + if (localeRegex instanceof RegExp) { + return { + ...processedTrigger, + localRegex: Regexes.parse(localeRegex), + }; + } else if (defaultRegex) { + const trans = translateRegex(defaultRegex, this.parserLang, set.timelineReplace); + + return { + ...processedTrigger, + localRegex: Regexes.parse(trans), + }; + } + } + + // `netRegexDe` (etc) is also deprecated, but they also may still be used by + // user triggers. If they exist, they will take precedence over `netRegex`. + // `netRegex` will be auto-translated into the parser language. `netRegexDe` + // and friends will never be auto-translated and are assumed to be correct. + if (isNetRegexTrigger(trigger)) { + const defaultNetRegex = trigger.netRegex; + const localeNetRegex = triggerObject['netRegex' + langSuffix]; + if (localeNetRegex instanceof RegExp) { + // localized regex don't need to handle net-regex auto build + return { + ...processedTrigger, + localNetRegex: Regexes.parse(localeNetRegex), + }; + } + + // user write a trigger { netRegex: undefined } + if (defaultNetRegex === undefined) { + console.error(`Trigger ${id}: netRegex can't be undefined`); + return; + } + + // simple netRegex trigger, need to build netRegex and translate + if (defaultNetRegex instanceof RegExp) { + const trans = translateRegex(defaultNetRegex, this.parserLang, set.timelineReplace); + + return { + ...processedTrigger, + localNetRegex: Regexes.parse(trans), + }; + } + + if (trigger.type === undefined) { + console.error(`Trigger ${id}: without type property need RegExp as netRegex`); + return; + } + + const re = buildNetRegexForTrigger( + trigger.type, + translateRegexBuildParam(defaultNetRegex, this.parserLang, set.timelineReplace), + ); + + return { + ...processedTrigger, + localNetRegex: Regexes.parse(re), + }; + } + + console.error(`Trigger ${id}: missing regex and netRegex`); + } + + private ProcessTimelineTrigger(trigger: LooseTimelineTrigger): ProcessedTrigger { + return { + ...trigger, + output: TriggerOutputProxy.makeOutput( + trigger, + this.options.DisplayLanguage, + this.options.PerTriggerAutoConfig, + ), + }; } OnJobChange(e: PlayerChangedDetail): void { diff --git a/ui/raidboss/raidboss_config.ts b/ui/raidboss/raidboss_config.ts index 2b28d3403c..bef208f1ef 100644 --- a/ui/raidboss/raidboss_config.ts +++ b/ui/raidboss/raidboss_config.ts @@ -1332,8 +1332,8 @@ class RaidbossConfigurator { if (triggerSet.timelineTriggers) rawTriggers.timeline.push(...triggerSet.timelineTriggers); - if (!triggerSet.isUserTriggerSet && triggerSet.filename) - flattenTimeline(triggerSet, triggerSet.filename, timelineFiles); + if (!triggerSet.loaded && triggerSet.filename) + flattenTimelinePlace(triggerSet, triggerSet.filename, timelineFiles); item.triggers = {}; for (const [key, triggerArr] of Object.entries(rawTriggers)) { @@ -1416,7 +1416,7 @@ class RaidbossConfigurator { } } -const flattenTimeline = ( +const pureFlattenTimeline = ( set: ConfigLooseTriggerSet, filename: string, files: { [filename: string]: string }, @@ -1438,7 +1438,19 @@ const flattenTimeline = ( } // set.timeline is processed recursively. - set.timeline = [set.timeline, files[timelineFile]]; + return [set.timeline, files[timelineFile]]; +}; + +const flattenTimelinePlace = ( + set: ConfigLooseTriggerSet, + filename: string, + files: { [filename: string]: string }, +) => { + const timeline = pureFlattenTimeline(set, filename, files); + if (timeline !== undefined) { + // set.timeline is processed recursively. + set.timeline = timeline; + } }; // Raidboss needs to do some extra processing of user files. @@ -1453,20 +1465,23 @@ const userFileHandler: UserFileCallback = ( if (!baseOptions.Triggers) return; + baseOptions.Triggers ??= []; + baseOptions.LoadedTriggers ??= []; + for (const baseTriggerSet of baseOptions.Triggers) { const set: ConfigLooseTriggerSet = baseTriggerSet; - // Annotate triggers with where they came from. Note, options is passed in repeatedly - // as multiple sets of user files add triggers, so only process each file once. - if (set.isUserTriggerSet) + if (set.loaded) { continue; + } - // `filename` here is just cosmetic for better debug printing to make it more clear - // where a trigger or an override is coming from. - set.filename = `${basePath}${name}`; - set.isUserTriggerSet = true; + baseOptions.LoadedTriggers.push({ + filename: `${basePath}${name}`, + ...baseTriggerSet, + timeline: pureFlattenTimeline(baseTriggerSet, name, files), + }); - flattenTimeline(set, name, files); + set.loaded = true; } }; diff --git a/ui/raidboss/raidboss_options.ts b/ui/raidboss/raidboss_options.ts index 4287bb0b3b..386fb64189 100644 --- a/ui/raidboss/raidboss_options.ts +++ b/ui/raidboss/raidboss_options.ts @@ -9,6 +9,8 @@ import { TriggerOutput, } from '../../types/trigger'; +import { LoadedTriggerSet } from './popup-text'; + // This file defines the base options that raidboss expects to see. // Backwards compat for this old style of overriding triggers. @@ -50,7 +52,11 @@ type RaidbossNonConfigOptions = { PerTriggerAutoConfig: PerTriggerAutoConfig; PerTriggerOptions: PerTriggerOptions; PerZoneTimelineConfig: PerZoneTimelineConfig; + + // loaded builtin triggers and user triggers Triggers: LooseTriggerSet[]; + LoadedTriggers: LoadedTriggerSet[]; + PlayerNameOverride?: string; IsRemoteRaidboss: boolean; // Transforms text before passing it to TTS. @@ -76,6 +82,7 @@ const defaultRaidbossNonConfigOptions: RaidbossNonConfigOptions = { PerZoneTimelineConfig: {}, Triggers: [], + LoadedTriggers: [], IsRemoteRaidboss: false, From 7cac6c5e97aaddcce0df9e1849fae1224a16fe8b Mon Sep 17 00:00:00 2001 From: Trim21 Date: Wed, 2 Nov 2022 08:10:02 +0800 Subject: [PATCH 04/19] handle trigger override --- ui/raidboss/popup-text.ts | 103 ++++++++++++++++++++++---------------- 1 file changed, 60 insertions(+), 43 deletions(-) diff --git a/ui/raidboss/popup-text.ts b/ui/raidboss/popup-text.ts index 8a10230072..6b133f6939 100644 --- a/ui/raidboss/popup-text.ts +++ b/ui/raidboss/popup-text.ts @@ -209,48 +209,64 @@ const textMap: TextMap = { }, }; -// Helper for handling trigger overrides. -// -// asList will return a list of triggers in the same order as append was called, except: -// If a later trigger has the same id as a previous trigger, it will replace the previous trigger -// and appear in the same order that the previous trigger appeared. -// e.g. a, b1, c, b2 (where b1 and b2 share the same id) yields [a, b2, c] as the final list. -// -// JavaScript dictionaries are *almost* ordered automatically as we would want, -// but want to handle missing ids and integer ids (you shouldn't, but just in case). -class OrderedTriggerList { - triggers: LocalizedTrigger[] = []; - idToIndex: { [id: string]: number } = {}; - - push(trigger: LocalizedTrigger) { - const idx = trigger.id !== undefined ? this.idToIndex[trigger.id] : undefined; - if (idx !== undefined && trigger.id !== undefined) { - const oldTrigger = this.triggers[idx]; - - if (oldTrigger === undefined) - throw new UnreachableCode(); - - // TODO: be verbose now while this is fresh, but hide this output behind debug flags later. - const triggerFile = (trigger: ProcessedTrigger) => - trigger.filename ? `'${trigger.filename}'` : 'user override'; - const oldFile = triggerFile(oldTrigger); - const newFile = triggerFile(trigger); - console.log(`Overriding '${trigger.id}' from ${oldFile} with ${newFile}.`); - - this.triggers[idx] = trigger; - return; +// this is an O(n^2) looping but perf is not important here. +const handleTriggerOverride = (triggers: Array): Array => { + const keep: Array = []; + + // loop from new trigger to old trigger. + // so if trigger with same id exists, just log Overriding and skip + for (const oldTrigger of triggers.slice().reverse()) { + if (oldTrigger.id === undefined) { + keep.push(oldTrigger); + continue; } - // Normal case of a new trigger, with no overriding. - if (trigger.id !== undefined) - this.idToIndex[trigger.id] = this.triggers.length; - this.triggers.push(trigger); - } + const sameID = keep.filter((x) => x.id === oldTrigger.id); + if (sameID.length === 0) { + keep.push(oldTrigger); + continue; + } + + const newTrigger = sameID[0]; + + if (!newTrigger) { + throw new UnreachableCode(); + } + + const triggerFile = (trigger: ProcessedTrigger) => + trigger.filename ? `'${trigger.filename}'` : 'user override'; + const oldFile = triggerFile(newTrigger); + const newFile = triggerFile(oldTrigger); - asList() { - return this.triggers; + console.log(`Overriding '${oldTrigger.id}' from ${oldFile} with ${newFile}.`); } -} + + return keep.reverse(); +}; + +// const idx = trigger.id !== undefined ? this.idToIndex[trigger.id] : undefined; +// if (idx !== undefined && trigger.id !== undefined) { +// const oldTrigger = this.triggers[idx]; +// +// if (oldTrigger === undefined) +// throw new UnreachableCode(); +// +// // TODO: be verbose now while this is fresh, but hide this output behind debug flags later. +// const triggerFile = (trigger: ProcessedTrigger) => +// trigger.filename ? `'${trigger.filename}'` : 'user override'; +// const oldFile = triggerFile(oldTrigger); +// const newFile = triggerFile(trigger); +// console.log(`Overriding '${trigger.id}' from ${oldFile} with ${newFile}.`); +// +// this.triggers[idx] = trigger; +// return; +// } +// +// // Normal case of a new trigger, with no overriding. +// if (trigger.id !== undefined) +// this.idToIndex[trigger.id] = this.triggers.length; +// this.triggers.push(trigger); +// }; const isObject = (x: unknown): x is { [key: string]: unknown } => x instanceof Object; @@ -624,8 +640,6 @@ export class PopupText { const timelineStyles = []; this.resetWhenOutOfCombat = true; - const orderedTriggers = new OrderedTriggerList(); - // Some user timelines may rely on having valid init data // Don't use `this.Reset()` since that clears other things as well this.data = this.getDataObject(); @@ -644,6 +658,7 @@ export class PopupText { }.bind(this); const timelineTriggers: ProcessedTrigger[] = []; + const triggers: LocalizedTrigger[] = []; for (const set of this.collectedTriggerSets) { if (!this.TriggerSetEnabled(set)) { @@ -652,7 +667,7 @@ export class PopupText { const processed = this.LocalizeTriggerSet(set); if (processed) { - processed.forEach((x) => orderedTriggers.push(x)); + processed.forEach((x) => triggers.push(x)); } if (set.timelineTriggers) { @@ -701,18 +716,20 @@ export class PopupText { const filterEnabled = (trigger: { disabled?: boolean }) => !('disabled' in trigger && trigger.disabled); - const allTriggers = orderedTriggers.asList().filter(filterEnabled); + const allTriggers = handleTriggerOverride(triggers).filter(filterEnabled); this.triggers = allTriggers.filter(isRegexTrigger); this.netTriggers = allTriggers.filter(isNetRegexTrigger); + const finalTimelineTriggers = handleTriggerOverride(timelineTriggers).filter(filterEnabled); + this.Reset(); this.timelineLoader.SetTimelines( timelineFiles, timelines, replacements, - timelineTriggers.filter(filterEnabled), + finalTimelineTriggers, timelineStyles, this.zoneId, ); From 391582212feb41daf96e3728c35f2f20f1dc4941 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Wed, 2 Nov 2022 08:18:30 +0800 Subject: [PATCH 05/19] use map to handle added trigger --- ui/raidboss/popup-text.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/ui/raidboss/popup-text.ts b/ui/raidboss/popup-text.ts index 6b133f6939..df33cf643c 100644 --- a/ui/raidboss/popup-text.ts +++ b/ui/raidboss/popup-text.ts @@ -209,10 +209,13 @@ const textMap: TextMap = { }, }; -// this is an O(n^2) looping but perf is not important here. const handleTriggerOverride = (triggers: Array): Array => { + console.log('handleTriggerOverride'); const keep: Array = []; + // keep the triggers with id + const container = new Map(); + // loop from new trigger to old trigger. // so if trigger with same id exists, just log Overriding and skip for (const oldTrigger of triggers.slice().reverse()) { @@ -221,18 +224,15 @@ const handleTriggerOverride = (triggers: Array): A continue; } - const sameID = keep.filter((x) => x.id === oldTrigger.id); - if (sameID.length === 0) { + // below, newTrigger and oldTrigger both have `id` + + const newTrigger = container.get(oldTrigger.id); + if (newTrigger === undefined) { keep.push(oldTrigger); + container.set(oldTrigger.id, oldTrigger as T & { id: string }); continue; } - const newTrigger = sameID[0]; - - if (!newTrigger) { - throw new UnreachableCode(); - } - const triggerFile = (trigger: ProcessedTrigger) => trigger.filename ? `'${trigger.filename}'` : 'user override'; const oldFile = triggerFile(newTrigger); From c448a95363076c4daef41ba70f0ddb7e571a32bf Mon Sep 17 00:00:00 2001 From: Trim21 Date: Wed, 2 Nov 2022 08:29:47 +0800 Subject: [PATCH 06/19] remove dead code --- ui/raidboss/popup-text.ts | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/ui/raidboss/popup-text.ts b/ui/raidboss/popup-text.ts index df33cf643c..13b532d67c 100644 --- a/ui/raidboss/popup-text.ts +++ b/ui/raidboss/popup-text.ts @@ -244,30 +244,6 @@ const handleTriggerOverride = (triggers: Array): A return keep.reverse(); }; -// const idx = trigger.id !== undefined ? this.idToIndex[trigger.id] : undefined; -// if (idx !== undefined && trigger.id !== undefined) { -// const oldTrigger = this.triggers[idx]; -// -// if (oldTrigger === undefined) -// throw new UnreachableCode(); -// -// // TODO: be verbose now while this is fresh, but hide this output behind debug flags later. -// const triggerFile = (trigger: ProcessedTrigger) => -// trigger.filename ? `'${trigger.filename}'` : 'user override'; -// const oldFile = triggerFile(oldTrigger); -// const newFile = triggerFile(trigger); -// console.log(`Overriding '${trigger.id}' from ${oldFile} with ${newFile}.`); -// -// this.triggers[idx] = trigger; -// return; -// } -// -// // Normal case of a new trigger, with no overriding. -// if (trigger.id !== undefined) -// this.idToIndex[trigger.id] = this.triggers.length; -// this.triggers.push(trigger); -// }; - const isObject = (x: unknown): x is { [key: string]: unknown } => x instanceof Object; // User trigger may pass anything as parameters From 06aafa80c2c12b311653acb013ce57df56732c63 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Thu, 3 Nov 2022 03:41:19 +0800 Subject: [PATCH 07/19] Update ui/raidboss/popup-text.ts Co-authored-by: Adrienne Walker --- ui/raidboss/popup-text.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/raidboss/popup-text.ts b/ui/raidboss/popup-text.ts index 13b532d67c..3bb046c3cf 100644 --- a/ui/raidboss/popup-text.ts +++ b/ui/raidboss/popup-text.ts @@ -210,7 +210,6 @@ const textMap: TextMap = { }; const handleTriggerOverride = (triggers: Array): Array => { - console.log('handleTriggerOverride'); const keep: Array = []; // keep the triggers with id From 9453c34f7a1748b32b9126dd373a6baaa9020347 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Thu, 3 Nov 2022 03:42:33 +0800 Subject: [PATCH 08/19] Update ui/raidboss/popup-text.ts Co-authored-by: Adrienne Walker --- ui/raidboss/popup-text.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/raidboss/popup-text.ts b/ui/raidboss/popup-text.ts index 3bb046c3cf..677196938f 100644 --- a/ui/raidboss/popup-text.ts +++ b/ui/raidboss/popup-text.ts @@ -469,7 +469,6 @@ export class PopupText { protected role: Role = 'none'; protected collectedTriggerSets: LoadedTriggerSet[] = []; - // protected processedTriggerSets: ProcessedTriggerSet[] = []; protected zoneName = ''; protected zoneId = -1; From 4fdec62f69fc076f030e68b6017045d4e46c8bb1 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Thu, 3 Nov 2022 04:08:11 +0800 Subject: [PATCH 09/19] some code review --- ui/raidboss/popup-text.ts | 8 +++----- ui/raidboss/raidboss_config.ts | 3 +-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/ui/raidboss/popup-text.ts b/ui/raidboss/popup-text.ts index 677196938f..ddf97146d7 100644 --- a/ui/raidboss/popup-text.ts +++ b/ui/raidboss/popup-text.ts @@ -635,7 +635,7 @@ export class PopupText { const triggers: LocalizedTrigger[] = []; for (const set of this.collectedTriggerSets) { - if (!this.TriggerSetEnabled(set)) { + if (!this.IsTriggerSetEnabled(set)) { continue; } @@ -652,9 +652,7 @@ export class PopupText { } if (set.overrideTimelineFile) { - // TODO: set.filename won't be undefined - const filename = set.filename ? `'${set.filename}'` : '(user file)'; - console.log(`Overriding timeline from ${filename}.`); + console.log(`Overriding timeline from ${set.filename}.`); // If the timeline file override is set, all previously loaded timeline info is dropped. // Styles, triggers, and translations are kept, as they may still apply to the new one. @@ -709,7 +707,7 @@ export class PopupText { ); } - TriggerSetEnabled(set: LoadedTriggerSet): boolean { + IsTriggerSetEnabled(set: LoadedTriggerSet): boolean { // zoneRegex can be undefined, a regex, or translatable object of regex. const haveZoneRegex = 'zoneRegex' in set; const haveZoneId = 'zoneId' in set; diff --git a/ui/raidboss/raidboss_config.ts b/ui/raidboss/raidboss_config.ts index bef208f1ef..d28ed88670 100644 --- a/ui/raidboss/raidboss_config.ts +++ b/ui/raidboss/raidboss_config.ts @@ -1417,7 +1417,7 @@ class RaidbossConfigurator { } const pureFlattenTimeline = ( - set: ConfigLooseTriggerSet, + set: Readonly, filename: string, files: { [filename: string]: string }, ) => { @@ -1430,7 +1430,6 @@ const pureFlattenTimeline = ( const dir = filename.slice(0, Math.max(0, lastIndex + 1)); const timelineFile = `${dir}${set.timelineFile}`; - delete set.timelineFile; if (!(timelineFile in files)) { console.log(`ERROR: '${filename}' specifies non-existent timeline file '${timelineFile}'.`); From 849c07033ac2800e50d6aa671052b7a9c0ef8fea Mon Sep 17 00:00:00 2001 From: Trim21 Date: Thu, 3 Nov 2022 04:11:10 +0800 Subject: [PATCH 10/19] fix LooseTimelineTrigger --- ui/raidboss/popup-text.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/raidboss/popup-text.ts b/ui/raidboss/popup-text.ts index ddf97146d7..bb6bd85dda 100644 --- a/ui/raidboss/popup-text.ts +++ b/ui/raidboss/popup-text.ts @@ -72,7 +72,7 @@ export type LocalizedTrigger = export type LoadedTriggerSet = LooseTriggerSet & { filename: string; - timelineTriggers?: Omit[]; + timelineTriggers?: LooseTimelineTrigger[]; triggers?: LooseTrigger[]; }; From a1259cad823fce6dff8102109d2f8cb1cf60353e Mon Sep 17 00:00:00 2001 From: Trim21 Date: Thu, 3 Nov 2022 04:25:24 +0800 Subject: [PATCH 11/19] fix undefined id --- ui/raidboss/popup-text.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/ui/raidboss/popup-text.ts b/ui/raidboss/popup-text.ts index bb6bd85dda..1dcfca6c56 100644 --- a/ui/raidboss/popup-text.ts +++ b/ui/raidboss/popup-text.ts @@ -67,8 +67,8 @@ export type ProcessedTrigger = LooseTrigger & { // a loaded and localized trigger export type LocalizedTrigger = & LooseTrigger - & { id: string; filename: string; output?: Output } - & ({ localRegex: RegExp } | { localNetRegex: RegExp }); + & { filename: string; output?: Output } + & ({ localRegex: RegExp } | { localNetRegex: RegExp } | { disabled: true }); export type LoadedTriggerSet = LooseTriggerSet & { filename: string; @@ -809,10 +809,16 @@ export class PopupText { triggerIndex: number, langSuffix: string, ): LocalizedTrigger | undefined { - const id: string = trigger.id ?? `${set.filename}.triggers[${triggerIndex}]`; + if ('disabled' in trigger && trigger.disabled) { + return { id: trigger.id, filename: set.filename, disabled: true }; + } + + // this ID is to log error message, not as final trigger ID. + const id: string = trigger.id ?? + `${set.filename}.triggers[${triggerIndex}] (undefined id trigger)`; const processedTrigger = { - id: id, + id: trigger.id, filename: set.filename, output: TriggerOutputProxy.makeOutput( trigger, From 263476d2a997e62852bcd8409db4bee1a9c15635 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Thu, 3 Nov 2022 04:27:55 +0800 Subject: [PATCH 12/19] fix always true case --- ui/raidboss/popup-text.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/ui/raidboss/popup-text.ts b/ui/raidboss/popup-text.ts index 1dcfca6c56..a1fa0f9b6e 100644 --- a/ui/raidboss/popup-text.ts +++ b/ui/raidboss/popup-text.ts @@ -775,17 +775,12 @@ export class PopupText { const processedTriggers = []; if (this.options.Debug) { - if (set.filename) - console.log('Loading ' + set.filename); - else - console.log('Loading user triggers for zone'); + console.log('Loading ' + set.filename); } - const setFilename = set.filename ?? 'Unknown'; - if (set.initData) { this.dataInitializers.push({ - file: setFilename, + file: set.filename, func: set.initData, }); } From a85117b1d19696e922379c01e631329ce9faa630 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Thu, 3 Nov 2022 04:31:51 +0800 Subject: [PATCH 13/19] fix always true case --- ui/raidboss/popup-text.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ui/raidboss/popup-text.ts b/ui/raidboss/popup-text.ts index a1fa0f9b6e..9515779255 100644 --- a/ui/raidboss/popup-text.ts +++ b/ui/raidboss/popup-text.ts @@ -717,10 +717,8 @@ export class PopupText { } if (haveZoneId && set.zoneId === undefined) { - // TODO: set.filename will never be undefined or null - const filename = set.filename ? `'${set.filename}'` : '(user file)'; console.error( - `Trigger set has zoneId, but with nothing specified in ${filename}. Did you misspell the ZoneId.ZoneName?`, + `Trigger set has zoneId, but with nothing specified in ${set.filename}. Did you misspell the ZoneId.ZoneName?`, ); return false; } From 0d52a7232bcae64d1519727f437ce82ee03ba8aa Mon Sep 17 00:00:00 2001 From: Trim21 Date: Thu, 3 Nov 2022 05:03:44 +0800 Subject: [PATCH 14/19] read from user set filename first --- ui/raidboss/raidboss_config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/raidboss/raidboss_config.ts b/ui/raidboss/raidboss_config.ts index d28ed88670..8178131106 100644 --- a/ui/raidboss/raidboss_config.ts +++ b/ui/raidboss/raidboss_config.ts @@ -1475,7 +1475,7 @@ const userFileHandler: UserFileCallback = ( } baseOptions.LoadedTriggers.push({ - filename: `${basePath}${name}`, + filename: set?.filename?.toString() ?? `${basePath}${name}`, ...baseTriggerSet, timeline: pureFlattenTimeline(baseTriggerSet, name, files), }); From e9fb6e944d14f74c64eb2f468390b769e00a0ca5 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Thu, 3 Nov 2022 05:05:09 +0800 Subject: [PATCH 15/19] avoid override --- ui/raidboss/raidboss_config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/raidboss/raidboss_config.ts b/ui/raidboss/raidboss_config.ts index 8178131106..1381a35f0b 100644 --- a/ui/raidboss/raidboss_config.ts +++ b/ui/raidboss/raidboss_config.ts @@ -1475,8 +1475,8 @@ const userFileHandler: UserFileCallback = ( } baseOptions.LoadedTriggers.push({ - filename: set?.filename?.toString() ?? `${basePath}${name}`, ...baseTriggerSet, + filename: set?.filename?.toString() ?? `${basePath}${name}`, timeline: pureFlattenTimeline(baseTriggerSet, name, files), }); From 2fec0f4d4a0b412e897fbb4e1c5ed77e88b8e908 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Thu, 3 Nov 2022 09:07:57 +0800 Subject: [PATCH 16/19] wip --- resources/languages.ts | 5 +++++ ui/raidboss/popup-text.ts | 33 +++++++++++++++++++++------------ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/resources/languages.ts b/resources/languages.ts index 55ac8d4443..490abfb7a2 100644 --- a/resources/languages.ts +++ b/resources/languages.ts @@ -4,6 +4,11 @@ export type Lang = typeof languages[number]; export type NonEnLang = Exclude; +/** + * @deprecated remove this after we don't support `netRegexCn` style triggers + */ +export type LegacyLangSuffix = 'En' | 'De' | 'Fr' | 'Ja' | 'Cn' | 'Ko'; + export const langMap: { [lang in Lang]: { [lang in Lang]: string } } = { en: { en: 'English', diff --git a/ui/raidboss/popup-text.ts b/ui/raidboss/popup-text.ts index 9515779255..1f2b61fb6e 100644 --- a/ui/raidboss/popup-text.ts +++ b/ui/raidboss/popup-text.ts @@ -16,13 +16,14 @@ import { EventResponses, LogEvent } from '../../types/event'; import { Job, Role } from '../../types/job'; import { Matches } from '../../types/net_matches'; import { + BaseNetTrigger, DataInitializeFunc, - GeneralNetRegexTrigger, + GeneralNetRegexTrigger, LegacyTrigger, LooseTimelineTrigger, LooseTrigger, LooseTriggerSet, Output, - OutputStrings, + OutputStrings, PartialRegexTrigger, PartialTriggerOutput, RaidbossFileData, RegexTrigger, @@ -57,18 +58,24 @@ export const isRegexTrigger = ( return false; }; -export type ProcessedTrigger = LooseTrigger & { - filename?: string; +// a loaded and localized trigger +export type LocalizedTrigger = + & LooseTrigger + & { + filename: string; + output?: Output; localRegex?: RegExp; localNetRegex?: RegExp; - output?: Output; + disabled?: true; }; -// a loaded and localized trigger -export type LocalizedTrigger = - & LooseTrigger - & { filename: string; output?: Output } - & ({ localRegex: RegExp } | { localNetRegex: RegExp } | { disabled: true }); +export type ProcessedTrigger = Omit & { + filename: string; + output?: Output; + localRegex?: RegExp; + localNetRegex?: RegExp; + disabled?: true; +} export type LoadedTriggerSet = LooseTriggerSet & { filename: string; @@ -209,7 +216,9 @@ const textMap: TextMap = { }, }; -const handleTriggerOverride = (triggers: Array): Array => { +const handleTriggerOverride = ( + triggers: Array, +): Array => { const keep: Array = []; // keep the triggers with id @@ -232,7 +241,7 @@ const handleTriggerOverride = (triggers: Array): A continue; } - const triggerFile = (trigger: ProcessedTrigger) => + const triggerFile = (trigger: { filename?: string }) => trigger.filename ? `'${trigger.filename}'` : 'user override'; const oldFile = triggerFile(newTrigger); const newFile = triggerFile(oldTrigger); From 3ade8168d297f2670f037ed89cae132354fa17d7 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Sat, 5 Nov 2022 00:37:45 +0800 Subject: [PATCH 17/19] wip --- types/trigger.d.ts | 58 ++++++++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/types/trigger.d.ts b/types/trigger.d.ts index ef89857b34..7c5e882723 100644 --- a/types/trigger.d.ts +++ b/types/trigger.d.ts @@ -25,8 +25,8 @@ export type LocaleObject = en: T; } & { - [s in NonEnLang]?: T; - }; + [s in NonEnLang]?: T; +}; export type LocaleText = LocaleObject; @@ -106,10 +106,8 @@ export type TriggerField = Omit, 'type' | 'netRegex'>; +export type BaseTrigger = Omit, + 'type' | 'netRegex'>; type BaseNetTrigger = { id: string; @@ -137,7 +135,7 @@ type BaseNetTrigger = { export type NetRegexTrigger = TriggerTypes extends infer T ? T extends TriggerTypes ? BaseNetTrigger - : never + : never : never; export type GeneralNetRegexTrigger = @@ -191,27 +189,47 @@ export type BaseTriggerSet = { export type TriggerSet = & BaseTriggerSet & (RequiredFieldsAsUnion extends RequiredFieldsAsUnion ? { - initData?: DataInitializeFunc; - } - : { - initData: DataInitializeFunc; - }); + initData?: DataInitializeFunc; + } + : { + initData: DataInitializeFunc; + }); // Less strict type for user triggers + built-in triggers, including deprecated fields. export type LooseTimelineTrigger = Partial>; -export type LooseTrigger = Partial & PartialRegexTrigger>; +/** + * @deprecated + */ +export type LegacyTrigger = { + regexEn?: RegExp; + regexDe?: RegExp; + regexFr?: RegExp; + regexCn?: RegExp; + regexJa?: RegExp; + regexKo?: RegExp; + + // @deprecated + netRegexEn?: RegExp; + netRegexFr?: RegExp; + netRegexDe?: RegExp; + netRegexCn?: RegExp; + netRegexJa?: RegExp; + netRegexKo?: RegExp; +}; + +export type LooseTrigger = Partial & PartialRegexTrigger> export type LooseTriggerSet = & Omit, 'triggers' | 'timelineTriggers'> & { - /** @deprecated Use zoneId instead */ - zoneRegex?: - | RegExp - | { [lang in Lang]?: RegExp }; - triggers?: LooseTrigger[]; - timelineTriggers?: LooseTimelineTrigger[]; - }; + /** @deprecated Use zoneId instead */ + zoneRegex?: + | RegExp + | { [lang in Lang]?: RegExp }; + triggers?: (LooseTrigger & LegacyTrigger)[]; + timelineTriggers?: LooseTimelineTrigger[]; +}; export interface RaidbossFileData { [filename: string]: LooseTriggerSet | string; From e6f9d8fe1b2b888619a54396b37d392f78aa016d Mon Sep 17 00:00:00 2001 From: Trim21 Date: Sat, 5 Nov 2022 03:31:51 +0800 Subject: [PATCH 18/19] code review --- types/trigger.d.ts | 38 +++++++++++++------------ ui/raidboss/popup-text.ts | 49 ++++++++++++++++++--------------- ui/raidboss/raidboss_config.ts | 36 ++++++++---------------- ui/raidboss/raidboss_options.ts | 14 ++-------- ui/raidboss/timeline.ts | 8 +++--- ui/raidboss/timeline_parser.ts | 3 +- 6 files changed, 67 insertions(+), 81 deletions(-) diff --git a/types/trigger.d.ts b/types/trigger.d.ts index 7c5e882723..0674db1d19 100644 --- a/types/trigger.d.ts +++ b/types/trigger.d.ts @@ -25,8 +25,8 @@ export type LocaleObject = en: T; } & { - [s in NonEnLang]?: T; -}; + [s in NonEnLang]?: T; + }; export type LocaleText = LocaleObject; @@ -106,8 +106,10 @@ export type TriggerField = Omit, - 'type' | 'netRegex'>; +export type BaseTrigger = Omit< + BaseNetTrigger, + 'type' | 'netRegex' +>; type BaseNetTrigger = { id: string; @@ -135,7 +137,7 @@ type BaseNetTrigger = { export type NetRegexTrigger = TriggerTypes extends infer T ? T extends TriggerTypes ? BaseNetTrigger - : never + : never : never; export type GeneralNetRegexTrigger = @@ -189,11 +191,11 @@ export type BaseTriggerSet = { export type TriggerSet = & BaseTriggerSet & (RequiredFieldsAsUnion extends RequiredFieldsAsUnion ? { - initData?: DataInitializeFunc; - } - : { - initData: DataInitializeFunc; - }); + initData?: DataInitializeFunc; + } + : { + initData: DataInitializeFunc; + }); // Less strict type for user triggers + built-in triggers, including deprecated fields. export type LooseTimelineTrigger = Partial>; @@ -218,18 +220,18 @@ export type LegacyTrigger = { netRegexKo?: RegExp; }; -export type LooseTrigger = Partial & PartialRegexTrigger> +export type LooseTrigger = Partial & PartialRegexTrigger>; export type LooseTriggerSet = & Omit, 'triggers' | 'timelineTriggers'> & { - /** @deprecated Use zoneId instead */ - zoneRegex?: - | RegExp - | { [lang in Lang]?: RegExp }; - triggers?: (LooseTrigger & LegacyTrigger)[]; - timelineTriggers?: LooseTimelineTrigger[]; -}; + /** @deprecated Use zoneId instead */ + zoneRegex?: + | RegExp + | { [lang in Lang]?: RegExp }; + triggers?: (LooseTrigger & LegacyTrigger)[]; + timelineTriggers?: LooseTimelineTrigger[]; + }; export interface RaidbossFileData { [filename: string]: LooseTriggerSet | string; diff --git a/ui/raidboss/popup-text.ts b/ui/raidboss/popup-text.ts index 1f2b61fb6e..dfb31adac4 100644 --- a/ui/raidboss/popup-text.ts +++ b/ui/raidboss/popup-text.ts @@ -16,14 +16,13 @@ import { EventResponses, LogEvent } from '../../types/event'; import { Job, Role } from '../../types/job'; import { Matches } from '../../types/net_matches'; import { - BaseNetTrigger, DataInitializeFunc, - GeneralNetRegexTrigger, LegacyTrigger, + GeneralNetRegexTrigger, + LegacyTrigger, LooseTimelineTrigger, LooseTrigger, - LooseTriggerSet, Output, - OutputStrings, PartialRegexTrigger, + OutputStrings, PartialTriggerOutput, RaidbossFileData, RegexTrigger, @@ -35,6 +34,7 @@ import { TriggerField, TriggerOutput, } from '../../types/trigger'; +import { ConfigLooseTriggerSet } from '../config/config'; import AutoplayHelper from './autoplay_helper'; import BrowserTTSEngine from './browser_tts_engine'; @@ -62,27 +62,26 @@ export const isRegexTrigger = ( export type LocalizedTrigger = & LooseTrigger & { - filename: string; + filename?: string; + output?: Output; + localRegex?: RegExp; + localNetRegex?: RegExp; + disabled?: boolean; + }; + +export type ProcessedTimelineTrigger = LooseTimelineTrigger & { output?: Output; - localRegex?: RegExp; - localNetRegex?: RegExp; - disabled?: true; }; export type ProcessedTrigger = Omit & { - filename: string; + filename?: string; output?: Output; localRegex?: RegExp; localNetRegex?: RegExp; - disabled?: true; -} - -export type LoadedTriggerSet = LooseTriggerSet & { - filename: string; - timelineTriggers?: LooseTimelineTrigger[]; - triggers?: LooseTrigger[]; }; +type LoadedTriggerSet = ConfigLooseTriggerSet & { filename: string }; + // There should be (at most) six lines of instructions. const raidbossInstructions: { [lang in Lang]: string[] } = { en: [ @@ -574,16 +573,16 @@ export class PopupText { continue; if (typeof set !== 'object') { - console.log('Unexpected TriggerSet from ' + filename + ', expected an array'); + console.log(`Unexpected TriggerSet from ${filename}, expected an array`); continue; } if (!set.triggers) { - console.log('Unexpected TriggerSet from ' + filename + ', expected a triggers'); + console.log(`Unexpected TriggerSet from ${filename}, expected a triggers`); continue; } if (typeof set.triggers !== 'object' || !(set.triggers.length >= 0)) { console.log( - 'Unexpected TriggerSet from ' + filename + ', expected triggers to be an array', + `Unexpected TriggerSet from ${filename}, expected triggers to be an array`, ); continue; } @@ -595,7 +594,13 @@ export class PopupText { } // User triggers must come last so that they override built-in files. - triggerSets.push(...this.options.LoadedTriggers); + triggerSets.push(...this.options.Triggers.map((t) => { + // TODO: filename can't be undefined, but for type checking + return { + filename: t.filename ?? '(default-file-name)', + ...t, + }; + })); return triggerSets; } @@ -640,7 +645,7 @@ export class PopupText { } }.bind(this); - const timelineTriggers: ProcessedTrigger[] = []; + const timelineTriggers: ProcessedTimelineTrigger[] = []; const triggers: LocalizedTrigger[] = []; for (const set of this.collectedTriggerSets) { @@ -903,7 +908,7 @@ export class PopupText { console.error(`Trigger ${id}: missing regex and netRegex`); } - private ProcessTimelineTrigger(trigger: LooseTimelineTrigger): ProcessedTrigger { + private ProcessTimelineTrigger(trigger: LooseTimelineTrigger): ProcessedTimelineTrigger { return { ...trigger, output: TriggerOutputProxy.makeOutput( diff --git a/ui/raidboss/raidboss_config.ts b/ui/raidboss/raidboss_config.ts index 1381a35f0b..342936e3bd 100644 --- a/ui/raidboss/raidboss_config.ts +++ b/ui/raidboss/raidboss_config.ts @@ -1333,7 +1333,7 @@ class RaidbossConfigurator { rawTriggers.timeline.push(...triggerSet.timelineTriggers); if (!triggerSet.loaded && triggerSet.filename) - flattenTimelinePlace(triggerSet, triggerSet.filename, timelineFiles); + flattenTimeline(triggerSet, triggerSet.filename, timelineFiles); item.triggers = {}; for (const [key, triggerArr] of Object.entries(rawTriggers)) { @@ -1416,8 +1416,8 @@ class RaidbossConfigurator { } } -const pureFlattenTimeline = ( - set: Readonly, +const flattenTimeline = ( + set: ConfigLooseTriggerSet, filename: string, files: { [filename: string]: string }, ) => { @@ -1430,6 +1430,7 @@ const pureFlattenTimeline = ( const dir = filename.slice(0, Math.max(0, lastIndex + 1)); const timelineFile = `${dir}${set.timelineFile}`; + delete set.timelineFile; if (!(timelineFile in files)) { console.log(`ERROR: '${filename}' specifies non-existent timeline file '${timelineFile}'.`); @@ -1437,19 +1438,7 @@ const pureFlattenTimeline = ( } // set.timeline is processed recursively. - return [set.timeline, files[timelineFile]]; -}; - -const flattenTimelinePlace = ( - set: ConfigLooseTriggerSet, - filename: string, - files: { [filename: string]: string }, -) => { - const timeline = pureFlattenTimeline(set, filename, files); - if (timeline !== undefined) { - // set.timeline is processed recursively. - set.timeline = timeline; - } + set.timeline = [set.timeline, files[timelineFile]]; }; // Raidboss needs to do some extra processing of user files. @@ -1465,20 +1454,17 @@ const userFileHandler: UserFileCallback = ( return; baseOptions.Triggers ??= []; - baseOptions.LoadedTriggers ??= []; - - for (const baseTriggerSet of baseOptions.Triggers) { - const set: ConfigLooseTriggerSet = baseTriggerSet; + for (const set of baseOptions.Triggers) { if (set.loaded) { continue; } - baseOptions.LoadedTriggers.push({ - ...baseTriggerSet, - filename: set?.filename?.toString() ?? `${basePath}${name}`, - timeline: pureFlattenTimeline(baseTriggerSet, name, files), - }); + // `filename` here is just cosmetic for better debug printing to make it more clear + // where a trigger or an override is coming from. + set.filename ??= `${basePath}${name}`; + + flattenTimeline(set, name, files); set.loaded = true; } diff --git a/ui/raidboss/raidboss_options.ts b/ui/raidboss/raidboss_options.ts index 386fb64189..e9f77cb8a4 100644 --- a/ui/raidboss/raidboss_options.ts +++ b/ui/raidboss/raidboss_options.ts @@ -2,14 +2,8 @@ import { Lang } from '../../resources/languages'; import UserConfig from '../../resources/user_config'; import { BaseOptions, RaidbossData } from '../../types/data'; import { Matches } from '../../types/net_matches'; -import { - LooseTriggerSet, - TriggerAutoConfig, - TriggerField, - TriggerOutput, -} from '../../types/trigger'; - -import { LoadedTriggerSet } from './popup-text'; +import { TriggerAutoConfig, TriggerField, TriggerOutput } from '../../types/trigger'; +import { ConfigLooseTriggerSet } from '../config/config'; // This file defines the base options that raidboss expects to see. @@ -54,8 +48,7 @@ type RaidbossNonConfigOptions = { PerZoneTimelineConfig: PerZoneTimelineConfig; // loaded builtin triggers and user triggers - Triggers: LooseTriggerSet[]; - LoadedTriggers: LoadedTriggerSet[]; + Triggers: ConfigLooseTriggerSet[]; PlayerNameOverride?: string; IsRemoteRaidboss: boolean; @@ -82,7 +75,6 @@ const defaultRaidbossNonConfigOptions: RaidbossNonConfigOptions = { PerZoneTimelineConfig: {}, Triggers: [], - LoadedTriggers: [], IsRemoteRaidboss: false, diff --git a/ui/raidboss/timeline.ts b/ui/raidboss/timeline.ts index 5fc6ba75a4..c3a1f13604 100644 --- a/ui/raidboss/timeline.ts +++ b/ui/raidboss/timeline.ts @@ -6,7 +6,7 @@ import { LogEvent } from '../../types/event'; import { CactbotBaseRegExp } from '../../types/net_trigger'; import { LooseTimelineTrigger, RaidbossFileData } from '../../types/trigger'; -import { PopupTextGenerator } from './popup-text'; +import { PopupTextGenerator, ProcessedTimelineTrigger, ProcessedTrigger } from './popup-text'; import { RaidbossOptions } from './raidboss_options'; import { Event, @@ -670,7 +670,7 @@ export class TimelineUI { } public OnTrigger( - trigger: LooseTimelineTrigger, + trigger: ProcessedTrigger, matches: RegExpExecArray | null, currentTime: number, ): void { @@ -777,7 +777,7 @@ export class TimelineController { timelineFiles: string[], timelines: string[], replacements: TimelineReplacement[], - triggers: LooseTimelineTrigger[], + triggers: ProcessedTimelineTrigger[], styles: TimelineStyle[], zoneId: number, ): void { @@ -824,7 +824,7 @@ export class TimelineLoader { timelineFiles: string[], timelines: string[], replacements: TimelineReplacement[], - triggers: LooseTimelineTrigger[], + triggers: ProcessedTimelineTrigger[], styles: TimelineStyle[], zoneId: number, ): void { diff --git a/ui/raidboss/timeline_parser.ts b/ui/raidboss/timeline_parser.ts index fc3d24babe..e83e3bcef0 100644 --- a/ui/raidboss/timeline_parser.ts +++ b/ui/raidboss/timeline_parser.ts @@ -4,6 +4,7 @@ import Regexes from '../../resources/regexes'; import { translateRegex, translateText } from '../../resources/translations'; import { LooseTimelineTrigger, TriggerAutoConfig } from '../../types/trigger'; +import { ProcessedTimelineTrigger } from './popup-text'; import defaultOptions, { RaidbossOptions, TimelineConfig } from './raidboss_options'; export type TimelineReplacement = { @@ -59,7 +60,7 @@ type ParsedTriggerText = { secondsBefore?: number; text?: string; matches: RegExpExecArray | null; - trigger: LooseTimelineTrigger; + trigger: ProcessedTimelineTrigger; }; export type ParsedText = ParsedPopupText | ParsedTriggerText; From ecbffefde631541fb0915731ea29111220d558d2 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Sat, 5 Nov 2022 09:23:27 +0800 Subject: [PATCH 19/19] Update ui/raidboss/raidboss_options.ts Co-authored-by: Adrienne Walker --- ui/raidboss/raidboss_options.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/raidboss/raidboss_options.ts b/ui/raidboss/raidboss_options.ts index e9f77cb8a4..5385e7b7b0 100644 --- a/ui/raidboss/raidboss_options.ts +++ b/ui/raidboss/raidboss_options.ts @@ -47,7 +47,7 @@ type RaidbossNonConfigOptions = { PerTriggerOptions: PerTriggerOptions; PerZoneTimelineConfig: PerZoneTimelineConfig; - // loaded builtin triggers and user triggers + // user triggers Triggers: ConfigLooseTriggerSet[]; PlayerNameOverride?: string;