Skip to content
Open
206 changes: 117 additions & 89 deletions ui/raidboss/emulator/data/AnalyzedEncounter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<void> {
// @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<void> {
if (!this.encounter.combatantTracker)
return;
async analyzeFor(partyMembers: string[], regexCache: LineRegExpCache): Promise<void> {
let currentLogIndex = 0;
const partyMember = this.encounter.combatantTracker.combatants[id];

const getCurLogLine = (): LineEvent => {
const line = this.encounter.logLines[currentLogIndex];
Expand All @@ -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(
Expand All @@ -157,98 +164,119 @@ 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];
if (!log)
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;
}
}
46 changes: 40 additions & 6 deletions ui/raidboss/emulator/data/PopupTextAnalysis.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -15,7 +16,7 @@ type ResolverFunc = () => void;
export interface ResolverStatus {
responseType?: string;
responseLabel?: string;
initialData: DataType;
initialData?: DataType;
finalData?: DataType;
condition?: boolean;
response?: undefined;
Expand All @@ -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<void>;
private run?: ResolverFunc;
Expand Down Expand Up @@ -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;
};
Expand Down Expand Up @@ -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,
});
Expand All @@ -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);
Expand All @@ -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,
});
Expand All @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions ui/raidboss/emulator/data/RaidEmulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}
Expand Down