diff --git a/ui/raidboss/data/00-misc/test.ts b/ui/raidboss/data/00-misc/test.ts index ec1eb779b8..c3c27e76ef 100644 --- a/ui/raidboss/data/00-misc/test.ts +++ b/ui/raidboss/data/00-misc/test.ts @@ -393,6 +393,7 @@ const triggerSet: TriggerSet = { timelineReplace: [ { locale: 'de', + missingTranslations: true, replaceSync: { 'You bid farewell to the striking dummy': 'Du winkst der Trainingspuppe zum Abschied zu', 'You bow courteously to the striking dummy': @@ -514,6 +515,7 @@ const triggerSet: TriggerSet = { }, { locale: 'cn', + missingTranslations: true, replaceSync: { 'You bid farewell to the striking dummy': '.*向木人告别', 'You bow courteously to the striking dummy': '.*恭敬地对木人行礼', @@ -552,6 +554,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 14e7aee9c7..1127898107 100644 --- a/ui/raidboss/data/00-misc/test.txt +++ b/ui/raidboss/data/00-misc/test.txt @@ -18,6 +18,7 @@ hideall "--sync--" 0 "--Reset--" sync /You bid farewell to the striking dummy/ window 10000 jump 0 +0 "--sync--" GameLog { "line": "testNetRegexTimeline" } window 100000,100000 0 "--sync--" sync /:Engage!/ window 100000,100000 0 "--sync--" sync /:You bow courteously to the striking dummy/ window 0,1 3 "Almagest" diff --git a/ui/raidboss/emulator/overrides/RaidEmulatorTimelineController.ts b/ui/raidboss/emulator/overrides/RaidEmulatorTimelineController.ts index ef4fb0be7c..68052b182b 100644 --- a/ui/raidboss/emulator/overrides/RaidEmulatorTimelineController.ts +++ b/ui/raidboss/emulator/overrides/RaidEmulatorTimelineController.ts @@ -1,5 +1,5 @@ import { UnreachableCode } from '../../../../resources/not_reached'; -import { LogEvent } from '../../../../types/event'; +import { EventResponses, LogEvent } from '../../../../types/event'; import { LooseTimelineTrigger } from '../../../../types/trigger'; import { TimelineController } from '../../timeline'; import { TimelineReplacement, TimelineStyle } from '../../timeline_parser'; @@ -63,12 +63,17 @@ export default class RaidEmulatorTimelineController extends TimelineController { throw new UnreachableCode(); } + public override OnNetLog(_e: EventResponses['LogLine']): void { + throw new UnreachableCode(); + } + public onEmulatorLogEvent(logs: LineEvent[]): void { if (!this.activeTimeline) return; for (const line of logs) { this.activeTimeline.OnLogLine(line.convertedLine, line.timestamp); + this.activeTimeline.OnNetLogLine(line.networkLine, line.timestamp); // Only call _OnUpdateTimer if we have a timebase from the previous call to OnLogLine // This avoids spamming the console with a ton of messages if (this.activeTimeline.timebase) diff --git a/ui/raidboss/raidboss.ts b/ui/raidboss/raidboss.ts index 8cacf93267..0aba28c6dd 100644 --- a/ui/raidboss/raidboss.ts +++ b/ui/raidboss/raidboss.ts @@ -95,4 +95,8 @@ UserConfig.getUserConfigLocation('raidboss', defaultOptions, () => { addOverlayListener('onLogEvent', (e) => { timelineController.OnLogEvent(e); }); + + addOverlayListener('LogLine', (e) => { + timelineController.OnNetLog(e); + }); }); diff --git a/ui/raidboss/timeline.ts b/ui/raidboss/timeline.ts index 8308acb12a..227e2bdc41 100644 --- a/ui/raidboss/timeline.ts +++ b/ui/raidboss/timeline.ts @@ -1,7 +1,7 @@ import { commonNetRegex } from '../../resources/netregexes'; import { UnreachableCode } from '../../resources/not_reached'; import { LocaleRegex } from '../../resources/translations'; -import { LogEvent } from '../../types/event'; +import { EventResponses, LogEvent } from '../../types/event'; import { CactbotBaseRegExp } from '../../types/net_trigger'; import { LooseTimelineTrigger, RaidbossFileData } from '../../types/trigger'; @@ -137,6 +137,7 @@ export class Timeline { private activeText: string; protected activeSyncs: Sync[]; + protected activeNetSyncs: Sync[]; private activeEvents: Event[]; private keepAliveEvents: { event: Event; @@ -184,6 +185,7 @@ export class Timeline { // Not sorted. this.activeSyncs = []; + this.activeNetSyncs = []; // Sorted by event occurrence time. this.activeEvents = []; // Events that are no longer active but we are keeping on screen briefly. @@ -281,10 +283,15 @@ export class Timeline { private _CollectActiveSyncs(fightNow: number): void { this.activeSyncs = []; + this.activeNetSyncs = []; for (let i = this.nextSyncEnd; i < this.syncEnds.length; ++i) { const syncEnd = this.syncEnds[i]; - if (syncEnd && syncEnd.start <= fightNow) - this.activeSyncs.push(syncEnd); + if (syncEnd && syncEnd.start <= fightNow) { + if (syncEnd.regexType === 'parsed') + this.activeSyncs.push(syncEnd); + else + this.activeNetSyncs.push(syncEnd); + } } if ( @@ -292,25 +299,41 @@ export class Timeline { this.activeLastForceJumpSync.start <= fightNow && this.activeLastForceJumpSync.end > fightNow ) { - this.activeSyncs.push(this.activeLastForceJumpSync); + if (this.activeLastForceJumpSync.regexType === 'parsed') + this.activeSyncs.push(this.activeLastForceJumpSync); + else + this.activeNetSyncs.push(this.activeLastForceJumpSync); } else { this.activeLastForceJumpSync = undefined; } } + public OnLogLineJump(sync: Sync, currentTime: number): void { + if ('jump' in sync) { + if (!sync.jump) { + this.SyncTo(0, currentTime, sync); + this.Stop(); + } else { + this.SyncTo(sync.jump, currentTime, sync); + } + } else { + this.SyncTo(sync.time, currentTime, sync); + } + } + public OnLogLine(line: string, currentTime: number): void { for (const sync of this.activeSyncs) { if (sync.regex.test(line)) { - if ('jump' in sync) { - if (!sync.jump) { - this.SyncTo(0, currentTime, sync); - this.Stop(); - } else { - this.SyncTo(sync.jump, currentTime, sync); - } - } else { - this.SyncTo(sync.time, currentTime, sync); - } + this.OnLogLineJump(sync, currentTime); + break; + } + } + } + + public OnNetLogLine(line: string, currentTime: number): void { + for (const sync of this.activeNetSyncs) { + if (sync.regex.test(line)) { + this.OnLogLineJump(sync, currentTime); break; } } @@ -723,6 +746,16 @@ export class TimelineController { } } + OnNetLog(e: EventResponses['LogLine']): void { + if (!this.activeTimeline) + return; + + const currentTime = Date.now(); + + // TODO: Check for the countdown => wipe => engage logic for network lines + this.activeTimeline.OnNetLogLine(e.rawLine, currentTime); + } + public SetActiveTimeline( timelineFiles: string[], timelines: string[], diff --git a/ui/raidboss/timeline_parser.ts b/ui/raidboss/timeline_parser.ts index bfe16f151a..74995ac8a1 100644 --- a/ui/raidboss/timeline_parser.ts +++ b/ui/raidboss/timeline_parser.ts @@ -1,11 +1,51 @@ import { Lang } from '../../resources/languages'; +import logDefinitions, { LogDefinitionTypes } from '../../resources/netlog_defs'; +import { buildNetRegexForTrigger } from '../../resources/netregexes'; import { UnreachableCode } from '../../resources/not_reached'; import Regexes from '../../resources/regexes'; import { translateRegex, translateText } from '../../resources/translations'; +import { NetParams } from '../../types/net_props'; import { LooseTimelineTrigger, TriggerAutoConfig } from '../../types/trigger'; import defaultOptions, { RaidbossOptions, TimelineConfig } from './raidboss_options'; +const isLogDefinitionTypes = (type: string): type is LogDefinitionTypes => { + return type in logDefinitions; +}; + +const isStringArray = (value: unknown[]): value is string[] => { + return value.find((v) => typeof v !== 'string') === undefined; +}; + +const isStringOrStringArray = (value: unknown): value is string | string[] => { + if (Array.isArray(value)) { + if (isStringArray(value)) + return true; + return false; + } + return typeof value === 'string'; +}; + +const isValidNetParams = ( + type: T, + params: Record, +): params is NetParams[T] => { + for (const key in params) { + // Make sure all keys are present on our definition type + if (!(key in logDefinitions[type].fields)) + return false; + // Make sure our value is either a string/int or an array of strings/ints + if (!isStringOrStringArray(params[key])) + return false; + } + return true; +}; + +const isObject = (x: unknown): x is { [key: string]: unknown } => { + // JavaScript considers [] to be an object, so check for that explicitly. + return x instanceof Object && !Array.isArray(x); +}; + export type TimelineReplacement = { locale: Lang; missingTranslations?: boolean; @@ -41,6 +81,7 @@ export type Error = { export type Sync = { id: number; origRegexStr: string; + regexType: 'parsed' | 'net'; regex: RegExp; start: number; end: number; @@ -71,6 +112,32 @@ export type ParsedText = ParsedPopupText | ParsedTriggerText; export type Text = ParsedText & { time: number }; +const regexes = { + comment: /^\s*#/, + commentLine: /#.*$/, + durationCommand: /(?:[^#]*?\s)?(?duration\s+(?[0-9]+(?:\.[0-9]+)?))(\s.*)?$/, + ignore: /^hideall\s+\"(?[^"]+)\"(?:\s*#.*)?$/, + jumpCommand: + /(?:[^#]*?\s)?(?(?(?:force|)jump)\s+(?:"(?