refactor(spawners): Approach A→B swap — deserialize via SpawnerDto records (atomic)
Replace the shadow-property (Approach A) JSON mechanism with plain DTO records
so malformed files fail as GC-only and never leak live world Items.
Changes:
- Add Json/SpawnerDto.cs: abstract SpawnerDto + SpawnerDataDto / RegionSpawnerDto /
ProximitySpawnerDto, each [JsonDiscoverableType("<name>")] with ToSpawner()
- Add BaseSpawner.Dto.cs: internal ApplyDto(SpawnerDto) + private-protected Dto*
export helpers (DtoGuid, DtoMinDelay, etc.)
- Add Spawner.Dto.cs, RegionSpawner.Dto.cs, ProximitySpawner.Dto.cs: ToDto() overrides
- Add public abstract SpawnerDto ToDto() to BaseSpawner
- Retarget SpawnerJsonSerializer: discovery filter BaseSpawner→SpawnerDto; polymorphism
gate typeof(SpawnerDto); remove PruneToJsonProperties + AddOnDeserialized modifiers
- Delete BaseSpawner.Json.cs, Spawner.Json.cs, RegionSpawner.Json.cs,
ProximitySpawner.Json.cs (Approach A shadow-property partials)
- Remove [JsonDiscoverableType] + [JsonConstructor] from Spawner/RegionSpawner/ProximitySpawner
- Rewire ExportSpawnersCommand: build List<SpawnerDto> via spawner.ToDto()
- Rewire ImportSpawnersCommand: Deserialize<List<SpawnerDto>> then dto.ToSpawner();
map-null check now fires before ToSpawner(), so no orphan Items on bad map
- Delete 3 Approach-A round-trip tests; add SpawnerDtoRoundTripTests (4 cases)
- Update ExportImportFileTests + LegacyHomeRangeTests to use DTO path
- Add Import_MalformedFile_LeaksNoWorldItems to ImportCleanupTests
Build: dotnet build ModernUO.slnx → 0 errors, 0 warnings
Tests: dotnet test UOContent.Tests → 478/478 pass
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a9d61ccd35
commit
bf31bceea4
20 changed files with 486 additions and 534 deletions
|
|
@ -14,27 +14,29 @@ public class ExportImportFileTests
|
|||
[Fact]
|
||||
public void Serialize_ThenDeserialize_File_PreservesSpawner()
|
||||
{
|
||||
var spawner = new Spawner(3, TimeSpan.FromMinutes(4), TimeSpan.FromMinutes(8), 1,
|
||||
new Rectangle3D(200, 200, 0, 9, 9, 0), "Tanner");
|
||||
spawner.MoveToWorld(new Point3D(204, 204, 0), Map.Felucca);
|
||||
|
||||
Spawner original = null;
|
||||
BaseSpawner rebuilt = null;
|
||||
var path = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
JsonConfig.Serialize(path, new List<BaseSpawner> { spawner }, SpawnerJsonSerializer.Options);
|
||||
original = new Spawner(3, TimeSpan.FromMinutes(4), TimeSpan.FromMinutes(8), 1,
|
||||
new Rectangle3D(200, 200, 0, 9, 9, 0), "Tanner");
|
||||
original.MoveToWorld(new Point3D(204, 204, 0), Map.Felucca);
|
||||
|
||||
var loaded = JsonConfig.Deserialize<List<BaseSpawner>>(path, SpawnerJsonSerializer.Options);
|
||||
var s = Assert.IsType<Spawner>(Assert.Single(loaded));
|
||||
JsonConfig.Serialize(path, new List<SpawnerDto> { original.ToDto() }, SpawnerJsonSerializer.Options);
|
||||
|
||||
var dtos = JsonConfig.Deserialize<List<SpawnerDto>>(path, SpawnerJsonSerializer.Options);
|
||||
rebuilt = Assert.Single(dtos).ToSpawner();
|
||||
var s = Assert.IsType<Spawner>(rebuilt);
|
||||
Assert.Equal(3, s.Count);
|
||||
Assert.Equal(1, s.Team);
|
||||
Assert.Equal(new Rectangle3D(200, 200, 0, 9, 9, 0), s.SpawnBounds);
|
||||
|
||||
s.Delete();
|
||||
}
|
||||
finally
|
||||
{
|
||||
rebuilt?.Delete();
|
||||
original?.Delete();
|
||||
File.Delete(path);
|
||||
spawner?.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,6 +98,43 @@ public class ImportCleanupTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Import_MalformedFile_LeaksNoWorldItems()
|
||||
{
|
||||
// First entry is syntactically valid JSON for a Spawner, second entry is truncated/garbage.
|
||||
// STJ throws a JsonException during array deserialization before any Items are created
|
||||
// (the DTO path is GC-only), so zero new spawners should appear at the valid entry's location.
|
||||
var dir = Path.Combine(Path.GetTempPath(), "muo-import-malformed-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(dir);
|
||||
File.WriteAllText(Path.Combine(dir, "bad.json"), """
|
||||
[ { "$type": "Spawner", "location": [320, 320, 0], "map": "Felucca", "count": 1,
|
||||
"entries": [ { "name": "Fisherman", "maxCount": 1, "probability": 100 } ] },
|
||||
{ "$type": "Spawner", "location": [ THIS IS NOT JSON
|
||||
""");
|
||||
try
|
||||
{
|
||||
var before = CountSpawnersNear(new Point3D(320, 320, 0));
|
||||
ImportSpawnersCommand.ImportFile(new FileInfo(Path.Combine(dir, "bad.json")), new Dictionary<Guid, ISpawner>());
|
||||
// Malformed parse must construct zero Items (DTO is GC-only); nothing placed or orphaned.
|
||||
Assert.Equal(before, CountSpawnersNear(new Point3D(320, 320, 0)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(dir, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static int CountSpawnersNear(Point3D p)
|
||||
{
|
||||
var n = 0;
|
||||
foreach (var _ in Map.Felucca.GetItemsAt<BaseSpawner>(p))
|
||||
{
|
||||
n++;
|
||||
}
|
||||
|
||||
return n;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Import_DuplicateLocation_ReplacesExistingSpawner()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -26,13 +26,19 @@ public class LegacyHomeRangeTests
|
|||
]
|
||||
""";
|
||||
|
||||
var rt = JsonSerializer.Deserialize<List<BaseSpawner>>(legacy, SpawnerJsonSerializer.Options);
|
||||
var s = Assert.IsType<Spawner>(Assert.Single(rt));
|
||||
var dtos = JsonSerializer.Deserialize<List<SpawnerDto>>(legacy, SpawnerJsonSerializer.Options);
|
||||
var dto = Assert.Single(dtos);
|
||||
var s = Assert.IsType<Spawner>(dto.ToSpawner());
|
||||
|
||||
// homeRange 3 -> Rectangle3D(100-3, 200-3, -128, 7, 7, 256)
|
||||
Assert.Equal(new Rectangle3D(97, 197, -128, 7, 7, 256), s.SpawnBounds);
|
||||
|
||||
s.Delete();
|
||||
try
|
||||
{
|
||||
// homeRange 3 -> Rectangle3D(100-3, 200-3, -128, 7, 7, 256)
|
||||
Assert.Equal(new Rectangle3D(97, 197, -128, 7, 7, 256), s.SpawnBounds);
|
||||
}
|
||||
finally
|
||||
{
|
||||
s.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -51,12 +57,18 @@ public class LegacyHomeRangeTests
|
|||
]
|
||||
""";
|
||||
|
||||
var rt = JsonSerializer.Deserialize<List<BaseSpawner>>(legacy, SpawnerJsonSerializer.Options);
|
||||
var s = Assert.IsType<Spawner>(Assert.Single(rt));
|
||||
var dtos = JsonSerializer.Deserialize<List<SpawnerDto>>(legacy, SpawnerJsonSerializer.Options);
|
||||
var dto = Assert.Single(dtos);
|
||||
var s = Assert.IsType<Spawner>(dto.ToSpawner());
|
||||
|
||||
// homeRange 0 -> Rectangle3D(100, 200, 5, 1, 1, 0)
|
||||
Assert.Equal(new Rectangle3D(100, 200, 5, 1, 1, 0), s.SpawnBounds);
|
||||
|
||||
s.Delete();
|
||||
try
|
||||
{
|
||||
// homeRange 0 -> Rectangle3D(100, 200, 5, 1, 1, 0)
|
||||
Assert.Equal(new Rectangle3D(100, 200, 5, 1, 1, 0), s.SpawnBounds);
|
||||
}
|
||||
finally
|
||||
{
|
||||
s.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using Server;
|
||||
using Server.Engines.Spawners;
|
||||
using Xunit;
|
||||
|
||||
namespace UOContent.Tests.Engines.Spawners.Json;
|
||||
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class ProximitySpawnerRoundTripTests
|
||||
{
|
||||
[Fact]
|
||||
public void ProximitySpawner_RoundTrips_ProximityFields()
|
||||
{
|
||||
ProximitySpawner spawner = null;
|
||||
ProximitySpawner s = null;
|
||||
try
|
||||
{
|
||||
spawner = new ProximitySpawner("Fisherman") { TriggerRange = 4, InstantFlag = true };
|
||||
spawner.SpawnMessage = 500000;
|
||||
spawner.MoveToWorld(new Point3D(120, 120, 0), Map.Felucca);
|
||||
|
||||
var json = JsonSerializer.Serialize<List<BaseSpawner>>(
|
||||
new List<BaseSpawner> { spawner }, SpawnerJsonSerializer.Options);
|
||||
Assert.Contains("\"$type\": \"ProximitySpawner\"", json);
|
||||
Assert.Contains("\"triggerRange\": 4", json);
|
||||
Assert.Contains("\"instant\": true", json);
|
||||
|
||||
var rt = JsonSerializer.Deserialize<List<BaseSpawner>>(json, SpawnerJsonSerializer.Options);
|
||||
s = Assert.IsType<ProximitySpawner>(Assert.Single(rt));
|
||||
Assert.Equal(4, s.TriggerRange);
|
||||
Assert.True(s.InstantFlag);
|
||||
Assert.Equal(500000, s.SpawnMessage.Number);
|
||||
}
|
||||
finally
|
||||
{
|
||||
s?.Delete();
|
||||
spawner?.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using Server;
|
||||
using Server.Engines.Spawners;
|
||||
using Server.Regions;
|
||||
using Xunit;
|
||||
|
||||
namespace UOContent.Tests.Engines.Spawners.Json;
|
||||
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class RegionSpawnerRoundTripTests
|
||||
{
|
||||
[Fact]
|
||||
public void RegionSpawner_RoundTrips_RegionByName()
|
||||
{
|
||||
// The test environment does not load game regions (no AssemblyHandler.Invoke("Initialize")),
|
||||
// so we create and register a test BaseRegion on Felucca directly.
|
||||
var region = new BaseRegion(
|
||||
"TestSpawnRegion",
|
||||
Map.Felucca,
|
||||
50,
|
||||
new Rectangle3D(1400, 1670, -128, 40, 40, 256)
|
||||
);
|
||||
region.Register();
|
||||
|
||||
RegionSpawner spawner = null;
|
||||
RegionSpawner s = null;
|
||||
try
|
||||
{
|
||||
spawner = new RegionSpawner("Fisherman") { SpawnRegion = region };
|
||||
spawner.MoveToWorld(new Point3D(1416, 1683, 0), Map.Felucca);
|
||||
|
||||
var json = JsonSerializer.Serialize<List<BaseSpawner>>(
|
||||
new List<BaseSpawner> { spawner }, SpawnerJsonSerializer.Options);
|
||||
Assert.Contains("\"$type\": \"RegionSpawner\"", json);
|
||||
Assert.Contains(region.Name, json);
|
||||
|
||||
var rt = JsonSerializer.Deserialize<List<BaseSpawner>>(json, SpawnerJsonSerializer.Options);
|
||||
s = Assert.IsType<RegionSpawner>(Assert.Single(rt));
|
||||
Assert.Equal(region.Name, s.SpawnRegion?.Name);
|
||||
}
|
||||
finally
|
||||
{
|
||||
s?.Delete();
|
||||
spawner?.Delete();
|
||||
region.Unregister();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using Server;
|
||||
using Server.Engines.Spawners;
|
||||
using Server.Regions;
|
||||
using Xunit;
|
||||
|
||||
namespace UOContent.Tests.Engines.Spawners.Json;
|
||||
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class SpawnerDtoRoundTripTests
|
||||
{
|
||||
[Fact]
|
||||
public void Spawner_RoundTrips_ThroughDto()
|
||||
{
|
||||
Spawner original = null;
|
||||
BaseSpawner rebuilt = null;
|
||||
try
|
||||
{
|
||||
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);
|
||||
|
||||
var json = JsonSerializer.Serialize(new List<SpawnerDto> { original.ToDto() }, SpawnerJsonSerializer.Options);
|
||||
Assert.Contains("\"$type\": \"Spawner\"", json);
|
||||
Assert.Contains("\"count\": 2", json);
|
||||
|
||||
var dtos = JsonSerializer.Deserialize<List<SpawnerDto>>(json, SpawnerJsonSerializer.Options);
|
||||
rebuilt = Assert.Single(dtos).ToSpawner();
|
||||
var s = Assert.IsType<Spawner>(rebuilt);
|
||||
Assert.Equal(2, s.Count);
|
||||
Assert.Equal(TimeSpan.FromMinutes(3), s.MinDelay);
|
||||
Assert.Equal(new Rectangle3D(100, 100, 0, 5, 5, 0), s.SpawnBounds);
|
||||
Assert.Equal("Fisherman", Assert.Single(s.Entries).SpawnedName);
|
||||
}
|
||||
finally
|
||||
{
|
||||
rebuilt?.Delete();
|
||||
original?.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spawner_OmitsDomainDefaults()
|
||||
{
|
||||
Spawner original = null;
|
||||
try
|
||||
{
|
||||
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);
|
||||
Assert.DoesNotContain("\"team\"", json);
|
||||
Assert.DoesNotContain("maxSpawnAttempts", json);
|
||||
}
|
||||
finally
|
||||
{
|
||||
original?.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegionSpawner_RoundTrips_RegionByName()
|
||||
{
|
||||
var region = new BaseRegion("DtoTestRegion", Map.Felucca, 50, new Rectangle3D(1400, 1670, 0, 40, 40, 0));
|
||||
region.Register();
|
||||
RegionSpawner original = null;
|
||||
BaseSpawner rebuilt = null;
|
||||
try
|
||||
{
|
||||
original = new RegionSpawner("Fisherman") { SpawnRegion = region };
|
||||
original.MoveToWorld(new Point3D(1416, 1683, 0), Map.Felucca);
|
||||
var json = JsonSerializer.Serialize(new List<SpawnerDto> { original.ToDto() }, SpawnerJsonSerializer.Options);
|
||||
Assert.Contains("\"$type\": \"RegionSpawner\"", json);
|
||||
Assert.Contains("DtoTestRegion", json);
|
||||
|
||||
var dtos = JsonSerializer.Deserialize<List<SpawnerDto>>(json, SpawnerJsonSerializer.Options);
|
||||
rebuilt = Assert.Single(dtos).ToSpawner();
|
||||
Assert.Equal("DtoTestRegion", Assert.IsType<RegionSpawner>(rebuilt).SpawnRegion?.Name);
|
||||
}
|
||||
finally
|
||||
{
|
||||
rebuilt?.Delete();
|
||||
original?.Delete();
|
||||
region.Unregister();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProximitySpawner_RoundTrips_Fields()
|
||||
{
|
||||
ProximitySpawner original = null;
|
||||
BaseSpawner rebuilt = null;
|
||||
try
|
||||
{
|
||||
original = new ProximitySpawner("Fisherman") { TriggerRange = 4, InstantFlag = true, SpawnMessage = 500000 };
|
||||
original.MoveToWorld(new Point3D(120, 120, 0), Map.Felucca);
|
||||
var json = JsonSerializer.Serialize(new List<SpawnerDto> { original.ToDto() }, SpawnerJsonSerializer.Options);
|
||||
Assert.Contains("\"$type\": \"ProximitySpawner\"", json);
|
||||
Assert.Contains("\"triggerRange\": 4", json);
|
||||
Assert.Contains("\"instant\": true", json);
|
||||
|
||||
var dtos = JsonSerializer.Deserialize<List<SpawnerDto>>(json, SpawnerJsonSerializer.Options);
|
||||
rebuilt = Assert.Single(dtos).ToSpawner();
|
||||
var p = Assert.IsType<ProximitySpawner>(rebuilt);
|
||||
Assert.Equal(4, p.TriggerRange);
|
||||
Assert.True(p.InstantFlag);
|
||||
Assert.Equal(500000, p.SpawnMessage.Number);
|
||||
}
|
||||
finally
|
||||
{
|
||||
rebuilt?.Delete();
|
||||
original?.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using Server;
|
||||
using Server.Engines.Spawners;
|
||||
using Xunit;
|
||||
|
||||
namespace UOContent.Tests.Engines.Spawners.Json;
|
||||
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class SpawnerRoundTripTests
|
||||
{
|
||||
private static Map Map => Map.Felucca;
|
||||
|
||||
[Fact]
|
||||
public void Spawner_RoundTrips_TypeAndCoreFields()
|
||||
{
|
||||
var spawner = new Spawner(2, TimeSpan.FromMinutes(3), TimeSpan.FromMinutes(7), 0,
|
||||
new Rectangle3D(100, 100, 0, 5, 5, 0), "Fisherman");
|
||||
spawner.MoveToWorld(new Point3D(105, 105, 0), Map);
|
||||
|
||||
var json = JsonSerializer.Serialize<List<BaseSpawner>>(
|
||||
new List<BaseSpawner> { spawner }, SpawnerJsonSerializer.Options);
|
||||
|
||||
Assert.Contains("\"$type\": \"Spawner\"", json);
|
||||
Assert.Contains("\"count\": 2", json);
|
||||
|
||||
var roundTripped = JsonSerializer.Deserialize<List<BaseSpawner>>(json, SpawnerJsonSerializer.Options);
|
||||
var s = Assert.IsType<Spawner>(Assert.Single(roundTripped));
|
||||
Assert.Equal(2, s.Count);
|
||||
Assert.Equal(TimeSpan.FromMinutes(3), s.MinDelay);
|
||||
Assert.Equal(TimeSpan.FromMinutes(7), s.MaxDelay);
|
||||
Assert.Equal(new Rectangle3D(100, 100, 0, 5, 5, 0), s.SpawnBounds);
|
||||
Assert.Single(s.Entries);
|
||||
Assert.Equal("Fisherman", s.Entries[0].SpawnedName);
|
||||
|
||||
s.Delete();
|
||||
spawner.Delete();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Spawner_OmitsDomainDefaults()
|
||||
{
|
||||
// Default delays (5/10 min), team 0, default maxSpawnAttempts → omitted.
|
||||
var spawner = new Spawner("Fisherman");
|
||||
spawner.MoveToWorld(new Point3D(110, 110, 0), Map);
|
||||
|
||||
var json = JsonSerializer.Serialize<List<BaseSpawner>>(
|
||||
new List<BaseSpawner> { spawner }, SpawnerJsonSerializer.Options);
|
||||
|
||||
Assert.DoesNotContain("minDelay", json);
|
||||
Assert.DoesNotContain("maxDelay", json);
|
||||
Assert.DoesNotContain("\"team\"", json);
|
||||
Assert.DoesNotContain("maxSpawnAttempts", json);
|
||||
|
||||
spawner.Delete();
|
||||
}
|
||||
}
|
||||
93
Projects/UOContent/Engines/Spawners/BaseSpawner.Dto.cs
Normal file
93
Projects/UOContent/Engines/Spawners/BaseSpawner.Dto.cs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
/*************************************************************************
|
||||
* ModernUO *
|
||||
* Copyright 2019-2026 - ModernUO Development Team *
|
||||
* Email: hi@modernuo.com *
|
||||
* File: BaseSpawner.Dto.cs *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
*************************************************************************/
|
||||
|
||||
using System;
|
||||
|
||||
namespace Server.Engines.Spawners;
|
||||
|
||||
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();
|
||||
|
||||
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)
|
||||
{
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
InitSpawn(dto.Count, dto.MinDelay ?? DefaultMinDelay, dto.MaxDelay ?? DefaultMaxDelay, dto.Team ?? 0, SpawnBounds);
|
||||
|
||||
_walkingRange = dto.WalkingRange ?? -1;
|
||||
_spawnLocationIsHome = dto.SpawnLocationIsHome ?? false;
|
||||
_spawnPositionMode = dto.SpawnPositionMode ?? SpawnPositionMode.Automatic;
|
||||
_maxSpawnAttempts = dto.MaxSpawnAttempts ?? DefaultMaxSpawnAttempts;
|
||||
|
||||
if (dto.Entries != null)
|
||||
{
|
||||
for (var i = 0; i < dto.Entries.Count; i++)
|
||||
{
|
||||
var entry = dto.Entries[i];
|
||||
AddEntry(entry.SpawnedName, entry.SpawnedProbability, entry.SpawnedMaxCount, false, entry.Properties, entry.Parameters);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export helpers — nullable so WhenWritingNull omits domain defaults, matching legacy ToJson.
|
||||
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 SpawnPositionMode? DtoSpawnPositionMode =>
|
||||
_spawnPositionMode is not SpawnPositionMode.Automatic and not SpawnPositionMode.Abandoned
|
||||
? _spawnPositionMode
|
||||
: null;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
|
@ -1,229 +0,0 @@
|
|||
/*************************************************************************
|
||||
* ModernUO *
|
||||
* Copyright 2019-2026 - ModernUO Development Team *
|
||||
* Email: hi@modernuo.com *
|
||||
* File: BaseSpawner.Json.cs *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
*************************************************************************/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Server.Engines.Spawners;
|
||||
|
||||
public abstract partial class BaseSpawner
|
||||
{
|
||||
// Transient import-only state (NOT [SerializableField]; never part of the binary save).
|
||||
private Guid _jsonGuid;
|
||||
private bool _jsonHasGuid;
|
||||
private int _jsonCount;
|
||||
private TimeSpan _jsonMinDelay = DefaultMinDelay;
|
||||
private TimeSpan _jsonMaxDelay = DefaultMaxDelay;
|
||||
private int _jsonTeam;
|
||||
private int _jsonWalkingRange = -1;
|
||||
private int _jsonHomeRange = -1;
|
||||
private bool _jsonSpawnLocationIsHome;
|
||||
private SpawnPositionMode _jsonSpawnPositionMode;
|
||||
private int _jsonMaxSpawnAttempts = DefaultMaxSpawnAttempts;
|
||||
private List<SpawnerEntry> _jsonEntries;
|
||||
private protected Point3D _jsonLocation;
|
||||
private protected Map _jsonMap;
|
||||
|
||||
// --- Import-placement accessors (not serialized; read by ImportSpawnersCommand after STJ deserialization) ---
|
||||
|
||||
[JsonIgnore]
|
||||
public Point3D ImportLocation => _jsonLocation;
|
||||
|
||||
[JsonIgnore]
|
||||
public Map ImportMap => _jsonMap;
|
||||
|
||||
// --- Always-written fields ---
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("guid")]
|
||||
public Guid JsonGuid
|
||||
{
|
||||
get => _guid;
|
||||
set
|
||||
{
|
||||
_jsonGuid = value;
|
||||
_jsonHasGuid = true;
|
||||
}
|
||||
}
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("location")]
|
||||
public Point3D JsonLocation
|
||||
{
|
||||
get => Location;
|
||||
set => _jsonLocation = value;
|
||||
}
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("map")]
|
||||
public Map JsonMap
|
||||
{
|
||||
get => Map;
|
||||
set => _jsonMap = value;
|
||||
}
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("count")]
|
||||
public int JsonCount
|
||||
{
|
||||
get => Count;
|
||||
set => _jsonCount = value;
|
||||
}
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("entries")]
|
||||
public List<SpawnerEntry> JsonEntries
|
||||
{
|
||||
get => Entries;
|
||||
set => _jsonEntries = value;
|
||||
}
|
||||
|
||||
// --- Conditionally-written fields (null getter => omitted under WhenWritingNull) ---
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("name")]
|
||||
public string JsonName
|
||||
{
|
||||
get => string.IsNullOrEmpty(Name) ? null : Name;
|
||||
set => Name = value;
|
||||
}
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("minDelay")]
|
||||
public TimeSpan? JsonMinDelay
|
||||
{
|
||||
get => _minDelay == DefaultMinDelay ? null : _minDelay;
|
||||
set => _jsonMinDelay = value ?? DefaultMinDelay;
|
||||
}
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("maxDelay")]
|
||||
public TimeSpan? JsonMaxDelay
|
||||
{
|
||||
get => _maxDelay == DefaultMaxDelay ? null : _maxDelay;
|
||||
set => _jsonMaxDelay = value ?? DefaultMaxDelay;
|
||||
}
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("team")]
|
||||
public int? JsonTeam
|
||||
{
|
||||
get => _team == 0 ? null : _team;
|
||||
set => _jsonTeam = value ?? 0;
|
||||
}
|
||||
|
||||
// Mirrors today's ToJson exactly: written when _walkingRange != 0, emitting the WalkingRange property.
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("walkingRange")]
|
||||
public int? JsonWalkingRange
|
||||
{
|
||||
get => _walkingRange != 0 ? WalkingRange : null;
|
||||
set => _jsonWalkingRange = value ?? -1;
|
||||
}
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("spawnLocationIsHome")]
|
||||
public bool? JsonSpawnLocationIsHome
|
||||
{
|
||||
get => _spawnLocationIsHome ? true : null;
|
||||
set => _jsonSpawnLocationIsHome = value ?? false;
|
||||
}
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("spawnPositionMode")]
|
||||
public SpawnPositionMode? JsonSpawnPositionMode
|
||||
{
|
||||
get => _spawnPositionMode is not SpawnPositionMode.Automatic and not SpawnPositionMode.Abandoned
|
||||
? _spawnPositionMode
|
||||
: null;
|
||||
set => _jsonSpawnPositionMode = value ?? SpawnPositionMode.Automatic;
|
||||
}
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("maxSpawnAttempts")]
|
||||
public int? JsonMaxSpawnAttempts
|
||||
{
|
||||
// _maxSpawnAttempts == 0 means "unset; use the default". GetSpawnPosition treats
|
||||
// both 0 and DefaultMaxSpawnAttempts identically, so omit both from JSON output.
|
||||
get => _maxSpawnAttempts > 0 && _maxSpawnAttempts != DefaultMaxSpawnAttempts ? _maxSpawnAttempts : null;
|
||||
set => _jsonMaxSpawnAttempts = value ?? DefaultMaxSpawnAttempts;
|
||||
}
|
||||
|
||||
// Legacy read-only: present in old files; converted in OnAfterJsonDeserialize. Never written
|
||||
// (getter always null) — modern files carry spawnBounds instead.
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("homeRange")]
|
||||
public int? JsonHomeRange
|
||||
{
|
||||
get => null;
|
||||
set => _jsonHomeRange = value ?? -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the deserialized JSON state to this live spawner. Fired by the resolver's
|
||||
/// OnDeserialized after all shadow properties are set. Overrides MUST call base first.
|
||||
/// This replaces the former (DynamicJson, options) constructor body.
|
||||
/// </summary>
|
||||
protected internal virtual void OnAfterJsonDeserialize()
|
||||
{
|
||||
if (_jsonHasGuid)
|
||||
{
|
||||
_guid = _jsonGuid;
|
||||
}
|
||||
|
||||
// Legacy homeRange -> spawnBounds (Map not available yet; use the deserialized location).
|
||||
if (_jsonHomeRange >= 0)
|
||||
{
|
||||
int z;
|
||||
int depth;
|
||||
if (_jsonHomeRange == 0)
|
||||
{
|
||||
z = _jsonLocation.Z;
|
||||
depth = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
z = -128;
|
||||
depth = 256;
|
||||
}
|
||||
|
||||
SpawnBounds = new Rectangle3D(
|
||||
_jsonLocation.X - _jsonHomeRange,
|
||||
_jsonLocation.Y - _jsonHomeRange,
|
||||
z,
|
||||
_jsonHomeRange * 2 + 1,
|
||||
_jsonHomeRange * 2 + 1,
|
||||
depth
|
||||
);
|
||||
}
|
||||
|
||||
InitSpawn(_jsonCount, _jsonMinDelay, _jsonMaxDelay, _jsonTeam, SpawnBounds);
|
||||
|
||||
_walkingRange = _jsonWalkingRange;
|
||||
_spawnLocationIsHome = _jsonSpawnLocationIsHome;
|
||||
_spawnPositionMode = _jsonSpawnPositionMode;
|
||||
_maxSpawnAttempts = _jsonMaxSpawnAttempts;
|
||||
|
||||
if (_jsonEntries != null)
|
||||
{
|
||||
for (var i = 0; i < _jsonEntries.Count; i++)
|
||||
{
|
||||
var entry = _jsonEntries[i];
|
||||
AddEntry(entry.SpawnedName, entry.SpawnedProbability, entry.SpawnedMaxCount, false, entry.Properties, entry.Parameters);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -120,6 +120,9 @@ public abstract partial class BaseSpawner : Item, ISpawner
|
|||
[CommandProperty(AccessLevel.Developer)]
|
||||
public abstract Rectangle3D SpawnBounds { get; set; }
|
||||
|
||||
/// <summary>Builds the JSON DTO for this spawner (export path).</summary>
|
||||
public abstract SpawnerDto ToDto();
|
||||
|
||||
/// <summary>
|
||||
/// If true, the home location of the spawn is the location where it spawned
|
||||
/// If false, the home location of the spawn is the location of the spawner
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ public class ExportSpawnersCommand : BaseCommand
|
|||
|
||||
NetState.FlushAll();
|
||||
|
||||
var spawnRecords = new List<BaseSpawner>(list.Count);
|
||||
var spawnRecords = new List<SpawnerDto>(list.Count);
|
||||
for (var i = 0; i < list.Count; i++)
|
||||
{
|
||||
// Not a spawner, not on a valid map, or is in a container
|
||||
|
|
@ -83,7 +83,7 @@ public class ExportSpawnersCommand : BaseCommand
|
|||
continue;
|
||||
}
|
||||
|
||||
spawnRecords.Add(spawner);
|
||||
spawnRecords.Add(spawner.ToDto());
|
||||
}
|
||||
|
||||
if (spawnRecords.Count == 0)
|
||||
|
|
|
|||
|
|
@ -207,10 +207,11 @@ public static class ImportSpawnersCommand
|
|||
ref int totalFailures
|
||||
)
|
||||
{
|
||||
List<BaseSpawner> spawners;
|
||||
List<SpawnerDto> dtos;
|
||||
try
|
||||
{
|
||||
spawners = JsonConfig.Deserialize<List<BaseSpawner>>(file.FullName, SpawnerJsonSerializer.Options);
|
||||
// DTO deserialization constructs NO world objects — a malformed file fails as GC only.
|
||||
dtos = JsonConfig.Deserialize<List<SpawnerDto>>(file.FullName, SpawnerJsonSerializer.Options);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
|
|
@ -220,7 +221,7 @@ public static class ImportSpawnersCommand
|
|||
return;
|
||||
}
|
||||
|
||||
if (spawners == null || spawners.Count == 0)
|
||||
if (dtos == null || dtos.Count == 0)
|
||||
{
|
||||
from?.SendMessage($"GenerateSpawners: Skipping empty spawner file {file.Name}");
|
||||
logger.Information("{User} is skipping empty spawner file {File}", from, file.FullName);
|
||||
|
|
@ -228,16 +229,27 @@ public static class ImportSpawnersCommand
|
|||
}
|
||||
|
||||
using var queue = PooledRefQueue<Item>.Create();
|
||||
for (var i = 0; i < spawners.Count; i++)
|
||||
for (var i = 0; i < dtos.Count; i++)
|
||||
{
|
||||
var spawner = spawners[i];
|
||||
var location = spawner.ImportLocation;
|
||||
var map = spawner.ImportMap;
|
||||
var dto = dtos[i];
|
||||
var location = dto.Location;
|
||||
var map = dto.Map;
|
||||
|
||||
if (map == null || map == Map.Internal)
|
||||
{
|
||||
logger.Error("Spawner {Guid} ({Index}) has no valid map; skipping.", spawner.Guid, i);
|
||||
spawner.Delete();
|
||||
logger.Error("Spawner {Guid} ({Index}) has no valid map; skipping.", dto.Guid, i);
|
||||
totalFailures++;
|
||||
continue;
|
||||
}
|
||||
|
||||
BaseSpawner spawner;
|
||||
try
|
||||
{
|
||||
spawner = dto.ToSpawner(); // constructs the single Item, now referenced
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TraceException(ex, $"Failed to build spawner {dto.Guid}.");
|
||||
totalFailures++;
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
116
Projects/UOContent/Engines/Spawners/Json/SpawnerDto.cs
Normal file
116
Projects/UOContent/Engines/Spawners/Json/SpawnerDto.cs
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
/*************************************************************************
|
||||
* ModernUO *
|
||||
* Copyright 2019-2026 - ModernUO Development Team *
|
||||
* Email: hi@modernuo.com *
|
||||
* File: SpawnerDto.cs *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
*************************************************************************/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Server.Json;
|
||||
using Server.Regions;
|
||||
|
||||
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.
|
||||
/// </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; }
|
||||
|
||||
/// <summary>Constructs the empty concrete spawner Item for this DTO.</summary>
|
||||
protected abstract BaseSpawner CreateEmpty();
|
||||
|
||||
/// <summary>Builds and populates the live spawner. Override to apply subtype fields after base.</summary>
|
||||
public virtual BaseSpawner ToSpawner()
|
||||
{
|
||||
var spawner = CreateEmpty();
|
||||
spawner.ApplyDto(this);
|
||||
return spawner;
|
||||
}
|
||||
}
|
||||
|
||||
[JsonDiscoverableType("Spawner")]
|
||||
public sealed record SpawnerDataDto : SpawnerDto
|
||||
{
|
||||
[JsonPropertyName("spawnBounds")] public Rectangle3D? SpawnBounds { get; init; }
|
||||
|
||||
protected override BaseSpawner CreateEmpty() => new Spawner();
|
||||
|
||||
public override BaseSpawner ToSpawner()
|
||||
{
|
||||
var spawner = (Spawner)base.ToSpawner();
|
||||
if (SpawnBounds is { } bounds && bounds != default)
|
||||
{
|
||||
spawner.SpawnBounds = bounds;
|
||||
}
|
||||
|
||||
return spawner;
|
||||
}
|
||||
}
|
||||
|
||||
[JsonDiscoverableType("RegionSpawner")]
|
||||
public sealed record RegionSpawnerDto : SpawnerDto
|
||||
{
|
||||
[JsonPropertyName("region")] public string Region { get; init; }
|
||||
|
||||
protected override BaseSpawner CreateEmpty() => new RegionSpawner();
|
||||
|
||||
public override BaseSpawner ToSpawner()
|
||||
{
|
||||
var spawner = (RegionSpawner)base.ToSpawner();
|
||||
spawner.SpawnRegion = Server.Region.Find(Region, Map) as BaseRegion;
|
||||
return spawner;
|
||||
}
|
||||
}
|
||||
|
||||
[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; }
|
||||
|
||||
protected override BaseSpawner CreateEmpty() => new ProximitySpawner();
|
||||
|
||||
public override BaseSpawner ToSpawner()
|
||||
{
|
||||
var spawner = (ProximitySpawner)base.ToSpawner();
|
||||
if (SpawnBounds is { } bounds && bounds != default)
|
||||
{
|
||||
spawner.SpawnBounds = bounds;
|
||||
}
|
||||
|
||||
spawner.TriggerRange = TriggerRange;
|
||||
spawner.SpawnMessage = SpawnMessage;
|
||||
spawner.InstantFlag = Instant;
|
||||
return spawner;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
* ModernUO *
|
||||
* Copyright 2019-2026 - ModernUO Development Team *
|
||||
* Email: hi@modernuo.com *
|
||||
* File: ProximitySpawner.Json.cs *
|
||||
* File: ProximitySpawner.Dto.cs *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
|
|
@ -13,46 +13,28 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
*************************************************************************/
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Server.Engines.Spawners;
|
||||
|
||||
public partial class ProximitySpawner
|
||||
{
|
||||
private int _jsonTriggerRange;
|
||||
private TextDefinition _jsonSpawnMessage;
|
||||
private bool _jsonInstant;
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("triggerRange")]
|
||||
public int JsonTriggerRange
|
||||
public override SpawnerDto ToDto() => new ProximitySpawnerDto
|
||||
{
|
||||
get => TriggerRange;
|
||||
set => _jsonTriggerRange = value;
|
||||
}
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("spawnMessage")]
|
||||
public TextDefinition JsonSpawnMessage
|
||||
{
|
||||
get => SpawnMessage;
|
||||
set => _jsonSpawnMessage = value;
|
||||
}
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("instant")]
|
||||
public bool JsonInstant
|
||||
{
|
||||
get => InstantFlag;
|
||||
set => _jsonInstant = value;
|
||||
}
|
||||
|
||||
protected internal override void OnAfterJsonDeserialize()
|
||||
{
|
||||
base.OnAfterJsonDeserialize();
|
||||
|
||||
TriggerRange = _jsonTriggerRange;
|
||||
SpawnMessage = _jsonSpawnMessage;
|
||||
InstantFlag = _jsonInstant;
|
||||
}
|
||||
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
|
||||
};
|
||||
}
|
||||
|
|
@ -22,7 +22,6 @@ using Server.Mobiles;
|
|||
namespace Server.Engines.Spawners;
|
||||
|
||||
[SerializationGenerator(0)]
|
||||
[JsonDiscoverableType]
|
||||
public partial class ProximitySpawner : Spawner
|
||||
{
|
||||
[SerializableField(0)]
|
||||
|
|
@ -38,7 +37,6 @@ public partial class ProximitySpawner : Spawner
|
|||
private bool _instantFlag;
|
||||
|
||||
[Constructible(AccessLevel.Developer)]
|
||||
[System.Text.Json.Serialization.JsonConstructor]
|
||||
public ProximitySpawner()
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* ModernUO *
|
||||
* Copyright 2019-2026 - ModernUO Development Team *
|
||||
* Email: hi@modernuo.com *
|
||||
* File: RegionSpawner.Json.cs *
|
||||
* File: RegionSpawner.Dto.cs *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
|
|
@ -13,29 +13,25 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
*************************************************************************/
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using Server.Regions;
|
||||
|
||||
namespace Server.Engines.Spawners;
|
||||
|
||||
public partial class RegionSpawner
|
||||
{
|
||||
private string _jsonRegion;
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("region")]
|
||||
public string JsonRegion
|
||||
public override SpawnerDto ToDto() => new RegionSpawnerDto
|
||||
{
|
||||
get => SpawnRegion?.Name;
|
||||
set => _jsonRegion = value;
|
||||
}
|
||||
|
||||
protected internal override void OnAfterJsonDeserialize()
|
||||
{
|
||||
base.OnAfterJsonDeserialize();
|
||||
|
||||
_spawnRegion = Region.Find(_jsonRegion, _jsonMap) as BaseRegion;
|
||||
_spawnRegion?.InitRectangles();
|
||||
SpawnRegionName = _spawnRegion?.Name;
|
||||
}
|
||||
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,
|
||||
Region = SpawnRegion?.Name
|
||||
};
|
||||
}
|
||||
|
|
@ -22,7 +22,6 @@ using Server.Regions;
|
|||
namespace Server.Engines.Spawners;
|
||||
|
||||
[SerializationGenerator(0)]
|
||||
[JsonDiscoverableType]
|
||||
public partial class RegionSpawner : Spawner
|
||||
{
|
||||
[SerializableField(0, getter: "private", setter: "private")]
|
||||
|
|
@ -33,7 +32,6 @@ public partial class RegionSpawner : Spawner
|
|||
public override Region Region => _spawnRegion;
|
||||
|
||||
[Constructible(AccessLevel.Developer)]
|
||||
[System.Text.Json.Serialization.JsonConstructor]
|
||||
public RegionSpawner()
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* ModernUO *
|
||||
* Copyright 2019-2026 - ModernUO Development Team *
|
||||
* Email: hi@modernuo.com *
|
||||
* File: Spawner.Json.cs *
|
||||
* File: Spawner.Dto.cs *
|
||||
* *
|
||||
* This program is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
|
|
@ -13,29 +13,25 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
*************************************************************************/
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Server.Engines.Spawners;
|
||||
|
||||
public partial class Spawner
|
||||
{
|
||||
private Rectangle3D _jsonSpawnBounds;
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("spawnBounds")]
|
||||
public Rectangle3D? JsonSpawnBounds
|
||||
public override SpawnerDto ToDto() => new SpawnerDataDto
|
||||
{
|
||||
get => _spawnBounds == default ? null : _spawnBounds;
|
||||
set => _jsonSpawnBounds = value ?? default;
|
||||
}
|
||||
|
||||
protected internal override void OnAfterJsonDeserialize()
|
||||
{
|
||||
base.OnAfterJsonDeserialize();
|
||||
|
||||
if (_jsonSpawnBounds != default)
|
||||
{
|
||||
SpawnBounds = _jsonSpawnBounds;
|
||||
}
|
||||
}
|
||||
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
|
||||
};
|
||||
}
|
||||
|
|
@ -6,7 +6,6 @@ using Server.Json;
|
|||
namespace Server.Engines.Spawners;
|
||||
|
||||
[SerializationGenerator(1)]
|
||||
[JsonDiscoverableType]
|
||||
public partial class Spawner : BaseSpawner
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -37,7 +36,6 @@ public partial class Spawner : BaseSpawner
|
|||
}
|
||||
|
||||
[Constructible(AccessLevel.Developer)]
|
||||
[System.Text.Json.Serialization.JsonConstructor]
|
||||
public Spawner()
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ public static class SpawnerJsonSerializer
|
|||
|
||||
/// <summary>
|
||||
/// Invoked automatically during the Configure bootstrap phase
|
||||
/// (AssemblyHandler.Invoke("Configure")). Discovers every concrete BaseSpawner subclass
|
||||
/// (AssemblyHandler.Invoke("Configure")). Discovers every concrete SpawnerDto subclass
|
||||
/// marked with [JsonDiscoverableType] and registers it for STJ polymorphism.
|
||||
/// </summary>
|
||||
public static void Configure()
|
||||
|
|
@ -58,7 +58,7 @@ public static class SpawnerJsonSerializer
|
|||
for (var i = 0; i < types.Length; i++)
|
||||
{
|
||||
var type = types[i];
|
||||
if (type.IsAbstract || !type.IsAssignableTo(typeof(BaseSpawner)))
|
||||
if (type.IsAbstract || !type.IsAssignableTo(typeof(SpawnerDto)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
@ -73,8 +73,8 @@ public static class SpawnerJsonSerializer
|
|||
if (!IsJsonConstructible(type))
|
||||
{
|
||||
throw new Exception(
|
||||
$"Spawner type '{type.FullName}' is marked [JsonDiscoverableType] but System.Text.Json cannot construct it. " +
|
||||
"Add a public parameterless constructor marked [JsonConstructor]."
|
||||
$"SpawnerDto type '{type.FullName}' is marked [JsonDiscoverableType] but System.Text.Json cannot construct it. " +
|
||||
"Add a public parameterless constructor or use a record with init-only properties."
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -117,16 +117,14 @@ public static class SpawnerJsonSerializer
|
|||
{
|
||||
Modifiers =
|
||||
{
|
||||
AddPolymorphism,
|
||||
PruneToJsonProperties,
|
||||
AddOnDeserialized
|
||||
AddPolymorphism
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private static void AddPolymorphism(JsonTypeInfo typeInfo)
|
||||
{
|
||||
if (typeInfo.Type != typeof(BaseSpawner))
|
||||
if (typeInfo.Type != typeof(SpawnerDto))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -137,34 +135,4 @@ public static class SpawnerJsonSerializer
|
|||
typeInfo.PolymorphismOptions.DerivedTypes.Add(_derivedTypes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Spawners are Items with many public engine properties STJ would otherwise (de)serialize.
|
||||
// Keep ONLY properties explicitly annotated with [JsonPropertyName] (our shadow properties).
|
||||
private static void PruneToJsonProperties(JsonTypeInfo typeInfo)
|
||||
{
|
||||
if (!typeInfo.Type.IsAssignableTo(typeof(BaseSpawner)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = typeInfo.Properties.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var provider = typeInfo.Properties[i].AttributeProvider;
|
||||
var keep = provider?.IsDefined(typeof(JsonPropertyNameAttribute), true) ?? false;
|
||||
if (!keep)
|
||||
{
|
||||
typeInfo.Properties.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddOnDeserialized(JsonTypeInfo typeInfo)
|
||||
{
|
||||
if (!typeInfo.Type.IsAssignableTo(typeof(BaseSpawner)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
typeInfo.OnDeserialized = static o => ((BaseSpawner)o).OnAfterJsonDeserialize();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue