style: cargo fmt across all crates
Browse filesCo-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- crates/openwa-core/src/address.rs +25 -30
- crates/openwa-core/src/display/dd_display.rs +15 -2
- crates/openwa-core/src/engine/ddgame.rs +89 -32
- crates/openwa-core/src/engine/ddgame_constructor.rs +28 -25
- crates/openwa-core/src/engine/game_info.rs +0 -1
- crates/openwa-core/src/engine/game_state_init.rs +4 -6
- crates/openwa-core/src/engine/mod.rs +8 -8
- crates/openwa-core/src/engine/replay.rs +7 -7
- crates/openwa-core/src/engine/team_arena.rs +10 -3
- crates/openwa-core/src/fixed.rs +10 -2
- crates/openwa-core/src/game/scheme.rs +12 -13
- crates/openwa-core/src/lib.rs +3 -3
- crates/openwa-core/src/mem.rs +7 -9
- crates/openwa-core/src/registry.rs +39 -11
- crates/openwa-core/src/snapshot.rs +36 -10
- crates/openwa-core/src/task/arrow.rs +2 -1
- crates/openwa-core/src/task/base.rs +12 -13
- crates/openwa-core/src/task/cloud.rs +2 -1
- crates/openwa-core/src/task/filter.rs +2 -1
- crates/openwa-core/src/task/fire.rs +2 -1
- crates/openwa-core/src/task/game_task.rs +2 -1
- crates/openwa-core/src/task/mine_oil_drum.rs +4 -2
- crates/openwa-core/src/task/missile.rs +65 -23
- crates/openwa-core/src/task/mod.rs +3 -1
- crates/openwa-core/src/task/supply_crate.rs +2 -1
- crates/openwa-core/src/task/team.rs +14 -10
- crates/openwa-core/src/task/turn_game.rs +2 -1
- crates/openwa-core/src/task/worm.rs +55 -20
- crates/openwa-debug-cli/src/main.rs +174 -73
- crates/openwa-debug-proto/src/lib.rs +56 -16
- crates/openwa-debugui/src/app.rs +6 -17
- crates/openwa-derive/src/lib.rs +15 -11
- crates/openwa-test-runner/src/main.rs +32 -13
- crates/openwa-wormkit/src/debug_server.rs +35 -21
- crates/openwa-wormkit/src/debug_sync.rs +13 -7
- crates/openwa-wormkit/src/debug_watchpoint.rs +6 -6
- crates/openwa-wormkit/src/lib.rs +6 -3
- crates/openwa-wormkit/src/replacements/file_isolation.rs +2 -3
- crates/openwa-wormkit/src/replacements/game_session.rs +22 -8
- crates/openwa-wormkit/src/replacements/input.rs +14 -5
- crates/openwa-wormkit/src/replacements/render.rs +4 -5
- crates/openwa-wormkit/src/replacements/replay.rs +198 -82
- crates/openwa-wormkit/src/replacements/task/cloud.rs +2 -1
- crates/openwa-wormkit/src/replacements/weapon_release.rs +98 -54
- crates/openwa-wormkit/src/snapshot.rs +25 -13
- crates/openwa-wormkit/src/validation/mod.rs +13 -11
crates/openwa-core/src/address.rs
CHANGED
|
@@ -29,46 +29,41 @@ pub mod va {
|
|
| 29 |
|
| 30 |
// Re-exported from task modules
|
| 31 |
pub use crate::task::base::{
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
CTASK_SEA_BUBBLE_VTABLE, CTASK_SEABUBBLE_CTOR,
|
| 40 |
-
CTASK_AIRSTRIKE_CTOR, CTASK_ARROW_CTOR, CTASK_CANISTER_CTOR,
|
| 41 |
-
CTASK_CROSS_CTOR, CTASK_FIREBALL_CTOR, CTASK_FLAME_CTOR,
|
| 42 |
-
CTASK_GAS_CTOR, CTASK_OLDWORM_CTOR, CTASK_SCOREBUBBLE_CTOR,
|
| 43 |
-
CTASK_SMOKE_CTOR,
|
| 44 |
};
|
|
|
|
|
|
|
|
|
|
| 45 |
pub use crate::task::game_task::{
|
| 46 |
-
|
| 47 |
-
|
| 48 |
};
|
| 49 |
-
pub use crate::task::worm::{CTASK_WORM_VTABLE, CTASK_WORM_CONSTRUCTOR};
|
| 50 |
-
pub use crate::task::turn_game::{
|
| 51 |
-
CTASK_TURN_GAME_VTABLE, CTASK_TURNGAME_CTOR,
|
| 52 |
-
TURNGAME_HANDLE_MESSAGE, TURNGAME_HURRY_HANDLER, TURNGAME_AUTO_SELECT_TEAMS,
|
| 53 |
-
};
|
| 54 |
-
pub use crate::task::team::{CTASK_TEAM_VTABLE, CTASK_TEAM_CTOR};
|
| 55 |
-
pub use crate::task::missile::{CTASK_MISSILE_VTABLE, CTASK_MISSILE_CTOR};
|
| 56 |
pub use crate::task::mine_oil_drum::{
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
};
|
| 59 |
-
pub use crate::task::
|
| 60 |
-
pub use crate::task::cloud::{CTASK_CLOUD_VTABLE, CTASK_CLOUD_CTOR};
|
| 61 |
-
pub use crate::task::filter::{CTASK_FILTER_VTABLE, CTASK_FILTER_CTOR};
|
| 62 |
-
pub use crate::task::fire::{CTASK_FIRE_VTABLE, CTASK_FIRE_CTOR};
|
| 63 |
|
| 64 |
// Re-exported from modules using #[vtable(...)] attribute
|
| 65 |
-
pub use crate::
|
|
|
|
| 66 |
pub use crate::display::dd_display::DD_DISPLAY_VTABLE;
|
| 67 |
-
pub use crate::
|
| 68 |
pub use crate::frontend::map_view::MAP_VIEW_VTABLE;
|
|
|
|
| 69 |
pub use crate::task::game_task::SOUND_EMITTER_VTABLE;
|
| 70 |
-
pub use crate::display::base::DISPLAY_BASE_VTABLE;
|
| 71 |
-
pub use crate::audio::dssound::DS_SOUND_VTABLE;
|
| 72 |
|
| 73 |
crate::define_addresses! {
|
| 74 |
class "DDGameWrapper" {
|
|
|
|
| 29 |
|
| 30 |
// Re-exported from task modules
|
| 31 |
pub use crate::task::base::{
|
| 32 |
+
CTASK_AIRSTRIKE_CTOR, CTASK_ARROW_CTOR, CTASK_CANISTER_CTOR, CTASK_CONSTRUCTOR,
|
| 33 |
+
CTASK_CPU_CTOR, CTASK_CPU_VTABLE, CTASK_CROSS_CTOR, CTASK_DIRT_CTOR, CTASK_DIRT_VTABLE,
|
| 34 |
+
CTASK_FIREBALL_CTOR, CTASK_FLAME_CTOR, CTASK_GAS_CTOR, CTASK_LAND_CTOR, CTASK_LAND_VTABLE,
|
| 35 |
+
CTASK_OLDWORM_CTOR, CTASK_SCOREBUBBLE_CTOR, CTASK_SEABUBBLE_CTOR, CTASK_SEA_BUBBLE_VTABLE,
|
| 36 |
+
CTASK_SMOKE_CTOR, CTASK_SPRITE_ANIM_CTOR, CTASK_SPRITE_ANIM_VTABLE, CTASK_VT0_INIT,
|
| 37 |
+
CTASK_VT1_FREE, CTASK_VT2_HANDLE_MESSAGE, CTASK_VT3, CTASK_VT5, CTASK_VT6,
|
| 38 |
+
CTASK_VT7_PROCESS_FRAME, CTASK_VTABLE,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
};
|
| 40 |
+
pub use crate::task::cloud::{CTASK_CLOUD_CTOR, CTASK_CLOUD_VTABLE};
|
| 41 |
+
pub use crate::task::filter::{CTASK_FILTER_CTOR, CTASK_FILTER_VTABLE};
|
| 42 |
+
pub use crate::task::fire::{CTASK_FIRE_CTOR, CTASK_FIRE_VTABLE};
|
| 43 |
pub use crate::task::game_task::{
|
| 44 |
+
CGAMETASK_CONSTRUCTOR, CGAMETASK_SOUND_EMITTER_VT, CGAMETASK_VT0, CGAMETASK_VT1_FREE,
|
| 45 |
+
CGAMETASK_VT2_HANDLE_MESSAGE, CGAMETASK_VTABLE,
|
| 46 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
pub use crate::task::mine_oil_drum::{
|
| 48 |
+
CTASK_MINE_CTOR, CTASK_MINE_VTABLE, CTASK_OILDRUM_CTOR, CTASK_OILDRUM_VTABLE,
|
| 49 |
+
};
|
| 50 |
+
pub use crate::task::missile::{CTASK_MISSILE_CTOR, CTASK_MISSILE_VTABLE};
|
| 51 |
+
pub use crate::task::supply_crate::{CTASK_CRATE_CTOR, CTASK_CRATE_VTABLE};
|
| 52 |
+
pub use crate::task::team::{CTASK_TEAM_CTOR, CTASK_TEAM_VTABLE};
|
| 53 |
+
pub use crate::task::turn_game::{
|
| 54 |
+
CTASK_TURNGAME_CTOR, CTASK_TURN_GAME_VTABLE, TURNGAME_AUTO_SELECT_TEAMS,
|
| 55 |
+
TURNGAME_HANDLE_MESSAGE, TURNGAME_HURRY_HANDLER,
|
| 56 |
};
|
| 57 |
+
pub use crate::task::worm::{CTASK_WORM_CONSTRUCTOR, CTASK_WORM_VTABLE};
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
// Re-exported from modules using #[vtable(...)] attribute
|
| 60 |
+
pub use crate::audio::dssound::DS_SOUND_VTABLE;
|
| 61 |
+
pub use crate::display::base::DISPLAY_BASE_VTABLE;
|
| 62 |
pub use crate::display::dd_display::DD_DISPLAY_VTABLE;
|
| 63 |
+
pub use crate::display::palette::PALETTE_VTABLE;
|
| 64 |
pub use crate::frontend::map_view::MAP_VIEW_VTABLE;
|
| 65 |
+
pub use crate::input::controller::INPUT_CTRL_VTABLE;
|
| 66 |
pub use crate::task::game_task::SOUND_EMITTER_VTABLE;
|
|
|
|
|
|
|
| 67 |
|
| 68 |
crate::define_addresses! {
|
| 69 |
class "DDGameWrapper" {
|
crates/openwa-core/src/display/dd_display.rs
CHANGED
|
@@ -31,10 +31,23 @@ pub struct DDDisplayVtable {
|
|
| 31 |
pub set_layer_visibility: fn(this: *mut DDDisplay, layer: i32, value: i32),
|
| 32 |
/// load sprite with flag (RET 0x14)
|
| 33 |
#[slot(31)]
|
| 34 |
-
pub load_sprite: fn(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
/// load sprite by layer (RET 0x10)
|
| 36 |
#[slot(37)]
|
| 37 |
-
pub load_sprite_by_layer: fn(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
}
|
| 39 |
|
| 40 |
// Generate calling wrappers: DDDisplay::set_layer_color(), etc.
|
|
|
|
| 31 |
pub set_layer_visibility: fn(this: *mut DDDisplay, layer: i32, value: i32),
|
| 32 |
/// load sprite with flag (RET 0x14)
|
| 33 |
#[slot(31)]
|
| 34 |
+
pub load_sprite: fn(
|
| 35 |
+
this: *mut DDDisplay,
|
| 36 |
+
layer: u32,
|
| 37 |
+
id: u32,
|
| 38 |
+
flag: u32,
|
| 39 |
+
gfx: *mut u8,
|
| 40 |
+
name: *const core::ffi::c_char,
|
| 41 |
+
) -> i32,
|
| 42 |
/// load sprite by layer (RET 0x10)
|
| 43 |
#[slot(37)]
|
| 44 |
+
pub load_sprite_by_layer: fn(
|
| 45 |
+
this: *mut DDDisplay,
|
| 46 |
+
layer: u32,
|
| 47 |
+
id: u32,
|
| 48 |
+
gfx: *mut u8,
|
| 49 |
+
name: *const core::ffi::c_char,
|
| 50 |
+
) -> i32,
|
| 51 |
}
|
| 52 |
|
| 53 |
// Generate calling wrappers: DDDisplay::set_layer_color(), etc.
|
crates/openwa-core/src/engine/ddgame.rs
CHANGED
|
@@ -1,17 +1,17 @@
|
|
| 1 |
-
use crate::FieldRegistry;
|
| 2 |
use crate::audio::active_sound::ActiveSoundTable;
|
| 3 |
-
use crate::fixed::Fixed;
|
| 4 |
use crate::audio::dssound::DSSound;
|
| 5 |
use crate::audio::music::Music;
|
| 6 |
use crate::audio::speech::SpeechSlotTable;
|
| 7 |
use crate::display::dd_display::DDDisplay;
|
| 8 |
use crate::display::palette::Palette;
|
| 9 |
use crate::engine::game_info::GameInfo;
|
|
|
|
| 10 |
use crate::game::weapon::WeaponTable;
|
| 11 |
use crate::input::keyboard::DDKeyboard;
|
| 12 |
use crate::render::landscape::PCLandscape;
|
| 13 |
use crate::render::queue::RenderQueue;
|
| 14 |
use crate::render::turn_order::TurnOrderWidget;
|
|
|
|
| 15 |
|
| 16 |
// Re-export constructor items so existing `engine::ddgame::*` imports keep working.
|
| 17 |
pub use super::ddgame_constructor::{
|
|
@@ -24,9 +24,9 @@ pub use crate::render::gfx_dir::gfx_dir_load_dir;
|
|
| 24 |
|
| 25 |
// Re-export all team_arena types so existing `engine::ddgame::*` imports keep working.
|
| 26 |
pub use crate::engine::team_arena::{
|
| 27 |
-
CoordEntry, CoordList, CoordListEntry,
|
| 28 |
-
|
| 29 |
-
|
| 30 |
};
|
| 31 |
|
| 32 |
/// DDGame β the main game engine object.
|
|
@@ -483,8 +483,8 @@ impl DDGame {
|
|
| 483 |
/// # Safety
|
| 484 |
/// `self.game_info` must be valid.
|
| 485 |
pub unsafe fn show_pool_overflow_warning(&mut self) {
|
| 486 |
-
use crate::rebase::rb;
|
| 487 |
use crate::address::va;
|
|
|
|
| 488 |
|
| 489 |
let game_version = (*self.game_info).game_version;
|
| 490 |
if game_version < 0x3C {
|
|
@@ -621,36 +621,91 @@ pub mod offsets {
|
|
| 621 |
|
| 622 |
#[cfg(target_arch = "x86")]
|
| 623 |
impl crate::snapshot::Snapshot for DDGame {
|
| 624 |
-
unsafe fn write_snapshot(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 625 |
use crate::fixed::Fixed;
|
| 626 |
-
use crate::snapshot::{
|
| 627 |
let i = indent;
|
| 628 |
|
| 629 |
-
write_indent(w, i)?;
|
| 630 |
-
|
| 631 |
-
write_indent(w, i)?;
|
| 632 |
-
|
| 633 |
-
write_indent(w, i)?;
|
| 634 |
-
|
| 635 |
-
write_indent(w, i)?;
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
write_indent(w, i)?;
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 646 |
|
| 647 |
// TeamArenaState summary
|
| 648 |
-
write_indent(w, i)?;
|
| 649 |
-
|
| 650 |
-
write_indent(w, i)?;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 651 |
|
| 652 |
// Dump weapon slots (ammo only, skip zeros)
|
| 653 |
-
write_indent(w, i)?;
|
|
|
|
| 654 |
for team in 0..6usize {
|
| 655 |
let slots = &self.team_arena.weapon_slots.teams[team];
|
| 656 |
let mut has_any = false;
|
|
@@ -658,7 +713,8 @@ impl crate::snapshot::Snapshot for DDGame {
|
|
| 658 |
let ammo = slots.ammo[wpn];
|
| 659 |
if ammo != 0 {
|
| 660 |
if !has_any {
|
| 661 |
-
write_indent(w, i + 1)?;
|
|
|
|
| 662 |
has_any = true;
|
| 663 |
}
|
| 664 |
if ammo == -1 {
|
|
@@ -668,10 +724,11 @@ impl crate::snapshot::Snapshot for DDGame {
|
|
| 668 |
}
|
| 669 |
}
|
| 670 |
}
|
| 671 |
-
if has_any {
|
|
|
|
|
|
|
| 672 |
}
|
| 673 |
|
| 674 |
Ok(())
|
| 675 |
}
|
| 676 |
}
|
| 677 |
-
|
|
|
|
|
|
|
| 1 |
use crate::audio::active_sound::ActiveSoundTable;
|
|
|
|
| 2 |
use crate::audio::dssound::DSSound;
|
| 3 |
use crate::audio::music::Music;
|
| 4 |
use crate::audio::speech::SpeechSlotTable;
|
| 5 |
use crate::display::dd_display::DDDisplay;
|
| 6 |
use crate::display::palette::Palette;
|
| 7 |
use crate::engine::game_info::GameInfo;
|
| 8 |
+
use crate::fixed::Fixed;
|
| 9 |
use crate::game::weapon::WeaponTable;
|
| 10 |
use crate::input::keyboard::DDKeyboard;
|
| 11 |
use crate::render::landscape::PCLandscape;
|
| 12 |
use crate::render::queue::RenderQueue;
|
| 13 |
use crate::render::turn_order::TurnOrderWidget;
|
| 14 |
+
use crate::FieldRegistry;
|
| 15 |
|
| 16 |
// Re-export constructor items so existing `engine::ddgame::*` imports keep working.
|
| 17 |
pub use super::ddgame_constructor::{
|
|
|
|
| 24 |
|
| 25 |
// Re-export all team_arena types so existing `engine::ddgame::*` imports keep working.
|
| 26 |
pub use crate::engine::team_arena::{
|
| 27 |
+
worm, CoordEntry, CoordList, CoordListEntry, RenderEntry, SoundQueueEntry, TeamArenaRef,
|
| 28 |
+
TeamArenaState, TeamBlock, TeamHeader, TeamIndexMap, TeamSlot0, TeamWeaponSlots, WeaponSlots,
|
| 29 |
+
WormEntry, GAME_PHASE_NORMAL_MIN, GAME_PHASE_SUDDEN_DEATH,
|
| 30 |
};
|
| 31 |
|
| 32 |
/// DDGame β the main game engine object.
|
|
|
|
| 483 |
/// # Safety
|
| 484 |
/// `self.game_info` must be valid.
|
| 485 |
pub unsafe fn show_pool_overflow_warning(&mut self) {
|
|
|
|
| 486 |
use crate::address::va;
|
| 487 |
+
use crate::rebase::rb;
|
| 488 |
|
| 489 |
let game_version = (*self.game_info).game_version;
|
| 490 |
if game_version < 0x3C {
|
|
|
|
| 621 |
|
| 622 |
#[cfg(target_arch = "x86")]
|
| 623 |
impl crate::snapshot::Snapshot for DDGame {
|
| 624 |
+
unsafe fn write_snapshot(
|
| 625 |
+
&self,
|
| 626 |
+
w: &mut dyn core::fmt::Write,
|
| 627 |
+
indent: usize,
|
| 628 |
+
) -> core::fmt::Result {
|
| 629 |
use crate::fixed::Fixed;
|
| 630 |
+
use crate::snapshot::{fmt_ptr, write_indent};
|
| 631 |
let i = indent;
|
| 632 |
|
| 633 |
+
write_indent(w, i)?;
|
| 634 |
+
writeln!(w, "frame_counter = {}", self.frame_counter)?;
|
| 635 |
+
write_indent(w, i)?;
|
| 636 |
+
writeln!(w, "game_speed = {}", Fixed(self.game_speed))?;
|
| 637 |
+
write_indent(w, i)?;
|
| 638 |
+
writeln!(w, "game_speed_target = {}", Fixed(self.game_speed_target))?;
|
| 639 |
+
write_indent(w, i)?;
|
| 640 |
+
writeln!(
|
| 641 |
+
w,
|
| 642 |
+
"rng_state = 0x{:08X} 0x{:08X}",
|
| 643 |
+
self.rng_state_1, self.rng_state_2
|
| 644 |
+
)?;
|
| 645 |
+
write_indent(w, i)?;
|
| 646 |
+
writeln!(
|
| 647 |
+
w,
|
| 648 |
+
"camera = ({}, {})",
|
| 649 |
+
Fixed(self.camera_x),
|
| 650 |
+
Fixed(self.camera_y)
|
| 651 |
+
)?;
|
| 652 |
+
write_indent(w, i)?;
|
| 653 |
+
writeln!(
|
| 654 |
+
w,
|
| 655 |
+
"camera_target = ({}, {})",
|
| 656 |
+
Fixed(self.camera_target_x),
|
| 657 |
+
Fixed(self.camera_target_y)
|
| 658 |
+
)?;
|
| 659 |
+
write_indent(w, i)?;
|
| 660 |
+
writeln!(w, "level_size = {}x{}", self.level_width, self.level_height)?;
|
| 661 |
+
write_indent(w, i)?;
|
| 662 |
+
writeln!(
|
| 663 |
+
w,
|
| 664 |
+
"level_size_raw = {}x{}",
|
| 665 |
+
self.level_width_raw, self.level_height_raw
|
| 666 |
+
)?;
|
| 667 |
+
write_indent(w, i)?;
|
| 668 |
+
writeln!(
|
| 669 |
+
w,
|
| 670 |
+
"landscape_property = {}",
|
| 671 |
+
fmt_ptr(self.landscape_property as *const u8)
|
| 672 |
+
)?;
|
| 673 |
+
write_indent(w, i)?;
|
| 674 |
+
writeln!(w, "gfx_color_table = {:?}", self.gfx_color_table)?;
|
| 675 |
+
write_indent(w, i)?;
|
| 676 |
+
writeln!(
|
| 677 |
+
w,
|
| 678 |
+
"fast_forward = req={} active={}",
|
| 679 |
+
self.fast_forward_request, self.fast_forward_active
|
| 680 |
+
)?;
|
| 681 |
+
write_indent(w, i)?;
|
| 682 |
+
writeln!(w, "keyboard = {}", fmt_ptr(self.keyboard as *const u8))?;
|
| 683 |
+
write_indent(w, i)?;
|
| 684 |
+
writeln!(w, "display = {}", fmt_ptr(self.display as *const u8))?;
|
| 685 |
+
write_indent(w, i)?;
|
| 686 |
+
writeln!(w, "sound = {}", fmt_ptr(self.sound as *const u8))?;
|
| 687 |
+
write_indent(w, i)?;
|
| 688 |
+
writeln!(w, "game_info = {}", fmt_ptr(self.game_info as *const u8))?;
|
| 689 |
+
write_indent(w, i)?;
|
| 690 |
+
writeln!(w, "landscape = {}", fmt_ptr(self.landscape as *const u8))?;
|
| 691 |
+
write_indent(w, i)?;
|
| 692 |
+
writeln!(w, "task_land = {}", fmt_ptr(self.task_land))?;
|
| 693 |
|
| 694 |
// TeamArenaState summary
|
| 695 |
+
write_indent(w, i)?;
|
| 696 |
+
writeln!(w, "team_arena.team_count = {}", self.team_arena.team_count)?;
|
| 697 |
+
write_indent(w, i)?;
|
| 698 |
+
writeln!(w, "team_arena.game_phase = {}", self.team_arena.game_phase)?;
|
| 699 |
+
write_indent(w, i)?;
|
| 700 |
+
writeln!(
|
| 701 |
+
w,
|
| 702 |
+
"team_arena.game_mode_flag = {}",
|
| 703 |
+
self.team_arena.game_mode_flag
|
| 704 |
+
)?;
|
| 705 |
|
| 706 |
// Dump weapon slots (ammo only, skip zeros)
|
| 707 |
+
write_indent(w, i)?;
|
| 708 |
+
writeln!(w, "weapon_slots:")?;
|
| 709 |
for team in 0..6usize {
|
| 710 |
let slots = &self.team_arena.weapon_slots.teams[team];
|
| 711 |
let mut has_any = false;
|
|
|
|
| 713 |
let ammo = slots.ammo[wpn];
|
| 714 |
if ammo != 0 {
|
| 715 |
if !has_any {
|
| 716 |
+
write_indent(w, i + 1)?;
|
| 717 |
+
write!(w, "team[{}] ammo:", team)?;
|
| 718 |
has_any = true;
|
| 719 |
}
|
| 720 |
if ammo == -1 {
|
|
|
|
| 724 |
}
|
| 725 |
}
|
| 726 |
}
|
| 727 |
+
if has_any {
|
| 728 |
+
writeln!(w)?;
|
| 729 |
+
}
|
| 730 |
}
|
| 731 |
|
| 732 |
Ok(())
|
| 733 |
}
|
| 734 |
}
|
|
|
crates/openwa-core/src/engine/ddgame_constructor.rs
CHANGED
|
@@ -52,8 +52,16 @@ pub unsafe fn ddgame_init_fields(ddgame: *mut DDGame) {
|
|
| 52 |
// These offsets are deep in the unknown 0x2E00-0x45EB region and don't
|
| 53 |
// have named fields yet β keep as raw offsets for now.
|
| 54 |
for &off in &[
|
| 55 |
-
0x379Cusize,
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
] {
|
| 58 |
*(base.add(off) as *mut u32) = 0;
|
| 59 |
}
|
|
@@ -482,13 +490,7 @@ unsafe fn init_graphics_and_resources(
|
|
| 482 |
let disp = (*wrapper).display;
|
| 483 |
let gfx_dir = (*wrapper).primary_gfx_dir;
|
| 484 |
(*disp).set_layer_color(1, 0xFE);
|
| 485 |
-
(*disp).load_sprite(
|
| 486 |
-
1,
|
| 487 |
-
1,
|
| 488 |
-
0,
|
| 489 |
-
gfx_dir,
|
| 490 |
-
rb(va::STR_CDROM_SPR) as *const c_char,
|
| 491 |
-
);
|
| 492 |
(*disp).set_layer_visibility(1, -100);
|
| 493 |
|
| 494 |
// Palette slot range init (raw byte offsets into DisplayBase)
|
|
@@ -618,9 +620,15 @@ unsafe fn init_graphics_and_resources(
|
|
| 618 |
let palette_ctx = wa_malloc_zeroed(0x900);
|
| 619 |
*(palette_ctx as *mut u16) = 1;
|
| 620 |
*(palette_ctx.add(2) as *mut u16) = 0xFE;
|
| 621 |
-
call_usercall_eax(
|
| 622 |
-
|
| 623 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 624 |
}
|
| 625 |
|
| 626 |
// ββ Dump GfxResource for A/B comparison ββ
|
|
@@ -771,13 +779,13 @@ unsafe fn init_graphics_and_resources(
|
|
| 771 |
let region = if !alloc.is_null() {
|
| 772 |
call_sprite_region_ctor(
|
| 773 |
alloc,
|
| 774 |
-
(sprite_w / 2) as u32,
|
| 775 |
-
(sprite_h - margin_h) as u32,
|
| 776 |
-
margin_w as u32,
|
| 777 |
-
margin_h as u32,
|
| 778 |
-
(sprite_w - margin_w) as u32,
|
| 779 |
-
(sprite_h / 2) as u32,
|
| 780 |
-
sprite as u32,
|
| 781 |
)
|
| 782 |
} else {
|
| 783 |
core::ptr::null_mut()
|
|
@@ -917,12 +925,7 @@ unsafe fn init_graphics_and_resources(
|
|
| 917 |
// GenerateDebrisParticles (0x546F70) for particle effects, which affects
|
| 918 |
// the game RNG (DDGame+0x45EC). The original constructor loads them even
|
| 919 |
// in headless mode. Skipping them causes replay desync.
|
| 920 |
-
(*disp).load_sprite_by_layer(
|
| 921 |
-
3,
|
| 922 |
-
0x26D,
|
| 923 |
-
land_layer,
|
| 924 |
-
c"back.spr".as_ptr().cast(),
|
| 925 |
-
);
|
| 926 |
// debris.spr must be loaded unconditionally β it's used by
|
| 927 |
// GenerateDebrisParticles (0x546F70) for particle effects, which
|
| 928 |
// affects the game RNG (DDGame+0x45EC). Skipping it in headless
|
|
|
|
| 52 |
// These offsets are deep in the unknown 0x2E00-0x45EB region and don't
|
| 53 |
// have named fields yet β keep as raw offsets for now.
|
| 54 |
for &off in &[
|
| 55 |
+
0x379Cusize,
|
| 56 |
+
0x3930,
|
| 57 |
+
0x3AC4,
|
| 58 |
+
0x3C58,
|
| 59 |
+
0x3DEC,
|
| 60 |
+
0x3F80,
|
| 61 |
+
0x4114,
|
| 62 |
+
0x42A8,
|
| 63 |
+
0x443C,
|
| 64 |
+
0x45D0,
|
| 65 |
] {
|
| 66 |
*(base.add(off) as *mut u32) = 0;
|
| 67 |
}
|
|
|
|
| 490 |
let disp = (*wrapper).display;
|
| 491 |
let gfx_dir = (*wrapper).primary_gfx_dir;
|
| 492 |
(*disp).set_layer_color(1, 0xFE);
|
| 493 |
+
(*disp).load_sprite(1, 1, 0, gfx_dir, rb(va::STR_CDROM_SPR) as *const c_char);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 494 |
(*disp).set_layer_visibility(1, -100);
|
| 495 |
|
| 496 |
// Palette slot range init (raw byte offsets into DisplayBase)
|
|
|
|
| 620 |
let palette_ctx = wa_malloc_zeroed(0x900);
|
| 621 |
*(palette_ctx as *mut u16) = 1;
|
| 622 |
*(palette_ctx.add(2) as *mut u16) = 0xFE;
|
| 623 |
+
call_usercall_eax(
|
| 624 |
+
palette_ctx as *mut DDGameWrapper,
|
| 625 |
+
rb(va::PALETTE_CONTEXT_INIT),
|
| 626 |
+
);
|
| 627 |
+
gfx_resource = gfx_resource_create(
|
| 628 |
+
gfx_dir,
|
| 629 |
+
rb(va::STR_MASKS_IMG) as *const core::ffi::c_char,
|
| 630 |
+
palette_ctx,
|
| 631 |
+
);
|
| 632 |
}
|
| 633 |
|
| 634 |
// ββ Dump GfxResource for A/B comparison ββ
|
|
|
|
| 779 |
let region = if !alloc.is_null() {
|
| 780 |
call_sprite_region_ctor(
|
| 781 |
alloc,
|
| 782 |
+
(sprite_w / 2) as u32, // ECX β this[5] = ECX - p2
|
| 783 |
+
(sprite_h - margin_h) as u32, // EDX β this[4] = EDX - p3
|
| 784 |
+
margin_w as u32, // p2 (left margin)
|
| 785 |
+
margin_h as u32, // p3 (top margin)
|
| 786 |
+
(sprite_w - margin_w) as u32, // p4 β this[3] = p4 - p2
|
| 787 |
+
(sprite_h / 2) as u32, // p5 β this[6] = p5 - p3
|
| 788 |
+
sprite as u32, // p6 = arrow sprite (NOT landscape gfx_resource)
|
| 789 |
)
|
| 790 |
} else {
|
| 791 |
core::ptr::null_mut()
|
|
|
|
| 925 |
// GenerateDebrisParticles (0x546F70) for particle effects, which affects
|
| 926 |
// the game RNG (DDGame+0x45EC). The original constructor loads them even
|
| 927 |
// in headless mode. Skipping them causes replay desync.
|
| 928 |
+
(*disp).load_sprite_by_layer(3, 0x26D, land_layer, c"back.spr".as_ptr().cast());
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 929 |
// debris.spr must be loaded unconditionally β it's used by
|
| 930 |
// GenerateDebrisParticles (0x546F70) for particle effects, which
|
| 931 |
// affects the game RNG (DDGame+0x45EC). Skipping it in headless
|
crates/openwa-core/src/engine/game_info.rs
CHANGED
|
@@ -94,7 +94,6 @@ pub struct GameInfo {
|
|
| 94 |
pub land_dat_path: [u8; 14],
|
| 95 |
|
| 96 |
// --- Replay configuration (populated by ReplayLoader) ---
|
| 97 |
-
|
| 98 |
/// 0xDAFA-0xDB07: Unknown
|
| 99 |
pub _unknown_dafa: [u8; 0xDB08 - 0xDAFA],
|
| 100 |
/// 0xDB08: Invisibility (weapon 0x42) mode flag (u32). Controls team-count
|
|
|
|
| 94 |
pub land_dat_path: [u8; 14],
|
| 95 |
|
| 96 |
// --- Replay configuration (populated by ReplayLoader) ---
|
|
|
|
| 97 |
/// 0xDAFA-0xDB07: Unknown
|
| 98 |
pub _unknown_dafa: [u8; 0xDB08 - 0xDAFA],
|
| 99 |
/// 0xDB08: Invisibility (weapon 0x42) mode flag (u32). Controls team-count
|
crates/openwa-core/src/engine/game_state_init.rs
CHANGED
|
@@ -376,10 +376,9 @@ pub unsafe fn check_weapon_avail(ddgame: *mut DDGame, weapon_index: u32) -> i32
|
|
| 376 |
}
|
| 377 |
}
|
| 378 |
w if w == Weapon::DoubleTurnTime as u32 => {
|
| 379 |
-
if game_version > 0xD1
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
}
|
| 383 |
}
|
| 384 |
_ => {}
|
| 385 |
}
|
|
@@ -401,8 +400,7 @@ pub unsafe fn check_weapon_avail(ddgame: *mut DDGame, weapon_index: u32) -> i32
|
|
| 401 |
}
|
| 402 |
|
| 403 |
// AquaSheep (25) or SuperSheep (24) depending on weapon_index_offset
|
| 404 |
-
let restricted_id = Weapon::AquaSheep as u32
|
| 405 |
-
- ((*gi).aquasheep_is_supersheep != 0) as u32;
|
| 406 |
if weapon_index != restricted_id {
|
| 407 |
return 1;
|
| 408 |
}
|
|
|
|
| 376 |
}
|
| 377 |
}
|
| 378 |
w if w == Weapon::DoubleTurnTime as u32 => {
|
| 379 |
+
if game_version > 0xD1 && (*gi).double_turn_time_threshold > 0x7FFF {
|
| 380 |
+
return 0;
|
| 381 |
+
}
|
|
|
|
| 382 |
}
|
| 383 |
_ => {}
|
| 384 |
}
|
|
|
|
| 400 |
}
|
| 401 |
|
| 402 |
// AquaSheep (25) or SuperSheep (24) depending on weapon_index_offset
|
| 403 |
+
let restricted_id = Weapon::AquaSheep as u32 - ((*gi).aquasheep_is_supersheep != 0) as u32;
|
|
|
|
| 404 |
if weapon_index != restricted_id {
|
| 405 |
return 1;
|
| 406 |
}
|
crates/openwa-core/src/engine/mod.rs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
pub mod ddgame;
|
| 2 |
pub mod ddgame_constructor;
|
| 3 |
-
pub mod replay;
|
| 4 |
pub mod ddgame_wrapper;
|
| 5 |
pub mod game_info;
|
| 6 |
pub mod game_session;
|
|
@@ -8,20 +7,21 @@ pub mod game_state_init;
|
|
| 8 |
pub mod game_timer;
|
| 9 |
pub mod net_bridge;
|
| 10 |
pub mod net_wrapper;
|
|
|
|
| 11 |
pub mod team_arena;
|
| 12 |
|
| 13 |
pub use ddgame::DDGame;
|
| 14 |
pub use ddgame_constructor::{
|
| 15 |
-
create_ddgame, ddgame_init_fields, ddgame_init_render_indices,
|
| 16 |
-
|
| 17 |
-
};
|
| 18 |
-
pub use team_arena::{
|
| 19 |
-
CoordEntry, CoordList, CoordListEntry, GAME_PHASE_NORMAL_MIN, GAME_PHASE_SUDDEN_DEATH,
|
| 20 |
-
RenderEntry, SoundQueueEntry, TeamArenaRef, TeamArenaState, TeamBlock, TeamHeader,
|
| 21 |
-
TeamIndexMap, TeamSlot0, TeamWeaponSlots, WeaponSlots, WormEntry,
|
| 22 |
};
|
| 23 |
pub use ddgame_wrapper::DDGameWrapper;
|
| 24 |
pub use game_info::GameInfo;
|
| 25 |
pub use game_session::GameSession;
|
| 26 |
pub use game_timer::GameTimer;
|
| 27 |
pub use net_wrapper::DDNetGameWrapper;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
pub mod ddgame;
|
| 2 |
pub mod ddgame_constructor;
|
|
|
|
| 3 |
pub mod ddgame_wrapper;
|
| 4 |
pub mod game_info;
|
| 5 |
pub mod game_session;
|
|
|
|
| 7 |
pub mod game_timer;
|
| 8 |
pub mod net_bridge;
|
| 9 |
pub mod net_wrapper;
|
| 10 |
+
pub mod replay;
|
| 11 |
pub mod team_arena;
|
| 12 |
|
| 13 |
pub use ddgame::DDGame;
|
| 14 |
pub use ddgame_constructor::{
|
| 15 |
+
create_ddgame, ddgame_init_fields, ddgame_init_render_indices, display_layer_color_init,
|
| 16 |
+
init_constructor_addrs, ON_DDGAME_ALLOC,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
};
|
| 18 |
pub use ddgame_wrapper::DDGameWrapper;
|
| 19 |
pub use game_info::GameInfo;
|
| 20 |
pub use game_session::GameSession;
|
| 21 |
pub use game_timer::GameTimer;
|
| 22 |
pub use net_wrapper::DDNetGameWrapper;
|
| 23 |
+
pub use team_arena::{
|
| 24 |
+
CoordEntry, CoordList, CoordListEntry, RenderEntry, SoundQueueEntry, TeamArenaRef,
|
| 25 |
+
TeamArenaState, TeamBlock, TeamHeader, TeamIndexMap, TeamSlot0, TeamWeaponSlots, WeaponSlots,
|
| 26 |
+
WormEntry, GAME_PHASE_NORMAL_MIN, GAME_PHASE_SUDDEN_DEATH,
|
| 27 |
+
};
|
crates/openwa-core/src/engine/replay.rs
CHANGED
|
@@ -7,13 +7,13 @@
|
|
| 7 |
#[repr(i32)]
|
| 8 |
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
| 9 |
pub enum ReplayError {
|
| 10 |
-
FileNotFound = -1,
|
| 11 |
-
InvalidFormat = -2,
|
| 12 |
-
VersionTooNew = -3,
|
| 13 |
-
MallocFailure = -4,
|
| 14 |
-
MapLoadFailure = -5,
|
| 15 |
-
ArtClassLimit = -6,
|
| 16 |
-
RepairWithTeams = -7,
|
| 17 |
// -8 is unused (gap in error codes)
|
| 18 |
NoScheme = -9, // 0xFFFFFFF7
|
| 19 |
SchemeSaveFailure = -10, // 0xFFFFFFF6
|
|
|
|
| 7 |
#[repr(i32)]
|
| 8 |
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
| 9 |
pub enum ReplayError {
|
| 10 |
+
FileNotFound = -1, // 0xFFFFFFFF
|
| 11 |
+
InvalidFormat = -2, // 0xFFFFFFFE
|
| 12 |
+
VersionTooNew = -3, // 0xFFFFFFFD
|
| 13 |
+
MallocFailure = -4, // 0xFFFFFFFC
|
| 14 |
+
MapLoadFailure = -5, // 0xFFFFFFFB
|
| 15 |
+
ArtClassLimit = -6, // 0xFFFFFFFA
|
| 16 |
+
RepairWithTeams = -7, // 0xFFFFFFF9
|
| 17 |
// -8 is unused (gap in error codes)
|
| 18 |
NoScheme = -9, // 0xFFFFFFF7
|
| 19 |
SchemeSaveFailure = -10, // 0xFFFFFFF6
|
crates/openwa-core/src/engine/team_arena.rs
CHANGED
|
@@ -531,14 +531,21 @@ impl TeamArenaRef {
|
|
| 531 |
// ββ Snapshot impl ββββββββββββββββββββββββββββββββββββββββββ
|
| 532 |
|
| 533 |
impl crate::snapshot::Snapshot for WormEntry {
|
| 534 |
-
unsafe fn write_snapshot(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 535 |
use crate::snapshot::write_indent;
|
| 536 |
let i = indent;
|
| 537 |
let name = core::ffi::CStr::from_ptr(self.name.as_ptr() as *const core::ffi::c_char)
|
| 538 |
.to_string_lossy();
|
| 539 |
write_indent(w, i)?;
|
| 540 |
-
writeln!(
|
| 541 |
-
|
|
|
|
|
|
|
|
|
|
| 542 |
Ok(())
|
| 543 |
}
|
| 544 |
}
|
|
|
|
| 531 |
// ββ Snapshot impl ββββββββββββββββββββββββββββββββββββββββββ
|
| 532 |
|
| 533 |
impl crate::snapshot::Snapshot for WormEntry {
|
| 534 |
+
unsafe fn write_snapshot(
|
| 535 |
+
&self,
|
| 536 |
+
w: &mut dyn core::fmt::Write,
|
| 537 |
+
indent: usize,
|
| 538 |
+
) -> core::fmt::Result {
|
| 539 |
use crate::snapshot::write_indent;
|
| 540 |
let i = indent;
|
| 541 |
let name = core::ffi::CStr::from_ptr(self.name.as_ptr() as *const core::ffi::c_char)
|
| 542 |
.to_string_lossy();
|
| 543 |
write_indent(w, i)?;
|
| 544 |
+
writeln!(
|
| 545 |
+
w,
|
| 546 |
+
"state=0x{:02X} active={} hp={}/{} name=\"{}\"",
|
| 547 |
+
self.state, self.active_flag, self.health, self.max_health, name
|
| 548 |
+
)?;
|
| 549 |
Ok(())
|
| 550 |
}
|
| 551 |
}
|
crates/openwa-core/src/fixed.rs
CHANGED
|
@@ -48,12 +48,20 @@ impl Fixed {
|
|
| 48 |
|
| 49 |
#[inline]
|
| 50 |
pub fn max(self, other: Self) -> Self {
|
| 51 |
-
if self > other {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
}
|
| 53 |
|
| 54 |
#[inline]
|
| 55 |
pub fn min(self, other: Self) -> Self {
|
| 56 |
-
if self < other {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
}
|
| 58 |
|
| 59 |
#[inline]
|
|
|
|
| 48 |
|
| 49 |
#[inline]
|
| 50 |
pub fn max(self, other: Self) -> Self {
|
| 51 |
+
if self > other {
|
| 52 |
+
self
|
| 53 |
+
} else {
|
| 54 |
+
other
|
| 55 |
+
}
|
| 56 |
}
|
| 57 |
|
| 58 |
#[inline]
|
| 59 |
pub fn min(self, other: Self) -> Self {
|
| 60 |
+
if self < other {
|
| 61 |
+
self
|
| 62 |
+
} else {
|
| 63 |
+
other
|
| 64 |
+
}
|
| 65 |
}
|
| 66 |
|
| 67 |
#[inline]
|
crates/openwa-core/src/game/scheme.rs
CHANGED
|
@@ -184,19 +184,18 @@ impl SchemeFile {
|
|
| 184 |
/// For V3 schemes, trailing extended options bytes that match the defaults
|
| 185 |
/// are trimmed, matching WA's minimal file format.
|
| 186 |
pub fn to_bytes(&self) -> Vec<u8> {
|
| 187 |
-
let payload =
|
| 188 |
-
&& self.payload.len() == SCHEME_PAYLOAD_V3
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
};
|
| 200 |
let mut buf = Vec::with_capacity(SCHEME_HEADER_SIZE + payload.len());
|
| 201 |
buf.extend_from_slice(&SCHEME_MAGIC);
|
| 202 |
buf.push(self.version.to_byte());
|
|
|
|
| 184 |
/// For V3 schemes, trailing extended options bytes that match the defaults
|
| 185 |
/// are trimmed, matching WA's minimal file format.
|
| 186 |
pub fn to_bytes(&self) -> Vec<u8> {
|
| 187 |
+
let payload =
|
| 188 |
+
if self.version == SchemeVersion::V3 && self.payload.len() == SCHEME_PAYLOAD_V3 {
|
| 189 |
+
// Trim trailing extended options bytes that match defaults
|
| 190 |
+
let ext = &self.payload[EXTENDED_OPTIONS_OFFSET..];
|
| 191 |
+
let mut trim_len = EXTENDED_OPTIONS_SIZE;
|
| 192 |
+
while trim_len > 0 && ext[trim_len - 1] == EXTENDED_OPTIONS_DEFAULTS[trim_len - 1] {
|
| 193 |
+
trim_len -= 1;
|
| 194 |
+
}
|
| 195 |
+
&self.payload[..EXTENDED_OPTIONS_OFFSET + trim_len]
|
| 196 |
+
} else {
|
| 197 |
+
&self.payload
|
| 198 |
+
};
|
|
|
|
| 199 |
let mut buf = Vec::with_capacity(SCHEME_HEADER_SIZE + payload.len());
|
| 200 |
buf.extend_from_slice(&SCHEME_MAGIC);
|
| 201 |
buf.push(self.version.to_byte());
|
crates/openwa-core/src/lib.rs
CHANGED
|
@@ -22,8 +22,8 @@ extern crate self as openwa_core;
|
|
| 22 |
pub use inventory;
|
| 23 |
|
| 24 |
// Re-export derive macros so users write `use openwa_core::FieldRegistry;`
|
| 25 |
-
pub use openwa_derive::FieldRegistry;
|
| 26 |
pub use openwa_derive::vtable;
|
|
|
|
| 27 |
|
| 28 |
#[macro_use]
|
| 29 |
pub mod macros;
|
|
@@ -37,13 +37,13 @@ pub mod input;
|
|
| 37 |
pub mod render;
|
| 38 |
|
| 39 |
pub mod address;
|
|
|
|
| 40 |
pub mod fixed;
|
| 41 |
-
pub mod registry;
|
| 42 |
pub mod log;
|
| 43 |
pub mod mem;
|
| 44 |
pub mod rebase;
|
|
|
|
| 45 |
pub mod snapshot;
|
| 46 |
-
pub mod field_format;
|
| 47 |
pub mod task;
|
| 48 |
pub mod vtable;
|
| 49 |
pub mod wa;
|
|
|
|
| 22 |
pub use inventory;
|
| 23 |
|
| 24 |
// Re-export derive macros so users write `use openwa_core::FieldRegistry;`
|
|
|
|
| 25 |
pub use openwa_derive::vtable;
|
| 26 |
+
pub use openwa_derive::FieldRegistry;
|
| 27 |
|
| 28 |
#[macro_use]
|
| 29 |
pub mod macros;
|
|
|
|
| 37 |
pub mod render;
|
| 38 |
|
| 39 |
pub mod address;
|
| 40 |
+
pub mod field_format;
|
| 41 |
pub mod fixed;
|
|
|
|
| 42 |
pub mod log;
|
| 43 |
pub mod mem;
|
| 44 |
pub mod rebase;
|
| 45 |
+
pub mod registry;
|
| 46 |
pub mod snapshot;
|
|
|
|
| 47 |
pub mod task;
|
| 48 |
pub mod vtable;
|
| 49 |
pub mod wa;
|
crates/openwa-core/src/mem.rs
CHANGED
|
@@ -41,7 +41,11 @@ pub unsafe fn classify_pointer(value: u32, delta: u32) -> Option<PointerInfo> {
|
|
| 41 |
let name = registry::format_va(ghidra_val);
|
| 42 |
let detail = if can_read(value, 4) {
|
| 43 |
let vt0 = *(value as *const u32);
|
| 44 |
-
Some(format!(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
} else {
|
| 46 |
Some(format!("{} (unreadable)", name))
|
| 47 |
};
|
|
@@ -164,9 +168,7 @@ pub unsafe fn identify_pointer(value: u32, delta: u32) -> Option<PointerIdentity
|
|
| 164 |
registry::AddrKind::Vtable | registry::AddrKind::VtableMethod => {
|
| 165 |
PointerKind::Vtable
|
| 166 |
}
|
| 167 |
-
registry::AddrKind::Function | registry::AddrKind::Constructor =>
|
| 168 |
-
PointerKind::Code
|
| 169 |
-
}
|
| 170 |
_ => PointerKind::Data,
|
| 171 |
};
|
| 172 |
let name = if resolved.offset == 0 {
|
|
@@ -241,11 +243,7 @@ pub unsafe fn identify_pointer(value: u32, delta: u32) -> Option<PointerIdentity
|
|
| 241 |
///
|
| 242 |
/// Returns a vec of `PointerInfo` with offsets filled in relative to `base_offset`.
|
| 243 |
#[cfg(target_os = "windows")]
|
| 244 |
-
pub unsafe fn classify_region(
|
| 245 |
-
data: &[u8],
|
| 246 |
-
base_offset: u32,
|
| 247 |
-
delta: u32,
|
| 248 |
-
) -> Vec<PointerInfo> {
|
| 249 |
let mut pointers = Vec::new();
|
| 250 |
let dword_count = data.len() / 4;
|
| 251 |
for i in 0..dword_count {
|
|
|
|
| 41 |
let name = registry::format_va(ghidra_val);
|
| 42 |
let detail = if can_read(value, 4) {
|
| 43 |
let vt0 = *(value as *const u32);
|
| 44 |
+
Some(format!(
|
| 45 |
+
"{} vt[0]=ghidra:0x{:08X}",
|
| 46 |
+
name,
|
| 47 |
+
vt0.wrapping_sub(delta)
|
| 48 |
+
))
|
| 49 |
} else {
|
| 50 |
Some(format!("{} (unreadable)", name))
|
| 51 |
};
|
|
|
|
| 168 |
registry::AddrKind::Vtable | registry::AddrKind::VtableMethod => {
|
| 169 |
PointerKind::Vtable
|
| 170 |
}
|
| 171 |
+
registry::AddrKind::Function | registry::AddrKind::Constructor => PointerKind::Code,
|
|
|
|
|
|
|
| 172 |
_ => PointerKind::Data,
|
| 173 |
};
|
| 174 |
let name = if resolved.offset == 0 {
|
|
|
|
| 243 |
///
|
| 244 |
/// Returns a vec of `PointerInfo` with offsets filled in relative to `base_offset`.
|
| 245 |
#[cfg(target_os = "windows")]
|
| 246 |
+
pub unsafe fn classify_region(data: &[u8], base_offset: u32, delta: u32) -> Vec<PointerInfo> {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
let mut pointers = Vec::new();
|
| 248 |
let dword_count = data.len() / 4;
|
| 249 |
for i in 0..dword_count {
|
crates/openwa-core/src/registry.rs
CHANGED
|
@@ -153,10 +153,7 @@ pub fn format_va(ghidra_va: u32) -> String {
|
|
| 153 |
format!("{} (0x{:X})", r.entry.name, ghidra_va)
|
| 154 |
}
|
| 155 |
Some(r) => {
|
| 156 |
-
format!(
|
| 157 |
-
"{}+0x{:X} (0x{:X})",
|
| 158 |
-
r.entry.name, r.offset, ghidra_va
|
| 159 |
-
)
|
| 160 |
}
|
| 161 |
None => format!("0x{:08X}", ghidra_va),
|
| 162 |
}
|
|
@@ -164,7 +161,10 @@ pub fn format_va(ghidra_va: u32) -> String {
|
|
| 164 |
|
| 165 |
/// Iterate all entries of a given kind.
|
| 166 |
pub fn entries_by_kind(kind: AddrKind) -> impl Iterator<Item = &'static AddrEntry> {
|
| 167 |
-
sorted_entries()
|
|
|
|
|
|
|
|
|
|
| 168 |
}
|
| 169 |
|
| 170 |
/// Iterate all registered entries (sorted by VA).
|
|
@@ -588,9 +588,27 @@ mod tests {
|
|
| 588 |
static FIELDS: StructFields = StructFields {
|
| 589 |
struct_name: "TestStruct",
|
| 590 |
fields: &[
|
| 591 |
-
FieldEntry {
|
| 592 |
-
|
| 593 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 594 |
],
|
| 595 |
};
|
| 596 |
|
|
@@ -674,7 +692,11 @@ mod tests {
|
|
| 674 |
let reg = CTask::field_registry();
|
| 675 |
let vtable = reg.field_at(0x00).unwrap();
|
| 676 |
// Doc comment should be non-empty (we have "0x00: Pointer to virtual method table")
|
| 677 |
-
assert!(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 678 |
}
|
| 679 |
|
| 680 |
#[test]
|
|
@@ -688,7 +710,10 @@ mod tests {
|
|
| 688 |
assert!(ctask.is_some(), "CTask not found in struct registry");
|
| 689 |
|
| 690 |
let session = struct_fields_for("GameSession");
|
| 691 |
-
assert!(
|
|
|
|
|
|
|
|
|
|
| 692 |
|
| 693 |
// Unknown struct returns None
|
| 694 |
assert!(struct_fields_for("FooBarBaz").is_none());
|
|
@@ -700,7 +725,10 @@ mod tests {
|
|
| 700 |
|
| 701 |
// DDGameWrapper vtable β "DDGameWrapper" β DDGameWrapper fields
|
| 702 |
let fields = struct_fields_for_vtable(va::DDGAME_WRAPPER_VTABLE);
|
| 703 |
-
assert!(
|
|
|
|
|
|
|
|
|
|
| 704 |
assert_eq!(fields.unwrap().struct_name, "DDGameWrapper");
|
| 705 |
|
| 706 |
// CTask vtable β "CTask" β CTask fields
|
|
|
|
| 153 |
format!("{} (0x{:X})", r.entry.name, ghidra_va)
|
| 154 |
}
|
| 155 |
Some(r) => {
|
| 156 |
+
format!("{}+0x{:X} (0x{:X})", r.entry.name, r.offset, ghidra_va)
|
|
|
|
|
|
|
|
|
|
| 157 |
}
|
| 158 |
None => format!("0x{:08X}", ghidra_va),
|
| 159 |
}
|
|
|
|
| 161 |
|
| 162 |
/// Iterate all entries of a given kind.
|
| 163 |
pub fn entries_by_kind(kind: AddrKind) -> impl Iterator<Item = &'static AddrEntry> {
|
| 164 |
+
sorted_entries()
|
| 165 |
+
.iter()
|
| 166 |
+
.copied()
|
| 167 |
+
.filter(move |e| e.kind == kind)
|
| 168 |
}
|
| 169 |
|
| 170 |
/// Iterate all registered entries (sorted by VA).
|
|
|
|
| 588 |
static FIELDS: StructFields = StructFields {
|
| 589 |
struct_name: "TestStruct",
|
| 590 |
fields: &[
|
| 591 |
+
FieldEntry {
|
| 592 |
+
offset: 0x00,
|
| 593 |
+
name: "vtable",
|
| 594 |
+
size: 4,
|
| 595 |
+
kind: ValueKind::Pointer,
|
| 596 |
+
doc: "",
|
| 597 |
+
},
|
| 598 |
+
FieldEntry {
|
| 599 |
+
offset: 0x10,
|
| 600 |
+
name: "health",
|
| 601 |
+
size: 4,
|
| 602 |
+
kind: ValueKind::U32,
|
| 603 |
+
doc: "",
|
| 604 |
+
},
|
| 605 |
+
FieldEntry {
|
| 606 |
+
offset: 0x20,
|
| 607 |
+
name: "name",
|
| 608 |
+
size: 16,
|
| 609 |
+
kind: ValueKind::Raw,
|
| 610 |
+
doc: "",
|
| 611 |
+
},
|
| 612 |
],
|
| 613 |
};
|
| 614 |
|
|
|
|
| 692 |
let reg = CTask::field_registry();
|
| 693 |
let vtable = reg.field_at(0x00).unwrap();
|
| 694 |
// Doc comment should be non-empty (we have "0x00: Pointer to virtual method table")
|
| 695 |
+
assert!(
|
| 696 |
+
!vtable.doc.is_empty(),
|
| 697 |
+
"doc should be extracted: {:?}",
|
| 698 |
+
vtable.doc
|
| 699 |
+
);
|
| 700 |
}
|
| 701 |
|
| 702 |
#[test]
|
|
|
|
| 710 |
assert!(ctask.is_some(), "CTask not found in struct registry");
|
| 711 |
|
| 712 |
let session = struct_fields_for("GameSession");
|
| 713 |
+
assert!(
|
| 714 |
+
session.is_some(),
|
| 715 |
+
"GameSession not found in struct registry"
|
| 716 |
+
);
|
| 717 |
|
| 718 |
// Unknown struct returns None
|
| 719 |
assert!(struct_fields_for("FooBarBaz").is_none());
|
|
|
|
| 725 |
|
| 726 |
// DDGameWrapper vtable β "DDGameWrapper" β DDGameWrapper fields
|
| 727 |
let fields = struct_fields_for_vtable(va::DDGAME_WRAPPER_VTABLE);
|
| 728 |
+
assert!(
|
| 729 |
+
fields.is_some(),
|
| 730 |
+
"DDGameWrapper fields not found via vtable"
|
| 731 |
+
);
|
| 732 |
assert_eq!(fields.unwrap().struct_name, "DDGameWrapper");
|
| 733 |
|
| 734 |
// CTask vtable β "CTask" β CTask fields
|
crates/openwa-core/src/snapshot.rs
CHANGED
|
@@ -6,9 +6,9 @@
|
|
| 6 |
|
| 7 |
use core::fmt;
|
| 8 |
|
|
|
|
| 9 |
use crate::mem;
|
| 10 |
use crate::rebase::rb;
|
| 11 |
-
use crate::address::va;
|
| 12 |
|
| 13 |
/// Trait for types that can write a canonicalized snapshot.
|
| 14 |
///
|
|
@@ -35,12 +35,20 @@ pub fn write_indent(w: &mut dyn fmt::Write, indent: usize) -> fmt::Result {
|
|
| 35 |
|
| 36 |
/// Format a pointer field: `<ptr>` if non-null, `null` if null.
|
| 37 |
pub fn fmt_ptr(ptr: *const u8) -> &'static str {
|
| 38 |
-
if ptr.is_null() {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
}
|
| 40 |
|
| 41 |
/// Format a pointer field for a raw u32 that might be a pointer.
|
| 42 |
pub fn fmt_ptr32(val: u32) -> &'static str {
|
| 43 |
-
if val == 0 {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
/// Dump a raw memory region as canonicalized hex lines.
|
|
@@ -121,12 +129,22 @@ pub unsafe fn hash_region_canonical(ptr: *const u8, len: usize) -> u32 {
|
|
| 121 |
for i in (0..len).step_by(4) {
|
| 122 |
let val = if i + 4 <= len {
|
| 123 |
*(ptr.add(i) as *const u32)
|
| 124 |
-
} else {
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
for byte in hash_val.to_le_bytes() {
|
| 127 |
h ^= byte as u32;
|
| 128 |
for _ in 0..8 {
|
| 129 |
-
h = if h & 1 != 0 {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
}
|
| 131 |
}
|
| 132 |
}
|
|
@@ -155,13 +173,21 @@ pub unsafe fn hash_pointer_targets(
|
|
| 155 |
|
| 156 |
for off in (0..struct_size).step_by(4) {
|
| 157 |
let val = *(base.add(off) as *const u32);
|
| 158 |
-
if val == 0 {
|
| 159 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
// Skip WA code/data pointers β only follow heap pointers
|
| 161 |
let ghidra = val.wrapping_sub(delta);
|
| 162 |
-
if (va::TEXT_START..va::DATA_END).contains(&ghidra) {
|
|
|
|
|
|
|
| 163 |
// Verify target is readable
|
| 164 |
-
if !mem::can_read(val, target_window as u32) {
|
|
|
|
|
|
|
| 165 |
|
| 166 |
let h = hash_region_canonical(val as *const u8, target_window);
|
| 167 |
writeln!(w, "[HASH] {}+0x{:04X} hash={:08X}", name, off, h)?;
|
|
|
|
| 6 |
|
| 7 |
use core::fmt;
|
| 8 |
|
| 9 |
+
use crate::address::va;
|
| 10 |
use crate::mem;
|
| 11 |
use crate::rebase::rb;
|
|
|
|
| 12 |
|
| 13 |
/// Trait for types that can write a canonicalized snapshot.
|
| 14 |
///
|
|
|
|
| 35 |
|
| 36 |
/// Format a pointer field: `<ptr>` if non-null, `null` if null.
|
| 37 |
pub fn fmt_ptr(ptr: *const u8) -> &'static str {
|
| 38 |
+
if ptr.is_null() {
|
| 39 |
+
"null"
|
| 40 |
+
} else {
|
| 41 |
+
"<ptr>"
|
| 42 |
+
}
|
| 43 |
}
|
| 44 |
|
| 45 |
/// Format a pointer field for a raw u32 that might be a pointer.
|
| 46 |
pub fn fmt_ptr32(val: u32) -> &'static str {
|
| 47 |
+
if val == 0 {
|
| 48 |
+
"null"
|
| 49 |
+
} else {
|
| 50 |
+
"<ptr>"
|
| 51 |
+
}
|
| 52 |
}
|
| 53 |
|
| 54 |
/// Dump a raw memory region as canonicalized hex lines.
|
|
|
|
| 129 |
for i in (0..len).step_by(4) {
|
| 130 |
let val = if i + 4 <= len {
|
| 131 |
*(ptr.add(i) as *const u32)
|
| 132 |
+
} else {
|
| 133 |
+
0
|
| 134 |
+
};
|
| 135 |
+
let hash_val = if is_likely_pointer(val, delta) {
|
| 136 |
+
0u32
|
| 137 |
+
} else {
|
| 138 |
+
val
|
| 139 |
+
};
|
| 140 |
for byte in hash_val.to_le_bytes() {
|
| 141 |
h ^= byte as u32;
|
| 142 |
for _ in 0..8 {
|
| 143 |
+
h = if h & 1 != 0 {
|
| 144 |
+
(h >> 1) ^ 0xEDB88320
|
| 145 |
+
} else {
|
| 146 |
+
h >> 1
|
| 147 |
+
};
|
| 148 |
}
|
| 149 |
}
|
| 150 |
}
|
|
|
|
| 173 |
|
| 174 |
for off in (0..struct_size).step_by(4) {
|
| 175 |
let val = *(base.add(off) as *const u32);
|
| 176 |
+
if val == 0 {
|
| 177 |
+
continue;
|
| 178 |
+
}
|
| 179 |
+
if !is_likely_pointer(val, delta) {
|
| 180 |
+
continue;
|
| 181 |
+
}
|
| 182 |
// Skip WA code/data pointers β only follow heap pointers
|
| 183 |
let ghidra = val.wrapping_sub(delta);
|
| 184 |
+
if (va::TEXT_START..va::DATA_END).contains(&ghidra) {
|
| 185 |
+
continue;
|
| 186 |
+
}
|
| 187 |
// Verify target is readable
|
| 188 |
+
if !mem::can_read(val, target_window as u32) {
|
| 189 |
+
continue;
|
| 190 |
+
}
|
| 191 |
|
| 192 |
let h = hash_region_canonical(val as *const u8, target_window);
|
| 193 |
writeln!(w, "[HASH] {}+0x{:04X} hash={:08X}", name, off, h)?;
|
crates/openwa-core/src/task/arrow.rs
CHANGED
|
@@ -17,7 +17,8 @@ pub struct CTaskArrowVTable {
|
|
| 17 |
/// HandleMessage β processes arrow messages.
|
| 18 |
/// thiscall + 4 stack params, RET 0x10.
|
| 19 |
#[slot(2)]
|
| 20 |
-
pub handle_message:
|
|
|
|
| 21 |
/// ProcessFrame β per-frame arrow update.
|
| 22 |
/// thiscall + 1 stack param (flags), RET 0x4.
|
| 23 |
#[slot(7)]
|
|
|
|
| 17 |
/// HandleMessage β processes arrow messages.
|
| 18 |
/// thiscall + 4 stack params, RET 0x10.
|
| 19 |
#[slot(2)]
|
| 20 |
+
pub handle_message:
|
| 21 |
+
fn(this: *mut CTaskArrow, sender: *mut CTask, msg_type: u32, size: u32, data: *const u8),
|
| 22 |
/// ProcessFrame β per-frame arrow update.
|
| 23 |
/// thiscall + 1 stack param (flags), RET 0x4.
|
| 24 |
#[slot(7)]
|
crates/openwa-core/src/task/base.rs
CHANGED
|
@@ -116,7 +116,8 @@ pub struct CTaskVtable {
|
|
| 116 |
pub free: fn(this: *mut CTask, flags: u8) -> *mut CTask,
|
| 117 |
/// HandleMessage β broadcasts message to all children (base implementation).
|
| 118 |
#[slot(2)]
|
| 119 |
-
pub handle_message:
|
|
|
|
| 120 |
/// ProcessChildren β iterates children with flags. Base at 0x562FA0.
|
| 121 |
#[slot(5)]
|
| 122 |
pub process_children: fn(this: *mut CTask, flags: u32),
|
|
@@ -246,15 +247,14 @@ pub unsafe trait Task {
|
|
| 246 |
loop {
|
| 247 |
// Scan for next non-null child
|
| 248 |
let child = loop {
|
| 249 |
-
let watermark =
|
| 250 |
-
core::ptr::addr_of!((*task_ptr).children_watermark)
|
| 251 |
-
|
| 252 |
if i >= watermark {
|
| 253 |
return;
|
| 254 |
}
|
| 255 |
-
let children =
|
| 256 |
-
core::ptr::addr_of!((*task_ptr).children_data)
|
| 257 |
-
);
|
| 258 |
let c = *children.add(i);
|
| 259 |
i += 1;
|
| 260 |
if !c.is_null() {
|
|
@@ -305,15 +305,14 @@ impl CTask {
|
|
| 305 |
let mut i: usize = 0;
|
| 306 |
loop {
|
| 307 |
let child = loop {
|
| 308 |
-
let watermark =
|
| 309 |
-
core::ptr::addr_of!((*task_ptr).children_watermark)
|
| 310 |
-
|
| 311 |
if i >= watermark {
|
| 312 |
return;
|
| 313 |
}
|
| 314 |
-
let children =
|
| 315 |
-
core::ptr::addr_of!((*task_ptr).children_data)
|
| 316 |
-
);
|
| 317 |
let c = *children.add(i);
|
| 318 |
i += 1;
|
| 319 |
if !c.is_null() {
|
|
|
|
| 116 |
pub free: fn(this: *mut CTask, flags: u8) -> *mut CTask,
|
| 117 |
/// HandleMessage β broadcasts message to all children (base implementation).
|
| 118 |
#[slot(2)]
|
| 119 |
+
pub handle_message:
|
| 120 |
+
fn(this: *mut CTask, sender: *mut CTask, msg_type: u32, size: u32, data: *const u8),
|
| 121 |
/// ProcessChildren β iterates children with flags. Base at 0x562FA0.
|
| 122 |
#[slot(5)]
|
| 123 |
pub process_children: fn(this: *mut CTask, flags: u32),
|
|
|
|
| 247 |
loop {
|
| 248 |
// Scan for next non-null child
|
| 249 |
let child = loop {
|
| 250 |
+
let watermark =
|
| 251 |
+
core::ptr::read_volatile(core::ptr::addr_of!((*task_ptr).children_watermark))
|
| 252 |
+
as usize;
|
| 253 |
if i >= watermark {
|
| 254 |
return;
|
| 255 |
}
|
| 256 |
+
let children =
|
| 257 |
+
core::ptr::read_volatile(core::ptr::addr_of!((*task_ptr).children_data));
|
|
|
|
| 258 |
let c = *children.add(i);
|
| 259 |
i += 1;
|
| 260 |
if !c.is_null() {
|
|
|
|
| 305 |
let mut i: usize = 0;
|
| 306 |
loop {
|
| 307 |
let child = loop {
|
| 308 |
+
let watermark =
|
| 309 |
+
core::ptr::read_volatile(core::ptr::addr_of!((*task_ptr).children_watermark))
|
| 310 |
+
as usize;
|
| 311 |
if i >= watermark {
|
| 312 |
return;
|
| 313 |
}
|
| 314 |
+
let children =
|
| 315 |
+
core::ptr::read_volatile(core::ptr::addr_of!((*task_ptr).children_data));
|
|
|
|
| 316 |
let c = *children.add(i);
|
| 317 |
i += 1;
|
| 318 |
if !c.is_null() {
|
crates/openwa-core/src/task/cloud.rs
CHANGED
|
@@ -18,7 +18,8 @@ pub struct CTaskCloudVTable {
|
|
| 18 |
/// HandleMessage β processes cloud messages (wind updates, render).
|
| 19 |
/// thiscall + 4 stack params, RET 0x10.
|
| 20 |
#[slot(2)]
|
| 21 |
-
pub handle_message:
|
|
|
|
| 22 |
/// ProcessFrame β per-frame cloud update.
|
| 23 |
/// thiscall + 1 stack param (flags), RET 0x4.
|
| 24 |
#[slot(7)]
|
|
|
|
| 18 |
/// HandleMessage β processes cloud messages (wind updates, render).
|
| 19 |
/// thiscall + 4 stack params, RET 0x10.
|
| 20 |
#[slot(2)]
|
| 21 |
+
pub handle_message:
|
| 22 |
+
fn(this: *mut CTaskCloud, sender: *mut CTask, msg_type: u32, size: u32, data: *const u8),
|
| 23 |
/// ProcessFrame β per-frame cloud update.
|
| 24 |
/// thiscall + 1 stack param (flags), RET 0x4.
|
| 25 |
#[slot(7)]
|
crates/openwa-core/src/task/filter.rs
CHANGED
|
@@ -18,7 +18,8 @@ pub struct CTaskFilterVTable {
|
|
| 18 |
/// HandleMessage β checks subscription table, forwards matching messages.
|
| 19 |
/// thiscall + 4 stack params, RET 0x10.
|
| 20 |
#[slot(2)]
|
| 21 |
-
pub handle_message:
|
|
|
|
| 22 |
/// ProcessFrame β per-frame filter update.
|
| 23 |
/// thiscall + 1 stack param (flags), RET 0x4.
|
| 24 |
#[slot(7)]
|
|
|
|
| 18 |
/// HandleMessage β checks subscription table, forwards matching messages.
|
| 19 |
/// thiscall + 4 stack params, RET 0x10.
|
| 20 |
#[slot(2)]
|
| 21 |
+
pub handle_message:
|
| 22 |
+
fn(this: *mut CTaskFilter, sender: *mut CTask, msg_type: u32, size: u32, data: *const u8),
|
| 23 |
/// ProcessFrame β per-frame filter update.
|
| 24 |
/// thiscall + 1 stack param (flags), RET 0x4.
|
| 25 |
#[slot(7)]
|
crates/openwa-core/src/task/fire.rs
CHANGED
|
@@ -17,7 +17,8 @@ pub struct CTaskFireVTable {
|
|
| 17 |
/// HandleMessage β processes fire messages.
|
| 18 |
/// thiscall + 4 stack params, RET 0x10.
|
| 19 |
#[slot(2)]
|
| 20 |
-
pub handle_message:
|
|
|
|
| 21 |
/// ProcessFrame β per-frame fire update (countdown, spread, damage).
|
| 22 |
/// thiscall + 1 stack param (flags), RET 0x4.
|
| 23 |
#[slot(7)]
|
|
|
|
| 17 |
/// HandleMessage β processes fire messages.
|
| 18 |
/// thiscall + 4 stack params, RET 0x10.
|
| 19 |
#[slot(2)]
|
| 20 |
+
pub handle_message:
|
| 21 |
+
fn(this: *mut CTaskFire, sender: *mut CTask, msg_type: u32, size: u32, data: *const u8),
|
| 22 |
/// ProcessFrame β per-frame fire update (countdown, spread, damage).
|
| 23 |
/// thiscall + 1 stack param (flags), RET 0x4.
|
| 24 |
#[slot(7)]
|
crates/openwa-core/src/task/game_task.rs
CHANGED
|
@@ -96,5 +96,6 @@ pub struct SoundEmitterVTable {
|
|
| 96 |
pub destructor: fn(this: *mut SoundEmitter, flags: u32) -> *mut SoundEmitter,
|
| 97 |
/// HandleMessage β sound queue manager
|
| 98 |
#[slot(4)]
|
| 99 |
-
pub handle_message:
|
|
|
|
| 100 |
}
|
|
|
|
| 96 |
pub destructor: fn(this: *mut SoundEmitter, flags: u32) -> *mut SoundEmitter,
|
| 97 |
/// HandleMessage β sound queue manager
|
| 98 |
#[slot(4)]
|
| 99 |
+
pub handle_message:
|
| 100 |
+
fn(this: *mut SoundEmitter, sender: u32, msg_type: u32, size: u32, data: u32),
|
| 101 |
}
|
crates/openwa-core/src/task/mine_oil_drum.rs
CHANGED
|
@@ -24,7 +24,8 @@ pub struct CTaskMineVTable {
|
|
| 24 |
/// HandleMessage β processes mine messages (arm, trigger, detonate).
|
| 25 |
/// thiscall + 4 stack params, RET 0x10.
|
| 26 |
#[slot(2)]
|
| 27 |
-
pub handle_message:
|
|
|
|
| 28 |
/// ProcessFrame β per-frame mine update.
|
| 29 |
/// thiscall + 1 stack param (flags), RET 0x4.
|
| 30 |
#[slot(7)]
|
|
@@ -39,7 +40,8 @@ pub struct CTaskOilDrumVTable {
|
|
| 39 |
/// HandleMessage β processes oil drum messages.
|
| 40 |
/// thiscall + 4 stack params, RET 0x10.
|
| 41 |
#[slot(2)]
|
| 42 |
-
pub handle_message:
|
|
|
|
| 43 |
/// ProcessFrame β per-frame oil drum update.
|
| 44 |
/// thiscall + 1 stack param (flags), RET 0x4.
|
| 45 |
#[slot(7)]
|
|
|
|
| 24 |
/// HandleMessage β processes mine messages (arm, trigger, detonate).
|
| 25 |
/// thiscall + 4 stack params, RET 0x10.
|
| 26 |
#[slot(2)]
|
| 27 |
+
pub handle_message:
|
| 28 |
+
fn(this: *mut CTaskMine, sender: *mut CTask, msg_type: u32, size: u32, data: *const u8),
|
| 29 |
/// ProcessFrame β per-frame mine update.
|
| 30 |
/// thiscall + 1 stack param (flags), RET 0x4.
|
| 31 |
#[slot(7)]
|
|
|
|
| 40 |
/// HandleMessage β processes oil drum messages.
|
| 41 |
/// thiscall + 4 stack params, RET 0x10.
|
| 42 |
#[slot(2)]
|
| 43 |
+
pub handle_message:
|
| 44 |
+
fn(this: *mut CTaskOilDrum, sender: *mut CTask, msg_type: u32, size: u32, data: *const u8),
|
| 45 |
/// ProcessFrame β per-frame oil drum update.
|
| 46 |
/// thiscall + 1 stack param (flags), RET 0x4.
|
| 47 |
#[slot(7)]
|
crates/openwa-core/src/task/missile.rs
CHANGED
|
@@ -20,7 +20,8 @@ pub struct CTaskMissileVTable {
|
|
| 20 |
/// HandleMessage β processes missile messages.
|
| 21 |
/// thiscall + 4 stack params, RET 0x10.
|
| 22 |
#[slot(2)]
|
| 23 |
-
pub handle_message:
|
|
|
|
| 24 |
/// ProcessFrame β per-frame missile update (physics, homing, detonation).
|
| 25 |
/// thiscall + 1 stack param (flags), RET 0x4.
|
| 26 |
#[slot(7)]
|
|
@@ -206,48 +207,89 @@ pub enum MissileType {
|
|
| 206 |
|
| 207 |
#[cfg(target_arch = "x86")]
|
| 208 |
impl crate::snapshot::Snapshot for CTaskMissile {
|
| 209 |
-
unsafe fn write_snapshot(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
use crate::snapshot::{write_indent, write_raw_region};
|
| 211 |
let i = indent;
|
| 212 |
let b = &self.base; // CGameTask
|
| 213 |
|
| 214 |
-
write_indent(w, i)?;
|
| 215 |
-
|
| 216 |
-
write_indent(w, i)?;
|
| 217 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
|
| 219 |
let sp = &self.spawn_params;
|
| 220 |
-
write_indent(w, i)?;
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
|
| 226 |
-
write_indent(w, i)?;
|
|
|
|
| 227 |
for (j, v) in self.weapon_data.iter().enumerate() {
|
| 228 |
-
if j % 16 == 0 {
|
|
|
|
|
|
|
|
|
|
| 229 |
write!(w, " {:08X}", v)?;
|
| 230 |
}
|
| 231 |
writeln!(w)?;
|
| 232 |
|
| 233 |
-
write_indent(w, i)?;
|
|
|
|
| 234 |
for (j, v) in self.render_data.iter().enumerate() {
|
| 235 |
-
if j % 16 == 0 {
|
|
|
|
|
|
|
|
|
|
| 236 |
write!(w, " {:08X}", v)?;
|
| 237 |
}
|
| 238 |
writeln!(w)?;
|
| 239 |
|
| 240 |
-
write_indent(w, i)?;
|
| 241 |
-
|
| 242 |
-
write_indent(w, i)?;
|
|
|
|
|
|
|
|
|
|
| 243 |
|
| 244 |
// Unknown regions
|
| 245 |
-
write_indent(w, i)?;
|
|
|
|
| 246 |
write_raw_region(w, self._unknown_fc.as_ptr(), self._unknown_fc.len(), i + 1)?;
|
| 247 |
-
write_indent(w, i)?;
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
|
| 252 |
Ok(())
|
| 253 |
}
|
|
|
|
| 20 |
/// HandleMessage β processes missile messages.
|
| 21 |
/// thiscall + 4 stack params, RET 0x10.
|
| 22 |
#[slot(2)]
|
| 23 |
+
pub handle_message:
|
| 24 |
+
fn(this: *mut CTaskMissile, sender: *mut CTask, msg_type: u32, size: u32, data: *const u8),
|
| 25 |
/// ProcessFrame β per-frame missile update (physics, homing, detonation).
|
| 26 |
/// thiscall + 1 stack param (flags), RET 0x4.
|
| 27 |
#[slot(7)]
|
|
|
|
| 207 |
|
| 208 |
#[cfg(target_arch = "x86")]
|
| 209 |
impl crate::snapshot::Snapshot for CTaskMissile {
|
| 210 |
+
unsafe fn write_snapshot(
|
| 211 |
+
&self,
|
| 212 |
+
w: &mut dyn core::fmt::Write,
|
| 213 |
+
indent: usize,
|
| 214 |
+
) -> core::fmt::Result {
|
| 215 |
use crate::snapshot::{write_indent, write_raw_region};
|
| 216 |
let i = indent;
|
| 217 |
let b = &self.base; // CGameTask
|
| 218 |
|
| 219 |
+
write_indent(w, i)?;
|
| 220 |
+
writeln!(w, "pos = ({}, {})", b.pos_x, b.pos_y)?;
|
| 221 |
+
write_indent(w, i)?;
|
| 222 |
+
writeln!(w, "speed = ({}, {})", b.speed_x, b.speed_y)?;
|
| 223 |
+
write_indent(w, i)?;
|
| 224 |
+
writeln!(w, "launch_seed = 0x{:08X}", self.launch_seed)?;
|
| 225 |
+
write_indent(w, i)?;
|
| 226 |
+
writeln!(w, "slot_id = {}", self.slot_id)?;
|
| 227 |
|
| 228 |
let sp = &self.spawn_params;
|
| 229 |
+
write_indent(w, i)?;
|
| 230 |
+
writeln!(w, "spawn_params:")?;
|
| 231 |
+
write_indent(w, i + 1)?;
|
| 232 |
+
writeln!(
|
| 233 |
+
w,
|
| 234 |
+
"owner={} spawn=({}, {}) speed=({}, {})",
|
| 235 |
+
sp.owner_id, sp.spawn_x, sp.spawn_y, sp.initial_speed_x, sp.initial_speed_y
|
| 236 |
+
)?;
|
| 237 |
+
write_indent(w, i + 1)?;
|
| 238 |
+
writeln!(
|
| 239 |
+
w,
|
| 240 |
+
"cursor=({}, {}) pellet={} fallback=({}, {})",
|
| 241 |
+
sp.cursor_x, sp.cursor_y, sp.pellet_index, sp.fallback_timer, sp.fallback_param
|
| 242 |
+
)?;
|
| 243 |
|
| 244 |
+
write_indent(w, i)?;
|
| 245 |
+
write!(w, "weapon_data =")?;
|
| 246 |
for (j, v) in self.weapon_data.iter().enumerate() {
|
| 247 |
+
if j % 16 == 0 {
|
| 248 |
+
writeln!(w)?;
|
| 249 |
+
write_indent(w, i + 1)?;
|
| 250 |
+
}
|
| 251 |
write!(w, " {:08X}", v)?;
|
| 252 |
}
|
| 253 |
writeln!(w)?;
|
| 254 |
|
| 255 |
+
write_indent(w, i)?;
|
| 256 |
+
write!(w, "render_data =")?;
|
| 257 |
for (j, v) in self.render_data.iter().enumerate() {
|
| 258 |
+
if j % 16 == 0 {
|
| 259 |
+
writeln!(w)?;
|
| 260 |
+
write_indent(w, i + 1)?;
|
| 261 |
+
}
|
| 262 |
write!(w, " {:08X}", v)?;
|
| 263 |
}
|
| 264 |
writeln!(w)?;
|
| 265 |
|
| 266 |
+
write_indent(w, i)?;
|
| 267 |
+
writeln!(w, "launch_speed_raw = {}", self.launch_speed_raw)?;
|
| 268 |
+
write_indent(w, i)?;
|
| 269 |
+
writeln!(w, "homing_enabled = {}", self.homing_enabled)?;
|
| 270 |
+
write_indent(w, i)?;
|
| 271 |
+
writeln!(w, "direction = {}", self.direction)?;
|
| 272 |
|
| 273 |
// Unknown regions
|
| 274 |
+
write_indent(w, i)?;
|
| 275 |
+
writeln!(w, "_unknown_fc ({} bytes):", self._unknown_fc.len())?;
|
| 276 |
write_raw_region(w, self._unknown_fc.as_ptr(), self._unknown_fc.len(), i + 1)?;
|
| 277 |
+
write_indent(w, i)?;
|
| 278 |
+
writeln!(w, "_unknown_37c ({} bytes):", self._unknown_37c.len())?;
|
| 279 |
+
write_raw_region(
|
| 280 |
+
w,
|
| 281 |
+
self._unknown_37c.as_ptr(),
|
| 282 |
+
self._unknown_37c.len(),
|
| 283 |
+
i + 1,
|
| 284 |
+
)?;
|
| 285 |
+
write_indent(w, i)?;
|
| 286 |
+
writeln!(w, "_unknown_3cc ({} bytes):", self._unknown_3cc.len())?;
|
| 287 |
+
write_raw_region(
|
| 288 |
+
w,
|
| 289 |
+
self._unknown_3cc.as_ptr(),
|
| 290 |
+
self._unknown_3cc.len(),
|
| 291 |
+
i + 1,
|
| 292 |
+
)?;
|
| 293 |
|
| 294 |
Ok(())
|
| 295 |
}
|
crates/openwa-core/src/task/mod.rs
CHANGED
|
@@ -15,7 +15,9 @@ pub mod turn_game;
|
|
| 15 |
pub mod worm;
|
| 16 |
|
| 17 |
pub use arrow::{CTaskArrow, CTaskArrowVTable};
|
| 18 |
-
pub use base::{
|
|
|
|
|
|
|
| 19 |
pub use cloud::{CTaskCloud, CTaskCloudVTable};
|
| 20 |
pub use filter::{CTaskFilter, CTaskFilterVTable};
|
| 21 |
pub use fire::{CTaskFire, CTaskFireVTable};
|
|
|
|
| 15 |
pub mod worm;
|
| 16 |
|
| 17 |
pub use arrow::{CTaskArrow, CTaskArrowVTable};
|
| 18 |
+
pub use base::{
|
| 19 |
+
CTask, CTaskBfsIter, CTaskVtable, SharedDataIter, SharedDataNode, SharedDataTable, Task, Vtable,
|
| 20 |
+
};
|
| 21 |
pub use cloud::{CTaskCloud, CTaskCloudVTable};
|
| 22 |
pub use filter::{CTaskFilter, CTaskFilterVTable};
|
| 23 |
pub use fire::{CTaskFire, CTaskFireVTable};
|
crates/openwa-core/src/task/supply_crate.rs
CHANGED
|
@@ -18,7 +18,8 @@ pub struct CTaskCrateVTable {
|
|
| 18 |
/// HandleMessage β processes crate messages (collection, parachute, etc.).
|
| 19 |
/// thiscall + 4 stack params, RET 0x10.
|
| 20 |
#[slot(2)]
|
| 21 |
-
pub handle_message:
|
|
|
|
| 22 |
/// ProcessFrame β per-frame crate update.
|
| 23 |
/// thiscall + 1 stack param (flags), RET 0x4.
|
| 24 |
#[slot(7)]
|
|
|
|
| 18 |
/// HandleMessage β processes crate messages (collection, parachute, etc.).
|
| 19 |
/// thiscall + 4 stack params, RET 0x10.
|
| 20 |
#[slot(2)]
|
| 21 |
+
pub handle_message:
|
| 22 |
+
fn(this: *mut CTaskCrate, sender: *mut CTask, msg_type: u32, size: u32, data: *const u8),
|
| 23 |
/// ProcessFrame β per-frame crate update.
|
| 24 |
/// thiscall + 1 stack param (flags), RET 0x4.
|
| 25 |
#[slot(7)]
|
crates/openwa-core/src/task/team.rs
CHANGED
|
@@ -27,7 +27,8 @@ pub struct CTaskTeamVTable {
|
|
| 27 |
/// surrender, worm selection, napalm strike, etc.)
|
| 28 |
/// thiscall + 4 stack params, RET 0x10.
|
| 29 |
#[slot(2)]
|
| 30 |
-
pub handle_message:
|
|
|
|
| 31 |
/// GetEntityData β returns team data by query code.
|
| 32 |
/// thiscall + 3 stack params, RET 0xC.
|
| 33 |
#[slot(3)]
|
|
@@ -133,11 +134,7 @@ impl CTaskTeam {
|
|
| 133 |
///
|
| 134 |
/// # Safety
|
| 135 |
/// `this` must be a valid CTaskTeam pointer with valid ddgame.
|
| 136 |
-
pub unsafe fn on_surrender_fire(
|
| 137 |
-
this: *mut Self,
|
| 138 |
-
sender: *mut CTask,
|
| 139 |
-
msg_team_index: u32,
|
| 140 |
-
) {
|
| 141 |
use crate::game::TaskMessage;
|
| 142 |
|
| 143 |
let team_index = (*this).team_index;
|
|
@@ -157,19 +154,26 @@ impl CTaskTeam {
|
|
| 157 |
let game_version = (*(*ddgame).game_info).game_version;
|
| 158 |
if game_version > 0xF4 {
|
| 159 |
CTask::broadcast_message_raw(
|
| 160 |
-
task_ptr,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
);
|
| 162 |
}
|
| 163 |
|
| 164 |
// 3. Set per-team napalm flag
|
| 165 |
// Original: *(ddgame + team_index * 0x51C + 0x4618) = 1
|
| 166 |
-
let flag_ptr = (ddgame as *mut u8)
|
| 167 |
-
.add(team_index as usize * 0x51C + 0x4618) as *mut u32;
|
| 168 |
*flag_ptr = 1;
|
| 169 |
|
| 170 |
// 4. Broadcast original message (0x2B) to children
|
| 171 |
CTask::broadcast_message_raw(
|
| 172 |
-
task_ptr,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
);
|
| 174 |
}
|
| 175 |
}
|
|
|
|
| 27 |
/// surrender, worm selection, napalm strike, etc.)
|
| 28 |
/// thiscall + 4 stack params, RET 0x10.
|
| 29 |
#[slot(2)]
|
| 30 |
+
pub handle_message:
|
| 31 |
+
fn(this: *mut CTaskTeam, sender: *mut CTask, msg_type: u32, size: u32, data: *const u8),
|
| 32 |
/// GetEntityData β returns team data by query code.
|
| 33 |
/// thiscall + 3 stack params, RET 0xC.
|
| 34 |
#[slot(3)]
|
|
|
|
| 134 |
///
|
| 135 |
/// # Safety
|
| 136 |
/// `this` must be a valid CTaskTeam pointer with valid ddgame.
|
| 137 |
+
pub unsafe fn on_surrender_fire(this: *mut Self, sender: *mut CTask, msg_team_index: u32) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
use crate::game::TaskMessage;
|
| 139 |
|
| 140 |
let team_index = (*this).team_index;
|
|
|
|
| 154 |
let game_version = (*(*ddgame).game_info).game_version;
|
| 155 |
if game_version > 0xF4 {
|
| 156 |
CTask::broadcast_message_raw(
|
| 157 |
+
task_ptr,
|
| 158 |
+
sender,
|
| 159 |
+
TaskMessage::DetonateWeapon as u32,
|
| 160 |
+
4,
|
| 161 |
+
buf.as_ptr(),
|
| 162 |
);
|
| 163 |
}
|
| 164 |
|
| 165 |
// 3. Set per-team napalm flag
|
| 166 |
// Original: *(ddgame + team_index * 0x51C + 0x4618) = 1
|
| 167 |
+
let flag_ptr = (ddgame as *mut u8).add(team_index as usize * 0x51C + 0x4618) as *mut u32;
|
|
|
|
| 168 |
*flag_ptr = 1;
|
| 169 |
|
| 170 |
// 4. Broadcast original message (0x2B) to children
|
| 171 |
CTask::broadcast_message_raw(
|
| 172 |
+
task_ptr,
|
| 173 |
+
sender,
|
| 174 |
+
TaskMessage::Surrender as u32,
|
| 175 |
+
4,
|
| 176 |
+
buf.as_ptr(),
|
| 177 |
);
|
| 178 |
}
|
| 179 |
}
|
crates/openwa-core/src/task/turn_game.rs
CHANGED
|
@@ -98,7 +98,8 @@ pub struct CTaskTurnGameVTable {
|
|
| 98 |
/// HandleMessage β processes messages for turn flow control
|
| 99 |
/// thiscall + 4 stack params, RET 0x10.
|
| 100 |
#[slot(2)]
|
| 101 |
-
pub handle_message:
|
|
|
|
| 102 |
/// ProcessFrame β per-frame turn update.
|
| 103 |
/// thiscall + 1 stack param (flags), RET 0x4.
|
| 104 |
#[slot(7)]
|
|
|
|
| 98 |
/// HandleMessage β processes messages for turn flow control
|
| 99 |
/// thiscall + 4 stack params, RET 0x10.
|
| 100 |
#[slot(2)]
|
| 101 |
+
pub handle_message:
|
| 102 |
+
fn(this: *mut CTaskTurnGame, sender: *mut CTask, msg_type: u32, size: u32, data: *const u8),
|
| 103 |
/// ProcessFrame β per-frame turn update.
|
| 104 |
/// thiscall + 1 stack param (flags), RET 0x4.
|
| 105 |
#[slot(7)]
|
crates/openwa-core/src/task/worm.rs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
use super::base::CTask;
|
| 2 |
use super::game_task::CGameTask;
|
| 3 |
use crate::fixed::Fixed;
|
| 4 |
-
use crate::game::Weapon;
|
| 5 |
use crate::game::weapon::WeaponEntry;
|
|
|
|
| 6 |
use crate::FieldRegistry;
|
| 7 |
|
| 8 |
/// Worm state machine states.
|
|
@@ -173,7 +173,8 @@ pub struct CTaskWormVTable {
|
|
| 173 |
pub free: fn(this: *mut CTaskWorm, flags: u8) -> *mut CTaskWorm,
|
| 174 |
/// HandleMessage β processes all TaskMessages sent to this worm
|
| 175 |
#[slot(2)]
|
| 176 |
-
pub handle_message:
|
|
|
|
| 177 |
/// GetEntityData β returns worm data by query code
|
| 178 |
#[slot(3)]
|
| 179 |
pub get_entity_data: fn(this: *mut CTaskWorm, query: u32, param: u32, out: *mut u32) -> u32,
|
|
@@ -451,32 +452,66 @@ impl CTaskWorm {
|
|
| 451 |
|
| 452 |
#[cfg(target_arch = "x86")]
|
| 453 |
impl crate::snapshot::Snapshot for CTaskWorm {
|
| 454 |
-
unsafe fn write_snapshot(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 455 |
use crate::snapshot::{write_indent, write_raw_region};
|
| 456 |
let i = indent;
|
| 457 |
let b = &self.base; // CGameTask
|
| 458 |
|
| 459 |
-
write_indent(w, i)?;
|
| 460 |
-
|
| 461 |
-
write_indent(w, i)?;
|
| 462 |
-
|
| 463 |
-
write_indent(w, i)?;
|
| 464 |
-
|
| 465 |
-
write_indent(w, i)?;
|
| 466 |
-
|
| 467 |
-
write_indent(w, i)?;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 468 |
|
| 469 |
-
write_indent(w, i)?;
|
| 470 |
-
|
|
|
|
|
|
|
|
|
|
| 471 |
writeln!(w)?;
|
| 472 |
|
| 473 |
// Raw dump of unknown regions
|
| 474 |
-
write_indent(w, i)?;
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 480 |
|
| 481 |
Ok(())
|
| 482 |
}
|
|
|
|
| 1 |
use super::base::CTask;
|
| 2 |
use super::game_task::CGameTask;
|
| 3 |
use crate::fixed::Fixed;
|
|
|
|
| 4 |
use crate::game::weapon::WeaponEntry;
|
| 5 |
+
use crate::game::Weapon;
|
| 6 |
use crate::FieldRegistry;
|
| 7 |
|
| 8 |
/// Worm state machine states.
|
|
|
|
| 173 |
pub free: fn(this: *mut CTaskWorm, flags: u8) -> *mut CTaskWorm,
|
| 174 |
/// HandleMessage β processes all TaskMessages sent to this worm
|
| 175 |
#[slot(2)]
|
| 176 |
+
pub handle_message:
|
| 177 |
+
fn(this: *mut CTaskWorm, sender: *mut CTask, msg_type: u32, size: u32, data: *const u8),
|
| 178 |
/// GetEntityData β returns worm data by query code
|
| 179 |
#[slot(3)]
|
| 180 |
pub get_entity_data: fn(this: *mut CTaskWorm, query: u32, param: u32, out: *mut u32) -> u32,
|
|
|
|
| 452 |
|
| 453 |
#[cfg(target_arch = "x86")]
|
| 454 |
impl crate::snapshot::Snapshot for CTaskWorm {
|
| 455 |
+
unsafe fn write_snapshot(
|
| 456 |
+
&self,
|
| 457 |
+
w: &mut dyn core::fmt::Write,
|
| 458 |
+
indent: usize,
|
| 459 |
+
) -> core::fmt::Result {
|
| 460 |
use crate::snapshot::{write_indent, write_raw_region};
|
| 461 |
let i = indent;
|
| 462 |
let b = &self.base; // CGameTask
|
| 463 |
|
| 464 |
+
write_indent(w, i)?;
|
| 465 |
+
writeln!(w, "pos = ({}, {})", b.pos_x, b.pos_y)?;
|
| 466 |
+
write_indent(w, i)?;
|
| 467 |
+
writeln!(w, "speed = ({}, {})", b.speed_x, b.speed_y)?;
|
| 468 |
+
write_indent(w, i)?;
|
| 469 |
+
writeln!(w, "angle = {}", b.angle)?;
|
| 470 |
+
write_indent(w, i)?;
|
| 471 |
+
writeln!(w, "team_index = {}", self.team_index)?;
|
| 472 |
+
write_indent(w, i)?;
|
| 473 |
+
writeln!(w, "worm_index = {}", self.worm_index)?;
|
| 474 |
+
write_indent(w, i)?;
|
| 475 |
+
writeln!(w, "slot_id = {}", self.slot_id)?;
|
| 476 |
+
write_indent(w, i)?;
|
| 477 |
+
writeln!(w, "selected_weapon = {:?}", self.selected_weapon)?;
|
| 478 |
+
write_indent(w, i)?;
|
| 479 |
+
writeln!(w, "facing = {}", self.facing_direction)?;
|
| 480 |
+
write_indent(w, i)?;
|
| 481 |
+
writeln!(w, "aim_angle = 0x{:08X}", self.aim_angle)?;
|
| 482 |
|
| 483 |
+
write_indent(w, i)?;
|
| 484 |
+
write!(w, "spawn_params =")?;
|
| 485 |
+
for v in &self.spawn_params {
|
| 486 |
+
write!(w, " {:08X}", v)?;
|
| 487 |
+
}
|
| 488 |
writeln!(w)?;
|
| 489 |
|
| 490 |
// Raw dump of unknown regions
|
| 491 |
+
write_indent(w, i)?;
|
| 492 |
+
writeln!(w, "_unknown_1f0 ({} bytes):", self._unknown_1f0.len())?;
|
| 493 |
+
write_raw_region(
|
| 494 |
+
w,
|
| 495 |
+
self._unknown_1f0.as_ptr(),
|
| 496 |
+
self._unknown_1f0.len(),
|
| 497 |
+
i + 1,
|
| 498 |
+
)?;
|
| 499 |
+
write_indent(w, i)?;
|
| 500 |
+
writeln!(w, "_unknown_28c ({} bytes):", self._unknown_28c.len())?;
|
| 501 |
+
write_raw_region(
|
| 502 |
+
w,
|
| 503 |
+
self._unknown_28c.as_ptr(),
|
| 504 |
+
self._unknown_28c.len(),
|
| 505 |
+
i + 1,
|
| 506 |
+
)?;
|
| 507 |
+
write_indent(w, i)?;
|
| 508 |
+
writeln!(w, "_unknown_370 ({} bytes):", self._unknown_370.len())?;
|
| 509 |
+
write_raw_region(
|
| 510 |
+
w,
|
| 511 |
+
self._unknown_370.as_ptr(),
|
| 512 |
+
self._unknown_370.len(),
|
| 513 |
+
i + 1,
|
| 514 |
+
)?;
|
| 515 |
|
| 516 |
Ok(())
|
| 517 |
}
|
crates/openwa-debug-cli/src/main.rs
CHANGED
|
@@ -31,16 +31,20 @@ fn main() {
|
|
| 31 |
match args[i].as_str() {
|
| 32 |
"--port" => {
|
| 33 |
i += 1;
|
| 34 |
-
port = args.get(i)
|
| 35 |
-
|
| 36 |
-
|
|
|
|
| 37 |
}
|
| 38 |
"--format" => {
|
| 39 |
i += 1;
|
| 40 |
format = match args.get(i).map(|s| s.as_str()) {
|
| 41 |
Some("hex") => Format::Hex,
|
| 42 |
Some("raw") => Format::Raw,
|
| 43 |
-
_ => {
|
|
|
|
|
|
|
|
|
|
| 44 |
};
|
| 45 |
}
|
| 46 |
other => positional.push(other.to_string()),
|
|
@@ -58,36 +62,63 @@ fn main() {
|
|
| 58 |
});
|
| 59 |
// Detect shell eating '->' as redirect: address ends with '-'
|
| 60 |
if addr_str.ends_with('-') {
|
| 61 |
-
eprintln!(
|
|
|
|
|
|
|
|
|
|
| 62 |
eprintln!(" Hint: quote the argument: read \"0x7A0884->0xA0->0x0\"");
|
| 63 |
process::exit(1);
|
| 64 |
}
|
| 65 |
let expr = resolve_address_expr(addr_str, port);
|
| 66 |
-
let len = positional.get(2)
|
| 67 |
-
.map(|s| parse_u32(s))
|
| 68 |
-
.unwrap_or(256);
|
| 69 |
match expr {
|
| 70 |
-
AddressExpr::Simple { addr, absolute } =>
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
}
|
| 75 |
}
|
| 76 |
Some("inspect") => {
|
| 77 |
-
let class_name = positional
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
| 81 |
let addr_str = positional.get(2).unwrap_or_else(|| {
|
| 82 |
eprintln!("Usage: inspect <class_name> <addr>");
|
| 83 |
process::exit(1);
|
| 84 |
});
|
| 85 |
let expr = resolve_address_expr(addr_str, port);
|
| 86 |
match expr {
|
| 87 |
-
AddressExpr::Simple { addr, absolute } =>
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
}
|
| 92 |
}
|
| 93 |
Some("objects") => Request::ListObjects,
|
|
@@ -99,9 +130,19 @@ fn main() {
|
|
| 99 |
}
|
| 100 |
Some("frame") => Request::Frame,
|
| 101 |
Some("break") => {
|
| 102 |
-
let frame = positional
|
| 103 |
-
.
|
| 104 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
Request::Break { frame }
|
| 106 |
}
|
| 107 |
Some("snapshot") => Request::Snapshot,
|
|
@@ -149,39 +190,50 @@ fn print_response(response: &Response, format: Format) {
|
|
| 149 |
println!(" {} β {}", cmd.usage, cmd.description);
|
| 150 |
}
|
| 151 |
}
|
| 152 |
-
Response::ReadResult {
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
|
|
|
|
|
|
|
|
|
| 160 |
}
|
| 161 |
-
}
|
| 162 |
-
Response::ReadChainResult {
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
println!(
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
let stdout = io::stdout();
|
| 177 |
-
let mut out = stdout.lock();
|
| 178 |
-
let _ = out.write_all(data);
|
| 179 |
}
|
|
|
|
|
|
|
| 180 |
}
|
| 181 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
Response::Suspended { frame } => println!("Suspended at frame {frame}"),
|
| 183 |
Response::Resumed => println!("Resumed"),
|
| 184 |
-
Response::FrameInfo {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
let state = if *paused { "PAUSED" } else { "running" };
|
| 186 |
print!("Frame {frame} [{state}]");
|
| 187 |
if *breakpoint >= 0 {
|
|
@@ -200,7 +252,12 @@ fn print_response(response: &Response, format: Format) {
|
|
| 200 |
println!("=== Snapshot at frame {frame} ===\n");
|
| 201 |
print!("{text}");
|
| 202 |
}
|
| 203 |
-
Response::InspectResult {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
println!(
|
| 205 |
"{} at ghidra:0x{:08X} (runtime:0x{:08X})\n",
|
| 206 |
class_name, ghidra_addr, runtime_addr
|
|
@@ -226,7 +283,10 @@ fn print_response(response: &Response, format: Format) {
|
|
| 226 |
}
|
| 227 |
}
|
| 228 |
Response::AliasResult(alias) => {
|
| 229 |
-
println!(
|
|
|
|
|
|
|
|
|
|
| 230 |
}
|
| 231 |
Response::FieldResult(field) => {
|
| 232 |
println!("offset:0x{:X} size:{}", field.offset, field.size);
|
|
@@ -241,7 +301,9 @@ fn print_response(response: &Response, format: Format) {
|
|
| 241 |
fn print_hex_read(ghidra_addr: u32, runtime_addr: u32, data: &[u8], pointers: &[PointerInfo]) {
|
| 242 |
println!(
|
| 243 |
"Reading 0x{:X} bytes at ghidra:0x{:08X} (runtime:0x{:08X})\n",
|
| 244 |
-
data.len(),
|
|
|
|
|
|
|
| 245 |
);
|
| 246 |
|
| 247 |
// Hex dump with ASCII sidebar
|
|
@@ -252,13 +314,17 @@ fn print_hex_read(ghidra_addr: u32, runtime_addr: u32, data: &[u8], pointers: &[
|
|
| 252 |
// Hex bytes
|
| 253 |
for (i, byte) in chunk.iter().enumerate() {
|
| 254 |
print!("{:02X} ", byte);
|
| 255 |
-
if i == 7 {
|
|
|
|
|
|
|
| 256 |
}
|
| 257 |
// Padding for short last line
|
| 258 |
if chunk.len() < 16 {
|
| 259 |
for i in chunk.len()..16 {
|
| 260 |
print!(" ");
|
| 261 |
-
if i == 7 {
|
|
|
|
|
|
|
| 262 |
}
|
| 263 |
}
|
| 264 |
// ASCII sidebar
|
|
@@ -284,7 +350,11 @@ fn print_hex_read(ghidra_addr: u32, runtime_addr: u32, data: &[u8], pointers: &[
|
|
| 284 |
PointerKind::Object => "OBJECT",
|
| 285 |
PointerKind::Heap => "HEAP",
|
| 286 |
};
|
| 287 |
-
let detail_str = p
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
println!(
|
| 289 |
" +0x{:03X} 0x{:08X} {:<7} ghidra:0x{:08X}{}",
|
| 290 |
p.offset, p.raw_value, kind_str, p.ghidra_value, detail_str
|
|
@@ -304,7 +374,11 @@ enum AddressExpr {
|
|
| 304 |
/// Simple address (with optional offset already applied)
|
| 305 |
Simple { addr: u32, absolute: bool },
|
| 306 |
/// Pointer chain: start address + list of deref offsets
|
| 307 |
-
Chain {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
}
|
| 309 |
|
| 310 |
/// Split a string on '+' or '[' into (base, offset_str). Returns (s, None) if no compound.
|
|
@@ -330,10 +404,13 @@ fn resolve_offset(s: &str, class: &Option<String>, port: u16) -> u32 {
|
|
| 330 |
|
| 331 |
// Try as field name
|
| 332 |
if let Some(cls) = class {
|
| 333 |
-
match send_request(
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
|
|
|
|
|
|
|
|
|
| 337 |
Ok(Response::FieldResult(f)) => return f.offset,
|
| 338 |
_ => {}
|
| 339 |
}
|
|
@@ -344,7 +421,10 @@ fn resolve_offset(s: &str, class: &Option<String>, port: u16) -> u32 {
|
|
| 344 |
}
|
| 345 |
|
| 346 |
fn parse_hex(s: &str) -> u32 {
|
| 347 |
-
let s = s
|
|
|
|
|
|
|
|
|
|
| 348 |
u32::from_str_radix(s, 16).unwrap_or_else(|_| {
|
| 349 |
eprintln!("Error: invalid hex address '{s}'");
|
| 350 |
process::exit(1);
|
|
@@ -408,7 +488,10 @@ fn resolve_address_expr(s: &str, port: u16) -> AddressExpr {
|
|
| 408 |
let (base_addr, base_absolute, mut current_class) = resolve_base(base_part, absolute, port);
|
| 409 |
|
| 410 |
if parts.len() == 1 {
|
| 411 |
-
return AddressExpr::Simple {
|
|
|
|
|
|
|
|
|
|
| 412 |
}
|
| 413 |
|
| 414 |
// Resolve each chain segment, tracking whether it's a field name or hex.
|
|
@@ -443,7 +526,11 @@ fn resolve_address_expr(s: &str, port: u16) -> AddressExpr {
|
|
| 443 |
}
|
| 444 |
}
|
| 445 |
|
| 446 |
-
AddressExpr::Chain {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 447 |
}
|
| 448 |
|
| 449 |
/// Resolve the base address part β could be a hex literal, a named alias, or alias+offset.
|
|
@@ -457,11 +544,18 @@ fn resolve_base(s: &str, absolute: bool, port: u16) -> (u32, bool, Option<String
|
|
| 457 |
}
|
| 458 |
|
| 459 |
// Try resolving as a named alias
|
| 460 |
-
match send_request(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
Ok(Response::AliasResult(alias)) => {
|
| 462 |
// Resolved! Use runtime address (absolute)
|
| 463 |
let class = Some(alias.class_name);
|
| 464 |
-
let extra = offset_str
|
|
|
|
|
|
|
| 465 |
(alias.runtime_addr.wrapping_add(extra), true, class)
|
| 466 |
}
|
| 467 |
_ => {
|
|
@@ -475,7 +569,9 @@ fn resolve_base(s: &str, absolute: bool, port: u16) -> (u32, bool, Option<String
|
|
| 475 |
/// Resolve a chain segment, returning whether it was a field name or hex offset.
|
| 476 |
fn resolve_chain_segment_typed(s: &str, current_class: &Option<String>, port: u16) -> ChainSegment {
|
| 477 |
let (base_str, offset_str) = split_compound(s);
|
| 478 |
-
let extra = offset_str
|
|
|
|
|
|
|
| 479 |
|
| 480 |
// If it looks like hex, parse directly β standard chain semantics
|
| 481 |
if looks_like_hex(base_str) {
|
|
@@ -484,10 +580,13 @@ fn resolve_chain_segment_typed(s: &str, current_class: &Option<String>, port: u1
|
|
| 484 |
|
| 485 |
// Try resolving as a field name if we know the current class
|
| 486 |
if let Some(class) = current_class {
|
| 487 |
-
match send_request(
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
|
|
|
|
|
|
|
|
|
| 491 |
Ok(Response::FieldResult(f)) => {
|
| 492 |
return ChainSegment::FieldOffset(f.offset.wrapping_add(extra));
|
| 493 |
}
|
|
@@ -501,7 +600,9 @@ fn resolve_chain_segment_typed(s: &str, current_class: &Option<String>, port: u1
|
|
| 501 |
|
| 502 |
/// Check if a string looks like a hex number (starts with 0x or a digit).
|
| 503 |
fn looks_like_hex(s: &str) -> bool {
|
| 504 |
-
s.starts_with("0x")
|
|
|
|
|
|
|
| 505 |
}
|
| 506 |
|
| 507 |
fn print_usage() {
|
|
|
|
| 31 |
match args[i].as_str() {
|
| 32 |
"--port" => {
|
| 33 |
i += 1;
|
| 34 |
+
port = args.get(i).and_then(|s| s.parse().ok()).unwrap_or_else(|| {
|
| 35 |
+
eprintln!("Error: --port requires a number");
|
| 36 |
+
process::exit(1);
|
| 37 |
+
});
|
| 38 |
}
|
| 39 |
"--format" => {
|
| 40 |
i += 1;
|
| 41 |
format = match args.get(i).map(|s| s.as_str()) {
|
| 42 |
Some("hex") => Format::Hex,
|
| 43 |
Some("raw") => Format::Raw,
|
| 44 |
+
_ => {
|
| 45 |
+
eprintln!("Error: --format must be 'hex' or 'raw'");
|
| 46 |
+
process::exit(1);
|
| 47 |
+
}
|
| 48 |
};
|
| 49 |
}
|
| 50 |
other => positional.push(other.to_string()),
|
|
|
|
| 62 |
});
|
| 63 |
// Detect shell eating '->' as redirect: address ends with '-'
|
| 64 |
if addr_str.ends_with('-') {
|
| 65 |
+
eprintln!(
|
| 66 |
+
"Error: address '{}' looks truncated β did the shell eat '->'?",
|
| 67 |
+
addr_str
|
| 68 |
+
);
|
| 69 |
eprintln!(" Hint: quote the argument: read \"0x7A0884->0xA0->0x0\"");
|
| 70 |
process::exit(1);
|
| 71 |
}
|
| 72 |
let expr = resolve_address_expr(addr_str, port);
|
| 73 |
+
let len = positional.get(2).map(|s| parse_u32(s)).unwrap_or(256);
|
|
|
|
|
|
|
| 74 |
match expr {
|
| 75 |
+
AddressExpr::Simple { addr, absolute } => Request::Read {
|
| 76 |
+
addr,
|
| 77 |
+
len,
|
| 78 |
+
absolute,
|
| 79 |
+
},
|
| 80 |
+
AddressExpr::Chain {
|
| 81 |
+
addr,
|
| 82 |
+
chain,
|
| 83 |
+
absolute,
|
| 84 |
+
} => Request::ReadChain {
|
| 85 |
+
addr,
|
| 86 |
+
chain,
|
| 87 |
+
len,
|
| 88 |
+
absolute,
|
| 89 |
+
},
|
| 90 |
}
|
| 91 |
}
|
| 92 |
Some("inspect") => {
|
| 93 |
+
let class_name = positional
|
| 94 |
+
.get(1)
|
| 95 |
+
.unwrap_or_else(|| {
|
| 96 |
+
eprintln!("Usage: inspect <class_name> <addr>");
|
| 97 |
+
process::exit(1);
|
| 98 |
+
})
|
| 99 |
+
.clone();
|
| 100 |
let addr_str = positional.get(2).unwrap_or_else(|| {
|
| 101 |
eprintln!("Usage: inspect <class_name> <addr>");
|
| 102 |
process::exit(1);
|
| 103 |
});
|
| 104 |
let expr = resolve_address_expr(addr_str, port);
|
| 105 |
match expr {
|
| 106 |
+
AddressExpr::Simple { addr, absolute } => Request::Inspect {
|
| 107 |
+
class_name,
|
| 108 |
+
addr,
|
| 109 |
+
chain: vec![],
|
| 110 |
+
absolute,
|
| 111 |
+
},
|
| 112 |
+
AddressExpr::Chain {
|
| 113 |
+
addr,
|
| 114 |
+
chain,
|
| 115 |
+
absolute,
|
| 116 |
+
} => Request::Inspect {
|
| 117 |
+
class_name,
|
| 118 |
+
addr,
|
| 119 |
+
chain,
|
| 120 |
+
absolute,
|
| 121 |
+
},
|
| 122 |
}
|
| 123 |
}
|
| 124 |
Some("objects") => Request::ListObjects,
|
|
|
|
| 130 |
}
|
| 131 |
Some("frame") => Request::Frame,
|
| 132 |
Some("break") => {
|
| 133 |
+
let frame = positional
|
| 134 |
+
.get(1)
|
| 135 |
+
.map(|s| {
|
| 136 |
+
if s == "clear" || s == "off" {
|
| 137 |
+
-1
|
| 138 |
+
} else {
|
| 139 |
+
parse_u32(s) as i32
|
| 140 |
+
}
|
| 141 |
+
})
|
| 142 |
+
.unwrap_or_else(|| {
|
| 143 |
+
eprintln!("Usage: break <frame> | break clear");
|
| 144 |
+
process::exit(1);
|
| 145 |
+
});
|
| 146 |
Request::Break { frame }
|
| 147 |
}
|
| 148 |
Some("snapshot") => Request::Snapshot,
|
|
|
|
| 190 |
println!(" {} β {}", cmd.usage, cmd.description);
|
| 191 |
}
|
| 192 |
}
|
| 193 |
+
Response::ReadResult {
|
| 194 |
+
ghidra_addr,
|
| 195 |
+
runtime_addr,
|
| 196 |
+
data,
|
| 197 |
+
pointers,
|
| 198 |
+
} => match format {
|
| 199 |
+
Format::Hex => print_hex_read(*ghidra_addr, *runtime_addr, data, pointers),
|
| 200 |
+
Format::Raw => {
|
| 201 |
+
let stdout = io::stdout();
|
| 202 |
+
let mut out = stdout.lock();
|
| 203 |
+
let _ = out.write_all(data);
|
| 204 |
}
|
| 205 |
+
},
|
| 206 |
+
Response::ReadChainResult {
|
| 207 |
+
steps,
|
| 208 |
+
ghidra_addr,
|
| 209 |
+
runtime_addr,
|
| 210 |
+
data,
|
| 211 |
+
pointers,
|
| 212 |
+
} => match format {
|
| 213 |
+
Format::Hex => {
|
| 214 |
+
println!("Pointer chain ({} steps):", steps.len());
|
| 215 |
+
for (i, step) in steps.iter().enumerate() {
|
| 216 |
+
println!(
|
| 217 |
+
" [{}] *(ghidra:0x{:08X}) = 0x{:08X} + 0x{:X} = 0x{:08X}",
|
| 218 |
+
i, step.deref_addr, step.value, step.offset, step.result_addr
|
| 219 |
+
);
|
|
|
|
|
|
|
|
|
|
| 220 |
}
|
| 221 |
+
println!();
|
| 222 |
+
print_hex_read(*ghidra_addr, *runtime_addr, data, pointers);
|
| 223 |
}
|
| 224 |
+
Format::Raw => {
|
| 225 |
+
let stdout = io::stdout();
|
| 226 |
+
let mut out = stdout.lock();
|
| 227 |
+
let _ = out.write_all(data);
|
| 228 |
+
}
|
| 229 |
+
},
|
| 230 |
Response::Suspended { frame } => println!("Suspended at frame {frame}"),
|
| 231 |
Response::Resumed => println!("Resumed"),
|
| 232 |
+
Response::FrameInfo {
|
| 233 |
+
frame,
|
| 234 |
+
paused,
|
| 235 |
+
breakpoint,
|
| 236 |
+
} => {
|
| 237 |
let state = if *paused { "PAUSED" } else { "running" };
|
| 238 |
print!("Frame {frame} [{state}]");
|
| 239 |
if *breakpoint >= 0 {
|
|
|
|
| 252 |
println!("=== Snapshot at frame {frame} ===\n");
|
| 253 |
print!("{text}");
|
| 254 |
}
|
| 255 |
+
Response::InspectResult {
|
| 256 |
+
class_name,
|
| 257 |
+
ghidra_addr,
|
| 258 |
+
runtime_addr,
|
| 259 |
+
fields,
|
| 260 |
+
} => {
|
| 261 |
println!(
|
| 262 |
"{} at ghidra:0x{:08X} (runtime:0x{:08X})\n",
|
| 263 |
class_name, ghidra_addr, runtime_addr
|
|
|
|
| 283 |
}
|
| 284 |
}
|
| 285 |
Response::AliasResult(alias) => {
|
| 286 |
+
println!(
|
| 287 |
+
"{} at runtime:0x{:08X}",
|
| 288 |
+
alias.class_name, alias.runtime_addr
|
| 289 |
+
);
|
| 290 |
}
|
| 291 |
Response::FieldResult(field) => {
|
| 292 |
println!("offset:0x{:X} size:{}", field.offset, field.size);
|
|
|
|
| 301 |
fn print_hex_read(ghidra_addr: u32, runtime_addr: u32, data: &[u8], pointers: &[PointerInfo]) {
|
| 302 |
println!(
|
| 303 |
"Reading 0x{:X} bytes at ghidra:0x{:08X} (runtime:0x{:08X})\n",
|
| 304 |
+
data.len(),
|
| 305 |
+
ghidra_addr,
|
| 306 |
+
runtime_addr
|
| 307 |
);
|
| 308 |
|
| 309 |
// Hex dump with ASCII sidebar
|
|
|
|
| 314 |
// Hex bytes
|
| 315 |
for (i, byte) in chunk.iter().enumerate() {
|
| 316 |
print!("{:02X} ", byte);
|
| 317 |
+
if i == 7 {
|
| 318 |
+
print!(" ");
|
| 319 |
+
}
|
| 320 |
}
|
| 321 |
// Padding for short last line
|
| 322 |
if chunk.len() < 16 {
|
| 323 |
for i in chunk.len()..16 {
|
| 324 |
print!(" ");
|
| 325 |
+
if i == 7 {
|
| 326 |
+
print!(" ");
|
| 327 |
+
}
|
| 328 |
}
|
| 329 |
}
|
| 330 |
// ASCII sidebar
|
|
|
|
| 350 |
PointerKind::Object => "OBJECT",
|
| 351 |
PointerKind::Heap => "HEAP",
|
| 352 |
};
|
| 353 |
+
let detail_str = p
|
| 354 |
+
.detail
|
| 355 |
+
.as_deref()
|
| 356 |
+
.map(|d| format!(" {d}"))
|
| 357 |
+
.unwrap_or_default();
|
| 358 |
println!(
|
| 359 |
" +0x{:03X} 0x{:08X} {:<7} ghidra:0x{:08X}{}",
|
| 360 |
p.offset, p.raw_value, kind_str, p.ghidra_value, detail_str
|
|
|
|
| 374 |
/// Simple address (with optional offset already applied)
|
| 375 |
Simple { addr: u32, absolute: bool },
|
| 376 |
/// Pointer chain: start address + list of deref offsets
|
| 377 |
+
Chain {
|
| 378 |
+
addr: u32,
|
| 379 |
+
chain: Vec<u32>,
|
| 380 |
+
absolute: bool,
|
| 381 |
+
},
|
| 382 |
}
|
| 383 |
|
| 384 |
/// Split a string on '+' or '[' into (base, offset_str). Returns (s, None) if no compound.
|
|
|
|
| 404 |
|
| 405 |
// Try as field name
|
| 406 |
if let Some(cls) = class {
|
| 407 |
+
match send_request(
|
| 408 |
+
port,
|
| 409 |
+
&Request::ResolveField {
|
| 410 |
+
class_name: cls.clone(),
|
| 411 |
+
field_name: s.to_string(),
|
| 412 |
+
},
|
| 413 |
+
) {
|
| 414 |
Ok(Response::FieldResult(f)) => return f.offset,
|
| 415 |
_ => {}
|
| 416 |
}
|
|
|
|
| 421 |
}
|
| 422 |
|
| 423 |
fn parse_hex(s: &str) -> u32 {
|
| 424 |
+
let s = s
|
| 425 |
+
.strip_prefix("0x")
|
| 426 |
+
.or_else(|| s.strip_prefix("0X"))
|
| 427 |
+
.unwrap_or(s);
|
| 428 |
u32::from_str_radix(s, 16).unwrap_or_else(|_| {
|
| 429 |
eprintln!("Error: invalid hex address '{s}'");
|
| 430 |
process::exit(1);
|
|
|
|
| 488 |
let (base_addr, base_absolute, mut current_class) = resolve_base(base_part, absolute, port);
|
| 489 |
|
| 490 |
if parts.len() == 1 {
|
| 491 |
+
return AddressExpr::Simple {
|
| 492 |
+
addr: base_addr,
|
| 493 |
+
absolute: base_absolute,
|
| 494 |
+
};
|
| 495 |
}
|
| 496 |
|
| 497 |
// Resolve each chain segment, tracking whether it's a field name or hex.
|
|
|
|
| 526 |
}
|
| 527 |
}
|
| 528 |
|
| 529 |
+
AddressExpr::Chain {
|
| 530 |
+
addr: final_base,
|
| 531 |
+
chain,
|
| 532 |
+
absolute: base_absolute,
|
| 533 |
+
}
|
| 534 |
}
|
| 535 |
|
| 536 |
/// Resolve the base address part β could be a hex literal, a named alias, or alias+offset.
|
|
|
|
| 544 |
}
|
| 545 |
|
| 546 |
// Try resolving as a named alias
|
| 547 |
+
match send_request(
|
| 548 |
+
port,
|
| 549 |
+
&Request::ResolveAlias {
|
| 550 |
+
name: base_str.to_string(),
|
| 551 |
+
},
|
| 552 |
+
) {
|
| 553 |
Ok(Response::AliasResult(alias)) => {
|
| 554 |
// Resolved! Use runtime address (absolute)
|
| 555 |
let class = Some(alias.class_name);
|
| 556 |
+
let extra = offset_str
|
| 557 |
+
.map(|o| resolve_offset(o, &class, port))
|
| 558 |
+
.unwrap_or(0);
|
| 559 |
(alias.runtime_addr.wrapping_add(extra), true, class)
|
| 560 |
}
|
| 561 |
_ => {
|
|
|
|
| 569 |
/// Resolve a chain segment, returning whether it was a field name or hex offset.
|
| 570 |
fn resolve_chain_segment_typed(s: &str, current_class: &Option<String>, port: u16) -> ChainSegment {
|
| 571 |
let (base_str, offset_str) = split_compound(s);
|
| 572 |
+
let extra = offset_str
|
| 573 |
+
.map(|o| resolve_offset(o, current_class, port))
|
| 574 |
+
.unwrap_or(0);
|
| 575 |
|
| 576 |
// If it looks like hex, parse directly β standard chain semantics
|
| 577 |
if looks_like_hex(base_str) {
|
|
|
|
| 580 |
|
| 581 |
// Try resolving as a field name if we know the current class
|
| 582 |
if let Some(class) = current_class {
|
| 583 |
+
match send_request(
|
| 584 |
+
port,
|
| 585 |
+
&Request::ResolveField {
|
| 586 |
+
class_name: class.clone(),
|
| 587 |
+
field_name: base_str.to_string(),
|
| 588 |
+
},
|
| 589 |
+
) {
|
| 590 |
Ok(Response::FieldResult(f)) => {
|
| 591 |
return ChainSegment::FieldOffset(f.offset.wrapping_add(extra));
|
| 592 |
}
|
|
|
|
| 600 |
|
| 601 |
/// Check if a string looks like a hex number (starts with 0x or a digit).
|
| 602 |
fn looks_like_hex(s: &str) -> bool {
|
| 603 |
+
s.starts_with("0x")
|
| 604 |
+
|| s.starts_with("0X")
|
| 605 |
+
|| s.chars().next().map_or(false, |c| c.is_ascii_digit())
|
| 606 |
}
|
| 607 |
|
| 608 |
fn print_usage() {
|
crates/openwa-debug-proto/src/lib.rs
CHANGED
|
@@ -65,30 +65,54 @@ pub struct ResolvedField {
|
|
| 65 |
pub enum Request {
|
| 66 |
Ping,
|
| 67 |
Help,
|
| 68 |
-
Read {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
/// Walk a pointer chain: start at `addr`, then for each offset in `chain`,
|
| 70 |
/// deref the current DWORD and add the offset. Read `len` bytes at the end.
|
| 71 |
-
ReadChain {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
/// Pause the game at the next frame boundary.
|
| 73 |
Suspend,
|
| 74 |
/// Resume the game.
|
| 75 |
Resume,
|
| 76 |
/// Advance `count` frames, then pause.
|
| 77 |
-
Step {
|
|
|
|
|
|
|
| 78 |
/// Query current frame number and pause state.
|
| 79 |
Frame,
|
| 80 |
/// Set a frame breakpoint (-1 to clear).
|
| 81 |
-
Break {
|
|
|
|
|
|
|
| 82 |
/// Capture a canonicalized game state snapshot.
|
| 83 |
Snapshot,
|
| 84 |
/// Typed struct inspection: read all named fields at an address.
|
| 85 |
-
Inspect {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
/// List all tracked live objects.
|
| 87 |
ListObjects,
|
| 88 |
/// Resolve a named alias (e.g., "ddgame") to a runtime address.
|
| 89 |
-
ResolveAlias {
|
|
|
|
|
|
|
| 90 |
/// Resolve a field name to its offset within a struct.
|
| 91 |
-
ResolveField {
|
|
|
|
|
|
|
|
|
|
| 92 |
}
|
| 93 |
|
| 94 |
/// One step in a resolved pointer chain, for display.
|
|
@@ -114,7 +138,9 @@ pub struct CommandHelp {
|
|
| 114 |
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 115 |
pub enum Response {
|
| 116 |
Pong,
|
| 117 |
-
Help {
|
|
|
|
|
|
|
| 118 |
ReadResult {
|
| 119 |
ghidra_addr: u32,
|
| 120 |
runtime_addr: u32,
|
|
@@ -134,15 +160,26 @@ pub enum Response {
|
|
| 134 |
pointers: Vec<PointerInfo>,
|
| 135 |
},
|
| 136 |
/// Game is now suspended at this frame.
|
| 137 |
-
Suspended {
|
|
|
|
|
|
|
| 138 |
/// Game resumed.
|
| 139 |
Resumed,
|
| 140 |
/// Current frame info.
|
| 141 |
-
FrameInfo {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
/// Breakpoint set/cleared.
|
| 143 |
-
BreakSet {
|
|
|
|
|
|
|
| 144 |
/// Game state snapshot.
|
| 145 |
-
Snapshot {
|
|
|
|
|
|
|
|
|
|
| 146 |
/// Typed struct inspection result.
|
| 147 |
InspectResult {
|
| 148 |
class_name: String,
|
|
@@ -151,12 +188,16 @@ pub enum Response {
|
|
| 151 |
fields: Vec<FieldValue>,
|
| 152 |
},
|
| 153 |
/// List of tracked live objects.
|
| 154 |
-
ObjectList {
|
|
|
|
|
|
|
| 155 |
/// Resolved named alias.
|
| 156 |
AliasResult(ResolvedAlias),
|
| 157 |
/// Resolved field offset.
|
| 158 |
FieldResult(ResolvedField),
|
| 159 |
-
Error {
|
|
|
|
|
|
|
| 160 |
}
|
| 161 |
|
| 162 |
// --- Length-prefixed framing ---
|
|
@@ -188,6 +229,5 @@ pub fn read_frame<R: Read, T: for<'de> Deserialize<'de>>(reader: &mut R) -> io::
|
|
| 188 |
}
|
| 189 |
let mut payload = vec![0u8; len];
|
| 190 |
reader.read_exact(&mut payload)?;
|
| 191 |
-
rmp_serde::from_slice(&payload)
|
| 192 |
-
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
|
| 193 |
}
|
|
|
|
| 65 |
pub enum Request {
|
| 66 |
Ping,
|
| 67 |
Help,
|
| 68 |
+
Read {
|
| 69 |
+
addr: u32,
|
| 70 |
+
len: u32,
|
| 71 |
+
#[serde(default)]
|
| 72 |
+
absolute: bool,
|
| 73 |
+
},
|
| 74 |
/// Walk a pointer chain: start at `addr`, then for each offset in `chain`,
|
| 75 |
/// deref the current DWORD and add the offset. Read `len` bytes at the end.
|
| 76 |
+
ReadChain {
|
| 77 |
+
addr: u32,
|
| 78 |
+
chain: Vec<u32>,
|
| 79 |
+
len: u32,
|
| 80 |
+
absolute: bool,
|
| 81 |
+
},
|
| 82 |
/// Pause the game at the next frame boundary.
|
| 83 |
Suspend,
|
| 84 |
/// Resume the game.
|
| 85 |
Resume,
|
| 86 |
/// Advance `count` frames, then pause.
|
| 87 |
+
Step {
|
| 88 |
+
count: i32,
|
| 89 |
+
},
|
| 90 |
/// Query current frame number and pause state.
|
| 91 |
Frame,
|
| 92 |
/// Set a frame breakpoint (-1 to clear).
|
| 93 |
+
Break {
|
| 94 |
+
frame: i32,
|
| 95 |
+
},
|
| 96 |
/// Capture a canonicalized game state snapshot.
|
| 97 |
Snapshot,
|
| 98 |
/// Typed struct inspection: read all named fields at an address.
|
| 99 |
+
Inspect {
|
| 100 |
+
class_name: String,
|
| 101 |
+
addr: u32,
|
| 102 |
+
chain: Vec<u32>,
|
| 103 |
+
absolute: bool,
|
| 104 |
+
},
|
| 105 |
/// List all tracked live objects.
|
| 106 |
ListObjects,
|
| 107 |
/// Resolve a named alias (e.g., "ddgame") to a runtime address.
|
| 108 |
+
ResolveAlias {
|
| 109 |
+
name: String,
|
| 110 |
+
},
|
| 111 |
/// Resolve a field name to its offset within a struct.
|
| 112 |
+
ResolveField {
|
| 113 |
+
class_name: String,
|
| 114 |
+
field_name: String,
|
| 115 |
+
},
|
| 116 |
}
|
| 117 |
|
| 118 |
/// One step in a resolved pointer chain, for display.
|
|
|
|
| 138 |
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 139 |
pub enum Response {
|
| 140 |
Pong,
|
| 141 |
+
Help {
|
| 142 |
+
commands: Vec<CommandHelp>,
|
| 143 |
+
},
|
| 144 |
ReadResult {
|
| 145 |
ghidra_addr: u32,
|
| 146 |
runtime_addr: u32,
|
|
|
|
| 160 |
pointers: Vec<PointerInfo>,
|
| 161 |
},
|
| 162 |
/// Game is now suspended at this frame.
|
| 163 |
+
Suspended {
|
| 164 |
+
frame: i32,
|
| 165 |
+
},
|
| 166 |
/// Game resumed.
|
| 167 |
Resumed,
|
| 168 |
/// Current frame info.
|
| 169 |
+
FrameInfo {
|
| 170 |
+
frame: i32,
|
| 171 |
+
paused: bool,
|
| 172 |
+
breakpoint: i32,
|
| 173 |
+
},
|
| 174 |
/// Breakpoint set/cleared.
|
| 175 |
+
BreakSet {
|
| 176 |
+
frame: i32,
|
| 177 |
+
},
|
| 178 |
/// Game state snapshot.
|
| 179 |
+
Snapshot {
|
| 180 |
+
frame: i32,
|
| 181 |
+
text: String,
|
| 182 |
+
},
|
| 183 |
/// Typed struct inspection result.
|
| 184 |
InspectResult {
|
| 185 |
class_name: String,
|
|
|
|
| 188 |
fields: Vec<FieldValue>,
|
| 189 |
},
|
| 190 |
/// List of tracked live objects.
|
| 191 |
+
ObjectList {
|
| 192 |
+
objects: Vec<LiveObjectInfo>,
|
| 193 |
+
},
|
| 194 |
/// Resolved named alias.
|
| 195 |
AliasResult(ResolvedAlias),
|
| 196 |
/// Resolved field offset.
|
| 197 |
FieldResult(ResolvedField),
|
| 198 |
+
Error {
|
| 199 |
+
message: String,
|
| 200 |
+
},
|
| 201 |
}
|
| 202 |
|
| 203 |
// --- Length-prefixed framing ---
|
|
|
|
| 229 |
}
|
| 230 |
let mut payload = vec![0u8; len];
|
| 231 |
reader.read_exact(&mut payload)?;
|
| 232 |
+
rmp_serde::from_slice(&payload).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
|
|
|
|
| 233 |
}
|
crates/openwa-debugui/src/app.rs
CHANGED
|
@@ -408,8 +408,7 @@ unsafe fn show_game_task_raw_fields(
|
|
| 408 |
// Pointer identification via registry
|
| 409 |
use openwa_core::mem;
|
| 410 |
let ptr_label = if val >= 0x10000 {
|
| 411 |
-
mem::identify_pointer(val, delta)
|
| 412 |
-
.and_then(|id| id.name)
|
| 413 |
} else {
|
| 414 |
None
|
| 415 |
};
|
|
@@ -569,23 +568,17 @@ impl DebugApp {
|
|
| 569 |
|
| 570 |
// --- CTaskMine-specific fields ---
|
| 571 |
if name == "CTaskMine" {
|
| 572 |
-
show_game_task_raw_fields(
|
| 573 |
-
ui, addr, "CTaskMine", 0x128,
|
| 574 |
-
);
|
| 575 |
}
|
| 576 |
|
| 577 |
// --- CTaskOilDrum-specific fields ---
|
| 578 |
if name == "CTaskOilDrum" {
|
| 579 |
-
show_game_task_raw_fields(
|
| 580 |
-
ui, addr, "CTaskOilDrum", 0x110,
|
| 581 |
-
);
|
| 582 |
}
|
| 583 |
|
| 584 |
// --- CTaskCrate-specific fields ---
|
| 585 |
if name == "CTaskCrate" {
|
| 586 |
-
show_game_task_raw_fields(
|
| 587 |
-
ui, addr, "CTaskCrate", 0x4B0,
|
| 588 |
-
);
|
| 589 |
}
|
| 590 |
|
| 591 |
// --- CTaskCloud-specific fields ---
|
|
@@ -803,9 +796,7 @@ impl DebugApp {
|
|
| 803 |
));
|
| 804 |
ui.separator();
|
| 805 |
|
| 806 |
-
show_game_task_raw_fields(
|
| 807 |
-
ui, addr, "CTaskWorm", 0x3FC,
|
| 808 |
-
);
|
| 809 |
}
|
| 810 |
|
| 811 |
// --- CTaskMissile-specific fields ---
|
|
@@ -821,9 +812,7 @@ impl DebugApp {
|
|
| 821 |
));
|
| 822 |
ui.separator();
|
| 823 |
|
| 824 |
-
show_game_task_raw_fields(
|
| 825 |
-
ui, addr, "CTaskMissile", 0x41C,
|
| 826 |
-
);
|
| 827 |
}
|
| 828 |
}
|
| 829 |
});
|
|
|
|
| 408 |
// Pointer identification via registry
|
| 409 |
use openwa_core::mem;
|
| 410 |
let ptr_label = if val >= 0x10000 {
|
| 411 |
+
mem::identify_pointer(val, delta).and_then(|id| id.name)
|
|
|
|
| 412 |
} else {
|
| 413 |
None
|
| 414 |
};
|
|
|
|
| 568 |
|
| 569 |
// --- CTaskMine-specific fields ---
|
| 570 |
if name == "CTaskMine" {
|
| 571 |
+
show_game_task_raw_fields(ui, addr, "CTaskMine", 0x128);
|
|
|
|
|
|
|
| 572 |
}
|
| 573 |
|
| 574 |
// --- CTaskOilDrum-specific fields ---
|
| 575 |
if name == "CTaskOilDrum" {
|
| 576 |
+
show_game_task_raw_fields(ui, addr, "CTaskOilDrum", 0x110);
|
|
|
|
|
|
|
| 577 |
}
|
| 578 |
|
| 579 |
// --- CTaskCrate-specific fields ---
|
| 580 |
if name == "CTaskCrate" {
|
| 581 |
+
show_game_task_raw_fields(ui, addr, "CTaskCrate", 0x4B0);
|
|
|
|
|
|
|
| 582 |
}
|
| 583 |
|
| 584 |
// --- CTaskCloud-specific fields ---
|
|
|
|
| 796 |
));
|
| 797 |
ui.separator();
|
| 798 |
|
| 799 |
+
show_game_task_raw_fields(ui, addr, "CTaskWorm", 0x3FC);
|
|
|
|
|
|
|
| 800 |
}
|
| 801 |
|
| 802 |
// --- CTaskMissile-specific fields ---
|
|
|
|
| 812 |
));
|
| 813 |
ui.separator();
|
| 814 |
|
| 815 |
+
show_game_task_raw_fields(ui, addr, "CTaskMissile", 0x41C);
|
|
|
|
|
|
|
| 816 |
}
|
| 817 |
}
|
| 818 |
});
|
crates/openwa-derive/src/lib.rs
CHANGED
|
@@ -110,9 +110,10 @@ fn vtable_impl(
|
|
| 110 |
let mut has_implicit_slot = false;
|
| 111 |
|
| 112 |
for (field_idx, field) in fields.iter().enumerate() {
|
| 113 |
-
let ident = field
|
| 114 |
-
|
| 115 |
-
|
|
|
|
| 116 |
|
| 117 |
let explicit_index = parse_slot_attr(&field.attrs, ident)?;
|
| 118 |
|
|
@@ -513,9 +514,9 @@ fn parse_vtable_attr_args(
|
|
| 513 |
|
| 514 |
syn::parse::Parser::parse2(parser, attr)?;
|
| 515 |
|
| 516 |
-
let size = args
|
| 517 |
-
|
| 518 |
-
|
| 519 |
|
| 520 |
Ok((size, args.va, args.class))
|
| 521 |
}
|
|
@@ -586,9 +587,12 @@ pub fn derive_field_registry(input: TokenStream) -> TokenStream {
|
|
| 586 |
}
|
| 587 |
},
|
| 588 |
_ => {
|
| 589 |
-
return syn::Error::new_spanned(
|
| 590 |
-
|
| 591 |
-
|
|
|
|
|
|
|
|
|
|
| 592 |
}
|
| 593 |
};
|
| 594 |
|
|
@@ -616,8 +620,8 @@ pub fn derive_field_registry(input: TokenStream) -> TokenStream {
|
|
| 616 |
let size_ty = substitute_generics(field_ty, &generic_defaults);
|
| 617 |
|
| 618 |
// Determine ValueKind: check for #[field(kind = "...")] override, else infer
|
| 619 |
-
let kind =
|
| 620 |
-
.unwrap_or_else(|| infer_value_kind(field_ty));
|
| 621 |
|
| 622 |
entries.push(quote! {
|
| 623 |
openwa_core::registry::FieldEntry {
|
|
|
|
| 110 |
let mut has_implicit_slot = false;
|
| 111 |
|
| 112 |
for (field_idx, field) in fields.iter().enumerate() {
|
| 113 |
+
let ident = field
|
| 114 |
+
.ident
|
| 115 |
+
.as_ref()
|
| 116 |
+
.ok_or_else(|| syn::Error::new_spanned(field, "Vtable fields must be named"))?;
|
| 117 |
|
| 118 |
let explicit_index = parse_slot_attr(&field.attrs, ident)?;
|
| 119 |
|
|
|
|
| 514 |
|
| 515 |
syn::parse::Parser::parse2(parser, attr)?;
|
| 516 |
|
| 517 |
+
let size = args
|
| 518 |
+
.size
|
| 519 |
+
.ok_or_else(|| syn::Error::new_spanned(span, "vtable attribute requires `size = N`"))?;
|
| 520 |
|
| 521 |
Ok((size, args.va, args.class))
|
| 522 |
}
|
|
|
|
| 587 |
}
|
| 588 |
},
|
| 589 |
_ => {
|
| 590 |
+
return syn::Error::new_spanned(
|
| 591 |
+
struct_name,
|
| 592 |
+
"FieldRegistry can only be used on structs",
|
| 593 |
+
)
|
| 594 |
+
.to_compile_error()
|
| 595 |
+
.into();
|
| 596 |
}
|
| 597 |
};
|
| 598 |
|
|
|
|
| 620 |
let size_ty = substitute_generics(field_ty, &generic_defaults);
|
| 621 |
|
| 622 |
// Determine ValueKind: check for #[field(kind = "...")] override, else infer
|
| 623 |
+
let kind =
|
| 624 |
+
parse_field_kind_attr(&field.attrs).unwrap_or_else(|| infer_value_kind(field_ty));
|
| 625 |
|
| 626 |
entries.push(quote! {
|
| 627 |
openwa_core::registry::FieldEntry {
|
crates/openwa-test-runner/src/main.rs
CHANGED
|
@@ -194,7 +194,14 @@ fn find_launcher() -> Option<PathBuf> {
|
|
| 194 |
fn build() -> Result<Duration, String> {
|
| 195 |
let start = Instant::now();
|
| 196 |
let status = Command::new("cargo")
|
| 197 |
-
.args([
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
.status()
|
| 199 |
.map_err(|e| format!("Failed to run cargo: {e}"))?;
|
| 200 |
|
|
@@ -227,7 +234,6 @@ fn run_test(test: &TestCase, launcher: &Path, wa_exe: &Path, run_dir: &Path) ->
|
|
| 227 |
.creation_flags(CREATE_NO_WINDOW)
|
| 228 |
.status();
|
| 229 |
|
| 230 |
-
|
| 231 |
let duration = start.elapsed();
|
| 232 |
|
| 233 |
match result {
|
|
@@ -262,8 +268,10 @@ fn run_test(test: &TestCase, launcher: &Path, wa_exe: &Path, run_dir: &Path) ->
|
|
| 262 |
passed: false,
|
| 263 |
duration,
|
| 264 |
diff_lines: Vec::new(),
|
| 265 |
-
error: Some(format!(
|
| 266 |
-
|
|
|
|
|
|
|
| 267 |
};
|
| 268 |
}
|
| 269 |
|
|
@@ -286,7 +294,8 @@ fn run_test(test: &TestCase, launcher: &Path, wa_exe: &Path, run_dir: &Path) ->
|
|
| 286 |
if diff.is_empty() {
|
| 287 |
diff.push(format!(
|
| 288 |
" (byte-level difference: expected {} bytes, actual {} bytes)",
|
| 289 |
-
expected.len(),
|
|
|
|
| 290 |
));
|
| 291 |
}
|
| 292 |
let _ = fs::remove_file(&test.output_log);
|
|
@@ -407,7 +416,11 @@ fn run_tests_parallel(
|
|
| 407 |
// βββ Output βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 408 |
|
| 409 |
fn print_result(result: &TestResult) {
|
| 410 |
-
let status = if result.passed {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
let secs = result.duration.as_secs_f64();
|
| 412 |
println!("{status} {:<28} ({secs:.1}s)", result.name);
|
| 413 |
|
|
@@ -443,7 +456,12 @@ fn write_summary(results: &[TestResult], wall_time: Duration, path: &Path) {
|
|
| 443 |
let mut s = String::new();
|
| 444 |
for r in results {
|
| 445 |
let status = if r.passed { "PASS" } else { "FAIL" };
|
| 446 |
-
let _ = writeln!(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 447 |
if let Some(err) = &r.error {
|
| 448 |
let _ = writeln!(s, " {err}");
|
| 449 |
}
|
|
@@ -453,8 +471,12 @@ fn write_summary(results: &[TestResult], wall_time: Duration, path: &Path) {
|
|
| 453 |
}
|
| 454 |
let passed = results.iter().filter(|r| r.passed).count();
|
| 455 |
let failed = results.len() - passed;
|
| 456 |
-
let _ = writeln!(
|
| 457 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
|
| 459 |
let _ = fs::write(path, s);
|
| 460 |
}
|
|
@@ -469,10 +491,7 @@ fn cleanup_temp_files(wa_exe: &Path) {
|
|
| 469 |
None => return,
|
| 470 |
};
|
| 471 |
|
| 472 |
-
let patterns = [
|
| 473 |
-
(game_dir, "mono_", ".tmp"),
|
| 474 |
-
(game_dir, "cur_", ".thm"),
|
| 475 |
-
];
|
| 476 |
let data_dir = game_dir.join("DATA");
|
| 477 |
let data_patterns = [
|
| 478 |
("land_", ".dat"),
|
|
|
|
| 194 |
fn build() -> Result<Duration, String> {
|
| 195 |
let start = Instant::now();
|
| 196 |
let status = Command::new("cargo")
|
| 197 |
+
.args([
|
| 198 |
+
"build",
|
| 199 |
+
"--release",
|
| 200 |
+
"-p",
|
| 201 |
+
"openwa-wormkit",
|
| 202 |
+
"-p",
|
| 203 |
+
"openwa-launcher",
|
| 204 |
+
])
|
| 205 |
.status()
|
| 206 |
.map_err(|e| format!("Failed to run cargo: {e}"))?;
|
| 207 |
|
|
|
|
| 234 |
.creation_flags(CREATE_NO_WINDOW)
|
| 235 |
.status();
|
| 236 |
|
|
|
|
| 237 |
let duration = start.elapsed();
|
| 238 |
|
| 239 |
match result {
|
|
|
|
| 268 |
passed: false,
|
| 269 |
duration,
|
| 270 |
diff_lines: Vec::new(),
|
| 271 |
+
error: Some(format!(
|
| 272 |
+
"No output log generated (exit code: {})",
|
| 273 |
+
status.code().unwrap_or(-1)
|
| 274 |
+
)),
|
| 275 |
};
|
| 276 |
}
|
| 277 |
|
|
|
|
| 294 |
if diff.is_empty() {
|
| 295 |
diff.push(format!(
|
| 296 |
" (byte-level difference: expected {} bytes, actual {} bytes)",
|
| 297 |
+
expected.len(),
|
| 298 |
+
actual.len()
|
| 299 |
));
|
| 300 |
}
|
| 301 |
let _ = fs::remove_file(&test.output_log);
|
|
|
|
| 416 |
// βββ Output βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 417 |
|
| 418 |
fn print_result(result: &TestResult) {
|
| 419 |
+
let status = if result.passed {
|
| 420 |
+
"\x1b[32m PASS\x1b[0m"
|
| 421 |
+
} else {
|
| 422 |
+
"\x1b[31m FAIL\x1b[0m"
|
| 423 |
+
};
|
| 424 |
let secs = result.duration.as_secs_f64();
|
| 425 |
println!("{status} {:<28} ({secs:.1}s)", result.name);
|
| 426 |
|
|
|
|
| 456 |
let mut s = String::new();
|
| 457 |
for r in results {
|
| 458 |
let status = if r.passed { "PASS" } else { "FAIL" };
|
| 459 |
+
let _ = writeln!(
|
| 460 |
+
s,
|
| 461 |
+
"{status} {:<28} ({:.1}s)",
|
| 462 |
+
r.name,
|
| 463 |
+
r.duration.as_secs_f64()
|
| 464 |
+
);
|
| 465 |
if let Some(err) = &r.error {
|
| 466 |
let _ = writeln!(s, " {err}");
|
| 467 |
}
|
|
|
|
| 471 |
}
|
| 472 |
let passed = results.iter().filter(|r| r.passed).count();
|
| 473 |
let failed = results.len() - passed;
|
| 474 |
+
let _ = writeln!(
|
| 475 |
+
s,
|
| 476 |
+
"\n{} tests: {passed} passed, {failed} failed (wall {:.1}s)",
|
| 477 |
+
results.len(),
|
| 478 |
+
wall_time.as_secs_f64()
|
| 479 |
+
);
|
| 480 |
|
| 481 |
let _ = fs::write(path, s);
|
| 482 |
}
|
|
|
|
| 491 |
None => return,
|
| 492 |
};
|
| 493 |
|
| 494 |
+
let patterns = [(game_dir, "mono_", ".tmp"), (game_dir, "cur_", ".thm")];
|
|
|
|
|
|
|
|
|
|
| 495 |
let data_dir = game_dir.join("DATA");
|
| 496 |
let data_patterns = [
|
| 497 |
("land_", ".dat"),
|
crates/openwa-wormkit/src/debug_server.rs
CHANGED
|
@@ -110,11 +110,22 @@ fn handle_request(request: Request) -> Response {
|
|
| 110 |
},
|
| 111 |
],
|
| 112 |
},
|
| 113 |
-
Request::Read {
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
Request::Suspend => {
|
| 116 |
crate::debug_sync::suspend();
|
| 117 |
-
Response::Suspended {
|
|
|
|
|
|
|
| 118 |
}
|
| 119 |
Request::Resume => {
|
| 120 |
crate::debug_sync::resume();
|
|
@@ -124,7 +135,9 @@ fn handle_request(request: Request) -> Response {
|
|
| 124 |
crate::debug_sync::step(count);
|
| 125 |
// Wait briefly for the step to complete, then report
|
| 126 |
std::thread::sleep(Duration::from_millis(100));
|
| 127 |
-
Response::Suspended {
|
|
|
|
|
|
|
| 128 |
}
|
| 129 |
Request::Frame => Response::FrameInfo {
|
| 130 |
frame: crate::debug_sync::current_frame(),
|
|
@@ -140,14 +153,18 @@ fn handle_request(request: Request) -> Response {
|
|
| 140 |
let frame = crate::debug_sync::current_frame();
|
| 141 |
Response::Snapshot { frame, text }
|
| 142 |
}
|
| 143 |
-
Request::Inspect {
|
| 144 |
-
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
| 146 |
Request::ListObjects => handle_list_objects(),
|
| 147 |
Request::ResolveAlias { name } => handle_resolve_alias(&name),
|
| 148 |
-
Request::ResolveField {
|
| 149 |
-
|
| 150 |
-
|
|
|
|
| 151 |
}
|
| 152 |
}
|
| 153 |
|
|
@@ -189,11 +206,7 @@ fn handle_read(addr: u32, len: u32, absolute: bool) -> Response {
|
|
| 189 |
|
| 190 |
// Copy the memory region
|
| 191 |
let mut data = vec![0u8; len as usize];
|
| 192 |
-
std::ptr::copy_nonoverlapping(
|
| 193 |
-
runtime_addr as *const u8,
|
| 194 |
-
data.as_mut_ptr(),
|
| 195 |
-
len as usize,
|
| 196 |
-
);
|
| 197 |
|
| 198 |
// Classify pointers in the copied data
|
| 199 |
let pointers = mem::classify_region(&data, 0, delta);
|
|
@@ -230,10 +243,7 @@ fn handle_inspect(class_name: &str, addr: u32, chain: &[u32], absolute: bool) ->
|
|
| 230 |
unsafe {
|
| 231 |
if !mem::can_read(current, 4) {
|
| 232 |
return Response::Error {
|
| 233 |
-
message: format!(
|
| 234 |
-
"Chain broke: cannot read at runtime:0x{:08X}",
|
| 235 |
-
current
|
| 236 |
-
),
|
| 237 |
};
|
| 238 |
}
|
| 239 |
let value = *(current as *const u32);
|
|
@@ -370,8 +380,12 @@ fn handle_resolve_field(class_name: &str, field_name: &str) -> Response {
|
|
| 370 |
}
|
| 371 |
|
| 372 |
Response::Error {
|
| 373 |
-
message: format!(
|
| 374 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 375 |
}
|
| 376 |
}
|
| 377 |
|
|
|
|
| 110 |
},
|
| 111 |
],
|
| 112 |
},
|
| 113 |
+
Request::Read {
|
| 114 |
+
addr,
|
| 115 |
+
len,
|
| 116 |
+
absolute,
|
| 117 |
+
} => handle_read(addr, len, absolute),
|
| 118 |
+
Request::ReadChain {
|
| 119 |
+
addr,
|
| 120 |
+
chain,
|
| 121 |
+
len,
|
| 122 |
+
absolute,
|
| 123 |
+
} => handle_read_chain(addr, &chain, len, absolute),
|
| 124 |
Request::Suspend => {
|
| 125 |
crate::debug_sync::suspend();
|
| 126 |
+
Response::Suspended {
|
| 127 |
+
frame: crate::debug_sync::current_frame(),
|
| 128 |
+
}
|
| 129 |
}
|
| 130 |
Request::Resume => {
|
| 131 |
crate::debug_sync::resume();
|
|
|
|
| 135 |
crate::debug_sync::step(count);
|
| 136 |
// Wait briefly for the step to complete, then report
|
| 137 |
std::thread::sleep(Duration::from_millis(100));
|
| 138 |
+
Response::Suspended {
|
| 139 |
+
frame: crate::debug_sync::current_frame(),
|
| 140 |
+
}
|
| 141 |
}
|
| 142 |
Request::Frame => Response::FrameInfo {
|
| 143 |
frame: crate::debug_sync::current_frame(),
|
|
|
|
| 153 |
let frame = crate::debug_sync::current_frame();
|
| 154 |
Response::Snapshot { frame, text }
|
| 155 |
}
|
| 156 |
+
Request::Inspect {
|
| 157 |
+
class_name,
|
| 158 |
+
addr,
|
| 159 |
+
chain,
|
| 160 |
+
absolute,
|
| 161 |
+
} => handle_inspect(&class_name, addr, &chain, absolute),
|
| 162 |
Request::ListObjects => handle_list_objects(),
|
| 163 |
Request::ResolveAlias { name } => handle_resolve_alias(&name),
|
| 164 |
+
Request::ResolveField {
|
| 165 |
+
class_name,
|
| 166 |
+
field_name,
|
| 167 |
+
} => handle_resolve_field(&class_name, &field_name),
|
| 168 |
}
|
| 169 |
}
|
| 170 |
|
|
|
|
| 206 |
|
| 207 |
// Copy the memory region
|
| 208 |
let mut data = vec![0u8; len as usize];
|
| 209 |
+
std::ptr::copy_nonoverlapping(runtime_addr as *const u8, data.as_mut_ptr(), len as usize);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
|
| 211 |
// Classify pointers in the copied data
|
| 212 |
let pointers = mem::classify_region(&data, 0, delta);
|
|
|
|
| 243 |
unsafe {
|
| 244 |
if !mem::can_read(current, 4) {
|
| 245 |
return Response::Error {
|
| 246 |
+
message: format!("Chain broke: cannot read at runtime:0x{:08X}", current),
|
|
|
|
|
|
|
|
|
|
| 247 |
};
|
| 248 |
}
|
| 249 |
let value = *(current as *const u32);
|
|
|
|
| 380 |
}
|
| 381 |
|
| 382 |
Response::Error {
|
| 383 |
+
message: format!(
|
| 384 |
+
"No field '{}' in '{}' (searched: {})",
|
| 385 |
+
field_name,
|
| 386 |
+
class_name,
|
| 387 |
+
search_chain.join(" -> ")
|
| 388 |
+
),
|
| 389 |
}
|
| 390 |
}
|
| 391 |
|
crates/openwa-wormkit/src/debug_sync.rs
CHANGED
|
@@ -36,9 +36,7 @@ fn event() -> HANDLE {
|
|
| 36 |
|
| 37 |
/// Initialize the sync primitives. Call once from DLL init.
|
| 38 |
pub fn init() {
|
| 39 |
-
let handle = unsafe {
|
| 40 |
-
CreateEventA(core::ptr::null(), 1, 1, core::ptr::null())
|
| 41 |
-
};
|
| 42 |
assert!(!handle.is_null(), "debug_sync: CreateEventA failed");
|
| 43 |
RESUME_EVENT.store(handle as usize, Ordering::Relaxed);
|
| 44 |
|
|
@@ -77,7 +75,9 @@ pub fn on_frame_start(frame: i32) {
|
|
| 77 |
|
| 78 |
// Block if paused
|
| 79 |
if PAUSED.load(Ordering::Acquire) {
|
| 80 |
-
unsafe {
|
|
|
|
|
|
|
| 81 |
}
|
| 82 |
}
|
| 83 |
|
|
@@ -94,7 +94,9 @@ pub fn suspend() {
|
|
| 94 |
pub fn resume() {
|
| 95 |
PAUSED.store(false, Ordering::Release);
|
| 96 |
STEP_REMAINING.store(-1, Ordering::Relaxed);
|
| 97 |
-
unsafe {
|
|
|
|
|
|
|
| 98 |
let _ = log_line("[DebugSync] Resumed");
|
| 99 |
}
|
| 100 |
|
|
@@ -102,7 +104,9 @@ pub fn resume() {
|
|
| 102 |
pub fn step(count: i32) {
|
| 103 |
STEP_REMAINING.store(count.max(1), Ordering::Relaxed);
|
| 104 |
PAUSED.store(false, Ordering::Release);
|
| 105 |
-
unsafe {
|
|
|
|
|
|
|
| 106 |
}
|
| 107 |
|
| 108 |
/// Set a frame breakpoint. -1 to clear.
|
|
@@ -129,5 +133,7 @@ pub fn breakpoint() -> i32 {
|
|
| 129 |
|
| 130 |
fn do_suspend() {
|
| 131 |
PAUSED.store(true, Ordering::Release);
|
| 132 |
-
unsafe {
|
|
|
|
|
|
|
| 133 |
}
|
|
|
|
| 36 |
|
| 37 |
/// Initialize the sync primitives. Call once from DLL init.
|
| 38 |
pub fn init() {
|
| 39 |
+
let handle = unsafe { CreateEventA(core::ptr::null(), 1, 1, core::ptr::null()) };
|
|
|
|
|
|
|
| 40 |
assert!(!handle.is_null(), "debug_sync: CreateEventA failed");
|
| 41 |
RESUME_EVENT.store(handle as usize, Ordering::Relaxed);
|
| 42 |
|
|
|
|
| 75 |
|
| 76 |
// Block if paused
|
| 77 |
if PAUSED.load(Ordering::Acquire) {
|
| 78 |
+
unsafe {
|
| 79 |
+
WaitForSingleObject(event(), 0xFFFFFFFF);
|
| 80 |
+
}
|
| 81 |
}
|
| 82 |
}
|
| 83 |
|
|
|
|
| 94 |
pub fn resume() {
|
| 95 |
PAUSED.store(false, Ordering::Release);
|
| 96 |
STEP_REMAINING.store(-1, Ordering::Relaxed);
|
| 97 |
+
unsafe {
|
| 98 |
+
SetEvent(event());
|
| 99 |
+
}
|
| 100 |
let _ = log_line("[DebugSync] Resumed");
|
| 101 |
}
|
| 102 |
|
|
|
|
| 104 |
pub fn step(count: i32) {
|
| 105 |
STEP_REMAINING.store(count.max(1), Ordering::Relaxed);
|
| 106 |
PAUSED.store(false, Ordering::Release);
|
| 107 |
+
unsafe {
|
| 108 |
+
SetEvent(event());
|
| 109 |
+
}
|
| 110 |
}
|
| 111 |
|
| 112 |
/// Set a frame breakpoint. -1 to clear.
|
|
|
|
| 133 |
|
| 134 |
fn do_suspend() {
|
| 135 |
PAUSED.store(true, Ordering::Release);
|
| 136 |
+
unsafe {
|
| 137 |
+
ResetEvent(event());
|
| 138 |
+
}
|
| 139 |
}
|
crates/openwa-wormkit/src/debug_watchpoint.rs
CHANGED
|
@@ -33,9 +33,7 @@ use windows_sys::Win32::System::Diagnostics::Debug::{
|
|
| 33 |
/// Offsets to watch. Hardware limit: 4 watchpoints (DR0βDR3).
|
| 34 |
/// Change these to investigate different fields.
|
| 35 |
/// NOTE: The base pointer is set by `on_ddgame_alloc()` β can be DDGame or DDGameWrapper.
|
| 36 |
-
const WATCH_OFFSETS: [(u32, &str); 1] = [
|
| 37 |
-
(0x3A40, "display+0x3A40"),
|
| 38 |
-
];
|
| 39 |
|
| 40 |
/// DDGame base address (set when allocation is reported).
|
| 41 |
static DDGAME_BASE: AtomicU32 = AtomicU32::new(0);
|
|
@@ -160,7 +158,10 @@ unsafe extern "system" fn veh_handler(info: *mut EXCEPTION_POINTERS) -> i32 {
|
|
| 160 |
|
| 161 |
let _ = log_line(&format!(
|
| 162 |
"[Watchpoint] {} = 0x{:08X} eip={} stack=[{}]",
|
| 163 |
-
name,
|
|
|
|
|
|
|
|
|
|
| 164 |
));
|
| 165 |
}
|
| 166 |
}
|
|
@@ -208,8 +209,7 @@ unsafe extern "cdecl" fn malloc_hook(size: u32) -> *mut u8 {
|
|
| 208 |
if size == DDGAME_ALLOC_SIZE && !result.is_null() {
|
| 209 |
let _ = log_line(&format!(
|
| 210 |
"[Watchpoint] Intercepted wa_malloc(0x{:X}) = 0x{:08X}",
|
| 211 |
-
size,
|
| 212 |
-
result as u32,
|
| 213 |
));
|
| 214 |
on_ddgame_alloc(result);
|
| 215 |
}
|
|
|
|
| 33 |
/// Offsets to watch. Hardware limit: 4 watchpoints (DR0βDR3).
|
| 34 |
/// Change these to investigate different fields.
|
| 35 |
/// NOTE: The base pointer is set by `on_ddgame_alloc()` β can be DDGame or DDGameWrapper.
|
| 36 |
+
const WATCH_OFFSETS: [(u32, &str); 1] = [(0x3A40, "display+0x3A40")];
|
|
|
|
|
|
|
| 37 |
|
| 38 |
/// DDGame base address (set when allocation is reported).
|
| 39 |
static DDGAME_BASE: AtomicU32 = AtomicU32::new(0);
|
|
|
|
| 158 |
|
| 159 |
let _ = log_line(&format!(
|
| 160 |
"[Watchpoint] {} = 0x{:08X} eip={} stack=[{}]",
|
| 161 |
+
name,
|
| 162 |
+
val,
|
| 163 |
+
openwa_core::registry::format_va(ghidra_eip),
|
| 164 |
+
trace,
|
| 165 |
));
|
| 166 |
}
|
| 167 |
}
|
|
|
|
| 209 |
if size == DDGAME_ALLOC_SIZE && !result.is_null() {
|
| 210 |
let _ = log_line(&format!(
|
| 211 |
"[Watchpoint] Intercepted wa_malloc(0x{:X}) = 0x{:08X}",
|
| 212 |
+
size, result as u32,
|
|
|
|
| 213 |
));
|
| 214 |
on_ddgame_alloc(result);
|
| 215 |
}
|
crates/openwa-wormkit/src/lib.rs
CHANGED
|
@@ -66,8 +66,8 @@ fn clear_log() -> std::io::Result<()> {
|
|
| 66 |
|
| 67 |
fn run() -> Result<(), String> {
|
| 68 |
let _ = clear_log();
|
| 69 |
-
let validation_path =
|
| 70 |
-
.unwrap_or("OpenWA_validation.log".into());
|
| 71 |
let _ = std::fs::write(validation_path, "");
|
| 72 |
|
| 73 |
// Install panic hook that writes to our log file
|
|
@@ -132,7 +132,10 @@ fn signal_hooks_ready() {
|
|
| 132 |
if !handle.is_null() {
|
| 133 |
SetEvent(handle);
|
| 134 |
CloseHandle(handle);
|
| 135 |
-
let _ = log_line(&format!(
|
|
|
|
|
|
|
|
|
|
| 136 |
}
|
| 137 |
// If handle is null, we weren't launched by our launcher β that's fine.
|
| 138 |
}
|
|
|
|
| 66 |
|
| 67 |
fn run() -> Result<(), String> {
|
| 68 |
let _ = clear_log();
|
| 69 |
+
let validation_path =
|
| 70 |
+
std::env::var_os("OPENWA_VALIDATION_LOG_PATH").unwrap_or("OpenWA_validation.log".into());
|
| 71 |
let _ = std::fs::write(validation_path, "");
|
| 72 |
|
| 73 |
// Install panic hook that writes to our log file
|
|
|
|
| 132 |
if !handle.is_null() {
|
| 133 |
SetEvent(handle);
|
| 134 |
CloseHandle(handle);
|
| 135 |
+
let _ = log_line(&format!(
|
| 136 |
+
"=== Signalled {}{} ===",
|
| 137 |
+
HOOKS_READY_EVENT_PREFIX, pid
|
| 138 |
+
));
|
| 139 |
}
|
| 140 |
// If handle is null, we weren't launched by our launcher β that's fine.
|
| 141 |
}
|
crates/openwa-wormkit/src/replacements/file_isolation.rs
CHANGED
|
@@ -96,9 +96,8 @@ pub fn install() -> Result<(), String> {
|
|
| 96 |
}
|
| 97 |
|
| 98 |
unsafe {
|
| 99 |
-
let module =
|
| 100 |
-
b"kernel32.dll\0".as_ptr()
|
| 101 |
-
);
|
| 102 |
if module.is_null() {
|
| 103 |
return Err("kernel32.dll not loaded".to_string());
|
| 104 |
}
|
|
|
|
| 96 |
}
|
| 97 |
|
| 98 |
unsafe {
|
| 99 |
+
let module =
|
| 100 |
+
windows_sys::Win32::System::LibraryLoader::GetModuleHandleA(b"kernel32.dll\0".as_ptr());
|
|
|
|
| 101 |
if module.is_null() {
|
| 102 |
return Err("kernel32.dll not loaded".to_string());
|
| 103 |
}
|
crates/openwa-wormkit/src/replacements/game_session.rs
CHANGED
|
@@ -47,9 +47,16 @@ unsafe extern "stdcall" fn call_init_replay(_game_info: *mut GameInfo, _this: *m
|
|
| 47 |
/// Temp: bridge to original DDGame__Constructor for comparison.
|
| 48 |
#[unsafe(naked)]
|
| 49 |
unsafe extern "C" fn call_original_ddgame_ctor(
|
| 50 |
-
_wrapper: *mut DDGameWrapper,
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
) {
|
| 54 |
core::arch::naked_asm!(
|
| 55 |
"mov ecx, [esp+40]",
|
|
@@ -123,8 +130,16 @@ pub(crate) unsafe fn construct_ddgame_wrapper(
|
|
| 123 |
let use_original = std::env::var("OPENWA_USE_ORIG_CTOR").is_ok();
|
| 124 |
if use_original {
|
| 125 |
call_original_ddgame_ctor(
|
| 126 |
-
this,
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
);
|
| 129 |
} else {
|
| 130 |
create_ddgame(
|
|
@@ -139,7 +154,6 @@ pub(crate) unsafe fn construct_ddgame_wrapper(
|
|
| 139 |
game_info,
|
| 140 |
input_ctrl as u32,
|
| 141 |
);
|
| 142 |
-
|
| 143 |
}
|
| 144 |
|
| 145 |
// Disarm display watchpoint
|
|
@@ -152,10 +166,10 @@ pub(crate) unsafe fn construct_ddgame_wrapper(
|
|
| 152 |
core::mem::transmute(rb(va::DDGAME_INIT_GAME_STATE) as usize);
|
| 153 |
init_state(this);
|
| 154 |
|
| 155 |
-
|
| 156 |
let _ = log_line(&format!(
|
| 157 |
"[GameSession] DDGameWrapper::Constructor done: wrapper=0x{:08X} ddgame=0x{:08X}",
|
| 158 |
-
this as u32,
|
|
|
|
| 159 |
));
|
| 160 |
|
| 161 |
// Register live objects for pointer identification in debug tools.
|
|
|
|
| 47 |
/// Temp: bridge to original DDGame__Constructor for comparison.
|
| 48 |
#[unsafe(naked)]
|
| 49 |
unsafe extern "C" fn call_original_ddgame_ctor(
|
| 50 |
+
_wrapper: *mut DDGameWrapper,
|
| 51 |
+
_display: *mut DDDisplay,
|
| 52 |
+
_sound: *mut DSSound,
|
| 53 |
+
_keyboard: *mut u8,
|
| 54 |
+
_palette: *mut Palette,
|
| 55 |
+
_music: *mut u8,
|
| 56 |
+
_timer: *mut u8,
|
| 57 |
+
_net_game: *mut u8,
|
| 58 |
+
_game_info: *mut GameInfo,
|
| 59 |
+
_input_ctrl: *mut u8,
|
| 60 |
) {
|
| 61 |
core::arch::naked_asm!(
|
| 62 |
"mov ecx, [esp+40]",
|
|
|
|
| 130 |
let use_original = std::env::var("OPENWA_USE_ORIG_CTOR").is_ok();
|
| 131 |
if use_original {
|
| 132 |
call_original_ddgame_ctor(
|
| 133 |
+
this,
|
| 134 |
+
display,
|
| 135 |
+
sound,
|
| 136 |
+
keyboard,
|
| 137 |
+
palette,
|
| 138 |
+
streaming_audio,
|
| 139 |
+
timer_obj,
|
| 140 |
+
net_game,
|
| 141 |
+
game_info,
|
| 142 |
+
input_ctrl,
|
| 143 |
);
|
| 144 |
} else {
|
| 145 |
create_ddgame(
|
|
|
|
| 154 |
game_info,
|
| 155 |
input_ctrl as u32,
|
| 156 |
);
|
|
|
|
| 157 |
}
|
| 158 |
|
| 159 |
// Disarm display watchpoint
|
|
|
|
| 166 |
core::mem::transmute(rb(va::DDGAME_INIT_GAME_STATE) as usize);
|
| 167 |
init_state(this);
|
| 168 |
|
|
|
|
| 169 |
let _ = log_line(&format!(
|
| 170 |
"[GameSession] DDGameWrapper::Constructor done: wrapper=0x{:08X} ddgame=0x{:08X}",
|
| 171 |
+
this as u32,
|
| 172 |
+
(*this).ddgame as u32,
|
| 173 |
));
|
| 174 |
|
| 175 |
// Register live objects for pointer identification in debug tools.
|
crates/openwa-wormkit/src/replacements/input.rs
CHANGED
|
@@ -188,7 +188,8 @@ unsafe extern "stdcall" fn hook_turn_manager(turngame: u32) {
|
|
| 188 |
crate::debug_sync::on_frame_start(game_frame);
|
| 189 |
|
| 190 |
// Hardware watchpoint: arm once at the watch frame
|
| 191 |
-
static WATCH_ARMED: core::sync::atomic::AtomicBool =
|
|
|
|
| 192 |
if !WATCH_ARMED.load(Ordering::Relaxed) {
|
| 193 |
if let Ok(val) = std::env::var("OPENWA_WATCH_FRAME") {
|
| 194 |
let target: i32 = val.parse().unwrap_or(0);
|
|
@@ -280,7 +281,9 @@ pub fn write_gameplay_report() {
|
|
| 280 |
frames
|
| 281 |
));
|
| 282 |
} else {
|
| 283 |
-
let _ = log_validation(
|
|
|
|
|
|
|
| 284 |
}
|
| 285 |
|
| 286 |
// Milestone 2: Match started (multiple teams with alive worms)
|
|
@@ -291,7 +294,8 @@ pub fn write_gameplay_report() {
|
|
| 291 |
teams
|
| 292 |
));
|
| 293 |
} else if frames > 0 {
|
| 294 |
-
let _ =
|
|
|
|
| 295 |
} else {
|
| 296 |
let _ = log_validation("[GAMEPLAY FAIL] Match started - game never initialized");
|
| 297 |
}
|
|
@@ -300,13 +304,18 @@ pub fn write_gameplay_report() {
|
|
| 300 |
if completed {
|
| 301 |
let end_frame = COMPLETION_FRAME.load(Ordering::Relaxed);
|
| 302 |
let alive = ALIVE_AT_END.load(Ordering::Relaxed);
|
| 303 |
-
let outcome = if alive == 1 {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
let _ = log_validation(&format!(
|
| 305 |
"[GAMEPLAY PASS] Match completed - {} at frame {}",
|
| 306 |
outcome, end_frame
|
| 307 |
));
|
| 308 |
} else if started {
|
| 309 |
-
let _ =
|
|
|
|
| 310 |
} else {
|
| 311 |
let _ = log_validation("[GAMEPLAY FAIL] Match completed - match never started");
|
| 312 |
}
|
|
|
|
| 188 |
crate::debug_sync::on_frame_start(game_frame);
|
| 189 |
|
| 190 |
// Hardware watchpoint: arm once at the watch frame
|
| 191 |
+
static WATCH_ARMED: core::sync::atomic::AtomicBool =
|
| 192 |
+
core::sync::atomic::AtomicBool::new(false);
|
| 193 |
if !WATCH_ARMED.load(Ordering::Relaxed) {
|
| 194 |
if let Ok(val) = std::env::var("OPENWA_WATCH_FRAME") {
|
| 195 |
let target: i32 = val.parse().unwrap_or(0);
|
|
|
|
| 281 |
frames
|
| 282 |
));
|
| 283 |
} else {
|
| 284 |
+
let _ = log_validation(
|
| 285 |
+
"[GAMEPLAY FAIL] Game initialized - no frames processed (game may not have started)",
|
| 286 |
+
);
|
| 287 |
}
|
| 288 |
|
| 289 |
// Milestone 2: Match started (multiple teams with alive worms)
|
|
|
|
| 294 |
teams
|
| 295 |
));
|
| 296 |
} else if frames > 0 {
|
| 297 |
+
let _ =
|
| 298 |
+
log_validation("[GAMEPLAY FAIL] Match started - never detected multiple alive teams");
|
| 299 |
} else {
|
| 300 |
let _ = log_validation("[GAMEPLAY FAIL] Match started - game never initialized");
|
| 301 |
}
|
|
|
|
| 304 |
if completed {
|
| 305 |
let end_frame = COMPLETION_FRAME.load(Ordering::Relaxed);
|
| 306 |
let alive = ALIVE_AT_END.load(Ordering::Relaxed);
|
| 307 |
+
let outcome = if alive == 1 {
|
| 308 |
+
"winner decided"
|
| 309 |
+
} else {
|
| 310 |
+
"draw (all eliminated)"
|
| 311 |
+
};
|
| 312 |
let _ = log_validation(&format!(
|
| 313 |
"[GAMEPLAY PASS] Match completed - {} at frame {}",
|
| 314 |
outcome, end_frame
|
| 315 |
));
|
| 316 |
} else if started {
|
| 317 |
+
let _ =
|
| 318 |
+
log_validation("[GAMEPLAY FAIL] Match completed - match started but never finished");
|
| 319 |
} else {
|
| 320 |
let _ = log_validation("[GAMEPLAY FAIL] Match completed - match never started");
|
| 321 |
}
|
crates/openwa-wormkit/src/replacements/render.rs
CHANGED
|
@@ -413,11 +413,10 @@ unsafe extern "stdcall" fn draw_bungee_trail_impl(task_ptr: u32, style: u32, fil
|
|
| 413 |
let seg_angle = *(seg_data.add(4 + i as usize * 8) as *const i32);
|
| 414 |
|
| 415 |
// Include vertex if: first segment, or segment has nonzero angle, or fill mode
|
| 416 |
-
if (i == 0 || seg_angle != 0 || fill != 0)
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
}
|
| 421 |
|
| 422 |
accumulated_angle = accumulated_angle.wrapping_add(seg_angle as u32);
|
| 423 |
|
|
|
|
| 413 |
let seg_angle = *(seg_data.add(4 + i as usize * 8) as *const i32);
|
| 414 |
|
| 415 |
// Include vertex if: first segment, or segment has nonzero angle, or fill mode
|
| 416 |
+
if (i == 0 || seg_angle != 0 || fill != 0) && vert_count < MAX_VERTICES {
|
| 417 |
+
verts[vert_count] = [x, y, 0];
|
| 418 |
+
vert_count += 1;
|
| 419 |
+
}
|
|
|
|
| 420 |
|
| 421 |
accumulated_angle = accumulated_angle.wrapping_add(seg_angle as u32);
|
| 422 |
|
crates/openwa-wormkit/src/replacements/replay.rs
CHANGED
|
@@ -7,8 +7,8 @@ use crate::hook;
|
|
| 7 |
use crate::log_line;
|
| 8 |
use openwa_core::address::va;
|
| 9 |
use openwa_core::engine::game_info::GameInfo;
|
| 10 |
-
use openwa_core::frontend::MapView;
|
| 11 |
use openwa_core::engine::replay::{self, ReplayError, ReplayStream, ReplayTeamEntry};
|
|
|
|
| 12 |
use openwa_core::rebase::rb;
|
| 13 |
use openwa_core::wa_alloc::{wa_free, wa_malloc};
|
| 14 |
|
|
@@ -48,10 +48,16 @@ unsafe fn wa_file_to_rust(file: *mut FILE) -> Option<ManuallyDrop<File>> {
|
|
| 48 |
core::mem::transmute(rb(va::WA_GET_OSFHANDLE));
|
| 49 |
|
| 50 |
let fd = fileno(file);
|
| 51 |
-
if fd < 0 {
|
|
|
|
|
|
|
| 52 |
let handle = get_osfhandle(fd);
|
| 53 |
-
if handle == -1 || handle == -2 {
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
}
|
| 56 |
|
| 57 |
/// Load a WA string resource by ID (stdcall). Returns null-terminated C string.
|
|
@@ -71,10 +77,16 @@ static mut REPLAY_TIMESTAMP: u32 = 0;
|
|
| 71 |
|
| 72 |
// βββ RAII cleanup guard (for wa_malloc'd payload) βββββββββββββββββββββββββββ
|
| 73 |
|
| 74 |
-
struct PayloadGuard {
|
|
|
|
|
|
|
| 75 |
impl Drop for PayloadGuard {
|
| 76 |
fn drop(&mut self) {
|
| 77 |
-
unsafe {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
}
|
| 79 |
}
|
| 80 |
|
|
@@ -96,7 +108,9 @@ unsafe extern "stdcall" fn hook_replay_loader(state: *mut GameInfo, mode: i32) -
|
|
| 96 |
|
| 97 |
unsafe fn replay_loader_play(gi: *mut GameInfo) -> Result<(), ReplayError> {
|
| 98 |
let artclass_counter = *(rb(va::G_ARTCLASS_COUNTER) as *const i32);
|
| 99 |
-
if artclass_counter >= 0x34 {
|
|
|
|
|
|
|
| 100 |
(*gi).replay_active = 1;
|
| 101 |
(*gi).replay_field_ef60 = 0;
|
| 102 |
|
|
@@ -113,22 +127,29 @@ unsafe fn replay_loader_play(gi: *mut GameInfo) -> Result<(), ReplayError> {
|
|
| 113 |
}
|
| 114 |
};
|
| 115 |
|
| 116 |
-
let mut file = File::open(&replay_path)
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
| 119 |
|
| 120 |
// Helper: read exact bytes from file into a typed value
|
| 121 |
fn read_u32(f: &mut File) -> Result<u32, ReplayError> {
|
| 122 |
let mut buf = [0u8; 4];
|
| 123 |
-
f.read_exact(&mut buf)
|
|
|
|
| 124 |
Ok(u32::from_le_bytes(buf))
|
| 125 |
}
|
| 126 |
|
| 127 |
// Header
|
| 128 |
let header = read_u32(&mut file)?;
|
| 129 |
-
if (header & 0xFFFF) != replay::REPLAY_MAGIC as u32 {
|
|
|
|
|
|
|
| 130 |
let version = header >> 16;
|
| 131 |
-
if version == 0 || version > 20 {
|
|
|
|
|
|
|
| 132 |
|
| 133 |
(*gi).replay_format_version = version;
|
| 134 |
(*gi).replay_format_version_2 = version;
|
|
@@ -136,12 +157,17 @@ unsafe fn replay_loader_play(gi: *mut GameInfo) -> Result<(), ReplayError> {
|
|
| 136 |
|
| 137 |
// First payload
|
| 138 |
let payload_size = read_u32(&mut file)?;
|
| 139 |
-
if (payload_size as u64 + 8) > file_size {
|
|
|
|
|
|
|
| 140 |
let payload = wa_malloc(payload_size);
|
| 141 |
-
if payload.is_null() {
|
|
|
|
|
|
|
| 142 |
let mut guard = PayloadGuard { payload };
|
| 143 |
let payload_slice = core::slice::from_raw_parts_mut(payload, payload_size as usize);
|
| 144 |
-
file.read_exact(payload_slice)
|
|
|
|
| 145 |
|
| 146 |
let first_dword = *(payload as *const i32);
|
| 147 |
(*gi).replay_map_type = first_dword;
|
|
@@ -156,24 +182,25 @@ unsafe fn replay_loader_play(gi: *mut GameInfo) -> Result<(), ReplayError> {
|
|
| 156 |
*(payload.add(8) as *const i32);
|
| 157 |
} else if first_dword == -2 {
|
| 158 |
let n = payload_size.saturating_sub(8) as usize;
|
| 159 |
-
if n > 0x20 {
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
n,
|
| 164 |
-
);
|
| 165 |
// Null-terminate: write 0 at replay_map_type + payload_size offset
|
| 166 |
// (the original uses base+0xDB1C+payload_size which spans into replay_payload_extra)
|
| 167 |
*((gi as *mut u8).add(0xDB1C + payload_size as usize)) = 0;
|
| 168 |
}
|
| 169 |
}
|
| 170 |
-
wa_free(payload);
|
|
|
|
| 171 |
|
| 172 |
// Clear global buffers
|
| 173 |
ptr::write_bytes(rb(va::G_TEAM_HEADER_DATA) as *mut u8, 0, 0x5728);
|
| 174 |
ptr::write_bytes(rb(va::G_TEAM_SECONDARY_DATA) as *mut u8, 0, 0xD9DC);
|
| 175 |
|
| 176 |
-
let _ = log_line(&format!(
|
|
|
|
|
|
|
| 177 |
|
| 178 |
if version == 1 {
|
| 179 |
drop(file);
|
|
@@ -183,18 +210,24 @@ unsafe fn replay_loader_play(gi: *mut GameInfo) -> Result<(), ReplayError> {
|
|
| 183 |
// βββ Version 2+: read second payload βββββββββββββββββββββββββββββββββ
|
| 184 |
|
| 185 |
let second_size = read_u32(&mut file)?;
|
| 186 |
-
if (4u64 + 4 + payload_size as u64 + 4 + second_size as u64) > file_size {
|
|
|
|
|
|
|
| 187 |
let p2 = wa_malloc(second_size);
|
| 188 |
-
if p2.is_null() {
|
|
|
|
|
|
|
| 189 |
guard.payload = p2;
|
| 190 |
let p2_slice = core::slice::from_raw_parts_mut(p2, second_size as usize);
|
| 191 |
-
file.read_exact(p2_slice)
|
|
|
|
| 192 |
drop(file); // Done reading β close replay file
|
| 193 |
|
| 194 |
// Parse and write to globals
|
| 195 |
let data = core::slice::from_raw_parts(p2, second_size as usize);
|
| 196 |
let result = parse_and_write_v2plus(gi, data, version);
|
| 197 |
-
wa_free(p2);
|
|
|
|
| 198 |
|
| 199 |
match result {
|
| 200 |
Ok(()) => {
|
|
@@ -209,7 +242,9 @@ unsafe fn replay_loader_play(gi: *mut GameInfo) -> Result<(), ReplayError> {
|
|
| 209 |
Ok(())
|
| 210 |
}
|
| 211 |
Err(e) => {
|
| 212 |
-
let _ = log_line(&format!(
|
|
|
|
|
|
|
| 213 |
delegate_to_original(gi)
|
| 214 |
}
|
| 215 |
}
|
|
@@ -220,22 +255,33 @@ unsafe fn parse_version1(gi: *mut GameInfo) -> Result<(), ReplayError> {
|
|
| 220 |
}
|
| 221 |
|
| 222 |
unsafe fn delegate_to_original(gi: *mut GameInfo) -> Result<(), ReplayError> {
|
| 223 |
-
let orig: unsafe extern "stdcall" fn(*mut GameInfo, i32) -> u32 =
|
|
|
|
| 224 |
let result = orig(gi, 1);
|
| 225 |
-
if result == 0 {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
}
|
| 227 |
|
| 228 |
// βββ Version 2+ parser that writes to globals βββββββββββββββββββββββββββββββ
|
| 229 |
|
| 230 |
/// Helper: write byte to rebased global address.
|
| 231 |
#[inline]
|
| 232 |
-
unsafe fn wb(addr: u32, val: u8) {
|
|
|
|
|
|
|
| 233 |
/// Helper: write u32 to rebased global address.
|
| 234 |
#[inline]
|
| 235 |
-
unsafe fn wd(addr: u32, val: u32) {
|
|
|
|
|
|
|
| 236 |
|
| 237 |
unsafe fn parse_and_write_v2plus(
|
| 238 |
-
gi: *mut GameInfo,
|
|
|
|
|
|
|
| 239 |
) -> Result<(), ReplayError> {
|
| 240 |
let mut s = ReplayStream::new(data);
|
| 241 |
|
|
@@ -250,7 +296,9 @@ unsafe fn parse_and_write_v2plus(
|
|
| 250 |
if version >= 10 {
|
| 251 |
let sub_format = s.read_u16()?;
|
| 252 |
wd(va::G_REPLAY_SUB_FORMAT, sub_format as u32);
|
| 253 |
-
if sub_format != 0 {
|
|
|
|
|
|
|
| 254 |
|
| 255 |
if version >= 12 {
|
| 256 |
if version < 18 {
|
|
@@ -260,7 +308,8 @@ unsafe fn parse_and_write_v2plus(
|
|
| 260 |
let raw = s.read_u8_validated(0, 3)?;
|
| 261 |
if (2..=3).contains(&raw) {
|
| 262 |
wb(va::G_REPLAY_GAME_MODE, raw - 1);
|
| 263 |
-
wb(va::G_REPLAY_VER_FLAG_A, 1);
|
|
|
|
| 264 |
} else {
|
| 265 |
wb(va::G_REPLAY_VER_FLAG_A, (raw != 0) as u8);
|
| 266 |
wb(va::G_REPLAY_GAME_MODE, 0);
|
|
@@ -284,7 +333,9 @@ unsafe fn parse_and_write_v2plus(
|
|
| 284 |
let obs_data: [u32; 4] = [team_id, 0, obs_type as u32, 0];
|
| 285 |
// RegisterObserver: usercall(ESI=0x88C35C) + stdcall(1 param = &data). RET 0x4.
|
| 286 |
call_register_observer(
|
| 287 |
-
rb(va::G_OBSERVER_ARRAY),
|
|
|
|
|
|
|
| 288 |
);
|
| 289 |
if obs_type == 0 {
|
| 290 |
// The terminating entry's team_id is the replay timestamp (time_t)
|
|
@@ -296,7 +347,9 @@ unsafe fn parse_and_write_v2plus(
|
|
| 296 |
|
| 297 |
let game_version_id = s.read_i32()?;
|
| 298 |
wd(va::G_REPLAY_VERSION_ID, game_version_id as u32);
|
| 299 |
-
if (game_version_id.wrapping_add(4) as u32) > 0x1F8 {
|
|
|
|
|
|
|
| 300 |
let use_fixed_names = game_version_id < 10;
|
| 301 |
|
| 302 |
// ββ Scheme presence ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -331,7 +384,9 @@ unsafe fn parse_and_write_v2plus(
|
|
| 331 |
// SCHM magic + version
|
| 332 |
let magic = s.read_u32()?;
|
| 333 |
let scheme_version = s.read_u8()?;
|
| 334 |
-
if magic != 0x4D484353 {
|
|
|
|
|
|
|
| 335 |
|
| 336 |
let scheme_data_size = match scheme_version {
|
| 337 |
1 => 0xD8_usize,
|
|
@@ -343,7 +398,8 @@ unsafe fn parse_and_write_v2plus(
|
|
| 343 |
// Copy defaults first for v3
|
| 344 |
ptr::copy_nonoverlapping(
|
| 345 |
rb(va::SCHEME_V3_DEFAULTS) as *const u8,
|
| 346 |
-
rb(va::G_SCHEME_V3_DATA) as *mut u8,
|
|
|
|
| 347 |
);
|
| 348 |
(scheme_size_indicator as usize) - 5
|
| 349 |
}
|
|
@@ -352,7 +408,11 @@ unsafe fn parse_and_write_v2plus(
|
|
| 352 |
|
| 353 |
// Copy scheme data from stream to global 0x88DAE0
|
| 354 |
let scheme_slice = s.advance_raw(scheme_data_size)?;
|
| 355 |
-
ptr::copy_nonoverlapping(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
|
| 357 |
let header_val = *(rb(va::G_SCHEME_HEADER) as *const i8);
|
| 358 |
|
|
@@ -366,7 +426,8 @@ unsafe fn parse_and_write_v2plus(
|
|
| 366 |
if scheme_version <= 2 {
|
| 367 |
ptr::copy_nonoverlapping(
|
| 368 |
rb(va::SCHEME_V3_DEFAULTS) as *const u8,
|
| 369 |
-
rb(va::G_SCHEME_V3_DATA) as *mut u8,
|
|
|
|
| 370 |
);
|
| 371 |
}
|
| 372 |
}
|
|
@@ -376,7 +437,9 @@ unsafe fn parse_and_write_v2plus(
|
|
| 376 |
let validate: unsafe extern "cdecl" fn() -> i32 =
|
| 377 |
core::mem::transmute(rb(va::SCHEME_VALIDATE_EXTENDED));
|
| 378 |
let r = validate();
|
| 379 |
-
if r != 0 {
|
|
|
|
|
|
|
| 380 |
}
|
| 381 |
|
| 382 |
// Random seed: the stream value REPLACES the current seed.
|
|
@@ -415,22 +478,36 @@ unsafe fn parse_and_write_v2plus(
|
|
| 415 |
let base = va::G_PLAYER_ARRAY + 0x74 + i * 0x78; // player[i].flag
|
| 416 |
let flag = s.read_u8()?;
|
| 417 |
wb(base, flag);
|
| 418 |
-
if flag == 0 {
|
|
|
|
|
|
|
| 419 |
|
| 420 |
// NOTE: original checks if i == host_player_index here (sets a local flag, unused)
|
| 421 |
player_count += 1;
|
| 422 |
|
| 423 |
let mut name = [0u8; 0x11];
|
| 424 |
s.read_prefixed_string(&mut name)?;
|
| 425 |
-
ptr::copy_nonoverlapping(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 426 |
|
| 427 |
let mut display = [0u8; 0x31];
|
| 428 |
s.read_prefixed_string(&mut display)?;
|
| 429 |
-
ptr::copy_nonoverlapping(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 430 |
|
| 431 |
let mut config = [0u8; 0x29];
|
| 432 |
s.read_prefixed_string(&mut config)?;
|
| 433 |
-
ptr::copy_nonoverlapping(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
|
| 435 |
let u16_val = s.read_u16()?;
|
| 436 |
// Ghidra shows (&DAT[i*0x3C]) with u16 elements β byte offset = i*0x3C*2 = i*0x78
|
|
@@ -460,11 +537,15 @@ unsafe fn parse_and_write_v2plus(
|
|
| 460 |
let team = &mut *teams.add(team_idx);
|
| 461 |
let team_flag = s.read_u8()?;
|
| 462 |
team.flag = team_flag;
|
| 463 |
-
if team_flag == 0 {
|
|
|
|
|
|
|
| 464 |
team_count += 1;
|
| 465 |
-
|
| 466 |
let team_type = s.read_u8()? as i8;
|
| 467 |
-
if !replay::validate_team_type(team_type) {
|
|
|
|
|
|
|
| 468 |
team.team_type = team_type as u8;
|
| 469 |
team.alliance = s.read_u8_validated(0, 5)?;
|
| 470 |
team.unknown_02 = s.read_u8()?;
|
|
@@ -496,7 +577,9 @@ unsafe fn parse_and_write_v2plus(
|
|
| 496 |
s.read_prefixed_string(&mut team.config_name)?;
|
| 497 |
|
| 498 |
let worm_count = s.read_u8()?;
|
| 499 |
-
if worm_count == 0 || worm_count > 8 {
|
|
|
|
|
|
|
| 500 |
team.worm_count = worm_count;
|
| 501 |
team.color = s.read_u8()?;
|
| 502 |
team.flag2 = s.read_u8()?;
|
|
@@ -515,7 +598,9 @@ unsafe fn parse_and_write_v2plus(
|
|
| 515 |
ptr::copy_nonoverlapping(w4.as_ptr(), team.weapons.as_mut_ptr().add(0x954), 0x300);
|
| 516 |
}
|
| 517 |
|
| 518 |
-
if team_count == 0 {
|
|
|
|
|
|
|
| 519 |
|
| 520 |
wb(va::G_TEAM_COUNT, team_count);
|
| 521 |
|
|
@@ -538,7 +623,10 @@ unsafe fn parse_and_write_v2plus(
|
|
| 538 |
if map_seed != 0 {
|
| 539 |
// Per-team weapon config reads from stream β not yet implemented in Rust.
|
| 540 |
// Fall back to the original parser which handles this from scratch.
|
| 541 |
-
let _ = log_line(&format!(
|
|
|
|
|
|
|
|
|
|
| 542 |
return Err(ReplayError::InvalidFormat); // triggers delegate_to_original
|
| 543 |
}
|
| 544 |
|
|
@@ -554,8 +642,8 @@ unsafe fn parse_and_write_v2plus(
|
|
| 554 |
srand(current_seed);
|
| 555 |
let r1 = rand_fn() as u32;
|
| 556 |
let r2 = rand_fn() as u32;
|
| 557 |
-
wd(va::G_RANDOM_SEED, r2 + (r1 << 16));
|
| 558 |
-
wd(va::G_SAVED_RANDOM_SEED, current_seed);
|
| 559 |
|
| 560 |
let ver = *(rb(va::G_REPLAY_VERSION_ID) as *const i32);
|
| 561 |
if ver != 0x22 && !(0x29..=0x2A).contains(&ver) && ver < 0x2D {
|
|
@@ -610,8 +698,6 @@ unsafe fn parse_and_write_v2plus(
|
|
| 610 |
|
| 611 |
// βββ Log output formatting ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 612 |
|
| 613 |
-
|
| 614 |
-
|
| 615 |
/// Write the /getlog replay header to the log file.
|
| 616 |
unsafe fn write_replay_log(gi: *const GameInfo, log_file: &mut File) -> Result<(), ReplayError> {
|
| 617 |
use std::io::Write as IoWrite;
|
|
@@ -635,9 +721,15 @@ unsafe fn write_replay_log(gi: *const GameInfo, log_file: &mut File) -> Result<(
|
|
| 635 |
if !tm.is_null() {
|
| 636 |
let tm = &*tm;
|
| 637 |
let mut s = heapless::String::<128>::new();
|
| 638 |
-
let _ = writeln!(
|
|
|
|
| 639 |
"Game Started at {:04}-{:02}-{:02} {:02}:{:02}:{:02} GMT",
|
| 640 |
-
tm[5] + 1900,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 641 |
);
|
| 642 |
let _ = log_file.write_all(s.as_bytes());
|
| 643 |
}
|
|
@@ -657,7 +749,7 @@ unsafe fn write_replay_log(gi: *const GameInfo, log_file: &mut File) -> Result<(
|
|
| 657 |
-4 => rb(0x650EAC) as *const u8, // "1.0"
|
| 658 |
-2 => rb(0x650EB0) as *const u8, // "3.0"
|
| 659 |
-1 => rb(0x650EB4) as *const u8, // "3.5 Beta 1"
|
| 660 |
-
_
|
| 661 |
}
|
| 662 |
} else {
|
| 663 |
let table = rb(va::VERSION_STRING_TABLE) as *const u32;
|
|
@@ -684,7 +776,7 @@ unsafe fn write_replay_log(gi: *const GameInfo, log_file: &mut File) -> Result<(
|
|
| 684 |
// ββ "Exported with Version" line βββββββββββββββββββββββββββββββββββββ
|
| 685 |
{
|
| 686 |
let label_exported = wa_load_string(0x6E9); // "Exported with Version"
|
| 687 |
-
|
| 688 |
let ver_byte = *(rb(va::G_VERSION_BYTE) as *const u8) as u32;
|
| 689 |
// "3.8.1" literal at 0x641C60 + suffix from version table
|
| 690 |
let ver_literal = rb(va::STR_VERSION_381) as *const u8; // "3.8.1"
|
|
@@ -699,7 +791,6 @@ unsafe fn write_replay_log(gi: *const GameInfo, log_file: &mut File) -> Result<(
|
|
| 699 |
let _ = log_file.write_all(s.as_bytes());
|
| 700 |
}
|
| 701 |
|
| 702 |
-
|
| 703 |
// ββ Team listing βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 704 |
// For each team: "Color: "TeamName" [CPU X.XX]\n" or similar
|
| 705 |
|
|
@@ -718,24 +809,36 @@ unsafe fn write_replay_log(gi: *const GameInfo, log_file: &mut File) -> Result<(
|
|
| 718 |
let mut max_name_len = 0usize;
|
| 719 |
for slot in 0..6usize {
|
| 720 |
let team = &*teams.add(slot);
|
| 721 |
-
if team.flag == 0 {
|
|
|
|
|
|
|
| 722 |
let ci = team.alliance as usize;
|
| 723 |
if ci < 6 {
|
| 724 |
let len = c_strlen(color_names[ci]);
|
| 725 |
-
if len > max_color_len {
|
|
|
|
|
|
|
| 726 |
}
|
| 727 |
let name_len = c_strlen(team.config_abbrev.as_ptr());
|
| 728 |
-
if name_len > max_name_len {
|
|
|
|
|
|
|
| 729 |
}
|
| 730 |
|
| 731 |
for slot in 0..6usize {
|
| 732 |
let team = &*teams.add(slot);
|
| 733 |
-
if team.flag == 0 {
|
|
|
|
|
|
|
| 734 |
|
| 735 |
let team_type = team.team_type as i8;
|
| 736 |
let color_idx = team.alliance as usize;
|
| 737 |
|
| 738 |
-
let color = if color_idx < 6 {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 739 |
let clen = c_strlen(color);
|
| 740 |
|
| 741 |
// The displayed team name comes from the config_abbrev field
|
|
@@ -798,7 +901,9 @@ unsafe fn push_cstr<const N: usize>(s: &mut heapless::String<N>, cstr: *const u8
|
|
| 798 |
/// Length of a null-terminated C string.
|
| 799 |
unsafe fn c_strlen(s: *const u8) -> usize {
|
| 800 |
let mut len = 0;
|
| 801 |
-
while *s.add(len) != 0 {
|
|
|
|
|
|
|
| 802 |
len
|
| 803 |
}
|
| 804 |
|
|
@@ -808,8 +913,8 @@ unsafe fn c_strlen(s: *const u8) -> usize {
|
|
| 808 |
#[unsafe(naked)]
|
| 809 |
unsafe extern "cdecl" fn call_usercall_eax(_eax_val: *mut GameInfo, _func: u32) {
|
| 810 |
core::arch::naked_asm!(
|
| 811 |
-
"mov eax, [esp+4]",
|
| 812 |
-
"mov ecx, [esp+8]",
|
| 813 |
"call ecx",
|
| 814 |
"ret",
|
| 815 |
);
|
|
@@ -821,8 +926,8 @@ unsafe extern "cdecl" fn call_usercall_esi(_esi_val: u32, _func: u32) {
|
|
| 821 |
core::arch::naked_asm!(
|
| 822 |
"push esi",
|
| 823 |
"push edi",
|
| 824 |
-
"mov esi, [esp+12]",
|
| 825 |
-
"mov eax, [esp+16]",
|
| 826 |
"call eax",
|
| 827 |
"pop edi",
|
| 828 |
"pop esi",
|
|
@@ -833,16 +938,14 @@ unsafe extern "cdecl" fn call_usercall_esi(_esi_val: u32, _func: u32) {
|
|
| 833 |
/// Bridge for RegisterObserver: usercall(ESI=array) + stdcall(1 param=data_ptr).
|
| 834 |
/// cdecl(esi_val, data_ptr, func_addr).
|
| 835 |
#[unsafe(naked)]
|
| 836 |
-
unsafe extern "cdecl" fn call_register_observer(
|
| 837 |
-
_esi_val: u32, _data_ptr: u32, _func: u32,
|
| 838 |
-
) {
|
| 839 |
core::arch::naked_asm!(
|
| 840 |
"push esi",
|
| 841 |
"push edi",
|
| 842 |
-
"mov esi, [esp+12]",
|
| 843 |
-
"push [esp+16]",
|
| 844 |
-
"mov eax, [esp+24]",
|
| 845 |
-
"call eax",
|
| 846 |
"pop edi",
|
| 847 |
"pop esi",
|
| 848 |
"ret",
|
|
@@ -860,7 +963,12 @@ unsafe fn call_process_scheme_defaults(gi: *mut GameInfo, func: u32) {
|
|
| 860 |
|
| 861 |
unsafe extern "stdcall" fn hook_parse_replay_position(input: *const u8) -> i32 {
|
| 862 |
let mut len = 0usize;
|
| 863 |
-
while *input.add(len) != 0 {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 864 |
let slice = core::slice::from_raw_parts(input, len + 1);
|
| 865 |
replay::parse_replay_position(slice)
|
| 866 |
}
|
|
@@ -869,8 +977,16 @@ unsafe extern "stdcall" fn hook_parse_replay_position(input: *const u8) -> i32 {
|
|
| 869 |
|
| 870 |
pub fn install() -> Result<(), String> {
|
| 871 |
unsafe {
|
| 872 |
-
REPLAY_LOADER_ORIG = hook::install(
|
| 873 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 874 |
}
|
| 875 |
Ok(())
|
| 876 |
}
|
|
|
|
| 7 |
use crate::log_line;
|
| 8 |
use openwa_core::address::va;
|
| 9 |
use openwa_core::engine::game_info::GameInfo;
|
|
|
|
| 10 |
use openwa_core::engine::replay::{self, ReplayError, ReplayStream, ReplayTeamEntry};
|
| 11 |
+
use openwa_core::frontend::MapView;
|
| 12 |
use openwa_core::rebase::rb;
|
| 13 |
use openwa_core::wa_alloc::{wa_free, wa_malloc};
|
| 14 |
|
|
|
|
| 48 |
core::mem::transmute(rb(va::WA_GET_OSFHANDLE));
|
| 49 |
|
| 50 |
let fd = fileno(file);
|
| 51 |
+
if fd < 0 {
|
| 52 |
+
return None;
|
| 53 |
+
}
|
| 54 |
let handle = get_osfhandle(fd);
|
| 55 |
+
if handle == -1 || handle == -2 {
|
| 56 |
+
return None;
|
| 57 |
+
}
|
| 58 |
+
Some(ManuallyDrop::new(File::from_raw_handle(
|
| 59 |
+
handle as *mut core::ffi::c_void,
|
| 60 |
+
)))
|
| 61 |
}
|
| 62 |
|
| 63 |
/// Load a WA string resource by ID (stdcall). Returns null-terminated C string.
|
|
|
|
| 77 |
|
| 78 |
// βββ RAII cleanup guard (for wa_malloc'd payload) βββββββββββββββββββββββββββ
|
| 79 |
|
| 80 |
+
struct PayloadGuard {
|
| 81 |
+
payload: *mut u8,
|
| 82 |
+
}
|
| 83 |
impl Drop for PayloadGuard {
|
| 84 |
fn drop(&mut self) {
|
| 85 |
+
unsafe {
|
| 86 |
+
if !self.payload.is_null() {
|
| 87 |
+
wa_free(self.payload);
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
}
|
| 91 |
}
|
| 92 |
|
|
|
|
| 108 |
|
| 109 |
unsafe fn replay_loader_play(gi: *mut GameInfo) -> Result<(), ReplayError> {
|
| 110 |
let artclass_counter = *(rb(va::G_ARTCLASS_COUNTER) as *const i32);
|
| 111 |
+
if artclass_counter >= 0x34 {
|
| 112 |
+
return Err(ReplayError::ArtClassLimit);
|
| 113 |
+
}
|
| 114 |
(*gi).replay_active = 1;
|
| 115 |
(*gi).replay_field_ef60 = 0;
|
| 116 |
|
|
|
|
| 127 |
}
|
| 128 |
};
|
| 129 |
|
| 130 |
+
let mut file = File::open(&replay_path).map_err(|_| ReplayError::FileNotFound)?;
|
| 131 |
+
let file_size = file
|
| 132 |
+
.metadata()
|
| 133 |
+
.map_err(|_| ReplayError::FileNotFound)?
|
| 134 |
+
.len();
|
| 135 |
|
| 136 |
// Helper: read exact bytes from file into a typed value
|
| 137 |
fn read_u32(f: &mut File) -> Result<u32, ReplayError> {
|
| 138 |
let mut buf = [0u8; 4];
|
| 139 |
+
f.read_exact(&mut buf)
|
| 140 |
+
.map_err(|_| ReplayError::InvalidFormat)?;
|
| 141 |
Ok(u32::from_le_bytes(buf))
|
| 142 |
}
|
| 143 |
|
| 144 |
// Header
|
| 145 |
let header = read_u32(&mut file)?;
|
| 146 |
+
if (header & 0xFFFF) != replay::REPLAY_MAGIC as u32 {
|
| 147 |
+
return Err(ReplayError::InvalidFormat);
|
| 148 |
+
}
|
| 149 |
let version = header >> 16;
|
| 150 |
+
if version == 0 || version > 20 {
|
| 151 |
+
return Err(ReplayError::VersionTooNew);
|
| 152 |
+
}
|
| 153 |
|
| 154 |
(*gi).replay_format_version = version;
|
| 155 |
(*gi).replay_format_version_2 = version;
|
|
|
|
| 157 |
|
| 158 |
// First payload
|
| 159 |
let payload_size = read_u32(&mut file)?;
|
| 160 |
+
if (payload_size as u64 + 8) > file_size {
|
| 161 |
+
return Err(ReplayError::InvalidFormat);
|
| 162 |
+
}
|
| 163 |
let payload = wa_malloc(payload_size);
|
| 164 |
+
if payload.is_null() {
|
| 165 |
+
return Err(ReplayError::MallocFailure);
|
| 166 |
+
}
|
| 167 |
let mut guard = PayloadGuard { payload };
|
| 168 |
let payload_slice = core::slice::from_raw_parts_mut(payload, payload_size as usize);
|
| 169 |
+
file.read_exact(payload_slice)
|
| 170 |
+
.map_err(|_| ReplayError::FileNotFound)?;
|
| 171 |
|
| 172 |
let first_dword = *(payload as *const i32);
|
| 173 |
(*gi).replay_map_type = first_dword;
|
|
|
|
| 182 |
*(payload.add(8) as *const i32);
|
| 183 |
} else if first_dword == -2 {
|
| 184 |
let n = payload_size.saturating_sub(8) as usize;
|
| 185 |
+
if n > 0x20 {
|
| 186 |
+
return Err(ReplayError::InvalidFormat);
|
| 187 |
+
}
|
| 188 |
+
ptr::copy_nonoverlapping(payload.add(8), (*gi).replay_payload_extra.as_mut_ptr(), n);
|
|
|
|
|
|
|
| 189 |
// Null-terminate: write 0 at replay_map_type + payload_size offset
|
| 190 |
// (the original uses base+0xDB1C+payload_size which spans into replay_payload_extra)
|
| 191 |
*((gi as *mut u8).add(0xDB1C + payload_size as usize)) = 0;
|
| 192 |
}
|
| 193 |
}
|
| 194 |
+
wa_free(payload);
|
| 195 |
+
guard.payload = ptr::null_mut();
|
| 196 |
|
| 197 |
// Clear global buffers
|
| 198 |
ptr::write_bytes(rb(va::G_TEAM_HEADER_DATA) as *mut u8, 0, 0x5728);
|
| 199 |
ptr::write_bytes(rb(va::G_TEAM_SECONDARY_DATA) as *mut u8, 0, 0xD9DC);
|
| 200 |
|
| 201 |
+
let _ = log_line(&format!(
|
| 202 |
+
"[Replay] Header: ver={version} payload={payload_size} sub={first_dword}"
|
| 203 |
+
));
|
| 204 |
|
| 205 |
if version == 1 {
|
| 206 |
drop(file);
|
|
|
|
| 210 |
// βββ Version 2+: read second payload βββββββββββββββββββββββββββββββββ
|
| 211 |
|
| 212 |
let second_size = read_u32(&mut file)?;
|
| 213 |
+
if (4u64 + 4 + payload_size as u64 + 4 + second_size as u64) > file_size {
|
| 214 |
+
return Err(ReplayError::InvalidFormat);
|
| 215 |
+
}
|
| 216 |
let p2 = wa_malloc(second_size);
|
| 217 |
+
if p2.is_null() {
|
| 218 |
+
return Err(ReplayError::MallocFailure);
|
| 219 |
+
}
|
| 220 |
guard.payload = p2;
|
| 221 |
let p2_slice = core::slice::from_raw_parts_mut(p2, second_size as usize);
|
| 222 |
+
file.read_exact(p2_slice)
|
| 223 |
+
.map_err(|_| ReplayError::InvalidFormat)?;
|
| 224 |
drop(file); // Done reading β close replay file
|
| 225 |
|
| 226 |
// Parse and write to globals
|
| 227 |
let data = core::slice::from_raw_parts(p2, second_size as usize);
|
| 228 |
let result = parse_and_write_v2plus(gi, data, version);
|
| 229 |
+
wa_free(p2);
|
| 230 |
+
guard.payload = ptr::null_mut();
|
| 231 |
|
| 232 |
match result {
|
| 233 |
Ok(()) => {
|
|
|
|
| 242 |
Ok(())
|
| 243 |
}
|
| 244 |
Err(e) => {
|
| 245 |
+
let _ = log_line(&format!(
|
| 246 |
+
"[Replay] Parse failed ({e:?}), falling back to original"
|
| 247 |
+
));
|
| 248 |
delegate_to_original(gi)
|
| 249 |
}
|
| 250 |
}
|
|
|
|
| 255 |
}
|
| 256 |
|
| 257 |
unsafe fn delegate_to_original(gi: *mut GameInfo) -> Result<(), ReplayError> {
|
| 258 |
+
let orig: unsafe extern "stdcall" fn(*mut GameInfo, i32) -> u32 =
|
| 259 |
+
core::mem::transmute(REPLAY_LOADER_ORIG);
|
| 260 |
let result = orig(gi, 1);
|
| 261 |
+
if result == 0 {
|
| 262 |
+
Ok(())
|
| 263 |
+
} else {
|
| 264 |
+
Err(ReplayError::FileNotFound)
|
| 265 |
+
}
|
| 266 |
}
|
| 267 |
|
| 268 |
// βββ Version 2+ parser that writes to globals βββββββββββββββββββββββββββββββ
|
| 269 |
|
| 270 |
/// Helper: write byte to rebased global address.
|
| 271 |
#[inline]
|
| 272 |
+
unsafe fn wb(addr: u32, val: u8) {
|
| 273 |
+
*(rb(addr) as *mut u8) = val;
|
| 274 |
+
}
|
| 275 |
/// Helper: write u32 to rebased global address.
|
| 276 |
#[inline]
|
| 277 |
+
unsafe fn wd(addr: u32, val: u32) {
|
| 278 |
+
*(rb(addr) as *mut u32) = val;
|
| 279 |
+
}
|
| 280 |
|
| 281 |
unsafe fn parse_and_write_v2plus(
|
| 282 |
+
gi: *mut GameInfo,
|
| 283 |
+
data: &[u8],
|
| 284 |
+
version: u32,
|
| 285 |
) -> Result<(), ReplayError> {
|
| 286 |
let mut s = ReplayStream::new(data);
|
| 287 |
|
|
|
|
| 296 |
if version >= 10 {
|
| 297 |
let sub_format = s.read_u16()?;
|
| 298 |
wd(va::G_REPLAY_SUB_FORMAT, sub_format as u32);
|
| 299 |
+
if sub_format != 0 {
|
| 300 |
+
return Err(ReplayError::VersionTooNew);
|
| 301 |
+
}
|
| 302 |
|
| 303 |
if version >= 12 {
|
| 304 |
if version < 18 {
|
|
|
|
| 308 |
let raw = s.read_u8_validated(0, 3)?;
|
| 309 |
if (2..=3).contains(&raw) {
|
| 310 |
wb(va::G_REPLAY_GAME_MODE, raw - 1);
|
| 311 |
+
wb(va::G_REPLAY_VER_FLAG_A, 1);
|
| 312 |
+
wb(va::G_REPLAY_VER_FLAG_B, 1);
|
| 313 |
} else {
|
| 314 |
wb(va::G_REPLAY_VER_FLAG_A, (raw != 0) as u8);
|
| 315 |
wb(va::G_REPLAY_GAME_MODE, 0);
|
|
|
|
| 333 |
let obs_data: [u32; 4] = [team_id, 0, obs_type as u32, 0];
|
| 334 |
// RegisterObserver: usercall(ESI=0x88C35C) + stdcall(1 param = &data). RET 0x4.
|
| 335 |
call_register_observer(
|
| 336 |
+
rb(va::G_OBSERVER_ARRAY),
|
| 337 |
+
obs_data.as_ptr() as u32,
|
| 338 |
+
rb(va::REPLAY_REGISTER_OBSERVER),
|
| 339 |
);
|
| 340 |
if obs_type == 0 {
|
| 341 |
// The terminating entry's team_id is the replay timestamp (time_t)
|
|
|
|
| 347 |
|
| 348 |
let game_version_id = s.read_i32()?;
|
| 349 |
wd(va::G_REPLAY_VERSION_ID, game_version_id as u32);
|
| 350 |
+
if (game_version_id.wrapping_add(4) as u32) > 0x1F8 {
|
| 351 |
+
return Err(ReplayError::VersionTooNew);
|
| 352 |
+
}
|
| 353 |
let use_fixed_names = game_version_id < 10;
|
| 354 |
|
| 355 |
// ββ Scheme presence ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 384 |
// SCHM magic + version
|
| 385 |
let magic = s.read_u32()?;
|
| 386 |
let scheme_version = s.read_u8()?;
|
| 387 |
+
if magic != 0x4D484353 {
|
| 388 |
+
return Err(ReplayError::InvalidFormat);
|
| 389 |
+
}
|
| 390 |
|
| 391 |
let scheme_data_size = match scheme_version {
|
| 392 |
1 => 0xD8_usize,
|
|
|
|
| 398 |
// Copy defaults first for v3
|
| 399 |
ptr::copy_nonoverlapping(
|
| 400 |
rb(va::SCHEME_V3_DEFAULTS) as *const u8,
|
| 401 |
+
rb(va::G_SCHEME_V3_DATA) as *mut u8,
|
| 402 |
+
0x6E,
|
| 403 |
);
|
| 404 |
(scheme_size_indicator as usize) - 5
|
| 405 |
}
|
|
|
|
| 408 |
|
| 409 |
// Copy scheme data from stream to global 0x88DAE0
|
| 410 |
let scheme_slice = s.advance_raw(scheme_data_size)?;
|
| 411 |
+
ptr::copy_nonoverlapping(
|
| 412 |
+
scheme_slice.as_ptr(),
|
| 413 |
+
rb(va::G_SCHEME_DATA) as *mut u8,
|
| 414 |
+
scheme_data_size,
|
| 415 |
+
);
|
| 416 |
|
| 417 |
let header_val = *(rb(va::G_SCHEME_HEADER) as *const i8);
|
| 418 |
|
|
|
|
| 426 |
if scheme_version <= 2 {
|
| 427 |
ptr::copy_nonoverlapping(
|
| 428 |
rb(va::SCHEME_V3_DEFAULTS) as *const u8,
|
| 429 |
+
rb(va::G_SCHEME_V3_DATA) as *mut u8,
|
| 430 |
+
0x6E,
|
| 431 |
);
|
| 432 |
}
|
| 433 |
}
|
|
|
|
| 437 |
let validate: unsafe extern "cdecl" fn() -> i32 =
|
| 438 |
core::mem::transmute(rb(va::SCHEME_VALIDATE_EXTENDED));
|
| 439 |
let r = validate();
|
| 440 |
+
if r != 0 {
|
| 441 |
+
return Err(ReplayError::InvalidFormat);
|
| 442 |
+
}
|
| 443 |
}
|
| 444 |
|
| 445 |
// Random seed: the stream value REPLACES the current seed.
|
|
|
|
| 478 |
let base = va::G_PLAYER_ARRAY + 0x74 + i * 0x78; // player[i].flag
|
| 479 |
let flag = s.read_u8()?;
|
| 480 |
wb(base, flag);
|
| 481 |
+
if flag == 0 {
|
| 482 |
+
continue;
|
| 483 |
+
}
|
| 484 |
|
| 485 |
// NOTE: original checks if i == host_player_index here (sets a local flag, unused)
|
| 486 |
player_count += 1;
|
| 487 |
|
| 488 |
let mut name = [0u8; 0x11];
|
| 489 |
s.read_prefixed_string(&mut name)?;
|
| 490 |
+
ptr::copy_nonoverlapping(
|
| 491 |
+
name.as_ptr(),
|
| 492 |
+
rb(va::G_PLAYER_ARRAY + i * 0x78) as *mut u8,
|
| 493 |
+
0x11,
|
| 494 |
+
);
|
| 495 |
|
| 496 |
let mut display = [0u8; 0x31];
|
| 497 |
s.read_prefixed_string(&mut display)?;
|
| 498 |
+
ptr::copy_nonoverlapping(
|
| 499 |
+
display.as_ptr(),
|
| 500 |
+
rb(va::G_PLAYER_ARRAY + 0x11 + i * 0x78) as *mut u8,
|
| 501 |
+
0x31,
|
| 502 |
+
);
|
| 503 |
|
| 504 |
let mut config = [0u8; 0x29];
|
| 505 |
s.read_prefixed_string(&mut config)?;
|
| 506 |
+
ptr::copy_nonoverlapping(
|
| 507 |
+
config.as_ptr(),
|
| 508 |
+
rb(va::G_PLAYER_ARRAY + 0x42 + i * 0x78) as *mut u8,
|
| 509 |
+
0x29,
|
| 510 |
+
);
|
| 511 |
|
| 512 |
let u16_val = s.read_u16()?;
|
| 513 |
// Ghidra shows (&DAT[i*0x3C]) with u16 elements β byte offset = i*0x3C*2 = i*0x78
|
|
|
|
| 537 |
let team = &mut *teams.add(team_idx);
|
| 538 |
let team_flag = s.read_u8()?;
|
| 539 |
team.flag = team_flag;
|
| 540 |
+
if team_flag == 0 {
|
| 541 |
+
continue;
|
| 542 |
+
}
|
| 543 |
team_count += 1;
|
| 544 |
+
|
| 545 |
let team_type = s.read_u8()? as i8;
|
| 546 |
+
if !replay::validate_team_type(team_type) {
|
| 547 |
+
return Err(ReplayError::InvalidFormat);
|
| 548 |
+
}
|
| 549 |
team.team_type = team_type as u8;
|
| 550 |
team.alliance = s.read_u8_validated(0, 5)?;
|
| 551 |
team.unknown_02 = s.read_u8()?;
|
|
|
|
| 577 |
s.read_prefixed_string(&mut team.config_name)?;
|
| 578 |
|
| 579 |
let worm_count = s.read_u8()?;
|
| 580 |
+
if worm_count == 0 || worm_count > 8 {
|
| 581 |
+
return Err(ReplayError::InvalidFormat);
|
| 582 |
+
}
|
| 583 |
team.worm_count = worm_count;
|
| 584 |
team.color = s.read_u8()?;
|
| 585 |
team.flag2 = s.read_u8()?;
|
|
|
|
| 598 |
ptr::copy_nonoverlapping(w4.as_ptr(), team.weapons.as_mut_ptr().add(0x954), 0x300);
|
| 599 |
}
|
| 600 |
|
| 601 |
+
if team_count == 0 {
|
| 602 |
+
return Err(ReplayError::InvalidFormat);
|
| 603 |
+
}
|
| 604 |
|
| 605 |
wb(va::G_TEAM_COUNT, team_count);
|
| 606 |
|
|
|
|
| 623 |
if map_seed != 0 {
|
| 624 |
// Per-team weapon config reads from stream β not yet implemented in Rust.
|
| 625 |
// Fall back to the original parser which handles this from scratch.
|
| 626 |
+
let _ = log_line(&format!(
|
| 627 |
+
"[Replay] Non-zero map_seed (0x{:04X}), delegating to original",
|
| 628 |
+
map_seed
|
| 629 |
+
));
|
| 630 |
return Err(ReplayError::InvalidFormat); // triggers delegate_to_original
|
| 631 |
}
|
| 632 |
|
|
|
|
| 642 |
srand(current_seed);
|
| 643 |
let r1 = rand_fn() as u32;
|
| 644 |
let r2 = rand_fn() as u32;
|
| 645 |
+
wd(va::G_RANDOM_SEED, r2 + (r1 << 16)); // new seed
|
| 646 |
+
wd(va::G_SAVED_RANDOM_SEED, current_seed); // save old seed
|
| 647 |
|
| 648 |
let ver = *(rb(va::G_REPLAY_VERSION_ID) as *const i32);
|
| 649 |
if ver != 0x22 && !(0x29..=0x2A).contains(&ver) && ver < 0x2D {
|
|
|
|
| 698 |
|
| 699 |
// βββ Log output formatting ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 700 |
|
|
|
|
|
|
|
| 701 |
/// Write the /getlog replay header to the log file.
|
| 702 |
unsafe fn write_replay_log(gi: *const GameInfo, log_file: &mut File) -> Result<(), ReplayError> {
|
| 703 |
use std::io::Write as IoWrite;
|
|
|
|
| 721 |
if !tm.is_null() {
|
| 722 |
let tm = &*tm;
|
| 723 |
let mut s = heapless::String::<128>::new();
|
| 724 |
+
let _ = writeln!(
|
| 725 |
+
s,
|
| 726 |
"Game Started at {:04}-{:02}-{:02} {:02}:{:02}:{:02} GMT",
|
| 727 |
+
tm[5] + 1900,
|
| 728 |
+
tm[4] + 1,
|
| 729 |
+
tm[3],
|
| 730 |
+
tm[2],
|
| 731 |
+
tm[1],
|
| 732 |
+
tm[0]
|
| 733 |
);
|
| 734 |
let _ = log_file.write_all(s.as_bytes());
|
| 735 |
}
|
|
|
|
| 749 |
-4 => rb(0x650EAC) as *const u8, // "1.0"
|
| 750 |
-2 => rb(0x650EB0) as *const u8, // "3.0"
|
| 751 |
-1 => rb(0x650EB4) as *const u8, // "3.5 Beta 1"
|
| 752 |
+
_ => wa_load_string(0x0F), // "Unknown"
|
| 753 |
}
|
| 754 |
} else {
|
| 755 |
let table = rb(va::VERSION_STRING_TABLE) as *const u32;
|
|
|
|
| 776 |
// ββ "Exported with Version" line βββββββββββββββββββββββββββββββββββββ
|
| 777 |
{
|
| 778 |
let label_exported = wa_load_string(0x6E9); // "Exported with Version"
|
| 779 |
+
// Current WA version from version table: 0x699814[DAT_00697702]
|
| 780 |
let ver_byte = *(rb(va::G_VERSION_BYTE) as *const u8) as u32;
|
| 781 |
// "3.8.1" literal at 0x641C60 + suffix from version table
|
| 782 |
let ver_literal = rb(va::STR_VERSION_381) as *const u8; // "3.8.1"
|
|
|
|
| 791 |
let _ = log_file.write_all(s.as_bytes());
|
| 792 |
}
|
| 793 |
|
|
|
|
| 794 |
// ββ Team listing βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 795 |
// For each team: "Color: "TeamName" [CPU X.XX]\n" or similar
|
| 796 |
|
|
|
|
| 809 |
let mut max_name_len = 0usize;
|
| 810 |
for slot in 0..6usize {
|
| 811 |
let team = &*teams.add(slot);
|
| 812 |
+
if team.flag == 0 {
|
| 813 |
+
continue;
|
| 814 |
+
}
|
| 815 |
let ci = team.alliance as usize;
|
| 816 |
if ci < 6 {
|
| 817 |
let len = c_strlen(color_names[ci]);
|
| 818 |
+
if len > max_color_len {
|
| 819 |
+
max_color_len = len;
|
| 820 |
+
}
|
| 821 |
}
|
| 822 |
let name_len = c_strlen(team.config_abbrev.as_ptr());
|
| 823 |
+
if name_len > max_name_len {
|
| 824 |
+
max_name_len = name_len;
|
| 825 |
+
}
|
| 826 |
}
|
| 827 |
|
| 828 |
for slot in 0..6usize {
|
| 829 |
let team = &*teams.add(slot);
|
| 830 |
+
if team.flag == 0 {
|
| 831 |
+
continue;
|
| 832 |
+
}
|
| 833 |
|
| 834 |
let team_type = team.team_type as i8;
|
| 835 |
let color_idx = team.alliance as usize;
|
| 836 |
|
| 837 |
+
let color = if color_idx < 6 {
|
| 838 |
+
color_names[color_idx]
|
| 839 |
+
} else {
|
| 840 |
+
color_names[0]
|
| 841 |
+
};
|
| 842 |
let clen = c_strlen(color);
|
| 843 |
|
| 844 |
// The displayed team name comes from the config_abbrev field
|
|
|
|
| 901 |
/// Length of a null-terminated C string.
|
| 902 |
unsafe fn c_strlen(s: *const u8) -> usize {
|
| 903 |
let mut len = 0;
|
| 904 |
+
while *s.add(len) != 0 {
|
| 905 |
+
len += 1;
|
| 906 |
+
}
|
| 907 |
len
|
| 908 |
}
|
| 909 |
|
|
|
|
| 913 |
#[unsafe(naked)]
|
| 914 |
unsafe extern "cdecl" fn call_usercall_eax(_eax_val: *mut GameInfo, _func: u32) {
|
| 915 |
core::arch::naked_asm!(
|
| 916 |
+
"mov eax, [esp+4]", // eax_val
|
| 917 |
+
"mov ecx, [esp+8]", // func_addr
|
| 918 |
"call ecx",
|
| 919 |
"ret",
|
| 920 |
);
|
|
|
|
| 926 |
core::arch::naked_asm!(
|
| 927 |
"push esi",
|
| 928 |
"push edi",
|
| 929 |
+
"mov esi, [esp+12]", // esi_val
|
| 930 |
+
"mov eax, [esp+16]", // func_addr
|
| 931 |
"call eax",
|
| 932 |
"pop edi",
|
| 933 |
"pop esi",
|
|
|
|
| 938 |
/// Bridge for RegisterObserver: usercall(ESI=array) + stdcall(1 param=data_ptr).
|
| 939 |
/// cdecl(esi_val, data_ptr, func_addr).
|
| 940 |
#[unsafe(naked)]
|
| 941 |
+
unsafe extern "cdecl" fn call_register_observer(_esi_val: u32, _data_ptr: u32, _func: u32) {
|
|
|
|
|
|
|
| 942 |
core::arch::naked_asm!(
|
| 943 |
"push esi",
|
| 944 |
"push edi",
|
| 945 |
+
"mov esi, [esp+12]", // esi_val (0x88C35C rebased)
|
| 946 |
+
"push [esp+16]", // data_ptr β stdcall param
|
| 947 |
+
"mov eax, [esp+24]", // func_addr (shifted by push)
|
| 948 |
+
"call eax", // stdcall cleans 1 param (4 bytes)
|
| 949 |
"pop edi",
|
| 950 |
"pop esi",
|
| 951 |
"ret",
|
|
|
|
| 963 |
|
| 964 |
unsafe extern "stdcall" fn hook_parse_replay_position(input: *const u8) -> i32 {
|
| 965 |
let mut len = 0usize;
|
| 966 |
+
while *input.add(len) != 0 {
|
| 967 |
+
len += 1;
|
| 968 |
+
if len > 256 {
|
| 969 |
+
break;
|
| 970 |
+
}
|
| 971 |
+
}
|
| 972 |
let slice = core::slice::from_raw_parts(input, len + 1);
|
| 973 |
replay::parse_replay_position(slice)
|
| 974 |
}
|
|
|
|
| 977 |
|
| 978 |
pub fn install() -> Result<(), String> {
|
| 979 |
unsafe {
|
| 980 |
+
REPLAY_LOADER_ORIG = hook::install(
|
| 981 |
+
"ReplayLoader",
|
| 982 |
+
va::REPLAY_LOADER,
|
| 983 |
+
hook_replay_loader as *const (),
|
| 984 |
+
)? as *const ();
|
| 985 |
+
PARSE_POSITION_ORIG = hook::install(
|
| 986 |
+
"ParseReplayPosition",
|
| 987 |
+
va::PARSE_REPLAY_POSITION,
|
| 988 |
+
hook_parse_replay_position as *const (),
|
| 989 |
+
)? as *const ();
|
| 990 |
}
|
| 991 |
Ok(())
|
| 992 |
}
|
crates/openwa-wormkit/src/replacements/task/cloud.rs
CHANGED
|
@@ -69,7 +69,8 @@ unsafe extern "thiscall" fn cloud_handle_message(
|
|
| 69 |
if (*ddgame).render_phase == 5 {
|
| 70 |
// Compute parallax X offset: (vel_x + wind * 10) * parallax_scale
|
| 71 |
let scroll_speed = (*this).vel_x.0 + (*this).wind_accel.0 * 10;
|
| 72 |
-
let parallax_x =
|
|
|
|
| 73 |
let x = parallax_x + (*this).pos_x.0;
|
| 74 |
|
| 75 |
let rq = &mut *(*ddgame).render_queue;
|
|
|
|
| 69 |
if (*ddgame).render_phase == 5 {
|
| 70 |
// Compute parallax X offset: (vel_x + wind * 10) * parallax_scale
|
| 71 |
let scroll_speed = (*this).vel_x.0 + (*this).wind_accel.0 * 10;
|
| 72 |
+
let parallax_x =
|
| 73 |
+
((scroll_speed as i64 * (*ddgame).parallax_scale as i64) >> 16) as i32;
|
| 74 |
let x = parallax_x + (*this).pos_x.0;
|
| 75 |
|
| 76 |
let rq = &mut *(*ddgame).render_queue;
|
crates/openwa-wormkit/src/replacements/weapon_release.rs
CHANGED
|
@@ -98,7 +98,10 @@ fn is_weapon_category_a(weapon: Weapon) -> bool {
|
|
| 98 |
/// FUN_00565920: weapon category B β fire/napalm weapons.
|
| 99 |
fn is_weapon_category_b(weapon: Weapon) -> bool {
|
| 100 |
use Weapon::*;
|
| 101 |
-
matches!(
|
|
|
|
|
|
|
|
|
|
| 102 |
}
|
| 103 |
|
| 104 |
// ββ Main implementation βββββββββββββββββββββββββββββββββββββ
|
|
@@ -152,7 +155,7 @@ unsafe extern "cdecl" fn weapon_release_impl(
|
|
| 152 |
let special_subtype = (*entry).special_subtype;
|
| 153 |
let fire_method = (*entry).fire_method;
|
| 154 |
|
| 155 |
-
use openwa_core::game::weapon::{
|
| 156 |
|
| 157 |
// ββ 3. Spawn offset calculation βββββββββββββββββββββββββ
|
| 158 |
let scale = w.landscape_scale;
|
|
@@ -243,11 +246,7 @@ unsafe extern "cdecl" fn weapon_release_impl(
|
|
| 243 |
write_u32(&mut msg_buf, 0x0C, w.shot_data_2);
|
| 244 |
write_u32(&mut msg_buf, 0x10, w.fire_sync_frame_1 as u32);
|
| 245 |
write_u32(&mut msg_buf, 0x14, w.fire_sync_frame_2 as u32);
|
| 246 |
-
write_u32(
|
| 247 |
-
&mut msg_buf,
|
| 248 |
-
0x18,
|
| 249 |
-
if w._unknown_2cc != 0 { 1 } else { 0 },
|
| 250 |
-
);
|
| 251 |
write_u32(&mut msg_buf, 0x1C, weapon as u32);
|
| 252 |
|
| 253 |
let team = weapon::lookup_turn_game(worm);
|
|
@@ -292,47 +291,63 @@ unsafe extern "cdecl" fn weapon_release_impl(
|
|
| 292 |
let entry = w.active_weapon_entry;
|
| 293 |
|
| 294 |
match FireType::try_from((*entry).fire_type) {
|
| 295 |
-
Ok(FireType::Projectile) => {
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
sound::play_worm_sound(worm, SoundId(0x1004E), Fixed::ONE);
|
| 300 |
-
}
|
| 301 |
-
do_effect = true;
|
| 302 |
-
effect_state = 0x73;
|
| 303 |
-
}
|
| 304 |
-
2 => {
|
| 305 |
-
sound::play_sound_local(task, KnownSoundId::ThrowRelease, 3, Fixed::ONE, Fixed::ONE);
|
| 306 |
-
sound::stop_worm_sound(worm);
|
| 307 |
}
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
|
|
|
|
|
|
| 332 |
}
|
| 333 |
-
|
|
|
|
| 334 |
}
|
| 335 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
Ok(FireType::Rope) => {
|
| 337 |
if w._unknown_2cc == 0 || w._unknown_2c8 == 1 {
|
| 338 |
let team_sound_raw = (*w.ddgame()).team_sound_id(team_id);
|
|
@@ -345,19 +360,41 @@ unsafe extern "cdecl" fn weapon_release_impl(
|
|
| 345 |
match S::try_from((*entry).special_subtype) {
|
| 346 |
Ok(S::BaseballBat) => {
|
| 347 |
sound::play_sound_local(
|
| 348 |
-
task,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 349 |
);
|
| 350 |
}
|
| 351 |
Ok(S::DragonBall) => {
|
| 352 |
-
sound::play_sound_local(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
}
|
| 354 |
Ok(S::Kamikaze) => {
|
| 355 |
// Sound ID from fire_params.spread (polymorphic use of field)
|
| 356 |
-
sound::play_sound_local(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
}
|
| 358 |
Ok(S::Teleport) => {
|
| 359 |
if w._unknown_208 == 0 {
|
| 360 |
-
sound::play_sound_local(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
}
|
| 362 |
}
|
| 363 |
Ok(S::Blowtorch) => {
|
|
@@ -390,8 +427,16 @@ unsafe extern "cdecl" fn weapon_release_impl(
|
|
| 390 |
let state_flag = facing_flag + effect_state;
|
| 391 |
|
| 392 |
spawn_effect(
|
| 393 |
-
worm,
|
| 394 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
);
|
| 396 |
}
|
| 397 |
|
|
@@ -454,9 +499,8 @@ pub(crate) unsafe fn spawn_effect(
|
|
| 454 |
let entity = table.lookup(0, 0x1A);
|
| 455 |
if !entity.is_null() {
|
| 456 |
let vtable = *(entity as *const *const usize);
|
| 457 |
-
let handle_msg: unsafe extern "thiscall" fn(
|
| 458 |
-
*
|
| 459 |
-
) = core::mem::transmute(*vtable.add(2));
|
| 460 |
handle_msg(
|
| 461 |
entity as *mut u8,
|
| 462 |
worm as *mut u8,
|
|
|
|
| 98 |
/// FUN_00565920: weapon category B β fire/napalm weapons.
|
| 99 |
fn is_weapon_category_b(weapon: Weapon) -> bool {
|
| 100 |
use Weapon::*;
|
| 101 |
+
matches!(
|
| 102 |
+
weapon,
|
| 103 |
+
NapalmStrike | FlameThrower | PetrolBomb | SheepStrike
|
| 104 |
+
)
|
| 105 |
}
|
| 106 |
|
| 107 |
// ββ Main implementation βββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 155 |
let special_subtype = (*entry).special_subtype;
|
| 156 |
let fire_method = (*entry).fire_method;
|
| 157 |
|
| 158 |
+
use openwa_core::game::weapon::{FireMethod, FireType};
|
| 159 |
|
| 160 |
// ββ 3. Spawn offset calculation βββββββββββββββββββββββββ
|
| 161 |
let scale = w.landscape_scale;
|
|
|
|
| 246 |
write_u32(&mut msg_buf, 0x0C, w.shot_data_2);
|
| 247 |
write_u32(&mut msg_buf, 0x10, w.fire_sync_frame_1 as u32);
|
| 248 |
write_u32(&mut msg_buf, 0x14, w.fire_sync_frame_2 as u32);
|
| 249 |
+
write_u32(&mut msg_buf, 0x18, if w._unknown_2cc != 0 { 1 } else { 0 });
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
write_u32(&mut msg_buf, 0x1C, weapon as u32);
|
| 251 |
|
| 252 |
let team = weapon::lookup_turn_game(worm);
|
|
|
|
| 291 |
let entry = w.active_weapon_entry;
|
| 292 |
|
| 293 |
match FireType::try_from((*entry).fire_type) {
|
| 294 |
+
Ok(FireType::Projectile) => match (*entry).special_subtype {
|
| 295 |
+
1 => {
|
| 296 |
+
if w.sound_handle == 0 {
|
| 297 |
+
sound::play_worm_sound(worm, SoundId(0x1004E), Fixed::ONE);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
}
|
| 299 |
+
do_effect = true;
|
| 300 |
+
effect_state = 0x73;
|
| 301 |
+
}
|
| 302 |
+
2 => {
|
| 303 |
+
sound::play_sound_local(
|
| 304 |
+
task,
|
| 305 |
+
KnownSoundId::ThrowRelease,
|
| 306 |
+
3,
|
| 307 |
+
Fixed::ONE,
|
| 308 |
+
Fixed::ONE,
|
| 309 |
+
);
|
| 310 |
+
sound::stop_worm_sound(worm);
|
| 311 |
+
}
|
| 312 |
+
3 | 7 | 0xB | 0xC => {
|
| 313 |
+
sound::play_sound_local(
|
| 314 |
+
task,
|
| 315 |
+
KnownSoundId::RocketRelease,
|
| 316 |
+
3,
|
| 317 |
+
Fixed::ONE,
|
| 318 |
+
Fixed::ONE,
|
| 319 |
+
);
|
| 320 |
+
sound::stop_worm_sound(worm);
|
| 321 |
+
}
|
| 322 |
+
4 => {
|
| 323 |
+
if w.sound_handle == 0 {
|
| 324 |
+
sound::play_worm_sound(worm, SoundId(0x1004F), Fixed::ONE);
|
| 325 |
}
|
| 326 |
+
do_effect = true;
|
| 327 |
+
effect_state = 0x73;
|
| 328 |
}
|
| 329 |
+
5 => {
|
| 330 |
+
sound::play_sound_local(task, KnownSoundId::ShotgunFire, 3, Fixed::ONE, Fixed::ONE);
|
| 331 |
+
do_effect = true;
|
| 332 |
+
effect_state = 0x75;
|
| 333 |
+
}
|
| 334 |
+
6 => {
|
| 335 |
+
sound::play_sound_local(task, KnownSoundId::HandgunFire, 3, Fixed::ONE, Fixed::ONE);
|
| 336 |
+
do_effect = true;
|
| 337 |
+
effect_state = 0x73;
|
| 338 |
+
}
|
| 339 |
+
10 => {
|
| 340 |
+
sound::play_sound_local(
|
| 341 |
+
task,
|
| 342 |
+
KnownSoundId::LongbowRelease,
|
| 343 |
+
3,
|
| 344 |
+
Fixed::ONE,
|
| 345 |
+
Fixed::ONE,
|
| 346 |
+
);
|
| 347 |
+
sound::stop_worm_sound(worm);
|
| 348 |
+
}
|
| 349 |
+
_ => {}
|
| 350 |
+
},
|
| 351 |
Ok(FireType::Rope) => {
|
| 352 |
if w._unknown_2cc == 0 || w._unknown_2c8 == 1 {
|
| 353 |
let team_sound_raw = (*w.ddgame()).team_sound_id(team_id);
|
|
|
|
| 360 |
match S::try_from((*entry).special_subtype) {
|
| 361 |
Ok(S::BaseballBat) => {
|
| 362 |
sound::play_sound_local(
|
| 363 |
+
task,
|
| 364 |
+
KnownSoundId::BaseballBatRelease,
|
| 365 |
+
3,
|
| 366 |
+
Fixed::ONE,
|
| 367 |
+
Fixed::ONE,
|
| 368 |
);
|
| 369 |
}
|
| 370 |
Ok(S::DragonBall) => {
|
| 371 |
+
sound::play_sound_local(
|
| 372 |
+
task,
|
| 373 |
+
SoundId((*entry).fire_method as u32),
|
| 374 |
+
3,
|
| 375 |
+
Fixed::ONE,
|
| 376 |
+
Fixed::ONE,
|
| 377 |
+
);
|
| 378 |
}
|
| 379 |
Ok(S::Kamikaze) => {
|
| 380 |
// Sound ID from fire_params.spread (polymorphic use of field)
|
| 381 |
+
sound::play_sound_local(
|
| 382 |
+
task,
|
| 383 |
+
SoundId((*entry).fire_params.spread as u32),
|
| 384 |
+
3,
|
| 385 |
+
Fixed::ONE,
|
| 386 |
+
Fixed::ONE,
|
| 387 |
+
);
|
| 388 |
}
|
| 389 |
Ok(S::Teleport) => {
|
| 390 |
if w._unknown_208 == 0 {
|
| 391 |
+
sound::play_sound_local(
|
| 392 |
+
task,
|
| 393 |
+
KnownSoundId::Teleport,
|
| 394 |
+
3,
|
| 395 |
+
Fixed::ONE,
|
| 396 |
+
Fixed::ONE,
|
| 397 |
+
);
|
| 398 |
}
|
| 399 |
}
|
| 400 |
Ok(S::Blowtorch) => {
|
|
|
|
| 427 |
let state_flag = facing_flag + effect_state;
|
| 428 |
|
| 429 |
spawn_effect(
|
| 430 |
+
worm,
|
| 431 |
+
0x80000,
|
| 432 |
+
speed_x,
|
| 433 |
+
speed_y,
|
| 434 |
+
rng_scaled,
|
| 435 |
+
rng1_offset,
|
| 436 |
+
palette,
|
| 437 |
+
state_flag,
|
| 438 |
+
Fixed(0xA0000),
|
| 439 |
+
Fixed(0x1999),
|
| 440 |
);
|
| 441 |
}
|
| 442 |
|
|
|
|
| 499 |
let entity = table.lookup(0, 0x1A);
|
| 500 |
if !entity.is_null() {
|
| 501 |
let vtable = *(entity as *const *const usize);
|
| 502 |
+
let handle_msg: unsafe extern "thiscall" fn(*mut u8, *mut u8, u32, u32, *const u8) =
|
| 503 |
+
core::mem::transmute(*vtable.add(2));
|
|
|
|
| 504 |
handle_msg(
|
| 505 |
entity as *mut u8,
|
| 506 |
worm as *mut u8,
|
crates/openwa-wormkit/src/snapshot.rs
CHANGED
|
@@ -60,19 +60,23 @@ pub unsafe fn capture() -> String {
|
|
| 60 |
// ββ Team blocks + worm entries ββ
|
| 61 |
let team_count = (*ddgame).team_arena.team_count as usize;
|
| 62 |
let _ = writeln!(out, "[Teams] count={}", team_count);
|
| 63 |
-
let arena = openwa_core::engine::ddgame::TeamArenaRef::from_ptr(
|
| 64 |
-
&raw mut (*ddgame).team_arena,
|
| 65 |
-
);
|
| 66 |
let blocks = arena.blocks();
|
| 67 |
for t in 0..team_count {
|
| 68 |
let header = arena.team_header(t);
|
| 69 |
-
let name =
|
| 70 |
-
.to_string_lossy();
|
| 71 |
let _ = writeln!(out, "\n [Team {}] \"{}\"", t, name);
|
| 72 |
-
let _ = writeln!(
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
header.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
// Worms are in block[t+1].worms[0..worm_count] (1-indexed blocks)
|
| 78 |
let worm_count = header.worm_count.max(0) as usize;
|
|
@@ -107,8 +111,11 @@ pub unsafe fn capture() -> String {
|
|
| 107 |
}
|
| 108 |
root = parent;
|
| 109 |
}
|
| 110 |
-
let _ = writeln!(
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
let iter = CTaskBfsIter::new(root);
|
| 114 |
|
|
@@ -150,7 +157,8 @@ pub unsafe fn capture() -> String {
|
|
| 150 |
let class = vtable_name(vt);
|
| 151 |
if class != "Unknown" {
|
| 152 |
if let Some(fields) = openwa_core::registry::struct_fields_for(class) {
|
| 153 |
-
let _ =
|
|
|
|
| 154 |
} else {
|
| 155 |
let _ = write_raw_region(&mut out, task as *const u8, 0x100, 2);
|
| 156 |
}
|
|
@@ -187,7 +195,11 @@ unsafe fn write_registry_fields(
|
|
| 187 |
let pad = " ".repeat(indent);
|
| 188 |
for field in fields.fields {
|
| 189 |
let addr = base as u32 + field.offset;
|
| 190 |
-
write!(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
if openwa_core::mem::can_read(addr, field.size) {
|
| 192 |
let ptr = base.add(field.offset as usize);
|
| 193 |
let data = core::slice::from_raw_parts(ptr, field.size as usize);
|
|
|
|
| 60 |
// ββ Team blocks + worm entries ββ
|
| 61 |
let team_count = (*ddgame).team_arena.team_count as usize;
|
| 62 |
let _ = writeln!(out, "[Teams] count={}", team_count);
|
| 63 |
+
let arena = openwa_core::engine::ddgame::TeamArenaRef::from_ptr(&raw mut (*ddgame).team_arena);
|
|
|
|
|
|
|
| 64 |
let blocks = arena.blocks();
|
| 65 |
for t in 0..team_count {
|
| 66 |
let header = arena.team_header(t);
|
| 67 |
+
let name =
|
| 68 |
+
core::ffi::CStr::from_ptr(header.team_name.as_ptr() as *const _).to_string_lossy();
|
| 69 |
let _ = writeln!(out, "\n [Team {}] \"{}\"", t, name);
|
| 70 |
+
let _ = writeln!(
|
| 71 |
+
out,
|
| 72 |
+
" eliminated={} alliance={} worm_count={} active_worm={}",
|
| 73 |
+
header.eliminated, header.alliance, header.worm_count, header.active_worm
|
| 74 |
+
);
|
| 75 |
+
let _ = writeln!(
|
| 76 |
+
out,
|
| 77 |
+
" weapon_alliance={} turn_action_flags=0x{:08X}",
|
| 78 |
+
header.weapon_alliance, header.turn_action_flags
|
| 79 |
+
);
|
| 80 |
|
| 81 |
// Worms are in block[t+1].worms[0..worm_count] (1-indexed blocks)
|
| 82 |
let worm_count = header.worm_count.max(0) as usize;
|
|
|
|
| 111 |
}
|
| 112 |
root = parent;
|
| 113 |
}
|
| 114 |
+
let _ = writeln!(
|
| 115 |
+
out,
|
| 116 |
+
"[Entities] root vt=0x{:08X}",
|
| 117 |
+
(*(root as *const u32)).wrapping_sub(delta)
|
| 118 |
+
);
|
| 119 |
|
| 120 |
let iter = CTaskBfsIter::new(root);
|
| 121 |
|
|
|
|
| 157 |
let class = vtable_name(vt);
|
| 158 |
if class != "Unknown" {
|
| 159 |
if let Some(fields) = openwa_core::registry::struct_fields_for(class) {
|
| 160 |
+
let _ =
|
| 161 |
+
write_registry_fields(&mut out, task as *const u8, fields, delta, 2);
|
| 162 |
} else {
|
| 163 |
let _ = write_raw_region(&mut out, task as *const u8, 0x100, 2);
|
| 164 |
}
|
|
|
|
| 195 |
let pad = " ".repeat(indent);
|
| 196 |
for field in fields.fields {
|
| 197 |
let addr = base as u32 + field.offset;
|
| 198 |
+
write!(
|
| 199 |
+
w,
|
| 200 |
+
"{}+0x{:04X} {:<16} [{:>2}] ",
|
| 201 |
+
pad, field.offset, field.name, field.size
|
| 202 |
+
)?;
|
| 203 |
if openwa_core::mem::can_read(addr, field.size) {
|
| 204 |
let ptr = base.add(field.offset as usize);
|
| 205 |
let data = core::slice::from_raw_parts(ptr, field.size as usize);
|
crates/openwa-wormkit/src/validation/mod.rs
CHANGED
|
@@ -109,14 +109,13 @@ fn validate_addresses(result: &mut ValidationResult) {
|
|
| 109 |
let _ = log_validation("--- Address Validation ---");
|
| 110 |
|
| 111 |
// Auto-discover all registered vtables from the address registry
|
| 112 |
-
let vtables: Vec<(&str, u32)> =
|
| 113 |
-
openwa_core::registry::AddrKind::Vtable
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
.collect();
|
| 120 |
|
| 121 |
let _ = log_validation("");
|
| 122 |
let _ = log_validation(&format!(
|
|
@@ -287,7 +286,11 @@ fn validate_typed_vtable_slots(result: &mut ValidationResult) {
|
|
| 287 |
&format!(
|
| 288 |
"0x{:08X} {}",
|
| 289 |
actual,
|
| 290 |
-
if in_text {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
),
|
| 292 |
);
|
| 293 |
}
|
|
@@ -1010,8 +1013,7 @@ fn dump_entity_census() {
|
|
| 1010 |
groups.sort_by(|a, b| b.1.len().cmp(&a.1.len()));
|
| 1011 |
|
| 1012 |
for (vt_ghidra, ptrs) in &groups {
|
| 1013 |
-
let name = openwa_core::registry::vtable_class_name(*vt_ghidra)
|
| 1014 |
-
.unwrap_or("UNKNOWN");
|
| 1015 |
let _ = log_validation(&format!(
|
| 1016 |
" {:>3}x {:<20} (vtable 0x{:08X})",
|
| 1017 |
ptrs.len(),
|
|
|
|
| 109 |
let _ = log_validation("--- Address Validation ---");
|
| 110 |
|
| 111 |
// Auto-discover all registered vtables from the address registry
|
| 112 |
+
let vtables: Vec<(&str, u32)> =
|
| 113 |
+
openwa_core::registry::entries_by_kind(openwa_core::registry::AddrKind::Vtable)
|
| 114 |
+
.map(|e| {
|
| 115 |
+
let name = e.class_name.map_or(e.name, |c| c);
|
| 116 |
+
(name, e.va)
|
| 117 |
+
})
|
| 118 |
+
.collect();
|
|
|
|
| 119 |
|
| 120 |
let _ = log_validation("");
|
| 121 |
let _ = log_validation(&format!(
|
|
|
|
| 286 |
&format!(
|
| 287 |
"0x{:08X} {}",
|
| 288 |
actual,
|
| 289 |
+
if in_text {
|
| 290 |
+
"in .text"
|
| 291 |
+
} else {
|
| 292 |
+
"NOT in .text β hooked or corrupt?"
|
| 293 |
+
}
|
| 294 |
),
|
| 295 |
);
|
| 296 |
}
|
|
|
|
| 1013 |
groups.sort_by(|a, b| b.1.len().cmp(&a.1.len()));
|
| 1014 |
|
| 1015 |
for (vt_ghidra, ptrs) in &groups {
|
| 1016 |
+
let name = openwa_core::registry::vtable_class_name(*vt_ghidra).unwrap_or("UNKNOWN");
|
|
|
|
| 1017 |
let _ = log_validation(&format!(
|
| 1018 |
" {:>3}x {:<20} (vtable 0x{:08X})",
|
| 1019 |
ptrs.len(),
|