diff --git a/ui/raidboss/data/04-sb/raid/o11n.ts b/ui/raidboss/data/04-sb/raid/o11n.ts index 065d728daf..f923e0f5d3 100644 --- a/ui/raidboss/data/04-sb/raid/o11n.ts +++ b/ui/raidboss/data/04-sb/raid/o11n.ts @@ -178,6 +178,7 @@ const triggerSet: TriggerSet = { timelineReplace: [ { 'locale': 'de', + 'missingTranslations': true, 'replaceSync': { 'Engaging Delta Attack protocol': 'Reinitialisiere Deltaprotokoll', 'Level Checker': 'Monitor', @@ -205,6 +206,7 @@ const triggerSet: TriggerSet = { }, { 'locale': 'fr', + 'missingTranslations': true, 'replaceSync': { 'Engaging Delta Attack protocol': 'Nécessité d\'utiliser l\'attaque Delta', 'Level Checker': 'vérifiniveau', @@ -233,6 +235,7 @@ const triggerSet: TriggerSet = { }, { 'locale': 'ja', + 'missingTranslations': true, 'replaceSync': { 'Engaging Delta Attack protocol': 'デルタアタックの必要性を認定します', 'Level Checker': 'レベルチェッカー', @@ -260,6 +263,7 @@ const triggerSet: TriggerSet = { }, { 'locale': 'cn', + 'missingTranslations': true, 'replaceSync': { 'Engaging Delta Attack protocol': '认定有必要使用三角攻击。', 'Level Checker': '等级检测仪', @@ -287,6 +291,7 @@ const triggerSet: TriggerSet = { }, { 'locale': 'ko', + 'missingTranslations': true, 'replaceSync': { 'Engaging Delta Attack protocol': '델타 공격의 필요성을 인정합니다', 'Level Checker': '레벨 측정기', diff --git a/ui/raidboss/data/04-sb/raid/o12n.ts b/ui/raidboss/data/04-sb/raid/o12n.ts index 6067a146f4..fc89d521b7 100644 --- a/ui/raidboss/data/04-sb/raid/o12n.ts +++ b/ui/raidboss/data/04-sb/raid/o12n.ts @@ -182,6 +182,7 @@ const triggerSet: TriggerSet = { timelineReplace: [ { 'locale': 'de', + 'missingTranslations': true, 'replaceSync': { 'Calculations indicate increased probability of defeat': 'Warnung. Erhöhte Wahrscheinlichkeit einer Niederlage', @@ -223,6 +224,7 @@ const triggerSet: TriggerSet = { }, { 'locale': 'fr', + 'missingTranslations': true, 'replaceSync': { '\\\\ Warning\\\\\. Calculations indicate': 'Alerte... Alerte... Forte augmentation', @@ -264,6 +266,7 @@ const triggerSet: TriggerSet = { }, { 'locale': 'ja', + 'missingTranslations': true, 'replaceSync': { 'Omega(?!-)': 'オメガ', 'Omega-M': 'オメガM', @@ -302,6 +305,7 @@ const triggerSet: TriggerSet = { }, { 'locale': 'cn', + 'missingTranslations': true, 'replaceSync': { 'Calculations indicate increased probability of defeat': '警告……警告……失败的危险性上升……', 'Omega(?!-)': '欧米茄', @@ -341,6 +345,7 @@ const triggerSet: TriggerSet = { }, { 'locale': 'ko', + 'missingTranslations': true, 'replaceSync': { 'Omega(?!-)': '오메가', 'Omega-M': '오메가 M', diff --git a/ui/raidboss/data/04-sb/raid/o9n.ts b/ui/raidboss/data/04-sb/raid/o9n.ts index c5d4e8c284..280be61b61 100644 --- a/ui/raidboss/data/04-sb/raid/o9n.ts +++ b/ui/raidboss/data/04-sb/raid/o9n.ts @@ -164,6 +164,7 @@ const triggerSet: TriggerSet = { timelineReplace: [ { 'locale': 'de', + 'missingTranslations': true, 'replaceSync': { 'Chaos': 'Chaos', 'YOU DARE!': 'Wie könnt ihr es wagen?!', @@ -188,6 +189,7 @@ const triggerSet: TriggerSet = { }, { 'locale': 'fr', + 'missingTranslations': true, 'replaceSync': { 'Chaos': 'Chaos', 'YOU DARE!': '... Mon cristal !? Impossible !', @@ -212,6 +214,7 @@ const triggerSet: TriggerSet = { }, { 'locale': 'ja', + 'missingTranslations': true, 'replaceSync': { 'Chaos': 'カオス', 'YOU DARE!': 'まさか……黒水晶を……!?', @@ -236,6 +239,7 @@ const triggerSet: TriggerSet = { }, { 'locale': 'cn', + 'missingTranslations': true, 'replaceSync': { 'Chaos': '卡奥斯', 'YOU DARE!': '居然……把黑水晶给……', @@ -260,6 +264,7 @@ const triggerSet: TriggerSet = { }, { 'locale': 'ko', + 'missingTranslations': true, 'replaceSync': { 'Chaos': '카오스', 'YOU DARE!': '네 이노오오옴', diff --git a/ui/raidboss/data/05-shb/trial/hades-ex.ts b/ui/raidboss/data/05-shb/trial/hades-ex.ts index 0c58707ada..144fd52775 100644 --- a/ui/raidboss/data/05-shb/trial/hades-ex.ts +++ b/ui/raidboss/data/05-shb/trial/hades-ex.ts @@ -704,6 +704,7 @@ const triggerSet: TriggerSet = { timelineReplace: [ { 'locale': 'de', + 'missingTranslations': true, 'replaceSync': { 'Aetherial Gaol': 'Ätherkerker', 'Arcane Font': 'Arkan(?:e|er|es|en) Körper', @@ -768,6 +769,7 @@ const triggerSet: TriggerSet = { }, { 'locale': 'fr', + 'missingTranslations': true, 'replaceSync': { 'Aetherial Gaol': 'Geôle Éthérée', 'Arcane Font': 'Solide Arcanique', @@ -962,6 +964,7 @@ const triggerSet: TriggerSet = { }, { 'locale': 'ko', + 'missingTranslations': true, 'replaceSync': { 'Aetherial Gaol': '에테르 감옥', 'Arcane Font': '입체 마법진', diff --git a/ui/raidboss/timeline_parser.ts b/ui/raidboss/timeline_parser.ts index dc4b54a83f..82330e50f4 100644 --- a/ui/raidboss/timeline_parser.ts +++ b/ui/raidboss/timeline_parser.ts @@ -677,6 +677,7 @@ export class TimelineParser { '^ 1\\[56\\]:\\[\\^:\\]\\*:\\[\\^:\\]\\*:', '^( ?260)? 104:', '^ ?29:', + '\\[:\\^\\]\\+', ].map((x) => Regexes.parse(x)); } diff --git a/util/nettify.ts b/util/nettify.ts new file mode 100644 index 0000000000..ec79e832d8 --- /dev/null +++ b/util/nettify.ts @@ -0,0 +1,276 @@ +import fs from 'fs'; +import path from 'path'; +import { argv, exit } from 'process'; +import readline from 'readline'; + +import glob from 'glob'; + +let validArgs = argv.length === 3; +let targetFolder = ''; + +if (validArgs) { + const tmpTargetFolder = argv[2]; + if (tmpTargetFolder === undefined) + validArgs = false; + else if (!fs.existsSync(tmpTargetFolder)) + validArgs = false; + else + targetFolder = tmpTargetFolder ?? ''; +} + +if (!validArgs) { + console.log('Invalid arguments'); + exit(1); +} + +/* eslint-disable max-len */ + +type LineReplacer = (line: string, fileName: string) => string | undefined; + +const lineReplacers: { [key: string]: LineReplacer } = { + ability: (line) => { + // 8.0 "Crackle Hiss" sync / 1[56]:[^:]*:Imdugud:B55:/ + const regex = /sync\s*\/ 1\[56\]:\[\^:\]\*:(?[^:]*):(?[^:]*):\//; + if (regex.exec(line)) + return line.replace(regex, `Ability { id: "$2", source: "$1" }`); + }, + abilityNoSource: (line) => { + // 451.8 "--sync--" sync / 1[56]:[^:]*:[^:]*:89B9:/ + const regex = /sync\s*\/ 1\[56\]:\[\^:\]\*:\[\^:\]\*:(?[^:]*):\//; + if (regex.exec(line)) + return line.replace(regex, `Ability { id: "$1" }`); + }, + abilityWithNonEmptyTarget: (line) => { + // 1002.5 "--sync--" sync / 1[56]:[^:]*:Art:3956:[^:]*:[^:]*:[^:]+:/ window 1500,0 + const regex = + /sync\s*\/ 1\[56\]:\[\^:\]\*:(?[^:]*):(?[^:]*):\[\^:\]\*:\[\^:\]\*:\[\^:\]\+:\//; + if (regex.exec(line)) + return line.replace(regex, `Ability { id: "$2", source: "$1", target: "[:^]+" }`); + }, + startsUsing: (line) => { + // "--sync--" sync / 14:[^:]*:Imdugud:B5D:/ window 200,0 + const regex = /sync\s*\/ 14:\[\^:\]\*:(?[^:]*):(?[^:]*):\//; + if (regex.exec(line)) + return line.replace(regex, `StartsUsing { id: "$2", source: "$1" }`); + }, + interrupt: (line) => { + // 3154.9 "--sync--" sync / 17:[^:]*:Raiden:3878:/ window 40,0 + const regex = /sync\s*\/ 17:\[\^:\]\*:(?[^:]*):(?[^:]*):\//; + if (regex.exec(line)) + return line.replace(regex, `NetworkCancelAbility { id: "$2", source: "$1" }`); + }, + inCombat: (line) => { + // 0.0 "--sync--" sync / 104:[^:]*:1($|:)/ window 0,1 + const regex = /sync\s*\/ 104:\[\^:\]\*:(?\w)\(\$\|:\)\//; + if (regex.exec(line)) + return line.replace(regex, `InCombat { inGameCombat: "$1" }`); + }, + systemLog: (line) => { + // 1000.0 "--sync--" sync / 29:[^:]*:7DC:[^:]*:E96/ window 1000,5 + const regex = /sync\s*\/ 29:\[\^:\]\*:(?[^:]*):\[\^:\]\*:(?[^:]*):\//; + if (regex.exec(line)) + return line.replace(regex, `SystemLogMessage { id: "$1", param1: "$2" }`); + }, + systemLogNoParam: (line) => { + // 0.0 "--Reset--" sync / 29:[^:]*:7DE:/ window 100000 jump 0 + const regex = /sync\s*\/ 29:\[\^:\]\*:(?[^:]*):\//; + if (regex.exec(line)) + return line.replace(regex, `SystemLogMessage { id: "$1" }`); + }, + actorControlNoData: (line) => { + // 0.0 "--Reset--" sync / 21:........:4000000F:/ window 100000 jump 0 + const regex = /sync\s*\/ 21:\.{8}:(?[^:]*):\//; + if (regex.exec(line)) + return line.replace(regex, `ActorControl { command: "$1" }`); + }, + actorControl: (line) => { + // 8000.0 "--sync--" sync / 21:........:80000014:210:/ window 100000,0 + const regex = /sync\s*\/ 21:\.{8}:(?[^:]*):(?[^:]*):\//; + if (regex.exec(line)) + return line.replace(regex, `ActorControl { command: "$1", data0: "$2" }`); + }, + gameLogWithName: (line) => { + // 2500.0 "--sync--" sync / 00:0044:Cagnazzo:No more games!/ window 500,0 + const regex = /sync\s*\/ 00:(?[^:]*):(?[^:]*):(?[^:]*)\//; + const matches = regex.exec(line)?.groups; + // Verify no empty names here. + if (matches !== undefined && matches.name !== '') + return line.replace(regex, `GameLog { code: "$1", name: "$2", line: "$3.*?" }`); + }, + gameLogNoName: (line) => { + // 1800.0 "Enrage" sync / 00:0044:[^:]*:\ Warning\. Calculations indicate/ window 1800,0 + const regex = /sync\s*\/ 00:(?[^:]*):\[\^:\]\*:(?[^:]*)\//; + if (regex.exec(line)) + return line.replace(regex, `GameLog { code: "$1", line: "$2.*?" }`); + }, + gameLogNoNameNoCode: (line) => { + // 1100.0 "--sync--" sync / 00:[^:]*::The liquid flame gains the effect of Chiromorph/ window 100,250 + const regex = /sync\s*\/ 00:\[\^:\]\*::(?[^:]*)\//; + if (regex.exec(line)) + return line.replace(regex, `GameLog { line: "$1.*?" }`); + }, + addedCombatant: (line) => { + // 2189.9 "Ruins Golem x2" sync / 03:........:Ruins Golem:/ + // # 2142.7 "--sync--" sync / 03:[^:]*:Aloalo Zaratan:/ # path 03 + const regex = /sync\s*\/ 03:(?:\.{8}|\[\^:\]\*):(?[^:]*):\//; + if (regex.exec(line)) + return line.replace(regex, `AddedCombatant { name: "$1" }`); + }, + addedCombatantOneOff: (line) => { + // 1.0 "--sync--" sync / 03:[^:]*:Hemitheos:00:5A:0{4}:00::12383:/ window 10,3 jump 1000.5 # Sync to P2 immediately through AddCombatant. + const regex = + /sync\s*\/ 03:(?:\.{8}|\[\^:\]\*):(?[^:]*):(?[^:]*):(?[^:]*):(?[^:]*):(?[^:]*)::(?[^:]*):\//; + if (regex.exec(line)) { + return line.replace( + regex, + `AddedCombatant { npcNameId: "$6", name: "$1", job: "$2", level: "$3", ownerId: "$4", worldId: "$5" }`, + ); + } + }, + removedCombatant: (line) => { + // 196.0 "--sync--" sync / 04:........:Liquid Hand:/ window 50,0 + const regex = /sync\s*\/ 04:\.{8}:(?[^:]*):\//; + if (regex.exec(line)) + return line.replace(regex, `RemovedCombatant { name: "$1" }`); + }, + nameToggle: (line) => { + // 10033.9 "--targetable--" sync / 22:........:Queen's Knight:........:Queen's Knight:01/ + // 300.0 "--targetable--" sync /22:[^:]*:Lamebrix Strikebocks:[^:]*:Lamebrix Strikebocks:01/ window 190,10 + const regex = + /sync\s*\/ ?22:(?:\.{8}|\[\^:\]\*):(?[^:]*):(?:\.{8}|\[\^:\]\*):(?[^:]*):(?[^:]*)\//; + // Deliberately ignore targetName here, as all cases are the same. + const matches = regex.exec(line)?.groups; + if (matches !== undefined && matches.name1 === matches.name2) + return line.replace(regex, `NameToggle { name: "$1", toggle: "$3" }`); + }, + mapEffect: (line) => { + // 3062.1 "--sync--" sync / 257 101:80038CA1:00020001:09:/ window 70,70 jump 3262.1 + const regex = /sync\s*\/ 257 101:(?[^:]*):(?[^:]*):(?[^:]*):\//; + if (regex.exec(line)) + return line.replace(regex, `MapEffect { instance: "$1", flags: "$2", location: "$3" }`); + }, + mapEffectNoInstanceNoLocation: (line) => { + // 12.9 "Wildlife Crossing 1" sync / 257 101:........:00020001:/ window 5,5 duration 15 + const regex = /sync\s*\/ 257 101:\.{8}:(?[^:]*):\//; + if (regex.exec(line)) + return line.replace(regex, `MapEffect { flags: "$1" }`); + }, + headMarkerNoName: (line) => { + // 503.3 "Golem Meteors" sync / 1B:........:[^:]*:....:....:0007:/ duration 11 window 505,0 + const regex = /sync\s*\/ 1B:\.{8}:\[\^:\]\*:\.{4}:\.{4}:(?[^:]*):\//; + if (regex.exec(line)) + return line.replace(regex, `HeadMarker { id: "$1" }`); + }, + headMarkerName: (line) => { + // 1048.0 "--sync--" sync / 1B:........:Minotaur:....:....:0036:/ window 45,15 jump 1151.0 + const regex = /sync\s*\/ 1B:\.{8}:(?[^:]*):\.{4}:\.{4}:(?[^:]*):\//; + if (regex.exec(line)) + return line.replace(regex, `HeadMarker { id: "$2", target: "$1" }`); + }, + gainsEffectNameOnly: (line) => { + // 1348 "Resync" sync / 1A:[^:]*:Pyretic:/ window 20 jump 1548 + const regex = /sync\s*\/ 1A:\[\^:\]\*:(?[^:]*):\//; + if (regex.exec(line)) + return line.replace(regex, `GainsEffect { effect: "$1" }`); + }, + gainsEffectWithTarget: (line) => { + // 200 "--sync--" sync / 1A:5D1:Ultros Simulation:[^:]*:[^:]*:[^:]*:[^:]*:Guardian:/ window 2000,2000 + // 50 "--sync--" sync / 1A:5D3:Dadaluma Simulation:[^:]+:[^:]*:[^:]*:[^:]*:Guardian:/ jump 1050 + const regex = + /sync\s*\/ 1A:(?[^:]*):(?[^:]*):\[\^:\]\*:\[\^:\]\*:\[\^:\]\*:\[\^:\]\*:(?[^:]*):\//; + if (regex.exec(line)) + return line.replace(regex, `GainsEffect { effectId: "$1", effect: "$2", target: "$3" }`); + }, + wasDefeated: (line) => { + // 6000.0 "--Reset--" sync / 19:[^:]*:Dahu:/ window 0,2000 jump 0 + const regex = /sync\s*\/ 19:\[\^:\]\*:(?[^:]*):\//; + if (regex.exec(line)) + return line.replace(regex, `WasDefeated { target: "$1" }`); + }, + backslashEscaper: (line) => { + // Double-escape any already escaped characters in already converted lines. + const regex = /(?<={[^}]*(?:name|source): ")[^"]*[\\][^"]*(?=")/; + const name = regex.exec(line)?.[0]; + if (name !== undefined) { + const replacedName = name.replace(/(\\)/g, '\\$1'); + return line.replace(regex, replacedName); + } + }, + debugMissing: (line, fileName) => { + const regex = /sync\s*\/.*\//; + if (regex.exec(line)) + console.log(`${fileName}: unconverted line: ${line}`); + return undefined; + }, +} as const; + +void (async () => { + const files = glob.sync(path.join(targetFolder, '**', '*.txt')); + + const numLinesToKeep = 3; + + type PerLine = { + count: number; + origLine: string[]; + newLine: string[]; + }; + const histogram: { [key: string]: PerLine } = {}; + + for (const fileName of files) { + try { + const lineReader = readline.createInterface({ + input: fs.createReadStream(fileName), + }); + const newFileLines: string[] = []; + let changed = false; + for await (const line of lineReader) { + let lineResult = line; + for (const [key, replacer] of Object.entries(lineReplacers)) { + const replacedLine = replacer(lineResult, fileName); + if (replacedLine !== undefined) { + const perLine = histogram[key] ??= { + count: 0, + origLine: [], + newLine: [], + }; + perLine.count++; + if (perLine.origLine.length < numLinesToKeep) { + perLine.origLine.push(lineResult); + perLine.newLine.push(replacedLine); + } + + lineResult = replacedLine; + changed = true; + } + } + newFileLines.push(lineResult); + } + if (changed) + fs.writeFileSync(fileName, `${newFileLines.join('\r\n')}\r\n`); + } catch (e) { + console.error(e); + } + } + + const keys = Object.keys(histogram).sort((a, b) => { + return (histogram[b]?.count ?? 0) - (histogram[a]?.count ?? 0); + }); + + for (const key of keys) { + const perLine = histogram[key]; + if (perLine === undefined) + continue; + console.log(`${key}: ${perLine.count}`); + console.log(`\`\`\`diff`); + for (let i = 0; i < perLine.origLine.length; ++i) { + const origLine = perLine.origLine[i]; + const newLine = perLine.newLine[i]; + if (origLine !== undefined && newLine !== undefined) { + console.log(`-${origLine}`); + console.log(`+${newLine}`); + } + } + console.log(`\`\`\``); + console.log(''); + } +})();