## Summary
Folds **113** hand-written `[SerializableProperty]` members into plain `[SerializableField]` declarations using the v4 setter hooks — value coercion/vetoes via `allowFieldChange`, post-change side effects via `fieldChanged` (whose `oldValue` parameter covers the old-house/old-sender unsubscribe patterns). Net **-450 lines** of setter boilerplate.
```cs
// before
[SerializableProperty(1)]
[CommandProperty(AccessLevel.GameMaster)]
public int Charges
{
get => _charges;
set
{
_charges = Math.Clamp(value, 0, MaxCharges);
InvalidateProperties();
this.MarkDirty();
}
}
// after
[SerializableField(1, allowFieldChange: nameof(AllowChargesChange))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
[InvalidateProperties]
private int _charges;
private bool AllowChargesChange(ref int value)
{
value = Math.Clamp(value, 0, MaxCharges);
return true;
}
```
## How sites were selected
A classifier parsed all 204 `[SerializableProperty]` sites and converted only those matching strict shapes: getter is exactly `get => _field;`, the assignment comes first (after at most an equality guard), and relocated side effects contain no `return`, no `value` mutation, and no field re-assignment. Everything else was left alone deliberately:
- **~34 custom getters** (fallback defaults like `_x == -1 ? Default : _x`, self-healing refs) — no setter hook can express these.
- **~35 pre-assignment logic** (durability Unscale/Scale sandwiches, old-state captures like PotionKeg's pile weight).
- **virtual/override members, name-mismatched backing fields (`m_`), exotic semantics** (guards' `Focus` does work on *equal* assignment; `ChampionSpawn.Active` never assigns its field).
Five sites the classifier refused were converted by hand where the hooks fit cleanly: `ReceiverCrystal.Sender`, `PlayerVendor.House`, `PlayerBarkeeper.House` (old-value unsubscribe via `oldValue`), `BaseSuit.AccessLevel` (its existing virtual `OnAccessLevelChanged` already had the exact callback shape), and `DyeTub.DyedHue` (a true veto: `AllowDyedHueChange(ref int value) => _redyable`).
## Verification
- Build: **0 errors, 0 warnings**.
- **Schema regeneration produces zero Migrations changes** — the conversion is wire- and schema-neutral by construction (same orders, types, and property names), and CI's schema diff check enforces it.
- **835 + 708 tests green.**
## Behavioral notes (all strict improvements, called out for review)
- Generated setters skip everything when the incoming value equals the current one; a few converted setters previously re-ran side effects on equal assignment (redundant `Update()`-style refreshes).
- Generated setters always `MarkDirty()` on change; several converted setters never did (e.g. `DyeTub.DyedHue`, `MorphItem` ranges) — their changes only persisted if something else dirtied the entity. Those latent persistence bugs are fixed by construction.
2196 lines
66 KiB
C#
2196 lines
66 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Runtime.CompilerServices;
|
||
using ModernUO.Serialization;
|
||
using Server.Collections;
|
||
using Server.Engines.Spawners;
|
||
using Server.Gumps;
|
||
using Server.Items;
|
||
using Server.Multis.Boats;
|
||
using Server.Network;
|
||
using CalcMoves = Server.Movement.Movement;
|
||
|
||
namespace Server.Multis
|
||
{
|
||
public enum BoatOrder
|
||
{
|
||
Move,
|
||
Course,
|
||
Single
|
||
}
|
||
|
||
[SerializationGenerator(4, false)]
|
||
public abstract partial class BaseBoat : BaseMulti
|
||
{
|
||
public enum DryDockResult
|
||
{
|
||
Valid,
|
||
Dead,
|
||
NoKey,
|
||
NotAnchored,
|
||
Mobiles,
|
||
Items,
|
||
Hold,
|
||
Decaying
|
||
}
|
||
|
||
private static readonly Rectangle2D[] BritWrap =
|
||
{ new(16, 16, 5120 - 32, 4096 - 32), new(5136, 2320, 992, 1760) };
|
||
private static readonly Rectangle2D[] IlshWrap = { new(16, 16, 2304 - 32, 1600 - 32) };
|
||
private static readonly Rectangle2D[] TokunoWrap = { new(16, 16, 1448 - 32, 1448 - 32) };
|
||
|
||
private static readonly TimeSpan BoatDecayDelay = TimeSpan.FromDays(9.0);
|
||
|
||
private static readonly TimeSpan SlowInterval = TimeSpan.FromSeconds(NewBoatMovement ? 0.50 : 0.75);
|
||
private static readonly TimeSpan FastInterval = TimeSpan.FromSeconds(NewBoatMovement ? 0.25 : 0.75);
|
||
|
||
private const int SlowSpeed = 1;
|
||
private static readonly int FastSpeed = NewBoatMovement ? 1 : 3;
|
||
|
||
private static readonly TimeSpan SlowDriftInterval = TimeSpan.FromSeconds(NewBoatMovement ? 0.50 : 1.50);
|
||
private static readonly TimeSpan FastDriftInterval = TimeSpan.FromSeconds(NewBoatMovement ? 0.25 : 0.75);
|
||
|
||
private const int SlowDriftSpeed = 1;
|
||
private const int FastDriftSpeed = 1;
|
||
|
||
private const Direction Forward = Direction.North;
|
||
private const Direction ForwardLeft = Direction.Up;
|
||
private const Direction ForwardRight = Direction.Right;
|
||
private const Direction Backward = Direction.South;
|
||
private const Direction BackwardLeft = Direction.Left;
|
||
private const Direction BackwardRight = Direction.Down;
|
||
private const Direction Left = Direction.West;
|
||
private const Direction Right = Direction.East;
|
||
private const Direction Port = Left;
|
||
private const Direction Starboard = Right;
|
||
|
||
[SerializableField(0)]
|
||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||
private MapItem _mapItem;
|
||
|
||
[SerializableField(1)]
|
||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||
private int _nextNavPoint;
|
||
|
||
[SerializableField(4)]
|
||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||
private Mobile _owner;
|
||
|
||
[SerializableField(5)]
|
||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||
private Plank _pPlank;
|
||
|
||
[SerializableField(6)]
|
||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||
private Plank _sPlank;
|
||
|
||
[SerializableField(7)]
|
||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||
private TillerMan _tillerMan;
|
||
|
||
[SerializableField(8)]
|
||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||
private Hold _hold;
|
||
|
||
[SerializableField(9)]
|
||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||
private bool _anchored;
|
||
|
||
private int _clientSpeed;
|
||
private bool _decaying;
|
||
|
||
private TurnTimer _turnTimer;
|
||
private MoveTimer _moveTimer;
|
||
|
||
public BaseBoat() : base(0x0)
|
||
{
|
||
_timeOfDecay = Core.Now + BoatDecayDelay;
|
||
|
||
TillerMan = new TillerMan(this);
|
||
Hold = new Hold(this);
|
||
|
||
PPlank = new Plank(this, PlankSide.Port, 0);
|
||
SPlank = new Plank(this, PlankSide.Starboard, 0);
|
||
|
||
PPlank.MoveToWorld(new Point3D(X + PortOffset.X, Y + PortOffset.Y, Z), Map);
|
||
SPlank.MoveToWorld(new Point3D(X + StarboardOffset.X, Y + StarboardOffset.Y, Z), Map);
|
||
|
||
Facing = Direction.North;
|
||
|
||
NextNavPoint = -1;
|
||
|
||
Movable = false;
|
||
|
||
Boats.Add(this);
|
||
}
|
||
|
||
[SerializableProperty(2)]
|
||
[CommandProperty(AccessLevel.GameMaster)]
|
||
public Direction Facing
|
||
{
|
||
get => _facing;
|
||
set
|
||
{
|
||
SetFacing(value);
|
||
this.MarkDirty();
|
||
}
|
||
}
|
||
|
||
[SerializableField(3, fieldChanged: nameof(OnTimeOfDecayChanged))]
|
||
[DeltaDateTime]
|
||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||
private DateTime _timeOfDecay;
|
||
|
||
private void OnTimeOfDecayChanged(DateTime oldValue, DateTime newValue)
|
||
{
|
||
TillerMan?.InvalidateProperties();
|
||
}
|
||
|
||
[SerializableField(10, fieldChanged: nameof(OnShipNameChanged))]
|
||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||
private string _shipName;
|
||
|
||
private void OnShipNameChanged(string oldValue, string newValue)
|
||
{
|
||
TillerMan?.InvalidateProperties();
|
||
}
|
||
|
||
[CommandProperty(AccessLevel.GameMaster)]
|
||
public Direction Moving { get; set; }
|
||
|
||
[CommandProperty(AccessLevel.GameMaster)]
|
||
public bool IsMoving => _moveTimer?.Running == true;
|
||
|
||
[CommandProperty(AccessLevel.GameMaster)]
|
||
public int Speed { get; set; }
|
||
|
||
[CommandProperty(AccessLevel.GameMaster)]
|
||
public BoatOrder Order { get; set; }
|
||
|
||
public int Status =>
|
||
(Core.Now - (TimeOfDecay - BoatDecayDelay)) switch
|
||
{
|
||
var start when start < TimeSpan.FromHours(1) => 1043010, // This structure is like new.
|
||
var start when start < TimeSpan.FromDays(2) => 1043011, // This structure is slightly worn.
|
||
var start when start < TimeSpan.FromDays(3) => 1043012, // This structure is somewhat worn.
|
||
var start when start < TimeSpan.FromDays(4) => 1043013, // This structure is fairly worn.
|
||
var start when start < TimeSpan.FromDays(5) => 1043014, // This structure is greatly worn.
|
||
_ => 1043015 // This structure is in danger of collapsing.
|
||
};
|
||
|
||
public virtual int NorthID => 0;
|
||
public virtual int EastID => 0;
|
||
public virtual int SouthID => 0;
|
||
public virtual int WestID => 0;
|
||
|
||
public virtual int HoldDistance => 0;
|
||
public virtual int TillerManDistance => 0;
|
||
public virtual Point2D StarboardOffset => Point2D.Zero;
|
||
public virtual Point2D PortOffset => Point2D.Zero;
|
||
public virtual Point3D MarkOffset => Point3D.Zero;
|
||
|
||
public virtual BaseDockedBoat DockedBoat => null;
|
||
|
||
public static List<BaseBoat> Boats { get; } = new();
|
||
|
||
/*
|
||
* Intervals:
|
||
* drift forward
|
||
* fast | 0.25| 0.25
|
||
* slow | 0.50| 0.50
|
||
*
|
||
* Speed:
|
||
* drift forward
|
||
* fast | 0x4| 0x4
|
||
* slow | 0x3| 0x3
|
||
*
|
||
* Tiles (per interval):
|
||
* drift forward
|
||
* fast | 1| 1
|
||
* slow | 1| 1
|
||
*
|
||
* 'walking' in piloting mode has a 1s interval, speed 0x2
|
||
*/
|
||
|
||
private static bool NewBoatMovement => Core.HS;
|
||
|
||
public override bool HandlesOnSpeech => true;
|
||
|
||
public override bool AllowsRelativeDrop => true;
|
||
|
||
public static BaseBoat FindBoatAt(Point3D loc, Map map)
|
||
{
|
||
foreach (var boat in map.GetMultisInSector<BaseBoat>(loc))
|
||
{
|
||
if (boat.Contains(loc.X, loc.Y))
|
||
{
|
||
return boat;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
public Point3D GetRotatedLocation(int x, int y)
|
||
{
|
||
var p = new Point3D(X + x, Y + y, Z);
|
||
|
||
return Rotate(p, (int)_facing / 2);
|
||
}
|
||
|
||
public void UpdateComponents()
|
||
{
|
||
if (PPlank != null)
|
||
{
|
||
PPlank.MoveToWorld(GetRotatedLocation(PortOffset.X, PortOffset.Y), Map);
|
||
PPlank.SetFacing(_facing);
|
||
}
|
||
|
||
if (SPlank != null)
|
||
{
|
||
SPlank.MoveToWorld(GetRotatedLocation(StarboardOffset.X, StarboardOffset.Y), Map);
|
||
SPlank.SetFacing(_facing);
|
||
}
|
||
|
||
var xOffset = 0;
|
||
var yOffset = 0;
|
||
CalcMoves.Offset(_facing, ref xOffset, ref yOffset);
|
||
|
||
if (TillerMan != null)
|
||
{
|
||
TillerMan.Location = new Point3D(
|
||
X + xOffset * TillerManDistance + (_facing == Direction.North ? 1 : 0),
|
||
Y + yOffset * TillerManDistance,
|
||
TillerMan.Z
|
||
);
|
||
TillerMan.SetFacing(_facing);
|
||
TillerMan.InvalidateProperties();
|
||
}
|
||
|
||
if (Hold != null)
|
||
{
|
||
Hold.Location = new Point3D(X + xOffset * HoldDistance, Y + yOffset * HoldDistance, Hold.Z);
|
||
Hold.SetFacing(_facing);
|
||
}
|
||
}
|
||
|
||
private void Deserialize(IGenericReader reader, int version)
|
||
{
|
||
MapItem = (MapItem)reader.ReadEntity<Item>();
|
||
NextNavPoint = reader.ReadInt();
|
||
_facing = (Direction)reader.ReadInt();
|
||
_timeOfDecay = reader.ReadDeltaTime();
|
||
Owner = reader.ReadEntity<Mobile>();
|
||
PPlank = reader.ReadEntity<Plank>();
|
||
SPlank = reader.ReadEntity<Plank>();
|
||
TillerMan = reader.ReadEntity<TillerMan>();
|
||
Hold = reader.ReadEntity<Hold>();
|
||
Anchored = reader.ReadBool();
|
||
_shipName = reader.ReadString();
|
||
}
|
||
|
||
[AfterDeserialization(false)]
|
||
private void AfterDeserialization()
|
||
{
|
||
Boats.Add(this);
|
||
CheckDecay();
|
||
}
|
||
|
||
public void RemoveKeys(Mobile m)
|
||
{
|
||
uint keyValue = 0;
|
||
|
||
if (PPlank != null)
|
||
{
|
||
keyValue = PPlank.KeyValue;
|
||
}
|
||
|
||
if (keyValue == 0 && SPlank != null)
|
||
{
|
||
keyValue = SPlank.KeyValue;
|
||
}
|
||
|
||
Key.RemoveKeys(m, keyValue);
|
||
}
|
||
|
||
public uint CreateKeys(Mobile m)
|
||
{
|
||
var value = Key.RandomValue();
|
||
|
||
var packKey = new Key(KeyType.Gold, value, this);
|
||
var bankKey = new Key(KeyType.Gold, value, this);
|
||
|
||
packKey.MaxRange = 10;
|
||
bankKey.MaxRange = 10;
|
||
|
||
packKey.Name = "a ship key";
|
||
bankKey.Name = "a ship key";
|
||
|
||
var box = m.BankBox;
|
||
|
||
if (!box.TryDropItem(m, bankKey, false))
|
||
{
|
||
bankKey.Delete();
|
||
}
|
||
else
|
||
{
|
||
m.LocalOverheadMessage(MessageType.Regular, 0x3B2, 502484); // A ship's key is now in my safety deposit box.
|
||
}
|
||
|
||
if (m.AddToBackpack(packKey))
|
||
{
|
||
m.LocalOverheadMessage(MessageType.Regular, 0x3B2, 502485); // A ship's key is now in my backpack.
|
||
}
|
||
else
|
||
{
|
||
m.LocalOverheadMessage(MessageType.Regular, 0x3B2, 502483); // A ship's key is now at my feet.
|
||
}
|
||
|
||
return value;
|
||
}
|
||
|
||
public override void OnAfterDelete()
|
||
{
|
||
TillerMan?.Delete();
|
||
Hold?.Delete();
|
||
PPlank?.Delete();
|
||
SPlank?.Delete();
|
||
_turnTimer?.Stop();
|
||
_moveTimer?.Stop();
|
||
|
||
Boats.Remove(this);
|
||
}
|
||
|
||
public override void OnLocationChange(Point3D old)
|
||
{
|
||
base.OnLocationChange(old);
|
||
|
||
TillerMan?.Location = new Point3D(
|
||
X + (TillerMan.X - old.X),
|
||
Y + (TillerMan.Y - old.Y),
|
||
Z + (TillerMan.Z - old.Z)
|
||
);
|
||
|
||
Hold?.Location = new Point3D(X + (Hold.X - old.X), Y + (Hold.Y - old.Y), Z + (Hold.Z - old.Z));
|
||
PPlank?.Location = new Point3D(X + (PPlank.X - old.X), Y + (PPlank.Y - old.Y), Z + (PPlank.Z - old.Z));
|
||
SPlank?.Location = new Point3D(X + (SPlank.X - old.X), Y + (SPlank.Y - old.Y), Z + (SPlank.Z - old.Z));
|
||
}
|
||
|
||
public override void OnMapChange()
|
||
{
|
||
base.OnMapChange();
|
||
|
||
TillerMan?.Map = Map;
|
||
Hold?.Map = Map;
|
||
PPlank?.Map = Map;
|
||
SPlank?.Map = Map;
|
||
}
|
||
|
||
public bool CanCommand(Mobile m) => true;
|
||
|
||
public Point3D GetMarkedLocation()
|
||
{
|
||
var p = new Point3D(X + MarkOffset.X, Y + MarkOffset.Y, Z + MarkOffset.Z);
|
||
|
||
return Rotate(p, (int)_facing / 2);
|
||
}
|
||
|
||
public bool CheckKey(uint keyValue) => SPlank?.KeyValue == keyValue || PPlank?.KeyValue == keyValue;
|
||
|
||
public void Refresh()
|
||
{
|
||
_timeOfDecay = Core.Now + BoatDecayDelay;
|
||
|
||
TillerMan?.InvalidateProperties();
|
||
}
|
||
|
||
private bool ShouldDecay => !IsMoving && Core.Now >= _timeOfDecay;
|
||
|
||
public bool CheckDecay()
|
||
{
|
||
if (_decaying)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
if (ShouldDecay)
|
||
{
|
||
new DecayTimer(this).Start();
|
||
_decaying = true;
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
public bool LowerAnchor(bool message)
|
||
{
|
||
if (CheckDecay())
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (Anchored)
|
||
{
|
||
if (message)
|
||
{
|
||
TillerMan?.Say(501445); // Ar, the anchor was already dropped sir.
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
StopMove(false);
|
||
|
||
Anchored = true;
|
||
|
||
if (message)
|
||
{
|
||
TillerMan?.Say(501444); // Ar, anchor dropped sir.
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
public bool RaiseAnchor(bool message)
|
||
{
|
||
if (CheckDecay())
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (!Anchored)
|
||
{
|
||
if (message)
|
||
{
|
||
TillerMan?.Say(501447); // Ar, the anchor has not been dropped sir.
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
Anchored = false;
|
||
|
||
if (message)
|
||
{
|
||
TillerMan?.Say(501446); // Ar, anchor raised sir.
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
public bool StartMove(Direction dir, bool fast)
|
||
{
|
||
if (CheckDecay())
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var drift = dir != Forward && dir != ForwardLeft && dir != ForwardRight;
|
||
var interval = fast ? drift ? FastDriftInterval : FastInterval :
|
||
drift ? SlowDriftInterval : SlowInterval;
|
||
var speed = fast ? drift ? FastDriftSpeed : FastSpeed :
|
||
drift ? SlowDriftSpeed : SlowSpeed;
|
||
var clientSpeed = fast ? 0x4 : 0x3;
|
||
|
||
if (StartMove(dir, speed, clientSpeed, interval, false, true))
|
||
{
|
||
TillerMan?.Say(501429); // Aye aye sir.
|
||
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
public bool OneMove(Direction dir)
|
||
{
|
||
if (CheckDecay())
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var drift = dir != Forward;
|
||
var interval = drift ? FastDriftInterval : FastInterval;
|
||
var speed = drift ? FastDriftSpeed : FastSpeed;
|
||
|
||
if (StartMove(dir, speed, 0x1, interval, true, true))
|
||
{
|
||
TillerMan?.Say(501429); // Aye aye sir.
|
||
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
public void BeginRename(Mobile from)
|
||
{
|
||
if (CheckDecay())
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (from.AccessLevel < AccessLevel.GameMaster && from != Owner)
|
||
{
|
||
TillerMan?.Say(
|
||
Utility.Random(
|
||
1042876,
|
||
4
|
||
)
|
||
); // Arr, don't do that! | Arr, leave me alone! | Arr, watch what thour'rt doing, matey! | Arr! Do that again and I’ll throw ye overhead!
|
||
|
||
return;
|
||
}
|
||
|
||
TillerMan?.Say(502580); // What dost thou wish to name thy ship?
|
||
|
||
from.Prompt = new RenameBoatPrompt(this);
|
||
}
|
||
|
||
public void EndRename(Mobile from, string newName)
|
||
{
|
||
if (Deleted || CheckDecay())
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (from.AccessLevel < AccessLevel.GameMaster && from != Owner)
|
||
{
|
||
TillerMan?.Say(1042880); // Arr! Only the owner of the ship may change its name!
|
||
|
||
return;
|
||
}
|
||
|
||
if (!from.Alive)
|
||
{
|
||
TillerMan?.Say(502582); // You appear to be dead.
|
||
|
||
return;
|
||
}
|
||
|
||
Rename(newName.Trim().DefaultIfNullOrEmpty(null));
|
||
}
|
||
|
||
public DryDockResult CheckDryDock(Mobile from)
|
||
{
|
||
if (CheckDecay())
|
||
{
|
||
return DryDockResult.Decaying;
|
||
}
|
||
|
||
if (!from.Alive)
|
||
{
|
||
return DryDockResult.Dead;
|
||
}
|
||
|
||
var pack = from.Backpack;
|
||
if ((SPlank == null || !Key.ContainsKey(pack, SPlank.KeyValue)) &&
|
||
(PPlank == null || !Key.ContainsKey(pack, PPlank.KeyValue)))
|
||
{
|
||
return DryDockResult.NoKey;
|
||
}
|
||
|
||
if (!Anchored)
|
||
{
|
||
return DryDockResult.NotAnchored;
|
||
}
|
||
|
||
if (Hold?.Items.Count > 0)
|
||
{
|
||
return DryDockResult.Hold;
|
||
}
|
||
|
||
var map = Map;
|
||
|
||
if (map == null || map == Map.Internal)
|
||
{
|
||
return DryDockResult.Items;
|
||
}
|
||
|
||
foreach (var ent in GetMovingEntities())
|
||
{
|
||
return ent is Mobile ? DryDockResult.Mobiles : DryDockResult.Items;
|
||
}
|
||
|
||
return DryDockResult.Valid;
|
||
}
|
||
|
||
public void BeginDryDock(Mobile from)
|
||
{
|
||
if (CheckDecay())
|
||
{
|
||
return;
|
||
}
|
||
|
||
var result = CheckDryDock(from);
|
||
|
||
if (result == DryDockResult.Dead)
|
||
{
|
||
from.SendLocalizedMessage(502493); // You appear to be dead.
|
||
}
|
||
else if (result == DryDockResult.NoKey)
|
||
{
|
||
from.SendLocalizedMessage(502494); // You must have a key to the ship to dock the boat.
|
||
}
|
||
else if (result == DryDockResult.NotAnchored)
|
||
{
|
||
from.SendLocalizedMessage(1010570); // You must lower the anchor to dock the boat.
|
||
}
|
||
else if (result == DryDockResult.Mobiles)
|
||
{
|
||
from.SendLocalizedMessage(502495); // You cannot dock the ship with beings on board!
|
||
}
|
||
else if (result == DryDockResult.Items)
|
||
{
|
||
from.SendLocalizedMessage(502496); // You cannot dock the ship with a cluttered deck.
|
||
}
|
||
else if (result == DryDockResult.Hold)
|
||
{
|
||
from.SendLocalizedMessage(502497); // Make sure your hold is empty, and try again!
|
||
}
|
||
else if (result == DryDockResult.Valid)
|
||
{
|
||
from.SendGump(new ConfirmDryDockGump(this));
|
||
}
|
||
}
|
||
|
||
public void EndDryDock(Mobile from)
|
||
{
|
||
if (Deleted || CheckDecay())
|
||
{
|
||
return;
|
||
}
|
||
|
||
var result = CheckDryDock(from);
|
||
|
||
if (result == DryDockResult.Dead)
|
||
{
|
||
from.SendLocalizedMessage(502493); // You appear to be dead.
|
||
}
|
||
else if (result == DryDockResult.NoKey)
|
||
{
|
||
from.SendLocalizedMessage(502494); // You must have a key to the ship to dock the boat.
|
||
}
|
||
else if (result == DryDockResult.NotAnchored)
|
||
{
|
||
from.SendLocalizedMessage(1010570); // You must lower the anchor to dock the boat.
|
||
}
|
||
else if (result == DryDockResult.Mobiles)
|
||
{
|
||
from.SendLocalizedMessage(502495); // You cannot dock the ship with beings on board!
|
||
}
|
||
else if (result == DryDockResult.Items)
|
||
{
|
||
from.SendLocalizedMessage(502496); // You cannot dock the ship with a cluttered deck.
|
||
}
|
||
else if (result == DryDockResult.Hold)
|
||
{
|
||
from.SendLocalizedMessage(502497); // Make sure your hold is empty, and try again!
|
||
}
|
||
|
||
if (result != DryDockResult.Valid)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var boat = DockedBoat;
|
||
|
||
if (boat == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
RemoveKeys(from);
|
||
|
||
from.AddToBackpack(boat);
|
||
Delete();
|
||
}
|
||
|
||
public void SetName(SpeechEventArgs e)
|
||
{
|
||
if (CheckDecay())
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (e.Mobile.AccessLevel < AccessLevel.GameMaster && e.Mobile != Owner)
|
||
{
|
||
TillerMan?.Say(1042880); // Arr! Only the owner of the ship may change its name!
|
||
|
||
return;
|
||
}
|
||
|
||
if (!e.Mobile.Alive)
|
||
{
|
||
TillerMan?.Say(502582); // You appear to be dead.
|
||
|
||
return;
|
||
}
|
||
|
||
if (e.Speech.Length > 8)
|
||
{
|
||
var newName = e.Speech.AsSpan(8).Trim();
|
||
Rename(newName.Length == 0 ? null : newName.ToString());
|
||
}
|
||
}
|
||
|
||
public void Rename(string newName)
|
||
{
|
||
if (CheckDecay())
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (newName?.Length > 40)
|
||
{
|
||
newName = newName[..40];
|
||
}
|
||
|
||
if (_shipName == newName)
|
||
{
|
||
TillerMan?.Say(502531); // Yes, sir.
|
||
|
||
return;
|
||
}
|
||
|
||
ShipName = newName;
|
||
|
||
if (TillerMan != null && _shipName != null)
|
||
{
|
||
TillerMan.Say(1042885, _shipName); // This ship is now called the ~1_NEW_SHIP_NAME~.
|
||
}
|
||
else
|
||
{
|
||
TillerMan?.Say(502534); // This ship now has no name.
|
||
}
|
||
}
|
||
|
||
public void RemoveName(Mobile m)
|
||
{
|
||
if (CheckDecay())
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (m.AccessLevel < AccessLevel.GameMaster && m != Owner)
|
||
{
|
||
TillerMan?.Say(1042880); // Arr! Only the owner of the ship may change its name!
|
||
|
||
return;
|
||
}
|
||
|
||
if (!m.Alive)
|
||
{
|
||
TillerMan?.Say(502582); // You appear to be dead.
|
||
|
||
return;
|
||
}
|
||
|
||
if (_shipName == null)
|
||
{
|
||
TillerMan?.Say(502526); // Ar, this ship has no name.
|
||
|
||
return;
|
||
}
|
||
|
||
ShipName = null;
|
||
|
||
TillerMan?.Say(502534); // This ship now has no name.
|
||
}
|
||
|
||
public void GiveName(Mobile m)
|
||
{
|
||
if (TillerMan == null || CheckDecay())
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (_shipName == null)
|
||
{
|
||
TillerMan.Say(502526); // Ar, this ship has no name.
|
||
}
|
||
else
|
||
{
|
||
TillerMan.Say(1042881, _shipName); // This is the ~1_BOAT_NAME~.
|
||
}
|
||
}
|
||
|
||
public void GiveNavPoint()
|
||
{
|
||
if (TillerMan == null || CheckDecay())
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (NextNavPoint < 0)
|
||
{
|
||
TillerMan.Say(1042882); // I have no current nav point.
|
||
}
|
||
else
|
||
{
|
||
TillerMan.Say(
|
||
1042883,
|
||
(NextNavPoint + 1).ToString()
|
||
); // My current destination navpoint is nav ~1_NAV_POINT_NUM~.
|
||
}
|
||
}
|
||
|
||
public void AssociateMap(MapItem map)
|
||
{
|
||
if (CheckDecay())
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (map is BlankMap)
|
||
{
|
||
TillerMan?.Say(502575); // Ar, that is not a map, tis but a blank piece of paper!
|
||
}
|
||
else if (map.Pins.Count == 0)
|
||
{
|
||
TillerMan?.Say(502576); // Arrrr, this map has no course on it!
|
||
}
|
||
else
|
||
{
|
||
StopMove(false);
|
||
|
||
MapItem = map;
|
||
NextNavPoint = -1;
|
||
|
||
TillerMan?.Say(502577); // A map!
|
||
}
|
||
}
|
||
|
||
public bool StartCourse(string navPoint, bool single, bool message)
|
||
{
|
||
var number = -1;
|
||
|
||
var start = -1;
|
||
for (var i = 0; i < navPoint.Length; i++)
|
||
{
|
||
if (char.IsDigit(navPoint[i]))
|
||
{
|
||
start = i;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (start != -1)
|
||
{
|
||
var sNumber = navPoint[start..];
|
||
|
||
if (!int.TryParse(sNumber, out number))
|
||
{
|
||
number = -1;
|
||
}
|
||
|
||
if (number != -1)
|
||
{
|
||
number--;
|
||
|
||
if (MapItem == null || number < 0 || number >= MapItem.Pins.Count)
|
||
{
|
||
number = -1;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (number == -1)
|
||
{
|
||
if (message)
|
||
{
|
||
TillerMan?.Say(1042551); // I don't see that navpoint, sir.
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
NextNavPoint = number;
|
||
return StartCourse(single, message);
|
||
}
|
||
|
||
public bool StartCourse(bool single, bool message)
|
||
{
|
||
if (CheckDecay())
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (Anchored)
|
||
{
|
||
if (message)
|
||
{
|
||
TillerMan?.Say(501419); // Ar, the anchor is down sir!
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
if (MapItem?.Deleted != false)
|
||
{
|
||
if (message)
|
||
{
|
||
TillerMan?.Say(502513); // I have seen no map, sir.
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
if (Map != MapItem.Map || !Contains(MapItem.GetWorldLocation()))
|
||
{
|
||
if (message)
|
||
{
|
||
TillerMan?.Say(502514); // The map is too far away from me, sir.
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
if (Map != Map.Trammel && Map != Map.Felucca || NextNavPoint < 0 || NextNavPoint >= MapItem.Pins.Count)
|
||
{
|
||
if (message)
|
||
{
|
||
TillerMan?.Say(1042551); // I don't see that navpoint, sir.
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
Speed = FastSpeed;
|
||
Order = single ? BoatOrder.Single : BoatOrder.Course;
|
||
|
||
if (!SafelyStartMoveTimer(FastInterval, single))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (message)
|
||
{
|
||
TillerMan?.Say(501429); // Aye aye sir.
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
private void StopBoat()
|
||
{
|
||
if (!DoMovement(true))
|
||
{
|
||
StopMove(false);
|
||
}
|
||
}
|
||
|
||
public override void OnSpeech(SpeechEventArgs e)
|
||
{
|
||
if (CheckDecay())
|
||
{
|
||
return;
|
||
}
|
||
|
||
var from = e.Mobile;
|
||
|
||
if (CanCommand(from) && Contains(from))
|
||
{
|
||
for (var i = 0; i < e.Keywords.Length; ++i)
|
||
{
|
||
var keyword = e.Keywords[i];
|
||
|
||
if (keyword >= 0x42 && keyword <= 0x6B)
|
||
{
|
||
switch (keyword)
|
||
{
|
||
case 0x42:
|
||
{
|
||
SetName(e);
|
||
break;
|
||
}
|
||
case 0x43:
|
||
{
|
||
RemoveName(e.Mobile);
|
||
break;
|
||
}
|
||
case 0x44:
|
||
{
|
||
GiveName(e.Mobile);
|
||
break;
|
||
}
|
||
case 0x45:
|
||
{
|
||
StartMove(Forward, true);
|
||
break;
|
||
}
|
||
case 0x46:
|
||
{
|
||
StartMove(Backward, true);
|
||
break;
|
||
}
|
||
case 0x47:
|
||
{
|
||
StartMove(Left, true);
|
||
break;
|
||
}
|
||
case 0x48:
|
||
{
|
||
StartMove(Right, true);
|
||
break;
|
||
}
|
||
case 0x4B:
|
||
{
|
||
StartMove(ForwardLeft, true);
|
||
break;
|
||
}
|
||
case 0x4C:
|
||
{
|
||
StartMove(ForwardRight, true);
|
||
break;
|
||
}
|
||
case 0x4D:
|
||
{
|
||
StartMove(BackwardLeft, true);
|
||
break;
|
||
}
|
||
case 0x4E:
|
||
{
|
||
StartMove(BackwardRight, true);
|
||
break;
|
||
}
|
||
case 0x4F:
|
||
{
|
||
StopMove(true);
|
||
break;
|
||
}
|
||
case 0x50:
|
||
{
|
||
StartMove(Left, false);
|
||
break;
|
||
}
|
||
case 0x51:
|
||
{
|
||
StartMove(Right, false);
|
||
break;
|
||
}
|
||
case 0x52:
|
||
{
|
||
StartMove(Forward, false);
|
||
break;
|
||
}
|
||
case 0x53:
|
||
{
|
||
StartMove(Backward, false);
|
||
break;
|
||
}
|
||
case 0x54:
|
||
{
|
||
StartMove(ForwardLeft, false);
|
||
break;
|
||
}
|
||
case 0x55:
|
||
{
|
||
StartMove(ForwardRight, false);
|
||
break;
|
||
}
|
||
case 0x56:
|
||
{
|
||
StartMove(BackwardRight, false);
|
||
break;
|
||
}
|
||
case 0x57:
|
||
{
|
||
StartMove(BackwardLeft, false);
|
||
break;
|
||
}
|
||
case 0x58:
|
||
{
|
||
OneMove(Left);
|
||
break;
|
||
}
|
||
case 0x59:
|
||
{
|
||
OneMove(Right);
|
||
break;
|
||
}
|
||
case 0x5A:
|
||
{
|
||
OneMove(Forward);
|
||
break;
|
||
}
|
||
case 0x5B:
|
||
{
|
||
OneMove(Backward);
|
||
break;
|
||
}
|
||
case 0x5C:
|
||
{
|
||
OneMove(ForwardLeft);
|
||
break;
|
||
}
|
||
case 0x5D:
|
||
{
|
||
OneMove(ForwardRight);
|
||
break;
|
||
}
|
||
case 0x5E:
|
||
{
|
||
OneMove(BackwardRight);
|
||
break;
|
||
}
|
||
case 0x5F:
|
||
{
|
||
OneMove(BackwardLeft);
|
||
break;
|
||
}
|
||
case 0x49:
|
||
case 0x65:
|
||
{
|
||
StartTurn(2, true);
|
||
break; // turn right
|
||
}
|
||
case 0x4A:
|
||
case 0x66:
|
||
{
|
||
StartTurn(-2, true);
|
||
break; // turn left
|
||
}
|
||
case 0x67:
|
||
{
|
||
StartTurn(-4, true);
|
||
break; // turn around, come about
|
||
}
|
||
case 0x68:
|
||
{
|
||
StartMove(Forward, true);
|
||
break;
|
||
}
|
||
case 0x69:
|
||
{
|
||
StopMove(true);
|
||
break;
|
||
}
|
||
case 0x6A:
|
||
{
|
||
LowerAnchor(true);
|
||
break;
|
||
}
|
||
case 0x6B:
|
||
{
|
||
RaiseAnchor(true);
|
||
break;
|
||
}
|
||
case 0x60:
|
||
{
|
||
GiveNavPoint();
|
||
break; // nav
|
||
}
|
||
case 0x61:
|
||
{
|
||
NextNavPoint = 0;
|
||
StartCourse(false, true);
|
||
break; // start
|
||
}
|
||
case 0x62:
|
||
{
|
||
StartCourse(false, true);
|
||
break; // continue
|
||
}
|
||
case 0x63:
|
||
{
|
||
StartCourse(e.Speech, false, true);
|
||
break; // goto*
|
||
}
|
||
case 0x64:
|
||
{
|
||
StartCourse(e.Speech, true, true);
|
||
break; // single*
|
||
}
|
||
}
|
||
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
public bool StartTurn(int offset, bool message)
|
||
{
|
||
if (CheckDecay())
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (Anchored)
|
||
{
|
||
if (message)
|
||
{
|
||
TillerMan.Say(501419); // Ar, the anchor is down sir!
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
if (Order != BoatOrder.Move)
|
||
{
|
||
_turnTimer?.Stop();
|
||
}
|
||
|
||
_turnTimer ??= new TurnTimer(offset, this, TimeSpan.FromMilliseconds(500), message);
|
||
|
||
if (_turnTimer.Running && _turnTimer._turn == offset)
|
||
{
|
||
// Dont let them spin around in circles like crazy
|
||
return true;
|
||
}
|
||
|
||
// Have they issued a new command too soon? Ignore it if so
|
||
var turnTimerNext = (_turnTimer.Next - Core.Now).TotalMilliseconds;
|
||
if (turnTimerNext is < 0 and > -250)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
_turnTimer._turn = offset;
|
||
_turnTimer.Stop();
|
||
_turnTimer.Start();
|
||
|
||
if (message)
|
||
{
|
||
TillerMan?.Say(501429); // Aye aye sir.
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
public bool Turn(int offset, bool message = true)
|
||
{
|
||
_turnTimer?.Stop();
|
||
|
||
if (CheckDecay())
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (Anchored)
|
||
{
|
||
if (message)
|
||
{
|
||
TillerMan.Say(501419); // Ar, the anchor is down sir!
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
if (SetFacing((Direction)(((int)_facing + offset) & 0x7)))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
if (message)
|
||
{
|
||
TillerMan.Say(501423); // Ar, can't turn sir.
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
public bool StartMove(Direction dir, int speed, int clientSpeed, TimeSpan interval, bool single, bool message)
|
||
{
|
||
if (CheckDecay())
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (Anchored)
|
||
{
|
||
if (message)
|
||
{
|
||
TillerMan?.Say(501419); // Ar, the anchor is down sir!
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
Moving = dir;
|
||
Speed = speed;
|
||
_clientSpeed = clientSpeed;
|
||
Order = BoatOrder.Move;
|
||
|
||
SafelyStartMoveTimer(interval, single);
|
||
|
||
return true;
|
||
}
|
||
|
||
private bool SafelyStartMoveTimer(TimeSpan interval, bool single)
|
||
{
|
||
var singleNum = single ? 1 : 0;
|
||
|
||
if (_moveTimer != null)
|
||
{
|
||
// Do not allow them to travel faster simply by respamming the command
|
||
if (_moveTimer.Running && _moveTimer.Interval == interval && _moveTimer.Count == singleNum)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// Have they issued a new command too soon? Ignore it if so
|
||
var moverTimerNext = (_moveTimer.Next - Core.Now).TotalMilliseconds;
|
||
if (moverTimerNext is < 0 and > -500)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// Changing the interval or flipping between commands: "Forward" and "Forward One"
|
||
var delay = TimeSpan.Zero;
|
||
if (interval.TotalMilliseconds > Math.Abs(moverTimerNext))
|
||
{
|
||
var addedDelay = (int)(interval.TotalMilliseconds - Math.Abs(moverTimerNext));
|
||
delay = TimeSpan.FromMilliseconds(addedDelay);
|
||
}
|
||
|
||
_moveTimer.Stop();
|
||
|
||
// We can reuse the timer if it's not a single count
|
||
if (_moveTimer.Count == 0 && singleNum == 0)
|
||
{
|
||
_moveTimer.Delay = delay;
|
||
_moveTimer.Interval = interval;
|
||
}
|
||
else
|
||
{
|
||
_moveTimer = new MoveTimer(this, delay, interval, singleNum);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
_moveTimer = new MoveTimer(this, interval, interval, singleNum);
|
||
}
|
||
|
||
_moveTimer.Start();
|
||
return true;
|
||
}
|
||
|
||
public bool StopMove(bool message)
|
||
{
|
||
if (CheckDecay())
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (_moveTimer?.Running == false)
|
||
{
|
||
if (message)
|
||
{
|
||
TillerMan?.Say(501443); // Er, the ship is not moving sir.
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
Moving = Direction.North;
|
||
Speed = 0;
|
||
_clientSpeed = 0;
|
||
_moveTimer?.Stop();
|
||
|
||
if (message)
|
||
{
|
||
TillerMan?.Say(501429); // Aye aye sir.
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
public bool CanFit(Point3D p, Map map, int itemID)
|
||
{
|
||
if (map == null || map == Map.Internal || Deleted || CheckDecay())
|
||
{
|
||
return false;
|
||
}
|
||
|
||
var newComponents = MultiData.GetComponents(itemID);
|
||
|
||
for (var x = 0; x < newComponents.Width; ++x)
|
||
{
|
||
for (var y = 0; y < newComponents.Height; ++y)
|
||
{
|
||
var tx = p.X + newComponents.Min.X + x;
|
||
var ty = p.Y + newComponents.Min.Y + y;
|
||
|
||
if (newComponents.Tiles[x][y].Length == 0 || Contains(tx, ty))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var landTile = map.Tiles.GetLandTile(tx, ty);
|
||
|
||
var hasWater = landTile.Z == p.Z &&
|
||
landTile.ID is >= 168 and <= 171 or >= 310 and <= 311;
|
||
|
||
// int z = p.Z;
|
||
|
||
// int landZ = 0, landAvg = 0, landTop = 0;
|
||
|
||
// map.GetAverageZ( tx, ty, ref landZ, ref landAvg, ref landTop );
|
||
|
||
// if (!landTile.Ignored && top > landZ && landTop > z)
|
||
// return false;
|
||
|
||
foreach (var tile in map.Tiles.GetStaticAndMultiTiles(tx, ty))
|
||
{
|
||
var isWater = tile.ID >= 0x1796 && tile.ID <= 0x17B2;
|
||
|
||
if (tile.Z == p.Z && isWater)
|
||
{
|
||
hasWater = true;
|
||
}
|
||
else if (tile.Z >= p.Z && !isWater)
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (!hasWater)
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
var bounds = new Rectangle2D(
|
||
p.X + newComponents.Min.X,
|
||
p.Y + newComponents.Min.Y,
|
||
newComponents.Width,
|
||
newComponents.Height
|
||
);
|
||
|
||
foreach (var item in map.GetItemsInBounds(bounds))
|
||
{
|
||
if (item is BaseMulti || item.ItemID > TileData.MaxItemValue || item.Z < p.Z || !item.Visible)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var x = item.X - p.X + newComponents.Min.X;
|
||
var y = item.Y - p.Y + newComponents.Min.Y;
|
||
|
||
// Out of bounds, return false - cannot fit
|
||
if ((x < 0 || x >= newComponents.Width || y < 0 || y >= newComponents.Height ||
|
||
newComponents.Tiles[x][y].Length != 0) && !Contains(item))
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
public Point3D Rotate(Point3D p, int count)
|
||
{
|
||
var rx = p.X - Location.X;
|
||
var ry = p.Y - Location.Y;
|
||
|
||
for (var i = 0; i < count; i++)
|
||
{
|
||
var temp = rx;
|
||
rx = -ry;
|
||
ry = temp;
|
||
}
|
||
|
||
return new Point3D(Location.X + rx, Location.Y + ry, p.Z);
|
||
}
|
||
|
||
public override bool Contains(int x, int y) =>
|
||
base.Contains(x, y) ||
|
||
TillerMan?.X == x && y == TillerMan.Y ||
|
||
Hold?.X == x && Hold.Y == y ||
|
||
PPlank?.X == x && PPlank.Y == y ||
|
||
SPlank?.X == x && SPlank.Y == y;
|
||
|
||
public static bool IsValidLocation(Point3D p, Map map)
|
||
{
|
||
var wrap = GetWrapFor(map);
|
||
|
||
for (var i = 0; i < wrap.Length; ++i)
|
||
{
|
||
if (wrap[i].Contains(p))
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
public static Rectangle2D[] GetWrapFor(Map m) => m == Map.Ilshenar ? IlshWrap :
|
||
m == Map.Tokuno ? TokunoWrap : BritWrap;
|
||
|
||
public Direction GetMovementFor(int x, int y, out int maxSpeed)
|
||
{
|
||
var dx = x - X;
|
||
var dy = y - Y;
|
||
|
||
var adx = dx.Abs();
|
||
var ady = dy.Abs();
|
||
|
||
var dir = Utility.GetDirection(Location.X, Location.Y, x, y);
|
||
var iDir = (int)dir;
|
||
|
||
// Compute the maximum distance we can travel without going too far away
|
||
if (iDir % 2 == 0) // North, East, South and West
|
||
{
|
||
maxSpeed = (adx - ady).Abs();
|
||
}
|
||
else // Right, Down, Left and Up
|
||
{
|
||
maxSpeed = Math.Min(adx, ady);
|
||
}
|
||
|
||
return (Direction)((iDir - (int)Facing) & 0x7);
|
||
}
|
||
|
||
public bool DoMovement(bool message)
|
||
{
|
||
Direction dir;
|
||
int speed, clientSpeed;
|
||
|
||
if (Order == BoatOrder.Move)
|
||
{
|
||
dir = Moving;
|
||
speed = Speed;
|
||
clientSpeed = _clientSpeed;
|
||
}
|
||
else if (MapItem?.Deleted != false)
|
||
{
|
||
if (message)
|
||
{
|
||
TillerMan?.Say(502513); // I have seen no map, sir.
|
||
}
|
||
|
||
return false;
|
||
}
|
||
else if (Map != MapItem.Map || !Contains(MapItem.GetWorldLocation()))
|
||
{
|
||
if (message)
|
||
{
|
||
TillerMan?.Say(502514); // The map is too far away from me, sir.
|
||
}
|
||
|
||
return false;
|
||
}
|
||
else if (Map != Map.Trammel && Map != Map.Felucca || NextNavPoint < 0 || NextNavPoint >= MapItem.Pins.Count)
|
||
{
|
||
if (message)
|
||
{
|
||
TillerMan?.Say(1042551); // I don't see that navpoint, sir.
|
||
}
|
||
|
||
return false;
|
||
}
|
||
else
|
||
{
|
||
var dest = MapItem.Pins[NextNavPoint];
|
||
|
||
MapItem.ConvertToWorld(dest.X, dest.Y, out var x, out var y);
|
||
|
||
dir = GetMovementFor(x, y, out var maxSpeed);
|
||
|
||
if (maxSpeed == 0)
|
||
{
|
||
if (message && Order == BoatOrder.Single)
|
||
{
|
||
TillerMan?.Say(
|
||
1042874,
|
||
(NextNavPoint + 1).ToString()
|
||
); // We have arrived at nav point ~1_POINT_NUM~ , sir.
|
||
}
|
||
|
||
if (NextNavPoint + 1 < MapItem.Pins.Count)
|
||
{
|
||
NextNavPoint++;
|
||
|
||
if (Order == BoatOrder.Course)
|
||
{
|
||
if (message)
|
||
{
|
||
TillerMan?.Say(
|
||
1042875,
|
||
(NextNavPoint + 1).ToString()
|
||
); // Heading to nav point ~1_POINT_NUM~, sir.
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
NextNavPoint = -1;
|
||
|
||
if (message && Order == BoatOrder.Course)
|
||
{
|
||
TillerMan?.Say(502515); // The course is completed, sir.
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
if (dir == Left || dir == BackwardLeft || dir == Backward)
|
||
{
|
||
return Turn(-2);
|
||
}
|
||
|
||
if (dir == Right || dir == BackwardRight)
|
||
{
|
||
return Turn(2);
|
||
}
|
||
|
||
speed = Math.Min(Speed, maxSpeed);
|
||
clientSpeed = 0x4;
|
||
}
|
||
|
||
return Move(dir, speed, clientSpeed, true);
|
||
}
|
||
|
||
public bool Move(Direction dir, int speed, int clientSpeed, bool message)
|
||
{
|
||
var map = Map;
|
||
|
||
if (map == null || Deleted || CheckDecay())
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (Anchored)
|
||
{
|
||
if (message)
|
||
{
|
||
TillerMan?.Say(501419); // Ar, the anchor is down sir!
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
var rx = 0;
|
||
var ry = 0;
|
||
var d = (Direction)(((int)_facing + (int)dir) & 0x7);
|
||
Movement.Movement.Offset(d, ref rx, ref ry);
|
||
|
||
for (var i = 1; i <= speed; ++i)
|
||
{
|
||
if (!CanFit(new Point3D(X + i * rx, Y + i * ry, Z), Map, ItemID))
|
||
{
|
||
if (i == 1)
|
||
{
|
||
if (message)
|
||
{
|
||
TillerMan?.Say(501424); // Ar, we've stopped sir.
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
speed = i - 1;
|
||
break;
|
||
}
|
||
}
|
||
|
||
var xOffset = speed * rx;
|
||
var yOffset = speed * ry;
|
||
|
||
var newX = X + xOffset;
|
||
var newY = Y + yOffset;
|
||
|
||
var wrap = GetWrapFor(map);
|
||
|
||
for (var i = 0; i < wrap.Length; ++i)
|
||
{
|
||
var rect = wrap[i];
|
||
|
||
if (rect.Contains(new Point2D(X, Y)) && !rect.Contains(new Point2D(newX, newY)))
|
||
{
|
||
if (newX < rect.X)
|
||
{
|
||
newX = rect.X + rect.Width - 1;
|
||
}
|
||
else if (newX >= rect.X + rect.Width)
|
||
{
|
||
newX = rect.X;
|
||
}
|
||
|
||
if (newY < rect.Y)
|
||
{
|
||
newY = rect.Y + rect.Height - 1;
|
||
}
|
||
else if (newY >= rect.Y + rect.Height)
|
||
{
|
||
newY = rect.Y;
|
||
}
|
||
|
||
for (var j = 1; j <= speed; ++j)
|
||
{
|
||
if (!CanFit(new Point3D(newX + j * rx, newY + j * ry, Z), Map, ItemID))
|
||
{
|
||
if (message)
|
||
{
|
||
TillerMan?.Say(501424); // Ar, we've stopped sir.
|
||
}
|
||
|
||
return false;
|
||
}
|
||
}
|
||
|
||
xOffset = newX - X;
|
||
yOffset = newY - Y;
|
||
}
|
||
}
|
||
|
||
if (!NewBoatMovement || xOffset.Abs() > 1 || yOffset.Abs() > 1)
|
||
{
|
||
Teleport(xOffset, yOffset, 0);
|
||
}
|
||
else
|
||
{
|
||
// We use a list because GetMovingEntities uses Map.GetItemsInRange which isn't safe for
|
||
// moving items while iterating.
|
||
using var list = PooledRefList<IEntity>.Create(64);
|
||
foreach (var e in GetMovingEntities(true))
|
||
{
|
||
list.Add(e);
|
||
}
|
||
|
||
var moveBoatPacket = stackalloc byte[BoatPackets.GetMoveBoatHSPacketLength(list.Count)]
|
||
.InitializePacket();
|
||
|
||
// Packet must be sent before actual locations are changed
|
||
foreach (var ns in Map.GetClientsInRange(Location, GetMaxUpdateRange()))
|
||
{
|
||
var m = ns.Mobile;
|
||
|
||
if (ns.HighSeas && m.CanSee(this) && m.InRange(Location, GetUpdateRange(m)))
|
||
{
|
||
ns.SendMoveBoatHSUsingCache(moveBoatPacket, this, list, d, clientSpeed, xOffset, yOffset);
|
||
}
|
||
}
|
||
|
||
for (var i = 0; i < list.Count; i++)
|
||
{
|
||
var e = list[i];
|
||
|
||
if (e is Item item)
|
||
{
|
||
// We want to enable smooth movement for boat parts, but they are ACTUALLY moved when the boat moves
|
||
if (item is not Server.Items.TillerMan and not Server.Items.Hold and not Plank)
|
||
{
|
||
item.NoMoveHS = true;
|
||
item.Location = new Point3D(item.X + xOffset, item.Y + yOffset, item.Z);
|
||
item.NoMoveHS = false;
|
||
}
|
||
}
|
||
else if (e is Mobile m)
|
||
{
|
||
m.NoMoveHS = true;
|
||
m.Location = new Point3D(m.X + xOffset, m.Y + yOffset, m.Z);
|
||
m.NoMoveHS = false;
|
||
}
|
||
}
|
||
|
||
if (TillerMan != null)
|
||
{
|
||
TillerMan.NoMoveHS = true;
|
||
}
|
||
if (SPlank != null)
|
||
{
|
||
SPlank.NoMoveHS = true;
|
||
}
|
||
if (PPlank != null)
|
||
{
|
||
PPlank.NoMoveHS = true;
|
||
}
|
||
if (Hold != null)
|
||
{
|
||
Hold.NoMoveHS = true;
|
||
}
|
||
|
||
NoMoveHS = true;
|
||
Location = new Point3D(X + xOffset, Y + yOffset, Z);
|
||
NoMoveHS = false;
|
||
|
||
if (TillerMan != null)
|
||
{
|
||
TillerMan.NoMoveHS = false;
|
||
}
|
||
if (SPlank != null)
|
||
{
|
||
SPlank.NoMoveHS = false;
|
||
}
|
||
if (PPlank != null)
|
||
{
|
||
PPlank.NoMoveHS = false;
|
||
}
|
||
if (Hold != null)
|
||
{
|
||
Hold.NoMoveHS = false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
public void Teleport(int xOffset, int yOffset, int zOffset)
|
||
{
|
||
using var queue = PooledRefQueue<IEntity>.Create(64);
|
||
foreach (var e in GetMovingEntities())
|
||
{
|
||
queue.Enqueue(e);
|
||
}
|
||
|
||
while (queue.Count > 0)
|
||
{
|
||
var e = queue.Dequeue();
|
||
|
||
if (e is Item item)
|
||
{
|
||
item.Location = new Point3D(item.X + xOffset, item.Y + yOffset, item.Z + zOffset);
|
||
}
|
||
else if (e is Mobile m)
|
||
{
|
||
m.Location = new Point3D(m.X + xOffset, m.Y + yOffset, m.Z + zOffset);
|
||
}
|
||
}
|
||
|
||
Location = new Point3D(X + xOffset, Y + yOffset, Z + zOffset);
|
||
}
|
||
|
||
public virtual MovingEntitiesEnumerable GetMovingEntities(bool includeBoat = false)
|
||
{
|
||
var map = Map;
|
||
|
||
if (map == null || map == Map.Internal)
|
||
{
|
||
return new MovingEntitiesEnumerable(this, includeBoat, Rectangle2D.Empty);
|
||
}
|
||
|
||
var mcl = Components;
|
||
|
||
var rect = new Rectangle2D(X + mcl.Min.X, Y + mcl.Min.Y, mcl.Width, mcl.Height);
|
||
return new MovingEntitiesEnumerable(this, includeBoat, rect);
|
||
}
|
||
|
||
public ref struct MovingEntitiesEnumerable
|
||
{
|
||
private readonly bool _includeBoat;
|
||
private readonly BaseBoat _boat;
|
||
private readonly Rectangle2D _bounds;
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public MovingEntitiesEnumerable(BaseBoat boat, bool includeBoat, Rectangle2D bounds)
|
||
{
|
||
_bounds = bounds;
|
||
_boat = boat;
|
||
_includeBoat = includeBoat;
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public MovingEntitiesEnumerator GetEnumerator() => new(_boat, _includeBoat, _bounds);
|
||
}
|
||
|
||
public ref struct MovingEntitiesEnumerator
|
||
{
|
||
private IEntity? _current;
|
||
private readonly bool _includeBoat;
|
||
private readonly BaseBoat _boat;
|
||
private bool _iterateMobiles;
|
||
private bool _iterateItems;
|
||
private Map.MobileEnumerator<Mobile> _mobiles;
|
||
private Map.ItemEnumerator<Item> _items;
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public MovingEntitiesEnumerator(BaseBoat boat, bool includeBoat, Rectangle2D bounds)
|
||
{
|
||
_current = default;
|
||
_includeBoat = includeBoat;
|
||
_boat = boat;
|
||
|
||
_mobiles = boat.Map.GetMobilesInBounds(bounds).GetEnumerator();
|
||
_items = boat.Map.GetItemsInBounds(bounds).GetEnumerator();
|
||
_iterateMobiles = true;
|
||
_iterateItems = true;
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public bool MoveNext()
|
||
{
|
||
if (_iterateMobiles)
|
||
{
|
||
while (_mobiles.MoveNext())
|
||
{
|
||
var current = _mobiles.Current;
|
||
if (_boat.Contains(current))
|
||
{
|
||
_current = current;
|
||
return true;
|
||
}
|
||
}
|
||
|
||
_iterateMobiles = false;
|
||
}
|
||
|
||
if (_iterateItems)
|
||
{
|
||
while (_items.MoveNext())
|
||
{
|
||
var current = _items.Current;
|
||
// Skip the boat, effects, spawners, or parts of the boat
|
||
if (current == _boat || current is EffectItem or BaseSpawner ||
|
||
!_includeBoat && (current == _boat.TillerMan || current == _boat.SPlank || current == _boat.PPlank || current == _boat.Hold))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
// TODO: Remove visible check and use something better, like check for spawners, or other things in the ocean we shouldn't pick up on accident
|
||
if (_boat!.Contains(current) && current.Visible && current.Z >= _boat.Z)
|
||
{
|
||
_current = current;
|
||
return true;
|
||
}
|
||
}
|
||
|
||
_iterateItems = false;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
public IEntity Current
|
||
{
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
get => _current;
|
||
}
|
||
}
|
||
|
||
public bool SetFacing(Direction facing)
|
||
{
|
||
if (Parent != null || Map == null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (CheckDecay())
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (Map != Map.Internal)
|
||
{
|
||
switch (facing)
|
||
{
|
||
case Direction.North:
|
||
{
|
||
if (!CanFit(Location, Map, NorthID))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
break;
|
||
}
|
||
case Direction.East:
|
||
{
|
||
if (!CanFit(Location, Map, EastID))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
break;
|
||
}
|
||
case Direction.South:
|
||
{
|
||
if (!CanFit(Location, Map, SouthID))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
break;
|
||
}
|
||
case Direction.West:
|
||
{
|
||
if (!CanFit(Location, Map, WestID))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
var old = _facing;
|
||
|
||
_facing = facing;
|
||
|
||
TillerMan?.SetFacing(facing);
|
||
Hold?.SetFacing(facing);
|
||
PPlank?.SetFacing(facing);
|
||
SPlank?.SetFacing(facing);
|
||
|
||
var xOffset = 0;
|
||
var yOffset = 0;
|
||
CalcMoves.Offset(facing, ref xOffset, ref yOffset);
|
||
|
||
var count = ((_facing - old) & 0x7) / 2;
|
||
|
||
using var queue = PooledRefQueue<IEntity>.Create();
|
||
foreach (var e in GetMovingEntities())
|
||
{
|
||
if (e == this)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (e is Mobile m)
|
||
{
|
||
m.Direction = (m.Direction - old + facing) & Direction.Mask;
|
||
}
|
||
|
||
queue.Enqueue(e);
|
||
}
|
||
|
||
while (queue.Count > 0)
|
||
{
|
||
var e = queue.Dequeue();
|
||
e.Location = Rotate(e.Location, count);
|
||
}
|
||
|
||
if (TillerMan != null)
|
||
{
|
||
TillerMan.Location = new Point3D(
|
||
X + xOffset * TillerManDistance + (facing == Direction.North ? 1 : 0),
|
||
Y + yOffset * TillerManDistance,
|
||
TillerMan.Z
|
||
);
|
||
}
|
||
|
||
if (Hold != null)
|
||
{
|
||
Hold.Location = new Point3D(X + xOffset * HoldDistance, Y + yOffset * HoldDistance, Hold.Z);
|
||
}
|
||
|
||
if (PPlank != null)
|
||
{
|
||
PPlank.Location = Rotate(PPlank.Location, count);
|
||
}
|
||
|
||
if (SPlank != null)
|
||
{
|
||
SPlank.Location = Rotate(SPlank.Location, count);
|
||
}
|
||
|
||
ItemID = facing switch
|
||
{
|
||
Direction.North => NorthID,
|
||
Direction.East => EastID,
|
||
Direction.South => SouthID,
|
||
Direction.West => WestID,
|
||
_ => ItemID
|
||
};
|
||
|
||
return true;
|
||
}
|
||
|
||
public static void UpdateAllComponents()
|
||
{
|
||
for (var i = Boats.Count - 1; i >= 0; --i)
|
||
{
|
||
Boats[i].UpdateComponents();
|
||
}
|
||
}
|
||
|
||
public static void Initialize()
|
||
{
|
||
EventSink.WorldLoad += UpdateAllComponents;
|
||
EventSink.WorldSave += UpdateAllComponents;
|
||
|
||
// Fix tiler/hold/plank locations
|
||
foreach (var item in World.Items.Values)
|
||
{
|
||
if (item is BaseBoat boat)
|
||
{
|
||
boat.UpdateComponents();
|
||
}
|
||
}
|
||
}
|
||
|
||
private class TurnTimer : Timer
|
||
{
|
||
internal int _turn;
|
||
private bool _message;
|
||
private BaseBoat _boat;
|
||
|
||
public TurnTimer(int turn, BaseBoat boat, TimeSpan delay, bool message = true) : base(delay)
|
||
{
|
||
_turn = turn;
|
||
_message = message;
|
||
_boat = boat;
|
||
}
|
||
protected override void OnTick()
|
||
{
|
||
_boat?.Turn(_turn, _message);
|
||
Stop();
|
||
}
|
||
}
|
||
|
||
private class MoveTimer : Timer
|
||
{
|
||
private readonly BaseBoat _boat;
|
||
|
||
public MoveTimer(BaseBoat boat, TimeSpan delay, TimeSpan interval, int single) : base(delay, interval, single) =>
|
||
_boat = boat;
|
||
|
||
protected override void OnTick()
|
||
{
|
||
_boat?.StopBoat();
|
||
}
|
||
}
|
||
|
||
private class DecayTimer : Timer
|
||
{
|
||
private readonly BaseBoat m_Boat;
|
||
private int m_Count;
|
||
|
||
public DecayTimer(BaseBoat boat) : base(TimeSpan.FromSeconds(1.0), TimeSpan.FromSeconds(5.0))
|
||
{
|
||
m_Boat = boat;
|
||
}
|
||
|
||
protected override void OnTick()
|
||
{
|
||
if (m_Count == 5)
|
||
{
|
||
m_Boat.Delete();
|
||
Stop();
|
||
}
|
||
else
|
||
{
|
||
m_Boat.Location = new Point3D(m_Boat.X, m_Boat.Y, m_Boat.Z - 1);
|
||
|
||
m_Boat.TillerMan?.Say(1007168 + m_Count);
|
||
|
||
++m_Count;
|
||
}
|
||
}
|
||
}
|
||
|
||
/*
|
||
* OSI sends the 0xF7 packet instead, holding 0xF3 packets
|
||
* for every entity on the boat. Though, the regular 0xF3
|
||
* packets are still being sent as well as entities come
|
||
* into sight. Do we really need it?
|
||
*/
|
||
/*
|
||
protected override Packet GetWorldPacketFor( NetState state )
|
||
{
|
||
if (NewBoatMovement && state.HighSeas)
|
||
return new DisplayBoatHS( state.Mobile, this );
|
||
else
|
||
return base.GetWorldPacketFor( state );
|
||
}
|
||
*/
|
||
}
|
||
}
|