Add ScriptBehavior DSL: data-driven, non-Turing-complete UGC scripting
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.
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
class_name PlayerInventory
|
||||
extends Node
|
||||
## Minimal per-player item-count store.
|
||||
##
|
||||
## docs/architecture/dynamic_object_node.gd's header notes that a dynamic
|
||||
## object's runtime state ("position, health, inventory") belongs to "the
|
||||
## entity system" -- but no such system exists yet (M9 Economy hasn't been
|
||||
## built). This is a deliberately small, local stub scoped to make
|
||||
## ScriptBehavior's `give_item` verb demonstrable now, not an attempt to
|
||||
## build the real economy/inventory system. Replace it wholesale when M9
|
||||
## lands; keep the give_item/get_count surface so existing ScriptBehavior
|
||||
## data keeps working against the real thing.
|
||||
##
|
||||
## Attach one of these as a child node named "Inventory" under a player (see
|
||||
## scenes/player.tscn) -- [ScriptInterpreterNode] looks it up by that name
|
||||
## when resolving `target: "player"` for the `give_item` verb.
|
||||
|
||||
## Emitted whenever an item's count changes, with the item's new total.
|
||||
signal item_changed(item_id: String, count: int)
|
||||
|
||||
var _items: Dictionary = {} # item_id (String) -> count (int)
|
||||
|
||||
|
||||
## Add (or, with a negative count, remove) units of an item. Counts never go
|
||||
## negative. `count == 0` is a no-op.
|
||||
func give_item(item_id: String, count: int) -> void:
|
||||
if item_id.is_empty() or count == 0:
|
||||
return
|
||||
var new_count: int = maxi(0, int(_items.get(item_id, 0)) + count)
|
||||
_items[item_id] = new_count
|
||||
item_changed.emit(item_id, new_count)
|
||||
|
||||
|
||||
## Current count for an item (0 if never granted).
|
||||
func get_count(item_id: String) -> int:
|
||||
return int(_items.get(item_id, 0))
|
||||
|
||||
|
||||
## A snapshot of the whole inventory (for UI/debugging/tests).
|
||||
func get_all() -> Dictionary:
|
||||
return _items.duplicate()
|
||||
Reference in New Issue
Block a user