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])
}

272
rust/src/godot/datafile.rs Normal file
View File

@@ -0,0 +1,272 @@
use crate::formats;
use crate::formats::datafile::{Datafile, FileEntry};
use crate::formats::sprites::{CropMode, RenderMode, SpriteType};
use crate::formats::{load_data, DatafileFile};
use crate::godot::font::load_bitmap_font;
use crate::godot::game_object::parse_game_object;
use crate::godot::image::{load_bmp_as_image_texture, load_rle_as_sprite_frames};
use crate::godot::sprites::load_sprite_frames;
use crate::godot::tile_map::{create_tile_map, TileCollision};
use crate::godot::ui::convert_ui;
use binrw::BinRead;
use godot::engine::global::Error;
use godot::engine::image::Format;
use godot::engine::resource_loader::CacheMode;
use godot::engine::resource_saver::SaverFlags;
use godot::engine::utilities::{printerr, prints};
use godot::engine::{
AtlasTexture, AudioStream, AudioStreamOggVorbis, DirAccess, OggPacketSequence,
PlaceholderTexture2D, SpriteFrames,
};
use godot::engine::{Image, PckPacker};
use godot::engine::{ImageTexture, ProjectSettings};
use godot::engine::{ResourceFormatLoader, ResourceSaver};
use godot::engine::{ResourceFormatLoaderVirtual, ResourceLoader};
use godot::prelude::*;
use itertools::Itertools;
use std::collections::HashMap;
use std::fs::File;
use std::str::FromStr;
const DAT_PATH: &str = "E:\\Games\\Schatzjäger\\data\\datafile.dat";
#[derive(GodotClass)]
#[class(base=ResourceFormatLoader)]
pub struct DatafileLoader {
pub datafile_table: HashMap<String, FileEntry>,
#[base]
pub base: Base<ResourceFormatLoader>,
}
fn convert_path(path: &GodotString) -> String {
path.to_string()
.strip_prefix("datafile://")
.map(|it| it.replace('/', "\\"))
.expect("Invalid path")
}
#[godot_api]
impl DatafileLoader {
fn save_to_cache(&self, resource: Gd<Resource>, path: String) {
let cache_path = self.get_cache_path(path);
match DirAccess::make_dir_recursive_absolute(cache_path.rsplit_once('/').unwrap().0.into())
{
Error::OK => (),
error => printerr(error.to_variant(), &[]),
}
ResourceSaver::singleton().save(resource, cache_path.into(), SaverFlags::FLAG_NONE);
}
fn get_cache_path(&self, path: String) -> String {
format!(
"{}/.cache/{}",
DAT_PATH
.replace('\\', "/")
.strip_suffix("datafile.dat")
.unwrap(),
path.replace('\\', "/")
)
}
fn retrieve_cache<T>(&self, path: String) -> Option<Gd<T>>
where
T: GodotClass + Inherits<Resource>,
{
let cache_path = self.get_cache_path(path);
let type_hint = T::CLASS_NAME;
if !ResourceLoader::singleton().exists(cache_path.clone().into(), type_hint.into()) {
return None;
}
ResourceLoader::singleton()
.load(
cache_path.into(),
type_hint.into(),
CacheMode::CACHE_MODE_REUSE,
)
.map(|it| it.cast())
}
}
#[godot_api]
impl ResourceFormatLoaderVirtual for DatafileLoader {
fn init(base: Base<Self::Base>) -> Self {
let mut file = File::open(DAT_PATH).unwrap();
let datafile_table = Datafile::read(&mut file).unwrap().into_index();
DatafileLoader {
base,
datafile_table,
}
}
fn get_recognized_extensions(&self) -> PackedStringArray {
PackedStringArray::from(&[
"xml".into(),
"txt".into(),
"rle".into(),
"bmp".into(),
"dat".into(),
])
}
fn recognize_path(&self, path: GodotString, _type: StringName) -> bool {
path.to_string().starts_with("datafile://")
}
fn get_resource_type(&self, path: GodotString) -> GodotString {
if path.to_string().ends_with(".dat") {
"PackedScene".into()
} else {
"Resource".into()
}
}
fn get_resource_script_class(&self, _path: GodotString) -> GodotString {
GodotString::from_str("").unwrap()
}
fn exists(&self, path: GodotString) -> bool {
self.datafile_table
.contains_key(convert_path(&path).as_str())
}
fn get_classes_used(&self, _path: GodotString) -> PackedStringArray {
PackedStringArray::from(&[])
}
fn load(
&self,
path: GodotString,
_original_path: GodotString,
_use_sub_threads: bool,
_cache_mode: i64,
) -> Variant {
let datafile_path = convert_path(&path);
if let Some(resource) = self.retrieve_cache::<Resource>(format!(
"{}.{}",
datafile_path,
if datafile_path.ends_with(".xml") || datafile_path.ends_with("dat") {
"scn"
} else {
"res"
}
)) {
return resource.to_variant();
}
if let Some(target) = self.datafile_table.get(datafile_path.as_str()) {
let mut file = File::open(DAT_PATH).unwrap();
match load_data(target, &mut file) {
Ok(DatafileFile::Level(level)) => {
let level_id = datafile_path
.split_terminator('\\')
.find(|i| i.starts_with("level"))
.map(|lvl| u32::from_str(lvl.strip_prefix("level").unwrap()).unwrap())
.unwrap();
let tile_map = create_tile_map(level, level_id);
self.save_to_cache(tile_map.share().upcast(), format!("{}.scn", datafile_path));
tile_map.to_variant()
}
Ok(DatafileFile::Txt(txt)) => {
let game_object = parse_game_object(txt);
self.save_to_cache(
game_object.share().upcast(),
format!("{}.res", datafile_path),
);
game_object.to_variant()
}
Ok(DatafileFile::Ui(ui)) => {
let ui = convert_ui(ui);
let mut scene = PackedScene::new();
scene.pack(ui.upcast());
self.save_to_cache(scene.share().upcast(), format!("{}.scn", datafile_path));
scene.to_variant()
}
Ok(DatafileFile::Vorbis(vorbis)) => {
let mut audio = AudioStreamOggVorbis::new();
audio.set_loop(true);
let mut packet = OggPacketSequence::new();
packet.set_packet_data(Array::from(&[Array::from(&[PackedByteArray::from(
vorbis.as_slice(),
)
.to_variant()])]));
audio.set_packet_sequence(packet);
audio.to_variant()
}
Ok(DatafileFile::RleSprite(rle)) => load_rle_as_sprite_frames(*rle).to_variant(),
Ok(DatafileFile::Sprites(sprites)) => {
let sprite_frames = load_sprite_frames(sprites, path);
self.save_to_cache(
sprite_frames.share().upcast(),
format!("{}.res", datafile_path),
);
sprite_frames.to_variant()
}
Ok(DatafileFile::Bitmap(data)) => {
let gd_image = match load_bmp_as_image_texture(data) {
Ok(image) => image,
Err(err) => return err.to_variant(),
};
if datafile_path.contains("\\fonts\\") {
let font = load_bitmap_font(gd_image);
self.save_to_cache(
font.share().upcast(),
format!("{}.tres", datafile_path),
);
font.to_variant()
} else {
let mut texture = ImageTexture::new();
texture.set_image(gd_image);
self.save_to_cache(
texture.share().upcast(),
format!("{}.res", datafile_path),
);
texture.to_variant()
}
}
Ok(DatafileFile::TileCollision(collision)) => {
let tile_collision = Gd::new(TileCollision {
collision: collision
.chars()
.filter_map(|c| c.to_digit(10))
.map(|d| d as u8)
.collect(),
});
// No need to save this to cache, we only use this internally
/*self.save_to_cache(
tile_collision.share().upcast(),
format!("{}.res", datafile_path),
);*/
tile_collision.to_variant()
}
Err(formats::Error::UnknownFormat(ext)) => {
printerr(format!("Unknown format <{}>", ext).to_variant(), &[]);
Error::ERR_FILE_UNRECOGNIZED.to_variant()
}
Err(formats::Error::Deserialization) => {
printerr("Failed to deserialize".to_variant(), &[]);
Error::ERR_FILE_CORRUPT.to_variant()
}
Err(formats::Error::Custom(message)) => {
printerr(message.to_variant(), &[]);
Error::ERR_BUG.to_variant()
}
_ => {
printerr("Unknown error".to_variant(), &[]);
Error::ERR_BUG.to_variant()
}
}
} else {
printerr("File not found".to_variant(), &[]);
Error::ERR_FILE_NOT_FOUND.to_variant()
}
}
}

88
rust/src/godot/font.rs Normal file
View File

@@ -0,0 +1,88 @@
use godot::builtin::{Rect2, Vector2, Vector2i};
use godot::engine::{FontFile, Image};
use godot::prelude::utilities::{print_verbose, prints};
use godot::prelude::{Color, Gd, Share, ToVariant};
use std::ops::Index;
use unicode_segmentation::UnicodeSegmentation;
const CHARSET: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÜabcdefghijklmnopqrstuvwxyzäöüß0123456789,;.:!?\
+-*/=<>()[]{}\"$%&#~_^@|¡¿™©®º¹²³ªÀÁÂÃÅÆÇÈÉÊËÌÍÎÏIÐGÑÒÓÔÕŒØSŠÙÚÛÝÞŸŽàáâãåæçèéêëìíî\
ïiðgñòóôõœøsšùúûýþÿž£¥ƒ¤¯¦¬¸¨·§×¢±÷µ«»";
pub fn load_bitmap_font(image: Gd<Image>) -> Gd<FontFile> {
let mut font_chars = CHARSET.as_bytes().iter();
let mut font_file = FontFile::new();
let chroma_key = Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 0.0,
};
let mut was_empty_column = true;
let mut char_x = 0;
let mut char_width = 0;
let char_height = image.get_height();
let char_y = 0;
let base_size = Vector2i { x: 16, y: 0 };
font_file.set_texture_image(0, base_size, 0, image.share());
for x in 0..image.get_width() {
let is_empty_column = (0..image.get_height()).all(|y| image.get_pixel(x, y).a == 0.0);
if !was_empty_column && is_empty_column {
let char = font_chars.next().expect("Font has too many characters!");
let glyph = *char as i64;
/*let mut glyph = 0i64;
for (i, c) in char.bytes().rev().enumerate() {
glyph |= (c as i64) << (i * 8);
}*/
let glyph_offset = Vector2 {
x: char_x as f32,
y: char_y as f32,
};
let glyph_size = Vector2 {
x: char_width as f32,
y: char_height as f32,
};
prints(
"Glyph".to_variant(),
&[
(*char as char).to_string().to_variant(),
glyph_offset.to_variant(),
glyph_size.to_variant(),
],
);
// font_file.set_glyph_offset(0, base_size, glyph, glyph_offset);
font_file.set_glyph_size(0, base_size, glyph, glyph_size);
font_file.set_glyph_uv_rect(
0,
base_size,
glyph,
Rect2 {
position: glyph_offset,
size: glyph_size,
},
);
font_file.set_glyph_texture_idx(0, base_size, glyph, 0);
} else if was_empty_column && !is_empty_column {
char_x = x;
char_width = 0;
}
char_width += 1;
was_empty_column = is_empty_column;
}
font_file.set_font_name("menufont".into());
// font_file.set_cache_ascent(0, base_size.x, )
font_file
}

View File

@@ -0,0 +1,138 @@
use godot::engine::Resource;
use godot::prelude::*;
use itertools::Itertools;
use std::str::FromStr;
#[derive(GodotClass)]
#[class(base=Resource, init)]
pub struct ObjectScript {
#[export]
pub dynamic_objects: Array<Gd<ObjectData>>,
#[export]
pub static_objects: Array<Gd<ObjectData>>,
#[base]
base: Base<Resource>,
}
#[godot_api]
impl ObjectScript {}
#[derive(GodotClass)]
#[class(base=Resource, init)]
pub struct ObjectData {
#[export]
pub class_type: GodotString,
#[export]
pub resource_type: GodotString,
#[export]
pub name: GodotString,
#[export]
pub props: Dictionary,
#[export]
pub children: Array<Gd<ObjectData>>,
#[base]
base: Base<Resource>,
}
#[godot_api]
impl ObjectData {}
pub fn parse_game_object(contents: String) -> Gd<ObjectScript> {
Gd::<ObjectScript>::with_base(|base| {
let mut object_script = ObjectScript {
dynamic_objects: Array::new(),
static_objects: Array::new(),
base,
};
let mut lines = contents
.lines()
.map(|l| l.trim())
.filter(|l| !l.is_empty())
.filter(|l| !l.starts_with('#'));
while let Some(line) = lines.next() {
match line {
"DYNAMIC OBJECT START" => {
object_script.dynamic_objects.push(read_object(&mut lines))
}
"OBJECT START" => object_script.static_objects.push(read_object(&mut lines)),
l => eprintln!("TODO: {}", l),
};
}
object_script
})
}
pub fn read_object<'s, I>(lines: &mut I) -> Gd<ObjectData>
where
I: Iterator<Item = &'s str>,
{
let class_type = lines
.next()
.unwrap()
.strip_prefix("class type:")
.unwrap()
.trim()
.trim_matches('"');
let (resource_type, name) = lines
.next()
.unwrap()
.splitn(2, ']')
.map(|x| x.trim())
.collect_tuple::<(&str, &str)>()
.unwrap();
Gd::<ObjectData>::with_base(|base| {
let mut object_data = ObjectData {
class_type: class_type.into(),
resource_type: resource_type
.trim_start_matches('[')
.trim_end_matches(']')
.into(),
name: name.trim_matches('"').into(),
props: Dictionary::new(),
children: Array::new(),
base,
};
lines.next();
loop {
match lines.next().unwrap() {
"}" => break,
l => {
let (_, key, value) = l
.splitn(3, '"')
.map(|x| x.trim())
.collect_tuple::<(&str, &str, &str)>()
.unwrap();
let values = value
.split_whitespace()
.map(|s| f32::from_str(s).unwrap())
.collect_vec();
object_data.props.insert(
key,
match values.len() {
1 => values[0].to_variant(),
2 => Vector2 {
x: values[0],
y: values[1],
}
.to_variant(),
3 => Vector3 {
x: values[0],
y: values[1],
z: values[2],
}
.to_variant(),
_ => panic!(),
},
);
}
}
}
object_data
})
}

57
rust/src/godot/image.rs Normal file
View File

@@ -0,0 +1,57 @@
use crate::formats::rle::RleImage;
use godot::builtin::{Color, PackedByteArray};
use godot::engine::global::Error;
use godot::engine::image::Format;
use godot::engine::{Image, ImageTexture, SpriteFrames};
use godot::obj::Gd;
const FPS: f64 = 15.0;
pub fn load_rle_as_sprite_frames(rle: RleImage) -> Gd<SpriteFrames> {
let mut frames = SpriteFrames::new();
frames.set_animation_loop("default".into(), true);
frames.set_animation_speed("default".into(), FPS);
for frame in rle.frames.iter() {
let mut image = Image::new();
image.set_data(
rle.width as i64,
rle.height as i64,
false,
Format::FORMAT_RGBA8,
PackedByteArray::from(rle.get_image_data(frame).as_slice()),
);
image.fix_alpha_edges();
let mut texture = ImageTexture::new();
texture.set_image(image);
frames.add_frame("default".into(), texture.upcast(), 1.0, 0);
}
frames
}
pub fn load_bmp_as_image_texture(data: Vec<u8>) -> Result<Gd<Image>, Error> {
let mut image = Image::new();
match image.load_bmp_from_buffer(data.as_slice().into()) {
Error::OK => {
for x in 0..image.get_width() {
for y in 0..image.get_height() {
if image.get_pixel(x, y).is_equal_approx(Color {
r: 1.0,
g: 0.0,
b: 1.0,
a: 1.0,
}) {
image.set_pixel(x, y, Color::TRANSPARENT_BLACK);
}
}
}
image.fix_alpha_edges();
Ok(image)
}
error => Err(error),
}
}

7
rust/src/godot/mod.rs Normal file
View File

@@ -0,0 +1,7 @@
pub mod datafile;
pub mod font;
pub mod game_object;
pub mod image;
pub mod sprites;
pub mod tile_map;
pub mod ui;

133
rust/src/godot/sprites.rs Normal file
View File

@@ -0,0 +1,133 @@
use crate::formats::sprites::{CropMode, RenderMode, Sprites};
use godot::builtin::{GodotString, Rect2, StringName, ToVariant, Vector2};
use godot::engine::utilities::printerr;
use godot::engine::{
load, AtlasTexture, ImageTexture, PlaceholderTexture2D, ResourceLoader, SpriteFrames,
};
use godot::obj::{Gd, Share};
use godot::prelude::GodotClass;
const FPS: f64 = 15.0;
const SPRITE_EXTENSIONS: &[&str] = &["bmp", "rle"];
pub fn load_sprite_frames(sprites: Vec<Sprites>, path: GodotString) -> Gd<SpriteFrames> {
let dir = path
.to_string()
.strip_suffix("/sprites.txt")
.unwrap()
.to_string();
let mut sprite_frames = SpriteFrames::new();
for sprite in sprites.into_iter() {
if let RenderMode::FlipX = sprite.render_mode {
continue;
}
sprite_frames.add_animation(StringName::from(&sprite.name));
sprite_frames.set_animation_speed(StringName::from(&sprite.name), FPS);
match select_from_extensions(&dir, &sprite.file_name) {
Some((path, "rle")) => extract_rle_frames(&mut sprite_frames, &sprite, path),
Some((path, "bmp")) => extract_bitmap_frames(&mut sprite_frames, &sprite, path),
Some(_) | None => {
printerr(
format!("Missing sprite '{}'", sprite.file_name).to_variant(),
&[],
);
let texture = PlaceholderTexture2D::new();
sprite_frames.add_frame(
StringName::from(&sprite.name),
texture.upcast(),
60.0 / FPS,
0,
);
}
}
}
sprite_frames
}
/// Loads an RLE file as SpriteFrames and extracts
/// its frames into `sprite_frames`
fn extract_rle_frames(sprite_frames: &mut SpriteFrames, sprite: &Sprites, path: String) {
let frames: Gd<SpriteFrames> = load(path);
for frame_idx in 0..frames.get_frame_count("default".into()) {
sprite_frames.add_frame(
StringName::from(&sprite.name),
frames
.get_frame_texture("default".into(), frame_idx)
.unwrap(),
60.0 / FPS,
0,
);
}
}
/// Loads a bitmap and extracts its frames into `sprite_frames`
/// creates an atlas if there are multiple frames.
fn extract_bitmap_frames(sprite_frames: &mut SpriteFrames, sprite: &Sprites, path: String) {
let texture: Gd<ImageTexture> = load(path);
let frame_count = if let Some(CropMode::FrameCount(frame_count)) = sprite.frames {
frame_count
} else {
1
};
if frame_count > 1 {
let height = texture.get_height();
let width = texture.get_width();
let frame_height = height / frame_count as i64;
for i in 0..frame_count as i64 {
let mut atlas = AtlasTexture::new();
atlas.set_atlas(texture.share().upcast());
atlas.set_region(Rect2 {
position: Vector2 {
x: 0.0,
y: (i * frame_height) as f32,
},
size: Vector2 {
x: width as f32,
y: frame_height as f32,
},
});
sprite_frames.add_frame(
StringName::from(&sprite.name),
atlas.upcast(),
60.0 / FPS,
0,
);
}
} else {
sprite_frames.add_frame(
StringName::from(&sprite.name),
texture.upcast(),
60.0 / FPS,
0,
);
}
}
/// Selects the extension based on which file exists
fn select_from_extensions(dir: &str, file_name: &str) -> Option<(String, &'static str)> {
SPRITE_EXTENSIONS
.iter()
.map(|ext| {
(
format!("{}/sprites/{}.{}", dir, file_name.to_lowercase(), ext),
*ext,
)
})
.find(|(path, ext)| {
ResourceLoader::singleton().exists(
path.clone().into(),
match *ext {
"rle" => SpriteFrames::CLASS_NAME.to_string(),
"bmp" => ImageTexture::CLASS_NAME.to_string(),
_ => panic!(),
}
.into(),
)
})
}

139
rust/src/godot/tile_map.rs Normal file
View File

@@ -0,0 +1,139 @@
use crate::formats::level::LevelLayer;
use godot::engine::global::Error;
use godot::engine::utilities::{clampi, printerr};
use godot::engine::{load, PackedScene};
use godot::engine::{ImageTexture, TileSet};
use godot::engine::{TileMap, TileSetAtlasSource};
use godot::prelude::*;
use godot::prelude::{Gd, PackedByteArray, Share, ToVariant};
pub fn create_tile_map(layer: LevelLayer, level_id: u32) -> Gd<PackedScene> {
let mut tile_set = TileSet::new();
tile_set.set_tile_size(Vector2i { x: 32, y: 32 });
tile_set.add_physics_layer(0);
let mut map = TileMap::new_alloc();
map.set_tileset(tile_set.share());
map.set_quadrant_size(32);
for x in 0..layer.width {
for y in 0..layer.height {
let tile = &layer.tiles[(y * layer.width + x) as usize];
if tile.id == 0 {
continue;
}
if !tile_set.has_source(tile.id as i64) {
let atlas_id = tile.id as u32 + 1;
let atlas = load_atlas(1, atlas_id, layer.tile_count);
tile_set.add_source(atlas.share().upcast(), tile.id as i64);
add_collision(atlas, level_id, atlas_id);
}
map.set_cell(
0,
Vector2i {
x: x as i32,
y: y as i32,
},
tile.id as i64,
Vector2i {
x: clampi(tile.index as i64 % 16, 0, 15) as i32,
y: clampi(tile.index as i64 / 16, 0, 15) as i32,
},
0,
);
}
}
let mut scene = PackedScene::new();
let error = scene.pack(map.upcast());
match error {
Error::OK => (),
e => printerr(e.to_variant(), &[]),
}
scene
}
#[derive(GodotClass)]
#[class(base=Resource, init)]
pub struct TileCollision {
#[export]
pub collision: PackedByteArray,
}
#[godot_api]
impl TileCollision {}
fn add_collision(atlas: Gd<TileSetAtlasSource>, level_id: u32, atlas_id: u32) {
let tile_collision: Gd<TileCollision> = load(format!(
"datafile://data/level{:02}/tile_collision_{:02}.txt",
level_id, atlas_id
));
let width = atlas.get_atlas_grid_size().x;
let height = atlas.get_atlas_grid_size().y;
let tile_width = atlas.get_texture_region_size().x as f32 / 2.0;
let tile_height = atlas.get_texture_region_size().y as f32 / 2.0;
let collision = &[
Vector2 {
x: -tile_width,
y: -tile_height,
},
Vector2 {
x: -tile_width,
y: tile_height,
},
Vector2 {
x: tile_width,
y: tile_height,
},
Vector2 {
x: tile_width,
y: -tile_height,
},
];
for x in 0..width {
for y in 0..height {
let collision_data = tile_collision
.bind()
.collision
.get((y * width + x) as usize);
let mut data = atlas.get_tile_data(Vector2i { x, y }, 0).unwrap();
if collision_data & 0x1 != 0 {
data.add_collision_polygon(0);
data.set_collision_polygon_points(0, 0, PackedVector2Array::from(collision));
} else if collision_data & 0xfe != 0 {
printerr(
format!("Missing collision info for {}", collision_data).to_variant(),
&[],
);
}
}
}
}
fn load_atlas(set_id: u32, atlas_id: u32, tile_count: u32) -> Gd<TileSetAtlasSource> {
let mut atlas = TileSetAtlasSource::new();
let tex: Gd<ImageTexture> = load(format!(
"datafile://data/set{}/sprites/tiles_{:02}.bmp",
set_id, atlas_id,
));
let region_size = (tile_count as f32).sqrt();
debug_assert_eq!(tex.get_width(), tex.get_height());
debug_assert_eq!(region_size, region_size.trunc());
let tile_size = (tex.get_width() / region_size as i64) as i32;
atlas.set_texture(tex.upcast());
atlas.set_texture_region_size(Vector2i {
x: tile_size,
y: tile_size,
});
for x in 0..region_size as i32 {
for y in 0..region_size as i32 {
atlas.create_tile(Vector2i { x, y }, Vector2i { x: 1, y: 1 });
}
}
atlas
}

58
rust/src/godot/ui.rs Normal file
View File

@@ -0,0 +1,58 @@
use crate::formats::ui_xml::{HorizontalAlign, UiTag};
use godot::builtin::{GodotString, Vector2};
use godot::engine::global::HorizontalAlignment;
use godot::engine::node::InternalMode;
use godot::engine::{Button, Container, Control, TextureRect};
use godot::prelude::*;
pub fn convert_ui(ui: UiTag) -> Gd<Control> {
match ui {
UiTag::Menu(menu) => {
let mut gd_menu = Container::new_alloc();
for child in menu.children {
gd_menu.add_child(
convert_ui(child).upcast(),
false,
InternalMode::INTERNAL_MODE_FRONT,
);
}
gd_menu.upcast()
}
UiTag::Image(image) => {
let mut gd_image = TextureRect::new_alloc();
gd_image.set_position(
Vector2 {
x: image.position[0] as f32,
y: image.position[1] as f32,
},
false,
);
gd_image.set_size(
Vector2 {
x: image.size[0] as f32,
y: image.size[1] as f32,
},
false,
);
gd_image.upcast()
}
UiTag::TextButton(button) => {
let mut gd_button = Button::new_alloc();
gd_button.set_position(
Vector2 {
x: button.position[0] as f32,
y: button.position[1] as f32,
},
false,
);
gd_button.set_text_alignment(match button.horizontal_align {
HorizontalAlign::Center => HorizontalAlignment::HORIZONTAL_ALIGNMENT_CENTER,
});
if let Some(name) = button.name {
gd_button.set_name(GodotString::from(name));
}
gd_button.set_text(GodotString::from(button.text));
gd_button.upcast()
}
}
}

40
rust/src/lib.rs Normal file
View File

@@ -0,0 +1,40 @@
use crate::godot::datafile::DatafileLoader;
use ::godot::engine::class_macros::auto_register_classes;
use ::godot::engine::{ResourceFormatLoaderVirtual, ResourceLoader};
use ::godot::init::{gdextension, ExtensionLayer};
use ::godot::prelude::{ExtensionLibrary, Gd, InitHandle, InitLevel, Share};
pub mod formats;
pub mod godot;
struct Main {}
#[gdextension]
unsafe impl ExtensionLibrary for Main {
fn load_library(handle: &mut InitHandle) -> bool {
handle.register_layer(InitLevel::Editor, ResourceLoaderLayer { datafile: None });
true
}
}
struct ResourceLoaderLayer {
pub datafile: Option<Gd<DatafileLoader>>,
}
impl ExtensionLayer for ResourceLoaderLayer {
fn initialize(&mut self) {
auto_register_classes();
self.datafile = Some(Gd::<DatafileLoader>::with_base(DatafileLoader::init));
ResourceLoader::singleton()
.add_resource_format_loader(self.datafile.as_ref().unwrap().share().upcast(), true);
}
fn deinitialize(&mut self) {
if let Some(datafile) = &self.datafile {
ResourceLoader::singleton().remove_resource_format_loader(datafile.share().upcast());
self.datafile = None;
}
}
}

130
rust/src/main.rs Normal file
View File

@@ -0,0 +1,130 @@
use binrw::{BinRead, NullString};
use image::codecs::gif::{GifEncoder, Repeat};
use image::{AnimationDecoder, ImageFormat};
use mhjnr::formats::datafile::Datafile;
use mhjnr::formats::level::level_tile_data_to_image;
use mhjnr::formats::rle::RleImage;
use mhjnr::formats::sprites::Sprites;
use mhjnr::formats::txt::{decrypt_exposed_txt, decrypt_txt};
use mhjnr::formats::ui_xml::UiTag;
use serde_xml_rs::from_str;
use std::ffi::OsStr;
use std::fs;
use std::fs::{File, OpenOptions};
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use std::path::Path;
fn extract(datafile: &Datafile, file: &mut File) {
let target = "E:\\Games\\Schatzjäger\\data3";
for entry in &datafile.files {
let file_name = format!("{}\\{}", target, entry.name);
fs::create_dir_all(file_name.rsplit_once('\\').unwrap().0).unwrap();
file.seek(SeekFrom::Start(entry.pos as u64)).unwrap();
let mut data = vec![0u8; entry.len as usize];
file.read_exact(&mut data).unwrap();
if entry.name.to_string().ends_with(".txt") {
let mut contents = decrypt_txt(data.into_iter()).unwrap();
/*if entry
.name
.to_string()
.split('\\')
.collect::<Vec<&str>>()
.len()
== 1
{
contents = decrypt_exposed_txt(contents).unwrap();
}*/
File::create(file_name)
.unwrap()
.write_all(contents.as_bytes())
.unwrap();
} else if entry.name.to_string().ends_with(".rle") {
let image: RleImage = RleImage::read(&mut Cursor::new(data)).unwrap();
let mut encoder = GifEncoder::new(
OpenOptions::new()
.create(true)
.write(true)
.open(format!(
"{}.{}",
file_name.strip_suffix(".rle").unwrap(),
".gif"
))
.unwrap(),
);
encoder.set_repeat(Repeat::Infinite).unwrap();
encoder.try_encode_frames(image.into_frames()).unwrap();
} else {
File::create(file_name)
.unwrap()
.write_all(data.as_slice())
.unwrap();
}
}
}
fn main() {
let file_name = Some(NullString::from("data\\loading\\sprites.txt"));
let dat_path = "E:\\Games\\Schatzjäger\\data\\datafile.dat";
let mut file = File::open(dat_path).unwrap();
let dat: Datafile = Datafile::read(&mut file).unwrap();
println!("{:#?}", dat);
extract(&dat, &mut file);
/*if let Some(file_name) = file_name {
let target = dat.files.iter().find(|it| it.name == file_name).unwrap();
file.seek(SeekFrom::Start(target.pos as u64)).unwrap();
let mut data = vec![0u8; target.len as usize];
file.read_exact(&mut data).unwrap();
match Path::new(&file_name.to_string())
.extension()
.and_then(OsStr::to_str)
{
Some("xml") => println!(
"{:#?}",
from_str::<UiTag>(String::from_utf8(data).unwrap().as_str())
),
Some("txt") => {
if false {
/*let decr = decrypt_txt(&mut data);
let entries: String = decrypt_exposed_txt(decr);*/
let decr = decrypt_txt(data.into_iter()).unwrap();
println!("{}", &decr);
let sprites = Sprites::parse(decr.as_str()).unwrap();
println!("{:#?}", sprites);
} else {
println!("{}", decrypt_txt(data.into_iter()).unwrap())
}
}
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();
}
Some(ext) => eprintln!("Unknown file extension <{}>", ext),
None => eprintln!("Failed to read"),
}
}*/
}
// pub fn decr2()