From de914613e8f67e6f659c0165aebdba6c6e0b5265 Mon Sep 17 00:00:00 2001 From: Jamie Wells Date: Wed, 14 Oct 2026 01:25:39 +0000 Subject: [PATCH] Add scene object, global service and Explorer file API paths across SDKs --- .../crates/engine-play/src/script_runtime.rs | 201 ++++++++++++- Engine/crates/engine-scripting/src/cpp_sdk.rs | 209 ++++++++++++- .../crates/engine-scripting/src/javascript.rs | 30 +- Engine/crates/engine-scripting/src/lua.rs | 46 ++- .../crates/engine-scripting/src/sdk/Rustic.cs | 226 +++++++++++++- .../engine-scripting/src/sdk/Rustic.java | 277 +++++++++++++++++- .../engine-scripting/src/sdk/game-api.d.ts | 29 ++ .../engine-scripting/src/sdk/game-api.lua | 26 ++ .../engine-scripting/src/sdk/gameplay.js | 202 ++++++++++++- .../engine-scripting/src/sdk/gameplay.lua | 220 ++++++++++++++ .../crates/engine-scripting/src/sdk/rustic.h | 116 +++++++- .../engine-scripting/src/sdk/rustic.luau | 13 +- .../engine-scripting/src/sdk/rustic.php | 53 +++- .../crates/engine-scripting/src/sdk/rustic.py | 220 ++++++++++++++ .../crates/engine-scripting/src/workspace.rs | 2 + Engine/docs/Scripting/apiPaths.md | 194 ++++++++++++ Engine/docs/Scripting/gameplayActions.md | 4 + Engine/docs/Scripting/sceneObjects.md | 4 + Engine/docs/Scripting/scriptingWeb.md | 4 +- 19 files changed, 2041 insertions(+), 35 deletions(-) create mode 100644 Engine/crates/engine-scripting/src/sdk/game-api.d.ts create mode 100644 Engine/crates/engine-scripting/src/sdk/game-api.lua create mode 100644 Engine/docs/Scripting/apiPaths.md diff --git a/Engine/crates/engine-play/src/script_runtime.rs b/Engine/crates/engine-play/src/script_runtime.rs index 3555c2c6..80968ea2 100644 --- a/Engine/crates/engine-play/src/script_runtime.rs +++ b/Engine/crates/engine-play/src/script_runtime.rs @@ -962,6 +962,15 @@ impl GameplayHost for RuntimeHost { .get("op") .and_then(serde_json::Value::as_str) .ok_or("query operation is missing")?; + if op == "scene_object" { + let source = query + .get("source") + .and_then(serde_json::Value::as_str) + .ok_or("scene object source is missing")?; + return Ok(serde_json::Value::String( + self.object_entity(source)?.to_string(), + )); + } if crate::entity_services::handles(op) { let mut world = self.world.lock().map_err(|_| "world lock poisoned")?; return crate::entity_services::query(&mut world, query); @@ -1646,7 +1655,7 @@ impl GameplayHost for RuntimeHost { .map_err(|error| error.to_string()) } fn find_entity(&self, query: &str) -> Result, String> { - if query.starts_with("rustic.game.") { + if query.starts_with("rustic.game.") || query.starts_with("game.scene.") { return self.object_entity(query).map(Some); } let world = self @@ -1689,6 +1698,7 @@ impl GameplayHost for RuntimeHost { paths.insert(path.clone(), id); paths.insert(format!("Game.scene.{path}"), id); paths.insert(format!("rustic.game.{}.{path}", self.scene_name), id); + paths.insert(format!("game.scene.{}.{path}", self.scene_name), id); } } paths @@ -1956,7 +1966,10 @@ impl GameplayHost for RuntimeHost { impl RuntimeHost { fn object_entity(&self, source: &str) -> Result { - if let Some(rest) = source.strip_prefix("rustic.game.") { + if let Some(rest) = source + .strip_prefix("game.scene.") + .or_else(|| source.strip_prefix("rustic.game.")) + { let prefix = format!("{}.", self.scene_name); let path = rest.strip_prefix(&prefix).ok_or_else(|| { format!( @@ -1986,6 +1999,9 @@ impl RuntimeHost { } fn resolve_entity(&self, source: &str) -> Result, String> { + if source.starts_with("game.scene.") || source.starts_with("rustic.game.") { + return self.object_entity(source).map(Some); + } let world = self .world .lock() @@ -2309,14 +2325,15 @@ mod tests { br#" local crash return {Start=function() - local ok,err=pcall(function() rustic.game.assets.sounds["crash.wav"]:play() end) + local ok,err=pcall(function() game.rustic.files.assets.sounds["crash.wav"]:play() end) assert(not ok and string.find(err,'load',1,true)) assert(not pcall(function() Audio.load('assets/missing.wav') end)) - assert(not pcall(function() rustic.game.assets["../outside.wav"]:load() end)) - local asset=rustic.game.assets.sounds["crash.wav"] + assert(not pcall(function() game.rustic.files.assets["../outside.wav"]:load() end)) + local asset=game.rustic.files.assets.sounds["crash.wav"] crash=asset:load() assert(crash:load()==crash) - asset:play({loop=true}) + assert(game.rustic.files.assets.sounds['crash.wav']:load()==crash) + game.rustic.files.assets.sounds['crash.wav']:play({loop=true}) crash:Unload() assert(not pcall(function() crash:play() end)) assert(crash:load()==crash) @@ -2648,6 +2665,8 @@ mod tests { for source in [ "rustic.game.Other.Room.Player", "rustic.game.Demo.Room.Missing", + "game.scene.Other.Room.Player", + "game.scene.Demo.Room.Missing", ] { assert!( host.edit_object_attribute(source, "Anchored", EngineValue::Boolean(false)) @@ -2677,6 +2696,15 @@ mod tests { .unwrap(), Some(EngineValue::String("Renamed".into())) ); + assert_eq!( + host.object_attribute("game.scene.Demo.Room.Renamed", "Name") + .unwrap(), + Some(EngineValue::String("Renamed".into())) + ); + assert!( + host.object_attribute("game.scene.Demo.Room.Player", "Name") + .is_err() + ); let duplicate = EntitySnapshot { name: Some("Renamed".into()), parent: target.parent, @@ -2692,6 +2720,11 @@ mod tests { .unwrap_err() .contains("ambiguous") ); + assert!( + host.object_attribute("game.scene.Demo.Room.Renamed", "Name") + .unwrap_err() + .contains("ambiguous") + ); } #[test] @@ -4221,4 +4254,160 @@ run({'on_start':start}) .any(|(_, m)| m == "value 2.0" || m == "value 2") ); } + + #[test] + #[allow( + clippy::too_many_lines, + clippy::float_cmp, + reason = "Complete cross-language fixtures compare exactly representable scene values" + )] + fn canonical_api_paths_target_live_objects_in_all_available_sdks() { + let fixtures: &[(ScriptLanguage, &str, &[u8])] = &[ + (ScriptLanguage::Lua54,"paths.lua",br"return {Start=function() + game.scene.Demo.Room.Player:EditAttribute('Name','Renamed') + assert(game.rustic:groupsCreate('selected')) + assert(game.scene.Demo.Room.Renamed:groupsAdd('selected')) + game.scene.Demo.Room.Renamed:tween('Position',{8,0,0},2,'InQuad') + game.rustic:after(0.5,function() game.rustic:log('info','timer') end) + game.rustic.files.assets['night.hdr']:setSkyTexture() +end}"), + (ScriptLanguage::Luau,"paths.luau",br"return {Start=function() + game.scene.Demo.Room.Player:EditAttribute('Name','Renamed') + assert(game.rustic:groupsCreate('selected')) + assert(game.scene.Demo.Room.Renamed:groupsAdd('selected')) + game.scene.Demo.Room.Renamed:tween('Position',{8,0,0},2,'InQuad') + game.rustic:after(0.5,function() game.rustic:log('info','timer') end) + game.rustic.files.assets['night.hdr']:setSkyTexture() +end}"), + (ScriptLanguage::JavaScript,"paths.js",br"globalThis.behavior={Start(){ + game.scene.Demo.Room.Player.EditAttribute('Name','Renamed'); + game.rustic.groupsCreate('selected'); + game.scene.Demo.Room.Renamed.groupsAdd('selected'); + game.scene.Demo.Room.Renamed.tween('Position',[8,0,0],2,'InQuad'); + game.rustic.after(0.5,()=>game.rustic.log('info','timer')); + game.rustic.files.assets['night.hdr'].setSkyTexture(); +}};"), + (ScriptLanguage::Web,"paths.html",br""), + (ScriptLanguage::Python,"paths.py",br"from rustic import game,run +def on_start(): + game.scene.Demo.Room.Player.EditAttribute('Name','Renamed') + assert game.rustic.groupsCreate('selected') + assert game.scene.Demo.Room.Renamed.groupsAdd(name='selected') + game.scene.Demo.Room.Renamed.tween(property='Position',to=[8,0,0],duration=2,easing='InQuad') + game.rustic.after(0.5,lambda:game.rustic.log('info','timer')) + game.rustic.files.assets['night.hdr'].setSkyTexture() +run(globals())"), + (ScriptLanguage::Cpp,"paths.cpp",br#"#include "rustic.hpp" +void start(){game.scene["Demo"]["Room"]["Player"].EditAttribute("Name","Renamed");game.rustic.groupsCreate("selected");game.scene["Demo"]["Room"]["Renamed"].groupsAdd("selected");game.scene["Demo"]["Room"]["Renamed"].tween("Position",RusticValue::Array{8,0,0},2.0,"InQuad");game.rustic.after(0.5,[]{game.rustic.log("info","timer");});game.rustic.files["assets"]["night.hdr"].setSkyTexture();} +int main(){return rustic_run(RusticBehavior{.on_start=start});}"#), + (ScriptLanguage::C,"paths.c",br#"#include "rustic.h" +void timer(void){game.rustic.log("info","timer");} +void start(void){RusticScenePath original=game.scene("Demo","Room.Player");original.EditAttribute(original,"Name",(RusticValue){.type=RUSTIC_STRING,.string="Renamed"});RusticScenePath player=game.scene("Demo","Room.Renamed");game.rustic.groupsCreate("selected");player.groupsAdd(player,"selected");player.tween(player,"Position",(RusticValue){.type=RUSTIC_VECTOR,.vector={8,0,0},.length=3},2,"InQuad",NULL);game.rustic.after(0.5,timer);RusticFilePath sky=game.rustic.files("assets/night.hdr");sky.setSkyTexture(sky);} +int main(void){return rustic_run((RusticBehavior){.on_start=start});}"#), + (ScriptLanguage::CSharp,"paths.cs",br#"using static Rustic; +Run((callback,dt)=>{if(callback=="on_start"){game.scene["Demo"]["Room"]["Player"].EditAttribute("Name","Renamed");game.rustic.groupsCreate("selected");game.scene["Demo"]["Room"]["Renamed"].groupsAdd("selected");game.scene["Demo"]["Room"]["Renamed"].tween("Position",new double[]{8,0,0},2,"InQuad");game.rustic.after(0.5,()=>game.rustic.log("info","timer"));game.rustic.files["assets"]["night.hdr"].setSkyTexture();}});"#), + (ScriptLanguage::Java,"RusticBehavior.java",br#"class RusticBehavior extends Rustic { +public static void main(String[]args)throws Exception{run((callback,dt)->{if(callback.equals("on_start")){game.scene.scene("Demo").object("Room.Player").EditAttribute("Name","Renamed");game.rustic.groupsCreate("selected");game.scene.scene("Demo").object("Room.Renamed").groupsAdd("selected");game.scene.scene("Demo").object("Room.Renamed").tween("Position",new double[]{8,0,0},2,"InQuad");game.rustic.after(0.5,()->game.rustic.log("info","timer"));game.rustic.files.object("assets/night.hdr").setSkyTexture();}});}}"#), + (ScriptLanguage::Php,"paths.php",br#"scene->Demo->Room->Player->EditAttribute("Name","Renamed");$game->rustic->groupsCreate("selected");$game->scene->Demo->Room->Renamed->groupsAdd(name:"selected");$game->scene->Demo->Room->Renamed->tween("Position",[8,0,0],2,"InQuad");$game->rustic->after(0.5,fn()=>$game->rustic->log("info","timer"));$game->rustic->files->assets->{"night.hdr"}->setSkyTexture();} +rustic_run(["on_start"=>"on_start"]);"#), + ]; + for (language, name, source) in fixtures { + if !matches!(language, ScriptLanguage::Lua54 | ScriptLanguage::JavaScript) + && !engine_scripting::probe_language_toolchain(*language).available + { + assert!( + std::env::var_os("RUSTIC_REQUIRE_ALL_SDKS").is_none(), + "{} toolchain is required", + language.display_name() + ); + continue; + } + eprintln!("Checking canonical API paths: {}", language.display_name()); + let room = EntitySnapshot { + name: Some("Room".into()), + ..Default::default() + }; + let player = EntitySnapshot { + name: Some("Player".into()), + parent: Some(room.id), + ..Default::default() + }; + let controller = EntitySnapshot { + name: Some("Controller".into()), + ..Default::default() + }; + let mut scene = SceneDocument::new("Demo"); + scene.entities = vec![room, player.clone(), controller.clone()]; + let world = Arc::new(Mutex::new(scene.create_world().unwrap())); + let gameplay = Arc::new(Mutex::new(GameplayContext::default())); + let logs = Arc::new(Mutex::new(VecDeque::new())); + let host = RuntimeHost { + gameplay: Arc::clone(&gameplay), + owner_script: ScriptId::new(), + entity_bound: true, + scene_name: "Demo".into(), + world: Arc::clone(&world), + entity: controller.id, + input_keys: Arc::new(Mutex::new(BTreeSet::new())), + snapshot_root: PathBuf::new(), + properties: Arc::new(Mutex::new(BTreeMap::new())), + logs: Arc::clone(&logs), + dropped_logs: Arc::new(Mutex::new(0)), + enabled: true, + }; + let mut behavior = + RuntimeBehavior::load(*language, ScriptId::new(), source, name, Box::new(host)) + .unwrap_or_else(|e| panic!("{} load: {e}", language.display_name())); + behavior + .on_start() + .unwrap_or_else(|e| panic!("{} Start: {e}", language.display_name())); + { + let mut context = gameplay.lock().unwrap(); + let mut live = world.lock().unwrap(); + context + .runtime + .tick( + 1.0, + &mut crate::gameplay_world::GameplayScene( + &mut live, + &mut engine_core::gameplay::audio::Mixer::default(), + ), + ) + .unwrap(); + assert_eq!( + live.local_transform(player.id).unwrap().translation.x, + 2.0, + "{}", + language.display_name() + ); + assert_eq!( + live.local_transform(controller.id) + .unwrap() + .translation + .to_array(), + [0.; 3] + ); + assert_eq!(live.environment().sky_image, "assets/night.hdr"); + assert_eq!(live.group_members("selected").unwrap(), vec![player.id]); + } + behavior.update(1.0).unwrap(); + assert!( + logs.lock() + .unwrap() + .iter() + .any(|(_, message)| message == "timer"), + "{} timer", + language.display_name() + ); + } + } } diff --git a/Engine/crates/engine-scripting/src/cpp_sdk.rs b/Engine/crates/engine-scripting/src/cpp_sdk.rs index 23d97112..fbf78382 100644 --- a/Engine/crates/engine-scripting/src/cpp_sdk.rs +++ b/Engine/crates/engine-scripting/src/cpp_sdk.rs @@ -93,6 +93,7 @@ class RusticApi { public: ObjectPath game{&commands}; RusticValue query(RusticValue request){std::cout<clear();std::string line;if(!std::getline(std::cin,line))throw std::runtime_error("query host exited");auto response=rustic_detail::Parser(line).parse();auto& object=response.object();if(auto error=object.find("error");error!=object.end())throw std::runtime_error(error->second.string());return response.at("result");} + void edit_object_attribute(const std::string& source,const std::string& name,RusticValue value){commands->push_back(rustic_detail::object({{"op","edit_attribute"},{"source",source},{"name",name},{"value",std::move(value)}}));} void gameplay(RusticValue request){commands->push_back(rustic_detail::object({{"op","gameplay"},{"request",std::move(request)}}));} RusticValue gameplay_connections()const{return state->at("gameplay_connections");} RusticValue gameplay_states()const{return state->at("gameplay_states");} @@ -140,7 +141,7 @@ struct Handle { Handle& control(const char* command){rustic.gameplay(rustic_detail::object({{"command",command},{"handle",id}}));if(std::string(command)=="cancel")release();return *this;} Handle& pause(){return control("pause");}Handle& resume(){return control("resume");}Handle& cancel(){return control("cancel");}Handle& reverse(){return control("reverse");} }; -inline std::string entity(const std::string& id){if(id.empty())return rustic.entity_id();return Game.scene.Find(id).value_or(id);} +inline std::string entity(const std::string& id){if(id.empty())return rustic.entity_id();if(id.starts_with("game.scene."))return rustic.query(rustic_detail::object({{"op","scene_object"},{"source",id}})).string();return Game.scene.Find(id).value_or(id);} inline std::shared_ptr start(RusticValue action,bool looping=false,int repeats=0,bool pingPong=false){ auto h=std::make_shared();h->id="gameplay-"+std::to_string(++serial);handles[h->id]=h;collect(action,h->tokens); rustic.gameplay(rustic_detail::object({{"command","start"},{"handle",h->id},{"action",std::move(action)},{"on_finished",h->id},{"playback",rustic_detail::object({{"looping",looping},{"repeats",repeats},{"ping_pong",pingPong}})}}));return h; @@ -344,5 +345,211 @@ inline std::vector find_in_radius(RusticVector3 center,double radiu } struct RusticBehavior {std::function on_create={},on_start={},on_enable={},on_disable={},on_destroy={},on_stop={};std::function fixed_update={},update={};}; + +// Canonical API paths. +struct CanonicalScenePath { + std::string source; + void clone(std::optional parent={})const{::instance.clone(source,parent);} + void add(std::optional parent={})const{::instance.add(source,parent);} + void setCurrentCamera()const{::Game.setCurrentCamera(source);} + void emit(const std::string& name,RusticValue::Array arguments)const{::Events::emit(name,std::move(arguments),source);} + CanonicalScenePath operator[](const std::string& name)const{return {source+"."+name};} + operator std::string()const{return source;} + template decltype(auto) tweenTo(A&&... args)const{return ::Tween::to(source,std::forward(args)...);} + template decltype(auto) tweenMove(A&&... args)const{return ::Tween::move(source,std::forward(args)...);} + template decltype(auto) tweenRotate(A&&... args)const{return ::Tween::rotate(source,std::forward(args)...);} + template decltype(auto) tweenScale(A&&... args)const{return ::Tween::scale(source,std::forward(args)...);} + template decltype(auto) tweenAction(A&&... args)const{return ::Tween::action(source,std::forward(args)...);} + template decltype(auto) movementMoveTo(A&&... args)const{return ::Movement::moveTo(source,std::forward(args)...);} + template decltype(auto) movementMove(A&&... args)const{return ::Movement::move(source,std::forward(args)...);} + template decltype(auto) movementRotateTo(A&&... args)const{return ::Movement::rotateTo(source,std::forward(args)...);} + template decltype(auto) movementLookAt(A&&... args)const{return ::Movement::lookAt(source,std::forward(args)...);} + template decltype(auto) movementFollow(A&&... args)const{return ::Movement::follow(source,std::forward(args)...);} + template decltype(auto) movementOrbit(A&&... args)const{return ::Movement::orbit(source,std::forward(args)...);} + template decltype(auto) pathFollow(A&&... args)const{return ::Path::follow(source,std::forward(args)...);} + template decltype(auto) effectsFade(A&&... args)const{return ::Effects::fade(source,std::forward(args)...);} + template decltype(auto) effectsFlash(A&&... args)const{return ::Effects::flash(source,std::forward(args)...);} + template decltype(auto) effectsShake(A&&... args)const{return ::Effects::shake(source,std::forward(args)...);} + template decltype(auto) effectsPulse(A&&... args)const{return ::Effects::pulse(source,std::forward(args)...);} + template decltype(auto) cameraZoom(A&&... args)const{return ::Camera::zoom(source,std::forward(args)...);} + template decltype(auto) cameraFov(A&&... args)const{return ::Camera::fov(source,std::forward(args)...);} + template decltype(auto) cameraTransition(A&&... args)const{return ::Camera::transition(source,std::forward(args)...);} + template decltype(auto) physicsImpulse(A&&... args)const{return ::Physics::impulse(source,std::forward(args)...);} + template decltype(auto) physicsForce(A&&... args)const{return ::Physics::force(source,std::forward(args)...);} + template decltype(auto) physicsLaunch(A&&... args)const{return ::Physics::launch(source,std::forward(args)...);} + template decltype(auto) physicsKnockback(A&&... args)const{return ::Physics::knockback(source,std::forward(args)...);} + template decltype(auto) animationRegisterClip(A&&... args)const{return ::Animation::registerClip(source,std::forward(args)...);} + template decltype(auto) animationClips(A&&... args)const{return ::Animation::clips(source,std::forward(args)...);} + template decltype(auto) animationAction(A&&... args)const{return ::Animation::action(source,std::forward(args)...);} + template decltype(auto) animationPlay(A&&... args)const{return ::Animation::play(source,std::forward(args)...);} + template decltype(auto) animationBlend(A&&... args)const{return ::Animation::blend(source,std::forward(args)...);} + template decltype(auto) animationTransition(A&&... args)const{return ::Animation::transition(source,std::forward(args)...);} + template decltype(auto) animationIk(A&&... args)const{return ::Animation::ik(source,std::forward(args)...);} + template decltype(auto) animationFootPlacement(A&&... args)const{return ::Animation::footPlacement(source,std::forward(args)...);} + template decltype(auto) animationLookAt(A&&... args)const{return ::Animation::lookAt(source,std::forward(args)...);} + template decltype(auto) animationHeadTracking(A&&... args)const{return ::Animation::headTracking(source,std::forward(args)...);} + template decltype(auto) animationRecoil(A&&... args)const{return ::Animation::recoil(source,std::forward(args)...);} + template decltype(auto) animationAddMarker(A&&... args)const{return ::Animation::addMarker(source,std::forward(args)...);} + template decltype(auto) eventsConnect(A&&... args)const{return ::Events::connect(source,std::forward(args)...);} + template decltype(auto) tagsAdd(A&&... args)const{return ::Tags::add(source,std::forward(args)...);} + template decltype(auto) tagsRemove(A&&... args)const{return ::Tags::remove(source,std::forward(args)...);} + template decltype(auto) groupsAdd(First&& first,A&&... args)const{return ::Groups::add(std::forward(first),source,std::forward(args)...);} + template decltype(auto) hierarchySetParent(A&&... args)const{return ::Hierarchy::set_parent(source,std::forward(args)...);} + template decltype(auto) hierarchyGetChildren(A&&... args)const{return ::Hierarchy::get_children(source,std::forward(args)...);} + template decltype(auto) hierarchyDetach(A&&... args)const{return ::Hierarchy::detach(source,std::forward(args)...);} + template decltype(auto) entityDestroy(A&&... args)const{return ::Entity::destroy(source,std::forward(args)...);} + template decltype(auto) entityDestroyAfter(A&&... args)const{return ::Entity::destroy_after(source,std::forward(args)...);} + template decltype(auto) entityIsValid(A&&... args)const{return ::Entity::is_valid(source,std::forward(args)...);} + template decltype(auto) componentsHas(A&&... args)const{return ::Components::has(source,std::forward(args)...);} + template decltype(auto) componentsGetAll(A&&... args)const{return ::Components::get_all(source,std::forward(args)...);} + template decltype(auto) componentsFindInChildren(A&&... args)const{return ::Components::find_in_children(source,std::forward(args)...);} + template decltype(auto) tween(A&&... args)const{return ::Tween::to(source,std::forward(args)...);} + template decltype(auto) moveTo(A&&... args)const{return ::Movement::moveTo(source,std::forward(args)...);} + template decltype(auto) move(A&&... args)const{return ::Movement::move(source,std::forward(args)...);} + template decltype(auto) rotateTo(A&&... args)const{return ::Movement::rotateTo(source,std::forward(args)...);} + template decltype(auto) lookAt(A&&... args)const{return ::Movement::lookAt(source,std::forward(args)...);} + template decltype(auto) follow(A&&... args)const{return ::Movement::follow(source,std::forward(args)...);} + template decltype(auto) orbit(A&&... args)const{return ::Movement::orbit(source,std::forward(args)...);} + template decltype(auto) fade(A&&... args)const{return ::Effects::fade(source,std::forward(args)...);} + template decltype(auto) flash(A&&... args)const{return ::Effects::flash(source,std::forward(args)...);} + template decltype(auto) shake(A&&... args)const{return ::Effects::shake(source,std::forward(args)...);} + template decltype(auto) pulse(A&&... args)const{return ::Effects::pulse(source,std::forward(args)...);} + template decltype(auto) impulse(A&&... args)const{return ::Physics::impulse(source,std::forward(args)...);} + template decltype(auto) force(A&&... args)const{return ::Physics::force(source,std::forward(args)...);} + template decltype(auto) launch(A&&... args)const{return ::Physics::launch(source,std::forward(args)...);} + template decltype(auto) knockback(A&&... args)const{return ::Physics::knockback(source,std::forward(args)...);} + template decltype(auto) playAnimation(A&&... args)const{return ::Animation::play(source,std::forward(args)...);} + template decltype(auto) destroy(A&&... args)const{return ::Entity::destroy(source,std::forward(args)...);} + template decltype(auto) destroyAfter(A&&... args)const{return ::Entity::destroy_after(source,std::forward(args)...);} + template decltype(auto) isValid(A&&... args)const{return ::Entity::is_valid(source,std::forward(args)...);} + template decltype(auto) addTag(A&&... args)const{return ::Tags::add(source,std::forward(args)...);} + template decltype(auto) removeTag(A&&... args)const{return ::Tags::remove(source,std::forward(args)...);} + template decltype(auto) setParent(A&&... args)const{return ::Hierarchy::set_parent(source,std::forward(args)...);} + template decltype(auto) getChildren(A&&... args)const{return ::Hierarchy::get_children(source,std::forward(args)...);} + template decltype(auto) detach(A&&... args)const{return ::Hierarchy::detach(source,std::forward(args)...);} + template decltype(auto) hasComponent(A&&... args)const{return ::Components::has(source,std::forward(args)...);} + template decltype(auto) getComponents(A&&... args)const{return ::Components::get_all(source,std::forward(args)...);} + template decltype(auto) findInChildren(A&&... args)const{return ::Components::find_in_children(source,std::forward(args)...);} + template decltype(auto) followPath(A&&... args)const{return ::Path::follow(source,std::forward(args)...);} + void EditAttribute(const std::string& name,RusticValue value)const{::rustic.edit_object_attribute(source,name,std::move(value));} + void edit_attribute(const std::string& name,RusticValue value)const{EditAttribute(name,std::move(value));} +}; +struct CanonicalFilePath { + std::string source; + CanonicalFilePath operator[](const std::string& name)const{return {source+(source.empty()?"":"/")+name};} + operator std::string()const{return source;} + template decltype(auto) audioPlay(A&&... args)const{return ::Audio::play(source,std::forward(args)...);} + template decltype(auto) audioPlayAt(A&&... args)const{return ::Audio::playAt(source,std::forward(args)...);} + template decltype(auto) animationLoad(First&& first,A&&... args)const{return ::Animation::load(std::forward(first),source,std::forward(args)...);} + template decltype(auto) play(A&&... args)const{return ::Audio::play(source,std::forward(args)...);} + template decltype(auto) playAt(A&&... args)const{return ::Audio::playAt(source,std::forward(args)...);} + template decltype(auto) loadAnimation(First&& first,A&&... args)const{return ::Animation::load(std::forward(first),source,std::forward(args)...);} + void add(std::optional parent={})const{::instance.add(source,parent);} + void clone(std::optional parent={})const{::instance.clone(source,parent);} + auto setSkyTexture()const{return ::Game.scene.setSkyTexture(source);} +}; +struct CanonicalGlobalApi { + CanonicalFilePath files{""}; + template decltype(auto) tweenValue(A&&... args)const{return ::Tween::value(std::forward(args)...);} + template decltype(auto) timerAfter(A&&... args)const{return ::Timer::after(std::forward(args)...);} + template decltype(auto) timerEvery(A&&... args)const{return ::Timer::every(std::forward(args)...);} + template decltype(auto) smoothLerp(A&&... args)const{return ::Smooth::lerp(std::forward(args)...);} + template decltype(auto) smoothSlerp(A&&... args)const{return ::Smooth::slerp(std::forward(args)...);} + template decltype(auto) smoothInverseLerp(A&&... args)const{return ::Smooth::inverseLerp(std::forward(args)...);} + template decltype(auto) smoothRemap(A&&... args)const{return ::Smooth::remap(std::forward(args)...);} + template decltype(auto) smoothSmoothDamp(A&&... args)const{return ::Smooth::smoothDamp(std::forward(args)...);} + template decltype(auto) physicsRaycast(A&&... args)const{return ::Physics::raycast(std::forward(args)...);} + template decltype(auto) physicsSphereCast(A&&... args)const{return ::Physics::sphereCast(std::forward(args)...);} + template decltype(auto) physicsOverlap(A&&... args)const{return ::Physics::overlap(std::forward(args)...);} + template decltype(auto) physicsExplosion(A&&... args)const{return ::Physics::explosion(std::forward(args)...);} + template decltype(auto) clockTimeScale(A&&... args)const{return ::Clock::timeScale(std::forward(args)...);} + template decltype(auto) clockPause(A&&... args)const{return ::Clock::pause(std::forward(args)...);} + template decltype(auto) clockResume(A&&... args)const{return ::Clock::resume(std::forward(args)...);} + template decltype(auto) pathCreate(A&&... args)const{return ::Path::create(std::forward(args)...);} + template decltype(auto) sequenceNew(A&&... args)const{return ::Sequence::create(std::forward(args)...);} + template decltype(auto) sequenceCreate(A&&... args)const{return ::Sequence::create(std::forward(args)...);} + template decltype(auto) timelineNew(A&&... args)const{return ::Timeline::create(std::forward(args)...);} + template decltype(auto) timelineCreate(A&&... args)const{return ::Timeline::create(std::forward(args)...);} + template decltype(auto) eventsOn(A&&... args)const{return ::Events::on(std::forward(args)...);} + template decltype(auto) eventsOnce(A&&... args)const{return ::Events::once(std::forward(args)...);} + template decltype(auto) eventsEmit(A&&... args)const{return ::Events::emit(std::forward(args)...);} + template decltype(auto) eventsDisconnect(A&&... args)const{return ::Events::disconnect(std::forward(args)...);} + template decltype(auto) audioGetOutputDevices(A&&... args)const{return ::Audio::get_output_devices(std::forward(args)...);} + template decltype(auto) audioGetInputDevices(A&&... args)const{return ::Audio::get_input_devices(std::forward(args)...);} + template decltype(auto) audioSetOutputDevice(A&&... args)const{return ::Audio::set_output_device(std::forward(args)...);} + template decltype(auto) audioSetInputDevice(A&&... args)const{return ::Audio::set_input_device(std::forward(args)...);} + template decltype(auto) audioVolume(A&&... args)const{return ::Audio::volume(std::forward(args)...);} + template decltype(auto) audioPitch(A&&... args)const{return ::Audio::pitch(std::forward(args)...);} + template decltype(auto) audioFadeIn(A&&... args)const{return ::Audio::fadeIn(std::forward(args)...);} + template decltype(auto) audioFadeOut(A&&... args)const{return ::Audio::fadeOut(std::forward(args)...);} + template decltype(auto) audioCrossfade(A&&... args)const{return ::Audio::crossfade(std::forward(args)...);} + template decltype(auto) animationValue(A&&... args)const{return ::Animation::value(std::forward(args)...);} + template decltype(auto) animationStop(A&&... args)const{return ::Animation::stop(std::forward(args)...);} + template decltype(auto) animationPause(A&&... args)const{return ::Animation::pause(std::forward(args)...);} + template decltype(auto) animationSpeed(A&&... args)const{return ::Animation::speed(std::forward(args)...);} + template decltype(auto) animationLoop(A&&... args)const{return ::Animation::loop(std::forward(args)...);} + template decltype(auto) cameraCurrent(A&&... args)const{return ::Camera::current(std::forward(args)...);} + template decltype(auto) tagsFindAll(A&&... args)const{return ::Tags::find_all(std::forward(args)...);} + template decltype(auto) groupsCreate(A&&... args)const{return ::Groups::create(std::forward(args)...);} + template decltype(auto) groupsGetMembers(A&&... args)const{return ::Groups::get_members(std::forward(args)...);} + template decltype(auto) worldFindByTag(A&&... args)const{return ::World::find_by_tag(std::forward(args)...);} + template decltype(auto) worldFindInRadius(A&&... args)const{return ::World::find_in_radius(std::forward(args)...);} + template decltype(auto) after(A&&... args)const{return ::Timer::after(std::forward(args)...);} + template decltype(auto) every(A&&... args)const{return ::Timer::every(std::forward(args)...);} + template decltype(auto) lerp(A&&... args)const{return ::Smooth::lerp(std::forward(args)...);} + template decltype(auto) slerp(A&&... args)const{return ::Smooth::slerp(std::forward(args)...);} + template decltype(auto) inverseLerp(A&&... args)const{return ::Smooth::inverseLerp(std::forward(args)...);} + template decltype(auto) remap(A&&... args)const{return ::Smooth::remap(std::forward(args)...);} + template decltype(auto) smoothDamp(A&&... args)const{return ::Smooth::smoothDamp(std::forward(args)...);} + template decltype(auto) raycast(A&&... args)const{return ::Physics::raycast(std::forward(args)...);} + template decltype(auto) sphereCast(A&&... args)const{return ::Physics::sphereCast(std::forward(args)...);} + template decltype(auto) overlap(A&&... args)const{return ::Physics::overlap(std::forward(args)...);} + template decltype(auto) explosion(A&&... args)const{return ::Physics::explosion(std::forward(args)...);} + template decltype(auto) timeScale(A&&... args)const{return ::Clock::timeScale(std::forward(args)...);} + template decltype(auto) pause(A&&... args)const{return ::Clock::pause(std::forward(args)...);} + template decltype(auto) resume(A&&... args)const{return ::Clock::resume(std::forward(args)...);} + template decltype(auto) createPath(A&&... args)const{return ::Path::create(std::forward(args)...);} + template decltype(auto) sequence(A&&... args)const{return ::Sequence::create(std::forward(args)...);} + template decltype(auto) timeline(A&&... args)const{return ::Timeline::create(std::forward(args)...);} + template decltype(auto) on(A&&... args)const{return ::Events::on(std::forward(args)...);} + template decltype(auto) once(A&&... args)const{return ::Events::once(std::forward(args)...);} + template decltype(auto) emit(A&&... args)const{return ::Events::emit(std::forward(args)...);} + template decltype(auto) disconnect(A&&... args)const{return ::Events::disconnect(std::forward(args)...);} + template decltype(auto) currentCamera(A&&... args)const{return ::Camera::current(std::forward(args)...);} + template decltype(auto) getOutputDevices(A&&... args)const{return ::Audio::get_output_devices(std::forward(args)...);} + template decltype(auto) get_output_devices(A&&... args)const{return ::Audio::get_output_devices(std::forward(args)...);} + template decltype(auto) getInputDevices(A&&... args)const{return ::Audio::get_input_devices(std::forward(args)...);} + template decltype(auto) get_input_devices(A&&... args)const{return ::Audio::get_input_devices(std::forward(args)...);} + template decltype(auto) setOutputDevice(A&&... args)const{return ::Audio::set_output_device(std::forward(args)...);} + template decltype(auto) set_output_device(A&&... args)const{return ::Audio::set_output_device(std::forward(args)...);} + template decltype(auto) setInputDevice(A&&... args)const{return ::Audio::set_input_device(std::forward(args)...);} + template decltype(auto) set_input_device(A&&... args)const{return ::Audio::set_input_device(std::forward(args)...);} + auto getEnvironment()const{return ::Game.scene.getEnvironment();} + auto setEnvironment(RusticValue::Object settings)const{return ::Game.scene.setEnvironment(std::move(settings));} + void setCurrentCamera(const std::string& source)const{::Game.setCurrentCamera(source);} + template decltype(auto) query(A&&... args)const{return ::rustic.query(std::forward(args)...);} + template decltype(auto) gameplay(A&&... args)const{return ::rustic.gameplay(std::forward(args)...);} + template decltype(auto) gameplay_connections(A&&... args)const{return ::rustic.gameplay_connections(std::forward(args)...);} + template decltype(auto) gameplay_states(A&&... args)const{return ::rustic.gameplay_states(std::forward(args)...);} + template decltype(auto) gameplay_callbacks(A&&... args)const{return ::rustic.gameplay_callbacks(std::forward(args)...);} + template decltype(auto) entity_id(A&&... args)const{return ::rustic.entity_id(std::forward(args)...);} + template decltype(auto) delta_time(A&&... args)const{return ::rustic.delta_time(std::forward(args)...);} + template decltype(auto) fixed_delta_time(A&&... args)const{return ::rustic.fixed_delta_time(std::forward(args)...);} + template decltype(auto) get_translation(A&&... args)const{return ::rustic.get_translation(std::forward(args)...);} + template decltype(auto) set_translation(A&&... args)const{return ::rustic.set_translation(std::forward(args)...);} + template decltype(auto) get_property(A&&... args)const{return ::rustic.get_property(std::forward(args)...);} + template decltype(auto) set_property(A&&... args)const{return ::rustic.set_property(std::forward(args)...);} + template decltype(auto) GetAttribute(A&&... args)const{return ::rustic.GetAttribute(std::forward(args)...);} + template decltype(auto) get_attribute(A&&... args)const{return ::rustic.get_attribute(std::forward(args)...);} + template decltype(auto) EditAttribute(A&&... args)const{return ::rustic.EditAttribute(std::forward(args)...);} + template decltype(auto) edit_attribute(A&&... args)const{return ::rustic.edit_attribute(std::forward(args)...);} + template decltype(auto) input(A&&... args)const{return ::rustic.input(std::forward(args)...);} + template decltype(auto) key(A&&... args)const{return ::rustic.key(std::forward(args)...);} + template decltype(auto) key_events(A&&... args)const{return ::rustic.key_events(std::forward(args)...);} + template decltype(auto) any_key_pressed(A&&... args)const{return ::rustic.any_key_pressed(std::forward(args)...);} + template decltype(auto) log(A&&... args)const{return ::rustic.log(std::forward(args)...);} + template decltype(auto) set_enabled(A&&... args)const{return ::rustic.set_enabled(std::forward(args)...);} +}; +struct CanonicalGame {CanonicalScenePath scene{"game.scene"};CanonicalGlobalApi rustic;};inline CanonicalGame game; inline int rustic_run(const RusticBehavior& b){std::string line;while(std::getline(std::cin,line)){try{auto state=rustic_detail::Parser(line).parse();std::vector commands;rustic.state=&state;rustic.commands=&commands;instance.commands=&commands;Game.commands=&commands;Game.scene.state=&state.at("scene_paths");auto cb=state.at("callback").string();double d=state.at("delta").is_null()?0:state.at("delta").number();if(cb=="update")Gameplay::dispatch();if(cb=="on_create"&&b.on_create)b.on_create();else if(cb=="on_start"&&b.on_start)b.on_start();else if(cb=="on_enable"&&b.on_enable)b.on_enable();else if(cb=="on_disable"&&b.on_disable)b.on_disable();else if(cb=="fixed_update"&&b.fixed_update)b.fixed_update(d);else if(cb=="update"&&b.update)b.update(d);else if(cb=="on_destroy"&&b.on_destroy)b.on_destroy();else if(cb=="on_stop"&&b.on_stop)b.on_stop();std::cout< JSON.parse(__rustic_query(JSON.stringify(request))), + query: request => { + if(request.op==='scene_object') { + const pending=commands;commands=[]; + request={...request,__commands:pending}; + } + return JSON.parse(__rustic_query(JSON.stringify(request))); + }, gameplay: request => commands.push({op:"gameplay",request}), gameplay_connections: () => state.gameplay_connections || [], gameplay_states: () => state.gameplay_states || [], @@ -182,12 +188,24 @@ impl JavaScriptBehavior { ctx.clone(), move |request: String| -> rquickjs::Result { let response = (|| -> Result { - let request = + let mut request: serde_json::Value = serde_json::from_str(&request).map_err(|e| e.to_string())?; - query_host - .lock() - .map_err(|_| "gameplay lock poisoned")? - .gameplay_query(request) + let mut host = + query_host.lock().map_err(|_| "gameplay lock poisoned")?; + if request.get("op").and_then(serde_json::Value::as_str) + == Some("scene_object") + && let Some(pending) = request + .as_object_mut() + .and_then(|fields| fields.remove("__commands")) + { + let commands: Vec = serde_json::from_value(pending) + .map_err(|e| e.to_string())?; + for command in commands { + apply_command(host.as_mut(), command) + .map_err(|e| e.to_string())?; + } + } + host.gameplay_query(request) })(); match response { Ok(value) => serde_json::to_string(&value) diff --git a/Engine/crates/engine-scripting/src/lua.rs b/Engine/crates/engine-scripting/src/lua.rs index 738f9e50..c7729c04 100644 --- a/Engine/crates/engine-scripting/src/lua.rs +++ b/Engine/crates/engine-scripting/src/lua.rs @@ -402,14 +402,25 @@ fn object_proxy( ) -> mlua::Result { let table = lua.create_table()?; table.raw_set("__source", path.clone())?; + table.raw_set("__file_path", asset_path.clone())?; let meta = lua.create_table()?; meta.set( "__index", lua.create_function(move |lua, (proxy, key): (Table, String)| { + if let Some(dispatch) = lua + .globals() + .get::>("__rustic_path_method")? + && let Some(method) = + dispatch.call::>((proxy.clone(), key.clone()))? + { + return Ok(Value::Function(method)); + } let h = Arc::clone(&host); let source = path.clone(); match key.as_str() { - "load" | "play" | "stop" | "pause" | "resume" | "Unload" | "unload" => { + "load" | "play" | "stop" | "pause" | "resume" | "Unload" | "unload" + if !source.starts_with("game.scene.") => + { let audio: Function = lua.globals().get("__rustic_audio_object")?; let object: Table = audio.call(asset_path.clone())?; for method in [ @@ -441,16 +452,22 @@ fn object_proxy( engine_to_lua(lua, value) }, )?)), - _ => Ok(Value::Table(object_proxy( - lua, - h, - format!("{source}.{key}"), - if asset_path.is_empty() { - key - } else { - format!("{asset_path}/{key}") - }, - )?)), + _ => { + let child = object_proxy( + lua, + h, + format!("{source}.{key}"), + if asset_path.is_empty() { + key.clone() + } else { + format!("{asset_path}/{key}") + }, + )?; + if source.starts_with("game.rustic.files") { + proxy.raw_set(key, child.clone())?; + } + Ok(Value::Table(child)) + } } })?, )?; @@ -460,6 +477,13 @@ fn object_proxy( #[allow(clippy::too_many_lines)] fn install_api(lua: &Lua, host: Arc>>) -> mlua::Result<()> { + let path_host = Arc::clone(&host); + lua.globals().set( + "__rustic_object_path", + lua.create_function(move |lua, source: String| { + object_proxy(lua, Arc::clone(&path_host), source, String::new()) + })?, + )?; let api = lua.create_table()?; let h = Arc::clone(&host); api.set( diff --git a/Engine/crates/engine-scripting/src/sdk/Rustic.cs b/Engine/crates/engine-scripting/src/sdk/Rustic.cs index 12284421..b7a63a7d 100644 --- a/Engine/crates/engine-scripting/src/sdk/Rustic.cs +++ b/Engine/crates/engine-scripting/src/sdk/Rustic.cs @@ -11,6 +11,7 @@ static class Rustic { public static readonly InstanceApi instance = new(); public static readonly RusticApi rustic = new(instance.Commands); public static readonly GameApi Game = new(instance.Commands); + public static readonly CanonicalGame game = new(); internal static JsonElement Query(object query){ Console.WriteLine(JsonSerializer.Serialize(new{query,commands=instance.Commands.ToArray()}));instance.Commands.Clear(); using var response=JsonDocument.Parse(Console.ReadLine()??throw new InvalidOperationException("query host exited")); @@ -79,7 +80,7 @@ public static class Gameplay { internal static void Release(Handle h){foreach(var t in h.Tokens)callbacks.Remove(t);callbacks.Remove(h.Token);handles.Remove(h.Id);} private static void Collect(JsonElement a,List tokens){if(a.TryGetProperty("token",out var token))tokens.Add(token.GetString()!);if(a.TryGetProperty("marker_token",out token)&&token.ValueKind==JsonValueKind.String)tokens.Add(token.GetString()!);if(a.TryGetProperty("actions",out var actions))foreach(var child in actions.EnumerateArray())Collect(child,tokens);} internal static void Send(object request)=>instance.Commands.Add(new {op="gameplay",request}); - internal static string Entity(string? id)=>id is null?rustic.entity_id():Game.scene.Find(id)??id; + internal static string Entity(string? id)=>id is null?rustic.entity_id():id.StartsWith("game.scene.",StringComparison.Ordinal)?Query(new{op="scene_object",source=id}).GetString()!:Game.scene.Find(id)??id; public static Handle Start(object action, bool loop=false, uint repeats=0, bool pingPong=false){ var h=new Handle(Guid.NewGuid().ToString());handles[h.Id]=h; using(var doc=JsonDocument.Parse(JsonSerializer.Serialize(action)))Collect(doc.RootElement,h.Tokens); @@ -342,3 +343,226 @@ sealed class GameApi { public void setCurrentCamera(string source) => SetCurrentCamera(source); } + +// Canonical API paths. +sealed class CanonicalScenePath : System.Dynamic.DynamicObject { + private readonly string source; + public CanonicalScenePath(string source="game.scene") { this.source=source; } + public CanonicalScenePath this[string name] => new(source+"."+name); + public override bool TryGetMember(System.Dynamic.GetMemberBinder binder,out object? result) { result=this[binder.Name];return true; } + public static implicit operator string(CanonicalScenePath path) => path.source; + public void clone(string? parent=null) => Rustic.instance.clone(source,parent); + public void add(string? parent=null) => Rustic.instance.add(source,parent); + public void setCurrentCamera() => Rustic.Game.setCurrentCamera(source); + public void emit(string name,object[] arguments) => Rustic.Events.emit(name,arguments,source); + public void EditAttribute(string name,object? value) => Rustic.instance.Commands.Add(new{op="edit_attribute",this.source,name,value}); + public void edit_attribute(string name,object? value) => EditAttribute(name,value); + public Rustic.Gameplay.Handle tweenTo(string p,object to,Rustic.MotionOptions opts) => Rustic.Tween.to(this.source,p,to,opts); + public Rustic.Gameplay.Handle tweenTo(string property,object value,double duration,string easing="Linear") => Rustic.Tween.to(this.source,property,value,duration,easing); + public Rustic.Gameplay.Handle tweenMove(double[] p,Rustic.MotionOptions opts) => Rustic.Tween.move(this.source,p,opts); + public Rustic.Gameplay.Handle tweenMove(double[] position,double duration,string easing="Linear") => Rustic.Tween.move(this.source,position,duration,easing); + public Rustic.Gameplay.Handle tweenRotate(double[] p,Rustic.MotionOptions opts) => Rustic.Tween.rotate(this.source,p,opts); + public Rustic.Gameplay.Handle tweenRotate(double[] rotation,double duration,string easing="Linear") => Rustic.Tween.rotate(this.source,rotation,duration,easing); + public Rustic.Gameplay.Handle tweenScale(double[] scale,double duration,string easing="Linear") => Rustic.Tween.scale(this.source,scale,duration,easing); + public Rustic.Gameplay.Handle movementMoveTo(double[] p,Rustic.MotionOptions opts) => Rustic.Movement.moveTo(this.source,p,opts); + public Rustic.Gameplay.Handle movementMoveTo(double[] p,double d,string ease="Linear") => Rustic.Movement.moveTo(this.source,p,d,ease); + public Rustic.Gameplay.Handle movementMove(double[] offset,double duration,string easing="Linear") => Rustic.Movement.move(this.source,offset,duration,easing); + public Rustic.Gameplay.Handle movementRotateTo(double[] p,Rustic.MotionOptions opts) => Rustic.Movement.rotateTo(this.source,p,opts); + public Rustic.Gameplay.Handle movementRotateTo(double[] q,double d,string ease="Linear") => Rustic.Movement.rotateTo(this.source,q,d,ease); + public Rustic.Gameplay.Handle movementLookAt(double[] position,double duration=0,string easing="Linear") => Rustic.Movement.lookAt(this.source,position,duration,easing); + public Rustic.Gameplay.Handle movementFollow(string target,double duration,string easing="Linear",double[]? offset=null) => Rustic.Movement.follow(this.source,target,duration,easing,offset); + public Rustic.Gameplay.Handle movementOrbit(double[] center,double radius,double turns,double duration,string easing="Linear") => Rustic.Movement.orbit(this.source,center,radius,turns,duration,easing); + public Rustic.Gameplay.Handle pathFollow(Rustic.Path.Curve path,double duration,string easing="Linear",bool loop=false,bool pingPong=false,bool orientToPath=false) => Rustic.Path.follow(this.source,path,duration,easing,loop,pingPong,orientToPath); + public Rustic.Gameplay.Handle pathFollowSpeed(Rustic.Path.Curve path,double speed,string easing="Linear",bool loop=false,bool pingPong=false,bool orientToPath=false) => Rustic.Path.followSpeed(this.source,path,speed,easing,loop,pingPong,orientToPath); + public Rustic.Gameplay.Handle effectsFade(double opacity,double duration,string easing="Linear") => Rustic.Effects.fade(this.source,opacity,duration,easing); + public Rustic.Gameplay.Handle effectsFlash(double[] color,double d,string ease="Linear") => Rustic.Effects.flash(this.source,color,d,ease); + public Rustic.Gameplay.Handle effectsShake(double strength,double duration,string easing="Linear") => Rustic.Effects.shake(this.source,strength,duration,easing); + public Rustic.Gameplay.Handle effectsPulse(double[] scale,double d,string ease="Linear") => Rustic.Effects.pulse(this.source,scale,d,ease); + public Rustic.Gameplay.Handle cameraMoveTo(double[] p,double duration,string easing="Linear") => Rustic.Camera.moveTo(this.source,p,duration,easing); + public Rustic.Gameplay.Handle cameraZoom(double fov,double duration,string easing="Linear") => Rustic.Camera.zoom(this.source,fov,duration,easing); + public Rustic.Gameplay.Handle cameraFov(double v,double d,string ease="Linear") => Rustic.Camera.fov(this.source,v,d,ease); + public Rustic.Gameplay.Handle cameraFollow(string target,double d,string ease="Linear") => Rustic.Camera.follow(this.source,target,d,ease); + public Rustic.Gameplay.Handle cameraOrbit(double[] c,double r,double turns,double d,string ease="Linear") => Rustic.Camera.orbit(this.source,c,r,turns,d,ease); + public Rustic.Gameplay.Handle cameraLookAt(double[] p,double d,string ease="Linear") => Rustic.Camera.lookAt(this.source,p,d,ease); + public Rustic.Gameplay.Handle cameraShake(double s,double d,string ease="Linear") => Rustic.Camera.shake(this.source,s,d,ease); + public Rustic.Gameplay.Handle cameraTransition(double[] p,double[] r,double f,double d,string ease="Linear") => Rustic.Camera.transition(this.source,p,r,f,d,ease); + public void physicsImpulse(double[] vector) => Rustic.Physics.impulse(this.source,vector); + public void physicsForce(double[] vector,double delta) => Rustic.Physics.force(this.source,vector,delta); + public void physicsLaunch(double[] vector) => Rustic.Physics.launch(this.source,vector); + public void physicsKnockback(double[] vector) => Rustic.Physics.knockback(this.source,vector); + public void animationRegister(Rustic.Animation.Clip clip) => Rustic.Animation.register(this.source,clip); + public string[] animationClips() => Rustic.Animation.clips(this.source); + public Rustic.Gameplay.Handle animationPlay(string name,Rustic.Animation.Options? opts=null) => Rustic.Animation.play(this.source,name,opts); + public Rustic.Gameplay.Handle animationBlend(string from,string to,double duration,string easing="Linear") => Rustic.Animation.blend(this.source,from,to,duration,easing); + public Rustic.Gameplay.Handle animationTransition(string from,string to,double duration,string easing="Linear") => Rustic.Animation.transition(this.source,from,to,duration,easing); + public Rustic.Gameplay.Handle animationIk(string middle,string tip,double[] target,double duration=0,string easing="Linear",double weight=1,double[]? pole=null) => Rustic.Animation.ik(this.source,middle,tip,target,duration,easing,weight,pole); + public Rustic.Gameplay.Handle animationFootPlacement(string middle,string tip,double[] target,double duration=0,string easing="Linear") => Rustic.Animation.footPlacement(this.source,middle,tip,target,duration,easing); + public Rustic.Gameplay.Handle animationLookAt(double[] target,double duration=0,string easing="Linear") => Rustic.Animation.lookAt(this.source,target,duration,easing); + public Rustic.Gameplay.Handle animationHeadTracking(double[] target,double duration=0,string easing="Linear") => Rustic.Animation.headTracking(this.source,target,duration,easing); + public Rustic.Gameplay.Handle animationRecoil(double[] rotation,double duration,string easing="Linear") => Rustic.Animation.recoil(this.source,rotation,duration,easing); + public void animationAddMarker(string clip,double time,string name) => Rustic.Animation.addMarker(this.source,clip,time,name); + public Rustic.Events.Connection eventsConnect(string name,Action fn) => Rustic.Events.connect(this.source,name,fn); + public bool tagsAdd(string name) => Rustic.Tags.add(this.source,name); + public bool tagsRemove(string name) => Rustic.Tags.remove(this.source,name); + public bool groupsAdd(string name) => Rustic.Groups.add(name,source); + public void hierarchySetParent(string? parent,bool keep_world=true) => Rustic.Hierarchy.set_parent(this.source,parent,keep_world); + public string[] hierarchyGetChildren() => Rustic.Hierarchy.get_children(this.source); + public void hierarchyDetach(bool keep_world=true) => Rustic.Hierarchy.detach(this.source,keep_world); + public void entityDestroy() => Rustic.Entity.destroy(this.source); + public void entityDestroyAfter(double delay) => Rustic.Entity.destroy_after(this.source,delay); + public bool entityIsValid() => Rustic.Entity.is_valid(this.source); + public bool componentsHas(string name) => Rustic.Components.has(this.source,name); + public string[] componentsGetAll() => Rustic.Components.get_all(this.source); + public string[] componentsFindInChildren(string name) => Rustic.Components.find_in_children(this.source,name); + public Rustic.Gameplay.Handle tween(string p,object to,Rustic.MotionOptions opts) => Rustic.Tween.to(this.source,p,to,opts); + public Rustic.Gameplay.Handle tween(string property,object value,double duration,string easing="Linear") => Rustic.Tween.to(this.source,property,value,duration,easing); + public Rustic.Gameplay.Handle moveTo(double[] p,Rustic.MotionOptions opts) => Rustic.Movement.moveTo(this.source,p,opts); + public Rustic.Gameplay.Handle moveTo(double[] p,double d,string ease="Linear") => Rustic.Movement.moveTo(this.source,p,d,ease); + public Rustic.Gameplay.Handle move(double[] offset,double duration,string easing="Linear") => Rustic.Movement.move(this.source,offset,duration,easing); + public Rustic.Gameplay.Handle rotateTo(double[] p,Rustic.MotionOptions opts) => Rustic.Movement.rotateTo(this.source,p,opts); + public Rustic.Gameplay.Handle rotateTo(double[] q,double d,string ease="Linear") => Rustic.Movement.rotateTo(this.source,q,d,ease); + public Rustic.Gameplay.Handle lookAt(double[] position,double duration=0,string easing="Linear") => Rustic.Movement.lookAt(this.source,position,duration,easing); + public Rustic.Gameplay.Handle follow(string target,double duration,string easing="Linear",double[]? offset=null) => Rustic.Movement.follow(this.source,target,duration,easing,offset); + public Rustic.Gameplay.Handle orbit(double[] center,double radius,double turns,double duration,string easing="Linear") => Rustic.Movement.orbit(this.source,center,radius,turns,duration,easing); + public Rustic.Gameplay.Handle fade(double opacity,double duration,string easing="Linear") => Rustic.Effects.fade(this.source,opacity,duration,easing); + public Rustic.Gameplay.Handle flash(double[] color,double d,string ease="Linear") => Rustic.Effects.flash(this.source,color,d,ease); + public Rustic.Gameplay.Handle shake(double strength,double duration,string easing="Linear") => Rustic.Effects.shake(this.source,strength,duration,easing); + public Rustic.Gameplay.Handle pulse(double[] scale,double d,string ease="Linear") => Rustic.Effects.pulse(this.source,scale,d,ease); + public void impulse(double[] vector) => Rustic.Physics.impulse(this.source,vector); + public void force(double[] vector,double delta) => Rustic.Physics.force(this.source,vector,delta); + public void launch(double[] vector) => Rustic.Physics.launch(this.source,vector); + public void knockback(double[] vector) => Rustic.Physics.knockback(this.source,vector); + public Rustic.Gameplay.Handle playAnimation(string name,Rustic.Animation.Options? opts=null) => Rustic.Animation.play(this.source,name,opts); + public void destroy() => Rustic.Entity.destroy(this.source); + public void destroyAfter(double delay) => Rustic.Entity.destroy_after(this.source,delay); + public bool isValid() => Rustic.Entity.is_valid(this.source); + public bool addTag(string name) => Rustic.Tags.add(this.source,name); + public bool removeTag(string name) => Rustic.Tags.remove(this.source,name); + public void setParent(string? parent,bool keep_world=true) => Rustic.Hierarchy.set_parent(this.source,parent,keep_world); + public string[] getChildren() => Rustic.Hierarchy.get_children(this.source); + public void detach(bool keep_world=true) => Rustic.Hierarchy.detach(this.source,keep_world); + public bool hasComponent(string name) => Rustic.Components.has(this.source,name); + public string[] getComponents() => Rustic.Components.get_all(this.source); + public string[] findInChildren(string name) => Rustic.Components.find_in_children(this.source,name); + public Rustic.Gameplay.Handle followPath(Rustic.Path.Curve path,double duration,string easing="Linear",bool loop=false,bool pingPong=false,bool orientToPath=false) => Rustic.Path.follow(this.source,path,duration,easing,loop,pingPong,orientToPath); +} +sealed class CanonicalFilePath { + private readonly string filePath; + public CanonicalFilePath(string filePath="") { this.filePath=filePath; } + public CanonicalFilePath this[string name] => new((filePath.Length==0?"":filePath+"/")+name); + public static implicit operator string(CanonicalFilePath path) => path.filePath; + public Rustic.Audio.Voice audioPlay(double volume=1,double pitch=1,bool loop=false) => Rustic.Audio.play(filePath,volume,pitch,loop); + public Rustic.Audio.Voice audioPlayAt(double[] position,double volume=1,double pitch=1,bool loop=false) => Rustic.Audio.playAt(filePath,position,volume,pitch,loop); + public string[] animationLoad(string? e) => Rustic.Animation.load(e,filePath); + public Rustic.Audio.Voice play(double volume=1,double pitch=1,bool loop=false) => Rustic.Audio.play(filePath,volume,pitch,loop); + public Rustic.Audio.Voice playAt(double[] position,double volume=1,double pitch=1,bool loop=false) => Rustic.Audio.playAt(filePath,position,volume,pitch,loop); + public string[] loadAnimation(string? e) => Rustic.Animation.load(e,filePath); + public void add(string? parent=null) => Rustic.instance.add(filePath,parent); + public void clone(string? parent=null) => Rustic.instance.clone(filePath,parent); + public JsonElement setSkyTexture() => Rustic.Game.scene.setSkyTexture(filePath); +} +sealed class CanonicalGlobalApi { + public CanonicalFilePath files { get; } = new(); + public Rustic.Gameplay.Handle tweenValue(object from,object to,double duration,Action sample,string easing="Linear") => Rustic.Tween.value(from,to,duration,sample,easing); + public Rustic.Gameplay.Handle timerAfter(double delay,Action fn) => Rustic.Timer.after(delay,fn); + public Rustic.Gameplay.Handle timerEvery(double interval,Action fn,uint? count=null) => Rustic.Timer.every(interval,fn,count); + public double smoothLerp(double a,double b,double t,string easing="Linear") => Rustic.Smooth.lerp(a,b,t,easing); + public double[] smoothLerp(double[] a,double[] b,double t,string easing="Linear") => Rustic.Smooth.lerp(a,b,t,easing); + public double[] smoothSlerp(double[] a,double[] b,double t,string easing="Linear") => Rustic.Smooth.slerp(a,b,t,easing); + public double smoothInverseLerp(double a,double b,double value) => Rustic.Smooth.inverseLerp(a,b,value); + public double smoothRemap(double value,double a,double b,double c,double d) => Rustic.Smooth.remap(value,a,b,c,d); + public (double value,double velocity) smoothSmoothDamp(double current,double target,double velocity,double smoothTime,double delta) => Rustic.Smooth.smoothDamp(current,target,velocity,smoothTime,delta); + public Rustic.Physics.Hit? physicsRaycast(double[] origin,double[] direction,double distance) => Rustic.Physics.raycast(origin,direction,distance); + public Rustic.Physics.Hit? physicsSphereCast(double[] origin,double[] direction,double distance,double radius) => Rustic.Physics.sphereCast(origin,direction,distance,radius); + public string[] physicsOverlap(double[] center,double radius) => Rustic.Physics.overlap(center,radius); + public string[] physicsExplosion(double[] center,double radius,double strength) => Rustic.Physics.explosion(center,radius,strength); + public double clockTimeScale(double? scale=null) => Rustic.Clock.timeScale(scale); + public void clockPause() => Rustic.Clock.pause(); + public void clockResume() => Rustic.Clock.resume(); + public Rustic.Path.Curve pathCreate(double[][] points,string kind="Linear") => Rustic.Path.create(points,kind); + public Rustic.Path.Curve pathCreate(Rustic.Path.Point[] points,string kind="Linear") => Rustic.Path.create(points,kind); + public Rustic.Sequence.Builder sequenceNew() => Rustic.Sequence.New(); + public Rustic.Sequence.Builder sequenceCreate() => Rustic.Sequence.New(); + public Rustic.Sequence.Builder timelineNew() => Rustic.Timeline.New(); + public Rustic.Sequence.Builder timelineCreate() => Rustic.Timeline.New(); + public Rustic.Events.Connection eventsOn(string name,Action fn) => Rustic.Events.on(name,fn); + public Rustic.Events.Connection eventsOnce(string name,Action fn) => Rustic.Events.once(name,fn); + public void eventsEmit(string name,object[]? arguments=null,string? source=null) => Rustic.Events.emit(name,arguments,source); + public void eventsDisconnect(Rustic.Events.Connection c) => Rustic.Events.disconnect(c); + public JsonElement audioGetOutputDevices() => Rustic.Audio.get_output_devices(); + public JsonElement audioGetInputDevices() => Rustic.Audio.get_input_devices(); + public void audioSetOutputDevice(string? device=null) => Rustic.Audio.set_output_device(device); + public void audioSetInputDevice(string? device=null) => Rustic.Audio.set_input_device(device); + public void audioVolume(Rustic.Audio.Voice v,double value) => Rustic.Audio.volume(v,value); + public void audioPitch(Rustic.Audio.Voice v,double value) => Rustic.Audio.pitch(v,value); + public Rustic.Gameplay.Handle audioFadeIn(Rustic.Audio.Voice v,double duration,string easing="Linear") => Rustic.Audio.fadeIn(v,duration,easing); + public Rustic.Gameplay.Handle audioFadeOut(Rustic.Audio.Voice v,double duration,string easing="Linear") => Rustic.Audio.fadeOut(v,duration,easing); + public Rustic.Gameplay.Handle audioCrossfade(Rustic.Audio.Voice a,Rustic.Audio.Voice b,double duration,string easing="Linear") => Rustic.Audio.crossfade(a,b,duration,easing); + public Rustic.Gameplay.Handle animationValue(Rustic.Animation.Key[] keys,Action sample) => Rustic.Animation.value(keys,sample); + public void animationStop(Rustic.Gameplay.Handle h) => Rustic.Animation.stop(h); + public void animationPause(Rustic.Gameplay.Handle h) => Rustic.Animation.pause(h); + public void animationSpeed(Rustic.Gameplay.Handle h,double v) => Rustic.Animation.speed(h,v); + public void animationLoop(Rustic.Gameplay.Handle h,bool v=true) => Rustic.Animation.loop(h,v); + public string cameraCurrent() => Rustic.Camera.current(); + public string[] tagsFindAll(string name) => Rustic.Tags.find_all(name); + public bool groupsCreate(string name) => Rustic.Groups.create(name); + public string[] groupsGetMembers(string name) => Rustic.Groups.get_members(name); + public string[] worldFindByTag(string name) => Rustic.World.find_by_tag(name); + public string[] worldFindInRadius(double[] center,double radius) => Rustic.World.find_in_radius(center,radius); + public Rustic.Gameplay.Handle after(double delay,Action fn) => Rustic.Timer.after(delay,fn); + public Rustic.Gameplay.Handle every(double interval,Action fn,uint? count=null) => Rustic.Timer.every(interval,fn,count); + public double lerp(double a,double b,double t,string easing="Linear") => Rustic.Smooth.lerp(a,b,t,easing); + public double[] lerp(double[] a,double[] b,double t,string easing="Linear") => Rustic.Smooth.lerp(a,b,t,easing); + public double[] slerp(double[] a,double[] b,double t,string easing="Linear") => Rustic.Smooth.slerp(a,b,t,easing); + public double inverseLerp(double a,double b,double value) => Rustic.Smooth.inverseLerp(a,b,value); + public double remap(double value,double a,double b,double c,double d) => Rustic.Smooth.remap(value,a,b,c,d); + public (double value,double velocity) smoothDamp(double current,double target,double velocity,double smoothTime,double delta) => Rustic.Smooth.smoothDamp(current,target,velocity,smoothTime,delta); + public Rustic.Physics.Hit? raycast(double[] origin,double[] direction,double distance) => Rustic.Physics.raycast(origin,direction,distance); + public Rustic.Physics.Hit? sphereCast(double[] origin,double[] direction,double distance,double radius) => Rustic.Physics.sphereCast(origin,direction,distance,radius); + public string[] overlap(double[] center,double radius) => Rustic.Physics.overlap(center,radius); + public string[] explosion(double[] center,double radius,double strength) => Rustic.Physics.explosion(center,radius,strength); + public double timeScale(double? scale=null) => Rustic.Clock.timeScale(scale); + public void pause() => Rustic.Clock.pause(); + public void resume() => Rustic.Clock.resume(); + public Rustic.Path.Curve createPath(double[][] points,string kind="Linear") => Rustic.Path.create(points,kind); + public Rustic.Path.Curve createPath(Rustic.Path.Point[] points,string kind="Linear") => Rustic.Path.create(points,kind); + public Rustic.Sequence.Builder sequence() => Rustic.Sequence.New(); + public Rustic.Sequence.Builder timeline() => Rustic.Timeline.New(); + public Rustic.Events.Connection on(string name,Action fn) => Rustic.Events.on(name,fn); + public Rustic.Events.Connection once(string name,Action fn) => Rustic.Events.once(name,fn); + public void emit(string name,object[]? arguments=null,string? source=null) => Rustic.Events.emit(name,arguments,source); + public void disconnect(Rustic.Events.Connection c) => Rustic.Events.disconnect(c); + public string currentCamera() => Rustic.Camera.current(); + public JsonElement getOutputDevices() => Rustic.Audio.get_output_devices(); + public JsonElement get_output_devices() => Rustic.Audio.get_output_devices(); + public JsonElement getInputDevices() => Rustic.Audio.get_input_devices(); + public JsonElement get_input_devices() => Rustic.Audio.get_input_devices(); + public void setOutputDevice(string? device=null) => Rustic.Audio.set_output_device(device); + public void set_output_device(string? device=null) => Rustic.Audio.set_output_device(device); + public void setInputDevice(string? device=null) => Rustic.Audio.set_input_device(device); + public void set_input_device(string? device=null) => Rustic.Audio.set_input_device(device); + public JsonElement getEnvironment() => Rustic.Game.scene.getEnvironment(); + public JsonElement setEnvironment(object settings) => Rustic.Game.scene.setEnvironment(settings); + public void setCurrentCamera(string path) => Rustic.Game.setCurrentCamera(path); + public string entity_id() => Rustic.rustic.entity_id(); + public double delta_time() => Rustic.rustic.delta_time(); + public double fixed_delta_time() => Rustic.rustic.fixed_delta_time(); + public double[] get_translation() => Rustic.rustic.get_translation(); + public void set_translation(double x,double y,double z) => Rustic.rustic.set_translation(x,y,z); + public object? get_property(string name) => Rustic.rustic.get_property(name); + public void set_property(string name, object value) => Rustic.rustic.set_property(name,value); + public object? GetAttribute(string name) => Rustic.rustic.GetAttribute(name); + public void EditAttribute(string name, object? value) => Rustic.rustic.EditAttribute(name,value); + public object? get_attribute(string name) => Rustic.rustic.get_attribute(name); + public void edit_attribute(string name, object? value) => Rustic.rustic.edit_attribute(name,value); + public RusticActionState input(string name) => Rustic.rustic.input(name); + public RusticActionState key(string name) => Rustic.rustic.key(name); + public RusticKeyEvent[] key_events() => Rustic.rustic.key_events(); + public bool any_key_pressed() => Rustic.rustic.any_key_pressed(); + public void log(string level,string message) => Rustic.rustic.log(level,message); + public void set_enabled(bool enabled) => Rustic.rustic.set_enabled(enabled); +} +sealed class CanonicalGame { + public CanonicalScenePath scene { get; } = new(); + public CanonicalGlobalApi rustic { get; } = new(); +} diff --git a/Engine/crates/engine-scripting/src/sdk/Rustic.java b/Engine/crates/engine-scripting/src/sdk/Rustic.java index febbe0b8..89ddcd6b 100644 --- a/Engine/crates/engine-scripting/src/sdk/Rustic.java +++ b/Engine/crates/engine-scripting/src/sdk/Rustic.java @@ -11,6 +11,7 @@ class Rustic { static final RusticApi rustic = new RusticApi(); static final InstanceApi instance = new InstanceApi(); static final GameApi Game = new GameApi(); + static final CanonicalGame game = new CanonicalGame(); static class Tags { static boolean add(String entity,String name){return (Boolean)query(args("tags_add","entity",Gameplay.entity(entity),"name",name));} @@ -102,7 +103,7 @@ static class Gameplay { static final Map> callbacks=new LinkedHashMap<>(); static final Set once=new HashSet<>(),connections=new HashSet<>(); static final Map handles=new LinkedHashMap<>(); - static String entity(String id){return id==null?rustic.entity_id():Optional.ofNullable(Game.scene.Find(id)).orElse(id);} + static String entity(String id){return id==null?rustic.entity_id():id.startsWith("game.scene.")?(String)query(args("scene_object","source",id)):Optional.ofNullable(Game.scene.Find(id)).orElse(id);} static String register(java.util.function.Consumer fn,boolean single){String id=UUID.randomUUID().toString();callbacks.put(id,fn);if(single)once.add(id);return id;} static void send(Object request){command("gameplay","request",request);} static void release(Handle h){for(String token:h.tokens){callbacks.remove(token);once.remove(token);}handles.remove(h.id);callbacks.remove(h.id);once.remove(h.id);} @@ -366,4 +367,278 @@ private static class Parser { switch(n){case "null":return null;case "true":return true;case "false":return false;default:if(n.indexOf('.')<0&&n.indexOf('e')<0&&n.indexOf('E')<0)return Long.valueOf(n);return Double.valueOf(n);} } } + // Canonical API paths. + static class CanonicalScenePath { + final String source; + CanonicalScenePath(String source){this.source=source;} + CanonicalScenePath scene(String name){return new CanonicalScenePath(source+"."+name);} + CanonicalScenePath object(String path){return new CanonicalScenePath(source+"."+path);} + void clone(String parent){instance.clone(source,parent);} + void add(String parent){instance.add(source,parent);} + void setCurrentCamera(){Game.setCurrentCamera(source);} + void emit(String name,Object[] arguments){Events.emit(name,arguments,source);} + void EditAttribute(String name,Object value){command("edit_attribute","source",this.source,"name",name,"value",value);} + void edit_attribute(String name,Object value){EditAttribute(name,value);} + Gameplay.Handle tweenTo(String p,Object to,MotionOptions opts){return Tween.to(this.source,p,to,opts);} + Gameplay.Handle tweenTo(String property,Object value,double duration){return Tween.to(this.source,property,value,duration);} + Gameplay.Handle tweenTo(String property,Object value,double duration,String easing){return Tween.to(this.source,property,value,duration,easing);} + Gameplay.Handle tweenMove(double[] p,MotionOptions opts){return Tween.move(this.source,p,opts);} + Gameplay.Handle tweenMove(double[] p,double duration){return Tween.move(this.source,p,duration);} + Gameplay.Handle tweenMove(double[] p,double duration,String easing){return Tween.move(this.source,p,duration,easing);} + Gameplay.Handle tweenRotate(double[] p,MotionOptions opts){return Tween.rotate(this.source,p,opts);} + Gameplay.Handle tweenRotate(double[] q,double duration){return Tween.rotate(this.source,q,duration);} + Gameplay.Handle tweenRotate(double[] q,double duration,String easing){return Tween.rotate(this.source,q,duration,easing);} + Gameplay.Handle tweenScale(double[] p,double duration){return Tween.scale(this.source,p,duration);} + Gameplay.Handle tweenScale(double[] p,double duration,String easing){return Tween.scale(this.source,p,duration,easing);} + Map tweenAction(String p,Object to,MotionOptions opts){return Tween.action(this.source,p,to,opts);} + Map tweenAction(String property,Object to,double duration,String easing){return Tween.action(this.source,property,to,duration,easing);} + Gameplay.Handle movementMoveTo(double[] p,MotionOptions opts){return Movement.moveTo(this.source,p,opts);} + Gameplay.Handle movementMoveTo(double[] p,double d){return Movement.moveTo(this.source,p,d);} + Gameplay.Handle movementMoveTo(double[] p,double d,String easing){return Movement.moveTo(this.source,p,d,easing);} + Gameplay.Handle movementMove(double[] offset,double duration){return Movement.move(this.source,offset,duration);} + Gameplay.Handle movementMove(double[] offset,double duration,String easing){return Movement.move(this.source,offset,duration,easing);} + Gameplay.Handle movementRotateTo(double[] p,MotionOptions opts){return Movement.rotateTo(this.source,p,opts);} + Gameplay.Handle movementRotateTo(double[] q,double d){return Movement.rotateTo(this.source,q,d);} + Gameplay.Handle movementRotateTo(double[] q,double d,String easing){return Movement.rotateTo(this.source,q,d,easing);} + Gameplay.Handle movementLookAt(double[] position,double duration){return Movement.lookAt(this.source,position,duration);} + Gameplay.Handle movementLookAt(double[] position,double duration,String easing){return Movement.lookAt(this.source,position,duration,easing);} + Gameplay.Handle movementFollow(String target,double duration,String easing,double[] offset){return Movement.follow(this.source,target,duration,easing,offset);} + Gameplay.Handle movementOrbit(double[] center,double radius,double turns,double duration){return Movement.orbit(this.source,center,radius,turns,duration);} + Gameplay.Handle movementOrbit(double[] center,double radius,double turns,double duration,String easing){return Movement.orbit(this.source,center,radius,turns,duration,easing);} + Gameplay.Handle pathFollow(Path.Curve path,double duration,String easing,boolean loop,boolean pingPong,boolean orientToPath){return Path.follow(this.source,path,duration,easing,loop,pingPong,orientToPath);} + Gameplay.Handle pathFollowSpeed(Path.Curve path,double speed,String easing,boolean loop,boolean pingPong,boolean orientToPath){return Path.followSpeed(this.source,path,speed,easing,loop,pingPong,orientToPath);} + Gameplay.Handle effectsFade(double opacity,double duration){return Effects.fade(this.source,opacity,duration);} + Gameplay.Handle effectsFade(double opacity,double duration,String easing){return Effects.fade(this.source,opacity,duration,easing);} + Gameplay.Handle effectsFlash(double[] color,double duration){return Effects.flash(this.source,color,duration);} + Gameplay.Handle effectsFlash(double[] color,double duration,String easing){return Effects.flash(this.source,color,duration,easing);} + Gameplay.Handle effectsShake(double strength,double duration){return Effects.shake(this.source,strength,duration);} + Gameplay.Handle effectsShake(double strength,double duration,String easing){return Effects.shake(this.source,strength,duration,easing);} + Gameplay.Handle effectsPulse(double[] scale,double duration){return Effects.pulse(this.source,scale,duration);} + Gameplay.Handle effectsPulse(double[] scale,double duration,String easing){return Effects.pulse(this.source,scale,duration,easing);} + Gameplay.Handle cameraMoveTo(double[] p,double d){return Camera.moveTo(this.source,p,d);} + Gameplay.Handle cameraMoveTo(double[] p,double d,String ease){return Camera.moveTo(this.source,p,d,ease);} + Gameplay.Handle cameraZoom(double f,double d){return Camera.zoom(this.source,f,d);} + Gameplay.Handle cameraZoom(double f,double d,String ease){return Camera.zoom(this.source,f,d,ease);} + Gameplay.Handle cameraFov(double f,double d){return Camera.fov(this.source,f,d);} + Gameplay.Handle cameraFov(double f,double d,String ease){return Camera.fov(this.source,f,d,ease);} + Gameplay.Handle cameraFollow(String target,double d){return Camera.follow(this.source,target,d);} + Gameplay.Handle cameraFollow(String target,double d,String ease){return Camera.follow(this.source,target,d,ease);} + Gameplay.Handle cameraOrbit(double[] c,double r,double turns,double d){return Camera.orbit(this.source,c,r,turns,d);} + Gameplay.Handle cameraOrbit(double[] c,double r,double turns,double d,String ease){return Camera.orbit(this.source,c,r,turns,d,ease);} + Gameplay.Handle cameraLookAt(double[] p,double d){return Camera.lookAt(this.source,p,d);} + Gameplay.Handle cameraLookAt(double[] p,double d,String ease){return Camera.lookAt(this.source,p,d,ease);} + Gameplay.Handle cameraShake(double s,double d){return Camera.shake(this.source,s,d);} + Gameplay.Handle cameraShake(double s,double d,String ease){return Camera.shake(this.source,s,d,ease);} + Gameplay.Handle cameraTransition(double[] p,double[] r,double f,double d){return Camera.transition(this.source,p,r,f,d);} + Gameplay.Handle cameraTransition(double[] p,double[] r,double f,double d,String ease){return Camera.transition(this.source,p,r,f,d,ease);} + void physicsImpulse(double[] v){Physics.impulse(this.source,v);} + void physicsForce(double[] v,double dt){Physics.force(this.source,v,dt);} + void physicsLaunch(double[] v){Physics.launch(this.source,v);} + void physicsKnockback(double[] v){Physics.knockback(this.source,v);} + void animationRegister(Animation.Clip clip){Animation.register(this.source,clip);} + Object animationClips(){return Animation.clips(this.source);} + Map animationAction(String name,Animation.Options opts,String marker,Object source){return Animation.action(this.source,name,opts,marker,source);} + Gameplay.Handle animationPlay(String name){return Animation.play(this.source,name);} + Gameplay.Handle animationPlay(String name,Animation.Options opts){return Animation.play(this.source,name,opts);} + Gameplay.Handle animationBlend(String from,String to,double duration){return Animation.blend(this.source,from,to,duration);} + Gameplay.Handle animationBlend(String from,String to,double duration,String ease){return Animation.blend(this.source,from,to,duration,ease);} + Gameplay.Handle animationTransition(String from,String to,double duration){return Animation.transition(this.source,from,to,duration);} + Gameplay.Handle animationTransition(String from,String to,double duration,String ease){return Animation.transition(this.source,from,to,duration,ease);} + Gameplay.Handle animationIk(String middle,String tip,double[] target,double duration,String easing,double weight,double[] pole){return Animation.ik(this.source,middle,tip,target,duration,easing,weight,pole);} + Gameplay.Handle animationFootPlacement(String middle,String tip,double[] target,double duration){return Animation.footPlacement(this.source,middle,tip,target,duration);} + Gameplay.Handle animationFootPlacement(String middle,String tip,double[] target,double duration,String ease){return Animation.footPlacement(this.source,middle,tip,target,duration,ease);} + Gameplay.Handle animationLookAt(double[] target,double d,String ease){return Animation.lookAt(this.source,target,d,ease);} + Gameplay.Handle animationLookAt(double[] target,double d){return Animation.lookAt(this.source,target,d);} + Gameplay.Handle animationHeadTracking(double[] target,double duration){return Animation.headTracking(this.source,target,duration);} + Gameplay.Handle animationHeadTracking(double[] target,double duration,String ease){return Animation.headTracking(this.source,target,duration,ease);} + Gameplay.Handle animationRecoil(double[] rotation,double duration){return Animation.recoil(this.source,rotation,duration);} + Gameplay.Handle animationRecoil(double[] rotation,double duration,String ease){return Animation.recoil(this.source,rotation,duration,ease);} + void animationAddMarker(String clip,double time,String name){Animation.addMarker(this.source,clip,time,name);} + Events.Connection eventsConnect(String name,java.util.function.Consumer fn){return Events.connect(this.source,name,fn);} + boolean tagsAdd(String name){return Tags.add(this.source,name);} + boolean tagsRemove(String name){return Tags.remove(this.source,name);} + boolean groupsAdd(String name){return Groups.add(name,source);} + void hierarchySetParent(String parent,boolean keep_world){Hierarchy.set_parent(this.source,parent,keep_world);} + void hierarchySetParent(String parent){Hierarchy.set_parent(this.source,parent);} + String[] hierarchyGetChildren(){return Hierarchy.get_children(this.source);} + void hierarchyDetach(boolean keep_world){Hierarchy.detach(this.source,keep_world);} + void hierarchyDetach(){Hierarchy.detach(this.source);} + void entityDestroy(){Entity.destroy(this.source);} + void entityDestroyAfter(double delay){Entity.destroy_after(this.source,delay);} + boolean entityIsValid(){return Entity.is_valid(this.source);} + boolean componentsHas(String name){return Components.has(this.source,name);} + String[] componentsGetAll(){return Components.get_all(this.source);} + String[] componentsFindInChildren(String name){return Components.find_in_children(this.source,name);} + Gameplay.Handle tween(String p,Object to,MotionOptions opts){return Tween.to(this.source,p,to,opts);} + Gameplay.Handle tween(String property,Object value,double duration){return Tween.to(this.source,property,value,duration);} + Gameplay.Handle tween(String property,Object value,double duration,String easing){return Tween.to(this.source,property,value,duration,easing);} + Gameplay.Handle moveTo(double[] p,MotionOptions opts){return Movement.moveTo(this.source,p,opts);} + Gameplay.Handle moveTo(double[] p,double d){return Movement.moveTo(this.source,p,d);} + Gameplay.Handle moveTo(double[] p,double d,String easing){return Movement.moveTo(this.source,p,d,easing);} + Gameplay.Handle move(double[] offset,double duration){return Movement.move(this.source,offset,duration);} + Gameplay.Handle move(double[] offset,double duration,String easing){return Movement.move(this.source,offset,duration,easing);} + Gameplay.Handle rotateTo(double[] p,MotionOptions opts){return Movement.rotateTo(this.source,p,opts);} + Gameplay.Handle rotateTo(double[] q,double d){return Movement.rotateTo(this.source,q,d);} + Gameplay.Handle rotateTo(double[] q,double d,String easing){return Movement.rotateTo(this.source,q,d,easing);} + Gameplay.Handle lookAt(double[] position,double duration){return Movement.lookAt(this.source,position,duration);} + Gameplay.Handle lookAt(double[] position,double duration,String easing){return Movement.lookAt(this.source,position,duration,easing);} + Gameplay.Handle follow(String target,double duration,String easing,double[] offset){return Movement.follow(this.source,target,duration,easing,offset);} + Gameplay.Handle orbit(double[] center,double radius,double turns,double duration){return Movement.orbit(this.source,center,radius,turns,duration);} + Gameplay.Handle orbit(double[] center,double radius,double turns,double duration,String easing){return Movement.orbit(this.source,center,radius,turns,duration,easing);} + Gameplay.Handle fade(double opacity,double duration){return Effects.fade(this.source,opacity,duration);} + Gameplay.Handle fade(double opacity,double duration,String easing){return Effects.fade(this.source,opacity,duration,easing);} + Gameplay.Handle flash(double[] color,double duration){return Effects.flash(this.source,color,duration);} + Gameplay.Handle flash(double[] color,double duration,String easing){return Effects.flash(this.source,color,duration,easing);} + Gameplay.Handle shake(double strength,double duration){return Effects.shake(this.source,strength,duration);} + Gameplay.Handle shake(double strength,double duration,String easing){return Effects.shake(this.source,strength,duration,easing);} + Gameplay.Handle pulse(double[] scale,double duration){return Effects.pulse(this.source,scale,duration);} + Gameplay.Handle pulse(double[] scale,double duration,String easing){return Effects.pulse(this.source,scale,duration,easing);} + void impulse(double[] v){Physics.impulse(this.source,v);} + void force(double[] v,double dt){Physics.force(this.source,v,dt);} + void launch(double[] v){Physics.launch(this.source,v);} + void knockback(double[] v){Physics.knockback(this.source,v);} + Gameplay.Handle playAnimation(String name){return Animation.play(this.source,name);} + Gameplay.Handle playAnimation(String name,Animation.Options opts){return Animation.play(this.source,name,opts);} + void destroy(){Entity.destroy(this.source);} + void destroyAfter(double delay){Entity.destroy_after(this.source,delay);} + boolean isValid(){return Entity.is_valid(this.source);} + boolean addTag(String name){return Tags.add(this.source,name);} + boolean removeTag(String name){return Tags.remove(this.source,name);} + void setParent(String parent,boolean keep_world){Hierarchy.set_parent(this.source,parent,keep_world);} + void setParent(String parent){Hierarchy.set_parent(this.source,parent);} + String[] getChildren(){return Hierarchy.get_children(this.source);} + void detach(boolean keep_world){Hierarchy.detach(this.source,keep_world);} + void detach(){Hierarchy.detach(this.source);} + boolean hasComponent(String name){return Components.has(this.source,name);} + String[] getComponents(){return Components.get_all(this.source);} + String[] findInChildren(String name){return Components.find_in_children(this.source,name);} + Gameplay.Handle followPath(Path.Curve path,double duration,String easing,boolean loop,boolean pingPong,boolean orientToPath){return Path.follow(this.source,path,duration,easing,loop,pingPong,orientToPath);} + } + static class CanonicalFilePath { + final String filePath; + CanonicalFilePath(String filePath){this.filePath=filePath;} + CanonicalFilePath object(String path){return new CanonicalFilePath(filePath.isEmpty()?path:filePath+"/"+path);} + Audio.Voice audioPlay(double volume,double pitch,boolean loop){return Audio.play(filePath,volume,pitch,loop);} + Audio.Voice audioPlayAt(double[] position,double volume,double pitch,boolean loop){return Audio.playAt(filePath,position,volume,pitch,loop);} + Object animationLoad(String e){return Animation.load(e,filePath);} + Audio.Voice play(double volume,double pitch,boolean loop){return Audio.play(filePath,volume,pitch,loop);} + Audio.Voice playAt(double[] position,double volume,double pitch,boolean loop){return Audio.playAt(filePath,position,volume,pitch,loop);} + Object loadAnimation(String e){return Animation.load(e,filePath);} + void add(String parent){instance.add(filePath,parent);} + void clone(String parent){instance.clone(filePath,parent);} + Object setSkyTexture(){return Game.scene.setSkyTexture(filePath);} + } + static class CanonicalGlobalApi { + final CanonicalFilePath files=new CanonicalFilePath(""); + Gameplay.Handle tweenValue(Object from,Object to,double duration,java.util.function.Consumer sample){return Tween.value(from,to,duration,sample);} + Gameplay.Handle tweenValue(Object from,Object to,double duration,java.util.function.Consumer sample,String easing){return Tween.value(from,to,duration,sample,easing);} + Gameplay.Handle timerAfter(double delay,Runnable fn){return Timer.after(delay,fn);} + Gameplay.Handle timerEvery(double interval,Runnable fn){return Timer.every(interval,fn);} + Gameplay.Handle timerEvery(double interval,Runnable fn,Integer count){return Timer.every(interval,fn,count);} + Object smoothLerp(Object a,Object b,double t){return Smooth.lerp(a,b,t);} + Object smoothLerp(Object a,Object b,double t,String ease){return Smooth.lerp(a,b,t,ease);} + double[] smoothSlerp(double[] a,double[] b,double t){return Smooth.slerp(a,b,t);} + double[] smoothSlerp(double[] a,double[] b,double t,String ease){return Smooth.slerp(a,b,t,ease);} + double smoothInverseLerp(double a,double b,double v){return Smooth.inverseLerp(a,b,v);} + double smoothRemap(double v,double a,double b,double c,double d){return Smooth.remap(v,a,b,c,d);} + double[] smoothSmoothDamp(double c,double t,double v,double s,double dt){return Smooth.smoothDamp(c,t,v,s,dt);} + Object physicsRaycast(double[] origin,double[] direction,double distance){return Physics.raycast(origin,direction,distance);} + Object physicsSphereCast(double[] origin,double[] direction,double distance,double radius){return Physics.sphereCast(origin,direction,distance,radius);} + Object physicsOverlap(double[] center,double radius){return Physics.overlap(center,radius);} + Object physicsExplosion(double[] center,double radius,double strength){return Physics.explosion(center,radius,strength);} + double clockTimeScale(double scale){return Clock.timeScale(scale);} + void clockPause(){Clock.pause();} + void clockResume(){Clock.resume();} + Path.Curve pathCreate(double[][] points,String kind){return Path.create(points,kind);} + Path.Curve pathCreate(List points,String kind){return Path.create(points,kind);} + Sequence.Builder sequenceNew(){return Sequence.create();} + Sequence.Builder sequenceCreate(){return Sequence.create();} + Sequence.Builder timelineNew(){return Sequence.create();} + Sequence.Builder timelineCreate(){return Sequence.create();} + Events.Connection eventsOn(String name,java.util.function.Consumer fn){return Events.on(name,fn);} + Events.Connection eventsOnce(String name,java.util.function.Consumer fn){return Events.once(name,fn);} + void eventsEmit(String name,Object[] arguments,String source){Events.emit(name,arguments,source);} + void eventsDisconnect(Events.Connection c){Events.disconnect(c);} + Object audioGetOutputDevices(){return Audio.get_output_devices();} + Object audioGetInputDevices(){return Audio.get_input_devices();} + void audioSetOutputDevice(String device){Audio.set_output_device(device);} + void audioSetInputDevice(String device){Audio.set_input_device(device);} + void audioVolume(Audio.Voice v,double value){Audio.volume(v,value);} + void audioPitch(Audio.Voice v,double value){Audio.pitch(v,value);} + Gameplay.Handle audioFadeIn(Audio.Voice v,double d){return Audio.fadeIn(v,d);} + Gameplay.Handle audioFadeIn(Audio.Voice v,double d,String ease){return Audio.fadeIn(v,d,ease);} + Gameplay.Handle audioFadeOut(Audio.Voice v,double d){return Audio.fadeOut(v,d);} + Gameplay.Handle audioFadeOut(Audio.Voice v,double d,String ease){return Audio.fadeOut(v,d,ease);} + Gameplay.Handle audioCrossfade(Audio.Voice a,Audio.Voice b,double d){return Audio.crossfade(a,b,d);} + Gameplay.Handle audioCrossfade(Audio.Voice a,Audio.Voice b,double d,String ease){return Audio.crossfade(a,b,d,ease);} + Gameplay.Handle animationValue(List keys,java.util.function.Consumer sample){return Animation.value(keys,sample);} + void animationStop(Gameplay.Handle h){Animation.stop(h);} + void animationPause(Gameplay.Handle h){Animation.pause(h);} + void animationSpeed(Gameplay.Handle h,double v){Animation.speed(h,v);} + void animationLoop(Gameplay.Handle h,boolean v){Animation.loop(h,v);} + String cameraCurrent(){return Camera.current();} + String[] tagsFindAll(String name){return Tags.find_all(name);} + boolean groupsCreate(String name){return Groups.create(name);} + String[] groupsGetMembers(String name){return Groups.get_members(name);} + String[] worldFindByTag(String name){return World.find_by_tag(name);} + String[] worldFindInRadius(double[] center,double radius){return World.find_in_radius(center,radius);} + Gameplay.Handle after(double delay,Runnable fn){return Timer.after(delay,fn);} + Gameplay.Handle every(double interval,Runnable fn){return Timer.every(interval,fn);} + Gameplay.Handle every(double interval,Runnable fn,Integer count){return Timer.every(interval,fn,count);} + Object lerp(Object a,Object b,double t){return Smooth.lerp(a,b,t);} + Object lerp(Object a,Object b,double t,String ease){return Smooth.lerp(a,b,t,ease);} + double[] slerp(double[] a,double[] b,double t){return Smooth.slerp(a,b,t);} + double[] slerp(double[] a,double[] b,double t,String ease){return Smooth.slerp(a,b,t,ease);} + double inverseLerp(double a,double b,double v){return Smooth.inverseLerp(a,b,v);} + double remap(double v,double a,double b,double c,double d){return Smooth.remap(v,a,b,c,d);} + double[] smoothDamp(double c,double t,double v,double s,double dt){return Smooth.smoothDamp(c,t,v,s,dt);} + Object raycast(double[] origin,double[] direction,double distance){return Physics.raycast(origin,direction,distance);} + Object sphereCast(double[] origin,double[] direction,double distance,double radius){return Physics.sphereCast(origin,direction,distance,radius);} + Object overlap(double[] center,double radius){return Physics.overlap(center,radius);} + Object explosion(double[] center,double radius,double strength){return Physics.explosion(center,radius,strength);} + double timeScale(double scale){return Clock.timeScale(scale);} + void pause(){Clock.pause();} + void resume(){Clock.resume();} + Path.Curve createPath(double[][] points,String kind){return Path.create(points,kind);} + Path.Curve createPath(List points,String kind){return Path.create(points,kind);} + Sequence.Builder sequence(){return Sequence.create();} + Sequence.Builder timeline(){return Sequence.create();} + Events.Connection on(String name,java.util.function.Consumer fn){return Events.on(name,fn);} + Events.Connection once(String name,java.util.function.Consumer fn){return Events.once(name,fn);} + void emit(String name,Object[] arguments,String source){Events.emit(name,arguments,source);} + void disconnect(Events.Connection c){Events.disconnect(c);} + String currentCamera(){return Camera.current();} + Object getOutputDevices(){return Audio.get_output_devices();} + Object get_output_devices(){return Audio.get_output_devices();} + Object getInputDevices(){return Audio.get_input_devices();} + Object get_input_devices(){return Audio.get_input_devices();} + void setOutputDevice(String device){Audio.set_output_device(device);} + void set_output_device(String device){Audio.set_output_device(device);} + void setInputDevice(String device){Audio.set_input_device(device);} + void set_input_device(String device){Audio.set_input_device(device);} + Object getEnvironment(){return Game.scene.getEnvironment();} + Object setEnvironment(Map settings){return Game.scene.setEnvironment(settings);} + void setCurrentCamera(String path){Game.setCurrentCamera(path);} + String entity_id(){return rustic.entity_id();} + double delta_time(){return rustic.delta_time();} + double fixed_delta_time(){return rustic.fixed_delta_time();} + double[] get_translation(){return rustic.get_translation();} + void set_translation(double x,double y,double z){rustic.set_translation(x,y,z);} + Object get_property(String n){return rustic.get_property(n);} + void set_property(String n,Object v){rustic.set_property(n,v);} + Object get_attribute(String n){return rustic.get_attribute(n);} + Object GetAttribute(String n){return rustic.GetAttribute(n);} + void edit_attribute(String n,Object v){rustic.edit_attribute(n,v);} + void EditAttribute(String n,Object v){rustic.EditAttribute(n,v);} + ActionState input(String n){return rustic.input(n);} + ActionState key(String n){return rustic.key(n);} + List key_events(){return rustic.key_events();} + boolean any_key_pressed(){return rustic.any_key_pressed();} + void log(String l,String m){rustic.log(l,m);} + void set_enabled(boolean e){rustic.set_enabled(e);} + } + static class CanonicalGame { + final CanonicalScenePath scene=new CanonicalScenePath("game.scene"); + final CanonicalGlobalApi rustic=new CanonicalGlobalApi(); + } } diff --git a/Engine/crates/engine-scripting/src/sdk/game-api.d.ts b/Engine/crates/engine-scripting/src/sdk/game-api.d.ts new file mode 100644 index 00000000..8ea379be --- /dev/null +++ b/Engine/crates/engine-scripting/src/sdk/game-api.d.ts @@ -0,0 +1,29 @@ +// Canonical roots. Named children use the scene and Explorer hierarchy. +interface RusticSceneObject { + [name: string]: any; + readonly __source: string; + EditAttribute(name: string, value: unknown): void; + edit_attribute(name: string, value: unknown): void; + tween(property: string, value: unknown, timing: number | { duration?: number; speed?: number; easing?: string }, easing?: string): any; + moveTo(position: number[], timing: number | { duration?: number; speed?: number; easing?: string }, easing?: string): any; + destroy(): void; +} +interface RusticExplorerFile { + [name: string]: any; + readonly __file_path: string; + play(options?: { volume?: number; pitch?: number; loop?: boolean }): any; + add(parent?: string): void; + clone(parent?: string): void; + setSkyTexture(): RusticSceneEnvironment; +} +interface RusticGlobalApi extends RusticApi { + [name: string]: any; + files: RusticExplorerFile; + after(delay: number, callback: () => void): any; + every(interval: number, callback: () => void, count?: number): any; + lerp(from: number | number[], to: number | number[], progress: number, easing?: string): number | number[]; + getEnvironment(): RusticSceneEnvironment; + setEnvironment(settings: Partial): RusticSceneEnvironment; + setCurrentCamera(object: string | RusticSceneObject): void; +} +declare const game: Readonly<{ scene: RusticSceneObject; rustic: RusticGlobalApi }>; diff --git a/Engine/crates/engine-scripting/src/sdk/game-api.lua b/Engine/crates/engine-scripting/src/sdk/game-api.lua new file mode 100644 index 00000000..93e90301 --- /dev/null +++ b/Engine/crates/engine-scripting/src/sdk/game-api.lua @@ -0,0 +1,26 @@ +-- Canonical roots for Lua 5.4 and Luau. +---@class RusticSceneObject +---@field [string] RusticSceneObject +---@field __source string +---@field EditAttribute fun(self:RusticSceneObject,name:string,value:any) +---@field tween fun(self:RusticSceneObject,property:string,value:any,timing:number|table,easing:string|nil):table +---@field moveTo fun(self:RusticSceneObject,position:number[],timing:number|table,easing:string|nil):table +---@field destroy fun(self:RusticSceneObject) +---@class RusticExplorerFile +---@field [string] RusticExplorerFile +---@field load fun(self:RusticExplorerFile):RusticExplorerFile +---@field play fun(self:RusticExplorerFile,options:table|nil):table +---@field Unload fun(self:RusticExplorerFile) +---@field setSkyTexture fun(self:RusticExplorerFile):RusticSceneEnvironment +---@class RusticGlobalApi +---@field files table +---@field after fun(self:RusticGlobalApi,delay:number,callback:fun()):table +---@field every fun(self:RusticGlobalApi,interval:number,callback:fun(),count:number|nil):table +---@field lerp fun(self:RusticGlobalApi,from:any,to:any,progress:number,easing:string|nil):any +---@field getEnvironment fun(self:RusticGlobalApi):RusticSceneEnvironment +---@field setEnvironment fun(self:RusticGlobalApi,settings:table):RusticSceneEnvironment +---@class CanonicalRusticGame +---@field scene table +---@field rustic RusticGlobalApi +---@type CanonicalRusticGame +game=game diff --git a/Engine/crates/engine-scripting/src/sdk/gameplay.js b/Engine/crates/engine-scripting/src/sdk/gameplay.js index 2397c6f2..49593fa5 100644 --- a/Engine/crates/engine-scripting/src/sdk/gameplay.js +++ b/Engine/crates/engine-scripting/src/sdk/gameplay.js @@ -12,6 +12,7 @@ if (object == null) return rustic.entity_id(); if (typeof object === 'object') object=object.__source||object.id||object.source; if (typeof object !== 'string') throw new TypeError('expected entity ID or scene path'); + if(object.startsWith('game.scene.'))return rustic.query({op:'scene_object',source:object}); const id = Game.scene.Find(object) || (/^[0-9a-f]{8}-/i.test(object) ? object : null); if (!id) throw new Error('object was not found: '+object); return id; }; @@ -139,7 +140,7 @@ const _rusticEntity = object => { if(object==null)return rustic.entity_id(); if(typeof object==="object")object=object.__source||object.id||object.source; if(typeof object!=="string")throw new TypeError("expected entity ID or path"); - return Game.scene.Find(object)||object; + return object.startsWith('game.scene.')?rustic.query({op:'scene_object',source:object}):Game.scene.Find(object)||object; }; // Entity services use the live shared Rust world. @@ -172,3 +173,202 @@ globalThis.World={ find_by_tag(name){return rustic.query({op:"world_find_by_tag",name:name});}, find_in_radius(center,radius){return rustic.query({op:"world_find_in_radius",center:center,radius:radius});}, }; + +// Canonical API paths. +(() => { + const sceneMethods={ + tweenTo:[Tween.to,0], + tweenMove:[Tween.move,0], + tweenRotate:[Tween.rotate,0], + tweenScale:[Tween.scale,0], + tweenAction:[Tween.action,0], + movementMoveTo:[Movement.moveTo,0], + movementMove:[Movement.move,0], + movementRotateTo:[Movement.rotateTo,0], + movementLookAt:[Movement.lookAt,0], + movementFollow:[Movement.follow,0], + movementOrbit:[Movement.orbit,0], + pathFollow:[Path.follow,0], + effectsFade:[Effects.fade,0], + effectsFlash:[Effects.flash,0], + effectsShake:[Effects.shake,0], + effectsPulse:[Effects.pulse,0], + cameraMoveTo:[Camera.moveTo,0], + cameraZoom:[Camera.zoom,0], + cameraFov:[Camera.fov,0], + cameraFollow:[Camera.follow,0], + cameraOrbit:[Camera.orbit,0], + cameraLookAt:[Camera.lookAt,0], + cameraShake:[Camera.shake,0], + cameraTransition:[Camera.transition,0], + physicsImpulse:[Physics.impulse,0], + physicsForce:[Physics.force,0], + physicsLaunch:[Physics.launch,0], + physicsKnockback:[Physics.knockback,0], + animationRegister:[Animation.register,0], + animationClips:[Animation.clips,0], + animationAction:[Animation.action,0], + animationPlay:[Animation.play,0], + animationBlend:[Animation.blend,0], + animationTransition:[Animation.transition,0], + animationIk:[Animation.ik,0], + animationFootPlacement:[Animation.footPlacement,0], + animationLookAt:[Animation.lookAt,0], + animationHeadTracking:[Animation.headTracking,0], + animationRecoil:[Animation.recoil,0], + animationAddMarker:[Animation.addMarker,0], + eventsConnect:[Events.connect,0], + tagsAdd:[Tags.add,0], + tagsRemove:[Tags.remove,0], + groupsAdd:[Groups.add,1], + hierarchySetParent:[Hierarchy.set_parent,0], + hierarchyGetChildren:[Hierarchy.get_children,0], + hierarchyDetach:[Hierarchy.detach,0], + entityDestroy:[Entity.destroy,0], + entityDestroyAfter:[Entity.destroy_after,0], + entityIsValid:[Entity.is_valid,0], + componentsHas:[Components.has,0], + componentsGetAll:[Components.get_all,0], + componentsFindInChildren:[Components.find_in_children,0], + tween:[Tween.to,0], + moveTo:[Movement.moveTo,0], + move:[Movement.move,0], + rotateTo:[Movement.rotateTo,0], + lookAt:[Movement.lookAt,0], + follow:[Movement.follow,0], + orbit:[Movement.orbit,0], + fade:[Effects.fade,0], + flash:[Effects.flash,0], + shake:[Effects.shake,0], + pulse:[Effects.pulse,0], + impulse:[Physics.impulse,0], + force:[Physics.force,0], + launch:[Physics.launch,0], + knockback:[Physics.knockback,0], + playAnimation:[Animation.play,0], + destroy:[Entity.destroy,0], + destroyAfter:[Entity.destroy_after,0], + isValid:[Entity.is_valid,0], + addTag:[Tags.add,0], + removeTag:[Tags.remove,0], + setParent:[Hierarchy.set_parent,0], + getChildren:[Hierarchy.get_children,0], + detach:[Hierarchy.detach,0], + hasComponent:[Components.has,0], + getComponents:[Components.get_all,0], + findInChildren:[Components.find_in_children,0], + followPath:[Path.follow,0], + }; + Object.assign(sceneMethods,{clone:[instance.clone,0],add:[instance.add,0],setCurrentCamera:[Game.setCurrentCamera,0],emit:[Events.emit,2]}); + const globalMethods={ + tweenValue:[Tween.value,0], + timerAfter:[Timer.after,0], + timerEvery:[Timer.every,0], + smoothLerp:[Smooth.lerp,0], + smoothSlerp:[Smooth.slerp,0], + smoothInverseLerp:[Smooth.inverseLerp,0], + smoothRemap:[Smooth.remap,0], + smoothSmoothDamp:[Smooth.smoothDamp,0], + physicsRaycast:[Physics.raycast,0], + physicsSphereCast:[Physics.sphereCast,0], + physicsOverlap:[Physics.overlap,0], + physicsExplosion:[Physics.explosion,0], + clockTimeScale:[Clock.timeScale,0], + clockPause:[Clock.pause,0], + clockResume:[Clock.resume,0], + clockState:[Clock.state,0], + pathCreate:[Path.create,0], + sequenceNew:[Sequence.new,0], + timelineNew:[Timeline.new,0], + eventsOn:[Events.on,0], + eventsOnce:[Events.once,0], + eventsEmit:[Events.emit,0], + eventsDisconnect:[Events.disconnect,0], + audioGetOutputDevices:[Audio.get_output_devices,0], + audioGetInputDevices:[Audio.get_input_devices,0], + audioSetOutputDevice:[Audio.set_output_device,0], + audioSetInputDevice:[Audio.set_input_device,0], + audioVolume:[Audio.volume,0], + audioPitch:[Audio.pitch,0], + audioFadeIn:[Audio.fadeIn,0], + audioFadeOut:[Audio.fadeOut,0], + audioCrossfade:[Audio.crossfade,0], + audioStop:[Audio.stop,0], + audioPause:[Audio.pause,0], + audioResume:[Audio.resume,0], + animationValue:[Animation.value,0], + animationStop:[Animation.stop,0], + animationPause:[Animation.pause,0], + animationSpeed:[Animation.speed,0], + animationLoop:[Animation.loop,0], + cameraCurrent:[Camera.current,0], + tagsFindAll:[Tags.find_all,0], + groupsCreate:[Groups.create,0], + groupsGetMembers:[Groups.get_members,0], + worldFindByTag:[World.find_by_tag,0], + worldFindInRadius:[World.find_in_radius,0], + after:[Timer.after,0], + every:[Timer.every,0], + lerp:[Smooth.lerp,0], + slerp:[Smooth.slerp,0], + inverseLerp:[Smooth.inverseLerp,0], + remap:[Smooth.remap,0], + smoothDamp:[Smooth.smoothDamp,0], + raycast:[Physics.raycast,0], + sphereCast:[Physics.sphereCast,0], + overlap:[Physics.overlap,0], + explosion:[Physics.explosion,0], + timeScale:[Clock.timeScale,0], + pause:[Clock.pause,0], + resume:[Clock.resume,0], + createPath:[Path.create,0], + sequence:[Sequence.new,0], + timeline:[Timeline.new,0], + on:[Events.on,0], + once:[Events.once,0], + emit:[Events.emit,0], + disconnect:[Events.disconnect,0], + currentCamera:[Camera.current,0], + getOutputDevices:[Audio.get_output_devices,0], + getInputDevices:[Audio.get_input_devices,0], + setOutputDevice:[Audio.set_output_device,0], + setInputDevice:[Audio.set_input_device,0], + }; + const fileMethods={ + audioPlay:[Audio.play,0], + audioPlayAt:[Audio.playAt,0], + audioLoad:[Audio.load,0], + animationLoad:[Animation.load,1], + play:[Audio.play,0], + playAt:[Audio.playAt,0], + load:[Audio.load,0], + loadAnimation:[Animation.load,1], + add:[instance.add,0], + clone:[instance.clone,0], + setSkyTexture:[Game.scene.setSkyTexture,0], + }; + const argument=v=>v && typeof v==='object' && typeof v.__file_path==='string'?v.__file_path:v; + const path=(source,filePath=null)=>{ + const cache=Object.create(null); + return new Proxy(cache,{get:(cache,key)=>{ + if(key==='__source')return source; + if(key==='__file_path')return filePath; + if(typeof key!=='string')return undefined; + if(Object.hasOwn(cache,key))return cache[key]; + if(key==='EditAttribute'||key==='edit_attribute')return cache[key]=(name,value)=>rustic.edit_object_attribute(source,name,value); + const binding=(filePath===null?sceneMethods:fileMethods)[key]; + if(binding){ + if(typeof binding[0]!=='function')throw new TypeError('API unavailable: '+key); + return cache[key]=(...args)=>{args=args.map(argument);while(args.lengthfn(...args); + api.getEnvironment=()=>Game.scene.getEnvironment(); + api.setEnvironment=settings=>Game.scene.setEnvironment(settings); + api.setCurrentCamera=object=>Game.setCurrentCamera(object && object.__source || object); + globalThis.game={scene:path('game.scene'),rustic:api}; +})(); diff --git a/Engine/crates/engine-scripting/src/sdk/gameplay.lua b/Engine/crates/engine-scripting/src/sdk/gameplay.lua index e001fcba..7b4e4489 100644 --- a/Engine/crates/engine-scripting/src/sdk/gameplay.lua +++ b/Engine/crates/engine-scripting/src/sdk/gameplay.lua @@ -27,6 +27,7 @@ local function entity(object) if object==nil then return rustic.entity_id() end if type(object)=='table' then object=rawget(object,'__source') or rawget(object,'id') end assert(type(object)=='string','expected an entity ID or scene path') + if string.sub(object,1,11)=='game.scene.' then return rustic.query({op='scene_object',source=object}) end local id=rustic.find_entity and rustic.find_entity(object) or Game.scene.Find(object) -- Stable UUIDs are already handles, not paths. if not id and string.match(object,'^%x%x%x%x%x%x%x%x%-') then id=object end @@ -369,3 +370,222 @@ function Components.find_in_children(entity,name) return rustic.query({op="compo World={} function World.find_by_tag(name) return rustic.query({op="world_find_by_tag",name=name}) end function World.find_in_radius(center,radius) return rustic.query({op="world_find_in_radius",center=center,radius=radius}) end + +-- Canonical API paths. +local sceneMethods={ + ['tweenTo']={Tween.to,0}, + ['tweenMove']={Tween.move,0}, + ['tweenRotate']={Tween.rotate,0}, + ['tweenScale']={Tween.scale,0}, + ['tweenAction']={Tween.action,0}, + ['movementMoveTo']={Movement.moveTo,0}, + ['movementMove']={Movement.move,0}, + ['movementRotateTo']={Movement.rotateTo,0}, + ['movementLookAt']={Movement.lookAt,0}, + ['movementFollow']={Movement.follow,0}, + ['movementOrbit']={Movement.orbit,0}, + ['pathFollow']={Path.follow,0}, + ['effectsFade']={Effects.fade,0}, + ['effectsFlash']={Effects.flash,0}, + ['effectsShake']={Effects.shake,0}, + ['effectsPulse']={Effects.pulse,0}, + ['cameraMoveTo']={Camera.moveTo,0}, + ['cameraZoom']={Camera.zoom,0}, + ['cameraFov']={Camera.fov,0}, + ['cameraFollow']={Camera.follow,0}, + ['cameraOrbit']={Camera.orbit,0}, + ['cameraLookAt']={Camera.lookAt,0}, + ['cameraShake']={Camera.shake,0}, + ['cameraTransition']={Camera.transition,0}, + ['physicsImpulse']={Physics.impulse,0}, + ['physicsForce']={Physics.force,0}, + ['physicsLaunch']={Physics.launch,0}, + ['physicsKnockback']={Physics.knockback,0}, + ['animationRegister']={Animation.register,0}, + ['animationClips']={Animation.clips,0}, + ['animationAction']={Animation.action,0}, + ['animationPlay']={Animation.play,0}, + ['animationBlend']={Animation.blend,0}, + ['animationTransition']={Animation.transition,0}, + ['animationIk']={Animation.ik,0}, + ['animationFootPlacement']={Animation.footPlacement,0}, + ['animationLookAt']={Animation.lookAt,0}, + ['animationHeadTracking']={Animation.headTracking,0}, + ['animationRecoil']={Animation.recoil,0}, + ['animationAddMarker']={Animation.addMarker,0}, + ['eventsConnect']={Events.connect,0}, + ['tagsAdd']={Tags.add,0}, + ['tagsRemove']={Tags.remove,0}, + ['groupsAdd']={Groups.add,1}, + ['hierarchySetParent']={Hierarchy.set_parent,0}, + ['hierarchyGetChildren']={Hierarchy.get_children,0}, + ['hierarchyDetach']={Hierarchy.detach,0}, + ['entityDestroy']={Entity.destroy,0}, + ['entityDestroyAfter']={Entity.destroy_after,0}, + ['entityIsValid']={Entity.is_valid,0}, + ['componentsHas']={Components.has,0}, + ['componentsGetAll']={Components.get_all,0}, + ['componentsFindInChildren']={Components.find_in_children,0}, + ['tween']={Tween.to,0}, + ['moveTo']={Movement.moveTo,0}, + ['move']={Movement.move,0}, + ['rotateTo']={Movement.rotateTo,0}, + ['lookAt']={Movement.lookAt,0}, + ['follow']={Movement.follow,0}, + ['orbit']={Movement.orbit,0}, + ['fade']={Effects.fade,0}, + ['flash']={Effects.flash,0}, + ['shake']={Effects.shake,0}, + ['pulse']={Effects.pulse,0}, + ['impulse']={Physics.impulse,0}, + ['force']={Physics.force,0}, + ['launch']={Physics.launch,0}, + ['knockback']={Physics.knockback,0}, + ['playAnimation']={Animation.play,0}, + ['destroy']={Entity.destroy,0}, + ['destroyAfter']={Entity.destroy_after,0}, + ['isValid']={Entity.is_valid,0}, + ['addTag']={Tags.add,0}, + ['removeTag']={Tags.remove,0}, + ['setParent']={Hierarchy.set_parent,0}, + ['getChildren']={Hierarchy.get_children,0}, + ['detach']={Hierarchy.detach,0}, + ['hasComponent']={Components.has,0}, + ['getComponents']={Components.get_all,0}, + ['findInChildren']={Components.find_in_children,0}, + ['followPath']={Path.follow,0}, +} +sceneMethods.clone={instance.clone,0} +sceneMethods.add={instance.add,0} +sceneMethods.setCurrentCamera={Game.setCurrentCamera,0} +sceneMethods.emit={Events.emit,2} +local globalMethods={ + ['tweenValue']={Tween.value,0}, + ['timerAfter']={Timer.after,0}, + ['timerEvery']={Timer.every,0}, + ['smoothLerp']={Smooth.lerp,0}, + ['smoothSlerp']={Smooth.slerp,0}, + ['smoothInverseLerp']={Smooth.inverseLerp,0}, + ['smoothRemap']={Smooth.remap,0}, + ['smoothSmoothDamp']={Smooth.smoothDamp,0}, + ['physicsRaycast']={Physics.raycast,0}, + ['physicsSphereCast']={Physics.sphereCast,0}, + ['physicsOverlap']={Physics.overlap,0}, + ['physicsExplosion']={Physics.explosion,0}, + ['clockTimeScale']={Clock.timeScale,0}, + ['clockPause']={Clock.pause,0}, + ['clockResume']={Clock.resume,0}, + ['clockState']={Clock.state,0}, + ['pathCreate']={Path.create,0}, + ['sequenceNew']={Sequence.new,0}, + ['timelineNew']={Timeline.new,0}, + ['eventsOn']={Events.on,0}, + ['eventsOnce']={Events.once,0}, + ['eventsEmit']={Events.emit,0}, + ['eventsDisconnect']={Events.disconnect,0}, + ['audioGetOutputDevices']={Audio.get_output_devices,0}, + ['audioGetInputDevices']={Audio.get_input_devices,0}, + ['audioSetOutputDevice']={Audio.set_output_device,0}, + ['audioSetInputDevice']={Audio.set_input_device,0}, + ['audioVolume']={Audio.volume,0}, + ['audioPitch']={Audio.pitch,0}, + ['audioFadeIn']={Audio.fadeIn,0}, + ['audioFadeOut']={Audio.fadeOut,0}, + ['audioCrossfade']={Audio.crossfade,0}, + ['animationValue']={Animation.value,0}, + ['animationStop']={Animation.stop,0}, + ['animationPause']={Animation.pause,0}, + ['animationSpeed']={Animation.speed,0}, + ['animationLoop']={Animation.loop,0}, + ['cameraCurrent']={Camera.current,0}, + ['tagsFindAll']={Tags.find_all,0}, + ['groupsCreate']={Groups.create,0}, + ['groupsGetMembers']={Groups.get_members,0}, + ['worldFindByTag']={World.find_by_tag,0}, + ['worldFindInRadius']={World.find_in_radius,0}, + ['after']={Timer.after,0}, + ['every']={Timer.every,0}, + ['lerp']={Smooth.lerp,0}, + ['slerp']={Smooth.slerp,0}, + ['inverseLerp']={Smooth.inverseLerp,0}, + ['remap']={Smooth.remap,0}, + ['smoothDamp']={Smooth.smoothDamp,0}, + ['raycast']={Physics.raycast,0}, + ['sphereCast']={Physics.sphereCast,0}, + ['overlap']={Physics.overlap,0}, + ['explosion']={Physics.explosion,0}, + ['timeScale']={Clock.timeScale,0}, + ['pause']={Clock.pause,0}, + ['resume']={Clock.resume,0}, + ['createPath']={Path.create,0}, + ['sequence']={Sequence.new,0}, + ['timeline']={Timeline.new,0}, + ['on']={Events.on,0}, + ['once']={Events.once,0}, + ['emit']={Events.emit,0}, + ['disconnect']={Events.disconnect,0}, + ['currentCamera']={Camera.current,0}, + ['getOutputDevices']={Audio.get_output_devices,0}, + ['getInputDevices']={Audio.get_input_devices,0}, + ['setOutputDevice']={Audio.set_output_device,0}, + ['setInputDevice']={Audio.set_input_device,0}, +} +local fileMethods={ + ['animationLoad']={Animation.load,1}, + ['loadAnimation']={Animation.load,1}, + ['add']={instance.add,0}, + ['clone']={instance.clone,0}, + ['setSkyTexture']={Game.scene.setSkyTexture,0}, +} +local function argument(value) + if type(value)=='table' and rawget(value,'__source') and string.sub(value.__source,1,18)=='game.rustic.files.' then + return rawget(value,'__file_path') + end + return value +end +function __rustic_path_method(proxy,name) + local source=rawget(proxy,'__source') + local methods + local target=proxy + if string.sub(source,1,11)=='game.scene.' then methods=sceneMethods + elseif string.sub(source,1,18)=='game.rustic.files.' then + methods=fileMethods;target=rawget(proxy,'__file_path') + else return nil end + if methods==fileMethods and (name=='startRecording' or name=='start_recording') then + local method=function(_,opts) return rustic.game:startRecording(target,opts) end + rawset(proxy,name,method);return method + end + local binding=methods[name] + if not binding then return nil end + local method=function(self,...) + assert(self==proxy,'call path APIs with :') + local args=table.pack(...) + for i=1,args.n do args[i]=argument(args[i]) end + args.n=math.max(args.n,binding[2]) + for i=args.n+1,binding[2]+2,-1 do args[i]=args[i-1] end + args[binding[2]+1]=target;args.n=args.n+1 + return binding[1](table.unpack(args,1,args.n)) + end + rawset(proxy,name,method) + return method +end +game={scene=__rustic_object_path('game.scene'),rustic={files=__rustic_object_path('game.rustic.files')}} +globalMethods.get_output_devices=globalMethods.getOutputDevices +globalMethods.get_input_devices=globalMethods.getInputDevices +globalMethods.set_output_device=globalMethods.setOutputDevice +globalMethods.set_input_device=globalMethods.setInputDevice +for name,binding in pairs(globalMethods) do + game.rustic[name]=function(_,...) return binding[1](...) end +end +for name,fn in pairs(rustic) do + if type(fn)=='function' then game.rustic[name]=function(_,...) return fn(...) end end +end +for _,name in ipairs({'getEnvironment','setEnvironment'}) do + game.rustic[name]=function(_,...) return Game.scene[name](...) end +end +function game.rustic:setCurrentCamera(object) + return Game.setCurrentCamera(entity(object)) +end +for _,name in ipairs({'startRecording','stopRecording','startHotMic','stopHotMic','start_recording','stop_recording','start_hot_mic','stop_hot_mic'}) do + game.rustic[name]=function(_,...) return rustic.game[name](rustic.game,...) end +end diff --git a/Engine/crates/engine-scripting/src/sdk/rustic.h b/Engine/crates/engine-scripting/src/sdk/rustic.h index 4f16302b..62c845b2 100644 --- a/Engine/crates/engine-scripting/src/sdk/rustic.h +++ b/Engine/crates/engine-scripting/src/sdk/rustic.h @@ -178,7 +178,7 @@ static void r_q_string(const char *key,const char *v){r_append(",");r_quote(key) static void r_q_value(const char *key,RusticValue v){r_append(",");r_quote(key);r_append(":");r_write_value(v);} static RusticValue r_vec(RusticVector3 v){return (RusticValue){.type=RUSTIC_VECTOR,.length=3,.vector={v.x,v.y,v.z}};} static RusticVector3 r_as_vec(RusticValue v){return (RusticVector3){v.vector[0],v.vector[1],v.vector[2]};} -static const char *r_resolve(const char *e){const char *id=e?r_find(e):NULL;return id?id:e?e:r_entity_id();} +static const char *r_resolve(const char *e); static RQueryResult r_query_end(RQuerySave saved){ r_append("}");fputs("{\"query\":",stdout);fputs(r_output,stdout);fputs(",\"commands\":",stdout); const char *array=strchr(saved.pending,'[');if(array)fputs(array,stdout);else fputs("[",stdout);fputs("]}\n",stdout);fflush(stdout); @@ -212,6 +212,10 @@ static RQueryResult r_query_end(RQuerySave saved){ free(saved.source);free(saved.pending);free(saved.tokens);r_written=r_commands=0;r_output[0]=0;r_append("{\"format_version\":1,\"commands\":[");return out; } /* Entity services: returned lists/strings live until the next callback. */ +static const char *r_resolve(const char *e){ + if(e && !strncmp(e,"game.scene.",11)){RQuerySave saved=r_query_begin("scene_object");r_q_string("source",e);return r_query_end(saved).value.string;} + const char *id=e?r_find(e):NULL;return id?id:e?e:r_entity_id(); +} static bool r_tags_add(const char* entity,const char* name){const char *resolved_entity=r_resolve(entity);RQuerySave s=r_query_begin("tags_add");r_q_string("entity",resolved_entity);r_q_string("name",name);return r_query_end(s).value.boolean;} static bool r_tags_remove(const char* entity,const char* name){const char *resolved_entity=r_resolve(entity);RQuerySave s=r_query_begin("tags_remove");r_q_string("entity",resolved_entity);r_q_string("name",name);return r_query_end(s).value.boolean;} static RusticList r_tags_find_all(const char* name){RQuerySave s=r_query_begin("tags_find_all");r_q_string("name",name);return r_query_end(s).list;} @@ -274,7 +278,7 @@ static void r_gameplay_start(RusticGameplayHandle id) { r_command("gameplay");r_append(",\"request\":{\"command\":\"start\",\"handle\":");r_gameplay_handle(id);r_append(",\"on_finished\":");r_gameplay_handle(id);r_append(",\"action\":"); } static RusticGameplayHandle r_tween_to(const char *entity,const char *property,RusticValue to,double duration,const char *easing,void (*finished)(void)) { - RusticGameplayHandle id=r_gameplay_new(finished);const char *resolved=entity?r_find(entity):NULL; + RusticGameplayHandle id=r_gameplay_new(finished);const char *resolved=r_resolve(entity); r_gameplay_start(id);r_append("{\"kind\":\"tween\",\"target\":{\"entity\":");r_quote(resolved?resolved:entity?entity:r_entity_id()); r_append(",\"property\":");r_quote(property);r_append("},\"to\":");r_write_value(to); r_append(",\"duration\":");r_double(duration);r_append(",\"easing\":");r_quote(easing?easing:"Linear");r_append("}}}");return id; @@ -439,9 +443,115 @@ static const struct {RusticGameplayHandle(*fade)(const char*,double,double,const static const struct {RusticGameplayHandle(*moveTo)(const char*,RusticVector3,double,const char*,void(*)(void)),(*zoom)(const char*,double,double,const char*,void(*)(void)),(*fov)(const char*,double,double,const char*,void(*)(void)),(*follow)(const char*,const char*,RusticVector3,double,const char*,void(*)(void)),(*lookAt)(const char*,RusticVector3,double,const char*,void(*)(void)),(*orbit)(const char*,RusticVector3,double,double,double,const char*,void(*)(void)),(*shake)(const char*,double,double,const char*,void(*)(void)),(*transition)(const char*,RusticVector3,RusticValue,double,double,const char*,void(*)(void));} Camera={r_tween_move,r_camera_zoom,r_camera_zoom,r_follow,r_look_at,r_orbit,r_shake,r_camera_transition}; static const struct {RusticOperationState(*state)(RusticGameplayHandle);void (*pause)(RusticGameplayHandle),(*resume)(RusticGameplayHandle),(*cancel)(RusticGameplayHandle),(*reverse)(RusticGameplayHandle),(*onFinished)(RusticGameplayHandle,void(*)(void)),(*speed)(RusticGameplayHandle,double),(*loop)(RusticGameplayHandle,bool);} Gameplay={r_gameplay_state,r_gameplay_pause,r_gameplay_resume,r_gameplay_cancel,r_gameplay_reverse,r_gameplay_finished,r_gameplay_speed,r_gameplay_loop}; static const struct {const char *Linear,*InSine,*OutSine,*InOutSine,*InQuad,*OutQuad,*InOutQuad,*InCubic,*OutCubic,*InOutCubic,*InQuart,*OutQuart,*InOutQuart,*InQuint,*OutQuint,*InOutQuint,*InExpo,*OutExpo,*InOutExpo,*InCirc,*OutCirc,*InOutCirc,*InBack,*OutBack,*InOutBack,*InElastic,*OutElastic,*InOutElastic,*InBounce,*OutBounce,*InOutBounce;} Ease={"Linear","InSine","OutSine","InOutSine","InQuad","OutQuad","InOutQuad","InCubic","OutCubic","InOutCubic","InQuart","OutQuart","InOutQuart","InQuint","OutQuint","InOutQuint","InExpo","OutExpo","InOutExpo","InCirc","OutCirc","InOutCirc","InBack","OutBack","InOutBack","InElastic","OutElastic","InOutElastic","InBounce","OutBounce","InOutBounce"}; + +/* Canonical API paths. C passes self explicitly. */ +typedef struct RusticScenePath RusticScenePath; +struct RusticScenePath {char source[4096];void(*clone)(RusticScenePath,const char*);void(*add)(RusticScenePath,const char*);void(*setCurrentCamera)(RusticScenePath);void(*emit)(RusticScenePath,const char*,const RusticValue*,size_t);RusticGameplayHandle(*tweenTo)(RusticScenePath self,const char *property,RusticValue to,double duration,const char *easing,void (*finished)(void));RusticGameplayHandle(*tweenMove)(RusticScenePath self,RusticVector3 p,double duration,const char *easing,void (*finished)(void));RusticGameplayHandle(*tweenRotate)(RusticScenePath self,RusticValue q,double d,const char *ease,void(*fn)(void));RusticGameplayHandle(*tweenScale)(RusticScenePath self,RusticVector3 scale,double d,const char *ease,void(*fn)(void));RusticAction(*tweenAction)(RusticScenePath self,const char *property,RusticValue to,double duration,const char *easing);RusticGameplayHandle(*tweenToOptions)(RusticScenePath self,const char *p,RusticValue to,RusticMotionOptions opts,void(*fn)(void));RusticGameplayHandle(*movementMoveTo)(RusticScenePath self,RusticVector3 p,double duration,const char *easing,void (*finished)(void));RusticGameplayHandle(*movementMove)(RusticScenePath self,RusticVector3 offset,double d,const char *ease,void(*fn)(void));RusticGameplayHandle(*movementRotateTo)(RusticScenePath self,RusticValue q,double d,const char *ease,void(*fn)(void));RusticGameplayHandle(*movementLookAt)(RusticScenePath self,RusticVector3 target,double d,const char *ease,void(*fn)(void));RusticGameplayHandle(*movementFollow)(RusticScenePath self,const char *target,RusticVector3 offset,double duration,const char *ease,void(*fn)(void));RusticGameplayHandle(*movementOrbit)(RusticScenePath self,RusticVector3 center,double radius,double turns,double duration,const char *ease,void(*fn)(void));RusticGameplayHandle(*movementMoveToOptions)(RusticScenePath self,RusticVector3 p,RusticMotionOptions opts,void(*fn)(void));RusticGameplayHandle(*pathFollow)(RusticScenePath self,RusticPath path,double duration,double speed,const char *ease,bool loop,bool pingPong,bool orient,void(*fn)(void));RusticGameplayHandle(*effectsFade)(RusticScenePath self,double opacity,double duration,const char *ease,void(*fn)(void));RusticGameplayHandle(*effectsFlash)(RusticScenePath self,RusticVector3 color,double duration,const char *ease,void(*fn)(void));RusticGameplayHandle(*effectsShake)(RusticScenePath self,double strength,double duration,const char *ease,void(*fn)(void));RusticGameplayHandle(*effectsPulse)(RusticScenePath self,RusticVector3 scale,double duration,const char *ease,void(*fn)(void));RusticGameplayHandle(*cameraMoveTo)(RusticScenePath self,RusticVector3 p,double duration,const char *easing,void (*finished)(void));RusticGameplayHandle(*cameraZoom)(RusticScenePath self,double fov,double duration,const char *ease,void(*fn)(void));RusticGameplayHandle(*cameraFov)(RusticScenePath self,double fov,double duration,const char *ease,void(*fn)(void));RusticGameplayHandle(*cameraFollow)(RusticScenePath self,const char *target,RusticVector3 offset,double duration,const char *ease,void(*fn)(void));RusticGameplayHandle(*cameraOrbit)(RusticScenePath self,RusticVector3 center,double radius,double turns,double duration,const char *ease,void(*fn)(void));RusticGameplayHandle(*cameraLookAt)(RusticScenePath self,RusticVector3 target,double d,const char *ease,void(*fn)(void));RusticGameplayHandle(*cameraShake)(RusticScenePath self,double strength,double duration,const char *ease,void(*fn)(void));RusticGameplayHandle(*cameraTransition)(RusticScenePath self,RusticVector3 p,RusticValue rotation,double fov,double duration,const char *ease,void(*fn)(void));void(*physicsImpulse)(RusticScenePath self,RusticVector3 v);void(*physicsForce)(RusticScenePath self,RusticVector3 v,double delta);void(*physicsLaunch)(RusticScenePath self,RusticVector3 v);void(*physicsKnockback)(RusticScenePath self,RusticVector3 v);void(*animationRegisterClip)(RusticScenePath self,RusticAnimationClip clip);RusticList(*animationClips)(RusticScenePath self);RusticGameplayHandle(*animationPlay)(RusticScenePath self,const char *name,const RusticAnimationOptions *opts,void(*fn)(void));RusticGameplayHandle(*animationBlend)(RusticScenePath self,const char *from,const char *to,double duration,const char *ease,void(*fn)(void));RusticGameplayHandle(*animationTransition)(RusticScenePath self,const char *from,const char *to,double duration,const char *ease,void(*fn)(void));RusticGameplayHandle(*animationIk)(RusticScenePath self,const char *middle,const char *tip,RusticVector3 target,RusticVector3 pole,double duration,double weight,const char *ease,void(*fn)(void));RusticGameplayHandle(*animationFootPlacement)(RusticScenePath self,const char *middle,const char *tip,RusticVector3 target,RusticVector3 pole,double duration,double weight,const char *ease,void(*fn)(void));RusticGameplayHandle(*animationLookAt)(RusticScenePath self,RusticVector3 target,double d,const char *ease,void(*fn)(void));RusticGameplayHandle(*animationHeadTracking)(RusticScenePath self,RusticVector3 target,double d,const char *ease,void(*fn)(void));RusticGameplayHandle(*animationRecoil)(RusticScenePath self,RusticValue rotation,double d,const char *ease,void(*fn)(void));void(*animationAddMarker)(RusticScenePath self,const char *clip,double time,const char *name);RusticConnection(*eventsConnect)(RusticScenePath self,const char *name,void(*fn)(const RusticValue*,size_t),bool once);bool(*tagsAdd)(RusticScenePath self,const char* name);bool(*tagsRemove)(RusticScenePath self,const char* name);bool(*groupsAdd)(RusticScenePath self,const char* name);void(*hierarchySetParent)(RusticScenePath self,const char* parent,bool keep_world);RusticList(*hierarchyGetChildren)(RusticScenePath self);void(*hierarchyDetach)(RusticScenePath self,bool keep_world);void(*entityDestroy)(RusticScenePath self);void(*entityDestroyAfter)(RusticScenePath self,double delay);bool(*entityIsValid)(RusticScenePath self);bool(*componentsHas)(RusticScenePath self,const char* name);RusticList(*componentsGetAll)(RusticScenePath self);RusticList(*componentsFindInChildren)(RusticScenePath self,const char* name);RusticGameplayHandle(*tween)(RusticScenePath self,const char *property,RusticValue to,double duration,const char *easing,void (*finished)(void));RusticGameplayHandle(*moveTo)(RusticScenePath self,RusticVector3 p,double duration,const char *easing,void (*finished)(void));RusticGameplayHandle(*move)(RusticScenePath self,RusticVector3 offset,double d,const char *ease,void(*fn)(void));RusticGameplayHandle(*rotateTo)(RusticScenePath self,RusticValue q,double d,const char *ease,void(*fn)(void));RusticGameplayHandle(*lookAt)(RusticScenePath self,RusticVector3 target,double d,const char *ease,void(*fn)(void));RusticGameplayHandle(*follow)(RusticScenePath self,const char *target,RusticVector3 offset,double duration,const char *ease,void(*fn)(void));RusticGameplayHandle(*orbit)(RusticScenePath self,RusticVector3 center,double radius,double turns,double duration,const char *ease,void(*fn)(void));RusticGameplayHandle(*fade)(RusticScenePath self,double opacity,double duration,const char *ease,void(*fn)(void));RusticGameplayHandle(*flash)(RusticScenePath self,RusticVector3 color,double duration,const char *ease,void(*fn)(void));RusticGameplayHandle(*shake)(RusticScenePath self,double strength,double duration,const char *ease,void(*fn)(void));RusticGameplayHandle(*pulse)(RusticScenePath self,RusticVector3 scale,double duration,const char *ease,void(*fn)(void));void(*impulse)(RusticScenePath self,RusticVector3 v);void(*force)(RusticScenePath self,RusticVector3 v,double delta);void(*launch)(RusticScenePath self,RusticVector3 v);void(*knockback)(RusticScenePath self,RusticVector3 v);RusticGameplayHandle(*playAnimation)(RusticScenePath self,const char *name,const RusticAnimationOptions *opts,void(*fn)(void));void(*destroy)(RusticScenePath self);void(*destroyAfter)(RusticScenePath self,double delay);bool(*isValid)(RusticScenePath self);bool(*addTag)(RusticScenePath self,const char* name);bool(*removeTag)(RusticScenePath self,const char* name);void(*setParent)(RusticScenePath self,const char* parent,bool keep_world);RusticList(*getChildren)(RusticScenePath self);void(*detach)(RusticScenePath self,bool keep_world);bool(*hasComponent)(RusticScenePath self,const char* name);RusticList(*getComponents)(RusticScenePath self);RusticList(*findInChildren)(RusticScenePath self,const char* name);RusticGameplayHandle(*followPath)(RusticScenePath self,RusticPath path,double duration,double speed,const char *ease,bool loop,bool pingPong,bool orient,void(*fn)(void));void(*EditAttribute)(RusticScenePath,const char*,RusticValue);}; +static RusticGameplayHandle r_path_scene_tweenTo(RusticScenePath self,const char *property,RusticValue to,double duration,const char *easing,void (*finished)(void)){return r_tween_to(self.source,property,to,duration,easing,finished);} +static RusticGameplayHandle r_path_scene_tweenMove(RusticScenePath self,RusticVector3 p,double duration,const char *easing,void (*finished)(void)){return r_tween_move(self.source,p,duration,easing,finished);} +static RusticGameplayHandle r_path_scene_tweenRotate(RusticScenePath self,RusticValue q,double d,const char *ease,void(*fn)(void)){return r_rotate_to(self.source,q,d,ease,fn);} +static RusticGameplayHandle r_path_scene_tweenScale(RusticScenePath self,RusticVector3 scale,double d,const char *ease,void(*fn)(void)){return r_scale_to(self.source,scale,d,ease,fn);} +static RusticAction r_path_scene_tweenAction(RusticScenePath self,const char *property,RusticValue to,double duration,const char *easing){return r_action_tween(self.source,property,to,duration,easing);} +static RusticGameplayHandle r_path_scene_tweenToOptions(RusticScenePath self,const char *p,RusticValue to,RusticMotionOptions opts,void(*fn)(void)){return r_tween_options(self.source,p,to,opts,fn);} +static RusticGameplayHandle r_path_scene_movementMoveTo(RusticScenePath self,RusticVector3 p,double duration,const char *easing,void (*finished)(void)){return r_tween_move(self.source,p,duration,easing,finished);} +static RusticGameplayHandle r_path_scene_movementMove(RusticScenePath self,RusticVector3 offset,double d,const char *ease,void(*fn)(void)){return r_move_relative(self.source,offset,d,ease,fn);} +static RusticGameplayHandle r_path_scene_movementRotateTo(RusticScenePath self,RusticValue q,double d,const char *ease,void(*fn)(void)){return r_rotate_to(self.source,q,d,ease,fn);} +static RusticGameplayHandle r_path_scene_movementLookAt(RusticScenePath self,RusticVector3 target,double d,const char *ease,void(*fn)(void)){return r_look_at(self.source,target,d,ease,fn);} +static RusticGameplayHandle r_path_scene_movementFollow(RusticScenePath self,const char *target,RusticVector3 offset,double duration,const char *ease,void(*fn)(void)){return r_follow(self.source,target,offset,duration,ease,fn);} +static RusticGameplayHandle r_path_scene_movementOrbit(RusticScenePath self,RusticVector3 center,double radius,double turns,double duration,const char *ease,void(*fn)(void)){return r_orbit(self.source,center,radius,turns,duration,ease,fn);} +static RusticGameplayHandle r_path_scene_movementMoveToOptions(RusticScenePath self,RusticVector3 p,RusticMotionOptions opts,void(*fn)(void)){return r_move_options(self.source,p,opts,fn);} +static RusticGameplayHandle r_path_scene_pathFollow(RusticScenePath self,RusticPath path,double duration,double speed,const char *ease,bool loop,bool pingPong,bool orient,void(*fn)(void)){return r_path_follow(self.source,path,duration,speed,ease,loop,pingPong,orient,fn);} +static RusticGameplayHandle r_path_scene_effectsFade(RusticScenePath self,double opacity,double duration,const char *ease,void(*fn)(void)){return r_fade(self.source,opacity,duration,ease,fn);} +static RusticGameplayHandle r_path_scene_effectsFlash(RusticScenePath self,RusticVector3 color,double duration,const char *ease,void(*fn)(void)){return r_flash(self.source,color,duration,ease,fn);} +static RusticGameplayHandle r_path_scene_effectsShake(RusticScenePath self,double strength,double duration,const char *ease,void(*fn)(void)){return r_shake(self.source,strength,duration,ease,fn);} +static RusticGameplayHandle r_path_scene_effectsPulse(RusticScenePath self,RusticVector3 scale,double duration,const char *ease,void(*fn)(void)){return r_pulse(self.source,scale,duration,ease,fn);} +static RusticGameplayHandle r_path_scene_cameraMoveTo(RusticScenePath self,RusticVector3 p,double duration,const char *easing,void (*finished)(void)){return r_tween_move(self.source,p,duration,easing,finished);} +static RusticGameplayHandle r_path_scene_cameraZoom(RusticScenePath self,double fov,double duration,const char *ease,void(*fn)(void)){return r_camera_zoom(self.source,fov,duration,ease,fn);} +static RusticGameplayHandle r_path_scene_cameraFov(RusticScenePath self,double fov,double duration,const char *ease,void(*fn)(void)){return r_camera_zoom(self.source,fov,duration,ease,fn);} +static RusticGameplayHandle r_path_scene_cameraFollow(RusticScenePath self,const char *target,RusticVector3 offset,double duration,const char *ease,void(*fn)(void)){return r_follow(self.source,target,offset,duration,ease,fn);} +static RusticGameplayHandle r_path_scene_cameraOrbit(RusticScenePath self,RusticVector3 center,double radius,double turns,double duration,const char *ease,void(*fn)(void)){return r_orbit(self.source,center,radius,turns,duration,ease,fn);} +static RusticGameplayHandle r_path_scene_cameraLookAt(RusticScenePath self,RusticVector3 target,double d,const char *ease,void(*fn)(void)){return r_look_at(self.source,target,d,ease,fn);} +static RusticGameplayHandle r_path_scene_cameraShake(RusticScenePath self,double strength,double duration,const char *ease,void(*fn)(void)){return r_shake(self.source,strength,duration,ease,fn);} +static RusticGameplayHandle r_path_scene_cameraTransition(RusticScenePath self,RusticVector3 p,RusticValue rotation,double fov,double duration,const char *ease,void(*fn)(void)){return r_camera_transition(self.source,p,rotation,fov,duration,ease,fn);} +static void r_path_scene_physicsImpulse(RusticScenePath self,RusticVector3 v){r_impulse(self.source,v);} +static void r_path_scene_physicsForce(RusticScenePath self,RusticVector3 v,double delta){r_force(self.source,v,delta);} +static void r_path_scene_physicsLaunch(RusticScenePath self,RusticVector3 v){r_launch(self.source,v);} +static void r_path_scene_physicsKnockback(RusticScenePath self,RusticVector3 v){r_impulse(self.source,v);} +static void r_path_scene_animationRegisterClip(RusticScenePath self,RusticAnimationClip clip){r_animation_register(self.source,clip);} +static RusticList r_path_scene_animationClips(RusticScenePath self){return r_animation_refs(self.source);} +static RusticGameplayHandle r_path_scene_animationPlay(RusticScenePath self,const char *name,const RusticAnimationOptions *opts,void(*fn)(void)){return r_animation_play(self.source,name,opts,fn);} +static RusticGameplayHandle r_path_scene_animationBlend(RusticScenePath self,const char *from,const char *to,double duration,const char *ease,void(*fn)(void)){return r_animation_blend(self.source,from,to,duration,ease,fn);} +static RusticGameplayHandle r_path_scene_animationTransition(RusticScenePath self,const char *from,const char *to,double duration,const char *ease,void(*fn)(void)){return r_animation_blend(self.source,from,to,duration,ease,fn);} +static RusticGameplayHandle r_path_scene_animationIk(RusticScenePath self,const char *middle,const char *tip,RusticVector3 target,RusticVector3 pole,double duration,double weight,const char *ease,void(*fn)(void)){return r_animation_ik(self.source,middle,tip,target,pole,duration,weight,ease,fn);} +static RusticGameplayHandle r_path_scene_animationFootPlacement(RusticScenePath self,const char *middle,const char *tip,RusticVector3 target,RusticVector3 pole,double duration,double weight,const char *ease,void(*fn)(void)){return r_animation_ik(self.source,middle,tip,target,pole,duration,weight,ease,fn);} +static RusticGameplayHandle r_path_scene_animationLookAt(RusticScenePath self,RusticVector3 target,double d,const char *ease,void(*fn)(void)){return r_look_at(self.source,target,d,ease,fn);} +static RusticGameplayHandle r_path_scene_animationHeadTracking(RusticScenePath self,RusticVector3 target,double d,const char *ease,void(*fn)(void)){return r_look_at(self.source,target,d,ease,fn);} +static RusticGameplayHandle r_path_scene_animationRecoil(RusticScenePath self,RusticValue rotation,double d,const char *ease,void(*fn)(void)){return r_recoil(self.source,rotation,d,ease,fn);} +static void r_path_scene_animationAddMarker(RusticScenePath self,const char *clip,double time,const char *name){r_animation_add_marker(self.source,clip,time,name);} +static RusticConnection r_path_scene_eventsConnect(RusticScenePath self,const char *name,void(*fn)(const RusticValue*,size_t),bool once){return r_events_connect(self.source,name,fn,once);} +static bool r_path_scene_tagsAdd(RusticScenePath self,const char* name){return r_tags_add(self.source,name);} +static bool r_path_scene_tagsRemove(RusticScenePath self,const char* name){return r_tags_remove(self.source,name);} +static bool r_path_scene_groupsAdd(RusticScenePath self,const char* name){return r_groups_add(name,self.source);} +static void r_path_scene_hierarchySetParent(RusticScenePath self,const char* parent,bool keep_world){r_hierarchy_set_parent(self.source,parent,keep_world);} +static RusticList r_path_scene_hierarchyGetChildren(RusticScenePath self){return r_hierarchy_get_children(self.source);} +static void r_path_scene_hierarchyDetach(RusticScenePath self,bool keep_world){r_hierarchy_detach(self.source,keep_world);} +static void r_path_scene_entityDestroy(RusticScenePath self){r_entity_destroy(self.source);} +static void r_path_scene_entityDestroyAfter(RusticScenePath self,double delay){r_entity_destroy_after(self.source,delay);} +static bool r_path_scene_entityIsValid(RusticScenePath self){return r_entity_is_valid(self.source);} +static bool r_path_scene_componentsHas(RusticScenePath self,const char* name){return r_components_has(self.source,name);} +static RusticList r_path_scene_componentsGetAll(RusticScenePath self){return r_components_get_all(self.source);} +static RusticList r_path_scene_componentsFindInChildren(RusticScenePath self,const char* name){return r_components_find_in_children(self.source,name);} +static RusticGameplayHandle r_path_scene_tween(RusticScenePath self,const char *property,RusticValue to,double duration,const char *easing,void (*finished)(void)){return r_tween_to(self.source,property,to,duration,easing,finished);} +static RusticGameplayHandle r_path_scene_moveTo(RusticScenePath self,RusticVector3 p,double duration,const char *easing,void (*finished)(void)){return r_tween_move(self.source,p,duration,easing,finished);} +static RusticGameplayHandle r_path_scene_move(RusticScenePath self,RusticVector3 offset,double d,const char *ease,void(*fn)(void)){return r_move_relative(self.source,offset,d,ease,fn);} +static RusticGameplayHandle r_path_scene_rotateTo(RusticScenePath self,RusticValue q,double d,const char *ease,void(*fn)(void)){return r_rotate_to(self.source,q,d,ease,fn);} +static RusticGameplayHandle r_path_scene_lookAt(RusticScenePath self,RusticVector3 target,double d,const char *ease,void(*fn)(void)){return r_look_at(self.source,target,d,ease,fn);} +static RusticGameplayHandle r_path_scene_follow(RusticScenePath self,const char *target,RusticVector3 offset,double duration,const char *ease,void(*fn)(void)){return r_follow(self.source,target,offset,duration,ease,fn);} +static RusticGameplayHandle r_path_scene_orbit(RusticScenePath self,RusticVector3 center,double radius,double turns,double duration,const char *ease,void(*fn)(void)){return r_orbit(self.source,center,radius,turns,duration,ease,fn);} +static RusticGameplayHandle r_path_scene_fade(RusticScenePath self,double opacity,double duration,const char *ease,void(*fn)(void)){return r_fade(self.source,opacity,duration,ease,fn);} +static RusticGameplayHandle r_path_scene_flash(RusticScenePath self,RusticVector3 color,double duration,const char *ease,void(*fn)(void)){return r_flash(self.source,color,duration,ease,fn);} +static RusticGameplayHandle r_path_scene_shake(RusticScenePath self,double strength,double duration,const char *ease,void(*fn)(void)){return r_shake(self.source,strength,duration,ease,fn);} +static RusticGameplayHandle r_path_scene_pulse(RusticScenePath self,RusticVector3 scale,double duration,const char *ease,void(*fn)(void)){return r_pulse(self.source,scale,duration,ease,fn);} +static void r_path_scene_impulse(RusticScenePath self,RusticVector3 v){r_impulse(self.source,v);} +static void r_path_scene_force(RusticScenePath self,RusticVector3 v,double delta){r_force(self.source,v,delta);} +static void r_path_scene_launch(RusticScenePath self,RusticVector3 v){r_launch(self.source,v);} +static void r_path_scene_knockback(RusticScenePath self,RusticVector3 v){r_impulse(self.source,v);} +static RusticGameplayHandle r_path_scene_playAnimation(RusticScenePath self,const char *name,const RusticAnimationOptions *opts,void(*fn)(void)){return r_animation_play(self.source,name,opts,fn);} +static void r_path_scene_destroy(RusticScenePath self){r_entity_destroy(self.source);} +static void r_path_scene_destroyAfter(RusticScenePath self,double delay){r_entity_destroy_after(self.source,delay);} +static bool r_path_scene_isValid(RusticScenePath self){return r_entity_is_valid(self.source);} +static bool r_path_scene_addTag(RusticScenePath self,const char* name){return r_tags_add(self.source,name);} +static bool r_path_scene_removeTag(RusticScenePath self,const char* name){return r_tags_remove(self.source,name);} +static void r_path_scene_setParent(RusticScenePath self,const char* parent,bool keep_world){r_hierarchy_set_parent(self.source,parent,keep_world);} +static RusticList r_path_scene_getChildren(RusticScenePath self){return r_hierarchy_get_children(self.source);} +static void r_path_scene_detach(RusticScenePath self,bool keep_world){r_hierarchy_detach(self.source,keep_world);} +static bool r_path_scene_hasComponent(RusticScenePath self,const char* name){return r_components_has(self.source,name);} +static RusticList r_path_scene_getComponents(RusticScenePath self){return r_components_get_all(self.source);} +static RusticList r_path_scene_findInChildren(RusticScenePath self,const char* name){return r_components_find_in_children(self.source,name);} +static RusticGameplayHandle r_path_scene_followPath(RusticScenePath self,RusticPath path,double duration,double speed,const char *ease,bool loop,bool pingPong,bool orient,void(*fn)(void)){return r_path_follow(self.source,path,duration,speed,ease,loop,pingPong,orient,fn);} +static void r_path_edit_attribute(RusticScenePath self,const char* name,RusticValue value){r_command("edit_attribute");r_append(",\"source\":");r_quote(self.source);r_append(",\"name\":");r_quote(name);r_append(",\"value\":");r_write_value(value);r_append("}");} +static void r_path_scene_clone(RusticScenePath self,const char* parent){r_clone(self.source,parent);} +static void r_path_scene_add(RusticScenePath self,const char* parent){r_add(self.source,parent);} +static void r_path_scene_camera(RusticScenePath self){r_camera(self.source);} +static void r_path_scene_emit(RusticScenePath self,const char* name,const RusticValue* args,size_t count){r_events_emit(name,args,count,self.source);} +static RusticScenePath r_game_scene(const char* scene,const char* path){RusticScenePath result={.source={0},.clone=r_path_scene_clone,.add=r_path_scene_add,.setCurrentCamera=r_path_scene_camera,.emit=r_path_scene_emit,.tweenTo=r_path_scene_tweenTo,.tweenMove=r_path_scene_tweenMove,.tweenRotate=r_path_scene_tweenRotate,.tweenScale=r_path_scene_tweenScale,.tweenAction=r_path_scene_tweenAction,.tweenToOptions=r_path_scene_tweenToOptions,.movementMoveTo=r_path_scene_movementMoveTo,.movementMove=r_path_scene_movementMove,.movementRotateTo=r_path_scene_movementRotateTo,.movementLookAt=r_path_scene_movementLookAt,.movementFollow=r_path_scene_movementFollow,.movementOrbit=r_path_scene_movementOrbit,.movementMoveToOptions=r_path_scene_movementMoveToOptions,.pathFollow=r_path_scene_pathFollow,.effectsFade=r_path_scene_effectsFade,.effectsFlash=r_path_scene_effectsFlash,.effectsShake=r_path_scene_effectsShake,.effectsPulse=r_path_scene_effectsPulse,.cameraMoveTo=r_path_scene_cameraMoveTo,.cameraZoom=r_path_scene_cameraZoom,.cameraFov=r_path_scene_cameraFov,.cameraFollow=r_path_scene_cameraFollow,.cameraOrbit=r_path_scene_cameraOrbit,.cameraLookAt=r_path_scene_cameraLookAt,.cameraShake=r_path_scene_cameraShake,.cameraTransition=r_path_scene_cameraTransition,.physicsImpulse=r_path_scene_physicsImpulse,.physicsForce=r_path_scene_physicsForce,.physicsLaunch=r_path_scene_physicsLaunch,.physicsKnockback=r_path_scene_physicsKnockback,.animationRegisterClip=r_path_scene_animationRegisterClip,.animationClips=r_path_scene_animationClips,.animationPlay=r_path_scene_animationPlay,.animationBlend=r_path_scene_animationBlend,.animationTransition=r_path_scene_animationTransition,.animationIk=r_path_scene_animationIk,.animationFootPlacement=r_path_scene_animationFootPlacement,.animationLookAt=r_path_scene_animationLookAt,.animationHeadTracking=r_path_scene_animationHeadTracking,.animationRecoil=r_path_scene_animationRecoil,.animationAddMarker=r_path_scene_animationAddMarker,.eventsConnect=r_path_scene_eventsConnect,.tagsAdd=r_path_scene_tagsAdd,.tagsRemove=r_path_scene_tagsRemove,.groupsAdd=r_path_scene_groupsAdd,.hierarchySetParent=r_path_scene_hierarchySetParent,.hierarchyGetChildren=r_path_scene_hierarchyGetChildren,.hierarchyDetach=r_path_scene_hierarchyDetach,.entityDestroy=r_path_scene_entityDestroy,.entityDestroyAfter=r_path_scene_entityDestroyAfter,.entityIsValid=r_path_scene_entityIsValid,.componentsHas=r_path_scene_componentsHas,.componentsGetAll=r_path_scene_componentsGetAll,.componentsFindInChildren=r_path_scene_componentsFindInChildren,.tween=r_path_scene_tween,.moveTo=r_path_scene_moveTo,.move=r_path_scene_move,.rotateTo=r_path_scene_rotateTo,.lookAt=r_path_scene_lookAt,.follow=r_path_scene_follow,.orbit=r_path_scene_orbit,.fade=r_path_scene_fade,.flash=r_path_scene_flash,.shake=r_path_scene_shake,.pulse=r_path_scene_pulse,.impulse=r_path_scene_impulse,.force=r_path_scene_force,.launch=r_path_scene_launch,.knockback=r_path_scene_knockback,.playAnimation=r_path_scene_playAnimation,.destroy=r_path_scene_destroy,.destroyAfter=r_path_scene_destroyAfter,.isValid=r_path_scene_isValid,.addTag=r_path_scene_addTag,.removeTag=r_path_scene_removeTag,.setParent=r_path_scene_setParent,.getChildren=r_path_scene_getChildren,.detach=r_path_scene_detach,.hasComponent=r_path_scene_hasComponent,.getComponents=r_path_scene_getComponents,.findInChildren=r_path_scene_findInChildren,.followPath=r_path_scene_followPath,.EditAttribute=r_path_edit_attribute};if(snprintf(result.source,sizeof result.source,"game.scene.%s.%s",scene,path)>=(int)sizeof result.source)r_fail("scene path too long");return result;} +typedef struct RusticFilePath RusticFilePath; +struct RusticFilePath {char source[4096];RusticAudioVoice(*audioPlay)(RusticFilePath self,const RusticAudioOptions *opts);RusticAudioVoice(*audioPlayAt)(RusticFilePath self,RusticVector3 position,const RusticAudioOptions *opts);RusticList(*animationLoad)(RusticFilePath self,const char *e);RusticAudioVoice(*play)(RusticFilePath self,const RusticAudioOptions *opts);RusticAudioVoice(*playAt)(RusticFilePath self,RusticVector3 position,const RusticAudioOptions *opts);RusticList(*loadAnimation)(RusticFilePath self,const char *e);void(*add)(RusticFilePath self,const char *p);void(*clone)(RusticFilePath self,const char *p);void(*setSkyTexture)(RusticFilePath self);}; +static RusticAudioVoice r_path_file_audioPlay(RusticFilePath self,const RusticAudioOptions *opts){return r_audio_play(self.source,opts);} +static RusticAudioVoice r_path_file_audioPlayAt(RusticFilePath self,RusticVector3 position,const RusticAudioOptions *opts){return r_audio_at(self.source,position,opts);} +static RusticList r_path_file_animationLoad(RusticFilePath self,const char *e){return r_animation_load(e,self.source);} +static RusticAudioVoice r_path_file_play(RusticFilePath self,const RusticAudioOptions *opts){return r_audio_play(self.source,opts);} +static RusticAudioVoice r_path_file_playAt(RusticFilePath self,RusticVector3 position,const RusticAudioOptions *opts){return r_audio_at(self.source,position,opts);} +static RusticList r_path_file_loadAnimation(RusticFilePath self,const char *e){return r_animation_load(e,self.source);} +static void r_path_file_add(RusticFilePath self,const char *p){r_add(self.source,p);} +static void r_path_file_clone(RusticFilePath self,const char *p){r_clone(self.source,p);} +static void r_path_file_setSkyTexture(RusticFilePath self){r_environment_sky(self.source);} +static RusticFilePath r_game_file(const char* path){RusticFilePath result={.source={0},.audioPlay=r_path_file_audioPlay,.audioPlayAt=r_path_file_audioPlayAt,.animationLoad=r_path_file_animationLoad,.play=r_path_file_play,.playAt=r_path_file_playAt,.loadAnimation=r_path_file_loadAnimation,.add=r_path_file_add,.clone=r_path_file_clone,.setSkyTexture=r_path_file_setSkyTexture};if(snprintf(result.source,sizeof result.source,"%s",path)>=(int)sizeof result.source)r_fail("file path too long");return result;} +static const struct {RusticScenePath(*scene)(const char*,const char*);struct {RusticFilePath(*files)(const char*);RusticGameplayHandle(*tweenValue)(RusticValue from,RusticValue to,double duration,const char *ease,void(*sample)(RusticValue),void(*finished)(void));RusticGameplayHandle(*timerAfter)(double duration,void (*finished)(void));RusticGameplayHandle(*timerEvery)(double interval,void(*fn)(void),unsigned count);RusticValue(*smoothLerp)(RusticValue a,RusticValue b,double progress,const char *easing);RusticValue(*smoothSlerp)(RusticValue a,RusticValue b,double progress,const char *easing);double(*smoothInverseLerp)(double a,double b,double v);double(*smoothRemap)(double v,double a,double b,double c,double d);RusticDamping(*smoothSmoothDamp)(double current,double target,double velocity,double smoothTime,double delta);RusticHit(*physicsRaycast)(RusticVector3 origin,RusticVector3 direction,double distance);RusticHit(*physicsSphereCast)(RusticVector3 origin,RusticVector3 direction,double distance,double radius);RusticList(*physicsOverlap)(RusticVector3 center,double radius);RusticList(*physicsExplosion)(RusticVector3 center,double radius,double strength);void(*clockTimeScale)(double value);void(*clockPause)(void);void(*clockResume)(void);RusticPath(*pathCreate)(const RusticPathPoint *points,size_t count,const char *kind);RusticSequence(*sequenceNew)(void);RusticSequence(*sequenceCreate)(void);RusticSequence(*timelineNew)(void);RusticSequence(*timelineCreate)(void);RusticConnection(*eventsOn)(const char *name,void(*fn)(const RusticValue*,size_t));RusticConnection(*eventsOnce)(const char *name,void(*fn)(const RusticValue*,size_t));void(*eventsEmit)(const char *name,const RusticValue *arguments,size_t count,const char *source);void(*eventsDisconnect)(RusticConnection token);RusticAudioDevices(*audioGetOutputDevices)(void);RusticAudioDevices(*audioGetInputDevices)(void);void(*audioSetOutputDevice)(const char *device);void(*audioSetInputDevice)(const char *device);void(*audioVolume)(RusticAudioVoice v,double value);void(*audioPitch)(RusticAudioVoice v,double value);RusticGameplayHandle(*audioFadeIn)(RusticAudioVoice v,double duration,const char *ease,void(*fn)(void));RusticGameplayHandle(*audioFadeOut)(RusticAudioVoice v,double duration,const char *ease,void(*fn)(void));RusticGameplayHandle(*audioCrossfade)(RusticAudioVoice a,RusticAudioVoice b,double duration,const char *ease,void(*fn)(void));void(*audioStop)(RusticAudioVoice v);void(*audioPause)(RusticAudioVoice v);void(*audioResume)(RusticAudioVoice v);RusticGameplayHandle(*animationValue)(const RusticKeyframe *keys,size_t count,void(*sample)(RusticValue),void(*finished)(void));void(*animationStop)(RusticGameplayHandle id);void(*animationPause)(RusticGameplayHandle id);void(*animationSpeed)(RusticGameplayHandle h,double speed);void(*animationLoop)(RusticGameplayHandle h,bool looping);RusticList(*tagsFindAll)(const char* name);bool(*groupsCreate)(const char* name);RusticList(*groupsGetMembers)(const char* name);RusticList(*worldFindByTag)(const char* name);RusticList(*worldFindInRadius)(RusticVector3 center,double radius);RusticGameplayHandle(*after)(double duration,void (*finished)(void));RusticGameplayHandle(*every)(double interval,void(*fn)(void),unsigned count);RusticValue(*lerp)(RusticValue a,RusticValue b,double progress,const char *easing);RusticValue(*slerp)(RusticValue a,RusticValue b,double progress,const char *easing);double(*inverseLerp)(double a,double b,double v);double(*remap)(double v,double a,double b,double c,double d);RusticDamping(*smoothDamp)(double current,double target,double velocity,double smoothTime,double delta);RusticHit(*raycast)(RusticVector3 origin,RusticVector3 direction,double distance);RusticHit(*sphereCast)(RusticVector3 origin,RusticVector3 direction,double distance,double radius);RusticList(*overlap)(RusticVector3 center,double radius);RusticList(*explosion)(RusticVector3 center,double radius,double strength);void(*timeScale)(double value);void(*pause)(void);void(*resume)(void);RusticPath(*createPath)(const RusticPathPoint *points,size_t count,const char *kind);RusticSequence(*sequence)(void);RusticSequence(*timeline)(void);RusticConnection(*on)(const char *name,void(*fn)(const RusticValue*,size_t));RusticConnection(*once)(const char *name,void(*fn)(const RusticValue*,size_t));void(*emit)(const char *name,const RusticValue *arguments,size_t count,const char *source);void(*disconnect)(RusticConnection token);RusticAudioDevices(*getOutputDevices)(void);RusticAudioDevices(*get_output_devices)(void);RusticAudioDevices(*getInputDevices)(void);RusticAudioDevices(*get_input_devices)(void);void(*setOutputDevice)(const char *device);void(*set_output_device)(const char *device);void(*setInputDevice)(const char *device);void(*set_input_device)(const char *device);const char *(*entity_id)(void);double(*delta_time)(void);double(*fixed_delta_time)(void);RusticVector3(*get_translation)(void);void(*set_translation)(double x,double y,double z);RusticValue(*get_property)(const char *n);RusticValue(*get_attribute)(const char *n);void(*set_property)(const char *n,RusticValue v);void(*edit_attribute)(const char *n,RusticValue v);RusticActionState(*input)(const char *n);RusticActionState(*key)(const char *n);RusticKeyEvents(*key_events)(void);bool(*any_key_pressed)(void);void(*log)(const char *l,const char *m);void(*set_enabled)(bool e);RusticValue(*getEnvironment)(const char *field);void(*setEnvironment)(const char *field,RusticValue value);void(*setCurrentCamera)(const char *s);} rustic;} game={r_game_scene,{r_game_file,r_tween_value,r_timer_after,r_timer_every,r_smooth_lerp,r_smooth_slerp,r_inverse_lerp,r_remap,r_smooth_damp,r_raycast,r_sphere_cast,r_overlap,r_explosion,r_clock_scale,r_clock_pause,r_clock_resume,r_path_create,r_sequence_new,r_sequence_new,r_sequence_new,r_sequence_new,r_events_on,r_events_once,r_events_emit,r_events_disconnect,r_audio_get_output_devices,r_audio_get_input_devices,r_audio_set_output_device,r_audio_set_input_device,r_audio_volume,r_audio_pitch,r_audio_fade_in,r_audio_fade_out,r_audio_crossfade,r_audio_stop,r_audio_pause,r_audio_resume,r_animation_value,r_gameplay_cancel,r_gameplay_pause,r_gameplay_speed,r_gameplay_loop,r_tags_find_all,r_groups_create,r_groups_get_members,r_world_find_by_tag,r_world_find_in_radius,r_timer_after,r_timer_every,r_smooth_lerp,r_smooth_slerp,r_inverse_lerp,r_remap,r_smooth_damp,r_raycast,r_sphere_cast,r_overlap,r_explosion,r_clock_scale,r_clock_pause,r_clock_resume,r_path_create,r_sequence_new,r_sequence_new,r_events_on,r_events_once,r_events_emit,r_events_disconnect,r_audio_get_output_devices,r_audio_get_output_devices,r_audio_get_input_devices,r_audio_get_input_devices,r_audio_set_output_device,r_audio_set_output_device,r_audio_set_input_device,r_audio_set_input_device,r_entity_id,r_delta_time,r_fixed_delta_time,r_translation,r_set_translation,r_property,r_attribute,r_set_property,r_edit_attribute,r_input,r_key,r_events,r_any,r_log,r_enabled,r_environment_get,r_environment_set,r_camera}}; + static int rustic_run(RusticBehavior b) { /* Reference exported objects even in an empty behavior to support -Werror. */ - (void)Animation;(void)Audio;(void)Events;(void)Sequence;(void)Timeline;(void)Path;(void)Effects;(void)Clock;(void)Camera;(void)Smooth;(void)Interpolation;(void)Physics;(void)rustic;(void)instance;(void)Game;(void)Tween;(void)Movement;(void)Timer;(void)Gameplay;(void)Ease; + (void)game;(void)Animation;(void)Audio;(void)Events;(void)Sequence;(void)Timeline;(void)Path;(void)Effects;(void)Clock;(void)Camera;(void)Smooth;(void)Interpolation;(void)Physics;(void)rustic;(void)instance;(void)Game;(void)Tween;(void)Movement;(void)Timer;(void)Gameplay;(void)Ease; while(fgets(r_source,sizeof r_source,stdin)) { for(size_t i=0;itokens as $t){unset(self::$callbacks[$t],self::$once[$t]);}unset(self::$handles[$h->id]);} private static function collect(array $a,array &$tokens):void{foreach(["token","marker_token"] as $key)if(isset($a[$key]))$tokens[]=$a[$key];foreach($a["actions"]??[] as $child)self::collect($child,$tokens);} public static function send(array $request): void {global $instance;$instance->commands[]=["op"=>"gameplay","request"=>$request];} - public static function entity(?string $id): string {global $rustic,$Game;return $id===null?$rustic->entity_id():($Game->scene->Find($id)??$id);} + public static function entity(?string $id): string {global $rustic,$Game;return $id===null?$rustic->entity_id():(str_starts_with($id,"game.scene.")?$rustic->query(["op"=>"scene_object","source"=>$id]):($Game->scene->Find($id)??$id));} public static function start(array $action,bool $loop=false,int $repeats=0,bool $pingPong=false): GameplayHandle { $id=bin2hex(random_bytes(16));$h=new GameplayHandle($id);self::$handles[$id]=$h;self::collect($action,$h->tokens); self::send(["command"=>"start","handle"=>$id,"action"=>$action,"on_finished"=>$id,"playback"=>["looping"=>$loop,"repeats"=>$repeats,"ping_pong"=>$pingPong]]);return $h; @@ -298,3 +298,54 @@ final class World { static function find_by_tag(string $name):array {global $rustic;return $rustic->query(["op"=>"world_find_by_tag","name"=>$name]);} static function find_in_radius(array $center,float $radius):array {global $rustic;return $rustic->query(["op"=>"world_find_in_radius","center"=>$center,"radius"=>$radius]);} } + +// Canonical API paths. +final class ApiPath { + private array $children=[]; + public function __construct(public string $source,private string $category,public ?string $filePath=null){} + public function __get(string $name): ApiPath { return $this->children[$name]??=new ApiPath($this->source.'.'.$name,$this->category,$this->category==='file'?($this->filePath? $this->filePath.'/':'').$name:null); } + public function __call(string $name,array $args):mixed { + global $Game,$instance; + if($name==='EditAttribute'||$name==='edit_attribute'){$instance->commands[]=['op'=>'edit_attribute','source'=>$this->source,'name'=>$args[0],'value'=>$args[1]];return null;} + $methods=$this->category==='scene'?["tweenTo"=>["Tween","to",0],"tweenMove"=>["Tween","move",0],"tweenRotate"=>["Tween","rotate",0],"tweenScale"=>["Tween","scale",0],"tweenAction"=>["Tween","action",0],"tweenToOptions"=>["Tween","toOptions",0],"movementMoveTo"=>["Movement","moveTo",0],"movementMove"=>["Movement","move",0],"movementRotateTo"=>["Movement","rotateTo",0],"movementLookAt"=>["Movement","lookAt",0],"movementFollow"=>["Movement","follow",0],"movementOrbit"=>["Movement","orbit",0],"movementMoveToOptions"=>["Movement","moveToOptions",0],"pathFollow"=>["Path","follow",0],"effectsFade"=>["Effects","fade",0],"effectsFlash"=>["Effects","flash",0],"effectsShake"=>["Effects","shake",0],"effectsPulse"=>["Effects","pulse",0],"cameraMoveTo"=>["Camera","moveTo",0],"cameraZoom"=>["Camera","zoom",0],"cameraFov"=>["Camera","fov",0],"cameraFollow"=>["Camera","follow",0],"cameraOrbit"=>["Camera","orbit",0],"cameraLookAt"=>["Camera","lookAt",0],"cameraShake"=>["Camera","shake",0],"cameraTransition"=>["Camera","transition",0],"physicsImpulse"=>["Physics","impulse",0],"physicsForce"=>["Physics","force",0],"physicsLaunch"=>["Physics","launch",0],"physicsKnockback"=>["Physics","knockback",0],"animationRegister"=>["Animation","register",0],"animationRegisterClip"=>["Animation","registerClip",0],"animationClips"=>["Animation","clips",0],"animationAction"=>["Animation","action",0],"animationPlay"=>["Animation","play",0],"animationBlend"=>["Animation","blend",0],"animationTransition"=>["Animation","transition",0],"animationIk"=>["Animation","ik",0],"animationFootPlacement"=>["Animation","footPlacement",0],"animationLookAt"=>["Animation","lookAt",0],"animationHeadTracking"=>["Animation","headTracking",0],"animationRecoil"=>["Animation","recoil",0],"animationAddMarker"=>["Animation","addMarker",0],"eventsConnect"=>["Events","connect",0],"tagsAdd"=>["Tags","add",0],"tagsRemove"=>["Tags","remove",0],"groupsAdd"=>["Groups","add",1],"hierarchySetParent"=>["Hierarchy","set_parent",0],"hierarchyGetChildren"=>["Hierarchy","get_children",0],"hierarchyDetach"=>["Hierarchy","detach",0],"entityDestroy"=>["Entity","destroy",0],"entityDestroyAfter"=>["Entity","destroy_after",0],"entityIsValid"=>["Entity","is_valid",0],"componentsHas"=>["Components","has",0],"componentsGetAll"=>["Components","get_all",0],"componentsFindInChildren"=>["Components","find_in_children",0],"tween"=>["Tween","to",0],"moveTo"=>["Movement","moveTo",0],"move"=>["Movement","move",0],"rotateTo"=>["Movement","rotateTo",0],"lookAt"=>["Movement","lookAt",0],"follow"=>["Movement","follow",0],"orbit"=>["Movement","orbit",0],"fade"=>["Effects","fade",0],"flash"=>["Effects","flash",0],"shake"=>["Effects","shake",0],"pulse"=>["Effects","pulse",0],"impulse"=>["Physics","impulse",0],"force"=>["Physics","force",0],"launch"=>["Physics","launch",0],"knockback"=>["Physics","knockback",0],"playAnimation"=>["Animation","play",0],"destroy"=>["Entity","destroy",0],"destroyAfter"=>["Entity","destroy_after",0],"isValid"=>["Entity","is_valid",0],"addTag"=>["Tags","add",0],"removeTag"=>["Tags","remove",0],"setParent"=>["Hierarchy","set_parent",0],"getChildren"=>["Hierarchy","get_children",0],"detach"=>["Hierarchy","detach",0],"hasComponent"=>["Components","has",0],"getComponents"=>["Components","get_all",0],"findInChildren"=>["Components","find_in_children",0],"followPath"=>["Path","follow",0]]:["audioPlay"=>["Audio","play",0],"audioPlayAt"=>["Audio","playAt",0],"audioLoad"=>["Audio","load",0],"animationLoad"=>["Animation","load",1],"play"=>["Audio","play",0],"playAt"=>["Audio","playAt",0],"load"=>["Audio","load",0],"loadAnimation"=>["Animation","load",1],"add"=>["instance","add",0],"clone"=>["instance","clone",0],"setSkyTexture"=>["scene","setSkyTexture",0]]; + if($this->category==='scene')$methods+=['clone'=>['instance','clone',0],'add'=>['instance','add',0],'setCurrentCamera'=>['Game','setCurrentCamera',0],'emit'=>['Events','emit',2]]; + if(!isset($methods[$name]))throw new BadMethodCallException('Unknown path API: '.$name); + [$family,$method,$index]=$methods[$name]; + $args=array_map(fn($a)=>$a instanceof ApiPath?($a->filePath??$a->source):$a,$args); + $owner=$family==='scene'?$Game->scene:($family==='Game'?$Game:($family==='instance'?$instance:$family)); + $parameters=(new ReflectionMethod($owner,$method))->getParameters(); + $targetName=$parameters[$index]->getName(); + array_splice($parameters,$index,1); + $bound=[]; + foreach($args as $key=>$value){ + $name=is_int($key)?($parameters[$key]??throw new InvalidArgumentException('Too many path API arguments'))->getName():$key; + if($name===$targetName)throw new InvalidArgumentException('The path supplies the target argument'); + $bound[$name]=$value; + } + $bound[$targetName]=$this->filePath??$this->source; + $args=$bound; + if($family==='Game')return $Game->$method(...$args); + if($family==='scene')return $Game->scene->$method(...$args); + if($family==='instance')return $instance->$method(...$args); + return $family::$method(...$args); + } +} +final class CanonicalGlobalApi { + public ApiPath $files; + public function __construct(){$this->files=new ApiPath('game.rustic.files','file','');} + public function __call(string $name,array $args):mixed { + global $rustic,$Game; + $name=['get_output_devices'=>'getOutputDevices','get_input_devices'=>'getInputDevices','set_output_device'=>'setOutputDevice','set_input_device'=>'setInputDevice'][$name]??$name; + $methods=["tweenValue"=>["Tween","value",0],"timerAfter"=>["Timer","after",0],"timerEvery"=>["Timer","every",0],"smoothLerp"=>["Smooth","lerp",0],"smoothSlerp"=>["Smooth","slerp",0],"smoothInverseLerp"=>["Smooth","inverseLerp",0],"smoothRemap"=>["Smooth","remap",0],"smoothSmoothDamp"=>["Smooth","smoothDamp",0],"physicsRaycast"=>["Physics","raycast",0],"physicsSphereCast"=>["Physics","sphereCast",0],"physicsOverlap"=>["Physics","overlap",0],"physicsExplosion"=>["Physics","explosion",0],"clockTimeScale"=>["Clock","timeScale",0],"clockPause"=>["Clock","pause",0],"clockResume"=>["Clock","resume",0],"clockState"=>["Clock","state",0],"pathCreate"=>["Path","create",0],"sequenceNew"=>["Sequence","new",0],"sequenceCreate"=>["Sequence","create",0],"timelineNew"=>["Timeline","new",0],"timelineCreate"=>["Timeline","create",0],"eventsOn"=>["Events","on",0],"eventsOnce"=>["Events","once",0],"eventsEmit"=>["Events","emit",0],"eventsDisconnect"=>["Events","disconnect",0],"audioGetOutputDevices"=>["Audio","get_output_devices",0],"audioGetInputDevices"=>["Audio","get_input_devices",0],"audioSetOutputDevice"=>["Audio","set_output_device",0],"audioSetInputDevice"=>["Audio","set_input_device",0],"audioVolume"=>["Audio","volume",0],"audioPitch"=>["Audio","pitch",0],"audioFadeIn"=>["Audio","fadeIn",0],"audioFadeOut"=>["Audio","fadeOut",0],"audioCrossfade"=>["Audio","crossfade",0],"audioStop"=>["Audio","stop",0],"audioPause"=>["Audio","pause",0],"audioResume"=>["Audio","resume",0],"animationValue"=>["Animation","value",0],"animationStop"=>["Animation","stop",0],"animationPause"=>["Animation","pause",0],"animationSpeed"=>["Animation","speed",0],"animationLoop"=>["Animation","loop",0],"cameraCurrent"=>["Camera","current",0],"tagsFindAll"=>["Tags","find_all",0],"groupsCreate"=>["Groups","create",0],"groupsGetMembers"=>["Groups","get_members",0],"worldFindByTag"=>["World","find_by_tag",0],"worldFindInRadius"=>["World","find_in_radius",0],"after"=>["Timer","after",0],"every"=>["Timer","every",0],"lerp"=>["Smooth","lerp",0],"slerp"=>["Smooth","slerp",0],"inverseLerp"=>["Smooth","inverseLerp",0],"remap"=>["Smooth","remap",0],"smoothDamp"=>["Smooth","smoothDamp",0],"raycast"=>["Physics","raycast",0],"sphereCast"=>["Physics","sphereCast",0],"overlap"=>["Physics","overlap",0],"explosion"=>["Physics","explosion",0],"timeScale"=>["Clock","timeScale",0],"pause"=>["Clock","pause",0],"resume"=>["Clock","resume",0],"createPath"=>["Path","create",0],"sequence"=>["Sequence","new",0],"timeline"=>["Timeline","new",0],"on"=>["Events","on",0],"once"=>["Events","once",0],"emit"=>["Events","emit",0],"disconnect"=>["Events","disconnect",0],"currentCamera"=>["Camera","current",0],"getOutputDevices"=>["Audio","get_output_devices",0],"getInputDevices"=>["Audio","get_input_devices",0],"setOutputDevice"=>["Audio","set_output_device",0],"setInputDevice"=>["Audio","set_input_device",0]]; + if(isset($methods[$name])){[$family,$method]=$methods[$name];if(($family==="Sequence"||$family==="Timeline")&&$method==="new")$method="create";return $family::$method(...$args);} + if($name==='getEnvironment'||$name==='setEnvironment')return $Game->scene->$name(...$args); + if($name==='setCurrentCamera')return $Game->setCurrentCamera($args[0] instanceof ApiPath?$args[0]->source:$args[0]); + return $rustic->$name(...$args); + } +} +final class CanonicalGame { + public ApiPath $scene; + public CanonicalGlobalApi $rustic; + public function __construct(){$this->scene=new ApiPath('game.scene','scene');$this->rustic=new CanonicalGlobalApi();} +} +$game=new CanonicalGame(); diff --git a/Engine/crates/engine-scripting/src/sdk/rustic.py b/Engine/crates/engine-scripting/src/sdk/rustic.py index a0d042be..af8aa9e5 100644 --- a/Engine/crates/engine-scripting/src/sdk/rustic.py +++ b/Engine/crates/engine-scripting/src/sdk/rustic.py @@ -95,6 +95,7 @@ def _entity(obj): if obj is None: return rustic.entity_id() if isinstance(obj,ObjectPath): obj=obj.source elif hasattr(obj,"id"): obj=obj.id + if str(obj).startswith("game.scene."): return rustic.query({"op":"scene_object","source":str(obj)}) result = Game.scene.Find(str(obj)) if result: return result uuid.UUID(str(obj)) @@ -397,3 +398,222 @@ class World: def find_by_tag(name): return rustic.query({"op":"world_find_by_tag",'name':name}) @staticmethod def find_in_radius(center,radius): return rustic.query({"op":"world_find_in_radius",'center':center,'radius':radius}) + +# Canonical API paths. +from inspect import signature, Signature +_path_methods={'scene': {'tweenTo': ['Tween', 'to', 0], + 'tweenMove': ['Tween', 'move', 0], + 'tweenRotate': ['Tween', 'rotate', 0], + 'tweenScale': ['Tween', 'scale', 0], + 'tweenAction': ['Tween', 'action', 0], + 'tweenToOptions': ['Tween', 'toOptions', 0], + 'movementMoveTo': ['Movement', 'moveTo', 0], + 'movementMove': ['Movement', 'move', 0], + 'movementRotateTo': ['Movement', 'rotateTo', 0], + 'movementLookAt': ['Movement', 'lookAt', 0], + 'movementFollow': ['Movement', 'follow', 0], + 'movementOrbit': ['Movement', 'orbit', 0], + 'movementMoveToOptions': ['Movement', 'moveToOptions', 0], + 'pathFollow': ['Path', 'follow', 0], + 'effectsFade': ['Effects', 'fade', 0], + 'effectsFlash': ['Effects', 'flash', 0], + 'effectsShake': ['Effects', 'shake', 0], + 'effectsPulse': ['Effects', 'pulse', 0], + 'cameraMoveTo': ['Camera', 'moveTo', 0], + 'cameraZoom': ['Camera', 'zoom', 0], + 'cameraFov': ['Camera', 'fov', 0], + 'cameraFollow': ['Camera', 'follow', 0], + 'cameraOrbit': ['Camera', 'orbit', 0], + 'cameraLookAt': ['Camera', 'lookAt', 0], + 'cameraShake': ['Camera', 'shake', 0], + 'cameraTransition': ['Camera', 'transition', 0], + 'physicsImpulse': ['Physics', 'impulse', 0], + 'physicsForce': ['Physics', 'force', 0], + 'physicsLaunch': ['Physics', 'launch', 0], + 'physicsKnockback': ['Physics', 'knockback', 0], + 'animationRegister': ['Animation', 'register', 0], + 'animationRegisterClip': ['Animation', 'registerClip', 0], + 'animationClips': ['Animation', 'clips', 0], + 'animationAction': ['Animation', 'action', 0], + 'animationPlay': ['Animation', 'play', 0], + 'animationBlend': ['Animation', 'blend', 0], + 'animationTransition': ['Animation', 'transition', 0], + 'animationIk': ['Animation', 'ik', 0], + 'animationFootPlacement': ['Animation', 'footPlacement', 0], + 'animationLookAt': ['Animation', 'lookAt', 0], + 'animationHeadTracking': ['Animation', 'headTracking', 0], + 'animationRecoil': ['Animation', 'recoil', 0], + 'animationAddMarker': ['Animation', 'addMarker', 0], + 'eventsConnect': ['Events', 'connect', 0], + 'tagsAdd': ['Tags', 'add', 0], + 'tagsRemove': ['Tags', 'remove', 0], + 'groupsAdd': ['Groups', 'add', 1], + 'hierarchySetParent': ['Hierarchy', 'set_parent', 0], + 'hierarchyGetChildren': ['Hierarchy', 'get_children', 0], + 'hierarchyDetach': ['Hierarchy', 'detach', 0], + 'entityDestroy': ['Entity', 'destroy', 0], + 'entityDestroyAfter': ['Entity', 'destroy_after', 0], + 'entityIsValid': ['Entity', 'is_valid', 0], + 'componentsHas': ['Components', 'has', 0], + 'componentsGetAll': ['Components', 'get_all', 0], + 'componentsFindInChildren': ['Components', 'find_in_children', 0], + 'tween': ['Tween', 'to', 0], + 'moveTo': ['Movement', 'moveTo', 0], + 'move': ['Movement', 'move', 0], + 'rotateTo': ['Movement', 'rotateTo', 0], + 'lookAt': ['Movement', 'lookAt', 0], + 'follow': ['Movement', 'follow', 0], + 'orbit': ['Movement', 'orbit', 0], + 'fade': ['Effects', 'fade', 0], + 'flash': ['Effects', 'flash', 0], + 'shake': ['Effects', 'shake', 0], + 'pulse': ['Effects', 'pulse', 0], + 'impulse': ['Physics', 'impulse', 0], + 'force': ['Physics', 'force', 0], + 'launch': ['Physics', 'launch', 0], + 'knockback': ['Physics', 'knockback', 0], + 'playAnimation': ['Animation', 'play', 0], + 'destroy': ['Entity', 'destroy', 0], + 'destroyAfter': ['Entity', 'destroy_after', 0], + 'isValid': ['Entity', 'is_valid', 0], + 'addTag': ['Tags', 'add', 0], + 'removeTag': ['Tags', 'remove', 0], + 'setParent': ['Hierarchy', 'set_parent', 0], + 'getChildren': ['Hierarchy', 'get_children', 0], + 'detach': ['Hierarchy', 'detach', 0], + 'hasComponent': ['Components', 'has', 0], + 'getComponents': ['Components', 'get_all', 0], + 'findInChildren': ['Components', 'find_in_children', 0], + 'followPath': ['Path', 'follow', 0]}, + 'global': {'tweenValue': ['Tween', 'value', 0], + 'timerAfter': ['Timer', 'after', 0], + 'timerEvery': ['Timer', 'every', 0], + 'smoothLerp': ['Smooth', 'lerp', 0], + 'smoothSlerp': ['Smooth', 'slerp', 0], + 'smoothInverseLerp': ['Smooth', 'inverseLerp', 0], + 'smoothRemap': ['Smooth', 'remap', 0], + 'smoothSmoothDamp': ['Smooth', 'smoothDamp', 0], + 'physicsRaycast': ['Physics', 'raycast', 0], + 'physicsSphereCast': ['Physics', 'sphereCast', 0], + 'physicsOverlap': ['Physics', 'overlap', 0], + 'physicsExplosion': ['Physics', 'explosion', 0], + 'clockTimeScale': ['Clock', 'timeScale', 0], + 'clockPause': ['Clock', 'pause', 0], + 'clockResume': ['Clock', 'resume', 0], + 'clockState': ['Clock', 'state', 0], + 'pathCreate': ['Path', 'create', 0], + 'sequenceNew': ['Sequence', 'new', 0], + 'sequenceCreate': ['Sequence', 'create', 0], + 'timelineNew': ['Timeline', 'new', 0], + 'timelineCreate': ['Timeline', 'create', 0], + 'eventsOn': ['Events', 'on', 0], + 'eventsOnce': ['Events', 'once', 0], + 'eventsEmit': ['Events', 'emit', 0], + 'eventsDisconnect': ['Events', 'disconnect', 0], + 'audioGetOutputDevices': ['Audio', 'get_output_devices', 0], + 'audioGetInputDevices': ['Audio', 'get_input_devices', 0], + 'audioSetOutputDevice': ['Audio', 'set_output_device', 0], + 'audioSetInputDevice': ['Audio', 'set_input_device', 0], + 'audioVolume': ['Audio', 'volume', 0], + 'audioPitch': ['Audio', 'pitch', 0], + 'audioFadeIn': ['Audio', 'fadeIn', 0], + 'audioFadeOut': ['Audio', 'fadeOut', 0], + 'audioCrossfade': ['Audio', 'crossfade', 0], + 'audioStop': ['Audio', 'stop', 0], + 'audioPause': ['Audio', 'pause', 0], + 'audioResume': ['Audio', 'resume', 0], + 'animationValue': ['Animation', 'value', 0], + 'animationStop': ['Animation', 'stop', 0], + 'animationPause': ['Animation', 'pause', 0], + 'animationSpeed': ['Animation', 'speed', 0], + 'animationLoop': ['Animation', 'loop', 0], + 'cameraCurrent': ['Camera', 'current', 0], + 'tagsFindAll': ['Tags', 'find_all', 0], + 'groupsCreate': ['Groups', 'create', 0], + 'groupsGetMembers': ['Groups', 'get_members', 0], + 'worldFindByTag': ['World', 'find_by_tag', 0], + 'worldFindInRadius': ['World', 'find_in_radius', 0], + 'after': ['Timer', 'after', 0], + 'every': ['Timer', 'every', 0], + 'lerp': ['Smooth', 'lerp', 0], + 'slerp': ['Smooth', 'slerp', 0], + 'inverseLerp': ['Smooth', 'inverseLerp', 0], + 'remap': ['Smooth', 'remap', 0], + 'smoothDamp': ['Smooth', 'smoothDamp', 0], + 'raycast': ['Physics', 'raycast', 0], + 'sphereCast': ['Physics', 'sphereCast', 0], + 'overlap': ['Physics', 'overlap', 0], + 'explosion': ['Physics', 'explosion', 0], + 'timeScale': ['Clock', 'timeScale', 0], + 'pause': ['Clock', 'pause', 0], + 'resume': ['Clock', 'resume', 0], + 'createPath': ['Path', 'create', 0], + 'sequence': ['Sequence', 'new', 0], + 'timeline': ['Timeline', 'new', 0], + 'on': ['Events', 'on', 0], + 'once': ['Events', 'once', 0], + 'emit': ['Events', 'emit', 0], + 'disconnect': ['Events', 'disconnect', 0], + 'currentCamera': ['Camera', 'current', 0], + 'getOutputDevices': ['Audio', 'get_output_devices', 0], + 'getInputDevices': ['Audio', 'get_input_devices', 0], + 'setOutputDevice': ['Audio', 'set_output_device', 0], + 'setInputDevice': ['Audio', 'set_input_device', 0]}, + 'file': {'audioPlay': ['Audio', 'play', 0], + 'audioPlayAt': ['Audio', 'playAt', 0], + 'audioLoad': ['Audio', 'load', 0], + 'animationLoad': ['Animation', 'load', 1], + 'play': ['Audio', 'play', 0], + 'playAt': ['Audio', 'playAt', 0], + 'load': ['Audio', 'load', 0], + 'loadAnimation': ['Animation', 'load', 1], + 'add': ['instance', 'add', 0], + 'clone': ['instance', 'clone', 0], + 'setSkyTexture': ['scene', 'setSkyTexture', 0]}} +_path_methods['global'].update({'get_output_devices':_path_methods['global']['getOutputDevices'],'get_input_devices':_path_methods['global']['getInputDevices'],'set_output_device':_path_methods['global']['setOutputDevice'],'set_input_device':_path_methods['global']['setInputDevice']}) +_path_methods['scene'].update({'clone':['instance','clone',0],'add':['instance','add',0],'setCurrentCamera':['Game','setCurrentCamera',0],'emit':['Events','emit',2]}) +class ApiPath: + def __init__(self,source,category,file_path=None): + self.source=source; self.category=category; self.file_path=file_path; self.children={} + def __getitem__(self,name): + name=str(name) + if name not in self.children: + self.children[name]=ApiPath(self.source+'.'+name,self.category,(self.file_path+'/' if self.file_path else '')+name if self.category=='file' else None) + return self.children[name] + def __getattr__(self,name): + if name in ('EditAttribute','edit_attribute'): + return lambda attribute,value: ObjectPath(self.source).EditAttribute(attribute,value) + binding=_path_methods[self.category].get(name) + if binding: + family,method,index=binding + owner=Game.scene if family=='scene' else globals()[family] + fn=getattr(owner,method,None) + if fn is None: raise AttributeError('API unavailable: '+name) + parameters=list(signature(fn).parameters.values()) + target_name=parameters[index].name + receiver_signature=Signature(parameters[:index]+parameters[index+1:]) + def argument(value): + if isinstance(value,ApiPath): return value.file_path if value.category=='file' else value.source + return value + def call(*args,**kwargs): + bound=receiver_signature.bind(*[argument(a) for a in args],**{k:argument(v) for k,v in kwargs.items()}) + bound.arguments[target_name]=self.file_path if self.category=='file' else self.source + return fn(**bound.arguments) + return call + return self[name] +class GlobalApi: + def __init__(self): self.files=ApiPath('game.rustic.files','file','') + def __getattr__(self,name): + binding=_path_methods['global'].get(name) + if binding: + family,method,_=binding + if family in ('Sequence','Timeline') and method=='new': method='new' + fn=getattr(globals()[family],method,None) + if fn is None: raise AttributeError('API unavailable: '+name) + return fn + if name in ('getEnvironment','setEnvironment'): return getattr(Game.scene,name) + if name=='setCurrentCamera': return lambda obj: Game.setCurrentCamera(obj.source if isinstance(obj,ApiPath) else obj) + return getattr(rustic,name) +class CanonicalGame: + def __init__(self): self.scene=ApiPath('game.scene','scene'); self.rustic=GlobalApi() +game=CanonicalGame() diff --git a/Engine/crates/engine-scripting/src/workspace.rs b/Engine/crates/engine-scripting/src/workspace.rs index 49423d18..21a6dc21 100644 --- a/Engine/crates/engine-scripting/src/workspace.rs +++ b/Engine/crates/engine-scripting/src/workspace.rs @@ -121,6 +121,7 @@ pub fn generate_programming_workspace( "---@meta\n-- {GENERATED_MARKER}\n---@class RusticTransform\n---@field x number\n---@field y number\n---@field z number\n---@class RusticObjectPath\n---@field EditAttribute fun(self:RusticObjectPath,name:string,value:any)\n---@field GetAttribute fun(self:RusticObjectPath,name:string): any\n---@class RusticApi\n---@field game table\n---@field edit_object_attribute fun(source:string,name:string,value:any)\n---@field entity_id fun(): string\n---@field delta_time fun(): number\n---@field fixed_delta_time fun(): number\n---@field get_translation fun(): number, number, number\n---@field set_translation fun(x:number,y:number,z:number)\n---@field find_entity fun(name_or_id:string): string|nil, string|nil\n---@field input fun(action:string): table\n---@field log fun(level:string,message:string,fields:table|nil)\n---@field get_property fun(name:string): any\n---@field set_property fun(name:string,value:any): boolean, string|nil\n---@field set_enabled fun(enabled:boolean)\n---@type RusticApi\nrustic = rustic\n---@class RusticInstanceApi\n---@field add fun(source:string,parent:string|nil): string\n---@field clone fun(source:string,parent:string|nil): string\n---@type RusticInstanceApi\ninstance = instance\n---@class RusticSceneEnvironment\n---@field enabled boolean\n---@field sky_image string\n---@field sky_color number[]\n---@field rotation_degrees number\n---@field exposure number\n---@field ambient_color number[]\n---@field ambient_intensity number\n---@field sun_color number[]\n---@field sun_intensity number\n---@field sun_direction number[]\n---@field haze_color number[]\n---@field haze_density number\n---@field haze_start number\n---@class RusticSceneApi\n---@field Find fun(path:string): string|nil\n---@field List fun(path:string|nil): string[]\n---@field getEnvironment fun(): RusticSceneEnvironment\n---@field setEnvironment fun(settings:table): RusticSceneEnvironment\n---@field setSkyTexture fun(path:string): RusticSceneEnvironment\n---@class RusticGameApi\n---@field scene RusticSceneApi\n---@field setCurrentCamera fun(source:string|table)\n---@type RusticGameApi\nGame = Game\n" ); let api_text = format!("{api_text}{}", include_str!("sdk/entity-api.lua")); + let api_text = format!("{api_text}{}", include_str!("sdk/game-api.lua")); files.write_with_options( &api, api_text.as_bytes(), @@ -134,6 +135,7 @@ pub fn generate_programming_workspace( "{javascript_api_text}{}", include_str!("sdk/entity-api.d.ts") ); + let javascript_api_text = format!("{javascript_api_text}{}", include_str!("sdk/game-api.d.ts")); files.write_with_options( &javascript_api, javascript_api_text.as_bytes(), diff --git a/Engine/docs/Scripting/apiPaths.md b/Engine/docs/Scripting/apiPaths.md new file mode 100644 index 00000000..044e5259 --- /dev/null +++ b/Engine/docs/Scripting/apiPaths.md @@ -0,0 +1,194 @@ +# Call APIs through scene and file paths + +All ten scripting types provide the same three roots. Choose the root from the +thing the call operates on: + +| Target | Root | Lua / Luau example | +| --- | --- | --- | +| An object in a named scene | `game.scene.SceneName.ObjectPath` | `game.scene.Demo.Room.Player:tween("Position", {8,0,0}, 2)` | +| A service that needs no scene object | `game.rustic` | `game.rustic:after(1, function() print("ready") end)` | +| A file in the Explorer | `game.rustic.files.ExplorerPath` | `game.rustic.files.assets.sounds["hit.wav"]:load()` | + +The scene path supplies the object argument. The file path supplies the source +file argument. Pass the remaining arguments in their usual order. Returned action +handles, voices, timers and builders keep their existing methods and lifecycle. +Old `Tween`, `Movement`, `Game.scene`, `rustic.game`, and other service calls remain +available for existing scripts. + +## Set up and run + +1. Open a scene whose document name is **Demo**. Create a root named **Room**, a + child named **Player**, and a separate root named **Controller**. Give siblings + unique names. Player's complete hierarchy path is `Room.Player`. +2. Select Controller. Use **Programming > Object Component Script**, choose Lua, + paste the example below, save, and attach it to Controller with + **Programming > Attach Existing Script** or **+ Add Component** in the Inspector. +3. Press **Play**. Player moves to local position `(8, 0, 0)` over two scene-clock + seconds. Controller retains its position. After one scene-clock second the log + shows `ready`. Stop Play to return to the authored scene. + +```lua +return {Start=function() + game.scene.Demo.Room.Player:tween("Position", {8,0,0}, 2, "OutCubic") + game.rustic:after(1, function() print("ready") end) +end} +``` + +Global and Scene startup scripts can also use named paths; the target does not +have to own the script. Calls resolve the live loaded scene, including edits +queued earlier in the same callback. A path retained in a variable remains a +path: after renaming or reparenting its target, use the new hierarchy path. +An unloaded scene, missing object, or duplicate full hierarchy path is rejected +rather than selecting a different object. Only the currently loaded scene is +addressable. + +## Native syntax in every language + +These snippets go inside your existing `Start` / `on_start` callback. Each starts +the same Player tween. See [Scopes, lifecycle and execution](/docs/guides/scripting) +for creating and attaching scripts in each language. + +| Language | Scene object call | +| --- | --- | +| Lua 5.4 / Luau | `game.scene.Demo.Room.Player:tween("Position", {8,0,0}, 2)` | +| JavaScript / HTML inline JS | `game.scene.Demo.Room.Player.tween("Position", [8,0,0], 2);` | +| Python | `game.scene.Demo.Room.Player.tween("Position", [8,0,0], 2)` (import `game` from `rustic`) | +| C# | `game.scene["Demo"]["Room"]["Player"].tween("Position", new double[]{8,0,0}, 2);` (use `using static Rustic;`) | +| C++ | `game.scene["Demo"]["Room"]["Player"].tween("Position", RusticValue::Array{8,0,0}, 2.0);` | +| Java | `game.scene.scene("Demo").object("Room.Player").tween("Position", new double[]{8,0,0}, 2);` (inside a `Rustic` subclass) | +| PHP | `$game->scene->Demo->Room->Player->tween("Position", [8,0,0], 2);` (declare `global $game;` in callbacks) | + +C has no implicit receiver or arbitrary named members. Select a path value and +pass it explicitly as `self`; the selected value supplies the target: + +```c +RusticScenePath player = game.scene("Demo", "Room.Player"); +player.tween(player, "Position", + (RusticValue){.type=RUSTIC_VECTOR,.vector={8,0,0},.length=3}, + 2, "Linear", NULL); +``` + +Use subscripts for names with spaces and filenames with extensions in Lua, +Luau, JavaScript, Python, C# and C++. For example, +`game.scene["Demo Scene"]["Room"]["Player One"]`. +Java accepts the scene and complete object path as strings. PHP accepts a +property such as `$game->scene->{"Demo Scene"}->Room->{"Player One"}`. + +## Object methods + +Short names cover common calls. Family-prefixed names distinguish APIs whose +original names overlap. Both forms bind the receiver to the target: + +| Family | Methods on `game.scene.Scene.ObjectPath` | +| --- | --- | +| Attributes | `EditAttribute`, `edit_attribute` (`GetAttribute` / `get_attribute` in embedded Lua) | +| Tween | `tween` / `tweenTo`, `tweenMove`, `tweenRotate`, `tweenScale`; action/options variants where already supported | +| Movement | `moveTo`, `move`, `rotateTo`, `lookAt`, `follow`, `orbit`; also `movementMoveTo`, `movementMove`, `movementRotateTo`, `movementLookAt`, `movementFollow`, `movementOrbit` | +| Paths | `followPath` / `pathFollow`, `pathFollowSpeed` where already supported | +| Effects | `fade`, `flash`, `shake`, `pulse`; also the `effects` prefix, such as `effectsFade` | +| Camera | `setCurrentCamera()`, `cameraMoveTo`, `cameraZoom`, `cameraFov`, `cameraFollow`, `cameraOrbit`, `cameraLookAt`, `cameraShake`, `cameraTransition` | +| Physics | `impulse`, `force`, `launch`, `knockback`; also the `physics` prefix | +| Animation | `playAnimation` / `animationPlay`, `animationRegister`, `animationClips`, `animationBlend`, `animationTransition`, `animationAddMarker`, `animationIk`, `animationFootPlacement`, `animationLookAt`, `animationHeadTracking`, `animationRecoil`; registration/action variants where already supported | +| Events | `eventsConnect(name, callback)`, `emit(name, arguments)` | +| Tags and groups | `addTag` / `tagsAdd`, `removeTag` / `tagsRemove`, `groupsAdd(groupName)` | +| Hierarchy | `setParent` / `hierarchySetParent`, `getChildren` / `hierarchyGetChildren`, `detach` / `hierarchyDetach` | +| Instances | `clone(parent)`, `add(parent)` | +| Lifetime | `destroy` / `entityDestroy`, `destroyAfter` / `entityDestroyAfter`, `isValid` / `entityIsValid` | +| Components | `hasComponent` / `componentsHas`, `getComponents` / `componentsGetAll`, `findInChildren` / `componentsFindInChildren` | + +Arguments and backend limits are those of the existing API in your language, +minus the receiver argument. For example, `groupsAdd` binds the **second** original +argument; pass only the group name. In languages whose follow/hierarchy APIs take +string targets, pass another canonical scene path string, such as +`"game.scene.Demo.Room.Leader"`. Lua, JavaScript, Python and PHP also accept scene +path objects as these arguments. + +## Global methods + +Call timers with `game.rustic:after(...)` or `:every(...)` in Lua/Luau, and native +member calls in other languages. Interpolation uses `lerp`, `slerp`, +`inverseLerp`, `remap`, and `smoothDamp`. World physics queries use `raycast`, +`sphereCast`, `overlap`, and `explosion`. Clock operations use `timeScale`, +`pause`, and `resume`; camera selection can use the scene object’s `setCurrentCamera()` method +(or the global `setCurrentCamera(path)` convenience); `currentCamera()` reads +the current camera where supported. Scene lighting uses `getEnvironment()` and +`setEnvironment(settings)`. + +Builders use `sequence()` and `timeline()`, and curves use `createPath(...)`. +Value animation uses `tweenValue(...)` / `animationValue(...)`. Global signals use +`on`, `once`, `emit`, and `disconnect`. Audio device enumeration/selection keeps +its snake case names; voice operations use `audioVolume`, `audioPitch`, +`audioFadeIn`, `audioFadeOut`, and `audioCrossfade`. Tag/group/world searches use +`tagsFindAll`, `groupsCreate`, `groupsGetMembers`, `worldFindByTag`, and +`worldFindInRadius`. + +Family-prefixed equivalents are available, such as `timerAfter`, `smoothLerp`, +`clockPause`, `physicsRaycast`, and `sequenceNew`. The existing owner API is also +reachable under `game.rustic`, including `entity_id`, `delta_time`, `input`, +`key`, `get_property`, and `log`. These owner calls keep their existing scope. +No adapter gains an API its previous SDK did not implement. + +## Explorer file methods + +The Explorer path begins at the project root; include `assets` where applicable. +Each selected child adds one `/` to the source path. An extension stays in a +single filename segment: `["hit.wav"]`, not `.hit.wav`. + +For Lua/Luau audio, import a WAV, MP3, OGG, FLAC or AAC file as +`assets/sounds/hit.wav` (use its actual extension) and ensure it appears in the Play +snapshot. Load the selected file before playback: + +```lua +local sound = game.rustic.files.assets.sounds["hit.wav"] +sound:load() +local voice = sound:play({volume=0.5}) +-- Later, release the decoded clip: +sound:Unload() +``` + +Separate expressions selecting the same Lua/Luau file retain its load state. +`:play()` before `:load()` reports an error. Other language SDKs keep their +existing direct playback model, for example: + +```javascript +game.rustic.files.assets.sounds["hit.wav"].play({volume: 0.5}); +``` + +```python +game.rustic.files.assets.sounds["hit.wav"].play(volume=0.5) +``` + +```cpp +game.rustic.files["assets"]["sounds"]["hit.wav"].play(0.5); +``` + +C# uses the same subscripts and `.play(...)`. Java uses +`game.rustic.files.object("assets/sounds/hit.wav").play(0.5, 1, false)`. +PHP uses `$game->rustic->files->assets->sounds->{"hit.wav"}->play(...)`. +C selects `RusticFilePath sound=game.rustic.files("assets/sounds/hit.wav")` and +calls `sound.play(sound, &options)`. + +Other file operations are `playAt` / `audioPlayAt`, `loadAnimation(object)`, +`add(parent)`, `clone(parent)`, and `setSkyTexture()`, using each language's +existing optional arguments. Loading animation binds the file's source argument +and still requires the scene entity argument. In Lua/Luau, file recordings use +`game.rustic.files["capture.wav"]:startRecording(options)`; recording without a +file and microphone capture use `game.rustic:startRecording()` and +`game.rustic:startHotMic()`. + +File paths retain the backend's validation. Missing assets, paths outside the +snapshot and unsupported file types fail with the existing diagnostics. + +## Troubleshooting + +- **Scene not loaded:** use the document name of the scene currently in Play. +- **Object missing:** include every parent, check exact spelling, and use the new + path after renaming or reparenting. Creating a path value does not validate it; + calling a method resolves it. +- **Ambiguous object:** rename siblings so the complete hierarchy path is unique. +- **Lua argument errors:** use `:` on the canonical roots and scene/file objects. +- **File missing:** select each directory separately and keep the extension in + one segment. Verify that the asset is included in the Play snapshot. +- **API unavailable:** check the original API's language support and signature in + [Use gameplay actions](/docs/guides/gameplay-actions). The path facade preserves + those capabilities and arguments. diff --git a/Engine/docs/Scripting/gameplayActions.md b/Engine/docs/Scripting/gameplayActions.md index 7d8fcfea..5e0afa65 100644 --- a/Engine/docs/Scripting/gameplayActions.md +++ b/Engine/docs/Scripting/gameplayActions.md @@ -1,5 +1,9 @@ # Shared gameplay actions (API 1.1) +Use the [canonical API paths](/docs/guides/api-paths) to call object APIs on +`game.scene.SceneName.ObjectPath`, global APIs on `game.rustic`, and file APIs +on `game.rustic.files.ExplorerPath`. Existing calls below remain supported. + The engine now owns an action scheduler shared by scripting languages. Easing, interpolation, path traversal, composition, and lifetime cleanup run in Rust. The script only starts an action and handles its callbacks. You do not edit JSON or diff --git a/Engine/docs/Scripting/sceneObjects.md b/Engine/docs/Scripting/sceneObjects.md index 035ec38a..24621d21 100644 --- a/Engine/docs/Scripting/sceneObjects.md +++ b/Engine/docs/Scripting/sceneObjects.md @@ -1,5 +1,9 @@ # Edit another scene object +Use the [canonical API paths](/docs/guides/api-paths) to call object APIs on +`game.scene.SceneName.ObjectPath`, global APIs on `game.rustic`, and file APIs +on `game.rustic.files.ExplorerPath`. Existing calls below remain supported. + Use a named scene and the object's full hierarchy path to change its built-in properties from a script attached elsewhere. For example: diff --git a/Engine/docs/Scripting/scriptingWeb.md b/Engine/docs/Scripting/scriptingWeb.md index ab7784e8..b5a2a9f1 100644 --- a/Engine/docs/Scripting/scriptingWeb.md +++ b/Engine/docs/Scripting/scriptingWeb.md @@ -66,7 +66,9 @@ if (rustic.key("KeyW").held) { } ``` -JavaScript mutations are applied in command order after the callback. Properties must +JavaScript mutations are applied in command order after the callback. Canonical +`game.scene.Scene.ObjectPath` target resolution flushes earlier queued commands +before resolving the live object, including renames and reparenting. Properties must be declared and type-compatible. Scene lookup returns stable IDs; JavaScript `Game.scene.List()` currently returns path strings. Instance operations are queued and do not return the created ID.