use crate::formats; use crate::formats::datafile::{Datafile, FileEntry}; 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::resource_loader::CacheMode; use godot::engine::resource_saver::SaverFlags; use godot::engine::utilities::printerr; use godot::engine::ImageTexture; use godot::engine::{AudioStreamOggVorbis, DirAccess, OggPacketSequence, Translation}; 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, #[base] pub base: Base, } 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, 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(&self, path: String) -> Option> where T: GodotClass + Inherits, { 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 { 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, virtual_path: GodotString, _original_path: GodotString, _use_sub_threads: bool, _cache_mode: i64, ) -> Variant { let datafile_path = convert_path(&virtual_path); if let Some(resource) = self.retrieve_cache::(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 full_path = virtual_path.to_string(); let (_, _, base_path) = full_path .rsplitn(3, '/') .collect_tuple() .expect("Illegal path for UI"); let ui = convert_ui(ui, None, base_path); let mut scene = PackedScene::new(); scene.pack(ui); self.save_to_cache(scene.share().upcast(), format!("{}.scn", datafile_path)); scene.to_variant() } Ok(DatafileFile::Translations(translations)) => { let mut translation = Translation::new(); for (key, message) in translations { translation.add_message(key.into(), message.join("\n").into(), "".into()); } translation.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, virtual_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() } } }