This commit is contained in:
2026-08-18 02:03:34 +08:00
parent d2cd3b9fa8
commit 517c4cf46e
157 changed files with 16536 additions and 37 deletions
@@ -0,0 +1,22 @@
{
"name": "Demo2.EditMode.Tests",
"rootNamespace": "Demo2.Tests",
"references": [
"Demo2.Domain",
"Demo2.ECS",
"Demo2.Runtime",
"NightShiftProtocol.Rules",
"Unity.Entities",
"Unity.Collections",
"UnityEngine.TestRunner",
"UnityEditor.TestRunner"
],
"includePlatforms": ["Editor"],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"autoReferenced": false,
"defineConstraints": ["UNITY_INCLUDE_TESTS"],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: b4942298112ebe74e8c777d9494bdb4c
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,74 @@
#nullable enable
using System.Linq;
using Demo2.Domain;
using NightShiftProtocol;
using NUnit.Framework;
namespace Demo2.Tests
{
public sealed class Demo2DomainTests
{
[Test]
public void BuiltInContent_HasFiveCyclesEighteenRulesAndFiveCategories()
{
var content = Demo2BuiltInContent.Create();
Assert.That(content.Rules.Count, Is.EqualTo(18));
Assert.That(content.Rules.Values.Select(rule => rule.Category).Distinct().Count(), Is.EqualTo(5));
Assert.That(content.Waves.Select(wave => wave.TotalHealth), Is.EqualTo(new long[] { 500, 900, 1620, 2916, 5248 }));
Assert.That(content.ComputeContentHash(), Has.Length.EqualTo(64));
}
[Test]
public void SaturatingMath_ClampsBothDirectionsAndFormatsLargeValues()
{
Assert.That(SaturatingMath.Add(long.MaxValue, 1), Is.EqualTo(long.MaxValue));
Assert.That(SaturatingMath.Add(long.MinValue, -1), Is.EqualTo(long.MinValue));
Assert.That(SaturatingMath.Multiply(long.MaxValue, 2), Is.EqualTo(long.MaxValue));
Assert.That(SaturatingMath.MultiplyScaled(200, 1500), Is.EqualTo(300));
Assert.That(SaturatingMath.Format(1_250_000_000), Is.EqualTo("1.25e9"));
}
[Test]
public void RuleOrder_IsStrictlyLeftToRight()
{
var rules = Demo2BuiltInContent.BuildRules().ToDictionary(rule => rule.Id);
var initial = new Demo2RuleContext { Payload = 100, MultiplierMilli = 1000 };
var addThenMultiply = Demo2RuleEngine.Evaluate(initial, new[] { rules["production.dense-ammo"], rules["machine.overclock"] });
var multiplyThenAdd = Demo2RuleEngine.Evaluate(initial, new[] { rules["machine.overclock"], rules["production.dense-ammo"] });
Assert.That(addThenMultiply.Payload, Is.EqualTo(420));
Assert.That(multiplyThenAdd.Payload, Is.EqualTo(330));
Assert.That(addThenMultiply.Faults, Is.EqualTo(1));
}
[Test]
public void ConditionalRule_OnlyRunsWhenEveryFlagMatches()
{
var content = Demo2BuiltInContent.Create();
var rule = content.Rules["logistics.long-haul"];
var missesFaultCondition = new Demo2RuleContext { Payload = 100, MultiplierMilli = 1000, BeltBelowEightyPercent = true, Faults = 1 };
var matches = missesFaultCondition;
matches.Faults = 0;
Assert.That(Demo2RuleEngine.Evaluate(missesFaultCondition, new[] { rule }).MultiplierMilli, Is.EqualTo(1000));
Assert.That(Demo2RuleEngine.Evaluate(matches, new[] { rule }).MultiplierMilli, Is.EqualTo(1200));
}
[Test]
public void NightShiftMod_AddsMachineRecipeAndConditionalRule()
{
var content = Demo2BuiltInContent.Create();
var before = content.ComputeContentHash();
new NightShiftContent().Register(content);
Assert.That(content.Machines.ContainsKey(Demo2MachineKind.NightShiftSmelter), Is.True);
Assert.That(content.Recipes.ContainsKey(100), Is.True);
Assert.That(content.Rules.ContainsKey("night-shift.continuous-production"), Is.True);
Assert.That(content.ComputeContentHash(), Is.Not.EqualTo(before));
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0018b00d882aac0418e7c356e3582bb1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,130 @@
#nullable enable
using System.Diagnostics;
using Demo2.Domain;
using Demo2.ECS;
using Demo2.Runtime;
using NUnit.Framework;
using Unity.Collections;
using Unity.Entities;
using UnityEngine;
namespace Demo2.Tests
{
public sealed class Demo2EcsTests
{
[Test]
public void SameSeedAndCommands_RemainIdenticalForTenThousandTicks()
{
using var left = new Demo2EcsWorld(20260817, Demo2BuiltInContent.BuildRules());
using var right = new Demo2EcsWorld(20260817, Demo2BuiltInContent.BuildRules());
left.SetPhase(Demo2Phase.Production);
right.SetPhase(Demo2Phase.Production);
for (var tick = 0; tick < 10_000; tick++)
{
left.Tick();
right.Tick();
}
var leftState = left.EntityManager.GetComponentData<Demo2SimulationState>(left.Singleton);
var rightState = right.EntityManager.GetComponentData<Demo2SimulationState>(right.Singleton);
Assert.That(leftState.Tick, Is.EqualTo(10_000));
Assert.That(leftState.StateHash, Is.EqualTo(rightState.StateHash));
}
[Test]
public void SnapshotRestore_ContinuesWithTheSameHash()
{
using var source = new Demo2EcsWorld(42, Demo2BuiltInContent.BuildRules());
source.SetPhase(Demo2Phase.Production);
for (var tick = 0; tick < 500; tick++) source.Tick();
var snapshot = source.CaptureSnapshot(new[] { "production.dense-ammo" });
using var restored = new Demo2EcsWorld(999, Demo2BuiltInContent.BuildRules());
restored.RestoreSnapshot(snapshot);
for (var tick = 0; tick < 200; tick++)
{
source.Tick();
restored.Tick();
}
var sourceState = source.EntityManager.GetComponentData<Demo2SimulationState>(source.Singleton);
var restoredState = restored.EntityManager.GetComponentData<Demo2SimulationState>(restored.Singleton);
Assert.That(restoredState.StateHash, Is.EqualTo(sourceState.StateHash));
}
[Test]
public void BuildCommands_AreIdempotentAndRemovalRefundsBeforeEntityDestruction()
{
using var service = new Demo2GameService();
service.StartNewGame(17);
var command = new Demo2BuildCommand
{
Sequence = 1,
IdempotencyToken = "test-place",
PlayerId = "test",
Kind = Demo2MachineKind.Conveyor,
X = 1,
Y = 1
};
var accepted = service.SubmitBuild(command);
var duplicate = service.SubmitBuild(command);
var removed = service.BuildLocal(Demo2MachineKind.Conveyor, 1, 1, remove: true);
Assert.That(accepted.Accepted, Is.True);
Assert.That(duplicate.Accepted && duplicate.Duplicate, Is.True);
Assert.That(removed.Accepted, Is.True);
Assert.That(removed.Refund, Is.EqualTo(5));
}
[Test]
public void MultipleTurrets_LoadWithoutSingletonExceptions()
{
using var world = new Demo2EcsWorld(31);
var manager = world.EntityManager;
var state = manager.GetComponentData<Demo2SimulationState>(world.Singleton);
var turret = Demo2EcsFactory.CreateMachine(manager, ++state.NextStableId, Demo2MachineKind.Turret, 22, 10, 0, 0);
manager.AddComponentData(turret, new Demo2TurretAmmo());
Demo2EcsFactory.CreateMaterial(manager, ++state.NextStableId, Demo2MaterialKind.Ammo, 0, 19, 0, 9);
manager.SetComponentData(world.Singleton, state);
world.SetPhase(Demo2Phase.Production);
world.Tick();
var query = manager.CreateEntityQuery(typeof(Demo2TurretAmmo));
using var ammo = query.ToComponentDataArray<Demo2TurretAmmo>(Allocator.Temp);
long total = 0;
for (var i = 0; i < ammo.Length; i++) total += ammo[i].Value;
Assert.That(total, Is.EqualTo(9));
}
[Test]
[Category("Demo2Performance")]
public void StressTick_HandlesTenThousandItemsAndOneThousandMachines()
{
using var world = new Demo2EcsWorld(77);
var manager = world.EntityManager;
var state = manager.GetComponentData<Demo2SimulationState>(world.Singleton);
for (var i = 0; i < 1_000; i++)
Demo2EcsFactory.CreateMachine(manager, ++state.NextStableId, Demo2MachineKind.Conveyor, i % Demo2Protocol.GridWidth, (i / Demo2Protocol.GridWidth) % 10, 0, 0);
for (var i = 0; i < 10_000; i++)
Demo2EcsFactory.CreateMaterial(manager, ++state.NextStableId, Demo2MaterialKind.IronOre, i % Demo2Protocol.GridWidth, 19, 0, 1);
manager.SetComponentData(world.Singleton, state);
world.SetPhase(Demo2Phase.Production);
var before = System.GC.GetAllocatedBytesForCurrentThread();
var stopwatch = Stopwatch.StartNew();
world.Tick();
stopwatch.Stop();
var allocated = System.GC.GetAllocatedBytesForCurrentThread() - before;
using var entities = manager.GetAllEntities(Allocator.Temp);
UnityEngine.Debug.Log($"[Demo2 Benchmark] entities={entities.Length} tickMs={stopwatch.Elapsed.TotalMilliseconds:0.00} managedBytes={allocated}");
Assert.That(entities.Length, Is.GreaterThanOrEqualTo(11_000));
Assert.That(stopwatch.ElapsedMilliseconds, Is.LessThan(10_000));
Assert.That(allocated, Is.LessThanOrEqualTo(4096));
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7fee46ad8185d234eba8c1cf30f4aafa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: