mirror of
https://github.com/Theaninova/mhlib.git
synced 2026-01-21 01:12:58 +00:00
initial commit
This commit is contained in:
46
rust/src/formats/datafile.rs
Normal file
46
rust/src/formats/datafile.rs
Normal 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
46
rust/src/formats/level.rs
Normal 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
115
rust/src/formats/mod.rs
Normal 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
131
rust/src/formats/rle.rs
Normal 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,
|
||||
))
|
||||
}
|
||||
75
rust/src/formats/sprites.rs
Normal file
75
rust/src/formats/sprites.rs
Normal 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
68
rust/src/formats/txt.rs
Normal 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 {}
|
||||
77
rust/src/formats/ui_xml.rs
Normal file
77
rust/src/formats/ui_xml.rs
Normal 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])
|
||||
}
|
||||
Reference in New Issue
Block a user