Textdata event.txt
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_InitFromCoords → TickKey_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
< 10000→GameWorld_GetObject(aWorld_Zone_Entry); id>= 10001→GameWorld_GetWorldNode(id - 10001)(aWorld_Node); sentinel11190→GameWorld_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_SetInstant→GameState_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 insideif (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:
- Drive an in-game clock (week/day/hour/min/sec → tick) using the calendar constants above.
- At each row's tick, send the matching
0xC062effect packet to clients in the target's area (col 12 resolves to a map area viaworld.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 nameEffect_Defoffsets 32/36/52/68. - Map
mode(col 13) values to the concreteEvent_Dispatchaction types (0–4). - Cross-check target ids (col 12,
11xxx) againstworld.wlistarea codes to label which area each event scripts.
See also
[edit]- BIN textdata
- textdata bgmtable.txt, textdata tenkoutable.txt, textdata tensoutable.txt — the related time-windowed per-area tables