Skip to main content

siglus_scene_vm/runtime/
tonecurve.rs

1use std::path::{Path, PathBuf};
2
3use anyhow::Result;
4use siglus_assets::gameexe::{GameexeConfig, decode_gameexe_dat_bytes};
5
6use crate::assets::RgbaImage;
7use crate::image_manager::{ImageHandle, ImageManager};
8
9#[derive(Debug, Clone)]
10struct ToneCurveRow {
11    lut_r: [u8; 256],
12    lut_g: [u8; 256],
13    lut_b: [u8; 256],
14    sat: i32,
15}
16
17#[derive(Debug, Default)]
18pub struct ToneCurveRuntime {
19    rows: Option<Vec<Option<ToneCurveRow>>>,
20    source_path: Option<PathBuf>,
21    lut_image_id: Option<ImageHandle>,
22}
23
24impl ToneCurveRuntime {
25    pub fn new(project_dir: &Path) -> Self {
26        let mut out = Self::default();
27        if let Some(path) = find_tonecurve_path(project_dir) {
28            out.source_path = Some(path.clone());
29            out.rows = load_tonecurve_rows(&path).ok();
30        }
31        out
32    }
33
34    pub fn has_table(&self) -> bool {
35        self.rows.is_some()
36    }
37
38    pub fn shader_binding(
39        &mut self,
40        images: &mut ImageManager,
41        tonecurve_no: i32,
42    ) -> Option<(ImageHandle, f32, f32)> {
43        let idx = tonecurve_no.max(0) as usize;
44        let row = self.rows.as_ref()?.get(idx)?.as_ref()?.clone();
45        let lut_id = self.ensure_lut_image(images)?;
46        let row_y = ((idx.min(255) as f32) + 0.5) / 256.0;
47        let sat = if row.sat < 0 {
48            ((-row.sat) as f32 / 100.0).clamp(0.0, 1.0)
49        } else {
50            0.0
51        };
52        Some((lut_id, row_y, sat))
53    }
54
55    fn ensure_lut_image(&mut self, images: &mut ImageManager) -> Option<ImageHandle> {
56        if let Some(ref id) = self.lut_image_id {
57            return Some(id.clone());
58        }
59        let rows = self.rows.as_ref()?;
60        let mut rgba = vec![0u8; 256 * 256 * 4];
61        for y in 0..256usize {
62            for x in 0..256usize {
63                let idx = (y * 256 + x) * 4;
64                if let Some(Some(row)) = rows.get(y) {
65                    rgba[idx] = row.lut_r[x];
66                    rgba[idx + 1] = row.lut_g[x];
67                    rgba[idx + 2] = row.lut_b[x];
68                } else {
69                    let v = x as u8;
70                    rgba[idx] = v;
71                    rgba[idx + 1] = v;
72                    rgba[idx + 2] = v;
73                }
74                rgba[idx + 3] = 255;
75            }
76        }
77        let id = images.insert_image(RgbaImage {
78            width: 256,
79            height: 256,
80            center_x: 0,
81            center_y: 0,
82            rgba,
83        });
84        self.lut_image_id = Some(id.clone());
85        Some(id)
86    }
87}
88
89fn find_tonecurve_path(project_dir: &Path) -> Option<PathBuf> {
90    let cfg = load_gameexe_config(project_dir)?;
91    let rel = cfg.get_unquoted("TONECURVE_FILE")?;
92    if rel.trim().is_empty() {
93        return None;
94    }
95    let p = PathBuf::from(rel.trim());
96    if p.is_absolute() {
97        return crate::resource::resolve_game_file(&p).ok().flatten();
98    }
99    let direct = project_dir.join(&p);
100    if let Some(path) = crate::resource::resolve_game_file(&direct).ok().flatten() {
101        return Some(path);
102    }
103    let dat = project_dir.join("dat").join(&p);
104    crate::resource::resolve_game_file(&dat).ok().flatten()
105}
106
107fn load_gameexe_config(project_dir: &Path) -> Option<GameexeConfig> {
108    let path = find_gameexe_path(project_dir)?;
109    let raw = crate::resource::read_file_bytes(&path).ok()?;
110    if path
111        .extension()
112        .and_then(|s| s.to_str())
113        .is_some_and(|ext| ext.eq_ignore_ascii_case("ini"))
114    {
115        let text = String::from_utf8(raw).ok()?;
116        return Some(GameexeConfig::from_text(&text));
117    }
118    let opt = crate::resource::load_gameexe_decode_options(project_dir).ok()?;
119    let (text, _report) = decode_gameexe_dat_bytes(&raw, &opt).ok()?;
120    Some(GameexeConfig::from_text(&text))
121}
122
123fn find_gameexe_path(project_dir: &Path) -> Option<PathBuf> {
124    const CANDIDATES: &[&str] = &[
125        "Gameexe.dat",
126        "Gameexe.ini",
127        "gameexe.dat",
128        "gameexe.ini",
129        "GameexeEN.dat",
130        "GameexeEN.ini",
131        "GameexeZH.dat",
132        "GameexeZH.ini",
133        "GameexeZHTW.dat",
134        "GameexeZHTW.ini",
135        "GameexeDE.dat",
136        "GameexeDE.ini",
137        "GameexeES.dat",
138        "GameexeES.ini",
139        "GameexeFR.dat",
140        "GameexeFR.ini",
141        "GameexeID.dat",
142        "GameexeID.ini",
143    ];
144    for name in CANDIDATES {
145        let p = project_dir.join(name);
146        if let Some(path) = crate::resource::resolve_game_file(&p).ok().flatten() {
147            return Some(path);
148        }
149    }
150    None
151}
152
153fn read_i32_le(data: &[u8], off: usize) -> Option<i32> {
154    let bytes: [u8; 4] = data.get(off..off + 4)?.try_into().ok()?;
155    Some(i32::from_le_bytes(bytes))
156}
157
158fn load_tonecurve_rows(path: &Path) -> Result<Vec<Option<ToneCurveRow>>> {
159    let data = crate::resource::read_file_bytes(path)?;
160    if data.len() < 8 {
161        anyhow::bail!("tonecurve file too small");
162    }
163    let cnt = read_i32_le(&data, 4).unwrap_or(0).clamp(0, 256) as usize;
164    let mut out = vec![None; 256];
165    for i in 0..cnt {
166        let off = read_i32_le(&data, 8 + i * 4).unwrap_or(0);
167        if off <= 0 {
168            continue;
169        }
170        let off = off as usize;
171        if off + 16 * 4 + 768 > data.len() {
172            continue;
173        }
174        let typ = read_i32_le(&data, off).unwrap_or(0);
175        let base = off + 16 * 4;
176        let mut lut_r = [0u8; 256];
177        let mut lut_g = [0u8; 256];
178        let mut lut_b = [0u8; 256];
179        lut_r.copy_from_slice(&data[base..base + 256]);
180        lut_g.copy_from_slice(&data[base + 256..base + 512]);
181        lut_b.copy_from_slice(&data[base + 512..base + 768]);
182        let sat = if typ == 1 {
183            read_i32_le(&data, base + 768).unwrap_or(0)
184        } else {
185            0
186        };
187        out[i] = Some(ToneCurveRow {
188            lut_r,
189            lut_g,
190            lut_b,
191            sat,
192        });
193    }
194    Ok(out)
195}