Exposure and permissions
Declare in Rust what scripts may touch, narrow it per script and per role in neo.toml, and control who may load live scripts.
On this page
A script can only reach what your game exposes. Exposure has two layers, and a script gets the overlap of both:
- Rust declares the most that can be exposed, with
#[derive(ScriptExpose)]on a component and#[script_methods]on its methods. neo.tomlgrants part of that, separately for the game's own scripts and for each player role.
A grant can narrow what Rust declares but never widen it. A grant that names something Rust doesn't declare is an error, so a typo can't silently expose nothing or the wrong thing.
Declaring a kind in Rust#
use neo_script::{ScriptExpose, script_methods};
use neo_script::expose::ScriptCtx;
/// A moving platform. Scripts move it and may boost it.
#[derive(Component, ScriptExpose)]
#[script(kind = "Platform", transform(get, set), methods)]
pub struct Platform {
/// Laps of its route per second.
#[script(get, set, range = 0.0..=1.0)]
pub speed: f32,
/// A label for UI.
#[script(get)]
pub label: String,
/// Only admins may rename the platform.
#[script(get, set, roles = ["admin"])]
pub display_name: String,
/// Not marked: invisible to scripts.
pub boosts: u32,
}
#[script_methods]
impl Platform {
/// Speeds the platform up; players need the `builder` role.
#[script(call, rate = 2)]
fn boost(&mut self, by: f32, ctx: &ScriptCtx) -> Result<f32, String> {
if !ctx.is_creator_or("builder") {
return Err(format!("{} can't boost platforms", ctx.player().unwrap_or("?")));
}
self.speed += by.clamp(0.0, 0.2);
Ok(self.speed)
}
}Then register the kind with the app, plus a spawner for any kind scripts may create:
app.add_plugins(NeoScriptPlugin { neo_toml, scripts, seed, modules })
.expose_script_kind::<Platform>()
.expose_script_kind::<Cube>()
.script_spawner("Cube", spawn_cube);The /// doc comments become the descriptions in the generated neo.d.ts and neo.pyi files and in script.api.
Options on the type#
| Option | Meaning |
|---|---|
kind = "Name" | The kind name scripts use (query("Platform"), tag: "Platform"). Defaults to the type's name. |
transform(get), transform(get, set) | Exposes the entity's position (from its Transform) as { x, y, z }. A field can't also be called position. |
methods | Says the type has a #[script_methods] impl block. |
Options on a field#
Only fields marked with #[script(...)] exist for scripts.
| Option | Meaning |
|---|---|
get (or read) | Scripts may read it. |
set (or write) | Scripts may write it. |
range = a..=b | Refuses numeric writes outside the range (inclusive). |
roles = ["..."] | Only players holding one of these roles may use it. The game's own scripts aren't affected. |
rename = "name" | The name scripts see, instead of the Rust field name. |
Supported field types include integers, floats, bool, String, Option<T>, Vec<T> and Vec3.
Options on a method#
Mark a method in a #[script_methods] block with #[script(call, ...)]. It takes &self or &mut self.
| Option | Meaning |
|---|---|
call | Scripts may call it. |
rate = n | At most n calls per simulation second, per script. Further calls throw. |
roles = ["..."] | Only players holding one of these roles may call it. |
A method that returns Result<T, E> turns Err into the calling script's fault, with the error's text. In a running game, the method runs right after the calling script's turn, so the script sees the call return null.
ScriptCtx: who is calling#
A method can take ctx: &ScriptCtx in any position after self. Scripts don't pass it and can't fake it: an extra argument a script passes is ignored.
| Member | Meaning |
|---|---|
ctx.principal | Who the script runs as: the game's own scripts (Principal::Creator), or a player with their roles (Principal::Player { id, roles }). |
ctx.player() | The calling player's id, or None for the game's own scripts. |
ctx.is_creator_or("role") | true for the game's own scripts, or for a player holding role. |
ctx.script | The calling script's name. |
ctx.tick | The simulation tick of the call. |
Use it for rules that depend on the caller, such as ownership checks.
Granting in neo.toml#
The game's own scripts get the grants under [script.expose.<Kind>]:
[script.expose.Platform]
fields = { position = "get set", speed = "get", label = "get" }
methods = ["boost"]
[script.expose.Cube]
fields = { position = "get set", size = "get set" }
spawn = truefieldsmaps each field to"get","set"or"get set".methodslists the methods scripts may call.spawn = truelets scripts create the kind withthis.spawn.- A kind with no section doesn't exist for the game's scripts, and a field you leave out isn't visible.
Here speed is declared get, set in Rust but granted only get, so the game's scripts can read the speed but not change it.
Player roles#
Players can load their own live scripts only if the game allows it (next section). A player's scripts start with nothing, then get:
- the grants of the
playerrole, which every player holds; - plus the grants of each other role the player holds.
[script.roles.player]
grant = ["Platform.position:get", "Player.position:get"]
[script.roles.builder]
grant = ["Cube:spawn", "Cube.position:set", "Cube.size:set", "Platform.boost:call"]
[script.roles.admin]
grant = ["Platform.display_name:get set"]A grant is one string:
| Grant | Allows |
|---|---|
Kind.field:get | Reading the field. |
Kind.field:set | Writing the field. |
Kind.field:get set | Both. |
Kind.method:call | Calling the method. |
Kind:spawn | Spawning the kind. |
Fields and methods marked with roles = [...] in Rust are removed for players who don't hold one of those roles, even if a grant names them.
Who may load live scripts#
[script.players] allowed decides which players may load live scripts into a running session:
| Value | Who |
|---|---|
"nobody" (the default) | No one. |
"owner" | The game's owner. |
"friends" | The owner and the owner's friends. |
"role:<name>" | The owner and players holding that role. |
"everyone" | Every player. |
Player scripts also have their own budgets (fuel_per_tick, calls_per_tick, memory in the same section), the same for every player and device. A player script must also pass the game's moderation check before it runs; by default that refuses sources over 64 KB. Refusals are written to the script log. See The [script] manifest for every key.
What denied access looks like#
| Situation | JavaScript | Python |
|---|---|---|
| Querying a kind you can't see | empty list | empty list |
| Reading a field you can't read | TypeError | PermissionError |
| Writing a field you can't write | TypeError | PermissionError |
| Calling a method you can't call | TypeError | PermissionError |
| Spawning a kind you can't spawn, or setting a field you can't write at spawn | TypeError | PermissionError |
Probing names such as then or toJSON on an entity | undefined (no error) | n/a |
Reaching interpreter internals (__class__, __globals__, constructor) | n/a | compile error, or AttributeError at run time |
Each error can be caught. Left uncaught, it ends the handler with a fault. If you change grants while the game runs, the change applies at the script's next access.
Checking your setup#
The Scripts panel's check validates [script] against the kinds your game exposes and writes neo.d.ts and neo.pyi next to your scripts, so your editor can autocomplete exactly what your scripts may use. script.types and script.api produce the same declarations as commands. See The Scripts panel and Script commands.