File size: 9,570 Bytes
2d732a2 6c987d4 2d732a2 9984c5c 3f42fbe 31262e9 2d732a2 902aa11 2d732a2 6c987d4 2d732a2 6c987d4 2d732a2 6c987d4 2d732a2 6c987d4 2d732a2 6c987d4 2d732a2 6c987d4 2d732a2 6c987d4 2d732a2 4f85ac3 4a4cdc9 2d732a2 6c987d4 d7eb276 2d732a2 1240d65 d7eb276 2d732a2 d7eb276 2d732a2 6c987d4 2d732a2 6c987d4 2d732a2 d7eb276 2d732a2 6c987d4 2d732a2 6c987d4 2d732a2 6c987d4 2d732a2 d7eb276 2d732a2 6c987d4 2d732a2 6c987d4 2d732a2 6c987d4 2d732a2 6c987d4 2d732a2 6c987d4 2d732a2 6c987d4 2d732a2 6c987d4 2d732a2 6c987d4 2d732a2 1240d65 2eaf41f 902aa11 2d732a2 dd5434f 2eaf41f d7eb276 2d732a2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 | //! 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<u8> {
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::<BitGrid>();
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;
}
|