Added compatibility mode to mkrlen.

Fixed null terminator not being written by `mkrlen`.

The compatibility mode is necessary because many rlens are formatted weirdly, and I couldn't find any algorithm to follow that would result in their format.
Also only figured out it has a null terminator because without it there's a map part that's a multiple of `0x800` and it was missing a sector.
This commit is contained in:
2023-10-11 19:01:07 +01:00
parent 311efdf4ca
commit 38427e80d3
6 changed files with 185 additions and 45 deletions

7
tools/Cargo.lock generated
View File

@@ -448,6 +448,7 @@ dependencies = [
"clap",
"ddw3-logger",
"ddw3-util",
"hex-literal",
"itertools 0.11.0",
"pathdiff",
"serde",
@@ -808,6 +809,12 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hex-literal"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46"
[[package]]
name = "iana-time-zone"
version = "0.1.57"

View File

@@ -67,6 +67,7 @@ thiserror = "1.0.47"
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
xml-rs = "0.8.16"
hex-literal = "0.4.1"
# Workspace
ddw3-bytes = { path = "ddw3-bytes" }

View File

@@ -15,3 +15,4 @@ pathdiff = { workspace = true }
serde = { workspace = true }
serde_yaml = { workspace = true }
tracing = { workspace = true }
hex-literal = { workspace = true }

View File

@@ -14,4 +14,11 @@ pub struct Args {
/// The output file
#[clap(long = "output", short = 'o')]
pub output: PathBuf,
/// Run in compatibility mode for file.
///
/// Expects a string of format `<map>.<map-part>`,
/// for example, `S232.12`
#[clap(long = "compatibility")]
pub compatibility: Option<String>,
}

View File

@@ -1,7 +1,15 @@
//! `RLEN` creator
// Features
#![feature(array_chunks, array_windows, seek_stream_len, let_chains)]
#![feature(
array_chunks,
array_windows,
seek_stream_len,
let_chains,
generic_arg_infer,
lint_reasons,
decl_macro
)]
// Modules
mod args;
@@ -12,6 +20,7 @@ use {
anyhow::Context,
byteorder::{LittleEndian, WriteBytesExt},
clap::Parser,
hex_literal::hex,
std::{
fs,
io::{self, BufWriter, Seek, Write},
@@ -29,6 +38,7 @@ fn main() -> Result<(), anyhow::Error> {
// Read the input file
// TODO: Stream the input instead of reading it all?
let input = fs::read(&args.input).context("Unable to read input file")?;
let input_len = input.len();
// Create the output file
let output = fs::File::create(&args.output).context("Unable to open output file")?;
@@ -39,65 +49,155 @@ fn main() -> Result<(), anyhow::Error> {
.seek(io::SeekFrom::Start(8))
.context("Unable to seek past header")?;
let mut total_size = 0;
let mut input = &*input;
loop {
// If we're running in compatibility mode, manually output some parts
if let Some(compatibility) = &args.compatibility {
macro if_eq_then($cmp:expr => $res:expr) {
(input == hex![$cmp]).then_some(&hex![$res] as &[u8])
}
let bytes = match compatibility.as_str() {
"S205.43" => if_eq_then!("6b5aff" => "026b5a81ff"),
"S210.16" | "S212.12" => if_eq_then!("1b18" => "011b8118"),
"S225.12" => if_eq_then!("03050a0c142a44454d251f4c231625211623481b2149201a48282348303f432f2c182230" =>
"2303050A0C142A44454D251F4C231625211623481B2149201A48282348303F432F2C18228130"
),
"S232.12" => if_eq_then!("2f" => "812f"),
"S237.0" | "S238.0" | "S245.0" | "S406.15" | "S540.161" | "S641.42" | "S735.36" | "S736.36" =>
if_eq_then!("00" => "8100"),
"S286.53" => if_eq_then!("0007" => "01008107"),
"S300.14" => if_eq_then!("19" => "8119"),
"S315.51" => if_eq_then!("bbb7c4bcb7b2bbc0ac" => "08bbb7c4bcb7b2bbc081ac"),
"S346.79" => if_eq_then!("9c93372ab8e78dbdc74dae6d" => "0b9c93372ab8e78dbdc74dae816d"),
"S360.9" => if_eq_then!("030016" => "0203008116"),
"S381.33" => if_eq_then!("ccd1c8b1cbccc2c3" => "07ccd1c8b1cbccc281c3"),
"S405.15" =>
if_eq_then!("e74f1f161d140e0c0a0b0a0e0f0e0b11120806040200" => "15e74f1f161d140e0c0a0b0a0e0f0e0b1112080604028100"),
"S406.109" => if_eq_then!("6d72642f" => "036d7264812f"),
"S490.79" => if_eq_then!("47" => "8147"),
"S545.45" => if_eq_then!("efc30469f48f00" => "06efc30469f48f8100"),
"S550.6" => if_eq_then!("1d00" => "011d8100"),
"S650.22" => if_eq_then!("101e" => "0110811e"),
"S760.57" => if_eq_then!("050b050703070e" => "06050b05070307810e"),
"S860.4" =>
if_eq_then!("6d5e474e63536e8765453c393c51768489dbd9a9f2ecf2f8d5deb789809ba78c91afc2d1781b00" => "266d5e474e63536e8765453c393c51768489dbd9a9f2ecf2f8d5deb789809ba78c91afc2d1781b8100"),
"S875.7" => if_eq_then!("696c6f7e8574642b080400" => "0a696c6f7e8574642b08048100"),
"S880.17" => if_eq_then!("0612242f2b241913417e927a300300" => "0e0612242f2b241913417e927a30038100"),
"S890.38" => if_eq_then!("093393a27e494440311d0a0100" => "0c093393a27e494440311d0a018100"),
"S895.10" => if_eq_then!("1d0b0200" => "031d0b028100"),
"S895.48" =>
if_eq_then!("4027140400010918344d5b4a38250d0200" => "104027140400010918344d5b4a38250d028100"),
"S895.50" =>
if_eq_then!("492a150501020c1f3c54665a412a100300" => "10492a150501020c1f3c54665a412a10038100"),
_ => {
tracing::debug!(?compatibility, "Ignoring unknown compatibility");
None
},
};
if let Some(bytes) = bytes {
output.write_all(bytes).context("Unable to write output")?;
break;
}
/*
//#[expect(clippy::type_complexity, reason = "This is the only mention of the type")]
const CASES: &[(&[&str], &[u8], &[u8]); 3] = &[
(&["S205"], &hex!["6b5aff"], &hex!["026b5a81ff"]),
(&["S210", "S212"], &hex!["1b18"], &hex!["011b8118"]),
(
&["S225"],
&
),
];
for &(filenames, needle, bytes) in CASES {
// If `filename` isn't part of the input file name, quit
if !args
.input.
.map_or(false, |input_filename| {
filenames.iter().any(|filename| input_filename.contains(filename))
}) {
continue;
}
// If we found it, write the output and break all the way out.
if input == needle {
output.write_all(bytes).context("Unable to write output")?;
break 'main;
}
}
*/
}
// Get the current byte
let Some(&cur) = input.first() else {
break;
};
match input.get(1) {
Some(&next) => match cur == next {
true => {
let mut len = match input[1..].iter().position(|&b| b != cur) {
Some(idx) => idx + 1,
None => input.len(),
};
assert!(len >= 2, "Tried to use repeat opcode on length less than 2: {len}");
// And the next one
// Note: If there is no next one, just write the current and leave
let Some(&next) = input.get(1) else {
if args.compatibility.as_deref() == Some("S641.42") {
println!("Got {cur:x}");
}
for bytes in input[..len].chunks(0x7f) {
// If we'd emit less than a `0x82`, instead quit
// Note: This implies that `bytes.len()` is 1, as it will
// never be 0, and we've just checked it's `< 2`
if bytes.len() < 2 {
len -= 1;
break;
}
output.write_u8(0x01).context("Unable to write opcode")?;
output.write_u8(cur).context("Unable to write data")?;
break;
};
let opcode = u8::try_from(0x80 + bytes.len()).expect("Chunk size was greater than 0x80");
output.write_u8(opcode).context("Unable to write opcode")?;
output.write_u8(cur).context("Unable to write data")?;
match cur == next {
true => {
let mut len = match input[1..].iter().position(|&b| b != cur) {
Some(idx) => idx + 1,
None => input.len(),
};
assert!(len >= 2, "Tried to use repeat opcode on length less than 2: {len}");
for bytes in input[..len].chunks(0x7f) {
// If we'd emit less than a `0x82`, instead quit
// Note: This implies that `bytes.len()` is 1, as it will
// never be 0, and we've just checked it's `< 2`
if bytes.len() < 2 {
len -= 1;
break;
}
input = &input[len..];
total_size += len;
},
false => {
let len = match input[1..].array_windows().position(|&[b0, b1]| b0 == b1) {
Some(idx) => idx + 1,
None => input.len(),
};
let opcode = u8::try_from(0x80 + bytes.len()).expect("Chunk size was greater than 0x80");
output.write_u8(opcode).context("Unable to write opcode")?;
output.write_u8(cur).context("Unable to write data")?;
}
for bytes in input[..len].chunks(0x7f) {
let opcode = u8::try_from(bytes.len()).expect("Chunk size was greater than 0x80");
assert!(opcode < 0x80, "Chunk size was greater than 0x80");
output.write_u8(opcode).context("Unable to write opcode")?;
output.write_all(bytes).context("Unable to write data")?;
}
input = &input[len..];
total_size += len;
},
input = &input[len..];
},
None => {
output.write_u8(0x01).context("Unable to write opcode")?;
output.write_u8(cur).context("Unable to write data")?;
total_size += 1;
break;
false => {
let len = match input[1..].array_windows().position(|&[b0, b1]| b0 == b1) {
Some(idx) => idx + 1,
None => match args.compatibility.as_deref() {
Some("S641.42") => input.len() - 1,
_ => input.len(),
},
};
for bytes in input[..len].chunks(0x7f) {
let opcode = u8::try_from(bytes.len()).expect("Chunk size was greater than 0x80");
assert!(opcode < 0x80, "Chunk size was greater than 0x80");
output.write_u8(opcode).context("Unable to write opcode")?;
output.write_all(bytes).context("Unable to write data")?;
}
input = &input[len..];
},
}
}
// Finally write the null terminator
output.write_u8(0x00).context("Unable to write null terminator")?;
// Then go back and write the header
output
.seek(io::SeekFrom::Start(0))
@@ -105,7 +205,7 @@ fn main() -> Result<(), anyhow::Error> {
output
.write_u32::<LittleEndian>(u32::from_le_bytes(*b"RLEN"))
.context("Unable to write magic")?;
let total_size = u32::try_from(total_size).context("Total size didn't fit into `u32`")?;
let total_size = u32::try_from(input_len).context("Total size didn't fit into `u32`")?;
output
.write_u32::<LittleEndian>(total_size)
.context("Unable to write total size")?;

24
tools/repack-rlen.sh Executable file
View File

@@ -0,0 +1,24 @@
#!/bin/env bash
set -e
DIR="$(basename "$(dirname "$1")")"
FILE="$(basename "$1")"
COMPATIBILITY="${DIR%PACK}.${FILE%.bin}"
OUTPUT="${1%.bin}.out.bin"
#cargo build \
# --manifest-path=tools/Cargo.toml \
# --release \
# -p ddw3-unrlen \
# -p ddw3-mkrlen
./tools/target/release/ddw3-unrlen "$1" \
--output "$OUTPUT"
./tools/target/release/ddw3-mkrlen "$OUTPUT" \
--compatibility "$COMPATIBILITY" \
--output "$1"
#rm "$OUTPUT"