Follow-up to #2721. It fixes how the remaining tests behave and changes no production code. ## CI - Both `dotnet test` steps pass `--filter "Category!=Manual"`. Before this, Manual-tagged tests ran on CI anyway. - The one surviving movement-throttle latency test is the only coverage of burst forgiveness over a full, wrapped history. It loses its Manual tag, so it runs on CI. ## Tests that passed on CI without testing anything - **Corpse and throwing tests.** They relied on tiledata for the weapon `Layer`, so on CI the equip silently failed and they asserted against empty lists. They now set `Layer` explicitly and assert the equip took effect. `TestThrown` also pins its own strength and range values, so a shard retuning SoulGlaive no longer breaks them. - **Healthbar poison rows.** Server.Tests registers no poisons, so every row tested `null`. They now use real poison levels. - **Account packet tests.** The mock accounts reported `Length` 0, so the character-list tests only ever encoded empty slots. They now encode the real characters. - **Pathing and AI tests that only mean something on real Trammel/Felucca terrain** are now guarded with `TileDataRequirement.SkipIfMissing()`: Britain desk and clutter scenes, house footprints (no MultiData on CI) and fingerprints. Tests that are valid on flat blank terrain stay unguarded. (A synthetic tiledata fixture to run these on CI is a planned follow-up.) - **Assertions that couldn't fail now check their claim.** Affected tests include the `DefaultWalker` cache use, cache on/off parity requiring a path, the recorder capability flags, the idle-pet gate, the monthly DST recurrence, the AdvancedSearch `Record.Exception` wrappers, `CanFitItem` with no branch-guarded assert, the Firewall callback, and `Import_NoMap`. The Geometry Parse asserts also now use the same input as their TryParse. ## Shared state Every test that changes process-wide state now restores it, even when an assert fails: - **The clock.** A new `TimerWheelScope` saves and restores `Core._tickCount`, `Core._now` and the timer wheel. `SimulatedClockScope` covers decay tests. - **StepCache settings and caches**, via `StepCacheStateScope`. - **Network state.** The ManualAllowlist, LoginAllowlist, firewall and BanChannel reporters are restored. Global packet handlers are now registered per test and removed afterwards, so they no longer stay registered for later tests. - **Data and config tables.** `ArchiveJournal` statics, `TileData.ItemTable`, `ObjectPropertyList.Enabled`, `Core.Expansion` and the localization table are restored. One `MobileStatusVersion` restore was resetting a different expansion than the one the test changed. - **Entities.** Tests with fixed serials now delete what they create. Fixed mobile serials moved to `World.NewMobile`, because deleting a hard-coded serial could unregister a live entity. - **Collection race.** `SpiralScanTests` no longer clears the spawn cache from a separate collection that could run in parallel with the cache-manager tests. - **Shared statics.** Classes reading shared static allowlists joined the sequential collection. ## Duplication New shared helpers replace copy-pasted harness code: - **Server.Tests:** `PacketAssert.Sends`, used by about 130 packet tests; `TestClientHarness`, `PersistencePipelineScope`, `NetTestHelpers`, `EntityCleanup` and one `TestMulti`. - **UOContent.Tests:** `TimerWheelScope`, which also holds one wraparound-safe `RunFor`/`RunUntil`; `AiTestStubs`, which merges six walker copies and five identical creature stubs; `MobileTestHarness`, `CommandTestHarness` and `TempDirectory`. Dead oracle packet classes are removed. ## Cost | Test | Before | After | |---|---|---| | `VirtualSerialTests` | 1.57s | 2ms (starts near the wrap via reflection instead of drawing ~17.9M serials) | | Event scheduler recurrence loop | 473ms | 1ms | | CrowdSec drain | 517ms | 16ms (event-driven instead of a fixed 500ms wait) | | AuthId tests | 248ms | 99ms (SHA2 except the two GameLogin tests) | | Container cap tests | 5,000 items | 3,449 / 3,277 (one past each protocol cap) | | Bulk removal | 100k children | 20k children | | OPL hash distribution | 60k builds | 10k builds | ## Shard-tuning decoupling - AI assertions read `ReacquireDelay` and `FollowMoveSpeed` instead of hard-coding production tuning. - The guard friendly-fire test uses an `AI_Mage` stub instead of WhiteWyrm. - Argon2 assertions pin the `argon2id` variant and `v=19` but read the cost parameters from the configured hasher. ## Tests | Run | Server.Tests | UOContent.Tests | |---|---|---| | With client files | 812 pass | 1,123 pass | | Without client files (CI simulation) | 800 pass, 12 skip | 1,021 pass, 102 skip | - The UOContent fixture reads `MODERNUO_TEST_DATA_DIR`; Server.Tests reads `MODERNUO_CLIENT_PATH`. - The 15 new UOContent skips are the map-dependent guards above. - Total run time with client files is about 5s, down from about 8s.
980 lines
36 KiB
C#
980 lines
36 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using Server;
|
|
using Server.Mobiles;
|
|
using Xunit;
|
|
|
|
namespace UOContent.Tests.Mobiles.AI;
|
|
|
|
[Collection("Sequential UOContent Tests")]
|
|
public class PetOrderTests : IDisposable
|
|
{
|
|
// Track and delete every mobile we spawn so they don't linger in the shared static World
|
|
// and pollute other tests in this collection (e.g. Tracking's nearby-mobile scan).
|
|
private readonly List<Mobile> _created = new();
|
|
|
|
private (PlayerMobile master, PetTestStub pet) Spawn(Point3D masterLoc, Point3D petLoc)
|
|
{
|
|
var pair = PetTestSetup.SpawnControlledPet(masterLoc, petLoc);
|
|
_created.Add(pair.master);
|
|
_created.Add(pair.pet);
|
|
return pair;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
foreach (var m in _created)
|
|
{
|
|
m?.Delete();
|
|
}
|
|
|
|
_created.Clear();
|
|
}
|
|
[Fact]
|
|
public void SetPersistentOrder_Stay_AnchorsHomeToCurrentLocation()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1005, 1000, 0));
|
|
|
|
pet.AIObject.SetPersistentOrder(OrderType.Stay);
|
|
|
|
Assert.Equal(OrderType.Stay, pet.AIObject.PersistentOrder);
|
|
Assert.Equal(pet.Location, pet.Home);
|
|
}
|
|
|
|
[Fact]
|
|
public void SetPersistentOrder_Follow_ClearsAnchor()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1005, 1000, 0));
|
|
pet.Home = new Point3D(900, 900, 0); // stale anchor
|
|
|
|
pet.AIObject.SetPersistentOrder(OrderType.Follow);
|
|
|
|
Assert.Equal(OrderType.Follow, pet.AIObject.PersistentOrder);
|
|
Assert.Equal(Point3D.Zero, pet.Home);
|
|
}
|
|
|
|
[Fact]
|
|
public void Stop_WhileAttacking_FallsBackToPersistentFollow()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1002, 1000, 0));
|
|
pet.ControlOrder = OrderType.Follow; // persistent = Follow
|
|
pet.ControlOrder = OrderType.Attack; // transient
|
|
Assert.Equal(OrderType.Follow, pet.AIObject.PersistentOrder);
|
|
|
|
pet.ControlOrder = OrderType.Stop;
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder); // resumed standing order
|
|
Assert.Equal(OrderType.Follow, pet.AIObject.PersistentOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public void Stop_WhileAttacking_ResumedFollowTargetsTheMaster()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.ControlTarget = master;
|
|
pet.ControlOrder = OrderType.Follow; // persistent = Follow
|
|
pet.ControlOrder = OrderType.Attack; // transient
|
|
Assert.Equal(OrderType.Follow, pet.AIObject.PersistentOrder);
|
|
|
|
pet.ControlOrder = OrderType.Stop;
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
Assert.Equal(master, pet.ControlTarget);
|
|
}
|
|
|
|
[Fact]
|
|
public void Stop_WhileAttacking_ResumedFollowKeepsAPetFriendAsItsTarget()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var friend = new PlayerMobile(World.NewMobile);
|
|
friend.DefaultMobileInit();
|
|
friend.MoveToWorld(new Point3D(1002, 1000, 0), pet.Map);
|
|
_created.Add(friend);
|
|
|
|
var victim = new PetTestStub();
|
|
victim.MoveToWorld(new Point3D(1003, 1000, 0), pet.Map);
|
|
_created.Add(victim);
|
|
|
|
pet.ControlTarget = friend; // a pet friend said "all follow me"
|
|
pet.ControlOrder = OrderType.Follow;
|
|
pet.ControlTarget = victim; // then "all kill" — the attack order takes the target
|
|
pet.ControlOrder = OrderType.Attack;
|
|
|
|
pet.ControlOrder = OrderType.Stop;
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
Assert.Equal(friend, pet.ControlTarget); // still the friend, not the owner
|
|
}
|
|
|
|
[Fact]
|
|
public void Stop_WhileAttacking_ResumedFollowSurvivesTheNextThink()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.ControlOrder = OrderType.Follow;
|
|
pet.ControlOrder = OrderType.Attack;
|
|
pet.ControlOrder = OrderType.Stop; // resumes Follow
|
|
|
|
pet.AIObject.Obey();
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder); // not dropped to idle
|
|
Assert.Equal(OrderType.Follow, pet.AIObject.PersistentOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public void Stop_WhileFollowing_CancelsToIdleNone()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1002, 1000, 0));
|
|
pet.ControlOrder = OrderType.Follow;
|
|
|
|
pet.ControlOrder = OrderType.Stop;
|
|
|
|
Assert.Equal(OrderType.None, pet.ControlOrder);
|
|
Assert.Equal(OrderType.None, pet.AIObject.PersistentOrder);
|
|
Assert.Equal(pet.Location, pet.Home); // idle anchor = where stopped
|
|
}
|
|
|
|
// Publish 51: "Stop: ... may wander." A stay is a standing order like follow or guard;
|
|
// stop cancels it and the pet idles, anchored where it stands.
|
|
[Fact]
|
|
public void Stop_WhileStaying_CancelsToIdleNone()
|
|
{
|
|
var post = new Point3D(1005, 1005, 0);
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), post);
|
|
pet.ControlOrder = OrderType.Stay; // Home = post
|
|
Assert.Equal(post, pet.Home);
|
|
|
|
pet.ControlOrder = OrderType.Stop;
|
|
|
|
Assert.Equal(OrderType.None, pet.ControlOrder);
|
|
Assert.Equal(OrderType.None, pet.AIObject.PersistentOrder);
|
|
Assert.Equal(pet.Location, pet.Home); // idle anchor = where stopped
|
|
}
|
|
|
|
// "all come" rests into Stay on arrival; "all stop" must not leave the pet at that post.
|
|
[Fact]
|
|
public void Come_ThenArrive_ThenStop_IdlesRatherThanStaying()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
Assert.Equal(OrderType.Come, pet.ControlOrder);
|
|
|
|
pet.AIObject.Obey(); // within 2 tiles -> Stay
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
|
|
pet.ControlOrder = OrderType.Stop;
|
|
|
|
Assert.Equal(OrderType.None, pet.ControlOrder);
|
|
Assert.Equal(OrderType.None, pet.AIObject.PersistentOrder);
|
|
Assert.Equal(pet.Location, pet.Home);
|
|
}
|
|
|
|
[Fact]
|
|
public void Stay_ThenFollow_ThenStop_DoesNotReturnToOldStayAnchor() // report 2
|
|
{
|
|
var postA = new Point3D(1005, 1005, 0);
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), postA);
|
|
pet.ControlOrder = OrderType.Stay; // Home = A
|
|
pet.ControlOrder = OrderType.Follow; // Home cleared to Zero
|
|
pet.MoveToWorld(new Point3D(1050, 1050, 0), pet.Map); // walked to B
|
|
pet.ControlOrder = OrderType.Stop; // stop while following
|
|
|
|
Assert.NotEqual(postA, pet.Home); // never re-acquires A
|
|
Assert.Equal(pet.Location, pet.Home); // idles at B
|
|
}
|
|
|
|
[Fact]
|
|
public void AttackTargetLost_ResumesPersistentFollow()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1002, 1000, 0));
|
|
pet.ControlOrder = OrderType.Follow; // persistent = Follow
|
|
pet.ControlOrder = OrderType.Attack;
|
|
pet.ControlTarget = null; // target gone
|
|
|
|
pet.AIObject.DoOrderAttack(); // invalid-target path
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public void StayPet_AttacksThenTargetLost_ReturnsToOriginalPost() // report 1
|
|
{
|
|
var post = new Point3D(1005, 1005, 0);
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), post);
|
|
pet.ControlOrder = OrderType.Stay; // persistent = Stay, Home = post
|
|
pet.ControlOrder = OrderType.Attack;
|
|
pet.MoveToWorld(new Point3D(1060, 1060, 0), pet.Map); // chased far to the "corpse"
|
|
pet.ControlTarget = null;
|
|
|
|
pet.AIObject.DoOrderAttack();
|
|
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
Assert.Equal(post, pet.Home); // anchor still the original post, not the corpse
|
|
}
|
|
|
|
// A step may be blocked by whatever terrain (1000, 1000) holds, but DoMoveImpl sets
|
|
// Mobile.Direction before attempting the move, so an *attempted* wander is observable via
|
|
// Direction either way. The subjective wander cadence is covered by manual QA; these tests
|
|
// verify the gate/frozen wiring deterministically.
|
|
[Fact]
|
|
public void IdlePet_DoesNotAttemptToMove_WhileResting()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1002, 1000, 0));
|
|
pet.ControlOrder = OrderType.Follow;
|
|
pet.ControlOrder = OrderType.Stop; // -> idle None
|
|
Assert.Equal(OrderType.None, pet.ControlOrder);
|
|
|
|
pet.ForceIdle = true; // CheckIdle() reports resting -> idle wander must be skipped
|
|
pet.Direction = Direction.North;
|
|
var start = pet.Location;
|
|
var ai = pet.AIObject;
|
|
for (var i = 0; i < 40; i++)
|
|
{
|
|
// Reopen the move clock every pass, so CheckIdle is the only gate left standing.
|
|
ai.NextMove = Core.TickCount;
|
|
Assert.True(ai.CanMoveNow(out _));
|
|
ai.DoOrderNone();
|
|
}
|
|
|
|
Assert.Equal(Direction.North, pet.Direction); // gated by CheckIdle -> never attempts a step
|
|
Assert.Equal(start, pet.Location);
|
|
}
|
|
|
|
[Fact]
|
|
public void StayingPet_DoesNotAttemptToMove()
|
|
{
|
|
var post = new Point3D(1005, 1005, 0);
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), post);
|
|
pet.ControlOrder = OrderType.Stay;
|
|
pet.Direction = Direction.North;
|
|
|
|
for (var i = 0; i < 40; i++)
|
|
{
|
|
pet.AIObject.DoOrderStay();
|
|
}
|
|
|
|
Assert.Equal(Direction.North, pet.Direction); // frozen -> no wander attempts
|
|
}
|
|
|
|
[Fact]
|
|
public void ReleaseOrder_ClearsTheMaster_AndStartsTheDeleteCountdown()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.IsBonded = true;
|
|
var followers = master.Followers;
|
|
|
|
pet.ControlOrder = OrderType.Release;
|
|
|
|
Assert.False(pet.Controlled);
|
|
Assert.Null(pet.ControlMaster);
|
|
Assert.False(pet.IsBonded);
|
|
Assert.Equal(followers - pet.ControlSlots, master.Followers);
|
|
Assert.True(pet.PendingDeleteTimer?.Running);
|
|
Assert.Equal(pet.Location, pet.Home);
|
|
}
|
|
|
|
[Fact]
|
|
public void Release_WithoutSpawner_AnchorsHomeToCurrentLocation()
|
|
{
|
|
var loc = new Point3D(1010, 1010, 0);
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), loc);
|
|
pet.ControlOrder = OrderType.Stay; // sets Home to loc
|
|
pet.Home = new Point3D(800, 800, 0); // simulate a stale anchor
|
|
pet.Spawner = null;
|
|
|
|
pet.ControlOrder = OrderType.Release;
|
|
|
|
Assert.Equal(loc, pet.Home); // released where it stands, not the stale point
|
|
}
|
|
|
|
[Fact]
|
|
public void Login_NearMaster_DerivesFollow()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
Assert.Equal(OrderType.None, pet.AIObject.PersistentOrder);
|
|
|
|
PetLoginHandler.DeriveFollowerOrders(master);
|
|
|
|
Assert.Equal(OrderType.Follow, pet.AIObject.PersistentOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public void Login_FarFromMaster_DerivesStay()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1040, 1000, 0));
|
|
|
|
PetLoginHandler.DeriveFollowerOrders(master);
|
|
|
|
Assert.Equal(OrderType.Stay, pet.AIObject.PersistentOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public void Login_RestoredStay_KeepsItsPostAnchor()
|
|
{
|
|
var post = new Point3D(1005, 1005, 0);
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), post);
|
|
pet.ControlOrder = OrderType.Stay; // Home = post
|
|
pet.ChangeAIType(pet.AI); // what AfterDeserialization does: fresh AI, PersistentOrder = None
|
|
Assert.Equal(OrderType.None, pet.AIObject.PersistentOrder);
|
|
|
|
PetLoginHandler.DeriveFollowerOrders(master); // master within 12 tiles
|
|
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
Assert.Equal(OrderType.Stay, pet.AIObject.PersistentOrder);
|
|
Assert.Equal(post, pet.Home); // not zeroed by a proximity-derived Follow
|
|
}
|
|
|
|
[Fact]
|
|
public void Login_RestoredNone_NearMaster_IssuesFollow()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.ControlOrder = OrderType.Follow;
|
|
pet.ControlOrder = OrderType.Stop; // -> None, no standing order
|
|
pet.ChangeAIType(pet.AI);
|
|
master.Hidden = true;
|
|
|
|
PetLoginHandler.DeriveFollowerOrders(master);
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
Assert.Same(master, pet.ControlTarget);
|
|
Assert.Equal(OrderType.Follow, pet.AIObject.PersistentOrder);
|
|
Assert.True(master.Hidden); // system-issued: nobody revealed
|
|
}
|
|
|
|
[Fact]
|
|
public void Login_RestoredAttack_FarFromMaster_IssuesStay()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1040, 1000, 0));
|
|
pet.ControlOrder = OrderType.Attack; // rests with no valid target
|
|
pet.ChangeAIType(pet.AI);
|
|
|
|
PetLoginHandler.DeriveFollowerOrders(master);
|
|
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
Assert.Equal(pet.Location, pet.Home);
|
|
}
|
|
|
|
[Fact]
|
|
public void Login_RestoredAttack_NearMaster_FollowsTheMaster_NotTheVictim()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var victim = SpawnPlayer(new Point3D(1003, 1000, 0));
|
|
pet.IssueOrder(OrderType.Attack, master, victim); // saved mid-fight: ControlTarget = victim
|
|
pet.ChangeAIType(pet.AI); // post-load fresh AI
|
|
|
|
PetLoginHandler.DeriveFollowerOrders(master);
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
Assert.Same(master, pet.ControlTarget);
|
|
Assert.Equal(OrderType.Follow, pet.AIObject.PersistentOrder);
|
|
}
|
|
// The parameterless ctor fully initializes a player; the Serial ctor leaves that to Deserialize.
|
|
private PlayerMobile SpawnPlayer(Point3D loc)
|
|
{
|
|
var pm = new PlayerMobile { Player = true };
|
|
pm.MoveToWorld(loc, Map.Felucca);
|
|
_created.Add(pm);
|
|
return pm;
|
|
}
|
|
|
|
// Administrative commands are not a change of what the pet is doing: it keeps fighting.
|
|
[Fact]
|
|
public void Drop_MidAttack_KeepsTheAttackAndItsTarget()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var victim = new PetTestStub();
|
|
victim.MoveToWorld(new Point3D(1003, 1000, 0), pet.Map);
|
|
_created.Add(victim);
|
|
|
|
pet.ControlOrder = OrderType.Follow; // standing order
|
|
pet.IssueOrder(OrderType.Attack, master, victim);
|
|
|
|
pet.IssueOrder(OrderType.Drop, master);
|
|
|
|
Assert.Equal(OrderType.Attack, pet.ControlOrder);
|
|
Assert.Same(victim, pet.ControlTarget);
|
|
Assert.Same(victim, pet.Combatant);
|
|
}
|
|
|
|
[Fact]
|
|
public void Rename_MidAttack_KeepsTheAttack()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var victim = new PetTestStub();
|
|
victim.MoveToWorld(new Point3D(1003, 1000, 0), pet.Map);
|
|
_created.Add(victim);
|
|
|
|
pet.ControlOrder = OrderType.Follow;
|
|
pet.IssueOrder(OrderType.Attack, master, victim);
|
|
|
|
pet.IssueOrder(OrderType.Rename, master);
|
|
|
|
Assert.Equal(OrderType.Attack, pet.ControlOrder);
|
|
Assert.Same(victim, pet.ControlTarget);
|
|
}
|
|
|
|
[Fact]
|
|
public void FriendRefusal_MidAttack_KeepsTheAttack()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var friend = SpawnPlayer(new Point3D(1002, 1000, 0));
|
|
pet.AddPetFriend(friend); // already a friend -> refusal
|
|
var victim = new PetTestStub();
|
|
victim.MoveToWorld(new Point3D(1003, 1000, 0), pet.Map);
|
|
_created.Add(victim);
|
|
|
|
pet.ControlOrder = OrderType.Follow;
|
|
pet.IssueOrder(OrderType.Attack, master, victim);
|
|
|
|
pet.IssueOrder(OrderType.Friend, master, friend);
|
|
|
|
Assert.Equal(OrderType.Attack, pet.ControlOrder);
|
|
Assert.Same(victim, pet.ControlTarget);
|
|
}
|
|
|
|
// Nothing to resume into: the interrupted attack's target is gone.
|
|
[Fact]
|
|
public void Drop_MidAttack_WithTheTargetGone_FallsBackToTheStandingOrder()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var victim = new PetTestStub();
|
|
victim.MoveToWorld(new Point3D(1003, 1000, 0), pet.Map);
|
|
_created.Add(victim);
|
|
|
|
pet.ControlOrder = OrderType.Follow;
|
|
pet.IssueOrder(OrderType.Attack, master, victim);
|
|
victim.Delete();
|
|
|
|
pet.IssueOrder(OrderType.Drop, master);
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
}
|
|
|
|
// Resuming an attack must not re-run the aggression that ordering it performed.
|
|
[Fact]
|
|
public void Drop_MidAttack_DoesNotRepeatTheHarm()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var victim = new PetTestStub();
|
|
victim.MoveToWorld(new Point3D(1003, 1000, 0), pet.Map);
|
|
_created.Add(victim);
|
|
|
|
pet.ControlOrder = OrderType.Follow;
|
|
pet.IssueOrder(OrderType.Attack, master, victim);
|
|
var aggressors = victim.Aggressors.Count;
|
|
|
|
pet.IssueOrder(OrderType.Drop, master);
|
|
|
|
Assert.Equal(aggressors, victim.Aggressors.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void Friend_Refused_RestsAtPersistentOrder_AndObeyDoesNotRepeat()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.ControlOrder = OrderType.Follow; // persistent = Follow
|
|
var friend = SpawnPlayer(new Point3D(1002, 1000, 0));
|
|
pet.AddPetFriend(friend); // already a friend -> refusal 1049691
|
|
|
|
pet.IssueOrder(OrderType.Friend, pet.ControlMaster, friend);
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder); // never rests at Friend
|
|
Assert.True(BaseAI.IsRestableOrder(pet.ControlOrder));
|
|
|
|
// Obey must not repeat the refusal.
|
|
pet.AIObject.Obey();
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public void Unfriend_OfNonFriend_RestsAtPersistentOrder()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.ControlOrder = OrderType.Stay; // persistent = Stay
|
|
var stranger = SpawnPlayer(new Point3D(1002, 1000, 0));
|
|
|
|
pet.IssueOrder(OrderType.Unfriend, pet.ControlMaster, stranger);
|
|
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
Assert.Equal(OrderType.Stay, pet.AIObject.PersistentOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public void Friend_Accepted_LeavesTheStandingOrderAlone()
|
|
{
|
|
var post = new Point3D(1001, 1000, 0);
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), post);
|
|
pet.ControlOrder = OrderType.Stay; // the owner's standing order, anchored at the post
|
|
var friend = SpawnPlayer(new Point3D(1002, 1000, 0));
|
|
|
|
pet.IssueOrder(OrderType.Friend, master, friend);
|
|
|
|
Assert.True(pet.IsPetFriend(friend));
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder); // still staying
|
|
Assert.Equal(OrderType.Stay, pet.AIObject.PersistentOrder); // owner's order not rewritten
|
|
Assert.Equal(post, pet.Home); // and not re-anchored
|
|
}
|
|
|
|
[Fact]
|
|
public void Unfriend_Accepted_LeavesTheStandingOrderAlone()
|
|
{
|
|
var post = new Point3D(1001, 1000, 0);
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), post);
|
|
var friend = SpawnPlayer(new Point3D(1002, 1000, 0));
|
|
pet.AddPetFriend(friend);
|
|
pet.ControlOrder = OrderType.Stay;
|
|
|
|
pet.IssueOrder(OrderType.Unfriend, master, friend);
|
|
|
|
Assert.False(pet.IsPetFriend(friend));
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
Assert.Equal(OrderType.Stay, pet.AIObject.PersistentOrder);
|
|
Assert.Equal(post, pet.Home);
|
|
}
|
|
|
|
[Fact]
|
|
public void Rename_RestsAtARestableOrder()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.ControlOrder = OrderType.Follow;
|
|
|
|
pet.IssueOrder(OrderType.Rename, master);
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
Assert.True(BaseAI.IsRestableOrder(pet.ControlOrder));
|
|
}
|
|
|
|
[Fact]
|
|
public void Drop_OnAPetThatCannotDrop_StillResolves()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.ControlOrder = OrderType.Stay;
|
|
pet.IsDeadPet = true; // refuses to drop
|
|
|
|
pet.IssueOrder(OrderType.Drop, master);
|
|
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public void Stop_ResolvesToARestableOrder_FromEveryPrevious()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
OrderType[] previousOrders = [OrderType.Come, OrderType.Attack, OrderType.Guard, OrderType.Follow, OrderType.Stay, OrderType.None];
|
|
|
|
for (var i = 0; i < previousOrders.Length; i++)
|
|
{
|
|
pet.ControlOrder = previousOrders[i];
|
|
pet.ControlOrder = OrderType.Stop;
|
|
Assert.True(BaseAI.IsRestableOrder(pet.ControlOrder));
|
|
Assert.NotEqual(OrderType.Stop, pet.ControlOrder);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void IssueOrder_RevealsTheIssuer_NeverTheMaster()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var friend = SpawnPlayer(new Point3D(1002, 1000, 0));
|
|
pet.AddPetFriend(friend);
|
|
master.Hidden = true;
|
|
friend.Hidden = true;
|
|
|
|
pet.IssueOrder(OrderType.Stay, friend);
|
|
|
|
Assert.False(friend.Hidden);
|
|
Assert.True(master.Hidden);
|
|
}
|
|
|
|
[Fact]
|
|
public void SystemIssuedOrder_RevealsNobody()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
master.Hidden = true;
|
|
|
|
pet.ControlOrder = OrderType.Follow; // raw assignment = system-issued
|
|
|
|
Assert.True(master.Hidden);
|
|
}
|
|
|
|
[Fact]
|
|
public void EndPickTarget_Attack_SetsCombatantAndFocus()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var victim = SpawnPlayer(new Point3D(1003, 1000, 0));
|
|
|
|
pet.AIObject.EndPickTarget(master, victim, OrderType.Attack);
|
|
|
|
Assert.Equal(OrderType.Attack, pet.ControlOrder);
|
|
Assert.Same(victim, pet.ControlTarget);
|
|
Assert.Same(victim, pet.Combatant);
|
|
Assert.Same(victim, pet.FocusMob);
|
|
Assert.True(pet.Warmode);
|
|
Assert.Equal(1, pet.CombatantSets); // the Issue phase is the only writer
|
|
|
|
pet.AIObject.Obey(); // the tick does not rewrite it
|
|
Assert.Equal(1, pet.CombatantSets);
|
|
}
|
|
|
|
[Fact]
|
|
public void ReIssuedAttack_OnTheSameTarget_DoesNotRewriteCombatantOrFlapWarmode()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var victim = SpawnPlayer(new Point3D(1003, 1000, 0));
|
|
|
|
pet.IssueOrder(OrderType.Attack, master, victim);
|
|
Assert.Equal(1, pet.CombatantSets);
|
|
Assert.True(pet.Warmode);
|
|
|
|
// Dropping Warmode would null Combatant and make the re-issue replay DoHarmful.
|
|
pet.IssueOrder(OrderType.Attack, master, victim);
|
|
|
|
Assert.Equal(1, pet.CombatantSets);
|
|
Assert.True(pet.Warmode);
|
|
Assert.Same(victim, pet.Combatant);
|
|
Assert.Same(victim, pet.FocusMob);
|
|
}
|
|
|
|
[Fact]
|
|
public void OrderedAttack_ReassertsTheCommandedTarget_AfterAnAggressorStealsCombatant()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var victim = SpawnPlayer(new Point3D(1003, 1000, 0));
|
|
var other = SpawnPlayer(new Point3D(1002, 1000, 0));
|
|
|
|
pet.IssueOrder(OrderType.Attack, master, victim);
|
|
|
|
// what OnAggressiveAction does
|
|
pet.Combatant = other;
|
|
Assert.Same(other, pet.Combatant);
|
|
|
|
pet.AIObject.Obey(); // the tick puts the kill order back on the commanded target
|
|
|
|
Assert.Same(victim, pet.Combatant);
|
|
Assert.Equal(OrderType.Attack, pet.ControlOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public void Rename_WhileFollowing_KeepsFollowingTheMaster()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.IssueOrder(OrderType.Follow, master, master);
|
|
|
|
pet.IssueOrder(OrderType.Rename, master); // the menu passes no target
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
Assert.Same(master, pet.ControlTarget); // restored, not null
|
|
|
|
pet.AIObject.Obey();
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder); // no "no one to follow" -> None
|
|
}
|
|
|
|
[Fact]
|
|
public void TransferRefused_ResumesFollowingTheMaster_NotTheRecipient()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.IssueOrder(OrderType.Follow, master, master);
|
|
var recipient = SpawnPlayer(new Point3D(1002, 1000, 0)); // no NetState -> the transfer is refused
|
|
|
|
pet.IssueOrder(OrderType.Transfer, master, recipient);
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
Assert.Same(master, pet.ControlTarget);
|
|
Assert.True(pet.Controlled);
|
|
Assert.Same(master, pet.ControlMaster);
|
|
}
|
|
|
|
[Fact]
|
|
public void SameOrderTwice_ReRunsIssue()
|
|
{
|
|
var postA = new Point3D(1005, 1005, 0);
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), postA);
|
|
pet.ControlOrder = OrderType.Stay; // Home = A
|
|
pet.MoveToWorld(new Point3D(1050, 1050, 0), pet.Map);
|
|
|
|
pet.ControlOrder = OrderType.Stay; // reissued: re-anchor
|
|
|
|
Assert.Equal(pet.Location, pet.Home);
|
|
}
|
|
|
|
[Fact]
|
|
public void LoyaltyRelease_AndManualRelease_ProduceTheSameEndState()
|
|
{
|
|
var (masterA, petA) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var (masterB, petB) = Spawn(new Point3D(1100, 1100, 0), new Point3D(1101, 1100, 0));
|
|
petA.Name = "Rex";
|
|
petB.Name = "Rex";
|
|
petA.IsBonded = true;
|
|
petB.IsBonded = true;
|
|
|
|
petA.IssueOrder(OrderType.Release, masterA); // player
|
|
petB.ControlOrder = OrderType.Release; // what the loyalty drain does
|
|
|
|
PetTestStub[] pets = [petA, petB];
|
|
|
|
for (var i = 0; i < pets.Length; i++)
|
|
{
|
|
var pet = pets[i];
|
|
Assert.False(pet.Controlled);
|
|
Assert.Null(pet.ControlMaster);
|
|
Assert.False(pet.IsBonded);
|
|
Assert.Null(pet.Name);
|
|
Assert.Equal(OrderType.None, pet.ControlOrder);
|
|
Assert.True(pet.PendingDeleteTimer?.Running);
|
|
Assert.Equal(pet.Location, pet.Home);
|
|
}
|
|
|
|
Assert.Equal(masterA.Followers, masterB.Followers);
|
|
}
|
|
|
|
[Fact]
|
|
public void SummonedPet_Released_IsKilledNotReleased()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.Summoned = true;
|
|
pet.Master = master;
|
|
|
|
pet.ControlOrder = OrderType.Release;
|
|
|
|
Assert.True(pet.Deleted || !pet.Alive);
|
|
}
|
|
|
|
[Fact]
|
|
public void Stop_WithNoStandingOrder_IdlesAnchoredWhereItStands()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
Assert.Equal(OrderType.Come, pet.ControlOrder); // fresh tame: no standing order, Home = Zero
|
|
Assert.Equal(Point3D.Zero, pet.Home);
|
|
|
|
pet.ControlOrder = OrderType.Stop; // what a vendor does after SetControlMaster(buyer)
|
|
|
|
Assert.Equal(OrderType.None, pet.ControlOrder);
|
|
Assert.Equal(pet.Location, pet.Home); // anchored: no unbounded wander
|
|
}
|
|
|
|
[Fact]
|
|
public void Release_ClearsFriendsAndTheStandingOrder()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var friend = SpawnPlayer(new Point3D(1002, 1000, 0));
|
|
pet.AddPetFriend(friend);
|
|
pet.ControlOrder = OrderType.Guard; // persistent = Guard
|
|
|
|
pet.IssueOrder(OrderType.Release, master);
|
|
|
|
Assert.False(pet.IsPetFriend(friend));
|
|
Assert.Equal(OrderType.None, pet.AIObject.PersistentOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public void PetDeath_IssuesFollowMaster_WithoutRevealingAnyone()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.IsBonded = true;
|
|
pet.ControlOrder = OrderType.Stay;
|
|
pet.ControlTarget = null;
|
|
master.Hidden = true;
|
|
|
|
pet.Kill(); // bonded pet death -> IsDeadPet, follows the master
|
|
|
|
Assert.True(pet.IsDeadPet);
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
Assert.Same(master, pet.ControlTarget);
|
|
Assert.True(master.Hidden);
|
|
Assert.False(pet.Warmode);
|
|
}
|
|
|
|
[Fact]
|
|
public void ObeyOnALegacyTransientOrder_FallsBackToPersistent()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.ControlOrder = OrderType.Follow; // persistent = Follow
|
|
|
|
// a pre-refactor save resting at Rename
|
|
var field = typeof(BaseCreature).GetField("_controlOrder", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
|
field!.SetValue(pet, OrderType.Rename);
|
|
Assert.Equal(OrderType.Rename, pet.ControlOrder);
|
|
|
|
pet.AIObject.Obey();
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public void SpeechCommand_FromAFriend_RevealsTheFriend_NotTheMaster()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var friend = SpawnPlayer(new Point3D(1002, 1000, 0));
|
|
pet.AddPetFriend(friend);
|
|
master.Hidden = true;
|
|
friend.Hidden = true;
|
|
|
|
// "all stay" keyword 0x170
|
|
pet.AIObject.OnSpeech(new SpeechEventArgs(friend, "all stay", MessageType.Regular, 0x3B2, [0x170]));
|
|
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
Assert.False(friend.Hidden);
|
|
Assert.True(master.Hidden);
|
|
}
|
|
|
|
[Fact]
|
|
public void ContextMenuCommand_FromAFriend_RevealsTheFriend_NotTheMaster()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var friend = SpawnPlayer(new Point3D(1002, 1000, 0));
|
|
pet.AddPetFriend(friend);
|
|
master.Hidden = true;
|
|
friend.Hidden = true;
|
|
|
|
new InternalEntry(3006114, 14, OrderType.Stay, true).OnClick(friend, pet); // Command: Stay
|
|
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
Assert.False(friend.Hidden);
|
|
Assert.True(master.Hidden);
|
|
}
|
|
|
|
[Fact]
|
|
public void ContextMenuCommand_FromAFriend_RefusesNonFriendOrders()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var friend = SpawnPlayer(new Point3D(1002, 1000, 0));
|
|
pet.AddPetFriend(friend);
|
|
pet.ControlOrder = OrderType.Follow;
|
|
|
|
new InternalEntry(3006107, 14, OrderType.Guard, true).OnClick(friend, pet); // Command: Guard
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public void ContextMenuRename_LeavesThePetOnARestableOrder()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.ControlOrder = OrderType.Follow;
|
|
|
|
new InternalEntry(3006098, 14, OrderType.Rename, true).OnClick(master, pet); // Rename
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
}
|
|
|
|
// Come with no standing order (fresh tame or post-load) must settle into one.
|
|
[Fact]
|
|
public void RestingCome_WithNoStandingOrder_SettlesIntoStayBesideTheMaster()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
Assert.Equal(OrderType.Come, pet.ControlOrder);
|
|
Assert.Equal(OrderType.None, pet.AIObject.PersistentOrder);
|
|
|
|
pet.AIObject.Obey(); // within 2 tiles -> Stay
|
|
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
Assert.Equal(OrderType.Stay, pet.AIObject.PersistentOrder);
|
|
Assert.Equal(pet.Location, pet.Home);
|
|
}
|
|
|
|
// Load never runs the Issue phase. Home (field 12) is read before ControlOrder (18); with an AI
|
|
// present, a setter-routed load would re-anchor Home to the restored Location.
|
|
[Fact]
|
|
public void ControlOrder_RoundTrips_AndLoadDoesNotRunIssue()
|
|
{
|
|
var post = new Point3D(1001, 1000, 0);
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), post);
|
|
pet.ControlOrder = OrderType.Stay; // Home = post
|
|
pet.MoveToWorld(new Point3D(1020, 1000, 0), pet.Map); // displaced: Home != Location
|
|
Assert.Equal(post, pet.Home);
|
|
|
|
var writer = new BufferWriter(true);
|
|
pet.Serialize(writer);
|
|
var buffer = new byte[writer.Position];
|
|
writer.Buffer.AsSpan(0, (int)writer.Position).CopyTo(buffer);
|
|
|
|
var copy = new PetTestStub(World.NewMobile);
|
|
_created.Add(copy);
|
|
copy.ChangeAIType(AIType.AI_Animal); // the Issue gate is ai != null
|
|
|
|
copy.Deserialize(new BufferReader(buffer));
|
|
|
|
Assert.Equal(OrderType.Stay, copy.ControlOrder);
|
|
Assert.Equal(new Point3D(1020, 1000, 0), copy.Location);
|
|
Assert.Equal(post, copy.Home); // not re-anchored
|
|
}
|
|
|
|
[Fact]
|
|
public void SpeechCommand_WithoutThePetsName_IsIgnored()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.Name = "Rex";
|
|
pet.ControlOrder = OrderType.Stay;
|
|
|
|
// bare "come" (keyword 0x155) with no name: not for this pet
|
|
pet.AIObject.OnSpeech(new SpeechEventArgs(master, "come", MessageType.Regular, 0x3B2, [0x155]));
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
|
|
pet.AIObject.OnSpeech(new SpeechEventArgs(master, "Rex come", MessageType.Regular, 0x3B2, [0x155]));
|
|
Assert.Equal(OrderType.Come, pet.ControlOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public void AllCommand_IssuesOnce()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.Name = "Rex";
|
|
pet.ControlOrder = OrderType.Follow;
|
|
var post = pet.Location;
|
|
|
|
// The client emits both 0x170 ("all stay") and 0x16F ("*stay") for "all stay".
|
|
pet.AIObject.OnSpeech(new SpeechEventArgs(master, "all stay", MessageType.Regular, 0x3B2, [0x170, 0x16F]));
|
|
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
Assert.Equal(post, pet.Home);
|
|
// The named keyword alone must be ignored: the speech starts with "all", not the name.
|
|
pet.MoveToWorld(new Point3D(1010, 1010, 0), pet.Map);
|
|
pet.AIObject.OnSpeech(new SpeechEventArgs(master, "all stay", MessageType.Regular, 0x3B2, [0x16F]));
|
|
Assert.Equal(post, pet.Home);
|
|
}
|
|
|
|
[Fact]
|
|
public void SpeechCommand_FromAFriend_CannotComeGuardOrDrop()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.Name = "Rex";
|
|
var friend = SpawnPlayer(new Point3D(1002, 1000, 0));
|
|
pet.AddPetFriend(friend);
|
|
pet.ControlOrder = OrderType.Stay;
|
|
var post = pet.Home;
|
|
|
|
pet.AIObject.OnSpeech(new SpeechEventArgs(friend, "Rex come", MessageType.Regular, 0x3B2, [0x155]));
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
|
|
pet.AIObject.OnSpeech(new SpeechEventArgs(friend, "Rex guard", MessageType.Regular, 0x3B2, [0x15C]));
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
Assert.Equal(OrderType.Stay, pet.AIObject.PersistentOrder);
|
|
|
|
pet.IsBonded = true; // CanDrop
|
|
pet.AIObject.OnSpeech(new SpeechEventArgs(friend, "Rex drop", MessageType.Regular, 0x3B2, [0x156]));
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
Assert.Equal(post, pet.Home); // never re-issued
|
|
|
|
// The friend can still Stay/Follow/Stop.
|
|
pet.AIObject.OnSpeech(new SpeechEventArgs(friend, "Rex follow me", MessageType.Regular, 0x3B2, [0x163]));
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
Assert.Same(friend, pet.ControlTarget);
|
|
}
|
|
|
|
[Fact]
|
|
public void GMAllObey_DoesNotTakeControlledPets()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.Name = "Rex";
|
|
var gm = SpawnPlayer(new Point3D(1002, 1000, 0));
|
|
gm.AccessLevel = AccessLevel.GameMaster;
|
|
|
|
// The mass form is for wild creatures; a controlled pet must be named.
|
|
pet.AIObject.OnSpeech(new SpeechEventArgs(gm, "all obey", MessageType.Regular, 0x3B2, []));
|
|
Assert.Same(master, pet.ControlMaster);
|
|
|
|
pet.AIObject.OnSpeech(new SpeechEventArgs(gm, "Rex obey", MessageType.Regular, 0x3B2, []));
|
|
Assert.Same(gm, pet.ControlMaster);
|
|
}
|
|
|
|
// Release is relinquishing control, not exerting it: no roll, so no loyalty either way.
|
|
// A creature nobody can command is not released either.
|
|
}
|