diff --git a/Projects/UOContent.Tests/Tests/Engines/Spawners/SpawnerTickAndGroupDtoTests.cs b/Projects/UOContent.Tests/Tests/Engines/Spawners/SpawnerTickAndGroupDtoTests.cs new file mode 100644 index 000000000..12081e205 --- /dev/null +++ b/Projects/UOContent.Tests/Tests/Engines/Spawners/SpawnerTickAndGroupDtoTests.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using Server.Engines.Spawners; +using Xunit; + +namespace Server.Tests.Engines.Spawners; + +[Collection("Sequential UOContent Tests")] +public class SpawnerTickAndGroupDtoTests +{ + private sealed class GatedSpawner : Spawner + { + public int Ticks; + public bool Gate = false; + + public GatedSpawner() : base(1, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(10), 0, default, "Rabbit") + { + } + + public GatedSpawner(Serial serial) : base(serial) + { + } + + public override void OnTick() + { + Ticks++; + if (Gate) + { + base.OnTick(); + } + } + } + + [Fact] + public void OnTick_IsVirtual_AndManualSpawnBypassesIt() + { + var spawner = new GatedSpawner(); + spawner.MoveToWorld(new Point3D(1500, 1500, 0), Map.Felucca); + try + { + spawner.OnTick(); + Assert.Equal(1, spawner.Ticks); + Assert.Empty(spawner.Spawned); // gate closed: base.OnTick not reached + + spawner.Spawn(); // manual API does not go through OnTick + Assert.Equal(1, spawner.Ticks); + Assert.Single(spawner.Spawned); + } + finally + { + spawner.Delete(); + } + } + + [Fact] + public void Dto_RoundTrip_CarriesGroup() + { + var spawner = new Spawner(1, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(10), 0, default, "Rabbit"); + spawner.MoveToWorld(new Point3D(1500, 1500, 0), Map.Felucca); + Spawner loaded = null; + try + { + spawner.Group = true; + var json = SpawnerJsonSerializer.SerializeCompact(new List { spawner.ToDto() }); + Assert.Contains("\"group\"", json); + + var dtos = JsonSerializer.Deserialize>(json, SpawnerJsonSerializer.Options); + loaded = (Spawner)dtos[0].ToSpawner(); + Assert.True(loaded.Group); + } + finally + { + loaded?.Delete(); + spawner.Delete(); + } + } +} diff --git a/Projects/UOContent/Engines/Spawners/BaseSpawner.Dto.cs b/Projects/UOContent/Engines/Spawners/BaseSpawner.Dto.cs index aab965232..7fe00406f 100644 --- a/Projects/UOContent/Engines/Spawners/BaseSpawner.Dto.cs +++ b/Projects/UOContent/Engines/Spawners/BaseSpawner.Dto.cs @@ -37,6 +37,7 @@ public abstract partial class BaseSpawner InitSpawn(dto.Count, dto.MinDelay, dto.MaxDelay, dto.Team, SpawnBounds); + _group = dto.Group; _walkingRange = dto.WalkingRange; _spawnLocationIsHome = dto.SpawnLocationIsHome; _spawnPositionMode = dto.SpawnPositionMode; diff --git a/Projects/UOContent/Engines/Spawners/BaseSpawner.cs b/Projects/UOContent/Engines/Spawners/BaseSpawner.cs index 3a78f3b4a..234723df1 100644 --- a/Projects/UOContent/Engines/Spawners/BaseSpawner.cs +++ b/Projects/UOContent/Engines/Spawners/BaseSpawner.cs @@ -822,7 +822,11 @@ public abstract partial class BaseSpawner : Item, ISpawner return remove && Spawned.Remove(spawned); } - public void OnTick() + /// + /// Timer callback. Override to gate or reorder tick work; manual does not + /// pass through here. + /// + public virtual void OnTick() { if (_group) { diff --git a/Projects/UOContent/Engines/Spawners/Json/SpawnerDto.cs b/Projects/UOContent/Engines/Spawners/Json/SpawnerDto.cs index e6bc92808..4e6d8e195 100644 --- a/Projects/UOContent/Engines/Spawners/Json/SpawnerDto.cs +++ b/Projects/UOContent/Engines/Spawners/Json/SpawnerDto.cs @@ -78,12 +78,19 @@ public abstract record SpawnerDto [JsonPropertyOrder(11)] public bool SpawnLocationIsHome { get; init; } - [JsonPropertyName("spawnPositionMode")] + // Order values are unique on purpose: STJ's JsonPropertyOrder ties are documented as + // undefined between equal values, so every field gets its own slot. + /// All dead, then respawn together. Mirrors . + [JsonPropertyName("group")] [JsonPropertyOrder(12)] + public bool Group { get; init; } + + [JsonPropertyName("spawnPositionMode")] + [JsonPropertyOrder(13)] public SpawnPositionMode SpawnPositionMode { get; init; } [JsonPropertyName("maxSpawnAttempts")] - [JsonPropertyOrder(13)] + [JsonPropertyOrder(14)] public int MaxSpawnAttempts { get; init; } // Compact square-bounds form, -1 when absent. Written only when >= 0 (ShouldSerialize, since 0 @@ -195,15 +202,15 @@ public sealed record ProximitySpawnerDto : SpawnerDto public Rectangle3D SpawnBounds { get; init; } [JsonPropertyName("triggerRange")] - [JsonPropertyOrder(14)] + [JsonPropertyOrder(15)] public int TriggerRange { get; init; } [JsonPropertyName("spawnMessage")] - [JsonPropertyOrder(15)] + [JsonPropertyOrder(16)] public TextDefinition SpawnMessage { get; init; } [JsonPropertyName("instant")] - [JsonPropertyOrder(16)] + [JsonPropertyOrder(17)] public bool Instant { get; init; } [JsonPropertyName("entries")] diff --git a/Projects/UOContent/Engines/Spawners/ProximitySpawner.Dto.cs b/Projects/UOContent/Engines/Spawners/ProximitySpawner.Dto.cs index d71676815..834f0326a 100644 --- a/Projects/UOContent/Engines/Spawners/ProximitySpawner.Dto.cs +++ b/Projects/UOContent/Engines/Spawners/ProximitySpawner.Dto.cs @@ -33,6 +33,7 @@ public partial class ProximitySpawner WalkingRange = DtoWalkingRange, Entries = EntryList ?? [], SpawnLocationIsHome = SpawnLocationIsHome, + Group = Group, SpawnPositionMode = DtoSpawnPositionMode, MaxSpawnAttempts = DtoMaxSpawnAttempts, HomeRange = homeRange, diff --git a/Projects/UOContent/Engines/Spawners/RegionSpawner.Dto.cs b/Projects/UOContent/Engines/Spawners/RegionSpawner.Dto.cs index d6c343071..ef097fee4 100644 --- a/Projects/UOContent/Engines/Spawners/RegionSpawner.Dto.cs +++ b/Projects/UOContent/Engines/Spawners/RegionSpawner.Dto.cs @@ -31,6 +31,7 @@ public partial class RegionSpawner WalkingRange = DtoWalkingRange, Entries = EntryList ?? [], SpawnLocationIsHome = SpawnLocationIsHome, + Group = Group, SpawnPositionMode = DtoSpawnPositionMode, MaxSpawnAttempts = DtoMaxSpawnAttempts, Region = SpawnRegion?.Name diff --git a/Projects/UOContent/Engines/Spawners/Spawner.Dto.cs b/Projects/UOContent/Engines/Spawners/Spawner.Dto.cs index 4b9e9f495..ae5b5f73a 100644 --- a/Projects/UOContent/Engines/Spawners/Spawner.Dto.cs +++ b/Projects/UOContent/Engines/Spawners/Spawner.Dto.cs @@ -33,6 +33,7 @@ public partial class Spawner WalkingRange = DtoWalkingRange, Entries = EntryList ?? [], SpawnLocationIsHome = SpawnLocationIsHome, + Group = Group, SpawnPositionMode = DtoSpawnPositionMode, MaxSpawnAttempts = DtoMaxSpawnAttempts, HomeRange = homeRange,