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; }