ModernUO/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs
Kamron Batman 6d7eb24cc1
test: drop the fossilized v22 legacy-stream test
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>
2026-08-23 19:18:07 -07:00

260 lines
8.8 KiB
C#

using System;
using System.Collections.Generic;
using Server;
using Server.Mobiles;
using Xunit;
namespace UOContent.Tests.Mobiles;
// BaseCreature's SaveFlag format round-trips both a default and a fully-populated
// creature with exact byte consumption, and back-to-back saves are byte-identical
// (freeze-time stability).
[Collection("Sequential UOContent Tests")]
public class BaseCreatureSerializationTests : IDisposable
{
private readonly List<Mobile> _created = new();
public void Dispose()
{
for (var i = 0; i < _created.Count; i++)
{
_created[i].Delete();
}
}
private class CreatureStub : BaseCreature
{
public CreatureStub() : base(AIType.AI_Melee) => Body = 0xC9;
public CreatureStub(Serial serial) : base(serial) => Body = 0xC9;
// Stands in for the npc-speeds table (unconfigured in the test fixture).
public override void GetSpeeds(out double activeSpeed, out double passiveSpeed)
{
activeSpeed = 0.3;
passiveSpeed = 0.6;
}
public override void GetMoveSpeeds(out double activeMoveSpeed, out double passiveMoveSpeed)
{
activeMoveSpeed = 0.6;
passiveMoveSpeed = 1.2;
}
}
private CreatureStub NewCreature()
{
var bc = new CreatureStub();
_created.Add(bc);
return bc;
}
private static byte[] Snapshot(Mobile m)
{
var writer = new BufferWriter(true);
m.Serialize(writer);
var buffer = new byte[writer.Position];
writer.Buffer.AsSpan(0, (int)writer.Position).CopyTo(buffer);
return buffer;
}
private CreatureStub Load(byte[] buffer)
{
var copy = new CreatureStub(World.NewMobile);
_created.Add(copy);
var reader = new BufferReader(buffer);
copy.Deserialize(reader);
Assert.Equal(buffer.Length, reader.Position); // exact consumption
return copy;
}
[Fact]
public void DefaultCreature_RoundTrips_AndElidesEverything()
{
var bc = NewCreature();
var buffer = Snapshot(bc);
var copy = Load(buffer);
Assert.Equal(AIType.AI_Melee, copy.AI);
Assert.Equal(BaseCreature.DefaultRangePerception, copy.RangePerception);
Assert.Equal(0.3, copy.ActiveSpeed);
Assert.Equal(0.6, copy.PassiveSpeed);
Assert.Equal(0.6, copy.CurrentSpeed);
Assert.Equal(0.6, copy.ActiveMoveSpeed); // class None (no table in tests): restored from the wire
Assert.Equal(1.2, copy.PassiveMoveSpeed);
Assert.Equal(100, copy.PhysicalDamage);
Assert.Equal(BaseCreature.MaxLoyalty, copy.Loyalty);
Assert.Equal(1, copy.ControlSlots);
Assert.NotNull(copy.Owners);
Assert.Empty(copy.Owners);
}
[Fact]
public void BackToBackSaves_AreByteIdentical()
{
var bc = NewCreature();
bc.SetDamage(5, 10);
bc.PhysicalResistanceSeed = 25;
Assert.Equal(Snapshot(bc), Snapshot(bc));
}
[Fact]
public void PopulatedCreature_RoundTrips()
{
var bc = NewCreature();
var master = new PlayerMobile(World.NewMobile);
master.DefaultMobileInit();
World.AddEntity(master); // ReadEntity resolves the reference through the world table
_created.Add(master);
bc.Tamable = true;
bc.MinTameSkill = 47.1;
bc.SetControlMaster(master);
bc.Owners.Add(master);
bc.ControlOrder = OrderType.Guard;
bc.SetDamage(11, 17);
bc.SetSpeed(0.2, 0.4); // hand-tuned: no longer matches the stub table
bc.SetMoveSpeed(0.25, 0.5);
bc.PhysicalResistanceSeed = 40;
bc.EnergyResistSeed = 15;
bc.FireDamage = 25;
bc.PhysicalDamage = 75;
bc.HitsMaxSeed = 250;
bc.Loyalty = 55;
bc.Home = new Point3D(1000, 1100, 5);
bc.RangeHome = 4;
bc.Team = 3;
bc.IsBonded = true;
bc.BondingBegin = Core.Now;
bc.RemoveIfUntamed = true;
bc.RemoveStep = 2;
bc.CorpseNameOverride = "a test corpse";
var copy = Load(Snapshot(bc));
Assert.True(copy.Controlled);
Assert.Equal(master, copy.ControlMaster);
Assert.Equal(OrderType.Guard, copy.ControlOrder);
Assert.True(copy.Tamable);
Assert.Equal(47.1, copy.MinTameSkill);
Assert.Equal(11, copy.DamageMin);
Assert.Equal(17, copy.DamageMax);
Assert.Equal(0.2, copy.ActiveSpeed);
Assert.Equal(0.4, copy.PassiveSpeed);
Assert.Equal(0.25, copy.ActiveMoveSpeed);
Assert.Equal(0.5, copy.PassiveMoveSpeed);
Assert.Equal(40, copy.PhysicalResistanceSeed);
Assert.Equal(15, copy.EnergyResistSeed);
Assert.Equal(25, copy.FireDamage);
Assert.Equal(75, copy.PhysicalDamage);
Assert.Equal(250, copy.HitsMaxSeed);
Assert.Equal(55, copy.Loyalty);
Assert.Equal(new Point3D(1000, 1100, 5), copy.Home);
Assert.Equal(4, copy.RangeHome);
Assert.Equal(3, copy.Team);
Assert.True(copy.IsBonded);
Assert.Equal(bc.BondingBegin, copy.BondingBegin);
Assert.True(copy.RemoveIfUntamed);
Assert.Equal(2, copy.RemoveStep);
Assert.Equal("a test corpse", copy.CorpseNameOverride);
Assert.Equal(master, copy.LastOwner);
}
private sealed class BucketStub : BaseCreature
{
public BucketStub() : base(AIType.AI_Melee) => Body = 0xC9;
public BucketStub(Serial serial) : base(serial) => Body = 0xC9;
public override SpeedLevel DefaultSpeedClass => SpeedLevel.Fast;
}
[Fact]
public void SpeedClass_Assignment_AppliesBucket_AndRoundTrips()
{
NPCSpeeds.RegisterSpeed(new NPCSpeeds.SpeedClassEntry
{
Level = SpeedLevel.Fast, ActiveSpeed = 0.2, PassiveSpeed = 0.4,
ActiveMoveSpeed = 0.3, PassiveMoveSpeed = 0.9, Types = new HashSet<Type>()
});
NPCSpeeds.RegisterSpeed(new NPCSpeeds.SpeedClassEntry
{
Level = SpeedLevel.VeryFast, ActiveSpeed = 0.125, PassiveSpeed = 0.3,
ActiveMoveSpeed = 0.125, PassiveMoveSpeed = 0.6, Types = new HashSet<Type>()
});
var bc = new BucketStub();
_created.Add(bc);
Assert.Equal(0.2, bc.ActiveSpeed); // seeded from the default bucket
Assert.Equal(0.3, bc.ActiveMoveSpeed);
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.3, bc.PassiveSpeed);
Assert.Equal(0.125, bc.ActiveMoveSpeed);
Assert.Equal(0.6, bc.PassiveMoveSpeed);
Assert.Equal(0.3, bc.CurrentSpeed); // stayed in the passive mode
// The changed bucket persists; the (bucket-matching) speeds elide but restore
// through the new bucket - the consistency the stateful SpeedClass guarantees.
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);
Assert.Equal(buffer.Length, reader.Position);
Assert.Equal(SpeedLevel.VeryFast, copy.SpeedClass);
Assert.Equal(0.125, copy.ActiveSpeed);
Assert.Equal(0.3, copy.PassiveSpeed);
Assert.Equal(0.125, copy.ActiveMoveSpeed);
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.None, 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.None, 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);
}
}