@jarrodmedrano/claude-skills 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/bevy/SKILL.md +406 -0
- package/.claude/skills/bevy/references/bevy_specific_tips.md +385 -0
- package/.claude/skills/bevy/references/common_pitfalls.md +217 -0
- package/.claude/skills/bevy/references/ecs_patterns.md +277 -0
- package/.claude/skills/bevy/references/project_structure.md +116 -0
- package/.claude/skills/bevy/references/ui_development.md +147 -0
- package/.claude/skills/domain-driven-design/SKILL.md +459 -0
- package/.claude/skills/domain-driven-design/references/ddd_foundations_and_patterns.md +664 -0
- package/.claude/skills/domain-driven-design/references/rich_hickey_principles.md +406 -0
- package/.claude/skills/domain-driven-design/references/visualization_examples.md +790 -0
- package/.claude/skills/domain-driven-design/references/wlaschin_patterns.md +639 -0
- package/.claude/skills/godot/SKILL.md +728 -0
- package/.claude/skills/godot/assets/templates/attribute_template.gd +109 -0
- package/.claude/skills/godot/assets/templates/component_template.gd +76 -0
- package/.claude/skills/godot/assets/templates/interaction_template.gd +108 -0
- package/.claude/skills/godot/assets/templates/item_resource.tres +11 -0
- package/.claude/skills/godot/assets/templates/spell_resource.tres +20 -0
- package/.claude/skills/godot/references/architecture-patterns.md +608 -0
- package/.claude/skills/godot/references/common-pitfalls.md +518 -0
- package/.claude/skills/godot/references/file-formats.md +491 -0
- package/.claude/skills/godot/references/godot4-physics-api.md +302 -0
- package/.claude/skills/godot/scripts/validate_tres.py +145 -0
- package/.claude/skills/godot/scripts/validate_tscn.py +170 -0
- package/.claude/skills/react-three-fiber/SKILL.md +2055 -0
- package/.claude/skills/react-three-fiber/scripts/build-scene.ts +171 -0
- package/package.json +1 -1
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# Godot 4.x Physics API Quick Reference
|
|
2
|
+
|
|
3
|
+
This reference covers common physics operations in Godot 4.x, focusing on correct API usage and common patterns.
|
|
4
|
+
|
|
5
|
+
## Raycasting
|
|
6
|
+
|
|
7
|
+
### PhysicsRayQueryParameters3D (Correct Class Name)
|
|
8
|
+
|
|
9
|
+
**Important**: The class is `PhysicsRayQueryParameters3D`, NOT `PhysicsRayQuery3D`.
|
|
10
|
+
|
|
11
|
+
### Basic Raycast Setup
|
|
12
|
+
|
|
13
|
+
```gdscript
|
|
14
|
+
# Get the physics space
|
|
15
|
+
var space_state = get_world_3d().direct_space_state
|
|
16
|
+
|
|
17
|
+
# Create ray query parameters
|
|
18
|
+
var query = PhysicsRayQueryParameters3D.create(from_position, to_position)
|
|
19
|
+
|
|
20
|
+
# Optional: Exclude specific bodies
|
|
21
|
+
query.exclude = [self, other_body]
|
|
22
|
+
|
|
23
|
+
# Optional: Set collision mask (which layers to check)
|
|
24
|
+
query.collision_mask = 1 # Layer 1 only
|
|
25
|
+
query.collision_mask = 0b0011 # Layers 1 and 2
|
|
26
|
+
|
|
27
|
+
# Perform the raycast
|
|
28
|
+
var result = space_state.intersect_ray(query)
|
|
29
|
+
|
|
30
|
+
# Check if hit something
|
|
31
|
+
if result:
|
|
32
|
+
var hit_object = result.collider
|
|
33
|
+
var hit_position = result.position
|
|
34
|
+
var hit_normal = result.normal
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Common Raycast Patterns
|
|
38
|
+
|
|
39
|
+
**Camera-based raycast (first-person interaction):**
|
|
40
|
+
```gdscript
|
|
41
|
+
var camera = get_viewport().get_camera_3d()
|
|
42
|
+
var from = camera.global_position
|
|
43
|
+
var to = from + (-camera.global_transform.basis.z * range)
|
|
44
|
+
|
|
45
|
+
var query = PhysicsRayQueryParameters3D.create(from, to)
|
|
46
|
+
query.exclude = [player]
|
|
47
|
+
var result = space_state.intersect_ray(query)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Downward raycast (ground detection):**
|
|
51
|
+
```gdscript
|
|
52
|
+
var from = global_position
|
|
53
|
+
var to = global_position + Vector3.DOWN * 10.0
|
|
54
|
+
|
|
55
|
+
var query = PhysicsRayQueryParameters3D.create(from, to)
|
|
56
|
+
var result = space_state.intersect_ray(query)
|
|
57
|
+
|
|
58
|
+
if result:
|
|
59
|
+
var distance_to_ground = from.distance_to(result.position)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Line-of-sight check:**
|
|
63
|
+
```gdscript
|
|
64
|
+
func has_line_of_sight(target: Node3D) -> bool:
|
|
65
|
+
var space_state = get_world_3d().direct_space_state
|
|
66
|
+
var from = global_position
|
|
67
|
+
var to = target.global_position
|
|
68
|
+
|
|
69
|
+
var query = PhysicsRayQueryParameters3D.create(from, to)
|
|
70
|
+
query.exclude = [self, target]
|
|
71
|
+
|
|
72
|
+
var result = space_state.intersect_ray(query)
|
|
73
|
+
return not result # True if nothing blocking
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Shape Queries
|
|
77
|
+
|
|
78
|
+
### PhysicsShapeQueryParameters3D
|
|
79
|
+
|
|
80
|
+
For checking if a shape overlaps with objects (useful for area attacks, detection zones).
|
|
81
|
+
|
|
82
|
+
```gdscript
|
|
83
|
+
# Create shape query
|
|
84
|
+
var query = PhysicsShapeQueryParameters3D.new()
|
|
85
|
+
|
|
86
|
+
# Set the shape (sphere, box, capsule, etc.)
|
|
87
|
+
var sphere = SphereShape3D.new()
|
|
88
|
+
sphere.radius = 2.0
|
|
89
|
+
query.shape = sphere
|
|
90
|
+
|
|
91
|
+
# Set transform (position and rotation)
|
|
92
|
+
query.transform = Transform3D(Basis(), global_position)
|
|
93
|
+
|
|
94
|
+
# Optional: collision mask
|
|
95
|
+
query.collision_mask = 1
|
|
96
|
+
|
|
97
|
+
# Perform query
|
|
98
|
+
var space_state = get_world_3d().direct_space_state
|
|
99
|
+
var results = space_state.intersect_shape(query)
|
|
100
|
+
|
|
101
|
+
# Iterate results
|
|
102
|
+
for result in results:
|
|
103
|
+
var collider = result.collider
|
|
104
|
+
# Do something with each overlapping object
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Common Shape Query Patterns
|
|
108
|
+
|
|
109
|
+
**Area attack (sphere around player):**
|
|
110
|
+
```gdscript
|
|
111
|
+
func perform_area_attack(radius: float, damage: float):
|
|
112
|
+
var query = PhysicsShapeQueryParameters3D.new()
|
|
113
|
+
var sphere = SphereShape3D.new()
|
|
114
|
+
sphere.radius = radius
|
|
115
|
+
query.shape = sphere
|
|
116
|
+
query.transform = Transform3D(Basis(), global_position)
|
|
117
|
+
query.collision_mask = 2 # Enemies layer
|
|
118
|
+
|
|
119
|
+
var space_state = get_world_3d().direct_space_state
|
|
120
|
+
var results = space_state.intersect_shape(query)
|
|
121
|
+
|
|
122
|
+
for result in results:
|
|
123
|
+
if result.collider.has_method("take_damage"):
|
|
124
|
+
result.collider.take_damage(damage)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Cone detection (enemy field of view):**
|
|
128
|
+
```gdscript
|
|
129
|
+
# Use multiple raycasts in a cone pattern
|
|
130
|
+
func detect_in_cone(angle_degrees: float, range: float) -> Array:
|
|
131
|
+
var detected = []
|
|
132
|
+
var rays = 5 # Number of rays in cone
|
|
133
|
+
|
|
134
|
+
for i in range(rays):
|
|
135
|
+
var angle = -angle_degrees/2 + (angle_degrees / (rays-1)) * i
|
|
136
|
+
var direction = global_transform.basis.z.rotated(Vector3.UP, deg_to_rad(angle))
|
|
137
|
+
|
|
138
|
+
var from = global_position
|
|
139
|
+
var to = from + direction * range
|
|
140
|
+
|
|
141
|
+
var query = PhysicsRayQueryParameters3D.create(from, to)
|
|
142
|
+
var result = space_state.intersect_ray(query)
|
|
143
|
+
|
|
144
|
+
if result:
|
|
145
|
+
detected.append(result.collider)
|
|
146
|
+
|
|
147
|
+
return detected
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Collision Layers and Masks
|
|
151
|
+
|
|
152
|
+
Understanding layers is critical for efficient physics.
|
|
153
|
+
|
|
154
|
+
### Layer Setup
|
|
155
|
+
|
|
156
|
+
```gdscript
|
|
157
|
+
# In project settings, name your layers:
|
|
158
|
+
# Layer 1: World (walls, floors)
|
|
159
|
+
# Layer 2: Player
|
|
160
|
+
# Layer 3: Enemies
|
|
161
|
+
# Layer 4: Projectiles
|
|
162
|
+
# Layer 5: Interactables
|
|
163
|
+
|
|
164
|
+
# Set what layers an object is ON
|
|
165
|
+
collision_layer = 0b00010 # Layer 2 (Player)
|
|
166
|
+
|
|
167
|
+
# Set what layers an object can COLLIDE WITH
|
|
168
|
+
collision_mask = 0b00101 # Layers 1 (World) and 3 (Enemies)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Common Layer Patterns
|
|
172
|
+
|
|
173
|
+
**Player should collide with world and enemies:**
|
|
174
|
+
```gdscript
|
|
175
|
+
# Player
|
|
176
|
+
collision_layer = 0b00010 # Layer 2
|
|
177
|
+
collision_mask = 0b00101 # Layers 1 (world) and 3 (enemies)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Enemy projectile should hit player but not other enemies:**
|
|
181
|
+
```gdscript
|
|
182
|
+
# Enemy Projectile
|
|
183
|
+
collision_layer = 0b01000 # Layer 4
|
|
184
|
+
collision_mask = 0b00011 # Layers 1 (world) and 2 (player)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Interaction raycast should only hit interactables:**
|
|
188
|
+
```gdscript
|
|
189
|
+
var query = PhysicsRayQueryParameters3D.create(from, to)
|
|
190
|
+
query.collision_mask = 0b10000 # Layer 5 (interactables) only
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Area3D vs RigidBody3D vs StaticBody3D vs CharacterBody3D
|
|
194
|
+
|
|
195
|
+
### When to Use Each
|
|
196
|
+
|
|
197
|
+
**StaticBody3D**
|
|
198
|
+
- Non-moving collision objects (walls, floors, obstacles)
|
|
199
|
+
- Cannot be moved by physics
|
|
200
|
+
- Very efficient
|
|
201
|
+
|
|
202
|
+
**CharacterBody3D**
|
|
203
|
+
- Player characters, NPCs with custom movement
|
|
204
|
+
- Controlled by code, not physics engine
|
|
205
|
+
- Has `move_and_slide()` for smooth movement
|
|
206
|
+
- Use for anything you want direct control over
|
|
207
|
+
|
|
208
|
+
**RigidBody3D**
|
|
209
|
+
- Objects controlled by physics (crates, barrels, ragdolls)
|
|
210
|
+
- Affected by gravity, forces, collisions
|
|
211
|
+
- Good for destructible/pushable objects
|
|
212
|
+
|
|
213
|
+
**Area3D**
|
|
214
|
+
- Trigger zones (doesn't block movement)
|
|
215
|
+
- Detection volumes (pickup radius, damage zones)
|
|
216
|
+
- Cannot collide physically, only detect overlaps
|
|
217
|
+
|
|
218
|
+
### Common Patterns
|
|
219
|
+
|
|
220
|
+
**Damage zone (Area3D):**
|
|
221
|
+
```gdscript
|
|
222
|
+
extends Area3D
|
|
223
|
+
|
|
224
|
+
signal body_damaged(body: Node3D, damage: float)
|
|
225
|
+
|
|
226
|
+
@export var damage_per_second: float = 10.0
|
|
227
|
+
|
|
228
|
+
func _ready():
|
|
229
|
+
body_entered.connect(_on_body_entered)
|
|
230
|
+
body_exited.connect(_on_body_exited)
|
|
231
|
+
|
|
232
|
+
var bodies_inside: Array[Node3D] = []
|
|
233
|
+
|
|
234
|
+
func _on_body_entered(body: Node3D):
|
|
235
|
+
bodies_inside.append(body)
|
|
236
|
+
|
|
237
|
+
func _on_body_exited(body: Node3D):
|
|
238
|
+
bodies_inside.erase(body)
|
|
239
|
+
|
|
240
|
+
func _process(delta: float):
|
|
241
|
+
for body in bodies_inside:
|
|
242
|
+
if body.has_method("take_damage"):
|
|
243
|
+
body.take_damage(damage_per_second * delta)
|
|
244
|
+
body_damaged.emit(body, damage_per_second * delta)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Pickup detection (Area3D child of player):**
|
|
248
|
+
```gdscript
|
|
249
|
+
# As child of CharacterBody3D (player)
|
|
250
|
+
extends Area3D
|
|
251
|
+
|
|
252
|
+
func _ready():
|
|
253
|
+
collision_layer = 0 # Not on any layer
|
|
254
|
+
collision_mask = 0b10000 # Only detect interactables
|
|
255
|
+
|
|
256
|
+
body_entered.connect(_on_pickup_entered)
|
|
257
|
+
|
|
258
|
+
func _on_pickup_entered(body: Node3D):
|
|
259
|
+
if body.has_method("pickup"):
|
|
260
|
+
body.pickup(get_parent()) # Pass player to pickup
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Performance Tips
|
|
264
|
+
|
|
265
|
+
1. **Use collision layers efficiently** - Don't check unnecessary layers
|
|
266
|
+
2. **Limit raycast distance** - Shorter rays are faster
|
|
267
|
+
3. **Cache space_state** - Don't call `get_world_3d().direct_space_state` every frame if possible
|
|
268
|
+
4. **Use Areas for detection** - More efficient than frequent raycasts
|
|
269
|
+
5. **Exclude irrelevant bodies** - Use `query.exclude` to skip known objects
|
|
270
|
+
|
|
271
|
+
## Common Gotchas
|
|
272
|
+
|
|
273
|
+
1. **Wrong class name**: It's `PhysicsRayQueryParameters3D`, not `PhysicsRayQuery3D`
|
|
274
|
+
2. **Forgetting collision mask**: Raycast won't hit anything if mask is 0
|
|
275
|
+
3. **Self-collision**: Always exclude `self` from queries
|
|
276
|
+
4. **Result dictionary**: Check `if result:` before accessing `result.collider`
|
|
277
|
+
5. **Global vs local positions**: Raycasts use global coordinates
|
|
278
|
+
6. **Basis direction**: `-transform.basis.z` is forward, not `transform.basis.z`
|
|
279
|
+
|
|
280
|
+
## Debugging Physics
|
|
281
|
+
|
|
282
|
+
```gdscript
|
|
283
|
+
# Visualize raycasts in editor
|
|
284
|
+
func _draw_debug_ray(from: Vector3, to: Vector3, hit: bool):
|
|
285
|
+
var immediate = ImmediateMesh.new()
|
|
286
|
+
var material = StandardMaterial3D.new()
|
|
287
|
+
material.albedo_color = Color.RED if hit else Color.GREEN
|
|
288
|
+
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
|
289
|
+
|
|
290
|
+
# Add debug line drawing here
|
|
291
|
+
# (Use MeshInstance3D with ImmediateMesh for runtime visualization)
|
|
292
|
+
|
|
293
|
+
# Print collision layers
|
|
294
|
+
func debug_print_layers():
|
|
295
|
+
print("collision_layer: ", collision_layer, " (binary: ", String.num_int64(collision_layer, 2), ")")
|
|
296
|
+
print("collision_mask: ", collision_mask, " (binary: ", String.num_int64(collision_mask, 2), ")")
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Further Reading
|
|
300
|
+
|
|
301
|
+
- Official Godot 4 Physics Docs: https://docs.godotengine.org/en/stable/tutorials/physics/
|
|
302
|
+
- Understanding collision layers: https://docs.godotengine.org/en/stable/tutorials/physics/physics_introduction.html#collision-layers-and-masks
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Validates Godot .tres (resource) files for common syntax errors.
|
|
4
|
+
|
|
5
|
+
Common mistakes this catches:
|
|
6
|
+
- Using preload() instead of ExtResource()
|
|
7
|
+
- Using GDScript syntax (var, const, func) in resource files
|
|
8
|
+
- Missing ExtResource declarations
|
|
9
|
+
- Incorrect array type syntax
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
import re
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TresValidator:
|
|
18
|
+
def __init__(self, file_path):
|
|
19
|
+
self.file_path = Path(file_path)
|
|
20
|
+
self.errors = []
|
|
21
|
+
self.warnings = []
|
|
22
|
+
|
|
23
|
+
def validate(self):
|
|
24
|
+
"""Run all validation checks."""
|
|
25
|
+
if not self.file_path.exists():
|
|
26
|
+
self.errors.append(f"File not found: {self.file_path}")
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
content = self.file_path.read_text()
|
|
30
|
+
|
|
31
|
+
self._check_preload_usage(content)
|
|
32
|
+
self._check_gdscript_keywords(content)
|
|
33
|
+
self._check_array_syntax(content)
|
|
34
|
+
self._check_resource_references(content)
|
|
35
|
+
|
|
36
|
+
return len(self.errors) == 0
|
|
37
|
+
|
|
38
|
+
def _check_preload_usage(self, content):
|
|
39
|
+
"""Check for illegal preload() usage."""
|
|
40
|
+
preload_pattern = r'preload\s*\('
|
|
41
|
+
matches = list(re.finditer(preload_pattern, content, re.IGNORECASE))
|
|
42
|
+
|
|
43
|
+
for match in matches:
|
|
44
|
+
line_num = content[:match.start()].count('\n') + 1
|
|
45
|
+
self.errors.append(
|
|
46
|
+
f"Line {line_num}: Found 'preload()' - use ExtResource() instead in .tres files"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def _check_gdscript_keywords(self, content):
|
|
50
|
+
"""Check for GDScript keywords that shouldn't be in .tres files."""
|
|
51
|
+
# Split into lines and check each
|
|
52
|
+
lines = content.split('\n')
|
|
53
|
+
keywords = ['var ', 'const ', 'func ', 'class_name ', 'extends ']
|
|
54
|
+
|
|
55
|
+
for i, line in enumerate(lines, 1):
|
|
56
|
+
# Skip comments
|
|
57
|
+
if line.strip().startswith('#'):
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
for keyword in keywords:
|
|
61
|
+
if keyword in line and not line.strip().startswith('['):
|
|
62
|
+
self.errors.append(
|
|
63
|
+
f"Line {i}: Found GDScript keyword '{keyword.strip()}' - "
|
|
64
|
+
f"not allowed in .tres files"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def _check_array_syntax(self, content):
|
|
68
|
+
"""Check for proper typed array syntax."""
|
|
69
|
+
# Look for array assignments without type
|
|
70
|
+
untyped_array_pattern = r'=\s*\[[^\]]*\]'
|
|
71
|
+
lines = content.split('\n')
|
|
72
|
+
|
|
73
|
+
for i, line in enumerate(lines, 1):
|
|
74
|
+
# Skip resource headers
|
|
75
|
+
if line.strip().startswith('[') and line.strip().endswith(']'):
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
if re.search(untyped_array_pattern, line):
|
|
79
|
+
# Check if it's preceded by Array[Type]
|
|
80
|
+
if 'Array[' not in line:
|
|
81
|
+
self.warnings.append(
|
|
82
|
+
f"Line {i}: Array may need type specification - "
|
|
83
|
+
f"use Array[Type]([...]) syntax"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def _check_resource_references(self, content):
|
|
87
|
+
"""Check that ExtResource IDs are declared."""
|
|
88
|
+
# Find all ExtResource usages
|
|
89
|
+
usage_pattern = r'ExtResource\s*\(\s*"([^"]+)"\s*\)'
|
|
90
|
+
usages = re.findall(usage_pattern, content)
|
|
91
|
+
|
|
92
|
+
# Find all ExtResource declarations
|
|
93
|
+
decl_pattern = r'\[ext_resource[^\]]*id\s*=\s*"([^"]+)"'
|
|
94
|
+
declarations = re.findall(decl_pattern, content)
|
|
95
|
+
|
|
96
|
+
# Check for undefined references
|
|
97
|
+
for resource_id in set(usages):
|
|
98
|
+
if resource_id not in declarations:
|
|
99
|
+
self.errors.append(
|
|
100
|
+
f"ExtResource('{resource_id}') used but not declared - "
|
|
101
|
+
f"add [ext_resource ...] declaration"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def print_results(self):
|
|
105
|
+
"""Print validation results."""
|
|
106
|
+
print(f"\n{'='*60}")
|
|
107
|
+
print(f"Validating: {self.file_path}")
|
|
108
|
+
print(f"{'='*60}\n")
|
|
109
|
+
|
|
110
|
+
if self.errors:
|
|
111
|
+
print("❌ ERRORS:")
|
|
112
|
+
for error in self.errors:
|
|
113
|
+
print(f" • {error}")
|
|
114
|
+
print()
|
|
115
|
+
|
|
116
|
+
if self.warnings:
|
|
117
|
+
print("⚠️ WARNINGS:")
|
|
118
|
+
for warning in self.warnings:
|
|
119
|
+
print(f" • {warning}")
|
|
120
|
+
print()
|
|
121
|
+
|
|
122
|
+
if not self.errors and not self.warnings:
|
|
123
|
+
print("✅ No issues found!\n")
|
|
124
|
+
elif not self.errors:
|
|
125
|
+
print("✅ No errors (only warnings)\n")
|
|
126
|
+
else:
|
|
127
|
+
print(f"❌ Found {len(self.errors)} error(s)\n")
|
|
128
|
+
|
|
129
|
+
return len(self.errors) == 0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def main():
|
|
133
|
+
if len(sys.argv) < 2:
|
|
134
|
+
print("Usage: validate_tres.py <file.tres>")
|
|
135
|
+
sys.exit(1)
|
|
136
|
+
|
|
137
|
+
validator = TresValidator(sys.argv[1])
|
|
138
|
+
validator.validate()
|
|
139
|
+
success = validator.print_results()
|
|
140
|
+
|
|
141
|
+
sys.exit(0 if success else 1)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
if __name__ == "__main__":
|
|
145
|
+
main()
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Validates Godot .tscn (scene) files for common structural errors.
|
|
4
|
+
|
|
5
|
+
Common mistakes this catches:
|
|
6
|
+
- Missing ExtResource declarations for used resources
|
|
7
|
+
- Invalid parent references
|
|
8
|
+
- Malformed node entries
|
|
9
|
+
- UID format issues
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
import re
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TscnValidator:
|
|
18
|
+
def __init__(self, file_path):
|
|
19
|
+
self.file_path = Path(file_path)
|
|
20
|
+
self.errors = []
|
|
21
|
+
self.warnings = []
|
|
22
|
+
|
|
23
|
+
def validate(self):
|
|
24
|
+
"""Run all validation checks."""
|
|
25
|
+
if not self.file_path.exists():
|
|
26
|
+
self.errors.append(f"File not found: {self.file_path}")
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
content = self.file_path.read_text()
|
|
30
|
+
|
|
31
|
+
self._check_header(content)
|
|
32
|
+
self._check_resource_references(content)
|
|
33
|
+
self._check_node_structure(content)
|
|
34
|
+
self._check_parent_references(content)
|
|
35
|
+
|
|
36
|
+
return len(self.errors) == 0
|
|
37
|
+
|
|
38
|
+
def _check_header(self, content):
|
|
39
|
+
"""Check for valid scene file header."""
|
|
40
|
+
lines = content.split('\n')
|
|
41
|
+
if not lines:
|
|
42
|
+
self.errors.append("Empty file")
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
first_line = lines[0].strip()
|
|
46
|
+
if not first_line.startswith('[gd_scene'):
|
|
47
|
+
self.errors.append(
|
|
48
|
+
"Invalid header - should start with [gd_scene load_steps=N format=3 ...]"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Check for UID
|
|
52
|
+
if 'uid=' not in first_line:
|
|
53
|
+
self.warnings.append("Missing UID in header - may cause reference issues")
|
|
54
|
+
|
|
55
|
+
def _check_resource_references(self, content):
|
|
56
|
+
"""Check that all ExtResource and SubResource references are declared."""
|
|
57
|
+
# Find all ExtResource usages
|
|
58
|
+
ext_usage_pattern = r'ExtResource\s*\(\s*"([^"]+)"\s*\)'
|
|
59
|
+
ext_usages = re.findall(ext_usage_pattern, content)
|
|
60
|
+
|
|
61
|
+
# Find all ExtResource declarations
|
|
62
|
+
ext_decl_pattern = r'\[ext_resource[^\]]*id\s*=\s*"([^"]+)"'
|
|
63
|
+
ext_declarations = re.findall(ext_decl_pattern, content)
|
|
64
|
+
|
|
65
|
+
# Check for undefined ExtResource references
|
|
66
|
+
for resource_id in set(ext_usages):
|
|
67
|
+
if resource_id not in ext_declarations:
|
|
68
|
+
self.errors.append(
|
|
69
|
+
f"ExtResource('{resource_id}') used but not declared"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Find all SubResource usages
|
|
73
|
+
sub_usage_pattern = r'SubResource\s*\(\s*"([^"]+)"\s*\)'
|
|
74
|
+
sub_usages = re.findall(sub_usage_pattern, content)
|
|
75
|
+
|
|
76
|
+
# Find all SubResource declarations
|
|
77
|
+
sub_decl_pattern = r'\[sub_resource[^\]]*id\s*=\s*"([^"]+)"'
|
|
78
|
+
sub_declarations = re.findall(sub_decl_pattern, content)
|
|
79
|
+
|
|
80
|
+
# Check for undefined SubResource references
|
|
81
|
+
for resource_id in set(sub_usages):
|
|
82
|
+
if resource_id not in sub_declarations:
|
|
83
|
+
self.errors.append(
|
|
84
|
+
f"SubResource('{resource_id}') used but not declared"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def _check_node_structure(self, content):
|
|
88
|
+
"""Check for valid node entries."""
|
|
89
|
+
lines = content.split('\n')
|
|
90
|
+
node_pattern = r'^\[node\s+name="([^"]+)"'
|
|
91
|
+
|
|
92
|
+
for i, line in enumerate(lines, 1):
|
|
93
|
+
match = re.match(node_pattern, line)
|
|
94
|
+
if match:
|
|
95
|
+
# Check if node has required attributes
|
|
96
|
+
if 'type=' not in line and 'instance=' not in line and 'parent=' in line:
|
|
97
|
+
# Child nodes without type or instance might be invalid
|
|
98
|
+
self.warnings.append(
|
|
99
|
+
f"Line {i}: Node '{match.group(1)}' has parent but no type or instance"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def _check_parent_references(self, content):
|
|
103
|
+
"""Check that parent references are valid."""
|
|
104
|
+
lines = content.split('\n')
|
|
105
|
+
|
|
106
|
+
# Collect all node names
|
|
107
|
+
node_pattern = r'\[node\s+name="([^"]+)"'
|
|
108
|
+
nodes = []
|
|
109
|
+
|
|
110
|
+
for line in lines:
|
|
111
|
+
match = re.match(node_pattern, line)
|
|
112
|
+
if match:
|
|
113
|
+
nodes.append(match.group(1))
|
|
114
|
+
|
|
115
|
+
# Check parent references
|
|
116
|
+
parent_pattern = r'parent="([^"]+)"'
|
|
117
|
+
for i, line in enumerate(lines, 1):
|
|
118
|
+
match = re.search(parent_pattern, line)
|
|
119
|
+
if match:
|
|
120
|
+
parent = match.group(1)
|
|
121
|
+
# "." is valid (root), others should exist
|
|
122
|
+
if parent != "." and '/' not in parent:
|
|
123
|
+
# Simple parent reference
|
|
124
|
+
if parent not in nodes:
|
|
125
|
+
self.warnings.append(
|
|
126
|
+
f"Line {i}: Parent '{parent}' not found in scene"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def print_results(self):
|
|
130
|
+
"""Print validation results."""
|
|
131
|
+
print(f"\n{'='*60}")
|
|
132
|
+
print(f"Validating: {self.file_path}")
|
|
133
|
+
print(f"{'='*60}\n")
|
|
134
|
+
|
|
135
|
+
if self.errors:
|
|
136
|
+
print("❌ ERRORS:")
|
|
137
|
+
for error in self.errors:
|
|
138
|
+
print(f" • {error}")
|
|
139
|
+
print()
|
|
140
|
+
|
|
141
|
+
if self.warnings:
|
|
142
|
+
print("⚠️ WARNINGS:")
|
|
143
|
+
for warning in self.warnings:
|
|
144
|
+
print(f" • {warning}")
|
|
145
|
+
print()
|
|
146
|
+
|
|
147
|
+
if not self.errors and not self.warnings:
|
|
148
|
+
print("✅ No issues found!\n")
|
|
149
|
+
elif not self.errors:
|
|
150
|
+
print("✅ No errors (only warnings)\n")
|
|
151
|
+
else:
|
|
152
|
+
print(f"❌ Found {len(self.errors)} error(s)\n")
|
|
153
|
+
|
|
154
|
+
return len(self.errors) == 0
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def main():
|
|
158
|
+
if len(sys.argv) < 2:
|
|
159
|
+
print("Usage: validate_tscn.py <file.tscn>")
|
|
160
|
+
sys.exit(1)
|
|
161
|
+
|
|
162
|
+
validator = TscnValidator(sys.argv[1])
|
|
163
|
+
validator.validate()
|
|
164
|
+
success = validator.print_results()
|
|
165
|
+
|
|
166
|
+
sys.exit(0 if success else 1)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
if __name__ == "__main__":
|
|
170
|
+
main()
|