### Changes/Fixes:
* Adds timer pooling.
* Allows pool to be configurable in ModernUO.json
* Pool replenishes itself asynchronously if depleted.
* Fixes an issue with barkeeps and town criers
* Fixes an issue with incognito buff icons not being removed
* Fixes an issue with polymorph name mod not being removed
* Fixes several places where timers go on forever even after an object is deleted, keeping a reference (memory leak)
* Eliminates the timer for MiningCart altogether.
* Deletes `AcidSlime` since it is a duplicate of `PoolOfAcid`
* Fixes HonorableExecution and standardizes the code for other Bushido moves.
## Changes to the Timer API:
```cs
public class Timer
{
// Creates a timer that will be returned to the pool once execution stops.
public static void StartTimer(Action callback);
public static void StartTimer(TimeSpan delay, Action callback);
public static void StartTimer(TimeSpan delay, TimeSpan interval, Action callback);
public static void StartTimer(TimeSpan interval, int count, Action callback);
public static void StartTimer(TimeSpan delay, TimeSpan interval, int count, Action callback);
// Creates a timer and returns a token for more control. Requires manual cancellation in order for the timer to be returned to the pool.
// If the token is dereferenced, the timer will be dereferenced too. While not returning a timer to the pool is not considered hazardous, it does defeat the purpose of pooled timers.
public static void StartTimer(Action callback, out TimerExecutionToken token);
public static void StartTimer(TimeSpan delay, Action callback, out TimerExecutionToken token);
public static void StartTimer(TimeSpan delay, TimeSpan interval, Action callback, out TimerExecutionToken token);
public static void StartTimer(TimeSpan interval, int count, Action callback, out TimerExecutionToken token);
public static void StartTimer(TimeSpan delay, TimeSpan interval, int count, Action callback, out TimerExecutionToken token);
// If you aren't sure how to use the API above, or you don't care about performance, then you can use the old RunUO Timer.DelayCall
public static DelayCallTimer DelayCall(Action callback);
public static DelayCallTimer DelayCall(TimeSpan delay, Action callback);
public static DelayCallTimer DelayCall(TimeSpan delay, TimeSpan interval, Action callback);
public static DelayCallTimer DelayCall(TimeSpan interval, int count, Action callback);
public static DelayCallTimer DelayCall(TimeSpan delay, TimeSpan interval, int count, Action callback);
}
public struct TimerExecutionToken
{
public bool Running { get; }
public int Index { get; }
public int RemainingCount { get; }
public DateTime Next { get; }
}
```
## When to use `TimerExecutionToken`?
Use tokens when you want to gain the performance benefit of using a pooled timer, but you need one of the following:
* Access to the next time the timer will tick:`token.Next`
* Access to which interval, how many intervals there are, or how many are remaining: `token.Index`, `token.Count`, and `token.RemainingCount`
* Stop a timer manually.
* Determine if the timer is running: `timer.Running`
* See notes below about requirements for using tokens!
## Notes about using the TimerExecutionToken:
When you opt-in to receive a token, you must call `Cancel()` to return the timer. This can be done inside of the callback, or outside of the callback at any time.
If this is not called and your timer is an infinite interval, then you will create a potential memory leak, or null pointer exception in your callback.
If the timer ends and is stopped, but cancel is not called, then the timer will never return to the pool and stay referenced until the token is deleted or cancel is called. (Memory leak)
## Is this thread safe?
No. The ModernUO timer system is not thread safe at all. If you require a thread safe timer system, contact me and I'll help adapt this system. Keep in mind that there is a massive performance hit to make this thread safe when there are literally no use cases for it.
If you need to synchronize execution, meaning you want to execute code from another thread on the core thread. Let's say you have a discord bot that is pushing commands to the game server. Then use `EventLoopContext.Post(SendOrPostCallback callback, object state);`.
2088 lines
61 KiB
C#
2088 lines
61 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Runtime.CompilerServices;
|
||
using Server.Items;
|
||
using Server.Multis.Boats;
|
||
using Server.Network;
|
||
|
||
namespace Server.Multis
|
||
{
|
||
public enum BoatOrder
|
||
{
|
||
Move,
|
||
Course,
|
||
Single
|
||
}
|
||
|
||
public abstract class BaseBoat : BaseMulti
|
||
{
|
||
public enum DryDockResult
|
||
{
|
||
Valid,
|
||
Dead,
|
||
NoKey,
|
||
NotAnchored,
|
||
Mobiles,
|
||
Items,
|
||
Hold,
|
||
Decaying
|
||
}
|
||
|
||
private static readonly Rectangle2D[] m_BritWrap =
|
||
{ new(16, 16, 5120 - 32, 4096 - 32), new(5136, 2320, 992, 1760) };
|
||
|
||
private static readonly Rectangle2D[] m_IlshWrap = { new(16, 16, 2304 - 32, 1600 - 32) };
|
||
private static readonly Rectangle2D[] m_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 static readonly 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 static readonly int SlowDriftSpeed = 1;
|
||
private static readonly int FastDriftSpeed = 1;
|
||
|
||
private static readonly Direction Forward = Direction.North;
|
||
private static readonly Direction ForwardLeft = Direction.Up;
|
||
private static readonly Direction ForwardRight = Direction.Right;
|
||
private static readonly Direction Backward = Direction.South;
|
||
private static readonly Direction BackwardLeft = Direction.Left;
|
||
private static readonly Direction BackwardRight = Direction.Down;
|
||
private static readonly Direction Left = Direction.West;
|
||
private static readonly Direction Right = Direction.East;
|
||
private static Direction Port = Left;
|
||
private static Direction Starboard = Right;
|
||
|
||
private int m_ClientSpeed;
|
||
|
||
private bool m_Decaying;
|
||
|
||
private DateTime m_DecayTime;
|
||
|
||
private Direction m_Facing;
|
||
private TimerExecutionToken _moveTimerToken;
|
||
|
||
private string m_ShipName;
|
||
|
||
private TimerExecutionToken _turnTimerToken;
|
||
|
||
public BaseBoat() : base(0x0)
|
||
{
|
||
m_DecayTime = 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);
|
||
}
|
||
|
||
public BaseBoat(Serial serial) : base(serial)
|
||
{
|
||
}
|
||
|
||
[CommandProperty(AccessLevel.GameMaster)]
|
||
public Hold Hold { get; set; }
|
||
|
||
[CommandProperty(AccessLevel.GameMaster)]
|
||
public TillerMan TillerMan { get; set; }
|
||
|
||
[CommandProperty(AccessLevel.GameMaster)]
|
||
public Plank PPlank { get; set; }
|
||
|
||
[CommandProperty(AccessLevel.GameMaster)]
|
||
public Plank SPlank { get; set; }
|
||
|
||
[CommandProperty(AccessLevel.GameMaster)]
|
||
public Mobile Owner { get; set; }
|
||
|
||
[CommandProperty(AccessLevel.GameMaster)]
|
||
public Direction Facing
|
||
{
|
||
get => m_Facing;
|
||
set => SetFacing(value);
|
||
}
|
||
|
||
[CommandProperty(AccessLevel.GameMaster)]
|
||
public Direction Moving { get; set; }
|
||
|
||
[CommandProperty(AccessLevel.GameMaster)]
|
||
public bool IsMoving => _moveTimerToken.Running;
|
||
|
||
[CommandProperty(AccessLevel.GameMaster)]
|
||
public int Speed { get; set; }
|
||
|
||
[CommandProperty(AccessLevel.GameMaster)]
|
||
public bool Anchored { get; set; }
|
||
|
||
[CommandProperty(AccessLevel.GameMaster)]
|
||
public string ShipName
|
||
{
|
||
get => m_ShipName;
|
||
set
|
||
{
|
||
m_ShipName = value;
|
||
TillerMan?.InvalidateProperties();
|
||
}
|
||
}
|
||
|
||
[CommandProperty(AccessLevel.GameMaster)]
|
||
public BoatOrder Order { get; set; }
|
||
|
||
[CommandProperty(AccessLevel.GameMaster)]
|
||
public MapItem MapItem { get; set; }
|
||
|
||
[CommandProperty(AccessLevel.GameMaster)]
|
||
public int NextNavPoint { get; set; }
|
||
|
||
[CommandProperty(AccessLevel.GameMaster)]
|
||
public DateTime TimeOfDecay
|
||
{
|
||
get => m_DecayTime;
|
||
set
|
||
{
|
||
m_DecayTime = value;
|
||
TillerMan?.InvalidateProperties();
|
||
}
|
||
}
|
||
|
||
public int Status
|
||
{
|
||
get
|
||
{
|
||
var start = Core.Now - TimeOfDecay - BoatDecayDelay;
|
||
|
||
if (start < TimeSpan.FromHours(1.0))
|
||
{
|
||
return 1043010; // This structure is like new.
|
||
}
|
||
|
||
if (start < TimeSpan.FromDays(2.0))
|
||
{
|
||
return 1043011; // This structure is slightly worn.
|
||
}
|
||
|
||
if (start < TimeSpan.FromDays(3.0))
|
||
{
|
||
return 1043012; // This structure is somewhat worn.
|
||
}
|
||
|
||
if (start < TimeSpan.FromDays(4.0))
|
||
{
|
||
return 1043013; // This structure is fairly worn.
|
||
}
|
||
|
||
if (start < TimeSpan.FromDays(5.0))
|
||
{
|
||
return 1043014; // This structure is greatly worn.
|
||
}
|
||
|
||
return 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(IPoint2D loc, Map map)
|
||
{
|
||
var sector = map.GetSector(loc);
|
||
|
||
for (var i = 0; i < sector.Multis.Count; i++)
|
||
{
|
||
if (sector.Multis[i] is BaseBoat boat && 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)m_Facing / 2);
|
||
}
|
||
|
||
public void UpdateComponents()
|
||
{
|
||
if (PPlank != null)
|
||
{
|
||
PPlank.MoveToWorld(GetRotatedLocation(PortOffset.X, PortOffset.Y), Map);
|
||
PPlank.SetFacing(m_Facing);
|
||
}
|
||
|
||
if (SPlank != null)
|
||
{
|
||
SPlank.MoveToWorld(GetRotatedLocation(StarboardOffset.X, StarboardOffset.Y), Map);
|
||
SPlank.SetFacing(m_Facing);
|
||
}
|
||
|
||
int xOffset = 0, yOffset = 0;
|
||
Movement.Movement.Offset(m_Facing, ref xOffset, ref yOffset);
|
||
|
||
if (TillerMan != null)
|
||
{
|
||
TillerMan.Location = new Point3D(
|
||
X + xOffset * TillerManDistance + (m_Facing == Direction.North ? 1 : 0),
|
||
Y + yOffset * TillerManDistance,
|
||
TillerMan.Z
|
||
);
|
||
TillerMan.SetFacing(m_Facing);
|
||
TillerMan.InvalidateProperties();
|
||
}
|
||
|
||
if (Hold != null)
|
||
{
|
||
Hold.Location = new Point3D(X + xOffset * HoldDistance, Y + yOffset * HoldDistance, Hold.Z);
|
||
Hold.SetFacing(m_Facing);
|
||
}
|
||
}
|
||
|
||
public override void Serialize(IGenericWriter writer)
|
||
{
|
||
base.Serialize(writer);
|
||
|
||
writer.Write(3);
|
||
|
||
writer.Write(MapItem);
|
||
writer.Write(NextNavPoint);
|
||
|
||
writer.Write((int)m_Facing);
|
||
|
||
writer.WriteDeltaTime(m_DecayTime);
|
||
|
||
writer.Write(Owner);
|
||
writer.Write(PPlank);
|
||
writer.Write(SPlank);
|
||
writer.Write(TillerMan);
|
||
writer.Write(Hold);
|
||
writer.Write(Anchored);
|
||
writer.Write(m_ShipName);
|
||
|
||
CheckDecay();
|
||
}
|
||
|
||
public override void Deserialize(IGenericReader reader)
|
||
{
|
||
base.Deserialize(reader);
|
||
|
||
var version = reader.ReadInt();
|
||
|
||
switch (version)
|
||
{
|
||
case 3:
|
||
{
|
||
MapItem = (MapItem)reader.ReadEntity<Item>();
|
||
NextNavPoint = reader.ReadInt();
|
||
|
||
goto case 2;
|
||
}
|
||
case 2:
|
||
{
|
||
m_Facing = (Direction)reader.ReadInt();
|
||
|
||
goto case 1;
|
||
}
|
||
case 1:
|
||
{
|
||
m_DecayTime = reader.ReadDeltaTime();
|
||
|
||
goto case 0;
|
||
}
|
||
case 0:
|
||
{
|
||
if (version < 3)
|
||
{
|
||
NextNavPoint = -1;
|
||
}
|
||
|
||
if (version < 2)
|
||
{
|
||
if (ItemID == NorthID)
|
||
{
|
||
m_Facing = Direction.North;
|
||
}
|
||
else if (ItemID == SouthID)
|
||
{
|
||
m_Facing = Direction.South;
|
||
}
|
||
else if (ItemID == EastID)
|
||
{
|
||
m_Facing = Direction.East;
|
||
}
|
||
else if (ItemID == WestID)
|
||
{
|
||
m_Facing = Direction.West;
|
||
}
|
||
}
|
||
|
||
Owner = reader.ReadEntity<Mobile>();
|
||
PPlank = reader.ReadEntity<Plank>();
|
||
SPlank = reader.ReadEntity<Plank>();
|
||
TillerMan = reader.ReadEntity<TillerMan>();
|
||
Hold = reader.ReadEntity<Hold>();
|
||
Anchored = reader.ReadBool();
|
||
m_ShipName = reader.ReadString();
|
||
|
||
if (version < 1)
|
||
{
|
||
Refresh();
|
||
}
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
Boats.Add(this);
|
||
}
|
||
|
||
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();
|
||
_turnTimerToken.Cancel();
|
||
_moveTimerToken.Cancel();
|
||
|
||
Boats.Remove(this);
|
||
}
|
||
|
||
public override void OnLocationChange(Point3D old)
|
||
{
|
||
if (TillerMan != null)
|
||
{
|
||
TillerMan.Location = new Point3D(
|
||
X + (TillerMan.X - old.X),
|
||
Y + (TillerMan.Y - old.Y),
|
||
Z + (TillerMan.Z - old.Z)
|
||
);
|
||
}
|
||
|
||
if (Hold != null)
|
||
{
|
||
Hold.Location = new Point3D(X + (Hold.X - old.X), Y + (Hold.Y - old.Y), Z + (Hold.Z - old.Z));
|
||
}
|
||
|
||
if (PPlank != null)
|
||
{
|
||
PPlank.Location = new Point3D(X + (PPlank.X - old.X), Y + (PPlank.Y - old.Y), Z + (PPlank.Z - old.Z));
|
||
}
|
||
|
||
if (SPlank != null)
|
||
{
|
||
SPlank.Location = new Point3D(X + (SPlank.X - old.X), Y + (SPlank.Y - old.Y), Z + (SPlank.Z - old.Z));
|
||
}
|
||
}
|
||
|
||
public override void OnMapChange()
|
||
{
|
||
if (TillerMan != null)
|
||
{
|
||
TillerMan.Map = Map;
|
||
}
|
||
|
||
if (Hold != null)
|
||
{
|
||
Hold.Map = Map;
|
||
}
|
||
|
||
if (PPlank != null)
|
||
{
|
||
PPlank.Map = Map;
|
||
}
|
||
|
||
if (SPlank != null)
|
||
{
|
||
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)m_Facing / 2);
|
||
}
|
||
|
||
public bool CheckKey(uint keyValue) => SPlank?.KeyValue == keyValue || PPlank?.KeyValue == keyValue;
|
||
|
||
public void Refresh()
|
||
{
|
||
m_DecayTime = Core.Now + BoatDecayDelay;
|
||
|
||
TillerMan?.InvalidateProperties();
|
||
}
|
||
|
||
public bool CheckDecay()
|
||
{
|
||
if (m_Decaying)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
if (!IsMoving && Core.Now >= m_DecayTime)
|
||
{
|
||
new DecayTimer(this).Start();
|
||
|
||
m_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;
|
||
}
|
||
|
||
using var ents = GetMovingEntities();
|
||
var enumerator = ents.GetEnumerator();
|
||
|
||
if (enumerator.MoveNext())
|
||
{
|
||
return enumerator.Current 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(from, 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)
|
||
{
|
||
Rename(e.Speech[8..].Trim().DefaultIfNullOrEmpty(null));
|
||
}
|
||
}
|
||
|
||
public void Rename(string newName)
|
||
{
|
||
if (CheckDecay())
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (newName?.Length > 40)
|
||
{
|
||
newName = newName[..40];
|
||
}
|
||
|
||
if (m_ShipName == newName)
|
||
{
|
||
TillerMan?.Say(502531); // Yes, sir.
|
||
|
||
return;
|
||
}
|
||
|
||
ShipName = newName;
|
||
|
||
if (TillerMan != null && m_ShipName != null)
|
||
{
|
||
TillerMan.Say(1042885, m_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 (m_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 (m_ShipName == null)
|
||
{
|
||
TillerMan.Say(502526); // Ar, this ship has no name.
|
||
}
|
||
else
|
||
{
|
||
TillerMan.Say(1042881, m_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;
|
||
|
||
_moveTimerToken.Cancel();
|
||
Timer.StartTimer(FastInterval, FastInterval, StopBoat, out _moveTimerToken);
|
||
|
||
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)
|
||
{
|
||
_moveTimerToken.Cancel();
|
||
}
|
||
|
||
_turnTimerToken.Cancel();
|
||
Timer.StartTimer(TimeSpan.FromMilliseconds(500), () => Turn(offset), out _turnTimerToken);
|
||
|
||
if (message)
|
||
{
|
||
TillerMan?.Say(501429); // Aye aye sir.
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
public bool Turn(int offset, bool message = true)
|
||
{
|
||
_turnTimerToken.Cancel();
|
||
|
||
if (CheckDecay())
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (Anchored)
|
||
{
|
||
if (message)
|
||
{
|
||
TillerMan.Say(501419); // Ar, the anchor is down sir!
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
if (SetFacing((Direction)(((int)m_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;
|
||
m_ClientSpeed = clientSpeed;
|
||
Order = BoatOrder.Move;
|
||
|
||
_moveTimerToken.Cancel();
|
||
Timer.StartTimer(interval, StopBoat, out _moveTimerToken);
|
||
|
||
return true;
|
||
}
|
||
|
||
public bool StopMove(bool message)
|
||
{
|
||
if (CheckDecay())
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (!_moveTimerToken.Running)
|
||
{
|
||
if (message)
|
||
{
|
||
TillerMan?.Say(501443); // Er, the ship is not moving sir.
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
Moving = Direction.North;
|
||
Speed = 0;
|
||
m_ClientSpeed = 0;
|
||
_moveTimerToken.Cancel();
|
||
|
||
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 tiles = map.Tiles.GetStaticTiles(tx, ty, true);
|
||
|
||
var hasWater = landTile.Z == p.Z &&
|
||
(landTile.ID >= 168 && landTile.ID <= 171 || landTile.ID >= 310 && landTile.ID <= 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;
|
||
|
||
for (var i = 0; i < tiles.Length; ++i)
|
||
{
|
||
var tile = tiles[i];
|
||
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 eable = map.GetItemsInBounds(
|
||
new Rectangle2D(
|
||
p.X + newComponents.Min.X,
|
||
p.Y + newComponents.Min.Y,
|
||
newComponents.Width,
|
||
newComponents.Height
|
||
)
|
||
);
|
||
|
||
var canFit = eable.All(
|
||
item =>
|
||
{
|
||
if (item is BaseMulti || item.ItemID > TileData.MaxItemValue || item.Z < p.Z || !item.Visible)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
var x = item.X - p.X + newComponents.Min.X;
|
||
var y = item.Y - p.Y + newComponents.Min.Y;
|
||
|
||
return x >= 0 && x < newComponents.Width && y >= 0 && y < newComponents.Height &&
|
||
newComponents.Tiles[x][y].Length == 0 || Contains(item);
|
||
}
|
||
);
|
||
|
||
eable.Free();
|
||
return canFit;
|
||
}
|
||
|
||
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 ? m_IlshWrap :
|
||
m == Map.Tokuno ? m_TokunoWrap : m_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(this, new Point2D(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 = m_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;
|
||
}
|
||
|
||
int rx = 0, ry = 0;
|
||
var d = (Direction)(((int)m_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
|
||
{
|
||
using var eable = GetMovingEntities(true);
|
||
|
||
// 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.SendMoveBoatHS(m, this, d, clientSpeed, eable, xOffset, yOffset);
|
||
eable.Reset();
|
||
}
|
||
}
|
||
|
||
foreach (var e in eable)
|
||
{
|
||
if (e is Item item)
|
||
{
|
||
item.NoMoveHS = true;
|
||
|
||
if (!(item is TillerMan || item is Hold || item is Plank))
|
||
{
|
||
item.Location = new Point3D(item.X + xOffset, item.Y + yOffset, item.Z);
|
||
}
|
||
}
|
||
else if (e is Mobile m)
|
||
{
|
||
m.NoMoveHS = true;
|
||
m.Location = new Point3D(m.X + xOffset, m.Y + yOffset, m.Z);
|
||
}
|
||
}
|
||
|
||
NoMoveHS = true;
|
||
Location = new Point3D(X + xOffset, Y + yOffset, Z);
|
||
|
||
eable.Reset();
|
||
|
||
foreach (var e in eable)
|
||
{
|
||
if (e is Item item)
|
||
{
|
||
item.NoMoveHS = false;
|
||
}
|
||
else if (e is Mobile mobile)
|
||
{
|
||
mobile.NoMoveHS = false;
|
||
}
|
||
}
|
||
|
||
NoMoveHS = false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
public void Teleport(int xOffset, int yOffset, int zOffset)
|
||
{
|
||
foreach (var e in GetMovingEntities())
|
||
{
|
||
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, null);
|
||
}
|
||
|
||
var mcl = Components;
|
||
|
||
var eable = map.GetObjectsInBounds(new Rectangle2D(X + mcl.Min.X, Y + mcl.Min.Y, mcl.Width, mcl.Height));
|
||
return new MovingEntitiesEnumerable(this, includeBoat, eable);
|
||
}
|
||
|
||
public ref struct MovingEntitiesEnumerable
|
||
{
|
||
private readonly IPooledEnumerable<IEntity> _entities;
|
||
private readonly IEnumerator<IEntity> _enumerator;
|
||
private readonly bool _includeBoat;
|
||
private BaseBoat _boat;
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public MovingEntitiesEnumerable(BaseBoat boat, bool includeBoat, IPooledEnumerable<IEntity> entities)
|
||
{
|
||
_entities = entities;
|
||
_enumerator = entities?.GetEnumerator();
|
||
_boat = boat;
|
||
_includeBoat = includeBoat;
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public MovingEntitiesEnumerator GetEnumerator() => new(_boat, _includeBoat, _enumerator);
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public void Dispose()
|
||
{
|
||
_entities?.Free();
|
||
_enumerator?.Dispose();
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public void Reset()
|
||
{
|
||
_enumerator?.Reset();
|
||
}
|
||
}
|
||
|
||
public ref struct MovingEntitiesEnumerator
|
||
{
|
||
private readonly IEnumerator<IEntity> _enumerator;
|
||
private IEntity? _current;
|
||
private readonly bool _includeBoat;
|
||
private readonly BaseBoat _boat;
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public MovingEntitiesEnumerator(BaseBoat boat, bool includeBoat, IEnumerator<IEntity> enumerator = null)
|
||
{
|
||
_enumerator = enumerator;
|
||
_current = default;
|
||
_includeBoat = includeBoat;
|
||
_boat = boat;
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public bool MoveNext()
|
||
{
|
||
IEntity current;
|
||
while (_enumerator?.MoveNext() == true)
|
||
{
|
||
current = _enumerator.Current;
|
||
|
||
if (!_includeBoat && current is Server.Items.TillerMan or Server.Items.Hold or Plank)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (current is Item item)
|
||
{
|
||
if (item == _boat)
|
||
{
|
||
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(item) && item.Visible && item.Z >= _boat.Z)
|
||
{
|
||
_current = current;
|
||
return true;
|
||
}
|
||
}
|
||
else if (current is Mobile m)
|
||
{
|
||
if (_boat.Contains(m))
|
||
{
|
||
_current = current;
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
|
||
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 = m_Facing;
|
||
|
||
m_Facing = facing;
|
||
|
||
TillerMan?.SetFacing(facing);
|
||
Hold?.SetFacing(facing);
|
||
PPlank?.SetFacing(facing);
|
||
SPlank?.SetFacing(facing);
|
||
|
||
int xOffset = 0, yOffset = 0;
|
||
Movement.Movement.Offset(facing, ref xOffset, ref yOffset);
|
||
|
||
var count = ((m_Facing - old) & 0x7) / 2;
|
||
|
||
foreach (var e in GetMovingEntities())
|
||
{
|
||
if (e == this)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (e is Item item)
|
||
{
|
||
item.Location = Rotate(item.Location, count);
|
||
}
|
||
else if (e is Mobile m)
|
||
{
|
||
m.Direction = (m.Direction - old + facing) & Direction.Mask;
|
||
m.Location = Rotate(m.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;
|
||
}
|
||
|
||
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 );
|
||
}
|
||
*/
|
||
}
|
||
}
|