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])
|
||||
}
|
||||
272
rust/src/godot/datafile.rs
Normal file
272
rust/src/godot/datafile.rs
Normal 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
88
rust/src/godot/font.rs
Normal 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
|
||||
}
|
||||
138
rust/src/godot/game_object.rs
Normal file
138
rust/src/godot/game_object.rs
Normal 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
57
rust/src/godot/image.rs
Normal 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
7
rust/src/godot/mod.rs
Normal 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
133
rust/src/godot/sprites.rs
Normal 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
139
rust/src/godot/tile_map.rs
Normal 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
58
rust/src/godot/ui.rs
Normal 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
40
rust/src/lib.rs
Normal 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
130
rust/src/main.rs
Normal 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()
|
||||
Reference in New Issue
Block a user