kitgit

tirbofish/dropbear · commit

953f5ec6d05479899d7e72ee2ec9c4636b73bd0f

Merge branch 'main' of github.com:4tkbytes/dropbear

Unverified

tk <4tkbytes@pm.me> · 2025-10-31 12:26

view full diff

diff --git a/Cargo.toml b/Cargo.toml
index 090d228..52e9f71 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,7 +8,6 @@ package.readme = "README.md"
 resolver = "3"
 members = [ "dropbear-engine", "dropbear-shader", "dropbear_future-queue", "eucalyptus-core", "eucalyptus-editor", "magna-carta", "redback-runtime"]
 
-
 [workspace.dependencies]
 anyhow = { version = "1.0", features = ["backtrace"] }
 app_dirs2 = "2.5"
diff --git a/eucalyptus-core/src/states.rs b/eucalyptus-core/src/states.rs
index e7fdd72..93aae3d 100644
--- a/eucalyptus-core/src/states.rs
+++ b/eucalyptus-core/src/states.rs
@@ -34,6 +34,57 @@ pub static SOURCE: Lazy<RwLock<SourceConfig>> = Lazy::new(|| RwLock::new(SourceC
 
 pub static SCENES: Lazy<RwLock<Vec<SceneConfig>>> = Lazy::new(|| RwLock::new(Vec::new()));
 
+/// Removes a scene with the provided name from the in-memory scene cache.
+/// Returns `true` when a scene was removed and `false` when no matching scene existed.
+pub fn unload_scene(scene_name: &str) -> bool {
+    let mut scenes = SCENES.write();
+    let initial_len = scenes.len();
+    scenes.retain(|scene| scene.scene_name != scene_name);
+    let removed = scenes.len() != initial_len;
+
+    if removed {
+        log::info!("Unloaded scene '{}' from memory", scene_name);
+    } else {
+        log::debug!("Scene '{}' was not loaded; nothing to unload", scene_name);
+    }
+
+    removed
+}
+
+/// Reads a scene configuration from disk based on the active project's path.
+pub fn load_scene(scene_name: &str) -> anyhow::Result<SceneConfig> {
+    let scene_path = {
+        let project = PROJECT.read();
+        if project.project_path.as_os_str().is_empty() {
+            return Err(anyhow::anyhow!("Project path is not set; cannot load scenes"));
+        }
+
+        project
+            .project_path
+            .join("scenes")
+            .join(format!("{}.eucs", scene_name))
+    };
+
+    let scene = SceneConfig::read_from(&scene_path)?;
+    log::info!("Loaded scene '{}' from {}", scene_name, scene_path.display());
+    Ok(scene)
+}
+
+/// Reloads a scene into the in-memory cache by unloading any existing copy first.
+pub fn load_scene_into_memory(scene_name: &str) -> anyhow::Result<()> {
+    unload_scene(scene_name);
+
+    let scene = load_scene(scene_name)?;
+    {
+        let mut scenes = SCENES.write();
+        scenes.insert(0, scene);
+    }
+
+    log::info!("Scene '{}' loaded into memory", scene_name);
+
+    Ok(())
+}
+
 /// The root config file, responsible for building and other metadata.
 ///
 /// # Location
diff --git a/eucalyptus-editor/src/editor/mod.rs b/eucalyptus-editor/src/editor/mod.rs
index 4add870..2cea8fd 100644
--- a/eucalyptus-editor/src/editor/mod.rs
+++ b/eucalyptus-editor/src/editor/mod.rs
@@ -16,7 +16,7 @@ use dropbear_engine::{
     future::FutureHandle,
     graphics::{RenderContext, SharedGraphicsContext},
     lighting::{Light, LightManager},
-    model::ModelId,
+    model::{ModelId, MODEL_CACHE},
     scene::SceneCommand,
 };
 use egui::{self, Context};
@@ -25,7 +25,8 @@ use eucalyptus_core::{
     camera::{CameraComponent, CameraType, DebugCamera}, fatal, info, input::InputState, ptr::{GraphicsPtr, InputStatePtr, WorldPtr}, scripting::{BuildStatus, ScriptManager, ScriptTarget},
     states,
     states::{
-        CameraConfig, EditorTab, EntityNode, LightConfig, ModelProperties, SceneEntity, ScriptComponent,
+        CameraConfig, EditorTab, EntityNode, LightConfig, ModelProperties, SceneConfig, SceneEntity,
+        ScriptComponent,
         WorldLoadingStatus, PROJECT, SCENES,
     },
     success,
@@ -36,9 +37,11 @@ use eucalyptus_core::{
 };
 use hecs::{Entity, World};
 use parking_lot::Mutex;
+use rfd::FileDialog;
 use std::path::Path;
 use std::{
     collections::{HashMap, HashSet},
+    fs,
     path::PathBuf,
     sync::{Arc, LazyLock},
     time::{Duration, Instant},
@@ -119,6 +122,13 @@ pub struct Editor {
     pub plugin_registry: PluginRegistry,
 
     pub dock_state_shared: Option<Arc<Mutex<DockState<EditorTab>>>>,
+
+    // scene creation
+    open_new_scene_window: bool,
+    new_scene_name: String,
+    current_scene_name: Option<String>,
+    pending_scene_load: Option<PendingSceneLoad>,
+    pending_scene_creation: Option<String>,
 }
 
 impl Editor {
@@ -203,6 +213,11 @@ impl Editor {
             plugin_registry,
             dock_state_shared: None,
             outline_pipeline: None,
+            open_new_scene_window: false,
+            new_scene_name: String::new(),
+            current_scene_name: None,
+            pending_scene_load: None,
+            pending_scene_creation: None,
         })
     }
 
@@ -503,15 +518,228 @@ impl Editor {
         Ok(())
     }
 
+    fn queue_scene_load_by_name(&mut self, scene_name: &str) -> anyhow::Result<()> {
+        if scene_name.trim().is_empty() {
+            return Err(anyhow::anyhow!("Scene name cannot be empty"));
+        }
+
+        if let Some(current) = self.current_scene_name.as_deref() {
+            states::unload_scene(current);
+        }
+
+        let scene = states::load_scene(scene_name)?;
+
+        {
+            let mut scenes = SCENES.write();
+            scenes.retain(|existing| existing.scene_name != scene.scene_name);
+            scenes.insert(0, scene.clone());
+        }
+
+        log::info!("Scene '{}' staged for loading", scene.scene_name);
+
+        self.current_scene_name = Some(scene.scene_name.clone());
+        self.pending_scene_load = Some(PendingSceneLoad { scene });
+
+        Ok(())
+    }
+
+    fn cleanup_scene_resources(&mut self, graphics: &mut RenderContext) {
+        if let Some(handle) = self.world_load_handle.take() {
+            graphics.shared.future_queue.cancel(&handle);
+        }
+
+        self.light_spawn_queue.clear();
+        self.progress_tx = None;
+        self.world_receiver = None;
+        self.current_state = WorldLoadingStatus::Idle;
+
+        self.world.clear();
+        self.selected_entity = None;
+        self.previously_selected_entity = None;
+        self.active_camera.lock().take();
+
+        self.render_pipeline = None;
+        self.outline_pipeline = None;
+        self.texture_id = None;
+        self.light_manager = LightManager::new();
+
+        {
+            let mut cache = MODEL_CACHE.lock();
+            cache.clear();
+        }
+    }
+
+    fn start_async_scene_load(&mut self, scene: SceneConfig, graphics: &mut RenderContext) {
+        self.cleanup_scene_resources(graphics);
+
+        let (progress_sender, progress_receiver) =
+            tokio::sync::mpsc::unbounded_channel::<WorldLoadingStatus>();
+        self.progress_tx = Some(progress_receiver);
+        self.current_state = WorldLoadingStatus::Idle;
+
+        let (world_sender, world_receiver) = oneshot::channel();
+        self.world_receiver = Some(world_receiver);
+
+        self.is_world_loaded = IsWorldLoadedYet::new();
+        self.is_world_loaded.mark_scene_loaded();
+
+        let graphics_shared = graphics.shared.clone();
+        let active_camera = self.active_camera.clone();
+        let scene_name = scene.scene_name.clone();
+
+        let handle = graphics.shared.future_queue.push(async move {
+            let mut temp_world = World::new();
+
+            let load_result = scene
+                .load_into_world(
+                    &mut temp_world,
+                    graphics_shared.clone(),
+                    Some(progress_sender.clone()),
+                )
+                .await;
+
+            match load_result {
+                Ok(active_entity) => {
+                    let mut camera_lock = active_camera.lock();
+                    *camera_lock = Some(active_entity);
+                }
+                Err(err) => {
+                    log::error!("Failed to load scene '{}': {}", scene_name, err);
+                }
+            }
+
+            let _ = progress_sender.send(WorldLoadingStatus::Completed);
+
+            if world_sender.send(temp_world).is_err() {
+                log::error!("Failed to deliver loaded world for scene '{}'", scene_name);
+            }
+        });
+
+        self.world_load_handle = Some(handle);
+    }
+
+    fn create_new_scene(&mut self, name: &str) -> anyhow::Result<()> {
+        let trimmed_name = name.trim();
+        if trimmed_name.is_empty() {
+            return Err(anyhow::anyhow!("Scene name cannot be empty"));
+        }
+
+        if trimmed_name.contains('/') || trimmed_name.contains('\\') || trimmed_name.contains(':') {
+            return Err(anyhow::anyhow!(
+                "Scene name cannot contain path separator characters"
+            ));
+        }
+
+        let scene_name_owned = trimmed_name.to_string();
+
+        let project_root = {
+            let cfg = PROJECT.read();
+            cfg.project_path.clone()
+        };
+
+        if project_root.as_os_str().is_empty() {
+            return Err(anyhow::anyhow!("Project path is not set"));
+        }
+
+        let scenes_dir = project_root.join("scenes");
+        if !scenes_dir.exists() {
+            fs::create_dir_all(&scenes_dir)?;
+        }
+
+        let target_path = scenes_dir.join(format!("{}.eucs", scene_name_owned));
+        if target_path.exists() {
+            return Err(anyhow::anyhow!(
+                "Scene '{}' already exists",
+                scene_name_owned
+            ));
+        }
+
+        let scene_config = SceneConfig::new(scene_name_owned.clone(), &target_path);
+        scene_config.write_to(&project_root)?;
+
+        self.queue_scene_load_by_name(&scene_name_owned)?;
+        success!("Created scene '{}'", scene_name_owned);
+        Ok(())
+    }
+
+    fn open_scene_from_path(&mut self, path: PathBuf) -> anyhow::Result<()> {
+        if path
+            .extension()
+            .and_then(|ext| ext.to_str())
+            .map(|ext| ext.eq_ignore_ascii_case("eucs"))
+            != Some(true)
+        {
+            return Err(anyhow::anyhow!("Selected file is not an .eucs scene"));
+        }
+
+        let project_root = {
+            let cfg = PROJECT.read();
+            cfg.project_path.clone()
+        };
+
+        if project_root.as_os_str().is_empty() {
+            return Err(anyhow::anyhow!("Project path is not set"));
+        }
+
+        let scenes_dir = project_root.join("scenes");
+        if !path.starts_with(&scenes_dir) {
+            return Err(anyhow::anyhow!(
+                "Scene '{}' is outside of the current project",
+                path.display()
+            ));
+        }
+
+        let scene_name = path
+            .file_stem()
+            .and_then(|stem| stem.to_str())
+            .ok_or_else(|| anyhow::anyhow!("Scene file name is invalid"))?;
+
+        self.queue_scene_load_by_name(scene_name)?;
+        info!("Queued scene '{}' for loading", scene_name);
+        Ok(())
+    }
+
     pub fn show_ui(&mut self, ctx: &Context) {
+        if let Some(scene_name) = self.pending_scene_creation.take() {
+            let result = self.create_new_scene(scene_name.as_str());
+            self.new_scene_name.clear();
+            if let Err(e) = result {
+                fatal!("Failed to create scene '{}': {}", scene_name, e);
+            }
+        }
+
         egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
             egui::MenuBar::new().ui(ui, |ui| {
                 ui.menu_button("File", |ui| {
-                    if ui
-                        .button("Main Menu (New + Open + Editor Settings)")
-                        .clicked()
-                    {
-                        self.scene_command = SceneCommand::SwitchScene("main_menu".into());
+                    // if ui
+                    //     .button("Main Menu (New + Open + Editor Settings)")
+                    //     .clicked()
+                    // {
+                    //     self.scene_command = SceneCommand::SwitchScene("main_menu".into());
+                    // }
+
+                    if ui.button("New Scene").clicked() {
+                        self.open_new_scene_window = true;
+                    }
+
+                    if ui.button("Open Scene").clicked() {
+                        let scenes_dir = {
+                            let project = PROJECT.read();
+                            project.project_path.join("scenes")
+                        };
+
+                        let mut dialog = FileDialog::new();
+                        if scenes_dir.exists() {
+                            dialog = dialog.set_directory(&scenes_dir);
+                        }
+
+                        let dialog = dialog.add_filter("Eucalyptus Scenes", &["eucs"]);
+
+                        if let Some(path) = dialog.pick_file() {
+                            if let Err(e) = self.open_scene_from_path(path) {
+                                fatal!("Failed to open scene: {}", e);
+                            }
+                        }
                     }
 
                     if ui.button("Save").clicked() {
@@ -679,6 +907,29 @@ impl Editor {
             self.scene_command = SceneCommand::SwitchScene("editor".to_string());
             self.pending_scene_switch = false;
         }
+
+        let mut open_flag = self.open_new_scene_window;
+        let mut close_requested = false;
+        if open_flag {
+            egui::Window::new("New Scene")
+                .open(&mut open_flag)
+                .show(ctx, |ui| {
+                    ui.vertical(|ui| {
+                        ui.label("Name: ");
+                        ui.text_edit_singleline(&mut self.new_scene_name);
+                        if ui.button("Create").clicked() {
+                            self.pending_scene_creation = Some(self.new_scene_name.clone());
+                            close_requested = true;
+                        }
+                    });
+                });
+        }
+
+        if close_requested {
+            open_flag = false;
+        }
+
+        self.open_new_scene_window = open_flag;
     }
 
     /// Restores transform components back to its original state before PlayMode.
@@ -839,7 +1090,7 @@ impl Editor {
     ///
     /// **Note**: To be ran AFTER [`Editor::load_project_config`]
     pub fn load_wgpu_nerdy_stuff<'a>(&mut self, graphics: &mut RenderContext<'a>) {
-        log::debug!("Contents of viewport shader: \n{:#?}", dropbear_engine::shader::shader_wesl::SHADER_SHADER);
+        // log::debug!("Contents of viewport shader: \n{:#?}", dropbear_engine::shader::shader_wesl::SHADER_SHADER);
         let shader = Shader::new(
             graphics.shared.clone(),
             dropbear_engine::shader::shader_wesl::SHADER_SHADER,
@@ -866,7 +1117,7 @@ impl Editor {
                     );
                     self.render_pipeline = Some(pipeline);
 
-                    log::debug!("Contents of light shader: \n{:#?}", dropbear_engine::shader::shader_wesl::LIGHT_SHADER);
+                    // log::debug!("Contents of light shader: \n{:#?}", dropbear_engine::shader::shader_wesl::LIGHT_SHADER);
                     self.light_manager.create_render_pipeline(
                         graphics.shared.clone(),
                         dropbear_engine::shader::shader_wesl::LIGHT_SHADER,
@@ -874,7 +1125,7 @@ impl Editor {
                         Some("Light Pipeline"),
                     );
 
-                    log::debug!("Contents of outline shader: \n{:#?}", dropbear_engine::shader::shader_wesl::OUTLINE_SHADER);
+                    // log::debug!("Contents of outline shader: \n{:#?}", dropbear_engine::shader::shader_wesl::OUTLINE_SHADER);
                     let outline_shader = OutlineShader::init(graphics.shared.clone(), camera.layout());
                     self.outline_pipeline = Some(outline_shader);
                 } else {
@@ -1303,6 +1554,10 @@ pub enum EditorState {
     Playing,
 }
 
+struct PendingSceneLoad {
+    scene: SceneConfig,
+}
+
 pub enum PendingSpawn2 {
     Light,
     Plane,
diff --git a/eucalyptus-editor/src/editor/scene.rs b/eucalyptus-editor/src/editor/scene.rs
index a648746..1e2a7f7 100644
--- a/eucalyptus-editor/src/editor/scene.rs
+++ b/eucalyptus-editor/src/editor/scene.rs
@@ -21,6 +21,11 @@ use winit::{event_loop::ActiveEventLoop, keyboard::KeyCode};
 
 impl Scene for Editor {
     fn load(&mut self, graphics: &mut RenderContext) {
+        self.current_scene_name = {
+            let scenes = SCENES.read();
+            scenes.first().map(|scene| scene.scene_name.clone())
+        };
+
         let (tx, rx) = unbounded_channel::<WorldLoadingStatus>();
         let (tx2, rx2) = oneshot::channel::<World>();
         self.progress_tx = Some(rx);
@@ -59,6 +64,10 @@ impl Scene for Editor {
     }
 
     fn update(&mut self, dt: f32, graphics: &mut RenderContext) {
+        if let Some(request) = self.pending_scene_load.take() {
+            self.start_async_scene_load(request.scene, graphics);
+        }
+
         if let Some(mut receiver) = self.world_receiver.take() {
             self.show_project_loading_window(&graphics.shared.get_egui_context());
             if let Ok(loaded_world) = receiver.try_recv() {
@@ -372,33 +381,33 @@ impl Scene for Editor {
                                     );
                                 }
 
-                                // outline rendering
-                                let has_selected = entities.iter()
-                                    .any(|e| e.model.id == model_ptr && e.is_selected);
-
-                                if has_selected && self.outline_pipeline.is_some() {
-                                    let outline = self.outline_pipeline.as_ref().unwrap();
-                                    let mut render_pass = graphics.continue_pass();
-                                    render_pass.set_pipeline(&outline.pipeline);
-
-                                    render_pass.set_bind_group(0, &outline.bind_group, &[]);
-                                    render_pass.set_bind_group(1, camera.bind_group(), &[]);
-
-                                    render_pass.set_vertex_buffer(1, instance_buffer.slice(..));
-
-                                    for mesh in &model.meshes {
-                                        render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
-                                        render_pass.set_index_buffer(
-                                            mesh.index_buffer.slice(..),
-                                            wgpu::IndexFormat::Uint32,
-                                        );
-                                        render_pass.draw_indexed(
-                                            0..mesh.num_elements,
-                                            0,
-                                            0..instances.len() as u32,
-                                        );
-                                    }
-                                }
+                                // // outline rendering
+                                // let has_selected = entities.iter()
+                                //     .any(|e| e.model.id == model_ptr && e.is_selected);
+                                //
+                                // if has_selected && self.outline_pipeline.is_some() {
+                                //     let outline = self.outline_pipeline.as_ref().unwrap();
+                                //     let mut render_pass = graphics.continue_pass();
+                                //     render_pass.set_pipeline(&outline.pipeline);
+                                //
+                                //     render_pass.set_bind_group(0, &outline.bind_group, &[]);
+                                //     render_pass.set_bind_group(1, camera.bind_group(), &[]);
+                                //
+                                //     render_pass.set_vertex_buffer(1, instance_buffer.slice(..));
+                                //
+                                //     for mesh in &model.meshes {
+                                //         render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
+                                //         render_pass.set_index_buffer(
+                                //             mesh.index_buffer.slice(..),
+                                //             wgpu::IndexFormat::Uint32,
+                                //         );
+                                //         render_pass.draw_indexed(
+                                //             0..mesh.num_elements,
+                                //             0,
+                                //             0..instances.len() as u32,
+                                //         );
+                                //     }
+                                // }
                                 log_once::debug_once!("Rendered {:?}", model_ptr);
                             } else {
                                 log_once::error_once!("No such MODEL as {:?}", model_ptr);
diff --git a/headers/dropbear.h b/headers/dropbear.h
index 7461501..7da4a0c 100644
--- a/headers/dropbear.h
+++ b/headers/dropbear.h
@@ -19,6 +19,12 @@ extern "C" {
 // ===========================================
 
 typedef struct {
+    float x;
+    float y;
+    float z;
+} Vector3D;
+
+typedef struct {
     double position_x;
     double position_y;
     double position_z;
@@ -31,6 +37,25 @@ typedef struct {
     double scale_z;
 } NativeTransform;
 
+typedef struct {
+    const char* label;
+    int64_t entity_id;
+
+    Vector3D eye;
+    Vector3D target;
+    Vector3D up;
+
+    double aspect;
+    double fov_y;
+    double znear;
+    double zfar;
+
+    double yaw;
+    double pitch;
+    double speed;
+    double sensitivity;
+} NativeCamera;
+
 // ===========================================
 
 int dropbear_get_entity(const char* label, const World* world_ptr, int64_t* out_entity);
@@ -47,14 +72,40 @@ int dropbear_set_transform(
     const NativeTransform transform
 );
 
+// property management
+int dropbear_get_string_property(const World* world_ptr, int64_t entity_handle, const char* label, char* out_value, int out_value_max_length);
+int dropbear_get_int_property(const World* world_ptr, int64_t entity_handle, const char* label, int* out_value);
+int dropbear_get_long_property(const World* world_ptr, int64_t entity_handle, const char* label, int64_t* out_value);
+int dropbear_get_float_property(const World* world_ptr, int64_t entity_handle, const char* label, float* out_value);
+int dropbear_get_double_property(const World* world_ptr, int64_t entity_handle, const char* label, double* out_value);
+int dropbear_get_bool_property(const World* world_ptr, int64_t entity_handle, const char* label, int* out_value); // out_value = 0 or 1
+int dropbear_get_vec3_property(const World* world_ptr, int64_t entity_handle, const char* label, float* out_x, float* out_y, float* out_z);
+
+int dropbear_set_string_property(const World* world_ptr, int64_t entity_handle, const char* label, const char* value);
+int dropbear_set_int_property(const World* world_ptr, int64_t entity_handle, const char* label, int value);
+int dropbear_set_long_property(const World* world_ptr, int64_t entity_handle, const char* label, int64_t value);
+int dropbear_set_float_property(const World* world_ptr, int64_t entity_handle, const char* label, float value);
+int dropbear_set_double_property(const World* world_ptr, int64_t entity_handle, const char* label, double value);
+int dropbear_set_bool_property(const World* world_ptr, int64_t entity_handle, const char* label, int value); // value = 0 or 1
+int dropbear_set_vec3_property(const World* world_ptr, int64_t entity_handle, const char* label, float x, float y, float z);
+
+
+// input stuff
 void dropbear_print_input_state(const InputState* input_state_ptr);
-int dropbear_is_key_pressed(const InputState* input_state_ptr, int keycode, int* out_value); // out_value is a boolean 0 or 1
+int dropbear_is_key_pressed(const InputState* input_state_ptr, int keycode, int* out_value); // out_value = 0 or 1
 int dropbear_get_mouse_position(const InputState* input_state_ptr, float* out_x, float* out_y);
 int dropbear_is_mouse_button_pressed(const InputState* input_state_ptr, int button_code, int* out_pressed);
 int dropbear_get_mouse_delta(const InputState* input_state_ptr, float* out_delta_x, float* out_delta_y);
 int dropbear_is_cursor_locked(const InputState* input_state_ptr, int* out_locked);
 int dropbear_set_cursor_locked(const InputState* input_state_ptr, int locked);
 int dropbear_get_last_mouse_pos(const InputState* input_state_ptr, float* out_x, float* out_y);
+int dropbear_is_cursor_hidden(const InputState* input_state_ptr, int* out_hidden);
+int dropbear_set_cursor_hidden(const InputState* input_state_ptr, int hidden);
+
+// camera
+int dropbear_get_camera(const World* world_ptr, const char* label, NativeCamera* out_camera);
+int dropbear_get_attached_camera(const World* world_ptr, int64_t id, NativeCamera* out_camera);
+int dropbear_set_camera(const World* world_ptr, const NativeCamera* camera);
 
 // ===========================================
 
diff --git a/src/nativeMain/kotlin/com/dropbear/ffi/NativeEngine.native.kt b/src/nativeMain/kotlin/com/dropbear/ffi/NativeEngine.native.kt
index 4ef5345..cb69e60 100644
--- a/src/nativeMain/kotlin/com/dropbear/ffi/NativeEngine.native.kt
+++ b/src/nativeMain/kotlin/com/dropbear/ffi/NativeEngine.native.kt
@@ -259,11 +259,50 @@ actual class NativeEngine {
     }
 
     actual fun getStringProperty(entityHandle: Long, label: String): String? {
-        TODO("Not yet implemented")
+        val world = worldHandle ?: return null
+        memScoped {
+            val bufferSize = 256
+            val output = allocArray<ByteVar>(bufferSize)
+
+            // warning: this could potentially cause a buffer overflow idk
+            val result = dropbear_get_string_property(
+                world.reinterpret(),
+                entityHandle,
+                label,
+                output,
+                bufferSize
+            )
+
+            if (result == 0) {
+                val string = output.toKString()
+                return string
+            } else {
+                println("getStringProperty failed with code: $result")
+                return null
+            }
+        }
     }
 
     actual fun getIntProperty(entityHandle: Long, label: String): Int? {
-        TODO("Not yet implemented")
+        val world = worldHandle ?: return null
+        memScoped {
+            val output = alloc<IntVar>()
+
+            val result = dropbear_get_int_property(
+                world.reinterpret(),
+                entityHandle,
+                label,
+                output.ptr,
+            )
+
+            if (result == 0) {
+                val string = output.value
+                return string
+            } else {
+                println("getIntProperty failed with code: $result")
+                return null
+            }
+        }
     }
 
     actual fun getLongProperty(entityHandle: Long, label: String): Long? {