This commit is contained in:
2023-05-10 17:04:23 +02:00
parent 4b6c4e1af8
commit 25787c8b65
47 changed files with 234 additions and 2348 deletions

View 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
}

View 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));
}
}

View 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
}

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

View 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),
}
}

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;

View 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(),
)
})
}

View 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
View 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(),
);
}
}