mirror of
https://github.com/Theaninova/mhlib.git
synced 2026-04-22 22:18:55 +00:00
add archive tests
This commit is contained in:
1
rust/springylib/src/media/font/charset-utf8.txt
Normal file
1
rust/springylib/src/media/font/charset-utf8.txt
Normal file
@@ -0,0 +1 @@
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÜabcdefghijklmnopqrstuvwxyzäöüß0123456789,;.:!?+-*/=<>()[]{}\"$%&#~_’^@|¡¿™©®º¹²³ªÀÁÂÃÅÆÇÈÉÊËÌÍÎÏIÐGÑÒÓÔÕŒØSŠÙÚÛÝÞŸŽàáâãåæçèéêëìíîïiðgñòóôõœøsšùúûýþÿž£¥ƒ¤¯¦¬¸¨·§×¢±÷µ«»
|
||||
1
rust/springylib/src/media/font/charset.txt
Normal file
1
rust/springylib/src/media/font/charset.txt
Normal file
@@ -0,0 +1 @@
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZトヨワabcdefghijklmnopqrstuvwxyz蔕<EFBFBD>0123456789,;.:!?+-*/=<>()[]{}\"$%&#~_耽@|。ソ勦ョコケイウェタチツテナニヌネノハヒフヘホマIミGムメモヤユ鈷S館レロン゙沁珮粤褂鉅鳰<E98985>濵<EFBFBD><E6BFB5><EFBFBD><EFBFBD><EFBFBD><EFBFBD>奠涇<E5A5A0><E6B687><EFBFBD>椒・Ζッヲャクィキァラ「ア<EFBDA2>ォサ
|
||||
2
rust/springylib/src/media/font/mod.rs
Normal file
2
rust/springylib/src/media/font/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub const CHARSET: &[u8] = include_bytes!("charset.txt");
|
||||
pub const CHARSET_UTF8: &str = include_str!("charset-utf8.txt");
|
||||
19
rust/springylib/src/media/level.rs
Normal file
19
rust/springylib/src/media/level.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use binrw::prelude::*;
|
||||
|
||||
#[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>,
|
||||
}
|
||||
@@ -1 +1,6 @@
|
||||
pub mod font;
|
||||
pub mod level;
|
||||
pub mod rle;
|
||||
pub mod sprites;
|
||||
pub mod txt;
|
||||
pub mod ui;
|
||||
|
||||
29
rust/springylib/src/media/rle/gif.rs
Normal file
29
rust/springylib/src/media/rle/gif.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use crate::media::rle::{bgra_to_rgba, RleImage};
|
||||
use image::error::{LimitError, LimitErrorKind};
|
||||
use image::{AnimationDecoder, Delay, Frame, Frames, ImageBuffer, ImageError};
|
||||
use std::time::Duration;
|
||||
|
||||
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(ImageError::Limits(LimitError::from_kind(
|
||||
LimitErrorKind::InsufficientMemory,
|
||||
)))?;
|
||||
Ok(Frame::from_parts(
|
||||
buffer,
|
||||
frame.left,
|
||||
frame.top,
|
||||
Delay::from_saturating_duration(Duration::from_millis(80)),
|
||||
))
|
||||
})))
|
||||
}
|
||||
}
|
||||
98
rust/springylib/src/media/rle/mod.rs
Normal file
98
rust/springylib/src/media/rle/mod.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
use binrw::prelude::*;
|
||||
use binrw::Endian;
|
||||
use std::io::{Read, Seek};
|
||||
|
||||
#[cfg(all(feature = "rle_gif"))]
|
||||
pub mod gif;
|
||||
|
||||
#[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
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bgra_to_rgba(pixel: [u8; 4]) -> [u8; 4] {
|
||||
[pixel[2], pixel[1], pixel[0], pixel[3]]
|
||||
}
|
||||
76
rust/springylib/src/media/sprites.rs
Normal file
76
rust/springylib/src/media/sprites.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use crate::error::Error;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Sprites {
|
||||
pub name: String,
|
||||
pub sprite_type: SpriteType,
|
||||
pub file_name: String,
|
||||
pub render_mode: RenderMode,
|
||||
pub frames: Option<CropMode>,
|
||||
}
|
||||
|
||||
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();
|
||||
let invalid_data = |info| Error::InvalidData {
|
||||
info,
|
||||
context: string.to_string(),
|
||||
};
|
||||
let eof = || invalid_data(Some("eof".to_string()));
|
||||
|
||||
Ok(Sprites {
|
||||
file_name: components.next().ok_or_else(eof)?.to_string(),
|
||||
sprite_type: match components.next().ok_or_else(eof)? {
|
||||
"anim_rle" => SpriteType::AnimRle,
|
||||
"anim" => SpriteType::Anim,
|
||||
"static" => SpriteType::Static,
|
||||
e => return Err(invalid_data(Some(e.to_string()))),
|
||||
},
|
||||
name: components.next().ok_or_else(eof)?.to_string(),
|
||||
render_mode: match components.next().ok_or_else(eof)? {
|
||||
"normx" => RenderMode::NormX,
|
||||
"flipx" => RenderMode::FlipX,
|
||||
e => return Err(invalid_data(Some(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(|err| Error::Custom(Box::new(err)))?,
|
||||
})
|
||||
} 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,
|
||||
}
|
||||
101
rust/springylib/src/media/txt.rs
Normal file
101
rust/springylib/src/media/txt.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::num::ParseIntError;
|
||||
use std::string::FromUtf8Error;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DecryptError {
|
||||
FromUtf8Error(FromUtf8Error),
|
||||
ParseIntError(ParseIntError),
|
||||
}
|
||||
|
||||
impl Display for DecryptError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
DecryptError::FromUtf8Error(error) => write!(f, "{}", error),
|
||||
DecryptError::ParseIntError(error) => write!(f, "{}", error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for DecryptError {}
|
||||
|
||||
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 {
|
||||
use crate::media::txt::{decrypt_exposed_txt, decrypt_txt, from_hex};
|
||||
|
||||
#[test]
|
||||
fn it_should_parse_hex() {
|
||||
assert_eq!(from_hex("abcdef").unwrap(), vec![0xab, 0xcd, 0xef]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_decrypt() {
|
||||
let data: Vec<u8> = vec![0x3a, 0x9b, 0x6f, 0x09, 0x7e, 0xd3, 0x74, 0xd6];
|
||||
assert_eq!(decrypt_txt(data.into_iter()).unwrap(), "\r\nsound ",)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_decrypt_exposed() {
|
||||
assert_eq!(
|
||||
decrypt_exposed_txt("83\r\n248ecc86d5d85f6fc6626a6ef5be3e".to_string()).unwrap(),
|
||||
"{\r\n \"isValid\" 1"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -54,8 +54,8 @@ impl Default for FadeMode {
|
||||
}
|
||||
|
||||
impl UiTag {
|
||||
pub fn post_process(&mut self) {
|
||||
if let UiTag::Menu(menu) = self {
|
||||
pub fn post_process(mut self) -> Self {
|
||||
if let UiTag::Menu(mut menu) = &self {
|
||||
let children: Vec<UiTag> = menu.children.drain(..).collect();
|
||||
let mut area_stack: Vec<Vec<UiTag>> = vec![vec![]];
|
||||
|
||||
@@ -84,6 +84,8 @@ impl UiTag {
|
||||
menu.children = area_stack.pop().unwrap();
|
||||
debug_assert!(area_stack.is_empty());
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,8 +104,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn it_should_post_process() {
|
||||
let mut xml: UiTag = serde_xml_rs::from_str(XML).unwrap();
|
||||
xml.post_process();
|
||||
let mut xml = serde_xml_rs::from_str::<UiTag>(XML).unwrap().post_process();
|
||||
|
||||
if let UiTag::Menu(UiMenu { children, .. }) = xml {
|
||||
if let &[UiTag::TextArea(UiTextArea { children, .. })] = &children.as_slice() {
|
||||
|
||||
Reference in New Issue
Block a user