//! Sky gradient computation for non-standard level heights. //! //! NOTE: This code path is UNTESTED — it only triggers on maps with //! non-standard dimensions (sky_height >= 0x61 or level_height != 0x2B8). //! The algorithm is ported from the DDGame constructor decompilation but //! may have subtle color differences due to the tangled decompiler output. use crate::address::va; use crate::asset::gfx_dir::call_gfx_find_and_load; #[cfg(target_arch = "x86")] use crate::asset::gfx_dir::GfxDir; use crate::bitgrid::{BitGrid, BitGridBaseVtable}; use crate::engine::ddgame::DDGame; use crate::rebase::rb; #[cfg(target_arch = "x86")] use crate::wa_alloc::wa_malloc_struct_zeroed; use crate::wa_alloc::{wa_free, wa_malloc}; /// Palette context for gradient color mapping. /// Reimplements the WA palette management functions: /// - PaletteContext__Init (0x5411A0) /// - PaletteContext__MapColor (0x5412B0) /// - PaletteContext__FindClosest (0x541420) struct PaletteContext { colors: [u32; 256], // RGB packed, indexed by palette index valid: [bool; 256], used_count: usize, used_list: [u8; 256], } impl PaletteContext { fn new() -> Self { Self { colors: [0; 256], valid: [false; 256], used_count: 0, used_list: [0; 256], } } /// Map an RGB color to a palette index (PaletteContext__MapColor, 0x5412B0). /// Returns existing index if already mapped, otherwise allocates a new one. fn map_color(&mut self, rgb: u32) -> Option { let masked = rgb & 0x00FF_FFFF; for i in 0..self.used_count { let idx = self.used_list[i] as usize; if idx != 0 && (self.colors[idx] & 0x00FF_FFFF) == masked { return Some(idx as u8); } } for idx in (1..=255usize).rev() { if !self.valid[idx] { self.colors[idx] = masked; self.valid[idx] = true; self.used_list[self.used_count] = idx as u8; self.used_count += 1; return Some(idx as u8); } } None } /// Find closest palette match (PaletteContext__FindClosest, 0x541420). /// Uses weighted perceptual distance: R*3 + G*5 + B*2. /// Input channels are 8.8 fixed-point (0..0xFF00). fn find_closest(&self, r: i32, g: i32, b: i32) -> u8 { let mut best_idx = 0u8; let mut best_dist = i32::MAX; for i in 0..self.used_count { let idx = self.used_list[i] as usize; if idx == 0 { continue; } let c = self.colors[idx]; let cr = ((c & 0xFF) as i32) << 8; let cg = (((c >> 8) & 0xFF) as i32) << 8; let cb = (((c >> 16) & 0xFF) as i32) << 8; let dist = (r - cr).abs() * 3 + (g - cg).abs() * 5 + (b - cb).abs() * 2; if dist == 0 { return idx as u8; } if dist < best_dist { best_dist = dist; best_idx = idx as u8; } } best_idx } /// Read RGB color for a palette index as 8.8 fixed-point (0..0xFF00). fn color_fp(&self, idx: u8) -> [i32; 3] { let c = self.colors[idx as usize]; [ ((c & 0xFF) as i32) << 8, (((c >> 8) & 0xFF) as i32) << 8, (((c >> 16) & 0xFF) as i32) << 8, ] } } /// Compute the complex sky gradient for non-standard level heights. /// /// Called when `sky_height >= 0x61 OR level_height != 0x2B8`. /// Loads gradient.img, samples 7 anchor colors, then creates an interpolated /// gradient image (64 columns × (level_height + 0xDC) rows). /// /// NOTE: This code path is untested — no replay or map available that /// triggers it. The algorithm follows the DDGame constructor decompilation /// but may have subtle differences in color output. #[cfg(target_arch = "x86")] pub(crate) unsafe fn compute_complex_gradient( ddgame: *mut DDGame, land_dir: *mut GfxDir, layer3_ctx: *mut crate::render::palette::PaletteContext, sky_height: i16, ) { let mut palette = PaletteContext::new(); // Step 1: Load gradient.img let gradient_decoded = call_gfx_find_and_load(land_dir, c"gradient.img", layer3_ctx); let gradient_sprite = match gradient_decoded { Some(d) => d.as_bitgrid_ptr(), None => return, }; // Sprite height from BitGrid layout (shared vtable 0x6640EC) let gradient_height = (*gradient_sprite).height as i32; if gradient_height <= 0 { return; } let get_pixel: unsafe extern "thiscall" fn(*mut BitGrid, i32, i32) -> u32 = core::mem::transmute(*(*(gradient_sprite as *const *const u32)).add(4)); // Step 2: Compute target rows and stretch gradient through palette let target_rows = (0x70i32 - sky_height as i32).min(gradient_height); if target_rows > 0 { let mut src_pos = 0i32; for _ in 0..target_rows { let src_row = src_pos / target_rows; let color = get_pixel(gradient_sprite, 0, src_row); palette.map_color(color); src_pos += gradient_height; } } // Step 3: If heights match, also set gradient_image_2 if target_rows == gradient_height { (*ddgame).gradient_image_2 = call_gfx_find_and_load(land_dir, c"gradient.img", layer3_ctx) .map(|d| d.as_bitgrid_ptr()) .unwrap_or(core::ptr::null_mut()); } // Step 4: Sample 7 anchor colors by averaging 8×2 pixel blocks. // get_pixel returns palette indices; we look up RGB from our palette context. let mut anchors = [[0i32; 3]; 7]; // [band][r, g, b] in 8.8 fixed-point let grad_height_minus2 = (gradient_height - 2).max(0); for band in 0..7u32 { let start_row = if grad_height_minus2 > 0 { (band as i32 * grad_height_minus2) / 6 } else { 0 }; let mut r_sum = 0i32; let mut g_sum = 0i32; let mut b_sum = 0i32; for dy in 0..2i32 { let row = start_row + dy; for col in 0..8i32 { let pidx = get_pixel(gradient_sprite, col, row) as usize; if pidx < 256 && palette.valid[pidx] { let c = palette.colors[pidx]; r_sum += (c & 0xFF) as i32; g_sum += ((c >> 8) & 0xFF) as i32; b_sum += ((c >> 16) & 0xFF) as i32; } } } // Shift left 4: 16 samples × 8-bit → 8.8 fixed-point anchors[band as usize] = [r_sum << 4, g_sum << 4, b_sum << 4]; } // Release the gradient sprite let gvt = *(gradient_sprite as *const *const u32); let release: unsafe extern "thiscall" fn(*mut BitGrid, u8) = core::mem::transmute(*gvt.add(3)); release(gradient_sprite, 1); // Step 5: Establish initial/fallback color via palette closest-match. // The original uses PaletteContext__FindClosest + PaletteContext__ReadColor // to get the canonical palette color for the first anchor point. let initial_idx = palette.find_closest(anchors[0][0], anchors[0][1], anchors[0][2]); let mut fallback = palette.color_fp(initial_idx); // Step 6: Create the gradient image buffer let total_height = (*ddgame).level_height as i32 + 0xDC; if total_height <= 0 { return; } let stride = 0x200u32; // 64 columns × 8 bytes per entry let data_size = total_height as u32 * stride; let data = wa_malloc(data_size + 0x20); if data.is_null() { return; } core::ptr::write_bytes(data, 0, data_size as usize); // Step 7: Interpolate between anchor colors and write each row. // For each row, determine which two anchors to interpolate between. // Rows beyond band 5 use the last interpolated color as fallback. let band_size = if total_height > 6 { total_height / 6 } else { 1 }; for row in 0..total_height { let band_idx = (row * 6) / total_height; if band_idx < 6 { let pos = row - band_idx * band_size; let bi = band_idx as usize; let a0 = &anchors[bi]; let a1 = &anchors[(bi + 1).min(6)]; for ch in 0..3 { fallback[ch] = a0[ch] + ((a1[ch] - a0[ch]) * pos) / band_size; fallback[ch] = fallback[ch].clamp(0, 0xFF00); } } // else: rows beyond band 5 keep the last fallback color let color_low = (fallback[0] as u32 & 0xFFFF) | ((fallback[1] as u32 & 0xFFFF) << 16); let color_high = fallback[2] as u32 & 0xFFFF; // Write to all 64 columns (matches GradientImage__WriteRow at 0x4F91C0) let row_base = data.add(row as usize * stride as usize); for col in 0..64u32 { let pixel = row_base.add(col as usize * 8) as *mut u32; *pixel = color_low; *pixel.add(1) = color_high; } } // Step 8: Wrap pixel data in a BitGrid-compatible object // (shares vtable 0x6640EC; CTaskLand reads .height to decide whether to render) let gfx_obj = wa_malloc_struct_zeroed::(); if gfx_obj.is_null() { wa_free(data); return; } (*gfx_obj).vtable = rb(va::BIT_GRID_BASE_VTABLE) as *const BitGridBaseVtable; (*gfx_obj).data = data; (*gfx_obj).row_stride = stride; (*gfx_obj).width = 0; (*gfx_obj).height = total_height as u32; (*ddgame).gradient_image = gfx_obj; }