Skip to content
5 changes: 4 additions & 1 deletion .github/scripts/auto-label.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,10 @@ const getTimelineReplaceChanges = (changedFiles) => {
const s = new Set();

changedFiles.forEach((file) => {
if (!file.filename.startsWith('ui/raidboss/data/'))
if (
!file.filename.startsWith('ui/raidboss/data/') &&
!file.filename.startsWith('ui/oopsyraidsy/data/')
)
return;

if (path.extname(file.filename) === '.js') {
Expand Down
2 changes: 1 addition & 1 deletion docs/RaidbossGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ There is a complete example that uses the **timeline** property in [test.ts](../
Key:value pairs to search and replace in timeline ability names. The display name for that ability is changed, but all `hideall`, `infotext`, `alerttext`, `alarmtext`, etc all refer to the original name. This enables translation/localization of the timeline files without having to edit those files directly.

**replaceSync**
Key:value pairs to search and replace in timeline file sync expressions. Necessary if localized names differ in the sync regexes.
Key:value pairs to search and replace in timeline file sync expressions, and trigger sources. Necessary if localized names differ in the sync regexes.

**resetWhenOutOfCombat**
Boolean, defaults to true.
Expand Down
2 changes: 1 addition & 1 deletion resources/translations.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { NetParams } from '../types/net_props';
import { CactbotBaseRegExp, TriggerTypes } from '../types/net_trigger';
import { TimelineReplacement } from '../types/trigger';
import {
commonReplacement,
partialCommonTimelineReplacementKeys,
partialCommonTriggerReplacementKeys,
} from '../ui/raidboss/common_replacement';
import { TimelineReplacement } from '../ui/raidboss/timeline_parser';

import { Lang } from './languages';
import NetRegexes, { keysThatRequireTranslation } from './netregexes';
Expand Down
9 changes: 2 additions & 7 deletions test/helper/test_timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,9 @@ import { keysThatRequireTranslation } from '../../resources/netregexes';
import { UnreachableCode } from '../../resources/not_reached';
import Regexes from '../../resources/regexes';
import { translateWithReplacements } from '../../resources/translations';
import { LooseTimelineTrigger, LooseTriggerSet } from '../../types/trigger';
import { LooseTimelineTrigger, LooseTriggerSet, TimelineReplacement } from '../../types/trigger';
import { CommonReplacement, commonReplacement } from '../../ui/raidboss/common_replacement';
import {
Error,
regexes,
TimelineParser,
TimelineReplacement,
} from '../../ui/raidboss/timeline_parser';
import { Error, regexes, TimelineParser } from '../../ui/raidboss/timeline_parser';

const parseTimelineFileFromTriggerFile = (filepath: string) => {
const fileContents = fs.readFileSync(filepath, 'utf8');
Expand Down
3 changes: 2 additions & 1 deletion types/oopsy.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { TrackedEvent } from '../ui/oopsyraidsy/player_state_tracker';
import { OopsyData } from './data';
import { NetAnyMatches, NetMatches } from './net_matches';
import { CactbotBaseRegExp, TriggerTypes } from './net_trigger';
import { LocaleText, ZoneIdType } from './trigger';
import { LocaleText, TimelineReplacement, ZoneIdType } from './trigger';

export type OopsyMistakeType =
| 'pull'
Expand Down Expand Up @@ -130,6 +130,7 @@ type SimpleOopsyTriggerSet<Data extends OopsyData> = {
zoneId: ZoneIdType | ZoneIdType[];
zoneLabel?: LocaleText;
triggers?: OopsyTrigger<Data>[];
timelineReplace?: TimelineReplacement[];
} & OopsyMistakeMapFields;

// If Data contains required properties that are not on OopsyData, require initData
Expand Down
9 changes: 8 additions & 1 deletion types/trigger.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Lang, NonEnLang } from '../resources/languages';
import { NamedConfigEntry } from '../resources/user_config';
import { TimelineReplacement, TimelineStyle } from '../ui/raidboss/timeline_parser';
import { TimelineStyle } from '../ui/raidboss/timeline_parser';

import { RaidbossData } from './data';
import { NetAnyMatches, NetMatches } from './net_matches';
Expand Down Expand Up @@ -183,6 +183,13 @@ export type TimelineTrigger<Data extends RaidbossData> = BaseTrigger<Data, 'None
beforeSeconds?: number;
};

export type TimelineReplacement = {
locale: Lang;
missingTranslations?: boolean;
replaceSync?: { [regexString: string]: string };
replaceText?: { [timelineText: string]: string };
};

// Because timeline functions run during loading, they only support the base RaidbossData.
export type TimelineFunc = (data: RaidbossData) => TimelineField;
export type TimelineField = string | TimelineFunc | undefined | TimelineField[];
Expand Down
25 changes: 19 additions & 6 deletions ui/oopsyraidsy/damage_tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import NetRegexes, { commonNetRegex } from '../../resources/netregexes';
import PartyTracker from '../../resources/party';
import { PlayerChangedDetail } from '../../resources/player_override';
import Regexes from '../../resources/regexes';
import { LocaleNetRegex } from '../../resources/translations';
import { LocaleNetRegex, translateRegex } from '../../resources/translations';
import Util from '../../resources/util';
import ZoneId from '../../resources/zone_id';
import ZoneInfo from '../../resources/zone_info';
Expand Down Expand Up @@ -726,25 +726,38 @@ export class DamageTracker {
this.AddSoloTriggers('fail', set.soloFail);

for (const trigger of set.triggers ?? [])
this.ProcessTrigger(trigger);
this.ProcessTrigger(trigger, set);

this.playerStateTracker.PushTriggerSet(set);
}
}

ProcessTrigger(trigger: OopsyTrigger<OopsyData>): void {
ProcessTrigger(trigger: OopsyTrigger<OopsyData>, set?: ProcessedOopsyTriggerSet): void {
// This is a bit of a hack, but LooseOopsyTrigger extends OopsyTrigger<OopsyData>
// but not vice versa. Because the NetMatches['Ability'] requires a number
// of fields, Matches cannot be assigned to Matches & NetMatches['Ability'].
const looseTrigger = trigger as LooseOopsyTrigger;

const regex = looseTrigger.netRegex;
const netRegex = looseTrigger.netRegex;
// Some oopsy triggers (e.g. early pull) have only an id.
if (!regex)
if (!netRegex)
return;

const parserLang = this.options.ParserLanguage;
const timelineReplace = set?.timelineReplace;

let localRegex: RegExp;
if (Array.isArray(netRegex)) {
localRegex = Regexes.parse(Regexes.anyOf(netRegex));
} else {
// RegExp (e.g. from NetRegexes.xxx()), translate the regex string
const translated = translateRegex(netRegex, parserLang, timelineReplace);
localRegex = Regexes.parse(translated);
}

this.triggers.push({
...looseTrigger,
localRegex: Regexes.parse(Array.isArray(regex) ? Regexes.anyOf(regex) : regex),
Copy link
Author

@Jaehyuk-Lee Jaehyuk-Lee Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code, checking if regex is an Array, was written a long time ago when we didn't have the NetRegex translation system.

cf. git blame of damage_tracker.ts. This was written 5 years ago.

example from old days

  netRegex: NetRegexes.startsUsing({ source: 'The Manipulator', id: '13E7', capture: false }),
  netRegexDe: NetRegexes.startsUsing({ source: 'Manipulator', id: '13E7', capture: false }),
  netRegexFr: NetRegexes.startsUsing({ source: 'Manipulateur', id: '13E7', capture: false }),
  netRegexJa: NetRegexes.startsUsing({ source: 'マニピュレーター', id: '13E7', capture: false }),
  netRegexCn: NetRegexes.startsUsing({ source: '操纵者', id: '13E7', capture: false }),
  netRegexKo: NetRegexes.startsUsing({ source: '조종자', id: '13E7', capture: false }),

So, I'm removing the if condition, and just use regex (localRegex from my commits). - see 4ed345d

Important

Please leave comments if this change breaks cactbot.

Copy link
Author

@Jaehyuk-Lee Jaehyuk-Lee Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Localized netRegexes were still being used in oopsy. I've replaced them with timelineReplace. - see
601287e

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is, unfortunately, a breaking change as currently written because technically users could still have user js files using this syntax:

  {
    id: 'Some Custom Trigger',
    netRegex: [/^21\|[^|]+\|10001234/, /^22\|[^|]+\|10001234/],
  }

Yet another reason to do some major cleanup before next expansion, with regards to #907.

I don't really have time right now to sit down and think through the logic required to allow this without breaking potential user triggers, but maybe something like this?

    const netRegexAny = Array.isArray(regex) ? Regexes.anyOf(regex) : regex;
    const translated = translateRegex(netRegexAny, parserLang, timelineReplace);

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@valarnin
🤦‍♂️ Right, user js.
I can just revert the 4ed345d commit.

localRegex,
});
}

Expand Down
48 changes: 38 additions & 10 deletions ui/oopsyraidsy/data/04-sb/raid/o3n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,6 @@ const triggerSet: OopsyTriggerSet<Data> = {
id: 'O3N Phase Tracker',
type: 'StartsUsing',
netRegex: NetRegexes.startsUsing({ id: '2304', source: 'Halicarnassus', capture: false }),
netRegexDe: NetRegexes.startsUsing({ id: '2304', source: 'Halikarnassos', capture: false }),
netRegexFr: NetRegexes.startsUsing({ id: '2304', source: 'Halicarnasse', capture: false }),
netRegexJa: NetRegexes.startsUsing({ id: '2304', source: 'ハリカルナッソス', capture: false }),
netRegexCn: NetRegexes.startsUsing({ id: '2304', source: '哈利卡纳苏斯', capture: false }),
netRegexKo: NetRegexes.startsUsing({ id: '2304', source: '할리카르나소스', capture: false }),
run: (data) => data.phaseNumber = (data.phaseNumber ?? 0) + 1,
},
{
Expand All @@ -45,11 +40,6 @@ const triggerSet: OopsyTriggerSet<Data> = {
id: 'O3N Initializing',
type: 'Ability',
netRegex: NetRegexes.ability({ id: '367', source: 'Halicarnassus', capture: false }),
netRegexDe: NetRegexes.ability({ id: '367', source: 'Halikarnassos', capture: false }),
netRegexFr: NetRegexes.ability({ id: '367', source: 'Halicarnasse', capture: false }),
netRegexJa: NetRegexes.ability({ id: '367', source: 'ハリカルナッソス', capture: false }),
netRegexCn: NetRegexes.ability({ id: '367', source: '哈利卡纳苏斯', capture: false }),
netRegexKo: NetRegexes.ability({ id: '367', source: '할리카르나소스', capture: false }),
condition: (data) => !data.initialized,
run: (data) => {
data.gameCount = 0;
Expand Down Expand Up @@ -100,6 +90,44 @@ const triggerSet: OopsyTriggerSet<Data> = {
run: (data) => data.gameCount = (data.gameCount ?? 0) + 1,
},
],
timelineReplace: [
{
'locale': 'de',
'replaceSync': {
'Halicarnassus': 'Halikarnassos',
},
},
{
'locale': 'fr',
'replaceSync': {
'Halicarnassus': 'Halicarnasse',
},
},
{
'locale': 'ja',
'replaceSync': {
'Halicarnassus': 'ハリカルナッソス',
},
},
{
'locale': 'cn',
'replaceSync': {
'Halicarnassus': '哈利卡纳苏斯',
},
},
{
'locale': 'tc',
'replaceSync': {
'Halicarnassus': '哈利卡納蘇斯',
},
},
{
'locale': 'ko',
'replaceSync': {
'Halicarnassus': '할리카르나소스',
},
},
],
};

export default triggerSet;
38 changes: 30 additions & 8 deletions ui/oopsyraidsy/data/05-shb/raid/e10s.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,6 @@ const triggerSet: OopsyTriggerSet<Data> = {
id: 'E10S Damage Down Orbs',
type: 'GainsEffect',
netRegex: NetRegexes.gainsEffect({ source: 'Flameshadow', effectId: '82C' }),
netRegexDe: NetRegexes.gainsEffect({ source: 'Schattenflamme', effectId: '82C' }),
netRegexFr: NetRegexes.gainsEffect({ source: 'Flamme ombrale', effectId: '82C' }),
netRegexJa: NetRegexes.gainsEffect({ source: 'シャドウフレイム', effectId: '82C' }),
netRegexCn: NetRegexes.gainsEffect({ source: '影烈火', effectId: '82C' }),
mistake: (_data, matches) => {
return {
type: 'damage',
Expand All @@ -69,10 +65,6 @@ const triggerSet: OopsyTriggerSet<Data> = {
// TODO: some of these will be duplicated with others, like `E10S Throne Of Shadow`.
// Maybe it'd be nice to figure out how to put the damage marker on that?
netRegex: NetRegexes.gainsEffect({ source: 'Shadowkeeper', effectId: '82C' }),
netRegexDe: NetRegexes.gainsEffect({ source: 'Schattenkönig', effectId: '82C' }),
netRegexFr: NetRegexes.gainsEffect({ source: 'Roi De L\'Ombre', effectId: '82C' }),
netRegexJa: NetRegexes.gainsEffect({ source: '影の王', effectId: '82C' }),
netRegexCn: NetRegexes.gainsEffect({ source: '影之王', effectId: '82C' }),
mistake: (_data, matches) => {
return {
type: 'damage',
Expand All @@ -99,6 +91,36 @@ const triggerSet: OopsyTriggerSet<Data> = {
},
},
],
timelineReplace: [
{
'locale': 'de',
'replaceSync': {
'Flameshadow': 'Schattenflamme',
'Shadowkeeper': 'Schattenkönig',
},
},
{
'locale': 'fr',
'replaceSync': {
'Flameshadow': 'Flamme ombrale',
'Shadowkeeper': 'Roi De L\'Ombre',
},
},
{
'locale': 'ja',
'replaceSync': {
'Flameshadow': 'シャドウフレイム',
'Shadowkeeper': '影の王',
},
},
{
'locale': 'cn',
'replaceSync': {
'Flameshadow': '影烈火',
'Shadowkeeper': '影之王',
},
},
],
};

export default triggerSet;
42 changes: 30 additions & 12 deletions ui/oopsyraidsy/data/05-shb/raid/e12s.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,10 +298,6 @@ const triggerSet: OopsyTriggerSet<Data> = {
id: 'E12S Promise Small Lion Tether',
type: 'Tether',
netRegex: NetRegexes.tether({ source: 'Beastly Sculpture', id: '0011' }),
netRegexDe: NetRegexes.tether({ source: 'Abbild Eines Löwen', id: '0011' }),
netRegexFr: NetRegexes.tether({ source: 'Création Léonine', id: '0011' }),
netRegexJa: NetRegexes.tether({ source: '創られた獅子', id: '0011' }),
netRegexCn: NetRegexes.tether({ source: '被创造的狮子', id: '0011' }),
run: (data, matches) => {
data.smallLionIdToOwner ??= {};
data.smallLionIdToOwner[matches.sourceId.toUpperCase()] = matches.target;
Expand All @@ -313,10 +309,6 @@ const triggerSet: OopsyTriggerSet<Data> = {
id: 'E12S Promise Small Lion Lionsblaze',
type: 'Ability',
netRegex: NetRegexes.ability({ source: 'Beastly Sculpture', id: '58B9' }),
netRegexDe: NetRegexes.ability({ source: 'Abbild Eines Löwen', id: '58B9' }),
netRegexFr: NetRegexes.ability({ source: 'Création Léonine', id: '58B9' }),
netRegexJa: NetRegexes.ability({ source: '創られた獅子', id: '58B9' }),
netRegexCn: NetRegexes.ability({ source: '被创造的狮子', id: '58B9' }),
mistake: (data, matches) => {
// Folks baiting the big lion second can take the first small lion hit,
// so it's not sufficient to check only the owner.
Expand Down Expand Up @@ -385,10 +377,6 @@ const triggerSet: OopsyTriggerSet<Data> = {
id: 'E12S Promise Big Lion Kingsblaze',
type: 'Ability',
netRegex: NetRegexes.ability({ source: 'Regal Sculpture', id: '4F9E' }),
netRegexDe: NetRegexes.ability({ source: 'Abbild eines großen Löwen', id: '4F9E' }),
netRegexFr: NetRegexes.ability({ source: 'création léonine royale', id: '4F9E' }),
netRegexJa: NetRegexes.ability({ source: '創られた獅子王', id: '4F9E' }),
netRegexCn: NetRegexes.ability({ source: '被创造的狮子王', id: '4F9E' }),
mistake: (data, matches) => {
const singleTarget = matches.type === '21';
const hasFireDebuff = data.fire && data.fire[matches.target];
Expand Down Expand Up @@ -495,6 +483,36 @@ const triggerSet: OopsyTriggerSet<Data> = {
},
},
],
timelineReplace: [
{
'locale': 'de',
'replaceSync': {
'Beastly Sculpture': 'Abbild Eines Löwen',
'Regal Sculpture': 'Abbild eines großen Löwen',
},
},
{
'locale': 'fr',
'replaceSync': {
'Beastly Sculpture': 'Création Léonine',
'Regal Sculpture': 'création léonine royale',
},
},
{
'locale': 'ja',
'replaceSync': {
'Beastly Sculpture': '創られた獅子',
'Regal Sculpture': '創られた獅子王',
},
},
{
'locale': 'cn',
'replaceSync': {
'Beastly Sculpture': '被创造的狮子',
'Regal Sculpture': '被创造的狮子王',
},
},
],
};

export default triggerSet;
37 changes: 32 additions & 5 deletions ui/oopsyraidsy/data/05-shb/raid/e4s.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,6 @@ const triggerSet: OopsyTriggerSet<Data> = {
id: 'E4S Fault Line Collect',
type: 'StartsUsing',
netRegex: NetRegexes.startsUsing({ id: '411E', source: 'Titan' }),
netRegexDe: NetRegexes.startsUsing({ id: '411E', source: 'Titan' }),
netRegexFr: NetRegexes.startsUsing({ id: '411E', source: 'Titan' }),
netRegexJa: NetRegexes.startsUsing({ id: '411E', source: 'タイタン' }),
netRegexCn: NetRegexes.startsUsing({ id: '411E', source: '泰坦' }),
netRegexKo: NetRegexes.startsUsing({ id: '411E', source: '타이탄' }),
run: (data, matches) => {
data.faultLineTarget = matches.target;
},
Expand Down Expand Up @@ -71,6 +66,38 @@ const triggerSet: OopsyTriggerSet<Data> = {
},
},
],
timelineReplace: [
{
'locale': 'de',
'replaceSync': {
'Titan': 'Titan',
},
},
{
'locale': 'fr',
'replaceSync': {
'Titan': 'Titan',
},
},
{
'locale': 'ja',
'replaceSync': {
'Titan': 'タイタン',
},
},
{
'locale': 'cn',
'replaceSync': {
'Titan': '泰坦',
},
},
{
'locale': 'ko',
'replaceSync': {
'Titan': '타이탄',
},
},
],
};

export default triggerSet;
Loading