diff --git a/project.godot b/project.godot
index 14dc707..3c9be25 100644
--- a/project.godot
+++ b/project.godot
@@ -23,7 +23,7 @@ config/version="in-dev"
run/main_scene="uid://cl00e2ocomk3m"
config/use_custom_user_dir=true
config/custom_user_dir_name="MrBrickAdventures"
-config/features=PackedStringArray("4.6", "C#", "GL Compatibility")
+config/features=PackedStringArray("4.7", "C#", "GL Compatibility")
run/max_fps=180
boot_splash/bg_color=Color(0, 0, 0, 1)
boot_splash/show_image=false
diff --git a/resources/levels/forest/forest_1.tres b/resources/levels/forest/forest_1.tres
index 00c828c..696aec5 100644
--- a/resources/levels/forest/forest_1.tres
+++ b/resources/levels/forest/forest_1.tres
@@ -1,11 +1,10 @@
-[gd_resource type="Resource" script_class="LevelResource" load_steps=3 format=3 uid="uid://dsq0y2iw7tfmk"]
+[gd_resource type="Resource" script_class="LevelResource" load_steps=2 format=3 uid="uid://dsq0y2iw7tfmk"]
[ext_resource type="Script" uid="uid://c2h0pqhxiqswe" path="res://scripts/Resources/LevelResource.cs" id="1_script"]
-[ext_resource type="PackedScene" uid="uid://cq4glvuewsno5" path="res://scenes/level_forest_1.tscn" id="2_scene"]
[resource]
script = ExtResource("1_script")
Id = "forest_1"
LevelName = "LEVEL_FOREST_1_NAME"
-Scene = ExtResource("2_scene")
+ScenePath = "res://scenes/level_forest_1.tscn"
metadata/_custom_type_script = "uid://cp68km8bykymb"
diff --git a/resources/levels/level_catalogue.tres b/resources/levels/level_catalogue.tres
index 5f4b344..f065dcf 100644
--- a/resources/levels/level_catalogue.tres
+++ b/resources/levels/level_catalogue.tres
@@ -1,6 +1,6 @@
[gd_resource type="Resource" script_class="LevelCatalogue" load_steps=8 format=3]
-[ext_resource type="Script" path="res://scripts/Resources/LevelCatalogue.cs" id="1_script"]
+[ext_resource type="Script" uid="uid://cxryfiktqs7s1" path="res://scripts/Resources/LevelCatalogue.cs" id="1_script"]
[ext_resource type="Resource" path="res://resources/levels/tutorial_1.tres" id="2_tutorial_1"]
[ext_resource type="Resource" uid="uid://cqtalsov2bkpo" path="res://resources/levels/village/village_1.tres" id="3_village_1"]
[ext_resource type="Resource" uid="uid://dlwkbjw1l33uq" path="res://resources/levels/village/village_2.tres" id="4_village_2"]
diff --git a/resources/levels/tutorial_1.tres b/resources/levels/tutorial_1.tres
index c5d90b6..d8f5d5b 100644
--- a/resources/levels/tutorial_1.tres
+++ b/resources/levels/tutorial_1.tres
@@ -1,11 +1,10 @@
-[gd_resource type="Resource" script_class="LevelResource" load_steps=3 format=3]
+[gd_resource type="Resource" script_class="LevelResource" load_steps=2 format=3]
[ext_resource type="Script" uid="uid://c2h0pqhxiqswe" path="res://scripts/Resources/LevelResource.cs" id="1_script"]
-[ext_resource type="PackedScene" uid="uid://dwr3avefuwnpy" path="res://scenes/level_tutorial_1.tscn" id="2_scene"]
[resource]
script = ExtResource("1_script")
Id = "tutorial_1"
LevelName = "LEVEL_TUTORIAL_1_NAME"
-Scene = ExtResource("2_scene")
+ScenePath = "res://scenes/level_tutorial_1.tscn"
metadata/_custom_type_script = "uid://cp68km8bykymb"
diff --git a/resources/levels/village/village_1.tres b/resources/levels/village/village_1.tres
index 0aaf363..d9bfb5c 100644
--- a/resources/levels/village/village_1.tres
+++ b/resources/levels/village/village_1.tres
@@ -1,11 +1,10 @@
-[gd_resource type="Resource" script_class="LevelResource" load_steps=3 format=3 uid="uid://cqtalsov2bkpo"]
+[gd_resource type="Resource" script_class="LevelResource" load_steps=2 format=3 uid="uid://cqtalsov2bkpo"]
[ext_resource type="Script" uid="uid://c2h0pqhxiqswe" path="res://scripts/Resources/LevelResource.cs" id="1_script"]
-[ext_resource type="PackedScene" uid="uid://bol7g83v2accs" path="res://scenes/level_village_1.tscn" id="2_scene"]
[resource]
script = ExtResource("1_script")
Id = "village_1"
LevelName = "LEVEL_1_NAME"
-Scene = ExtResource("2_scene")
+ScenePath = "res://scenes/level_village_1.tscn"
metadata/_custom_type_script = "uid://cp68km8bykymb"
diff --git a/resources/levels/village/village_2.tres b/resources/levels/village/village_2.tres
index 9eaf32d..2895d18 100644
--- a/resources/levels/village/village_2.tres
+++ b/resources/levels/village/village_2.tres
@@ -1,11 +1,10 @@
-[gd_resource type="Resource" script_class="LevelResource" load_steps=3 format=3 uid="uid://dlwkbjw1l33uq"]
+[gd_resource type="Resource" script_class="LevelResource" load_steps=2 format=3 uid="uid://dlwkbjw1l33uq"]
[ext_resource type="Script" uid="uid://c2h0pqhxiqswe" path="res://scripts/Resources/LevelResource.cs" id="1_script"]
-[ext_resource type="PackedScene" uid="uid://chqb11pfoqmeb" path="res://scenes/level_village_2.tscn" id="2_scene"]
[resource]
script = ExtResource("1_script")
Id = "village_2"
LevelName = "LEVEL_2_NAME"
-Scene = ExtResource("2_scene")
+ScenePath = "res://scenes/level_village_2.tscn"
metadata/_custom_type_script = "uid://cp68km8bykymb"
diff --git a/resources/levels/village/village_3.tres b/resources/levels/village/village_3.tres
index bc1e157..2521c5b 100644
--- a/resources/levels/village/village_3.tres
+++ b/resources/levels/village/village_3.tres
@@ -1,11 +1,10 @@
-[gd_resource type="Resource" script_class="LevelResource" load_steps=3 format=3 uid="uid://b63u5qfp8p7pv"]
+[gd_resource type="Resource" script_class="LevelResource" load_steps=2 format=3 uid="uid://b63u5qfp8p7pv"]
[ext_resource type="Script" uid="uid://c2h0pqhxiqswe" path="res://scripts/Resources/LevelResource.cs" id="1_script"]
-[ext_resource type="PackedScene" uid="uid://h60obxmju6mo" path="res://scenes/level_village_3.tscn" id="2_scene"]
[resource]
script = ExtResource("1_script")
Id = "village_3"
LevelName = "LEVEL_3_NAME"
-Scene = ExtResource("2_scene")
+ScenePath = "res://scenes/level_village_3.tscn"
metadata/_custom_type_script = "uid://cp68km8bykymb"
diff --git a/resources/levels/village/village_4.tres b/resources/levels/village/village_4.tres
index 2cd00e4..b1aa6d5 100644
--- a/resources/levels/village/village_4.tres
+++ b/resources/levels/village/village_4.tres
@@ -1,11 +1,10 @@
-[gd_resource type="Resource" script_class="LevelResource" load_steps=3 format=3 uid="uid://cxfei28yu3buh"]
+[gd_resource type="Resource" script_class="LevelResource" load_steps=2 format=3 uid="uid://cxfei28yu3buh"]
[ext_resource type="Script" uid="uid://c2h0pqhxiqswe" path="res://scripts/Resources/LevelResource.cs" id="1_script"]
-[ext_resource type="PackedScene" uid="uid://bhad760x3vvco" path="res://scenes/level_village_4.tscn" id="2_scene"]
[resource]
script = ExtResource("1_script")
Id = "village_4"
LevelName = "LEVEL_4_NAME"
-Scene = ExtResource("2_scene")
+ScenePath = "res://scenes/level_village_4.tscn"
metadata/_custom_type_script = "uid://cp68km8bykymb"
diff --git a/resources/levels/village/village_5.tres b/resources/levels/village/village_5.tres
index 11d73cb..4e8bd75 100644
--- a/resources/levels/village/village_5.tres
+++ b/resources/levels/village/village_5.tres
@@ -1,11 +1,10 @@
-[gd_resource type="Resource" script_class="LevelResource" load_steps=3 format=3 uid="uid://qld841xv5ui0"]
+[gd_resource type="Resource" script_class="LevelResource" load_steps=2 format=3 uid="uid://qld841xv5ui0"]
[ext_resource type="Script" uid="uid://c2h0pqhxiqswe" path="res://scripts/Resources/LevelResource.cs" id="1_script"]
-[ext_resource type="PackedScene" uid="uid://bbwef3n2gjkt8" path="res://scenes/level_village_5.tscn" id="2_scene"]
[resource]
script = ExtResource("1_script")
Id = "village_5"
LevelName = "LEVEL_5_NAME"
-Scene = ExtResource("2_scene")
+ScenePath = "res://scenes/level_village_5.tscn"
metadata/_custom_type_script = "uid://cp68km8bykymb"
diff --git a/resources/levels/village/village_6.tres b/resources/levels/village/village_6.tres
index b6e2d68..8bc4be1 100644
--- a/resources/levels/village/village_6.tres
+++ b/resources/levels/village/village_6.tres
@@ -1,11 +1,10 @@
-[gd_resource type="Resource" script_class="LevelResource" load_steps=3 format=3]
+[gd_resource type="Resource" script_class="LevelResource" load_steps=2 format=3]
[ext_resource type="Script" uid="uid://c2h0pqhxiqswe" path="res://scripts/Resources/LevelResource.cs" id="1_script"]
-[ext_resource type="PackedScene" uid="uid://dqekccjkwev4y" path="res://scenes/level_village_6.tscn" id="2_scene"]
[resource]
script = ExtResource("1_script")
Id = "village_6"
LevelName = "LEVEL_6_NAME"
-Scene = ExtResource("2_scene")
+ScenePath = "res://scenes/level_village_6.tscn"
metadata/_custom_type_script = "uid://cp68km8bykymb"
diff --git a/scripts/Resources/LevelCatalogue.cs b/scripts/Resources/LevelCatalogue.cs
index dee1023..c8bed0d 100644
--- a/scripts/Resources/LevelCatalogue.cs
+++ b/scripts/Resources/LevelCatalogue.cs
@@ -27,8 +27,8 @@ public partial class LevelCatalogue : Resource
///
/// The ordering the domain works from. Throws if the authored data is inconsistent — a blank or
- /// duplicated id cannot produce a , so a mistake here fails at boot rather
- /// than corrupting a save later.
+ /// duplicated id cannot produce a , so a mistake fails at boot rather than
+ /// corrupting a save later.
///
public Campaign ToCampaign()
{
@@ -36,19 +36,48 @@ public partial class LevelCatalogue : Resource
for (var i = 0; i < Levels.Count; i++)
{
- var level = Levels[i];
- if (level == null)
- throw new System.InvalidOperationException(
- $"LevelCatalogue has an empty slot at index {i}.");
+ if (Levels[i] == null)
+ throw new System.InvalidOperationException($"LevelCatalogue has an empty slot at index {i}.");
- ids.Add(LevelId.Of(level.Id));
+ ids.Add(LevelId.Of(Levels[i].Id));
}
return Campaign.Of(ids);
}
+ ///
+ /// Checks every catalogued Level names a scene that actually exists, returning the problems found.
+ ///
+ ///
+ /// This is what keeps honest. The path cannot be a typed
+ /// PackedScene — several level scenes reference their own LevelResource, so a reference back
+ /// to the scene is a cycle Godot reports as "Parse Error: Busy" and the level fails to load. A
+ /// string plus this check gets the safety without the cycle.
+ ///
+ public IReadOnlyList Validate()
+ {
+ var problems = new List();
+
+ foreach (var level in Levels.Where(l => l != null))
+ {
+ if (string.IsNullOrWhiteSpace(level.ScenePath))
+ problems.Add($"level '{level.Id}' has no scene path");
+ else if (!ResourceLoader.Exists(level.ScenePath))
+ problems.Add($"level '{level.Id}' points at '{level.ScenePath}', which does not exist");
+ }
+
+ return problems;
+ }
+
public LevelResource Find(LevelId id) =>
Levels.FirstOrDefault(level => level != null && level.Id == id.Value);
- public PackedScene SceneFor(LevelId id) => Find(id)?.Scene;
+ /// Loads a Level's scene on demand. Null when the Level is not catalogued.
+ public PackedScene SceneFor(LevelId id)
+ {
+ var path = Find(id)?.ScenePath;
+ return string.IsNullOrWhiteSpace(path) ? null : ResourceLoader.Load(path);
+ }
+
+ public string DisplayNameKeyFor(LevelId id) => Find(id)?.LevelName ?? string.Empty;
}
diff --git a/scripts/Resources/LevelResource.cs b/scripts/Resources/LevelResource.cs
index 51b003d..674d01a 100644
--- a/scripts/Resources/LevelResource.cs
+++ b/scripts/Resources/LevelResource.cs
@@ -3,19 +3,25 @@ using Godot;
namespace Mr.BrickAdventures.scripts.Resources;
///
-/// One authored Level: its stable id, its display name key, and the scene to load.
+/// One authored Level: its stable id, its display name key, and where its scene lives.
///
///
///
/// is what Playthroughs and Best Times reference, so it must never change once a
-/// build has shipped — unlike position in a list, which is what the old
-/// GameManager.LevelScenes array used and which every save depended on.
+/// build has shipped — unlike position in a list, which is what the old GameManager.LevelScenes
+/// array used and which every save depended on.
///
///
-/// The scene is a rather than a path string. The previous
-/// ScenePath field was never read by anything and had rotted: forest_1.tres pointed at
-/// res://scenes/level_forest_5.tscn, which does not exist. An exported reference is tracked by
-/// the editor and cannot drift.
+/// is deliberately a path string and not an exported PackedScene.
+/// Several level scenes reference their own LevelResource (for the death screen's label), so a
+/// typed reference back to the scene forms a cycle that Godot cannot resolve — it fails with
+/// "Parse Error: Busy" and the level will not load at all.
+///
+///
+/// The trade-off is that a path can rot: this field previously pointed at
+/// res://scenes/level_forest_5.tscn, which does not exist. That is fixed by validation rather
+/// than by typing — LevelCatalogue.Validate checks every path resolves, so a bad one fails
+/// loudly at boot instead of silently.
///
///
[GlobalClass]
@@ -29,6 +35,10 @@ public partial class LevelResource : Resource
[Export]
public string LevelName { get; set; } = string.Empty;
- [Export]
- public PackedScene Scene { get; set; }
+ ///
+ /// res:// or uid:// path to the scene. Loaded on demand, never held as a typed
+ /// reference — see the remarks above.
+ ///
+ [Export(PropertyHint.File, "*.tscn")]
+ public string ScenePath { get; set; } = string.Empty;
}