paavohtl Claude Opus 4.6 (1M context) commited on
Commit
1843fe0
Β·
1 Parent(s): d6be19f

refactor: use heapless::String for replay log formatting

Browse files

Replace hand-rolled SliceFmtWriter + write_cstr with heapless::String<N>
and push_cstr helper for fixed-capacity string building in the replay
log output. Added heapless dependency to openwa-core.

as_cstr() helper null-terminates the heapless::String buffer for C
interop (heapless::String is NOT null-terminated by default).

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

crates/openwa-core/Cargo.toml CHANGED
@@ -5,6 +5,7 @@ edition = "2021"
5
  description = "Core library for Worms Armageddon reverse engineering β€” types, addresses, and runtime utilities"
6
 
7
  [dependencies]
 
8
  hound = "3"
9
 
10
  [target.'cfg(target_os = "windows")'.dependencies]
 
5
  description = "Core library for Worms Armageddon reverse engineering β€” types, addresses, and runtime utilities"
6
 
7
  [dependencies]
8
+ heapless = "0.9"
9
  hound = "3"
10
 
11
  [target.'cfg(target_os = "windows")'.dependencies]
crates/openwa-wormkit/src/replacements/replay.rs CHANGED
@@ -10,6 +10,7 @@ use openwa_core::engine::replay::{self, ReplayError, ReplayStream};
10
  use openwa_core::rebase::rb;
11
 
12
  use core::ffi::c_void;
 
13
  use core::ptr;
14
 
15
  // ─── WA CRT wrappers (rebased addresses, NOT Rust's UCRT) ───────────────────
@@ -637,13 +638,12 @@ unsafe fn write_replay_log(state: u32, log_file: *mut FILE) -> Result<(), Replay
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
 
@@ -673,20 +673,16 @@ unsafe fn write_replay_log(state: u32, log_file: *mut FILE) -> Result<(), Replay
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);
678
- // Game Engine Version line
679
- write_cstr(&mut w, label_game_engine);
680
- let _ = core::fmt::write(&mut w, format_args!(": "));
681
- write_cstr(&mut w, version_str);
682
- // Append mod/compat suffixes (empty for standard replays)
683
- let _ = core::fmt::write(&mut w, format_args!("\n"));
684
- // File Format Version line
685
- write_cstr(&mut w, label_file_format);
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 ─────────────────────────────────────
@@ -698,14 +694,13 @@ unsafe fn write_replay_log(state: u32, log_file: *mut FILE) -> Result<(), Replay
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
 
@@ -756,40 +751,36 @@ unsafe fn write_replay_log(state: u32, log_file: *mut FILE) -> Result<(), Replay
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!(" "));
772
  }
773
 
774
  // Quoted team name
775
- let _ = core::fmt::write(&mut w, format_args!("\""));
776
- write_cstr(&mut w, team_name);
777
- let _ = core::fmt::write(&mut w, format_args!("\""));
778
 
779
  // Team type info
780
  if (team_type as i32) < 0 {
781
- // CPU team: difficulty = abs(team_type) / 20
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);
@@ -802,37 +793,35 @@ unsafe fn write_replay_log(state: u32, log_file: *mut FILE) -> Result<(), Replay
802
  Ok(())
803
  }
804
 
805
- /// Helper: write a C string (null-terminated) into a SliceFmtWriter.
806
- unsafe fn write_cstr(w: &mut SliceFmtWriter, s: *const u8) {
807
- let mut p = s;
808
  while *p != 0 {
809
- let _ = core::fmt::write(w, format_args!("{}", *p as char));
810
  p = p.add(1);
811
  }
812
  }
813
 
814
- /// Helper: length of a null-terminated C string.
 
 
 
 
 
 
 
 
 
 
 
 
 
815
  unsafe fn c_strlen(s: *const u8) -> usize {
816
  let mut len = 0;
817
  while *s.add(len) != 0 { len += 1; }
818
  len
819
  }
820
 
821
- /// A fmt::Write adapter for writing into a fixed-size byte slice with null termination.
822
- struct SliceFmtWriter<'a>(&'a mut [u8], usize);
823
-
824
- impl<'a> core::fmt::Write for SliceFmtWriter<'a> {
825
- fn write_str(&mut self, s: &str) -> core::fmt::Result {
826
- let bytes = s.as_bytes();
827
- let remaining = self.0.len().saturating_sub(self.1 + 1); // leave room for null
828
- let n = bytes.len().min(remaining);
829
- self.0[self.1..self.1 + n].copy_from_slice(&bytes[..n]);
830
- self.1 += n;
831
- self.0[self.1] = 0; // null terminate
832
- Ok(())
833
- }
834
- }
835
-
836
  // ─── Naked asm bridges ──────────────────────────────────────────────────────
837
 
838
  /// Bridge for usercall(ESI=val) + stdcall(1 param). cdecl(esi_val, param, func_addr).
 
10
  use openwa_core::rebase::rb;
11
 
12
  use core::ffi::c_void;
13
+ use core::fmt::Write;
14
  use core::ptr;
15
 
16
  // ─── WA CRT wrappers (rebased addresses, NOT Rust's UCRT) ───────────────────
 
638
  let tm = gmtime(&ts);
639
  if !tm.is_null() {
640
  let tm = &*tm;
641
+ let mut s = heapless::String::<128>::new();
642
+ let _ = write!(s,
 
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(as_cstr(&mut s), log_file);
647
  }
648
  }
649
 
 
673
  let format_ver_str = *(rb(0x6AC624 + replay_ver * 4) as *const u32) as *const u8;
674
 
675
  {
676
+ let mut s = heapless::String::<512>::new();
677
+ push_cstr(&mut s, label_game_engine);
678
+ let _ = write!(s, ": ");
679
+ push_cstr(&mut s, version_str);
680
+ let _ = write!(s, "\n");
681
+ push_cstr(&mut s, label_file_format);
682
+ let _ = write!(s, ": ");
683
+ push_cstr(&mut s, format_ver_str);
684
+ let _ = write!(s, "\n");
685
+ wa_fputs(as_cstr(&mut s), log_file);
 
 
 
 
686
  }
687
 
688
  // ── "Exported with Version" line ─────────────────────────────────────
 
694
  let ver_literal = rb(0x641C60) as *const u8; // "3.8.1"
695
  let ver_suffix = *(rb(0x699814 + ver_byte * 4) as *const u32) as *const u8; // " (Steam)"
696
 
697
+ let mut s = heapless::String::<256>::new();
698
+ push_cstr(&mut s, label_exported);
699
+ let _ = write!(s, ": ");
700
+ push_cstr(&mut s, ver_literal);
701
+ push_cstr(&mut s, ver_suffix);
702
+ let _ = write!(s, "\n\n");
703
+ wa_fputs(as_cstr(&mut s), log_file);
 
704
  }
705
 
706
 
 
751
 
752
  let color_len = clen;
753
 
754
+ let mut s = heapless::String::<256>::new();
 
755
 
756
  // Color name: capitalize first letter (original uses CharUpperA)
757
  if clen > 0 {
758
+ let _ = s.push((*color as char).to_ascii_uppercase());
759
+ push_cstr(&mut s, color.add(1));
 
 
760
  }
761
+ let _ = write!(s, ":");
762
  for _ in 0..(max_color_len - color_len + 1) {
763
+ let _ = s.push(' ');
764
  }
765
 
766
  // Quoted team name
767
+ let _ = s.push('"');
768
+ push_cstr(&mut s, team_name);
769
+ let _ = s.push('"');
770
 
771
  // Team type info
772
  if (team_type as i32) < 0 {
 
773
  let abs_type = -(team_type as i32) as u32;
774
  let whole = abs_type / 20;
775
+ let frac = (abs_type % 20) * 5;
776
+ let cpu_label = wa_load_string(0x6ED);
777
+ let _ = write!(s, " [");
778
+ push_cstr(&mut s, cpu_label);
779
+ let _ = write!(s, " {whole}.{frac:02}]");
780
  }
781
 
782
+ let _ = write!(s, "\n");
783
+ wa_fputs(as_cstr(&mut s), log_file);
784
  }
785
 
786
  wa_fputs(b"\n\0".as_ptr(), log_file);
 
793
  Ok(())
794
  }
795
 
796
+ /// Append a null-terminated C string to a heapless::String.
797
+ unsafe fn push_cstr<const N: usize>(s: &mut heapless::String<N>, cstr: *const u8) {
798
+ let mut p = cstr;
799
  while *p != 0 {
800
+ let _ = s.push(*p as char);
801
  p = p.add(1);
802
  }
803
  }
804
 
805
+ /// Get a null-terminated pointer from a heapless::String for C interop.
806
+ /// Writes a null byte at position len() in the underlying buffer.
807
+ unsafe fn as_cstr<const N: usize>(s: &mut heapless::String<N>) -> *const u8 {
808
+ let len = s.len();
809
+ let buf = s.as_mut_ptr();
810
+ // heapless::String<N> has capacity N, string uses len bytes.
811
+ // Write null at position len (safe if len < N).
812
+ if len < N {
813
+ *buf.add(len) = 0;
814
+ }
815
+ buf as *const u8
816
+ }
817
+
818
+ /// Length of a null-terminated C string.
819
  unsafe fn c_strlen(s: *const u8) -> usize {
820
  let mut len = 0;
821
  while *s.add(len) != 0 { len += 1; }
822
  len
823
  }
824
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
825
  // ─── Naked asm bridges ──────────────────────────────────────────────────────
826
 
827
  /// Bridge for usercall(ESI=val) + stdcall(1 param). cdecl(esi_val, param, func_addr).