Remove obsolete project files for MrGameEng.AI, MrGameEng.Assets, MrGameEng.Atlases, and MrGameEng.Collisions modules. Introduce new AssetManager and related classes for asset loading and management, including support for texture atlases and mod definitions. Enhance mod loading capabilities with DefDatabase and LanguageManager for JSON-based definitions and localization. Implement a shelf packing algorithm for efficient texture atlas creation.

This commit is contained in:
Leonid Pershin
2026-06-12 07:47:04 +03:00
parent 1f87fb0b74
commit c30e2ce764
70 changed files with 0 additions and 233 deletions
@@ -0,0 +1,159 @@
using MrGameEng.Atlases;
using Xunit;
namespace MrGameEng.Atlases.Tests;
public class ShelfPackerTests
{
private static List<PackItem> Squares(int count, int size) =>
Enumerable.Range(0, count).Select(i => new PackItem($"item{i:D3}", size, size)).ToList();
[Fact]
public void Pack_PlacesEveryItem_WithinPageBounds()
{
var result = ShelfPacker.Pack(Squares(50, 60), maxPageSize: 256, padding: 2);
Assert.Equal(50, result.Placements.Count);
foreach (var p in result.Placements)
{
var (pageWidth, pageHeight) = result.PageSizes[p.Page];
Assert.True(p.X >= 2 && p.Y >= 2);
Assert.True(p.X + p.Width <= pageWidth);
Assert.True(p.Y + p.Height <= pageHeight);
}
}
[Fact]
public void Pack_NoTwoPlacements_Overlap()
{
var items = new List<PackItem>();
var random = new Random(42);
for (var i = 0; i < 200; i++)
{
items.Add(new PackItem($"r{i:D3}", random.Next(4, 90), random.Next(4, 90)));
}
var result = ShelfPacker.Pack(items, maxPageSize: 512, padding: 2);
var byPage = result.Placements.GroupBy(p => p.Page);
foreach (var page in byPage)
{
var list = page.ToList();
for (var i = 0; i < list.Count; i++)
{
for (var j = i + 1; j < list.Count; j++)
{
var a = list[i];
var b = list[j];
var separated =
a.X + a.Width + 2 <= b.X
|| b.X + b.Width + 2 <= a.X
|| a.Y + a.Height + 2 <= b.Y
|| b.Y + b.Height + 2 <= a.Y;
Assert.True(separated, $"{a.Key} overlaps {b.Key} (padding included)");
}
}
}
}
[Fact]
public void Pack_Overflows_ToMultiplePages()
{
// 9 квадратов 100² на страницу 256² помещаются максимум по 4.
var result = ShelfPacker.Pack(Squares(9, 100), maxPageSize: 256, padding: 2);
Assert.True(result.PageSizes.Count >= 3);
Assert.Equal(
Enumerable.Range(0, result.PageSizes.Count),
result.Placements.Select(p => p.Page).Distinct().Order()
);
}
[Fact]
public void Pack_OversizedItem_GetsDedicatedPage()
{
var items = Squares(2, 30);
items.Add(new PackItem("huge", 500, 40));
var result = ShelfPacker.Pack(items, maxPageSize: 256, padding: 2);
var huge = result.Placements.Single(p => p.Key == "huge");
Assert.Single(result.Placements, p => p.Page == huge.Page);
Assert.True(result.PageSizes[huge.Page].Width >= 504);
}
[Fact]
public void Pack_IsDeterministic_RegardlessOfInputOrder()
{
var items = Squares(30, 20)
.Concat(Squares(10, 50).Select(i => i with { Key = "b" + i.Key }))
.ToList();
var shuffled = items.AsEnumerable().Reverse().ToList();
var a = ShelfPacker.Pack(items, 128, 2);
var b = ShelfPacker.Pack(shuffled, 128, 2);
Assert.Equal(
a.Placements.OrderBy(p => p.Key, StringComparer.Ordinal),
b.Placements.OrderBy(p => p.Key, StringComparer.Ordinal)
);
}
[Fact]
public void Pack_TrimsPages_ToPowerOfTwo()
{
var result = ShelfPacker.Pack(Squares(1, 50), maxPageSize: 2048, padding: 2);
Assert.Equal((64, 64), result.PageSizes.Single());
}
[Theory]
[InlineData(1, 1)]
[InlineData(64, 64)]
[InlineData(65, 128)]
[InlineData(2048, 2048)]
public void NextPowerOfTwo_RoundsUp(int value, int expected)
{
Assert.Equal(expected, ShelfPacker.NextPowerOfTwo(value));
}
[Fact]
public void Pack_NonPowerOfTwoLimit_SharedPagesNeverExceedIt()
{
// 40² с padding на лимит 100: POT-округление дало бы 128 > лимита.
var result = ShelfPacker.Pack(Squares(10, 40), maxPageSize: 100, padding: 2);
Assert.All(
result.PageSizes,
size =>
{
Assert.True(size.Width <= 100, $"page width {size.Width} > 100");
Assert.True(size.Height <= 100, $"page height {size.Height} > 100");
}
);
}
[Fact]
public void Pack_OversizedItem_PageHasExactPaddedSize()
{
var result = ShelfPacker.Pack([new PackItem("big", 300, 50)], maxPageSize: 128, padding: 2);
Assert.Equal((304, 54), result.PageSizes.Single());
}
[Fact]
public void Pack_OversizedAmongSmall_DoesNotSplitSharedPage()
{
// Широкий и низкий негабарит сортируется в конец по высоте; раньше он закрывал
// наполовину заполненную общую страницу, и остаток уезжал на новую.
var items = new List<PackItem> { new("big", 300, 10) };
for (var i = 0; i < 4; i++)
{
items.Add(new PackItem($"s{i}", 30, 30));
}
var result = ShelfPacker.Pack(items, maxPageSize: 128, padding: 2);
Assert.Equal(2, result.PageSizes.Count); // отдельная страница big + одна общая
}
}