wip: Rust log output — structure works, content needs fixing
Browse filesLog 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>
|
@@ -164,22 +164,17 @@ unsafe fn replay_loader_play(state: u32) -> Result<(), ReplayError> {
|
|
| 164 |
|
| 165 |
match result {
|
| 166 |
Ok(()) => {
|
| 167 |
-
let _ = log_line("[Replay]
|
| 168 |
-
|
| 169 |
-
|
| 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]
|
| 179 |
-
|
| 180 |
-
|
| 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 |
-
//
|
| 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) {
|