ControlMaster and SummonMaster, when both set, are always the same
mobile: BaseCreature.Summon assigns both to the caster, and every pet
management flow (transfer, stable, claim, ball of summoning, GM obey,
login overflow) followed SetControlMaster with an identical SummonMaster
assignment. They differ only in presence - uncontrolled summons carry
only a summon master, pets only a control master.
So one _master reference serializes (refreshed at save, fanned back out
through the Controlled/Summoned flags in AfterDeserialization; legacy
loads feed the same path), and SetControlMaster now keeps SummonMaster
in lockstep itself, deleting the six hand-rolled copies of that
boilerplate.
Also: a creature constructed without speeds (missing npc-speeds.json)
now logs debug and defaults to Medium (0.25/0.5) instead of throwing -
this is the place a sane default belongs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
It served its purpose validating the migration during development; the
legacy path is one-time upgrade code and the replica writer was most of
the file.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The None -> type list -> Medium chain was construction-time defaulting
(so creatures without a bucket or a SetSpeed call never spawn at 0/0),
not a live semantic. The constructor now resolves it once into
_speedClass itself, so at runtime a concrete bucket means table-backed
and None means the creature's own speeds are authoritative - which is
what SpeedLevel.Custom was; it is removed. Runtime entry resolution
collapses to a single level lookup, and the SpeedClass byte still
elides by comparing against the (cached) resolved type default.
Legacy loads guess the type default and demote to None when the loaded
speeds do not conform, so pre-codegen customized creatures (SetSpeed
vendors) come out honestly labeled. A constructor guard keeps a missing
speed table loud instead of spawning 0-delay creatures that spin their
AI timers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Either all four values (active/passive think + move) conform to the
creature's speed entry - elided as a set - or the creature is fully
custom and all four serialize. Partial conformance cannot exist on the
wire, so no value is ever left silently tracking the table beside a
hand-tuned sibling.
"Fully custom" is a real state: SpeedLevel.Custom (None already means
"resolve by type list" for the type-listed species, so it cannot double
as the custom marker). Tuning any speed flips the bucket to Custom (the
label never lies), Custom resolves no entry and short-circuits the
conformance check, a custom creature is its own GetSpeeds reference
(paragon snap becomes a natural no-op), and assigning a real bucket
un-customs it via ApplySpeedClass. A re-entrancy guard keeps the flip
from misreading ApplySpeedClass's half-assigned block, and constructors
seed raw fields so DefaultSpeedClass types do not flip at birth.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A virtual SpeedClass could be overridden dynamically (boss state change)
and silently diverge from the cached speed entry. SpeedClass is now
non-virtual instance state: assigning it invalidates the cached entry,
applies the new bucket's think and move speeds (preserving the
active/passive mode), and serializes only when it differs from the
type's DefaultSpeedClass - so a runtime bucket change survives a save
while its (bucket-matching) speeds still elide. Works from [props too.
Overrides become: DefaultSpeedClass for a type's constant bucket,
SpeedClass assignment for state changes, GetSpeeds/GetMoveSpeeds to
bypass the table entirely - none of which can leave the cache stale.
SpeedClass deserializes before the speed fields (index 7; later indexes
shift by one - v23 was never released, schema regenerated).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ActiveMoveSpeed/PassiveMoveSpeed become plain [SerializableField]s (not
virtual): the properties now read the raw override (0 = inheriting) and
CurrentMoveSpeed carries the inherit resolution - it was the only
production reader of the resolving getters. The <=0 coercion moves to an
allowFieldChange hook. Wire format unchanged (schema diff is empty).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Converts BaseCreature's hand-written v22 serialization to codegen v23.
Nearly every field sits behind a [SaveFlag], so a creature matching its
defaults writes only [version int][ulong flags][default AI] - 13 bytes -
instead of ~236, and the writer's work is mostly branch-not-taken. With
500k-1M creatures in a world, this is the dominant slice of mobile save
freeze time and disk.
- Speeds serialize only when they differ from the creature's npc-speeds
values (GetSpeeds/GetMoveSpeeds on both the flag check and the load
default), so table edits now reach existing unmodified spawns on
restart, and former paragons (snapped back to table values) elide
fully. CurrentSpeed writes only when it differs from PassiveSpeed.
- The delete countdown is a [DeserializeTimer] field (anchored);
stabled/controlled pets never persist one, and the abandoned-pet
3-day fallback lives in AfterDeserialization for both load paths.
- SummonEnd stays anchored, written only while summoned.
- Old saves (v0-22) load through the retained legacy
Deserialize(reader, version), now assigning raw fields; the shared
post-load fixups (stat timers, AI creation, followers, animate-dead
registration, unsummon timer) moved to [AfterDeserialization].
- Side-effect setters became generated-field hooks (Team, Controlled,
Summoned, Loyalty clamp, resistance seeds, CurrentSpeed); properties
whose semantics the hooks cannot express stay hand-written as
[SerializableProperty] (ControlMaster/SummonMaster bracket the
assignment with follower bookkeeping, ControlOrder must run on equal
re-assignment, Tamable/IsParagon/move speeds have custom getters).
- CreatureDeathEvent/CreatureDeletedEvent moved to a CreatureEvents
host class: the events generator and the serialization generator each
emit a [GeneratedCode] partial for the declaring type, and the
attribute forbids duplicates (CS0579).
- m_ fields renamed to _camelCase.
Tests: new-format round trips (default and fully populated) with exact
byte consumption, back-to-back saves byte-identical, and a
byte-authentic fossilized v22 stream loading through the legacy path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>