8822a03f53
Implements the DSL that docs/architecture/scripting_sandbox.md recommends
as the primary (and, for now, sufficient) approach in place of running
untrusted GDScript in-process: a fixed set of trigger/condition/action
verbs, expressed as plain JSON data, interpreted by trusted GDScript with
zero ability to reference engine singletons, call arbitrary methods, or
execute code.
- ScriptBehavior (scripts/scripting/script_behavior.gd): parses and
validates rules from JSON/Dictionary data. Every trigger, condition
type/op, and verb is checked against an explicit whitelist at parse
time; malformed or unknown entries are dropped and recorded in
rejected_rules, never partially executed. Carries an optional
content_flags passthrough so this data type isn't hostile to future
moderation-queue integration (docs/architecture/content_moderation.md).
- ScriptInterpreterNode (scripts/scripting/script_interpreter.gd): the
only code that executes rules. Triggers: on_interact (proximity +
"interact" input action), on_enter (edge-triggered Area3D overlap),
on_touch (continuous overlap), on_timer (physics-clocked Timer). Verbs:
give_item, play_sound, move, rotate, spawn_from_catalog, set_timer --
each a fixed, hand-written function; verb dispatch is an explicit
match(), never call()-by-name from data. move/rotate animate via Tween
rather than teleporting.
- PlayerInventory (scripts/scripting/player_inventory.gd): a small
per-player item-count stub for give_item to target, since no real
inventory/entity-state system exists yet (M9 Economy). Attached to
scenes/player.tscn as "Inventory".
- SpawnCatalog (scripts/scripting/spawn_catalog.gd): trusted catalog_id ->
asset_id lookup for spawn_from_catalog, mirroring the AvatarCatalog
data-driven-JSON convention. Rule data only ever supplies a catalog_id
key, never a resource path.
- Wired a real demo ("ScriptedChest") into scenes/worlds/test_level.tscn
with an on_interact rule (give_item + play_sound + rotate), plus an
"interact" input action (project.godot).
- scripts/smoke_test_script_behavior.gd: exercises every trigger type,
every verb, and every condition type against the real interpreter and
the wired demo scene, and proves unknown/malformed/unsafe rules
(unknown verb, unknown trigger, non-scalar smuggled arguments, etc.)
are rejected at parse time and never run -- 50/50 checks pass.
33 lines
1.5 KiB
GDScript
33 lines
1.5 KiB
GDScript
class_name ScriptRule
|
|
extends RefCounted
|
|
## One parsed, validated rule: WHEN [trigger] IF [conditions] THEN [actions].
|
|
##
|
|
## Rules are parsed once, by [ScriptBehavior], from a plain [Dictionary] that
|
|
## ultimately comes from untrusted UGC data (a JSON file on disk, or a dict
|
|
## embedded in a [WorldObject]'s `properties`). Every field on this class is
|
|
## a plain scalar (String/int/float/bool) or a small [Dictionary]/[Array] of
|
|
## scalars -- never a [Callable], a [Script], a [Node] reference, or anything
|
|
## else the data could use to reach into the engine. [ScriptInterpreterNode]
|
|
## is the only code that reads these fields, and it does so through an
|
|
## explicit `match` over a fixed whitelist -- see script_interpreter.gd.
|
|
##
|
|
## See docs/architecture/scripting_sandbox.md for why the data format has to
|
|
## look like this (no eval, no Expression, no call-by-name).
|
|
|
|
## One of "on_interact", "on_enter", "on_touch", "on_timer".
|
|
var trigger_type: String = ""
|
|
|
|
## Only meaningful when trigger_type == "on_timer": seconds between firings.
|
|
var trigger_interval: float = 0.0
|
|
|
|
## Normalized condition dicts. Each has a "type" (one of
|
|
## ScriptBehavior.VALID_CONDITION_TYPES), an "op" (one of
|
|
## ScriptBehavior.VALID_OPS) and scalar operands -- see script_behavior.gd
|
|
## for the exact per-type shape.
|
|
var conditions: Array[Dictionary] = []
|
|
|
|
## Normalized action dicts. Each has a "verb" (one of
|
|
## ScriptBehavior.VALID_VERBS) and scalar arguments -- see
|
|
## script_behavior.gd for the exact per-verb shape.
|
|
var actions: Array[Dictionary] = []
|