paavohtl Claude Opus 4.6 (1M context) commited on
Commit
2c6f096
Β·
1 Parent(s): a6758d9

feat: add game state snapshot system

Browse files

New `snapshot` command captures canonicalized game state for diffing:
- DDGame: frame counter, RNG, game speed, camera, team arena, ammo
- Teams: name, alliance, worm count, per-worm state/hp/name
- Entities: BFS walk of task tree, rich dump for CTaskWorm/CTaskMissile,
raw hex with pointer canonicalization for unknown entity types

Snapshot trait implemented for DDGame, WormEntry, CTaskWorm, CTaskMissile.
Unknown regions dumped as raw hex with [ptr---] replacing heap pointers.

Usage: openwa-debug snapshot > /tmp/state.txt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

crates/openwa-core/src/engine/ddgame.rs CHANGED
@@ -2039,3 +2039,74 @@ impl TeamArenaRef {
2039
  (alliance_id, weapon_id as usize)
2040
  }
2041
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2039
  (alliance_id, weapon_id as usize)
2040
  }
2041
  }
2042
+
2043
+ // ── Snapshot impls ──────────────────────────────────────────
2044
+
2045
+ #[cfg(target_arch = "x86")]
2046
+ impl crate::snapshot::Snapshot for DDGame {
2047
+ unsafe fn write_snapshot(&self, w: &mut dyn core::fmt::Write, indent: usize) -> core::fmt::Result {
2048
+ use crate::fixed::Fixed;
2049
+ use crate::snapshot::{write_indent, fmt_ptr};
2050
+ let i = indent;
2051
+
2052
+ write_indent(w, i)?; writeln!(w, "frame_counter = {}", self.frame_counter)?;
2053
+ write_indent(w, i)?; writeln!(w, "game_speed = {}", Fixed(self.game_speed))?;
2054
+ write_indent(w, i)?; writeln!(w, "game_speed_target = {}", Fixed(self.game_speed_target))?;
2055
+ write_indent(w, i)?; writeln!(w, "rng_state = 0x{:08X} 0x{:08X}", self.rng_state_1, self.rng_state_2)?;
2056
+ write_indent(w, i)?; writeln!(w, "camera = ({}, {})", Fixed(self.camera_x), Fixed(self.camera_y))?;
2057
+ write_indent(w, i)?; writeln!(w, "camera_target = ({}, {})", Fixed(self.camera_target_x), Fixed(self.camera_target_y))?;
2058
+ write_indent(w, i)?; writeln!(w, "level_size = {}x{}", self.level_width, self.level_height)?;
2059
+ write_indent(w, i)?; writeln!(w, "level_size_raw = {}x{}", self.level_width_raw, self.level_height_raw)?;
2060
+ write_indent(w, i)?; writeln!(w, "landscape_property = {}", self.landscape_property)?;
2061
+ write_indent(w, i)?; writeln!(w, "gfx_color_table = {:?}", self.gfx_color_table)?;
2062
+ write_indent(w, i)?; writeln!(w, "fast_forward = req={} active={}", self.fast_forward_request, self.fast_forward_active)?;
2063
+ write_indent(w, i)?; writeln!(w, "keyboard = {}", fmt_ptr(self.keyboard as *const u8))?;
2064
+ write_indent(w, i)?; writeln!(w, "display = {}", fmt_ptr(self.display as *const u8))?;
2065
+ write_indent(w, i)?; writeln!(w, "sound = {}", fmt_ptr(self.sound as *const u8))?;
2066
+ write_indent(w, i)?; writeln!(w, "game_info = {}", fmt_ptr(self.game_info as *const u8))?;
2067
+ write_indent(w, i)?; writeln!(w, "landscape = {}", fmt_ptr(self.landscape as *const u8))?;
2068
+ write_indent(w, i)?; writeln!(w, "task_land = {}", fmt_ptr(self.task_land))?;
2069
+
2070
+ // TeamArenaState summary
2071
+ write_indent(w, i)?; writeln!(w, "team_arena.team_count = {}", self.team_arena.team_count)?;
2072
+ write_indent(w, i)?; writeln!(w, "team_arena.game_phase = {}", self.team_arena.game_phase)?;
2073
+ write_indent(w, i)?; writeln!(w, "team_arena.game_mode_flag = {}", self.team_arena.game_mode_flag)?;
2074
+
2075
+ // Dump weapon slots (ammo only, skip zeros)
2076
+ write_indent(w, i)?; writeln!(w, "weapon_slots:")?;
2077
+ for team in 0..6usize {
2078
+ let slots = &self.team_arena.weapon_slots.teams[team];
2079
+ let mut has_any = false;
2080
+ for wpn in 0..71usize {
2081
+ let ammo = slots.ammo[wpn];
2082
+ if ammo != 0 {
2083
+ if !has_any {
2084
+ write_indent(w, i + 1)?; write!(w, "team[{}] ammo:", team)?;
2085
+ has_any = true;
2086
+ }
2087
+ if ammo == -1 {
2088
+ write!(w, " {}=inf", wpn)?;
2089
+ } else {
2090
+ write!(w, " {}={}", wpn, ammo)?;
2091
+ }
2092
+ }
2093
+ }
2094
+ if has_any { writeln!(w)?; }
2095
+ }
2096
+
2097
+ Ok(())
2098
+ }
2099
+ }
2100
+
2101
+ impl crate::snapshot::Snapshot for WormEntry {
2102
+ unsafe fn write_snapshot(&self, w: &mut dyn core::fmt::Write, indent: usize) -> core::fmt::Result {
2103
+ use crate::snapshot::write_indent;
2104
+ let i = indent;
2105
+ let name = core::ffi::CStr::from_ptr(self.name.as_ptr() as *const core::ffi::c_char)
2106
+ .to_string_lossy();
2107
+ write_indent(w, i)?;
2108
+ writeln!(w, "state=0x{:02X} active={} hp={}/{} name=\"{}\"",
2109
+ self.state, self.active_flag, self.health, self.max_health, name)?;
2110
+ Ok(())
2111
+ }
2112
+ }
crates/openwa-core/src/task/missile.rs CHANGED
@@ -182,3 +182,51 @@ pub enum MissileType {
182
  /// Unknown type code (value 1 never observed; any other unrecognised value).
183
  Unknown(u32),
184
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  /// Unknown type code (value 1 never observed; any other unrecognised value).
183
  Unknown(u32),
184
  }
185
+
186
+ // ── Snapshot impl ──────────────────────────────────────────
187
+
188
+ #[cfg(target_arch = "x86")]
189
+ impl crate::snapshot::Snapshot for CTaskMissile {
190
+ unsafe fn write_snapshot(&self, w: &mut dyn core::fmt::Write, indent: usize) -> core::fmt::Result {
191
+ use crate::snapshot::{write_indent, write_raw_region};
192
+ let i = indent;
193
+ let b = &self.base; // CGameTask
194
+
195
+ write_indent(w, i)?; writeln!(w, "pos = ({}, {})", b.pos_x, b.pos_y)?;
196
+ write_indent(w, i)?; writeln!(w, "speed = ({}, {})", b.speed_x, b.speed_y)?;
197
+ write_indent(w, i)?; writeln!(w, "launch_seed = 0x{:08X}", self.launch_seed)?;
198
+ write_indent(w, i)?; writeln!(w, "slot_id = {}", self.slot_id)?;
199
+
200
+ write_indent(w, i)?; write!(w, "spawn_params =")?;
201
+ for v in &self.spawn_params { write!(w, " {:08X}", v)?; }
202
+ writeln!(w)?;
203
+
204
+ write_indent(w, i)?; write!(w, "weapon_data =")?;
205
+ for (j, v) in self.weapon_data.iter().enumerate() {
206
+ if j % 16 == 0 { writeln!(w)?; write_indent(w, i + 1)?; }
207
+ write!(w, " {:08X}", v)?;
208
+ }
209
+ writeln!(w)?;
210
+
211
+ write_indent(w, i)?; write!(w, "render_data =")?;
212
+ for (j, v) in self.render_data.iter().enumerate() {
213
+ if j % 16 == 0 { writeln!(w)?; write_indent(w, i + 1)?; }
214
+ write!(w, " {:08X}", v)?;
215
+ }
216
+ writeln!(w)?;
217
+
218
+ write_indent(w, i)?; writeln!(w, "launch_speed_raw = {}", self.launch_speed_raw)?;
219
+ write_indent(w, i)?; writeln!(w, "homing_enabled = {}", self.homing_enabled)?;
220
+ write_indent(w, i)?; writeln!(w, "direction = {}", self.direction)?;
221
+
222
+ // Unknown regions
223
+ write_indent(w, i)?; writeln!(w, "_unknown_fc ({} bytes):", self._unknown_fc.len())?;
224
+ write_raw_region(w, self._unknown_fc.as_ptr(), self._unknown_fc.len(), i + 1)?;
225
+ write_indent(w, i)?; writeln!(w, "_unknown_37c ({} bytes):", self._unknown_37c.len())?;
226
+ write_raw_region(w, self._unknown_37c.as_ptr(), self._unknown_37c.len(), i + 1)?;
227
+ write_indent(w, i)?; writeln!(w, "_unknown_3cc ({} bytes):", self._unknown_3cc.len())?;
228
+ write_raw_region(w, self._unknown_3cc.as_ptr(), self._unknown_3cc.len(), i + 1)?;
229
+
230
+ Ok(())
231
+ }
232
+ }
crates/openwa-core/src/task/worm.rs CHANGED
@@ -210,3 +210,38 @@ impl CTaskWorm {
210
  &*(self.base.base.vtable as *const CTaskWormVTable)
211
  }
212
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  &*(self.base.base.vtable as *const CTaskWormVTable)
211
  }
212
  }
213
+
214
+ // ── Snapshot impl ──────────────────────────────────────────
215
+
216
+ #[cfg(target_arch = "x86")]
217
+ impl crate::snapshot::Snapshot for CTaskWorm {
218
+ unsafe fn write_snapshot(&self, w: &mut dyn core::fmt::Write, indent: usize) -> core::fmt::Result {
219
+ use crate::snapshot::{write_indent, write_raw_region};
220
+ let i = indent;
221
+ let b = &self.base; // CGameTask
222
+
223
+ write_indent(w, i)?; writeln!(w, "pos = ({}, {})", b.pos_x, b.pos_y)?;
224
+ write_indent(w, i)?; writeln!(w, "speed = ({}, {})", b.speed_x, b.speed_y)?;
225
+ write_indent(w, i)?; writeln!(w, "angle = {}", b.angle)?;
226
+ write_indent(w, i)?; writeln!(w, "team_index = {}", self.team_index)?;
227
+ write_indent(w, i)?; writeln!(w, "worm_index = {}", self.worm_index)?;
228
+ write_indent(w, i)?; writeln!(w, "slot_id = {}", self.slot_id)?;
229
+ write_indent(w, i)?; writeln!(w, "selected_weapon = {}", self.selected_weapon)?;
230
+ write_indent(w, i)?; writeln!(w, "facing = {}", self.facing_direction)?;
231
+ write_indent(w, i)?; writeln!(w, "aim_angle = 0x{:08X}", self.aim_angle)?;
232
+
233
+ write_indent(w, i)?; write!(w, "spawn_params =")?;
234
+ for v in &self.spawn_params { write!(w, " {:08X}", v)?; }
235
+ writeln!(w)?;
236
+
237
+ // Raw dump of unknown regions
238
+ write_indent(w, i)?; writeln!(w, "_unknown_1f0 ({} bytes):", self._unknown_1f0.len())?;
239
+ write_raw_region(w, self._unknown_1f0.as_ptr(), self._unknown_1f0.len(), i + 1)?;
240
+ write_indent(w, i)?; writeln!(w, "_unknown_28c ({} bytes):", self._unknown_28c.len())?;
241
+ write_raw_region(w, self._unknown_28c.as_ptr(), self._unknown_28c.len(), i + 1)?;
242
+ write_indent(w, i)?; writeln!(w, "_unknown_370 ({} bytes):", self._unknown_370.len())?;
243
+ write_raw_region(w, self._unknown_370.as_ptr(), self._unknown_370.len(), i + 1)?;
244
+
245
+ Ok(())
246
+ }
247
+ }
crates/openwa-debug-cli/src/main.rs CHANGED
@@ -86,6 +86,7 @@ fn main() {
86
  .unwrap_or_else(|| { eprintln!("Usage: break <frame> | break clear"); process::exit(1); });
87
  Request::Break { frame }
88
  }
 
89
  Some(cmd) => {
90
  eprintln!("Unknown command: {cmd}");
91
  print_usage();
@@ -177,6 +178,10 @@ fn print_response(response: &Response, format: Format) {
177
  println!("Breakpoint cleared");
178
  }
179
  }
 
 
 
 
180
  Response::Error { message } => {
181
  eprintln!("Server error: {message}");
182
  process::exit(1);
@@ -347,6 +352,7 @@ fn print_usage() {
347
  eprintln!(" step [N] Advance N frames (default 1), then pause");
348
  eprintln!(" frame Show current frame and pause state");
349
  eprintln!(" break <N> Set frame breakpoint (break clear to remove)");
 
350
  eprintln!();
351
  eprintln!("Address syntax:");
352
  eprintln!(" 0x669F8C Ghidra VA (rebased automatically)");
 
86
  .unwrap_or_else(|| { eprintln!("Usage: break <frame> | break clear"); process::exit(1); });
87
  Request::Break { frame }
88
  }
89
+ Some("snapshot") => Request::Snapshot,
90
  Some(cmd) => {
91
  eprintln!("Unknown command: {cmd}");
92
  print_usage();
 
178
  println!("Breakpoint cleared");
179
  }
180
  }
181
+ Response::Snapshot { frame, text } => {
182
+ println!("=== Snapshot at frame {frame} ===\n");
183
+ print!("{text}");
184
+ }
185
  Response::Error { message } => {
186
  eprintln!("Server error: {message}");
187
  process::exit(1);
 
352
  eprintln!(" step [N] Advance N frames (default 1), then pause");
353
  eprintln!(" frame Show current frame and pause state");
354
  eprintln!(" break <N> Set frame breakpoint (break clear to remove)");
355
+ eprintln!(" snapshot Dump canonicalized game state (for diffing)");
356
  eprintln!();
357
  eprintln!("Address syntax:");
358
  eprintln!(" 0x669F8C Ghidra VA (rebased automatically)");
crates/openwa-debug-proto/src/lib.rs CHANGED
@@ -41,6 +41,8 @@ pub enum Request {
41
  Frame,
42
  /// Set a frame breakpoint (-1 to clear).
43
  Break { frame: i32 },
 
 
44
  }
45
 
46
  /// One step in a resolved pointer chain, for display.
@@ -93,6 +95,8 @@ pub enum Response {
93
  FrameInfo { frame: i32, paused: bool, breakpoint: i32 },
94
  /// Breakpoint set/cleared.
95
  BreakSet { frame: i32 },
 
 
96
  Error { message: String },
97
  }
98
 
 
41
  Frame,
42
  /// Set a frame breakpoint (-1 to clear).
43
  Break { frame: i32 },
44
+ /// Capture a canonicalized game state snapshot.
45
+ Snapshot,
46
  }
47
 
48
  /// One step in a resolved pointer chain, for display.
 
95
  FrameInfo { frame: i32, paused: bool, breakpoint: i32 },
96
  /// Breakpoint set/cleared.
97
  BreakSet { frame: i32 },
98
+ /// Game state snapshot.
99
+ Snapshot { frame: i32, text: String },
100
  Error { message: String },
101
  }
102
 
crates/openwa-wormkit/src/debug_server.rs CHANGED
@@ -135,6 +135,11 @@ fn handle_request(request: Request) -> Response {
135
  crate::debug_sync::set_breakpoint(frame);
136
  Response::BreakSet { frame }
137
  }
 
 
 
 
 
138
  }
139
  }
140
 
 
135
  crate::debug_sync::set_breakpoint(frame);
136
  Response::BreakSet { frame }
137
  }
138
+ Request::Snapshot => {
139
+ let text = unsafe { crate::snapshot::capture() };
140
+ let frame = crate::debug_sync::current_frame();
141
+ Response::Snapshot { frame, text }
142
+ }
143
  }
144
  }
145
 
crates/openwa-wormkit/src/lib.rs CHANGED
@@ -9,6 +9,7 @@ mod debug_ui;
9
  mod debug_watchpoint;
10
  pub mod hook;
11
  mod replacements;
 
12
  mod validation;
13
 
14
  // ---------------------------------------------------------------------------
 
9
  mod debug_watchpoint;
10
  pub mod hook;
11
  mod replacements;
12
+ mod snapshot;
13
  mod validation;
14
 
15
  // ---------------------------------------------------------------------------
crates/openwa-wormkit/src/snapshot.rs ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Game state snapshot capture.
2
+ //!
3
+ //! Walks DDGame, the team arena, and the entity tree to produce a
4
+ //! canonicalized text dump suitable for cross-run diffing.
5
+
6
+ use core::fmt::Write;
7
+
8
+ use openwa_core::address::va;
9
+ use openwa_core::engine::GameSession;
10
+ use openwa_core::rebase::rb;
11
+ use openwa_core::snapshot::{write_raw_region, Snapshot};
12
+ use openwa_core::task::{CTask, CTaskBfsIter, CTaskMissile, CTaskWorm};
13
+
14
+ /// Capture a full game state snapshot as text.
15
+ ///
16
+ /// # Safety
17
+ /// Must be called from the DLL while the game is paused (frame breakpoint).
18
+ #[cfg(target_arch = "x86")]
19
+ pub unsafe fn capture() -> String {
20
+ let mut out = String::with_capacity(128 * 1024);
21
+
22
+ let session_ptr = rb(va::G_GAME_SESSION) as *const *mut GameSession;
23
+ let session = *session_ptr;
24
+ if session.is_null() {
25
+ let _ = writeln!(out, "ERROR: g_GameSession is null");
26
+ return out;
27
+ }
28
+ let wrapper = (*session).ddgame_wrapper;
29
+ if wrapper.is_null() {
30
+ let _ = writeln!(out, "ERROR: DDGameWrapper is null");
31
+ return out;
32
+ }
33
+ let ddgame = (*wrapper).ddgame;
34
+ if ddgame.is_null() {
35
+ let _ = writeln!(out, "ERROR: DDGame is null");
36
+ return out;
37
+ }
38
+
39
+ let _ = writeln!(out, "=== Frame {} ===\n", (*ddgame).frame_counter);
40
+
41
+ // ── DDGame ──
42
+ let _ = writeln!(out, "[DDGame]");
43
+ let _ = (*ddgame).write_snapshot(&mut out, 1);
44
+ let _ = writeln!(out);
45
+
46
+ // ── Team blocks + worm entries ──
47
+ let team_count = (*ddgame).team_arena.team_count as usize;
48
+ let _ = writeln!(out, "[Teams] count={}", team_count);
49
+ let arena = openwa_core::engine::ddgame::TeamArenaRef::from_ptr(
50
+ &raw mut (*ddgame).team_arena,
51
+ );
52
+ let blocks = arena.blocks();
53
+ for t in 0..team_count {
54
+ let header = arena.team_header(t);
55
+ let name = core::ffi::CStr::from_ptr(header.team_name.as_ptr() as *const _)
56
+ .to_string_lossy();
57
+ let _ = writeln!(out, "\n [Team {}] \"{}\"", t, name);
58
+ let _ = writeln!(out, " eliminated={} alliance={} worm_count={} active_worm={}",
59
+ header.eliminated, header.alliance, header.worm_count, header.active_worm);
60
+ let _ = writeln!(out, " weapon_alliance={} turn_action_flags=0x{:08X}",
61
+ header.weapon_alliance, header.turn_action_flags);
62
+
63
+ // Worms are in block[t+1].worms[0..worm_count] (1-indexed blocks)
64
+ let worm_count = header.worm_count.max(0) as usize;
65
+ let block = &*blocks.add(t + 1);
66
+ for wi in 0..worm_count.min(7) {
67
+ let worm = &block.worms[wi];
68
+ let _ = write!(out, " [Worm {}] ", wi);
69
+ let _ = worm.write_snapshot(&mut out, 0);
70
+ }
71
+ }
72
+ let _ = writeln!(out);
73
+
74
+ // ── Entity tree ──
75
+ let task_land = (*ddgame).task_land;
76
+ if task_land.is_null() {
77
+ let _ = writeln!(out, "[Entities] task_land=null");
78
+ return out;
79
+ }
80
+
81
+ let delta = rb(va::IMAGE_BASE).wrapping_sub(va::IMAGE_BASE);
82
+ let root = task_land as *const CTask;
83
+ let iter = CTaskBfsIter::new(root);
84
+
85
+ // Census + detailed dump
86
+ let entities: Vec<*const CTask> = iter.collect();
87
+ let _ = writeln!(out, "[Entities] {} total", entities.len());
88
+
89
+ // Census by type
90
+ let mut counts = std::collections::BTreeMap::<&str, usize>::new();
91
+ for &task in &entities {
92
+ let vt = (*(task as *const u32)).wrapping_sub(delta);
93
+ let name = vtable_name(vt);
94
+ *counts.entry(name).or_default() += 1;
95
+ }
96
+ let _ = write!(out, " ");
97
+ for (name, count) in &counts {
98
+ let _ = write!(out, "{}x{} ", name, count);
99
+ }
100
+ let _ = writeln!(out, "\n");
101
+
102
+ // Detail per entity
103
+ for &task in &entities {
104
+ let vt = (*(task as *const u32)).wrapping_sub(delta);
105
+ let name = vtable_name(vt);
106
+ let class_type = (*task).class_type;
107
+ let _ = writeln!(out, " [{}] class_type={:?}", name, class_type);
108
+
109
+ match vt {
110
+ x if x == va::CTASK_WORM_VTABLE => {
111
+ let worm = task as *const CTaskWorm;
112
+ let _ = (*worm).write_snapshot(&mut out, 2);
113
+ }
114
+ x if x == va::CTASK_MISSILE_VTABLE => {
115
+ let missile = task as *const CTaskMissile;
116
+ let _ = (*missile).write_snapshot(&mut out, 2);
117
+ }
118
+ _ => {
119
+ // Unknown entity β€” raw dump of first 0x100 bytes
120
+ let size = 0x100usize;
121
+ let _ = write_raw_region(&mut out, task as *const u8, size, 2);
122
+ }
123
+ }
124
+ let _ = writeln!(out);
125
+ }
126
+
127
+ out
128
+ }
129
+
130
+ fn vtable_name(ghidra_vt: u32) -> &'static str {
131
+ match ghidra_vt {
132
+ x if x == va::CTASK_WORM_VTABLE => "CTaskWorm",
133
+ x if x == va::CTASK_MISSILE_VTABLE => "CTaskMissile",
134
+ x if x == va::CTASK_TEAM_VTABLE => "CTaskTeam",
135
+ x if x == va::CTASK_LAND_VTABLE => "CTaskLand",
136
+ x if x == va::CTASK_TURN_GAME_VTABLE => "CTaskTurnGame",
137
+ x if x == va::CTASK_CLOUD_VTABLE => "CTaskCloud",
138
+ x if x == va::CTASK_DIRT_VTABLE => "CTaskDirt",
139
+ x if x == va::CTASK_FIRE_VTABLE => "CTaskFire",
140
+ x if x == va::CTASK_SEA_BUBBLE_VTABLE => "CTaskSeaBubble",
141
+ x if x == va::CTASK_MINE_VTABLE => "CTaskMine",
142
+ x if x == va::CTASK_OILDRUM_VTABLE => "CTaskOilDrum",
143
+ x if x == va::CTASK_CRATE_VTABLE => "CTaskCrate",
144
+ x if x == va::CTASK_CPU_VTABLE => "CTaskCPU",
145
+ x if x == va::CTASK_FILTER_VTABLE => "CTaskFilter",
146
+ x if x == va::CTASK_SPRITE_ANIM_VTABLE => "CTaskSpriteAnim",
147
+ _ => "Unknown",
148
+ }
149
+ }