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;
}