mirror of
https://github.com/Theaninova/mhlib.git
synced 2025-12-12 20:46:20 +00:00
mhk3
This commit is contained in:
68
rust/mhgd/src/lightwave_object.rs
Normal file
68
rust/mhgd/src/lightwave_object.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use godot::bind::godot_api;
|
||||
use godot::builtin::{GodotString, PackedInt32Array, PackedVector3Array, Vector3};
|
||||
use godot::engine::mesh::ArrayType;
|
||||
use godot::engine::{ArrayMesh, PackedScene};
|
||||
use godot::obj::{EngineEnum, Gd};
|
||||
use godot::prelude::{Array, GodotClass, ToVariant};
|
||||
use lightwave_3d::lwo2::tags::Tag;
|
||||
use lightwave_3d::LightWaveObject;
|
||||
use std::fs::File;
|
||||
|
||||
#[derive(GodotClass)]
|
||||
struct Lwo {}
|
||||
|
||||
#[godot_api]
|
||||
impl Lwo {
|
||||
pub fn get_mesh(path: GodotString) -> Gd<ArrayMesh> {
|
||||
lightwave_to_gd(LightWaveObject::read_file(path.to_string()).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lightwave_to_gd(lightwave: LightWaveObject) -> Gd<ArrayMesh> {
|
||||
let mesh = ArrayMesh::new();
|
||||
let mut arrays = Array::new();
|
||||
arrays.resize(ArrayType::ARRAY_MAX.ord() as usize);
|
||||
|
||||
for tag in lightwave.data {
|
||||
match tag {
|
||||
Tag::PointList(points) => {
|
||||
arrays.set(
|
||||
ArrayType::ARRAY_VERTEX.ord() as usize,
|
||||
PackedVector3Array::from(
|
||||
points
|
||||
.point_location
|
||||
.iter()
|
||||
.map(|[x, y, z]| Vector3 {
|
||||
x: *x,
|
||||
y: *y,
|
||||
z: *z,
|
||||
})
|
||||
.collect::<Vec<Vector3>>()
|
||||
.as_slice(),
|
||||
)
|
||||
.to_variant(),
|
||||
);
|
||||
}
|
||||
Tag::PolygonList(polygons) => match &polygons.kind {
|
||||
b"FACE" => {
|
||||
arrays.set(
|
||||
ArrayType::ARRAY_INDEX.ord() as usize,
|
||||
PackedInt32Array::from(
|
||||
polygons
|
||||
.polygons
|
||||
.iter()
|
||||
.flat_map(|it| it.vert.iter().map(|it| *it as i32))
|
||||
.collect::<Vec<i32>>()
|
||||
.as_slice(),
|
||||
)
|
||||
.to_variant(),
|
||||
);
|
||||
}
|
||||
_ => panic!(),
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
mesh
|
||||
}
|
||||
301
rust/mhgd/src/sproing/datafile.rs
Normal file
301
rust/mhgd/src/sproing/datafile.rs
Normal file
@@ -0,0 +1,301 @@
|
||||
use crate::sproing::game_object::parse_game_object;
|
||||
use crate::sproing::image::{load_bmp_as_image_texture, load_rle_as_sprite_frames};
|
||||
use crate::sproing::sprites::load_sprite_frames;
|
||||
use crate::sproing::tile_map::{create_tile_map, TileCollision};
|
||||
use crate::sproing::ui::convert_ui;
|
||||
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 springylib::archive::Archive;
|
||||
use springylib::DatafileFile;
|
||||
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: Archive,
|
||||
|
||||
#[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 = Archive::read(&mut file).unwrap();
|
||||
|
||||
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::<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 target.load_from(&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 mut ui = convert_ui(ui, base_path);
|
||||
own_children(&mut ui, None);
|
||||
|
||||
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(
|
||||
format!("%{}%", key).into(),
|
||||
message.join("\n").into(),
|
||||
"".into(),
|
||||
);
|
||||
}
|
||||
self.save_to_cache(
|
||||
translation.share().upcast(),
|
||||
format!("{}.res", datafile_path),
|
||||
);
|
||||
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\\") {
|
||||
panic!();
|
||||
/*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(springylib::error::Error::UnknownFormat(ext)) => {
|
||||
printerr(format!("Unknown format <{}>", ext).to_variant(), &[]);
|
||||
Error::ERR_FILE_UNRECOGNIZED.to_variant()
|
||||
}
|
||||
Err(springylib::error::Error::InvalidData { info, context }) => {
|
||||
printerr(
|
||||
"Failed to deserialize".to_variant(),
|
||||
&[
|
||||
info.unwrap_or("".to_string()).to_variant(),
|
||||
context.to_variant(),
|
||||
],
|
||||
);
|
||||
Error::ERR_FILE_CORRUPT.to_variant()
|
||||
}
|
||||
Err(springylib::error::Error::Custom(message)) => {
|
||||
printerr(message.to_string().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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn own_children(node: &mut Gd<Node>, owner: Option<&mut Gd<Node>>) {
|
||||
let iter = node.get_children(false);
|
||||
let owner = owner.unwrap_or(node);
|
||||
for mut child in iter.iter_shared() {
|
||||
println!("{:#?}", child);
|
||||
child.set_owner(owner.share());
|
||||
own_children(&mut child, Some(owner));
|
||||
}
|
||||
}
|
||||
80
rust/mhgd/src/sproing/font.rs
Normal file
80
rust/mhgd/src/sproing/font.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use godot::builtin::{Rect2, Vector2, Vector2i};
|
||||
use godot::engine::{FontFile, Image};
|
||||
use godot::prelude::utilities::prints;
|
||||
use godot::prelude::{Gd, Share, ToVariant};
|
||||
|
||||
pub fn load_bitmap_font(image: Gd<Image>) -> Gd<FontFile> {
|
||||
let mut font_chars = CHARSET.iter();
|
||||
|
||||
let mut font_file = FontFile::new();
|
||||
|
||||
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 mut glyph = 0i64;
|
||||
for (i, c) in WINDOWS_1252
|
||||
.decode(&[*char])
|
||||
.0
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.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/mhgd/src/sproing/game_object.rs
Normal file
138
rust/mhgd/src/sproing/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/mhgd/src/sproing/image.rs
Normal file
57
rust/mhgd/src/sproing/image.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
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;
|
||||
use springylib::media::rle::RleImage;
|
||||
|
||||
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/mhgd/src/sproing/mod.rs
Normal file
7
rust/mhgd/src/sproing/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/mhgd/src/sproing/sprites.rs
Normal file
133
rust/mhgd/src/sproing/sprites.rs
Normal file
@@ -0,0 +1,133 @@
|
||||
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;
|
||||
use springylib::media::sprites::{CropMode, RenderMode, Sprites};
|
||||
|
||||
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/mhgd/src/sproing/tile_map.rs
Normal file
139
rust/mhgd/src/sproing/tile_map.rs
Normal file
@@ -0,0 +1,139 @@
|
||||
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};
|
||||
use springylib::media::level::LevelLayer;
|
||||
|
||||
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
|
||||
}
|
||||
147
rust/mhgd/src/sproing/ui.rs
Normal file
147
rust/mhgd/src/sproing/ui.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
use godot::builtin::{Array, Dictionary, GodotString, ToVariant, Vector2};
|
||||
use godot::engine::control::LayoutPreset;
|
||||
use godot::engine::global::HorizontalAlignment;
|
||||
use godot::engine::node::InternalMode;
|
||||
use godot::engine::{load, Button, Control, Label, LineEdit, Node, SpinBox, TextureRect};
|
||||
use godot::obj::{Gd, Inherits, Share};
|
||||
use itertools::Itertools;
|
||||
use springylib::media::ui::{HorizontalAlign, UiTag};
|
||||
|
||||
const ACTION_META_NAME: &str = "action";
|
||||
|
||||
pub fn convert_ui(ui: UiTag, base_path: &str) -> Gd<Node> {
|
||||
match ui {
|
||||
UiTag::Menu(menu) => {
|
||||
let mut gd_menu = Control::new_alloc();
|
||||
gd_menu.set_anchors_preset(LayoutPreset::PRESET_FULL_RECT, false);
|
||||
attach_children(&mut gd_menu, menu.children, base_path);
|
||||
gd_menu.upcast()
|
||||
}
|
||||
UiTag::Image(image) => {
|
||||
let mut gd_image = TextureRect::new_alloc();
|
||||
let texture = load(format!("{}/sprites/{}.bmp", base_path, image.texture));
|
||||
|
||||
gd_image.set_texture(texture);
|
||||
gd_image.set_name(image.texture.into());
|
||||
gd_image.set_position(to_vec2(image.position), false);
|
||||
gd_image.set_size(to_vec2(image.size), false);
|
||||
gd_image.upcast()
|
||||
}
|
||||
UiTag::StaticText(text) => {
|
||||
let mut label = Label::new_alloc();
|
||||
label.set_anchors_preset(LayoutPreset::PRESET_TOP_WIDE, false);
|
||||
label.set_position(to_vec2(text.position), false);
|
||||
label.set_horizontal_alignment(to_h_alignment(text.horizontal_align));
|
||||
label.set_text(text.text.into());
|
||||
label.upcast()
|
||||
}
|
||||
UiTag::TextArea(area) => {
|
||||
let mut text_area = Control::new_alloc();
|
||||
// text_area.set_anchors_preset(LayoutPreset::PRESET_, false);
|
||||
text_area.set_position(to_vec2(area.position.unwrap()), false);
|
||||
text_area.set_size(to_vec2(area.size.unwrap()), false);
|
||||
attach_children(&mut text_area, area.children, base_path);
|
||||
text_area.upcast()
|
||||
}
|
||||
UiTag::TextField(field) => {
|
||||
let mut text_field = LineEdit::new_alloc();
|
||||
if let Some(name) = field.name {
|
||||
text_field.set_name(name.into());
|
||||
}
|
||||
text_field.set_text(field.text.into());
|
||||
text_field.set_horizontal_alignment(to_h_alignment(field.horizontal_align));
|
||||
text_field.set_position(to_vec2([field.area[0], field.area[1]]), false);
|
||||
text_field.set_size(to_vec2([field.area[2], field.area[3]]), false);
|
||||
text_field.set_meta("buffer_var".into(), field.buffer_var.to_variant());
|
||||
attach_call_meta(&mut text_field, field.on_select);
|
||||
text_field.upcast()
|
||||
}
|
||||
UiTag::ToggleButton(toggle) => {
|
||||
let mut spin_box = SpinBox::new_alloc();
|
||||
spin_box.set_position(to_vec2(toggle.position), false);
|
||||
spin_box.set_min(toggle.min_value as f64);
|
||||
spin_box.set_max(toggle.max_value as f64);
|
||||
spin_box.set_step(toggle.value_step as f64);
|
||||
if let Some(name) = toggle.name {
|
||||
spin_box.set_name(GodotString::from(name));
|
||||
}
|
||||
spin_box.set_meta("text".into(), toggle.text.to_variant());
|
||||
spin_box.set_meta("target".into(), toggle.target.to_variant());
|
||||
spin_box.set_meta("no_sound".into(), toggle.no_sound.to_variant());
|
||||
attach_call_meta(&mut spin_box, toggle.on_change);
|
||||
spin_box.upcast()
|
||||
}
|
||||
UiTag::TextButton(button) => {
|
||||
let mut gd_button = Button::new_alloc();
|
||||
gd_button.set_anchors_preset(LayoutPreset::PRESET_TOP_WIDE, false);
|
||||
gd_button.set_flat(true);
|
||||
gd_button.set_position(to_vec2(button.position), false);
|
||||
gd_button.set_text_alignment(to_h_alignment(button.horizontal_align));
|
||||
if let Some(name) = button.name {
|
||||
gd_button.set_name(GodotString::from(name));
|
||||
}
|
||||
gd_button.set_text(GodotString::from(button.text));
|
||||
attach_call_meta(&mut gd_button, button.on_select);
|
||||
gd_button.upcast()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_h_alignment(align: HorizontalAlign) -> HorizontalAlignment {
|
||||
match align {
|
||||
HorizontalAlign::Center => HorizontalAlignment::HORIZONTAL_ALIGNMENT_CENTER,
|
||||
HorizontalAlign::Left => HorizontalAlignment::HORIZONTAL_ALIGNMENT_LEFT,
|
||||
HorizontalAlign::Right => HorizontalAlignment::HORIZONTAL_ALIGNMENT_RIGHT,
|
||||
}
|
||||
}
|
||||
|
||||
fn attach_children<T>(node: &mut Gd<T>, children: Vec<UiTag>, base_path: &str)
|
||||
where
|
||||
T: Inherits<Node>,
|
||||
{
|
||||
let mut parent = node.share().upcast();
|
||||
|
||||
for child in children {
|
||||
parent.add_child(
|
||||
convert_ui(child, base_path),
|
||||
false,
|
||||
InternalMode::INTERNAL_MODE_DISABLED,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn to_vec2(vec: [i32; 2]) -> Vector2 {
|
||||
Vector2 {
|
||||
x: vec[0] as f32,
|
||||
y: vec[1] as f32,
|
||||
}
|
||||
}
|
||||
|
||||
fn attach_call_meta<T>(button: &mut Gd<T>, call_string: String)
|
||||
where
|
||||
T: Inherits<Node>,
|
||||
{
|
||||
let mut call = call_string.split_whitespace().collect_vec();
|
||||
if call.is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Some((name,)) = call.drain(..1).collect_tuple() {
|
||||
button.share().upcast().set_meta(
|
||||
ACTION_META_NAME.into(),
|
||||
Dictionary::from([
|
||||
(&"name".to_variant(), &name.to_variant()),
|
||||
(
|
||||
&"args".to_variant(),
|
||||
&Array::from(
|
||||
call.into_iter()
|
||||
.map(GodotString::from)
|
||||
.collect::<Vec<GodotString>>()
|
||||
.as_slice(),
|
||||
)
|
||||
.to_variant(),
|
||||
),
|
||||
])
|
||||
.to_variant(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user