diff --git a/ui/raidboss/emulator/data/AnalyzedEncounter.ts b/ui/raidboss/emulator/data/AnalyzedEncounter.ts index 300f5c64d82..9487f875ef7 100644 --- a/ui/raidboss/emulator/data/AnalyzedEncounter.ts +++ b/ui/raidboss/emulator/data/AnalyzedEncounter.ts @@ -33,7 +33,6 @@ type Perspectives = { [id: string]: Perspective }; export default class AnalyzedEncounter extends EventBus { perspectives: Perspectives = {}; - regexCache: LineRegExpCache | undefined; constructor( public options: RaidbossOptions, public encounter: Encounter, @@ -110,24 +109,43 @@ export default class AnalyzedEncounter extends EventBus { }); } + checkPartyMember(id: string): boolean { + const partyMember = this.encounter.combatantTracker?.combatants[id]; + + if (!partyMember) + return false; + + const initState = partyMember?.nextState(0); + + if (initState.Job === 0) { + this.perspectives[id] = { + initialData: {}, + triggers: [], + }; + return false; + } + + return true; + } + async analyze(): Promise { - // @TODO: Make this run in parallel sometime in the future, since it could be really slow? + const regexCache: LineRegExpCache = new Map(); + if (this.encounter.combatantTracker) { - for (const id of this.encounter.combatantTracker.partyMembers) - await this.analyzeFor(id); - } + const partyMembers = this.encounter.combatantTracker.partyMembers; + const batchSize = 24; - // Free up this memory - delete this.regexCache; + for (let i = 0; i < partyMembers.length; i += batchSize) { + const batch = partyMembers.slice(i, i + batchSize); + await this.analyzeFor(batch, regexCache); + } + } return this.dispatch('analyzed'); } - async analyzeFor(id: string): Promise { - if (!this.encounter.combatantTracker) - return; + async analyzeFor(partyMembers: string[], regexCache: LineRegExpCache): Promise { let currentLogIndex = 0; - const partyMember = this.encounter.combatantTracker.combatants[id]; const getCurLogLine = (): LineEvent => { const line = this.encounter.logLines[currentLogIndex]; @@ -136,18 +154,7 @@ export default class AnalyzedEncounter extends EventBus { return line; }; - if (!partyMember) - return; - - const initState = partyMember?.nextState(0); - - if (initState.Job === 0) { - this.perspectives[id] = { - initialData: {}, - triggers: [], - }; - return; - } + const validPartyMembers = partyMembers.filter((id) => this.checkPartyMember(id)); const timelineUI = new RaidEmulatorAnalysisTimelineUI(this.options); const timelineController = new RaidEmulatorTimelineController( @@ -157,79 +164,94 @@ export default class AnalyzedEncounter extends EventBus { ); timelineController.bindTo(this.emulator); - const popupText = new PopupTextAnalysis( - this.options, - new TimelineLoader(timelineController), - raidbossFileData, - ); + type PlayerContext = { + popupText: PopupTextAnalysis; + id: string; + }; - if (this.regexCache) - popupText.regexCache = this.regexCache; + const partyContext: PlayerContext[] = validPartyMembers.map((id) => { + const popupText = new PopupTextAnalysis( + this.options, + new TimelineLoader(timelineController), + raidbossFileData, + regexCache, + ); - const generator = new PopupTextGenerator(popupText); - timelineUI.SetPopupTextInterface(generator); + const generator = new PopupTextGenerator(popupText); + timelineUI.SetPopupTextInterface(generator); - timelineController.SetPopupTextInterface(generator); + timelineController.SetPopupTextInterface(generator); - this.selectPerspective(id, popupText); + if (timelineController.activeTimeline?.ui) { + timelineController.activeTimeline.ui.OnTrigger = (trigger: LooseTrigger, matches) => { + const currentLine = this.encounter.logLines[currentLogIndex]; + if (!currentLine) + throw new UnreachableCode(); - if (timelineController.activeTimeline?.ui) { - timelineController.activeTimeline.ui.OnTrigger = (trigger: LooseTrigger, matches) => { - const currentLine = this.encounter.logLines[currentLogIndex]; - if (!currentLine) + const resolver = popupText.currentResolver = new Resolver({ + initialData: EmulatorCommon.cloneData(popupText.getData()), + suppressed: false, + executed: false, + }); + resolver.triggerHelper = popupText._onTriggerInternalGetHelper( + trigger, + matches?.groups ?? {}, + currentLine?.timestamp, + ); + popupText.triggerResolvers.push(resolver); + + popupText.OnTrigger(trigger, matches, currentLine.timestamp); + + resolver.setFinal(() => { + // Get the current log line when the callback is executed instead of the line + // when the trigger initially fires + const resolvedLine = getCurLogLine(); + resolver.status.finalData = EmulatorCommon.cloneData(popupText.getData()); + delete resolver.triggerHelper?.resolver; + if (popupText.callback) { + popupText.callback( + resolvedLine, + resolver.triggerHelper, + resolver.status, + popupText.getData(), + ); + } + }); + }; + } + + popupText.callback = (log, triggerHelper, currentTriggerStatus) => { + const perspective = this.perspectives[id]; + if (!perspective || !triggerHelper) throw new UnreachableCode(); - const resolver = popupText.currentResolver = new Resolver({ - initialData: EmulatorCommon.cloneData(popupText.getData()), - suppressed: false, - executed: false, - }); - resolver.triggerHelper = popupText._onTriggerInternalGetHelper( - trigger, - matches?.groups ?? {}, - currentLine?.timestamp, - ); - popupText.triggerResolvers.push(resolver); - - popupText.OnTrigger(trigger, matches, currentLine.timestamp); - - resolver.setFinal(() => { - // Get the current log line when the callback is executed instead of the line - // when the trigger initially fires - const resolvedLine = getCurLogLine(); - resolver.status.finalData = EmulatorCommon.cloneData(popupText.getData()); - delete resolver.triggerHelper?.resolver; - if (popupText.callback) { - popupText.callback( - resolvedLine, - resolver.triggerHelper, - resolver.status, - popupText.getData(), - ); - } + perspective.triggers.push({ + triggerHelper: triggerHelper, + status: currentTriggerStatus, + logLine: log, + resolvedOffset: log.timestamp - this.encounter.startTimestamp, }); }; - } + popupText.triggerResolvers = []; - popupText.callback = (log, triggerHelper, currentTriggerStatus) => { - const perspective = this.perspectives[id]; - if (!perspective || !triggerHelper) - throw new UnreachableCode(); + this.selectPerspective(id, popupText); - perspective.triggers.push({ - triggerHelper: triggerHelper, - status: currentTriggerStatus, - logLine: log, - resolvedOffset: log.timestamp - this.encounter.startTimestamp, - }); - }; - popupText.triggerResolvers = []; + return { + popupText: popupText, + id: id, + }; + }); - this.perspectives[id] = { - initialData: EmulatorCommon.cloneData(popupText.getData(), []), - triggers: [], - finalData: popupText.getData(), - }; + for (const ctx of partyContext) { + const popupText = ctx.popupText; + const id = ctx.id; + + this.perspectives[id] = { + initialData: EmulatorCommon.cloneData(popupText.getData(), []), + triggers: [], + finalData: popupText.getData(), + }; + } for (; currentLogIndex < this.encounter.logLines.length; ++currentLogIndex) { const log = this.encounter.logLines[currentLogIndex]; @@ -237,18 +259,24 @@ export default class AnalyzedEncounter extends EventBus { throw new UnreachableCode(); await this.dispatch('analyzeLine', log); - const combatant = this.encounter?.combatantTracker?.combatants[id]; + for (const ctx of partyContext) { + const popupText = ctx.popupText; + const id = ctx.id; + + const combatant = this.encounter?.combatantTracker?.combatants[id]; + + if (combatant && combatant.hasState(log.timestamp)) { + this.updateState(combatant, log.timestamp, popupText); + } - if (combatant && combatant.hasState(log.timestamp)) - this.updateState(combatant, log.timestamp, popupText); + await popupText.onEmulatorLog([log], getCurLogLine); + } this.watchCombatantsOverride.tick(log.timestamp); - await popupText.onEmulatorLog([log], getCurLogLine); timelineController.onEmulatorLogEvent([log]); } this.watchCombatantsOverride.clear(); timelineUI.stop(); - this.regexCache = popupText.regexCache; } } diff --git a/ui/raidboss/emulator/data/PopupTextAnalysis.ts b/ui/raidboss/emulator/data/PopupTextAnalysis.ts index 5a398bf0641..f0de7224906 100644 --- a/ui/raidboss/emulator/data/PopupTextAnalysis.ts +++ b/ui/raidboss/emulator/data/PopupTextAnalysis.ts @@ -1,4 +1,5 @@ import { UnreachableCode } from '../../../../resources/not_reached'; +import { RaidbossData } from '../../../../types/data'; import { EventResponses, LogEvent } from '../../../../types/event'; import { Matches } from '../../../../types/net_matches'; import { LooseTrigger, RaidbossFileData } from '../../../../types/trigger'; @@ -15,7 +16,7 @@ type ResolverFunc = () => void; export interface ResolverStatus { responseType?: string; responseLabel?: string; - initialData: DataType; + initialData?: DataType; finalData?: DataType; condition?: boolean; response?: undefined; @@ -30,6 +31,30 @@ type EmulatorTriggerHelper = TriggerHelper & { resolver?: Resolver; }; +const dataPropsToExcludeMap: { [key in keyof RaidbossData]: boolean } = { + job: true, + me: true, + role: true, + party: true, + lang: true, + parserLang: true, + displayLang: true, + currentHP: true, + options: true, + inCombat: true, + triggerSetConfig: true, + ShortName: true, + StopCombat: true, + ParseLocaleFloat: true, + CanStun: true, + CanSilence: true, + CanSleep: true, + CanCleanse: true, + CanFeint: true, + CanAddle: true, +}; +const dataPropsToExclude = Object.keys(dataPropsToExcludeMap); + export class Resolver { private promise?: Promise; private run?: ResolverFunc; @@ -103,9 +128,10 @@ export default class PopupTextAnalysis extends StubbedPopupText { options: RaidbossOptions, timelineLoader: TimelineLoader, raidbossFileData: RaidbossFileData, + regexCache: LineRegExpCache, ) { super(options, timelineLoader, raidbossFileData); - this.regexCache = new Map(); + this.regexCache = regexCache; this.ttsSay = (_text: string) => { return; }; @@ -152,7 +178,7 @@ export default class PopupTextAnalysis extends StubbedPopupText { continue; const resolver = this.currentResolver = new Resolver({ - initialData: EmulatorCommon.cloneData(this.data), + initialData: EmulatorCommon.cloneData(this.data, dataPropsToExclude), suppressed: false, executed: false, }); @@ -161,7 +187,11 @@ export default class PopupTextAnalysis extends StubbedPopupText { resolver.setFinal(() => { const currentLine = getCurrentLogLine(); - resolver.status.finalData = EmulatorCommon.cloneData(this.data); + const finalData = EmulatorCommon.cloneData(this.data, dataPropsToExclude); + if (JSON.stringify(finalData) !== JSON.stringify(resolver.status.initialData)) + resolver.status.finalData = finalData; + else + delete resolver.status.initialData; delete resolver.triggerHelper?.resolver; if (this.callback) this.callback(currentLine, resolver.triggerHelper, resolver.status, this.data); @@ -185,7 +215,7 @@ export default class PopupTextAnalysis extends StubbedPopupText { } if (r !== false) { const resolver = this.currentResolver = new Resolver({ - initialData: EmulatorCommon.cloneData(this.data), + initialData: EmulatorCommon.cloneData(this.data, dataPropsToExclude), suppressed: false, executed: false, }); @@ -197,7 +227,11 @@ export default class PopupTextAnalysis extends StubbedPopupText { resolver.setFinal(() => { const currentLine = getCurrentLogLine(); - resolver.status.finalData = EmulatorCommon.cloneData(this.data); + const finalData = EmulatorCommon.cloneData(this.data, dataPropsToExclude); + if (JSON.stringify(finalData) !== JSON.stringify(resolver.status.initialData)) + resolver.status.finalData = finalData; + else + delete resolver.status.initialData; delete resolver.triggerHelper?.resolver; if (this.callback) this.callback(currentLine, resolver.triggerHelper, resolver.status, this.data); diff --git a/ui/raidboss/emulator/data/RaidEmulator.ts b/ui/raidboss/emulator/data/RaidEmulator.ts index 971c46f4255..447c670a3cf 100644 --- a/ui/raidboss/emulator/data/RaidEmulator.ts +++ b/ui/raidboss/emulator/data/RaidEmulator.ts @@ -45,7 +45,10 @@ export default class RaidEmulator extends EventBus { this.currentEncounter = new AnalyzedEncounter(this.options, enc, this, watchCombatantsOverride); void this.dispatch('preCurrentEncounterChanged', this.currentEncounter); + const start = performance.now(); void this.currentEncounter.analyze().then(() => { + const duration = performance.now() - start; + console.log(`Analyzing encounter took ${duration.toFixed(2)}ms`); void this.dispatch('currentEncounterChanged', this.currentEncounter); }); }