fix(levels): ScenePath must be a string, not a PackedScene
slice 7 regression. level_village_2..5.tscn each reference their own LevelResource for the death screen label, so pointing the resource back at the scene forms a cycle Godot cannot resolve - "Parse Error: Busy" and the level fails to load entirely. ScenePath was a string for exactly this reason. Reverted, and the rot it allowed (forest_1 aimed at a nonexistent scene) is now caught by LevelCatalogue.Validate checking every path resolves, plus a repo-level structural check - fixed by validation rather than by typing. project.godot: restores the C# feature flag and dialogue_manager uses_dotnet, which `godot --headless --import` strips on every run.
This commit is contained in:
@@ -27,8 +27,8 @@ public partial class LevelCatalogue : Resource
|
||||
|
||||
/// <summary>
|
||||
/// The ordering the domain works from. Throws if the authored data is inconsistent — a blank or
|
||||
/// duplicated id cannot produce a <see cref="Campaign"/>, so a mistake here fails at boot rather
|
||||
/// than corrupting a save later.
|
||||
/// duplicated id cannot produce a <see cref="Campaign"/>, so a mistake fails at boot rather than
|
||||
/// corrupting a save later.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks every catalogued Level names a scene that actually exists, returning the problems found.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is what keeps <see cref="LevelResource.ScenePath"/> honest. The path cannot be a typed
|
||||
/// <c>PackedScene</c> — 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.
|
||||
/// </remarks>
|
||||
public IReadOnlyList<string> Validate()
|
||||
{
|
||||
var problems = new List<string>();
|
||||
|
||||
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;
|
||||
/// <summary>Loads a Level's scene on demand. Null when the Level is not catalogued.</summary>
|
||||
public PackedScene SceneFor(LevelId id)
|
||||
{
|
||||
var path = Find(id)?.ScenePath;
|
||||
return string.IsNullOrWhiteSpace(path) ? null : ResourceLoader.Load<PackedScene>(path);
|
||||
}
|
||||
|
||||
public string DisplayNameKeyFor(LevelId id) => Find(id)?.LevelName ?? string.Empty;
|
||||
}
|
||||
|
||||
@@ -3,19 +3,25 @@ using Godot;
|
||||
namespace Mr.BrickAdventures.scripts.Resources;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see cref="Id"/> 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
|
||||
/// <c>GameManager.LevelScenes</c> array used and which every save depended on.
|
||||
/// build has shipped — unlike position in a list, which is what the old <c>GameManager.LevelScenes</c>
|
||||
/// array used and which every save depended on.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The scene is a <see cref="PackedScene"/> rather than a path string. The previous
|
||||
/// <c>ScenePath</c> field was never read by anything and had rotted: <c>forest_1.tres</c> pointed at
|
||||
/// <c>res://scenes/level_forest_5.tscn</c>, which does not exist. An exported reference is tracked by
|
||||
/// the editor and cannot drift.
|
||||
/// <see cref="ScenePath"/> is deliberately a path string and not an exported <c>PackedScene</c>.
|
||||
/// Several level scenes reference their own <c>LevelResource</c> (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.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The trade-off is that a path can rot: this field previously pointed at
|
||||
/// <c>res://scenes/level_forest_5.tscn</c>, which does not exist. That is fixed by validation rather
|
||||
/// than by typing — <c>LevelCatalogue.Validate</c> checks every path resolves, so a bad one fails
|
||||
/// loudly at boot instead of silently.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[GlobalClass]
|
||||
@@ -29,6 +35,10 @@ public partial class LevelResource : Resource
|
||||
[Export]
|
||||
public string LevelName { get; set; } = string.Empty;
|
||||
|
||||
[Export]
|
||||
public PackedScene Scene { get; set; }
|
||||
/// <summary>
|
||||
/// <c>res://</c> or <c>uid://</c> path to the scene. Loaded on demand, never held as a typed
|
||||
/// reference — see the remarks above.
|
||||
/// </summary>
|
||||
[Export(PropertyHint.File, "*.tscn")]
|
||||
public string ScenePath { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user