tirbofish/dropbear · commit
a22b80279c050ddb8034cc23847bacea06fd38a8
Merge pull request #50 from 4tkbytes/sockets
pr: more work on ffi
Signature present but could not be verified.
Unverified
@@ -0,0 +1,2 @@ +GITHUB_USERNAME=your-github-username +GITHUB_TOKEN=your-personal-access-token @@ -0,0 +1,39 @@ +name: Deploy Documentation + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: write + +jobs: + deploy-docs: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: 'gradle' + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Generate documentation + run: ./gradlew dokkaHtml + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./build/dokka/html + publish_branch: gh-pages + commit_message: 'Deploy documentation from ${{ github.sha }}' @@ -51,4 +51,8 @@ Cargo.lock **/*.rs.bk *.pdb -.idea/ +.idea/ + +libs/ + +.env @@ -0,0 +1,20 @@ +<component name="ProjectRunConfigurationManager"> + <configuration default="false" name="Build eucalyptus-editor" type="CargoCommandRunConfiguration" factoryName="Cargo Command"> + <option name="buildProfileId" value="dev" /> + <option name="command" value="build" /> + <option name="workingDirectory" value="file://$PROJECT_DIR$" /> + <envs /> + <option name="emulateTerminal" value="true" /> + <option name="channel" value="DEFAULT" /> + <option name="requiredFeatures" value="true" /> + <option name="allFeatures" value="false" /> + <option name="withSudo" value="false" /> + <option name="buildTarget" value="REMOTE" /> + <option name="backtrace" value="SHORT" /> + <option name="isRedirectInput" value="false" /> + <option name="redirectInputPath" value="" /> + <method v="2"> + <option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" /> + </method> + </configuration> +</component> @@ -0,0 +1,24 @@ +<component name="ProjectRunConfigurationManager"> + <configuration default="false" name="Build gradle" type="GradleRunConfiguration" factoryName="Gradle"> + <ExternalSystemSettings> + <option name="executionName" /> + <option name="externalProjectPath" value="$PROJECT_DIR$" /> + <option name="externalSystemIdString" value="GRADLE" /> + <option name="scriptParameters" value="" /> + <option name="taskDescriptions"> + <list /> + </option> + <option name="taskNames"> + <list> + <option value="build" /> + </list> + </option> + <option name="vmOptions" /> + </ExternalSystemSettings> + <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess> + <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess> + <DebugAllEnabled>false</DebugAllEnabled> + <RunAsTest>false</RunAsTest> + <method v="2" /> + </configuration> +</component> @@ -0,0 +1,26 @@ +<component name="ProjectRunConfigurationManager"> + <configuration default="false" name="Build literally everything" type="GradleRunConfiguration" factoryName="Gradle"> + <ExternalSystemSettings> + <option name="executionName" /> + <option name="externalProjectPath" value="$PROJECT_DIR$" /> + <option name="externalSystemIdString" value="GRADLE" /> + <option name="scriptParameters" value="" /> + <option name="taskDescriptions"> + <list /> + </option> + <option name="taskNames"> + <list> + <option value="build" /> + </list> + </option> + <option name="vmOptions" /> + </ExternalSystemSettings> + <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess> + <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess> + <DebugAllEnabled>false</DebugAllEnabled> + <RunAsTest>false</RunAsTest> + <method v="2"> + <option name="RunConfigurationTask" enabled="true" run_configuration_name="Build eucalyptus-editor" run_configuration_type="CargoCommandRunConfiguration" /> + </method> + </configuration> +</component> @@ -0,0 +1,20 @@ +<component name="ProjectRunConfigurationManager"> + <configuration default="false" name="Clean literally everything" type="CargoCommandRunConfiguration" factoryName="Cargo Command"> + <option name="command" value="clean" /> + <option name="workingDirectory" value="file://$PROJECT_DIR$" /> + <envs /> + <option name="emulateTerminal" value="true" /> + <option name="channel" value="DEFAULT" /> + <option name="requiredFeatures" value="true" /> + <option name="allFeatures" value="false" /> + <option name="withSudo" value="false" /> + <option name="buildTarget" value="REMOTE" /> + <option name="backtrace" value="SHORT" /> + <option name="isRedirectInput" value="false" /> + <option name="redirectInputPath" value="" /> + <method v="2"> + <option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" /> + <option name="Gradle.BeforeRunTask" enabled="true" tasks="clean" externalProjectPath="$PROJECT_DIR$" vmOptions="" scriptParameters="" /> + </method> + </configuration> +</component> @@ -0,0 +1,24 @@ +<component name="ProjectRunConfigurationManager"> + <configuration default="false" name="Publish to GitHub Packages" type="GradleRunConfiguration" factoryName="Gradle"> + <ExternalSystemSettings> + <option name="executionName" /> + <option name="externalProjectPath" value="$PROJECT_DIR$" /> + <option name="externalSystemIdString" value="GRADLE" /> + <option name="scriptParameters" value="" /> + <option name="taskDescriptions"> + <list /> + </option> + <option name="taskNames"> + <list> + <option value="publish" /> + </list> + </option> + <option name="vmOptions" /> + </ExternalSystemSettings> + <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess> + <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess> + <DebugAllEnabled>false</DebugAllEnabled> + <RunAsTest>false</RunAsTest> + <method v="2" /> + </configuration> +</component> @@ -0,0 +1,24 @@ +<component name="ProjectRunConfigurationManager"> + <configuration default="false" name="Publish to Maven Local" type="GradleRunConfiguration" factoryName="Gradle"> + <ExternalSystemSettings> + <option name="executionName" /> + <option name="externalProjectPath" value="$PROJECT_DIR$" /> + <option name="externalSystemIdString" value="GRADLE" /> + <option name="scriptParameters" value="" /> + <option name="taskDescriptions"> + <list /> + </option> + <option name="taskNames"> + <list> + <option value="publishToMavenLocal" /> + </list> + </option> + <option name="vmOptions" /> + </ExternalSystemSettings> + <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess> + <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess> + <DebugAllEnabled>false</DebugAllEnabled> + <RunAsTest>false</RunAsTest> + <method v="2" /> + </configuration> +</component> @@ -0,0 +1,20 @@ +<component name="ProjectRunConfigurationManager"> + <configuration default="false" name="Run eucalyptus-editor" type="CargoCommandRunConfiguration" factoryName="Cargo Command"> + <option name="buildProfileId" value="dev" /> + <option name="command" value="run" /> + <option name="workingDirectory" value="file://$PROJECT_DIR$" /> + <envs /> + <option name="emulateTerminal" value="true" /> + <option name="channel" value="DEFAULT" /> + <option name="requiredFeatures" value="true" /> + <option name="allFeatures" value="false" /> + <option name="withSudo" value="false" /> + <option name="buildTarget" value="REMOTE" /> + <option name="backtrace" value="SHORT" /> + <option name="isRedirectInput" value="false" /> + <option name="redirectInputPath" value="" /> + <method v="2"> + <option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" /> + </method> + </configuration> +</component> @@ -1,24 +0,0 @@ -<component name="ProjectRunConfigurationManager"> - <configuration default="false" name="build" type="GradleRunConfiguration" factoryName="Gradle"> - <ExternalSystemSettings> - <option name="executionName" /> - <option name="externalProjectPath" value="$PROJECT_DIR$" /> - <option name="externalSystemIdString" value="GRADLE" /> - <option name="scriptParameters" value="" /> - <option name="taskDescriptions"> - <list /> - </option> - <option name="taskNames"> - <list> - <option value="build" /> - </list> - </option> - <option name="vmOptions" /> - </ExternalSystemSettings> - <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess> - <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess> - <DebugAllEnabled>false</DebugAllEnabled> - <RunAsTest>false</RunAsTest> - <method v="2" /> - </configuration> -</component> @@ -1,20 +0,0 @@ -<component name="ProjectRunConfigurationManager"> - <configuration default="false" name="eucalyptus-editor" type="CargoCommandRunConfiguration" factoryName="Cargo Command"> - <option name="buildProfileId" value="dev" /> - <option name="command" value="run" /> - <option name="workingDirectory" value="file://$PROJECT_DIR$" /> - <envs /> - <option name="emulateTerminal" value="true" /> - <option name="channel" value="DEFAULT" /> - <option name="requiredFeatures" value="true" /> - <option name="allFeatures" value="false" /> - <option name="withSudo" value="false" /> - <option name="buildTarget" value="REMOTE" /> - <option name="backtrace" value="SHORT" /> - <option name="isRedirectInput" value="false" /> - <option name="redirectInputPath" value="" /> - <method v="2"> - <option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" /> - </method> - </configuration> -</component> @@ -6,7 +6,7 @@ package.repository = "https://github.com/4tkbytes/dropbear" package.readme = "README.md" resolver = "3" -members = [ "dropbear-engine", "dropbear_future-queue", "eucalyptus-core", "eucalyptus-editor"] +members = [ "dropbear-engine", "dropbear_future-queue", "eucalyptus-core", "eucalyptus-editor", "magna-carta"] # members = ["dropbear-engine", "eucalyptus-core", "eucalyptus-editor", "redback-runtime"] @@ -61,6 +61,8 @@ gltf = "1" os_info = "3.12" rustc_version_runtime = "0.3" jni = { version = "0.21", features = ["invocation"] } +tree-sitter = "0.22" +tree-sitter-kotlin = "0.3.8" [workspace.dependencies.image] version = "0.25" @@ -15,6 +15,7 @@ If you might have not realised, all the crates/projects names are after Australi ### Related Projects +- [magna-carta](https://github.com/4tkbytes/dropbear/tree/main/magna-carta) is a rust library used to generate compile-time Kotlin/Native and Kotlin/JVM metadata for searching. - [dropbear_future-queue](https://github.com/4tkbytes/dropbear/tree/main/dropbear_future-queue) is a handy library for dealing with async in a sync context - [model_to_image](https://github.com/4tkbytes/model_to_image) is a library used to generate thumbnails and images from a 3D model with the help of `russimp-ng` and a custom made rasteriser. _(very crude but usable)_ @@ -26,7 +27,6 @@ With Unix systems (macOS not tested), you will have to download a couple of depe <!-- If you have a macOS system, please create a PR and add your own implementation. I know you need to use brew, but I don't know what dependencies to install. --> -### Dependencies ```bash # ubuntu @@ -37,7 +37,7 @@ sudo pacman -Syu base-devel systemd pkgconf openssl clang cmake meson assimp jdk ``` -### Engine Build +Then run this to build the project ```bash git clone git@github.com:4tkbytes/dropbear @@ -45,6 +45,8 @@ cd dropbear # this will build all the projects in the workspace cargo build +# ensure that rust is built before gradlew as rust produces a cdylib which gradlew needs to link to +./gradlew build ``` [//]: # (# ensure submodules are checked-out) @@ -70,7 +72,14 @@ If you do not want to build it locally, you are able to download the latest acti Despite the dropbear-engine (and other components) being made in Rust, the editor has chosen the scripting language of choice to be `Kotlin` because of previous experience and that Kotlin is more multiplatform than Swift. -Java is possible (as the JVM runs the script), however it is not officially supported and Kotlin is recommended. . +The dropbear engine uses Kotlin Multiplatform, which allows the cooked up product to be compatible with all platforms +KMP can support, which includes mobile, WASM and desktop. Because the editor is only available on desktop, the JVM is +used to evaluate the scripts as it allows for hot-reloading (not made yet). + +It is recommended to use IntelliJ IDEA with the Rust plugin to help contribute to the engine. If you are a normal joe, +then just use the standard IntelliJ IDEA. + +## Documentation API documentation and articles are available at (todo) @@ -83,11 +92,11 @@ API documentation and articles are available at (todo) <sup>1</sup> Will never be implemented; not intended for that platform. -<sup>2</sup> Made some progress on implementing, but currently a WIP. +<sup>2</sup> Made some progress on implementing, but currently a WIP. ## Contributions -Yeah yeah, go ahead and contribute. Make sure it works, and its not spam, and any tests pass. +Yeah, yeah, go ahead and contribute. Make sure it works, and its not spam, and any tests pass. # Licensing @@ -1,6 +1,8 @@ plugins { - kotlin("jvm") version "2.1.21" -// kotlin("multiplatform") version "2.1.21" + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.kotlinxSerialization) + `maven-publish` + id("org.jetbrains.dokka") version "2.0.0" } group = "com.dropbear" @@ -10,21 +12,95 @@ repositories { mavenCentral() } -dependencies { - testImplementation(kotlin("test")) - implementation(kotlin("test")) +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath("io.github.cdimascio:dotenv-kotlin:6.4.1") + } } -tasks.test { - useJUnitPlatform() -} kotlin { - jvmToolchain(21) -} + jvm() + + val hostOs = System.getProperty("os.name") + val isArm64 = System.getProperty("os.arch") == "aarch64" + val isMingwX64 = hostOs.startsWith("Windows") + val nativeTarget = when { + hostOs == "Mac OS X" && isArm64 -> macosArm64("nativeLib") + hostOs == "Mac OS X" && !isArm64 -> macosX64("nativeLib") + hostOs == "Linux" && isArm64 -> linuxArm64("nativeLib") + hostOs == "Linux" && !isArm64 -> linuxX64("nativeLib") + isMingwX64 -> mingwX64("nativeLib") + else -> throw GradleException("Host OS is not supported in Kotlin/Native.") + } + + val libName = when { + hostOs == "Mac OS X" -> "libeucalyptus_core.dylib" + hostOs == "Linux" -> "libeucalyptus_core.so" + isMingwX64 -> "eucalyptus_core.dll" + else -> throw GradleException("Host OS is not supported in Kotlin/Native.") + } + + if (file("${project.rootDir}/target/debug/$libName").exists()) { + println("Debug library exists") +// "${project.rootDir}/target/debug/$libName" + } else if (file("${project.rootDir}/target/release/$libName").exists()) { + println("Release library exists") +// "${project.rootDir}/target/debug/$libName" + } else if (file("${project.rootDir}/libs/$libName").exists()) { + println("Local library exists") +// "${project.rootDir}/libs/$libName" + } else { + throw GradleException("libeucalyptus_core.so does not exist. This is a local build, so most likely you haven't built the rust library yet. \n" + + "Try running cargo build") + } + + nativeTarget.apply { + compilations.getByName("main") { + cinterops { + val dropbear by creating { + defFile(project.file("src/dropbear.def")) + includeDirs.headerFilterOnly(project.file("headers")) + } + } + } + binaries { + sharedLib { + baseName = "dropbear" + } + } + } + + sourceSets { + commonMain { + dependencies { + api("co.touchlab:kermit:2.0.4") + } + } + nativeMain { + dependencies { + implementation(libs.kotlinxSerializationJson) + } + } + + jvmMain { + kotlin.srcDirs("src/jvmMain/kotlin", "src/jvmMain/java") + dependencies { + + } + } + } -sourceSets { - main { - java.srcDirs("src/main/kotlin", "src/main/java") + targets.all { + compilations.all { + compileTaskProvider.configure { + compilerOptions { + freeCompilerArgs.add("-Xexpect-actual-classes") + } + } + } } } @@ -32,14 +108,40 @@ tasks.register<JavaCompile>("generateJniHeaders") { val outputDir = layout.buildDirectory.dir("generated/jni-headers") options.headerOutputDirectory.set(outputDir.get().asFile) + destinationDirectory.set(layout.buildDirectory.dir("classes/java/jni")) + classpath = files( - tasks.named("compileKotlin"), - tasks.named("compileJava") + tasks.named("compileKotlinJvm"), ) - source = fileTree("src/main/java") { + source = fileTree("src/jvmMain/java") { include("**/*.java") } - dependsOn("compileJava", "compileKotlin") + dependsOn("compileKotlinJvm") } + +val dotenv = io.github.cdimascio.dotenv.dotenv() + +publishing { + repositories { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/4tkbytes/dropbear") + credentials { + username = dotenv["GITHUB_USERNAME"] + password = dotenv["GITHUB_TOKEN"] + } + } + } + + publications { + create<MavenPublication>("release") { + groupId = group as String? + artifactId = rootProject.name + version = version + + from(components["kotlin"]) + } + } +} @@ -7,7 +7,7 @@ repository.workspace = true readme = "README.md" [lib] -crate-type = ["cdylib", "rlib"] +crate-type = ["rlib", "cdylib"] [dependencies] anyhow.workspace = true @@ -29,11 +29,14 @@ winit.workspace = true tokio.workspace = true rayon.workspace = true jni.workspace = true -lazy_static = "1.5.0" crossbeam-channel = "0.5.15" +interprocess = "2.2" +serde_json = "1.0.143" +libloading = "0.8.9" [features] -editor = [] +editor = ["jvm"] +jvm = [] # [target.'cfg(not(target_os = "android"))'.dependencies] # rfd.workspace = true @@ -2,4 +2,6 @@ The core libraries of the eucalyptus editor. Great for embedding into `redback-runtime` and `eucalyptus-editor` as one big change instead of a bunch of features. -This is a library, so if tools are wished to be made, this is the perfect library for you. +This is a library, so if tools are wished to be made, this is the perfect library for you. + +it also produces a shared library for Kotlin/Native and the JVM :) @@ -1,60 +1,56 @@ -use std::fs::{self, File}; -use std::io::Cursor; -use std::process::Command; - fn main() -> anyhow::Result<()> { - // todo: move this into the "setup" process + // // todo: move this into the "setup" process // let repo_zip_url = "https://github.com/4tkbytes/dropbear/archive/refs/heads/main.zip"; // let response = reqwest::blocking::get(repo_zip_url) // .map_err(|e| anyhow::anyhow!("Failed to download repo zip: {}", e))? // .bytes() // .map_err(|e| anyhow::anyhow!("Failed to read zip bytes: {}", e))?; - // + // // let reader = Cursor::new(response); // let mut zip = zip::ZipArchive::new(reader) // .map_err(|e| anyhow::anyhow!("Failed to read zip archive: {}", e))?; - // + // // let app_info = app_dirs2::AppInfo { // name: "Eucalyptus", // author: "4tkbytes", // }; // let app_data_dir = app_dirs2::app_root(app_dirs2::AppDataType::UserData, &app_info) // .map_err(|e| anyhow::anyhow!("Could not determine app data directory: {}", e))?; - // + // // fs::create_dir_all(&app_data_dir) // .map_err(|e| anyhow::anyhow!("Failed to create app data directory: {}", e))?; - // + // // let resource_prefix = "dropbear-main/resources/"; // let mut found_resource = false; // for i in 0..zip.len() { // let mut file = zip.by_index(i).unwrap(); // let name = file.name(); - // + // // if name.starts_with(resource_prefix) && !name.ends_with('/') { // found_resource = true; // let rel_path = &name[resource_prefix.len()..]; // let rel_path = rel_path.strip_prefix('/').unwrap_or(rel_path); // let dest_path = app_data_dir.join(rel_path); - // + // // if let Some(parent) = dest_path.parent() { // fs::create_dir_all(parent) // .map_err(|e| anyhow::anyhow!("Failed to create parent directory: {}", e))?; // } - // + // // println!("Copying {} to {:?}", name, dest_path); - // + // // let mut outfile = File::create(&dest_path) // .map_err(|e| anyhow::anyhow!("Failed to create file: {}", e))?; // std::io::copy(&mut file, &mut outfile) // .map_err(|e| anyhow::anyhow!("Failed to copy file: {}", e))?; // } // } - // + // // if !found_resource { // return Err(anyhow::anyhow!( // "No resources folder found in the github repository [4tkbytes/dropbear] :(" // )); - // } + // } // fuck you windows :( #[cfg(target_os = "windows")] @@ -5,4 +5,5 @@ pub mod logging; pub mod scripting; pub mod states; pub mod utils; -pub mod spawn; +pub mod spawn; +mod ptr; @@ -0,0 +1,29 @@ +#![allow(dead_code)] // needed because some fields arent accessed +use std::marker::PhantomData; + +/// A clonable Send/Sync pointer. Typically unsafe, but fuck it we ball. +/// Anything to not deal with Mutex and RwLock amirite??? +#[derive(Clone)] +pub struct SafePointer<T> { + ptr: *const T, + _marker: PhantomData<T> +} + +unsafe impl<T> Send for SafePointer<T> where T: Send {} + +unsafe impl<T> Sync for SafePointer<T> where T: Sync {} + +impl<T> SafePointer<T> { + /// Creates a new safe pointer from an unsafe pointer + pub fn new(ptr: *const T) -> Self { + SafePointer { + ptr, + _marker: PhantomData, + } + } + + /// Accesses the [`SafePointer`] as an unsafe pointer + pub unsafe fn get(&self) -> *const T { + self.ptr + } +} @@ -1,179 +1,98 @@ -mod java; +pub mod kmp; +pub mod jni; use crate::input::InputState; use crate::states::{EntityNode, PROJECT, SOURCE, ScriptComponent, Value}; use dropbear_engine::entity::{AdoptedEntity, Transform}; use hecs::{Entity, World}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::{collections::HashMap, fs}; -use crate::scripting::java::JavaContext; +use std::ffi::OsString; +use std::sync::{Arc, LazyLock}; +use libloading::Library; +use crate::scripting::jni::JavaContext; pub const TEMPLATE_SCRIPT: &str = include_str!("../../resources/scripting/kotlin/Template.kt"); -#[derive(Clone)] -pub struct DropbearScriptingAPIContext { - pub current_entity: Option<Entity>, - // fyi: im pretty sure this is safe because I can just null with [`Option::None`] - current_world: Option<*const World>, - pub current_input: Option<InputState>, - pub persistent_data: HashMap<String, Value>, - pub frame_data: HashMap<String, Value>, -} - -impl Default for DropbearScriptingAPIContext { - fn default() -> Self { - Self::new() - } -} - -impl DropbearScriptingAPIContext { - pub fn new() -> Self { - Self { - current_entity: None, - current_world: None, - current_input: None, - persistent_data: HashMap::new(), - frame_data: HashMap::new(), - } - } - - pub fn set_context(&mut self, entity: Entity, world: &mut World, input: &InputState) { - self.current_entity = Some(entity); - self.current_world = Some(world as *mut World); - self.current_input = Some(input.clone()); - } - - pub fn clear_context(&mut self) { - self.current_entity = None; - self.current_world = None; - self.current_input = None; - self.frame_data.clear(); - } - - pub fn get_current_entity(&self) -> Option<Entity> { - self.current_entity - } - - pub fn get_input(&self) -> Option<&InputState> { - self.current_input.as_ref() - } - - pub fn set_persistent_data(&mut self, key: String, value: Value) { - self.persistent_data.insert(key, value); - } - - pub fn get_persistent_data(&self, key: &str) -> Option<&Value> { - self.persistent_data.get(key) - } - - pub fn set_frame_data(&mut self, key: String, value: Value) { - self.frame_data.insert(key, value); - } - - pub fn get_frame_data(&self, key: &str) -> Option<&Value> { - self.frame_data.get(key) - } - - pub fn cleanup_entity_data(&mut self, entity: Entity) { - let entity_prefix = format!("entity_{:?}_", entity); - self.persistent_data - .retain(|k, _| !k.starts_with(&entity_prefix)); - } -} - -/// A message from Kotlin that gets sent to Rust -pub enum KotlinMessage { - -} - -/// A message from Rust that gets sent to Kotlin -pub enum RustMessage { - Ping +#[derive(Default)] +pub enum ScriptTarget { + #[default] + None, + JVM { library_path: PathBuf }, + Native { library_path: PathBuf }, } pub struct ScriptManager { - #[allow(dead_code)] - script_context: DropbearScriptingAPIContext, - java: JavaContext, + jvm: Option<JavaContext>, + library: Option<Library>, + script_target: ScriptTarget, + entity_tag_database: HashMap<String, Vec<Entity>>, } impl ScriptManager { pub fn new() -> anyhow::Result<Self> { - // let lib_path: PathBuf = Self::look_for_potential_library()?; - // let library = unsafe { Library::new(lib_path.clone())? }; - - let result = Self { - java: JavaContext::new()?, - script_context: DropbearScriptingAPIContext::new(), - }; - - log::debug!("Initialised ScriptManager"); - Ok(result) + Ok(Self { + jvm: None, + library: None, + script_target: Default::default(), + entity_tag_database: HashMap::new(), + }) } - pub fn load_script( + pub fn init_script( &mut self, - script_name: &String, - _script_content: String, - ) -> anyhow::Result<String> { - - log::debug!("Loaded library [{}]", script_name); - Ok(script_name.clone()) + entity_tag_database: HashMap<String, Vec<Entity>>, + target: ScriptTarget, + ) -> anyhow::Result<()> { + self.entity_tag_database = entity_tag_database; + + match target { + ScriptTarget::JVM { library_path } => { + let jvm = JavaContext::new(library_path)?; + self.jvm = Some(jvm); + } + ScriptTarget::Native { library_path } => { + let library = unsafe { + Library::new(library_path)? + }; + + self.library = Some(library); + } + _ => { + anyhow::bail!("Invalid script target, must be either JVM or Native"); + } + } + + Ok(()) } - pub fn init_entity_script( + pub fn load_script( &mut self, entity_id: hecs::Entity, - script_name: &str, + tag: String, world: &mut World, input_state: &InputState, ) -> anyhow::Result<()> { - log_once::debug_once!("init_entity_script: {} for {:?}", script_name, entity_id); - - // Update the script context - java::update_script_context( - entity_id, - world, - input_state.mouse_pos, - &input_state.pressed_keys.iter().copied().collect::<Vec<_>>() - ); - // Try to load and initialize the script - if let Ok(script_instance) = self.java.load_script_class(script_name) { - self.java.call_script_load(script_instance)?; - log::info!("Initialized script {} for entity {:?}", script_name, entity_id); + #[cfg(feature = "jvm")] + { + if let Some(jvm) = &mut self.jvm { + jvm.init(world)?; + } } - Ok(()) + Err(anyhow::anyhow!("it aint ready yet bozo")) } - pub fn update_entity_script( + pub fn update_script( &mut self, entity_id: hecs::Entity, - script_name: &str, world: &mut World, input_state: &InputState, - _dt: f32, + dt: f32, ) -> anyhow::Result<()> { - log_once::debug_once!("Update entity script name: {}", script_name); - // Update the script context before calling script - java::update_script_context( - entity_id, - world, - input_state.mouse_pos, - &input_state.pressed_keys.iter().copied().collect::<Vec<_>>() - ); - - // Call the update method on the script - if let Ok(script_instance) = self.java.load_script_class(script_name) { - self.java.call_script_update(script_instance)?; - } - - // Sync changes back to world - java::sync_transform_to_world(world); - - Ok(()) + Err(anyhow::anyhow!("it aint ready yet bozo")) } } @@ -265,8 +184,7 @@ pub fn convert_entity_to_group( let script_node = if let Ok(script) = world.get::<&ScriptComponent>(entity_id) { Some(EntityNode::Script { - name: script.name.clone(), - path: script.path.clone(), + tags: script.tags.clone(), }) } else { None @@ -309,15 +227,15 @@ pub fn attach_script_to_entity( Ok(()) } -pub enum ScriptAction { - AttachScript { - script_path: PathBuf, - script_name: String, - }, - CreateAndAttachScript { - script_path: PathBuf, - script_name: String, - }, - RemoveScript, - EditScript, -} +// pub enum ScriptAction { +// AttachScript { +// script_path: PathBuf, +// script_name: String, +// }, +// CreateAndAttachScript { +// script_path: PathBuf, +// script_name: String, +// }, +// RemoveScript, +// EditScript, +// } @@ -1,302 +0,0 @@ -#![allow(non_snake_case)] -use jni::{InitArgsBuilder, JNIEnv, JNIVersion, JavaVM}; -use jni::objects::{JClass, JObject}; -use jni::sys::{jdouble, jlong, jboolean}; -use parking_lot::RwLock; -use once_cell::sync::Lazy; -use dropbear_engine::entity::Transform; -use glam::{DVec3, DQuat}; -use hecs::{Entity, World}; -use winit::keyboard::KeyCode; - -// Thread-safe global context using RwLock instead of Mutex for better performance -static CURRENT_ENTITY: Lazy<RwLock<Option<Entity>>> = Lazy::new(|| RwLock::new(None)); -static CURRENT_WORLD: Lazy<RwLock<Option<usize>>> = Lazy::new(|| RwLock::new(None)); // Store as usize -static CURRENT_POSITION: Lazy<RwLock<DVec3>> = Lazy::new(|| RwLock::new(DVec3::ZERO)); -static CURRENT_ROTATION: Lazy<RwLock<DQuat>> = Lazy::new(|| RwLock::new(DQuat::IDENTITY)); -static CURRENT_SCALE: Lazy<RwLock<DVec3>> = Lazy::new(|| RwLock::new(DVec3::ONE)); -static CURRENT_MOUSE: Lazy<RwLock<(f64, f64)>> = Lazy::new(|| RwLock::new((0.0, 0.0))); -static CURRENT_KEYS: Lazy<RwLock<Vec<i64>>> = Lazy::new(|| RwLock::new(Vec::new())); - -/// A dropbear wrapper for Java Virtual Machine (JVM) based functions -pub(crate) struct JavaContext { - jvm: JavaVM, -} - -impl JavaContext { - /// Creates a new [`JavaContext`] - pub fn new() -> anyhow::Result<Self> { - let jvm_args = InitArgsBuilder::new() - .version(JNIVersion::V8) - .option("-Djava.class.path=./build/libs/dropbear-1.0-SNAPSHOT.jar") - .build()?; - - let jvm = JavaVM::new(jvm_args)?; - log::info!("Initialised JVM"); - Ok(Self { - jvm - }) - } - - /// Get a JNI environment for the current thread - pub fn get_env(&self) -> anyhow::Result<jni::AttachGuard<'_>> { - Ok(self.jvm.attach_current_thread()?) - } - - /// Call a script's load method - pub fn call_script_load(&self, script_instance: JObject) -> anyhow::Result<()> { - let mut env = self.get_env()?; - env.call_method(script_instance, "load", "()V", &[])?; - Ok(()) - } - - /// Call a script's update method - pub fn call_script_update(&self, script_instance: JObject) -> anyhow::Result<()> { - let mut env = self.get_env()?; - env.call_method(script_instance, "update", "()V", &[])?; - Ok(()) - } - - /// Load a script class and create an instance - pub fn load_script_class(&self, class_name: &str) -> anyhow::Result<JObject<'_>> { - let mut env = self.get_env()?; - - // Find the class - let class = env.find_class(class_name)?; - - // Create an instance - let instance = env.new_object(class, "()V", &[])?; - - Ok(instance) - } -} - -/// Update context from Rust side before calling scripts -pub fn update_script_context(entity: Entity, world: &World, mouse_pos: (f64, f64), pressed_keys: &[KeyCode]) { - // Store entity - *CURRENT_ENTITY.write() = Some(entity); - - // Store world pointer as usize - *CURRENT_WORLD.write() = Some(world as *const World as usize); - - // Read transform from world - if let Ok(transform) = world.get::<&Transform>(entity) { - *CURRENT_POSITION.write() = transform.position; - *CURRENT_ROTATION.write() = transform.rotation; - *CURRENT_SCALE.write() = transform.scale; - } - - // Store input state - *CURRENT_MOUSE.write() = mouse_pos; - *CURRENT_KEYS.write() = pressed_keys.iter().map(|k| keycode_to_i64(k)).collect(); -} - -/// Sync changes back to world after script execution -pub fn sync_transform_to_world(world: &mut World) { - if let Some(entity) = *CURRENT_ENTITY.read() { - if let Ok(mut transform) = world.get::<&mut Transform>(entity) { - transform.position = *CURRENT_POSITION.read(); - transform.rotation = *CURRENT_ROTATION.read(); - transform.scale = *CURRENT_SCALE.read(); - } - } -} - -// ============================================================================= -// JNI Native Functions - Called from Kotlin -// ============================================================================= - -/// Get the transform position of the current entity -#[unsafe(no_mangle)] -pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_getPositionX( - _env: JNIEnv, - _class: JClass, -) -> jdouble { - CURRENT_POSITION.read().x -} - -#[unsafe(no_mangle)] -pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_getPositionY( - _env: JNIEnv, - _class: JClass, -) -> jdouble { - CURRENT_POSITION.read().y -} - -#[unsafe(no_mangle)] -pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_getPositionZ( - _env: JNIEnv, - _class: JClass, -) -> jdouble { - CURRENT_POSITION.read().z -} - -/// Set the transform position of the current entity -#[unsafe(no_mangle)] -pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_setPosition( - _env: JNIEnv, - _class: JClass, - x: jdouble, - y: jdouble, - z: jdouble, -) { - *CURRENT_POSITION.write() = DVec3::new(x, y, z); -} - -/// Get rotation quaternion components -#[unsafe(no_mangle)] -pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_getRotationX( - _env: JNIEnv, - _class: JClass, -) -> jdouble { - CURRENT_ROTATION.read().x -} - -#[unsafe(no_mangle)] -pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_getRotationY( - _env: JNIEnv, - _class: JClass, -) -> jdouble { - CURRENT_ROTATION.read().y -} - -#[unsafe(no_mangle)] -pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_getRotationZ( - _env: JNIEnv, - _class: JClass, -) -> jdouble { - CURRENT_ROTATION.read().z -} - -#[unsafe(no_mangle)] -pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_getRotationW( - _env: JNIEnv, - _class: JClass, -) -> jdouble { - CURRENT_ROTATION.read().w -} - -/// Set rotation quaternion -#[unsafe(no_mangle)] -pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_setRotation( - _env: JNIEnv, - _class: JClass, - x: jdouble, - y: jdouble, - z: jdouble, - w: jdouble, -) { - *CURRENT_ROTATION.write() = DQuat::from_xyzw(x, y, z, w); -} - -/// Get scale components -#[unsafe(no_mangle)] -pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_getScaleX( - _env: JNIEnv, - _class: JClass, -) -> jdouble { - CURRENT_SCALE.read().x -} - -#[unsafe(no_mangle)] -pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_getScaleY( - _env: JNIEnv, - _class: JClass, -) -> jdouble { - CURRENT_SCALE.read().y -} - -#[unsafe(no_mangle)] -pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_getScaleZ( - _env: JNIEnv, - _class: JClass, -) -> jdouble { - CURRENT_SCALE.read().z -} - -/// Set scale -#[unsafe(no_mangle)] -pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_setScale( - _env: JNIEnv, - _class: JClass, - x: jdouble, - y: jdouble, - z: jdouble, -) { - *CURRENT_SCALE.write() = DVec3::new(x, y, z); -} - -// ============================================================================= -// Input System JNI Functions -// ============================================================================= - -/// Check if a key is pressed -#[unsafe(no_mangle)] -pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_isKeyPressed( - _env: JNIEnv, - _class: JClass, - keycode: jlong, -) -> jboolean { - let keys = CURRENT_KEYS.read(); - keys.contains(&keycode) as jboolean -} - -/// Get mouse X position -#[unsafe(no_mangle)] -pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_getMouseX( - _env: JNIEnv, - _class: JClass, -) -> jdouble { - CURRENT_MOUSE.read().0 -} - -/// Get mouse Y position -#[unsafe(no_mangle)] -pub extern "system" fn Java_com_dropbear_ffi_NativeEngine_getMouseY( - _env: JNIEnv, - _class: JClass, -) -> jdouble { - CURRENT_MOUSE.read().1 -} - -/// Helper function to convert KeyCode to i64 -fn keycode_to_i64(key: &KeyCode) -> i64 { - match key { - KeyCode::KeyA => 65, - KeyCode::KeyB => 66, - KeyCode::KeyC => 67, - KeyCode::KeyD => 68, - KeyCode::KeyE => 69, - KeyCode::KeyF => 70, - KeyCode::KeyG => 71, - KeyCode::KeyH => 72, - KeyCode::KeyI => 73, - KeyCode::KeyJ => 74, - KeyCode::KeyK => 75, - KeyCode::KeyL => 76, - KeyCode::KeyM => 77, - KeyCode::KeyN => 78, - KeyCode::KeyO => 79, - KeyCode::KeyP => 80, - KeyCode::KeyQ => 81, - KeyCode::KeyR => 82, - KeyCode::KeyS => 83, - KeyCode::KeyT => 84, - KeyCode::KeyU => 85, - KeyCode::KeyV => 86, - KeyCode::KeyW => 87, - KeyCode::KeyX => 88, - KeyCode::KeyY => 89, - KeyCode::KeyZ => 90, - KeyCode::ArrowLeft => 37, - KeyCode::ArrowUp => 38, - KeyCode::ArrowRight => 39, - KeyCode::ArrowDown => 40, - KeyCode::Space => 32, - KeyCode::ShiftLeft | KeyCode::ShiftRight => 16, - KeyCode::ControlLeft | KeyCode::ControlRight => 17, - KeyCode::Escape => 27, - _ => 0, - } -} - @@ -0,0 +1,89 @@ +#![allow(non_snake_case)] +//! Deals with the Java Native Interface (JNI) with the help of the [`jni`] crate + +use std::path::Path; +use std::sync::Arc; +use hecs::World; +use jni::{InitArgsBuilder, JNIEnv, JNIVersion, JavaVM}; +use jni::objects::{GlobalRef, JClass, JString, JValue}; +use jni::sys::jobject; +use jni::sys::jlong; +use dropbear_engine::entity::AdoptedEntity; + +pub struct JavaContext { + jvm: Arc<JavaVM>, + dropbear_engine_class: Option<GlobalRef>, +} + +impl JavaContext { + pub fn new(jar_path: impl AsRef<Path>) -> anyhow::Result<Self> { + let jvm_args = InitArgsBuilder::new() + .version(JNIVersion::V8) + .option(format!("-Djava.class.path={}", jar_path.as_ref().display())) + .build()?; + + let jvm = Arc::new(JavaVM::new(jvm_args)?); + + log::info!("Created JVM instance"); + + Ok(Self { + jvm, + dropbear_engine_class: None, + }) + } + + pub fn init(&mut self, world: &World) -> anyhow::Result<()> { + let mut env = self.jvm.attach_current_thread()?; + + // create native engine first + let native_engine_class: JClass = env.find_class("com/dropbear/ffi/NativeEngine")?; + let native_engine_obj = env.new_object(native_engine_class, "()V", &[])?; + + let world_ptr = world as *const World; + + let world_handle = world_ptr as jlong; + env.call_method( + &native_engine_obj, + "init", + "(J)V", + &[JValue::Long(world_handle)], + )?; + + // create dropbear engine after + let dropbear_class: JClass = env.find_class("com/dropbear/DropbearEngine")?; + let dropbear_obj = env.new_object( + dropbear_class, + "(Lcom/dropbear/ffi/NativeEngine;)V", + &[JValue::Object(&native_engine_obj)], + )?; + + let global_ref = env.new_global_ref(dropbear_obj)?; + + self.dropbear_engine_class = Some(global_ref); + + if let Some(global_ref) = &self.dropbear_engine_class { + env.call_method(&global_ref, "init", "()V", &[])?; + } + + Ok(()) + } +} + +#[unsafe(no_mangle)] +// JNIEXPORT jlong JNICALL Java_com_dropbear_ffi_JNINative_getEntity +// (JNIEnv *, jobject, jlong, jstring); +pub fn Java_com_dropbear_ffi_JNINative_getEntity(env: &mut JNIEnv, _obj: jobject, world_handle: jlong, label: JString) -> jlong { + let label = env.get_string(&label).unwrap(); + let world = world_handle as *mut World; + + let world = unsafe { &mut *world }; + + let rust_label = label.to_str().unwrap().to_string(); + + for (id, entity) in world.query::<&AdoptedEntity>().iter() { + if entity.model.label == rust_label { + return jlong::from(id.id()) + } + } + jlong::from(-1) +} @@ -0,0 +1,48 @@ +//! Deals with Kotlin/Native library loading for different platforms. + +use std::ffi::{c_char, CStr}; +use dropbear_engine::entity::AdoptedEntity; + +/// Looks up an entity by its string label in the given world and writes the result to `out_entity`. +/// +/// # Safety +/// +/// - `label` must be a valid null-terminated C string. +/// - `world_ptr` must be a non-null pointer to a valid, initialized `hecs::World` +/// that is not being mutably aliased (i.e., no concurrent mutable access). +/// - `out_entity` must be a non-null pointer to a `uint64_t` (`u64`) that the caller owns. +/// - The `hecs::World` must outlive the duration of this call. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn dropbear_get_entity( + label: *const c_char, + world_ptr: *const hecs::World, + out_entity: *mut u64, +) -> i32 { + if label.is_null() || world_ptr.is_null() || out_entity.is_null() { + log::warn!("dropbear_get_entity: received null pointer"); + return -1; + } + + let world = unsafe { &*world_ptr }; + + let label_str = unsafe { + match CStr::from_ptr(label).to_str() { + Ok(s) => s, + Err(_) => { + log::warn!("dropbear_get_entity: invalid UTF-8 in label"); + return -1; + } + } + }; + + for (id, entity) in world.query::<&AdoptedEntity>().iter() { + if entity.model.label == label_str { + *out_entity = id.id() as u64; + log::debug!("Found entity with label: {:?}", label_str); + return 0; + } + } + + log::warn!("Entity with label '{:?}' not found", label_str); + -1 +} @@ -242,12 +242,27 @@ pub enum Node { } #[derive(Default, Debug, Serialize, Deserialize, Clone)] -pub struct File { - pub name: String, - pub path: PathBuf, - pub resource_type: Option<ResourceType>, +pub enum File { + #[default] + Unknown, + ResourceFile { + name: String, + path: PathBuf, + resource_type: ResourceType, + }, + SourceFile { + name: String, + path: PathBuf, + }, } +// #[derive(Default, Debug, Serialize, Deserialize, Clone)] +// pub struct File { +// pub name: String, +// pub path: PathBuf, +// pub resource_type: Option<ResourceType>, +// } + #[derive(Default, Debug, Serialize, Deserialize, Clone)] pub struct Folder { pub name: String, @@ -259,6 +274,8 @@ pub struct Folder { #[derive(Debug, Serialize, Deserialize, Clone, Hash)] pub enum ResourceType { Unknown, + Config, + Script, Model, Thumbnail, Texture, @@ -273,6 +290,8 @@ impl Display for ResourceType { ResourceType::Texture => "texture", ResourceType::Shader => "shader", ResourceType::Thumbnail => "thumbnail", + ResourceType::Script => "script", + ResourceType::Config => "eucalyptus project config" }; write!(f, "{}", str) } @@ -406,13 +425,17 @@ fn collect_nodes(dir: impl AsRef<Path>, project_path: impl AsRef<Path>, exclude_ .unwrap_or_default(); let resource_type = if parent_folder.contains("model") { - Some(ResourceType::Model) + ResourceType::Model } else if parent_folder.contains("texture") { - Some(ResourceType::Texture) + ResourceType::Texture } else if parent_folder.contains("shader") { - Some(ResourceType::Shader) + ResourceType::Shader + } else if entry_path.extension().map(|e| e.to_string_lossy().to_lowercase()) == Some("kt".to_string()) { + ResourceType::Script + } else if entry_path.extension().map(|e| e.to_string_lossy().to_lowercase().contains("eu")).unwrap_or_default() { + ResourceType::Config } else { - Some(ResourceType::Unknown) + ResourceType::Unknown }; // Store relative path from the project root instead of absolute path @@ -421,7 +444,7 @@ fn collect_nodes(dir: impl AsRef<Path>, project_path: impl AsRef<Path>, exclude_ .unwrap_or(&entry_path) .to_path_buf(); - nodes.push(Node::File(File { + nodes.push(Node::File(File::ResourceFile { name, path: relative_path, resource_type, @@ -439,8 +462,7 @@ pub enum EntityNode { name: String, }, Script { - name: String, - path: PathBuf, + tags: Vec<String> }, Light { id: hecs::Entity, @@ -460,8 +482,7 @@ pub enum EntityNode { #[derive(Default, Debug, Serialize, Deserialize, Clone)] pub struct ScriptComponent { - pub name: String, - pub path: PathBuf, + pub tags: Vec<String> } impl EntityNode { @@ -484,8 +505,7 @@ impl EntityNode { name: name.clone(), }, EntityNode::Script { - name: script.name.clone(), - path: script.path.clone(), + tags: script.tags.clone(), }, ]; @@ -892,8 +912,7 @@ impl SceneConfig { if let Some(script_config) = &entity_config.script { let script = ScriptComponent { - name: script_config.name.clone(), - path: script_config.path.clone(), + tags: script_config.tags.clone(), }; // if let (Some(target_label), Some(offset)) = ( // &camera_config.follow_target_entity_label, @@ -924,8 +943,7 @@ impl SceneConfig { } else { if let Some(script_config) = &entity_config.script { let script = ScriptComponent { - name: script_config.name.clone(), - path: script_config.path.clone(), + tags: script_config.tags.clone(), }; world.spawn((adopted, transform, script, entity_config.properties.clone())) } else { @@ -977,20 +995,8 @@ impl SceneConfig { if let Some(script_config) = &entity_config.script { let script = ScriptComponent { - name: script_config.name.clone(), - path: script_config.path.clone(), + tags: script_config.tags.clone(), }; - // if let (Some(target_label), Some(offset)) = ( - // &camera_config.follow_target_entity_label, - // &camera_config.follow_offset, - // ) { - // let follow_target = CameraFollowTarget { - // follow_target: target_label.clone(), - // offset: DVec3::from_array(*offset), - // }; - // world.spawn((adopted, transform, script, entity_config.properties.clone(), camera, camera_component, follow_target)) - // } else { - // } world.spawn((adopted, transform, script, entity_config.properties.clone(), camera, camera_component)) } else { world.spawn((adopted, transform, entity_config.properties.clone(), camera, camera_component)) @@ -999,8 +1005,7 @@ impl SceneConfig { // Entity without camera components if let Some(script_config) = &entity_config.script { let script = ScriptComponent { - name: script_config.name.clone(), - path: script_config.path.clone(), + tags: script_config.tags.clone(), }; world.spawn((adopted, transform, script, entity_config.properties.clone())) } else { @@ -1090,8 +1095,7 @@ impl SceneConfig { if let Some(script_config) = &entity_config.script { let script = ScriptComponent { - name: script_config.name.clone(), - path: script_config.path.clone(), + tags: script_config.tags.clone(), }; // if let (Some(target_label), Some(offset)) = ( // &camera_config.follow_target_entity_label, @@ -1123,8 +1127,7 @@ impl SceneConfig { // Entity without camera components if let Some(script_config) = &entity_config.script { let script = ScriptComponent { - name: script_config.name.clone(), - path: script_config.path.clone(), + tags: script_config.tags.clone(), }; world.spawn((plane, transform, script, entity_config.properties.clone())) } else { @@ -1,5 +1,3 @@ -use std::fs::{self, File}; -use std::io::Cursor; use std::process::Command; fn main() -> anyhow::Result<()> { @@ -16,59 +14,6 @@ fn main() -> anyhow::Result<()> { println!("cargo:rerun-if-changed=.git/HEAD"); println!("cargo:rerun-if-changed=.git/refs/heads"); - // todo: move this into the "setup" process - // let repo_zip_url = "https://github.com/4tkbytes/dropbear/archive/refs/heads/main.zip"; - // let response = reqwest::blocking::get(repo_zip_url) - // .map_err(|e| anyhow::anyhow!("Failed to download repo zip: {}", e))? - // .bytes() - // .map_err(|e| anyhow::anyhow!("Failed to read zip bytes: {}", e))?; - // - // let reader = Cursor::new(response); - // let mut zip = zip::ZipArchive::new(reader) - // .map_err(|e| anyhow::anyhow!("Failed to read zip archive: {}", e))?; - // - // let app_info = app_dirs2::AppInfo { - // name: "Eucalyptus", - // author: "4tkbytes", - // }; - // let app_data_dir = app_dirs2::app_root(app_dirs2::AppDataType::UserData, &app_info) - // .map_err(|e| anyhow::anyhow!("Could not determine app data directory: {}", e))?; - // - // fs::create_dir_all(&app_data_dir) - // .map_err(|e| anyhow::anyhow!("Failed to create app data directory: {}", e))?; - // - // let resource_prefix = "dropbear-main/resources/"; - // let mut found_resource = false; - // for i in 0..zip.len() { - // let mut file = zip.by_index(i).unwrap(); - // let name = file.name(); - // - // if name.starts_with(resource_prefix) && !name.ends_with('/') { - // found_resource = true; - // let rel_path = &name[resource_prefix.len()..]; - // let rel_path = rel_path.strip_prefix('/').unwrap_or(rel_path); - // let dest_path = app_data_dir.join(rel_path); - // - // if let Some(parent) = dest_path.parent() { - // fs::create_dir_all(parent) - // .map_err(|e| anyhow::anyhow!("Failed to create parent directory: {}", e))?; - // } - // - // println!("Copying {} to {:?}", name, dest_path); - // - // let mut outfile = File::create(&dest_path) - // .map_err(|e| anyhow::anyhow!("Failed to create file: {}", e))?; - // std::io::copy(&mut file, &mut outfile) - // .map_err(|e| anyhow::anyhow!("Failed to copy file: {}", e))?; - // } - // } - // - // if !found_resource { - // return Err(anyhow::anyhow!( - // "No resources folder found in the github repository [4tkbytes/dropbear] :(" - // )); - // } - // fuck you windows :( #[cfg(target_os = "windows")] { @@ -17,5 +17,7 @@ pub(crate) fn show_menu_bar( log::info!("Show Entities Loaded under Debug Menu is clicked"); *signal = Signal::LogEntities; } + + }); } @@ -6,7 +6,6 @@ use dropbear_engine::attenuation::ATTENUATION_PRESETS; use dropbear_engine::entity::{AdoptedEntity, Transform}; use dropbear_engine::lighting::{Light, LightComponent, LightType}; use egui::{CollapsingHeader, ComboBox, DragValue, Grid, RichText, TextEdit, Ui}; -use eucalyptus_core::scripting::{ScriptAction, TEMPLATE_SCRIPT}; use eucalyptus_core::states::{ModelProperties, ScriptComponent, Value}; use eucalyptus_core::warn; use glam::Vec3; @@ -585,70 +584,30 @@ impl InspectableComponent for ScriptComponent { signal: &mut Signal, label: &mut String, ) { - let script_loc = self.path.to_str().unwrap_or("").to_string(); - ui.vertical(|ui| { CollapsingHeader::new("Scripting") .default_open(true) .show(ui, |ui| { - ui.horizontal(|ui| { - if ui.button("Browse").clicked() - && let Some(script_file) = rfd::FileDialog::new() - .add_filter("Kotlin", &["kt"]) - .pick_file() - { - let script_name = script_file - .file_stem() - .unwrap_or_default() - .to_string_lossy() - .to_string(); - *signal = Signal::ScriptAction(ScriptAction::AttachScript { - script_path: script_file, - script_name, + CollapsingHeader::new("Tags") + .default_open(true) + .show(ui, |ui| { + let mut local_del: Option<usize> = None; + for (i, tag) in self.tags.iter_mut().enumerate() { + let current_width = ui.available_width(); + ui.horizontal(|ui| { + ui.add_sized([current_width*70.0/100.0, 20.0], TextEdit::singleline(tag)); + if ui.button("🗑️").clicked() { + local_del = Some(i); + } }); } - - if ui.button("New").clicked() - && let Some(script_path) = rfd::FileDialog::new() - .add_filter("Kotlin", &["kt"]) - .set_file_name(format!("{}_script.kt", label)) - .save_file() - { - match std::fs::write(&script_path, TEMPLATE_SCRIPT) { - Ok(_) => { - let script_name = script_path - .file_stem() - .unwrap_or_default() - .to_string_lossy() - .to_string(); - *signal = Signal::ScriptAction(ScriptAction::CreateAndAttachScript { - script_path, - script_name, - }); - }, - Err(e) => { - warn!("Failed to create script file: {}", e); - }, - } + if let Some(i) = local_del { + self.tags.remove(i); } - }); - - ui.separator(); - - ui.horizontal_wrapped(|ui| { - ui.label("Script Location:"); - ui.label(script_loc); - }); - - if ui.button("Remove").clicked() { - *signal = Signal::ScriptAction(ScriptAction::RemoveScript); - } - ui.separator(); - ui.horizontal(|ui| { - if ui.button("Edit Script").clicked() { - *signal = Signal::ScriptAction(ScriptAction::EditScript); - } - }); + if ui.button("➕ Add").clicked() { + self.tags.push(String::new()) + } + }); }); }); } @@ -12,7 +12,7 @@ use egui; use egui::{CollapsingHeader}; use egui_dock_fork::TabViewer; use egui_extras; -use eucalyptus_core::states::{Node, RESOURCES, ResourceType}; +use eucalyptus_core::states::{Node, RESOURCES, ResourceType, File}; use log; use parking_lot::Mutex; use transform_gizmo_egui::{EnumSet, Gizmo, GizmoConfig, GizmoExt, GizmoMode, math::DVec3}; @@ -453,91 +453,94 @@ impl<'a> TabViewer for EditorTabViewer<'a> { for node in res { match node { Node::File(file) => { - if !logged.contains(&file.name) { - logged.insert(file.name.clone()); - log::debug!( - "Adding image for {} of type {}", - file.name, - file.resource_type.as_ref().unwrap() - ); - } - if let Some(ref res_type) = file.resource_type { - match res_type { - ResourceType::Model => { - let ad_dir = app_dirs2::get_app_root( - app_dirs2::AppDataType::UserData, - &APP_INFO, - ) - .unwrap(); - - let model_thumbnail = - ad_dir.join(format!("{}.png", file.name)); - - if !model_thumbnail.exists() { - // gen image - log::debug!( - "Model thumbnail [{}] does not exist, generating one now", - file.name + match file { + File::Unknown => {} + File::ResourceFile { name, path, resource_type } => { + if !logged.contains(name) { + logged.insert(name.clone()); + log::debug!( + "Adding image for {} of type {}", + name, + resource_type + ); + } + match resource_type { + ResourceType::Model => { + let ad_dir = app_dirs2::get_app_root( + app_dirs2::AppDataType::UserData, + &APP_INFO, + ) + .unwrap(); + + let model_thumbnail = + ad_dir.join(format!("{}.png", name)); + + if !model_thumbnail.exists() { + // gen image + log::debug!( + "Model thumbnail [{}] does not exist, generating one now", + name + ); + let mut model = match model_to_image::ModelToImageBuilder::new(&path) + .with_size((600, 600)) + .build() { + Ok(v) => v, + Err(e) => panic!("Error occurred while loading file from path: {}", e), + }; + if let Err(e) = + model.render().unwrap().write_to(Some( + &ad_dir + .join(format!("{}.png", name)), + )) + { + log::error!( + "Failed to write model thumbnail for {}: {}", + name, + e ); - let mut model = match model_to_image::ModelToImageBuilder::new(&file.path) - .with_size((600, 600)) - .build() { - Ok(v) => v, - Err(e) => panic!("Error occurred while loading file from path: {}", e), - }; - if let Err(e) = - model.render().unwrap().write_to(Some( - &ad_dir - .join(format!("{}.png", file.name)), - )) - { - log::error!( - "Failed to write model thumbnail for {}: {}", - file.name, - e - ); + } } - } - let image_uri = - model_thumbnail.to_string_lossy().to_string(); + let image_uri = + model_thumbnail.to_string_lossy().to_string(); - assets.push(( - format!("file://{}", image_uri), - file.name.clone(), - file.path.clone(), - res_type.clone(), - )) - } - ResourceType::Texture => assets.push(( - format!( - "file://{}", - file.path.to_string_lossy() - ), - file.name.clone(), - file.path.clone(), - res_type.clone(), - )), - _ => { - if file - .path - .clone() - .extension() - .unwrap() - .to_str() - .unwrap() - .contains("euc") - { - continue; + assets.push(( + format!("file://{}", image_uri), + name.clone(), + path.clone(), + resource_type.clone(), + )) + } + ResourceType::Texture => assets.push(( + format!( + "file://{}", + path.to_string_lossy() + ), + name.clone(), + path.clone(), + resource_type.clone(), + )), + _ => { + if path + .clone() + .extension() + .unwrap() + .to_str() + .unwrap() + .contains("euc") + { + continue; + } + assets.push(( + "NO_TEXTURE".into(), + name.clone(), + path.clone(), + resource_type.clone(), + )) } - assets.push(( - "NO_TEXTURE".into(), - file.name.clone(), - file.path.clone(), - res_type.clone(), - )) } } + File::SourceFile { .. } => {} } } Node::Folder(folder) => { @@ -29,7 +29,7 @@ use eucalyptus_core::{camera::{ CameraAction, CameraComponent, CameraType, DebugCamera, }, states::WorldLoadingStatus}; use eucalyptus_core::input::InputState; -use eucalyptus_core::scripting::{ScriptAction, ScriptManager}; +use eucalyptus_core::scripting::{ScriptManager}; use eucalyptus_core::states::{ CameraConfig, EditorTab, EntityNode, LightConfig, ModelProperties, PROJECT, SCENES, SceneEntity, ScriptComponent, @@ -49,7 +49,7 @@ use dropbear_engine::model::{ModelId}; pub struct Editor { scene_command: SceneCommand, - pub world: World, + pub world: Box<World>, dock_state: DockState<EditorTab>, texture_id: Option<egui::TextureId>, size: Extent3d, @@ -150,7 +150,7 @@ impl Editor { is_viewport_focused: false, // is_cursor_locked: false, window: None, - world: World::new(), + world: Box::new(World::new()), show_new_project: false, project_name: String::new(), project_path: Arc::new(Mutex::new(None)), @@ -903,12 +903,12 @@ fn show_entity_tree( } }); } - EntityNode::Script { name, path: _ } => { + EntityNode::Script { tags } => { ui.horizontal(|ui| { handle.ui(ui, |ui| { ui.label("📜"); }); - ui.label(name.to_string()); + ui.label("Script"); }); } EntityNode::Group { @@ -1172,10 +1172,9 @@ pub enum Signal { Paste(SceneEntity), Delete, Undo, - ScriptAction(ScriptAction), - #[allow(dead_code)] + // ScriptAction(ScriptAction), // not actions required because follow target is set through scripting. - CameraAction(CameraAction), + // CameraAction(CameraAction), Play, StopPlaying, AddComponent(hecs::Entity, EntityType), @@ -52,7 +52,7 @@ impl Scene for Editor { 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() { - self.world = loaded_world; + self.world = Box::new(loaded_world); self.is_world_loaded.mark_project_loaded(); if let Some(dock_state_shared) = &self.dock_state_shared && @@ -106,18 +106,11 @@ impl Scene for Editor { .iter() { log_once::debug_once!( - "Script Entity -> id: {:?}, component: {:?}", + "Script Entity -> id: {:?}, tags: {:?}", entity_id, - script + script.tags ); - script.name = script - .path - .file_name() - .unwrap() - .to_str() - .unwrap() - .to_string(); - script_entities.push((entity_id, script.name.clone())); + script_entities.push((entity_id, script.tags.clone())); } } @@ -126,16 +119,14 @@ impl Scene for Editor { } for (entity_id, script_name) in script_entities { - if let Err(e) = self.script_manager.update_entity_script( + if let Err(e) = self.script_manager.update_script( entity_id, - &script_name, &mut self.world, &self.input_state, dt, ) { log_once::warn_once!( - "Failed to update script '{}' for entity {:?}: {}", - script_name, + "Failed to update script for entity {:?}: {}", entity_id, e ); @@ -183,46 +174,6 @@ impl Scene for Editor { } } - // let camera_follow_data: Vec<(Entity, String, glam::Vec3)> = { - // self.world - // .query::<(&Camera, &CameraComponent)>() - // .iter() - // .filter_map(|(entity_id, (_, _))| { - // follow_target.map(|target| { - // ( - // entity_id, - // target.follow_target.clone(), - // target.offset.as_vec3() - // ) - // }) - // }) - // .collect() - // }; - - - // for (camera_entity, target_label, offset) in camera_follow_data { - // let target_position = { - // self.world - // .query::<(&AdoptedEntity, &Transform)>() - // .iter() - // .find_map(|(_, (adopted, transform))| { - // if adopted.model.label == target_label { - // Some(transform.position) - // } else { - // None - // } - // }) - // }; - // - // - // if let Some(pos) = target_position - // && let Ok(mut query) = self.world.query_one::<&mut Camera>(camera_entity) - // && let Some(camera) = query.get() { - // camera.eye = pos + offset.as_dvec3(); - // camera.target = pos; - // } - // } - { for (_entity_id, (camera, component)) in self.world .query::<(&mut Camera, &mut CameraComponent)>() @@ -1,5 +1,7 @@ +use std::collections::HashMap; use std::sync::Arc; use egui::{Align2, Image}; +use hecs::Entity; use dropbear_engine::camera::Camera; use dropbear_engine::entity::{AdoptedEntity, Transform}; use dropbear_engine::graphics::SharedGraphicsContext; @@ -8,7 +10,7 @@ use dropbear_engine::utils::{ResourceReference, ResourceReferenceType}; use eucalyptus_core::states::{ModelProperties, ScriptComponent, Value}; use eucalyptus_core::{fatal, info, scripting, success, success_without_console, warn, warn_without_console}; use eucalyptus_core::camera::{CameraAction, CameraComponent, CameraType}; -use eucalyptus_core::scripting::ScriptAction; +use eucalyptus_core::scripting::ScriptTarget; use eucalyptus_core::spawn::{push_pending_spawn, PendingSpawn}; use crate::editor::{ComponentType, Editor, EditorState, EntityType, PendingSpawn2, Signal, UndoableAction}; @@ -25,6 +27,7 @@ impl SignalController for Editor { match &self.signal { Signal::None => { + // returns absolutely nothing because no signal is set. Ok::<(), anyhow::Error>(()) } Signal::Copy(_) => {Ok(())} @@ -93,210 +96,6 @@ impl SignalController for Editor { self.signal = Signal::None; Ok(()) } - Signal::ScriptAction(action) => match action { - ScriptAction::AttachScript { - script_path, - script_name, - } => { - if let Some(selected_entity) = self.selected_entity { - match scripting::move_script_to_src(script_path) { - Ok(moved_path) => { - let new_script = ScriptComponent { - name: script_name.clone(), - path: moved_path.clone(), - }; - - let replaced = { - if let Ok(mut sc) = self.world.get::<&mut ScriptComponent>(selected_entity) { - sc.name = new_script.name.clone(); - sc.path = new_script.path.clone(); - true - } else { - false - } - }; - - if !replaced { - match scripting::attach_script_to_entity( - &mut self.world, - selected_entity, - new_script.clone(), - ) { - Ok(_) => { - } - Err(e) => { - self.signal = Signal::None; - fatal!("Failed to attach script to entity {:?}: {}", - selected_entity, - e); - return Err(anyhow::anyhow!(e)); - } - } - } - - { - if let Err(e) = scripting::convert_entity_to_group( - &mut self.world, - selected_entity, - ) { - log::warn!("convert_entity_to_group failed (non-fatal): {}", e); - } - } - - success!( - "{} script '{}' at {} to entity {:?}", - if replaced { "Reattached" } else { "Attached" }, - script_name, - moved_path.display(), - selected_entity - ); - } - Err(e) => { - fatal!("Move failed: {}", e); - } - } - } else { - fatal!("AttachScript requested but no entity is selected"); - } - - self.signal = Signal::None; - Ok(()) - } - ScriptAction::CreateAndAttachScript { - script_path, - script_name, - } => { - if let Some(selected_entity) = self.selected_entity { - let new_script = ScriptComponent { - name: script_name.clone(), - path: script_path.clone(), - }; - - let replaced = { - if let Ok(mut sc) = self.world.get::<&mut ScriptComponent>(selected_entity) { - sc.name = new_script.name.clone(); - sc.path = new_script.path.clone(); - true - } else { - false - } - }; - - if !replaced { - match scripting::attach_script_to_entity( - &mut self.world, - selected_entity, - new_script.clone(), - ) { - Ok(_) => { - } - Err(e) => { - self.signal = Signal::None; - fatal!("Failed to attach new script: {}", e); - return Err(anyhow::anyhow!(e)); - } - } - } - - { - if let Err(e) = scripting::convert_entity_to_group( - &mut self.world, - selected_entity, - ) { - log::warn!("convert_entity_to_group failed (non-fatal): {}", e); - } - } - - success!( - "{} new script '{}' at {} to entity {:?}", - if replaced { "Replaced" } else { "Attached" }, - script_name, - script_path.display(), - selected_entity - ); - } else { - warn_without_console!("No selected entity to attach new script"); - log::warn!("CreateAndAttachScript requested but no entity is selected"); - } - self.signal = Signal::None; - Ok(()) - } - ScriptAction::RemoveScript => { - if let Some(selected_entity) = self.selected_entity { - let mut success = false; - let mut comp = ScriptComponent::default(); - { - if let Ok(script) = self.world - .remove_one::<ScriptComponent>(selected_entity) - { - success!("Removed script from entity {:?}", selected_entity); - success = true; - comp = script.clone(); - } else { - warn!("No script component found on entity {:?}", selected_entity); - } - match self.world.insert_one(selected_entity, ScriptComponent::default()) { - Ok(_) => { - log::debug!("Inserted default script component"); - } - Err(e) => { - log::warn!("No such entity is available. Additional info: {}", e); - } - } - } - - if success { - if let Err(e) = scripting::convert_entity_to_group( - &mut self.world, - selected_entity, - ) { - log::warn!("convert_entity_to_group failed (non-fatal): {}", e); - } - log::debug!("Pushing remove component to undo stack"); - UndoableAction::push_to_undo( - &mut self.undo_stack, - UndoableAction::RemoveComponent( - selected_entity, - Box::new(ComponentType::Script(comp)), - ), - ); - } - } else { - warn!("No entity selected to remove script from"); - } - - self.signal = Signal::None; - Ok(()) - } - ScriptAction::EditScript => { - if let Some(selected_entity) = self.selected_entity { - let script_opt = { - if let Ok(mut q) = self.world.query_one::<&ScriptComponent>(selected_entity) { - q.get().cloned() - } else { - None - } - }; - - if let Some(script) = script_opt { - match open::that(script.path.clone()) { - Ok(()) => { - success!("Opened {}", script.name) - } - Err(e) => { - warn!("Error while opening {}: {}", script.name, e); - } - } - } else { - warn!("No script component found on entity {:?}", selected_entity); - } - } else { - warn!("No entity selected to edit script"); - } - self.signal = Signal::None; - Ok(()) - } - }, Signal::Play => { if matches!(self.editor_state, EditorState::Playing) { fatal!("Unable to play: already in playing mode"); @@ -329,60 +128,43 @@ impl SignalController for Editor { } } + let mut etag: HashMap<String, Vec<Entity>> = HashMap::new(); for (entity_id, script) in script_entities { + for tag in script.tags { + if etag.contains_key(&tag) { + etag.get_mut(&tag).unwrap().push(entity_id); + } else { + etag.insert(tag.clone(), vec![entity_id]); + } + } + } + + let etag_clone = etag.clone(); + + // todo: get the library name working + if let Err(e) = self.script_manager.init_script(etag_clone, ScriptTarget::None) { + fatal!("Failed to ready script manager interface because {}", e); + self.signal = Signal::StopPlaying; + return Err(anyhow::anyhow!(e)); + } + + for (tag, entities) in &etag { log::debug!( - "Initialising entity script [{}] from path: {}", - script.name, - script.path.display() + "Initialising script for tag {:?} with entities: {:?}", + tag, + entities ); - let bytes = match std::fs::read_to_string(&script.path) { - Ok(val) => val, - Err(e) => { - self.signal = Signal::None; - fatal!( - "Unable to read script {} to bytes because {}", - &script.path.display(), - e - ); + for e in entities { + if let Err(e) = self.script_manager.load_script(*e, tag.clone(), &mut self.world, &self.input_state) { + fatal!("Failed to initialise script for tag {:?} because {}", tag, e); + self.signal = Signal::StopPlaying; return Err(anyhow::anyhow!(e)); - } - }; - - match self.script_manager.load_script( - &script - .path - .file_name() - .unwrap() - .to_string_lossy() - .to_string(), - bytes, - ) { - Ok(script_name) => { - if let Err(e) = self.script_manager.init_entity_script( - entity_id, - &script_name, - &mut self.world, - &self.input_state, - ) { - log::warn!( - "Failed to initialise script '{}' for entity {:?}: {}", - script.name, - entity_id, - e - ); - self.signal = Signal::StopPlaying; - } else { - success_without_console!( + } else { + success_without_console!( "You are in play mode now! Press Escape to exit" ); - log::info!("You are in play mode now! Press Escape to exit"); - } - } - Err(e) => { - // todo: proper error menu - self.signal = Signal::StopPlaying; - fatal!("Failed to load script '{}': {}", script.name, e); + log::info!("You are in play mode now! Press Escape to exit"); } } } @@ -415,28 +197,6 @@ impl SignalController for Editor { self.signal = Signal::None; Ok(()) } - Signal::CameraAction(action) => match action { - CameraAction::SetPlayerTarget { .. } => { - log::warn!("Deprecated: CameraAction::SetPlayerTarget"); - self.signal = Signal::None; - Ok(()) - } - CameraAction::ClearPlayerTarget => { - log::warn!("Deprecated: CameraAction::ClearPlayerTarget"); - self.signal = Signal::None; - Ok(()) - } - CameraAction::SetCurrentPositionAsOffset(_) => { - // if let Ok((camera, target)) = self.world.query_one_mut::<(&Camera, &mut CameraFollowTarget)>(*entity) { - // target.offset = camera.target - // } else { - // warn!("Unable to query camera to set current camera position to offset"); - // } - log::warn!("Deprecated: CameraAction::SetCurrentPositionAsOffset"); - self.signal = Signal::None; - Ok(()) - } - }, Signal::AddComponent(entity, e_type) => { match e_type { EntityType::Entity => { @@ -702,18 +462,18 @@ impl SignalController for Editor { Signal::LogEntities => { log::debug!("===================="); let mut counter = 0; - for entity in self.world.iter() { - if let Some(entity) = entity.get::<&AdoptedEntity>() { - log::info!("Model: {:?}", entity.model.label); + for e in self.world.iter() { + if let Some(entity) = e.get::<&AdoptedEntity>() { + log::info!("Model: {:?} with u32 id: {:?}", entity.model.label, e.entity().id()); log::info!(" |-> Using model: {:?}", entity.model.id); } - if let Some(entity) = entity.get::<&Light>() { + if let Some(entity) = e.get::<&Light>() { log::info!("Light: {:?}", entity.cube_model.label); log::info!(" |-> Using model: {:?}", entity.cube_model.id); } - if entity.get::<&Camera>().is_some() { + if e.get::<&Camera>().is_some() { log::info!("Camera"); } counter += 1; @@ -1 +1,3 @@ kotlin.code.style=official +org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled +org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true @@ -0,0 +1,10 @@ +[versions] +kotlin = "2.1.0" +kotlinxSerialization = "1.8.0" + +[libraries] +kotlinxSerializationJson = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" } + +[plugins] +kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } @@ -0,0 +1,23 @@ +#ifndef DROPBEAR_H +#define DROPBEAR_H + +#include <stddef.h> +#include <stdint.h> + +typedef struct World World; // opaque pointer + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// =========================================== + +// returns 0 on success, non-zero on failure +int dropbear_get_entity(const char* label, const World* world_ptr, uint64_t* out_entity); + +// =========================================== + +#ifdef __cplusplus +} // extern "C" +#endif // __cplusplus +#endif // DROPBEAR_H @@ -0,0 +1,12 @@ +[package] +name = "magna-carta" +version.workspace = true +edition.workspace = true +license = "MIT" +repository.workspace = true +readme = "README.md" + +[dependencies] +anyhow.workspace = true +tree-sitter.workspace = true +tree-sitter-kotlin.workspace = true @@ -0,0 +1,24 @@ +# magna-carta + +Creates a manifest for kotlin files to allow for compile-time annotation processing. + +This crate was created because Kotlin Symbol Processor (KSP) does not support Kotlin Multiplatform (KMP) Native +targets. + +## Behaviour + +### Common + +magna-carta will look for all files in the `src/commonMain/kotlin` directory and generate a manifest file in +`build/magna-carta/manifest.json`. This can be used for either [Native](#native) or [JVM](#jvm) targets, which +each have their own behavior. + +### Native + +In the case the Native build is requested, it will generate a manifest Kotlin file in `src/nativeMain/kotlin`, which +includes exported C ABI entry points for dropbear to call into. + +### JVM + +The JVM will also generate a manifest Kotlin file in `src/jvmMain/kotlin`, however because of its integration +with the `jni` crate, it will not generate any C ABI entry points, instead allowing for reflection to be used. @@ -0,0 +1,48 @@ +use std::fmt::Write; +use crate::generator::Generator; +use crate::ScriptManifest; + +pub struct KotlinJVMGenerator; + +impl Generator for KotlinJVMGenerator { + fn generate(&self, manifest: &ScriptManifest) -> anyhow::Result<String> { + let mut output = String::new(); + + writeln!(output, "// Auto-generated by dropbear-engine with the magna-carta parser")?; + writeln!(output)?; + + // Import all discovered classes + let mut imported_packages = std::collections::HashSet::new(); + for item in manifest.items() { + if let Some(last_dot) = item.fqcn().rfind('.') { + let package = &item.fqcn()[..last_dot]; + imported_packages.insert(package.to_string()); + } + } + + for package in imported_packages.iter() { + writeln!(output, "import {}.*", package)?; + } + writeln!(output)?; + + // Simple registry class for JVM + writeln!(output, "object RunnableRegistry {{")?; + writeln!(output, " val SCRIPTS = listOf(")?; + + for (i, item) in manifest.items().iter().enumerate() { + if i > 0 { + writeln!(output, ",")?; + } + writeln!(output, " {}::class", item.simple_name())?; + } + + if !manifest.items().is_empty() { + writeln!(output)?; + } + writeln!(output, " )")?; + writeln!(output, "}}")?; + + Ok(output) + } +} + @@ -0,0 +1,80 @@ +use std::path::Path; +use crate::ScriptManifest; + +pub mod native; +pub mod jvm; + +#[allow(dead_code)] +pub trait Generator { + fn generate(&self, manifest: &ScriptManifest) -> anyhow::Result<String>; + + fn write_to_file(&self, manifest: &ScriptManifest, path: impl AsRef<Path>) -> anyhow::Result<()> { + let content = self.generate(manifest)?; + std::fs::write(path, content)?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + use crate::generator::jvm::KotlinJVMGenerator; + use crate::generator::native::KotlinNativeGenerator; + use crate::ManifestItem; + + #[test] + fn test_native_generator() { + let mut manifest = ScriptManifest::new(); + manifest.add_item(ManifestItem::new( + "com.game.Player".to_string(), + "Player".to_string(), + vec!["player".to_string(), "movement".to_string()], + PathBuf::from("src/Player.kt"), + )); + + manifest.add_item(ManifestItem::new( + "com.game.GlobalLogger".to_string(), + "GlobalLogger".to_string(), + vec![], + PathBuf::from("src/GlobalLogger.kt"), + )); + + let generator = KotlinNativeGenerator; + let output = generator.generate(&manifest).unwrap(); + + // Verify imports + assert!(output.contains("import com.game.Player")); + assert!(output.contains("import com.game.GlobalLogger")); + + // Verify tags + assert!(output.contains("tags = listOf(\"player\", \"movement\")")); + assert!(output.contains("tags = listOf()")); + + // Verify class instantiations + assert!(output.contains("script = Player()")); + assert!(output.contains("script = GlobalLogger()")); + + // Verify CName exports + assert!(output.contains("@CName(\"dropbear_load\")")); + assert!(output.contains("@CName(\"dropbear_update\")")); + assert!(output.contains("@CName(\"dropbear_destroy\")")); + } + + #[test] + fn test_jvm_generator() { + let mut manifest = ScriptManifest::new(); + manifest.add_item(ManifestItem::new( + "com.game.Player".to_string(), + "Player".to_string(), + vec!["player".to_string()], + PathBuf::from("src/Player.kt"), + )); + + let generator = KotlinJVMGenerator; + let output = generator.generate(&manifest).unwrap(); + + assert!(output.contains("import com.game.*")); + assert!(output.contains("Player::class")); + } +} @@ -0,0 +1,134 @@ +use std::fmt::Write; +use crate::generator::Generator; +use crate::ScriptManifest; + +pub struct KotlinNativeGenerator; + +impl Generator for KotlinNativeGenerator { + fn generate(&self, manifest: &ScriptManifest) -> anyhow::Result<String> { + let mut output = String::new(); + + // Header + writeln!(output, "// Auto-generated by dropbear-engine with the magna-carta parser")?; + writeln!(output, "@file:OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class)")?; + writeln!(output)?; + + // Imports + writeln!(output, "import com.dropbear.DropbearEngine")?; + writeln!(output, "import com.dropbear.EntityId")?; + writeln!(output, "import com.dropbear.EntityRef")?; + writeln!(output, "import com.dropbear.ProjectScriptingMetadata")?; + writeln!(output, "import com.dropbear.ScriptRegistration")?; + writeln!(output, "import com.dropbear.ffi.NativeEngine")?; + writeln!(output, "import kotlinx.cinterop.COpaquePointer")?; + writeln!(output, "import kotlinx.cinterop.ExperimentalForeignApi")?; + writeln!(output, "import kotlin.experimental.ExperimentalNativeApi")?; + writeln!(output, "import kotlin.native.CName")?; + writeln!(output)?; + + // Import all discovered classes + let mut imported_classes = std::collections::HashSet::new(); + for item in manifest.items() { + // Extract package from FQCN to create import + if let Some(last_dot) = item.fqcn().rfind('.') { + let package = &item.fqcn()[..last_dot]; + let simple_name = &item.fqcn()[last_dot + 1..]; + writeln!(output, "import {}.{}", package, simple_name)?; + imported_classes.insert(simple_name.to_string()); + } else { + // No package, just the class name + imported_classes.insert(item.simple_name().to_string()); + } + } + writeln!(output)?; + + // Script instances registry + writeln!(output, "private val scriptInstances: List<ScriptRegistration> by lazy {{")?; + writeln!(output, " Metadata().getScripts()")?; + writeln!(output, "}}")?; + writeln!(output)?; + + // Metadata class + writeln!(output, "class Metadata: ProjectScriptingMetadata {{")?; + writeln!(output, " override fun getScripts(): List<ScriptRegistration> {{")?; + writeln!(output, " return listOf(")?; + + for (i, item) in manifest.items().iter().enumerate() { + if i > 0 { + writeln!(output, ",")?; + } + + // Format tags + let tags_str = if item.tags().is_empty() { + "listOf()".to_string() + } else { + let tag_list: Vec<String> = item.tags() + .iter() + .map(|tag| format!("\"{}\"", tag)) + .collect(); + format!("listOf({})", tag_list.join(", ")) + }; + + writeln!( + output, + " ScriptRegistration(\n tags = {},\n script = {}()\n )", + tags_str, + item.simple_name() + )?; + } + + if !manifest.items().is_empty() { + writeln!(output)?; + } + writeln!(output, " )")?; + writeln!(output, " }}")?; + writeln!(output, "}}")?; + writeln!(output)?; + + // Engine factory function + writeln!(output, "fun getDropbearEngine(worldPointer: COpaquePointer?, currentEntity: Long?): DropbearEngine {{")?; + writeln!(output, " val nativeEngine = NativeEngine()")?; + writeln!(output, " nativeEngine.init(worldPointer)")?; + writeln!(output, " val dropbearEngine = DropbearEngine(nativeEngine, if (currentEntity == null) null else EntityRef(EntityId(currentEntity)))")?; + writeln!(output, " return dropbearEngine")?; + writeln!(output, "}}")?; + writeln!(output)?; + + // Load function + writeln!(output, "@CName(\"dropbear_load\")")?; + writeln!(output, "fun loadScriptByTag(worldPointer: COpaquePointer?, currentEntity: Long?, tag: String?) {{")?; + writeln!(output, " if (tag == null) return")?; + writeln!(output, " val scripts = scriptInstances.filter {{ it.tags.contains(tag) }}")?; + writeln!(output, " val engine = getDropbearEngine(worldPointer, currentEntity)")?; + writeln!(output, " for (script in scripts) {{")?; + writeln!(output, " script.script.load(engine)")?; + writeln!(output, " }}")?; + writeln!(output, "}}")?; + writeln!(output)?; + + // Update function + writeln!(output, "@CName(\"dropbear_update\")")?; + writeln!(output, "fun updateScriptByTag(worldPointer: COpaquePointer?, currentEntity: Long?, tag: String?, deltaTime: Double) {{")?; + writeln!(output, " if (tag == null) return")?; + writeln!(output, " val scripts = scriptInstances.filter {{ it.tags.contains(tag) }}")?; + writeln!(output, " val engine = getDropbearEngine(worldPointer, currentEntity)")?; + writeln!(output, " for (script in scripts) {{")?; + writeln!(output, " script.script.update(engine, deltaTime.toFloat())")?; + writeln!(output, " }}")?; + writeln!(output, "}}")?; + writeln!(output)?; + + // Destroy function + writeln!(output, "@CName(\"dropbear_destroy\")")?; + writeln!(output, "fun destroyScriptByTag(worldPointer: COpaquePointer?, currentEntity: Long?, tag: String?) {{")?; + writeln!(output, " if (tag == null) return")?; + writeln!(output, " val scripts = scriptInstances.filter {{ it.tags.contains(tag) }}")?; + writeln!(output, " val engine = getDropbearEngine(worldPointer, currentEntity)")?; + writeln!(output, " for (script in scripts) {{")?; + writeln!(output, " script.script.destroy(engine)")?; + writeln!(output, " }}")?; + writeln!(output, "}}")?; + + Ok(output) + } +} @@ -0,0 +1,329 @@ +mod generator; + +use std::path::PathBuf; +use tree_sitter::{Parser, Query, QueryCursor}; + +#[derive(Debug, Clone)] +pub struct ScriptManifest { + items: Vec<ManifestItem>, +} + +impl Default for ScriptManifest { + fn default() -> Self { + Self::new() + } +} + +impl ScriptManifest { + pub fn new() -> Self { + Self { + items: Vec::new(), + } + } + + pub fn add_item(&mut self, item: ManifestItem) { + self.items.push(item); + } + + pub fn items(&self) -> &[ManifestItem] { + &self.items + } +} + +#[derive(Debug, Clone)] +pub struct ManifestItem { + /// Fully qualified class name + /// + /// Example: `foo.bar.Enemy` + fqcn: String, + /// Simple name of the class + /// + /// Example: `Enemy` + simple_name: String, + /// Tags to identify the class + /// + /// Example: `["goomba", "shell"]` + tags: Vec<String>, + /// Path to the source file in reference to the project root + /// + /// Example: `src/commonMain/kotlin/foo/bar/Enemy.kt` + file_path: PathBuf, +} + +impl ManifestItem { + pub fn new( + fqcn: String, + simple_name: String, + tags: Vec<String>, + file_path: PathBuf, + ) -> Self { + Self { + fqcn, + simple_name, + tags, + file_path, + } + } + + pub fn fqcn(&self) -> &str { + &self.fqcn + } + + pub fn simple_name(&self) -> &str { + &self.simple_name + } + + pub fn tags(&self) -> &[String] { + &self.tags + } + + pub fn file_path(&self) -> &PathBuf { + &self.file_path + } +} + +pub struct KotlinProcessor { + parser: Parser, +} + +impl KotlinProcessor { + pub fn new() -> anyhow::Result<Self> { + let mut parser = Parser::new(); + parser.set_language(&tree_sitter_kotlin::language())?; + Ok(Self { parser }) + } + + pub fn process_file( + &mut self, + source_code: &str, + file_path: PathBuf, + ) -> anyhow::Result<Option<ManifestItem>> { + let tree = self + .parser + .parse(source_code, None) + .ok_or_else(|| anyhow::anyhow!("Failed to parse source code"))?; + + let root_node = tree.root_node(); + + let package = self.extract_package(root_node, source_code)?; + + if let Some(class_info) = self.extract_class_info(root_node, source_code)? { + let (class_name, tags) = class_info; + + let fqcn = if package.is_empty() { + class_name.clone() + } else { + format!("{}.{}", package, class_name) + }; + + return Ok(Some(ManifestItem::new( + fqcn, + class_name, + tags, + file_path, + ))); + } + + Ok(None) + } + + fn extract_package( + &self, + root_node: tree_sitter::Node, + source: &str, + ) -> anyhow::Result<String> { + let query = Query::new( + &tree_sitter_kotlin::language(), + r#" + (package_header + (identifier) @package) + "#, + )?; + + let mut cursor = QueryCursor::new(); + let mut matches = cursor.matches(&query, root_node, source.as_bytes()); + + if let Some(match_) = matches.next() && let Some(capture) = match_.captures.first() { + let package_node = capture.node; + let package_text = package_node.utf8_text(source.as_bytes())?; + + return Ok(package_text.replace('\n', "").trim().to_string()); + } + + Ok(String::new()) + } + + fn extract_class_info( + &self, + root_node: tree_sitter::Node, + source: &str, + ) -> anyhow::Result<Option<(String, Vec<String>)>> { + let query = Query::new( + &tree_sitter_kotlin::language(), + r#" + ; Case 1: @Runnable (no parentheses) + (class_declaration + (modifiers + (annotation + (user_type + (type_identifier) @annotation_name) + (#eq? @annotation_name "Runnable"))) + (type_identifier) @class_name) + + ; Case 2: @Runnable(...) (with parentheses) + (class_declaration + (modifiers + (annotation + (constructor_invocation + (user_type + (type_identifier) @annotation_name2) + (value_arguments)? @value_args) + (#eq? @annotation_name2 "Runnable"))) + (type_identifier) @class_name2) + "#, + )?; + + let mut cursor = QueryCursor::new(); + let matches = cursor.matches(&query, root_node, source.as_bytes()); + + let annotation_name_idx = query.capture_index_for_name("annotation_name"); + let annotation_name2_idx = query.capture_index_for_name("annotation_name2"); + let class_name_idx = query.capture_index_for_name("class_name"); + let class_name2_idx = query.capture_index_for_name("class_name2"); + let value_args_idx = query.capture_index_for_name("value_args"); + + for match_ in matches { + let mut class_name = String::new(); + let mut found_runnable = false; + let mut value_args_node = None; + + for capture in match_.captures { + let text = capture.node.utf8_text(source.as_bytes())?; + + // case 1 (no brackets) + if let Some(idx) = annotation_name_idx && capture.index == idx && text == "Runnable" { + found_runnable = true; + } + + // case 2 (with brackets) + if let Some(idx) = annotation_name2_idx && capture.index == idx && text == "Runnable" { + found_runnable = true; + } + + // class names + if let Some(idx) = class_name_idx && capture.index == idx { + class_name = text.to_string(); + } + + if let Some(idx) = class_name2_idx && capture.index == idx { + class_name = text.to_string(); + } + + + // case 2 value args + if let Some(idx) = value_args_idx && capture.index == idx { + value_args_node = Some(capture.node); + } + + } + + if found_runnable && !class_name.is_empty() { + let tags = if let Some(value_args) = value_args_node { + self.extract_tags_from_value_args(value_args, source)? + } else { + Vec::new() + }; + + return Ok(Some((class_name, tags))); + } + } + + Ok(None) + } + + fn extract_tags_from_value_args( + &self, + value_args_node: tree_sitter::Node, + source: &str, + ) -> anyhow::Result<Vec<String>> { + let mut tags = Vec::new(); + + let mut cursor = value_args_node.walk(); + for value_arg in value_args_node.children(&mut cursor) { + if value_arg.kind() == "value_argument" { + let mut arg_cursor = value_arg.walk(); + for child in value_arg.children(&mut arg_cursor) { + // Case 1: Direct string literal (vararg style) + if child.kind() == "string_literal" { + let text = child.utf8_text(source.as_bytes())?; + let clean_tag = text.trim_matches(|c| c == '"' || c == '\'').to_string(); + if !clean_tag.is_empty() { + tags.push(clean_tag); + } + } + else if child.kind() == "collection_literal" { + let mut collection_cursor = child.walk(); + for collection_item in child.children(&mut collection_cursor) { + if collection_item.kind() == "string_literal" { + let text = collection_item.utf8_text(source.as_bytes())?; + let clean_tag = text.trim_matches(|c| c == '"' || c == '\'').to_string(); + if !clean_tag.is_empty() { + tags.push(clean_tag); + } + } + } + } + } + } + } + + Ok(tags) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_process_player_class() { + let source = r#" +package com.dropbear + +@Runnable(["player", "movement"]) +class Player: System { + override fun load(engine: DropbearEngine) { + TODO("Not yet implemented") + } + override fun update(engine: DropbearEngine, deltaTime: Float) { + + TODO("Not yet implemented") + + } + + override fun destroy(engine: DropbearEngine) { + + TODO("Not yet implemented") + + } + +} +"#; + + let mut processor = KotlinProcessor::new().unwrap(); + let result = processor + .process_file(source, PathBuf::from("src/main/kotlin/com/dropbear/Player.kt")) + .unwrap(); + + assert!(result.is_some()); + let item = result.unwrap(); + + assert_eq!(item.fqcn(), "com.dropbear.Player"); + assert_eq!(item.simple_name(), "Player"); + assert_eq!(item.tags(), &["player", "movement"]); + assert_eq!( + item.file_path(), + &PathBuf::from("src/main/kotlin/com/dropbear/Player.kt") + ); + } +} Binary files /dev/null and b/resources/templates/gradle_template.tar.gz differ @@ -1,4 +1,8 @@ -plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" -} -rootProject.name = "dropbear" +rootProject.name = "dropbear" + +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + } +} @@ -0,0 +1,22 @@ +package com.dropbear + +import com.dropbear.ffi.NativeEngine + +class DropbearEngine(val native: NativeEngine, val currentEntity: EntityRef? = null) { + private val globalScripts = mutableListOf<System>() + + fun init() { + } + + fun getEntity(label: String): Result<EntityRef> { + val entityId = native.getEntity(label) + return (when (entityId) { + null -> { + Result.failure(Exception("JNI returned null")) + } + else -> { + Result.success(EntityRef(EntityId(entityId))) + } + }) + } +} @@ -0,0 +1,3 @@ +package com.dropbear + +data class EntityId(val id: ULong) @@ -0,0 +1,22 @@ +package com.dropbear + +import com.dropbear.math.Vector3D + +/** + * A class to hold a reference to an entity. + * + * The dropbear engine interface is made in Rust, which uses a + * borrow checker paradigm, which heavily utilises immutability + * unless explicitly provided with the `mut` keyword. + * + * Because of this, the primary source of the World (place to store + * entities) is stored in Rust, and passing an EntityRef (which contains + * an ID) follows Rust's ideologies of passing references instead of the entire entity, + * which provides immutability. + * + * To edit any values part of the entity, take a look at the functions provided + * by [EntityRef], which will require a reference to [DropbearEngine] to push the commands. + */ +class EntityRef(val id: EntityId = EntityId(0u)) { + fun setPosition(position: Vector3D, engine: DropbearEngine) {} +} @@ -0,0 +1,21 @@ +import com.dropbear.DropbearEngine +import com.dropbear.EntityId +import com.dropbear.System +import com.dropbear.ProjectScriptingMetadata +import com.dropbear.Runnable +import com.dropbear.ScriptRegistration + +@Runnable(["player"]) +class Player: System { + override fun load(engine: DropbearEngine) { + TODO("Not yet implemented") + } + + override fun update(engine: DropbearEngine, deltaTime: Float) { + TODO("Not yet implemented") + } + + override fun destroy(engine: DropbearEngine) { + TODO("Not yet implemented") + } +} @@ -0,0 +1,5 @@ +package com.dropbear + +interface ProjectScriptingMetadata { + fun getScripts(): List<ScriptRegistration> +} @@ -0,0 +1,5 @@ +package com.dropbear + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.SOURCE) +annotation class Runnable(val tags: Array<String> = []) @@ -0,0 +1,6 @@ +package com.dropbear + +data class ScriptRegistration( + val tags: List<String>, + val script: System +) @@ -0,0 +1,7 @@ +package com.dropbear + +interface System { + fun load(engine: DropbearEngine) + fun update(engine: DropbearEngine, deltaTime: Float) + fun destroy(engine: DropbearEngine) +} @@ -0,0 +1,7 @@ +package com.dropbear.ffi + +import com.dropbear.EntityRef + +expect class NativeEngine { + fun getEntity(label: String): ULong? +} @@ -0,0 +1,11 @@ +package com.dropbear.math + +typealias QuaternionD = Quaternion<Double> + +class Quaternion<T: Number>(var x: T, var y: T, var z: T, var w: T) { + companion object { + fun identity(): Quaternion<Double> { + return Quaternion(0.0, 0.0, 0.0, 1.0) + } + } +} @@ -0,0 +1,29 @@ +package com.dropbear.math + +/** + * A class for holding a vector of `3` values of the same type. + */ +class Vector3<T: Number>(var x: T, var y: T, var z: T) { + companion object { + /** + * Creates a new [com.dropbear.math.Vector3] of type `T` with one value. + */ + fun <T: Number> uniform(value: T): Vector3<T> { + return Vector3(value, value, value) + } + + /** + * Creates a [com.dropbear.math.Vector3] of type [Double] filled with only zeroes + */ + fun zero(): Vector3<Double> { + return Vector3(0.0, 0.0, 0.0) + } + } + + fun asDoubleVector(): Vector3<Double> { + return Vector3(this.x.toDouble(), this.y.toDouble(), this.z.toDouble()) + } + +} + +typealias Vector3D = Vector3<Double> @@ -0,0 +1,4 @@ +package: com.dropbear.ffi.generated +headers: dropbear.h +headerFilter: dropbear.h +libraryPaths: src @@ -0,0 +1,9 @@ +package com.dropbear.ffi; + +public class JNINative { + static { + System.loadLibrary("eucalyptus_core"); + } + + public static native long getEntity(long handle, String label); +} @@ -0,0 +1,22 @@ +package com.dropbear.ffi + +actual class NativeEngine { + private var worldHandle: ULong = 0u + + actual fun getEntity(label: String): ULong? { + val result = JNINative.getEntity(worldHandle.toLong(), label) + return if (result < 0) { + null + } else { + result.toULong() + } + } + + fun init(handle: ULong) { + this.worldHandle = handle + if (this.worldHandle == 0uL) { + println("NativeEngine: Error - Invalid world handle received!") + return + } + } +} @@ -1,49 +0,0 @@ -package com.dropbear - -import com.dropbear.ffi.NativeEngine -import com.dropbear.math.Transform - -/** - * Main interface to the Dropbear game engine. - * - * This class provides high-level access to engine features for Kotlin scripts. - * It wraps the low-level NativeEngine JNI bindings with a more ergonomic API. - */ -class DropbearEngine { - /** - * Get the transform of the current entity. - * - * @return A Transform object that provides live access to position, rotation, and scale - * - * @example - * ```kotlin - * val transform = engine.getTransform() - * transform.position.y += 0.1 // Move up - * ``` - */ - fun getTransform(): Transform { - return Transform.fromNative() - } - - /** - * Fetches the currently active entity the script is attached to. - * - * If there is no entity this script is attached to, it will return - * a `null` in the form of a [Result] - */ - fun getActiveEntity(): Result<EntityRef> { - // This would need additional native support to get entity ID and label - return Result.failure(Exception("Function not fully implemented - requires entity label support")) - } - - /** - * Fetches an entity based on its label in the editor. - * - * If there is no entity under that label, it will return a - * `null` in the form of a [Result] - */ - fun getEntity(label: String): EntityRef? { - // This would need additional native support for entity lookup by label - return null - } -} @@ -1,18 +0,0 @@ -package com.dropbear - -import com.dropbear.math.Transform -import com.dropbear.math.Vector3D - -/** - * A class to hold a reference to an entity. - */ -class EntityRef(val label: String) { - fun getTransform(engine: DropbearEngine): Transform { - return engine.getTransform() - } - - fun setPosition(position: Vector3D, engine: DropbearEngine) { - val transform = engine.getTransform() - transform.position = position - } -} @@ -1,40 +0,0 @@ -package com.dropbear - -import com.dropbear.input.Input -import com.dropbear.input.KeyCode -import com.dropbear.math.Vector3D - -class Example : RunnableScript { - override var engine: DropbearEngine = DropbearEngine() - - private var speed = 0.1 - - override fun load() { - println("Example script loaded!") - } - - override fun update() { - val transform = engine.getTransform() - engine.getEntity("player")?.getTransform(engine) - - if (Input.isKeyPressed(KeyCode.W)) { - transform.translate(Vector3D(0.0, 0.0, -speed)) - } - if (Input.isKeyPressed(KeyCode.S)) { - transform.translate(Vector3D(0.0, 0.0, speed)) - } - if (Input.isKeyPressed(KeyCode.A)) { - transform.translate(Vector3D(-speed, 0.0, 0.0)) - } - if (Input.isKeyPressed(KeyCode.D)) { - transform.translate(Vector3D(speed, 0.0, 0.0)) - } - - if (Input.isKeyPressed(KeyCode.SPACE)) { - transform.translate(Vector3D(0.0, speed, 0.0)) - } - if (Input.isKeyPressed(KeyCode.SHIFT)) { - transform.translate(Vector3D(0.0, -speed, 0.0)) - } - } -} @@ -1,21 +0,0 @@ -package com.dropbear - -/** - * The basic interface that all classes implement for the class to be run. - */ -interface RunnableScript { - var engine: DropbearEngine - /** - * A function that is run once during the lifetime of the entity. - * - * It can be used to set initial properties such as health and more. - * - * ALl classes that implement RunnableScript need to implement the load function. - */ - fun load() - - /** - * A function that is run every frame during the lifetime of the entity. - */ - fun update() -} @@ -1,77 +0,0 @@ -package com.dropbear.ffi; - -/** - * Native interface to the Rust game engine. - * This class provides low-level FFI bindings via JNI. - * - * All methods are static and call into native Rust code. - */ -public class NativeEngine { - static { - // Load the native library - // In production, this should load from the JAR or a known location - try { - System.loadLibrary("eucalyptus_core"); - } catch (UnsatisfiedLinkError e) { - System.err.println("Failed to load native library: " + e.getMessage()); - } - } - - // ============================================================================= - // Context Management - // ============================================================================= - - /** - * Set the current scripting context (internal use only) - */ - public static native void setContext(long entityId, long worldPtr); - - // ============================================================================= - // Transform - Position - // ============================================================================= - - public static native double getPositionX(); - public static native double getPositionY(); - public static native double getPositionZ(); - public static native void setPosition(double x, double y, double z); - - // ============================================================================= - // Transform - Rotation (Quaternion) - // ============================================================================= - - public static native double getRotationX(); - public static native double getRotationY(); - public static native double getRotationZ(); - public static native double getRotationW(); - public static native void setRotation(double x, double y, double z, double w); - - // ============================================================================= - // Transform - Scale - // ============================================================================= - - public static native double getScaleX(); - public static native double getScaleY(); - public static native double getScaleZ(); - public static native void setScale(double x, double y, double z); - - // ============================================================================= - // Input System - // ============================================================================= - - /** - * Check if a key is currently pressed - * @param keycode The keycode to check (use KeyCode constants) - * @return true if the key is pressed, false otherwise - */ - public static native boolean isKeyPressed(long keycode); - - /** - * Get the current mouse X position - */ - public static native double getMouseX(); - - /** - * Get the current mouse Y position - */ - public static native double getMouseY(); -} @@ -1,11 +0,0 @@ -package com.dropbear.math - -typealias QuaternionD = Quaternion<Double> - -class Quaternion<T: Number>(var x: T, var y: T, var z: T, var w: T) { - companion object { - fun identity(): Quaternion<Double> { - return Quaternion(0.0, 0.0, 0.0, 1.0) - } - } -} @@ -1,116 +0,0 @@ -package com.dropbear.math - -import com.dropbear.ffi.NativeEngine - -/** - * A class that keeps all the values of a Transform, which consists of a - * position ([Vector3D]), rotation([QuaternionD]) and a scale([Vector3D]). - * - * This class provides direct access to transform data from the Rust engine. - */ -class Transform private constructor( - private var _position: Vector3D, - private var _rotation: QuaternionD, - private var _scale: Vector3D -) { - /** - * Get the position from the native engine - */ - var position: Vector3D - get() { - _position = Vector3D( - NativeEngine.getPositionX(), - NativeEngine.getPositionY(), - NativeEngine.getPositionZ() - ) - return _position - } - set(value) { - _position = value - NativeEngine.setPosition(value.x, value.y, value.z) - } - - /** - * Get the rotation from the native engine - */ - var rotation: QuaternionD - get() { - _rotation = Quaternion( - NativeEngine.getRotationX(), - NativeEngine.getRotationY(), - NativeEngine.getRotationZ(), - NativeEngine.getRotationW() - ) - return _rotation - } - set(value) { - _rotation = value - NativeEngine.setRotation(value.x, value.y, value.z, value.w) - } - - /** - * Get the scale from the native engine - */ - var scale: Vector3D - get() { - _scale = Vector3D( - NativeEngine.getScaleX(), - NativeEngine.getScaleY(), - NativeEngine.getScaleZ() - ) - return _scale - } - set(value) { - _scale = value - NativeEngine.setScale(value.x, value.y, value.z) - } - - companion object { - /** - * Creates a new Transform with everything to its default values. - * This creates a live Transform that reads from the native engine. - */ - fun default(): Transform { - return Transform( - Vector3D.zero(), - Quaternion.identity(), - Vector3D(1.0, 1.0, 1.0) - ) - } - - /** - * Creates a Transform from the current native engine state - */ - fun fromNative(): Transform { - val pos = Vector3D( - NativeEngine.getPositionX(), - NativeEngine.getPositionY(), - NativeEngine.getPositionZ() - ) - val rot = Quaternion( - NativeEngine.getRotationX(), - NativeEngine.getRotationY(), - NativeEngine.getRotationZ(), - NativeEngine.getRotationW() - ) - val scale = Vector3D( - NativeEngine.getScaleX(), - NativeEngine.getScaleY(), - NativeEngine.getScaleZ() - ) - return Transform(pos, rot, scale) - } - } - - /** - * Translate (move) the transform by a delta - */ - fun translate(delta: Vector3D) { - val current = position - position = Vector3D( - current.x + delta.x, - current.y + delta.y, - current.z + delta.z - ) - } -} @@ -1,29 +0,0 @@ -package com.dropbear.math - -/** - * A class for holding a vector of `3` values of the same type. - */ -class Vector3<T: Number>(var x: T, var y: T, var z: T) { - companion object { - /** - * Creates a new [com.dropbear.math.Vector3] of type `T` with one value. - */ - fun <T: Number> uniform(value: T): Vector3<T> { - return Vector3(value, value, value) - } - - /** - * Creates a [com.dropbear.math.Vector3] of type [Double] filled with only zeroes - */ - fun zero(): Vector3<Double> { - return Vector3(0.0, 0.0, 0.0) - } - } - - fun asDoubleVector(): Vector3<Double> { - return Vector3(this.x.toDouble(), this.y.toDouble(), this.z.toDouble()) - } - -} - -typealias Vector3D = Vector3<Double> @@ -0,0 +1,72 @@ +// File to get an idea of what is generated +@file:OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class) +import com.dropbear.DropbearEngine +import com.dropbear.EntityId +import com.dropbear.EntityRef +import com.dropbear.ProjectScriptingMetadata +import com.dropbear.ScriptRegistration +import com.dropbear.ffi.NativeEngine +import kotlinx.cinterop.COpaquePointer +import kotlinx.cinterop.ExperimentalForeignApi +import kotlin.experimental.ExperimentalNativeApi +import kotlin.native.CName + +// import /* CLASS */ + +private val scriptInstances: List<ScriptRegistration> by lazy { + Metadata().getScripts() +} + +class Metadata: ProjectScriptingMetadata { + override fun getScripts(): List<ScriptRegistration> { + return listOf( + ScriptRegistration( + tags = listOf("player"), + script = Player() + ), + +// ScriptRegistration( +// tags = /* TAGS */, +// script = /* CLASS NAME */ +// ), + + ) + } +} + +fun getDropbearEngine(worldPointer: COpaquePointer?, currentEntity: Long?): DropbearEngine { + val nativeEngine = NativeEngine() + nativeEngine.init(worldPointer) + val dropbearEngine = DropbearEngine(nativeEngine, if (currentEntity == null) null else EntityRef(EntityId(currentEntity.toULong()))) + return dropbearEngine +} + +@CName("dropbear_load") +fun loadScriptByTag(worldPointer: COpaquePointer?, currentEntity: Long?, tag: String?) { + if (tag == null) return + val scripts = scriptInstances.filter { it.tags.contains(tag) } + val engine = getDropbearEngine(worldPointer, currentEntity) + for (script in scripts) { + script.script.load(engine) + } +} + +@CName("dropbear_update") +fun updateScriptByTag(worldPointer: COpaquePointer?, currentEntity: Long?, tag: String?, deltaTime: Double) { + if (tag == null) return + val scripts = scriptInstances.filter { it.tags.contains(tag) } + val engine = getDropbearEngine(worldPointer, currentEntity) + for (script in scripts) { + script.script.update(engine, deltaTime.toFloat()) + } +} + +@CName("dropbear_destroy") +fun destroyScriptByTag(worldPointer: COpaquePointer?, currentEntity: Long?, tag: String?) { + if (tag == null) return + val scripts = scriptInstances.filter { it.tags.contains(tag) } + val engine = getDropbearEngine(worldPointer, currentEntity) + for (script in scripts) { + script.script.destroy(engine) + } +} @@ -0,0 +1,35 @@ +@file:OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class) +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + +package com.dropbear.ffi + +import co.touchlab.kermit.Logger +import com.dropbear.ffi.generated.dropbear_get_entity +import kotlinx.cinterop.* +import kotlin.experimental.ExperimentalNativeApi + +actual class NativeEngine { + private var worldHandle: COpaquePointer? = null + + fun init(handle: COpaquePointer?) { + this.worldHandle = handle + if (this.worldHandle == null) { + Logger.i("NativeEngine: Error - Invalid world handle received!") + } else { + Logger.i("NativeEngine: Initialized with world handle") + } + } + + actual fun getEntity(label: String): ULong? { + val world = worldHandle ?: return null + memScoped { + val outEntity = alloc<ULongVar>() + val result = dropbear_get_entity( + label = label, + world_ptr = world.reinterpret(), + out_entity = outEntity.ptr + ) + return if (result == 0) outEntity.value else null + } + } +}