paavohtl's picture
Claude Opus 4.6 (1M context)
refactor: port IMG_Decode and rename GfxResource to IMG
d7eb276
Raw History Blame Contribute Delete
38.1 kB
use crate::audio::active_sound::ActiveSoundTable;
use crate::audio::dssound::DSSound;
use crate::audio::music::Music;
use crate::audio::speech::SpeechSlotTable;
use crate::audio::SoundQueueEntry;
use crate::bitgrid::{BitGrid, CollisionBitGrid, DisplayBitGrid};
use crate::engine::game_info::GameInfo;
use crate::engine::{CoordEntry, CoordList, TeamArena, TeamIndexMap};
use crate::fixed::Fixed;
use crate::game::weapon::WeaponTable;
use crate::input::keyboard::DDKeyboard;
use crate::render::display::gfx::DisplayGfx;
use crate::render::display::palette::Palette;
use crate::render::landscape::PCLandscape;
use crate::render::queue::RenderQueue;
use crate::render::turn_order::TurnOrderWidget;
use crate::render::RenderEntry;
use crate::FieldRegistry;
/// DDGame β€” the main game engine object.
///
/// This is a massive ~39KB struct (0x98D8 bytes) that owns all major subsystems:
/// display, landscape, sound, graphics handlers, and task state machines.
///
/// Allocated in DDGame__Constructor (0x56E220).
/// The DDGame pointer is stored at DDGameWrapper+0x488 (DWORD index 0x122).
///
/// PARTIAL: Fields up to 0x54C are densely mapped from the constructor.
/// Beyond that, only scattered fields are known β€” use the `offsets` module.
///
/// Note on offsets: The constructor accesses DDGame fields via
/// `*(param_2[0x122] + byte_offset)` β€” these are byte offsets, NOT DWORD-indexed.
/// DWORD indexing only applies to param_2 (DDGameWrapper) itself.
#[derive(FieldRegistry)]
#[repr(C)]
pub struct DDGame {
/// 0x000: DDKeyboard pointer (vtable 0x66AEC8). Constructor param "keyboard".
pub keyboard: *mut DDKeyboard,
/// 0x004: DisplayGfx pointer (vtable 0x66A218). Constructor param "display".
pub display: *mut DisplayGfx,
/// 0x008: DSSound pointer (vtable 0x66AF20). Constructor param "sound".
/// Null means sound is disabled (checked by PlaySoundGlobal).
pub sound: *mut DSSound,
/// 0x00C: Active sound position table (0x608 bytes, conditional on sound available).
/// Tracks up to 64 positional sounds with world coordinates for 3D audio mixing.
/// NULL when `game_info+0xF914 != 0` (headless/server) or `sound == NULL`.
pub active_sounds: *mut ActiveSoundTable,
/// 0x010: Palette object pointer (vtable 0x66A2E4). Constructor param "palette".
pub palette: *mut Palette,
/// 0x014: Music object pointer (vtable 0x66B3E0). Constructor param "music".
pub music: *mut Music,
/// 0x018: Timer object pointer (0x30 bytes, from GameSession+0xBC).
pub timer_obj: *mut u8,
/// 0x01C: Network/caller ECX value from constructor (often NULL for offline).
pub network_ecx: u32,
/// 0x020: PCLandscape pointer (copied from DDGameWrapper[0x133])
pub landscape: *mut PCLandscape,
/// 0x024: GameInfo pointer (passed as param_10 to constructor).
pub game_info: *mut GameInfo,
/// 0x028: Network game object (param_9 to DDGameWrapper constructor).
pub net_game: *mut u8,
/// 0x02C: Secondary PaletteContext (0x70C bytes, conditional on secondary GfxDir)
pub secondary_palette_ctx: *mut crate::render::palette::PaletteContext,
/// 0x030: Gradient image (decoded from gradient.img via IMG_Decode).
pub gradient_image: *mut BitGrid,
/// 0x034: Gradient image 2 pointer.
pub gradient_image_2: *mut BitGrid,
/// 0x038-0x0B4: Arrow sprite BitGrid pointers (32 entries, decoded from arrow*.img).
pub arrow_sprites: [*mut BitGrid; 32],
/// 0x0B8-0x134: Arrow GfxDir pointers (32 entries, conditional)
pub arrow_gfxdirs: [*mut u8; 32],
/// 0x138: Primary display BitGrid (vtable 0x664144, 8bpp pixel buffer).
/// Allocated as 0x4C bytes, initialized with BitGrid::init(8, 0x100, 0x1E0).
pub display_bitgrid: *mut DisplayBitGrid,
/// 0x13C-0x37F: Sprite/image BitGrid cache (145 pointer slots).
/// All populated entries have vtable 0x664144 (same class as `display_bitgrid`).
/// Not initialized in DDGame__Constructor β€” filled during gameplay with
/// weapon sprites, effect images, cursor graphics, etc.
pub sprite_cache: [*mut u8; 145],
/// 0x380: Collision BitGrid pointer (vtable 0x664118, 0x2C bytes).
/// Used for terrain collision/spatial queries.
pub collision_grid: *mut CollisionBitGrid,
/// 0x384-0x467: Additional sprite/image object slots.
/// Same vtable 0x664144 as sprite_cache. ~20 entries populated at runtime.
pub sprite_cache_2: [*mut u8; 57],
/// 0x468: Landscape property from PCLandscape vtable[0xB] (thiscall getter).
/// Set during DDGame construction if landscape is non-null.
pub landscape_property: u32,
/// 0x46C-0x488: 8 SpriteRegion pointers (0x9C bytes each, vtable 0x66B268)
/// Created by SpriteRegion__Constructor (0x57DB20).
/// Each contains 32 BitGrid sub-objects.
pub sprite_regions: [*mut u8; 8],
/// 0x48C-0x508: Arrow collision region pointers (32 entries)
pub arrow_collision_regions: [*mut u8; 32],
/// 0x50C: Coordinate list β€” dynamic array of packed (x,y) terrain coords.
pub coord_list: *mut CoordList,
/// 0x510: Weapon table pointer (0x10 header + 71 Γ— WeaponEntry).
pub weapon_table: *mut WeaponTable,
/// 0x514: Unknown pointer (populated at runtime)
pub _unknown_514: *mut u8,
/// 0x518: Unknown pointer (populated at runtime)
pub _unknown_518: *mut u8,
/// 0x51C: Unknown pointer (populated at runtime)
pub _unknown_51c: *mut u8,
/// 0x520: Unknown (zero in runtime dump)
pub _unknown_520: u32,
/// 0x524: RenderQueue pointer (passed as `this` to all Draw* functions)
pub render_queue: *mut RenderQueue,
/// 0x528: Game state stream object (vtable 0x664194, vt[0]=0x4FB5C0).
/// Created in DDGame_InitGameState_Maybe (0x526690), constructor 0x4FB5F0.
/// Reads from replay/packet data stream.
pub game_state_stream: *mut u8,
/// 0x52C: Unknown pointer
pub _unknown_52c: *mut u8,
/// 0x530: Turn order widget (vtable 0x66A088, vt[0]=0x563E90).
/// Constructor 0x563D40. UI component that renders team banners with
/// animated sliding transitions (sin-table interpolation). Groups teams
/// by alliance, creates per-team entries with textbox + DisplayGfx.
pub turn_order_widget: *mut TurnOrderWidget,
/// 0x534: HUD panel object (vtable 0x664698, vt[0]=0x5241F0).
/// Constructor 0x524070. 104Γ—28 px, 3 DisplayGfx layers, 2296-byte LUT.
pub hud_panel: *mut u8,
/// 0x538-0x53F: Unknown (zero in runtime dump)
pub _unknown_538: [u8; 8],
/// 0x540: Unknown pointer
pub _unknown_540: *mut u8,
/// 0x544: Unknown pointer (*=0x1BC at runtime)
pub _unknown_544: *mut u8,
/// 0x548: Weapon panel pointer
pub weapon_panel: *mut u8,
/// 0x54C: CTaskLand pointer (set by CTaskLand__InitLandscape at 0x5056F0).
/// The landscape/terrain task. Vtable at 0x664388.
pub task_land: *mut u8,
/// 0x550-0x5C3: Unknown.
pub _unknown_550: [u8; 0x5C4 - 0x550],
/// 0x5C4: Vector normalize function pointer. Selected by game version in InitGameState:
/// < 0x99 β†’ VECTOR_NORMALIZE_SIMPLE (0x5755D0), >= 0x99 β†’ VECTOR_NORMALIZE_OVERFLOW.
pub vector_normalize_fn: u32,
/// 0x5C8: Stack height config. Version-dependent: < -1 β†’ 0x700, >= -1 β†’ 0x800.
pub stack_height: u32,
/// 0x5CC: Game frame counter. Incremented at end of each frame in
/// GameFrameDispatcher. Compared against GameInfo+0xF344 (sound start
/// threshold) by IsSoundSuppressed and DispatchGlobalSound.
pub frame_counter: i32,
/// 0x5D0: Frame processing flag (set 0/1 by GameFrameEndProcessor, zeroed by InitGameState).
pub _field_5d0: u32,
/// 0x5D4: Unknown (zeroed by InitGameState).
pub _field_5d4: u32,
/// 0x5D8: Unknown (zeroed by InitGameState).
pub _field_5d8: u32,
/// 0x5DC: Unknown (zeroed by InitGameState).
pub _field_5dc: u32,
/// 0x5E0: Water level in pixels. Computed as `(100 - water_pct) * level_height / 100`.
pub water_level: i32,
/// 0x5E4: Water kill Y boundary (pixels). Derived from level_bound_max_y integer part + 0x28.
pub water_kill_y: i32,
/// 0x5E8: Initial water level (copy of water_level at init time).
pub water_level_initial: i32,
/// 0x5EC: Unknown (zeroed by InitGameState).
pub _field_5ec: u32,
/// 0x5F0: Unknown (set to 1 by InitGameState).
pub _field_5f0: u32,
/// 0x5F4: Unknown (set to 100 by InitGameState).
pub _field_5f4: u32,
/// 0x5F8: Unknown (zeroed by InitGameState).
pub _field_5f8: u32,
/// 0x5FC: Unknown (zeroed by InitGameState).
pub _field_5fc: u32,
/// 0x600-0x25FF: Identity permutation [0,1,2,...,~2048] β€” SpriteGfxTable.
pub _unknown_600: [u8; 0x2600 - 0x600],
/// 0x2600-0x2DFF: Block of 0xFFFFFFFF values at runtime (512 i32 entries).
/// May be unused slots in a parallel table to the 0x600 permutation.
pub _unknown_2600: [u8; 0x2E00 - 0x2600],
/// 0x2E00-0x45D3: Unknown (mostly zero at runtime).
///
/// Contains FUN_00526120 zeroed offsets at stride 0x194:
/// 0x379C, 0x3930, 0x3AC4, 0x3C58, 0x3DEC, 0x3F80, 0x4114, 0x42A8, 0x443C, 0x45D0
pub _unknown_2e00: [u8; 0x45D4 - 0x2E00],
/// 0x45D4: Terrain drop percentage A (from GameInfo+0xD955).
pub terrain_pct_a: u32,
/// 0x45D8: Terrain drop percentage B (from GameInfo+0xD958).
pub terrain_pct_b: u32,
/// 0x45DC: Terrain drop percentage C (from GameInfo+0xD957).
pub terrain_pct_c: u32,
/// 0x45E0: Unknown (zeroed by InitGameState version config).
pub _field_45e0: u32,
/// 0x45E4: Unknown (zeroed by InitGameState version config).
pub _field_45e4: u32,
/// 0x45E8: Unknown (zeroed by InitGameState version config).
pub _field_45e8: u32,
/// 0x45EC: Game RNG state. Advanced each frame and by weapon spread:
/// `rng = (frame_counter + rng) * 0x19660D + 0x3C6EF35F`.
/// See also `ADVANCE_GAME_RNG` (0x53F320) which is the WA.exe function.
/// Single shared RNG β€” affects both gameplay AND visual effects.
pub game_rng: u32,
/// 0x45F0-0x4607: Per-team health ratio (Fixed-point, 6 entries, 1-indexed).
/// 0x10000 = 100% health. Rendered as bar width: `value * 100 >> 16 + 4` pixels.
/// Read by TurnOrderTeamEntry render method (0x563620).
/// Initialized to 0x10000 (1.0) by TurnOrderTeamEntry constructor (0x5630B0).
pub team_health_ratio: [i32; 6],
/// 0x4608-0x461F: Per-team health ratio 2 (Fixed-point, 6 entries, 1-indexed).
/// Initialized to 0x10000 (1.0). Not read by the render method β€” may be
/// target/previous value for interpolation, or used by update logic.
pub team_health_ratio_2: [i32; 6],
/// 0x4620-0x4623: Unknown
pub _unknown_4620: u32,
/// 0x4624: HUD status message code. Set to 6 when object pool is full
/// (too many projectiles). Read by HUD rendering to display warning text.
pub hud_status_code: i32,
/// 0x4628: Team arena state β€” per-team data, ammo, delays, alliance tracking.
/// Note: fields previously named init_field_64d8 (= team_count at arena+0x1EB0)
/// and init_field_72a4 (= weapon_slots entry at arena+0x2A7C) are inside this struct.
pub team_arena: TeamArena,
/// 0x7270-0x72A3: Unknown
pub _unknown_7270: [u8; 0x72A4 - 0x7270],
/// 0x72A4: Object pool counter. Incremented by +7 per CTaskMissile, +2 per CTaskArrow.
/// Checked before spawning: `pool_count + N <= 700` or overflow warning shown.
/// Written by CTaskArrow ctor at DDGame+0x4628+0x2C7C.
pub object_pool_count: i32,
/// 0x72A8-0x72D7: Unknown
pub _unknown_72a8: [u8; 0x72D8 - 0x72A8],
/// 0x72D8: Game speed multiplier (Fixed16.16, 1.0 = normal speed).
pub game_speed: Fixed,
/// 0x72DC: Game speed target (Fixed16.16, 1.0 = normal speed).
pub game_speed_target: Fixed,
/// 0x72E0-0x72E3: Unknown
pub _unknown_72e0: u32,
/// 0x72E4: Active render/weapon slot count. Reset to 14 (0x0E) by WeaponRelease
/// sync check, matching the render_entries array size.
pub render_slot_count: u32,
/// 0x72E8-0x72EB: Unknown
pub _unknown_72e8: u32,
/// 0x72EC: RNG state word 1 (changes every frame).
pub rng_state_1: u32,
/// 0x72F0: RNG state word 2 (changes every frame).
pub rng_state_2: u32,
/// 0x72F4: Unknown (zeroed by InitTurnState).
pub _field_72f4: u32,
/// 0x72F8: Unknown (zeroed by InitTurnState).
pub _field_72f8: u32,
/// 0x72FC: Unknown (zeroed by InitTurnState).
pub _field_72fc: u32,
/// 0x7300: Unknown (zeroed by InitTurnState).
pub _field_7300: u32,
/// 0x7304: Unknown (zeroed by InitTurnState).
pub _field_7304: u32,
/// 0x7308: Sprite/gfx dimension data (passed to GFX_DIR_LOAD_SPRITES).
pub gfx_sprite_data: [u8; 0x730C - 0x7308],
/// 0x730C-0x7337: GfxDir color table (11 entries).
/// Populated from colours.img pixel row: `color_table[i] = get_pixel(sprite, i, 0)`.
/// Known entries:
/// - [6] (0x7324): Crosshair line color (DrawPolygon param_2)
/// - [8] (0x732C): Crosshair line style (DrawPolygon param_1)
/// - [9]-[10] (0x7330-0x7334): Font palette entries (used by LoadFontExtension)
pub gfx_color_table: [u32; 11],
/// 0x7338: Fill pixel value
pub fill_pixel: u32,
/// 0x733C-0x733F: Unknown
pub _unknown_733c: [u8; 4],
/// 0x7340: Landscape dimension param (passed to PCLandscape vtable slot 6).
pub _field_7340: u32,
/// 0x7344-0x734B: Unknown
pub _unknown_7344: [u8; 8],
/// 0x734C: Landscape dimension param (passed to PCLandscape vtable slot 6).
pub _field_734c: u32,
/// 0x7350-0x736F: Unknown
pub _unknown_7350: [u8; 0x7370 - 0x7350],
/// 0x7370: Current team color index. Set from GameInfo+0xD924;
/// overridden to -1 (0xFFFFFFFF) if any CPU team is detected.
pub team_color: u32,
/// 0x7374: Unknown (zeroed by InitTurnState).
pub _field_7374: u32,
/// 0x7378: Unknown (zeroed by InitTurnState).
pub _field_7378: u32,
/// 0x737C: Unknown (zeroed by InitTurnState).
pub _field_737c: u32,
/// 0x7380: Viewport width (Fixed-point, e.g. 960.0 = 0x03C00000).
pub viewport_width: i32,
/// 0x7384: Viewport height (Fixed-point, e.g. 348.0 = 0x015C0000).
pub viewport_height: i32,
/// 0x7388: Viewport width max/duplicate (Fixed-point).
pub viewport_width_2: i32,
/// 0x738C: Viewport height max/duplicate (Fixed-point).
pub viewport_height_2: i32,
/// 0x7390: Unknown (zeroed by InitGameState).
pub _field_7390: u32,
/// 0x7394: Render scale factor (Fixed-point, 0x10000 = 1.0).
pub render_scale: Fixed,
/// 0x7398: Unknown (zeroed by InitGameState).
pub _field_7398: u32,
/// 0x739C: Render state flag (zeroed by InitRenderIndices).
pub render_state_flag: u32,
/// 0x73A0: Camera X position (Fixed-point, e.g. 393.0).
pub camera_x: i32,
/// 0x73A4: Camera Y position (Fixed-point, e.g. 532.0).
pub camera_y: i32,
/// 0x73A8: Camera target X (Fixed-point, duplicate/interpolation target).
pub camera_target_x: i32,
/// 0x73AC: Camera target Y (Fixed-point).
pub camera_target_y: i32,
/// 0x73B0: Render entry table (14 entries Γ— 0x14 bytes).
/// First u32 of each entry zeroed by InitRenderIndices.
pub render_entries: [RenderEntry; 14],
/// 0x74C8-0x763F: Unknown
pub _unknown_74c8: [u8; 0x7640 - 0x74C8],
/// 0x7640: Unknown (zeroed by InitGameState).
pub _field_7640: u32,
/// 0x7644: Unknown config (from GameInfo+0xF363).
pub _field_7644: u32,
/// 0x7648: Unknown config (from GameInfo+0xF364).
pub _field_7648: u32,
/// 0x764C: Rendering phase. Checked by CTaskCloud::HandleMessage:
/// clouds only render when this == 5 (in-game rendering active).
pub render_phase: i32,
/// 0x7650: Team index permutation maps (3 Γ— 0x64 bytes).
/// Used for team-to-slot mapping (render order, turn order, display order).
/// Initialized as identity permutations [0..15] with count=16.
pub team_index_maps: [TeamIndexMap; 3],
/// 0x777C: Level width output (written by PCLandscape constructor param 10).
pub level_width_raw: u32,
/// 0x7780: Level height output (written by PCLandscape constructor param 11).
pub level_height_raw: u32,
/// 0x7784: Unknown (zeroed by InitTurnState).
pub _field_7784: u32,
/// 0x7788: Unknown (set from GameInfo+0xF362 during InitTurnState).
pub _field_7788: u32,
/// 0x778C: Unknown (set to Fixed 1.0 by InitTurnState).
pub _field_778c: Fixed,
/// 0x7790: Unknown (zeroed by InitTurnState).
pub _field_7790: u32,
/// 0x7794: Unknown (zeroed by InitGameState level bounds).
pub _field_7794: u32,
/// 0x7798: Unknown (zeroed by InitGameState level bounds).
pub _field_7798: u32,
/// 0x779C: Level bound min X (Fixed16.16, negative = off-screen left).
pub level_bound_min_x: Fixed,
/// 0x77A0: Level bound max X (Fixed16.16).
pub level_bound_max_x: Fixed,
/// 0x77A4: Level bound min Y (Fixed16.16).
pub level_bound_min_y: Fixed,
/// 0x77A8: Level bound max Y (Fixed16.16).
pub level_bound_max_y: Fixed,
/// 0x77AC-0x77AF: Unknown
pub _unknown_77ac: u32,
/// 0x77B0: Unknown (zeroed by InitGameState).
pub _field_77b0: u32,
/// 0x77B4: Unknown
pub _unknown_77b4: u32,
/// 0x77B8: Level width for 3D sound distance computation (pixels, not fixed-point).
/// Read by ComputeDistanceParams, shifted left 16 before passing to Distance3D_Attenuation.
pub level_width_sound: i32,
/// 0x77BC: Screen height in pixels (output from display vtable[1]).
/// Read by InitGameState for resolution-dependent layout.
pub screen_height_pixels: i32,
/// 0x77C0: Level width in pixels (set by PCLandscape constructor).
pub level_width: u32,
/// 0x77C4: Level height in pixels (set by PCLandscape constructor).
pub level_height: u32,
/// 0x77C8: Total pixels (width Γ— height).
pub level_total_pixels: u32,
/// 0x77CC: Map boundary width. Default 0x30D4; updated to `level_w + 0x2954` for version > 0x32.
pub map_boundary_width: u32,
/// 0x77D0: Map boundary height. Default = level_height; updated to 0x2B8 for version > 0x32.
pub map_boundary_height: u32,
/// 0x77D4: Unknown (zeroed by InitTurnState).
pub _field_77d4: u32,
/// 0x77D8: Unknown (zeroed by InitTurnState).
pub _field_77d8: u32,
/// 0x77DC: Unknown (zeroed by InitTurnState).
pub _field_77dc: u32,
/// 0x77E0: Unknown (zeroed by InitTurnState).
pub _field_77e0: u32,
/// 0x77E4: Speech slot table. Maps (team, speech_line_id) β†’ DSSound buffer index.
/// Cleared by DSSound_LoadAllSpeechBanks (0x571A70), filled by DDGameWrapper__LoadSpeechWAV (0x571530).
pub speech_slot_table: SpeechSlotTable,
/// 0x7D84: Unknown (zeroed by InitTurnState).
pub _field_7d84: u32,
/// 0x7D88: Per-team u32 flags (13 entries, zeroed per team by InitTurnState).
pub _field_7d88: [u32; 13],
/// 0x7DBC: Per-team byte flags array 1 (13 entries, set to 1 by InitTurnState).
pub _field_7dbc: [u8; 13],
/// 0x7DC9: Per-team byte flags array 2 (13 entries, set to 1 by InitTurnState).
pub _field_7dc9: [u8; 13],
/// 0x7DD6: Per-team byte flags array 3 (13 entries, zeroed by InitTurnState).
pub _field_7dd6: [u8; 13],
/// 0x7DE3: Per-team byte flags array 4 (13 entries, zeroed by InitTurnState).
pub _field_7de3: [u8; 13],
/// 0x7DF0: Per-team byte flags array 5 (13 entries, zeroed by InitTurnState).
pub _field_7df0: [u8; 13],
/// 0x7DFD-0x7E02: Unknown
pub _unknown_7dfd: [u8; 6],
/// 0x7E03: Unknown byte (zeroed by InitTurnState).
pub _field_7e03: u8,
/// 0x7E04: Unknown byte (zeroed by InitTurnState).
pub _field_7e04: u8,
/// 0x7E05-0x7E24: Unknown
pub _unknown_7e05: [u8; 32],
/// 0x7E25: SuperSheep/AquaSheep restriction active flag (byte).
/// When nonzero, the SuperSheep/AquaSheep weapon check is applied.
pub supersheep_restricted: u8,
/// 0x7E26-0x7E2D: Unknown
pub _unknown_7e26: [u8; 0x7E2E - 0x7E26],
/// 0x7E2E: Version flag byte 1 (set by InitVersionFlags).
pub version_flag_1: u8,
/// 0x7E2F: Version flag byte 2 (set by InitVersionFlags).
pub version_flag_2: u8,
/// 0x7E30-0x7E3E: Unknown
pub _unknown_7e30: [u8; 0x7E3F - 0x7E30],
/// 0x7E3F: Version flag byte 3 (set by InitVersionFlags).
/// Passed to is_super_weapon as the version/mode parameter.
pub version_flag_3: u8,
/// 0x7E40: Game logic version byte 4 (set by InitVersionFlags).
/// Used in FireWeapon__MailMineMole to gate vtable call behavior by version.
pub version_flag_4: u8,
/// 0x7E41: Unknown byte (zeroed by InitTurnState).
pub _field_7e41: u8,
/// 0x7E42-0x7E4B: Unknown
pub _unknown_7e42: [u8; 10],
/// 0x7E4C: Unknown (zeroed by InitTurnState).
pub _field_7e4c: u32,
/// 0x7E50-0x7E6F: Unknown fields (all zeroed by InitTurnState).
pub _fields_7e50: [u32; 8],
/// 0x7E70: Per-team scoring flags (6 entries, written by InitAllianceData).
pub team_scoring_flags: [u32; 6],
/// 0x7E88-0x7E9B: Unknown fields (all zeroed by InitTurnState).
pub _fields_7e88: [u32; 5],
/// 0x7E9C-0x7E9F: Unknown
pub _unknown_7e9c: [u8; 4],
/// 0x7EA0: Flag/counter (value 4 at runtime β€” likely team count).
pub field_7ea0: u32,
/// 0x7EA4: Unknown.
pub field_7ea4: u32,
/// 0x7EA8: Turn time limit in seconds (150 = 2:30 default).
pub turn_time_limit: u32,
/// 0x7EAC-0x7ECF: Unknown
pub _unknown_7eac: [u8; 0x7ED0 - 0x7EAC],
/// 0x7ED0-0x7EEF: Unknown
pub _unknown_7ed0: [u8; 0x7EF0 - 0x7ED0],
/// 0x7EF0: Flag (-1 = 0xFFFFFFFF at runtime).
pub field_7ef0: i32,
/// 0x7EF4: HUD status message string pointer. Set when object pool overflows
/// (loaded via string resource 0x70F). Read by HUD rendering for warning display.
pub hud_status_text: *const core::ffi::c_char,
/// 0x7EF8: Sound available flag (1 when game_info+0xF914 == 0, i.e. not headless).
pub sound_available: u32,
/// 0x7EFC: Always initialized to 1 in constructor.
pub field_7efc: u32,
// === Sound queue (0x7F00-0x8143) ===
/// 0x7F00: Sound queue (16 entries, stride 0x24). Appended by PlaySoundGlobal.
pub sound_queue: [SoundQueueEntry; 16],
/// 0x8140: Number of entries currently in the sound queue (0–16).
pub sound_queue_count: i32,
/// 0x8144-0x8147: Unknown
pub _unknown_8144: [u8; 4],
/// 0x8148: Unknown (set to 1 by InitTurnState).
pub _field_8148: u32,
/// 0x814C-0x814F: Unknown
pub _unknown_814c: [u8; 4],
/// 0x8150: Parallax/camera scale factor (Fixed-point multiplier).
/// Used by DrawCrosshairLine (multiplied by 0x140000) and
/// CTaskCloud render (parallax X offset = wind_speed * this >> 16).
pub parallax_scale: i32,
/// 0x8154-0x8157: Unknown
pub _unknown_8154: [u8; 4],
/// 0x8158: Unknown (zeroed by InitTurnState).
pub _field_8158: u32,
/// 0x815C: Unknown (zeroed by InitTurnState).
pub _field_815c: u32,
/// 0x8160: Unknown (zeroed by InitTurnState).
pub _field_8160: u32,
/// 0x8164: Unknown (zeroed by InitTurnState).
pub _field_8164: u32,
/// 0x8168-0x818B: Unknown
pub _unknown_8168: [u8; 36],
/// 0x818C: Turn status text buffer (null-terminated ASCII).
/// Shows on screen during gameplay, e.g. "Cheesy harkitseee siirtoaan"
/// ("Cheesy is considering their move" in Finnish).
pub turn_status_text: [u8; 64],
/// 0x81CC-0x8CBB: Unknown
///
/// Known landmarks:
/// - 0x8174: value 0x3FC (1020) at runtime
pub _unknown_81cc: [u8; 0x8CBC - 0x81CC],
/// 0x8CBC-0x8D0B: 5 viewport coordinate entries (0x10-byte stride).
/// InitGameState writes camera center to entries 1..5 (indexed from base 0..4).
/// Entry 0 is not written by init (used for other purposes).
pub viewport_coords: [CoordEntry; 5],
/// 0x8D0C-0x984F: Unknown
pub _unknown_8d0c: [u8; 0x9850 - 0x8D0C],
/// 0x9850-0x988F: 4 coordinate entries (0x10-byte stride, zeroed by InitFields).
pub screen_coords_2: [CoordEntry; 4],
/// 0x9890-0x98A3: Flags
pub _unknown_9890: [u8; 0x98A4 - 0x9890],
/// 0x98A4: Checkpoint active flag.
pub checkpoint_active: u32,
/// 0x98A8: Unknown
pub _unknown_98a8: u32,
/// 0x98AC: Fast-forward request flag.
pub fast_forward_request: u32,
/// 0x98B0: Fast-forward active flag.
/// When set to 1, FUN_005307A0 processes up to 50 game frames per render
/// cycle. Sound is suppressed and rendering is skipped. Cleared at turn
/// boundaries (FUN_00534540, FUN_0055BDD0), so must be re-set continuously.
/// This is the same flag toggled by spacebar (key 0x35) during replay.
pub fast_forward_active: u32,
/// 0x98B4-0x98D7: Unknown tail region.
/// The original constructor allocates 0x98D8 bytes but memsets only 0x98B8.
/// The remaining 0x20 bytes are not zeroed by the initial memset but may be
/// initialized by subsequent constructor steps or game code.
pub _unknown_98b4: [u8; 0x98D8 - 0x98B4],
}
const _: () = assert!(core::mem::size_of::<DDGame>() == 0x98D8);
// ============================================================
// DDGame methods
// ============================================================
impl DDGame {
/// Advance the game RNG and return the new state.
///
/// Formula: `rng = (frame_counter + rng) * 0x19660D + 0x3C6EF35F`
///
/// This is the same LCG used by `ADVANCE_GAME_RNG` (0x53F320). There is a single
/// shared RNG β€” both gameplay and visual effects advance it. Any difference in
/// RNG call count between Rust and original code causes replay desync.
pub fn advance_rng(&mut self) -> u32 {
let rng = crate::rng::wa_lcg((self.frame_counter as u32).wrapping_add(self.game_rng));
self.game_rng = rng;
rng
}
/// Advance the secondary effect RNG at DDGame+0x45F0 and return the new state.
///
/// Formula: `rng = rng * 0x19660D + 0x3C6EF35F` (simpler than [`advance_rng`] β€” no
/// frame_counter). Uses `team_health_ratio[0]`, the unused index-0 slot of the
/// 1-indexed health ratio array, repurposed by WA as a secondary RNG for weapon
/// release visual effects.
pub fn advance_effect_rng(&mut self) -> u32 {
let rng = crate::rng::wa_lcg(self.team_health_ratio[0] as u32);
self.team_health_ratio[0] = rng as i32;
rng
}
/// Read a per-team sound ID from the team sound table at DDGame+0x7768.
///
/// The table has stride 0xF0 per team (240-byte per-team config blocks).
/// The u32 at the base of each block is a sound ID used for type-2 (rope)
/// weapon release sounds.
///
/// # Safety
/// `team_id` must be a valid team index (0–5).
pub unsafe fn team_sound_id(&self, team_id: u32) -> u32 {
let base = (self as *const DDGame as *const u8).add(0x7768);
*(base.add((team_id as usize) * 0xF0) as *const u32)
}
/// Get a mutable pointer to a per-team/per-worm weapon stat counter.
///
/// Four counters exist at DDGame base offsets 0x40CC, 0x40D0, 0x40D4, 0x40D8,
/// indexed by `team_id * 0x51C + worm_id * 0x9C` (same stride as FullTeamBlock
/// and WormEntry). These track weapon usage stats during gameplay.
///
/// # Safety
/// `team_id` and `worm_id` must be valid indices.
pub unsafe fn weapon_stat_counter(
&mut self,
team_id: u32,
worm_id: u32,
base_offset: usize,
) -> *mut i32 {
(self as *mut DDGame as *mut u8)
.add(base_offset)
.add((team_id as usize) * 0x51C)
.add((worm_id as usize) * 0x9C) as *mut i32
}
/// Show the "too many objects" warning on the HUD.
///
/// Sets `hud_status_code = 6` and loads string resource 0x70F into
/// `hud_status_text`. Only writes if `game_info.game_version < 60`.
///
/// # Safety
/// `self.game_info` must be valid.
pub unsafe fn show_pool_overflow_warning(&mut self) {
use crate::address::va;
use crate::rebase::rb;
let game_version = (*self.game_info).game_version;
if game_version < 0x3C {
self.hud_status_code = 6;
let load_string: unsafe extern "cdecl" fn(u32) -> *const core::ffi::c_char =
core::mem::transmute(rb(va::LOAD_STRING_RESOURCE));
self.hud_status_text = load_string(0x70F);
}
}
/// Listener position for 3D audio β€” stored at DDGame+0x8CEC (viewport_coords[3]).
///
/// Returns `(x, y)` as raw i32 values (fixed-point 16.16).
/// Used by ComputeDistanceParams / Distance3D_Attenuation.
pub fn listener_pos(&self) -> (i32, i32) {
(
self.viewport_coords[3].center_x.0,
self.viewport_coords[3].center_y.0,
)
}
}
// DDGame constructor code (create_ddgame, init helpers, usercall bridges)
// has been moved to ddgame_constructor.rs.
// BitGrid__Init lives in crate::bitgrid
// Re-exported via ddgame_constructor.rs
/// Well-known byte offsets into DDGame, for use with raw pointer access.
///
/// The DDGame pointer is at DDGameWrapper+0x488 (DWORD index 0x122).
pub mod offsets {
// === Header / init params (0x000-0x02C) ===
pub const LANDSCAPE: usize = 0x020;
pub const GAME_INFO: usize = 0x024;
pub const SECONDARY_GFXDIR: usize = 0x02C;
pub const GRADIENT_IMAGE: usize = 0x030;
// === Sprite arrays (0x038-0x138) ===
pub const ARROW_SPRITES: usize = 0x038;
pub const ARROW_GFXDIRS: usize = 0x0B8;
pub const DISPLAY_GFX: usize = 0x138;
// === Task/state machines (0x380-0x488) ===
pub const TASK_STATE_MACHINE: usize = 0x380;
pub const SPRITE_REGIONS: usize = 0x46C;
// === Arrow collision (0x48C-0x50C) ===
pub const ARROW_COLLISION_REGIONS: usize = 0x48C;
pub const COORD_LIST: usize = 0x50C;
// === WormKit-documented offsets ===
pub const WEAPON_TABLE: usize = 0x510;
pub const WEAPON_PANEL: usize = 0x548;
// === Team weapon state (DDGame + 0x4628) ===
/// Base of TeamArena sub-struct within DDGame.
/// Callers pass DDGame + TEAM_ARENA_STATE as base pointer to
/// GetAmmo/AddAmmo/SubtractAmmo.
pub const TEAM_ARENA_STATE: usize = 0x4628;
// === Team block array (7 Γ— TeamBlock, stride 0x51C) ===
/// Start of team block array within DDGame (7 blocks, stride 0x51C).
/// Derived: entry_ptr(team=0) - 0x598 = 0x4628 - 0x598 = 0x4090.
/// Runtime-confirmed: block[0] is zeroed preamble, blocks[1-6] hold team data.
pub const TEAM_BLOCKS: usize = 0x4090;
/// Byte offset from TeamArena base back to TeamBlock array start.
/// `blocks_ptr = (tws_base as *const c_char).sub(ARENA_TO_BLOCKS) as *const TeamBlock`
///
/// entry_ptr(0) = DDGame+0x4628 = TEAM_BLOCKS + 0x598.
/// 0x598 = sizeof(TeamBlock) + 0x7C = one block + offset into TeamHeader.
pub const ARENA_TO_BLOCKS: usize = 0x598;
// === FUN_00526120 init offsets (stride 0x194, 10 entries) ===
pub const INIT_TABLE_BASE: usize = 0x379C;
pub const INIT_TABLE_STRIDE: usize = 0x194;
// === Game objects (0x528-0x54C) ===
pub const GAME_STATE_STREAM: usize = 0x528;
pub const TURN_ORDER_WIDGET: usize = 0x530;
pub const HUD_PANEL: usize = 0x534;
/// CTaskLand pointer (landscape/terrain task, vtable 0x664388).
pub const TASK_LAND: usize = 0x54C;
// === Per-team health ratio (turn order health bar) ===
/// Per-team health ratio array (6 Γ— i32, 1-indexed by team).
/// 0x10000 = 100%. Rendered as `value * 100 >> 16` pixel width.
pub const TEAM_HEALTH_RATIO: usize = 0x45F0;
/// Per-team health ratio 2 (6 Γ— i32, 1-indexed by team).
pub const TEAM_HEALTH_RATIO_2: usize = 0x4608;
// === RNG (0x72EC) ===
pub const RNG_STATE_1: usize = 0x72EC;
pub const RNG_STATE_2: usize = 0x72F0;
// === Sparse fields in upper region ===
pub const GFX_COLOR_ENTRIES: usize = 0x730C;
// === Camera/viewport (0x7380-0x73AC) ===
pub const VIEWPORT_WIDTH: usize = 0x7380;
pub const VIEWPORT_HEIGHT: usize = 0x7384;
pub const CAMERA_X: usize = 0x73A0;
pub const CAMERA_Y: usize = 0x73A4;
pub const CAMERA_TARGET_X: usize = 0x73A8;
pub const CAMERA_TARGET_Y: usize = 0x73AC;
// === Game speed (0x72D8) ===
pub const GAME_SPEED: usize = 0x72D8;
pub const GAME_SPEED_TARGET: usize = 0x72DC;
// === Level bounds (0x779C-0x77A8) ===
pub const LEVEL_BOUND_MIN_X: usize = 0x779C;
pub const LEVEL_BOUND_MAX_X: usize = 0x77A0;
pub const LEVEL_BOUND_MIN_Y: usize = 0x77A4;
pub const LEVEL_BOUND_MAX_Y: usize = 0x77A8;
pub const TURN_TIME_LIMIT: usize = 0x7EA8;
pub const SOUND_AVAILABLE: usize = 0x7EF8;
/// Scale factor used by DrawCrosshairLine (multiplied by 0x140000).
pub const PARALLAX_SCALE: usize = 0x8150;
/// Turn status text (null-terminated ASCII, shown during gameplay).
pub const TURN_STATUS_TEXT: usize = 0x818C;
/// Checkpoint active flag.
pub const CHECKPOINT_ACTIVE: usize = 0x98A4;
/// Fast-forward request flag.
pub const FAST_FORWARD_REQUEST: usize = 0x98AC;
// === Speech slot table (DDGame + 0x77E4) ===
/// Speech slot table: maps (team, speech_line_id) β†’ DSSound buffer index.
pub const SPEECH_SLOT_TABLE: usize = 0x77E4;
// === Fast-forward (DDGame + 0x98B0) ===
/// Fast-forward active flag (u32, 1 = active).
pub const FAST_FORWARD_ACTIVE: usize = 0x98B0;
// === Sound queue (DDGame + 0x7F00) ===
/// DSSound pointer (null = sound disabled).
pub const SOUND: usize = 0x0008;
/// Sound queue base (16 Γ— SoundQueueEntry, stride 0x24).
pub const SOUND_QUEUE: usize = 0x7F00;
/// Sound queue count (i32, 0–16).
pub const SOUND_QUEUE_COUNT: usize = 0x8140;
}
// ── Snapshot impls ──────────────────────────────────────────
#[cfg(target_arch = "x86")]
impl crate::snapshot::Snapshot for DDGame {
unsafe fn write_snapshot(
&self,
w: &mut dyn core::fmt::Write,
indent: usize,
) -> core::fmt::Result {
use crate::fixed::Fixed;
use crate::snapshot::{fmt_ptr, write_indent};
let i = indent;
write_indent(w, i)?;
writeln!(w, "frame_counter = {}", self.frame_counter)?;
write_indent(w, i)?;
writeln!(w, "game_speed = {}", self.game_speed)?;
write_indent(w, i)?;
writeln!(w, "game_speed_target = {}", self.game_speed_target)?;
write_indent(w, i)?;
writeln!(
w,
"rng_state = 0x{:08X} 0x{:08X}",
self.rng_state_1, self.rng_state_2
)?;
write_indent(w, i)?;
writeln!(
w,
"camera = ({}, {})",
Fixed(self.camera_x),
Fixed(self.camera_y)
)?;
write_indent(w, i)?;
writeln!(
w,
"camera_target = ({}, {})",
Fixed(self.camera_target_x),
Fixed(self.camera_target_y)
)?;
write_indent(w, i)?;
writeln!(w, "level_size = {}x{}", self.level_width, self.level_height)?;
write_indent(w, i)?;
writeln!(
w,
"level_size_raw = {}x{}",
self.level_width_raw, self.level_height_raw
)?;
write_indent(w, i)?;
writeln!(
w,
"landscape_property = {}",
fmt_ptr(self.landscape_property as *const u8)
)?;
write_indent(w, i)?;
writeln!(w, "gfx_color_table = {:?}", self.gfx_color_table)?;
write_indent(w, i)?;
writeln!(
w,
"fast_forward = req={} active={}",
self.fast_forward_request, self.fast_forward_active
)?;
write_indent(w, i)?;
writeln!(w, "keyboard = {}", fmt_ptr(self.keyboard as *const u8))?;
write_indent(w, i)?;
writeln!(w, "display = {}", fmt_ptr(self.display as *const u8))?;
write_indent(w, i)?;
writeln!(w, "sound = {}", fmt_ptr(self.sound as *const u8))?;
write_indent(w, i)?;
writeln!(w, "game_info = {}", fmt_ptr(self.game_info as *const u8))?;
write_indent(w, i)?;
writeln!(w, "landscape = {}", fmt_ptr(self.landscape as *const u8))?;
write_indent(w, i)?;
writeln!(w, "task_land = {}", fmt_ptr(self.task_land))?;
// TeamArena summary
write_indent(w, i)?;
writeln!(w, "team_arena.team_count = {}", self.team_arena.team_count)?;
write_indent(w, i)?;
writeln!(w, "team_arena.game_phase = {}", self.team_arena.game_phase)?;
write_indent(w, i)?;
writeln!(
w,
"team_arena.game_mode_flag = {}",
self.team_arena.game_mode_flag
)?;
// Dump weapon slots (ammo only, skip zeros)
write_indent(w, i)?;
writeln!(w, "weapon_slots:")?;
for team in 0..6usize {
let slots = &self.team_arena.weapon_slots.teams[team];
let mut has_any = false;
for wpn in 0..71usize {
let ammo = slots.ammo[wpn];
if ammo != 0 {
if !has_any {
write_indent(w, i + 1)?;
write!(w, "team[{}] ammo:", team)?;
has_any = true;
}
if ammo == -1 {
write!(w, " {}=inf", wpn)?;
} else {
write!(w, " {}={}", wpn, ammo)?;
}
}
}
if has_any {
writeln!(w)?;
}
}
Ok(())
}
}