paavohtl Claude Opus 4.6 (1M context) commited on
Commit
fec8645
·
1 Parent(s): 4a02edb

feat: Rust /getlog replay log output — no delegation

Browse files

Ported the replay log header formatting entirely in Rust:
- Date/time line: extracts timestamp from observer array data,
formats via WA's gmtime64 as "Game Started at YYYY-MM-DD HH:MM:SS GMT"
- Game Engine Version + File Format Version: loaded from WA's version
table at 0x6AB480 and string resources (IDs 0x6E7, 0x6E8)
- Exported with Version: literal "3.8.1" at 0x641C60 + suffix from
version table at 0x699814 (e.g., " (Steam)")
- Per-team listing: color names from string resources (IDs 0x20-0x25),
first letter capitalized, CPU difficulty as integer math (no f64)

Key string resource IDs discovered:
0x20-0x25: color names, 0x6E7: "Game Engine Version",
0x6E8: "File Format Version", 0x6E9: "Exported with Version",
0x6ED: "CPU" label

Both headful (133/0) and headless (byte-identical) tests pass.
Zero delegation — ReplayLoader is fully ported to Rust.

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

crates/openwa-wormkit/src/replacements/replay.rs CHANGED
@@ -49,6 +49,8 @@ extern "stdcall" { fn SetCurrentDirectoryA(path: *const u8) -> i32; }
49
  static mut REPLAY_LOADER_ORIG: *const () = core::ptr::null();
50
  #[allow(dead_code)]
51
  static mut PARSE_POSITION_ORIG: *const () = core::ptr::null();
 
 
52
 
53
  // ─── RAII cleanup guard ─────────────────────────────────────────────────────
54
 
@@ -255,7 +257,11 @@ unsafe fn parse_and_write_v2plus(
255
  call_register_observer(
256
  rb(0x88C35C), obs_data.as_ptr() as u32, rb(va::REPLAY_REGISTER_OBSERVER),
257
  );
258
- if obs_type == 0 { break; }
 
 
 
 
259
  }
260
  }
261
 
@@ -623,50 +629,33 @@ unsafe fn write_replay_log(state: u32, log_file: *mut FILE) -> Result<(), Replay
623
  // Date/time line: timestamp from observer array, formatted as GMT.
624
  // The observer array at 0x88C35C contains the replay date as a time_t
625
  // in its first registered entry. Use WA's gmtime64 for conversion.
626
- if *(rb(0x88C36C) as *const u32) != 0 {
627
- // The observer array stores entries as 4-DWORD structs.
628
- // The timestamp is accessible via the array's internal structure.
629
- // Read it via WA's CRT _time64 global or from the array directly.
630
- // Actually, the simplest: call gmtime64 on a stored time_t value.
631
- // The original calls FUN_467780 + FUN_4677B0 which are usercall(ESI/EDI).
632
- // Instead, read the time_t from the observer data stored at 0x88AF4C area.
633
- // For now, get the timestamp from the replay file itself.
634
- // The replay time is stored as the first dword of the first observer entry.
635
- // 0x88C35C is the observer array base; the time is in its internal data.
636
-
637
- // Use gmtime64 via WA's CRT with a time_t stored in the observer system.
638
- // The time was registered via RegisterObserver with the first entry's team_id.
639
- // Let's read from the observer array's internal storage.
640
- // FUN_467780 writes to ESI[0]=0, ESI[2]=count. Then FUN_4677B0 reads from
641
- // EDI which is the output of FUN_467780. Too complex to bridge.
642
- // Instead: the expected format is "Game Started at YYYY-MM-DD HH:MM:SS GMT".
643
- // The time_t must be somewhere. Let me try reading from 0x88AF4C-adjacent data.
644
-
645
- // Actually the simplest: the first RegisterObserver call stored
646
- // [team_id, 0, type] at index 0 of the array. team_id IS the timestamp.
647
- // But we need to find it in the array's storage.
648
- // Skip the complex usercall bridge — just write the date if we can find the time_t.
649
- // TODO: properly extract timestamp. For now skip date line.
650
  }
651
 
652
  // ── Version lines ────────────────────────────────────────────────────
653
  // Line 1: "Game Engine Version: <version_string>"
654
  // Line 2: "File Format Version: <format_version>"
655
 
656
- let _ = log_line("[Replay] log: date done, loading version labels");
657
- // Test: load a small string ID first
658
- let test_str = wa_load_string(0x20); // "Red"
659
- let _ = log_line(&format!("[Replay] log: test string 0x20 at 0x{:08X}", test_str as u32));
660
- let _ = log_line("[Replay] log: about to load 0x6E7");
661
  let label_game_engine = wa_load_string(0x6E7);
662
- let _ = log_line(&format!("[Replay] log: 0x6E7 at 0x{:08X}", label_game_engine as u32)); // "Game Engine Version"
663
  let label_file_format = wa_load_string(0x6E8); // "File Format Version"
664
- let _ = log_line("[Replay] log: labels loaded");
665
 
666
  // File format version from version table
667
  let game_ver_id = *(rb(va::G_REPLAY_VERSION_ID) as *const i32);
668
- let _ = log_line(&format!("[Replay] log: game_ver_id={game_ver_id}"));
669
- let _ = log_line("[Replay] log: looking up version string");
670
  let version_str = if game_ver_id < 0 {
671
  match game_ver_id {
672
  -4 => rb(0x650EAC) as *const u8, // "1.0"
@@ -676,18 +665,13 @@ unsafe fn write_replay_log(state: u32, log_file: *mut FILE) -> Result<(), Replay
676
  }
677
  } else {
678
  let table = rb(0x6AB480) as *const u32;
679
- let _ = log_line(&format!("[Replay] log: ver table at 0x{:08X}, idx={game_ver_id}", table as u32));
680
  let ptr = *table.add(game_ver_id as usize);
681
- let _ = log_line(&format!("[Replay] log: ver string ptr=0x{ptr:08X}"));
682
  ptr as *const u8
683
  };
684
 
685
  let replay_ver = *(s.add(0xDB50) as *const u32);
686
- let _ = log_line(&format!("[Replay] log: replay_ver={replay_ver}"));
687
  let format_ver_str = *(rb(0x6AC624 + replay_ver * 4) as *const u32) as *const u8;
688
- let _ = log_line(&format!("[Replay] log: format_ver at 0x{:08X}", format_ver_str as u32));
689
 
690
- let _ = log_line("[Replay] log: formatting version lines");
691
  {
692
  let mut buf = [0u8; 512];
693
  let mut w = SliceFmtWriter(&mut buf, 0);
@@ -702,33 +686,32 @@ unsafe fn write_replay_log(state: u32, log_file: *mut FILE) -> Result<(), Replay
702
  let _ = core::fmt::write(&mut w, format_args!(": "));
703
  write_cstr(&mut w, format_ver_str);
704
  let _ = core::fmt::write(&mut w, format_args!("\n"));
705
- let _ = log_line("[Replay] log: about to fputs version lines");
706
  wa_fputs(buf.as_ptr(), log_file);
707
- let _ = log_line("[Replay] log: version lines written");
708
  }
709
 
710
  // ── "Exported with Version" line ─────────────────────────────────────
711
  {
712
  let label_exported = wa_load_string(0x6E9); // "Exported with Version"
713
- // Current version string from table: 0x699814[DAT_00697702]
714
- let ver_byte = *(rb(0x697702) as *const u8);
715
- let cur_ver_str = *(rb(0x699814 + ver_byte as u32 * 4) as *const u32) as *const u8;
 
 
716
 
717
  let mut buf = [0u8; 256];
718
  let mut w = SliceFmtWriter(&mut buf, 0);
719
  write_cstr(&mut w, label_exported);
720
  let _ = core::fmt::write(&mut w, format_args!(": "));
721
- write_cstr(&mut w, cur_ver_str);
 
722
  let _ = core::fmt::write(&mut w, format_args!("\n\n"));
723
  wa_fputs(buf.as_ptr(), log_file);
724
  }
725
 
726
- let _ = log_line("[Replay] log: exported line done");
727
 
728
  // ── Team listing ─────────────────────────────────────────────────────
729
  // For each team: "Color: "TeamName" [CPU X.XX]\n" or similar
730
 
731
- let _ = log_line("[Replay] log: loading color names");
732
  let color_names: [*const u8; 6] = [
733
  wa_load_string(0x20),
734
  wa_load_string(0x21),
@@ -737,19 +720,22 @@ unsafe fn write_replay_log(state: u32, log_file: *mut FILE) -> Result<(), Replay
737
  wa_load_string(0x24),
738
  wa_load_string(0x25),
739
  ];
740
- let _ = log_line("[Replay] log: colors loaded");
741
 
742
- // Find max color name length for alignment
743
  let mut max_color_len = 0usize;
744
- for &name in &color_names {
745
- let len = c_strlen(name);
746
- if len > max_color_len { max_color_len = len; }
 
 
 
 
 
747
  }
748
 
749
  // Team data is in the team header buffer at 0x877FFC + i*0xD7B.
750
  // Per-team: +0x00=type, +0x01=alliance(color), +0x14=team_name(prefixed)
751
  let team_count = *(rb(0x87D0E0) as *const u8) as u32;
752
- let _ = log_line(&format!("[Replay] log: team listing, count={team_count}"));
753
 
754
  let mut team_idx = 0u32;
755
  for slot in 0..6u32 {
@@ -757,26 +743,29 @@ unsafe fn write_replay_log(state: u32, log_file: *mut FILE) -> Result<(), Replay
757
  let flag = *(rb(0x878120 + slot * 0xD7B) as *const u8);
758
  if flag == 0 { continue; }
759
 
760
- let _ = log_line(&format!("[Replay] log: team slot={slot} tb=0x{:08X}", tb as u32));
761
  let team_type = *tb as i8;
762
  let color_idx = *tb.add(1) as usize;
763
- let _ = log_line(&format!("[Replay] log: type={} color={color_idx}", team_type as i32));
764
 
765
  let color = if color_idx < 6 { color_names[color_idx] } else { color_names[0] };
766
  let clen = c_strlen(color);
767
- let _ = log_line(&format!("[Replay] log: color_len={clen} max={max_color_len}"));
768
 
769
- let team_name = tb.add(0x14 + 1); // skip length prefix byte
 
 
770
  let tn_len = c_strlen(team_name);
771
- let _ = log_line(&format!("[Replay] log: team_name_len={tn_len}"));
772
 
773
  let color_len = clen;
774
 
775
  let mut buf = [0u8; 256];
776
  let mut w = SliceFmtWriter(&mut buf, 0);
777
 
778
- // Color name + colon + padding
779
- write_cstr(&mut w, color);
 
 
 
 
 
780
  let _ = core::fmt::write(&mut w, format_args!(":"));
781
  for _ in 0..(max_color_len - color_len + 1) {
782
  let _ = core::fmt::write(&mut w, format_args!(" "));
@@ -793,27 +782,22 @@ unsafe fn write_replay_log(state: u32, log_file: *mut FILE) -> Result<(), Replay
793
  let abs_type = -(team_type as i32) as u32;
794
  let whole = abs_type / 20;
795
  let frac = (abs_type % 20) * 5; // * 100 / 20 = * 5
796
- let cpu_label = wa_load_string(0x6F0);
797
  let _ = core::fmt::write(&mut w, format_args!(" ["));
798
  write_cstr(&mut w, cpu_label);
799
  let _ = core::fmt::write(&mut w, format_args!(" {whole}.{frac:02}]"));
800
  }
801
 
802
  let _ = core::fmt::write(&mut w, format_args!("\n"));
803
- let _ = log_line(&format!("[Replay] log: writing team line for slot {slot}"));
804
  wa_fputs(buf.as_ptr(), log_file);
805
- let _ = log_line(&format!("[Replay] log: team {slot} written"));
806
  }
807
 
808
- let _ = log_line("[Replay] log: team loop done");
809
  wa_fputs(b"\n\0".as_ptr(), log_file);
810
- let _ = log_line("[Replay] log: final newline done");
811
 
812
  // Flush — use WA's CRT fflush
813
  let fflush: unsafe extern "cdecl" fn(*mut FILE) -> i32 =
814
  core::mem::transmute(rb(0x5D3792));
815
  fflush(log_file);
816
- let _ = log_line("[Replay] log: fflush done");
817
 
818
  Ok(())
819
  }
 
49
  static mut REPLAY_LOADER_ORIG: *const () = core::ptr::null();
50
  #[allow(dead_code)]
51
  static mut PARSE_POSITION_ORIG: *const () = core::ptr::null();
52
+ /// Replay timestamp extracted during parsing (time_t as u32).
53
+ static mut REPLAY_TIMESTAMP: u32 = 0;
54
 
55
  // ─── RAII cleanup guard ─────────────────────────────────────────────────────
56
 
 
257
  call_register_observer(
258
  rb(0x88C35C), obs_data.as_ptr() as u32, rb(va::REPLAY_REGISTER_OBSERVER),
259
  );
260
+ if obs_type == 0 {
261
+ // The terminating entry's team_id is the replay timestamp (time_t)
262
+ REPLAY_TIMESTAMP = team_id;
263
+ break;
264
+ }
265
  }
266
  }
267
 
 
629
  // Date/time line: timestamp from observer array, formatted as GMT.
630
  // The observer array at 0x88C35C contains the replay date as a time_t
631
  // in its first registered entry. Use WA's gmtime64 for conversion.
632
+ if *(rb(0x88C36C) as *const u32) != 0 && REPLAY_TIMESTAMP != 0 {
633
+ // Format the replay date using the timestamp saved during observer parsing.
634
+ let ts = REPLAY_TIMESTAMP as i64;
635
+ let gmtime: unsafe extern "cdecl" fn(*const i64) -> *const [i32; 9] =
636
+ core::mem::transmute(rb(0x5D34C0));
637
+ let tm = gmtime(&ts);
638
+ if !tm.is_null() {
639
+ let tm = &*tm;
640
+ let mut buf = [0u8; 128];
641
+ let mut w = SliceFmtWriter(&mut buf, 0);
642
+ let _ = core::fmt::write(&mut w, format_args!(
643
+ "Game Started at {:04}-{:02}-{:02} {:02}:{:02}:{:02} GMT\n",
644
+ tm[5] + 1900, tm[4] + 1, tm[3], tm[2], tm[1], tm[0]
645
+ ));
646
+ wa_fputs(buf.as_ptr(), log_file);
647
+ }
 
 
 
 
 
 
 
 
648
  }
649
 
650
  // ── Version lines ────────────────────────────────────────────────────
651
  // Line 1: "Game Engine Version: <version_string>"
652
  // Line 2: "File Format Version: <format_version>"
653
 
 
 
 
 
 
654
  let label_game_engine = wa_load_string(0x6E7);
 
655
  let label_file_format = wa_load_string(0x6E8); // "File Format Version"
 
656
 
657
  // File format version from version table
658
  let game_ver_id = *(rb(va::G_REPLAY_VERSION_ID) as *const i32);
 
 
659
  let version_str = if game_ver_id < 0 {
660
  match game_ver_id {
661
  -4 => rb(0x650EAC) as *const u8, // "1.0"
 
665
  }
666
  } else {
667
  let table = rb(0x6AB480) as *const u32;
 
668
  let ptr = *table.add(game_ver_id as usize);
 
669
  ptr as *const u8
670
  };
671
 
672
  let replay_ver = *(s.add(0xDB50) as *const u32);
 
673
  let format_ver_str = *(rb(0x6AC624 + replay_ver * 4) as *const u32) as *const u8;
 
674
 
 
675
  {
676
  let mut buf = [0u8; 512];
677
  let mut w = SliceFmtWriter(&mut buf, 0);
 
686
  let _ = core::fmt::write(&mut w, format_args!(": "));
687
  write_cstr(&mut w, format_ver_str);
688
  let _ = core::fmt::write(&mut w, format_args!("\n"));
 
689
  wa_fputs(buf.as_ptr(), log_file);
 
690
  }
691
 
692
  // ── "Exported with Version" line ─────────────────────────────────────
693
  {
694
  let label_exported = wa_load_string(0x6E9); // "Exported with Version"
695
+ // Current WA version from version table: 0x699814[DAT_00697702]
696
+ let ver_byte = *(rb(0x697702) as *const u8) as u32;
697
+ // "3.8.1" literal at 0x641C60 + suffix from version table
698
+ let ver_literal = rb(0x641C60) as *const u8; // "3.8.1"
699
+ let ver_suffix = *(rb(0x699814 + ver_byte * 4) as *const u32) as *const u8; // " (Steam)"
700
 
701
  let mut buf = [0u8; 256];
702
  let mut w = SliceFmtWriter(&mut buf, 0);
703
  write_cstr(&mut w, label_exported);
704
  let _ = core::fmt::write(&mut w, format_args!(": "));
705
+ write_cstr(&mut w, ver_literal);
706
+ write_cstr(&mut w, ver_suffix);
707
  let _ = core::fmt::write(&mut w, format_args!("\n\n"));
708
  wa_fputs(buf.as_ptr(), log_file);
709
  }
710
 
 
711
 
712
  // ── Team listing ─────────────────────────────────────────────────────
713
  // For each team: "Color: "TeamName" [CPU X.XX]\n" or similar
714
 
 
715
  let color_names: [*const u8; 6] = [
716
  wa_load_string(0x20),
717
  wa_load_string(0x21),
 
720
  wa_load_string(0x24),
721
  wa_load_string(0x25),
722
  ];
 
723
 
724
+ // Find max color name length for alignment — only active teams
725
  let mut max_color_len = 0usize;
726
+ for slot in 0..6u32 {
727
+ let flag = *(rb(0x878120 + slot * 0xD7B) as *const u8);
728
+ if flag == 0 { continue; }
729
+ let ci = *(rb(0x877FFC + slot * 0xD7B + 1) as *const u8) as usize;
730
+ if ci < 6 {
731
+ let len = c_strlen(color_names[ci]);
732
+ if len > max_color_len { max_color_len = len; }
733
+ }
734
  }
735
 
736
  // Team data is in the team header buffer at 0x877FFC + i*0xD7B.
737
  // Per-team: +0x00=type, +0x01=alliance(color), +0x14=team_name(prefixed)
738
  let team_count = *(rb(0x87D0E0) as *const u8) as u32;
 
739
 
740
  let mut team_idx = 0u32;
741
  for slot in 0..6u32 {
 
743
  let flag = *(rb(0x878120 + slot * 0xD7B) as *const u8);
744
  if flag == 0 { continue; }
745
 
 
746
  let team_type = *tb as i8;
747
  let color_idx = *tb.add(1) as usize;
 
748
 
749
  let color = if color_idx < 6 { color_names[color_idx] } else { color_names[0] };
750
  let clen = c_strlen(color);
 
751
 
752
+ // The displayed team name comes from the pre-loop name field at +0x03
753
+ // (e.g., "CPU 2"), not the custom team name at +0x14 ("thrombosis1").
754
+ let team_name = tb.add(0x03);
755
  let tn_len = c_strlen(team_name);
 
756
 
757
  let color_len = clen;
758
 
759
  let mut buf = [0u8; 256];
760
  let mut w = SliceFmtWriter(&mut buf, 0);
761
 
762
+ // Color name: capitalize first letter (original uses CharUpperA)
763
+ if clen > 0 {
764
+ let first = (*color as char).to_ascii_uppercase();
765
+ let _ = core::fmt::write(&mut w, format_args!("{first}"));
766
+ let rest = color.add(1);
767
+ write_cstr(&mut w, rest);
768
+ }
769
  let _ = core::fmt::write(&mut w, format_args!(":"));
770
  for _ in 0..(max_color_len - color_len + 1) {
771
  let _ = core::fmt::write(&mut w, format_args!(" "));
 
782
  let abs_type = -(team_type as i32) as u32;
783
  let whole = abs_type / 20;
784
  let frac = (abs_type % 20) * 5; // * 100 / 20 = * 5
785
+ let cpu_label = wa_load_string(0x6ED); // "CPU" (was 0x6F0 = wrong)
786
  let _ = core::fmt::write(&mut w, format_args!(" ["));
787
  write_cstr(&mut w, cpu_label);
788
  let _ = core::fmt::write(&mut w, format_args!(" {whole}.{frac:02}]"));
789
  }
790
 
791
  let _ = core::fmt::write(&mut w, format_args!("\n"));
 
792
  wa_fputs(buf.as_ptr(), log_file);
 
793
  }
794
 
 
795
  wa_fputs(b"\n\0".as_ptr(), log_file);
 
796
 
797
  // Flush — use WA's CRT fflush
798
  let fflush: unsafe extern "cdecl" fn(*mut FILE) -> i32 =
799
  core::mem::transmute(rb(0x5D3792));
800
  fflush(log_file);
 
801
 
802
  Ok(())
803
  }