refactor(spawners): non-nullable SpawnerDto; write homeRange for square bounds
Drop nullable DTO types in favor of WhenWritingDefault + sentinels (ModernUO avoids nullable). guid/location/map/count/minDelay/maxDelay/entries are mandatory (force-written via [JsonIgnore(Never)]); optional fields omit at their default. spawnPositionMode/ maxSpawnAttempts map their non-CLR-default "omit" value onto the default in ToDto. Reintroduce compact homeRange: ToDto writes homeRange (the radius) only when SpawnBounds is EXACTLY what that radius reconstructs (square, centered, standard z/depth) so the round-trip is lossless; otherwise it writes spawnBounds. homeRange uses a -1 "absent" sentinel and a ShouldSerialize(hr >= 0) since 0 is a valid radius WhenWritingDefault cannot emit. JsonPropertyOrder preserves the on-disk field order. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7c79dde63e
commit
3c9434a52a
7 changed files with 231 additions and 112 deletions
|
|
@ -12,12 +12,13 @@ namespace UOContent.Tests.Engines.Spawners.Json;
|
|||
public class SpawnerDtoRoundTripTests
|
||||
{
|
||||
[Fact]
|
||||
public void Spawner_RoundTrips_ThroughDto()
|
||||
public void Spawner_NonSquareBounds_WritesSpawnBounds_RoundTrips()
|
||||
{
|
||||
Spawner original = null;
|
||||
BaseSpawner rebuilt = null;
|
||||
try
|
||||
{
|
||||
// Bounds centered on (102,102), placed at (105,105) -> not a homeRange square.
|
||||
original = new Spawner(2, TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(7), 0,
|
||||
new Rectangle3D(100, 100, 0, 5, 5, 0), "Fisherman");
|
||||
original.MoveToWorld(new Point3D(105, 105, 0), Map.Felucca);
|
||||
|
|
@ -25,6 +26,8 @@ public class SpawnerDtoRoundTripTests
|
|||
var json = JsonSerializer.Serialize(new List<SpawnerDto> { original.ToDto() }, SpawnerJsonSerializer.Options);
|
||||
Assert.Contains("\"$type\": \"Spawner\"", json);
|
||||
Assert.Contains("\"count\": 2", json);
|
||||
Assert.Contains("spawnBounds", json);
|
||||
Assert.DoesNotContain("homeRange", json);
|
||||
|
||||
var dtos = JsonSerializer.Deserialize<List<SpawnerDto>>(json, SpawnerJsonSerializer.Options);
|
||||
rebuilt = Assert.Single(dtos).ToSpawner();
|
||||
|
|
@ -42,18 +45,52 @@ public class SpawnerDtoRoundTripTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void Spawner_OmitsDomainDefaults()
|
||||
public void Spawner_HomeRangeSquare_WritesHomeRange_RoundTrips()
|
||||
{
|
||||
Spawner original = null;
|
||||
BaseSpawner rebuilt = null;
|
||||
try
|
||||
{
|
||||
original = new Spawner("Fisherman");
|
||||
original.MoveToWorld(new Point3D(200, 200, 0), Map.Felucca);
|
||||
// Exactly what homeRange 5 reconstructs (centered square, standard z/depth).
|
||||
original.SpawnBounds = new Rectangle3D(195, 195, -128, 11, 11, 256);
|
||||
|
||||
var json = JsonSerializer.Serialize(new List<SpawnerDto> { original.ToDto() }, SpawnerJsonSerializer.Options);
|
||||
Assert.Contains("\"homeRange\": 5", json);
|
||||
Assert.DoesNotContain("spawnBounds", json);
|
||||
|
||||
var dtos = JsonSerializer.Deserialize<List<SpawnerDto>>(json, SpawnerJsonSerializer.Options);
|
||||
rebuilt = Assert.Single(dtos).ToSpawner();
|
||||
Assert.Equal(new Rectangle3D(195, 195, -128, 11, 11, 256), rebuilt.SpawnBounds);
|
||||
}
|
||||
finally
|
||||
{
|
||||
rebuilt?.Delete();
|
||||
original?.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spawner_WritesMandatory_OmitsOptionalDefaults()
|
||||
{
|
||||
Spawner original = null;
|
||||
try
|
||||
{
|
||||
// Default delays (5/10), team 0, default maxSpawnAttempts.
|
||||
original = new Spawner("Fisherman");
|
||||
original.MoveToWorld(new Point3D(110, 110, 0), Map.Felucca);
|
||||
var json = JsonSerializer.Serialize(new List<SpawnerDto> { original.ToDto() }, SpawnerJsonSerializer.Options);
|
||||
Assert.DoesNotContain("minDelay", json);
|
||||
Assert.DoesNotContain("maxDelay", json);
|
||||
|
||||
// Mandatory — always written, even at default.
|
||||
Assert.Contains("\"guid\":", json);
|
||||
Assert.Contains("minDelay", json);
|
||||
Assert.Contains("maxDelay", json);
|
||||
|
||||
// Optional defaults — omitted.
|
||||
Assert.DoesNotContain("\"team\"", json);
|
||||
Assert.DoesNotContain("maxSpawnAttempts", json);
|
||||
Assert.DoesNotContain("spawnLocationIsHome", json);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -75,6 +112,8 @@ public class SpawnerDtoRoundTripTests
|
|||
var json = JsonSerializer.Serialize(new List<SpawnerDto> { original.ToDto() }, SpawnerJsonSerializer.Options);
|
||||
Assert.Contains("\"$type\": \"RegionSpawner\"", json);
|
||||
Assert.Contains("DtoTestRegion", json);
|
||||
Assert.DoesNotContain("homeRange", json);
|
||||
Assert.DoesNotContain("spawnBounds", json);
|
||||
|
||||
var dtos = JsonSerializer.Deserialize<List<SpawnerDto>>(json, SpawnerJsonSerializer.Options);
|
||||
rebuilt = Assert.Single(dtos).ToSpawner();
|
||||
|
|
|
|||
|
|
@ -22,45 +22,25 @@ public abstract partial class BaseSpawner
|
|||
/// <summary>Applies the common DTO fields to this freshly-created spawner (import path).</summary>
|
||||
internal void ApplyDto(SpawnerDto dto)
|
||||
{
|
||||
_guid = dto.Guid ?? Guid.NewGuid();
|
||||
_guid = dto.Guid == Guid.Empty ? Guid.NewGuid() : dto.Guid;
|
||||
|
||||
if (!string.IsNullOrEmpty(dto.Name))
|
||||
{
|
||||
Name = dto.Name;
|
||||
}
|
||||
|
||||
// Legacy homeRange -> spawnBounds (Map not available yet; use the DTO location).
|
||||
if (dto.HomeRange is int homeRange && homeRange >= 0)
|
||||
// Compact homeRange -> spawnBounds (Map not available yet; use the DTO location).
|
||||
if (dto.HomeRange >= 0)
|
||||
{
|
||||
int z;
|
||||
int depth;
|
||||
if (homeRange == 0)
|
||||
{
|
||||
z = dto.Location.Z;
|
||||
depth = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
z = -128;
|
||||
depth = 256;
|
||||
}
|
||||
|
||||
SpawnBounds = new Rectangle3D(
|
||||
dto.Location.X - homeRange,
|
||||
dto.Location.Y - homeRange,
|
||||
z,
|
||||
homeRange * 2 + 1,
|
||||
homeRange * 2 + 1,
|
||||
depth
|
||||
);
|
||||
SpawnBounds = BoundsFromHomeRange(dto.Location, dto.HomeRange);
|
||||
}
|
||||
|
||||
InitSpawn(dto.Count, dto.MinDelay ?? DefaultMinDelay, dto.MaxDelay ?? DefaultMaxDelay, dto.Team ?? 0, SpawnBounds);
|
||||
InitSpawn(dto.Count, dto.MinDelay, dto.MaxDelay, dto.Team, SpawnBounds);
|
||||
|
||||
_walkingRange = dto.WalkingRange ?? -1;
|
||||
_spawnLocationIsHome = dto.SpawnLocationIsHome ?? false;
|
||||
_spawnPositionMode = dto.SpawnPositionMode ?? SpawnPositionMode.Automatic;
|
||||
_maxSpawnAttempts = dto.MaxSpawnAttempts ?? DefaultMaxSpawnAttempts;
|
||||
_walkingRange = dto.WalkingRange;
|
||||
_spawnLocationIsHome = dto.SpawnLocationIsHome;
|
||||
_spawnPositionMode = dto.SpawnPositionMode;
|
||||
_maxSpawnAttempts = dto.MaxSpawnAttempts;
|
||||
|
||||
if (dto.Entries != null)
|
||||
{
|
||||
|
|
@ -72,22 +52,63 @@ public abstract partial class BaseSpawner
|
|||
}
|
||||
}
|
||||
|
||||
// Export helpers — nullable so WhenWritingNull omits domain defaults, matching legacy ToJson.
|
||||
private protected Guid? DtoGuid => _guid;
|
||||
/// <summary>The square spawn bounds a homeRange radius represents (centered on the location).</summary>
|
||||
private protected static Rectangle3D BoundsFromHomeRange(Point3D location, int homeRange)
|
||||
{
|
||||
int z;
|
||||
int depth;
|
||||
if (homeRange == 0)
|
||||
{
|
||||
z = location.Z;
|
||||
depth = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
z = -128;
|
||||
depth = 256;
|
||||
}
|
||||
|
||||
return new Rectangle3D(
|
||||
location.X - homeRange,
|
||||
location.Y - homeRange,
|
||||
z,
|
||||
homeRange * 2 + 1,
|
||||
homeRange * 2 + 1,
|
||||
depth
|
||||
);
|
||||
}
|
||||
|
||||
// Export helpers. Options use WhenWritingDefault, so optional fields are omitted at their CLR
|
||||
// default; helpers below map non-CLR-default "omit" values onto the default so they drop out.
|
||||
private protected Guid DtoGuid => _guid;
|
||||
private protected string DtoName => string.IsNullOrEmpty(Name) ? null : Name;
|
||||
private protected TimeSpan? DtoMinDelay => _minDelay == DefaultMinDelay ? null : _minDelay;
|
||||
private protected TimeSpan? DtoMaxDelay => _maxDelay == DefaultMaxDelay ? null : _maxDelay;
|
||||
private protected int? DtoTeam => _team == 0 ? null : _team;
|
||||
private protected int? DtoWalkingRange => _walkingRange != 0 ? WalkingRange : null;
|
||||
private protected bool? DtoSpawnLocationIsHome => _spawnLocationIsHome ? true : null;
|
||||
private protected TimeSpan DtoMinDelay => _minDelay;
|
||||
private protected TimeSpan DtoMaxDelay => _maxDelay;
|
||||
private protected int DtoTeam => _team;
|
||||
private protected int DtoWalkingRange => _walkingRange;
|
||||
private protected bool DtoSpawnLocationIsHome => _spawnLocationIsHome;
|
||||
|
||||
private protected SpawnPositionMode? DtoSpawnPositionMode =>
|
||||
_spawnPositionMode is not SpawnPositionMode.Automatic and not SpawnPositionMode.Abandoned
|
||||
? _spawnPositionMode
|
||||
: null;
|
||||
// Abandoned is a transient "gave up" runtime state, not persisted -> map to Automatic (omitted).
|
||||
private protected SpawnPositionMode DtoSpawnPositionMode =>
|
||||
_spawnPositionMode == SpawnPositionMode.Abandoned ? SpawnPositionMode.Automatic : _spawnPositionMode;
|
||||
|
||||
// Runtime treats 0 identically to DefaultMaxSpawnAttempts (BaseSpawner.cs maxAttempts clamp),
|
||||
// so both are omitted — fresh spawners have _maxSpawnAttempts == 0.
|
||||
private protected int? DtoMaxSpawnAttempts =>
|
||||
_maxSpawnAttempts > 0 && _maxSpawnAttempts != DefaultMaxSpawnAttempts ? _maxSpawnAttempts : null;
|
||||
// Runtime treats 0 identically to DefaultMaxSpawnAttempts(10) -> map the default to 0 (omitted).
|
||||
private protected int DtoMaxSpawnAttempts =>
|
||||
_maxSpawnAttempts == DefaultMaxSpawnAttempts ? 0 : _maxSpawnAttempts;
|
||||
|
||||
// The homeRange radius if SpawnBounds is EXACTLY what that radius reconstructs (square, centered,
|
||||
// standard z/depth) so the round-trip is lossless; otherwise -1 (write spawnBounds instead).
|
||||
private protected int DtoHomeRange
|
||||
{
|
||||
get
|
||||
{
|
||||
if (SpawnBounds == default || !IsHomeRangeStyle)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
var homeRange = HomeRange;
|
||||
return SpawnBounds == BoundsFromHomeRange(Location, homeRange) ? homeRange : -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,25 +24,51 @@ namespace Server.Engines.Spawners;
|
|||
/// <summary>
|
||||
/// Plain data carrier for spawner JSON. System.Text.Json deserializes into these records
|
||||
/// (never a live Item), so a malformed file fails as GC only. <see cref="ToSpawner"/> builds
|
||||
/// the real spawner from a fully-validated DTO. Sparse fields are nullable so WhenWritingNull
|
||||
/// omits domain defaults, matching the legacy ToJson output.
|
||||
/// the real spawner from a fully-validated DTO. No nullable types: the serializer options use
|
||||
/// <c>WhenWritingDefault</c> so optional fields are omitted at their default, mandatory fields
|
||||
/// are force-written with <c>[JsonIgnore(Condition = Never)]</c>, and fields whose "omit" value
|
||||
/// is not the CLR default (maxSpawnAttempts, spawnPositionMode) are mapped to the default in
|
||||
/// the producing ToDto. <c>[JsonPropertyOrder]</c> preserves the on-disk field order.
|
||||
/// </summary>
|
||||
public abstract record SpawnerDto
|
||||
{
|
||||
[JsonPropertyName("guid")] public Guid? Guid { get; init; }
|
||||
[JsonPropertyName("location")] public Point3D Location { get; init; }
|
||||
[JsonPropertyName("map")] public Map Map { get; init; }
|
||||
[JsonPropertyName("count")] public int Count { get; init; }
|
||||
[JsonPropertyName("name")] public string Name { get; init; }
|
||||
[JsonPropertyName("minDelay")] public TimeSpan? MinDelay { get; init; }
|
||||
[JsonPropertyName("maxDelay")] public TimeSpan? MaxDelay { get; init; }
|
||||
[JsonPropertyName("team")] public int? Team { get; init; }
|
||||
[JsonPropertyName("walkingRange")] public int? WalkingRange { get; init; }
|
||||
[JsonPropertyName("homeRange")] public int? HomeRange { get; init; } // legacy read; never written
|
||||
[JsonPropertyName("spawnLocationIsHome")] public bool? SpawnLocationIsHome { get; init; }
|
||||
[JsonPropertyName("spawnPositionMode")] public SpawnPositionMode? SpawnPositionMode { get; init; }
|
||||
[JsonPropertyName("maxSpawnAttempts")] public int? MaxSpawnAttempts { get; init; }
|
||||
[JsonPropertyName("entries")] public List<SpawnerEntry> Entries { get; init; }
|
||||
// --- Mandatory: always written (overrides the options' WhenWritingDefault) ---
|
||||
[JsonPropertyName("guid")][JsonPropertyOrder(0)][JsonIgnore(Condition = JsonIgnoreCondition.Never)]
|
||||
public Guid Guid { get; init; }
|
||||
|
||||
[JsonPropertyName("name")][JsonPropertyOrder(1)]
|
||||
public string Name { get; init; }
|
||||
|
||||
[JsonPropertyName("location")][JsonPropertyOrder(2)][JsonIgnore(Condition = JsonIgnoreCondition.Never)]
|
||||
public Point3D Location { get; init; }
|
||||
|
||||
[JsonPropertyName("map")][JsonPropertyOrder(3)][JsonIgnore(Condition = JsonIgnoreCondition.Never)]
|
||||
public Map Map { get; init; }
|
||||
|
||||
[JsonPropertyName("count")][JsonPropertyOrder(4)][JsonIgnore(Condition = JsonIgnoreCondition.Never)]
|
||||
public int Count { get; init; }
|
||||
|
||||
[JsonPropertyName("minDelay")][JsonPropertyOrder(5)][JsonIgnore(Condition = JsonIgnoreCondition.Never)]
|
||||
public TimeSpan MinDelay { get; init; }
|
||||
|
||||
[JsonPropertyName("maxDelay")][JsonPropertyOrder(6)][JsonIgnore(Condition = JsonIgnoreCondition.Never)]
|
||||
public TimeSpan MaxDelay { get; init; }
|
||||
|
||||
// --- Optional: omitted at default via WhenWritingDefault (spawnBounds/region are on subtypes, order 8) ---
|
||||
[JsonPropertyName("team")][JsonPropertyOrder(7)] public int Team { get; init; }
|
||||
[JsonPropertyName("walkingRange")][JsonPropertyOrder(9)] public int WalkingRange { get; init; }
|
||||
|
||||
[JsonPropertyName("entries")][JsonPropertyOrder(10)][JsonIgnore(Condition = JsonIgnoreCondition.Never)]
|
||||
public List<SpawnerEntry> Entries { get; init; }
|
||||
|
||||
[JsonPropertyName("spawnLocationIsHome")][JsonPropertyOrder(11)] public bool SpawnLocationIsHome { get; init; }
|
||||
[JsonPropertyName("spawnPositionMode")][JsonPropertyOrder(12)] public SpawnPositionMode SpawnPositionMode { get; init; }
|
||||
[JsonPropertyName("maxSpawnAttempts")][JsonPropertyOrder(13)] public int MaxSpawnAttempts { get; init; }
|
||||
|
||||
// Compact square-bounds form. -1 = "not a homeRange square" (use spawnBounds instead). Written
|
||||
// only when >= 0 (SpawnerJsonSerializer sets ShouldSerialize: hr >= 0, since 0 is a valid radius
|
||||
// that WhenWritingDefault cannot emit). On read it reconstructs SpawnBounds in ApplyDto.
|
||||
[JsonPropertyName("homeRange")][JsonPropertyOrder(8)] public int HomeRange { get; init; } = -1;
|
||||
|
||||
/// <summary>Constructs the empty concrete spawner Item for this DTO.</summary>
|
||||
protected abstract BaseSpawner CreateEmpty();
|
||||
|
|
@ -69,7 +95,7 @@ public abstract record SpawnerDto
|
|||
[JsonDiscoverableType("Spawner")]
|
||||
public sealed record SpawnerDataDto : SpawnerDto
|
||||
{
|
||||
[JsonPropertyName("spawnBounds")] public Rectangle3D? SpawnBounds { get; init; }
|
||||
[JsonPropertyName("spawnBounds")][JsonPropertyOrder(8)] public Rectangle3D SpawnBounds { get; init; }
|
||||
|
||||
protected override BaseSpawner CreateEmpty() => new Spawner();
|
||||
|
||||
|
|
@ -78,9 +104,9 @@ public sealed record SpawnerDataDto : SpawnerDto
|
|||
var spawner = (Spawner)base.ToSpawner();
|
||||
try
|
||||
{
|
||||
if (SpawnBounds is { } bounds && bounds != default)
|
||||
if (SpawnBounds != default)
|
||||
{
|
||||
spawner.SpawnBounds = bounds;
|
||||
spawner.SpawnBounds = SpawnBounds;
|
||||
}
|
||||
|
||||
return spawner;
|
||||
|
|
@ -96,7 +122,7 @@ public sealed record SpawnerDataDto : SpawnerDto
|
|||
[JsonDiscoverableType("RegionSpawner")]
|
||||
public sealed record RegionSpawnerDto : SpawnerDto
|
||||
{
|
||||
[JsonPropertyName("region")] public string Region { get; init; }
|
||||
[JsonPropertyName("region")][JsonPropertyOrder(8)] public string Region { get; init; }
|
||||
|
||||
protected override BaseSpawner CreateEmpty() => new RegionSpawner();
|
||||
|
||||
|
|
@ -119,10 +145,10 @@ public sealed record RegionSpawnerDto : SpawnerDto
|
|||
[JsonDiscoverableType("ProximitySpawner")]
|
||||
public sealed record ProximitySpawnerDto : SpawnerDto
|
||||
{
|
||||
[JsonPropertyName("spawnBounds")] public Rectangle3D? SpawnBounds { get; init; }
|
||||
[JsonPropertyName("triggerRange")] public int TriggerRange { get; init; }
|
||||
[JsonPropertyName("spawnMessage")] public TextDefinition SpawnMessage { get; init; }
|
||||
[JsonPropertyName("instant")] public bool Instant { get; init; }
|
||||
[JsonPropertyName("spawnBounds")][JsonPropertyOrder(8)] public Rectangle3D SpawnBounds { get; init; }
|
||||
[JsonPropertyName("triggerRange")][JsonPropertyOrder(14)] public int TriggerRange { get; init; }
|
||||
[JsonPropertyName("spawnMessage")][JsonPropertyOrder(15)] public TextDefinition SpawnMessage { get; init; }
|
||||
[JsonPropertyName("instant")][JsonPropertyOrder(16)] public bool Instant { get; init; }
|
||||
|
||||
protected override BaseSpawner CreateEmpty() => new ProximitySpawner();
|
||||
|
||||
|
|
@ -131,9 +157,9 @@ public sealed record ProximitySpawnerDto : SpawnerDto
|
|||
var spawner = (ProximitySpawner)base.ToSpawner();
|
||||
try
|
||||
{
|
||||
if (SpawnBounds is { } bounds && bounds != default)
|
||||
if (SpawnBounds != default)
|
||||
{
|
||||
spawner.SpawnBounds = bounds;
|
||||
spawner.SpawnBounds = SpawnBounds;
|
||||
}
|
||||
|
||||
spawner.TriggerRange = TriggerRange;
|
||||
|
|
|
|||
|
|
@ -17,24 +17,29 @@ namespace Server.Engines.Spawners;
|
|||
|
||||
public partial class ProximitySpawner
|
||||
{
|
||||
public override SpawnerDto ToDto() => new ProximitySpawnerDto
|
||||
public override SpawnerDto ToDto()
|
||||
{
|
||||
Guid = DtoGuid,
|
||||
Location = Location,
|
||||
Map = Map,
|
||||
Count = Count,
|
||||
Name = DtoName,
|
||||
MinDelay = DtoMinDelay,
|
||||
MaxDelay = DtoMaxDelay,
|
||||
Team = DtoTeam,
|
||||
WalkingRange = DtoWalkingRange,
|
||||
SpawnLocationIsHome = DtoSpawnLocationIsHome,
|
||||
SpawnPositionMode = DtoSpawnPositionMode,
|
||||
MaxSpawnAttempts = DtoMaxSpawnAttempts,
|
||||
Entries = Entries,
|
||||
SpawnBounds = SpawnBounds == default ? null : SpawnBounds,
|
||||
TriggerRange = TriggerRange,
|
||||
SpawnMessage = SpawnMessage,
|
||||
Instant = InstantFlag
|
||||
};
|
||||
var homeRange = DtoHomeRange;
|
||||
return new ProximitySpawnerDto
|
||||
{
|
||||
Guid = DtoGuid,
|
||||
Name = DtoName,
|
||||
Location = Location,
|
||||
Map = Map,
|
||||
Count = Count,
|
||||
MinDelay = DtoMinDelay,
|
||||
MaxDelay = DtoMaxDelay,
|
||||
Team = DtoTeam,
|
||||
WalkingRange = DtoWalkingRange,
|
||||
Entries = Entries,
|
||||
SpawnLocationIsHome = DtoSpawnLocationIsHome,
|
||||
SpawnPositionMode = DtoSpawnPositionMode,
|
||||
MaxSpawnAttempts = DtoMaxSpawnAttempts,
|
||||
HomeRange = homeRange,
|
||||
SpawnBounds = homeRange >= 0 ? default : SpawnBounds,
|
||||
TriggerRange = TriggerRange,
|
||||
SpawnMessage = SpawnMessage,
|
||||
Instant = InstantFlag
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,21 +17,22 @@ namespace Server.Engines.Spawners;
|
|||
|
||||
public partial class RegionSpawner
|
||||
{
|
||||
// RegionSpawner spawns from region rectangles, so it writes neither homeRange nor spawnBounds.
|
||||
public override SpawnerDto ToDto() => new RegionSpawnerDto
|
||||
{
|
||||
Guid = DtoGuid,
|
||||
Name = DtoName,
|
||||
Location = Location,
|
||||
Map = Map,
|
||||
Count = Count,
|
||||
Name = DtoName,
|
||||
MinDelay = DtoMinDelay,
|
||||
MaxDelay = DtoMaxDelay,
|
||||
Team = DtoTeam,
|
||||
WalkingRange = DtoWalkingRange,
|
||||
Entries = Entries,
|
||||
SpawnLocationIsHome = DtoSpawnLocationIsHome,
|
||||
SpawnPositionMode = DtoSpawnPositionMode,
|
||||
MaxSpawnAttempts = DtoMaxSpawnAttempts,
|
||||
Entries = Entries,
|
||||
Region = SpawnRegion?.Name
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,21 +17,26 @@ namespace Server.Engines.Spawners;
|
|||
|
||||
public partial class Spawner
|
||||
{
|
||||
public override SpawnerDto ToDto() => new SpawnerDataDto
|
||||
public override SpawnerDto ToDto()
|
||||
{
|
||||
Guid = DtoGuid,
|
||||
Location = Location,
|
||||
Map = Map,
|
||||
Count = Count,
|
||||
Name = DtoName,
|
||||
MinDelay = DtoMinDelay,
|
||||
MaxDelay = DtoMaxDelay,
|
||||
Team = DtoTeam,
|
||||
WalkingRange = DtoWalkingRange,
|
||||
SpawnLocationIsHome = DtoSpawnLocationIsHome,
|
||||
SpawnPositionMode = DtoSpawnPositionMode,
|
||||
MaxSpawnAttempts = DtoMaxSpawnAttempts,
|
||||
Entries = Entries,
|
||||
SpawnBounds = SpawnBounds == default ? null : SpawnBounds
|
||||
};
|
||||
var homeRange = DtoHomeRange;
|
||||
return new SpawnerDataDto
|
||||
{
|
||||
Guid = DtoGuid,
|
||||
Name = DtoName,
|
||||
Location = Location,
|
||||
Map = Map,
|
||||
Count = Count,
|
||||
MinDelay = DtoMinDelay,
|
||||
MaxDelay = DtoMaxDelay,
|
||||
Team = DtoTeam,
|
||||
WalkingRange = DtoWalkingRange,
|
||||
Entries = Entries,
|
||||
SpawnLocationIsHome = DtoSpawnLocationIsHome,
|
||||
SpawnPositionMode = DtoSpawnPositionMode,
|
||||
MaxSpawnAttempts = DtoMaxSpawnAttempts,
|
||||
HomeRange = homeRange,
|
||||
SpawnBounds = homeRange >= 0 ? default : SpawnBounds
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,11 +122,15 @@ public static class SpawnerJsonSerializer
|
|||
public static JsonSerializerOptions Options =>
|
||||
_options ??= new JsonSerializerOptions(JsonConfig.GetOptions(new TextDefinitionConverterFactory()))
|
||||
{
|
||||
// Optional DTO fields are omitted at their CLR default; mandatory fields force-write with
|
||||
// [JsonIgnore(Condition = Never)]. (homeRange is special-cased below since 0 is valid.)
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault,
|
||||
TypeInfoResolver = new DefaultJsonTypeInfoResolver
|
||||
{
|
||||
Modifiers =
|
||||
{
|
||||
AddPolymorphism
|
||||
AddPolymorphism,
|
||||
ConfigureHomeRange
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -144,4 +148,22 @@ public static class SpawnerJsonSerializer
|
|||
typeInfo.PolymorphismOptions.DerivedTypes.Add(_derivedTypes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// homeRange uses -1 as the "absent" sentinel (a real radius is >= 0, and 0 is a valid radius that
|
||||
// WhenWritingDefault could not emit). Write it only when it represents a real homeRange square.
|
||||
private static void ConfigureHomeRange(JsonTypeInfo typeInfo)
|
||||
{
|
||||
if (!typeInfo.Type.IsAssignableTo(typeof(SpawnerDto)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < typeInfo.Properties.Count; i++)
|
||||
{
|
||||
if (typeInfo.Properties[i].Name == "homeRange")
|
||||
{
|
||||
typeInfo.Properties[i].ShouldSerialize = static (_, value) => value is int hr && hr >= 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue