Textdata event.txt

From Raynegard Wiki
Jump to navigationJump to search

event.txt (268,065 bytes, part of BIN textdata) is the largest textdata file: a time-scheduled world-event / object-scripting table. Each row schedules a scripted action (object state change, sound, lighting, scene transition) on a specific world object / map-area node / zone at a specific in-game calendar time.

Important: in this build the client's time-driven dispatch of these rows is permanently gated off (see Dispatch gating below) — the rows are meant to be server-driven. The server should walk this schedule and emit 0xC062 effect packets at the scheduled ticks.

Loader / parser (Ghidra)

[edit]
Function Addr Role
Struct_Setup_Effect_Manager 00498546 opens event.txt, parses every row, builds the trees
Archive_OpenTextFileByName 004bc800 opens the file from g_NewArchive2
TextAsset_ReadNextLine row iterator
TickKey_InitFromCoordsTickKey_ComputeAndDecompose 004b50c0 / 004b5250 converts cols 2–6 (calendar) → absolute tick key
EffectDef_AppendKeyframe stores the row payload as a keyframe on the effect def

Parse loop (paraphrased):

while (TextAsset_ReadNextLine(&buf, line)) {
    if (line[0] == '#') continue;                       // '#' = comment line
    if (sscanf(line, "%d,%d,...,%d", &c1..&c16) != 16)   // exactly 16 ints
        continue;                                        // malformed -> skip
    if (c12 < 10000) c12 -= 1;                            // sprite id -> 0-based for <10000
    key  = TickKey(c2,c3,c4,c5,c6);                       // week,day,hour,min,sec -> tick
    tree = (c1 == 0) ? ground_effect_tree : air_effect_tree;
    ... insert effect def at key into tree, append keyframe(c7..c16) ...
}

Format string "%d,%d,…,%d" (16 fields) @ 004e4dd4. Lines beginning with # are comments; any line that doesn't scan to exactly 16 ints is skipped.

Column layout (16 comma-separated ints)

[edit]
# Name Meaning
1 layer / kind 0 → ground tree, ≠0 → air tree. In practice always ≥ 1 (no row is 0), so every row goes to the air tree — the gated-off path.
2 week in-game week (1-based). 99 = recurring/any week — selects a separate keyframe layout.
3 day in-game day (1-based).
4 hour 0–23.
5 minute 0–59.
6 second 0–59.
7 aux param A effect payload; only used when week ≠ 99 (one-shot events).
8 aux param B effect payload.
9 keyframe_count Effect_Def.keyframe_count (struct off 40).
10 keyframe_start Effect_Def.keyframe_start (struct off 44).
11 keyframe_end Effect_Def.keyframe_end (struct off 48).
12 target id object/node/zone the action runs on: < 10000 → world object; >= 10001 → map-area node = world.wlist record id − 10001; 11190 → zone. If < 10000 it's decremented to a 0-based index. (struct off 56)
13 mode / action which Event_Dispatch action runs (state change / sound / lighting+transition). Values 0, 1, 3 get special count handling (col 14). (struct off 60)
14 count / repeat if mode∈{1,3} and ≠99−1; if mode==0 and ==0−1. 99 kept as "infinite/any". (struct off 64)
15 aux param C action payload (e.g. sound id / state value). (struct off 68)
16 aux param D action payload. (struct off 52)

Cols 9/10/11/13/14 map to named Effect_Def fields and col 12 is the dispatch target id. Cols 7/8/15/16 land in currently-unnamed Effect_Def offsets (32/36/52/68) and carry the per-action payload — treat these column-to-payload bindings as provisional.

The calendar / tick key (cols 2–6)

[edit]

TickKey_ComputeAndDecompose @ 004b5250 converts (week, day, hour, minute, second) into an absolute tick. Calendar is 7-day weeks, 24-hour days; week/day are 1-indexed, hour/minute/second are 0-indexed:

day_index = (week - 1) * 7 + day            // stored at Effect_Def off 8 (tick_raw)
tick      = (day_index - 1) * 86400
          + hour * 3600 + minute * 60 + second   // stored at Effect_Def off 4 (tick_value)

A negative tick marks the entry invalid. week == 99 is the wildcard for recurring effects. Full derived tick constants: 1 week base, 1 day base, 7 days/week, 24 hours/day, 60 min/hour, 60 sec/min (all read from rg.exe .data).

Example rows

[edit]
1,1,1,22,0,0,0,0,0,0,0,11308,1,44,0,1
1,1,1,22,0,0,0,0,0,0,0,11425,1,99,0,1

Row 1: week 1 / day 1 / 22:00:00, target id 11308, mode 1, count 44. Row 2: same time, target 11425, count 99 (kept = recurring/infinite).

What a fired row does (Event_Dispatch @ 00497ae0)

[edit]

When a row's tick arrives it is not rendered as a free particle. Event_Dispatch looks up a target by id and runs a scripted action on it:

  • id < 10000GameWorld_GetObject (a World_Zone_Entry); id >= 10001GameWorld_GetWorldNode(id - 10001) (a World_Node); sentinel 11190GameWorld_GetZone(...).
  • action type (col 13) switches between: change the target's state slots, load a sound (SoundManager_Load_Single_SSF / TryLoad_SoundEffect), or set lighting + trigger a scene transition (Lighting_SetInstantGameState_Transition(0xf)).
  • it only acts if the player is in the target's area, gated by target->Vtable[5](player_x, player_y, player_z, active_area).

The >= 10001 target is a map-area NODE, not a character NPC. GameWorld_GetWorldNode returns g_pGameWorld->node_array[id - 10001] — a World_Node carrying that area's interaction_type, sound_index, and per-node BGM/SE/effect arrays. The index maps 1:1 to world.wlist record order (1,471 area records, id range 10001–11471). Verified: node 0 (id 10001) = field (overworld), node 1307 (id 11308) = 1ta16, node 1124 (id 11425) = ir_b2p06. So event.txt wires scheduled night-time ambiance (sounds/effects/BGM) to specific map areas, never to characters.

The /effect 1 0 console command (0xC062, channel 1) reaches this same machinery directly (EffectManager_ScheduleKeyframesForObject).

Dispatch gating — why the client never self-fires these

[edit]

The per-frame dispatcher EffectManager_TickAndDispatchEffects @ 00498da0 is called from GameStateMachine_Update @ 004a4180 and processes two trees:

  • ground_effect_tree (col1 == 0) — dispatched unconditionally, but empty: of 6,590 rows, none have col1 == 0.
  • air_effect_tree (col1 != 0) — holds every row, dispatched only inside if (g_NetworkGlobal->field4_0x8 == 0).

field4_0x8 is written exactly once — in Struct_Setup_Net0x98_0x1C @ 004b7010, and its only caller passes a literal 1. No state, command, or packet ever clears it. So the air path is permanently off, and the client's time-driven event.txt dispatch never runs in normal play. Two further gates apply even if that flag were 0: the dispatcher only runs on a minute boundary (if (nSecond == 0)), and Event_Dispatch only acts when g_EffectSystem_Initialized == 0 (armed on world-entry).

Server relevance

[edit]

Because client self-dispatch is fused off, event.txt is effectively the server's schedule. To reproduce original behavior the server should:

  1. Drive an in-game clock (week/day/hour/min/sec → tick) using the calendar constants above.
  2. At each row's tick, send the matching 0xC062 effect packet to clients in the target's area (col 12 resolves to a map area via world.wlist, id − 10001, so the broadcast can be filtered to players actually in that area).

The clock bases must match the client's (set via 0xC05C "/timeconfig" + 0xC05D "/time") so server schedule and client clock stay in sync.

Open items

[edit]
  • Trace the keyframe→task copy to pin the exact column → Event_Dispatch param_1[0..3] mapping, and name Effect_Def offsets 32/36/52/68.
  • Map mode (col 13) values to the concrete Event_Dispatch action types (0–4).
  • Cross-check target ids (col 12, 11xxx) against world.wlist area codes to label which area each event scripts.

See also

[edit]