Compare commits
7 Commits
265dd12b37
..
multy
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c2227432c | |||
| 84433160fe | |||
| 588d11a9ff | |||
| 3df2a0af58 | |||
| 832e539a96 | |||
| 4e6c645938 | |||
| 1869dbf054 |
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"godotTools.editorPath.godot4": "c:\\Program Files (x86)\\Godot\\Godot_v4.7-stable_win64.exe"
|
||||
}
|
||||
+6
-21
@@ -1,7 +1,5 @@
|
||||
class_name ItemContainer
|
||||
extends Node3D
|
||||
|
||||
@export var enabled: bool = true
|
||||
@export var target_group : String # Items with this tag can be added to the container
|
||||
@export var meal_positions: Array[Node3D] = []
|
||||
@export var side_positions: Array[Node3D] = []
|
||||
@@ -25,13 +23,14 @@ func _ready() -> void:
|
||||
|
||||
|
||||
func _on_body_entered(body: Node3D) -> void:
|
||||
print("Container enabled: ", enabled)
|
||||
if not enabled:
|
||||
print("Container disabled in _on_body_entered body")
|
||||
# Container fill mutates world state (reparent/freeze); run only on the
|
||||
# world owner. Item transforms replicate to clients via each item's
|
||||
# MultiplayerSynchronizer (global_transform), so contents follow the plate.
|
||||
if not NetworkManager.owns_world():
|
||||
return
|
||||
if not body.is_in_group(target_group):
|
||||
return
|
||||
print("Container _on_body_entered body: ", body)
|
||||
|
||||
var picked_by = xr_pickable.get_picked_up_by()
|
||||
if picked_by and picked_by.is_in_group("station"):
|
||||
|
||||
@@ -78,23 +77,9 @@ func _add_item(item: Node3D, positions: Array[Node3D], container_root: Node3D) -
|
||||
if food_node:
|
||||
contained_items.append(food_node as FoodItem)
|
||||
|
||||
|
||||
func erase_item(item: FoodItem) -> void:
|
||||
for container_root in [_meal_container, _side_container]:
|
||||
for child in container_root.get_children():
|
||||
var food_item = child.get_node_or_null("FoodItem") as FoodItem
|
||||
if food_item.id == item.id:
|
||||
contained_items.erase(item)
|
||||
child.queue_free()
|
||||
|
||||
|
||||
func clear() -> void:
|
||||
# delete all nodes in containers and contained_items
|
||||
for child in _meal_container.get_children():
|
||||
child.queue_free()
|
||||
for child in _side_container.get_children():
|
||||
child.queue_free()
|
||||
contained_items.clear()
|
||||
pass
|
||||
|
||||
|
||||
#plate (Pickalbe)
|
||||
|
||||
+12
-51
@@ -2,12 +2,17 @@
|
||||
|
||||
[ext_resource type="Script" uid="uid://bfrlpbsqg5lqr" path="res://addons/godot-xr-tools/objects/pickable.gd" id="1_nq5sa"]
|
||||
[ext_resource type="PackedScene" uid="uid://du31pqeytu8as" path="res://Containers/container.tscn" id="1_s5yhh"]
|
||||
[ext_resource type="Script" uid="uid://iqjqsk4v6qfh" path="res://Containers/plate_controller.gd" id="1_vjsmi"]
|
||||
[ext_resource type="PackedScene" uid="uid://c25yxb0vt53vc" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_left.tscn" id="2_3bkoa"]
|
||||
[ext_resource type="Animation" uid="uid://db62hs5s4n2b3" path="res://addons/godot-xr-tools/hands/animations/left/Grip 4.res" id="3_6v47p"]
|
||||
[ext_resource type="Script" uid="uid://dvobm6vcfnqe8" path="res://addons/godot-xr-tools/hands/poses/hand_pose_settings.gd" id="4_dhtvl"]
|
||||
[ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="5_vlcpl"]
|
||||
[ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="6_el51w"]
|
||||
[ext_resource type="Script" path="res://Prefabs/net_pickable.gd" id="7_net"]
|
||||
|
||||
[sub_resource type="SceneReplicationConfig" id="Repl_item"]
|
||||
properties/0/path = NodePath(".:global_transform")
|
||||
properties/0/spawn = true
|
||||
properties/0/replication_mode = 1
|
||||
|
||||
[sub_resource type="CylinderShape3D" id="CylinderShape3D_kek77"]
|
||||
height = 0.0635376
|
||||
@@ -23,18 +28,12 @@ script = ExtResource("4_dhtvl")
|
||||
closed_pose = ExtResource("6_el51w")
|
||||
metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_vlqg6"]
|
||||
albedo_color = Color(0.36656043, 0.16690676, 0.12531222, 1)
|
||||
|
||||
[node name="Plate" type="RigidBody3D" unique_id=190487773]
|
||||
collision_layer = 4
|
||||
collision_mask = 196615
|
||||
freeze_mode = 1
|
||||
script = ExtResource("1_nq5sa")
|
||||
|
||||
[node name="PlateController" type="Node" parent="." unique_id=1503264193]
|
||||
script = ExtResource("1_vjsmi")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1365637864]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.007171631, 0)
|
||||
shape = SubResource("CylinderShape3D_kek77")
|
||||
@@ -68,6 +67,12 @@ hand_pose = SubResource("Resource_2hcq0")
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.16517048, 0.042291984, -0.08604182)
|
||||
hand_pose = SubResource("Resource_dlngj")
|
||||
|
||||
[node name="NetPickable" type="Node" parent="."]
|
||||
script = ExtResource("7_net")
|
||||
|
||||
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
|
||||
replication_config = SubResource("Repl_item")
|
||||
|
||||
[node name="Container" parent="." unique_id=377793422 node_paths=PackedStringArray("meal_positions", "side_positions") instance=ExtResource("1_s5yhh")]
|
||||
target_group = "platalbe_item"
|
||||
meal_positions = [NodePath("MealPositions/Slot1")]
|
||||
@@ -90,47 +95,3 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.08457637, 0, 0.08950478)
|
||||
|
||||
[node name="Slot4" type="Node3D" parent="Container/SidePositions" unique_id=379534402]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.06938871, 0, 0.072598234)
|
||||
|
||||
[node name="Dirty" type="Node3D" parent="." unique_id=319270152]
|
||||
visible = false
|
||||
|
||||
[node name="CSGCylinder3D" type="CSGCylinder3D" parent="Dirty" unique_id=1075901752]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.074060306, -1.4901161e-08, -0.021821946)
|
||||
radius = 0.02
|
||||
height = 0.005
|
||||
sides = 16
|
||||
material = SubResource("StandardMaterial3D_vlqg6")
|
||||
|
||||
[node name="CSGCylinder3D2" type="CSGCylinder3D" parent="Dirty" unique_id=1566886557]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.035480514, 0, 0.09590546)
|
||||
radius = 0.04
|
||||
height = 0.005
|
||||
sides = 16
|
||||
material = SubResource("StandardMaterial3D_vlqg6")
|
||||
|
||||
[node name="CSGCylinder3D3" type="CSGCylinder3D" parent="Dirty" unique_id=1761966340]
|
||||
transform = Transform3D(0.9644101, 0.26441094, 0, -0.26441094, 0.9644101, 0, 0, 0, 1, -0.11377221, 0.005669184, 0.010442272)
|
||||
radius = 0.05
|
||||
height = 0.005
|
||||
sides = 16
|
||||
material = SubResource("StandardMaterial3D_vlqg6")
|
||||
|
||||
[node name="CSGCylinder3D4" type="CSGCylinder3D" parent="Dirty" unique_id=1869071428]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.023516195, 0, -0.088724144)
|
||||
radius = 0.024
|
||||
height = 0.005
|
||||
sides = 16
|
||||
material = SubResource("StandardMaterial3D_vlqg6")
|
||||
|
||||
[node name="CSGTorus3D" type="CSGTorus3D" parent="Dirty" unique_id=753275027]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.0009794924, -0.005010009, -0.0004204195)
|
||||
inner_radius = 0.10664626
|
||||
outer_radius = 0.14543402
|
||||
sides = 16
|
||||
material = SubResource("StandardMaterial3D_vlqg6")
|
||||
|
||||
[node name="CSGPolygon3D" type="CSGPolygon3D" parent="Dirty" unique_id=280929999]
|
||||
transform = Transform3D(0.21189868, 0, 0, 0, -9.2623855e-09, 0.21189868, 0, -0.21189868, -9.2623855e-09, -0.04347625, 0, 0.017095774)
|
||||
polygon = PackedVector2Array(-0.053057775, 0.2586278, 0.09469998, 0.40791017, 0.31746316, 0.26044407, 0.4514293, -0.05809097, 0.105977096, -0.11683442, -4.3913722e-05, 0.02302903)
|
||||
depth = 0.02
|
||||
material = SubResource("StandardMaterial3D_vlqg6")
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
class_name PlateController
|
||||
extends Node
|
||||
|
||||
@onready var dirty_node: Node3D = $"../Dirty"
|
||||
@onready var container: ItemContainer = $"../Container"
|
||||
|
||||
@export var is_dirty: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if not dirty_node:
|
||||
push_error("Plate is missing its dirty_node")
|
||||
dirty_node.visible = is_dirty
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if is_dirty:
|
||||
container.enabled = false
|
||||
dirty_node.visible = true
|
||||
else:
|
||||
container.enabled = true
|
||||
dirty_node.visible = false
|
||||
@@ -1 +0,0 @@
|
||||
uid://iqjqsk4v6qfh
|
||||
@@ -1,24 +0,0 @@
|
||||
class_name GameManager
|
||||
extends Node
|
||||
|
||||
static var money: int
|
||||
static var meals_in_play: Array[String]
|
||||
static var sides_in_play: Array[String]
|
||||
|
||||
|
||||
static func get_random_meal() -> String:
|
||||
if meals_in_play.size() == 0:
|
||||
push_error("GameManager: get_random_meal() called but meals_in_play is empty")
|
||||
return ""
|
||||
var rand_index = randi() % meals_in_play.size()
|
||||
print("GameManager: get_random_meal() returning ", meals_in_play[rand_index])
|
||||
return meals_in_play[rand_index]
|
||||
|
||||
|
||||
static func get_random_side() -> String:
|
||||
if sides_in_play.size() == 0:
|
||||
push_error("GameManager: get_random_side() called but sides_in_play is empty")
|
||||
return ""
|
||||
var rand_index = randi() % sides_in_play.size()
|
||||
print("GameManager: get_random_side() returning ", sides_in_play[rand_index])
|
||||
return sides_in_play[rand_index]
|
||||
@@ -1 +0,0 @@
|
||||
uid://y4sol8l8vih1
|
||||
+11
-6
@@ -7,7 +7,8 @@
|
||||
[ext_resource type="PackedScene" uid="uid://c25yxb0vt53vc" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_left.tscn" id="5_wqyr2"]
|
||||
[ext_resource type="Animation" uid="uid://bediglpx0rj7i" path="res://addons/godot-xr-tools/hands/animations/left/Grip 5.res" id="6_ftab3"]
|
||||
[ext_resource type="PackedScene" uid="uid://3lr2dhy62rhk" path="res://Prefabs/combinable_item.tscn" id="7_wb51u"]
|
||||
[ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="8_t9y3x"]
|
||||
[ext_resource type="Script" uid="uid://coidnv8b2yvxr" path="res://Prefabs/combine_recipe.gd" id="8_wqyr2"]
|
||||
[ext_resource type="PackedScene" uid="uid://b3m2ag8g5rj4r" path="res://Items/hamburger.tscn" id="9_wb51u"]
|
||||
|
||||
[sub_resource type="CylinderShape3D" id="CylinderShape3D_vlqg6"]
|
||||
height = 0.10708985
|
||||
@@ -26,6 +27,12 @@ metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_hoqox"]
|
||||
albedo_color = Color(0.6784191, 0.54546416, 0.016760282, 1)
|
||||
|
||||
[sub_resource type="Resource" id="Resource_hwyyd"]
|
||||
script = ExtResource("8_wqyr2")
|
||||
ingredient_id = &"cooked_burger"
|
||||
result = ExtResource("9_wb51u")
|
||||
metadata/_custom_type_script = "uid://coidnv8b2yvxr"
|
||||
|
||||
[node name="BurgerBuns" unique_id=1088240294 instance=ExtResource("1_g1t48")]
|
||||
|
||||
[node name="CollisionShape3D" parent="." index="0"]
|
||||
@@ -67,8 +74,6 @@ sides = 16
|
||||
cone = true
|
||||
material = SubResource("StandardMaterial3D_hoqox")
|
||||
|
||||
[node name="FoodItem" parent="." index="4" unique_id=123456789 instance=ExtResource("8_t9y3x")]
|
||||
id = "burger_buns"
|
||||
type = 2
|
||||
|
||||
[node name="CombinableItem" parent="." index="5" unique_id=996936271 instance=ExtResource("7_wb51u")]
|
||||
[node name="CombinableItem" parent="." index="4" unique_id=996936271 instance=ExtResource("7_wb51u")]
|
||||
id = &"burger_buns"
|
||||
recipes = Array[ExtResource("8_wqyr2")]([SubResource("Resource_hwyyd")])
|
||||
|
||||
+10
-6
@@ -6,8 +6,12 @@
|
||||
[ext_resource type="Script" uid="uid://dvobm6vcfnqe8" path="res://addons/godot-xr-tools/hands/poses/hand_pose_settings.gd" id="4_mgacb"]
|
||||
[ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="5_vw7i6"]
|
||||
[ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="6_cp3eg"]
|
||||
[ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="7_mde49"]
|
||||
[ext_resource type="PackedScene" uid="uid://3lr2dhy62rhk" path="res://Prefabs/combinable_item.tscn" id="8_wmrff"]
|
||||
[ext_resource type="Script" path="res://Prefabs/net_pickable.gd" id="7_net"]
|
||||
|
||||
[sub_resource type="SceneReplicationConfig" id="Repl_item"]
|
||||
properties/0/path = NodePath(".:global_transform")
|
||||
properties/0/spawn = true
|
||||
properties/0/replication_mode = 1
|
||||
|
||||
[sub_resource type="CylinderShape3D" id="CylinderShape3D_gkni8"]
|
||||
custom_solver_bias = 0.1
|
||||
@@ -49,8 +53,8 @@ hand_pose = SubResource("Resource_lc22d")
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.06531982, 0.042291984, -0.08604182)
|
||||
hand_pose = SubResource("Resource_qyiot")
|
||||
|
||||
[node name="FoodItem" parent="." index="4" unique_id=345678912 instance=ExtResource("7_mde49")]
|
||||
id = "charcoal"
|
||||
type = 2
|
||||
[node name="NetPickable" type="Node" parent="." index="4"]
|
||||
script = ExtResource("7_net")
|
||||
|
||||
[node name="CombinableItem" parent="." index="5" unique_id=996936271 instance=ExtResource("8_wmrff")]
|
||||
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="." index="5"]
|
||||
replication_config = SubResource("Repl_item")
|
||||
|
||||
+27
-2
@@ -7,7 +7,16 @@
|
||||
[ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="5_a0a5b"]
|
||||
[ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="6_bdp75"]
|
||||
[ext_resource type="PackedScene" uid="uid://3lr2dhy62rhk" path="res://Prefabs/combinable_item.tscn" id="7_bdp75"]
|
||||
[ext_resource type="PackedScene" uid="uid://cucc3gaqu2nab" path="res://Items/Charcoal.tscn" id="8_a0a5b"]
|
||||
[ext_resource type="PackedScene" uid="uid://7earnjgdmwgx" path="res://Prefabs/cookable_item.tscn" id="10_65ccv"]
|
||||
[ext_resource type="Script" uid="uid://coidnv8b2yvxr" path="res://Prefabs/combine_recipe.gd" id="11_recipe"]
|
||||
[ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="11_vjw41"]
|
||||
[ext_resource type="Script" path="res://Prefabs/net_pickable.gd" id="12_net"]
|
||||
|
||||
[sub_resource type="SceneReplicationConfig" id="Repl_item"]
|
||||
properties/0/path = NodePath(".:global_transform")
|
||||
properties/0/spawn = true
|
||||
properties/0/replication_mode = 1
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_m1xbq"]
|
||||
size = Vector3(0.1, 0.1, 0.1)
|
||||
@@ -29,6 +38,12 @@ script = ExtResource("4_hc3f7")
|
||||
closed_pose = ExtResource("6_bdp75")
|
||||
metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
|
||||
|
||||
[sub_resource type="Resource" id="Resource_i8ixk"]
|
||||
script = ExtResource("11_recipe")
|
||||
ingredient_id = &"cube"
|
||||
result = ExtResource("8_a0a5b")
|
||||
metadata/_custom_type_script = "uid://coidnv8b2yvxr"
|
||||
|
||||
[node name="PickableObject" unique_id=1675596942 groups=["platalbe_item"] instance=ExtResource("1_dbtw8")]
|
||||
|
||||
[node name="CollisionShape3D" parent="." index="0"]
|
||||
@@ -45,8 +60,18 @@ hand_pose = SubResource("Resource_lc22d")
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.06531982, 0.042291984, -0.08604182)
|
||||
hand_pose = SubResource("Resource_qyiot")
|
||||
|
||||
[node name="FoodItem" parent="." index="4" unique_id=63948206 instance=ExtResource("11_vjw41")]
|
||||
[node name="CombinableItem" parent="." index="4" unique_id=996936271 instance=ExtResource("7_bdp75")]
|
||||
id = &"cube"
|
||||
recipes = Array[ExtResource("11_recipe")]([SubResource("Resource_i8ixk")])
|
||||
|
||||
[node name="CookableItem" parent="." index="5" unique_id=280820828 instance=ExtResource("10_65ccv")]
|
||||
|
||||
[node name="FoodItem" parent="." index="6" unique_id=63948206 instance=ExtResource("11_vjw41")]
|
||||
id = "cube"
|
||||
type = 1
|
||||
|
||||
[node name="CombinableItem" parent="." index="5" unique_id=996936271 instance=ExtResource("7_bdp75")]
|
||||
[node name="NetPickable" type="Node" parent="." index="7"]
|
||||
script = ExtResource("12_net")
|
||||
|
||||
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="." index="8"]
|
||||
replication_config = SubResource("Repl_item")
|
||||
|
||||
+17
-5
@@ -7,7 +7,14 @@
|
||||
[ext_resource type="Script" uid="uid://dvobm6vcfnqe8" path="res://addons/godot-xr-tools/hands/poses/hand_pose_settings.gd" id="5_w8sii"]
|
||||
[ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="6_mvnl5"]
|
||||
[ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="7_wqxjj"]
|
||||
[ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="10_yxtxs"]
|
||||
[ext_resource type="PackedScene" uid="uid://7earnjgdmwgx" path="res://Prefabs/cookable_item.tscn" id="9_hwyyd"]
|
||||
[ext_resource type="PackedScene" uid="uid://cfc4ho67u4r5e" path="res://Items/cooked_burger.tscn" id="9_r8jvx"]
|
||||
[ext_resource type="Script" path="res://Prefabs/net_pickable.gd" id="12_net"]
|
||||
|
||||
[sub_resource type="SceneReplicationConfig" id="Repl_item"]
|
||||
properties/0/path = NodePath(".:global_transform")
|
||||
properties/0/spawn = true
|
||||
properties/0/replication_mode = 1
|
||||
|
||||
[sub_resource type="CylinderShape3D" id="CylinderShape3D_fco8w"]
|
||||
height = 0.0338974
|
||||
@@ -26,7 +33,7 @@ script = ExtResource("5_w8sii")
|
||||
closed_pose = ExtResource("7_wqxjj")
|
||||
metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
|
||||
|
||||
[node name="raw_burger" unique_id=1675596942 instance=ExtResource("1_fco8w")]
|
||||
[node name="burger" unique_id=1675596942 instance=ExtResource("1_fco8w")]
|
||||
|
||||
[node name="CollisionShape3D" parent="." index="0"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.00077831745, 0)
|
||||
@@ -46,6 +53,11 @@ hand_pose = SubResource("Resource_lc22d")
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.06531982, 0.042291984, -0.08604182)
|
||||
hand_pose = SubResource("Resource_qyiot")
|
||||
|
||||
[node name="FoodItem" parent="." index="4" unique_id=63948206 instance=ExtResource("10_yxtxs")]
|
||||
id = "raw_burger"
|
||||
type = 2
|
||||
[node name="CookableItem" parent="." index="4" unique_id=280820828 instance=ExtResource("9_hwyyd")]
|
||||
turns_into = ExtResource("9_r8jvx")
|
||||
|
||||
[node name="NetPickable" type="Node" parent="." index="5"]
|
||||
script = ExtResource("12_net")
|
||||
|
||||
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="." index="6"]
|
||||
replication_config = SubResource("Repl_item")
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
[ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="6_etdv2"]
|
||||
[ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="7_rl64h"]
|
||||
[ext_resource type="PackedScene" uid="uid://3lr2dhy62rhk" path="res://Prefabs/combinable_item.tscn" id="10_ut7mg"]
|
||||
[ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="10_zz42p"]
|
||||
[ext_resource type="PackedScene" uid="uid://7earnjgdmwgx" path="res://Prefabs/cookable_item.tscn" id="11_5ch2b"]
|
||||
[ext_resource type="Script" uid="uid://coidnv8b2yvxr" path="res://Prefabs/combine_recipe.gd" id="11_rjayc"]
|
||||
[ext_resource type="PackedScene" uid="uid://b3m2ag8g5rj4r" path="res://Items/hamburger.tscn" id="11_ut7mg"]
|
||||
|
||||
[sub_resource type="CylinderShape3D" id="CylinderShape3D_fco8w"]
|
||||
height = 0.04777527
|
||||
@@ -26,6 +28,12 @@ script = ExtResource("5_ychvb")
|
||||
closed_pose = ExtResource("7_rl64h")
|
||||
metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
|
||||
|
||||
[sub_resource type="Resource" id="Resource_wi1yl"]
|
||||
script = ExtResource("11_rjayc")
|
||||
ingredient_id = &"burger_buns"
|
||||
result = ExtResource("11_ut7mg")
|
||||
metadata/_custom_type_script = "uid://coidnv8b2yvxr"
|
||||
|
||||
[node name="CookedBurger" unique_id=1675596942 instance=ExtResource("1_ut7mg")]
|
||||
|
||||
[node name="CollisionShape3D" parent="." index="0"]
|
||||
@@ -46,8 +54,7 @@ hand_pose = SubResource("Resource_lc22d")
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.06531982, 0.042291984, -0.08604182)
|
||||
hand_pose = SubResource("Resource_qyiot")
|
||||
|
||||
[node name="FoodItem" parent="." index="4" unique_id=234567891 instance=ExtResource("10_zz42p")]
|
||||
id = "cooked_burger"
|
||||
type = 2
|
||||
[node name="CombinableItem" parent="." index="4" unique_id=434304805 instance=ExtResource("10_ut7mg")]
|
||||
recipes = Array[ExtResource("11_rjayc")]([SubResource("Resource_wi1yl")])
|
||||
|
||||
[node name="CombinableItem" parent="." index="5" unique_id=996936271 instance=ExtResource("10_ut7mg")]
|
||||
[node name="CookableItem" parent="." index="5" unique_id=280820828 instance=ExtResource("11_5ch2b")]
|
||||
|
||||
@@ -29,7 +29,7 @@ albedo_color = Color(0.6784191, 0.54546416, 0.016760282, 1)
|
||||
albedo_color = Color(0.29, 0.101500005, 0, 1)
|
||||
|
||||
[node name="Hamburger" unique_id=1675596942 groups=["platalbe_item"] instance=ExtResource("1_3gf3l")]
|
||||
gravity_scale = 0.04
|
||||
gravity_scale = 0.4
|
||||
|
||||
[node name="CollisionShape3D" parent="." index="0"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.02, 0.02, 0)
|
||||
@@ -78,6 +78,4 @@ sides = 16
|
||||
material = SubResource("StandardMaterial3D_6l01i")
|
||||
|
||||
[node name="FoodItem" parent="." index="5" unique_id=63948206 instance=ExtResource("7_yy3y8")]
|
||||
id = "hamburger"
|
||||
type = 0
|
||||
sell_value = 4
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
extends Control
|
||||
|
||||
## The 2D UI rendered inside the world-space menu (XRToolsViewport2DIn3D).
|
||||
## Lets the player type an IP on a numeric keypad and pick Host / Join / Solo.
|
||||
## Pure UI: it emits intents; main.gd performs the networking.
|
||||
|
||||
signal host_pressed()
|
||||
signal join_pressed(ip: String)
|
||||
signal solo_pressed()
|
||||
|
||||
#var _ip := "127.0.0.1"
|
||||
var _ip := "51.175.10.161"
|
||||
|
||||
@onready var _ip_label: Label = %IPLabel
|
||||
@onready var _status: Label = %Status
|
||||
@onready var _keypad: GridContainer = %Keypad
|
||||
@onready var _host_btn: Button = %HostButton
|
||||
@onready var _join_btn: Button = %JoinButton
|
||||
@onready var _solo_btn: Button = %SoloButton
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
for child in _keypad.get_children():
|
||||
if child is Button:
|
||||
child.pressed.connect(_on_key.bind(child.text))
|
||||
_host_btn.pressed.connect(func(): host_pressed.emit())
|
||||
_join_btn.pressed.connect(func(): join_pressed.emit(_ip))
|
||||
_solo_btn.pressed.connect(func(): solo_pressed.emit())
|
||||
_refresh()
|
||||
|
||||
|
||||
func _on_key(key: String) -> void:
|
||||
match key:
|
||||
"DEL":
|
||||
_ip = _ip.substr(0, max(0, _ip.length() - 1))
|
||||
_:
|
||||
if _ip.length() < 21:
|
||||
_ip += key
|
||||
_refresh()
|
||||
|
||||
|
||||
func _refresh() -> void:
|
||||
_ip_label.text = _ip if not _ip.is_empty() else "_"
|
||||
|
||||
|
||||
## Called by main.gd (via network_menu) to show connection feedback.
|
||||
func set_status(text: String) -> void:
|
||||
if _status:
|
||||
_status.text = text
|
||||
@@ -0,0 +1 @@
|
||||
uid://dvivyrspjcjgm
|
||||
@@ -0,0 +1,172 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://Net/join_panel.gd" id="1_panel"]
|
||||
|
||||
[node name="JoinPanel" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
script = ExtResource("1_panel")
|
||||
|
||||
[node name="Background" type="ColorRect" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
color = Color(0.09, 0.1, 0.13, 1)
|
||||
|
||||
[node name="Margin" type="MarginContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
theme_override_constants/margin_left = 24
|
||||
theme_override_constants/margin_top = 16
|
||||
theme_override_constants/margin_right = 24
|
||||
theme_override_constants/margin_bottom = 16
|
||||
|
||||
[node name="VBox" type="VBoxContainer" parent="Margin"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="Title" type="Label" parent="Margin/VBox"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "VRyHungry — Multiplayer"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="IPLabel" type="Label" parent="Margin/VBox"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 34
|
||||
text = "127.0.0.1"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Status" type="Label" parent="Margin/VBox"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.8, 0.8, 0.5, 1)
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "Enter host IP, then Join"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Keypad" type="GridContainer" parent="Margin/VBox"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/h_separation = 8
|
||||
theme_override_constants/v_separation = 8
|
||||
columns = 3
|
||||
|
||||
[node name="B1" type="Button" parent="Margin/VBox/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "1"
|
||||
|
||||
[node name="B2" type="Button" parent="Margin/VBox/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "2"
|
||||
|
||||
[node name="B3" type="Button" parent="Margin/VBox/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "3"
|
||||
|
||||
[node name="B4" type="Button" parent="Margin/VBox/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "4"
|
||||
|
||||
[node name="B5" type="Button" parent="Margin/VBox/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "5"
|
||||
|
||||
[node name="B6" type="Button" parent="Margin/VBox/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "6"
|
||||
|
||||
[node name="B7" type="Button" parent="Margin/VBox/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "7"
|
||||
|
||||
[node name="B8" type="Button" parent="Margin/VBox/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "8"
|
||||
|
||||
[node name="B9" type="Button" parent="Margin/VBox/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "9"
|
||||
|
||||
[node name="BDot" type="Button" parent="Margin/VBox/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "."
|
||||
|
||||
[node name="B0" type="Button" parent="Margin/VBox/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "0"
|
||||
|
||||
[node name="BDel" type="Button" parent="Margin/VBox/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 22
|
||||
text = "DEL"
|
||||
|
||||
[node name="Buttons" type="HBoxContainer" parent="Margin/VBox"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="HostButton" type="Button" parent="Margin/VBox/Buttons"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
custom_minimum_size = Vector2(0, 56)
|
||||
theme_override_font_sizes/font_size = 24
|
||||
text = "HOST"
|
||||
|
||||
[node name="JoinButton" type="Button" parent="Margin/VBox/Buttons"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
custom_minimum_size = Vector2(0, 56)
|
||||
theme_override_font_sizes/font_size = 24
|
||||
text = "JOIN"
|
||||
|
||||
[node name="SoloButton" type="Button" parent="Margin/VBox/Buttons"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
custom_minimum_size = Vector2(0, 56)
|
||||
theme_override_font_sizes/font_size = 24
|
||||
text = "SOLO"
|
||||
@@ -0,0 +1,319 @@
|
||||
extends Node
|
||||
|
||||
## Client-server session manager for VRyHungry (listen-server model).
|
||||
##
|
||||
## Registered as the "NetworkManager" autoload. Owns transport (ENet), tracks
|
||||
## the session, and is the single place that reassigns multiplayer authority
|
||||
## (only the server does so). The world scene (main.gd) registers its spawners
|
||||
## here via [method register_world]; higher layers (players, items, stations)
|
||||
## build on top of this in later phases.
|
||||
|
||||
const DEFAULT_PORT := 24565
|
||||
const MAX_CLIENTS := 7
|
||||
|
||||
## Emitted on every peer (including the server for its own local player) when a
|
||||
## player peer joins. On the server this fires for each remote peer; the server
|
||||
## uses it to spawn that peer's player.
|
||||
signal player_joined(peer_id: int)
|
||||
signal player_left(peer_id: int)
|
||||
signal session_started(is_server: bool)
|
||||
signal session_ended()
|
||||
signal connection_failed()
|
||||
|
||||
# World hooks, registered by main.gd once the scene tree exists.
|
||||
var _world: Node = null
|
||||
var _players_spawner: MultiplayerSpawner = null
|
||||
var _items_spawner: MultiplayerSpawner = null
|
||||
|
||||
var _log_file: FileAccess
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_open_log()
|
||||
multiplayer.peer_connected.connect(_on_peer_connected)
|
||||
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|
||||
multiplayer.connected_to_server.connect(_on_connected_to_server)
|
||||
multiplayer.connection_failed.connect(_on_connection_failed)
|
||||
multiplayer.server_disconnected.connect(_on_server_disconnected)
|
||||
|
||||
|
||||
# --- Public API ------------------------------------------------------------
|
||||
|
||||
## Start hosting. The host is peer 1 and also plays (listen server).
|
||||
func host(port: int = DEFAULT_PORT) -> Error:
|
||||
var peer := ENetMultiplayerPeer.new()
|
||||
var err := peer.create_server(port, MAX_CLIENTS)
|
||||
if err != OK:
|
||||
log_line("HOST failed to create_server on port %d: %s" % [port, error_string(err)])
|
||||
return err
|
||||
multiplayer.multiplayer_peer = peer
|
||||
log_line("HOST started on port %d (peer id %d)" % [port, multiplayer.get_unique_id()])
|
||||
session_started.emit(true)
|
||||
# The host's own local player joins immediately.
|
||||
_on_player_present(multiplayer.get_unique_id())
|
||||
return OK
|
||||
|
||||
|
||||
## Join an existing host.
|
||||
func join(address: String = "127.0.0.1", port: int = DEFAULT_PORT) -> Error:
|
||||
var peer := ENetMultiplayerPeer.new()
|
||||
var err := peer.create_client(address, port)
|
||||
if err != OK:
|
||||
log_line("JOIN failed to create_client %s:%d: %s" % [address, port, error_string(err)])
|
||||
return err
|
||||
multiplayer.multiplayer_peer = peer
|
||||
log_line("JOIN connecting to %s:%d ..." % [address, port])
|
||||
return OK
|
||||
|
||||
|
||||
## Leave the session and tear down transport.
|
||||
func leave() -> void:
|
||||
_go_offline()
|
||||
log_line("Session ended")
|
||||
session_ended.emit()
|
||||
|
||||
|
||||
# Restore Godot's default OfflineMultiplayerPeer (rather than leaving the peer
|
||||
# null), so is_multiplayer_authority()/get_unique_id() keep working while we are
|
||||
# back in single-player / menu state.
|
||||
func _go_offline() -> void:
|
||||
if multiplayer.multiplayer_peer:
|
||||
multiplayer.multiplayer_peer.close()
|
||||
multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
|
||||
|
||||
|
||||
# --- Item spawning ---------------------------------------------------------
|
||||
|
||||
## Spawn a networked item. Server-only when online (replicates to all peers via
|
||||
## the ItemsSpawner); works directly when offline. Returns the new node on the
|
||||
## machine that owns spawning, else null.
|
||||
func spawn_item(scene_path: String, xform: Transform3D) -> Node:
|
||||
if is_online() and not is_server():
|
||||
return null
|
||||
var data := {"scene": scene_path, "xform": xform}
|
||||
if is_online() and _items_spawner:
|
||||
return _items_spawner.spawn(data)
|
||||
# Offline: instantiate directly under the world.
|
||||
var inst := _spawn_item_from_data(data)
|
||||
if _world and inst:
|
||||
_world.add_child(inst)
|
||||
return inst
|
||||
|
||||
|
||||
# MultiplayerSpawner custom spawn function: runs on every peer to build the node
|
||||
# from the replicated payload.
|
||||
func _spawn_item_from_data(data: Variant) -> Node:
|
||||
var scene: PackedScene = load(data["scene"])
|
||||
if not scene:
|
||||
push_error("spawn_item: could not load scene %s" % str(data.get("scene")))
|
||||
return null
|
||||
var inst := scene.instantiate()
|
||||
if inst is Node3D:
|
||||
inst.transform = data["xform"]
|
||||
return inst
|
||||
|
||||
|
||||
# --- Item grab-authority transfer -----------------------------------------
|
||||
|
||||
## A client (or host) requests authority over an item it just grabbed. Runs on
|
||||
## the server. If the item was snapped into a station, the station releases it
|
||||
## so the grabber cleanly takes ownership.
|
||||
@rpc("any_peer", "reliable")
|
||||
func request_item_authority(item_path: NodePath) -> void:
|
||||
if not is_server():
|
||||
return
|
||||
var sender := multiplayer.get_remote_sender_id()
|
||||
# Assign authority + held state first (disables the item on the server so its
|
||||
# snap zone won't re-grab it), then release it from any station.
|
||||
_set_item_authority.rpc(item_path, sender)
|
||||
var item := get_node_or_null(item_path)
|
||||
if item:
|
||||
_release_from_snap_zones(item)
|
||||
|
||||
|
||||
## A player releases an item, forwarding its throw velocity so the server can
|
||||
## resume simulating it. Runs on the server. If released next to a station, the
|
||||
## server snaps it in (server-authoritative placement).
|
||||
@rpc("any_peer", "reliable")
|
||||
func release_item_authority(item_path: NodePath, lin: Vector3, ang: Vector3) -> void:
|
||||
if not is_server():
|
||||
return
|
||||
_set_item_authority.rpc(item_path, 1)
|
||||
var item := get_node_or_null(item_path)
|
||||
if item is RigidBody3D:
|
||||
item.freeze = false
|
||||
item.linear_velocity = lin
|
||||
item.angular_velocity = ang
|
||||
_try_snap_into_station(item)
|
||||
|
||||
|
||||
# All station snap zones in the world (nodes in the "station" group).
|
||||
func _station_snap_zones() -> Array:
|
||||
var zones := []
|
||||
for station in get_tree().get_nodes_in_group("station"):
|
||||
var zone = station.get_node_or_null("XRToolsSnapZone")
|
||||
if zone:
|
||||
zones.append(zone)
|
||||
return zones
|
||||
|
||||
|
||||
# If the item is snapped into any station, drop it from that station.
|
||||
func _release_from_snap_zones(item: Node) -> void:
|
||||
for zone in _station_snap_zones():
|
||||
if zone.picked_up_object == item:
|
||||
zone.drop_object()
|
||||
|
||||
|
||||
# Snap the item into the nearest empty station snap zone within grab range.
|
||||
func _try_snap_into_station(item: Node) -> void:
|
||||
if not (item is Node3D):
|
||||
return
|
||||
for zone in _station_snap_zones():
|
||||
if is_instance_valid(zone.picked_up_object):
|
||||
continue
|
||||
if zone.global_position.distance_to(item.global_position) <= zone.grab_distance:
|
||||
zone.pick_up_object(item)
|
||||
return
|
||||
|
||||
|
||||
# Server broadcasts an authority assignment so every peer agrees on who owns the
|
||||
# item (set_multiplayer_authority is a local call and must run everywhere).
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func _set_item_authority(item_path: NodePath, peer: int) -> void:
|
||||
var item := get_node_or_null(item_path)
|
||||
if not item:
|
||||
return
|
||||
item.set_multiplayer_authority(peer) # recursive: item + synchronizer + NetPickable
|
||||
var np := item.get_node_or_null("NetPickable")
|
||||
if np:
|
||||
np.net_held_by = 0 if peer == 1 else peer
|
||||
np.apply_held_state()
|
||||
|
||||
|
||||
func is_server() -> bool:
|
||||
return is_online() and multiplayer.is_server()
|
||||
|
||||
|
||||
## True only when a real ENet session is active. Godot installs a default
|
||||
## OfflineMultiplayerPeer, so a non-null peer alone does not mean "online".
|
||||
func is_online() -> bool:
|
||||
var p := multiplayer.multiplayer_peer
|
||||
return p != null and not (p is OfflineMultiplayerPeer)
|
||||
|
||||
|
||||
## True on the machine that owns authoritative world logic: the server when
|
||||
## online, or the single player when offline. Station logic and spawning should
|
||||
## only run where this is true, so state has one source of truth.
|
||||
func owns_world() -> bool:
|
||||
return not is_online() or is_server()
|
||||
|
||||
|
||||
# --- Station work-progress seam -------------------------------------------
|
||||
|
||||
## Reusable entry point for a client to contribute work to a station (e.g. a
|
||||
## future chopping/gesture station). The client detects the gesture locally and
|
||||
## calls this; the server validates and accumulates. Timer-driven stations like
|
||||
## the Hob don't need it, but it is the drop-in seam for input-driven ones.
|
||||
@rpc("any_peer", "reliable")
|
||||
func submit_work(station_path: NodePath, amount: float) -> void:
|
||||
if not is_server():
|
||||
return
|
||||
var station := get_node_or_null(station_path)
|
||||
if station and station.has_method("add_work"):
|
||||
station.add_work(multiplayer.get_remote_sender_id(), amount)
|
||||
|
||||
|
||||
## Called by main.gd once the world scene is ready, passing its spawners.
|
||||
func register_world(world: Node, players_spawner: MultiplayerSpawner, items_spawner: MultiplayerSpawner) -> void:
|
||||
_world = world
|
||||
_players_spawner = players_spawner
|
||||
_items_spawner = items_spawner
|
||||
if _items_spawner:
|
||||
_items_spawner.spawn_function = _spawn_item_from_data
|
||||
log_line("World registered (players_spawner=%s items_spawner=%s)" % [str(players_spawner != null), str(items_spawner != null)])
|
||||
|
||||
|
||||
## Called by main.gd after it has registered the world and connected its
|
||||
## player_joined/left listeners. Kicks off any command-line driven session so
|
||||
## that session signals never fire before the world is listening.
|
||||
func world_ready() -> void:
|
||||
_handle_cmdline()
|
||||
|
||||
|
||||
# --- Session signal handlers ----------------------------------------------
|
||||
|
||||
func _on_peer_connected(peer_id: int) -> void:
|
||||
log_line("peer_connected: %d" % peer_id)
|
||||
# Only the server reacts by materialising that peer's player.
|
||||
if is_server():
|
||||
_on_player_present(peer_id)
|
||||
|
||||
func _on_peer_disconnected(peer_id: int) -> void:
|
||||
log_line("peer_disconnected: %d" % peer_id)
|
||||
if is_server():
|
||||
_on_player_absent(peer_id)
|
||||
|
||||
func _on_connected_to_server() -> void:
|
||||
log_line("connected_to_server (my id=%d)" % multiplayer.get_unique_id())
|
||||
session_started.emit(false)
|
||||
|
||||
func _on_connection_failed() -> void:
|
||||
log_line("connection_failed")
|
||||
_go_offline()
|
||||
connection_failed.emit()
|
||||
|
||||
func _on_server_disconnected() -> void:
|
||||
log_line("server_disconnected")
|
||||
_go_offline()
|
||||
session_ended.emit()
|
||||
|
||||
|
||||
# Player materialise/dematerialise. Phase 2 wires these to the PlayersSpawner;
|
||||
# for now they announce presence so the transport layer is independently testable.
|
||||
func _on_player_present(peer_id: int) -> void:
|
||||
log_line("player_present: %d" % peer_id)
|
||||
player_joined.emit(peer_id)
|
||||
|
||||
func _on_player_absent(peer_id: int) -> void:
|
||||
log_line("player_absent: %d" % peer_id)
|
||||
player_left.emit(peer_id)
|
||||
|
||||
|
||||
# --- Command-line driven test bootstrap -----------------------------------
|
||||
|
||||
func _handle_cmdline() -> void:
|
||||
var args := OS.get_cmdline_user_args()
|
||||
if args.has("--server"):
|
||||
log_line("cmdline: --server")
|
||||
host()
|
||||
elif args.has("--join"):
|
||||
var idx := args.find("--join")
|
||||
var addr := "127.0.0.1"
|
||||
if idx + 1 < args.size():
|
||||
addr = args[idx + 1]
|
||||
log_line("cmdline: --join %s" % addr)
|
||||
join(addr)
|
||||
|
||||
|
||||
# --- Logging ---------------------------------------------------------------
|
||||
|
||||
func _open_log() -> void:
|
||||
var dir := OS.get_environment("TEMP")
|
||||
if dir.is_empty():
|
||||
dir = OS.get_environment("TMPDIR")
|
||||
if dir.is_empty():
|
||||
dir = "user://"
|
||||
var path := dir.path_join("vryhungry_net_%d.log" % OS.get_process_id())
|
||||
_log_file = FileAccess.open(path, FileAccess.WRITE)
|
||||
log_line("=== NetworkManager log (pid %d) ===" % OS.get_process_id())
|
||||
|
||||
func log_line(s: String) -> void:
|
||||
var id := 0
|
||||
var p := multiplayer.multiplayer_peer
|
||||
if p != null and p.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
|
||||
id = multiplayer.get_unique_id()
|
||||
var line := "[NET %d] %s" % [id, s]
|
||||
print(line)
|
||||
if _log_file:
|
||||
_log_file.store_line(line)
|
||||
_log_file.flush()
|
||||
@@ -0,0 +1 @@
|
||||
uid://deefoory0vqmm
|
||||
@@ -0,0 +1,37 @@
|
||||
extends Node3D
|
||||
|
||||
## World-space main menu: hosts the 2D join_panel on an XRToolsViewport2DIn3D so
|
||||
## the player can point at it with a controller laser. Re-emits the panel's
|
||||
## intents; main.gd drives the actual networking.
|
||||
|
||||
signal host_pressed()
|
||||
signal join_pressed(ip: String)
|
||||
signal solo_pressed()
|
||||
|
||||
@onready var _vp: XRToolsViewport2DIn3D = $Viewport2Din3D
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
# The viewport instantiates its 2D scene during its own _ready; connect once
|
||||
# that has happened.
|
||||
_connect_scene.call_deferred()
|
||||
|
||||
|
||||
func _connect_scene() -> void:
|
||||
_vp.connect_scene_signal("host_pressed", func(): host_pressed.emit())
|
||||
_vp.connect_scene_signal("join_pressed", func(ip): join_pressed.emit(ip))
|
||||
_vp.connect_scene_signal("solo_pressed", func(): solo_pressed.emit())
|
||||
|
||||
|
||||
func show_menu() -> void:
|
||||
visible = true
|
||||
|
||||
|
||||
func hide_menu() -> void:
|
||||
visible = false
|
||||
|
||||
|
||||
func set_status(text: String) -> void:
|
||||
var scene := _vp.get_scene_instance()
|
||||
if scene and scene.has_method("set_status"):
|
||||
scene.set_status(text)
|
||||
@@ -0,0 +1 @@
|
||||
uid://1o2nca75po1f
|
||||
@@ -0,0 +1,13 @@
|
||||
[gd_scene load_steps=4 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://Net/network_menu.gd" id="1_menu"]
|
||||
[ext_resource type="PackedScene" uid="uid://clujaf3u776a3" path="res://addons/godot-xr-tools/objects/viewport_2d_in_3d.tscn" id="2_vp"]
|
||||
[ext_resource type="PackedScene" path="res://Net/join_panel.tscn" id="3_panel"]
|
||||
|
||||
[node name="NetworkMenu" type="Node3D"]
|
||||
script = ExtResource("1_menu")
|
||||
|
||||
[node name="Viewport2Din3D" parent="." instance=ExtResource("2_vp")]
|
||||
screen_size = Vector2(0.6, 0.4)
|
||||
scene = ExtResource("3_panel")
|
||||
viewport_size = Vector2(600, 400)
|
||||
@@ -0,0 +1,48 @@
|
||||
[gd_scene load_steps=7 format=3 uid="uid://cnetplayer0001"]
|
||||
|
||||
[ext_resource type="Script" path="res://Player/player.gd" id="1_player"]
|
||||
|
||||
[sub_resource type="SceneReplicationConfig" id="Repl_player"]
|
||||
properties/0/path = NodePath(".:head_xform")
|
||||
properties/0/spawn = true
|
||||
properties/0/replication_mode = 1
|
||||
properties/1/path = NodePath(".:left_xform")
|
||||
properties/1/spawn = true
|
||||
properties/1/replication_mode = 1
|
||||
properties/2/path = NodePath(".:right_xform")
|
||||
properties/2/spawn = true
|
||||
properties/2/replication_mode = 1
|
||||
properties/3/path = NodePath(".:net_heartbeat")
|
||||
properties/3/spawn = true
|
||||
properties/3/replication_mode = 1
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Mat_head"]
|
||||
albedo_color = Color(0.85, 0.7, 0.55, 1)
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_head"]
|
||||
material = SubResource("Mat_head")
|
||||
size = Vector3(0.18, 0.24, 0.16)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Mat_hand"]
|
||||
albedo_color = Color(0.2, 0.5, 0.9, 1)
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_hand"]
|
||||
material = SubResource("Mat_hand")
|
||||
size = Vector3(0.09, 0.05, 0.12)
|
||||
|
||||
[node name="Player" type="Node3D"]
|
||||
script = ExtResource("1_player")
|
||||
|
||||
[node name="Avatar" type="Node3D" parent="."]
|
||||
|
||||
[node name="Head" type="MeshInstance3D" parent="Avatar"]
|
||||
mesh = SubResource("Mesh_head")
|
||||
|
||||
[node name="LeftHand" type="MeshInstance3D" parent="Avatar"]
|
||||
mesh = SubResource("Mesh_hand")
|
||||
|
||||
[node name="RightHand" type="MeshInstance3D" parent="Avatar"]
|
||||
mesh = SubResource("Mesh_hand")
|
||||
|
||||
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
|
||||
replication_config = SubResource("Repl_player")
|
||||
@@ -0,0 +1,80 @@
|
||||
extends Node3D
|
||||
|
||||
## One instance per connected peer. The owning peer is the multiplayer
|
||||
## authority. The local (authority) player builds the full XR rig from
|
||||
## XROrigin.tscn and feeds its head/hand poses into synced properties; remote
|
||||
## players show a lightweight avatar driven by those synced poses.
|
||||
|
||||
const RIG_SCENE := preload("res://XROrigin.tscn")
|
||||
|
||||
# Synced avatar pose. Authority writes these from the XR trackers each frame;
|
||||
# remotes read them onto the Avatar. net_heartbeat is a monotonic counter used
|
||||
# to confirm replication is live (also handy for debugging).
|
||||
@export var head_xform: Transform3D = Transform3D.IDENTITY
|
||||
@export var left_xform: Transform3D = Transform3D.IDENTITY
|
||||
@export var right_xform: Transform3D = Transform3D.IDENTITY
|
||||
@export var net_heartbeat: int = 0
|
||||
|
||||
var _rig: Node3D
|
||||
var _camera: Node3D
|
||||
var _left: Node3D
|
||||
var _right: Node3D
|
||||
var _is_local := false
|
||||
|
||||
@onready var _avatar: Node3D = $Avatar
|
||||
@onready var _avatar_head: Node3D = $Avatar/Head
|
||||
@onready var _avatar_left: Node3D = $Avatar/LeftHand
|
||||
@onready var _avatar_right: Node3D = $Avatar/RightHand
|
||||
|
||||
|
||||
func _enter_tree() -> void:
|
||||
# The spawner names each player by its owning peer id.
|
||||
set_multiplayer_authority(str(name).to_int())
|
||||
|
||||
|
||||
# (replication config is authored in Player.tscn's MultiplayerSynchronizer)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_is_local = is_multiplayer_authority()
|
||||
if _is_local:
|
||||
_setup_local()
|
||||
else:
|
||||
_setup_remote()
|
||||
if Engine.has_singleton("NetworkManager") or get_node_or_null("/root/NetworkManager"):
|
||||
NetworkManager.log_line("Player %s ready (authority=%d local=%s)" % [name, get_multiplayer_authority(), str(_is_local)])
|
||||
|
||||
|
||||
func _setup_local() -> void:
|
||||
# We see through the headset; hide our own avatar mesh.
|
||||
_avatar.visible = false
|
||||
_rig = RIG_SCENE.instantiate()
|
||||
add_child(_rig)
|
||||
_camera = _rig.get_node_or_null("XRCamera3D")
|
||||
_left = _rig.get_node_or_null("XRControllerLeftHand")
|
||||
_right = _rig.get_node_or_null("XRControllerRightHand")
|
||||
_enable_xr()
|
||||
|
||||
|
||||
func _enable_xr() -> void:
|
||||
var xr := XRServer.find_interface("OpenXR")
|
||||
if xr and xr.is_initialized():
|
||||
get_viewport().use_xr = true
|
||||
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
|
||||
|
||||
|
||||
func _setup_remote() -> void:
|
||||
_avatar.visible = true
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if _is_local:
|
||||
if _camera:
|
||||
head_xform = _camera.global_transform
|
||||
left_xform = _left.global_transform
|
||||
right_xform = _right.global_transform
|
||||
net_heartbeat += 1
|
||||
else:
|
||||
_avatar_head.global_transform = head_xform
|
||||
_avatar_left.global_transform = left_xform
|
||||
_avatar_right.global_transform = right_xform
|
||||
@@ -0,0 +1 @@
|
||||
uid://ndrfa2xle033
|
||||
+14
-96
@@ -1,106 +1,24 @@
|
||||
class_name CombinableItem
|
||||
extends Node
|
||||
|
||||
const RECIPE_MANAGER = preload("res://RecipeManager.gd")
|
||||
## Identity of this item, referenced by other items' recipes.
|
||||
@export var id: StringName
|
||||
|
||||
@onready var combine_area_3d: Area3D = $Area3d
|
||||
@onready var _pickable: XRToolsPickable = get_parent() as XRToolsPickable
|
||||
|
||||
# Guard so a combine only fires once.
|
||||
var _combining: bool = false
|
||||
var _food_item: FoodItem
|
||||
## Recipes describing what the item holding this component turns into when
|
||||
## another item is combined into it while snapped. Recipes are directional:
|
||||
## only the snapped (base) item's recipes are consulted.
|
||||
@export var recipes: Array[CombineRecipe] = []
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
print("CombinableItem _ready(): ", _pickable.name)
|
||||
_food_item = get_parent().get_node_or_null("FoodItem") as FoodItem
|
||||
if not _food_item:
|
||||
push_error("CombinableItem is missing FoodItem reference. must be a sibling of a FoodItem on ", get_parent().name, ".")
|
||||
|
||||
if not _pickable:
|
||||
push_error("CombineZone must be a grand child of an XRToolsPickable.")
|
||||
return
|
||||
if not _food_item:
|
||||
push_error("CombineZone requires a FoodItem sibling on ", _pickable.name, ".")
|
||||
return
|
||||
|
||||
# Detect items whether held (layer 17 "Held Objects") or loose
|
||||
# (layer 3 "Pickable Objects"). Don't advertise a layer of our own.
|
||||
combine_area_3d.collision_mask = 0b0000_0000_0000_0001_0000_0000_0000_0100
|
||||
combine_area_3d.collision_layer = 0
|
||||
# Deferred: _ready can run during a physics signal flush (when a combine
|
||||
# spawns the result), where toggling monitoring directly is forbidden.
|
||||
set_deferred("monitoring", false)
|
||||
set_deferred("monitorable", false)
|
||||
|
||||
# Signals
|
||||
_pickable.picked_up.connect(_on_item_picked_up)
|
||||
_pickable.dropped.connect(_on_item_dropped)
|
||||
combine_area_3d.body_entered.connect(_on_body_entered)
|
||||
if id.is_empty():
|
||||
push_error("CombinableItem on ", get_parent().name, " has no 'id' set.")
|
||||
|
||||
|
||||
# Enable the trigger only while snapped into a snap zone (not hand-held).
|
||||
func _on_item_picked_up(_item: Node3D) -> void:
|
||||
var by := _pickable.get_picked_up_by()
|
||||
var snapped: bool = by != null and by.has_method("is_xr_class") and by.is_xr_class("XRToolsSnapZone")
|
||||
set_deferred("monitoring", snapped)
|
||||
|
||||
func _on_item_dropped(_item: Node3D) -> void:
|
||||
set_deferred("monitoring", false)
|
||||
|
||||
# If the other body is a FoodItem, check for a recipe and combine if one exists.
|
||||
func _on_body_entered(body: Node3D) -> void:
|
||||
if _combining or body == _pickable:
|
||||
print("CombinableItem _on_body_entered: other is our own pickable")
|
||||
return
|
||||
|
||||
var other := _find_food_item(body)
|
||||
if not other:
|
||||
print("CombinableItem _on_body_entered: other is not a foodItem")
|
||||
return
|
||||
|
||||
var result: PackedScene = RECIPE_MANAGER.get_combination_result(_food_item.id, other.id)
|
||||
if not result:
|
||||
print("CombinableItem _on_body_entered: There is no recipe for %s + %s" % [_food_item.id, other.id])
|
||||
return
|
||||
|
||||
_combine(body, result)
|
||||
|
||||
# Instantiate the result of combination and free the two ingredient items
|
||||
func _combine(other_body: Node3D, result: PackedScene) -> void:
|
||||
print("CombinableItem _combine: combining %s + %s into %s" % [_food_item.id, _find_food_item(other_body).id, result.resource_path])
|
||||
_combining = true
|
||||
|
||||
# Get Snapzone
|
||||
var snap_zone := _pickable.get_picked_up_by()
|
||||
if not snap_zone or not snap_zone.has_method("pick_up_object"):
|
||||
push_warning("CombineZone: base item is not held by a snap zone; cannot combine.")
|
||||
_combining = false
|
||||
return
|
||||
|
||||
# Spawn the result at the base item's location, in world space.
|
||||
var base_transform := _pickable.global_transform
|
||||
var result_instance: Node3D = result.instantiate()
|
||||
_pickable.get_tree().current_scene.add_child(result_instance)
|
||||
result_instance.global_transform = base_transform
|
||||
|
||||
# Free the pickable / root of this item and consume the incoming item.
|
||||
snap_zone.drop_object()
|
||||
_pickable.queue_free()
|
||||
if other_body.has_method("drop_and_free"): # XRToolsPickable has this method
|
||||
other_body.drop_and_free()
|
||||
else:
|
||||
other_body.queue_free()
|
||||
|
||||
# Snap the result into the now-empty zone.
|
||||
snap_zone.pick_up_object(result_instance)
|
||||
|
||||
|
||||
# Find a FoodItem among the direct children of [param node].
|
||||
func _find_food_item(node: Node) -> FoodItem:
|
||||
if not node:
|
||||
return null
|
||||
for child in node.get_children():
|
||||
if child is FoodItem:
|
||||
return child
|
||||
## Returns the scene this item becomes when combined with [param other_id],
|
||||
## or null if there is no matching recipe.
|
||||
func get_result_for(other_id: StringName) -> PackedScene:
|
||||
for recipe in recipes:
|
||||
if recipe and recipe.ingredient_id == other_id:
|
||||
return recipe.result
|
||||
return null
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
[gd_scene format=3 uid="uid://3lr2dhy62rhk"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dounic663dn5q" path="res://Prefabs/combinable_item.gd" id="1_q21ti"]
|
||||
[ext_resource type="Script" uid="uid://coidnv8b2yvxr" path="res://Prefabs/combine_recipe.gd" id="2_anbu7"]
|
||||
[ext_resource type="Script" uid="uid://e3im02nq5cye" path="res://Prefabs/combine_zone.gd" id="4_0om48"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_combine"]
|
||||
size = Vector3(0.16, 0.16, 0.16)
|
||||
|
||||
[node name="CombinableItem" type="Node3D" unique_id=996936271]
|
||||
script = ExtResource("1_q21ti")
|
||||
id = &"some_food_item"
|
||||
recipes = Array[ExtResource("2_anbu7")]([null])
|
||||
|
||||
[node name="Area3d" type="Area3D" parent="." unique_id=1514178016]
|
||||
[node name="CombineZone" type="Area3D" parent="." unique_id=1514178016]
|
||||
script = ExtResource("4_0om48")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Area3d" unique_id=706892516]
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="CombineZone" unique_id=706892516]
|
||||
shape = SubResource("BoxShape3D_combine")
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
class_name CombineZone
|
||||
extends Area3D
|
||||
|
||||
## Trigger area that lets a snapped item combine with another item pushed into
|
||||
## it. The zone only monitors while its owning item is held by an
|
||||
## [XRToolsSnapZone]; when an item carrying a matching [CombinableItem.id]
|
||||
## enters, the snapped (base) item transforms into the recipe result and the
|
||||
## incoming item is consumed.
|
||||
|
||||
# The pickable this zone belongs to (its parent).
|
||||
@onready var _item: XRToolsPickable = get_parent().get_parent() as XRToolsPickable
|
||||
|
||||
# The base item's recipe data.
|
||||
@onready var _combinable: CombinableItem = _find_combinable(_item)
|
||||
|
||||
# Guard so a combine only fires once.
|
||||
var _combining: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
# Detect items whether held (layer 17 "Held Objects") or loose
|
||||
# (layer 3 "Pickable Objects"). Don't advertise a layer of our own.
|
||||
collision_mask = 0b0000_0000_0000_0001_0000_0000_0000_0100
|
||||
collision_layer = 0
|
||||
# Deferred: _ready can run during a physics signal flush (when a combine
|
||||
# spawns the result), where toggling monitoring directly is forbidden.
|
||||
set_deferred("monitoring", false)
|
||||
set_deferred("monitorable", false)
|
||||
|
||||
if not _item:
|
||||
push_error("CombineZone must be a grand child of an XRToolsPickable.")
|
||||
return
|
||||
if not _combinable:
|
||||
push_error("CombineZone requires a CombinableItem sibling on ", _item.name, ".")
|
||||
return
|
||||
|
||||
_item.picked_up.connect(_on_item_picked_up)
|
||||
_item.dropped.connect(_on_item_dropped)
|
||||
body_entered.connect(_on_body_entered)
|
||||
|
||||
|
||||
# Enable the trigger only while snapped into a snap zone (not hand-held).
|
||||
func _on_item_picked_up(_pickable: Node3D) -> void:
|
||||
var by := _item.get_picked_up_by()
|
||||
var snapped: bool = by != null and by.has_method("is_xr_class") and by.is_xr_class("XRToolsSnapZone")
|
||||
set_deferred("monitoring", snapped)
|
||||
|
||||
|
||||
func _on_item_dropped(_pickable: Node3D) -> void:
|
||||
set_deferred("monitoring", false)
|
||||
|
||||
|
||||
func _on_body_entered(body: Node3D) -> void:
|
||||
if _combining or body == _item:
|
||||
return
|
||||
|
||||
# Combining is a world-state change: only the server/offline host runs it.
|
||||
# (Clients never have the base item snapped, so their zone won't monitor, but
|
||||
# guard explicitly in case of overlap during authority handoff.)
|
||||
if not NetworkManager.owns_world():
|
||||
return
|
||||
|
||||
var other := _find_combinable(body)
|
||||
if not other:
|
||||
return
|
||||
|
||||
var result: PackedScene = _combinable.get_result_for(other.id)
|
||||
if not result:
|
||||
return
|
||||
|
||||
_combine(body, result)
|
||||
|
||||
|
||||
# Transform the base item into [param result], consuming the incoming item.
|
||||
func _combine(other_body: Node3D, result: PackedScene) -> void:
|
||||
_combining = true
|
||||
|
||||
var snap_zone := _item.get_picked_up_by()
|
||||
if not snap_zone or not snap_zone.has_method("pick_up_object"):
|
||||
push_warning("CombineZone: base item is not held by a snap zone; cannot combine.")
|
||||
_combining = false
|
||||
return
|
||||
|
||||
# Spawn the result (networked) at the base item's location, in world space.
|
||||
var base_transform := _item.global_transform
|
||||
var result_instance := NetworkManager.spawn_item(result.resource_path, base_transform)
|
||||
|
||||
# Free the base item and consume the incoming item.
|
||||
snap_zone.drop_object()
|
||||
_item.queue_free()
|
||||
if other_body.has_method("drop_and_free"):
|
||||
other_body.drop_and_free()
|
||||
else:
|
||||
other_body.queue_free()
|
||||
|
||||
# Snap the result into the now-empty zone.
|
||||
if result_instance:
|
||||
snap_zone.pick_up_object(result_instance)
|
||||
|
||||
|
||||
# Find a CombinableItem among the direct children of [param node].
|
||||
func _find_combinable(node: Node) -> CombinableItem:
|
||||
if not node:
|
||||
return null
|
||||
for child in node.get_children():
|
||||
if child is CombinableItem:
|
||||
return child
|
||||
return null
|
||||
@@ -0,0 +1 @@
|
||||
uid://e3im02nq5cye
|
||||
@@ -6,4 +6,3 @@ enum Type { MEAL, SIDE, INGREDIENT, NONE }
|
||||
|
||||
@export var id: String
|
||||
@export var type: Type = Type.NONE
|
||||
@export var sell_value = 1
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
|
||||
[node name="FoodItem" type="Node" unique_id=63948206]
|
||||
script = ExtResource("1_0na5k")
|
||||
id = "some_food_id"
|
||||
id = "hamburger"
|
||||
item_type = "Meal"
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
class_name NetPickable
|
||||
extends Node
|
||||
|
||||
## Networks an XRToolsPickable (RigidBody3D). Default authority is the server,
|
||||
## which simulates free items and syncs their transform. Grabbing an item with a
|
||||
## hand transfers authority to the grabbing client (its local kinematic grab
|
||||
## driver then drives the synced transform); releasing returns authority and the
|
||||
## throw velocity to the server. Non-authority peers keep the body frozen and
|
||||
## follow the synced transform, so only one machine ever simulates an item.
|
||||
|
||||
@onready var _item: XRToolsPickable = get_parent() as XRToolsPickable
|
||||
|
||||
var net_held_by := 0 # peer id currently holding (0 = free); kept consistent via NetworkManager
|
||||
var _was_authority := true
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if not _item:
|
||||
push_error("NetPickable must be a child of an XRToolsPickable")
|
||||
return
|
||||
_item.grabbed.connect(_on_grabbed)
|
||||
_item.released.connect(_on_released)
|
||||
_was_authority = _item.is_multiplayer_authority()
|
||||
_apply_authority_state()
|
||||
|
||||
|
||||
## Enable the item only where it is free or where we are its current holder, so
|
||||
## a peer's snap zone / hand never grabs an item another peer is holding. Called
|
||||
## by NetworkManager whenever net_held_by changes. Note: net_held_by is set per
|
||||
## peer by RPC (not by a synchronizer), so the holder keeps its item enabled
|
||||
## while everyone else disables their copy.
|
||||
func apply_held_state() -> void:
|
||||
if not _item:
|
||||
return
|
||||
_item.enabled = net_held_by == 0 or net_held_by == multiplayer.get_unique_id()
|
||||
|
||||
|
||||
func _physics_process(_delta: float) -> void:
|
||||
if not NetworkManager.is_online():
|
||||
return
|
||||
var mine := _item.is_multiplayer_authority()
|
||||
if mine != _was_authority:
|
||||
_was_authority = mine
|
||||
_apply_authority_state()
|
||||
|
||||
|
||||
# Configure physics for whether we own this item or merely mirror it.
|
||||
func _apply_authority_state() -> void:
|
||||
if not _item:
|
||||
return
|
||||
if _item.is_multiplayer_authority():
|
||||
# We own physics: simulate the item whenever it is not held.
|
||||
if not _item.is_picked_up():
|
||||
_item.freeze = false
|
||||
else:
|
||||
# Someone else owns it: follow the synced transform kinematically.
|
||||
_item.freeze = true
|
||||
_item.freeze_mode = RigidBody3D.FREEZE_MODE_KINEMATIC
|
||||
|
||||
|
||||
func _on_grabbed(_pickable: Node3D, by: Node3D) -> void:
|
||||
if not NetworkManager.is_online():
|
||||
return
|
||||
# Only hand grabs transfer authority; snap-zone grabs are server-driven.
|
||||
if not (by is XRToolsFunctionPickup):
|
||||
return
|
||||
NetworkManager.request_item_authority.rpc_id(1, _item.get_path())
|
||||
|
||||
|
||||
func _on_released(_pickable: Node3D, by: Node3D) -> void:
|
||||
if not NetworkManager.is_online():
|
||||
return
|
||||
if not (by is XRToolsFunctionPickup):
|
||||
return
|
||||
NetworkManager.release_item_authority.rpc_id(1, _item.get_path(), _item.linear_velocity, _item.angular_velocity)
|
||||
@@ -0,0 +1 @@
|
||||
uid://6n784li8n5ck
|
||||
@@ -1,341 +0,0 @@
|
||||
class_name RecipeManager
|
||||
extends Node
|
||||
|
||||
static var _recipes: Dictionary
|
||||
static var _combining_map: Dictionary
|
||||
static var _cooking_map: Dictionary
|
||||
static var _chopping_map: Dictionary
|
||||
static var _rolling_map: Dictionary
|
||||
static var _augmenting_map: Dictionary
|
||||
static var _scene_paths: Dictionary
|
||||
static var _loaded: bool = false
|
||||
|
||||
static func load_recipes() -> void:
|
||||
if _loaded:
|
||||
return
|
||||
|
||||
var recipe_path := "res://recipes.yaml"
|
||||
if not FileAccess.file_exists(recipe_path):
|
||||
push_error("RecipeManager: recipes.yaml was not found at %s" % recipe_path)
|
||||
return
|
||||
|
||||
var yaml_available := ClassDB.class_exists("YAML")
|
||||
if yaml_available:
|
||||
var result = ClassDB.class_call_static("YAML", "load_file", recipe_path)
|
||||
if result != null and not result.has_error():
|
||||
var data = result.get_data()
|
||||
if typeof(data) != TYPE_DICTIONARY:
|
||||
push_warning("RecipeManager: YAML addon parsed recipes.yaml but returned an unsupported structure")
|
||||
|
||||
_recipes = data
|
||||
_build_scene_paths()
|
||||
_build_combining_map()
|
||||
_build_cooking_map()
|
||||
_build_chopping_map()
|
||||
_build_rolling_map()
|
||||
_build_augmenting_map()
|
||||
_loaded = true
|
||||
|
||||
|
||||
static func get_combination_result(first_id: StringName, second_id: StringName) -> PackedScene:
|
||||
load_recipes()
|
||||
var result_id = _combining_map.get(_make_pair_key(first_id, second_id))
|
||||
if result_id == null:
|
||||
return null
|
||||
return get_item_scene(result_id)
|
||||
|
||||
|
||||
static func get_cooking_result(ingredient_id: StringName) -> PackedScene:
|
||||
load_recipes()
|
||||
for cooked_id in _cooking_map.keys():
|
||||
var cook_defs = _cooking_map[cooked_id]
|
||||
# cook_defs may be an Array of recipe dicts (new) or a single dict (backwards compat)
|
||||
if typeof(cook_defs) == TYPE_ARRAY:
|
||||
for cook_def in cook_defs:
|
||||
if cook_def["ingredient"] == ingredient_id:
|
||||
return get_item_scene(cooked_id)
|
||||
else:
|
||||
var cook_def = cook_defs
|
||||
if cook_def["ingredient"] == ingredient_id:
|
||||
return get_item_scene(cooked_id)
|
||||
return null
|
||||
|
||||
|
||||
static func get_cooking_time(ingredient_id: StringName) -> float:
|
||||
load_recipes()
|
||||
for cooked_id in _cooking_map.keys():
|
||||
var cook_defs = _cooking_map[cooked_id]
|
||||
if typeof(cook_defs) == TYPE_ARRAY:
|
||||
for cook_def in cook_defs:
|
||||
if cook_def["ingredient"] == ingredient_id:
|
||||
return cook_def["time"]
|
||||
else:
|
||||
var cook_def = cook_defs
|
||||
if cook_def["ingredient"] == ingredient_id:
|
||||
return cook_def["time"]
|
||||
return 0.0
|
||||
|
||||
|
||||
static func get_item_scene(item_id: StringName) -> PackedScene:
|
||||
load_recipes()
|
||||
var scene_path = _scene_paths.get(str(item_id), "")
|
||||
if typeof(scene_path) != TYPE_STRING or scene_path.is_empty():
|
||||
push_error("RecipeManager: no scene mapping found for item id '%s'" % item_id)
|
||||
return null
|
||||
|
||||
var scene = ResourceLoader.load(scene_path)
|
||||
if not scene:
|
||||
push_error("RecipeManager: failed to load scene '%s' for item '%s'" % [scene_path, item_id])
|
||||
return null
|
||||
return scene as PackedScene
|
||||
|
||||
|
||||
static func print_all_recipes() -> void:
|
||||
load_recipes()
|
||||
if not _loaded:
|
||||
print("RecipeManager: could not load recipes.yaml; no recipes printed.")
|
||||
return
|
||||
|
||||
if _combining_map.is_empty():
|
||||
print("RecipeManager: no combining recipes found")
|
||||
return
|
||||
|
||||
print("#### RecipeManager: Loaded recipes ####")
|
||||
_print_combining_recipes()
|
||||
print("")
|
||||
_print_cooking_recipes()
|
||||
print("")
|
||||
_print_chopping_recipes()
|
||||
print("")
|
||||
_print_rolling_recipes()
|
||||
print("")
|
||||
_print_augmenting_recipes()
|
||||
|
||||
static func _print_combining_recipes() -> void:
|
||||
for pair_key in _combining_map.keys():
|
||||
var result_id = _combining_map[pair_key]
|
||||
var key_str = str(pair_key)
|
||||
var separator_index = key_str.find("|")
|
||||
if separator_index == -1:
|
||||
print("RecipeManager: invalid combining map key '%s'" % key_str)
|
||||
continue
|
||||
|
||||
var first = key_str.substr(0, separator_index)
|
||||
var second = key_str.substr(separator_index + 1, key_str.length() - separator_index - 1)
|
||||
print(" %s <-combine-- %s + %s" % [result_id, first, second])
|
||||
|
||||
|
||||
static func _print_cooking_recipes() -> void:
|
||||
for cooked_id in _cooking_map.keys():
|
||||
var cook_defs = _cooking_map[cooked_id]
|
||||
if typeof(cook_defs) == TYPE_ARRAY:
|
||||
for cook_def in cook_defs:
|
||||
print(" %s <-cook-- %s - time: %.1f" % [cooked_id, cook_def["ingredient"], cook_def["time"]])
|
||||
else:
|
||||
var cook_def = cook_defs
|
||||
print(" %s <-cook-- %s - time: %.1f" % [cooked_id, cook_def["ingredient"], cook_def["time"]])
|
||||
|
||||
|
||||
static func _print_chopping_recipes() -> void:
|
||||
for chopped_id in _chopping_map.keys():
|
||||
var chop_def = _chopping_map[chopped_id]
|
||||
print(" %s <-chop-- %s - work: %.1f" % [chopped_id, chop_def["ingredient"], chop_def["work"]])
|
||||
|
||||
|
||||
static func _print_rolling_recipes() -> void:
|
||||
for rolled_id in _rolling_map.keys():
|
||||
var roll_def = _rolling_map[rolled_id]
|
||||
print(" %s <-roll-- %s - work: %.1f" % [rolled_id, roll_def["ingredient"], roll_def["work"]])
|
||||
|
||||
|
||||
static func _print_augmenting_recipes() -> void:
|
||||
for target_id in _augmenting_map.keys():
|
||||
var augment_def = _augmenting_map[target_id]
|
||||
for ingredient_id in augment_def.keys():
|
||||
var attr_key = augment_def[ingredient_id]
|
||||
print(" %s <-augment-- %s adds attribute '%s'" % [target_id, ingredient_id, attr_key])
|
||||
|
||||
|
||||
static func _build_scene_paths() -> void:
|
||||
_scene_paths.clear()
|
||||
var items = _recipes.get("items", {})
|
||||
if typeof(items) != TYPE_DICTIONARY:
|
||||
return
|
||||
for item_id in items.keys():
|
||||
var item_def = items[item_id]
|
||||
if typeof(item_def) != TYPE_DICTIONARY:
|
||||
continue
|
||||
var scene_path = item_def.get("scene", "")
|
||||
if typeof(scene_path) == TYPE_STRING and not scene_path.is_empty():
|
||||
_scene_paths[str(item_id)] = scene_path
|
||||
|
||||
|
||||
# Reads strucure into _combining_map:
|
||||
# combining:
|
||||
# hamburger:
|
||||
# - [cooked_burger, burger_buns]
|
||||
static func _build_combining_map() -> void:
|
||||
_combining_map.clear()
|
||||
var combining = _recipes.get("combining", {})
|
||||
for result_id in combining.keys():
|
||||
var recipe_entries = _get_recipe_entries(combining[result_id])
|
||||
if recipe_entries.is_empty():
|
||||
push_error("RecipeManager: combining recipe '%s' must contain at least one valid recipe" % result_id)
|
||||
continue
|
||||
for recipe in recipe_entries:
|
||||
var first = StringName(recipe[0])
|
||||
var second = StringName(recipe[1])
|
||||
var key = _make_pair_key(first, second)
|
||||
_combining_map[key] = result_id
|
||||
|
||||
|
||||
static func _get_recipe_entries(recipe_value: Variant) -> Array:
|
||||
var recipes: Array = []
|
||||
if typeof(recipe_value) != TYPE_ARRAY:
|
||||
return recipes
|
||||
if recipe_value.is_empty():
|
||||
return recipes
|
||||
if typeof(recipe_value[0]) == TYPE_ARRAY:
|
||||
for recipe in recipe_value:
|
||||
if typeof(recipe) == TYPE_ARRAY and recipe.size() == 2:
|
||||
recipes.append(recipe)
|
||||
else:
|
||||
if recipe_value.size() == 2:
|
||||
recipes.append(recipe_value)
|
||||
return recipes
|
||||
|
||||
|
||||
static func _make_pair_key(first_id: StringName, second_id: StringName) -> StringName:
|
||||
if str(first_id) <= str(second_id):
|
||||
return StringName(str(first_id) + "|" + str(second_id))
|
||||
return StringName(str(second_id) + "|" + str(first_id))
|
||||
|
||||
|
||||
# Reads structure into _cooking_map:
|
||||
# cooking:
|
||||
# cooked_burger:
|
||||
# ingredient: raw_burger
|
||||
# time: 3
|
||||
# or allowing multiple recipes for the same cooked item:
|
||||
# charcoal:
|
||||
# - ingredient: cube
|
||||
# time: 1
|
||||
# - ingredient: some_other
|
||||
# time: 2
|
||||
static func _build_cooking_map() -> void:
|
||||
_cooking_map.clear()
|
||||
var cooking = _recipes.get("cooking", {})
|
||||
if typeof(cooking) != TYPE_DICTIONARY:
|
||||
return
|
||||
for cooked_id in cooking.keys():
|
||||
var cook_val = cooking[cooked_id]
|
||||
var entries: Array = []
|
||||
if typeof(cook_val) == TYPE_ARRAY:
|
||||
for entry in cook_val:
|
||||
if typeof(entry) != TYPE_DICTIONARY:
|
||||
push_error("RecipeManager: cooking recipe for '%s' contains a non-dictionary entry" % cooked_id)
|
||||
continue
|
||||
entries.append(entry)
|
||||
elif typeof(cook_val) == TYPE_DICTIONARY:
|
||||
entries.append(cook_val)
|
||||
else:
|
||||
push_error("RecipeManager: cooking recipe for '%s' must be a dictionary or array of dictionaries" % cooked_id)
|
||||
continue
|
||||
|
||||
var parsed_entries: Array = []
|
||||
for cook_def in entries:
|
||||
var ingredient = cook_def.get("ingredient", "")
|
||||
if typeof(ingredient) != TYPE_STRING or str(ingredient).is_empty():
|
||||
push_error("RecipeManager: cooking recipe '%s' missing a valid 'ingredient' field" % cooked_id)
|
||||
continue
|
||||
var time_val = cook_def.get("time", null)
|
||||
if typeof(time_val) != TYPE_INT and typeof(time_val) != TYPE_FLOAT:
|
||||
push_error("RecipeManager: cooking recipe '%s' missing a valid 'time' field" % cooked_id)
|
||||
continue
|
||||
parsed_entries.append({"ingredient": StringName(str(ingredient)), "time": float(time_val)})
|
||||
|
||||
if not parsed_entries.is_empty():
|
||||
_cooking_map[StringName(cooked_id)] = parsed_entries
|
||||
|
||||
|
||||
# Reads structure into _chopping_map:
|
||||
# chopping:
|
||||
# chopped_onion:
|
||||
# ingredient: onion
|
||||
# work: 2
|
||||
static func _build_chopping_map() -> void:
|
||||
_chopping_map.clear()
|
||||
var chopping = _recipes.get("chopping", {})
|
||||
if typeof(chopping) != TYPE_DICTIONARY:
|
||||
return
|
||||
for chopped_id in chopping.keys():
|
||||
var chop_def = chopping[chopped_id]
|
||||
if typeof(chop_def) != TYPE_DICTIONARY:
|
||||
push_error("RecipeManager: chopping recipe for '%s' must be a dictionary" % chopped_id)
|
||||
continue
|
||||
var ingredient = chop_def.get("ingredient", "")
|
||||
if typeof(ingredient) != TYPE_STRING or str(ingredient).is_empty():
|
||||
push_error("RecipeManager: chopping recipe '%s' missing a valid 'ingredient' field" % chopped_id)
|
||||
continue
|
||||
var work_val = chop_def.get("work", null)
|
||||
if typeof(work_val) != TYPE_INT and typeof(work_val) != TYPE_FLOAT:
|
||||
push_error("RecipeManager: chopping recipe '%s' missing a valid 'work' field" % chopped_id)
|
||||
continue
|
||||
# store chopped item -> { ingredient: StringName, work: float }
|
||||
_chopping_map[StringName(chopped_id)] = {"ingredient": StringName(str(ingredient)), "work": float(work_val)}
|
||||
|
||||
|
||||
# Reads structure into _rolling_map:
|
||||
# rolling:
|
||||
# pie_base:
|
||||
# ingredient: dough
|
||||
# work: 80
|
||||
static func _build_rolling_map() -> void:
|
||||
_rolling_map.clear()
|
||||
var rolling = _recipes.get("rolling", {})
|
||||
if typeof(rolling) != TYPE_DICTIONARY:
|
||||
return
|
||||
for rolled_id in rolling.keys():
|
||||
var roll_def = rolling[rolled_id]
|
||||
if typeof(roll_def) != TYPE_DICTIONARY:
|
||||
push_error("RecipeManager: rolling recipe for '%s' must be a dictionary" % rolled_id)
|
||||
continue
|
||||
var ingredient = roll_def.get("ingredient", "")
|
||||
if typeof(ingredient) != TYPE_STRING or str(ingredient).is_empty():
|
||||
push_error("RecipeManager: rolling recipe '%s' missing a valid 'ingredient' field" % rolled_id)
|
||||
continue
|
||||
var work_val = roll_def.get("work", null)
|
||||
if typeof(work_val) != TYPE_INT and typeof(work_val) != TYPE_FLOAT:
|
||||
push_error("RecipeManager: rolling recipe '%s' missing a valid 'work' field" % rolled_id)
|
||||
continue
|
||||
# store rolled item -> { ingredient: StringName, work: float }
|
||||
_rolling_map[StringName(rolled_id)] = {"ingredient": StringName(str(ingredient)), "work": float(work_val)}
|
||||
|
||||
|
||||
# Reads structure into _augmenting_map:
|
||||
# augmenting: # Process of adding an item onto another without changing it's type, but toggling attributes.
|
||||
# cooked_burger:
|
||||
# has_tomato: sliced_tomato
|
||||
# has_cheese: cheese
|
||||
static func _build_augmenting_map() -> void:
|
||||
_augmenting_map.clear()
|
||||
var augmenting = _recipes.get("augmenting", {})
|
||||
if typeof(augmenting) != TYPE_DICTIONARY:
|
||||
return
|
||||
for target_id in augmenting.keys():
|
||||
var augment_def = augmenting[target_id]
|
||||
if typeof(augment_def) != TYPE_DICTIONARY:
|
||||
push_error("RecipeManager: augmenting recipe for '%s' must be a dictionary" % target_id)
|
||||
continue
|
||||
var map: Dictionary = {}
|
||||
for attr_key in augment_def.keys():
|
||||
var ingredient = augment_def[attr_key]
|
||||
if typeof(ingredient) != TYPE_STRING or str(ingredient).is_empty():
|
||||
push_error("RecipeManager: augmenting recipe '%s' has invalid ingredient for '%s'" % [target_id, attr_key])
|
||||
continue
|
||||
# store ingredient -> attribute_key (e.g. sliced_tomato -> has_tomato)
|
||||
map[StringName(str(ingredient))] = StringName(str(attr_key))
|
||||
if not map.is_empty():
|
||||
_augmenting_map[StringName(target_id)] = map
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
uid://cuo88u56uqwuy
|
||||
@@ -1,68 +0,0 @@
|
||||
# RecipeManager
|
||||
|
||||
## Overview
|
||||
|
||||
`RecipeManager` is a static Godot class that centralizes recipe data for food item combinations and allows runtime lookup of result scenes.
|
||||
|
||||
The manager loads a single YAML file at startup: `res://recipes.yaml`, using the installed `addons/yaml` addon.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
- Load recipe definitions from `recipes.yaml`.
|
||||
- Provide symmetric lookups for two-item combining recipes so `A + B` and `B + A` map to the same result.
|
||||
- Resolve item IDs to their corresponding scene resources.
|
||||
- Print all loaded combining recipes when the application starts.
|
||||
|
||||
## Data structure in `recipes.yaml`
|
||||
|
||||
The recipe YAML file contains two top-level sections:
|
||||
|
||||
- `items`: maps item IDs to their scene path and optional metadata.
|
||||
- `combining`: maps result item IDs to one or more recipe pairs.
|
||||
|
||||
Example structure:
|
||||
|
||||
```yaml
|
||||
items:
|
||||
hamburger:
|
||||
scene: res://Items/hamburger.tscn
|
||||
type: meal
|
||||
burger_buns:
|
||||
scene: res://Items/BurgerBuns.tscn
|
||||
type: ingredient
|
||||
|
||||
combining:
|
||||
hamburger:
|
||||
- [cooked_burger, burger_buns]
|
||||
- [charcoal, charcoal]
|
||||
charcoal:
|
||||
- [cube, cube]
|
||||
```
|
||||
|
||||
### `items`
|
||||
|
||||
Each entry under `items` uses the item ID as the key.
|
||||
The manager reads the `scene` field for each item and uses it to resolve the packed scene for recipe results.
|
||||
|
||||
### `combining`
|
||||
|
||||
Each entry under `combining` represents a result item ID, and its value is a list of recipe pairs.
|
||||
Each recipe pair is a two-item array of ingredient IDs.
|
||||
This allows a single result item to have multiple valid recipes.
|
||||
|
||||
The manager normalizes each ingredient pair using a sorted key string internally, so lookups are symmetric.
|
||||
|
||||
## Runtime behavior
|
||||
|
||||
- `FoodItem.id` is used when combining items to look up the recipe result.
|
||||
- `RecipeManager.get_combination(first_id, second_id)` computes a canonical key for the pair and returns the resulting item's scene.
|
||||
- On startup, `main.gd` calls `RecipeManager.print_all_recipes()`, which logs each combining recipe in the form:
|
||||
|
||||
`ingredient_a + ingredient_b -> result_id`
|
||||
|
||||
## Notes
|
||||
|
||||
- `RecipeManager` prefers the installed YAML addon to parse `recipes.yaml` when it is available.
|
||||
- If the addon is unavailable in the current runtime, the manager falls back to a lightweight built-in parser for the simple `items`/`combining` file format.
|
||||
- Scene paths now live in the YAML data instead of being hardcoded in the manager.
|
||||
- Because `RecipeManager` is static, it can be used from any script without creating an instance.
|
||||
+47
-80
@@ -4,6 +4,7 @@
|
||||
[ext_resource type="PackedScene" uid="uid://j5s5wus7tuhb" path="res://XROrigin.tscn" id="2_o1b3t"]
|
||||
[ext_resource type="Material" uid="uid://cmia50cfqxxo4" path="res://Textures/dev_material_3d.tres" id="3_irf4n"]
|
||||
[ext_resource type="PackedScene" uid="uid://j7caslh27nor" path="res://Stations/Hob.tscn" id="4_ctden"]
|
||||
[ext_resource type="Script" uid="uid://cquqe4f1m1sw6" path="res://addons/godot-xr-tools/objects/snap_zone.gd" id="5_57ppd"]
|
||||
[ext_resource type="PackedScene" uid="uid://e4i6o5oriecx" path="res://Items/PickupCube.tscn" id="6_eoxxy"]
|
||||
[ext_resource type="Sky" uid="uid://c1abtpwhdm2d5" path="res://Textures/sky/NightSkyHDRI009_16K.tres" id="7_n8fyw"]
|
||||
[ext_resource type="PackedScene" uid="uid://c0lknik4noobs" path="res://Items/BurgerBuns.tscn" id="8_rraok"]
|
||||
@@ -11,11 +12,6 @@
|
||||
[ext_resource type="PackedScene" uid="uid://bp3v1jl8pctro" path="res://Containers/plate.tscn" id="10_ay2w6"]
|
||||
[ext_resource type="PackedScene" uid="uid://dpot5qie20vf6" path="res://Items/burger.tscn" id="11_qn1ku"]
|
||||
[ext_resource type="PackedScene" uid="uid://b3m2ag8g5rj4r" path="res://Items/hamburger.tscn" id="12_hfwnn"]
|
||||
[ext_resource type="PackedScene" uid="uid://dvrk268s7gkxh" path="res://Stations/sink.tscn" id="13_o1b3t"]
|
||||
[ext_resource type="PackedScene" uid="uid://cnjwtnhwh0i8q" path="res://Stations/dirt_station.tscn" id="14_irf4n"]
|
||||
[ext_resource type="PackedScene" uid="uid://cfc4ho67u4r5e" path="res://Items/cooked_burger.tscn" id="15_ctden"]
|
||||
[ext_resource type="PackedScene" uid="uid://efaec6ymgabo" path="res://Stations/Counter.tscn" id="15_di04w"]
|
||||
[ext_resource type="PackedScene" uid="uid://caf0xanmxbshy" path="res://Stations/table.tscn" id="16_di04w"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_vlqg6"]
|
||||
size = Vector3(5, 0.1, 5)
|
||||
@@ -24,6 +20,13 @@ size = Vector3(5, 0.1, 5)
|
||||
material = ExtResource("3_irf4n")
|
||||
size = Vector3(5, 0.1, 5)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_24d3s"]
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_i5j2u"]
|
||||
|
||||
[sub_resource type="SphereShape3D" id="SphereShape3D_dlkho"]
|
||||
radius = 0.3
|
||||
|
||||
[sub_resource type="Environment" id="Environment_bvwq1"]
|
||||
background_mode = 2
|
||||
sky = ExtResource("7_n8fyw")
|
||||
@@ -37,7 +40,6 @@ sdfgi_enabled = true
|
||||
script = ExtResource("1_57ppd")
|
||||
|
||||
[node name="XROrigin3D" parent="." unique_id=2055526621 instance=ExtResource("2_o1b3t")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.36171648, 0, 0.81835127)
|
||||
|
||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1376644384]
|
||||
transform = Transform3D(-0.8660254, -0.43301278, 0.24999999, 0.3022632, -0.05510214, 0.9516306, -0.39829266, 0.8997021, 0.17860365, 0, 0, 0)
|
||||
@@ -55,6 +57,32 @@ mesh = SubResource("BoxMesh_24d3s")
|
||||
[node name="Hob" parent="." unique_id=1687971542 instance=ExtResource("4_ctden")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.29336345, 0.8981018, -1.484)
|
||||
|
||||
[node name="Counter" type="StaticBody3D" parent="." unique_id=1487893288 groups=["station"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.73186195, 0.8981018, -1.484)
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Counter" unique_id=692568688]
|
||||
shape = SubResource("BoxShape3D_24d3s")
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="Counter/CollisionShape3D" unique_id=127773521]
|
||||
mesh = SubResource("BoxMesh_i5j2u")
|
||||
|
||||
[node name="Label3D" type="Label3D" parent="Counter/CollisionShape3D" unique_id=149978729]
|
||||
transform = Transform3D(0.9999992, 0, 0, 0, 0.99999946, 0, 0, 0, 0.9999992, 0.002797456, 0.6309581, -0.2812457)
|
||||
text = "Counter"
|
||||
|
||||
[node name="XRToolsSnapZone" type="Area3D" parent="Counter" unique_id=1647380272 groups=["station"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5270121, 0)
|
||||
collision_layer = 65536
|
||||
collision_mask = 65536
|
||||
script = ExtResource("5_57ppd")
|
||||
snap_mode = 1
|
||||
initial_object = NodePath("../../Plate")
|
||||
metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
||||
|
||||
[node name="CollisionShape3D2" type="CollisionShape3D" parent="Counter/XRToolsSnapZone" unique_id=969340130]
|
||||
transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, -0.000975132, 0)
|
||||
shape = SubResource("SphereShape3D_dlkho")
|
||||
|
||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1177077250]
|
||||
environment = SubResource("Environment_bvwq1")
|
||||
|
||||
@@ -65,101 +93,40 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.8162017, 1.6081157, -1.471
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.832131, 0.40028095, -1.4813508)
|
||||
|
||||
[node name="Plate" parent="." unique_id=190487773 instance=ExtResource("10_ay2w6")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.5717233, 1.5454081, 1.5373346)
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.72025335, 1.4458435, -1.5107731)
|
||||
|
||||
[node name="burger" parent="." unique_id=1417604760 instance=ExtResource("11_qn1ku")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.6888188, 1.4195822, -1.7110313)
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.1439226, 1.4195822, -1.2004558)
|
||||
|
||||
[node name="burger2" parent="." unique_id=1584460510 instance=ExtResource("11_qn1ku")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.6931299, 1.5300478, -1.71225)
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.1396115, 1.5300478, -1.2016745)
|
||||
|
||||
[node name="burger3" parent="." unique_id=1884095916 instance=ExtResource("11_qn1ku")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.69027674, 1.4969791, -1.7210286)
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.1424646, 1.4969791, -1.210453)
|
||||
|
||||
[node name="burger4" parent="." unique_id=1844818864 instance=ExtResource("11_qn1ku")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.69134104, 1.4543622, -1.7210286)
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.1414003, 1.4543622, -1.210453)
|
||||
|
||||
[node name="Hamburger" parent="." unique_id=761091445 instance=ExtResource("12_hfwnn")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.9352558, 1.623975, 1.2447833)
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.18456237, 1.5438088, -1.3716147)
|
||||
|
||||
[node name="PickableObject" parent="." unique_id=1675596942 instance=ExtResource("6_eoxxy")]
|
||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, 0.6225724, 1.4792972, -1.0473135)
|
||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, -0.042440414, 1.4792972, -1.0473135)
|
||||
|
||||
[node name="PickableObject2" parent="." unique_id=713087634 instance=ExtResource("6_eoxxy")]
|
||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, -5.293956e-23, -2.6077032e-08, 1.4901161e-08, 1, 0.6359743, 1.4639391, -1.1673055)
|
||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, -5.293956e-23, -2.6077032e-08, 1.4901161e-08, 1, -0.029038578, 1.4639391, -1.1673055)
|
||||
|
||||
[node name="PickableObject3" parent="." unique_id=879935619 instance=ExtResource("6_eoxxy")]
|
||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, 0.5142721, 1.4792972, -1.0473135)
|
||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, -0.15074077, 1.4792972, -1.0473135)
|
||||
|
||||
[node name="PickableObject4" parent="." unique_id=198541902 instance=ExtResource("6_eoxxy")]
|
||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, -5.293956e-23, -2.6077032e-08, 1.4901161e-08, 1, 0.5276739, 1.4639391, -1.1673055)
|
||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, -5.293956e-23, -2.6077032e-08, 1.4901161e-08, 1, -0.13733894, 1.4639391, -1.1673055)
|
||||
|
||||
[node name="Hamburger2" parent="." unique_id=1349934579 instance=ExtResource("12_hfwnn")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.9857153, 1.5329368, 0.9472374)
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.15603885, 1.5329368, -1.6386203)
|
||||
|
||||
[node name="PickableObject5" parent="." unique_id=1021333509 instance=ExtResource("6_eoxxy")]
|
||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, 0.73287535, 1.4792972, -1.0473135)
|
||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, 0.06786254, 1.4792972, -1.0473135)
|
||||
|
||||
[node name="PickableObject6" parent="." unique_id=1268843669 instance=ExtResource("6_eoxxy")]
|
||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, -5.293956e-23, -2.6077032e-08, 1.4901161e-08, 1, 0.7462772, 1.4639391, -1.1673055)
|
||||
|
||||
[node name="Sink" parent="." unique_id=2055277359 instance=ExtResource("13_o1b3t")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.3140475, 0.9061539, -1.4941733)
|
||||
|
||||
[node name="DirtStation" parent="." unique_id=160842153 instance=ExtResource("14_irf4n")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.0864775, 0.8981018, -1.244947)
|
||||
|
||||
[node name="Plate2" parent="." unique_id=356790445 instance=ExtResource("10_ay2w6")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.5730225, 1.499024, 0.59790254)
|
||||
|
||||
[node name="CookedBurger" parent="." unique_id=1127807542 instance=ExtResource("15_ctden")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.30671906, 1.4131018, -1.0928738)
|
||||
|
||||
[node name="CookedBurger2" parent="." unique_id=160088590 instance=ExtResource("15_ctden")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.30671906, 1.4496142, -1.0928738)
|
||||
|
||||
[node name="CookedBurger3" parent="." unique_id=992183367 instance=ExtResource("15_ctden")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.3344773, 1.4398065, -0.7096845)
|
||||
|
||||
[node name="CookedBurger4" parent="." unique_id=1837607086 instance=ExtResource("15_ctden")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.30671906, 1.525444, -1.0928738)
|
||||
|
||||
[node name="BurgerBuns2" parent="." unique_id=1210358958 instance=ExtResource("8_rraok")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.3596323, 1.4110342, -0.22482127)
|
||||
|
||||
[node name="PickableObject7" parent="." unique_id=641653545 instance=ExtResource("6_eoxxy")]
|
||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, -1.3556751, 1.4792972, 0.28881657)
|
||||
|
||||
[node name="PickableObject8" parent="." unique_id=1733819363 instance=ExtResource("6_eoxxy")]
|
||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, -5.293956e-23, -2.6077032e-08, 1.4901161e-08, 1, -1.3422732, 1.4639391, 0.16882455)
|
||||
|
||||
[node name="PickableObject9" parent="." unique_id=12419746 instance=ExtResource("6_eoxxy")]
|
||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, -1.4639754, 1.4792972, 0.28881657)
|
||||
|
||||
[node name="PickableObject10" parent="." unique_id=313160217 instance=ExtResource("6_eoxxy")]
|
||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, -5.293956e-23, -2.6077032e-08, 1.4901161e-08, 1, -1.4505737, 1.4639391, 0.16882455)
|
||||
|
||||
[node name="Counter" parent="." unique_id=1487893288 instance=ExtResource("15_di04w")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.7124918, 0.90304357, -1.4886917)
|
||||
|
||||
[node name="Counter2" parent="." unique_id=368890752 instance=ExtResource("15_di04w")]
|
||||
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.7648025, 0.90304357, -0.4458799)
|
||||
|
||||
[node name="Counter3" parent="." unique_id=1498817646 instance=ExtResource("15_di04w")]
|
||||
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.7648025, 0.90304357, 0.55367994)
|
||||
|
||||
[node name="Counter4" parent="." unique_id=906748771 instance=ExtResource("15_di04w")]
|
||||
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.7648025, 0.90304357, 1.5539298)
|
||||
|
||||
[node name="Table" parent="." unique_id=1863572470 instance=ExtResource("16_di04w")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.50244665, 0.9030438, -0.20709503)
|
||||
initial_thinking_time = 8.0
|
||||
initial_primary_time = 40.0
|
||||
initial_friend_time = 3.0
|
||||
initial_eating_time = 3.0
|
||||
|
||||
[node name="Hamburger3" parent="." unique_id=369573848 instance=ExtResource("12_hfwnn")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.7201865, 1.5329367, 0.14773655)
|
||||
|
||||
[node name="Plate3" parent="." unique_id=2004250522 instance=ExtResource("10_ay2w6")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.5818124, 1.499024, -0.4634577)
|
||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, -5.293956e-23, -2.6077032e-08, 1.4901161e-08, 1, 0.08126438, 1.4639391, -1.1673055)
|
||||
|
||||
+11
-51
@@ -4,71 +4,31 @@
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_24d3s"]
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_i5j2u"]
|
||||
|
||||
[sub_resource type="SphereShape3D" id="SphereShape3D_dlkho"]
|
||||
radius = 0.3
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_24d3s"]
|
||||
albedo_color = Color(0.49, 0.3854667, 0.29400003, 1)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_vlqg6"]
|
||||
albedo_color = Color(1.1759251, 0.741115, 0.5692133, 1)
|
||||
|
||||
[node name="Counter" type="StaticBody3D" unique_id=1487893288 groups=["station"]]
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=692568688]
|
||||
shape = SubResource("BoxShape3D_24d3s")
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="CollisionShape3D" unique_id=127773521]
|
||||
mesh = SubResource("BoxMesh_i5j2u")
|
||||
|
||||
[node name="Label3D" type="Label3D" parent="CollisionShape3D" unique_id=149978729]
|
||||
transform = Transform3D(0.9999992, 0, 0, 0, 0.99999946, 0, 0, 0, 0.9999992, 0.002797456, 0.6309581, -0.2812457)
|
||||
text = "Counter"
|
||||
|
||||
[node name="XRToolsSnapZone" type="Area3D" parent="." unique_id=1647380272 groups=["station"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5608841, 0)
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5270121, 0)
|
||||
collision_layer = 65536
|
||||
collision_mask = 65536
|
||||
script = ExtResource("1_dlkho")
|
||||
snap_mode = 1
|
||||
snap_mode = 0
|
||||
metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
||||
|
||||
[node name="CollisionShape3D2" type="CollisionShape3D" parent="XRToolsSnapZone" unique_id=969340130]
|
||||
transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, -0.000975132, 0)
|
||||
shape = SubResource("SphereShape3D_dlkho")
|
||||
|
||||
[node name="CSGCombiner3D" type="CSGCombiner3D" parent="." unique_id=451757283]
|
||||
|
||||
[node name="CSGBox3D" type="CSGBox3D" parent="CSGCombiner3D" unique_id=305755330]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.045776367, 0)
|
||||
size = Vector3(1, 0.90844727, 1)
|
||||
material = SubResource("StandardMaterial3D_24d3s")
|
||||
|
||||
[node name="CSGBox3D2" type="CSGBox3D" parent="CSGCombiner3D" unique_id=43663195]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.45385742, 0.025390625)
|
||||
size = Vector3(1, 0.092285156, 1.0507813)
|
||||
|
||||
[node name="CSGBox3D3" type="CSGBox3D" parent="CSGCombiner3D" unique_id=1210567516]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5040283, -0.46044922)
|
||||
size = Vector3(1, 0.19262695, 0.07910156)
|
||||
|
||||
[node name="CSGBox3D4" type="CSGBox3D" parent="CSGCombiner3D" unique_id=1533996708]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.25531006, -0.054498255, 0.5233302)
|
||||
size = Vector3(0.48937988, 0.90844727, 0.023986816)
|
||||
material = SubResource("StandardMaterial3D_24d3s")
|
||||
|
||||
[node name="CSGBox3D5" type="CSGBox3D" parent="CSGCombiner3D" unique_id=645225767]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.24944723, -0.054498255, 0.5233302)
|
||||
size = Vector3(0.48937988, 0.90844727, 0.023986816)
|
||||
material = SubResource("StandardMaterial3D_24d3s")
|
||||
|
||||
[node name="CSGSphere3D" type="CSGSphere3D" parent="CSGCombiner3D" unique_id=1785057925]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.057649717, 0.12459713, 0.55428153)
|
||||
radius = 0.025492515
|
||||
|
||||
[node name="CSGSphere3D2" type="CSGSphere3D" parent="CSGCombiner3D" unique_id=1224446005]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.060837455, 0.12459713, 0.55428153)
|
||||
radius = 0.025492515
|
||||
|
||||
[node name="CSGBox3D6" type="CSGBox3D" parent="CSGCombiner3D" unique_id=187377667]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.060742255, 0.4885016, 0.0011152327)
|
||||
size = Vector3(0.56365967, 0.092285156, 0.42160034)
|
||||
material = SubResource("StandardMaterial3D_vlqg6")
|
||||
|
||||
[node name="CSGBox3D6" type="CSGBox3D" parent="CSGCombiner3D/CSGBox3D6" unique_id=1146824846]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.19631815, 0.01924485, -0.0039587077)
|
||||
operation = 2
|
||||
size = Vector3(0.06542969, 0.092285156, 0.26757813)
|
||||
|
||||
+23
-78
@@ -1,105 +1,50 @@
|
||||
[gd_scene format=3 uid="uid://j7caslh27nor"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bsn8vhv5adxdo" path="res://Stations/hob.gd" id="1_7jc4g"]
|
||||
[ext_resource type="Texture2D" uid="uid://b7n8d4krwbcvl" path="res://Textures/5.png" id="2_0sasq"]
|
||||
[ext_resource type="Script" uid="uid://cquqe4f1m1sw6" path="res://addons/godot-xr-tools/objects/snap_zone.gd" id="2_er1dp"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_24d3s"]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_m5xe2"]
|
||||
albedo_texture = ExtResource("2_0sasq")
|
||||
uv1_triplanar = true
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_i5j2u"]
|
||||
material = SubResource("StandardMaterial3D_m5xe2")
|
||||
|
||||
[sub_resource type="SphereShape3D" id="SphereShape3D_vlqg6"]
|
||||
radius = 0.3
|
||||
|
||||
[sub_resource type="SceneReplicationConfig" id="Repl_hob"]
|
||||
properties/0/path = NodePath(".:progress")
|
||||
properties/0/spawn = true
|
||||
properties/0/replication_mode = 1
|
||||
|
||||
[node name="Hob" type="StaticBody3D" unique_id=1687971542 groups=["station"]]
|
||||
script = ExtResource("1_7jc4g")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1283846023]
|
||||
shape = SubResource("BoxShape3D_24d3s")
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="CollisionShape3D" unique_id=1454284058]
|
||||
mesh = SubResource("BoxMesh_i5j2u")
|
||||
|
||||
[node name="Label3D" type="Label3D" parent="CollisionShape3D" unique_id=757148654]
|
||||
transform = Transform3D(0.9999992, 0, 0, 0, 0.99999946, 0, 0, 0, 0.9999992, 0.002797456, 0.6309581, -0.2812457)
|
||||
text = "Hob"
|
||||
|
||||
[node name="XRToolsSnapZone" type="Area3D" parent="." unique_id=538157739]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5270121, 0)
|
||||
collision_layer = 65536
|
||||
collision_mask = 65536
|
||||
script = ExtResource("2_er1dp")
|
||||
snap_mode = 1
|
||||
snap_mode = 0
|
||||
metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
||||
|
||||
[node name="CollisionShape3D2" type="CollisionShape3D" parent="XRToolsSnapZone" unique_id=370920392]
|
||||
transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, -0.000975132, 0)
|
||||
shape = SubResource("SphereShape3D_vlqg6")
|
||||
|
||||
[node name="CSGCombiner3D" type="CSGCombiner3D" parent="." unique_id=70901698]
|
||||
|
||||
[node name="CSGBox3D" type="CSGBox3D" parent="CSGCombiner3D" unique_id=1111505172]
|
||||
|
||||
[node name="CSGCylinder3D" type="CSGCylinder3D" parent="CSGCombiner3D/CSGBox3D" unique_id=1338463939]
|
||||
transform = Transform3D(0.8943019, 0, 0, 0, 0.8943019, 0, 0, 0, 0.8943019, 0, 0.5103538, -7.827557e-05)
|
||||
operation = 2
|
||||
radius = 0.25976563
|
||||
height = 0.08898926
|
||||
sides = 32
|
||||
|
||||
[node name="CSGTorus3D3" type="CSGTorus3D" parent="CSGCombiner3D" unique_id=666891341]
|
||||
transform = Transform3D(0.8943019, 0, 0, 0, 0.8943019, 0, 0, 0, 0.8943019, 0, 0.5003869, -7.827557e-05)
|
||||
inner_radius = 0.09153879
|
||||
outer_radius = 0.120084204
|
||||
sides = 32
|
||||
|
||||
[node name="CSGTorus3D" type="CSGTorus3D" parent="CSGCombiner3D" unique_id=1901978664]
|
||||
transform = Transform3D(0.8943019, 0, 0, 0, 0.8943019, 0, 0, 0, 0.8943019, 0, 0.5003869, -7.827557e-05)
|
||||
inner_radius = 0.20426142
|
||||
outer_radius = 0.2321898
|
||||
sides = 32
|
||||
|
||||
[node name="CSGTorus3D2" type="CSGTorus3D" parent="CSGCombiner3D" unique_id=24541894]
|
||||
transform = Transform3D(0.8943019, 0, 0, 0, 0.8943019, 0, 0, 0, 0.8943019, 0, 0.5003869, -7.827557e-05)
|
||||
inner_radius = 0.15036294
|
||||
outer_radius = 0.17592031
|
||||
sides = 32
|
||||
|
||||
[node name="CSGCylinder3D" type="CSGCylinder3D" parent="CSGCombiner3D" unique_id=1107239311]
|
||||
transform = Transform3D(0.8943019, 0, 0, 0, 0.8943019, 0, 0, 0, 0.8943019, 0, 0.4498226, -7.827557e-05)
|
||||
radius = 0.071777344
|
||||
height = 0.09637451
|
||||
sides = 32
|
||||
|
||||
[node name="CSGCylinder3D2" type="CSGCylinder3D" parent="CSGCombiner3D" unique_id=435540815]
|
||||
transform = Transform3D(0.8943019, 0, 0, 0, -3.9091177e-08, 0.8943019, 0, -0.8943019, -3.9091177e-08, 0, 0.48668858, -0.0020651226)
|
||||
radius = 0.008
|
||||
height = 0.44487303
|
||||
|
||||
[node name="CSGCylinder3D3" type="CSGCylinder3D" parent="CSGCombiner3D" unique_id=878171801]
|
||||
transform = Transform3D(-3.9091177e-08, -0.8943019, -3.9091177e-08, 0, -3.9091177e-08, 0.8943019, -0.8943019, 3.9091177e-08, 1.7087296e-15, 0, 0.48668858, -0.0020651226)
|
||||
radius = 0.008
|
||||
height = 0.44487303
|
||||
|
||||
[node name="CSGCylinder3D4" type="CSGCylinder3D" parent="CSGCombiner3D" unique_id=1619646436]
|
||||
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, -0.26920843, 0.42728558, 0.4856163)
|
||||
radius = 0.044433594
|
||||
height = 0.10932617
|
||||
|
||||
[node name="CSGCylinder3D5" type="CSGCylinder3D" parent="CSGCombiner3D" unique_id=1748405277]
|
||||
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0.1318646, 0.42728558, 0.4856163)
|
||||
radius = 0.044433594
|
||||
height = 0.10932617
|
||||
|
||||
[node name="CSGCylinder3D6" type="CSGCylinder3D" parent="CSGCombiner3D" unique_id=624102144]
|
||||
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0.25760216, 0.42728558, 0.4856163)
|
||||
radius = 0.044433594
|
||||
height = 0.10932617
|
||||
|
||||
[node name="CSGBox3D2" type="CSGBox3D" parent="CSGCombiner3D" unique_id=465340362]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.039733887, -0.07685089, 0.47770447)
|
||||
operation = 2
|
||||
size = Vector3(1.0999756, 0.8462982, 0.072387695)
|
||||
|
||||
[node name="CSGBox3D3" type="CSGBox3D" parent="CSGCombiner3D" unique_id=1813079759]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.003479004, -0.08253479, 0.4977833)
|
||||
size = Vector3(0.9842529, 0.8349304, 0.072387695)
|
||||
|
||||
[node name="CSGBox3D4" type="CSGBox3D" parent="CSGCombiner3D" unique_id=1978186019]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.003479004, -0.45215607, 0.46387827)
|
||||
size = Vector3(0.9842529, 0.095687866, 0.14019775)
|
||||
|
||||
[node name="CSGBox3D5" type="CSGBox3D" parent="CSGCombiner3D" unique_id=1423530445]
|
||||
transform = Transform3D(0.94465846, 0, 0, 0, 0.94465846, 0, 0, 0, 0.94465846, -0.003479004, -0.08253479, 0.5616675)
|
||||
operation = 2
|
||||
size = Vector3(0.9842529, 0.8349304, 0.072387695)
|
||||
[node name="ProgressSync" type="MultiplayerSynchronizer" parent="."]
|
||||
replication_config = SubResource("Repl_hob")
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
extends StaticBody3D
|
||||
|
||||
@onready var snap_zone: XRToolsSnapZone = $XRToolsSnapZone
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready() -> void:
|
||||
snap_zone.has_picked_up.connect(_makeDirty)
|
||||
|
||||
func _makeDirty(item) -> void:
|
||||
var plate: PlateController = item.get_node_or_null("PlateController") as PlateController
|
||||
if not plate.is_dirty:
|
||||
plate.is_dirty = true
|
||||
print("Dirt Station could not find Dirty in: ", item)
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
uid://q7tpwmrbmu1m
|
||||
@@ -1,43 +0,0 @@
|
||||
[gd_scene format=3 uid="uid://cnjwtnhwh0i8q"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://q7tpwmrbmu1m" path="res://Stations/dirt_station.gd" id="1_hc1d4"]
|
||||
[ext_resource type="Script" uid="uid://cquqe4f1m1sw6" path="res://addons/godot-xr-tools/objects/snap_zone.gd" id="2_v0ytd"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_24d3s"]
|
||||
size = Vector3(0.5, 1, 0.5)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_qn1ku"]
|
||||
albedo_color = Color(0.45262197, 0.26770625, 0.2601587, 1)
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_ay2w6"]
|
||||
material = SubResource("StandardMaterial3D_qn1ku")
|
||||
size = Vector3(0.5, 1, 0.5)
|
||||
|
||||
[sub_resource type="SphereShape3D" id="SphereShape3D_dlkho"]
|
||||
radius = 0.3
|
||||
|
||||
[node name="DirtStation" type="StaticBody3D" unique_id=160842153 groups=["station"]]
|
||||
script = ExtResource("1_hc1d4")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1400366312]
|
||||
shape = SubResource("BoxShape3D_24d3s")
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="CollisionShape3D" unique_id=55726639]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.008318901, 0, -0.022509098)
|
||||
mesh = SubResource("BoxMesh_ay2w6")
|
||||
|
||||
[node name="Label3D" type="Label3D" parent="CollisionShape3D" unique_id=1251766743]
|
||||
transform = Transform3D(0.9999992, 0, 0, 0, 0.99999946, 0, 0, 0, 0.9999992, 0.002797456, 0.6309581, -0.2812457)
|
||||
text = "Dirt"
|
||||
|
||||
[node name="XRToolsSnapZone" type="Area3D" parent="." unique_id=333161027 groups=["station"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5270121, 0)
|
||||
collision_layer = 65536
|
||||
collision_mask = 65536
|
||||
script = ExtResource("2_v0ytd")
|
||||
snap_mode = 1
|
||||
metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
||||
|
||||
[node name="CollisionShape3D2" type="CollisionShape3D" parent="XRToolsSnapZone" unique_id=1398712192]
|
||||
transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, -0.000975132, 0)
|
||||
shape = SubResource("SphereShape3D_dlkho")
|
||||
+42
-48
@@ -1,68 +1,62 @@
|
||||
extends StaticBody3D
|
||||
|
||||
const RECIPE_MANAGER = preload("res://RecipeManager.gd")
|
||||
## Cooking station. The cook timer and item conversion run only on the machine
|
||||
## that owns world logic (server or offline host); `progress` is replicated to
|
||||
## clients for UI via a MultiplayerSynchronizer. This is the reference
|
||||
## implementation of the reusable "station work-progress" pattern: a server-owned
|
||||
## progress value advanced by an input source (here, a timer), which converts the
|
||||
## held item once it reaches the threshold and spawns the result over the network.
|
||||
|
||||
@export var cook_speed = 1
|
||||
@export var cook_speed := 1.0
|
||||
@onready var snap_zone: XRToolsSnapZone = $XRToolsSnapZone
|
||||
|
||||
var time_cooked = 0
|
||||
var cooking_result = null
|
||||
var cooking_result_time = 0
|
||||
var item = null
|
||||
var progress: float = 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
snap_zone.has_picked_up.connect(_on_object_picked_up)
|
||||
snap_zone.has_dropped.connect(_on_object_dropped)
|
||||
|
||||
func _on_object_picked_up(_item) -> void:
|
||||
print("Hob: object picked up: ", _item)
|
||||
|
||||
# Find the CookableItem in held object
|
||||
var _food_item = _item.get_node_or_null("FoodItem") as FoodItem
|
||||
if not _food_item:
|
||||
print("Hob: held object is not a FoodItem")
|
||||
return
|
||||
var result = RECIPE_MANAGER.get_cooking_result(_food_item.id)
|
||||
if not result:
|
||||
print("Hob: held a FoodItem that is not cookable, id: ", _food_item.id)
|
||||
return
|
||||
|
||||
cooking_result = result
|
||||
cooking_result_time = RECIPE_MANAGER.get_cooking_time(_food_item.id)
|
||||
if snap_zone.initial_object:
|
||||
item = snap_zone.initial_object
|
||||
|
||||
|
||||
func _on_object_dropped() -> void:
|
||||
print("Hob: object drop ")
|
||||
time_cooked = 0
|
||||
cooking_result = null
|
||||
cooking_result_time = 0
|
||||
|
||||
|
||||
func convert_held_to_item(_item: PackedScene) -> void:
|
||||
print("Hob converting ", _item)
|
||||
|
||||
func convert_item(cookable: CookableItem) -> void:
|
||||
var old_pickable = snap_zone.picked_up_object
|
||||
if not old_pickable:
|
||||
push_warning("Hob finished cooking _item, but snap zone is missing its reference")
|
||||
push_warning("Hob finished cooking item, but snap zone is missing its reference")
|
||||
return
|
||||
|
||||
# Create new item as child of root in the same position
|
||||
var original_transform = old_pickable.global_transform
|
||||
var new_scene_instance = _item.instantiate()
|
||||
get_tree().get_root().add_child(new_scene_instance)
|
||||
new_scene_instance.global_transform = original_transform
|
||||
var original_transform: Transform3D = old_pickable.global_transform
|
||||
|
||||
# Drop and free the old item and pick up the new one
|
||||
print("Hob freeing old_pickable ", old_pickable)
|
||||
# Spawn the result over the network (server) or locally (offline).
|
||||
var result := NetworkManager.spawn_item(cookable.turns_into.resource_path, original_transform)
|
||||
|
||||
# Remove the cooked item (despawn replicates for spawner-managed items).
|
||||
snap_zone.drop_object()
|
||||
old_pickable.queue_free()
|
||||
snap_zone.pick_up_object(new_scene_instance)
|
||||
|
||||
# Snap the result into the now-empty zone.
|
||||
if result:
|
||||
snap_zone.pick_up_object(result)
|
||||
|
||||
|
||||
# If cooking something, progress cooking, when done, convert item
|
||||
func _process(delta: float) -> void:
|
||||
if cooking_result:
|
||||
time_cooked += cook_speed * delta
|
||||
if time_cooked >= cooking_result_time:
|
||||
time_cooked = 0
|
||||
convert_held_to_item(cooking_result)
|
||||
# Only the world owner runs cooking logic; clients receive `progress` + the
|
||||
# converted item via replication.
|
||||
if not NetworkManager.owns_world():
|
||||
return
|
||||
|
||||
# Find the CookableItem in the held object.
|
||||
var cookable = null
|
||||
if snap_zone.picked_up_object:
|
||||
var matches = snap_zone.picked_up_object.get_children().filter(func(c): return c is CookableItem)
|
||||
if matches.size() > 0:
|
||||
cookable = matches[0]
|
||||
|
||||
# Advance progress and convert once cooked.
|
||||
if cookable:
|
||||
progress += cook_speed * delta
|
||||
if progress >= cookable.cooking_time:
|
||||
progress = 0.0
|
||||
convert_item(cookable)
|
||||
else:
|
||||
progress = 0.0
|
||||
|
||||
@@ -13,10 +13,10 @@ func _ready() -> void:
|
||||
|
||||
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
||||
func _process(delta: float) -> void:
|
||||
# Only the world owner dispenses, so every client doesn't spawn its own copy.
|
||||
if not NetworkManager.owns_world():
|
||||
return
|
||||
if not snap_zone.picked_up_object:
|
||||
var new_item = item_scene.instantiate()
|
||||
|
||||
# Add the new item to the hob's parent (so it lives in the main world space)
|
||||
get_parent().add_child(new_item)
|
||||
new_item.global_transform = snap_zone.transform
|
||||
snap_zone.pick_up_object(new_item)
|
||||
var new_item := NetworkManager.spawn_item(item_scene.resource_path, snap_zone.global_transform)
|
||||
if new_item:
|
||||
snap_zone.pick_up_object(new_item)
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
extends StaticBody3D
|
||||
|
||||
const plate_wash_time: float = 3.0
|
||||
@export var plate_scene : PackedScene
|
||||
@export_range(0.0, 10.0, 0.1) var wash_speed: float = 1.0
|
||||
|
||||
@onready var snap_zone: XRToolsSnapZone = $XRToolsSnapZone
|
||||
|
||||
var progress: float = 0.0
|
||||
|
||||
func _ready() -> void:
|
||||
if not plate_scene:
|
||||
push_error("Sink is missing reference to plate scene")
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
var item = snap_zone.picked_up_object
|
||||
|
||||
if not item:
|
||||
progress = 0
|
||||
return # Exit early since there's nothing to process
|
||||
|
||||
|
||||
var plate: PlateController = item.get_node_or_null("PlateController") as PlateController
|
||||
if plate:
|
||||
# Not dirty? Nothing to wash
|
||||
if not plate.is_dirty:
|
||||
return
|
||||
|
||||
# Washing logic using the encapsulated property
|
||||
if progress >= plate_wash_time:
|
||||
plate.is_dirty = false # This automatically updates visibility and the container state!
|
||||
print("Sink washing complete!")
|
||||
else:
|
||||
progress += wash_speed * delta
|
||||
print("Sink washing plate: ", progress)
|
||||
|
||||
# Sink empty, reset progress
|
||||
if not snap_zone.picked_up_object:
|
||||
progress = 0
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
uid://byh5j25mwt3oc
|
||||
@@ -1,77 +0,0 @@
|
||||
[gd_scene format=3 uid="uid://dvrk268s7gkxh"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://byh5j25mwt3oc" path="res://Stations/sink.gd" id="1_7hh4b"]
|
||||
[ext_resource type="Script" uid="uid://cquqe4f1m1sw6" path="res://addons/godot-xr-tools/objects/snap_zone.gd" id="2_3ocod"]
|
||||
[ext_resource type="PackedScene" uid="uid://bp3v1jl8pctro" path="res://Containers/plate.tscn" id="2_ai6d4"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_24d3s"]
|
||||
|
||||
[sub_resource type="SphereShape3D" id="SphereShape3D_dlkho"]
|
||||
radius = 0.3
|
||||
|
||||
[node name="Sink" type="StaticBody3D" unique_id=2055277359 groups=["station"]]
|
||||
script = ExtResource("1_7hh4b")
|
||||
plate_scene = ExtResource("2_ai6d4")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=964735164]
|
||||
shape = SubResource("BoxShape3D_24d3s")
|
||||
|
||||
[node name="XRToolsSnapZone" type="Area3D" parent="." unique_id=1762550988 groups=["station"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5163779, 0)
|
||||
collision_layer = 65536
|
||||
collision_mask = 65536
|
||||
script = ExtResource("2_3ocod")
|
||||
snap_mode = 1
|
||||
metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
||||
|
||||
[node name="CollisionShape3D2" type="CollisionShape3D" parent="XRToolsSnapZone" unique_id=860566355]
|
||||
transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, -0.000975132, 0)
|
||||
shape = SubResource("SphereShape3D_dlkho")
|
||||
|
||||
[node name="CSGCombiner3D" type="CSGCombiner3D" parent="." unique_id=311064646]
|
||||
|
||||
[node name="CSGBox3D" type="CSGBox3D" parent="CSGCombiner3D" unique_id=1147823280]
|
||||
|
||||
[node name="CSGSphere3D" type="CSGSphere3D" parent="CSGCombiner3D" unique_id=2129615695]
|
||||
transform = Transform3D(1.1506689, 0, 0, 0, 2.334253, 0, 0, 0, 1, 0, 1.2831618, 0.06031096)
|
||||
operation = 2
|
||||
radial_segments = 20
|
||||
|
||||
[node name="CSGTorus3D" type="CSGTorus3D" parent="CSGCombiner3D" unique_id=2055166617]
|
||||
transform = Transform3D(-4.371139e-08, -1, 0, 1, -4.371139e-08, 0, 0, 0, 0.84928787, 0, 0.5107032, -0.20686007)
|
||||
inner_radius = 0.2
|
||||
outer_radius = 0.3
|
||||
sides = 16
|
||||
|
||||
[node name="CSGBox3D" type="CSGBox3D" parent="CSGCombiner3D/CSGTorus3D" unique_id=1722820789]
|
||||
transform = Transform3D(0.5128697, 0, 0, 0, 0.3125893, 0, 0, 0, 1, -0.29027426, 2.3841858e-07, 0)
|
||||
operation = 2
|
||||
|
||||
[node name="CSGBox3D2" type="CSGBox3D" parent="CSGCombiner3D/CSGTorus3D" unique_id=1812160684]
|
||||
transform = Transform3D(0.7700283, 1.4431469e-09, 0.15563977, 3.974847e-09, 0.3125893, -6.8032304e-09, -0.38511342, 6.1118572e-09, 0.3111993, -0.2084502, 8.34465e-07, 0.36219144)
|
||||
operation = 2
|
||||
|
||||
[node name="CSGBox3D2" type="CSGBox3D" parent="CSGCombiner3D" unique_id=45680409]
|
||||
transform = Transform3D(1, 0, 0, 0, 0.9915717, -0.12955965, 0, 0.12955965, 0.9915717, 0, 0.019589934, 0.57785285)
|
||||
operation = 2
|
||||
size = Vector3(1, 1.2053223, 0.352417)
|
||||
|
||||
[node name="CSGBox3D3" type="CSGBox3D" parent="CSGCombiner3D" unique_id=641957703]
|
||||
transform = Transform3D(1, 0, 0, 0, 0.9367828, -0.34991136, 0, 0.34991136, 0.9367828, 0, -0.16928616, 0.4441058)
|
||||
operation = 2
|
||||
size = Vector3(1, 1.2053223, 0.352417)
|
||||
|
||||
[node name="CSGBox3D4" type="CSGBox3D" parent="CSGCombiner3D" unique_id=1059266043]
|
||||
transform = Transform3D(1, 0, 0, 0, 0.7815701, -0.62381756, 0, 0.62381756, 0.7815701, 0, -0.2719775, 0.22231093)
|
||||
operation = 2
|
||||
size = Vector3(1, 1.2053223, 0.352417)
|
||||
|
||||
[node name="CSGBox3D5" type="CSGBox3D" parent="CSGCombiner3D" unique_id=880649240]
|
||||
transform = Transform3D(-4.371139e-08, -0.12955964, -0.9915717, 0, 0.99157166, -0.12955962, 0.99999994, -5.6632317e-09, -4.3342972e-08, -0.6016656, 0.019589934, -0.20879287)
|
||||
operation = 2
|
||||
size = Vector3(1.4033203, 1.2053223, 0.352417)
|
||||
|
||||
[node name="CSGBox3D6" type="CSGBox3D" parent="CSGCombiner3D" unique_id=770532411]
|
||||
transform = Transform3D(1.3113414e-07, 0.12955962, 0.99157166, 8.8817837e-16, 0.9915716, -0.1295596, -0.9999999, 1.6989695e-08, 1.3002891e-07, 0.6035124, 0.019589934, -0.20879287)
|
||||
operation = 2
|
||||
size = Vector3(1.4033203, 1.2053223, 0.352417)
|
||||
@@ -1,161 +0,0 @@
|
||||
extends StaticBody3D
|
||||
|
||||
const GAME_MANAGER = preload("res://GameManager.gd")
|
||||
|
||||
@export var initial_thinking_time: float = 6
|
||||
@export var initial_primary_time: float = 60
|
||||
@export var initial_friend_time: float = 10
|
||||
@export var initial_eating_time: float = 4
|
||||
|
||||
@onready var label_3d: Label3D = $Label3D
|
||||
@onready var label_3d_time: Label3D = $Label3DTime
|
||||
@onready var snap_zones: Array[XRToolsSnapZone] = []
|
||||
|
||||
const lbl_thinking: String = "Thinking..."
|
||||
const lbl_waiting_primary: String = "Waiting for food"
|
||||
const lbl_gameover: String = "Game Over!"
|
||||
const lbl_eating: String = "Eating..."
|
||||
|
||||
### Finite State Machine ####
|
||||
|
||||
enum TableState {
|
||||
EMPTY, # Just finished eating or havent eaten yet
|
||||
THINKING,
|
||||
WAITING_PRIMARY, # Inital wait to be served
|
||||
WAITING_FRIEND, # Waiting for friends food to arrive
|
||||
EATING
|
||||
}
|
||||
var _state = TableState.EMPTY # use _setState(), never set directly
|
||||
var _state_time: float = 0.0
|
||||
var _unsatisfied_orders: Array[String]
|
||||
|
||||
func _ready() -> void:
|
||||
if not label_3d:
|
||||
push_error("Table is missing reference to Label3D")
|
||||
if not label_3d_time:
|
||||
push_error("Table is missing reference to Label3DTime")
|
||||
|
||||
# Get snap_zones for plates, store in array snap_zones
|
||||
for child in get_children():
|
||||
var snap_zone_node = child as XRToolsSnapZone
|
||||
if snap_zone_node:
|
||||
snap_zones.append(snap_zone_node)
|
||||
|
||||
if snap_zones.is_empty():
|
||||
push_error("Table is missing XRToolsSnapZone children")
|
||||
return
|
||||
|
||||
for snap_zone_node in snap_zones:
|
||||
snap_zone_node.has_picked_up.connect(_on_object_picked_up)
|
||||
snap_zone_node.has_dropped.connect(_on_object_dropped)
|
||||
_setState(TableState.EMPTY)
|
||||
|
||||
|
||||
func _on_object_picked_up(_item) -> void:
|
||||
print("Table: object picked up: ", _item)
|
||||
_absorb_item_if_correct(_item)
|
||||
|
||||
|
||||
|
||||
func _on_object_dropped() -> void:
|
||||
print("Table: object dropped ")
|
||||
|
||||
|
||||
func _get_plate_from_item(_item: Node) -> PlateController:
|
||||
return _item.get_children().filter(func(c): return c is PlateController).front() as PlateController
|
||||
|
||||
|
||||
func _absorb_item_if_correct(_item: Node) -> void:
|
||||
# Get plate controller in childer of _item (hopefully a plate XRpickable)
|
||||
var plate = _get_plate_from_item(_item)
|
||||
if not plate:
|
||||
print("Table: held object is not a Plate")
|
||||
return
|
||||
print("Table: held a Plate!")
|
||||
|
||||
# Absorm items from the plate we want
|
||||
for food_item in plate.container.contained_items:
|
||||
print("Table: held a plate with FoodItem: ", food_item.id)
|
||||
if food_item.id in _unsatisfied_orders:
|
||||
print("Table: held FoodItem is in unsatisfied orders, removing it")
|
||||
_unsatisfied_orders.erase(food_item.id)
|
||||
GAME_MANAGER.money += food_item.sell_value
|
||||
_setState(TableState.WAITING_FRIEND)
|
||||
_update_order_text()
|
||||
|
||||
# Table has everything it wants. Start eating
|
||||
if _unsatisfied_orders.is_empty():
|
||||
_setState(TableState.EATING)
|
||||
|
||||
|
||||
func place_order() -> void:
|
||||
print("Table: place_order()")
|
||||
_unsatisfied_orders.append(GAME_MANAGER.get_random_meal())
|
||||
_unsatisfied_orders.append(GAME_MANAGER.get_random_meal())
|
||||
|
||||
|
||||
func _update_order_text() -> void:
|
||||
label_3d.text = "%s\n%s" % [TableState.keys()[_state], "\n".join(_unsatisfied_orders)]
|
||||
|
||||
|
||||
func _setState(newState: TableState) -> void:
|
||||
print("Table set _state: ", TableState.keys()[newState])
|
||||
|
||||
if newState == TableState.EMPTY:
|
||||
label_3d.text = "empty"
|
||||
|
||||
if newState == TableState.THINKING:
|
||||
_state_time = initial_eating_time
|
||||
label_3d.text = lbl_thinking
|
||||
|
||||
if newState == TableState.WAITING_PRIMARY:
|
||||
_state_time = initial_primary_time
|
||||
_update_order_text()
|
||||
|
||||
if newState == TableState.WAITING_FRIEND:
|
||||
_state_time = initial_friend_time
|
||||
_update_order_text()
|
||||
|
||||
if newState == TableState.EATING:
|
||||
_state_time = initial_eating_time
|
||||
label_3d.text = lbl_eating
|
||||
|
||||
_state = newState
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
# Wait for state to finish
|
||||
if _state_time > 0:
|
||||
_state_time -= delta
|
||||
label_3d_time.text = "%.1f" % _state_time
|
||||
return
|
||||
|
||||
# When state timer is finished, do this stuff before moving to next state
|
||||
match _state:
|
||||
TableState.EMPTY:
|
||||
_setState(TableState.THINKING)
|
||||
|
||||
TableState.THINKING:
|
||||
_setState(TableState.WAITING_PRIMARY)
|
||||
place_order()
|
||||
_update_order_text()
|
||||
|
||||
|
||||
TableState.WAITING_PRIMARY:
|
||||
label_3d.text = lbl_gameover
|
||||
|
||||
TableState.WAITING_FRIEND:
|
||||
label_3d.text = lbl_gameover
|
||||
|
||||
TableState.EATING:
|
||||
for snap_zone_node in snap_zones:
|
||||
var held_object = snap_zone_node.picked_up_object
|
||||
if not held_object:
|
||||
continue
|
||||
|
||||
var plate = _get_plate_from_item(held_object)
|
||||
if plate:
|
||||
plate.container.clear()
|
||||
plate.is_dirty = true
|
||||
_setState(TableState.EMPTY)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
uid://caeikc7e3igkd
|
||||
@@ -1,157 +0,0 @@
|
||||
[gd_scene format=3 uid="uid://caf0xanmxbshy"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://caeikc7e3igkd" path="res://Stations/table.gd" id="1_2vcpj"]
|
||||
[ext_resource type="Script" uid="uid://cquqe4f1m1sw6" path="res://addons/godot-xr-tools/objects/snap_zone.gd" id="1_8j1nt"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_24d3s"]
|
||||
|
||||
[sub_resource type="SphereShape3D" id="SphereShape3D_dlkho"]
|
||||
radius = 0.3
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_24d3s"]
|
||||
albedo_color = Color(0.31, 0.21576, 0.1333, 1)
|
||||
|
||||
[node name="Table" type="StaticBody3D" unique_id=1863572470 groups=["station"]]
|
||||
script = ExtResource("1_2vcpj")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=228053941]
|
||||
shape = SubResource("BoxShape3D_24d3s")
|
||||
|
||||
[node name="XRToolsSnapZone" type="Area3D" parent="." unique_id=1947858647 groups=["station"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.31874347, 0.5270121, 0)
|
||||
collision_layer = 65536
|
||||
collision_mask = 65536
|
||||
script = ExtResource("1_8j1nt")
|
||||
snap_mode = 1
|
||||
metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
||||
|
||||
[node name="CollisionShape3D2" type="CollisionShape3D" parent="XRToolsSnapZone" unique_id=1176887393]
|
||||
transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, -0.000975132, 0)
|
||||
shape = SubResource("SphereShape3D_dlkho")
|
||||
|
||||
[node name="XRToolsSnapZone2" type="Area3D" parent="." unique_id=694448387 groups=["station"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.34755045, 0.5270121, 0)
|
||||
collision_layer = 65536
|
||||
collision_mask = 65536
|
||||
script = ExtResource("1_8j1nt")
|
||||
snap_mode = 1
|
||||
metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
||||
|
||||
[node name="CollisionShape3D2" type="CollisionShape3D" parent="XRToolsSnapZone2" unique_id=650960099]
|
||||
transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, -0.000975132, 0)
|
||||
shape = SubResource("SphereShape3D_dlkho")
|
||||
|
||||
[node name="XRToolsSnapZone3" type="Area3D" parent="." unique_id=1734026785 groups=["station"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.012355864, 0.5270121, 0.33038777)
|
||||
collision_layer = 65536
|
||||
collision_mask = 65536
|
||||
script = ExtResource("1_8j1nt")
|
||||
snap_mode = 1
|
||||
metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
||||
|
||||
[node name="CollisionShape3D2" type="CollisionShape3D" parent="XRToolsSnapZone3" unique_id=1271210642]
|
||||
transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, -0.000975132, 0)
|
||||
shape = SubResource("SphereShape3D_dlkho")
|
||||
|
||||
[node name="XRToolsSnapZone4" type="Area3D" parent="." unique_id=1358457948 groups=["station"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.022546828, 0.5270121, -0.36528283)
|
||||
collision_layer = 65536
|
||||
collision_mask = 65536
|
||||
script = ExtResource("1_8j1nt")
|
||||
snap_mode = 1
|
||||
metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
||||
|
||||
[node name="CollisionShape3D2" type="CollisionShape3D" parent="XRToolsSnapZone4" unique_id=1880499470]
|
||||
transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, -0.000975132, 0)
|
||||
shape = SubResource("SphereShape3D_dlkho")
|
||||
|
||||
[node name="CSGCombiner3D" type="CSGCombiner3D" parent="." unique_id=2085635675]
|
||||
|
||||
[node name="CSGBox3D" type="CSGBox3D" parent="CSGCombiner3D" unique_id=411048765]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.46137518, 0)
|
||||
size = Vector3(1, 0.07678223, 1)
|
||||
material = SubResource("StandardMaterial3D_24d3s")
|
||||
|
||||
[node name="CSGBox3D2" type="CSGBox3D" parent="CSGCombiner3D" unique_id=2093582402]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.0023368, 0)
|
||||
size = Vector3(0.1, 0.8768555, 0.1)
|
||||
material = SubResource("StandardMaterial3D_24d3s")
|
||||
|
||||
[node name="CSGBox3D3" type="CSGBox3D" parent="CSGCombiner3D" unique_id=839228683]
|
||||
transform = Transform3D(0.86602545, 0.50000006, 0, -0.50000006, 0.86602545, 0, 0, 0, 1, -0.21657634, -0.5113707, 0)
|
||||
size = Vector3(0.1, 0.8768555, 0.1)
|
||||
material = SubResource("StandardMaterial3D_24d3s")
|
||||
|
||||
[node name="CSGBox3D4" type="CSGBox3D" parent="CSGCombiner3D" unique_id=1028598922]
|
||||
transform = Transform3D(-0.45971727, -0.2654179, 0.8474747, -0.50000006, 0.86602545, 0, -0.73393464, -0.4237374, -0.53083575, 0.10796261, -0.5113707, 0.16580808)
|
||||
size = Vector3(0.1, 0.8768555, 0.1)
|
||||
material = SubResource("StandardMaterial3D_24d3s")
|
||||
|
||||
[node name="CSGBox3D5" type="CSGBox3D" parent="CSGCombiner3D" unique_id=892392906]
|
||||
transform = Transform3D(-0.38498235, -0.12489955, -0.9144338, -0.5286442, 0.8420017, 0.107556336, 0.756521, 0.5248176, -0.39018327, 0.059352398, -0.5113707, -0.2290039)
|
||||
size = Vector3(0.1, 0.8768555, 0.1)
|
||||
material = SubResource("StandardMaterial3D_24d3s")
|
||||
|
||||
[node name="Customers" type="Node3D" parent="." unique_id=2147179512]
|
||||
|
||||
[node name="Customer" type="Node3D" parent="Customers" unique_id=1265943831]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.016780734, -0.08218986, 0)
|
||||
|
||||
[node name="CSGCombiner3D" type="CSGCombiner3D" parent="Customers/Customer" unique_id=1211819222]
|
||||
transform = Transform3D(1.095, 0, 0, 0, 1.095, 0, 0, 0, 1.095, 0.08442788, -0.19989291, 0)
|
||||
|
||||
[node name="CSGSphere3D" type="CSGSphere3D" parent="Customers/Customer/CSGCombiner3D" unique_id=2104132980]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.88519466, 0.9358529, 0)
|
||||
radius = 0.17520222
|
||||
|
||||
[node name="CSGSphere3D2" type="CSGSphere3D" parent="Customers/Customer/CSGCombiner3D" unique_id=808477724]
|
||||
transform = Transform3D(1, 0, 0, 0, 1.747647, 0, 0, 0, 1, -0.9068739, 0.51278543, 0)
|
||||
radius = 0.17520222
|
||||
|
||||
[node name="Seats" type="Node3D" parent="." unique_id=1101423789]
|
||||
|
||||
[node name="Seat" type="Node3D" parent="Seats" unique_id=1951252898]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.9079602, -0.37259835, 0)
|
||||
|
||||
[node name="CSGCombiner3D" type="CSGCombiner3D" parent="Seats/Seat" unique_id=384912485]
|
||||
|
||||
[node name="CSGCylinder3D" type="CSGCylinder3D" parent="Seats/Seat/CSGCombiner3D" unique_id=170426769]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.14400849, 0)
|
||||
radius = 0.25634766
|
||||
height = 0.051940918
|
||||
sides = 16
|
||||
|
||||
[node name="CSGCylinder3D2" type="CSGCylinder3D" parent="Seats/Seat/CSGCombiner3D" unique_id=1294605073]
|
||||
transform = Transform3D(0.24355066, 0.98373884, 0, -1.3339849, 0.1796049, 0, 0, 0, 0.8365013, -0.23884833, 0.48196742, 0)
|
||||
radius = 0.25634766
|
||||
height = 0.051940918
|
||||
sides = 16
|
||||
|
||||
[node name="CSGBox3D" type="CSGBox3D" parent="Seats/Seat/CSGCombiner3D" unique_id=1761839440]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.15600169, -0.18527734, 0.1341562)
|
||||
size = Vector3(0.05, 0.6713196, 0.05)
|
||||
|
||||
[node name="CSGBox3D2" type="CSGBox3D" parent="Seats/Seat/CSGCombiner3D" unique_id=1395371703]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.15600169, -0.18527734, -0.15065354)
|
||||
size = Vector3(0.05, 0.6713196, 0.05)
|
||||
|
||||
[node name="CSGBox3D3" type="CSGBox3D" parent="Seats/Seat/CSGCombiner3D" unique_id=96608061]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.1444692, -0.18527734, 0.1341562)
|
||||
size = Vector3(0.05, 0.6713196, 0.05)
|
||||
|
||||
[node name="CSGBox3D4" type="CSGBox3D" parent="Seats/Seat/CSGCombiner3D" unique_id=1684738537]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.1444692, -0.18527734, -0.15065354)
|
||||
size = Vector3(0.05, 0.6713196, 0.05)
|
||||
|
||||
[node name="Label3D" type="Label3D" parent="." unique_id=662960977]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.3247777, 0)
|
||||
billboard = 2
|
||||
text = "table state"
|
||||
vertical_alignment = 0
|
||||
line_spacing = -15.0
|
||||
|
||||
[node name="Label3DTime" type="Label3D" parent="." unique_id=1534849507]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.3306234, 0)
|
||||
pixel_size = 0.003
|
||||
billboard = 2
|
||||
text = "20.1s"
|
||||
@@ -1,6 +1,3 @@
|
||||
|
||||
|
||||
|
||||
- Move recipes into a Yaml file that is read, and handles symetry through a Static manager class?
|
||||
A `FoodItem` has an ID.
|
||||
Example:
|
||||
|
||||
@@ -10,6 +10,4 @@ uv1_triplanar = true
|
||||
|
||||
[resource]
|
||||
next_pass = SubResource("StandardMaterial3D_0gbf8")
|
||||
metallic = 1.0
|
||||
metallic_specular = 0.0
|
||||
roughness = 0.0
|
||||
albedo_color = Color(0.35156325, 0.3515627, 0.3515629, 1)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
[ext_resource type="PackedScene" uid="uid://diyu06cw06syv" path="res://addons/godot-xr-tools/player/player_body.tscn" id="4_6xr3x"]
|
||||
[ext_resource type="PackedScene" uid="uid://b6bk2pj8vbj28" path="res://addons/godot-xr-tools/functions/movement_turn.tscn" id="4_rd8py"]
|
||||
[ext_resource type="Script" uid="uid://ck4yn3hxuobj7" path="res://addons/godot-xr-tools/player/player_body.gd" id="5_rd8py"]
|
||||
[ext_resource type="PackedScene" uid="uid://cqhw276realc" path="res://addons/godot-xr-tools/functions/function_pointer.tscn" id="6_ptr"]
|
||||
|
||||
[node name="XROrigin3D" type="XROrigin3D" unique_id=2055526621]
|
||||
|
||||
@@ -25,6 +26,10 @@ strafe = true
|
||||
[node name="FunctionPickup" parent="XRControllerLeftHand" unique_id=2133415351 instance=ExtResource("3_rd8py")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.009340696, -0.0074635968, 0.024033919)
|
||||
|
||||
[node name="FunctionPointer" parent="XRControllerLeftHand" instance=ExtResource("6_ptr")]
|
||||
show_laser = 2
|
||||
show_target = true
|
||||
|
||||
[node name="XRControllerRightHand" type="XRController3D" parent="." unique_id=202756852]
|
||||
tracker = &"right_hand"
|
||||
|
||||
@@ -39,6 +44,10 @@ smooth_turn_speed = 3.5
|
||||
[node name="FunctionPickup" parent="XRControllerRightHand" unique_id=1876210450 instance=ExtResource("3_rd8py")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.0074635968, 0.024033919)
|
||||
|
||||
[node name="FunctionPointer" parent="XRControllerRightHand" instance=ExtResource("6_ptr")]
|
||||
show_laser = 2
|
||||
show_target = true
|
||||
|
||||
[node name="PlayerBody" type="CharacterBody3D" parent="." unique_id=1444632058 groups=["player_body"] instance=ExtResource("4_6xr3x")]
|
||||
process_priority = -100
|
||||
process_physics_priority = -100
|
||||
|
||||
@@ -1,323 +0,0 @@
|
||||
# Godot YAML Changelog
|
||||
|
||||
## Version 2.2.0
|
||||
|
||||
- Updated GDSchema to version 2.0.0, with JSON schema draft 2020-12 validation spec
|
||||
- Enable CTRL+mouse wheel to zoom in the YAML editor ([Issue #17](https://github.com/fimbul-works/godot-yaml/issues/17))
|
||||
- Renamed HistoryState class to YAMLEditorHistoryState, fixing [Issue #19](https://github.com/fimbul-works/godot-yaml/issues/19)
|
||||
- Various small fixes
|
||||
|
||||
## Version 2.1.3
|
||||
|
||||
- Changed the behavior of the `x-yaml-tag` Schema validation rule to ignore tagless nodes
|
||||
|
||||
## Version 2.1.2
|
||||
|
||||
- Added support for Android (arm64, x86_64) platforms thanks to @Nemo1166
|
||||
- Rewrote `String` variant strigification rules, resolving [Issue #15](https://github.com/fimbul-works/godot-yaml/issues/15)
|
||||
- Fixed application hanging when closed due to an issue with the YAML class registry
|
||||
|
||||
## Version 2.1.1
|
||||
|
||||
Empty strings are stringified with quotes.
|
||||
|
||||
## Version 2.1.0
|
||||
|
||||
When passing the custom tag to `YAML.schema_register(class, serialize_method, deserialize_static, custom_tag)` use the custom tag when emitting, resolving [Issue #13](https://github.com/fimbul-works/godot-yaml/issues/13).
|
||||
|
||||
## Version 2.0.0
|
||||
|
||||
The second major version of Godot YAML brings powerful schema validation capabilities through the integration of [GDSchema](https://github.com/fimbul-works/gdschema), enabling [JSON Schema Draft-7](https://json-schema.org/) validation for your YAML data. This release also includes improved multi-document handling, custom tag support, and several important bug fixes. See the [migration guide](#migration-guide-from-version-1-to-200) for breaking changes.
|
||||
|
||||
### Major Features
|
||||
|
||||
#### Schema Validation (New!)
|
||||
|
||||
Version 2.0.0 introduces comprehensive schema validation powered by [GDSchema](https://github.com/fimbul-works/gdschema), bringing the full power of [JSON Schema Draft-7](https://json-schema.org/) to your YAML workflows. This flagship feature enables you to:
|
||||
|
||||
- **Validate YAML data** against industry-standard JSON Schema specifications
|
||||
- **Define schemas in YAML** with native YAML syntax for better readability
|
||||
- **Auto-apply defaults** when properties are missing using the `default` keyword
|
||||
- **Validate YAML tags** with the custom `x-yaml-tag` keyword for type safety
|
||||
- **Reference schemas** using `$ref` for modular, reusable validation rules
|
||||
- **Register schemas globally** with `$id` for cross-file validation
|
||||
- **Get detailed error reports** with JSON Pointer paths and constraint information
|
||||
|
||||
New classes:
|
||||
- `Schema` - Validates data against JSON Schema Draft-7 specifications
|
||||
- `SchemaValidationResult` - Contains detailed validation results and errors
|
||||
|
||||
New YAML methods:
|
||||
- `YAML.load_schema_from_string()` - Parse YAML schema definitions
|
||||
- `YAML.load_schema_from_file()` - Load schema files with auto-registration
|
||||
- `YAML.parse_and_validate()` - Parse and validate YAML in one step
|
||||
|
||||
Schema features include:
|
||||
- Full JSON Schema Draft-7 support (type checking, numeric constraints, string patterns, array/object validation)
|
||||
- Logical composition (`allOf`, `anyOf`, `oneOf`, `not`)
|
||||
- Conditional validation (`if`/`then`/`else`)
|
||||
- Schema definitions and references (`$defs`, `$ref`)
|
||||
- Custom format validators
|
||||
- Thread-safe lazy compilation with caching
|
||||
- YAML-specific extensions:
|
||||
- `default` - Provides default values for missing properties
|
||||
- `x-yaml-tag` - Validates YAML type tags (e.g., `!Resource`, `!CustomType`)
|
||||
|
||||
Example:
|
||||
```gdscript
|
||||
# Define a schema in YAML
|
||||
var schema_yaml = """
|
||||
$id: "http://example.com/user.yaml"
|
||||
type: object
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
minLength: 3
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
role:
|
||||
type: string
|
||||
default: user
|
||||
x-yaml-tag: UserRole
|
||||
required:
|
||||
- username
|
||||
- email
|
||||
"""
|
||||
|
||||
# Build the schema (auto-registered with $id)
|
||||
var schema = YAML.load_schema_from_string(schema_yaml)
|
||||
|
||||
# Validate data
|
||||
var data_yaml = """
|
||||
username: Alice
|
||||
email: alice@exampe.com
|
||||
"""
|
||||
|
||||
var result = YAML.parse_and_validate(data_yaml, schema)
|
||||
|
||||
if result.is_valid():
|
||||
print(result.get_data().role) # "user" (default applied)
|
||||
else:
|
||||
print(result.get_summary()) # Detailed error report
|
||||
```
|
||||
|
||||
### Other Changes
|
||||
|
||||
- **Breaking:** Removed the `YAMLResult.get_data(index)` parameter. Use `YAMLResult.get_document(index)` instead for multi-document access.
|
||||
- Added `YAMLResult.get_documents()` and `YAMLResult.has_multiple_documents()` methods for easier multi-document YAML handling
|
||||
- Added support for custom tags when registering classes with `YAML.register_class()` - now you can specify custom YAML tags like `!Item` or `!ruby:object/Item`
|
||||
- Added `YAMLCodeEdit` class to enable embedding the YAML editor directly into your projects
|
||||
- Added missing example for handling multi-document YAML files
|
||||
- Upgraded [RapidYAML](https://github.com/biojppm/rapidyaml) to version 0.10.0
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed `YAMLResult.get_data()` only returning the first element of a document that only contains an array
|
||||
- Fixed invalid numeric value parsing that could cause crashes or incorrect values
|
||||
- Fixed empty strings being parsed and emitted as null values instead of empty strings
|
||||
- Fixed YAML files not opening correctly in Godot 4.3 editor
|
||||
|
||||
### Migration Guide From Version 1.* to 2.0.0
|
||||
|
||||
Replace `YAMLResult.get_data(index)` calls with `YAMLResult.get_document(index)`:
|
||||
|
||||
**Old version:**
|
||||
```gdscript
|
||||
var result = YAML.parse(multi_doc_yaml)
|
||||
var first_doc = result.get_data(0)
|
||||
var second_doc = result.get_data(1)
|
||||
```
|
||||
|
||||
**New version:**
|
||||
```gdscript
|
||||
var result = YAML.parse(multi_doc_yaml)
|
||||
var first_doc = result.get_document(0)
|
||||
var second_doc = result.get_document(1)
|
||||
|
||||
# Or get all documents at once
|
||||
var all_docs = result.get_documents()
|
||||
```
|
||||
|
||||
## Version 1.1.0
|
||||
|
||||
- YAML files can now be loaded with the `!Resource` tag, allowing modular composition
|
||||
- Fixes:
|
||||
- Force YAML indentation to always use spaces instead of tabs
|
||||
- The file system should now update properly when saving a new YAML file
|
||||
- Fixed tab indentation not working correctly in the YAML editor
|
||||
- Fixed Packed Array types not detecting array templates
|
||||
- Prevent duplicates in YAML editor file list
|
||||
|
||||
## Version 1.0.0
|
||||
|
||||
The first major version of Godot YAML includes many improvements, and some breaking changes. See the [migration guide](#migration-guide-from-version-0121-to-100) for details.
|
||||
|
||||
### Changes
|
||||
|
||||
- Streamlined API in many places
|
||||
- Added class documentation for the in-editor help
|
||||
- Added custom YAML editor inside the Godot engine editor
|
||||
- Added `YAMLSecurity` class for safer resource loading during parsing
|
||||
- `YAMLStyle` API changes:
|
||||
- Adopted Godot terminology for container form (Array and Dictionary instead of Sequence and Map)
|
||||
- Merged scalar and quote styles into unified string style
|
||||
- Separated number format into integer and float formats respectively
|
||||
- Removed binary encoding (now always uses base64 encoding)
|
||||
- Implemented serializing/deserializing style definitions
|
||||
- Implemented stringifying non-local-to-scene resource references
|
||||
- Performance optimizations
|
||||
- Improved error reporting across the board with line/column tracking when parsing
|
||||
- Fixes:
|
||||
- Style definitions should now be parsed correctly
|
||||
- Stringifying with styles should now work consistently
|
||||
- Fixed custom tags not being used when stringifying with styles
|
||||
- Fixed integer to string conversion with 64 bit values
|
||||
|
||||
### Migration Guide From Version 0.12.1 to 1.0.0
|
||||
|
||||
This migration guide should help you update your code to work with the latest version of the YAML plugin. The core functionality and architecture remain similar, but the style system has been refined for better consistency and usability.
|
||||
|
||||
#### Custom Class Serialization and Deserialization
|
||||
|
||||
Custom class serialization has been changed to allow serializing to/from just dictionaries to any of the supported Godot variants:
|
||||
|
||||
**Old version:**
|
||||
```gdscript
|
||||
class_name CustomClass extends Resource
|
||||
|
||||
@export var name: String
|
||||
|
||||
func _init(p_name: String = "") -> void:
|
||||
name = p_name
|
||||
|
||||
func to_dict() -> Dictionary:
|
||||
return {
|
||||
"name": name
|
||||
}
|
||||
|
||||
static func from_dict(data: Dictionary) -> CustomClass:
|
||||
return CustomClass.new(data["name"])
|
||||
```
|
||||
|
||||
**New version:**
|
||||
```gdscript
|
||||
class_name CustomClass extends Resource
|
||||
|
||||
@export var name: String
|
||||
|
||||
func _init(p_name: String = "") -> void:
|
||||
name = p_name
|
||||
|
||||
func serialize() -> Variant:
|
||||
return {
|
||||
"name": name
|
||||
}
|
||||
|
||||
static func deserialize(data: Variant) -> Variant:
|
||||
if typeof(data) != TYPE_DICTIONARY:
|
||||
return YAMLResult.error("CustomClass expects Dictionary")
|
||||
|
||||
return CustomClass.new(data["name"])
|
||||
```
|
||||
|
||||
#### Container Form Changes
|
||||
|
||||
Previously the container forms used the YAML standard naming of *Sequences* and *Maps*, but to make the extension more Godot friendly the containers now follow Godot's naming conventions of *Arrays* and *Dictionaries*:
|
||||
|
||||
```gdscript
|
||||
# Specify how containers are formatted
|
||||
style.set_container_form(YAMLStyle.FORM_ARRAY) # Previously FORM_SEQ
|
||||
style.set_container_form(YAMLStyle.FORM_DICTIONARY) # Previously FORM_MAP
|
||||
```
|
||||
|
||||
#### Scalar Style and Quote Style Merged
|
||||
|
||||
**Old version:**
|
||||
```gdscript
|
||||
var style = YAML.create_style()
|
||||
# Separate methods for different aspects of string formatting
|
||||
style.set_scalar_style(YAMLStyle.SCALAR_LITERAL) # For block style (|)
|
||||
style.set_quote_style(YAMLStyle.QUOTE_DOUBLE) # For quoting style
|
||||
```
|
||||
|
||||
**New version:**
|
||||
```gdscript
|
||||
var style = YAML.create_style()
|
||||
# Single method combining both formatting aspects
|
||||
style.set_string_style(YAMLStyle.STRING_LITERAL) # For block style (|)
|
||||
style.set_string_style(YAMLStyle.STRING_QUOTE_DOUBLE) # For quoting style
|
||||
```
|
||||
|
||||
The constants have been unified:
|
||||
- `YAMLStyle.SCALAR_PLAIN`, `YAMLStyle.SCALAR_LITERAL`, `YAMLStyle.SCALAR_FOLDED` are now accessible through the `STRING_*` constants
|
||||
- `YAMLStyle.QUOTE_NONE`, `YAMLStyle.QUOTE_SINGLE`, `YAMLStyle.QUOTE_DOUBLE` have been replaced with `STRING_PLAIN`, `STRING_QUOTE_SINGLE`, `STRING_QUOTE_DOUBLE`
|
||||
|
||||
#### Number Format Changes
|
||||
|
||||
**Old version:**
|
||||
```gdscript
|
||||
style.set_number_format(YAMLStyle.NUM_HEX)
|
||||
```
|
||||
|
||||
**New version:**
|
||||
```gdscript
|
||||
style.set_integer_format(YAMLStyle.INT_HEX)
|
||||
style.set_float_format(YAMLStyle.FLOAT_SCIENTIFIC)
|
||||
```
|
||||
|
||||
Integer and float formatting have been split into two methods with dedicated enum values:
|
||||
- `YAMLStyle.NUM_DECIMAL`, `YAMLStyle.NUM_HEX`, etc. are now `YAMLStyle.INT_DECIMAL`, `YAMLStyle.INT_HEX`, etc.
|
||||
- `YAMLStyle.NUM_SCIENTIFIC` is now `YAMLStyle.FLOAT_SCIENTIFIC`
|
||||
|
||||
#### Removed YAMLLoader and YAMLWriter Classes
|
||||
|
||||
If using the old loaders, they have been integrated to the main `YAML` class:
|
||||
|
||||
**Old version:**
|
||||
```gdscript
|
||||
# Load YAML from a file
|
||||
var data = YAMLLoader.load_file("res://data.yaml")
|
||||
if YAMLLoader.last_error != null:
|
||||
print("Error loading file: ", YAMLLoader.last_error)
|
||||
else:
|
||||
print("Loaded data: ", data)
|
||||
|
||||
# Save data to a YAML file
|
||||
var data = {"key": "value", "list": [1, 2, 3]}
|
||||
var success = YAMLWriter.save_file(data, "user://output.yaml")
|
||||
if !success:
|
||||
print("Error saving file: ", YAMLWriter.last_error)
|
||||
```
|
||||
|
||||
**New version:**
|
||||
```gdscript
|
||||
# Load YAML from a file
|
||||
var load_result = YAML.load_file("res://example.yaml")
|
||||
if load_result.has_error():
|
||||
push_error(load_result.get_error())
|
||||
return null
|
||||
var data = load_result.get_data()
|
||||
print("Loaded data: ", data)
|
||||
|
||||
# Save data to a YAML file
|
||||
var save_result = YAML.save_file(data, "user://output.yaml")
|
||||
if save_result.has_error():
|
||||
push_error(save_result.get_error())
|
||||
return null
|
||||
var yaml_text = save_result.get_data()
|
||||
print("Saved YAML:\n", yaml_text)
|
||||
|
||||
# The above can also be written as
|
||||
var null_on_fail = YAML.try_load_file("res://example.yaml")
|
||||
var success = YAML.try_save_file(data, "user://output.yaml")
|
||||
```
|
||||
|
||||
## Older Versions
|
||||
|
||||
- **0.12.1** - Build support for Linux (x86 64-bit)
|
||||
- **0.12.0** - Performance optimizations, bug fixes, and comprehensive tests for all variant types
|
||||
- **0.11.0** - Added support for parsing multiple documents, and error handling for custom class deserialization
|
||||
- **0.10.1** - Fixed issue with custom Resources not being serializable
|
||||
- **0.10.0** - Added custom class serialization support, upgraded to Godot 4.3
|
||||
- **0.9.0** - Initial public release
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 FimbulWorks
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -1,767 +0,0 @@
|
||||
# Godot YAML
|
||||
|
||||
A high-performance YAML parsing and serialization plugin for Godot 4.3, powered by [RapidYAML](https://github.com/biojppm/rapidyaml). This plugin offers comprehensive YAML support with customizable styling options, full Godot variant type handling, custom class serialization, and industry-standard schema validation.
|
||||
|
||||
**New to YAML in Godot?** Check out the [`examples/`](./addons/yaml/examples/) directory for comprehensive usage examples covering all features.
|
||||
|
||||
## Version History
|
||||
|
||||
- **2.2.0** (Current) - Upgraded GDSchema to 2.0.0 for JSON validation draft 2020-12 spec, and various bug fixes
|
||||
- **2.1.2** - Added support for Android (arm64, x86_64) platforms thanks to @Nemo1166, and fixed some bugs
|
||||
- **2.1.1** - Empty strings are stringified with quotes
|
||||
- **2.1.0** - When passing custom tag to `YAML.schema_register(class, serialize_method, deserialize_static, custom_tag)` use the custom tag when stringifying
|
||||
- **2.0.0** - Major release with schema validation powered by [GDSchema](https://github.com/fimbul-works/gdschema), improved multi-document handling, and bug fixes.
|
||||
|
||||
See [the full changelog](./CHANGELOG.md) for more details.
|
||||
|
||||
## Features
|
||||
|
||||
- ⚡ **High Performance**: Built on the lightweight and efficient [RapidYAML](https://github.com/biojppm/rapidyaml) library
|
||||
- 🧩 **Comprehensive Variant Support**: Handles all Godot built-in Variant types (except Callable and RID)
|
||||
- ✅ **Schema Validation**: Full JSON Schema Draft 2020-12 validation powered by [GDSchema](https://github.com/fimbul-works/gdschema) with YAML-specific extensions
|
||||
- 🧪 **Custom Class Serialization**: Register your GDScript classes for seamless serialization and deserialization
|
||||
- 📄 **Multi-Document Support**: Parse YAML files with multiple `---` separated documents
|
||||
- 🎨 **Style Customization**: Control how YAML is formatted with customizable style options
|
||||
- 📝 **Comprehensive Error Handling**: Detailed error reporting with line and column information
|
||||
- 🔀 **Thread-Safe**: Fully supports multi-threaded parsing and emission
|
||||
- 🗂️ **Resource References**: Use `!Resource` tags to reference and load external resources
|
||||
- 🛡️ **Security Controls**: Manage resource loading security during YAML parsing
|
||||
|
||||
## Compatibility
|
||||
|
||||
- Requires **Godot 4.3** or higher
|
||||
- Supported platforms:
|
||||
- Windows (x86 64-bit)
|
||||
- Linux (x86 64-bit)
|
||||
- macOS: (Universal)
|
||||
- **Note**: Some macOS configurations (particularly newer versions with stricter Gatekeeper policies) may prevent loading of GDExtensions generally, not just this plugin. If the extension fails to load, try building from source or test with other GDExtensions to determine if this is a system-wide issue.
|
||||
- Android (x86 64-bit, ARM 64-bit)
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Parsing YAML
|
||||
|
||||
```gdscript
|
||||
# Parse a YAML string
|
||||
var yaml_text = """
|
||||
player:
|
||||
name: Knight
|
||||
health: 100
|
||||
inventory:
|
||||
- Sword
|
||||
- Shield
|
||||
- Health Potion
|
||||
"""
|
||||
|
||||
var result = YAML.parse(yaml_text)
|
||||
if result.has_error():
|
||||
push_error("Parse error: %s" % result.get_error())
|
||||
return
|
||||
|
||||
var data = result.get_data()
|
||||
print("Player name: %s" % data.player.name)
|
||||
print("Health: %d" % data.player.health)
|
||||
print("First item: %s" % data.player.inventory[0])
|
||||
```
|
||||
|
||||
### Converting Data to YAML
|
||||
|
||||
```gdscript
|
||||
# Convert Godot data to YAML
|
||||
var enemy_data = {
|
||||
"name": "Dragon",
|
||||
"health": 500,
|
||||
"attacks": ["Bite", "Fire Breath", "Tail Whip"]
|
||||
}
|
||||
|
||||
var string_result = YAML.stringify(enemy_data)
|
||||
if !string_result.has_error():
|
||||
print(string_result.get_data())
|
||||
```
|
||||
|
||||
### Working with Files
|
||||
|
||||
```gdscript
|
||||
# Load YAML from a file
|
||||
var result = YAML.load_file("res://data/level_data.yaml")
|
||||
|
||||
if result.has_error():
|
||||
push_error("YAML parsing failed: " + result.get_error())
|
||||
return
|
||||
|
||||
# Success - get the data and use it
|
||||
var level_data = result.get_data()
|
||||
print("Loaded level: " + level_data.name)
|
||||
|
||||
# Save data to a YAML file
|
||||
var save_data = {
|
||||
"player": {
|
||||
"name": "Hero",
|
||||
"level": 10,
|
||||
"position": [25, 48]
|
||||
},
|
||||
"quests_completed": ["Rats in the Cellar", "Lost Artifact"]
|
||||
}
|
||||
|
||||
var save_result = YAML.save_file(save_data, "user://save_game.yaml")
|
||||
if !save_result.has_error():
|
||||
print("Game saved successfully!")
|
||||
else:
|
||||
push_error("Save failed: " + save_result.get_error())
|
||||
```
|
||||
|
||||
### Simplified API
|
||||
|
||||
The extension provides simplified methods that return direct results rather than YAMLResult objects:
|
||||
|
||||
```gdscript
|
||||
# Quick parsing without error checking
|
||||
var data = YAML.try_parse("""
|
||||
weapon: Axe
|
||||
damage: 25
|
||||
""")
|
||||
# Or YAML.try_load_file
|
||||
|
||||
if data:
|
||||
print("Weapon: %s (Damage: %d)" % [data.weapon, data.damage])
|
||||
else:
|
||||
print("Failed to parse weapon data")
|
||||
|
||||
# Quick stringify
|
||||
var npc = {
|
||||
"name": "Merchant",
|
||||
"dialog": "Welcome to my shop!",
|
||||
"shop_items": ["Potion", "Map", "Torch"]
|
||||
}
|
||||
|
||||
var yaml_text = YAML.try_stringify(npc)
|
||||
# Or YAML.try_save_file
|
||||
if yaml_text:
|
||||
save_to_file(yaml_text)
|
||||
```
|
||||
|
||||
## Schema Validation
|
||||
|
||||
Version 2.0.0 introduces powerful schema validation capabilities through the integration of [GDSchema](https://github.com/fimbul-works/gdschema). Define your data structures using industry-standard JSON Schema Draft-7 syntax, enhanced with YAML-specific features for an optimal validation experience.
|
||||
|
||||
### Why Use Schema Validation?
|
||||
|
||||
Schema validation ensures your YAML data meets specific requirements before your game uses it. This is invaluable for:
|
||||
|
||||
- **Configuration files**: Validate game settings, difficulty parameters, and preferences
|
||||
- **User-generated content**: Ensure mod data and custom levels follow your specifications
|
||||
- **Save files**: Verify save data integrity with automatic default values
|
||||
- **Data interchange**: Validate API responses and external data sources
|
||||
- **Development**: Catch data errors early with detailed validation reports
|
||||
|
||||
### Quick Start
|
||||
|
||||
The `YAML.parse_and_validate()` method combines parsing and validation in one step, returning a `YAMLResult` that may contain both parse errors and validation errors:
|
||||
|
||||
```gdscript
|
||||
# Define a schema in YAML (more readable than JSON!)
|
||||
var schema_yaml = """
|
||||
type: object
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
minLength: 3
|
||||
maxLength: 20
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
level:
|
||||
type: integer
|
||||
minimum: 1
|
||||
default: 1
|
||||
required:
|
||||
- username
|
||||
- email
|
||||
"""
|
||||
|
||||
# Build the schema
|
||||
var schema = YAML.load_schema_from_string(schema_yaml)
|
||||
if not schema:
|
||||
push_error("Failed to parse schema")
|
||||
return
|
||||
|
||||
# Parse and validate YAML data
|
||||
var player_yaml = """
|
||||
username: hero
|
||||
email: hero@example.com
|
||||
"""
|
||||
|
||||
var result = YAML.parse_and_validate(player_yaml, schema)
|
||||
|
||||
# Check for parse errors first
|
||||
if result.has_error():
|
||||
push_error("Parse error: %s" % result.get_error())
|
||||
return
|
||||
|
||||
# Then check for validation errors
|
||||
if result.has_validation_errors():
|
||||
print(result.get_validation_summary()) # Detailed error report
|
||||
return
|
||||
|
||||
# Success - use the validated data with defaults applied
|
||||
var player_data = result.get_data()
|
||||
print("Level: %d" % player_data.level) # 1 (default applied!)
|
||||
```
|
||||
|
||||
### YAML-Specific Schema Extensions
|
||||
|
||||
Godot YAML includes two powerful extensions to standard JSON Schema:
|
||||
|
||||
#### 1. The `default` Keyword
|
||||
|
||||
Automatically apply default values when properties are missing:
|
||||
|
||||
```gdscript
|
||||
var schema = YAML.load_schema_from_string("""
|
||||
type: object
|
||||
properties:
|
||||
difficulty:
|
||||
type: string
|
||||
enum: [easy, normal, hard]
|
||||
default: normal
|
||||
music_volume:
|
||||
type: number
|
||||
minimum: 0
|
||||
maximum: 1
|
||||
default: 0.8
|
||||
show_tutorial:
|
||||
type: boolean
|
||||
default: true
|
||||
""")
|
||||
|
||||
# Parse empty YAML - defaults are applied during validation!
|
||||
var result = YAML.parse_and_validate('{}', schema)
|
||||
|
||||
if !result.has_validation_errors():
|
||||
var settings = result.get_data()
|
||||
print(settings.difficulty) # "normal"
|
||||
print(settings.music_volume) # 0.8
|
||||
print(settings.show_tutorial) # true
|
||||
```
|
||||
|
||||
#### 2. The `x-yaml-tag` Keyword
|
||||
|
||||
Validate that values have the correct YAML type tag:
|
||||
|
||||
```gdscript
|
||||
var schema = YAML.load_schema_from_string("""
|
||||
type: object
|
||||
properties:
|
||||
player_sprite:
|
||||
type: string
|
||||
x-yaml-tag: Resource # Must be tagged with !Resource
|
||||
custom_item:
|
||||
type: object
|
||||
x-yaml-tag: Item # Must be tagged with !Item
|
||||
""")
|
||||
|
||||
# This YAML will validate successfully
|
||||
var valid_yaml = """
|
||||
player_sprite: !Resource "res://player.png"
|
||||
custom_item: !Item
|
||||
name: Sword
|
||||
damage: 10
|
||||
"""
|
||||
|
||||
var result = YAML.parse_and_validate(valid_yaml, schema)
|
||||
if !result.has_validation_errors():
|
||||
var data = result.get_data()
|
||||
print("Sprite loaded: %s" % (data.player_sprite is Texture2D))
|
||||
```
|
||||
|
||||
### Reusable Schemas with Global Registration
|
||||
|
||||
Schemas with an `$id` field are automatically registered globally, enabling modular schema design:
|
||||
|
||||
```gdscript
|
||||
# Define a reusable schema (saved as user_schema.yaml)
|
||||
var user_schema_yaml = """
|
||||
$id: "http://mygame.com/schemas/user.yaml"
|
||||
type: object
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
minLength: 3
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
avatar:
|
||||
type: string
|
||||
x-yaml-tag: Resource
|
||||
required:
|
||||
- username
|
||||
- email
|
||||
"""
|
||||
|
||||
# Load and auto-register the schema
|
||||
var user_schema = YAML.load_schema_from_string(user_schema_yaml)
|
||||
|
||||
# Now reference it from other schemas!
|
||||
var game_data_schema = YAML.load_schema_from_string("""
|
||||
type: object
|
||||
properties:
|
||||
player:
|
||||
$ref: "http://mygame.com/schemas/user.yaml"
|
||||
high_score:
|
||||
type: integer
|
||||
minimum: 0
|
||||
""")
|
||||
|
||||
# Parse and validate nested YAML data
|
||||
var game_yaml = """
|
||||
player:
|
||||
username: alice
|
||||
email: alice@example.com
|
||||
high_score: 1000
|
||||
"""
|
||||
|
||||
var result = YAML.parse_and_validate(game_yaml, game_data_schema)
|
||||
|
||||
if result.has_error():
|
||||
push_error("Parse error: %s" % result.get_error())
|
||||
elif result.has_validation_errors():
|
||||
push_error(result.get_validation_summary())
|
||||
else:
|
||||
var game_data = result.get_data()
|
||||
print("Player: %s, Score: %d" % [game_data.player.username, game_data.high_score])
|
||||
```
|
||||
|
||||
### Parse and Validate in One Step
|
||||
|
||||
For the most streamlined workflow, use `YAML.parse_and_validate()` which returns a `YAMLResult` with integrated validation information:
|
||||
|
||||
```gdscript
|
||||
# Load a schema file once at startup
|
||||
var config_schema = YAML.load_schema_from_file("res://schemas/config_schema.yaml")
|
||||
|
||||
# Later, parse and validate user config in one call
|
||||
var user_config = """
|
||||
graphics:
|
||||
resolution: 1920x1080
|
||||
vsync: true
|
||||
audio:
|
||||
master_volume: 0.8
|
||||
"""
|
||||
|
||||
var result = YAML.parse_and_validate(user_config, config_schema)
|
||||
|
||||
# Check for parse errors
|
||||
if result.has_error():
|
||||
push_error("YAML parse error: %s" % result.get_error())
|
||||
return
|
||||
|
||||
# Check for validation errors using YAMLResult methods
|
||||
if result.has_validation_errors():
|
||||
# Show detailed error report to user
|
||||
print("Configuration has %d error(s):" % result.get_validation_error_count())
|
||||
print(result.get_validation_summary())
|
||||
|
||||
# Or iterate through individual errors
|
||||
for error in result.get_validation_errors():
|
||||
print(" - %s at %s" % [error.message, error.instance_path])
|
||||
return
|
||||
|
||||
# Both parsing and validation succeeded
|
||||
var config = result.get_data()
|
||||
apply_settings(config)
|
||||
```
|
||||
|
||||
### Auto-Discovery with `$schema`
|
||||
|
||||
Include a `$schema` field in your YAML to automatically validate against a registered schema:
|
||||
|
||||
```gdscript
|
||||
# Register your schema once
|
||||
YAML.load_schema_from_file("res://schemas/save_game.yaml")
|
||||
# This schema has: $id: "http://mygame.com/schemas/save_game.yaml"
|
||||
|
||||
# YAML files can reference the schema directly
|
||||
var save_data = """
|
||||
$schema: "http://mygame.com/schemas/save_game.yaml"
|
||||
player:
|
||||
name: Hero
|
||||
level: 10
|
||||
checkpoint: forest_entrance
|
||||
"""
|
||||
|
||||
# Parse and validate - no need to specify schema!
|
||||
var result = YAML.parse_and_validate(save_data)
|
||||
|
||||
# YAMLResult provides validation checking methods
|
||||
if result.has_error():
|
||||
push_error("Parse error: %s" % result.get_error())
|
||||
elif result.has_validation_errors():
|
||||
push_error("Validation failed:\n%s" % result.get_validation_summary())
|
||||
else:
|
||||
# Safe to use - both parsing and validation succeeded
|
||||
load_game(result.get_data())
|
||||
```
|
||||
|
||||
### Advanced Schema Features
|
||||
|
||||
JSON Schema Draft-7 offers powerful composition and validation features:
|
||||
|
||||
```gdscript
|
||||
var advanced_schema = YAML.load_schema_from_string("""
|
||||
$defs:
|
||||
# Reusable definitions
|
||||
positive_integer:
|
||||
type: integer
|
||||
minimum: 1
|
||||
|
||||
item_base:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
value:
|
||||
$ref: "#/$defs/positive_integer"
|
||||
required: [name, value]
|
||||
|
||||
type: object
|
||||
properties:
|
||||
inventory:
|
||||
type: array
|
||||
items:
|
||||
# Logical composition - item must match base AND have quantity
|
||||
allOf:
|
||||
- $ref: "#/$defs/item_base"
|
||||
- type: object
|
||||
properties:
|
||||
quantity:
|
||||
$ref: "#/$defs/positive_integer"
|
||||
|
||||
equipment:
|
||||
# Conditional validation
|
||||
if:
|
||||
properties:
|
||||
type:
|
||||
const: weapon
|
||||
then:
|
||||
properties:
|
||||
damage:
|
||||
type: integer
|
||||
minimum: 1
|
||||
required: [damage]
|
||||
else:
|
||||
properties:
|
||||
defense:
|
||||
type: integer
|
||||
minimum: 1
|
||||
required: [defense]
|
||||
""")
|
||||
```
|
||||
|
||||
### Detailed Validation Error Reports
|
||||
|
||||
When validation fails, `YAMLResult` provides comprehensive error information through dedicated methods:
|
||||
|
||||
```gdscript
|
||||
var schema = YAML.load_schema_from_string("""
|
||||
type: object
|
||||
properties:
|
||||
player:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
minLength: 3
|
||||
age:
|
||||
type: integer
|
||||
minimum: 0
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
required: [name, email]
|
||||
""")
|
||||
|
||||
var invalid_yaml = """
|
||||
player:
|
||||
name: ab
|
||||
age: -5
|
||||
email: not-email
|
||||
"""
|
||||
|
||||
var result = YAML.parse_and_validate(invalid_yaml, schema)
|
||||
|
||||
# Check parse error first
|
||||
if result.has_error():
|
||||
push_error("YAML syntax error: %s" % result.get_error())
|
||||
return
|
||||
|
||||
# Then check validation with YAMLResult methods
|
||||
if result.has_validation_errors():
|
||||
# Get formatted summary for display
|
||||
print(result.get_validation_summary())
|
||||
# Schema validation failed with 3 error(s):
|
||||
# [1] At '/player/name': String length 2 is less than minimum 3 (minLength)
|
||||
# [2] At '/player/age': Value -5 is less than minimum 0 (minimum)
|
||||
# [3] At '/player/email': String "not-email" does not match format email (format)
|
||||
|
||||
# Get error count
|
||||
print("\nTotal errors: %d" % result.get_validation_error_count())
|
||||
|
||||
# Iterate through individual errors
|
||||
for error in result.get_validation_errors():
|
||||
print("\nError details:")
|
||||
print(" Path: %s" % error.instance_path)
|
||||
print(" Constraint: %s" % error.keyword)
|
||||
print(" Message: %s" % error.message)
|
||||
print(" Invalid value: %s" % error.invalid_value)
|
||||
|
||||
# Or use the full SchemaValidationResult for advanced inspection
|
||||
var validation = result.get_validation_result()
|
||||
print("\nAll error paths: %s" % validation.get_all_error_paths())
|
||||
print("Violated constraints: %s" % validation.get_violated_constraints())
|
||||
```
|
||||
|
||||
For more information on JSON Schema features, see the [GDSchema documentation](https://github.com/fimbul-works/gdschema).
|
||||
|
||||
### Understanding Parse vs Validation Errors
|
||||
|
||||
When using `YAML.parse_and_validate()`, the `YAMLResult` may contain two types of errors:
|
||||
|
||||
1. **Parse Errors** (checked with `has_error()`): YAML syntax errors that prevent parsing
|
||||
2. **Validation Errors** (checked with `has_validation_errors()`): Schema validation failures after successful parsing
|
||||
|
||||
Always check parse errors first, as validation only happens if parsing succeeds:
|
||||
|
||||
```gdscript
|
||||
var result = YAML.parse_and_validate(yaml_text, schema)
|
||||
|
||||
# Check parse error first
|
||||
if result.has_error():
|
||||
print("YAML syntax error at line %d: %s" % [
|
||||
result.get_error_line(),
|
||||
result.get_error_message()
|
||||
])
|
||||
return
|
||||
|
||||
# Then check validation
|
||||
if result.has_validation_errors():
|
||||
print("Data is valid YAML but doesn't match schema:")
|
||||
print(result.get_validation_summary())
|
||||
return
|
||||
|
||||
# Both checks passed - safe to use
|
||||
var data = result.get_data()
|
||||
```
|
||||
|
||||
## Multi-Document YAML Support
|
||||
|
||||
```gdscript
|
||||
var yaml_text = """
|
||||
# Player stats
|
||||
name: Hero
|
||||
health: 100
|
||||
---
|
||||
# Game settings
|
||||
difficulty: hard
|
||||
enable_tutorial: false
|
||||
"""
|
||||
|
||||
var result = YAML.parse(yaml_text)
|
||||
if !result.has_error():
|
||||
# Check if the result contains multiple documents
|
||||
print("Has multiple documents: %s" % result.has_multiple_documents())
|
||||
|
||||
# Check document count
|
||||
var doc_count = result.get_document_count()
|
||||
print("Found %d documents" % doc_count)
|
||||
|
||||
# Get the documents
|
||||
var player_data = result.get_document(0)
|
||||
var settings = result.get_document(1)
|
||||
|
||||
print("Player: %s (Health: %d)" % [player_data.name, player_data.health])
|
||||
print("Difficulty: %s" % settings.difficulty)
|
||||
|
||||
# Get documents as an array
|
||||
var docs = result.get_documents()
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The `YAMLResult` class provides detailed error information:
|
||||
|
||||
```gdscript
|
||||
var result = YAML.parse(user_yaml)
|
||||
|
||||
if result.has_error():
|
||||
push_error("YAML parse error: " + result.get_error())
|
||||
# Example output: "parse error (line 3, column 5)"
|
||||
|
||||
# Get detailed error information
|
||||
var error_message = result.get_error_message()
|
||||
var error_line = result.get_error_line()
|
||||
var error_column = result.get_error_column()
|
||||
|
||||
print("Error at line %d, column %d: %s" % [error_line, error_column, error_message])
|
||||
|
||||
# Highlight the error position
|
||||
if error_line > 0 and error_column > 0:
|
||||
var yaml_lines = yaml_text.split("\n")
|
||||
var error_line_content = yaml_lines[error_line - 1]
|
||||
print(error_line_content)
|
||||
print(" ".repeat(error_column - 1) + "^ Error here")
|
||||
```
|
||||
|
||||
## Custom Class Serialization
|
||||
|
||||
You can register your custom GDScript classes for seamless serialization:
|
||||
|
||||
```gdscript
|
||||
# Define a custom class
|
||||
class_name Item extends Resource
|
||||
|
||||
var name: String
|
||||
var weight: float
|
||||
var value: int
|
||||
|
||||
func _init(p_name = "", p_weight = 0.0, p_value = 0):
|
||||
name = p_name
|
||||
weight = p_weight
|
||||
value = p_value
|
||||
|
||||
static func deserialize(data):
|
||||
if typeof(data) != TYPE_DICTIONARY:
|
||||
return YAMLResult.error("Item requires a dictionary")
|
||||
|
||||
return Item.new(
|
||||
data.get("name", ""),
|
||||
data.get("weight", 0.0),
|
||||
data.get("value", 0)
|
||||
)
|
||||
|
||||
func serialize():
|
||||
return {
|
||||
"name": name,
|
||||
"weight": weight,
|
||||
"value": value
|
||||
}
|
||||
|
||||
# Register the class for YAML serialization
|
||||
YAML.register_class(Item)
|
||||
|
||||
# Now we can serialize/deserialize Item objects
|
||||
var sword = Item.new("Iron Sword", 5.0, 100)
|
||||
var result = YAML.stringify(sword)
|
||||
print(result.get_data())
|
||||
# Output: !Item {name: Iron Sword, weight: 5.0, value: 100}
|
||||
```
|
||||
|
||||
## Security Controls with YAMLSecurity
|
||||
|
||||
The `YAMLSecurity` class helps guard against unsafe loading of untrusted content:
|
||||
|
||||
```gdscript
|
||||
var security = YAML.create_security()
|
||||
|
||||
# Only allow textures from the game's asset folder
|
||||
security.allow_path("res://assets/textures", ["Texture2D"])
|
||||
|
||||
# Block scenes for safety
|
||||
security.block_type("PackedScene")
|
||||
|
||||
# Parse YAML with custom security settings
|
||||
var yaml_text = """
|
||||
player:
|
||||
name: Hero
|
||||
sprite: !Resource 'res://assets/textures/player.png'
|
||||
"""
|
||||
|
||||
var result = YAML.parse(yaml_text, security)
|
||||
if result.has_error():
|
||||
push_error(result.get_error())
|
||||
else:
|
||||
var data = result.get_data()
|
||||
print("Player sprite loaded: " + str(data.player.sprite is Texture2D))
|
||||
```
|
||||
|
||||
## Style Customization with YAMLStyle
|
||||
|
||||
Control the formatting and appearance of your YAML output:
|
||||
|
||||
```gdscript
|
||||
# Create a new style configuration
|
||||
var style = YAML.create_style()
|
||||
|
||||
# Set global string style to double-quoted
|
||||
style.set_string_style(YAMLStyle.STRING_QUOTE_DOUBLE)
|
||||
|
||||
# Set integers to display in hexadecimal format
|
||||
style.set_integer_format(YAMLStyle.INT_HEX)
|
||||
|
||||
# Define specific style for player inventory items to use flow style
|
||||
var inventory_style = style.create_child("inventory")
|
||||
inventory_style.set_flow_style(YAMLStyle.FLOW_SINGLE)
|
||||
|
||||
# Create some data to format
|
||||
var player_data = {
|
||||
"name": "Hero",
|
||||
"level": 42,
|
||||
"inventory": ["Sword", "Shield", "Potion"]
|
||||
}
|
||||
|
||||
# Apply the style when stringifying
|
||||
var result = YAML.stringify(player_data, style)
|
||||
print(result.get_data())
|
||||
|
||||
# Output will look like:
|
||||
# name: "Hero"
|
||||
# level: 0x2A
|
||||
# inventory: ["Sword", "Shield", "Potion"]
|
||||
```
|
||||
|
||||
### Style Detection and Preservation
|
||||
|
||||
You can detect and preserve the style of existing YAML:
|
||||
|
||||
```gdscript
|
||||
# Parse with style detection
|
||||
var result = YAML.parse(yaml_text, null, true)
|
||||
|
||||
if !result.has_error() and result.has_style():
|
||||
var data = result.get_data()
|
||||
var style = result.get_style()
|
||||
|
||||
# Modify the data but preserve formatting
|
||||
data.player.health = 200
|
||||
|
||||
# Reserialize with the same style
|
||||
var output = YAML.stringify(data, style)
|
||||
save_file("user://modified_config.yaml", output.get_data())
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Check out the [`examples/`](./addons/yaml/examples/) directory for comprehensive code samples covering:
|
||||
|
||||
- Basic parsing and stringification
|
||||
- Multi-document YAML handling
|
||||
- Custom class serialization
|
||||
- **Schema validation workflows**
|
||||
- Security configurations
|
||||
- Style customization
|
||||
- Error handling patterns
|
||||
- And more!
|
||||
|
||||
## Installation
|
||||
|
||||
1. Download the plugin from the Godot Asset Library or from the GitHub repository
|
||||
2. Extract the contents into your project's `addons/` directory
|
||||
3. Enable the plugin in Project Settings → Plugins
|
||||
|
||||
## License
|
||||
|
||||
MIT License - See LICENSE file for details.
|
||||
|
||||
---
|
||||
|
||||
Built with ⚡ by [FimbulWorks](https://github.com/fimbul-works)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,14 +0,0 @@
|
||||
class_name YAMLCodeEdit extends CodeEdit
|
||||
|
||||
var syntax_highlighter_script = preload("res://addons/yaml/editor/syntax_highlighting/syntax_highlighter.gd")
|
||||
|
||||
func _init() -> void:
|
||||
# YAML indentation
|
||||
set_indent_size(2)
|
||||
set_indent_using_spaces(true)
|
||||
indent_automatic = true
|
||||
indent_automatic_prefixes = [":"]
|
||||
|
||||
# Syntax highlighting
|
||||
if not syntax_highlighter:
|
||||
syntax_highlighter = syntax_highlighter_script.new()
|
||||
@@ -1 +0,0 @@
|
||||
uid://bex4rlxea675g
|
||||
@@ -1,394 +0,0 @@
|
||||
@tool
|
||||
class_name YAMLCodeEditor extends CodeEdit
|
||||
|
||||
signal content_changed
|
||||
signal save_requested
|
||||
signal close_requested
|
||||
signal undo_requested
|
||||
signal redo_requested
|
||||
signal validation_requested
|
||||
signal zoom_changed(zoom_level)
|
||||
|
||||
var error_indicators := {}
|
||||
var snapshot_debounce_timer: Timer
|
||||
var error_line_color: Color = Color(1.0, 0.3, 0.3, 0.1)
|
||||
var syntax_highlighter_script = preload("res://addons/yaml/editor/syntax_highlighting/editor_syntax_highlighter.gd")
|
||||
var suppress_text_changed: bool = false
|
||||
|
||||
# Zoom functionality variables
|
||||
var zoom_level: float = 1.0 # 100%
|
||||
var default_font_size: int = 14 # Default font size
|
||||
|
||||
func _ready() -> void:
|
||||
# Clear text to reset the editor state
|
||||
text = ""
|
||||
|
||||
# YAML indentation
|
||||
set_indent_size(2)
|
||||
set_indent_using_spaces(true)
|
||||
indent_automatic_prefixes = [":"]
|
||||
scroll_smooth = true
|
||||
set_highlight_current_line(true)
|
||||
|
||||
# Syntax highlighting
|
||||
if not syntax_highlighter:
|
||||
syntax_highlighter = syntax_highlighter_script.new()
|
||||
|
||||
# Do not lose selection when focus is lost
|
||||
deselect_on_focus_loss_enabled = false
|
||||
set_focus_mode(Control.FOCUS_ALL)
|
||||
|
||||
# Create debounce timer for content changes
|
||||
snapshot_debounce_timer = Timer.new()
|
||||
add_child(snapshot_debounce_timer)
|
||||
snapshot_debounce_timer.one_shot = true
|
||||
snapshot_debounce_timer.wait_time = 0.3 # 300ms
|
||||
snapshot_debounce_timer.timeout.connect(_on_snapshot_debounce_timeout)
|
||||
|
||||
# Connect signals
|
||||
text_changed.connect(_on_text_changed)
|
||||
gui_input.connect(_on_gui_input_focus)
|
||||
|
||||
# Register YAML code completion
|
||||
register_yaml_code_completion()
|
||||
|
||||
# Apply initial font size
|
||||
_update_font_size()
|
||||
|
||||
func _on_text_changed() -> void:
|
||||
if suppress_text_changed:
|
||||
return
|
||||
|
||||
# Clear error indicators when text changes
|
||||
clear_error_indicators()
|
||||
|
||||
# Request a snapshot with debounce
|
||||
snapshot_debounce_timer.start()
|
||||
|
||||
func _on_snapshot_debounce_timeout() -> void:
|
||||
# Emit content changed signal
|
||||
content_changed.emit()
|
||||
|
||||
# Request validation
|
||||
validation_requested.emit()
|
||||
|
||||
func _on_gui_input_focus(event: InputEvent) -> void:
|
||||
# Grab focus when clicked
|
||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
grab_focus()
|
||||
|
||||
func cut_selection() -> void:
|
||||
if has_selection():
|
||||
# Cut the selected text to clipboard
|
||||
DisplayServer.clipboard_set(get_selected_text())
|
||||
delete_selection()
|
||||
else:
|
||||
# If no selection, cut the current line (like default script editor)
|
||||
var line := get_caret_line()
|
||||
var line_text := get_line(line)
|
||||
DisplayServer.clipboard_set(line_text)
|
||||
|
||||
# Delete the current line
|
||||
select(line, 0, line, line_text.length())
|
||||
delete_selection()
|
||||
|
||||
# If this isn't the last line, also remove the line break
|
||||
if line < get_line_count() - 1:
|
||||
select(line, 0, line + 1, 0)
|
||||
delete_selection()
|
||||
|
||||
# Trigger content changed
|
||||
text_changed.emit()
|
||||
|
||||
func copy_selection() -> void:
|
||||
if has_selection():
|
||||
# Copy selected text to clipboard
|
||||
DisplayServer.clipboard_set(get_selected_text())
|
||||
else:
|
||||
# If no selection, copy the current line
|
||||
var line := get_caret_line()
|
||||
var line_text := get_line(line)
|
||||
DisplayServer.clipboard_set(line_text)
|
||||
|
||||
func paste_clipboard() -> void:
|
||||
# Get clipboard content
|
||||
var clipboard = DisplayServer.clipboard_get()
|
||||
if clipboard.is_empty():
|
||||
return
|
||||
|
||||
if has_selection():
|
||||
# Replace selected text with clipboard content
|
||||
delete_selection()
|
||||
|
||||
# Insert clipboard content at caret position
|
||||
insert_text_at_caret(clipboard)
|
||||
text_changed.emit()
|
||||
|
||||
# Zoom management functions
|
||||
func zoom_in() -> void:
|
||||
zoom_level = min(zoom_level + 0.07, 3.0) # Max 200%
|
||||
_update_font_size()
|
||||
zoom_changed.emit(zoom_level)
|
||||
|
||||
func zoom_out() -> void:
|
||||
zoom_level = max(zoom_level - 0.07, 0.25) # Min 50%
|
||||
_update_font_size()
|
||||
zoom_changed.emit(zoom_level)
|
||||
|
||||
func zoom_reset() -> void:
|
||||
zoom_level = 1.0
|
||||
_update_font_size()
|
||||
zoom_changed.emit(zoom_level)
|
||||
|
||||
func set_zoom(zoom: float) -> void:
|
||||
zoom_level = max(0.25, min(3.0, zoom))
|
||||
_update_font_size()
|
||||
zoom_changed.emit(zoom_level)
|
||||
|
||||
func _update_font_size() -> void:
|
||||
var new_size = int(default_font_size * zoom_level)
|
||||
add_theme_font_size_override("font_size", new_size)
|
||||
|
||||
func _unhandled_key_input(event: InputEvent) -> void:
|
||||
# Handle tab key before focus system gets it
|
||||
if event is InputEventKey and event.pressed and has_focus():
|
||||
match event.keycode:
|
||||
KEY_TAB:
|
||||
if event.shift_pressed:
|
||||
# Handle Shift+Tab for unindent
|
||||
_handle_unindent()
|
||||
else:
|
||||
# Handle Tab for indent
|
||||
_handle_indent()
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
# Handle shortcuts for saving/closing
|
||||
if event is InputEventKey and event.pressed:
|
||||
match event.get_keycode_with_modifiers():
|
||||
KEY_MASK_CTRL | KEY_S:
|
||||
save_requested.emit()
|
||||
get_viewport().set_input_as_handled()
|
||||
KEY_MASK_CTRL | KEY_W:
|
||||
close_requested.emit()
|
||||
get_viewport().set_input_as_handled()
|
||||
KEY_ENTER, KEY_KP_ENTER:
|
||||
# Handle auto-continuation of YAML structures
|
||||
_handle_enter_key()
|
||||
get_viewport().set_input_as_handled()
|
||||
KEY_MASK_CTRL | KEY_Z:
|
||||
# Handle undo
|
||||
undo_requested.emit()
|
||||
get_viewport().set_input_as_handled()
|
||||
KEY_MASK_CTRL | KEY_Y, KEY_MASK_CTRL | KEY_MASK_SHIFT | KEY_Z:
|
||||
# Handle redo (supports both Ctrl+Y and Ctrl+Shift+Z)
|
||||
redo_requested.emit()
|
||||
get_viewport().set_input_as_handled()
|
||||
KEY_MASK_CTRL | KEY_EQUAL, KEY_MASK_CTRL | KEY_KP_ADD:
|
||||
zoom_in()
|
||||
KEY_MASK_CTRL | KEY_MINUS, KEY_MASK_CTRL | KEY_KP_SUBTRACT:
|
||||
zoom_out()
|
||||
KEY_MASK_CTRL | KEY_0, KEY_MASK_CTRL | KEY_KP_0:
|
||||
zoom_reset()
|
||||
if event is InputEventMouseButton and event.pressed and event.is_command_or_control_pressed():
|
||||
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
|
||||
zoom_in()
|
||||
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
|
||||
zoom_out()
|
||||
|
||||
func set_text_and_preserve_state(new_text: String, preserve_state: bool = true) -> void:
|
||||
if preserve_state:
|
||||
# Save current state
|
||||
var previous_caret_pos := get_caret_column()
|
||||
var previous_line := get_caret_line()
|
||||
var previous_scroll_v := get_v_scroll_bar().value
|
||||
var previous_scroll_h := get_h_scroll_bar().value
|
||||
|
||||
# Set text without triggering our own text_changed handler
|
||||
suppress_text_changed = true
|
||||
text = new_text
|
||||
suppress_text_changed = false
|
||||
|
||||
# Restore state if possible
|
||||
if previous_line < get_line_count():
|
||||
set_caret_line(previous_line)
|
||||
var line_length := get_line(previous_line).length()
|
||||
if previous_caret_pos <= line_length:
|
||||
set_caret_column(previous_caret_pos)
|
||||
|
||||
# Restore scroll position (with a small delay to ensure the text is updated first)
|
||||
call_deferred("_restore_scroll_position", previous_scroll_v, previous_scroll_h)
|
||||
else:
|
||||
# Just set the text without preserving state
|
||||
suppress_text_changed = true
|
||||
text = new_text
|
||||
suppress_text_changed = false
|
||||
|
||||
func _restore_scroll_position(v_scroll: float, h_scroll: float) -> void:
|
||||
# Wait for one frame to ensure the text has been updated and rendered
|
||||
if get_tree():
|
||||
await get_tree().process_frame
|
||||
get_v_scroll_bar().value = v_scroll
|
||||
get_h_scroll_bar().value = h_scroll
|
||||
|
||||
func _handle_indent() -> void:
|
||||
# Get current line and text
|
||||
var line := get_caret_line()
|
||||
var line_text := get_line(line)
|
||||
|
||||
# Get selection so we can handle multi-line indentation
|
||||
var selection_active := has_selection()
|
||||
var selection_from := get_selection_from_line()
|
||||
var selection_to := get_selection_to_line()
|
||||
|
||||
if selection_active:
|
||||
# Indent multiple lines
|
||||
begin_complex_operation()
|
||||
for i in range(selection_from, selection_to + 1):
|
||||
set_line(i, " " + get_line(i))
|
||||
end_complex_operation()
|
||||
else:
|
||||
# Simple indent - insert 2 spaces at caret position
|
||||
insert_text_at_caret(" ")
|
||||
|
||||
# Trigger text changed to update the document
|
||||
text_changed.emit()
|
||||
|
||||
func _handle_unindent() -> void:
|
||||
# Get current line and text
|
||||
var line := get_caret_line()
|
||||
var text := get_line(line)
|
||||
|
||||
# Get selection so we can handle multi-line unindentation
|
||||
var selection_active := has_selection()
|
||||
var selection_from := get_selection_from_line()
|
||||
var selection_to := get_selection_to_line()
|
||||
|
||||
if selection_active:
|
||||
# Unindent multiple lines
|
||||
begin_complex_operation()
|
||||
for i in range(selection_from, selection_to + 1):
|
||||
var line_text := get_line(i)
|
||||
if line_text.begins_with(" "):
|
||||
set_line(i, line_text.substr(2))
|
||||
elif line_text.begins_with(" "):
|
||||
set_line(i, line_text.substr(1))
|
||||
end_complex_operation()
|
||||
else:
|
||||
# Simple unindent - remove up to 2 spaces from beginning of line
|
||||
if text.begins_with(" "):
|
||||
set_line(line, text.substr(2))
|
||||
set_caret_column(max(0, get_caret_column() - 2))
|
||||
elif text.begins_with(" "):
|
||||
set_line(line, text.substr(1))
|
||||
set_caret_column(max(0, get_caret_column() - 1))
|
||||
|
||||
func _handle_enter_key() -> void:
|
||||
var line := get_caret_line()
|
||||
var line_text := get_line(line)
|
||||
|
||||
# Auto-continuation for lists
|
||||
if "- " in line_text:
|
||||
var indent_level := 0
|
||||
for c in line_text:
|
||||
if c == ' ':
|
||||
indent_level += 1
|
||||
else:
|
||||
break
|
||||
|
||||
# Insert the line with the same indentation and list marker
|
||||
var new_line := "\n" + " ".repeat(indent_level) + "- "
|
||||
insert_text_at_caret(new_line)
|
||||
else:
|
||||
# Regular line break with preserved indentation
|
||||
var indent_level := 0
|
||||
for c in line_text:
|
||||
if c == ' ':
|
||||
indent_level += 1
|
||||
else:
|
||||
break
|
||||
|
||||
# Increased indentation if the line ends with a colon
|
||||
if line_text.strip_edges().ends_with(":"):
|
||||
indent_level += 2
|
||||
|
||||
insert_text_at_caret("\n" + " ".repeat(indent_level))
|
||||
|
||||
func register_yaml_code_completion() -> void:
|
||||
# Register common YAML keywords and patterns for code completion
|
||||
var keyword_list: PackedStringArray = [
|
||||
"true",
|
||||
"false",
|
||||
"null",
|
||||
"~",
|
||||
"INF",
|
||||
"-INF"
|
||||
]
|
||||
|
||||
# Add keywords and tags to completion
|
||||
for keyword in keyword_list:
|
||||
add_code_completion_option(CodeCompletionKind.KIND_CONSTANT, keyword, keyword)
|
||||
|
||||
## YAML tags
|
||||
var tag_list: PackedStringArray = [
|
||||
"!Resource",
|
||||
"!AABB",
|
||||
"!Basis",
|
||||
"!Color",
|
||||
"!NodePath",
|
||||
"!PackedByteArray",
|
||||
"!PackedColorArray",
|
||||
"!PackedFloat32Array",
|
||||
"!PackedFloat64Array",
|
||||
"!PackedInt32Array",
|
||||
"!PackedInt64Array",
|
||||
"!PackedStringArray",
|
||||
"!PackedVector2Array",
|
||||
"!PackedVector3Array",
|
||||
"!Plane",
|
||||
"!Projection",
|
||||
"!Quaternion",
|
||||
"!Rect2",
|
||||
"!Rect2i",
|
||||
"!StringName",
|
||||
"!Transform2D",
|
||||
"!Transform3D",
|
||||
"!Vector2",
|
||||
"!Vector2i",
|
||||
"!Vector3",
|
||||
"!Vector3i",
|
||||
"!Vector4",
|
||||
"!Vector4i"
|
||||
]
|
||||
|
||||
for tag in tag_list:
|
||||
add_code_completion_option(CodeCompletionKind.KIND_CLASS, tag, tag)
|
||||
|
||||
func mark_error_line(line: int, message: String) -> void:
|
||||
if line < 0 or line >= get_line_count():
|
||||
return
|
||||
|
||||
# Set line background to error color
|
||||
var error_color: Color = EditorInterface.get_editor_settings().get_setting("text_editor/theme/highlighting/mark_color")
|
||||
set_line_background_color(line, error_color)
|
||||
|
||||
# Set gutter icon
|
||||
var error_icon := get_theme_icon("StatusError", "EditorIcons")
|
||||
if error_icon:
|
||||
set_line_gutter_icon(line, 0, error_icon)
|
||||
|
||||
# Store for later reference
|
||||
error_indicators[line] = message
|
||||
|
||||
func clear_error_indicators() -> void:
|
||||
for line: int in error_indicators:
|
||||
set_line_background_color(line, Color(0, 0, 0, 0))
|
||||
set_line_gutter_icon(line, 0, null)
|
||||
|
||||
error_indicators.clear()
|
||||
|
||||
func get_current_line_col_info() -> Array[int]:
|
||||
var line := get_caret_line() + 1
|
||||
var col := get_caret_column() + 1
|
||||
return [line, col]
|
||||
@@ -1 +0,0 @@
|
||||
uid://drkui6da5o1ou
|
||||
@@ -1,136 +0,0 @@
|
||||
@tool
|
||||
class_name YAMLEditorDocument extends RefCounted
|
||||
|
||||
# Primary document data
|
||||
var path: String
|
||||
var content: String
|
||||
var is_modified: bool = false
|
||||
var validation_result: YAMLResult
|
||||
|
||||
# History management
|
||||
class YAMLEditorHistoryState extends RefCounted:
|
||||
var text: String
|
||||
var caret_line: int = 0
|
||||
var caret_column: int = 0
|
||||
|
||||
func _init(p_text: String, p_line: int = 0, p_column: int = 0) -> void:
|
||||
text = p_text
|
||||
caret_line = p_line
|
||||
caret_column = p_column
|
||||
|
||||
func _to_string() -> String:
|
||||
return "YAMLEditorHistoryState(text_length=%d, line=%d, column=%d)" % [text.length(), caret_line, caret_column]
|
||||
|
||||
# Limit history size to prevent excessive memory use
|
||||
const MAX_HISTORY := 100
|
||||
|
||||
var history_states: Array[YAMLEditorHistoryState] = []
|
||||
var current_history_index: int = -1
|
||||
var saved_history_index: int = -1
|
||||
|
||||
# Signals
|
||||
signal content_changed(document)
|
||||
signal validation_changed(document)
|
||||
signal modified_changed(document)
|
||||
|
||||
# Constructor
|
||||
func _init(p_path: String, p_content: String = "") -> void:
|
||||
path = p_path
|
||||
content = p_content
|
||||
validation_result = YAMLResult.new() # Empty result
|
||||
|
||||
# Take initial snapshot if content isn't empty
|
||||
if not p_content.is_empty():
|
||||
_add_history_state(YAMLEditorHistoryState.new(p_content))
|
||||
|
||||
# File path utilities
|
||||
func get_file_name() -> String:
|
||||
return path.get_file()
|
||||
|
||||
func is_untitled() -> bool:
|
||||
return path.begins_with("untitled")
|
||||
|
||||
# Content management
|
||||
func set_content(new_content: String, caret_line: int = 0, caret_column: int = 0) -> void:
|
||||
if content == new_content:
|
||||
return
|
||||
|
||||
content = new_content
|
||||
_add_history_state(YAMLEditorHistoryState.new(new_content, caret_line, caret_column))
|
||||
set_modified(true)
|
||||
content_changed.emit(self)
|
||||
|
||||
# Modification state
|
||||
func set_modified(modified: bool) -> void:
|
||||
if is_modified == modified:
|
||||
return
|
||||
|
||||
is_modified = modified
|
||||
modified_changed.emit(self)
|
||||
|
||||
# Validation management
|
||||
func set_validation_result(result: YAMLResult) -> void:
|
||||
validation_result = result
|
||||
validation_changed.emit(self)
|
||||
|
||||
func has_error() -> bool:
|
||||
return validation_result and validation_result.has_error()
|
||||
|
||||
# History management
|
||||
func can_undo() -> bool:
|
||||
return current_history_index > 0 # Need at least one previous state
|
||||
|
||||
func can_redo() -> bool:
|
||||
return current_history_index < history_states.size() - 1
|
||||
|
||||
func undo() -> YAMLEditorHistoryState:
|
||||
if not can_undo():
|
||||
return null
|
||||
|
||||
current_history_index -= 1
|
||||
var state := history_states[current_history_index]
|
||||
content = state.text
|
||||
|
||||
# Update modification state
|
||||
set_modified(current_history_index != saved_history_index)
|
||||
content_changed.emit(self)
|
||||
|
||||
return state
|
||||
|
||||
func redo() -> YAMLEditorHistoryState:
|
||||
if not can_redo():
|
||||
return null
|
||||
|
||||
current_history_index += 1
|
||||
var state := history_states[current_history_index]
|
||||
content = state.text
|
||||
|
||||
# Update modification state
|
||||
set_modified(current_history_index != saved_history_index)
|
||||
content_changed.emit(self)
|
||||
|
||||
return state
|
||||
|
||||
func mark_saved() -> void:
|
||||
saved_history_index = current_history_index
|
||||
set_modified(false)
|
||||
|
||||
func _add_history_state(state: YAMLEditorHistoryState) -> void:
|
||||
# If we're not at the end of history, truncate future states
|
||||
if current_history_index < history_states.size() - 1:
|
||||
history_states = history_states.slice(0, current_history_index + 1)
|
||||
|
||||
# Add the new state
|
||||
history_states.append(state)
|
||||
current_history_index = history_states.size() - 1
|
||||
|
||||
if history_states.size() > MAX_HISTORY:
|
||||
var excess := history_states.size() - MAX_HISTORY
|
||||
history_states = history_states.slice(excess)
|
||||
current_history_index -= excess
|
||||
|
||||
# Adjust saved index if needed
|
||||
if saved_history_index >= 0:
|
||||
saved_history_index -= excess
|
||||
if saved_history_index < 0:
|
||||
saved_history_index = -1
|
||||
@@ -1 +0,0 @@
|
||||
uid://5ow36wsuc7uj
|
||||
@@ -1,578 +0,0 @@
|
||||
@tool
|
||||
class_name YAMLEditorDocumentManager extends Node
|
||||
|
||||
signal document_changed(document)
|
||||
signal document_created(document)
|
||||
signal document_closed(document)
|
||||
|
||||
# Dictionary of open documents: {path: YAMLEditorDocument}
|
||||
var documents: Dictionary = {}
|
||||
var current_document: YAMLEditorDocument = null
|
||||
|
||||
# UI components
|
||||
var file_list: YAMLEditorFileList
|
||||
var code_editor: YAMLCodeEditor
|
||||
var file_popup_menu: PopupMenu
|
||||
var editor_node: Control
|
||||
|
||||
# Reference to the singleton
|
||||
var file_system: YAMLFileSystem
|
||||
|
||||
# Track recently saved files to avoid external update conflicts
|
||||
var recently_saved_files: Dictionary = {}
|
||||
var ignore_update_timer: Timer
|
||||
|
||||
func _init(_editor: Control) -> void:
|
||||
editor_node = _editor
|
||||
|
||||
func _ready() -> void:
|
||||
# Get singleton reference
|
||||
file_system = YAMLFileSystem.get_singleton()
|
||||
|
||||
# Listen for external file updates
|
||||
file_system.file_updated.connect(_on_external_file_updated)
|
||||
file_system.file_renamed.connect(_on_file_renamed)
|
||||
|
||||
# Create file popup menu
|
||||
file_popup_menu = PopupMenu.new()
|
||||
add_child(file_popup_menu)
|
||||
|
||||
# Add menu items
|
||||
file_popup_menu.add_item("Save", 0)
|
||||
file_popup_menu.add_item("Save As...", 1)
|
||||
file_popup_menu.add_separator()
|
||||
file_popup_menu.add_item("Close", 2)
|
||||
file_popup_menu.add_separator()
|
||||
file_popup_menu.add_item("Show in FileSystem", 3)
|
||||
|
||||
# Connect popup menu signals
|
||||
file_popup_menu.id_pressed.connect(_on_file_popup_menu_id_pressed)
|
||||
|
||||
# Create timer for clearing recent saves
|
||||
ignore_update_timer = Timer.new()
|
||||
add_child(ignore_update_timer)
|
||||
ignore_update_timer.one_shot = true
|
||||
ignore_update_timer.wait_time = 0.5 # 500ms
|
||||
ignore_update_timer.timeout.connect(_on_ignore_update_timer_timeout)
|
||||
|
||||
# Update UI
|
||||
update_ui()
|
||||
|
||||
func setup(p_file_list: YAMLEditorFileList, p_code_editor: YAMLCodeEditor) -> void:
|
||||
file_list = p_file_list
|
||||
code_editor = p_code_editor
|
||||
|
||||
# Connect signals from file list component
|
||||
file_list.file_selected.connect(_on_file_selected)
|
||||
file_list.file_context_requested.connect(_on_file_context_requested)
|
||||
|
||||
func create_document(path: String, content: String = "") -> YAMLEditorDocument:
|
||||
var normalized_path = _normalize_path(path)
|
||||
|
||||
# Check if document already exists with normalized path
|
||||
if documents.has(normalized_path):
|
||||
return documents[normalized_path]
|
||||
|
||||
var document := YAMLEditorDocument.new(normalized_path, content)
|
||||
|
||||
# Connect document signals
|
||||
document.content_changed.connect(_on_document_content_changed)
|
||||
document.modified_changed.connect(_on_document_modified_changed)
|
||||
|
||||
# Store document
|
||||
documents[normalized_path] = document
|
||||
document_created.emit(document)
|
||||
|
||||
return document
|
||||
|
||||
func open_file(path: String) -> void:
|
||||
var normalized_path = _normalize_path(path)
|
||||
|
||||
# Check if already open
|
||||
if documents.has(normalized_path):
|
||||
set_current_document(documents[normalized_path])
|
||||
return
|
||||
|
||||
# Look for any document with the same base filename
|
||||
var filename = normalized_path.get_file()
|
||||
for existing_path in documents.keys():
|
||||
if existing_path.get_file() == filename and existing_path != normalized_path:
|
||||
# Check if they point to the same actual file
|
||||
if _paths_point_to_same_file(normalized_path, existing_path):
|
||||
set_current_document(documents[existing_path])
|
||||
return
|
||||
|
||||
# Use the file system singleton to read the file
|
||||
var content := file_system.read_file(normalized_path)
|
||||
if typeof(content) == TYPE_INT: # Error code
|
||||
push_error("Could not open file '%s': %s" % [normalized_path, error_string(content)])
|
||||
return
|
||||
|
||||
# Create new document
|
||||
var document := create_document(normalized_path, content)
|
||||
document.mark_saved() # Initial state is saved
|
||||
|
||||
# Switch to the new document
|
||||
set_current_document(document)
|
||||
|
||||
# Notify the file system
|
||||
file_system.notify_file_opened(normalized_path)
|
||||
|
||||
func close_document(document: YAMLEditorDocument) -> bool:
|
||||
if document == null:
|
||||
return true
|
||||
|
||||
if document.is_modified:
|
||||
# Show confirmation dialog for unsaved changes
|
||||
var dialog := ConfirmationDialog.new()
|
||||
dialog.title = "Unsaved Changes"
|
||||
dialog.dialog_text = "Save changes to '" + document.get_file_name() + "' before closing?"
|
||||
dialog.add_button("Don't Save", true, "dont_save")
|
||||
dialog.add_cancel_button("Cancel")
|
||||
|
||||
dialog.confirmed.connect(
|
||||
func():
|
||||
# Save was chosen
|
||||
if save_document(document):
|
||||
_close_document_internal(document)
|
||||
dialog.queue_free()
|
||||
)
|
||||
|
||||
dialog.custom_action.connect(
|
||||
func(action):
|
||||
if action == "dont_save":
|
||||
_close_document_internal(document)
|
||||
dialog.queue_free()
|
||||
)
|
||||
|
||||
dialog.canceled.connect(func(): dialog.queue_free())
|
||||
|
||||
add_child(dialog)
|
||||
dialog.popup_centered()
|
||||
return false
|
||||
|
||||
return _close_document_internal(document)
|
||||
|
||||
func _close_document_internal(document: YAMLEditorDocument) -> bool:
|
||||
if document == null:
|
||||
return false
|
||||
|
||||
# Find the document in our dictionary
|
||||
var path_to_remove = ""
|
||||
for path in documents.keys():
|
||||
if documents[path] == document:
|
||||
path_to_remove = path
|
||||
break
|
||||
|
||||
if path_to_remove.is_empty():
|
||||
return false
|
||||
|
||||
# Notify document is being closed
|
||||
document_closed.emit(document)
|
||||
|
||||
# Remove document
|
||||
documents.erase(document.path)
|
||||
|
||||
# If this was the current document, switch to another
|
||||
if current_document == document:
|
||||
current_document = null
|
||||
|
||||
# Select another document if available
|
||||
if not documents.is_empty():
|
||||
set_current_document(documents.values()[0])
|
||||
else:
|
||||
# Clear the editor if no documents left
|
||||
code_editor.text = ""
|
||||
|
||||
# Update UI
|
||||
update_ui()
|
||||
|
||||
# Notify the file system
|
||||
file_system.notify_file_closed(document.path)
|
||||
|
||||
return true
|
||||
|
||||
func save_document(document: YAMLEditorDocument) -> bool:
|
||||
if document == null:
|
||||
return false
|
||||
|
||||
# Don't save untitled files directly
|
||||
if document.is_untitled():
|
||||
return false # Caller should handle "Save As" dialog
|
||||
|
||||
# Mark this file as recently saved to ignore update notifications
|
||||
recently_saved_files[document.path] = Time.get_unix_time_from_system()
|
||||
ignore_update_timer.start()
|
||||
|
||||
# Use the file system singleton to save the file
|
||||
var result := file_system.save_file(document.path, document.content)
|
||||
if result != OK:
|
||||
push_error("Could not save file '%s': %s" % [document.path, error_string(result)])
|
||||
recently_saved_files.erase(document.path) # Remove from recently saved if error
|
||||
return false
|
||||
|
||||
# Mark document as saved
|
||||
document.mark_saved()
|
||||
|
||||
# Update UI
|
||||
update_ui()
|
||||
|
||||
return true
|
||||
|
||||
func save_document_as(document: YAMLEditorDocument, new_path: String) -> bool:
|
||||
if document == null or new_path.is_empty():
|
||||
return false
|
||||
|
||||
var normalized_new_path = _normalize_path(new_path)
|
||||
|
||||
# Check if we're trying to save to a path that's already open
|
||||
if documents.has(normalized_new_path) and documents[normalized_new_path] != document:
|
||||
push_error("Cannot save as '%s' - file is already open" % normalized_new_path)
|
||||
return false
|
||||
|
||||
# Remember the old path
|
||||
var old_path := document.path
|
||||
|
||||
# Update document path
|
||||
document.path = normalized_new_path
|
||||
|
||||
# Update the documents dictionary
|
||||
if old_path != normalized_new_path:
|
||||
documents.erase(old_path)
|
||||
documents[normalized_new_path] = document
|
||||
|
||||
# Save the document
|
||||
if save_document(document):
|
||||
# If old path was temporary, clean up
|
||||
if old_path != normalized_new_path and old_path.begins_with("untitled"):
|
||||
file_system.notify_file_closed(old_path)
|
||||
|
||||
# Update UI
|
||||
update_ui()
|
||||
|
||||
# Notify file system
|
||||
file_system.notify_file_opened(new_path)
|
||||
|
||||
return true
|
||||
|
||||
# Restore old path if save failed
|
||||
if old_path != normalized_new_path:
|
||||
document.path = old_path
|
||||
documents.erase(normalized_new_path)
|
||||
documents[old_path] = document
|
||||
|
||||
return false
|
||||
|
||||
func new_file() -> void:
|
||||
# Create a new untitled file
|
||||
var untitled_name := "untitled.yaml"
|
||||
var index := 1
|
||||
|
||||
while documents.has(untitled_name):
|
||||
index += 1
|
||||
untitled_name = "untitled%d.yaml" % index
|
||||
|
||||
# Create a new document
|
||||
var document := create_document(untitled_name)
|
||||
document.set_modified(true) # New document is always modified
|
||||
|
||||
# Switch to the new document
|
||||
set_current_document(document)
|
||||
|
||||
# Set focus to code editor
|
||||
code_editor.grab_focus()
|
||||
|
||||
# Notify the file system
|
||||
file_system.notify_file_opened(untitled_name)
|
||||
|
||||
func set_current_document(document: YAMLEditorDocument) -> void:
|
||||
if document == null or document == current_document:
|
||||
return
|
||||
|
||||
current_document = document
|
||||
|
||||
# Update editor content
|
||||
if is_instance_valid(code_editor):
|
||||
code_editor.set_text_and_preserve_state(document.content)
|
||||
|
||||
# Update UI
|
||||
update_ui()
|
||||
|
||||
# Emit signal
|
||||
document_changed.emit(document)
|
||||
|
||||
func update_document_content(document: YAMLEditorDocument, new_content: String) -> void:
|
||||
if document == null:
|
||||
return
|
||||
|
||||
var caret_line := 0
|
||||
var caret_column := 0
|
||||
|
||||
if is_instance_valid(code_editor):
|
||||
caret_line = code_editor.get_caret_line()
|
||||
caret_column = code_editor.get_caret_column()
|
||||
|
||||
document.set_content(new_content, caret_line, caret_column)
|
||||
|
||||
func _normalize_path(path: String) -> String:
|
||||
# Convert to absolute path and normalize
|
||||
var normalized = path
|
||||
|
||||
# Handle different path formats
|
||||
if normalized.begins_with("res://"):
|
||||
normalized = ProjectSettings.globalize_path(normalized)
|
||||
|
||||
# Convert to canonical form
|
||||
normalized = normalized.simplify_path()
|
||||
|
||||
# Convert back to res:// format if it was originally a project path
|
||||
if path.begins_with("res://"):
|
||||
normalized = ProjectSettings.localize_path(normalized)
|
||||
|
||||
return normalized
|
||||
|
||||
func _paths_point_to_same_file(path1: String, path2: String) -> bool:
|
||||
# For project files, compare the res:// paths
|
||||
if path1.begins_with("res://") and path2.begins_with("res://"):
|
||||
return path1 == path2
|
||||
|
||||
# For absolute paths, normalize and compare
|
||||
var abs_path1 = ProjectSettings.globalize_path(path1) if path1.begins_with("res://") else path1
|
||||
var abs_path2 = ProjectSettings.globalize_path(path2) if path2.begins_with("res://") else path2
|
||||
|
||||
return abs_path1.simplify_path() == abs_path2.simplify_path()
|
||||
|
||||
func _on_document_content_changed(document: YAMLEditorDocument) -> void:
|
||||
# Update UI if this is the current document
|
||||
if document == current_document:
|
||||
update_ui()
|
||||
|
||||
func _on_document_modified_changed(document: YAMLEditorDocument) -> void:
|
||||
# Update UI if this is the current document
|
||||
if document == current_document:
|
||||
update_ui()
|
||||
|
||||
func update_ui() -> void:
|
||||
# Toggle editor visibility
|
||||
editor_node.visible = documents.size() > 0
|
||||
|
||||
if not is_instance_valid(file_list):
|
||||
return
|
||||
|
||||
# Prepare file data for the file list component
|
||||
var file_data := {}
|
||||
for path in documents:
|
||||
var document = documents[path]
|
||||
file_data[path] = {
|
||||
"name": document.get_file_name(),
|
||||
"modified": document.is_modified
|
||||
}
|
||||
|
||||
# Update the file list component
|
||||
var current_path = current_document.path if current_document else ""
|
||||
file_list.update_files(file_data, current_path)
|
||||
|
||||
func _on_file_selected(path: String) -> void:
|
||||
if path.is_empty():
|
||||
return
|
||||
|
||||
var normalized_path = _normalize_path(path)
|
||||
if not documents.has(normalized_path):
|
||||
return
|
||||
|
||||
set_current_document(documents[normalized_path])
|
||||
|
||||
func _on_file_context_requested(path: String, at_position: Vector2) -> void:
|
||||
if path.is_empty():
|
||||
return
|
||||
|
||||
var normalized_path = _normalize_path(path)
|
||||
if not documents.has(normalized_path):
|
||||
return
|
||||
|
||||
set_current_document(documents[normalized_path])
|
||||
|
||||
# Calculate the global position for the popup
|
||||
var global_rect := Rect2(file_list.get_global_mouse_position(), Vector2.ZERO)
|
||||
file_popup_menu.popup_on_parent(global_rect)
|
||||
|
||||
func _on_file_popup_menu_id_pressed(id: int) -> void:
|
||||
var path := file_list.get_selected_file_path()
|
||||
if path.is_empty():
|
||||
return
|
||||
|
||||
var normalized_path = _normalize_path(path)
|
||||
if not documents.has(normalized_path):
|
||||
return
|
||||
|
||||
var document: YAMLEditorDocument = documents[normalized_path]
|
||||
|
||||
match id:
|
||||
0: # Save
|
||||
save_document(document)
|
||||
1: # Save As
|
||||
# Main editor should handle the save as dialog
|
||||
set_current_document(document)
|
||||
2: # Close
|
||||
close_document(document)
|
||||
3: # Show in FileSystem
|
||||
if not document.is_untitled() and document.path.begins_with("res://"):
|
||||
EditorInterface.get_file_system_dock().navigate_to_path(document.path)
|
||||
|
||||
func _on_external_file_updated(path: String) -> void:
|
||||
var normalized_path = _normalize_path(path)
|
||||
|
||||
# Only process if the file is open and it's a YAML file
|
||||
if documents.has(normalized_path) and file_system.is_yaml_file(normalized_path):
|
||||
# Check if we just saved this file ourselves
|
||||
if recently_saved_files.has(normalized_path):
|
||||
var save_time: int = recently_saved_files[normalized_path]
|
||||
var current_time := Time.get_unix_time_from_system()
|
||||
|
||||
# If saved less than 1 second ago, ignore this update
|
||||
if current_time - save_time < 1.0:
|
||||
return
|
||||
|
||||
var document: YAMLEditorDocument = documents[normalized_path]
|
||||
|
||||
# Check if the document has unsaved changes
|
||||
if not document.is_modified:
|
||||
# Document is not modified locally, safe to reload
|
||||
var content = file_system.read_file(normalized_path)
|
||||
if typeof(content) != TYPE_INT: # Not an error
|
||||
# Update document content
|
||||
document.content = content
|
||||
document.mark_saved()
|
||||
|
||||
# If this is the current document, update the editor
|
||||
if document == current_document:
|
||||
# Preserve cursor position and scroll state
|
||||
var previous_caret_line := code_editor.get_caret_line()
|
||||
var previous_caret_column := code_editor.get_caret_column()
|
||||
var previous_scroll_v := code_editor.get_v_scroll_bar().value
|
||||
var previous_scroll_h := code_editor.get_h_scroll_bar().value
|
||||
|
||||
code_editor.text = content
|
||||
|
||||
# Restore position if possible
|
||||
if previous_caret_line < code_editor.get_line_count():
|
||||
code_editor.set_caret_line(previous_caret_line)
|
||||
var line_length := code_editor.get_line(previous_caret_line).length()
|
||||
if previous_caret_column <= line_length:
|
||||
code_editor.set_caret_column(previous_caret_column)
|
||||
|
||||
# Restore scroll position
|
||||
code_editor.get_v_scroll_bar().value = previous_scroll_v
|
||||
code_editor.get_h_scroll_bar().value = previous_scroll_h
|
||||
|
||||
update_ui()
|
||||
else:
|
||||
# Document has unsaved changes, show conflict dialog
|
||||
if document == current_document:
|
||||
var dialog := ConfirmationDialog.new()
|
||||
dialog.title = "External Changes Detected"
|
||||
dialog.dialog_text = "The file '" + document.get_file_name() + "' has been modified externally. Do you want to reload it and lose your changes?"
|
||||
dialog.confirmed.connect(
|
||||
func():
|
||||
var content := file_system.read_file(path)
|
||||
if typeof(content) != TYPE_INT:
|
||||
document.content = content
|
||||
document.mark_saved()
|
||||
|
||||
if document == current_document:
|
||||
code_editor.text = content
|
||||
|
||||
update_ui()
|
||||
dialog.queue_free()
|
||||
)
|
||||
dialog.canceled.connect(func(): dialog.queue_free())
|
||||
add_child(dialog)
|
||||
dialog.popup_centered()
|
||||
|
||||
func _on_ignore_update_timer_timeout() -> void:
|
||||
# Clear out any old saved entries
|
||||
var current_time := Time.get_unix_time_from_system()
|
||||
var keys_to_remove: PackedStringArray = []
|
||||
|
||||
for path in recently_saved_files:
|
||||
var save_time = recently_saved_files[path]
|
||||
if current_time - save_time >= 1.0:
|
||||
keys_to_remove.append(path)
|
||||
|
||||
for path in keys_to_remove:
|
||||
recently_saved_files.erase(path)
|
||||
|
||||
func _on_file_renamed(old_path: String, new_path: String) -> void:
|
||||
var normalized_old_path = _normalize_path(old_path)
|
||||
var normalized_new_path = _normalize_path(new_path)
|
||||
|
||||
# If we have this document open, update our references
|
||||
if documents.has(normalized_old_path):
|
||||
var document: YAMLEditorDocument = documents[normalized_old_path]
|
||||
document.path = normalized_new_path
|
||||
|
||||
documents.erase(normalized_old_path)
|
||||
documents[normalized_new_path] = document
|
||||
|
||||
update_ui()
|
||||
|
||||
func handle_filesystem_change() -> void:
|
||||
# Check if any of our open res:// files no longer exist
|
||||
var missing_files: PackedStringArray = []
|
||||
|
||||
for path in documents.keys():
|
||||
if path.begins_with("res://") and not file_system.file_exists(path):
|
||||
missing_files.append(path)
|
||||
|
||||
# Handle missing files
|
||||
for old_path in missing_files:
|
||||
var document: YAMLEditorDocument = documents[old_path]
|
||||
|
||||
# Try to find a file with the same name but different path in the filesystem
|
||||
var filename := old_path.get_file()
|
||||
var filesystem_root := EditorInterface.get_resource_filesystem().get_filesystem()
|
||||
var new_path := file_system.find_file_in_filesystem(filesystem_root, filename)
|
||||
|
||||
if not new_path.is_empty():
|
||||
var normalized_new_path = _normalize_path(new_path)
|
||||
|
||||
# Found potential match - update the document path
|
||||
document.path = normalized_new_path
|
||||
documents.erase(old_path)
|
||||
documents[normalized_new_path] = document
|
||||
|
||||
# If this is the current document, emit signal
|
||||
if document == current_document:
|
||||
document_changed.emit(document)
|
||||
|
||||
# Update UI
|
||||
update_ui()
|
||||
|
||||
# Notify file system
|
||||
file_system.notify_file_closed(old_path)
|
||||
file_system.notify_file_opened(normalized_new_path)
|
||||
file_system.notify_file_renamed(old_path, normalized_new_path)
|
||||
else:
|
||||
# Keep it open but mark as potentially moved/deleted to avoid losing unsaved changes
|
||||
pass
|
||||
|
||||
func has_unsaved_changes() -> bool:
|
||||
for document in documents.values():
|
||||
if document.is_modified:
|
||||
return true
|
||||
return false
|
||||
|
||||
func get_open_documents() -> Array:
|
||||
return documents.values()
|
||||
|
||||
func get_open_paths() -> Array:
|
||||
return documents.keys()
|
||||
|
||||
func has_document(path: String) -> bool:
|
||||
return documents.has(path)
|
||||
|
||||
func get_document(path: String) -> YAMLEditorDocument:
|
||||
return documents.get(path, null)
|
||||
|
||||
func get_current_document() -> YAMLEditorDocument:
|
||||
return current_document
|
||||
@@ -1 +0,0 @@
|
||||
uid://cpm2siqxhgj8w
|
||||
@@ -1,166 +0,0 @@
|
||||
@tool
|
||||
class_name YAMLEditorShortcuts
|
||||
|
||||
# This helper class registers editor shortcuts for the YAML editor
|
||||
|
||||
const SHORTCUTS = [
|
||||
{
|
||||
"name": "Save",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_S,
|
||||
"callback": "_on_save_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Save As",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_MASK_SHIFT | KEY_S,
|
||||
"callback": "_on_save_as_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Close File",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_W,
|
||||
"callback": "_on_close_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "New File",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_N,
|
||||
"callback": "_on_new_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Open File",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_O,
|
||||
"callback": "_on_open_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Validate YAML",
|
||||
"shortcut": KEY_F4,
|
||||
"callback": "_on_validate_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Undo",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_Z,
|
||||
"callback": "_on_undo_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Redo",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_MASK_SHIFT | KEY_Z,
|
||||
"callback": "_on_redo_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Cut",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_X,
|
||||
"callback": "_on_cut_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Copy",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_C,
|
||||
"callback": "_on_copy_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Paste",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_V,
|
||||
"callback": "_on_paste_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Select All",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_A,
|
||||
"callback": "_on_select_all_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Find",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_F,
|
||||
"callback": "_on_find_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Find Next",
|
||||
"shortcut": KEY_F3,
|
||||
"callback": "_on_find_next_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Find Previous",
|
||||
"shortcut": KEY_MASK_SHIFT | KEY_F3,
|
||||
"callback": "_on_find_previous_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Replace",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_R,
|
||||
"callback": "_on_replace_shortcut"
|
||||
},
|
||||
# Zoom shortcuts
|
||||
{
|
||||
"name": "Zoom In",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_EQUAL,
|
||||
"callback": "_on_zoom_in_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Zoom In (Numpad)",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_KP_ADD,
|
||||
"callback": "_on_zoom_in_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Zoom Out",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_MINUS,
|
||||
"callback": "_on_zoom_out_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Zoom Out (Numpad)",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_KP_SUBTRACT,
|
||||
"callback": "_on_zoom_out_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Reset Zoom",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_0,
|
||||
"callback": "_on_zoom_reset_shortcut"
|
||||
},
|
||||
{
|
||||
"name": "Reset Zoom (Numpad)",
|
||||
"shortcut": KEY_MASK_CTRL | KEY_KP_0,
|
||||
"callback": "_on_zoom_reset_shortcut"
|
||||
}
|
||||
]
|
||||
|
||||
static func register_shortcuts(editor_plugin: EditorPlugin, target_object: Object) -> void:
|
||||
# Create shortcut inputs for the YAML editor
|
||||
var editor_settings := editor_plugin.get_editor_interface().get_editor_settings()
|
||||
var shortcuts_settings := editor_settings.get_setting("shortcuts") if editor_settings.has_setting("shortcuts") else {}
|
||||
|
||||
# Create a unique editor name for our shortcuts
|
||||
var editor_name := "YAML Editor"
|
||||
|
||||
# Register each shortcut
|
||||
for shortcut_data in SHORTCUTS:
|
||||
var shortcut_name: String = "yaml_editor/" + shortcut_data.name.to_lower().replace(" ", "_")
|
||||
var input_event := InputEventKey.new()
|
||||
input_event.keycode = shortcut_data.shortcut
|
||||
|
||||
# Create a shortcut
|
||||
var shortcut := Shortcut.new()
|
||||
shortcut.events = [input_event]
|
||||
|
||||
# Register the shortcut with Godot's input map
|
||||
if not InputMap.has_action(shortcut_name):
|
||||
InputMap.add_action(shortcut_name)
|
||||
InputMap.action_add_event(shortcut_name, input_event)
|
||||
|
||||
# Connect to the target object's _unhandled_key_input method if it exists
|
||||
if !target_object.has_method("_unhandled_key_input"):
|
||||
# Create connections for shortcuts if the target doesn't handle key input directly
|
||||
for shortcut_data in SHORTCUTS:
|
||||
var shortcut_name: String = "yaml_editor/" + shortcut_data.name.to_lower().replace(" ", "_")
|
||||
if target_object.has_method(shortcut_data.callback):
|
||||
InputMap.action_add_event(shortcut_name, InputEventAction.new())
|
||||
# Connect the shortcut action to the target's callback
|
||||
var root := editor_plugin.get_tree().root
|
||||
root.connect("input_event",
|
||||
func(event):
|
||||
if event is InputEventKey and event.pressed:
|
||||
if event.get_keycode_with_modifiers() == shortcut_data.shortcut:
|
||||
target_object.call(shortcut_data.callback)
|
||||
print("called a thing")
|
||||
root.get_viewport().set_input_as_handled()
|
||||
)
|
||||
|
||||
static func unregister_shortcuts() -> void:
|
||||
# Remove all registered shortcuts
|
||||
for shortcut_data in SHORTCUTS:
|
||||
var shortcut_name: String = "yaml_editor/" + shortcut_data.name.to_lower().replace(" ", "_")
|
||||
if InputMap.has_action(shortcut_name):
|
||||
InputMap.erase_action(shortcut_name)
|
||||
@@ -1 +0,0 @@
|
||||
uid://dgcufn1xonjkp
|
||||
@@ -1,107 +0,0 @@
|
||||
@tool
|
||||
class_name YAMLEditorFileList extends VBoxContainer
|
||||
|
||||
signal file_selected(path)
|
||||
signal file_context_requested(path, position)
|
||||
|
||||
# References to UI components
|
||||
@export var filter_input: LineEdit
|
||||
@export var file_list: ItemList
|
||||
|
||||
# File data
|
||||
var files: Dictionary = {} # {path: {name, modified}}
|
||||
var filtered_files: Array = []
|
||||
var current_path: String = ""
|
||||
|
||||
func _ready() -> void:
|
||||
# SessionManager will handle loading of the files
|
||||
file_list.clear()
|
||||
|
||||
# Connect internal signals
|
||||
if is_instance_valid(file_list):
|
||||
file_list.item_selected.connect(_on_item_selected)
|
||||
file_list.item_clicked.connect(_on_item_clicked)
|
||||
|
||||
if is_instance_valid(filter_input):
|
||||
filter_input.text_changed.connect(_on_filter_text_changed)
|
||||
filter_input.right_icon = get_theme_icon("Search", "EditorIcons")
|
||||
|
||||
# Public API
|
||||
func update_files(p_files: Dictionary, p_current_path: String) -> void:
|
||||
files = p_files.duplicate()
|
||||
current_path = p_current_path
|
||||
_update_ui()
|
||||
|
||||
func mark_file_modified(path: String, is_modified: bool) -> void:
|
||||
if files.has(path):
|
||||
files[path].modified = is_modified
|
||||
_update_ui()
|
||||
|
||||
func get_selected_file_path() -> String:
|
||||
if not is_instance_valid(file_list):
|
||||
return ""
|
||||
|
||||
var selected_items := file_list.get_selected_items()
|
||||
if selected_items.is_empty():
|
||||
return ""
|
||||
|
||||
var selected_index := selected_items[0]
|
||||
if selected_index >= 0 and selected_index < filtered_files.size():
|
||||
return filtered_files[selected_index]
|
||||
|
||||
return ""
|
||||
|
||||
# UI update
|
||||
func _update_ui() -> void:
|
||||
if not is_instance_valid(file_list):
|
||||
return
|
||||
|
||||
var current_selection := get_selected_file_path()
|
||||
|
||||
file_list.clear()
|
||||
filtered_files.clear()
|
||||
|
||||
var filter_text := filter_input.text.to_lower() if is_instance_valid(filter_input) else ""
|
||||
|
||||
var current_index := -1
|
||||
var index := 0
|
||||
|
||||
for path: String in files.keys():
|
||||
var file_data: Dictionary = files[path]
|
||||
var file_name := path.get_file()
|
||||
|
||||
if not filter_text.is_empty() and file_name.to_lower().find(filter_text) == -1:
|
||||
continue
|
||||
|
||||
var display_name := file_name
|
||||
if file_data.modified:
|
||||
display_name += " (*)"
|
||||
|
||||
file_list.add_item(display_name)
|
||||
filtered_files.append(path)
|
||||
file_list.set_item_tooltip(index, path)
|
||||
|
||||
if path == current_path:
|
||||
current_index = index
|
||||
|
||||
if path == current_selection:
|
||||
file_list.select(index)
|
||||
|
||||
index += 1
|
||||
|
||||
if current_index >= 0 and (file_list.get_selected_items().is_empty() or current_path != current_selection):
|
||||
file_list.select(current_index)
|
||||
|
||||
# Signal handlers
|
||||
func _on_filter_text_changed(_text: String) -> void:
|
||||
_update_ui()
|
||||
|
||||
func _on_item_selected(index: int) -> void:
|
||||
if index >= 0 and index < filtered_files.size():
|
||||
file_selected.emit(filtered_files[index])
|
||||
|
||||
func _on_item_clicked(index: int, at_position: Vector2, mouse_button_index: int) -> void:
|
||||
if index >= 0 and index < filtered_files.size():
|
||||
if mouse_button_index == MOUSE_BUTTON_RIGHT:
|
||||
file_list.select(index)
|
||||
file_context_requested.emit(filtered_files[index], at_position)
|
||||
@@ -1 +0,0 @@
|
||||
uid://c15odacm31d03
|
||||
@@ -1,93 +0,0 @@
|
||||
@tool
|
||||
class_name YAMLFileSystem extends Node
|
||||
|
||||
signal file_opened(path)
|
||||
signal file_saved(path)
|
||||
signal file_updated(path)
|
||||
signal file_closed(path)
|
||||
signal file_renamed(old_path, new_path)
|
||||
|
||||
# Singleton pattern
|
||||
static var _instance: YAMLFileSystem
|
||||
static func get_singleton() -> YAMLFileSystem:
|
||||
if not _instance:
|
||||
_instance = YAMLFileSystem.new()
|
||||
Engine.get_main_loop().root.call_deferred("add_child", _instance)
|
||||
return _instance
|
||||
|
||||
func _init() -> void:
|
||||
if _instance != null:
|
||||
push_error("YAMLFileSystem singleton already exists")
|
||||
return
|
||||
_instance = self
|
||||
# Mark as persistent so it doesn't get destroyed on scene changes
|
||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
|
||||
# File operations with signals
|
||||
func save_file(path: String, content: String) -> Error:
|
||||
var was_new_file = not file_exists(path)
|
||||
|
||||
var file := FileAccess.open(path, FileAccess.WRITE)
|
||||
if not file:
|
||||
return FileAccess.get_open_error()
|
||||
|
||||
file.store_string(content)
|
||||
file_saved.emit(path)
|
||||
file_updated.emit(path)
|
||||
|
||||
# If this was a new file, notify Godot's filesystem
|
||||
if was_new_file:
|
||||
call_deferred("_refresh_filesystem", path)
|
||||
|
||||
return OK
|
||||
|
||||
func read_file(path: String) -> Variant:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if not file:
|
||||
return FileAccess.get_open_error()
|
||||
|
||||
return file.get_as_text()
|
||||
|
||||
# Check if a file exists
|
||||
func file_exists(path: String) -> bool:
|
||||
return FileAccess.file_exists(path)
|
||||
|
||||
# Utility to check if a path is a YAML file
|
||||
func is_yaml_file(path: String) -> bool:
|
||||
return path.get_extension().to_lower() in ["yaml", "yml"]
|
||||
|
||||
# For external updates, allow code to manually trigger the signal
|
||||
func notify_file_updated(path: String) -> void:
|
||||
file_updated.emit(path)
|
||||
|
||||
# Called when a file is opened in the editor
|
||||
func notify_file_opened(path: String) -> void:
|
||||
file_opened.emit(path)
|
||||
|
||||
# Called when a file is closed in the editor
|
||||
func notify_file_closed(path: String) -> void:
|
||||
file_closed.emit(path)
|
||||
|
||||
# Called when a file is renamed (by the filesystem or editor)
|
||||
func notify_file_renamed(old_path: String, new_path: String) -> void:
|
||||
file_renamed.emit(old_path, new_path)
|
||||
|
||||
# Find a file by name in the filesystem
|
||||
func find_file_in_filesystem(dir: EditorFileSystemDirectory, filename: String) -> String:
|
||||
# Check files in current directory
|
||||
for i in range(dir.get_file_count()):
|
||||
var file_path := dir.get_file_path(i)
|
||||
if file_path.get_file() == filename:
|
||||
return file_path
|
||||
|
||||
# Recursively check subdirectories
|
||||
for i in range(dir.get_subdir_count()):
|
||||
var subdir := dir.get_subdir(i)
|
||||
var result := find_file_in_filesystem(subdir, filename)
|
||||
if not result.is_empty():
|
||||
return result
|
||||
|
||||
return ""
|
||||
|
||||
func _refresh_filesystem(path: String) -> void:
|
||||
EditorInterface.get_resource_filesystem().update_file(path)
|
||||
@@ -1 +0,0 @@
|
||||
uid://p8fujg7vubpn
|
||||
@@ -1,496 +0,0 @@
|
||||
@tool
|
||||
class_name YAMLEditorFindReplaceBar extends Control
|
||||
|
||||
signal replace_performed
|
||||
signal replace_all_performed
|
||||
|
||||
@export_category("Find Panel Components")
|
||||
@export var find_input: LineEdit
|
||||
@export var matches_label: Label
|
||||
@export var previous_button: Button
|
||||
@export var next_button: Button
|
||||
@export var match_case_checkbox: CheckBox
|
||||
@export var whole_words_checkbox: CheckBox
|
||||
@export var find_button_container: HBoxContainer
|
||||
@export var find_options_container: HBoxContainer
|
||||
|
||||
@export_category("Replace Panel Components")
|
||||
@export var replace_input: LineEdit
|
||||
@export var replace_button: Button
|
||||
@export var replace_all_button: Button
|
||||
@export var selection_only_checkbox: CheckBox
|
||||
@export var replace_button_container: HBoxContainer
|
||||
@export var replace_options_container: HBoxContainer
|
||||
|
||||
@export_category("Visibility Toggle")
|
||||
@export var hide_button: Button
|
||||
|
||||
@export_category("Node References")
|
||||
@export var editor: YAMLCodeEditor
|
||||
@export var vbox_container: VBoxContainer # Container holding both panels
|
||||
@export var find_panel: Control # First row (find)
|
||||
@export var replace_panel: Control # Second row (replace)
|
||||
|
||||
# Matching state
|
||||
var matches: Array[Vector2i] = [] # Store line/column pairs of matches
|
||||
var current_match_index: int = -1 # Index of current selection in matches array
|
||||
var search_regex: RegEx = RegEx.new()
|
||||
|
||||
func _ready() -> void:
|
||||
# Setup UI
|
||||
previous_button.icon = get_theme_icon("MoveUp", "EditorIcons")
|
||||
next_button.icon = get_theme_icon("MoveDown", "EditorIcons")
|
||||
hide_button.icon = get_theme_icon("Close", "EditorIcons")
|
||||
|
||||
# Connect signals
|
||||
find_input.text_changed.connect(_on_find_input_changed)
|
||||
find_input.text_submitted.connect(_on_find_input_submitted)
|
||||
previous_button.pressed.connect(_on_previous_button_pressed)
|
||||
next_button.pressed.connect(_on_next_button_pressed)
|
||||
match_case_checkbox.toggled.connect(_on_option_changed)
|
||||
whole_words_checkbox.toggled.connect(_on_option_changed)
|
||||
hide_button.pressed.connect(_on_hide_button_pressed)
|
||||
|
||||
replace_button.pressed.connect(_on_replace_button_pressed)
|
||||
replace_all_button.pressed.connect(_on_replace_all_button_pressed)
|
||||
selection_only_checkbox.toggled.connect(_on_option_changed)
|
||||
|
||||
# Disable buttons initially
|
||||
previous_button.disabled = true
|
||||
next_button.disabled = true
|
||||
replace_button.disabled = true
|
||||
replace_all_button.disabled = true
|
||||
|
||||
# Hide by default
|
||||
visible = false
|
||||
|
||||
# Make sure editor preserves selection when focus changes
|
||||
if editor:
|
||||
editor.set_deselect_on_focus_loss_enabled(false)
|
||||
|
||||
var find_panel_visible: bool:
|
||||
get(): return find_input.visible
|
||||
set(value):
|
||||
find_input.visible = value
|
||||
find_button_container.visible = value
|
||||
find_options_container.visible = value
|
||||
|
||||
var replace_panel_visible: bool:
|
||||
get(): return replace_input.visible
|
||||
set(value):
|
||||
replace_input.visible = value
|
||||
replace_button_container.visible = value
|
||||
replace_options_container.visible = value
|
||||
|
||||
# Public methods
|
||||
func show_find_panel() -> void:
|
||||
if not is_instance_valid(editor):
|
||||
return
|
||||
|
||||
visible = true
|
||||
find_panel_visible = true
|
||||
|
||||
# If there's a selection, use it as search text
|
||||
if editor.has_selection():
|
||||
find_input.text = editor.get_selected_text()
|
||||
|
||||
# Run initial search and update UI
|
||||
trigger_search()
|
||||
|
||||
# Focus the search input
|
||||
find_input.grab_focus()
|
||||
find_input.select_all()
|
||||
|
||||
func show_replace_panel() -> void:
|
||||
if not is_instance_valid(editor):
|
||||
return
|
||||
|
||||
visible = true
|
||||
find_panel_visible = true
|
||||
replace_panel_visible = true
|
||||
|
||||
# If there's a selection, use it as search text
|
||||
if editor.has_selection():
|
||||
find_input.text = editor.get_selected_text()
|
||||
|
||||
# Run initial search and update UI
|
||||
trigger_search()
|
||||
|
||||
# Focus the search input
|
||||
find_input.grab_focus()
|
||||
find_input.select_all()
|
||||
|
||||
func hide_panel() -> void:
|
||||
find_panel_visible = false
|
||||
replace_panel_visible = false
|
||||
visible = false
|
||||
|
||||
# Clear search when hiding
|
||||
if is_instance_valid(editor):
|
||||
editor.set_search_text("")
|
||||
editor.set_search_flags(0)
|
||||
editor.queue_redraw()
|
||||
|
||||
# Core functionality
|
||||
func trigger_search() -> void:
|
||||
if not is_instance_valid(editor):
|
||||
return
|
||||
|
||||
# Store old cursor position to find closest match
|
||||
var old_cursor_line = editor.get_caret_line()
|
||||
var old_cursor_column = editor.get_caret_column()
|
||||
|
||||
# Update TextEdit search settings for highlighting
|
||||
var search_text = find_input.text
|
||||
editor.set_search_text(search_text if visible else "")
|
||||
|
||||
var flags = 0
|
||||
if match_case_checkbox.button_pressed:
|
||||
flags |= TextEdit.SEARCH_MATCH_CASE
|
||||
if whole_words_checkbox.button_pressed:
|
||||
flags |= TextEdit.SEARCH_WHOLE_WORDS
|
||||
editor.set_search_flags(flags)
|
||||
|
||||
# Find all matches
|
||||
matches.clear()
|
||||
current_match_index = -1
|
||||
|
||||
if search_text.is_empty():
|
||||
_update_match_label()
|
||||
_update_button_states()
|
||||
return
|
||||
|
||||
# Create regex pattern
|
||||
_create_search_regex(search_text, match_case_checkbox.button_pressed, whole_words_checkbox.button_pressed)
|
||||
|
||||
# Find all matches using regex
|
||||
for line_num in range(editor.get_line_count()):
|
||||
var line_text = editor.get_line(line_num)
|
||||
var search_results = search_regex.search_all(line_text)
|
||||
|
||||
for result in search_results:
|
||||
matches.append(Vector2i(line_num, result.get_start()))
|
||||
|
||||
# Determine which match to select
|
||||
if matches.is_empty():
|
||||
current_match_index = -1
|
||||
else:
|
||||
# Find closest match to current cursor position
|
||||
var best_distance = -1
|
||||
var best_match = 0
|
||||
|
||||
for i in range(matches.size()):
|
||||
var pos = matches[i]
|
||||
|
||||
# Check if this match is after cursor
|
||||
if pos.x > old_cursor_line or (pos.x == old_cursor_line and pos.y >= old_cursor_column):
|
||||
var distance = (pos.x - old_cursor_line) * 1000 + (pos.y - old_cursor_column)
|
||||
if best_distance < 0 or distance < best_distance:
|
||||
best_distance = distance
|
||||
best_match = i
|
||||
|
||||
# If no match after cursor, wrap to first match
|
||||
if best_distance < 0:
|
||||
current_match_index = 0
|
||||
else:
|
||||
current_match_index = best_match
|
||||
|
||||
# Always ensure we have a selected match if there are any matches
|
||||
if matches.size() > 0 and current_match_index == -1:
|
||||
current_match_index = 0
|
||||
|
||||
# Select the current match if appropriate
|
||||
if current_match_index >= 0 and not (selection_only_checkbox.button_pressed and replace_panel.visible):
|
||||
_select_current_match()
|
||||
|
||||
# Update UI
|
||||
_update_match_label()
|
||||
_update_button_states()
|
||||
|
||||
func find_next() -> void:
|
||||
if matches.is_empty() or not is_instance_valid(editor):
|
||||
return
|
||||
|
||||
# Don't navigate if selection only is active
|
||||
if replace_panel.visible and selection_only_checkbox.button_pressed:
|
||||
return
|
||||
|
||||
# Move to next match
|
||||
current_match_index = (current_match_index + 1) % matches.size()
|
||||
_select_current_match()
|
||||
_update_match_label()
|
||||
|
||||
func find_previous() -> void:
|
||||
if matches.is_empty() or not is_instance_valid(editor):
|
||||
return
|
||||
|
||||
# Don't navigate if selection only is active
|
||||
if replace_panel.visible and selection_only_checkbox.button_pressed:
|
||||
return
|
||||
|
||||
# Move to previous match
|
||||
current_match_index = (current_match_index - 1 + matches.size()) % matches.size()
|
||||
_select_current_match()
|
||||
_update_match_label()
|
||||
|
||||
# Helper methods
|
||||
func _create_search_regex(search_text: String, case_sensitive: bool, whole_words: bool) -> void:
|
||||
# Escape special regex characters
|
||||
var pattern = ""
|
||||
for i in range(search_text.length()):
|
||||
var c = search_text[i]
|
||||
# Escape regex special characters
|
||||
if c in "\\.*+?^$[](){}|":
|
||||
pattern += "\\" + c
|
||||
else:
|
||||
pattern += c
|
||||
|
||||
# Add word boundary anchors if needed
|
||||
if whole_words:
|
||||
pattern = "\\b%s\\b" % pattern
|
||||
|
||||
if not case_sensitive:
|
||||
pattern = "(?i)%s" % pattern
|
||||
|
||||
search_regex = RegEx.new()
|
||||
search_regex.compile(pattern)
|
||||
|
||||
func _select_current_match() -> void:
|
||||
if current_match_index < 0 or current_match_index >= matches.size():
|
||||
return
|
||||
|
||||
var match_pos = matches[current_match_index]
|
||||
var search_length = find_input.text.length()
|
||||
|
||||
# Select the text
|
||||
editor.set_caret_line(match_pos.x)
|
||||
editor.set_caret_column(match_pos.y)
|
||||
editor.select(match_pos.x, match_pos.y, match_pos.x, match_pos.y + search_length)
|
||||
|
||||
# Center the view
|
||||
editor.center_viewport_to_caret()
|
||||
|
||||
func _update_match_label() -> void:
|
||||
if not visible:
|
||||
return
|
||||
|
||||
var count = matches.size()
|
||||
|
||||
matches_label.visible = true
|
||||
|
||||
if count > 0:
|
||||
matches_label.modulate = Color.WHITE
|
||||
matches_label.text = "%d of %d matches" % [current_match_index + 1, count]
|
||||
elif not find_input.text.is_empty():
|
||||
matches_label.modulate = EditorInterface.get_editor_settings().get_setting("text_editor/theme/highlighting/brace_mismatch_color")
|
||||
matches_label.text = "No matches"
|
||||
else:
|
||||
matches_label.visible = false
|
||||
matches_label.text = ""
|
||||
|
||||
func _update_button_states() -> void:
|
||||
var has_matches = matches.size() > 0
|
||||
var selection_only_active = replace_panel.visible and selection_only_checkbox.button_pressed
|
||||
|
||||
# Disable navigation buttons if selection only is checked
|
||||
previous_button.disabled = not has_matches or selection_only_active
|
||||
next_button.disabled = not has_matches or selection_only_active
|
||||
|
||||
# Enable/disable replace buttons
|
||||
if selection_only_active and editor.has_selection():
|
||||
var has_matches_in_selection = get_matches_in_selection().size() > 0
|
||||
replace_button.disabled = not has_matches_in_selection
|
||||
replace_all_button.disabled = not has_matches_in_selection
|
||||
else:
|
||||
replace_button.disabled = not has_matches
|
||||
replace_all_button.disabled = not has_matches
|
||||
|
||||
# Get matches within the current selection when Selection Only is active
|
||||
func get_matches_in_selection() -> Array[Vector2i]:
|
||||
var result: Array[Vector2i] = []
|
||||
|
||||
if not editor.has_selection() or not selection_only_checkbox.button_pressed:
|
||||
return matches.duplicate()
|
||||
|
||||
var search_text = find_input.text
|
||||
var selection_from_line = editor.get_selection_from_line()
|
||||
var selection_from_column = editor.get_selection_from_column()
|
||||
var selection_to_line = editor.get_selection_to_line()
|
||||
var selection_to_column = editor.get_selection_to_column()
|
||||
|
||||
# Convert selection to absolute character index
|
||||
var selection_start_index = _get_absolute_index(selection_from_line, selection_from_column)
|
||||
var selection_end_index = _get_absolute_index(selection_to_line, selection_to_column)
|
||||
|
||||
for match_pos in matches:
|
||||
# Convert match position to absolute character index
|
||||
var match_start_index = _get_absolute_index(match_pos.x, match_pos.y)
|
||||
var match_end_index = match_start_index + search_text.length()
|
||||
|
||||
# Check if match is fully contained in selection
|
||||
if match_start_index >= selection_start_index and match_end_index <= selection_end_index:
|
||||
result.append(match_pos)
|
||||
|
||||
return result
|
||||
|
||||
# Get the next match after the cursor that's inside the selection
|
||||
func get_next_match_in_selection() -> Vector2i:
|
||||
if not editor.has_selection() or not selection_only_checkbox.button_pressed:
|
||||
return Vector2i(-1, -1)
|
||||
|
||||
var matches_in_selection = get_matches_in_selection()
|
||||
if matches_in_selection.is_empty():
|
||||
return Vector2i(-1, -1)
|
||||
|
||||
var cursor_line = editor.get_caret_line()
|
||||
var cursor_column = editor.get_caret_column()
|
||||
|
||||
# Sort matches by position
|
||||
matches_in_selection.sort_custom(func(a, b):
|
||||
if a.x == b.x:
|
||||
return a.y < b.y
|
||||
return a.x < b.x
|
||||
)
|
||||
|
||||
# Find the first match after cursor
|
||||
for match_pos in matches_in_selection:
|
||||
if match_pos.x > cursor_line or (match_pos.x == cursor_line and match_pos.y >= cursor_column):
|
||||
return match_pos
|
||||
|
||||
# If no match after cursor, wrap to first match
|
||||
return matches_in_selection[0]
|
||||
|
||||
func _get_absolute_index(line: int, column: int) -> int:
|
||||
# Calculate absolute character index from line and column
|
||||
var index = 0
|
||||
for i in range(line):
|
||||
index += editor.get_line(i).length() + 1 # +1 for newline
|
||||
|
||||
index += column
|
||||
return index
|
||||
|
||||
# Signal handlers
|
||||
func _on_find_input_changed(_text: String) -> void:
|
||||
trigger_search()
|
||||
|
||||
func _on_find_input_submitted(_text: String) -> void:
|
||||
find_next()
|
||||
|
||||
func _on_option_changed(_toggled: bool) -> void:
|
||||
trigger_search()
|
||||
|
||||
func _on_previous_button_pressed() -> void:
|
||||
find_previous()
|
||||
|
||||
func _on_next_button_pressed() -> void:
|
||||
find_next()
|
||||
|
||||
func _on_hide_button_pressed() -> void:
|
||||
hide_panel()
|
||||
|
||||
func _on_replace_button_pressed() -> void:
|
||||
if not is_instance_valid(editor):
|
||||
return
|
||||
|
||||
var search_text = find_input.text
|
||||
if search_text.is_empty() or matches.is_empty():
|
||||
return
|
||||
|
||||
var replace_text = replace_input.text
|
||||
|
||||
# Different behavior based on Selection Only mode
|
||||
if selection_only_checkbox.button_pressed and editor.has_selection():
|
||||
# Get next match in selection
|
||||
var match_pos = get_next_match_in_selection()
|
||||
if match_pos.x < 0: # No match in selection
|
||||
return
|
||||
|
||||
# Replace the text
|
||||
var line_text = editor.get_line(match_pos.x)
|
||||
var new_line_text = line_text.substr(0, match_pos.y) + replace_text + line_text.substr(match_pos.y + search_text.length())
|
||||
editor.set_line(match_pos.x, new_line_text)
|
||||
|
||||
# Move cursor to the end of the replaced text for next find
|
||||
var cursor_line = match_pos.x
|
||||
var cursor_column = match_pos.y + replace_text.length()
|
||||
|
||||
# Update the document's content
|
||||
editor.text_changed.emit()
|
||||
|
||||
# Refresh search
|
||||
trigger_search()
|
||||
|
||||
# Restore cursor position for next replacement
|
||||
editor.set_caret_line(cursor_line)
|
||||
editor.set_caret_column(cursor_column)
|
||||
else:
|
||||
# Normal replace mode - use the current highlighted match
|
||||
if current_match_index < 0 or current_match_index >= matches.size():
|
||||
return
|
||||
|
||||
# Get current match
|
||||
var match_pos = matches[current_match_index]
|
||||
|
||||
# Replace the text
|
||||
var line_text = editor.get_line(match_pos.x)
|
||||
var new_line_text = line_text.substr(0, match_pos.y) + replace_text + line_text.substr(match_pos.y + search_text.length())
|
||||
editor.set_line(match_pos.x, new_line_text)
|
||||
|
||||
# Update the document's content
|
||||
editor.text_changed.emit()
|
||||
|
||||
# Refresh search
|
||||
trigger_search()
|
||||
|
||||
replace_performed.emit()
|
||||
|
||||
func _on_replace_all_button_pressed() -> void:
|
||||
if not is_instance_valid(editor):
|
||||
return
|
||||
|
||||
var search_text = find_input.text
|
||||
if search_text.is_empty() or matches.is_empty():
|
||||
return
|
||||
|
||||
var replace_text = replace_input.text
|
||||
|
||||
# Determine which matches to replace
|
||||
var matches_to_replace: Array[Vector2i]
|
||||
|
||||
if selection_only_checkbox.button_pressed and editor.has_selection():
|
||||
matches_to_replace = get_matches_in_selection()
|
||||
else:
|
||||
matches_to_replace = matches.duplicate()
|
||||
|
||||
if matches_to_replace.is_empty():
|
||||
return
|
||||
|
||||
# Sort matches in reverse order (to not affect positions of earlier matches)
|
||||
matches_to_replace.sort_custom(func(a, b):
|
||||
if a.x == b.x:
|
||||
return a.y > b.y
|
||||
return a.x > b.x
|
||||
)
|
||||
|
||||
# Process replacements
|
||||
var lines = editor.text.split("\n", false)
|
||||
var replacements_count = 0
|
||||
|
||||
for match_pos in matches_to_replace:
|
||||
# Replace text in the line
|
||||
var line_text = lines[match_pos.x]
|
||||
lines[match_pos.x] = line_text.substr(0, match_pos.y) + replace_text + line_text.substr(match_pos.y + search_text.length())
|
||||
replacements_count += 1
|
||||
|
||||
# Only update if we made changes
|
||||
if replacements_count > 0:
|
||||
# Set the new text
|
||||
editor.text = "\n".join(lines)
|
||||
|
||||
# Update the document's content
|
||||
editor.text_changed.emit()
|
||||
|
||||
# Refresh search
|
||||
trigger_search()
|
||||
|
||||
replace_all_performed.emit()
|
||||
@@ -1 +0,0 @@
|
||||
uid://bep06otwjntx1
|
||||
@@ -1,91 +0,0 @@
|
||||
@tool
|
||||
class_name YAMLEditorMenuBar extends MenuBar
|
||||
|
||||
signal new_file
|
||||
signal open_file
|
||||
signal save_requested
|
||||
signal save_as_requested
|
||||
signal close_requested
|
||||
|
||||
signal undo_requested
|
||||
signal redo_requested
|
||||
|
||||
signal cut_requested
|
||||
signal copy_requested
|
||||
signal paste_requested
|
||||
signal select_all_requested
|
||||
|
||||
signal find_requested
|
||||
signal find_next_requested
|
||||
signal find_previous_requested
|
||||
signal replace_requested
|
||||
|
||||
@export var file_menu: PopupMenu
|
||||
@export var edit_menu: PopupMenu
|
||||
@export var search_menu: PopupMenu
|
||||
|
||||
func _ready() -> void:
|
||||
# Wait for UI to be ready
|
||||
await get_tree().process_frame
|
||||
|
||||
# Set up the menu bar
|
||||
_setup_menus()
|
||||
|
||||
func _setup_menus() -> void:
|
||||
# File menu
|
||||
file_menu.clear()
|
||||
file_menu.add_item("New", 0, KEY_MASK_CTRL | KEY_N)
|
||||
file_menu.add_item("Open...", 1, KEY_MASK_CTRL | KEY_O)
|
||||
file_menu.add_separator()
|
||||
file_menu.add_item("Save", 2, KEY_MASK_CTRL | KEY_S)
|
||||
file_menu.add_item("Save As...", 3, KEY_MASK_CTRL | KEY_MASK_SHIFT | KEY_S)
|
||||
file_menu.add_separator()
|
||||
file_menu.add_item("Close", 4, KEY_MASK_CTRL | KEY_W)
|
||||
|
||||
# Edit menu
|
||||
edit_menu.clear()
|
||||
edit_menu.add_item("Undo", 0, KEY_MASK_CTRL | KEY_Z)
|
||||
edit_menu.add_item("Redo", 1, KEY_MASK_CTRL | KEY_MASK_SHIFT | KEY_Z)
|
||||
edit_menu.add_separator()
|
||||
edit_menu.add_item("Cut", 2, KEY_MASK_CTRL | KEY_X)
|
||||
edit_menu.add_item("Copy", 3, KEY_MASK_CTRL | KEY_C)
|
||||
edit_menu.add_item("Paste", 4, KEY_MASK_CTRL | KEY_V)
|
||||
edit_menu.add_separator()
|
||||
edit_menu.add_item("Select All", 5, KEY_MASK_CTRL | KEY_A)
|
||||
|
||||
# Search menu
|
||||
search_menu.clear()
|
||||
search_menu.add_item("Find...", 0, KEY_MASK_CTRL | KEY_F)
|
||||
search_menu.add_item("Find Next", 1, KEY_F3)
|
||||
search_menu.add_item("Find Previous", 2, KEY_MASK_SHIFT | KEY_F3)
|
||||
search_menu.add_separator()
|
||||
search_menu.add_item("Replace...", 3, KEY_MASK_CTRL | KEY_R)
|
||||
|
||||
# Connect signals
|
||||
file_menu.id_pressed.connect(_on_file_menu_id_pressed)
|
||||
edit_menu.id_pressed.connect(_on_edit_menu_id_pressed)
|
||||
search_menu.id_pressed.connect(_on_search_menu_id_pressed)
|
||||
|
||||
func _on_file_menu_id_pressed(id: int) -> void:
|
||||
match id:
|
||||
0: new_file.emit()
|
||||
1: open_file.emit()
|
||||
2: save_requested.emit()
|
||||
3: save_as_requested.emit()
|
||||
4: close_requested.emit()
|
||||
|
||||
func _on_edit_menu_id_pressed(id: int) -> void:
|
||||
match id:
|
||||
0: undo_requested.emit()
|
||||
1: redo_requested.emit()
|
||||
2: cut_requested.emit()
|
||||
3: copy_requested.emit()
|
||||
4: paste_requested.emit()
|
||||
5: select_all_requested.emit()
|
||||
|
||||
func _on_search_menu_id_pressed(id: int) -> void:
|
||||
match id:
|
||||
0: find_requested.emit()
|
||||
1: find_next_requested.emit()
|
||||
2: find_previous_requested.emit()
|
||||
3: replace_requested.emit()
|
||||
@@ -1 +0,0 @@
|
||||
uid://b35lyu1onhcoj
|
||||
@@ -1,109 +0,0 @@
|
||||
@tool
|
||||
class_name YAMLEditorSessionManager extends Node
|
||||
|
||||
const CONFIG_PATH := "res://.godot/yaml_editor_session.cfg"
|
||||
const CONFIG_SECTION := "yaml_editor"
|
||||
const CONFIG_KEY_OPEN_FILES := "open_files"
|
||||
const CONFIG_KEY_SPLIT_OFFSET := "split_offset"
|
||||
const CONFIG_KEY_CURRENT_FILE := "current_file"
|
||||
|
||||
var file_manager: YAMLEditorDocumentManager
|
||||
var file_system: YAMLFileSystem
|
||||
var config: ConfigFile
|
||||
var autosave_timer: Timer
|
||||
var resizable_container: HSplitContainer
|
||||
|
||||
func _ready() -> void:
|
||||
file_system = YAMLFileSystem.get_singleton()
|
||||
|
||||
config = ConfigFile.new()
|
||||
|
||||
# Setup autosave timer
|
||||
autosave_timer = Timer.new()
|
||||
add_child(autosave_timer)
|
||||
autosave_timer.wait_time = 10.0 # Save session every 10 seconds
|
||||
autosave_timer.one_shot = false
|
||||
autosave_timer.autostart = true
|
||||
autosave_timer.timeout.connect(_on_autosave_timer_timeout)
|
||||
|
||||
func setup(p_file_manager: YAMLEditorDocumentManager, p_resizable_container: HSplitContainer) -> void:
|
||||
file_manager = p_file_manager
|
||||
resizable_container = p_resizable_container
|
||||
|
||||
# Connect to signals
|
||||
file_manager.document_changed.connect(_on_session_changed)
|
||||
file_manager.document_created.connect(_on_session_changed)
|
||||
file_manager.document_closed.connect(_on_session_changed)
|
||||
resizable_container.dragged.connect(_on_split_dragged)
|
||||
|
||||
func _on_split_dragged(_offset: int) -> void:
|
||||
# The split position has changed, save the session
|
||||
_on_session_changed()
|
||||
|
||||
func save_session() -> void:
|
||||
# Don't save anything if we have no files
|
||||
if not is_instance_valid(file_manager):
|
||||
return
|
||||
|
||||
var documents: Array = file_manager.get_open_documents()
|
||||
|
||||
# Create array of persistent file paths (skip untitled files)
|
||||
var persistent_files: PackedStringArray = []
|
||||
for document in documents:
|
||||
if not document.is_untitled():
|
||||
persistent_files.append(document.path)
|
||||
|
||||
# Get current file path
|
||||
var current_path := ""
|
||||
var current_document := file_manager.get_current_document()
|
||||
if current_document and not current_document.is_untitled():
|
||||
current_path = current_document.path
|
||||
|
||||
# Save to config file
|
||||
config.set_value(CONFIG_SECTION, CONFIG_KEY_OPEN_FILES, persistent_files)
|
||||
config.set_value(CONFIG_SECTION, CONFIG_KEY_CURRENT_FILE, current_path)
|
||||
|
||||
# Save the split offset
|
||||
if is_instance_valid(resizable_container):
|
||||
config.set_value(CONFIG_SECTION, CONFIG_KEY_SPLIT_OFFSET, resizable_container.split_offset)
|
||||
|
||||
var error := config.save(CONFIG_PATH)
|
||||
if error != OK:
|
||||
push_error("Failed to save YAML editor session: %s" % error_string(error))
|
||||
|
||||
func load_session() -> void:
|
||||
var error := config.load(CONFIG_PATH)
|
||||
if error != OK:
|
||||
# No saved session or error loading it
|
||||
if error != ERR_FILE_NOT_FOUND:
|
||||
push_error("Failed to load YAML editor session: ", error_string(error))
|
||||
return
|
||||
|
||||
# Get saved file paths
|
||||
var file_paths: PackedStringArray = config.get_value(CONFIG_SECTION, CONFIG_KEY_OPEN_FILES, [])
|
||||
|
||||
# Open each file
|
||||
for path in file_paths:
|
||||
if file_system.file_exists(path):
|
||||
file_manager.open_file(path)
|
||||
|
||||
# Set current file
|
||||
var last_current: String = config.get_value(CONFIG_SECTION, CONFIG_KEY_CURRENT_FILE, "")
|
||||
if not last_current.is_empty() and file_manager.has_document(last_current):
|
||||
var document := file_manager.get_document(last_current)
|
||||
file_manager.set_current_document(document)
|
||||
|
||||
# Restore split offset (deferred to ensure UI is ready)
|
||||
call_deferred("_restore_split_offset")
|
||||
|
||||
func _restore_split_offset() -> void:
|
||||
if is_instance_valid(resizable_container):
|
||||
var saved_offset: int = config.get_value(CONFIG_SECTION, CONFIG_KEY_SPLIT_OFFSET, resizable_container.split_offset)
|
||||
resizable_container.split_offset = saved_offset
|
||||
|
||||
func _on_session_changed(_document = null) -> void:
|
||||
# Set a short timer to prevent saving too frequently during batch operations
|
||||
autosave_timer.start()
|
||||
|
||||
func _on_autosave_timer_timeout() -> void:
|
||||
save_session()
|
||||
@@ -1 +0,0 @@
|
||||
uid://c3hnw5vrco2iy
|
||||
@@ -1,64 +0,0 @@
|
||||
@tool
|
||||
class_name YAMLEditorStatusBar extends HBoxContainer
|
||||
|
||||
@export var editor: YAMLCodeEditor
|
||||
@export var status_label: Label
|
||||
@export var zoom_button: Button
|
||||
@export var line_column_label: Label
|
||||
|
||||
var zoom_popup_menu: PopupMenu
|
||||
|
||||
func _ready() -> void:
|
||||
# Zoom popup menu
|
||||
zoom_popup_menu = PopupMenu.new()
|
||||
add_child(zoom_popup_menu)
|
||||
zoom_popup_menu.add_item("25 %", 0)
|
||||
zoom_popup_menu.add_item("50 %", 1)
|
||||
zoom_popup_menu.add_item("75 %", 2)
|
||||
zoom_popup_menu.add_item("100 %", 3)
|
||||
zoom_popup_menu.add_item("150 %", 4)
|
||||
zoom_popup_menu.add_item("200 %", 5)
|
||||
zoom_popup_menu.add_item("300 %", 6)
|
||||
zoom_popup_menu.id_pressed.connect(_on_zoom_popup_menu_id_pressed)
|
||||
zoom_button.pressed.connect(
|
||||
func():
|
||||
var global_rect := Rect2(get_global_mouse_position(), Vector2.ZERO)
|
||||
zoom_popup_menu.popup_on_parent(global_rect)
|
||||
)
|
||||
|
||||
status_label.set("theme_override_constants/use_pixel_snap", true)
|
||||
|
||||
func _on_zoom_popup_menu_id_pressed(idx: int) -> void:
|
||||
match idx:
|
||||
0: editor.set_zoom(0.25)
|
||||
1: editor.set_zoom(0.5)
|
||||
2: editor.set_zoom(0.75)
|
||||
3: editor.set_zoom(1.0)
|
||||
4: editor.set_zoom(1.5)
|
||||
5: editor.set_zoom(2.0)
|
||||
6: editor.set_zoom(3.0)
|
||||
zoom_popup_menu.hide()
|
||||
|
||||
func set_status(text: String, color := Color.WHITE) -> void:
|
||||
status_label.text = text
|
||||
status_label.modulate = color
|
||||
|
||||
func set_line_column(line_column: Array[int]) -> void:
|
||||
var line := line_column[0]
|
||||
var col := line_column[1]
|
||||
line_column_label.text = "%d : %d" % [line, col]
|
||||
|
||||
func set_zoom_level(level: float) -> void:
|
||||
zoom_button.text = str(int(level * 100)) + " %"
|
||||
|
||||
func set_validation_result(result: YAMLResult) -> void:
|
||||
if !result.has_error():
|
||||
return set_status("")
|
||||
|
||||
var error := result.get_error_message()
|
||||
var line := result.get_error_line()
|
||||
var col := result.get_error_column()
|
||||
var error_text := "Error at (%d, %d): %s" % [line, col, error] if line >= 0 else "Error: %s" % error
|
||||
|
||||
var error_color: Color = EditorInterface.get_editor_settings().get_setting("text_editor/theme/highlighting/brace_mismatch_color")
|
||||
set_status(error_text, error_color)
|
||||
@@ -1 +0,0 @@
|
||||
uid://bkanguis54ea3
|
||||
@@ -1,21 +0,0 @@
|
||||
@tool
|
||||
class_name YAMLEditorSyntaxHighlighter extends EditorSyntaxHighlighter
|
||||
|
||||
var cache: Dictionary = {}
|
||||
var syntax_parser := YAMLSyntaxParser.new()
|
||||
var color_provider := YAMLSyntaxParser.ColorProvider.new()
|
||||
|
||||
func clear_highlighting_cache() -> void:
|
||||
cache.clear()
|
||||
|
||||
func _get_line_syntax_highlighting(line: int) -> Dictionary:
|
||||
var text: String = get_text_edit().get_line(line)
|
||||
if text in cache:
|
||||
return cache[text]
|
||||
|
||||
color_provider.update_theme()
|
||||
cache[text] = _highlight_line(text)
|
||||
return cache[text]
|
||||
|
||||
func _highlight_line(text: String) -> Dictionary:
|
||||
return syntax_parser.highlight_line(text, color_provider)
|
||||
@@ -1 +0,0 @@
|
||||
uid://c0q2685eddg35
|
||||
@@ -1,20 +0,0 @@
|
||||
class_name YAMLSyntaxHighlighter extends SyntaxHighlighter
|
||||
|
||||
var cache: Dictionary = {}
|
||||
var syntax_parser := YAMLSyntaxParser.new()
|
||||
var color_provider := YAMLSyntaxParser.ColorProvider.new()
|
||||
|
||||
func clear_highlighting_cache() -> void:
|
||||
cache.clear()
|
||||
|
||||
func _get_line_syntax_highlighting(line: int) -> Dictionary:
|
||||
var text: String = get_text_edit().get_line(line)
|
||||
if text in cache:
|
||||
return cache[text]
|
||||
|
||||
color_provider.update_theme()
|
||||
cache[text] = _highlight_line(text)
|
||||
return cache[text]
|
||||
|
||||
func _highlight_line(text: String) -> Dictionary:
|
||||
return syntax_parser.highlight_line(text, color_provider)
|
||||
@@ -1 +0,0 @@
|
||||
uid://8yvnf7wlu3ud
|
||||
@@ -1,384 +0,0 @@
|
||||
@tool
|
||||
class_name YAMLSyntaxParser extends RefCounted
|
||||
|
||||
## Token types
|
||||
enum TokenType {
|
||||
TEXT, # For keys only
|
||||
COMMENT, # Comments
|
||||
SYMBOL, # Structural elements like :, -, >, |, &, *, [, ], {, }
|
||||
STRING, # String values (default for unmatched values)
|
||||
NUMBER, # Numeric values
|
||||
KEYWORD, # Booleans, null, merge keys, tags
|
||||
DOCUMENT_SEPARATOR, # New document separator
|
||||
}
|
||||
|
||||
var re_patterns := {
|
||||
"merge_key": RegEx.create_from_string("^\\s*<<:\\s*\\*[^\\s]+"),
|
||||
"multiline_indicator": RegEx.create_from_string("(>|\\|-?)\\s*$"),
|
||||
"array_item": RegEx.create_from_string("^(\\s*-(?:\\s*-)*\\s*)(.*)$"),
|
||||
"key_value": RegEx.create_from_string("^\\s*([^:]+):(.*)$"),
|
||||
|
||||
# Scalar patterns
|
||||
"quoted_string": RegEx.create_from_string("^(['\"])(?:\\\\.|[^\\\\])*\\1$"),
|
||||
"number": RegEx.create_from_string("^(?:0[xX][0-9a-fA-F]+|0[oO][0-7]+|0[bB][0-1]+|[-+]?(?:\\d+\\.?\\d*|\\.\\d+)(?:[eE][-+]?\\d+)?)$"),
|
||||
"boolean": RegEx.create_from_string("^(true|false)$"),
|
||||
"nullish": RegEx.create_from_string("^(null|~)$"),
|
||||
"special": RegEx.create_from_string("^(\\.inf|\\.nan)$"),
|
||||
|
||||
# YAML functionality
|
||||
"anchor": RegEx.create_from_string("^\\s*&([^\\s]+)"),
|
||||
"alias": RegEx.create_from_string("^\\s*\\*([^\\s]+)"),
|
||||
"tag": RegEx.create_from_string("!(?:![\\w\\-\\.]+|[^\\s\\[\\]{}'\"`]+)"),
|
||||
"top_level_tag": RegEx.create_from_string("^\\s*(!(?:![\\w\\-\\.]+|[^\\s\\[\\]{}'\"`]+))\\s*(.*)$"),
|
||||
"document_separator": RegEx.create_from_string("^---$")
|
||||
}
|
||||
|
||||
class ParserState:
|
||||
var in_string: bool = false
|
||||
var string_char: String = ""
|
||||
var stack: Array = [] # For nested flow collections
|
||||
var token_start: int = -1
|
||||
var colors: Dictionary = {}
|
||||
|
||||
func push(char: String) -> void:
|
||||
stack.push_back(char)
|
||||
|
||||
func pop() -> String:
|
||||
return stack.pop_back() if not stack.is_empty() else ""
|
||||
|
||||
func peek() -> String:
|
||||
return stack.back() if not stack.is_empty() else ""
|
||||
|
||||
## Provides colors to the syntax highlighter
|
||||
class ColorProvider:
|
||||
# Default theme colors
|
||||
var theme: Dictionary = {
|
||||
"text": Color(0.8025, 0.81, 0.8225, 1),
|
||||
"comment": Color(0.8025, 0.81, 0.8225, 0.5),
|
||||
"symbol": Color(0.67, 0.79, 1, 1),
|
||||
"string": Color(1, 0.93, 0.63, 1),
|
||||
"number": Color(0.63, 1, 0.88, 1),
|
||||
"keyword": Color(1, 0.44, 0.52, 1),
|
||||
"document_separator": Color(0.8025, 0.81, 0.8225, 0.5)
|
||||
}
|
||||
|
||||
func _init():
|
||||
update_theme()
|
||||
|
||||
## Updates the theme colors from Godot Editor settings
|
||||
func update_theme() -> void:
|
||||
# Only inside the Godot Editor
|
||||
if !Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
# Read theme from editor settings
|
||||
var settings = EditorInterface.get_editor_settings()
|
||||
theme = {
|
||||
"text": settings.get_setting("text_editor/theme/highlighting/text_color"),
|
||||
"comment": settings.get_setting("text_editor/theme/highlighting/comment_color"),
|
||||
"symbol": settings.get_setting("text_editor/theme/highlighting/symbol_color"),
|
||||
"string": settings.get_setting("text_editor/theme/highlighting/string_color"),
|
||||
"number": settings.get_setting("text_editor/theme/highlighting/number_color"),
|
||||
"keyword": settings.get_setting("text_editor/theme/highlighting/keyword_color"),
|
||||
"document_separator": settings.get_setting("text_editor/theme/highlighting/comment_color"),
|
||||
}
|
||||
|
||||
## Get color for tokens
|
||||
func get_color_for_type(type: TokenType) -> Color:
|
||||
match type:
|
||||
YAMLSyntaxParser.TokenType.TEXT: return theme.text
|
||||
YAMLSyntaxParser.TokenType.COMMENT: return theme.comment
|
||||
YAMLSyntaxParser.TokenType.SYMBOL: return theme.symbol
|
||||
YAMLSyntaxParser.TokenType.STRING: return theme.string
|
||||
YAMLSyntaxParser.TokenType.NUMBER: return theme.number
|
||||
YAMLSyntaxParser.TokenType.KEYWORD: return theme.keyword
|
||||
YAMLSyntaxParser.TokenType.DOCUMENT_SEPARATOR: return theme.document_separator
|
||||
_: return theme.string # Default fallback is string color
|
||||
|
||||
## Highlight a line of YAML
|
||||
func highlight_line(text: String, color_provider: ColorProvider = ColorProvider.new()) -> Dictionary:
|
||||
var comment_pos := _find_comment_start(text)
|
||||
var content := text if comment_pos == -1 else text.substr(0, comment_pos).rstrip(" \t")
|
||||
var colors := {}
|
||||
|
||||
if content.strip_edges():
|
||||
colors = _highlight_line_content(content, color_provider)
|
||||
|
||||
if comment_pos != -1:
|
||||
colors[comment_pos] = {"color": color_provider.get_color_for_type(TokenType.COMMENT)}
|
||||
|
||||
return _sort_colors(colors)
|
||||
|
||||
# Find the beginning position of a comment
|
||||
func _find_comment_start(text: String) -> int:
|
||||
var in_string := false
|
||||
var string_char := ""
|
||||
|
||||
for i in range(text.length()):
|
||||
var char := text[i]
|
||||
|
||||
if not in_string:
|
||||
if char in ['"', "'"] and (i == 0 or text[i - 1] != '\\'):
|
||||
in_string = true
|
||||
string_char = char
|
||||
elif char == '#' and (i == 0 or text[i - 1] == ' ' or text[i - 1] == '\t'):
|
||||
return i
|
||||
else:
|
||||
if char == string_char and (i == 0 or text[i - 1] != '\\'):
|
||||
in_string = false
|
||||
string_char = ""
|
||||
|
||||
return -1
|
||||
|
||||
# Highlight line content
|
||||
func _highlight_line_content(text: String, color_provider: ColorProvider) -> Dictionary:
|
||||
# Handle document separator
|
||||
var separator_match: RegExMatch = re_patterns.document_separator.search(text)
|
||||
if separator_match:
|
||||
return {0: {"color": color_provider.get_color_for_type(TokenType.DOCUMENT_SEPARATOR)}}
|
||||
|
||||
# Handle merge keys
|
||||
var merge_match: RegExMatch = re_patterns.merge_key.search(text)
|
||||
if merge_match:
|
||||
return {
|
||||
merge_match.get_start(0): {"color": color_provider.get_color_for_type(TokenType.KEYWORD)}
|
||||
}
|
||||
|
||||
# Check for top-level tags
|
||||
var tag_match: RegExMatch = re_patterns.top_level_tag.search(text)
|
||||
if tag_match:
|
||||
var colors := {}
|
||||
# Color just the tag part as keyword (red)
|
||||
_add_color(color_provider, colors, tag_match.get_start(1), tag_match.get_end(1), TokenType.KEYWORD)
|
||||
|
||||
# Process any remaining content after the tag
|
||||
var remaining = tag_match.get_string(2).strip_edges()
|
||||
if remaining:
|
||||
var remaining_start = text.find(remaining, tag_match.get_end(1))
|
||||
if remaining_start != -1:
|
||||
if remaining.begins_with("{") or remaining.begins_with("["):
|
||||
colors.merge(_parse_flow_style(color_provider, remaining, remaining_start))
|
||||
else:
|
||||
_add_scalar_color(color_provider, colors, remaining, remaining_start)
|
||||
return colors
|
||||
|
||||
# Handle array items
|
||||
var array_match: RegExMatch = re_patterns.array_item.search(text)
|
||||
if array_match:
|
||||
var colors := {}
|
||||
|
||||
# Color the entire dash section as symbols
|
||||
_add_color(color_provider, colors, array_match.get_start(1), array_match.get_end(1), TokenType.SYMBOL)
|
||||
|
||||
# Process the content after the dashes
|
||||
var content: String = array_match.get_string(2).strip_edges()
|
||||
if content:
|
||||
var content_start: int = array_match.get_start(2)
|
||||
if content.begins_with("[") or content.begins_with("{"):
|
||||
colors.merge(_parse_flow_style(color_provider, content, content_start))
|
||||
else:
|
||||
_add_scalar_color(color_provider, colors, content, content_start)
|
||||
return colors
|
||||
|
||||
# Handle regular key-value pairs
|
||||
var key_value_match: RegExMatch = re_patterns.key_value.search(text)
|
||||
if key_value_match:
|
||||
return _parse_key_value(color_provider, text, key_value_match)
|
||||
|
||||
# Handle flow-style collections at the root level
|
||||
if "[" in text or "{" in text:
|
||||
return _parse_flow_style(color_provider, text, 0)
|
||||
|
||||
# Handle multi-line string indicators
|
||||
var multiline_match: RegExMatch = re_patterns.multiline_indicator.search(text)
|
||||
if multiline_match:
|
||||
var colors := {}
|
||||
# Color the indicator (> or |) as symbol
|
||||
_add_color(color_provider, colors, multiline_match.get_start(1), multiline_match.get_end(1), TokenType.SYMBOL)
|
||||
return colors
|
||||
|
||||
# Default case: treat as string content (for multi-line string content)
|
||||
if text.strip_edges():
|
||||
return {0: {"color": color_provider.get_color_for_type(TokenType.STRING)}}
|
||||
|
||||
return {}
|
||||
|
||||
# Parse flow collections
|
||||
func _parse_flow_style(color_provider: ColorProvider, text: String, offset: int) -> Dictionary:
|
||||
var state := ParserState.new()
|
||||
var pos := 0
|
||||
|
||||
while pos < text.length():
|
||||
var char := text[pos]
|
||||
|
||||
# Handle string literals
|
||||
if char in ['"', "'"] and (pos == 0 or text[pos - 1] != '\\'):
|
||||
if not state.in_string:
|
||||
state.in_string = true
|
||||
state.string_char = char
|
||||
state.token_start = pos
|
||||
elif char == state.string_char:
|
||||
state.in_string = false
|
||||
_add_color(color_provider, state.colors, offset + state.token_start, offset + pos + 1, TokenType.STRING)
|
||||
state.token_start = -1
|
||||
|
||||
# Handle flow collection brackets when not in string
|
||||
elif not state.in_string:
|
||||
if char in ['[', '{']:
|
||||
state.push(char)
|
||||
_add_color(color_provider, state.colors, offset + pos, offset + pos + 1, TokenType.SYMBOL)
|
||||
state.token_start = pos + 1
|
||||
|
||||
elif char in [']', '}']:
|
||||
var matching := '[' if char == ']' else '{'
|
||||
if state.peek() == matching:
|
||||
state.pop()
|
||||
if state.token_start != -1:
|
||||
var token := text.substr(state.token_start, pos - state.token_start).strip_edges()
|
||||
if token:
|
||||
_add_scalar_color(color_provider, state.colors, token, offset + state.token_start)
|
||||
_add_color(color_provider, state.colors, offset + pos, offset + pos + 1, TokenType.SYMBOL)
|
||||
state.token_start = -1
|
||||
|
||||
elif char in [':', ',']:
|
||||
if state.token_start != -1:
|
||||
var token := text.substr(state.token_start, pos - state.token_start).strip_edges()
|
||||
if token:
|
||||
if char == ':':
|
||||
# All map keys should be text colored, regardless of content
|
||||
_add_color(color_provider, state.colors, offset + state.token_start, offset + pos, TokenType.TEXT)
|
||||
else:
|
||||
_add_scalar_color(color_provider, state.colors, token, offset + state.token_start)
|
||||
_add_color(color_provider, state.colors, offset + pos, offset + pos + 1, TokenType.SYMBOL)
|
||||
state.token_start = pos + 1
|
||||
|
||||
elif char != ' ' and state.token_start == -1:
|
||||
state.token_start = pos
|
||||
|
||||
pos += 1
|
||||
|
||||
# Handle any remaining token
|
||||
if state.token_start != -1 and state.token_start < pos:
|
||||
var token := text.substr(state.token_start, pos - state.token_start).strip_edges()
|
||||
if token:
|
||||
# Check if this is a key in a map context
|
||||
if not state.stack.is_empty() and state.stack.back() == '{' and ':' in text.substr(pos):
|
||||
_add_color(color_provider, state.colors, offset + state.token_start, offset + pos, TokenType.TEXT)
|
||||
else:
|
||||
_add_scalar_color(color_provider, state.colors, token, offset + state.token_start)
|
||||
|
||||
return state.colors
|
||||
|
||||
# Parse dictionary key and value
|
||||
func _parse_key_value(color_provider: ColorProvider, text: String, match: RegExMatch) -> Dictionary:
|
||||
var colors := {}
|
||||
|
||||
# Color the key
|
||||
_add_color(color_provider, colors, match.get_start(1), match.get_end(1), TokenType.TEXT)
|
||||
|
||||
# Color the colon
|
||||
_add_color(color_provider,colors, match.get_end(1), match.get_end(1) + 1, TokenType.SYMBOL)
|
||||
|
||||
# Get and process the value if present
|
||||
var value := match.get_string(2).strip_edges()
|
||||
if value:
|
||||
var value_start := text.find(value, match.get_end(1))
|
||||
if value_start != -1:
|
||||
# First check for and handle any tags
|
||||
var tag_match: RegExMatch = re_patterns.tag.search(value)
|
||||
if tag_match:
|
||||
_add_color(color_provider, colors, value_start + tag_match.get_start(0),
|
||||
value_start + tag_match.get_end(0), TokenType.KEYWORD)
|
||||
# Get remaining content after tag
|
||||
var after_tag := value.substr(tag_match.get_end(0)).strip_edges()
|
||||
if after_tag:
|
||||
var after_tag_start = text.find(after_tag, value_start + tag_match.get_end(0))
|
||||
if after_tag_start != -1:
|
||||
# Now check for multiline indicator in remaining content
|
||||
var indicator_match: RegExMatch = re_patterns.multiline_indicator.search(after_tag)
|
||||
if indicator_match:
|
||||
_add_color(color_provider, colors, after_tag_start + indicator_match.get_start(1), after_tag_start + indicator_match.get_end(1), TokenType.SYMBOL)
|
||||
elif after_tag.begins_with("{") or after_tag.begins_with("["):
|
||||
# Process flow style collections after the tag
|
||||
colors.merge(_parse_flow_style(color_provider, after_tag, after_tag_start))
|
||||
else:
|
||||
# Process normal scalar after the tag
|
||||
_add_scalar_color(color_provider, colors, after_tag, after_tag_start)
|
||||
return colors
|
||||
|
||||
# If no tag, check for multiline indicator in full value
|
||||
var indicator_match: RegExMatch = re_patterns.multiline_indicator.search(value)
|
||||
if indicator_match:
|
||||
_add_color(color_provider, colors, value_start + indicator_match.get_start(1), value_start + indicator_match.get_end(1), TokenType.SYMBOL)
|
||||
elif value.begins_with("[") or value.begins_with("{"):
|
||||
colors.merge(_parse_flow_style(color_provider, value, value_start))
|
||||
else:
|
||||
_add_scalar_color(color_provider, colors, value, value_start)
|
||||
return colors
|
||||
|
||||
# Colors for scalar values
|
||||
func _add_scalar_color(color_provider: ColorProvider, colors: Dictionary, token: String, start_index: int) -> void:
|
||||
# Handle empty or whitespace-only tokens
|
||||
token = token.strip_edges()
|
||||
if token.is_empty():
|
||||
return
|
||||
|
||||
# Check for quoted strings first
|
||||
if re_patterns.quoted_string.search(token):
|
||||
_add_color(color_provider, colors, start_index, start_index + token.length(), TokenType.STRING)
|
||||
return # Important: return early to prevent parsing tags inside strings
|
||||
|
||||
# Check for tags
|
||||
var tag_match: RegExMatch = re_patterns.tag.search(token)
|
||||
if tag_match:
|
||||
var tag_start := tag_match.get_start(0)
|
||||
var tag_end := tag_match.get_end(0)
|
||||
|
||||
# Only color the tag portion
|
||||
_add_color(color_provider, colors, start_index + tag_start, start_index + tag_end, TokenType.KEYWORD)
|
||||
|
||||
# Process any remaining content after the tag
|
||||
if tag_end < token.length():
|
||||
var remaining := token.substr(tag_end).strip_edges()
|
||||
if remaining:
|
||||
var remaining_start = start_index + token.find(remaining, tag_end)
|
||||
if remaining_start != -1:
|
||||
if remaining.begins_with("{") or remaining.begins_with("["):
|
||||
colors.merge(_parse_flow_style(color_provider, remaining, remaining_start))
|
||||
else:
|
||||
# Apply appropriate coloring for the remaining content
|
||||
if re_patterns.number.search(remaining):
|
||||
_add_color(color_provider, colors, remaining_start, remaining_start + remaining.length(), TokenType.NUMBER)
|
||||
elif re_patterns.boolean.search(remaining) or re_patterns.nullish.search(remaining) or re_patterns.special.search(remaining):
|
||||
_add_color(color_provider, colors, remaining_start, remaining_start + remaining.length(), TokenType.KEYWORD)
|
||||
else:
|
||||
_add_color(color_provider, colors, remaining_start, remaining_start + remaining.length(), TokenType.STRING)
|
||||
return
|
||||
|
||||
# Rest of the scalar checks for non-tag content
|
||||
elif re_patterns.number.search(token):
|
||||
_add_color(color_provider, colors, start_index, start_index + token.length(), TokenType.NUMBER)
|
||||
elif re_patterns.boolean.search(token) or re_patterns.nullish.search(token) or re_patterns.special.search(token):
|
||||
_add_color(color_provider, colors, start_index, start_index + token.length(), TokenType.KEYWORD)
|
||||
elif re_patterns.anchor.search(token) or re_patterns.alias.search(token):
|
||||
_add_color(color_provider, colors, start_index, start_index + token.length(), TokenType.SYMBOL)
|
||||
else:
|
||||
# Default fallback is string color
|
||||
_add_color(color_provider, colors, start_index, start_index + token.length(), TokenType.STRING)
|
||||
|
||||
# Add color for a type
|
||||
func _add_color(color_provider: ColorProvider, colors: Dictionary, start: int, end: int, type: TokenType) -> void:
|
||||
colors[start] = {"color": color_provider.get_color_for_type(type)}
|
||||
|
||||
# Sort the colors dictionary by index
|
||||
func _sort_colors(colors: Dictionary) -> Dictionary:
|
||||
# Get all indices as an array
|
||||
var indices := colors.keys()
|
||||
indices.sort() # Sort indices in ascending order
|
||||
|
||||
# Create new dictionary with sorted indices
|
||||
var sorted_colors := {}
|
||||
for idx in indices:
|
||||
sorted_colors[idx] = colors[idx]
|
||||
|
||||
return sorted_colors
|
||||
@@ -1 +0,0 @@
|
||||
uid://dco608883k7yl
|
||||
@@ -1,109 +0,0 @@
|
||||
@tool
|
||||
class_name YAMLEditorValidator extends Node
|
||||
|
||||
signal validation_completed(document)
|
||||
|
||||
var _thread: Thread
|
||||
var _is_validating: bool = false
|
||||
var _pending_validation: bool = false
|
||||
var _validation_queue: Array = []
|
||||
|
||||
var code_editor: YAMLCodeEditor
|
||||
var validation_timer: Timer
|
||||
var file_system: YAMLFileSystem
|
||||
var file_manager: YAMLEditorDocumentManager
|
||||
|
||||
func _ready() -> void:
|
||||
file_system = YAMLFileSystem.get_singleton()
|
||||
|
||||
# Create validation timer
|
||||
validation_timer = Timer.new()
|
||||
add_child(validation_timer)
|
||||
validation_timer.one_shot = true
|
||||
validation_timer.wait_time = 0.5 # 500ms delay
|
||||
validation_timer.timeout.connect(_on_validation_timer_timeout)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if _thread and _thread.is_started():
|
||||
_thread.wait_to_finish()
|
||||
|
||||
func setup(p_code_editor: YAMLCodeEditor, p_file_manager: YAMLEditorDocumentManager) -> void:
|
||||
code_editor = p_code_editor
|
||||
file_manager = p_file_manager
|
||||
|
||||
# Connect to code editor changes
|
||||
code_editor.validation_requested.connect(_on_validation_requested)
|
||||
|
||||
# Connect to document changes
|
||||
file_manager.document_changed.connect(_on_document_changed)
|
||||
file_manager.document_created.connect(_on_document_created)
|
||||
|
||||
func _on_validation_requested() -> void:
|
||||
# Reset and start the validation timer
|
||||
validation_timer.stop()
|
||||
validation_timer.start()
|
||||
|
||||
func _on_validation_timer_timeout() -> void:
|
||||
var document = file_manager.get_current_document()
|
||||
if document:
|
||||
validate_document(document)
|
||||
|
||||
func _on_document_changed(document: YAMLEditorDocument) -> void:
|
||||
# Show any existing validation results
|
||||
if document.validation_result:
|
||||
validation_completed.emit(document)
|
||||
|
||||
# Run validation if no results exist or document has errors
|
||||
if document.validation_result == null or document.has_error():
|
||||
validate_document(document)
|
||||
|
||||
func _on_document_created(document: YAMLEditorDocument) -> void:
|
||||
# Validate new document
|
||||
validate_document(document)
|
||||
|
||||
func validate_document(document: YAMLEditorDocument) -> void:
|
||||
if document == null:
|
||||
return
|
||||
|
||||
if _is_validating:
|
||||
# Add to validation queue
|
||||
if not _validation_queue.has(document):
|
||||
_validation_queue.append(document)
|
||||
return
|
||||
|
||||
_is_validating = true
|
||||
|
||||
if _thread and _thread.is_started():
|
||||
_thread.wait_to_finish()
|
||||
|
||||
_thread = Thread.new()
|
||||
_thread.start(_validation_thread_function.bind(document))
|
||||
|
||||
func _validation_thread_function(document: YAMLEditorDocument) -> void:
|
||||
# YAML validation is thread-safe
|
||||
var result = YAML.validate_syntax(document.content)
|
||||
|
||||
# Update document on main thread
|
||||
call_deferred("_finish_validation", document, result)
|
||||
|
||||
func _finish_validation(document: YAMLEditorDocument, result: YAMLResult) -> void:
|
||||
# Update document with validation result
|
||||
document.set_validation_result(result)
|
||||
|
||||
# Emit signal
|
||||
validation_completed.emit(document)
|
||||
|
||||
# Process any pending validations
|
||||
_is_validating = false
|
||||
|
||||
if not _validation_queue.is_empty():
|
||||
var next_document = _validation_queue.pop_front()
|
||||
validate_document(next_document)
|
||||
|
||||
func mark_error_in_editor(line: int, message: String) -> void:
|
||||
if is_instance_valid(code_editor):
|
||||
code_editor.mark_error_line(line, message)
|
||||
|
||||
func clear_errors_in_editor() -> void:
|
||||
if is_instance_valid(code_editor):
|
||||
code_editor.clear_error_indicators()
|
||||
@@ -1 +0,0 @@
|
||||
uid://dbtbggxu57ydy
|
||||
@@ -1,350 +0,0 @@
|
||||
@tool
|
||||
class_name YAMLEditor extends Control
|
||||
|
||||
# Components
|
||||
var file_manager: YAMLEditorDocumentManager
|
||||
var validator: YAMLEditorValidator
|
||||
var session_manager: YAMLEditorSessionManager
|
||||
|
||||
# File system singleton
|
||||
var file_system: YAMLFileSystem
|
||||
|
||||
# UI references
|
||||
@export var menu_bar: YAMLEditorMenuBar
|
||||
@export var file_list: YAMLEditorFileList
|
||||
@export var resizable_container: HSplitContainer
|
||||
@export var code_edit: YAMLCodeEditor
|
||||
@export var status_panel: YAMLEditorStatusBar
|
||||
@export var find_replace_panel: YAMLEditorFindReplaceBar
|
||||
|
||||
func _ready() -> void:
|
||||
# Get reference to file system singleton first
|
||||
file_system = YAMLFileSystem.get_singleton()
|
||||
|
||||
# Initialize components
|
||||
file_manager = YAMLEditorDocumentManager.new(resizable_container)
|
||||
add_child(file_manager)
|
||||
|
||||
validator = YAMLEditorValidator.new()
|
||||
add_child(validator)
|
||||
|
||||
session_manager = YAMLEditorSessionManager.new()
|
||||
add_child(session_manager)
|
||||
|
||||
# Wait for UI to be ready
|
||||
await get_tree().process_frame
|
||||
|
||||
# Set up components
|
||||
file_manager.setup(file_list, code_edit)
|
||||
validator.setup(code_edit, file_manager)
|
||||
session_manager.setup(file_manager, resizable_container)
|
||||
|
||||
# Connect menu signals for file operations
|
||||
menu_bar.new_file.connect(_on_new_button_pressed)
|
||||
menu_bar.open_file.connect(_on_open_button_pressed)
|
||||
menu_bar.save_requested.connect(_on_save_button_pressed)
|
||||
menu_bar.save_as_requested.connect(_on_save_as_button_pressed)
|
||||
menu_bar.close_requested.connect(_on_close_current_file)
|
||||
|
||||
# Connect menu signals for edit options
|
||||
menu_bar.undo_requested.connect(_on_undo_requested)
|
||||
menu_bar.redo_requested.connect(_on_redo_requested)
|
||||
menu_bar.cut_requested.connect(_on_cut_requested)
|
||||
menu_bar.copy_requested.connect(_on_copy_requested)
|
||||
menu_bar.paste_requested.connect(_on_paste_requested)
|
||||
menu_bar.select_all_requested.connect(_on_select_all_requested)
|
||||
|
||||
# Connect menu signals for search
|
||||
menu_bar.find_requested.connect(_on_find_requested)
|
||||
menu_bar.find_next_requested.connect(_on_find_next_requested)
|
||||
menu_bar.find_previous_requested.connect(_on_find_previous_requested)
|
||||
menu_bar.replace_requested.connect(_on_replace_requested)
|
||||
|
||||
# Connect code editor signals
|
||||
code_edit.content_changed.connect(_on_content_changed)
|
||||
code_edit.save_requested.connect(_on_save_button_pressed)
|
||||
code_edit.close_requested.connect(_on_close_current_file)
|
||||
code_edit.undo_requested.connect(_on_undo_requested)
|
||||
code_edit.redo_requested.connect(_on_redo_requested)
|
||||
code_edit.caret_changed.connect(_on_caret_changed)
|
||||
code_edit.zoom_changed.connect(_on_zoom_changed) # Connect to new zoom signal
|
||||
|
||||
# Connect file manager signals
|
||||
file_manager.document_changed.connect(_on_document_changed)
|
||||
|
||||
# Connect validation signals
|
||||
validator.validation_completed.connect(_on_validation_completed)
|
||||
|
||||
# Set initial zoom text
|
||||
_on_zoom_changed(code_edit.zoom_level)
|
||||
|
||||
# Setup the find and replace panels
|
||||
find_replace_panel.replace_performed.connect(_on_replace_performed)
|
||||
find_replace_panel.replace_all_performed.connect(_on_replace_all_performed)
|
||||
|
||||
# Load previous session
|
||||
session_manager.load_session()
|
||||
|
||||
func _input(event):
|
||||
if event is InputEventKey and event.keycode == KEY_ESCAPE and event.pressed:
|
||||
if find_replace_panel.visible:
|
||||
find_replace_panel.hide_panel()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
func _unhandled_key_input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed:
|
||||
match event.get_keycode_with_modifiers():
|
||||
KEY_MASK_CTRL | KEY_F:
|
||||
_on_find_requested()
|
||||
get_viewport().set_input_as_handled()
|
||||
KEY_MASK_CTRL | KEY_R:
|
||||
_on_replace_requested()
|
||||
get_viewport().set_input_as_handled()
|
||||
KEY_F3:
|
||||
_on_find_next_requested()
|
||||
get_viewport().set_input_as_handled()
|
||||
KEY_MASK_SHIFT | KEY_F3:
|
||||
_on_find_previous_requested()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
func _on_zoom_changed(new_zoom_level: float) -> void:
|
||||
status_panel.set_zoom_level(new_zoom_level)
|
||||
|
||||
func _on_new_button_pressed() -> void:
|
||||
file_manager.new_file()
|
||||
|
||||
func _on_open_button_pressed() -> void:
|
||||
var file_dialog := EditorFileDialog.new()
|
||||
file_dialog.file_mode = EditorFileDialog.FILE_MODE_OPEN_FILE
|
||||
file_dialog.access = EditorFileDialog.ACCESS_FILESYSTEM
|
||||
file_dialog.add_filter("*.yaml;YAML Files")
|
||||
file_dialog.add_filter("*.yml;YML Files")
|
||||
file_dialog.title = "Open YAML File"
|
||||
|
||||
file_dialog.file_selected.connect(
|
||||
func(path):
|
||||
file_manager.open_file(path)
|
||||
file_dialog.queue_free()
|
||||
)
|
||||
file_dialog.canceled.connect(func(): file_dialog.queue_free())
|
||||
|
||||
add_child(file_dialog)
|
||||
file_dialog.popup_centered_ratio(0.7)
|
||||
|
||||
func _on_save_button_pressed() -> void:
|
||||
var document := file_manager.get_current_document()
|
||||
if not document:
|
||||
return
|
||||
|
||||
if document.is_untitled():
|
||||
_on_save_as_button_pressed()
|
||||
else:
|
||||
file_manager.save_document(document)
|
||||
|
||||
func _on_save_as_button_pressed() -> void:
|
||||
var document := file_manager.get_current_document()
|
||||
if not document:
|
||||
return
|
||||
|
||||
var file_dialog := EditorFileDialog.new()
|
||||
file_dialog.file_mode = EditorFileDialog.FILE_MODE_SAVE_FILE
|
||||
file_dialog.access = EditorFileDialog.ACCESS_FILESYSTEM
|
||||
file_dialog.add_filter("*.yaml;YAML Files")
|
||||
file_dialog.add_filter("*.yml;YML Files")
|
||||
file_dialog.title = "Save YAML File As"
|
||||
|
||||
if not document.is_untitled():
|
||||
file_dialog.current_path = document.path
|
||||
|
||||
file_dialog.file_selected.connect(
|
||||
func(path):
|
||||
file_manager.save_document_as(document, path)
|
||||
file_dialog.queue_free()
|
||||
)
|
||||
file_dialog.canceled.connect(func(): file_dialog.queue_free())
|
||||
|
||||
add_child(file_dialog)
|
||||
file_dialog.popup_centered_ratio(0.7)
|
||||
|
||||
func _on_content_changed() -> void:
|
||||
var document := file_manager.get_current_document()
|
||||
if not document:
|
||||
return
|
||||
|
||||
# Update document content
|
||||
file_manager.update_document_content(document, code_edit.text)
|
||||
|
||||
# Request validation
|
||||
validator.validate_document(document)
|
||||
|
||||
func _on_close_current_file() -> void:
|
||||
var document := file_manager.get_current_document()
|
||||
if document:
|
||||
file_manager.close_document(document)
|
||||
|
||||
func _on_undo_requested() -> void:
|
||||
var document := file_manager.get_current_document()
|
||||
if not document:
|
||||
return
|
||||
|
||||
var state := document.undo()
|
||||
if not state:
|
||||
return
|
||||
|
||||
code_edit.set_text_and_preserve_state(document.content)
|
||||
|
||||
# Optionally restore caret position if needed
|
||||
if state.caret_line > 0 and state.caret_column > 0:
|
||||
if state.caret_line < code_edit.get_line_count():
|
||||
code_edit.set_caret_line(state.caret_line)
|
||||
if state.caret_column <= code_edit.get_line(state.caret_line).length():
|
||||
code_edit.set_caret_column(state.caret_column)
|
||||
|
||||
# Validate after undo
|
||||
validator.validate_document(document)
|
||||
|
||||
# And re-trigger search if we did undo
|
||||
if find_replace_panel.visible:
|
||||
find_replace_panel.trigger_search()
|
||||
|
||||
func _on_redo_requested() -> void:
|
||||
var document := file_manager.get_current_document()
|
||||
if not document:
|
||||
return
|
||||
|
||||
var state := document.redo()
|
||||
if not state:
|
||||
return
|
||||
|
||||
code_edit.set_text_and_preserve_state(document.content)
|
||||
|
||||
# Optionally restore caret position if needed
|
||||
if state.caret_line > 0 and state.caret_column > 0:
|
||||
if state.caret_line < code_edit.get_line_count():
|
||||
code_edit.set_caret_line(state.caret_line)
|
||||
if state.caret_column <= code_edit.get_line(state.caret_line).length():
|
||||
code_edit.set_caret_column(state.caret_column)
|
||||
|
||||
# Validate after redo
|
||||
validator.validate_document(document)
|
||||
|
||||
# And re-trigger search if we did redo
|
||||
if find_replace_panel.visible:
|
||||
find_replace_panel.trigger_search()
|
||||
|
||||
func _on_cut_requested() -> void:
|
||||
code_edit.cut_selection()
|
||||
|
||||
func _on_copy_requested() -> void:
|
||||
code_edit.copy_selection()
|
||||
|
||||
func _on_paste_requested() -> void:
|
||||
code_edit.paste_clipboard()
|
||||
|
||||
func _on_select_all_requested() -> void:
|
||||
code_edit.select_all()
|
||||
|
||||
# Search-related methods
|
||||
func _on_find_requested() -> void:
|
||||
find_replace_panel.show_find_panel()
|
||||
# Hide replace panel if we request just search
|
||||
if find_replace_panel.replace_panel_visible:
|
||||
find_replace_panel.replace_panel_visible = false
|
||||
|
||||
func _on_replace_requested() -> void:
|
||||
find_replace_panel.show_replace_panel()
|
||||
|
||||
func _on_find_next_requested() -> void:
|
||||
if find_replace_panel and find_replace_panel.visible:
|
||||
find_replace_panel.find_next()
|
||||
else:
|
||||
_on_find_requested()
|
||||
|
||||
func _on_find_previous_requested() -> void:
|
||||
if find_replace_panel and find_replace_panel.visible:
|
||||
find_replace_panel.find_previous()
|
||||
else:
|
||||
_on_find_requested()
|
||||
|
||||
func _on_replace_performed() -> void:
|
||||
# After a replace, request validation
|
||||
code_edit.validation_requested.emit()
|
||||
|
||||
# Update the current document
|
||||
_on_content_changed()
|
||||
|
||||
func _on_replace_all_performed() -> void:
|
||||
# After replace all, request validation
|
||||
code_edit.validation_requested.emit()
|
||||
|
||||
# Update the current document
|
||||
_on_content_changed()
|
||||
|
||||
# Show a message in the status bar
|
||||
if find_replace_panel.visible and find_replace_panel.replace_panel.visible:
|
||||
status_panel.set_status("Replacement complete", Color.GREEN)
|
||||
# Clear the status after a delay
|
||||
if get_tree():
|
||||
await get_tree().create_timer(2.0).timeout
|
||||
status_panel.set_status("")
|
||||
|
||||
func _on_document_changed(document: YAMLEditorDocument) -> void:
|
||||
# Update status panel with document info
|
||||
_update_line_col_label()
|
||||
|
||||
# Show any validation errors
|
||||
if document.has_error():
|
||||
_display_validation_error(document)
|
||||
else:
|
||||
status_panel.set_status("")
|
||||
validator.clear_errors_in_editor()
|
||||
|
||||
# Re-trigger search
|
||||
if find_replace_panel.visible:
|
||||
find_replace_panel.trigger_search()
|
||||
|
||||
func _on_caret_changed() -> void:
|
||||
status_panel.set_line_column(code_edit.get_current_line_col_info())
|
||||
|
||||
func _on_validation_completed(document: YAMLEditorDocument) -> void:
|
||||
if document != file_manager.get_current_document():
|
||||
return
|
||||
|
||||
if document.has_error():
|
||||
_display_validation_error(document)
|
||||
else:
|
||||
status_panel.set_status("")
|
||||
validator.clear_errors_in_editor()
|
||||
|
||||
func _display_validation_error(document: YAMLEditorDocument) -> void:
|
||||
status_panel.set_validation_result(document.validation_result)
|
||||
|
||||
var result := document.validation_result
|
||||
if not result.has_error():
|
||||
return
|
||||
|
||||
# Mark error line in editor if possible
|
||||
var error := result.get_error_message()
|
||||
var line := result.get_error_line()
|
||||
if line >= 0:
|
||||
validator.mark_error_in_editor(line - 1, error) # Convert to 0-based line
|
||||
|
||||
func _has_unsaved_changes() -> bool:
|
||||
return file_manager.has_unsaved_changes()
|
||||
|
||||
func get_open_files() -> Array:
|
||||
return file_manager.get_open_paths()
|
||||
|
||||
func handle_filesystem_change() -> void:
|
||||
file_manager.handle_filesystem_change()
|
||||
|
||||
func _notification(what):
|
||||
if what == NOTIFICATION_WM_CLOSE_REQUEST:
|
||||
# Save session when editor is closing
|
||||
session_manager.save_session()
|
||||
|
||||
func _update_line_col_label() -> void:
|
||||
# Allow one frame to pass to ensure the UI is updated
|
||||
if get_tree():
|
||||
await get_tree().process_frame
|
||||
_on_caret_changed()
|
||||
@@ -1 +0,0 @@
|
||||
uid://bbbai2ba18l5f
|
||||
@@ -1,400 +0,0 @@
|
||||
[gd_scene load_steps=27 format=3 uid="uid://b7wsl0ss24hvb"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bbbai2ba18l5f" path="res://addons/yaml/editor/yaml_editor.gd" id="1_jqpkv"]
|
||||
[ext_resource type="Script" uid="uid://c0q2685eddg35" path="res://addons/yaml/editor/syntax_highlighting/editor_syntax_highlighter.gd" id="2_0xmrl"]
|
||||
[ext_resource type="Script" uid="uid://drkui6da5o1ou" path="res://addons/yaml/editor/code_editor.gd" id="2_lgcdy"]
|
||||
[ext_resource type="Script" uid="uid://c15odacm31d03" path="res://addons/yaml/editor/file_list.gd" id="3_ufmqr"]
|
||||
[ext_resource type="Script" uid="uid://b35lyu1onhcoj" path="res://addons/yaml/editor/menu_bar.gd" id="5_bqqrn"]
|
||||
[ext_resource type="Script" uid="uid://bkanguis54ea3" path="res://addons/yaml/editor/status_bar.gd" id="6_rk03l"]
|
||||
[ext_resource type="Script" uid="uid://bep06otwjntx1" path="res://addons/yaml/editor/find_replace_bar.gd" id="7_vo7uj"]
|
||||
|
||||
[sub_resource type="Image" id="Image_cjchp"]
|
||||
data = {
|
||||
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 0, 0, 0, 0, 255, 92, 92, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 0, 0, 0, 0, 255, 93, 93, 255, 255, 92, 92, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 0, 0, 0, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 92, 92, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 0, 0, 0, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 92, 92, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 93, 93, 231, 255, 90, 90, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 90, 90, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 91, 91, 42, 0, 0, 0, 0, 0, 0, 0, 0, 255, 91, 91, 42, 255, 93, 93, 233, 255, 92, 92, 232, 255, 93, 93, 41, 0, 0, 0, 0, 0, 0, 0, 0, 255, 91, 91, 42, 255, 93, 93, 233, 255, 92, 92, 232, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 91, 91, 45, 255, 93, 93, 44, 0, 0, 0, 0, 255, 91, 91, 42, 255, 91, 91, 42, 0, 0, 0, 0, 255, 91, 91, 45, 255, 93, 93, 44, 0, 0, 0, 0, 255, 91, 91, 42, 255, 91, 91, 42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 91, 91, 45, 255, 92, 92, 235, 255, 92, 92, 234, 255, 89, 89, 43, 0, 0, 0, 0, 0, 0, 0, 0, 255, 91, 91, 45, 255, 92, 92, 235, 255, 92, 92, 234, 255, 89, 89, 43, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 92, 92, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 91, 91, 59, 255, 92, 92, 61, 255, 92, 92, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 91, 91, 59, 255, 92, 92, 61, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
|
||||
"format": "RGBA8",
|
||||
"height": 16,
|
||||
"mipmaps": false,
|
||||
"width": 16
|
||||
}
|
||||
|
||||
[sub_resource type="ImageTexture" id="ImageTexture_oh2kv"]
|
||||
image = SubResource("Image_cjchp")
|
||||
|
||||
[sub_resource type="EditorSyntaxHighlighter" id="EditorSyntaxHighlighter_68y40"]
|
||||
script = ExtResource("2_0xmrl")
|
||||
|
||||
[sub_resource type="SystemFont" id="SystemFont_k3025"]
|
||||
subpixel_positioning = 0
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_42q8o"]
|
||||
|
||||
[sub_resource type="SystemFont" id="SystemFont_k8p4n"]
|
||||
font_names = PackedStringArray("Monospace")
|
||||
subpixel_positioning = 0
|
||||
|
||||
[sub_resource type="SystemFont" id="SystemFont_wntl2"]
|
||||
fallbacks = Array[Font]([SubResource("SystemFont_k8p4n")])
|
||||
font_names = PackedStringArray("JetBrains Mono")
|
||||
subpixel_positioning = 0
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_i8tps"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_r4bqd"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_3ksor"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_33vym"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_jwbjn"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_xhoyn"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_vnqw7"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_1i5or"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_vlfg7"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_ki2vh"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_ke1f7"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_v3fgn"]
|
||||
|
||||
[node name="YamlEditor" type="Control" node_paths=PackedStringArray("menu_bar", "file_list", "resizable_container", "code_edit", "status_panel", "find_replace_panel")]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
script = ExtResource("1_jqpkv")
|
||||
menu_bar = NodePath("MainMargin/MainVBox/MenuBarMargin/MenuBar")
|
||||
file_list = NodePath("MainMargin/MainVBox/ResizableContainer/FileList")
|
||||
resizable_container = NodePath("MainMargin/MainVBox/ResizableContainer")
|
||||
code_edit = NodePath("MainMargin/MainVBox/ResizableContainer/EditorPanel/YAMLCodeEdit")
|
||||
status_panel = NodePath("MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/StatusBarMargin/StatusBar")
|
||||
find_replace_panel = NodePath("MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar")
|
||||
|
||||
[node name="MainMargin" type="MarginContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme_override_constants/margin_left = 4
|
||||
theme_override_constants/margin_top = 1
|
||||
theme_override_constants/margin_right = 4
|
||||
theme_override_constants/margin_bottom = 4
|
||||
metadata/_edit_use_anchors_ = true
|
||||
|
||||
[node name="MainVBox" type="VBoxContainer" parent="MainMargin"]
|
||||
custom_minimum_size = Vector2(200, 0)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="MenuBarMargin" type="MarginContainer" parent="MainMargin/MainVBox"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_bottom = 1
|
||||
|
||||
[node name="MenuBar" type="MenuBar" parent="MainMargin/MainVBox/MenuBarMargin" node_paths=PackedStringArray("file_menu", "edit_menu", "search_menu")]
|
||||
layout_mode = 2
|
||||
flat = true
|
||||
script = ExtResource("5_bqqrn")
|
||||
file_menu = NodePath("File")
|
||||
edit_menu = NodePath("Edit")
|
||||
search_menu = NodePath("Search")
|
||||
|
||||
[node name="File" type="PopupMenu" parent="MainMargin/MainVBox/MenuBarMargin/MenuBar"]
|
||||
title = "File"
|
||||
hide_on_checkable_item_selection = false
|
||||
item_count = 7
|
||||
item_0/text = "New"
|
||||
item_0/id = 0
|
||||
item_1/text = "Open..."
|
||||
item_1/id = 1
|
||||
item_2/id = -1
|
||||
item_2/separator = true
|
||||
item_3/text = "Save"
|
||||
item_3/id = 2
|
||||
item_4/text = "Save As..."
|
||||
item_4/id = 3
|
||||
item_5/id = -1
|
||||
item_5/separator = true
|
||||
item_6/text = "Close"
|
||||
item_6/id = 4
|
||||
|
||||
[node name="Edit" type="PopupMenu" parent="MainMargin/MainVBox/MenuBarMargin/MenuBar"]
|
||||
auto_translate_mode = 1
|
||||
item_count = 8
|
||||
item_0/text = "Undo"
|
||||
item_0/id = 0
|
||||
item_1/text = "Redo"
|
||||
item_1/id = 1
|
||||
item_2/id = -1
|
||||
item_2/separator = true
|
||||
item_3/text = "Cut"
|
||||
item_3/id = 2
|
||||
item_4/text = "Copy"
|
||||
item_4/id = 3
|
||||
item_5/text = "Paste"
|
||||
item_5/id = 4
|
||||
item_6/id = -1
|
||||
item_6/separator = true
|
||||
item_7/text = "Select All"
|
||||
item_7/id = 5
|
||||
|
||||
[node name="Search" type="PopupMenu" parent="MainMargin/MainVBox/MenuBarMargin/MenuBar"]
|
||||
auto_translate_mode = 1
|
||||
item_count = 5
|
||||
item_0/text = "Find..."
|
||||
item_0/id = 0
|
||||
item_1/text = "Find Next"
|
||||
item_1/id = 1
|
||||
item_2/text = "Find Previous"
|
||||
item_2/id = 2
|
||||
item_3/id = -1
|
||||
item_3/separator = true
|
||||
item_4/text = "Replace..."
|
||||
item_4/id = 3
|
||||
|
||||
[node name="ResizableContainer" type="HSplitContainer" parent="MainMargin/MainVBox"]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
split_offset = 150
|
||||
|
||||
[node name="FileList" type="VBoxContainer" parent="MainMargin/MainVBox/ResizableContainer" node_paths=PackedStringArray("filter_input", "file_list")]
|
||||
custom_minimum_size = Vector2(150, 0)
|
||||
layout_mode = 2
|
||||
size_flags_stretch_ratio = 0.25
|
||||
script = ExtResource("3_ufmqr")
|
||||
filter_input = NodePath("FilterInput")
|
||||
file_list = NodePath("ItemList")
|
||||
|
||||
[node name="FilterInput" type="LineEdit" parent="MainMargin/MainVBox/ResizableContainer/FileList"]
|
||||
layout_mode = 2
|
||||
placeholder_text = "Filter Files"
|
||||
clear_button_enabled = true
|
||||
right_icon = SubResource("ImageTexture_oh2kv")
|
||||
|
||||
[node name="ItemList" type="ItemList" parent="MainMargin/MainVBox/ResizableContainer/FileList"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
allow_rmb_select = true
|
||||
auto_height = true
|
||||
|
||||
[node name="EditorPanel" type="VBoxContainer" parent="MainMargin/MainVBox/ResizableContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="YAMLCodeEdit" type="CodeEdit" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 14
|
||||
deselect_on_focus_loss_enabled = false
|
||||
scroll_smooth = true
|
||||
minimap_draw = true
|
||||
syntax_highlighter = SubResource("EditorSyntaxHighlighter_68y40")
|
||||
highlight_all_occurrences = true
|
||||
highlight_current_line = true
|
||||
draw_control_chars = true
|
||||
draw_tabs = true
|
||||
draw_spaces = true
|
||||
line_folding = true
|
||||
gutters_draw_breakpoints_gutter = true
|
||||
gutters_draw_bookmarks = true
|
||||
gutters_draw_executing_lines = true
|
||||
gutters_draw_line_numbers = true
|
||||
gutters_zero_pad_line_numbers = true
|
||||
gutters_draw_fold_gutter = true
|
||||
indent_size = 2
|
||||
indent_use_spaces = true
|
||||
indent_automatic = true
|
||||
indent_automatic_prefixes = Array[String]([":"])
|
||||
auto_brace_completion_enabled = true
|
||||
auto_brace_completion_highlight_matching = true
|
||||
script = ExtResource("2_lgcdy")
|
||||
|
||||
[node name="BottomToolbar" type="VBoxContainer" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="StatusBarMargin" type="MarginContainer" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 0
|
||||
theme_override_constants/margin_top = 2
|
||||
theme_override_constants/margin_right = 4
|
||||
theme_override_constants/margin_bottom = 2
|
||||
|
||||
[node name="StatusBar" type="HBoxContainer" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/StatusBarMargin" node_paths=PackedStringArray("editor", "status_label", "zoom_button", "line_column_label")]
|
||||
layout_mode = 2
|
||||
script = ExtResource("6_rk03l")
|
||||
editor = NodePath("../../../YAMLCodeEdit")
|
||||
status_label = NodePath("StatusLabel")
|
||||
zoom_button = NodePath("ZoomButton")
|
||||
line_column_label = NodePath("LineColumnLabel")
|
||||
|
||||
[node name="StatusLabel" type="Label" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/StatusBarMargin/StatusBar"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 2
|
||||
focus_mode = 2
|
||||
theme_override_fonts/font = SubResource("SystemFont_k3025")
|
||||
theme_override_styles/normal = SubResource("StyleBoxEmpty_42q8o")
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="ZoomButton" type="Button" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/StatusBarMargin/StatusBar"]
|
||||
layout_mode = 2
|
||||
theme_override_fonts/font = SubResource("SystemFont_wntl2")
|
||||
text = "100 %"
|
||||
flat = true
|
||||
|
||||
[node name="VSeparator" type="VSeparator" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/StatusBarMargin/StatusBar"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 6
|
||||
|
||||
[node name="LineColumnLabel" type="Label" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/StatusBarMargin/StatusBar"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 8
|
||||
theme_override_fonts/font = SubResource("SystemFont_wntl2")
|
||||
text = "1 : 1"
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="FindReplaceBar" type="HBoxContainer" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar" node_paths=PackedStringArray("find_input", "matches_label", "previous_button", "next_button", "match_case_checkbox", "whole_words_checkbox", "find_button_container", "find_options_container", "replace_input", "replace_button", "replace_all_button", "selection_only_checkbox", "replace_button_container", "replace_options_container", "hide_button", "editor", "vbox_container", "find_panel", "replace_panel")]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
script = ExtResource("7_vo7uj")
|
||||
find_input = NodePath("InputFieldContainer/FindInput")
|
||||
matches_label = NodePath("ButtonsContainer/MatchesContainer/MatchesLabel")
|
||||
previous_button = NodePath("ButtonsContainer/MatchesContainer/PreviousButton")
|
||||
next_button = NodePath("ButtonsContainer/MatchesContainer/NextButton")
|
||||
match_case_checkbox = NodePath("CheckBoxContainer/FindOptions/MatchCaseCheckbox")
|
||||
whole_words_checkbox = NodePath("CheckBoxContainer/FindOptions/WholeWordsCheckbox")
|
||||
find_button_container = NodePath("ButtonsContainer/MatchesContainer")
|
||||
find_options_container = NodePath("CheckBoxContainer/FindOptions")
|
||||
replace_input = NodePath("InputFieldContainer/ReplaceInput")
|
||||
replace_button = NodePath("ButtonsContainer/ReplaceButtonsContainer/ReplaceButton")
|
||||
replace_all_button = NodePath("ButtonsContainer/ReplaceButtonsContainer/ReplaceAllButton")
|
||||
selection_only_checkbox = NodePath("CheckBoxContainer/ReplaceOptions/SelectionOnlyCheckBox")
|
||||
replace_button_container = NodePath("ButtonsContainer/ReplaceButtonsContainer")
|
||||
replace_options_container = NodePath("CheckBoxContainer/ReplaceOptions")
|
||||
hide_button = NodePath("HideButton")
|
||||
editor = NodePath("../../YAMLCodeEdit")
|
||||
vbox_container = NodePath("CheckBoxContainer")
|
||||
find_panel = NodePath("CheckBoxContainer/FindOptions")
|
||||
replace_panel = NodePath("CheckBoxContainer/ReplaceOptions")
|
||||
|
||||
[node name="InputFieldContainer" type="VBoxContainer" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="FindInput" type="LineEdit" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar/InputFieldContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
tooltip_text = "Find"
|
||||
placeholder_text = "Find"
|
||||
|
||||
[node name="ReplaceInput" type="LineEdit" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar/InputFieldContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
placeholder_text = "Replace"
|
||||
|
||||
[node name="ButtonsContainer" type="VBoxContainer" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 0
|
||||
|
||||
[node name="MatchesContainer" type="HBoxContainer" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar/ButtonsContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 8
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/separation = 16
|
||||
|
||||
[node name="MatchesLabel" type="Label" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar/ButtonsContainer/MatchesContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 0
|
||||
size_flags_vertical = 1
|
||||
theme_override_fonts/font = SubResource("SystemFont_k3025")
|
||||
text = "10 matches"
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="PreviousButton" type="Button" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar/ButtonsContainer/MatchesContainer"]
|
||||
layout_mode = 2
|
||||
tooltip_text = "Previous Match"
|
||||
theme_override_styles/focus = SubResource("StyleBoxEmpty_i8tps")
|
||||
theme_override_styles/disabled = SubResource("StyleBoxEmpty_r4bqd")
|
||||
theme_override_styles/hover_pressed = SubResource("StyleBoxEmpty_3ksor")
|
||||
theme_override_styles/hover = SubResource("StyleBoxEmpty_33vym")
|
||||
theme_override_styles/pressed = SubResource("StyleBoxEmpty_jwbjn")
|
||||
theme_override_styles/normal = SubResource("StyleBoxEmpty_xhoyn")
|
||||
disabled = true
|
||||
icon = SubResource("ImageTexture_oh2kv")
|
||||
flat = true
|
||||
|
||||
[node name="NextButton" type="Button" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar/ButtonsContainer/MatchesContainer"]
|
||||
layout_mode = 2
|
||||
tooltip_text = "Next Match"
|
||||
theme_override_styles/focus = SubResource("StyleBoxEmpty_vnqw7")
|
||||
theme_override_styles/disabled = SubResource("StyleBoxEmpty_1i5or")
|
||||
theme_override_styles/hover_pressed = SubResource("StyleBoxEmpty_vlfg7")
|
||||
theme_override_styles/hover = SubResource("StyleBoxEmpty_ki2vh")
|
||||
theme_override_styles/pressed = SubResource("StyleBoxEmpty_ke1f7")
|
||||
theme_override_styles/normal = SubResource("StyleBoxEmpty_v3fgn")
|
||||
disabled = true
|
||||
icon = SubResource("ImageTexture_oh2kv")
|
||||
flat = true
|
||||
|
||||
[node name="ReplaceButtonsContainer" type="HBoxContainer" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar/ButtonsContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 8
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="ReplaceButton" type="Button" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar/ButtonsContainer/ReplaceButtonsContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 8
|
||||
size_flags_vertical = 4
|
||||
disabled = true
|
||||
text = "Replace"
|
||||
|
||||
[node name="ReplaceAllButton" type="Button" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar/ButtonsContainer/ReplaceButtonsContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 8
|
||||
size_flags_vertical = 4
|
||||
disabled = true
|
||||
text = "Replace All"
|
||||
|
||||
[node name="CheckBoxContainer" type="VBoxContainer" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="FindOptions" type="HBoxContainer" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar/CheckBoxContainer"]
|
||||
custom_minimum_size = Vector2(0, 30)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 0
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="MatchCaseCheckbox" type="CheckBox" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar/CheckBoxContainer/FindOptions"]
|
||||
layout_mode = 2
|
||||
text = "Match Case"
|
||||
|
||||
[node name="WholeWordsCheckbox" type="CheckBox" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar/CheckBoxContainer/FindOptions"]
|
||||
layout_mode = 2
|
||||
text = "Whole Words"
|
||||
|
||||
[node name="ReplaceOptions" type="HBoxContainer" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar/CheckBoxContainer"]
|
||||
custom_minimum_size = Vector2(0, 30)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 0
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="SelectionOnlyCheckBox" type="CheckBox" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar/CheckBoxContainer/ReplaceOptions"]
|
||||
layout_mode = 2
|
||||
text = "Selection Only
|
||||
"
|
||||
|
||||
[node name="HideButton" type="Button" parent="MainMargin/MainVBox/ResizableContainer/EditorPanel/BottomToolbar/FindReplaceBar"]
|
||||
layout_mode = 2
|
||||
tooltip_text = "Hide"
|
||||
icon = SubResource("ImageTexture_oh2kv")
|
||||
flat = true
|
||||
@@ -1,3 +0,0 @@
|
||||
[gd_resource type="CanvasItemMaterial" format=3 uid="uid://g4tjqwskvd62"]
|
||||
|
||||
[resource]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user