diff --git a/test/helper/test_trigger.ts b/test/helper/test_trigger.ts index 780f6e50df5..3a50f23c249 100644 --- a/test/helper/test_trigger.ts +++ b/test/helper/test_trigger.ts @@ -40,6 +40,9 @@ const emptyPartyTracker = new PartyTracker(raidbossOptions); const getFakeRaidbossData = (triggerSet?: LooseTriggerSet): RaidbossData => { return { me: '', + meId: '10001234', + zoneName: '', + zoneId: -1, job: 'NONE', role: 'none', party: emptyPartyTracker, @@ -50,6 +53,10 @@ const getFakeRaidbossData = (triggerSet?: LooseTriggerSet): RaidbossData => { options: raidbossOptions, inCombat: true, triggerSetConfig: {}, + timeline: { + currentTime: () => 0, + jumpTo: (_label) => 0, + }, ShortName: (x: string | undefined) => x ?? '', StopCombat: (): void => {/* noop */}, ParseLocaleFloat: () => 0, diff --git a/types/data.d.ts b/types/data.d.ts index b69c24a8241..e86a455888c 100644 --- a/types/data.d.ts +++ b/types/data.d.ts @@ -24,6 +24,9 @@ export interface BaseOptions { export interface RaidbossData { job: Job; me: string; + meId: string; + zoneName: string; + zoneId: number; role: Role; party: PartyTracker; lang: Lang; @@ -33,6 +36,10 @@ export interface RaidbossData { options: BaseOptions; inCombat: boolean; triggerSetConfig: { [key: string]: ConfigValue }; + timeline: { + jumpTo: (label: string) => void; + currentTime: () => number; + }; /** @deprecated Use data.party.member instead */ ShortName: (x?: string) => string; StopCombat: () => void; diff --git a/ui/raidboss/data/00-misc/test.ts b/ui/raidboss/data/00-misc/test.ts index 2d9603bcaed..41dd96b5cc2 100644 --- a/ui/raidboss/data/00-misc/test.ts +++ b/ui/raidboss/data/00-misc/test.ts @@ -508,6 +508,35 @@ const triggerSet: TriggerSet = { }, }, }, + { + id: 'Test Trigger Timeline Jump', + type: 'GameLog', + netRegex: { + line: 'cactbot test timeline jump.*?', + code: Util.gameLogCodes.echo, + capture: false, + }, + run: (data) => { + data.timeline.jumpTo('timeline-jump'); + }, + }, + { + id: 'Test Timeline Get Current Time', + type: 'GameLog', + netRegex: { + line: 'cactbot test current time.*?', + code: Util.gameLogCodes.echo, + capture: false, + }, + infoText: (data, _matches, output) => { + return output.text!({ time: data.timeline.currentTime() }); + }, + outputStrings: { + text: { + en: 'Current Timeline Time: ${time}', + }, + }, + }, ], timelineReplace: [ { @@ -638,6 +667,7 @@ const triggerSet: TriggerSet = { }, { locale: 'cn', + missingTranslations: true, replaceSync: { 'You bid farewell to the striking dummy': '.*向木人告别', 'You bow courteously to the striking dummy': '.*恭敬地对木人行礼', @@ -678,6 +708,7 @@ const triggerSet: TriggerSet = { }, { locale: 'tc', + missingTranslations: true, replaceSync: { 'You bid farewell to the striking dummy': '.*向木人告别', 'You bow courteously to the striking dummy': '.*恭敬地對木人行禮', @@ -718,6 +749,7 @@ const triggerSet: TriggerSet = { }, { locale: 'ko', + missingTranslations: true, replaceSync: { 'You bid farewell to the striking dummy': '.*나무인형에게 작별 인사를 합니다', 'You bow courteously to the striking dummy': '.*나무인형에게 공손하게 인사합니다', diff --git a/ui/raidboss/data/00-misc/test.txt b/ui/raidboss/data/00-misc/test.txt index ae84dcdd165..2ab4b19bd4d 100644 --- a/ui/raidboss/data/00-misc/test.txt +++ b/ui/raidboss/data/00-misc/test.txt @@ -42,6 +42,7 @@ hideall "--sync--" 103 "Three" 104 "Four" 106 "Six" +107 label "timeline-jump" 110 "Ten" #duration 100 115 "Fifteen" 118 "Force Jump Three" GameLog { code: "0038", line: "test sync3.*?" } window 10,10 forcejump "three" diff --git a/ui/raidboss/data/07-dt/raid/r10n.ts b/ui/raidboss/data/07-dt/raid/r10n.ts index a2fad685b35..10ff1505fae 100644 --- a/ui/raidboss/data/07-dt/raid/r10n.ts +++ b/ui/raidboss/data/07-dt/raid/r10n.ts @@ -308,7 +308,7 @@ const triggerSet: TriggerSet = { data0: '1[0-9A-F]{7}', capture: true, }, - condition: (data, matches) => data.me === data.party?.idToName_?.[matches.data0], + condition: (data, matches) => data.meId === matches.data0, infoText: (_data, _matches, output) => output.text!(), outputStrings: { text: { diff --git a/ui/raidboss/data/07-dt/trial/doomtrain.ts b/ui/raidboss/data/07-dt/trial/doomtrain.ts index 91be5d18829..83a5539cdfc 100644 --- a/ui/raidboss/data/07-dt/trial/doomtrain.ts +++ b/ui/raidboss/data/07-dt/trial/doomtrain.ts @@ -224,7 +224,7 @@ const triggerSet: TriggerSet = { id: 'Doomtrain Add Train Direction Predictor', type: 'HeadMarker', netRegex: { id: '0282', data0: '1[0-9A-F]{7}', capture: true }, - condition: (data, matches) => data.me === data.party?.idToName_?.[matches.data0], + condition: (data, matches) => data.meId === matches.data0, durationSeconds: 7.6, countdownSeconds: 7.6, alertText: (data, matches, output) => { diff --git a/ui/raidboss/data/07-dt/trial/necron-ex.ts b/ui/raidboss/data/07-dt/trial/necron-ex.ts index d771355c2e0..7839b15780f 100644 --- a/ui/raidboss/data/07-dt/trial/necron-ex.ts +++ b/ui/raidboss/data/07-dt/trial/necron-ex.ts @@ -76,10 +76,8 @@ const triggerSet: TriggerSet = { id: 'NecronEx Blue Shockwave', type: 'HeadMarker', netRegex: { id: '0267', capture: true }, - // Annoyingly, the "target" of this headmarker is the boss, and the actual player ID is stored - // in `data0`. So we need to map back to party info to determine if target is self or another condition: (data, matches) => { - if (data.me === data.party?.idToName_?.[matches.data0]) + if (data.meId === matches.data0) return true; return data.role === 'tank'; }, diff --git a/ui/raidboss/emulator/overrides/RaidEmulatorTimeline.ts b/ui/raidboss/emulator/overrides/RaidEmulatorTimeline.ts index 70a3d45f848..c6be964686f 100644 --- a/ui/raidboss/emulator/overrides/RaidEmulatorTimeline.ts +++ b/ui/raidboss/emulator/overrides/RaidEmulatorTimeline.ts @@ -59,6 +59,16 @@ export default class RaidEmulatorTimeline extends Timeline { super._OnUpdateTimer(currentTime); } + public override JumpTo(label: string, currentTime: number): void { + // Override JumpTo to use the emulated timestamp, same logic as _OnUpdateTimer + const lastLogTimestamp = this.emulator?.currentEncounter?.encounter + .logLines.slice(-1)[0]?.timestamp; + if (lastLogTimestamp && currentTime > lastLogTimestamp) + currentTime = this.emulator?.currentLogTime ?? currentTime; + + super.JumpTo(label, currentTime); + } + override _ScheduleUpdate(_fightNow: number): void { // Override } diff --git a/ui/raidboss/popup-text.ts b/ui/raidboss/popup-text.ts index 8e68f7f338e..9344afa9459 100644 --- a/ui/raidboss/popup-text.ts +++ b/ui/raidboss/popup-text.ts @@ -581,6 +581,7 @@ export class PopupText { protected readonly kMaxRowsOfText = 2; protected data: RaidbossData; protected me = ''; + protected meId = ''; protected job: Job = 'NONE'; protected role: Role = 'none'; protected triggerSets: ProcessedTriggerSet[] = []; @@ -1033,6 +1034,7 @@ export class PopupText { OnJobChange(e: PlayerChangedDetail): void { this.me = e.detail.name; + this.meId = e.detail.id.toString(16).toUpperCase(); this.job = e.detail.job; this.role = Util.jobToRole(this.job); this.ReloadTimelines(); @@ -1738,6 +1740,9 @@ export class PopupText { // make all this style consistent, sorry. const data: RaidbossData = { me: this.me, + meId: this.meId, + zoneName: this.zoneName, + zoneId: this.zoneId, job: this.job, role: this.role, party: this.partyTracker, @@ -1748,6 +1753,10 @@ export class PopupText { options: this.options, inCombat: this.inCombat, triggerSetConfig: this.triggerSetConfig, + timeline: { + currentTime: () => this.timelineLoader.CurrentTime(), + jumpTo: (label: string) => this.timelineLoader.JumpTo(label, Date.now()), + }, ShortName: (name?: string) => Util.shortName(name, this.options.PlayerNicks), StopCombat: () => this.SetInCombat(false), ParseLocaleFloat: parseFloat, diff --git a/ui/raidboss/raidboss_config.ts b/ui/raidboss/raidboss_config.ts index c005d154929..7f6da1f5f51 100644 --- a/ui/raidboss/raidboss_config.ts +++ b/ui/raidboss/raidboss_config.ts @@ -1328,6 +1328,9 @@ class RaidbossConfigurator { const baseFakeData: RaidbossData = { me: '', + meId: '10001234', + zoneName: '', + zoneId: -1, job: 'NONE', role: 'none', party: new PartyTracker(raidbossOptions), @@ -1336,6 +1339,10 @@ class RaidbossConfigurator { options: this.base.configOptions, inCombat: true, triggerSetConfig: {}, + timeline: { + currentTime: () => 0, + jumpTo: (_label) => 0, + }, ShortName: (x?: string) => x ?? '???', StopCombat: () => {/* noop */}, ParseLocaleFloat: parseFloat, diff --git a/ui/raidboss/timeline.ts b/ui/raidboss/timeline.ts index e3f4635be8f..c01899b6cf1 100644 --- a/ui/raidboss/timeline.ts +++ b/ui/raidboss/timeline.ts @@ -145,6 +145,7 @@ export class Timeline { public syncStarts: Sync[]; public syncEnds: Sync[]; public forceJumps: Sync[]; + public labelToTime: { [name: string]: number }; public timebase = 0; @@ -196,6 +197,8 @@ export class Timeline { this.syncEnds = []; // Sorted by event occurrence time. this.forceJumps = []; + // A set of label names to their sync times + this.labelToTime = {}; this.LoadFile(text, triggers, styles); this.Stop(); @@ -216,6 +219,7 @@ export class Timeline { this.syncStarts = parsed.syncStarts; this.syncEnds = parsed.syncEnds; this.forceJumps = parsed.forceJumps; + this.labelToTime = parsed.labelToTime; } public Stop(): void { @@ -333,6 +337,15 @@ export class Timeline { } } + public JumpTo(label: string, currentTime: number): void { + const time = this.labelToTime[label]; + + if (time === undefined) + return; + + this.SyncTo(time, currentTime); + } + private _AdvanceTimeTo(fightNow: number): void { // This function advances time to fightNow without processing any events. let event = this.events[this.nextEventState.index]; @@ -791,6 +804,13 @@ export class TimelineController { public IsReady(): boolean { return this.timelines !== null; } + + public CurrentTime(): number { + return this.activeTimeline?.timebase ?? 0; + } + public JumpTo(label: string, currentTime: number): void { + this.activeTimeline?.JumpTo(label, currentTime); + } } export class TimelineLoader { @@ -823,4 +843,11 @@ export class TimelineLoader { public StopCombat(): void { this.timelineController.SetInCombat(false); } + + public CurrentTime(): number { + return this.timelineController.CurrentTime(); + } + public JumpTo(label: string, currentTime: number): void { + this.timelineController.JumpTo(label, currentTime); + } } diff --git a/ui/raidboss/timeline_parser.ts b/ui/raidboss/timeline_parser.ts index e5a8e3fd9a1..0b92a855cc3 100644 --- a/ui/raidboss/timeline_parser.ts +++ b/ui/raidboss/timeline_parser.ts @@ -133,6 +133,7 @@ export type Sync = { lineNumber: number; event: Event; jump?: number; + label?: string; // TODO: could consider "maybe" jumps here to say "Ability?". // TODO: also it'd be nice to be able to `forcejump` with out a `sync //` jumpType?: 'force' | 'normal'; @@ -205,7 +206,7 @@ export class TimelineParser { // Sorted by line. public errors: Error[] = []; // Map of encountered label names to their time. - private labelToTime: { [name: string]: number } = {}; + public labelToTime: { [name: string]: number } = {}; // Map of encountered syncs to the label they are jumping to. private labelToSync: { [name: string]: Sync[] } = {};