feat: treat the four speeds as one block against the bucket
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>
This commit is contained in:
parent
695efc7d6e
commit
4238980c6d
3 changed files with 119 additions and 47 deletions
|
|
@ -197,6 +197,7 @@ public class BaseCreatureSerializationTests : IDisposable
|
||||||
|
|
||||||
bc.SpeedClass = SpeedLevel.VeryFast; // boss state change
|
bc.SpeedClass = SpeedLevel.VeryFast; // boss state change
|
||||||
|
|
||||||
|
Assert.Equal(SpeedLevel.VeryFast, bc.SpeedClass); // conforming assignment holds
|
||||||
Assert.Equal(0.125, bc.ActiveSpeed);
|
Assert.Equal(0.125, bc.ActiveSpeed);
|
||||||
Assert.Equal(0.3, bc.PassiveSpeed);
|
Assert.Equal(0.3, bc.PassiveSpeed);
|
||||||
Assert.Equal(0.125, bc.ActiveMoveSpeed);
|
Assert.Equal(0.125, bc.ActiveMoveSpeed);
|
||||||
|
|
@ -223,6 +224,41 @@ public class BaseCreatureSerializationTests : IDisposable
|
||||||
Assert.Equal(0.6, copy.PassiveMoveSpeed);
|
Assert.Equal(0.6, copy.PassiveMoveSpeed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PartialSpeedTuning_MakesTheCreatureFullyCustom()
|
||||||
|
{
|
||||||
|
NPCSpeeds.RegisterSpeed(new NPCSpeeds.SpeedClassEntry
|
||||||
|
{
|
||||||
|
Level = SpeedLevel.Fast, ActiveSpeed = 0.2, PassiveSpeed = 0.4,
|
||||||
|
ActiveMoveSpeed = 0.3, PassiveMoveSpeed = 0.9, Types = new HashSet<Type>()
|
||||||
|
});
|
||||||
|
|
||||||
|
var bc = new BucketStub();
|
||||||
|
_created.Add(bc);
|
||||||
|
|
||||||
|
bc.ActiveSpeed = 0.25; // one tuned value customizes the whole block
|
||||||
|
|
||||||
|
Assert.Equal(SpeedLevel.Custom, bc.SpeedClass); // the bucket label never lies
|
||||||
|
|
||||||
|
var writer = new BufferWriter(true);
|
||||||
|
bc.Serialize(writer);
|
||||||
|
var buffer = new byte[writer.Position];
|
||||||
|
writer.Buffer.AsSpan(0, (int)writer.Position).CopyTo(buffer);
|
||||||
|
|
||||||
|
var copy = new BucketStub(World.NewMobile);
|
||||||
|
_created.Add(copy);
|
||||||
|
var reader = new BufferReader(buffer);
|
||||||
|
copy.Deserialize(reader);
|
||||||
|
|
||||||
|
// All four persisted raw - no value is left silently tracking the table.
|
||||||
|
Assert.Equal(buffer.Length, reader.Position);
|
||||||
|
Assert.Equal(SpeedLevel.Custom, copy.SpeedClass);
|
||||||
|
Assert.Equal(0.25, copy.ActiveSpeed);
|
||||||
|
Assert.Equal(0.4, copy.PassiveSpeed);
|
||||||
|
Assert.Equal(0.3, copy.ActiveMoveSpeed);
|
||||||
|
Assert.Equal(0.9, copy.PassiveMoveSpeed);
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class MobileStub : Mobile
|
private sealed class MobileStub : Mobile
|
||||||
{
|
{
|
||||||
public MobileStub() => Body = 0xC9;
|
public MobileStub() => Body = 0xC9;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using ModernUO.CodeGeneratedEvents;
|
|
||||||
using ModernUO.Serialization;
|
using ModernUO.Serialization;
|
||||||
using Server.Collections;
|
using Server.Collections;
|
||||||
using Server.ContextMenus;
|
using Server.ContextMenus;
|
||||||
|
|
@ -335,29 +334,68 @@ namespace Server.Mobiles
|
||||||
ApplySpeedClass();
|
ApplySpeedClass();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Applies the current bucket's speeds, preserving the active/passive mode.
|
private bool _applyingSpeedClass;
|
||||||
|
|
||||||
|
// Applies the current bucket's speeds, preserving the active/passive mode. The
|
||||||
|
// guard keeps OnSpeedTuned from reading the half-assigned block as customization.
|
||||||
private void ApplySpeedClass()
|
private void ApplySpeedClass()
|
||||||
{
|
{
|
||||||
var wasActive = _currentSpeed == _activeSpeed && _currentSpeed != _passiveSpeed;
|
if (SpeedEntry == null)
|
||||||
|
{
|
||||||
|
return; // Custom (or an unloaded table) has no bucket to apply
|
||||||
|
}
|
||||||
|
|
||||||
GetSpeeds(out var activeSpeed, out var passiveSpeed);
|
_applyingSpeedClass = true;
|
||||||
GetMoveSpeeds(out _activeMoveSpeed, out _passiveMoveSpeed);
|
|
||||||
|
|
||||||
ActiveSpeed = activeSpeed;
|
try
|
||||||
PassiveSpeed = passiveSpeed;
|
{
|
||||||
CurrentSpeed = wasActive ? activeSpeed : passiveSpeed;
|
var wasActive = _currentSpeed == _activeSpeed && _currentSpeed != _passiveSpeed;
|
||||||
|
|
||||||
|
GetSpeeds(out var activeSpeed, out var passiveSpeed);
|
||||||
|
GetMoveSpeeds(out _activeMoveSpeed, out _passiveMoveSpeed);
|
||||||
|
|
||||||
|
ActiveSpeed = activeSpeed;
|
||||||
|
PassiveSpeed = passiveSpeed;
|
||||||
|
CurrentSpeed = wasActive ? activeSpeed : passiveSpeed;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_applyingSpeedClass = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Seconds per AI decision while engaged; see <see cref="ActiveMoveSpeed"/> for movement pace.</summary>
|
/// <summary>Seconds per AI decision while engaged; see <see cref="ActiveMoveSpeed"/> for movement pace.</summary>
|
||||||
[SerializableField(8)]
|
[SerializableField(8, fieldChanged: nameof(OnSpeedTuned))]
|
||||||
[SaveFlag(nameof(ShouldSerializeActiveSpeed), nameof(ActiveSpeedDefaultValue))]
|
[SaveFlag(nameof(ShouldSerializeSpeeds), nameof(ActiveSpeedDefaultValue))]
|
||||||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||||||
private double _activeSpeed;
|
private double _activeSpeed;
|
||||||
|
|
||||||
private bool ShouldSerializeActiveSpeed()
|
// The four speeds are one block: either all conform to the bucket (elided as a
|
||||||
|
// set), or the creature is custom and all four serialize. Partial conformance
|
||||||
|
// cannot exist on the wire.
|
||||||
|
private bool ShouldSerializeSpeeds()
|
||||||
{
|
{
|
||||||
GetSpeeds(out var activeSpeed, out _);
|
if (_speedClass == SpeedLevel.Custom)
|
||||||
return _activeSpeed != activeSpeed;
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
GetSpeeds(out var activeSpeed, out var passiveSpeed);
|
||||||
|
GetMoveSpeeds(out var activeMoveSpeed, out var passiveMoveSpeed);
|
||||||
|
|
||||||
|
return _activeSpeed != activeSpeed || _passiveSpeed != passiveSpeed ||
|
||||||
|
_activeMoveSpeed != activeMoveSpeed || _passiveMoveSpeed != passiveMoveSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tuning any speed away from the bucket makes the creature fully custom - the
|
||||||
|
// bucket label must never lie. ApplySpeedClass assigns mid-transition and guards.
|
||||||
|
private void OnSpeedTuned(double oldValue, double newValue)
|
||||||
|
{
|
||||||
|
if (!_applyingSpeedClass && _speedClass != SpeedLevel.Custom && ShouldSerializeSpeeds())
|
||||||
|
{
|
||||||
|
_speedClass = SpeedLevel.Custom;
|
||||||
|
_speedEntry = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private double ActiveSpeedDefaultValue()
|
private double ActiveSpeedDefaultValue()
|
||||||
|
|
@ -367,17 +405,11 @@ namespace Server.Mobiles
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Seconds per AI decision while idle; see <see cref="PassiveMoveSpeed"/> for movement pace.</summary>
|
/// <summary>Seconds per AI decision while idle; see <see cref="PassiveMoveSpeed"/> for movement pace.</summary>
|
||||||
[SerializableField(9)]
|
[SerializableField(9, fieldChanged: nameof(OnSpeedTuned))]
|
||||||
[SaveFlag(nameof(ShouldSerializePassiveSpeed), nameof(PassiveSpeedDefaultValue))]
|
[SaveFlag(nameof(ShouldSerializeSpeeds), nameof(PassiveSpeedDefaultValue))]
|
||||||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||||||
private double _passiveSpeed;
|
private double _passiveSpeed;
|
||||||
|
|
||||||
private bool ShouldSerializePassiveSpeed()
|
|
||||||
{
|
|
||||||
GetSpeeds(out _, out var passiveSpeed);
|
|
||||||
return _passiveSpeed != passiveSpeed;
|
|
||||||
}
|
|
||||||
|
|
||||||
private double PassiveSpeedDefaultValue()
|
private double PassiveSpeedDefaultValue()
|
||||||
{
|
{
|
||||||
GetSpeeds(out _, out var passiveSpeed);
|
GetSpeeds(out _, out var passiveSpeed);
|
||||||
|
|
@ -399,8 +431,8 @@ namespace Server.Mobiles
|
||||||
/// Movement clock (seconds per step) while engaged; 0 = inherit
|
/// Movement clock (seconds per step) while engaged; 0 = inherit
|
||||||
/// <see cref="ActiveSpeed"/>. <see cref="CurrentMoveSpeed"/> resolves the pace.
|
/// <see cref="ActiveSpeed"/>. <see cref="CurrentMoveSpeed"/> resolves the pace.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[SerializableField(11, allowFieldChange: nameof(CoerceMoveSpeed))]
|
[SerializableField(11, allowFieldChange: nameof(CoerceMoveSpeed), fieldChanged: nameof(OnSpeedTuned))]
|
||||||
[SaveFlag(nameof(ShouldSerializeActiveMoveSpeed), nameof(ActiveMoveSpeedDefaultValue))]
|
[SaveFlag(nameof(ShouldSerializeSpeeds), nameof(ActiveMoveSpeedDefaultValue))]
|
||||||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||||||
private double _activeMoveSpeed;
|
private double _activeMoveSpeed;
|
||||||
|
|
||||||
|
|
@ -408,8 +440,8 @@ namespace Server.Mobiles
|
||||||
/// Movement clock (seconds per step) while idle; 0 = inherit
|
/// Movement clock (seconds per step) while idle; 0 = inherit
|
||||||
/// <see cref="PassiveSpeed"/>. <see cref="CurrentMoveSpeed"/> resolves the pace.
|
/// <see cref="PassiveSpeed"/>. <see cref="CurrentMoveSpeed"/> resolves the pace.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[SerializableField(12, allowFieldChange: nameof(CoerceMoveSpeed))]
|
[SerializableField(12, allowFieldChange: nameof(CoerceMoveSpeed), fieldChanged: nameof(OnSpeedTuned))]
|
||||||
[SaveFlag(nameof(ShouldSerializePassiveMoveSpeed), nameof(PassiveMoveSpeedDefaultValue))]
|
[SaveFlag(nameof(ShouldSerializeSpeeds), nameof(PassiveMoveSpeedDefaultValue))]
|
||||||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||||||
private double _passiveMoveSpeed;
|
private double _passiveMoveSpeed;
|
||||||
|
|
||||||
|
|
@ -419,24 +451,12 @@ namespace Server.Mobiles
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ShouldSerializeActiveMoveSpeed()
|
|
||||||
{
|
|
||||||
GetMoveSpeeds(out var activeMoveSpeed, out _);
|
|
||||||
return _activeMoveSpeed != activeMoveSpeed;
|
|
||||||
}
|
|
||||||
|
|
||||||
private double ActiveMoveSpeedDefaultValue()
|
private double ActiveMoveSpeedDefaultValue()
|
||||||
{
|
{
|
||||||
GetMoveSpeeds(out var activeMoveSpeed, out _);
|
GetMoveSpeeds(out var activeMoveSpeed, out _);
|
||||||
return activeMoveSpeed;
|
return activeMoveSpeed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ShouldSerializePassiveMoveSpeed()
|
|
||||||
{
|
|
||||||
GetMoveSpeeds(out _, out var passiveMoveSpeed);
|
|
||||||
return _passiveMoveSpeed != passiveMoveSpeed;
|
|
||||||
}
|
|
||||||
|
|
||||||
private double PassiveMoveSpeedDefaultValue()
|
private double PassiveMoveSpeedDefaultValue()
|
||||||
{
|
{
|
||||||
GetMoveSpeeds(out _, out var passiveMoveSpeed);
|
GetMoveSpeeds(out _, out var passiveMoveSpeed);
|
||||||
|
|
@ -853,12 +873,9 @@ namespace Server.Mobiles
|
||||||
|
|
||||||
FightMode = mode;
|
FightMode = mode;
|
||||||
|
|
||||||
GetSpeeds(out var activeSpeed, out var passiveSpeed);
|
GetSpeeds(out _activeSpeed, out _passiveSpeed);
|
||||||
GetMoveSpeeds(out _activeMoveSpeed, out _passiveMoveSpeed);
|
GetMoveSpeeds(out _activeMoveSpeed, out _passiveMoveSpeed);
|
||||||
|
_currentSpeed = _passiveSpeed;
|
||||||
ActiveSpeed = activeSpeed;
|
|
||||||
PassiveSpeed = passiveSpeed;
|
|
||||||
CurrentSpeed = passiveSpeed;
|
|
||||||
|
|
||||||
_team = 0;
|
_team = 0;
|
||||||
|
|
||||||
|
|
@ -5121,9 +5138,22 @@ namespace Server.Mobiles
|
||||||
|
|
||||||
public virtual void GetSpeeds(out double activeSpeed, out double passiveSpeed)
|
public virtual void GetSpeeds(out double activeSpeed, out double passiveSpeed)
|
||||||
{
|
{
|
||||||
var entry = SpeedEntry ?? throw new InvalidOperationException(
|
var entry = SpeedEntry;
|
||||||
$"{GetType()} has no speed entry - is {"Data/npc-speeds.json"} missing?"
|
|
||||||
);
|
if (entry == null)
|
||||||
|
{
|
||||||
|
if (_speedClass == SpeedLevel.Custom)
|
||||||
|
{
|
||||||
|
// A custom creature is its own reference.
|
||||||
|
activeSpeed = _activeSpeed;
|
||||||
|
passiveSpeed = _passiveSpeed;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"{GetType()} has no speed entry - is {"Data/npc-speeds.json"} missing?"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
activeSpeed = entry.ActiveSpeed;
|
activeSpeed = entry.ActiveSpeed;
|
||||||
passiveSpeed = entry.PassiveSpeed;
|
passiveSpeed = entry.PassiveSpeed;
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,13 @@ namespace Server.Mobiles;
|
||||||
|
|
||||||
public enum SpeedLevel
|
public enum SpeedLevel
|
||||||
{
|
{
|
||||||
None,
|
None, // resolve by type list, falling back to Medium
|
||||||
VerySlow,
|
VerySlow,
|
||||||
Slow,
|
Slow,
|
||||||
Medium,
|
Medium,
|
||||||
Fast,
|
Fast,
|
||||||
VeryFast
|
VeryFast,
|
||||||
|
Custom // hand-tuned: no table entry; all four speeds serialize
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class NPCSpeeds
|
public static class NPCSpeeds
|
||||||
|
|
@ -30,6 +31,11 @@ public static class NPCSpeeds
|
||||||
// table is immutable after Configure.
|
// table is immutable after Configure.
|
||||||
public static SpeedClassEntry FindEntry(BaseCreature bc)
|
public static SpeedClassEntry FindEntry(BaseCreature bc)
|
||||||
{
|
{
|
||||||
|
if (bc.SpeedClass == SpeedLevel.Custom)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if ((bc.SpeedClass == SpeedLevel.None || !_speedsByLevel.TryGetValue(bc.SpeedClass, out var sp)) &&
|
if ((bc.SpeedClass == SpeedLevel.None || !_speedsByLevel.TryGetValue(bc.SpeedClass, out var sp)) &&
|
||||||
!_speedsByType.TryGetValue(bc.GetType(), out sp))
|
!_speedsByType.TryGetValue(bc.GetType(), out sp))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue