diff --git a/docs/SCRIPTING_EXPANSION_STATUS.md b/docs/SCRIPTING_EXPANSION_STATUS.md new file mode 100644 index 0000000..f95d809 --- /dev/null +++ b/docs/SCRIPTING_EXPANSION_STATUS.md @@ -0,0 +1,391 @@ +# Scripting expansion audit and implementation status + +This is an incremental implementation record, not a claim that the comprehensive +expansion is complete. Baseline: engine `017db99b`, docs `1a3fa6c`. + +## Architecture and reuse audit + +- `engine-world`: private Bevy ECS, stable UUIDs and generational runtime refs; + validated batched commands, deterministic hierarchy and transform propagation; + versioned scene documents and additive instantiation. Reuse these for hierarchy, + destruction, component queries, prefab/scene foundations; preserve scene format. +- `engine-core/gameplay`: engine-owned easing, movement, tween, timers, sequences, + paths, animation, IK, signal delivery and audio mixer. These are implemented; + no replacement scheduler, timer, event bus, or easing implementation is needed. +- `engine-scripting`: bundled Lua 5.4/QuickJS; external isolated C/C++, Python, + .NET, Java, PHP and Luau hosts. All use `GameplayHost.gameplay_query` and generated + SDKs; extend this single boundary. PHP/Web remain UI-scoped. Installed toolchains + determine external adapter availability. Snapshot state differs from live queries. +- `engine-play`: shared runtime world/context, fixed simulation, isolated scripts, + snapshot asset reads and lifecycle cleanup. Physics is a custom primitive AABB + solver (`physics.rs`), not Rapier/Jolt: advanced joints, ragdolls, vehicles, CCD, + slopes and robust shape casts require a deliberate backend integration first. +- `engine-rhi`, `renderer-wgpu`: typed generational GPU resources, renderer selection, + mesh/primitive extraction, WGSL pipelines and environment/sky. Resource presence + is not evidence of a script API or an advanced rendering effect. Do not advertise + volumetrics, cloth or post-processing before rendering paths exist. +- `apps/runtime/game_ui.rs`: static HTML text/CSS to SVG raster overlay. Embedded + Web JavaScript runs separately in QuickJS. No browser DOM, forms, layout engine, + input routing or reactive rendering bridge exists. Browser runtime integration + is a prerequisite for Category F; state maps alone cannot implement that category. +- `engine-assets`, `engine-project`, `engine-platform`: existing import workers, + VFS/path validation, project storage, task/logging facilities and window plumbing. + Script saves must use a per-game writable directory, not immutable play snapshots. +- `apps/editor`, `engine-editor`: scene attachment/inspection and embedded isolated + Play already share engine-world/runtime; no competing native UI framework needed. +- `docs`: Markdown guide/API sources, generated API reference, `docs-catalog.ts` + navigation and API example tests. Engine-local documentation mirrors relevant pages. +- `examples`: currently contains only a license; add small runnable scripts with + editor setup rather than fake project files or placeholders. + +## Constraints and verification + +No new runtime dependencies are needed for entity foundations. Existing project +serialization and public function names must stay compatible. Runtime-only tags, +groups and lifetime deadlines must be documented as such. Hierarchy preservation +must reject transforms that need shear rather than silently changing world poses. +Linux can run Rust and available language tests; Windows installer qualification +requires the repository Windows workflow. Hardware-dependent rendering remains +unverified unless executed. + +## Feature-by-feature checklist + +“Partial” records an existing engine substrate, not completion of the requested API. +“Not started” means no implementation was found in the inspected runtime/API paths. +The advanced physics and browser UI prerequisites above block those dependent +features; other unchecked work remains independent and not started. + +| Category | Requested capability | Status | +| --- | --- | --- | +| Category A | `Tags.add()` | Implemented: Rust/runtime/bindings/docs; executed validation below | +| Category A | `Tags.remove()` | Implemented: Rust/runtime/bindings/docs; executed validation below | +| Category A | `Tags.find_all()` | Implemented: Rust/runtime/bindings/docs; executed validation below | +| Category A | `Groups.create()` | Implemented: Rust/runtime/bindings/docs; executed validation below | +| Category A | `Groups.add()` | Implemented: Rust/runtime/bindings/docs; executed validation below | +| Category A | `Groups.get_members()` | Implemented: Rust/runtime/bindings/docs; executed validation below | +| Category A | `Hierarchy.set_parent()` | Implemented: Rust/runtime/bindings/docs; executed validation below | +| Category A | `Hierarchy.get_children()` | Implemented: Rust/runtime/bindings/docs; executed validation below | +| Category A | `Hierarchy.detach()` | Implemented: Rust/runtime/bindings/docs; executed validation below | +| Category A | `Entity.destroy()` | Implemented: Rust/runtime/bindings/docs; executed validation below | +| Category A | `Entity.destroy_after()` | Implemented: Rust/runtime/bindings/docs; executed validation below | +| Category A | `Entity.is_valid()` | Implemented: Rust/runtime/bindings/docs; executed validation below | +| Category A | `Components.has()` | Implemented: Rust/runtime/bindings/docs; executed validation below | +| Category A | `Components.get_all()` | Implemented: Rust/runtime/bindings/docs; executed validation below | +| Category A | `Components.find_in_children()` | Implemented: Rust/runtime/bindings/docs; executed validation below | +| Category A | `Components.attach()` | Partial: existing substrate; requested binding/behavior missing | +| Category A | `Components.detach()` | Partial: existing substrate; requested binding/behavior missing | +| Category A | `Components.set_active()` | Partial: existing substrate; requested binding/behavior missing | +| Category A | `Prefab.instantiate()` | Partial: existing substrate; requested binding/behavior missing | +| Category A | `Prefab.override()` | Partial: existing substrate; requested binding/behavior missing | +| Category A | `Prefab.reset()` | Partial: existing substrate; requested binding/behavior missing | +| Category A | `Scene.load_additive()` | Partial: existing substrate; requested binding/behavior missing | +| Category A | `Scene.unload_additive()` | Partial: existing substrate; requested binding/behavior missing | +| Category A | `Scene.mark_persistent()` | Partial: existing substrate; requested binding/behavior missing | +| Category A | `Scene.transfer_entity()` | Partial: existing substrate; requested binding/behavior missing | +| Category A | `GameState.set()` | Not started | +| Category A | `GameState.get()` | Not started | +| Category A | `GameState.observe()` | Not started | +| Category A | `World.find_by_tag()` | Implemented: Rust/runtime/bindings/docs; executed validation below | +| Category A | `World.find_in_radius()` | Implemented: Rust/runtime/bindings/docs; executed validation below | +| Category A | `Pool.register()` | Not started | +| Category A | `Pool.acquire()` | Not started | +| Category A | `Pool.release()` | Not started | +| Category A | `Interaction.register()` | Not started | +| Category A | `Interaction.use()` | Not started | +| Category A | `Interaction.can_use()` | Not started | +| Category A | `Damage.apply()` | Not started | +| Category A | `Damage.set_resistance()` | Not started | +| Category A | `Damage.on_death()` | Not started | +| Category A | `Inventory.add_item()` | Not started | +| Category A | `Inventory.remove_item()` | Not started | +| Category A | `Inventory.transfer()` | Not started | +| Category A | `Quest.start()` | Not started | +| Category A | `Quest.complete_objective()` | Not started | +| Category A | `Quest.get_progress()` | Not started | +| Category A | `Checkpoint.activate()` | Not started | +| Category A | `Checkpoint.respawn()` | Not started | +| Category A | `Ability.activate()` | Not started | +| Category A | `Ability.set_cooldown()` | Not started | +| Category A | `Ability.can_activate()` | Not started | +| Category B | `RigidBody.set_mass()` | Partial: existing substrate; requested binding/behavior missing | +| Category B | `RigidBody.set_velocity()` | Partial: existing substrate; requested binding/behavior missing | +| Category B | `RigidBody.set_angular_velocity()` | Partial: existing substrate; requested binding/behavior missing | +| Category B | `Gravity.set_global()` | Partial: existing substrate; requested binding/behavior missing | +| Category B | `Gravity.set_for_body()` | Partial: existing substrate; requested binding/behavior missing | +| Category B | `Gravity.enable()` | Partial: existing substrate; requested binding/behavior missing | +| Category B | `PhysicsMaterial.set_friction()` | Partial: existing substrate; requested binding/behavior missing | +| Category B | `PhysicsMaterial.set_restitution()` | Partial: existing substrate; requested binding/behavior missing | +| Category B | `CollisionLayer.set()` | Partial: existing substrate; requested binding/behavior missing | +| Category B | `CollisionLayer.ignore_pair()` | Partial: existing substrate; requested binding/behavior missing | +| Category B | `on_collision_enter()` | Not started | +| Category B | `on_collision_stay()` | Not started | +| Category B | `on_collision_exit()` | Not started | +| Category B | `Trigger.on_enter()` | Not started | +| Category B | `Trigger.on_exit()` | Not started | +| Category B | `Trigger.get_overlaps()` | Not started | +| Category B | `Joint.create()` | Not started | +| Category B | `Joint.set_limits()` | Not started | +| Category B | `Joint.break()` | Not started | +| Category B | `Character.move()` | Not started | +| Category B | `Character.is_grounded()` | Not started | +| Category B | `Character.set_slope_limit()` | Not started | +| Category B | `Vehicle.set_throttle()` | Not started | +| Category B | `Vehicle.set_steering()` | Not started | +| Category B | `Vehicle.set_brake()` | Not started | +| Category B | `Buoyancy.add_body()` | Not started | +| Category B | `Buoyancy.set_density()` | Not started | +| Category B | `Ragdoll.enable()` | Not started | +| Category B | `Ragdoll.disable()` | Not started | +| Category B | `Ragdoll.blend_weight()` | Not started | +| Category B | `RigidBody.set_ccd_mode()` | Partial: existing substrate; requested binding/behavior missing | +| Category B | `Physics.box_cast()` | Partial: existing substrate; requested binding/behavior missing | +| Category B | `Physics.capsule_cast()` | Partial: existing substrate; requested binding/behavior missing | +| Category B | `Physics.set_substeps()` | Partial: existing substrate; requested binding/behavior missing | +| Category B | `Physics.get_step_stats()` | Partial: existing substrate; requested binding/behavior missing | +| Category B | `Physics2D.set_axis_lock()` | Not started | +| Category B | `Physics2D.set_gravity()` | Not started | +| Category C | `NavMesh.build()` | Not started | +| Category C | `NavMesh.find_path()` | Not started | +| Category C | `NavMesh.update()` | Not started | +| Category C | `NavAgent.set_destination()` | Not started | +| Category C | `NavAgent.stop()` | Not started | +| Category C | `NavAgent.has_arrived()` | Not started | +| Category C | `BehaviorTree.create()` | Not started | +| Category C | `BehaviorTree.run()` | Not started | +| Category C | `BehaviorTree.set_blackboard()` | Not started | +| Category C | `StateMachine.add_state()` | Not started | +| Category C | `StateMachine.transition()` | Not started | +| Category C | `StateMachine.get_state()` | Not started | +| Category C | `Perception.can_see()` | Not started | +| Category C | `Perception.can_hear()` | Not started | +| Category C | `Perception.get_targets()` | Not started | +| Category C | `AIMemory.remember()` | Not started | +| Category C | `AIMemory.recall()` | Not started | +| Category C | `AIMemory.forget()` | Not started | +| Category C | `Patrol.set_points()` | Not started | +| Category C | `Patrol.start()` | Not started | +| Category C | `Patrol.pause()` | Not started | +| Category C | `Steering.seek()` | Not started | +| Category C | `Steering.flee()` | Not started | +| Category C | `Steering.avoid()` | Not started | +| Category C | `Flock.create()` | Not started | +| Category C | `Flock.set_cohesion()` | Not started | +| Category C | `Flock.set_separation()` | Not started | +| Category C | `Cover.find_nearest()` | Not started | +| Category C | `Cover.reserve()` | Not started | +| Category C | `Cover.release()` | Not started | +| Category C | `Threat.add()` | Not started | +| Category C | `Threat.get_highest()` | Not started | +| Category C | `Threat.clear()` | Not started | +| Category C | `Spawner.configure()` | Not started | +| Category C | `Spawner.set_limit()` | Not started | +| Category C | `Spawner.spawn_wave()` | Not started | +| Category C | `WorldStream.load_region()` | Not started | +| Category C | `WorldStream.unload_region()` | Not started | +| Category C | `WorldGen.generate_chunk()` | Not started | +| Category C | `WorldGen.set_seed()` | Not started | +| Category C | `Biome.get_at()` | Not started | +| Category C | `Biome.set_region()` | Not started | +| Category C | `Biome.get_properties()` | Not started | +| Category C | `Weather.set_rain()` | Not started | +| Category C | `Weather.set_wind()` | Not started | +| Category C | `Weather.set_snow()` | Not started | +| Category C | `DayNight.set_time()` | Not started | +| Category C | `DayNight.set_cycle_speed()` | Not started | +| Category C | `Destruction.fracture()` | Not started | +| Category C | `Destruction.apply_damage()` | Not started | +| Category D | `Material.set_texture()` | Partial: existing substrate; requested binding/behavior missing | +| Category D | `Material.set_parameter()` | Partial: existing substrate; requested binding/behavior missing | +| Category D | `Shader.load()` | Not started | +| Category D | `Shader.set_uniform()` | Not started | +| Category D | `Shader.enable_keyword()` | Not started | +| Category D | `Particles.create()` | Not started | +| Category D | `Particles.emit()` | Not started | +| Category D | `Particles.set_rate()` | Not started | +| Category D | `PostFX.set_bloom()` | Not started | +| Category D | `PostFX.set_exposure()` | Not started | +| Category D | `PostFX.set_vignette()` | Not started | +| Category D | `Light.set_intensity()` | Partial: existing substrate; requested binding/behavior missing | +| Category D | `Light.set_range()` | Partial: existing substrate; requested binding/behavior missing | +| Category D | `Light.set_shadow_enabled()` | Partial: existing substrate; requested binding/behavior missing | +| Category D | `Volumetrics.set_density()` | Not started | +| Category D | `Volumetrics.set_scattering()` | Not started | +| Category D | `Mesh.create()` | Not started | +| Category D | `Mesh.set_vertices()` | Not started | +| Category D | `Mesh.set_indices()` | Not started | +| Category D | `MeshDeform.set_vertex()` | Not started | +| Category D | `MeshDeform.apply()` | Not started | +| Category D | `Terrain.set_height()` | Not started | +| Category D | `Terrain.paint_layer()` | Not started | +| Category D | `Texture.create()` | Not started | +| Category D | `Texture.set_pixels()` | Not started | +| Category D | `Texture.generate_mipmaps()` | Not started | +| Category D | `RenderTarget.create()` | Partial: existing substrate; requested binding/behavior missing | +| Category D | `RenderTarget.capture_camera()` | Partial: existing substrate; requested binding/behavior missing | +| Category D | `Decal.spawn()` | Not started | +| Category D | `Decal.set_material()` | Not started | +| Category D | `Cloth.set_wind()` | Not started | +| Category D | `Cloth.set_stiffness()` | Not started | +| Category D | `Instancing.create_batch()` | Partial: existing substrate; requested binding/behavior missing | +| Category D | `Instancing.update_instances()` | Partial: existing substrate; requested binding/behavior missing | +| Category D | `LOD.set_distances()` | Partial: existing substrate; requested binding/behavior missing | +| Category D | `LOD.set_bias()` | Partial: existing substrate; requested binding/behavior missing | +| Category D | `Occlusion.set_enabled()` | Not started | +| Category D | `Occlusion.set_occluder()` | Not started | +| Category D | `RenderSettings.set_shadow_quality()` | Partial: existing substrate; requested binding/behavior missing | +| Category D | `RenderSettings.set_antialiasing()` | Partial: existing substrate; requested binding/behavior missing | +| Category D | `Sprite.set_region()` | Partial: existing substrate; requested binding/behavior missing | +| Category D | `Sprite.set_flip()` | Partial: existing substrate; requested binding/behavior missing | +| Category D | `Sprite.set_sorting_order()` | Partial: existing substrate; requested binding/behavior missing | +| Category D | `Tilemap.set_tile()` | Not started | +| Category D | `Tilemap.fill_area()` | Not started | +| Category D | `Tilemap.get_tile()` | Not started | +| Category E | `SaveGame.write()` | Not started | +| Category E | `SaveGame.read()` | Not started | +| Category E | `SaveGame.delete()` | Not started | +| Category E | `Preferences.set()` | Not started | +| Category E | `Preferences.get()` | Not started | +| Category E | `Preferences.reset()` | Not started | +| Category E | `InputMap.register()` | Partial: existing substrate; requested binding/behavior missing | +| Category E | `InputMap.rebind()` | Partial: existing substrate; requested binding/behavior missing | +| Category E | `InputMap.get_strength()` | Partial: existing substrate; requested binding/behavior missing | +| Category E | `InputContext.push()` | Not started | +| Category E | `InputContext.pop()` | Not started | +| Category E | `Gamepad.get_axis()` | Not started | +| Category E | `Gamepad.get_button()` | Not started | +| Category E | `Gamepad.set_vibration()` | Not started | +| Category E | `Touch.get_points()` | Not started | +| Category E | `Touch.get_swipe()` | Not started | +| Category E | `Touch.get_pinch()` | Not started | +| Category E | `Mouse.capture()` | Not started | +| Category E | `Mouse.release()` | Not started | +| Category E | `Mouse.get_delta()` | Not started | +| Category E | `Assets.load_async()` | Partial: existing substrate; requested binding/behavior missing | +| Category E | `Assets.release()` | Partial: existing substrate; requested binding/behavior missing | +| Category E | `Assets.get_status()` | Partial: existing substrate; requested binding/behavior missing | +| Category E | `AssetBundle.load()` | Not started | +| Category E | `AssetBundle.unload()` | Not started | +| Category E | `Serializer.encode()` | Partial: existing substrate; requested binding/behavior missing | +| Category E | `Serializer.decode()` | Partial: existing substrate; requested binding/behavior missing | +| Category E | `Localization.translate()` | Not started | +| Category E | `Localization.set_locale()` | Not started | +| Category E | `Random.set_seed()` | Not started | +| Category E | `Random.range()` | Not started | +| Category E | `Random.weighted_choice()` | Not started | +| Category E | `Display.set_resolution()` | Partial: existing substrate; requested binding/behavior missing | +| Category E | `Display.set_fullscreen()` | Partial: existing substrate; requested binding/behavior missing | +| Category E | `Profiler.begin_sample()` | Partial: existing substrate; requested binding/behavior missing | +| Category E | `Profiler.end_sample()` | Partial: existing substrate; requested binding/behavior missing | +| Category E | `Profiler.get_results()` | Partial: existing substrate; requested binding/behavior missing | +| Category E | `Memory.get_usage()` | Partial: existing substrate; requested binding/behavior missing | +| Category E | `Memory.get_allocations()` | Partial: existing substrate; requested binding/behavior missing | +| Category E | `Debug.draw_line()` | Partial: existing substrate; requested binding/behavior missing | +| Category E | `Debug.draw_sphere()` | Partial: existing substrate; requested binding/behavior missing | +| Category E | `Debug.draw_box()` | Partial: existing substrate; requested binding/behavior missing | +| Category E | `Capture.screenshot()` | Partial: existing substrate; requested binding/behavior missing | +| Category E | `Capture.save_to_user_folder()` | Partial: existing substrate; requested binding/behavior missing | +| Category E | `Task.run_async()` | Not started | +| Category E | `Task.cancel()` | Not started | +| Category E | `Task.get_status()` | Not started | +| Category E | `Platform.get_os()` | Not started | +| Category E | `Platform.get_architecture()` | Not started | +| Category E | `Platform.is_mobile()` | Not started | +| Category E | `Accessibility.get_preferences()` | Not started | +| Category E | `Accessibility.set_subtitles()` | Not started | +| Category F | `UI.open()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `UI.close()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `UI.is_open()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `UI.set_state()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `UI.get_state()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `UI.watch_state()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `UI.send()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `UI.on_message()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `UI.focus()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `UI.blur()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `UI.get_focused()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `UI.set_z_index()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `UI.bring_to_front()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `UI.open_modal()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `UI.close_modal()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `UI.capture_input()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `UI.release_input()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `UITheme.set()` | Not started | +| Category F | `UITheme.get()` | Not started | +| Category F | `UITheme.update_variable()` | Not started | +| Category F | `UI.translate()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `UI.set_locale()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `UI.set_viewport()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `UI.set_scale()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `WorldUI.attach()` | Not started | +| Category F | `WorldUI.set_size()` | Not started | +| Category F | `WorldUI.face_camera()` | Not started | +| Category F | `UI.world_to_screen()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `UI.screen_to_world()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `UI.notify()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `UI.dismiss_notification()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `UI.set_loading_progress()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `UI.set_loading_message()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `UI.set_screen_reader_label()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `UI.announce()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `UI.add_class()` | Partial: existing substrate; requested binding/behavior missing | +| Category F | `UI.remove_class()` | Partial: existing substrate; requested binding/behavior missing | + +## Entity-foundations slice + +17 shared runtime APIs now use the existing SceneWorld command/lifetime system. +This is the first incremental slice of Phase 1; the rest of the comprehensive +request remains unfinished. Dynamic attachment, prefabs/pooling, streaming, +physics expansion, input devices, persistence, AI, rendering and browser UI are +not implemented by this slice. Do not infer their support from this checklist. + +Implementation choices: runtime-only metadata avoids a project schema change; +subtree deletion uses validated batches; deadlines use generation-checked refs; +world-preserving reparent rejects shear; component queries use existing schemas. +C and C++ preserve their established error/ownership conventions. No dependencies +were added. The next independent slice should finish component lifecycle and +prefab/scene instance lifecycle before pooling; then wire fixed input and saves. +Advanced physics needs a backend decision/integration, and interactive HTML needs +an actual browser DOM runtime with a capability-limited gameplay bridge. + +Validation is recorded here after execution. Missing toolchains and Windows +installer/hardware qualification remain separate from API test results. + +## Executed validation (Linux) + +- `cargo test -p engine-world -p engine-play -p engine-scripting --locked`: + **137 passed**, 0 failures (30 world, 67 play, 40 scripting). +- `cargo check --workspace --all-targets --locked`: passed, including editor/runtime. +- Clippy for all targets of the three changed crates with `-D warnings`: passed. +- `cargo fmt --all --check` and Git whitespace checks: passed. +- Live foundation fixtures: Lua 5.4, Luau, JavaScript, HTML inline JavaScript, + Python, C and C++ passed. C#, Java and PHP fixtures were **skipped** because + those toolchains are unavailable. Their SDK code is included but unverified here. +- Built the separate bundled Luau host and put it on PATH for the Rust tests. + This also resolved the original audio-recording test's missing-host failure. +- Both published Lua/Python example scripts executed in the real world adapter. +- Docs: lint with zero warnings, static build, TypeScript check, moderate npm audit + (0 vulnerabilities), 17 site tests, API reference/example validation all passed. + The example validator checks coverage/rendering (1600 existing/new snippets); + it does not execute every documented snippet in every external toolchain. +- Local Windows installer command failed: `powershell` is absent on this Linux + host. Windows workflow qualification is attempted separately; no fresh installer + or Windows/macOS hardware verification is claimed by these Linux results. + +## Changed files and deliverables + +Engine implementation: `crates/engine-world/src/foundations.rs`, `world.rs`, +`lib.rs`; `crates/engine-play/src/entity_services.rs`, `script_runtime.rs`, `lib.rs`. +Bindings/editor hints: `crates/engine-scripting/src/lua.rs`, `cpp_sdk.rs`, +`workspace.rs`, and `sdk/{gameplay.lua,gameplay.js,rustic.h,rustic.py,rustic.php, +Rustic.cs,Rustic.java,entity-api.lua,entity-api.d.ts}`. +Engine docs: this checklist, `Scripting/API/entity-foundations.md`, +`Scripting/README.md`. Tested examples: `examples/entity-foundations/`. + +Docs repository: matching checklist/API/README Markdown, `lib/api-reference.ts`, +`lib/api-reference-markdown.ts`, `lib/api-example-data.json`, `lib/api-examples.ts`, +`lib/docs-catalog.ts`. Navigation adds a setup guide, status guide and 17 +individual API pages. Signatures and language differences are listed in the +[entity foundations reference](Scripting/API/entity-foundations.md). + +Examples repository: `entity-foundations/{README.md,behavior.lua,behavior.py}`. +No cloud hosting, database, billing, infrastructure or other unrelated repo changed. diff --git a/docs/Scripting/API/entity-foundations.md b/docs/Scripting/API/entity-foundations.md new file mode 100644 index 0000000..700cb95 --- /dev/null +++ b/docs/Scripting/API/entity-foundations.md @@ -0,0 +1,355 @@ +# Entity foundations + +Use the shared runtime world to label, organize, reparent, query and destroy objects. +These are live synchronous queries, so a change is visible to the next query in the +same callback, including queries from another scripting language. + +## Setup and language support + +Create an object in the editor. Use **Programming > New Script > Object Component +Script**, choose a language, and attach the script to that object with **+ Add +Component** or by dragging it from Explorer. Press **Play**. Do not edit scene or +manifest files to attach a behavior. Stop Play to restore the authored scene. + +The SDKs provide these calls in Lua 5.4, Luau, JavaScript, Python, C, C++, C#, Java, +and PHP; inline HTML JavaScript shares the JavaScript calls. External languages +require their existing toolchains. PHP and HTML remain restricted to `ui/` assets; +for those scripts pass a real scene entity ID/path rather than the UI script owner. +The HTML adapter does not provide a browser DOM or interactive HTML controls. + +Python imports services from `rustic`; C includes `rustic.h`; C++ includes +`rustic.hpp` and uses `Tags::add`; C# uses `using static Rustic`; Java extends +`Rustic` or qualifies `Rustic.Tags`; PHP uses `Tags::add`. Lua and JavaScript use +`Tags.add`. New service methods use the exact snake_case spellings below. + +## Parameters + +| Argument | Type and meaning | +| --- | --- | +| entity | Live stable UUID string or scene path; SDK object-path handles are accepted in Lua, JavaScript and Python. Passing null/nil/None to an entity parameter selects the component owner. A global/UI owner need not be a real scene entity. | +| name | Case-sensitive tag/group string, or exact component schema string for Components. Tag/group names must contain non-whitespace text, be at most 128 UTF-8 bytes, and contain no control characters. | +| parent | Entity ID/path or null to detach. C uses NULL, C++ uses an empty string, Python None, Lua/Luau nil. Required argument. | +| keep_world | Boolean, default true. With false, keep the local transform. C requires the explicit boolean; Java has a true-default overload. | +| delay | Finite nonnegative seconds on the scaled fixed simulation clock. | +| center | Three finite numbers in world coordinates. C/C++ use RusticVector3; other SDKs use arrays/tables. Values must fit finite f32 scene coordinates. | +| radius | Finite nonnegative world-space distance. | + +## Return and timing + +| Signature | Return | +| --- | --- | +| Tags.add(entity, name) | bool: true if newly added | +| Tags.remove(entity, name) | bool: true if removed | +| Tags.find_all(name) | entity ID list | +| Groups.create(name) | bool: true if newly created | +| Groups.add(name, entity) | bool: true if newly added | +| Groups.get_members(name) | entity ID list | +| Hierarchy.set_parent(entity, parent, keep_world = true) | void | +| Hierarchy.get_children(entity) | direct child entity ID list | +| Hierarchy.detach(entity, keep_world = true) | void | +| Entity.destroy(entity) | void | +| Entity.destroy_after(entity, delay) | void | +| Entity.is_valid(entity) | bool | +| Components.has(entity, name) | bool | +| Components.get_all(entity) | component schema/script ID string list | +| Components.find_in_children(entity, name) | descendant entity ID list | +| World.find_by_tag(name) | entity ID list; same implementation as Tags.find_all | +| World.find_in_radius(center, radius) | entity ID list | + +Lists are deterministic in ascending stable ID order (component names sort +lexically). Lua/Luau use tables, JavaScript/Python/PHP arrays/lists, Java and C# +string arrays, C++ `std::vector`, and C `RusticList` with `count` and +`items`. C query strings/lists live until the next callback; copy them to retain +across callbacks. An empty query returns an empty list, never a fake entity. + +## Behavior and limitations + +- Tags, groups and destruction deadlines belong to the runtime world. They reset + on Stop/new Play and are not serialized into authored scenes or save files. + Tags are idempotent, with at most 128 tags per entity; at most 4096 groups exist + per world. A group must be created before use. Empty groups remain available. +- Destruction removes the entire subtree in one validated command batch and + invalidates generational references. Tags, group membership and deadlines clean + up even when another engine subsystem despawns an object. A query that uses a + destroyed entity errors, except `Entity.is_valid`, which returns false for a + well-formed absent UUID. Malformed UUIDs are errors. IDs are never raw pointers. +- A destroyed component owner stops receiving Update/FixedUpdate; OnDisable and + OnDestroy run once at the runtime cleanup boundary, and owned operations/signals + are released. The current callback can finish; do not read owner properties + after destroying it. Global scripts do not end when an unrelated object dies. +- A new `destroy_after` replaces the old deadline. Zero destroys immediately; + positive delays advance once per fixed step using the existing Clock time + scale. Clock pause and paused Play stop countdowns. Destruction occurs at a fixed + boundary, so timing is quantized to simulation ticks and does not depend on + render frequency. Deadlines survive the scheduling script's successful reload; + they belong to the entity/world, not a script callback. +- Reparenting defaults to preserving the world pose, including descendants. It + validates both entities and rejects cycles before mutation. A nonuniform parent + scale combined with rotation can require shear; the current TRS representation + cannot preserve that pose, and the call errors without mutation. Pass false + only if keeping the local transform is intended. +- Components queries report the existing authored built-ins: rustic.transform, + rustic.part_attributes, rustic.name, rustic.folder, rustic.parent, rustic.mesh, + rustic.material, rustic.camera, rustic.light, rustic.primitive, rustic.scripts; + each attached script also reports `script:`. Missing names return + false/no matches. These are presence queries, not component payload handles; + disabled script components are still present. Unknown preserved serialized + schemas and derived private ECS bookkeeping are not reported. +- find_in_children searches every descendant and excludes the root. get_children + returns only immediate children. Radius queries measure world **origins**, not + collider extents: use [Physics queries](/docs/api/physics-actions/queries) for + collision overlap. A zero radius matches coincident origins. +- Invalid names, absent objects/groups, non-finite/negative delays or radii, bad + vector sizes, cycles and unsupported transforms produce script errors through + the existing adapter. Lua/PHP/Python/etc. can catch errors using their normal + mechanisms; C's SDK reports a callback failure. Uncaught callback failures use + the existing script disabling/logging behavior. Check the Play Console. +- Tag/radius/component searches are linear scans. Hierarchy changes/destruction + validate a whole command batch; descendant operations scale with the subtree. + Cache query results for a frame and use direct IDs for frequent queries. This + implementation adds no new dependencies or GPU/backend-specific requirements. + Linux headless tests are verification evidence, not Windows/macOS hardware + qualification or a guarantee of exported-game packaging. +- Dynamic component attachment/activation, prefabs, pooling, scene streaming, + navigation, advanced physics/rendering and DOM UI are separate unfinished work. + See the [expansion status](/docs/guides/scripting-expansion-status). Existing + [events](/docs/api/events) and [timers](/docs/api/timer) remain the callback tools. + +## Example + +Create and attach this Lua Object Component Script to one object. Press Play. +Expect `entity foundations ready` in the Console; after one scaled fixed second, +the object disappears. Stop Play to restore it. No extra assets are required. + +```lua +return {Start=function() + local e=rustic.entity_id() + Tags.add(e,'enemy') + Groups.create('enemies') + Groups.add('enemies',e) + assert(Entity.is_valid(e)) + assert(Components.has(e,'rustic.transform')) + assert(#Tags.find_all('enemy')==1) + rustic.log('info','entity foundations ready') + Entity.destroy_after(e,1) +end} +``` + +Python uses the same live services: + +```python +from rustic import rustic, Tags, Groups, Entity, Components, run + +def on_start(): + e = rustic.entity_id() + Tags.add(e, 'enemy') + Groups.create('enemies') + Groups.add('enemies', e) + assert Entity.is_valid(e) + assert Components.has(e, 'rustic.transform') + assert len(Tags.find_all('enemy')) == 1 + rustic.log('info', 'entity foundations ready') + Entity.destroy_after(e, 1) + +run(globals()) +``` + +For a world-preserving hierarchy edit in JavaScript, use live IDs returned by +`Game.scene.Find` for existing objects: + +```javascript +const child = Game.scene.Find('Child'); +const parent = Game.scene.Find('Parent'); +Hierarchy.set_parent(child, parent); // child keeps its world pose +Hierarchy.detach(child); // child keeps that pose as a root +``` + +A missing path returns null from Find; check the lookup result before passing it +as an entity parameter, because null selects the behavior owner. + +## Tags.add + +Add a runtime tag. Repeated additions return false. + +Signature: `Tags.add(entity, name)`. Parameter types, return values, lifecycle, errors and +performance are specified in the tables and limitations above. + +```lua +Tags.add(rustic.entity_id(), "enemy") +``` + +## Tags.remove + +Remove a runtime tag; return false if it was absent. + +Signature: `Tags.remove(entity, name)`. Parameter types, return values, lifecycle, errors and +performance are specified in the tables and limitations above. + +```lua +Tags.remove(rustic.entity_id(), "enemy") +``` + +## Tags.find_all + +Find all live tagged entities in stable ID order. + +Signature: `Tags.find_all(name)`. Parameter types, return values, lifecycle, errors and +performance are specified in the tables and limitations above. + +```lua +Tags.find_all("enemy") +``` + +## Groups.create + +Create an empty runtime group; return false if it already exists. + +Signature: `Groups.create(name)`. Parameter types, return values, lifecycle, errors and +performance are specified in the tables and limitations above. + +```lua +Groups.create("enemies") +``` + +## Groups.add + +Add an entity to an existing group; return false for existing membership. + +Signature: `Groups.add(name, entity)`. Parameter types, return values, lifecycle, errors and +performance are specified in the tables and limitations above. + +```lua +Groups.add("enemies", rustic.entity_id()) +``` + +## Groups.get_members + +Return live members of an existing group. + +Signature: `Groups.get_members(name)`. Parameter types, return values, lifecycle, errors and +performance are specified in the tables and limitations above. + +```lua +Groups.get_members("enemies") +``` + +## Hierarchy.set_parent + +Change the parent transactionally, rejecting cycles and unrepresentable world poses. + +Signature: `Hierarchy.set_parent(entity, parent, keep_world = true)`. Parameter types, return values, lifecycle, errors and +performance are specified in the tables and limitations above. + +```lua +Hierarchy.set_parent(rustic.entity_id(), nil, true) +``` + +## Hierarchy.get_children + +Return direct children; use Components.find_in_children to search descendants. + +Signature: `Hierarchy.get_children(entity)`. Parameter types, return values, lifecycle, errors and +performance are specified in the tables and limitations above. + +```lua +Hierarchy.get_children(rustic.entity_id()) +``` + +## Hierarchy.detach + +Make an entity a root, preserving the world pose by default. + +Signature: `Hierarchy.detach(entity, keep_world = true)`. Parameter types, return values, lifecycle, errors and +performance are specified in the tables and limitations above. + +```lua +Hierarchy.detach(rustic.entity_id()) +``` + +## Entity.destroy + +Immediately remove the entity and its entire subtree. Further reads fail safely. + +Signature: `Entity.destroy(entity)`. Parameter types, return values, lifecycle, errors and +performance are specified in the tables and limitations above. + +```lua +Entity.destroy(rustic.entity_id()) +``` + +## Entity.destroy_after + +Replace the entity destruction deadline on the scaled fixed clock. Zero destroys now. + +Signature: `Entity.destroy_after(entity, delay)`. Parameter types, return values, lifecycle, errors and +performance are specified in the tables and limitations above. + +```lua +Entity.destroy_after(rustic.entity_id(), 1) +``` + +## Entity.is_valid + +Check a well-formed stable UUID; absent IDs return false, malformed IDs error. + +Signature: `Entity.is_valid(entity)`. Parameter types, return values, lifecycle, errors and +performance are specified in the tables and limitations above. + +```lua +Entity.is_valid(rustic.entity_id()) +``` + +## Components.has + +Check presence of an exact component schema/script ID. Missing names return false. + +Signature: `Components.has(entity, name)`. Parameter types, return values, lifecycle, errors and +performance are specified in the tables and limitations above. + +```lua +Components.has(rustic.entity_id(), "rustic.transform") +``` + +## Components.get_all + +Return sorted component schema and script ID strings, not mutable component handles. + +Signature: `Components.get_all(entity)`. Parameter types, return values, lifecycle, errors and +performance are specified in the tables and limitations above. + +```lua +Components.get_all(rustic.entity_id()) +``` + +## Components.find_in_children + +Return all descendants having the component; exclude the root. + +Signature: `Components.find_in_children(entity, name)`. Parameter types, return values, lifecycle, errors and +performance are specified in the tables and limitations above. + +```lua +Components.find_in_children(rustic.entity_id(), "rustic.transform") +``` + +## World.find_by_tag + +Use the existing Tags.find_all implementation to query tagged objects. + +Signature: `World.find_by_tag(name)`. Parameter types, return values, lifecycle, errors and +performance are specified in the tables and limitations above. + +```lua +World.find_by_tag("enemy") +``` + +## World.find_in_radius + +Find entity world origins inside an inclusive radius; no physics collider test. + +Signature: `World.find_in_radius(center, radius)`. Parameter types, return values, lifecycle, errors and +performance are specified in the tables and limitations above. + +```lua +World.find_in_radius({0, 0, 0}, 5) +``` diff --git a/docs/Scripting/README.md b/docs/Scripting/README.md index 3be6531..3f1603a 100644 --- a/docs/Scripting/README.md +++ b/docs/Scripting/README.md @@ -66,3 +66,6 @@ last good instance running. External callbacks have a three-second deadline and See [Edit scene objects](sceneObjects.md) to target built-in properties on another object using `rustic.game..` in native language syntax. + +See [Entity foundations](API/entity-foundations.md) for live tags, groups, hierarchy, +lifetime, component and world queries in the shared scripting runtime. diff --git a/lib/api-example-data.json b/lib/api-example-data.json index ce56fc0..eb8946a 100644 --- a/lib/api-example-data.json +++ b/lib/api-example-data.json @@ -2078,7 +2078,7 @@ }, { "title": "Animation.ik", - "note": "Create a direct Root \u2192 Middle \u2192 Tip joint chain with nonzero bone lengths before Play. Target is a world-space position.", + "note": "Create a direct Root → Middle → Tip joint chain with nonzero bone lengths before Play. Target is a world-space position.", "examples": { "Lua 5.4": "Animation.ik(\"Game.scene.Root\", \"Game.scene.Root.Middle\", \"Game.scene.Root.Middle.Tip\", {1,1,0})", "JavaScript": "Animation.ik(\"Game.scene.Root\", \"Game.scene.Root.Middle\", \"Game.scene.Root.Middle.Tip\", [1,1,0]);", @@ -2094,7 +2094,7 @@ }, { "title": "Animation.footPlacement", - "note": "Create a direct Root \u2192 Middle \u2192 Tip joint chain with nonzero bone lengths before Play. Target is a world-space position.", + "note": "Create a direct Root → Middle → Tip joint chain with nonzero bone lengths before Play. Target is a world-space position.", "examples": { "Lua 5.4": "Animation.footPlacement(\"Game.scene.Root\", \"Game.scene.Root.Middle\", \"Game.scene.Root.Middle.Tip\", {1,1,0})", "JavaScript": "Animation.footPlacement(\"Game.scene.Root\", \"Game.scene.Root.Middle\", \"Game.scene.Root.Middle.Tip\", [1,1,0]);", @@ -2318,5 +2318,279 @@ "HTML / inline JS": "// This SDK manages builder storage automatically." } } + ], + "entity-foundations": [ + { + "title": "Tags.add", + "note": "Use a live component owner. Lists return stable IDs or component schema names; see the function reference for exact returns. Destroy examples end this owner’s lifetime.", + "examples": { + "Lua 5.4": "Tags.add(rustic.entity_id(), \"enemy\")", + "JavaScript": "Tags.add(rustic.entity_id(), \"enemy\");", + "Python": "Tags.add(rustic.entity_id(), \"enemy\")", + "C++": "Tags::add(rustic.entity_id(), \"enemy\");", + "C#": "Tags.add(rustic.entity_id(), \"enemy\");", + "Luau": "Tags.add(rustic.entity_id(), \"enemy\")", + "C": "Tags.add(rustic.entity_id(), \"enemy\");", + "Java": "Tags.add(rustic.entity_id(), \"enemy\");", + "PHP": "Tags::add($rustic->entity_id(), \"enemy\");", + "HTML / inline JS": "Tags.add(rustic.entity_id(), \"enemy\");" + } + }, + { + "title": "Tags.remove", + "note": "Use a live component owner. Lists return stable IDs or component schema names; see the function reference for exact returns. Destroy examples end this owner’s lifetime.", + "examples": { + "Lua 5.4": "Tags.add(rustic.entity_id(), \"enemy\")\nTags.remove(rustic.entity_id(), \"enemy\")", + "JavaScript": "Tags.add(rustic.entity_id(), \"enemy\");\nTags.remove(rustic.entity_id(), \"enemy\");", + "Python": "Tags.add(rustic.entity_id(), \"enemy\")\nTags.remove(rustic.entity_id(), \"enemy\")", + "C++": "Tags::add(rustic.entity_id(), \"enemy\");\nTags::remove(rustic.entity_id(), \"enemy\");", + "C#": "Tags.add(rustic.entity_id(), \"enemy\");\nTags.remove(rustic.entity_id(), \"enemy\");", + "Luau": "Tags.add(rustic.entity_id(), \"enemy\")\nTags.remove(rustic.entity_id(), \"enemy\")", + "C": "Tags.add(rustic.entity_id(), \"enemy\");\nTags.remove(rustic.entity_id(), \"enemy\");", + "Java": "Tags.add(rustic.entity_id(), \"enemy\");\nTags.remove(rustic.entity_id(), \"enemy\");", + "PHP": "Tags::add($rustic->entity_id(), \"enemy\");\nTags::remove($rustic->entity_id(), \"enemy\");", + "HTML / inline JS": "Tags.add(rustic.entity_id(), \"enemy\");\nTags.remove(rustic.entity_id(), \"enemy\");" + } + }, + { + "title": "Tags.find_all", + "note": "Use a live component owner. Lists return stable IDs or component schema names; see the function reference for exact returns. Destroy examples end this owner’s lifetime.", + "examples": { + "Lua 5.4": "Tags.add(rustic.entity_id(), \"enemy\")\nTags.find_all(\"enemy\")", + "JavaScript": "Tags.add(rustic.entity_id(), \"enemy\");\nTags.find_all(\"enemy\");", + "Python": "Tags.add(rustic.entity_id(), \"enemy\")\nTags.find_all(\"enemy\")", + "C++": "Tags::add(rustic.entity_id(), \"enemy\");\nTags::find_all(\"enemy\");", + "C#": "Tags.add(rustic.entity_id(), \"enemy\");\nTags.find_all(\"enemy\");", + "Luau": "Tags.add(rustic.entity_id(), \"enemy\")\nTags.find_all(\"enemy\")", + "C": "Tags.add(rustic.entity_id(), \"enemy\");\nTags.find_all(\"enemy\");", + "Java": "Tags.add(rustic.entity_id(), \"enemy\");\nTags.find_all(\"enemy\");", + "PHP": "Tags::add($rustic->entity_id(), \"enemy\");\nTags::find_all(\"enemy\");", + "HTML / inline JS": "Tags.add(rustic.entity_id(), \"enemy\");\nTags.find_all(\"enemy\");" + } + }, + { + "title": "Groups.create", + "note": "Use a live component owner. Lists return stable IDs or component schema names; see the function reference for exact returns. Destroy examples end this owner’s lifetime.", + "examples": { + "Lua 5.4": "Groups.create(\"enemies\")", + "JavaScript": "Groups.create(\"enemies\");", + "Python": "Groups.create(\"enemies\")", + "C++": "Groups::create(\"enemies\");", + "C#": "Groups.create(\"enemies\");", + "Luau": "Groups.create(\"enemies\")", + "C": "Groups.create(\"enemies\");", + "Java": "Groups.create(\"enemies\");", + "PHP": "Groups::create(\"enemies\");", + "HTML / inline JS": "Groups.create(\"enemies\");" + } + }, + { + "title": "Groups.add", + "note": "Use a live component owner. Lists return stable IDs or component schema names; see the function reference for exact returns. Destroy examples end this owner’s lifetime.", + "examples": { + "Lua 5.4": "Groups.create(\"enemies\")\nGroups.add(\"enemies\", rustic.entity_id())", + "JavaScript": "Groups.create(\"enemies\");\nGroups.add(\"enemies\", rustic.entity_id());", + "Python": "Groups.create(\"enemies\")\nGroups.add(\"enemies\", rustic.entity_id())", + "C++": "Groups::create(\"enemies\");\nGroups::add(\"enemies\", rustic.entity_id());", + "C#": "Groups.create(\"enemies\");\nGroups.add(\"enemies\", rustic.entity_id());", + "Luau": "Groups.create(\"enemies\")\nGroups.add(\"enemies\", rustic.entity_id())", + "C": "Groups.create(\"enemies\");\nGroups.add(\"enemies\", rustic.entity_id());", + "Java": "Groups.create(\"enemies\");\nGroups.add(\"enemies\", rustic.entity_id());", + "PHP": "Groups::create(\"enemies\");\nGroups::add(\"enemies\", $rustic->entity_id());", + "HTML / inline JS": "Groups.create(\"enemies\");\nGroups.add(\"enemies\", rustic.entity_id());" + } + }, + { + "title": "Groups.get_members", + "note": "Use a live component owner. Lists return stable IDs or component schema names; see the function reference for exact returns. Destroy examples end this owner’s lifetime.", + "examples": { + "Lua 5.4": "Groups.create(\"enemies\")\nGroups.get_members(\"enemies\")", + "JavaScript": "Groups.create(\"enemies\");\nGroups.get_members(\"enemies\");", + "Python": "Groups.create(\"enemies\")\nGroups.get_members(\"enemies\")", + "C++": "Groups::create(\"enemies\");\nGroups::get_members(\"enemies\");", + "C#": "Groups.create(\"enemies\");\nGroups.get_members(\"enemies\");", + "Luau": "Groups.create(\"enemies\")\nGroups.get_members(\"enemies\")", + "C": "Groups.create(\"enemies\");\nGroups.get_members(\"enemies\");", + "Java": "Groups.create(\"enemies\");\nGroups.get_members(\"enemies\");", + "PHP": "Groups::create(\"enemies\");\nGroups::get_members(\"enemies\");", + "HTML / inline JS": "Groups.create(\"enemies\");\nGroups.get_members(\"enemies\");" + } + }, + { + "title": "Hierarchy.set_parent", + "note": "Use a live component owner. Lists return stable IDs or component schema names; see the function reference for exact returns. Destroy examples end this owner’s lifetime.", + "examples": { + "Lua 5.4": "Hierarchy.set_parent(rustic.entity_id(), nil, true)", + "JavaScript": "Hierarchy.set_parent(rustic.entity_id(), null, true);", + "Python": "Hierarchy.set_parent(rustic.entity_id(), None, True)", + "C++": "Hierarchy::set_parent(rustic.entity_id(), \"\", true);", + "C#": "Hierarchy.set_parent(rustic.entity_id(), null, true);", + "Luau": "Hierarchy.set_parent(rustic.entity_id(), nil, true)", + "C": "Hierarchy.set_parent(rustic.entity_id(), NULL, true);", + "Java": "Hierarchy.set_parent(rustic.entity_id(), null, true);", + "PHP": "Hierarchy::set_parent($rustic->entity_id(), null, true);", + "HTML / inline JS": "Hierarchy.set_parent(rustic.entity_id(), null, true);" + } + }, + { + "title": "Hierarchy.get_children", + "note": "Use a live component owner. Lists return stable IDs or component schema names; see the function reference for exact returns. Destroy examples end this owner’s lifetime.", + "examples": { + "Lua 5.4": "Hierarchy.get_children(rustic.entity_id())", + "JavaScript": "Hierarchy.get_children(rustic.entity_id());", + "Python": "Hierarchy.get_children(rustic.entity_id())", + "C++": "Hierarchy::get_children(rustic.entity_id());", + "C#": "Hierarchy.get_children(rustic.entity_id());", + "Luau": "Hierarchy.get_children(rustic.entity_id())", + "C": "Hierarchy.get_children(rustic.entity_id());", + "Java": "Hierarchy.get_children(rustic.entity_id());", + "PHP": "Hierarchy::get_children($rustic->entity_id());", + "HTML / inline JS": "Hierarchy.get_children(rustic.entity_id());" + } + }, + { + "title": "Hierarchy.detach", + "note": "Use a live component owner. Lists return stable IDs or component schema names; see the function reference for exact returns. Destroy examples end this owner’s lifetime.", + "examples": { + "Lua 5.4": "Hierarchy.detach(rustic.entity_id(), true)", + "JavaScript": "Hierarchy.detach(rustic.entity_id(), true);", + "Python": "Hierarchy.detach(rustic.entity_id(), True)", + "C++": "Hierarchy::detach(rustic.entity_id(), true);", + "C#": "Hierarchy.detach(rustic.entity_id(), true);", + "Luau": "Hierarchy.detach(rustic.entity_id(), true)", + "C": "Hierarchy.detach(rustic.entity_id(), true);", + "Java": "Hierarchy.detach(rustic.entity_id(), true);", + "PHP": "Hierarchy::detach($rustic->entity_id(), true);", + "HTML / inline JS": "Hierarchy.detach(rustic.entity_id(), true);" + } + }, + { + "title": "Entity.destroy", + "note": "Use a live component owner. Lists return stable IDs or component schema names; see the function reference for exact returns. Destroy examples end this owner’s lifetime.", + "examples": { + "Lua 5.4": "Entity.destroy(rustic.entity_id())", + "JavaScript": "Entity.destroy(rustic.entity_id());", + "Python": "Entity.destroy(rustic.entity_id())", + "C++": "Entity::destroy(rustic.entity_id());", + "C#": "Entity.destroy(rustic.entity_id());", + "Luau": "Entity.destroy(rustic.entity_id())", + "C": "Entity.destroy(rustic.entity_id());", + "Java": "Entity.destroy(rustic.entity_id());", + "PHP": "Entity::destroy($rustic->entity_id());", + "HTML / inline JS": "Entity.destroy(rustic.entity_id());" + } + }, + { + "title": "Entity.destroy_after", + "note": "Use a live component owner. Lists return stable IDs or component schema names; see the function reference for exact returns. Destroy examples end this owner’s lifetime.", + "examples": { + "Lua 5.4": "Entity.destroy_after(rustic.entity_id(), 1)", + "JavaScript": "Entity.destroy_after(rustic.entity_id(), 1);", + "Python": "Entity.destroy_after(rustic.entity_id(), 1)", + "C++": "Entity::destroy_after(rustic.entity_id(), 1);", + "C#": "Entity.destroy_after(rustic.entity_id(), 1);", + "Luau": "Entity.destroy_after(rustic.entity_id(), 1)", + "C": "Entity.destroy_after(rustic.entity_id(), 1);", + "Java": "Entity.destroy_after(rustic.entity_id(), 1);", + "PHP": "Entity::destroy_after($rustic->entity_id(), 1);", + "HTML / inline JS": "Entity.destroy_after(rustic.entity_id(), 1);" + } + }, + { + "title": "Entity.is_valid", + "note": "Use a live component owner. Lists return stable IDs or component schema names; see the function reference for exact returns. Destroy examples end this owner’s lifetime.", + "examples": { + "Lua 5.4": "Entity.is_valid(rustic.entity_id())", + "JavaScript": "Entity.is_valid(rustic.entity_id());", + "Python": "Entity.is_valid(rustic.entity_id())", + "C++": "Entity::is_valid(rustic.entity_id());", + "C#": "Entity.is_valid(rustic.entity_id());", + "Luau": "Entity.is_valid(rustic.entity_id())", + "C": "Entity.is_valid(rustic.entity_id());", + "Java": "Entity.is_valid(rustic.entity_id());", + "PHP": "Entity::is_valid($rustic->entity_id());", + "HTML / inline JS": "Entity.is_valid(rustic.entity_id());" + } + }, + { + "title": "Components.has", + "note": "Use a live component owner. Lists return stable IDs or component schema names; see the function reference for exact returns. Destroy examples end this owner’s lifetime.", + "examples": { + "Lua 5.4": "Components.has(rustic.entity_id(), \"rustic.transform\")", + "JavaScript": "Components.has(rustic.entity_id(), \"rustic.transform\");", + "Python": "Components.has(rustic.entity_id(), \"rustic.transform\")", + "C++": "Components::has(rustic.entity_id(), \"rustic.transform\");", + "C#": "Components.has(rustic.entity_id(), \"rustic.transform\");", + "Luau": "Components.has(rustic.entity_id(), \"rustic.transform\")", + "C": "Components.has(rustic.entity_id(), \"rustic.transform\");", + "Java": "Components.has(rustic.entity_id(), \"rustic.transform\");", + "PHP": "Components::has($rustic->entity_id(), \"rustic.transform\");", + "HTML / inline JS": "Components.has(rustic.entity_id(), \"rustic.transform\");" + } + }, + { + "title": "Components.get_all", + "note": "Use a live component owner. Lists return stable IDs or component schema names; see the function reference for exact returns. Destroy examples end this owner’s lifetime.", + "examples": { + "Lua 5.4": "Components.get_all(rustic.entity_id())", + "JavaScript": "Components.get_all(rustic.entity_id());", + "Python": "Components.get_all(rustic.entity_id())", + "C++": "Components::get_all(rustic.entity_id());", + "C#": "Components.get_all(rustic.entity_id());", + "Luau": "Components.get_all(rustic.entity_id())", + "C": "Components.get_all(rustic.entity_id());", + "Java": "Components.get_all(rustic.entity_id());", + "PHP": "Components::get_all($rustic->entity_id());", + "HTML / inline JS": "Components.get_all(rustic.entity_id());" + } + }, + { + "title": "Components.find_in_children", + "note": "Use a live component owner. Lists return stable IDs or component schema names; see the function reference for exact returns. Destroy examples end this owner’s lifetime.", + "examples": { + "Lua 5.4": "Components.find_in_children(rustic.entity_id(), \"rustic.transform\")", + "JavaScript": "Components.find_in_children(rustic.entity_id(), \"rustic.transform\");", + "Python": "Components.find_in_children(rustic.entity_id(), \"rustic.transform\")", + "C++": "Components::find_in_children(rustic.entity_id(), \"rustic.transform\");", + "C#": "Components.find_in_children(rustic.entity_id(), \"rustic.transform\");", + "Luau": "Components.find_in_children(rustic.entity_id(), \"rustic.transform\")", + "C": "Components.find_in_children(rustic.entity_id(), \"rustic.transform\");", + "Java": "Components.find_in_children(rustic.entity_id(), \"rustic.transform\");", + "PHP": "Components::find_in_children($rustic->entity_id(), \"rustic.transform\");", + "HTML / inline JS": "Components.find_in_children(rustic.entity_id(), \"rustic.transform\");" + } + }, + { + "title": "World.find_by_tag", + "note": "Use a live component owner. Lists return stable IDs or component schema names; see the function reference for exact returns. Destroy examples end this owner’s lifetime.", + "examples": { + "Lua 5.4": "Tags.add(rustic.entity_id(), \"enemy\")\nWorld.find_by_tag(\"enemy\")", + "JavaScript": "Tags.add(rustic.entity_id(), \"enemy\");\nWorld.find_by_tag(\"enemy\");", + "Python": "Tags.add(rustic.entity_id(), \"enemy\")\nWorld.find_by_tag(\"enemy\")", + "C++": "Tags::add(rustic.entity_id(), \"enemy\");\nWorld::find_by_tag(\"enemy\");", + "C#": "Tags.add(rustic.entity_id(), \"enemy\");\nWorld.find_by_tag(\"enemy\");", + "Luau": "Tags.add(rustic.entity_id(), \"enemy\")\nWorld.find_by_tag(\"enemy\")", + "C": "Tags.add(rustic.entity_id(), \"enemy\");\nWorld.find_by_tag(\"enemy\");", + "Java": "Tags.add(rustic.entity_id(), \"enemy\");\nWorld.find_by_tag(\"enemy\");", + "PHP": "Tags::add($rustic->entity_id(), \"enemy\");\nWorld::find_by_tag(\"enemy\");", + "HTML / inline JS": "Tags.add(rustic.entity_id(), \"enemy\");\nWorld.find_by_tag(\"enemy\");" + } + }, + { + "title": "World.find_in_radius", + "note": "Use a live component owner. Lists return stable IDs or component schema names; see the function reference for exact returns. Destroy examples end this owner’s lifetime.", + "examples": { + "Lua 5.4": "World.find_in_radius({0, 0, 0}, 5)", + "JavaScript": "World.find_in_radius([0, 0, 0], 5);", + "Python": "World.find_in_radius([0, 0, 0], 5)", + "C++": "World::find_in_radius(RusticVector3{0, 0, 0}, 5);", + "C#": "World.find_in_radius(new double[]{0, 0, 0}, 5);", + "Luau": "World.find_in_radius({0, 0, 0}, 5)", + "C": "World.find_in_radius((RusticVector3){0, 0, 0}, 5);", + "Java": "World.find_in_radius(new double[]{0, 0, 0}, 5);", + "PHP": "World::find_in_radius([0, 0, 0], 5);", + "HTML / inline JS": "World.find_in_radius([0, 0, 0], 5);" + } + } ] } diff --git a/lib/api-examples.ts b/lib/api-examples.ts index 8ec81fe..73bf264 100644 --- a/lib/api-examples.ts +++ b/lib/api-examples.ts @@ -7,6 +7,8 @@ const examples: Record = exampleData; const exampleSetup = `Choose one snippet at a time and place it inside your language's Start/on_start callback. These are independent examples; each creates its own handles or builders. Follow [Gameplay language bindings](/docs/guides/gameplay-actions/language-bindings) for imports and callback registration. Python must import the modules used in its snippet from rustic; C# uses using static Rustic; Java examples belong inside a Rustic subclass; PHP callbacks declare global $rustic. HTML snippets go inside an inline script in a ui/ HTML asset.\n\nUse an anchored Part for transform and effect examples, an unanchored collidable Part for forces, and a perspective camera named Camera for camera examples. Create Target for follow examples. Audio examples require assets/music.ogg in the Play snapshot. Replace scene paths with those listed by Game.scene.List(). Animation playback and sequence-animation examples require the Reveal fixture on the [Animation page](/docs/api/animation); marker callbacks also require its Halfway marker. Run clock controls separately; a paused clock cannot advance a timer.\n\nC callback examples use these file-scope functions before on_start:\n\n\`\`\`c\nstatic void on_done(void) { rustic.log("info", "done"); }\nstatic void on_sample(RusticValue value) { (void)value; rustic.log("info", "sample received"); }\nstatic void on_event(const RusticValue *args, size_t count) {\n (void)args; (void)count; rustic.log("info", "event received");\n}\n\`\`\``; +const entitySetup = "Place one snippet inside a Start/on_start callback attached to a live scene object. Python imports the named service from rustic; C includes rustic.h; C++ includes rustic.hpp; C# uses using static Rustic; Java uses a Rustic subclass; PHP requires rustic.php and global $rustic. See [Entity foundations](/docs/guides/entity-foundations) for full setup, errors and lifetime. For PHP and HTML UI scripts, replace the UI owner with a live scene ID. Hierarchy keep_world defaults to true except C, which requires it. These snippets do not require any assets or a browser DOM."; + function languageBlocks(entry: Example, heading: string): string { return exampleLanguages.map((language, index) => { const code = entry.examples[language]; @@ -20,13 +22,13 @@ export function buildCallExamples(topic: string, name: string): string { if (!entries) return ""; const entry = entries.find((entry) => entry.title.split(" / ").includes(name)); if (!entry) throw new Error(`Missing examples for ${topic}/${name}`); - return `${exampleSetup}\n\n${entry.note ? entry.note + "\n\n" : ""}${languageBlocks(entry, "### ")}`; + return `${topic === "entity-foundations" ? entitySetup : exampleSetup}\n\n${entry.note ? entry.note + "\n\n" : ""}${languageBlocks(entry, "### ")}`; } export function buildFunctionExamples(topic: string): string { const entries = examples[topic]; if (!entries) return ""; - return `\n\n## Examples for every function\n\n${exampleSetup}\n\n${entries.map((entry) => { + return `\n\n## Examples for every function\n\n${topic === "entity-foundations" ? entitySetup : exampleSetup}\n\n${entries.map((entry) => { const blocks = languageBlocks(entry, `#### ${entry.title} — `); return `### ${entry.title}\n\n${entry.note ? entry.note + "\n\n" : ""}${blocks}`; }).join("\n\n")}`; diff --git a/lib/api-reference-markdown.ts b/lib/api-reference-markdown.ts index c06c9a6..67d1f05 100644 --- a/lib/api-reference-markdown.ts +++ b/lib/api-reference-markdown.ts @@ -124,6 +124,20 @@ function coreCallExamples(shared: string, name: string): string { } export function buildApiCallMarkdown(category: ApiCategory, call: ApiCall, title: string, shared: string) { + if (category.slug === "api/entity-foundations") { + const name = call.signature.split("(")[0]; + return [ + `# ${call.title}`, + `[${title}](/docs/${category.slug})`, + `## Signature\n\n\`${call.signature}\``, + `## Purpose and example\n\n${section(shared, name)}`, + `## Language examples\n\n${buildCallExamples("entity-foundations", name)}`, + `## Setup and language support\n\n${section(shared, "Setup and language support")}`, + `## Parameters\n\n${section(shared, "Parameters")}`, + `## Return and timing\n\n${section(shared, "Return and timing")}`, + `## Behavior and limitations\n\n${section(shared, "Behavior and limitations")}`, + ].join("\n\n"); + } const method = call.slug.split("/").pop()!; const audio = category.slug === "api/audio"; const capture = audio ? audioCaptureReference[method] : undefined; diff --git a/lib/api-reference.ts b/lib/api-reference.ts index c5baf13..3900b93 100644 --- a/lib/api-reference.ts +++ b/lib/api-reference.ts @@ -25,6 +25,25 @@ function category(slug: string, source: string, signatures: string[]): ApiCatego } export const apiCategories: ApiCategory[] = [ + category("entity-foundations", "docs/Scripting/API/entity-foundations.md", [ + "Tags.add(entity, name)", + "Tags.remove(entity, name)", + "Tags.find_all(name)", + "Groups.create(name)", + "Groups.add(name, entity)", + "Groups.get_members(name)", + "Hierarchy.set_parent(entity, parent, keep_world = true)", + "Hierarchy.get_children(entity)", + "Hierarchy.detach(entity, keep_world = true)", + "Entity.destroy(entity)", + "Entity.destroy_after(entity, delay)", + "Entity.is_valid(entity)", + "Components.has(entity, name)", + "Components.get_all(entity)", + "Components.find_in_children(entity, name)", + "World.find_by_tag(name)", + "World.find_in_radius(center, radius)" +]), category("animation", "docs/Scripting/API/animation.md", [ "Animation.clips(entity)", "Animation.load(entity, source)", diff --git a/lib/docs-catalog.ts b/lib/docs-catalog.ts index 4ab3fdf..3a63dab 100644 --- a/lib/docs-catalog.ts +++ b/lib/docs-catalog.ts @@ -50,6 +50,9 @@ const categoryGroups: DocGroup[] = [ { slug: "guides/gameplay-actions/validation", title: "Validate gameplay services", description: "Validate gameplay services for Use gameplay actions.", source: "docs/Scripting/gameplayActions/validation.md", parent: "guides/gameplay-actions" }, { slug: "guides/gameplay-actions/extension", title: "Extend gameplay services", description: "Extend gameplay services for Use gameplay actions.", source: "docs/Scripting/gameplayActions/extension.md", parent: "guides/gameplay-actions" }, { slug: "guides/gameplay-architecture", title: "Gameplay API architecture", description: "Core services, property adapters, clip handles, timing and lifetime boundaries.", source: "docs/GAMEPLAY_API_ARCHITECTURE.md" }, + { slug: "api/entity-foundations", title: "Entity foundations", description: "Tags, groups, safe hierarchy edits, destruction and live component/world queries.", source: "docs/Scripting/API/entity-foundations.md" }, + { slug: "guides/entity-foundations", title: "Use entity foundations", description: "Attach scripts that organize and destroy scene objects through live shared services.", source: "docs/Scripting/API/entity-foundations.md" }, + { slug: "guides/scripting-expansion-status", title: "Scripting expansion status", description: "Feature-by-feature implementation status, existing systems and blockers.", source: "docs/SCRIPTING_EXPANSION_STATUS.md" }, { slug: "api/tween", title: "Tween", description: "Animate an entity property or script value without writing an Update loop.", source: "docs/Scripting/API/tween.md" }, { slug: "api/ease", title: "Ease", description: "Choose one of 31 shared easing curves for actions and synchronous interpolation.", source: "docs/Scripting/API/ease.md" }, { slug: "api/movement", title: "Movement", description: "Move, turn, follow and orbit objects with finite engine-scheduled actions.", source: "docs/Scripting/API/movement.md" },