refactor: use heapless::String for replay log formatting
Browse filesReplace 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
|
| 641 |
-
let
|
| 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(
|
| 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
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
let _ =
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 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
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 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
|
| 760 |
-
let mut w = SliceFmtWriter(&mut buf, 0);
|
| 761 |
|
| 762 |
// Color name: capitalize first letter (original uses CharUpperA)
|
| 763 |
if clen > 0 {
|
| 764 |
-
let
|
| 765 |
-
|
| 766 |
-
let rest = color.add(1);
|
| 767 |
-
write_cstr(&mut w, rest);
|
| 768 |
}
|
| 769 |
-
let _ =
|
| 770 |
for _ in 0..(max_color_len - color_len + 1) {
|
| 771 |
-
let _ =
|
| 772 |
}
|
| 773 |
|
| 774 |
// Quoted team name
|
| 775 |
-
let _ =
|
| 776 |
-
|
| 777 |
-
let _ =
|
| 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;
|
| 785 |
-
let cpu_label = wa_load_string(0x6ED);
|
| 786 |
-
let _ =
|
| 787 |
-
|
| 788 |
-
let _ =
|
| 789 |
}
|
| 790 |
|
| 791 |
-
let _ =
|
| 792 |
-
wa_fputs(
|
| 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 |
-
///
|
| 806 |
-
unsafe fn
|
| 807 |
-
let mut p =
|
| 808 |
while *p != 0 {
|
| 809 |
-
let _ =
|
| 810 |
p = p.add(1);
|
| 811 |
}
|
| 812 |
}
|
| 813 |
|
| 814 |
-
///
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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).
|