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:
co-authored by
Claude Fable 5
parent
56f2a85478
commit
501d81e19f
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user