tirbofish/dropbear · diff
added docs with dokka, added publishing (mavenLocal) because fuck dealing with namespacing. worked a bunch on native functions and rust.
also got native library linking working, a better gradle project and *soon to be* some more updated UI to allow for better script attachment.
a lot of stuff a broken, but thats an issue for later me.
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 }}' @@ -53,6 +53,6 @@ Cargo.lock .idea/ -src/*.dll -src/*.dylib -src/*.so +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> @@ -1,24 +0,0 @@ -<component name="ProjectRunConfigurationManager"> - <configuration default="false" name="generateJniHeaders" 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="generateJniHeaders" /> - </list> - </option> - <option name="vmOptions" /> - </ExternalSystemSettings> - <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess> - <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess> - <DebugAllEnabled>false</DebugAllEnabled> - <RunAsTest>false</RunAsTest> - <method v="2" /> - </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)_ @@ -44,6 +45,7 @@ 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 ``` @@ -1,6 +1,8 @@ plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.kotlinxSerialization) + `maven-publish` + id("org.jetbrains.dokka") version "2.0.0" } group = "com.dropbear" @@ -10,6 +12,15 @@ repositories { mavenCentral() } +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath("io.github.cdimascio:dotenv-kotlin:6.4.1") + } +} + kotlin { jvm() @@ -25,6 +36,27 @@ kotlin { 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 { @@ -44,7 +76,7 @@ kotlin { sourceSets { commonMain { dependencies { - implementation("co.touchlab:kermit:2.0.4") + api("co.touchlab:kermit:2.0.4") } } nativeMain { @@ -87,4 +119,29 @@ tasks.register<JavaCompile>("generateJniHeaders") { } 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"]) + } + } } @@ -32,9 +32,11 @@ jni.workspace = true 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 @@ -1,47 +1,92 @@ -mod kmp; -mod jni; +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 std::sync::LazyLock; -use crate::ptr::SafePointer; +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"); -pub struct ScriptManager; +#[derive(Default)] +pub enum ScriptTarget { + #[default] + None, + JVM { library_path: PathBuf }, + Native { library_path: PathBuf }, +} + +pub struct ScriptManager { + jvm: Option<JavaContext>, + library: Option<Library>, + script_target: ScriptTarget, + entity_tag_database: HashMap<String, Vec<Entity>>, +} impl ScriptManager { pub fn new() -> anyhow::Result<Self> { - Ok(Self) - // Err(anyhow::anyhow!("it aint ready yet bozo")) + 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, + 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, - tags: Vec<String>, + tag: String, world: &mut World, input_state: &InputState, ) -> anyhow::Result<()> { + #[cfg(feature = "jvm")] + { + if let Some(jvm) = &mut self.jvm { + jvm.init(world)?; + } + } + Err(anyhow::anyhow!("it aint ready yet bozo")) } - pub fn update_entity_script( + pub fn update_script( &mut self, entity_id: hecs::Entity, - tags: Vec<String>, world: &mut World, input_state: &InputState, dt: f32, @@ -1,2 +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) +} @@ -1,28 +1,48 @@ //! Deals with Kotlin/Native library loading for different platforms. -use std::ffi::c_char; -use hecs::World; +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 extern "C" fn dropbear_get_entity(label: *const c_char, world_ptr: *const World) -> usize { - if world_ptr.is_null() { - log::debug!("World pointer is null"); - return 0; +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; } - unsafe { - let world = &*world_ptr; + let world = unsafe { &*world_ptr }; - for (id, entity) in world.query::<&AdoptedEntity>().iter() { - if let Ok(label) = std::ffi::CStr::from_ptr(label).to_str() - && entity.model.label == label - { - log::debug!("Found entity with label: {}", label); - return id.id() as usize; + 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; } } - log::warn!("Entity with label: {} not found", std::ffi::CStr::from_ptr(label).to_str().unwrap()); - 0 + }; + + 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, @@ -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}; @@ -448,91 +448,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) => { @@ -119,9 +119,8 @@ 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.clone(), &mut self.world, &self.input_state, dt, @@ -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,6 +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::ScriptTarget; use eucalyptus_core::spawn::{push_pending_spawn, PendingSpawn}; use crate::editor::{ComponentType, Editor, EditorState, EntityType, PendingSpawn2, Signal, UndoableAction}; @@ -125,38 +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 for entity {:?} with tags: {:?}", - entity_id, - script.tags + "Initialising script for tag {:?} with entities: {:?}", + tag, + entities ); - match self.script_manager.load_script() { - Ok(_) => { - if let Err(e) = self.script_manager.init_entity_script( - entity_id, - script.tags.clone(), - &mut self.world, - &self.input_state, - ) { - log::warn!( - "Failed to initialise script for entity {:?}: {}", - entity_id, - e - ); - self.signal = Signal::StopPlaying; - } else { - success_without_console!( + 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)); + } 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 for {:?} with tags {:?} because {}", entity_id, script.tags, e); + log::info!("You are in play mode now! Press Escape to exit"); } } } @@ -454,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 +kotlin.code.style=official +org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled +org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true @@ -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,13 +1,11 @@ package com.dropbear import com.dropbear.ffi.NativeEngine -import getProjectScriptMetadata -class DropbearEngine(val native: NativeEngine) { +class DropbearEngine(val native: NativeEngine, val currentEntity: EntityRef? = null) { private val globalScripts = mutableListOf<System>() fun init() { - val scriptRegistration = getProjectScriptMetadata() } fun getEntity(label: String): Result<EntityRef> { @@ -18,8 +18,5 @@ import com.dropbear.math.Vector3D * by [EntityRef], which will require a reference to [DropbearEngine] to push the commands. */ class EntityRef(val id: EntityId = EntityId(0u)) { - /** - * Sets the position of the entity by a Vector - */ fun setPosition(position: Vector3D, engine: DropbearEngine) {} } @@ -2,12 +2,10 @@ import com.dropbear.DropbearEngine import com.dropbear.EntityId import com.dropbear.System import com.dropbear.ProjectScriptingMetadata +import com.dropbear.Runnable import com.dropbear.ScriptRegistration -fun playerMovement(engine: DropbearEngine, entityId: EntityId, deltaTime: Double) { - -} - +@Runnable(["player"]) class Player: System { override fun load(engine: DropbearEngine) { TODO("Not yet implemented") @@ -20,17 +18,4 @@ class Player: System { override fun destroy(engine: DropbearEngine) { TODO("Not yet implemented") } -} - -class Metadata : ProjectScriptingMetadata { - override fun getScripts(): List<ScriptRegistration> { - return listOf ( - ScriptRegistration( - tags = listOf("player", "movement"), - script = Player() - ), - ) - } -} - -fun getProjectScriptMetadata(): ProjectScriptingMetadata = Metadata() +} @@ -1,16 +1,5 @@ package com.dropbear -/** - * A mandatory class defined in a eucalyptus project that "discovers" and attaches - * tags to files for the registry to use. - */ interface ProjectScriptingMetadata { - /** - * A function that is placed in Manifest.kt, and allows you to attach the required - * tags to the correct files. - * - * The original method was to use Kotlin Symbol Processing, however Kotlin/Native - * doesn't support KSP. - */ fun getScripts(): List<ScriptRegistration> } @@ -1,17 +0,0 @@ -package com.dropbear - -internal object Registry { - private val _entries = mutableListOf<ScriptEntry>() - val entries: List<ScriptEntry> get() = _entries.toList() - - fun register(entry: ScriptEntry) { - _entries.add(entry) - } - - fun findByTag(tag: String): List<ScriptEntry> { - return _entries.filter { tag in it.tags } - } - - internal fun registerAll() { - } -} @@ -0,0 +1,5 @@ +package com.dropbear + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.SOURCE) +annotation class Runnable(val tags: Array<String> = []) @@ -1,20 +0,0 @@ -package com.dropbear - -/** - * The basic interface that all classes implement for the class to be run. - */ -sealed class RunnableScript { - /** - * 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. - */ - abstract fun load() - - /** - * A function that is run every frame during the lifetime of the entity. - */ - abstract fun update() -} @@ -1,7 +0,0 @@ -package com.dropbear - -internal data class ScriptEntry( - val tags: List<String>, - val functionName: String, - val invoker: (DropbearEngine, EntityId, Double) -> Unit -) @@ -3,6 +3,5 @@ package com.dropbear.ffi import com.dropbear.EntityRef expect class NativeEngine { - fun init(handle: ULong) fun getEntity(label: String): ULong? } @@ -1,5 +1,9 @@ package com.dropbear.ffi; public class JNINative { - public native long getEntity(long handle, String label); + static { + System.loadLibrary("eucalyptus_core"); + } + + public static native long getEntity(long handle, String label); } @@ -2,10 +2,9 @@ package com.dropbear.ffi actual class NativeEngine { private var worldHandle: ULong = 0u - private val jni = JNINative() actual fun getEntity(label: String): ULong? { - val result = jni.getEntity(worldHandle.toLong(), label) + val result = JNINative.getEntity(worldHandle.toLong(), label) return if (result < 0) { null } else { @@ -13,7 +12,7 @@ actual class NativeEngine { } } - actual fun init(handle: ULong) { + fun init(handle: ULong) { this.worldHandle = handle if (this.worldHandle == 0uL) { println("NativeEngine: Error - Invalid world handle received!") @@ -1,25 +0,0 @@ -@file:OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class) - -package com.dropbear.ffi - -import co.touchlab.kermit.Logger -import com.dropbear.DropbearEngine -import kotlinx.cinterop.ExperimentalForeignApi -import kotlin.experimental.ExperimentalNativeApi - -@CName("dropbear_entry") -fun entry(worldHandle: ULong) { - Logger.i { "Starting kotlin scripting guest" } - val nativeEngine = NativeEngine() - nativeEngine.init(worldHandle) -} - -@CName("dropbear_load") -fun loadScriptByTag(tag: String?) { - -} - -@CName("dropbear_update") -fun updateScriptByTag(tag: String?, deltaTime: 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) + } +} @@ -4,23 +4,32 @@ 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: ULong = 0u + private var worldHandle: COpaquePointer? = null - actual fun init(handle: ULong) { + fun init(handle: COpaquePointer?) { this.worldHandle = handle - if (this.worldHandle == 0uL) { + if (this.worldHandle == null) { Logger.i("NativeEngine: Error - Invalid world handle received!") - return } else { - Logger.i("NativeEngine: Initialized with world handle: ${this.worldHandle}") + Logger.i("NativeEngine: Initialized with world handle") } } actual fun getEntity(label: String): ULong? { - TODO("Not yet implemented") + 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 + } } }