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.
142 lines
4.4 KiB
Plaintext
142 lines
4.4 KiB
Plaintext
[gd_scene load_steps=2 format=3 uid="uid://c49a36fc7db_test_level"]
|
|
|
|
[ext_resource type="Script" path="res://scripts/scripting/script_interpreter.gd" id="1_scripted_chest"]
|
|
|
|
[sub_resource type="BoxShape3D" id="BoxShape3D_ground"]
|
|
size = Vector3(40, 1, 40)
|
|
|
|
[sub_resource type="BoxMesh" id="BoxMesh_ground"]
|
|
size = Vector3(40, 1, 40)
|
|
|
|
[sub_resource type="BoxShape3D" id="BoxShape3D_prop1"]
|
|
size = Vector3(2, 3, 2)
|
|
|
|
[sub_resource type="BoxMesh" id="BoxMesh_prop1"]
|
|
size = Vector3(2, 3, 2)
|
|
|
|
[sub_resource type="BoxShape3D" id="BoxShape3D_prop2"]
|
|
size = Vector3(3, 1, 3)
|
|
|
|
[sub_resource type="BoxMesh" id="BoxMesh_prop2"]
|
|
size = Vector3(3, 1, 3)
|
|
|
|
[sub_resource type="CylinderShape3D" id="CylinderShape3D_prop3"]
|
|
top_radius = 1.0
|
|
height = 4.0
|
|
|
|
[sub_resource type="CylinderMesh" id="CylinderMesh_prop3"]
|
|
top_radius = 1.0
|
|
bottom_radius = 1.0
|
|
height = 4.0
|
|
subdivisions_top_radius = 8
|
|
|
|
[sub_resource type="SphereShape3D" id="SphereShape3D_prop4"]
|
|
radius = 1.0
|
|
|
|
[sub_resource type="SphereMesh" id="SphereMesh_prop4"]
|
|
radius = 1.0
|
|
subdivisions_ring = 12
|
|
subdivisions_height = 12
|
|
|
|
[sub_resource type="SphereShape3D" id="SphereShape3D_prop5"]
|
|
radius = 0.5
|
|
|
|
[sub_resource type="SphereMesh" id="SphereMesh_prop5"]
|
|
radius = 0.5
|
|
subdivisions_ring = 8
|
|
subdivisions_height = 8
|
|
|
|
[sub_resource type="BoxShape3D" id="BoxShape3D_chest"]
|
|
size = Vector3(1, 0.8, 0.6)
|
|
|
|
[sub_resource type="BoxMesh" id="BoxMesh_chest"]
|
|
size = Vector3(1, 0.8, 0.6)
|
|
|
|
[sub_resource type="Environment" id="WorldEnvironment_env"]
|
|
background_mode = 1
|
|
background_color = Color(0.45, 0.62, 0.82, 1)
|
|
ambient_light_source = 2
|
|
ambient_light_color = Color(0.7, 0.75, 0.82, 1)
|
|
ambient_light_energy = 0.8
|
|
|
|
[node name="TestLevel" type="Node3D"]
|
|
|
|
[node name="Ground" type="StaticBody3D" parent="."]
|
|
position = Vector3(0, -0.5, 0)
|
|
|
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="Ground"]
|
|
shape = SubResource("BoxShape3D_ground")
|
|
|
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="Ground"]
|
|
mesh = SubResource("BoxMesh_ground")
|
|
|
|
[node name="Props" type="Node3D" parent="."]
|
|
|
|
[node name="Prop1_Box" type="StaticBody3D" parent="Props"]
|
|
position = Vector3(8, 1.5, 8)
|
|
|
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="Props/Prop1_Box"]
|
|
shape = SubResource("BoxShape3D_prop1")
|
|
|
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="Props/Prop1_Box"]
|
|
mesh = SubResource("BoxMesh_prop1")
|
|
|
|
[node name="Prop2_Pedestal" type="StaticBody3D" parent="Props"]
|
|
position = Vector3(-8, 0.5, 6)
|
|
|
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="Props/Prop2_Pedestal"]
|
|
shape = SubResource("BoxShape3D_prop2")
|
|
|
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="Props/Prop2_Pedestal"]
|
|
mesh = SubResource("BoxMesh_prop2")
|
|
|
|
[node name="Prop3_Column" type="StaticBody3D" parent="Props"]
|
|
position = Vector3(6, 2.0, -8)
|
|
|
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="Props/Prop3_Column"]
|
|
shape = SubResource("CylinderShape3D_prop3")
|
|
|
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="Props/Prop3_Column"]
|
|
mesh = SubResource("CylinderMesh_prop3")
|
|
|
|
[node name="Prop4_Sphere" type="StaticBody3D" parent="Props"]
|
|
position = Vector3(-6, 1.0, -6)
|
|
|
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="Props/Prop4_Sphere"]
|
|
shape = SubResource("SphereShape3D_prop4")
|
|
|
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="Props/Prop4_Sphere"]
|
|
mesh = SubResource("SphereMesh_prop4")
|
|
|
|
[node name="Prop5_SmallSphere" type="StaticBody3D" parent="Props"]
|
|
position = Vector3(0, 0.5, 12)
|
|
|
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="Props/Prop5_SmallSphere"]
|
|
shape = SubResource("SphereShape3D_prop5")
|
|
|
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="Props/Prop5_SmallSphere"]
|
|
mesh = SubResource("SphereMesh_prop5")
|
|
|
|
[node name="ScriptedChest" type="StaticBody3D" parent="."]
|
|
position = Vector3(0, 1.0, 1.5)
|
|
script = ExtResource("1_scripted_chest")
|
|
behavior_path = "res://assets/scripting/behaviors/demo_chest.json"
|
|
interact_radius = 2.5
|
|
|
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="ScriptedChest"]
|
|
shape = SubResource("BoxShape3D_chest")
|
|
|
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="ScriptedChest"]
|
|
mesh = SubResource("BoxMesh_chest")
|
|
|
|
[node name="Sun" type="DirectionalLight3D" parent="."]
|
|
rotation = Vector3(-0.95, -0.5, 0)
|
|
light_energy = 1.3
|
|
shadow_enabled = true
|
|
|
|
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
|
|
environment = SubResource("WorldEnvironment_env")
|
|
|
|
[node name="PlayerSpawn" type="Marker3D" parent="."]
|
|
position = Vector3(0, 1.5, 0)
|