Fix engine-wide code review findings
CI / build-test (push) Failing after 1m7s

Collisions: init bucket heads to -1 (QueryAabb hung before the first
rebuild), reset query stamps on truncated QueryAabb (later queries
silently dropped entities), inside-origin raycasts hit at fraction 0 for
circles too, exactly-touching boxes now pair like touching circles.

Graphics: render into the letterbox viewport so the picture matches
ScreenToWorld/WorldToScreen instead of stretching; Y-sort by the
transform pivot rather than the quad center; lock-free snapshot
LayerRegistry (parallel submit read it unsynchronized); validate
InitialCapacity; warn when UseRenderer2D drops options of a later scene.

Core: scenes are explicitly single-use (re-loading threw silently
duplicated systems/entities before — now it throws), Scene.RegisterUnload
for per-scene resources, a switch requested during the reveal phase
covers again instead of hard-swapping, borderless fullscreen
(HardwareModeSwitch off), InputCapture service for input-suppressing
overlays, host disposes the transition renderer and IDisposable services
on shutdown.

Input: game input reads as released while InputCapture is held; mouse
position and wheel freeze so deltas stay zero.

DevConsole: holds InputCapture while open (typing no longer drives the
camera), Revision increments only under the lock, quoted command
arguments, history capped at 256.

UI: scene Desktop skips Myra input processing while the console is open
(clicks no longer fall through), is disposed on scene unload, and Myra
init no longer depends on a process-static flag.

Audio: validate channel count/sample rate before stopping the previous
track, empty looped oggs no longer hang FillBuffers, the instance stops
when a non-looping track drains (IsPlaying was stuck true).

Atlases: metadata v2 stores per-source size+mtime snapshots, so
timestamp-preserving copies and renames invalidate correctly; loader
checks the version and disposes pages on partial load failure; shared
pages never exceed a non-POT MaxPageSize; oversized items pack first
onto exact-size pages instead of splitting an open shared page; the CLI
validates numeric options.

Assets.Generator: file names are escaped in XML docs and string
literals, members no longer collide with the enclosing class (CS0542),
and the Assets root is resolved against build_property.projectdir so
nested "Assets" directories do not shift region paths.

Pathfinding: queries throw when the grid was resized after construction;
generation stamps survive int overflow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-11 21:18:08 +03:00
co-authored by Claude Fable 5
parent 56f2a85478
commit 501d81e19f
35 changed files with 926 additions and 98 deletions
+28 -13
View File
@@ -54,6 +54,9 @@ public static class AtlasBuilder
{
private static readonly string[] SourceExtensions = [".png", ".jpg", ".jpeg", ".bmp"];
/// <summary>Snapshot of one source image taken at scan time (size/mtime feed the staleness check).</summary>
private readonly record struct SourceFile(string FullPath, string Key, long Size, long ModifiedTicks);
/// <summary>Builds (or incrementally refreshes) all atlases for <paramref name="options"/>.</summary>
public static AtlasBuildResult Build(AtlasBuildOptions options)
{
@@ -87,10 +90,10 @@ public static class AtlasBuilder
return (name, key);
}
private static SortedDictionary<string, List<(string FullPath, string Key)>> ScanGroups(
private static SortedDictionary<string, List<SourceFile>> ScanGroups(
string sourceRoot, AtlasBuildOptions options)
{
var groups = new SortedDictionary<string, List<(string, string)>>(StringComparer.Ordinal);
var groups = new SortedDictionary<string, List<SourceFile>>(StringComparer.Ordinal);
var keys = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var fullPath in Directory.EnumerateFiles(sourceRoot, "*", SearchOption.AllDirectories))
{
@@ -113,14 +116,15 @@ public static class AtlasBuilder
groups.Add(atlasName, list);
}
list.Add((fullPath, key));
var info = new FileInfo(fullPath);
list.Add(new SourceFile(fullPath, key, info.Length, info.LastWriteTimeUtc.Ticks));
}
return groups;
}
private static AtlasGroupResult BuildGroup(
string name, List<(string FullPath, string Key)> files, AtlasBuildOptions options)
string name, List<SourceFile> files, AtlasBuildOptions options)
{
var metadataPath = Path.Combine(options.OutputDirectory, name + ".atlas");
if (!options.Force && IsUpToDate(metadataPath, files, options, out var existingPages))
@@ -151,14 +155,14 @@ public static class AtlasBuilder
}
WritePages(name, packed, pixelsByKey, options.OutputDirectory);
WriteMetadata(name, packed, options, metadataPath);
WriteMetadata(name, packed, files, options, metadataPath);
DeleteExtraPages(name, packed.PageSizes.Count, options.OutputDirectory);
return new AtlasGroupResult(name, files.Count, packed.PageSizes.Count, Skipped: false);
}
private static bool IsUpToDate(
string metadataPath, List<(string FullPath, string Key)> files, AtlasBuildOptions options, out int pages)
string metadataPath, List<SourceFile> files, AtlasBuildOptions options, out int pages)
{
pages = 0;
if (!File.Exists(metadataPath))
@@ -176,7 +180,8 @@ public static class AtlasBuilder
return false;
}
if (metadata.PageSize != options.MaxPageSize || metadata.Padding != options.Padding)
if (metadata.Version != AtlasMetadata.CurrentVersion ||
metadata.PageSize != options.MaxPageSize || metadata.Padding != options.Padding)
{
return false;
}
@@ -187,16 +192,21 @@ public static class AtlasBuilder
return false;
}
if (!metadata.Regions.Select(r => r.Key).Order(StringComparer.Ordinal)
.SequenceEqual(files.Select(f => f.Key).Order(StringComparer.Ordinal)))
// Источники сравниваются по точному снапшоту (ключ + размер + mtime), а не по
// «новее метаданных»: переименования и копии с сохранением времени тоже ловятся.
if (metadata.Sources.Count != files.Count)
{
return false;
}
var builtAt = File.GetLastWriteTimeUtc(metadataPath);
if (files.Any(f => File.GetLastWriteTimeUtc(f.FullPath) > builtAt))
var sourcesByKey = metadata.Sources.ToDictionary(s => s.Key, StringComparer.Ordinal);
foreach (var file in files)
{
return false;
if (!sourcesByKey.TryGetValue(file.Key, out var source) ||
source.Size != file.Size || source.Modified != file.ModifiedTicks)
{
return false;
}
}
pages = metadata.Pages.Count;
@@ -233,13 +243,18 @@ public static class AtlasBuilder
});
}
private static void WriteMetadata(string name, PackResult packed, AtlasBuildOptions options, string metadataPath)
private static void WriteMetadata(
string name, PackResult packed, List<SourceFile> files, AtlasBuildOptions options, string metadataPath)
{
var metadata = new AtlasMetadata
{
Name = name,
PageSize = options.MaxPageSize,
Padding = options.Padding,
Sources = files
.OrderBy(f => f.Key, StringComparer.Ordinal)
.Select(f => new AtlasSource { Key = f.Key, Size = f.Size, Modified = f.ModifiedTicks })
.ToList(),
Pages = packed.PageSizes
.Select((size, index) => new AtlasPage
{