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

wip: Rust log output — structure works, content needs fixing

Browse files

Log output now writes to /getlog file without crashing:
- Version labels loaded via WA's string resources (stdcall!)
- Version string lookup from table at 0x6AB480
- Team listing with color names and CPU difficulty
- Date line skipped (needs timestamp extraction)

Known issues to fix:
- Team name: wrong offset (shows "hrombosis1" not "CPU 2")
- Version string missing from "Exported with" line
- Color names lowercase (string resource gives lowercase?)
- "Spectator" instead of "CPU" for team type label
- Padding alignment too wide (uses all 6 colors, not just active)
- Date line missing (timestamp extraction TODO)

Bugs found and fixed:
- wa_load_string: stdcall (RET 0x4), not cdecl
- fflush: wrong address (was 0x5D5068 inside _filelengthi64)

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

crates/openwa-wormkit/src/replacements/replay.rs CHANGED
@@ -164,22 +164,17 @@ unsafe fn replay_loader_play(state: u32) -> Result<(), ReplayError> {
164
 
165
  match result {
166
  Ok(()) => {
167
- let _ = log_line("[Replay] Rust replay loading complete");
168
-
169
- // The /getlog log header (date, version strings, team listing) is
170
- // produced by the original ReplayLoader's log output section (~600
171
- // lines of string resource formatting + codepage conversion).
172
- // Rather than porting all of that, we delegate to the original which
173
- // re-parses the replay and writes the log header. The globals are
174
- // overwritten with identical values since the replay data is the same.
175
- // This is double work but produces byte-identical log output.
176
  let log_file = *(rb(0x88C370) as *const *mut FILE);
177
  if !log_file.is_null() {
178
- let _ = log_line("[Replay] Delegating for /getlog header output");
179
- delegate_to_original(state)
180
- } else {
181
- Ok(())
182
  }
 
 
183
  }
184
  Err(e) => {
185
  let _ = log_line(&format!("[Replay] Parse failed ({e:?}), falling back to original"));
@@ -593,16 +588,301 @@ unsafe fn parse_and_write_v2plus(
593
  release(map_obj, 1);
594
  }
595
 
596
- // ── Log output ───────────────────────────────────────────────────────
597
- // TODO: Port the ~600-line /getlog formatted output.
598
- // For now, the log output is missing — headless tests will fail.
599
- // Headful tests should work since log output is only for /getlog.
600
-
601
  Ok(())
602
  }
603
 
604
  // ─── Naked asm bridge for ProcessSchemeDefaults (usercall ESI=state) ─────────
605
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
606
  /// Bridge for usercall(ESI=value) + plain call. cdecl(esi_val, func_addr).
607
  #[unsafe(naked)]
608
  unsafe extern "cdecl" fn call_usercall_esi(_esi_val: u32, _func: u32) {
 
164
 
165
  match result {
166
  Ok(()) => {
167
+ let _ = log_line("[Replay] about to call ProcessReplayFlags");
168
+ call_usercall_eax(state, rb(va::REPLAY_PROCESS_FLAGS));
169
+ let _ = log_line("[Replay] ProcessReplayFlags done");
 
 
 
 
 
 
170
  let log_file = *(rb(0x88C370) as *const *mut FILE);
171
  if !log_file.is_null() {
172
+ let _ = log_line("[Replay] writing log output");
173
+ write_replay_log(state, log_file)?;
174
+ let _ = log_line("[Replay] log output done");
 
175
  }
176
+ let _ = log_line("[Replay] Rust replay loading complete");
177
+ Ok(())
178
  }
179
  Err(e) => {
180
  let _ = log_line(&format!("[Replay] Parse failed ({e:?}), falling back to original"));
 
588
  release(map_obj, 1);
589
  }
590
 
591
+ // Log output is written by the caller after map loading completes.
 
 
 
 
592
  Ok(())
593
  }
594
 
595
  // ─── Naked asm bridge for ProcessSchemeDefaults (usercall ESI=state) ─────────
596
 
597
+ // ─── Log output formatting ──────────────────────────────────────────────────
598
+
599
+ /// Load a string resource by ID using WA's string table.
600
+ unsafe fn wa_load_string(id: u32) -> *const u8 {
601
+ let f: unsafe extern "stdcall" fn(u32) -> *const u8 =
602
+ core::mem::transmute(rb(0x593180));
603
+ f(id)
604
+ }
605
+
606
+ /// Write a C string to the log file via WA's CRT fputs.
607
+ unsafe fn wa_fputs(s: *const u8, file: *mut FILE) {
608
+ let fputs: unsafe extern "cdecl" fn(*const u8, *mut FILE) -> i32 =
609
+ core::mem::transmute(*(rb(0x649468) as *const u32));
610
+ fputs(s, file);
611
+ }
612
+
613
+ /// Write the /getlog replay header to the log file.
614
+ unsafe fn write_replay_log(state: u32, log_file: *mut FILE) -> Result<(), ReplayError> {
615
+ let s = state as *const u8;
616
+
617
+ // ── Date/time line ───────────────────────────────────────────────────
618
+ // "Game Started at YYYY-MM-DD HH:MM:SS GMT"
619
+ // Only if DAT_0088C36C != 0 (recording timestamp available)
620
+ // Date/time line: uses FUN_467780 (stdcall+usercall ESI=output)
621
+ // and FUN_4677B0 + gmtime64. Complex calling convention.
622
+ // For now, use the original's date formatting via a bridge call.
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"
673
+ -2 => rb(0x650EB0) as *const u8, // "3.0"
674
+ -1 => rb(0x650EB4) as *const u8, // "3.5 Beta 1"
675
+ _ => wa_load_string(0x0F), // generic "Unknown"
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);
694
+ // Game Engine Version line
695
+ write_cstr(&mut w, label_game_engine);
696
+ let _ = core::fmt::write(&mut w, format_args!(": "));
697
+ write_cstr(&mut w, version_str);
698
+ // Append mod/compat suffixes (empty for standard replays)
699
+ let _ = core::fmt::write(&mut w, format_args!("\n"));
700
+ // File Format Version line
701
+ write_cstr(&mut w, label_file_format);
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),
735
+ wa_load_string(0x22),
736
+ wa_load_string(0x23),
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 {
756
+ let tb = rb(0x877FFC + slot * 0xD7B) as *const u8;
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!(" "));
783
+ }
784
+
785
+ // Quoted team name
786
+ let _ = core::fmt::write(&mut w, format_args!("\""));
787
+ write_cstr(&mut w, team_name);
788
+ let _ = core::fmt::write(&mut w, format_args!("\""));
789
+
790
+ // Team type info
791
+ if (team_type as i32) < 0 {
792
+ // CPU team: difficulty = abs(team_type) / 20
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
+ }
820
+
821
+ /// Helper: write a C string (null-terminated) into a SliceFmtWriter.
822
+ unsafe fn write_cstr(w: &mut SliceFmtWriter, s: *const u8) {
823
+ let mut p = s;
824
+ while *p != 0 {
825
+ let _ = core::fmt::write(w, format_args!("{}", *p as char));
826
+ p = p.add(1);
827
+ }
828
+ }
829
+
830
+ /// Helper: length of a null-terminated C string.
831
+ unsafe fn c_strlen(s: *const u8) -> usize {
832
+ let mut len = 0;
833
+ while *s.add(len) != 0 { len += 1; }
834
+ len
835
+ }
836
+
837
+ /// A fmt::Write adapter for writing into a fixed-size byte slice with null termination.
838
+ struct SliceFmtWriter<'a>(&'a mut [u8], usize);
839
+
840
+ impl<'a> core::fmt::Write for SliceFmtWriter<'a> {
841
+ fn write_str(&mut self, s: &str) -> core::fmt::Result {
842
+ let bytes = s.as_bytes();
843
+ let remaining = self.0.len().saturating_sub(self.1 + 1); // leave room for null
844
+ let n = bytes.len().min(remaining);
845
+ self.0[self.1..self.1 + n].copy_from_slice(&bytes[..n]);
846
+ self.1 += n;
847
+ self.0[self.1] = 0; // null terminate
848
+ Ok(())
849
+ }
850
+ }
851
+
852
+ // ─── Naked asm bridges ──────────────────────────────────────────────────────
853
+
854
+ /// Bridge for usercall(ESI=val) + stdcall(1 param). cdecl(esi_val, param, func_addr).
855
+ #[unsafe(naked)]
856
+ unsafe extern "cdecl" fn call_usercall_esi_stdcall1(
857
+ _esi_val: u32, _param: u32, _func: u32,
858
+ ) {
859
+ // Stack on entry: [ESP+0]=ret [ESP+4]=esi_val [ESP+8]=param [ESP+12]=func
860
+ core::arch::naked_asm!(
861
+ "push esi",
862
+ "push edi",
863
+ // Now: [ESP+0]=edi [ESP+4]=esi [ESP+8]=ret [ESP+12]=esi_val [ESP+16]=param [ESP+20]=func
864
+ "mov esi, [esp+12]", // esi_val
865
+ "push [esp+16]", // push param for stdcall
866
+ // Now: [ESP+0]=param [ESP+4]=edi [ESP+8]=esi [ESP+12]=ret [ESP+16]=esi_val [ESP+20]=param_orig [ESP+24]=func
867
+ "mov eax, [esp+24]", // func_addr
868
+ "call eax", // stdcall cleans 1 param (4 bytes)
869
+ "pop edi",
870
+ "pop esi",
871
+ "ret",
872
+ );
873
+ }
874
+
875
+ /// Bridge for usercall(EAX=value) + plain call. cdecl(eax_val, func_addr).
876
+ #[unsafe(naked)]
877
+ unsafe extern "cdecl" fn call_usercall_eax(_eax_val: u32, _func: u32) {
878
+ core::arch::naked_asm!(
879
+ "mov eax, [esp+4]", // eax_val
880
+ "mov ecx, [esp+8]", // func_addr
881
+ "call ecx",
882
+ "ret",
883
+ );
884
+ }
885
+
886
  /// Bridge for usercall(ESI=value) + plain call. cdecl(esi_val, func_addr).
887
  #[unsafe(naked)]
888
  unsafe extern "cdecl" fn call_usercall_esi(_esi_val: u32, _func: u32) {