paavohtl Claude Opus 4.6 (1M context) commited on
Commit
a8f286b
Β·
1 Parent(s): d3f3b34

feat: test runner crash detection, ERRORLOG capture, and dialog suppression

Browse files

- Launcher passes /silentcrash to WA.exe in headless mode, preventing
the crash handler from relaunching with /handlecrash and showing the
"Fatal error" dialog that blocks test execution
- Hook ERRORLOG.TXT creation via OPENWA_ERRORLOG_PATH env var to
redirect crash logs to per-test paths in the run directory
- Detect crashes by NTSTATUS exit codes or ERRORLOG.TXT presence,
show CRASH status with exception details and first 20 lines of
the error log (using lossy UTF-8 for binary memory dumps)
- Strip ANSI color codes when stdout is not a terminal
- Add RE notes for Frontend__MainNavigationLoop (crash handler mechanism)

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

crates/openwa-launcher/src/main.rs CHANGED
@@ -139,6 +139,13 @@ fn run() -> Result<(), String> {
139
  .parent()
140
  .ok_or("WA.exe path has no parent directory")?;
141
 
 
 
 
 
 
 
 
142
  let cmdline = build_cmdline(&wa_exe, &wa_args);
143
 
144
  println!(
 
139
  .parent()
140
  .ok_or("WA.exe path has no parent directory")?;
141
 
142
+ // In headless mode, suppress WA.exe's crash dialog. The crash handler
143
+ // (SEH at 0x5A5A20) checks this flag before deciding to relaunch with
144
+ // /handlecrash β€” if set, it silently writes ERRORLOG.TXT and returns.
145
+ if std::env::var("OPENWA_HEADLESS").is_ok() {
146
+ wa_args.push("/silentcrash".to_string());
147
+ }
148
+
149
  let cmdline = build_cmdline(&wa_exe, &wa_args);
150
 
151
  println!(
crates/openwa-test-runner/src/main.rs CHANGED
@@ -7,6 +7,7 @@
7
  use std::env;
8
  use std::fmt::Write as FmtWrite;
9
  use std::fs;
 
10
  use std::os::windows::process::CommandExt;
11
  use std::path::{Path, PathBuf};
12
  use std::process::Command;
@@ -38,6 +39,13 @@ struct TestCase {
38
  output_log: PathBuf,
39
  }
40
 
 
 
 
 
 
 
 
41
  #[derive(Clone)]
42
  struct TestResult {
43
  name: String,
@@ -45,6 +53,7 @@ struct TestResult {
45
  duration: Duration,
46
  diff_lines: Vec<String>,
47
  error: Option<String>,
 
48
  }
49
 
50
  struct Args {
@@ -222,6 +231,7 @@ fn run_test(test: &TestCase, launcher: &Path, wa_exe: &Path, run_dir: &Path) ->
222
 
223
  // Per-instance log paths
224
  let openwa_log = run_dir.join(format!("{}.openwa.log", test.name));
 
225
 
226
  let result = Command::new(launcher)
227
  .arg(wa_exe)
@@ -229,6 +239,7 @@ fn run_test(test: &TestCase, launcher: &Path, wa_exe: &Path, run_dir: &Path) ->
229
  .arg(&test.replay_path)
230
  .env("OPENWA_HEADLESS", "1")
231
  .env("OPENWA_LOG_PATH", &openwa_log)
 
232
  .stdout(std::process::Stdio::null())
233
  .stderr(std::process::Stdio::null())
234
  .creation_flags(CREATE_NO_WINDOW)
@@ -243,8 +254,11 @@ fn run_test(test: &TestCase, launcher: &Path, wa_exe: &Path, run_dir: &Path) ->
243
  duration,
244
  diff_lines: Vec::new(),
245
  error: Some(format!("Failed to launch: {e}")),
 
246
  },
247
  Ok(status) => {
 
 
248
  if duration >= Duration::from_secs(TIMEOUT_SECS) {
249
  return TestResult {
250
  name: test.name.clone(),
@@ -252,6 +266,40 @@ fn run_test(test: &TestCase, launcher: &Path, wa_exe: &Path, run_dir: &Path) ->
252
  duration,
253
  diff_lines: Vec::new(),
254
  error: Some("Timeout".to_string()),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  };
256
  }
257
 
@@ -269,9 +317,9 @@ fn run_test(test: &TestCase, launcher: &Path, wa_exe: &Path, run_dir: &Path) ->
269
  duration,
270
  diff_lines: Vec::new(),
271
  error: Some(format!(
272
- "No output log generated (exit code: {})",
273
- status.code().unwrap_or(-1)
274
  )),
 
275
  };
276
  }
277
 
@@ -288,6 +336,7 @@ fn run_test(test: &TestCase, launcher: &Path, wa_exe: &Path, run_dir: &Path) ->
288
  duration,
289
  diff_lines: Vec::new(),
290
  error: None,
 
291
  }
292
  } else {
293
  let mut diff = compute_diff(&expected, &actual);
@@ -305,6 +354,7 @@ fn run_test(test: &TestCase, launcher: &Path, wa_exe: &Path, run_dir: &Path) ->
305
  duration,
306
  diff_lines: diff,
307
  error: None,
 
308
  }
309
  }
310
  }
@@ -415,15 +465,53 @@ fn run_tests_parallel(
415
 
416
  // ─── Output ─────────────────────────────────────────────────────────────────
417
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
  fn print_result(result: &TestResult) {
419
  let status = if result.passed {
420
- "\x1b[32m PASS\x1b[0m"
 
 
421
  } else {
422
- "\x1b[31m FAIL\x1b[0m"
423
  };
424
  let secs = result.duration.as_secs_f64();
425
  println!("{status} {:<28} ({secs:.1}s)", result.name);
426
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
427
  if let Some(err) = &result.error {
428
  println!(" {err}");
429
  }
@@ -434,34 +522,62 @@ fn print_result(result: &TestResult) {
434
 
435
  fn print_summary(results: &[TestResult], wall_time: Duration) {
436
  let passed = results.iter().filter(|r| r.passed).count();
 
437
  let failed = results.len() - passed;
438
  let cpu_time: f64 = results.iter().map(|r| r.duration.as_secs_f64()).sum();
439
  let wall = wall_time.as_secs_f64();
440
 
441
  println!();
442
  if failed == 0 {
443
- println!(
444
- "\x1b[32m{} tests: all passed\x1b[0m (wall {wall:.1}s, cpu {cpu_time:.1}s)",
445
- results.len()
446
- );
447
  } else {
448
- println!(
449
- "\x1b[31m{} tests: {passed} passed, {failed} failed\x1b[0m (wall {wall:.1}s, cpu {cpu_time:.1}s)",
 
 
 
 
 
450
  results.len()
451
  );
 
452
  }
453
  }
454
 
455
  fn write_summary(results: &[TestResult], wall_time: Duration, path: &Path) {
456
  let mut s = String::new();
457
  for r in results {
458
- let status = if r.passed { "PASS" } else { "FAIL" };
 
 
 
 
 
 
459
  let _ = writeln!(
460
  s,
461
  "{status} {:<28} ({:.1}s)",
462
  r.name,
463
  r.duration.as_secs_f64()
464
  );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
465
  if let Some(err) = &r.error {
466
  let _ = writeln!(s, " {err}");
467
  }
@@ -470,10 +586,16 @@ fn write_summary(results: &[TestResult], wall_time: Duration, path: &Path) {
470
  }
471
  }
472
  let passed = results.iter().filter(|r| r.passed).count();
 
473
  let failed = results.len() - passed;
 
 
 
 
 
474
  let _ = writeln!(
475
  s,
476
- "\n{} tests: {passed} passed, {failed} failed (wall {:.1}s)",
477
  results.len(),
478
  wall_time.as_secs_f64()
479
  );
@@ -505,6 +627,10 @@ fn cleanup_temp_files(wa_exe: &Path) {
505
  for (prefix, suffix) in &data_patterns {
506
  cleanup_matching(&data_dir, prefix, suffix);
507
  }
 
 
 
 
508
  }
509
 
510
  fn cleanup_matching(dir: &Path, prefix: &str, suffix: &str) {
@@ -540,6 +666,45 @@ fn strip_unc(p: PathBuf) -> PathBuf {
540
  }
541
  }
542
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
543
  // ─── Main ───────────────────────────────────────────────────────────────────
544
 
545
  fn main() {
 
7
  use std::env;
8
  use std::fmt::Write as FmtWrite;
9
  use std::fs;
10
+ use std::io::IsTerminal;
11
  use std::os::windows::process::CommandExt;
12
  use std::path::{Path, PathBuf};
13
  use std::process::Command;
 
39
  output_log: PathBuf,
40
  }
41
 
42
+ #[derive(Clone)]
43
+ struct CrashInfo {
44
+ exit_code: u32,
45
+ name: &'static str,
46
+ errorlog_content: Option<String>,
47
+ }
48
+
49
  #[derive(Clone)]
50
  struct TestResult {
51
  name: String,
 
53
  duration: Duration,
54
  diff_lines: Vec<String>,
55
  error: Option<String>,
56
+ crashed: Option<CrashInfo>,
57
  }
58
 
59
  struct Args {
 
231
 
232
  // Per-instance log paths
233
  let openwa_log = run_dir.join(format!("{}.openwa.log", test.name));
234
+ let errorlog_path = run_dir.join(format!("{}.errorlog.txt", test.name));
235
 
236
  let result = Command::new(launcher)
237
  .arg(wa_exe)
 
239
  .arg(&test.replay_path)
240
  .env("OPENWA_HEADLESS", "1")
241
  .env("OPENWA_LOG_PATH", &openwa_log)
242
+ .env("OPENWA_ERRORLOG_PATH", &errorlog_path)
243
  .stdout(std::process::Stdio::null())
244
  .stderr(std::process::Stdio::null())
245
  .creation_flags(CREATE_NO_WINDOW)
 
254
  duration,
255
  diff_lines: Vec::new(),
256
  error: Some(format!("Failed to launch: {e}")),
257
+ crashed: None,
258
  },
259
  Ok(status) => {
260
+ let exit_code = status.code().unwrap_or(0);
261
+
262
  if duration >= Duration::from_secs(TIMEOUT_SECS) {
263
  return TestResult {
264
  name: test.name.clone(),
 
266
  duration,
267
  diff_lines: Vec::new(),
268
  error: Some("Timeout".to_string()),
269
+ crashed: None,
270
+ };
271
+ }
272
+
273
+ // Check for crash: either an NTSTATUS exit code (negative i32)
274
+ // or the presence of ERRORLOG.TXT (WA.exe's SEH handler catches
275
+ // exceptions and exits cleanly, so exit code may be 0).
276
+ // Use lossy conversion β€” ERRORLOG.TXT may contain binary
277
+ // memory dump data that isn't valid UTF-8.
278
+ let errorlog_content = fs::read(&errorlog_path)
279
+ .ok()
280
+ .filter(|b| !b.is_empty())
281
+ .map(|b| String::from_utf8_lossy(&b).into_owned());
282
+
283
+ if is_crash_exit_code(exit_code) || errorlog_content.is_some() {
284
+ let unsigned = exit_code as u32;
285
+ let name = if is_crash_exit_code(exit_code) {
286
+ ntstatus_name(unsigned)
287
+ } else {
288
+ // Parse exception from ERRORLOG first line, e.g.
289
+ // "WA caused an Access Violation (0xc0000005)"
290
+ parse_errorlog_exception(errorlog_content.as_deref())
291
+ };
292
+ return TestResult {
293
+ name: test.name.clone(),
294
+ passed: false,
295
+ duration,
296
+ diff_lines: Vec::new(),
297
+ error: None,
298
+ crashed: Some(CrashInfo {
299
+ exit_code: unsigned,
300
+ name,
301
+ errorlog_content,
302
+ }),
303
  };
304
  }
305
 
 
317
  duration,
318
  diff_lines: Vec::new(),
319
  error: Some(format!(
320
+ "No output log generated (exit code: {exit_code})"
 
321
  )),
322
+ crashed: None,
323
  };
324
  }
325
 
 
336
  duration,
337
  diff_lines: Vec::new(),
338
  error: None,
339
+ crashed: None,
340
  }
341
  } else {
342
  let mut diff = compute_diff(&expected, &actual);
 
354
  duration,
355
  diff_lines: diff,
356
  error: None,
357
+ crashed: None,
358
  }
359
  }
360
  }
 
465
 
466
  // ─── Output ─────────────────────────────────────────────────────────────────
467
 
468
+ fn use_color() -> bool {
469
+ std::io::stdout().is_terminal()
470
+ }
471
+
472
+ fn green(text: &str) -> String {
473
+ if use_color() {
474
+ format!("\x1b[32m{text}\x1b[0m")
475
+ } else {
476
+ text.to_string()
477
+ }
478
+ }
479
+
480
+ fn red(text: &str) -> String {
481
+ if use_color() {
482
+ format!("\x1b[31m{text}\x1b[0m")
483
+ } else {
484
+ text.to_string()
485
+ }
486
+ }
487
+
488
  fn print_result(result: &TestResult) {
489
  let status = if result.passed {
490
+ green(" PASS")
491
+ } else if result.crashed.is_some() {
492
+ red(" CRASH")
493
  } else {
494
+ red(" FAIL")
495
  };
496
  let secs = result.duration.as_secs_f64();
497
  println!("{status} {:<28} ({secs:.1}s)", result.name);
498
 
499
+ if let Some(crash) = &result.crashed {
500
+ println!(
501
+ " {} (exit code: 0x{:08X})",
502
+ crash.name, crash.exit_code
503
+ );
504
+ if let Some(content) = &crash.errorlog_content {
505
+ println!(" --- ERRORLOG.TXT ---");
506
+ let lines: Vec<&str> = content.lines().collect();
507
+ for line in lines.iter().take(20) {
508
+ println!(" | {line}");
509
+ }
510
+ if lines.len() > 20 {
511
+ println!(" | ... ({} lines truncated)", lines.len() - 20);
512
+ }
513
+ }
514
+ }
515
  if let Some(err) = &result.error {
516
  println!(" {err}");
517
  }
 
522
 
523
  fn print_summary(results: &[TestResult], wall_time: Duration) {
524
  let passed = results.iter().filter(|r| r.passed).count();
525
+ let crashed = results.iter().filter(|r| r.crashed.is_some()).count();
526
  let failed = results.len() - passed;
527
  let cpu_time: f64 = results.iter().map(|r| r.duration.as_secs_f64()).sum();
528
  let wall = wall_time.as_secs_f64();
529
 
530
  println!();
531
  if failed == 0 {
532
+ let msg = format!("{} tests: all passed", results.len());
533
+ println!("{} (wall {wall:.1}s, cpu {cpu_time:.1}s)", green(&msg));
 
 
534
  } else {
535
+ let crash_info = if crashed > 0 {
536
+ format!(" ({crashed} crashed)")
537
+ } else {
538
+ String::new()
539
+ };
540
+ let msg = format!(
541
+ "{} tests: {passed} passed, {failed} failed{crash_info}",
542
  results.len()
543
  );
544
+ println!("{} (wall {wall:.1}s, cpu {cpu_time:.1}s)", red(&msg));
545
  }
546
  }
547
 
548
  fn write_summary(results: &[TestResult], wall_time: Duration, path: &Path) {
549
  let mut s = String::new();
550
  for r in results {
551
+ let status = if r.passed {
552
+ "PASS"
553
+ } else if r.crashed.is_some() {
554
+ "CRASH"
555
+ } else {
556
+ "FAIL"
557
+ };
558
  let _ = writeln!(
559
  s,
560
  "{status} {:<28} ({:.1}s)",
561
  r.name,
562
  r.duration.as_secs_f64()
563
  );
564
+ if let Some(crash) = &r.crashed {
565
+ let _ = writeln!(
566
+ s,
567
+ " {} (exit code: 0x{:08X})",
568
+ crash.name, crash.exit_code
569
+ );
570
+ if let Some(content) = &crash.errorlog_content {
571
+ let _ = writeln!(s, " --- ERRORLOG.TXT ---");
572
+ for line in content.lines().take(20) {
573
+ let _ = writeln!(s, " | {line}");
574
+ }
575
+ let line_count = content.lines().count();
576
+ if line_count > 20 {
577
+ let _ = writeln!(s, " | ... ({} lines truncated)", line_count - 20);
578
+ }
579
+ }
580
+ }
581
  if let Some(err) = &r.error {
582
  let _ = writeln!(s, " {err}");
583
  }
 
586
  }
587
  }
588
  let passed = results.iter().filter(|r| r.passed).count();
589
+ let crashed = results.iter().filter(|r| r.crashed.is_some()).count();
590
  let failed = results.len() - passed;
591
+ let crash_info = if crashed > 0 {
592
+ format!(" ({crashed} crashed)")
593
+ } else {
594
+ String::new()
595
+ };
596
  let _ = writeln!(
597
  s,
598
+ "\n{} tests: {passed} passed, {failed} failed{crash_info} (wall {:.1}s)",
599
  results.len(),
600
  wall_time.as_secs_f64()
601
  );
 
627
  for (prefix, suffix) in &data_patterns {
628
  cleanup_matching(&data_dir, prefix, suffix);
629
  }
630
+
631
+ // Clean up any stale ERRORLOG.TXT / CRASH.DMP in the game directory
632
+ let _ = fs::remove_file(game_dir.join("ERRORLOG.TXT"));
633
+ let _ = fs::remove_file(game_dir.join("CRASH.DMP"));
634
  }
635
 
636
  fn cleanup_matching(dir: &Path, prefix: &str, suffix: &str) {
 
666
  }
667
  }
668
 
669
+ /// Map common NTSTATUS exception codes to human-readable names.
670
+ fn ntstatus_name(code: u32) -> &'static str {
671
+ match code {
672
+ 0xC0000005 => "access violation",
673
+ 0xC0000374 => "heap corruption",
674
+ 0xC00000FD => "stack overflow",
675
+ 0xC0000409 => "stack buffer overrun",
676
+ 0xC000001D => "illegal instruction",
677
+ 0xC0000135 => "DLL not found",
678
+ 0xC0000142 => "DLL init failed",
679
+ 0x80000003 => "breakpoint",
680
+ 0xE06D7363 => "C++ exception", // MSVC __CxxThrowException
681
+ _ => "exception",
682
+ }
683
+ }
684
+
685
+ /// NTSTATUS exception codes have the high bit set, so they're negative as i32.
686
+ fn is_crash_exit_code(code: i32) -> bool {
687
+ code < 0
688
+ }
689
+
690
+ /// Parse the exception name from the first line of ERRORLOG.TXT.
691
+ /// Expected format: "WA caused an Access Violation (0xc0000005)"
692
+ fn parse_errorlog_exception(content: Option<&str>) -> &'static str {
693
+ if let Some(line) = content.and_then(|c| c.lines().next()) {
694
+ // Try to extract the NTSTATUS code from parentheses
695
+ if let Some(start) = line.find("(0x") {
696
+ let hex_start = start + 1; // skip '('
697
+ if let Some(end) = line[hex_start..].find(')') {
698
+ let hex_str = &line[hex_start + 2..hex_start + end]; // skip "0x"
699
+ if let Ok(code) = u32::from_str_radix(hex_str, 16) {
700
+ return ntstatus_name(code);
701
+ }
702
+ }
703
+ }
704
+ }
705
+ "crash (SEH handled)"
706
+ }
707
+
708
  // ─── Main ───────────────────────────────────────────────────────────────────
709
 
710
  fn main() {
crates/openwa-wormkit/src/replacements/file_isolation.rs CHANGED
@@ -14,6 +14,7 @@ use crate::log_line;
14
  use core::ffi::c_void;
15
  use core::sync::atomic::{AtomicU32, Ordering};
16
  use std::ffi::CStr;
 
17
 
18
  type HANDLE = *mut c_void;
19
  type DWORD = u32;
@@ -31,9 +32,27 @@ type CreateFileAFn = unsafe extern "system" fn(
31
 
32
  static ORIG_CREATE_FILE_A: AtomicU32 = AtomicU32::new(0);
33
 
 
 
 
 
 
 
 
 
34
  /// Check if a path ends with one of our target filenames (case-insensitive).
35
  /// Returns the replacement path if it matches, None otherwise.
36
  fn redirect_path(path: &str) -> Option<String> {
 
 
 
 
 
 
 
 
 
 
37
  let pid = std::process::id();
38
  let lower = path.to_ascii_lowercase();
39
 
@@ -116,8 +135,13 @@ pub fn install() -> Result<(), String> {
116
  ORIG_CREATE_FILE_A.store(trampoline as u32, Ordering::Relaxed);
117
 
118
  let pid = std::process::id();
 
 
 
 
 
119
  let _ = log_line(&format!(
120
- "[FileIsolation] Hooked CreateFileA (pid={pid}): mono.tmp, land.dat, landgen.svg β†’ per-PID paths"
121
  ));
122
  }
123
 
 
14
  use core::ffi::c_void;
15
  use core::sync::atomic::{AtomicU32, Ordering};
16
  use std::ffi::CStr;
17
+ use std::sync::OnceLock;
18
 
19
  type HANDLE = *mut c_void;
20
  type DWORD = u32;
 
32
 
33
  static ORIG_CREATE_FILE_A: AtomicU32 = AtomicU32::new(0);
34
 
35
+ static ERRORLOG_PATH: OnceLock<Option<String>> = OnceLock::new();
36
+
37
+ fn errorlog_redirect() -> Option<&'static str> {
38
+ ERRORLOG_PATH
39
+ .get_or_init(|| std::env::var("OPENWA_ERRORLOG_PATH").ok())
40
+ .as_deref()
41
+ }
42
+
43
  /// Check if a path ends with one of our target filenames (case-insensitive).
44
  /// Returns the replacement path if it matches, None otherwise.
45
  fn redirect_path(path: &str) -> Option<String> {
46
+ // ERRORLOG.TXT β†’ env var path (if set by test runner)
47
+ if let Some(target) = errorlog_redirect() {
48
+ let lower = path.to_ascii_lowercase();
49
+ if lower.ends_with("\\errorlog.txt") || lower.ends_with("/errorlog.txt")
50
+ || lower == "errorlog.txt"
51
+ {
52
+ return Some(target.to_string());
53
+ }
54
+ }
55
+
56
  let pid = std::process::id();
57
  let lower = path.to_ascii_lowercase();
58
 
 
135
  ORIG_CREATE_FILE_A.store(trampoline as u32, Ordering::Relaxed);
136
 
137
  let pid = std::process::id();
138
+ let errorlog_msg = if let Some(target) = errorlog_redirect() {
139
+ format!(", ERRORLOG.TXT β†’ {target}")
140
+ } else {
141
+ String::new()
142
+ };
143
  let _ = log_line(&format!(
144
+ "[FileIsolation] Hooked CreateFileA (pid={pid}): mono.tmp, land.dat, landgen.svg β†’ per-PID paths{errorlog_msg}"
145
  ));
146
  }
147
 
docs/re-notes/frontend-main-navigation-loop.md ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Frontend__MainNavigationLoop (0x4E6700)
2
+
3
+ Ghidra address: `0x004E6700`
4
+ Calling convention: `__fastcall` (ECX = CWinApp*)
5
+ Size: ~3500 lines of decompiled C
6
+
7
+ This is the main entry point / event loop of WA.exe. It handles:
8
+ - Command-line argument parsing
9
+ - Crash handling (`/handlecrash`, `/silentcrash`, `/disablecrashhandler`)
10
+ - Replay playback (`/play`, `/playat`, `/getlog`, `/getmap`, `/getscheme`, `/getvideo`, `/getlogs`)
11
+ - Sanitize/repair modes (`/sanitise`, `/repair`)
12
+ - Network hosting (`/host`, `wa:` protocol)
13
+ - WormKit module loading (`/wk`, `/nowk`, `/wkargs`)
14
+ - Frontend screen navigation loop (main menu, multiplayer, campaigns, etc.)
15
+ - Renderer selection (DirectDraw, OpenGL, D3D9, etc.)
16
+
17
+ ## Crash Handler Mechanism
18
+
19
+ WA.exe's crash handler works by **relaunching itself** with `/handlecrash`:
20
+
21
+ 1. Original WA.exe crashes (C++ exception via `__CxxThrowException`)
22
+ 2. The crash handler (likely SEH-based) writes `ERRORLOG.TXT` and relaunches WA.exe with `/handlecrash`
23
+ 3. The new WA.exe instance processes `/handlecrash` in this function
24
+ 4. It checks `Options__GetCrashReportURL()` β€” if crash reporting is available, shows a dialog offering to send the report
25
+ 5. It checks `DAT_0088e410` (WormKit modules loaded flag) β€” if set, shows the "Fatal error" dialog with Yes/No asking to disable WormKit modules
26
+
27
+ ### Key flags:
28
+ - `/silentcrash` β†’ sets `DAT_007a0f10+1 = 1` β€” suppresses the crash dialog
29
+ - `/disablecrashhandler` β†’ sets `DAT_007a0f10+0 = 1` β€” disables the crash handler entirely
30
+ - `/crash` β†’ intentionally crashes via `__CxxThrowException("Goodbye, cruel world!")`
31
+
32
+ ### Crash dialog code (at ~0x4E6730):
33
+ ```c
34
+ // Check for crash report URL
35
+ iVar11 = Options__GetCrashReportURL();
36
+ if (iVar11 != 0) {
37
+ // Show "send bug report?" dialog (MB_YESNO | MB_ICONQUESTION = 0x14)
38
+ iVar10 = MessageBoxA(NULL,
39
+ "Worms Armageddon has encountered an error...\nWould you like to send the bug report?",
40
+ "Fatal error", 0x14);
41
+ if (iVar10 == IDYES) FUN_005a6470(); // send crash report
42
+ }
43
+
44
+ // Check if WormKit modules were loaded
45
+ if (DAT_0088e410 == 0) {
46
+ // No WormKit β€” just show error (MB_ICONERROR = 0x10)
47
+ MessageBoxA(NULL, error_msg, "Fatal error", 0x10);
48
+ } else {
49
+ // WormKit loaded β€” ask to disable (MB_YESNO | MB_ICONQUESTION = 0x14)
50
+ FUN_00444d30(); // builds error message mentioning WormKit
51
+ iVar11 = MessageBoxA(NULL, error_msg, "Fatal error", 0x14);
52
+ if (iVar11 == IDYES) {
53
+ DAT_0088e410 = 0;
54
+ FUN_004c8c70(); // disable WormKit in registry
55
+ MessageBoxA(NULL, "WormKit modules have been disabled.", "Worms Armageddon", 0x40);
56
+ }
57
+ }
58
+ ```
59
+
60
+ ## ERRORLOG.TXT
61
+
62
+ String at `0x00677FE0`: `"ERRORLOG.TXT"`
63
+ String at `0x00678484`: `"Can't open ERRORLOG.TXT"`
64
+ Written by the crash handler before relaunching (separate from this function).
65
+
66
+ ## `/getlog` Mode
67
+
68
+ The `/getlog` flag sets:
69
+ - `DAT_0088cd4c = 1` (headless replay mode)
70
+ - `local_31 = 1` (getlog flag)
71
+ - Copies replay path to `DAT_0088af58`
72
+
73
+ Later in the function, `/getlog` mode opens the replay for writing the game log output.
74
+
75
+ ## WormKit Module Loading
76
+
77
+ Around line 1200 of decompilation:
78
+ - Searches for `wk*.dll` files in the game directory
79
+ - Loads each via `LoadLibraryA`
80
+ - If loading fails, shows a MessageBox with the error
81
+ - Flag at `DAT_0088e410` tracks whether WormKit modules are loaded
82
+
83
+ ## Key Addresses
84
+
85
+ | Address | Purpose |
86
+ |---------|---------|
87
+ | `0x007A0F10+0` | `/disablecrashhandler` flag |
88
+ | `0x007A0F10+1` | `/silentcrash` flag |
89
+ | `0x0088E410` | WormKit modules loaded flag |
90
+ | `0x0088AF58` | Replay path buffer |
91
+ | `0x0088CD4C` | Headless replay mode flag |
92
+ | `0x0088E17D` | Current directory buffer |
93
+ | `0x006B1FEC` | argc |
94
+ | `0x006B1FF0` | argv |