initial commit

This commit is contained in:
2023-05-04 00:32:54 +02:00
commit 44b4d847e5
34 changed files with 3510 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
use binrw::{binread, NullString};
use std::collections::HashMap;
#[binread]
#[br(little, magic = b"MHJNR")]
#[derive(Debug)]
pub struct Datafile {
#[br(align_after = 0x20)]
pub edition: Edition,
#[br(temp)]
pub count: u32,
#[br(align_after = 0x20)]
pub unk1: u32,
#[br(count = count)]
pub files: Vec<FileEntry>,
}
#[binread]
#[derive(Debug)]
pub enum Edition {
#[br(magic = b"-XS")]
Xs,
#[br(magic = b"-XXL")]
Xxl,
}
#[derive(Debug)]
#[binread]
pub struct FileEntry {
#[br(pad_size_to = 0x68)]
pub name: NullString,
pub pos: u32,
#[br(pad_after = 0x10)]
pub len: u32,
}
impl Datafile {
pub fn into_index(self) -> HashMap<String, FileEntry> {
self.files
.into_iter()
.map(|entry| (entry.name.to_string(), entry))
.collect()
}
}

46
rust/src/formats/level.rs Normal file
View File

@@ -0,0 +1,46 @@
use binrw::prelude::*;
use binrw::{BinRead, Error};
use image;
use image::error::{DecodingError, ImageFormatHint};
use image::{ImageError, ImageResult, Rgb, RgbImage};
use std::io::Cursor;
#[binrw]
#[brw(little)]
pub struct LevelTile {
pub index: u8,
pub id: u8,
}
#[binrw]
#[brw(little)]
pub struct LevelLayer {
pub tile_count: u32,
pub width: u32,
pub height: u32,
pub unknown_2: u32,
#[br(count = width * height)]
pub tiles: Vec<LevelTile>,
}
pub fn level_tile_data_to_image(tile_data: &[u8]) -> ImageResult<RgbImage> {
let mut cursor = Cursor::new(tile_data);
let layer = LevelLayer::read(&mut cursor).map_err(to_decoding_err)?;
let mut image = RgbImage::new(layer.width, layer.height);
for y in 0..layer.height {
for x in 0..layer.width {
let tile = LevelTile::read(&mut cursor).map_err(to_decoding_err)?;
image.put_pixel(x, y, Rgb([tile.id, tile.index, 0]));
}
}
Ok(image)
}
fn to_decoding_err(err: Error) -> ImageError {
ImageError::Decoding(DecodingError::new(
ImageFormatHint::Name(String::from("mhjnr_layer")),
err,
))
}

115
rust/src/formats/mod.rs Normal file
View File

@@ -0,0 +1,115 @@
use crate::formats::datafile::FileEntry;
use crate::formats::level::LevelLayer;
use crate::formats::rle::RleImage;
use crate::formats::sprites::Sprites;
use crate::formats::txt::{decrypt_txt, DecryptError};
use crate::formats::ui_xml::UiTag;
use binrw::BinRead;
use std::ffi::OsStr;
use std::fmt::Debug;
use std::io::{Cursor, Read, Seek, SeekFrom};
use std::path::Path;
pub mod datafile;
pub mod level;
pub mod rle;
pub mod sprites;
pub mod txt;
pub mod ui_xml;
pub enum DatafileFile {
Txt(String),
Level(LevelLayer),
Sprites(Vec<Sprites>),
RleSprite(Box<RleImage>),
Bitmap(Vec<u8>),
Vorbis(Vec<u8>),
TileCollision(String),
Ui(UiTag),
}
pub enum Error {
Deserialization,
UnknownFormat(String),
UnknownError,
Custom(String),
DecryptError(DecryptError),
}
fn custom_err<T>(e: T) -> Error
where
T: Debug,
{
Error::Custom(format!("{:#?}", e))
}
pub fn load_data<R>(entry: &FileEntry, reader: &mut R) -> Result<DatafileFile, Error>
where
R: Read + Seek,
{
reader
.seek(SeekFrom::Start(entry.pos as u64))
.map_err(custom_err)?;
let mut data = vec![0u8; entry.len as usize];
reader.read_exact(&mut data).map_err(custom_err)?;
let name = entry.name.to_string();
let path = Path::new(&name);
match path
.extension()
.and_then(OsStr::to_str)
.ok_or(Error::Custom("No extension".to_string()))?
{
"dat" => Ok(DatafileFile::Level(
LevelLayer::read(&mut Cursor::new(data)).map_err(custom_err)?,
)),
"rle" => Ok(DatafileFile::RleSprite(Box::new(
RleImage::read(&mut Cursor::new(data)).map_err(custom_err)?,
))),
"bmp" => Ok(DatafileFile::Bitmap(data)),
"ogg" => Ok(DatafileFile::Vorbis(data)),
"xml" => {
serde_xml_rs::from_str::<UiTag>(String::from_utf8(data).map_err(custom_err)?.as_str())
.map_err(custom_err)
.map(DatafileFile::Ui)
}
"txt" => {
let stem = path
.file_stem()
.and_then(OsStr::to_str)
.ok_or(Error::Custom("Stem".to_string()))?;
let decr = decrypt_txt(data.into_iter()).map_err(|e| Error::DecryptError(e))?;
if stem.starts_with("tile_collision") {
Ok(DatafileFile::TileCollision(decr))
} else if stem == "sprites" {
Ok(DatafileFile::Sprites(
Sprites::parse(decr.as_str()).map_err(custom_err)?,
))
} else {
Ok(DatafileFile::Txt(decr))
}
}
/*Some("rle") => {
let image: RleImage = RleImage::read(&mut Cursor::new(data)).unwrap();
let path = Path::new(dat_path).with_file_name("res.gif");
println!("{:?}", path);
let mut encoder = GifEncoder::new(
OpenOptions::new()
.create(true)
.write(true)
.open(path)
.unwrap(),
);
encoder.set_repeat(Repeat::Infinite).unwrap();
encoder.try_encode_frames(image.into_frames()).unwrap();
}
Some("dat") => {
let image = level_tile_data_to_image(&data).unwrap();
let path = Path::new(dat_path).with_file_name("res.png");
println!("{:?}", path);
image.save_with_format(path, ImageFormat::Png).unwrap();
}*/
ext => Err(Error::UnknownFormat(ext.to_string())),
}
}

131
rust/src/formats/rle.rs Normal file
View File

@@ -0,0 +1,131 @@
use binrw::prelude::*;
use binrw::Endian;
use image::error::{DecodingError, ImageFormatHint};
use image::{AnimationDecoder, Delay, Frame, Frames, ImageBuffer, ImageError};
use std::io::{Read, Seek};
use std::time::Duration;
#[binread]
#[br(little, magic = 0x67u32)]
pub struct RleImage {
pub hash: u64,
pub color_table: [[u8; 4]; 512],
pub width: u32,
pub height: u32,
pub numerator: u32,
pub denominator: u32,
#[br(temp)]
pub frame_count: u32,
#[br(count = frame_count)]
pub frames: Vec<RleLayer>,
}
#[binread]
#[br(little)]
pub struct RleLayer {
pub width: u32,
pub height: u32,
pub left: u32,
pub top: u32,
pub numerator: u32,
pub denominator: u32,
pub data_size: u32,
pub unknown3: u32,
#[br(args(width * height), parse_with = parse_rle)]
pub data: Vec<u8>,
}
pub fn parse_rle<R: Read + Seek>(
reader: &mut R,
endian: Endian,
(size,): (u32,),
) -> BinResult<Vec<u8>> {
let mut data = Vec::with_capacity(size as usize);
while data.len() != size as usize {
let count: i8 = reader.read_type(endian)?;
if count > 0 {
let value: u8 = reader.read_type(endian)?;
for _ in 0..count {
data.push(value);
}
} else {
for _ in 0..-count {
data.push(reader.read_type(endian)?);
}
}
}
Ok(data)
}
impl RleImage {
pub fn get_image_data(&self, layer: &RleLayer) -> Vec<u8> {
let mut data = Vec::<u8>::with_capacity(self.width as usize * self.height as usize * 4);
let mut i = 0;
for y in 0..self.height {
for x in 0..self.width {
if y < layer.top
|| y >= layer.top + layer.height
|| x < layer.left
|| x >= layer.left + layer.width
{
data.push(0);
data.push(0);
data.push(0);
data.push(0);
} else {
let color = self.color_table[layer.data[i] as usize];
i += 1;
data.push(color[2]);
data.push(color[1]);
data.push(color[0]);
data.push(if color[2] == 0 && color[1] == 0 && color[0] == 0 {
0
} else {
255
});
}
}
}
data
}
}
impl<'a> AnimationDecoder<'a> for RleImage {
fn into_frames(self) -> Frames<'a> {
Frames::new(Box::new(self.frames.into_iter().map(move |frame| {
let buffer = ImageBuffer::from_raw(
frame.width,
frame.height,
frame
.data
.into_iter()
.flat_map(|it| bgra_to_rgba(self.color_table[it as usize]))
.collect(),
)
.ok_or(to_rle_image_err(std::fmt::Error::default()))?;
Ok(Frame::from_parts(
buffer,
frame.left,
frame.top,
Delay::from_saturating_duration(Duration::from_millis(80)),
))
})))
}
}
pub fn bgra_to_rgba(pixel: [u8; 4]) -> [u8; 4] {
[pixel[2], pixel[1], pixel[0], pixel[3]]
}
fn to_rle_image_err<T>(err: T) -> ImageError
where
T: Into<Box<dyn std::error::Error + Send + Sync>>,
{
ImageError::Decoding(DecodingError::new(
ImageFormatHint::Name(String::from("mhjnr_rle")),
err,
))
}

View File

@@ -0,0 +1,75 @@
#[derive(Debug)]
pub struct Sprites {
pub name: String,
pub sprite_type: SpriteType,
pub file_name: String,
pub render_mode: RenderMode,
pub frames: Option<CropMode>,
}
#[derive(Debug)]
pub enum Error {
InvalidData,
UnknownEnum(String),
}
impl Sprites {
pub fn parse(string: &str) -> Result<Vec<Self>, Error> {
string
.split('\n')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.map(Sprites::parse_single)
.collect()
}
pub fn parse_single(string: &str) -> Result<Self, Error> {
let mut components = string.split_whitespace();
Ok(Sprites {
file_name: components.next().ok_or(Error::InvalidData)?.to_string(),
sprite_type: match components.next().ok_or(Error::InvalidData)? {
"anim_rle" => SpriteType::AnimRle,
"anim" => SpriteType::Anim,
"static" => SpriteType::Static,
e => return Err(Error::UnknownEnum(e.to_string())),
},
name: components.next().ok_or(Error::InvalidData)?.to_string(),
render_mode: match components.next().ok_or(Error::InvalidData)? {
"normx" => RenderMode::NormX,
"flipx" => RenderMode::FlipX,
e => return Err(Error::UnknownEnum(e.to_string())),
},
frames: if let Some(c) = components.next() {
Some(match c {
"nocrop" => CropMode::NoCrop,
x => x
.parse::<i32>()
.map(CropMode::FrameCount)
.map_err(|e| Error::UnknownEnum(e.to_string()))?,
})
} else {
None
},
})
}
}
#[derive(Debug)]
pub enum CropMode {
FrameCount(i32),
NoCrop,
}
#[derive(Debug)]
pub enum RenderMode {
NormX,
FlipX,
}
#[derive(Debug)]
pub enum SpriteType {
Static,
Anim,
AnimRle,
}

68
rust/src/formats/txt.rs Normal file
View File

@@ -0,0 +1,68 @@
use std::num::ParseIntError;
use std::string::FromUtf8Error;
#[derive(Debug)]
pub enum DecryptError {
FromUtf8Error(FromUtf8Error),
ParseIntError(ParseIntError),
}
impl From<FromUtf8Error> for DecryptError {
fn from(e: FromUtf8Error) -> DecryptError {
DecryptError::FromUtf8Error(e)
}
}
impl From<ParseIntError> for DecryptError {
fn from(e: ParseIntError) -> DecryptError {
DecryptError::ParseIntError(e)
}
}
/// Decrypts txt files contained inside the dat file
pub fn decrypt_txt<I>(buffer: I) -> Result<String, DecryptError>
where
I: Iterator<Item = u8>,
{
let mut key = 0x1234u16;
String::from_utf8(
buffer
.map(|char| {
let decr = char ^ key as u8;
key = key.wrapping_mul(3).wrapping_add(2);
decr
})
.map(|char| (((char >> 1) ^ (char << 1)) & 0x55) ^ (char << 1))
.collect(),
)
.map_err(DecryptError::from)
}
/// Parses a hex string to a Vec<u8>
fn from_hex(line: &str) -> Result<Vec<u8>, ParseIntError> {
(0..line.len())
.step_by(2)
.map(|i| u8::from_str_radix(line.get(i..=i + 1).unwrap_or(""), 16))
.collect()
}
/// This function is applied to *exposed* txt files,
/// such as the player profile or high scores
///
/// If the file is contained in the datafile, it has
/// to first be decrypted normally and then again
/// with this function.
pub fn decrypt_exposed_txt(contents: String) -> Result<String, DecryptError> {
contents
.split_terminator("\r\n")
.map(|line| line.trim())
.filter(|line| !line.is_empty())
.map(from_hex)
.map(|line| decrypt_txt(line.map_err(DecryptError::from)?.into_iter()))
.collect::<Result<Vec<String>, _>>()
.map(|l| l.join("\r\n"))
}
#[cfg(test)]
mod tests {}

View File

@@ -0,0 +1,77 @@
use serde::de::Error;
use serde::{Deserialize, Deserializer};
#[derive(Debug, Deserialize)]
pub enum UiTag {
Menu(UiMenu),
Image(UiImage),
TextButton(UiTextButton),
}
#[derive(Debug, Deserialize)]
pub struct UiMenu {
pub selected: String,
#[serde(rename = "OnBack")]
pub on_back: String,
#[serde(rename = "$value")]
pub children: Vec<UiTag>,
}
#[derive(Debug, Deserialize)]
pub struct UiImage {
pub texture: String,
#[serde(deserialize_with = "deserialize_vec2")]
pub position: [i32; 2],
#[serde(deserialize_with = "deserialize_vec2")]
pub size: [i32; 2],
#[serde(rename = "fademode")]
pub fade_mode: FadeMode,
}
#[derive(Debug, Deserialize)]
pub struct UiTextButton {
pub name: Option<String>,
pub text: String,
#[serde(deserialize_with = "deserialize_vec2")]
pub position: [i32; 2],
#[serde(rename = "halign")]
pub horizontal_align: HorizontalAlign,
#[serde(rename = "fademode")]
pub fade_mode: FadeMode,
#[serde(rename = "OnSelect")]
pub on_select: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum HorizontalAlign {
Center,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum FadeMode {
None,
}
fn deserialize_vec2<'de, D>(deserializer: D) -> Result<[i32; 2], D::Error>
where
D: Deserializer<'de>,
{
let buf = String::deserialize(deserializer)?;
let mut values: Vec<Result<i32, D::Error>> = buf
.split(',')
.into_iter()
.map(|value| {
// there's some typos so we have to cover that...
value.split_ascii_whitespace().collect::<Vec<&str>>()[0]
.trim()
.parse::<i32>()
.map_err(|err| Error::custom(err.to_string()))
})
.collect();
let y = values.pop().ok_or(Error::custom("InvalidField"))??;
let x = values.pop().ok_or(Error::custom("InvalidField"))??;
Ok([x, y])
}