Use get_var! and set_var! when a generic tool knows a target node and member
name but does not know the target's Rust state type.
tool data(member name) -> adapter.target NodeID
-> get_var(member) -> transform Variant -> set_var(member)
The target script owns its values. The adapter owns generic transformation and knows neither the target state type nor the member until runtime. The scene injects the fixed target, while tool data supplies the dynamic name.
#[State]
struct RuntimeInspectorState {
// pub because the scene injects it via script_vars.
#[expose]
#[node_ref(Node2D, Node3D, UiNode)]
pub target: Option<NodeID>,
}
lifecycle!({});
methods!({
fn add_i32(
&self,
ctx: &mut ScriptContext<'_, API>,
member: String,
amount: i32,
) {
let Some(target) = with_state!(
ctx.run,
RuntimeInspectorState,
ctx.id,
|state| state.target
).unwrap_or_default() else {
return;
};
let old = get_var!(ctx.run, target, member.as_str())
.as_i32()
.unwrap_or(0);
set_var!(
ctx.run,
target,
member.as_str(),
variant!(old + amount)
);
}
});For a known State, prefer with_state_mut!. Dynamic vars fit generic cross script calls.
The target only exposes fields declared pub in its #[State] struct:
non-pub members read as Variant::Null and ignore writes β see
state visibility.
set_var! performs strict runtime decode. A string such as
"res://textures/icon.png" does not load into TextureID; asset path coercion
only applies while scene vars load.
Missing target returns without work. Missing/wrong-type members use a deliberate
fallback here; production tools should surface that mismatch to their caller.
For known State, typed mutation is clearer, faster, and compiler-checked.
Use a targeted method instead when the change must preserve target invariants.
Extend the adapter with an allow-list, expected schema, or undo record. Runnable source: adapter.rs.