use mercs2_formats::crc32::crc32_mercs2;
use mercs2_formats::hash::pandemic_hash_m2;
use mercs2_formats::ucfx::walk_decompressed_block;
const LUAQ_SIG: &[u8; 4] = b"\x1bLua";
#[derive(Clone)]
pub struct Entry {
pub name_hash: u32,
pub type_hash: u32,
pub field_c: u32,
pub bytes: Vec<u8>,
}
pub struct ScriptsBlock {
pub entries: Vec<Entry>,
}
fn rd_u32(b: &[u8], off: usize) -> u32 {
u32::from_le_bytes([b[off], b[off + 1], b[off + 2], b[off + 3]])
}
impl ScriptsBlock {
pub fn parse(block: &[u8]) -> Result<Self, String> {
let (parsed, issues) = walk_decompressed_block(block, "scripts_vz");
if let Some(first) = issues.first() {
return Err(format!("walk issue: {} — {}", first.context, first.detail));
}
if parsed.entries.len() != parsed.containers.len() {
return Err(format!(
"entry/container count mismatch: {} vs {}",
parsed.entries.len(),
parsed.containers.len()
));
}
let entries = parsed
.entries
.iter()
.zip(parsed.containers.into_iter())
.map(|(e, bytes)| Entry {
name_hash: e.name_hash,
type_hash: e.type_hash,
field_c: e.field_c,
bytes,
})
.collect();
Ok(Self { entries })
}
pub fn serialize(&self) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&(self.entries.len() as u32).to_le_bytes());
for e in &self.entries {
out.extend_from_slice(&e.name_hash.to_le_bytes());
out.extend_from_slice(&e.type_hash.to_le_bytes());
out.extend_from_slice(&e.field_c.to_le_bytes());
out.extend_from_slice(&(e.bytes.len() as u32).to_le_bytes());
}
for e in &self.entries {
out.extend_from_slice(&e.bytes);
}
out
}
pub fn find_by_name(&self, name: &str) -> Option<usize> {
let h = pandemic_hash_m2(name);
self.entries.iter().position(|e| e.name_hash == h)
}
pub fn verify_csums(&self) -> Result<usize, String> {
for (i, e) in self.entries.iter().enumerate() {
csum_check(&e.bytes).map_err(|m| format!("entry {i}: {m}"))?;
}
Ok(self.entries.len())
}
pub fn extract_lua(&self, idx: usize) -> Result<Vec<u8>, String> {
let c = &self.entries[idx].bytes;
let lay = parse_container(c)?;
Ok(c[lay.luaq_off..lay.csum_off].to_vec())
}
pub fn replace_lua(&mut self, idx: usize, new_luaq: &[u8]) -> Result<(), String> {
let c = &self.entries[idx].bytes;
let lay = parse_container(c)?;
if lay.binn_body_size as usize != lay.luaq_len {
return Err(format!(
"BINN.body_size ({}) != LuaQ length ({}); metadata-bearing BINN not yet supported",
lay.binn_body_size, lay.luaq_len
));
}
let mut nc = Vec::with_capacity(lay.luaq_off + new_luaq.len() + 8);
nc.extend_from_slice(&c[..lay.luaq_off]);
nc.extend_from_slice(new_luaq);
let bs_off = lay.binn_desc_off + 8;
nc[bs_off..bs_off + 4].copy_from_slice(&(new_luaq.len() as u32).to_le_bytes());
let csum = crc32_mercs2(&nc);
nc.extend_from_slice(b"CSUM");
nc.extend_from_slice(&csum.to_le_bytes());
self.entries[idx].bytes = nc;
Ok(())
}
}
pub struct ContainerLayout {
pub data_base: usize,
pub binn_desc_off: usize, pub binn_body_size: u32, pub luaq_off: usize, pub luaq_len: usize, pub csum_off: usize, pub stored_csum: u32,
}
pub fn parse_container(c: &[u8]) -> Result<ContainerLayout, String> {
if c.len() < 28 || &c[0..4] != b"UCFX" {
return Err("not a UCFX container".into());
}
let data_base = rd_u32(c, 4) as usize;
let n_desc = rd_u32(c, 16) as usize;
let mut binn_desc_off = 0usize;
let mut binn_body_size = 0u32;
let mut found = false;
for d in 0..n_desc {
let off = 20 + d * 20;
if off + 20 > c.len() {
return Err("descriptor table overruns container".into());
}
if &c[off..off + 4] == b"BINN" {
binn_desc_off = off;
binn_body_size = rd_u32(c, off + 8);
found = true;
}
}
if !found {
return Err("no BINN descriptor".into());
}
if c.len() < 8 || &c[c.len() - 8..c.len() - 4] != b"CSUM" {
return Err("missing CSUM trailer".into());
}
let csum_off = c.len() - 8;
let stored_csum = rd_u32(c, csum_off + 4);
let luaq_off = find_luaq(c).ok_or("no \\x1bLua signature")?;
let luaq_len = csum_off - luaq_off;
Ok(ContainerLayout {
data_base,
binn_desc_off,
binn_body_size,
luaq_off,
luaq_len,
csum_off,
stored_csum,
})
}
fn find_luaq(c: &[u8]) -> Option<usize> {
c.windows(4).position(|w| w == LUAQ_SIG)
}
fn csum_check(c: &[u8]) -> Result<(), String> {
if c.len() < 8 || &c[c.len() - 8..c.len() - 4] != b"CSUM" {
return Err("missing CSUM trailer".into());
}
let stored = rd_u32(c, c.len() - 4);
let computed = crc32_mercs2(&c[..c.len() - 8]);
if stored != computed {
return Err(format!(
"CSUM mismatch: stored 0x{stored:08X} computed 0x{computed:08X}"
));
}
Ok(())
}