ModernUO/Projects/UOContent/Engines/Doom/LeverPuzzle/LeverPuzzleController.cs
Kamron Batman 9b554f69b0
feat(timers): Adds timer pooling, fixes timer related bugs, and changes timer api (#667)
### 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);`.
2021-08-07 14:33:35 -07:00

750 lines
23 KiB
C#

using System;
using System.Collections.Generic;
using Server.Mobiles;
using Server.Network;
using Server.Spells;
namespace Server.Engines.Doom
{
public class LeverPuzzleController : Item
{
private static bool installed;
public static string[] Msgs =
{
"You are pinned down by the weight of the boulder!!!", // 0
"A speeding rock hits you in the head!", // 1
"OUCH!" // 2
};
/* font&hue for above msgs. index matches */
public static int[][] MsgParams =
{
new[] { 0x66d, 3 },
new[] { 0x66d, 3 },
new[] { 0x34, 3 }
};
/* World data for items */
public static int[][] TA =
{
new[] { 316, 64, 5 }, /* 3D Coords for levers */
new[] { 323, 58, 5 },
new[] { 332, 63, 5 },
new[] { 323, 71, 5 },
new[] { 324, 64 }, /* 2D Coords for standing regions */
new[] { 316, 65 },
new[] { 324, 58 },
new[] { 332, 64 },
new[] { 323, 72 },
new[] { 468, 92, -1 }, new[] { 0x181D, 0x482 }, /* 3D coord, itemid+hue for L.R. teles */
new[] { 469, 92, -1 }, new[] { 0x1821, 0x3fd },
new[] { 470, 92, -1 }, new[] { 0x1825, 0x66d },
new[] { 319, 70, 18 }, new[] { 0x12d8 }, /* 3D coord, itemid for statues */
new[] { 329, 60, 18 }, new[] { 0x12d9 },
new[] { 469, 96, 6 } /* 3D Coords for Fake Box */
};
/* CLILOC data for statue "correct souls" messages */
public static int[] Statue_Msg = { 1050009, 1050007, 1050008, 1050008 };
/* Exit & Enter locations for the lamp room */
public static Point3D lr_Exit = new(353, 172, -1);
public static Point3D lr_Enter = new(467, 96, -1);
/* "Center" location in puzzle */
public static Point3D lp_Center = new(324, 64, -1);
/* Lamp Room Area */
public static Rectangle2D lr_Rect = new(465, 92, 10, 10);
/* Lamp Room area Poison message data */
public static int[][] PA =
{
new[] { 0, 0, 0xA6 },
new[] { 1050001, 0x485, 0xAA },
new[] { 1050003, 0x485, 0xAC },
new[] { 1050056, 0x485, 0xA8 },
new[] { 1050057, 0x485, 0xA4 },
new[] { 1062091, 0x23F3, 0xAC }
};
public static Poison[] PA2 =
{
Poison.Lesser,
Poison.Regular,
Poison.Greater,
Poison.Deadly,
Poison.Lethal,
Poison.Lethal
};
/* SOUNDS */
private static readonly int[] fs = { 0x144, 0x154 };
private static readonly int[] ms = { 0x144, 0x14B };
private static readonly int[] fs2 = { 0x13F, 0x154 };
private static readonly int[] ms2 = { 0x13F, 0x14B };
private static readonly int[] cs1 = { 0x244 };
private static readonly int[] exp = { 0x307 };
private TimerExecutionToken _resetTimerToken;
private LampRoomBox m_Box;
private Region m_LampRoom;
private List<Item> m_Levers;
private List<Item> m_Statues;
private List<Item> m_Teles;
private List<LeverPuzzleRegion> m_Tiles;
private Timer m_Timer;
public LeverPuzzleController() : base(0x1822)
{
Movable = false;
Hue = 0x4c;
installed = true;
var i = 0;
m_Levers = new List<Item>(); /* codes are 0x1 shifted left x # of bits, easily handled here */
for (; i < 4; i++)
{
m_Levers.Add(AddLeverPuzzlePart(TA[i], new LeverPuzzleLever((ushort)(1 << i), this)));
}
m_Tiles = new List<LeverPuzzleRegion>();
for (; i < 9; i++)
{
m_Tiles.Add(new LeverPuzzleRegion(TA[i]));
}
m_Teles = new List<Item>();
for (; i < 15; i++)
{
m_Teles.Add(AddLeverPuzzlePart(TA[i], new LampRoomTeleporter(TA[++i])));
}
m_Statues = new List<Item>();
for (; i < 19; i++)
{
m_Statues.Add(AddLeverPuzzlePart(TA[i], new LeverPuzzleStatue(TA[++i], this)));
}
if (!installed)
{
Delete();
}
else
{
Enabled = true;
}
m_Box = (LampRoomBox)AddLeverPuzzlePart(TA[i], new LampRoomBox(this));
m_LampRoom = new LampRoomRegion(this);
GenKey();
}
public LeverPuzzleController(Serial serial) : base(serial)
{
}
[CommandProperty(AccessLevel.GameMaster)]
public ushort MyKey { get; set; }
[CommandProperty(AccessLevel.GameMaster)]
public ushort TheirKey { get; set; }
[CommandProperty(AccessLevel.GameMaster)]
public bool Enabled { get; set; }
public Mobile Successful { get; private set; }
public bool CircleComplete
{
get /* OSI: all 5 must be occupied */
{
for (var i = 0; i < 5; i++)
{
if (GetOccupant(i) == null)
{
return false;
}
}
return true;
}
}
public static void Initialize()
{
CommandSystem.Register("GenLeverPuzzle", AccessLevel.Administrator, GenLampPuzzle_OnCommand);
}
[Usage("GenLeverPuzzle"), Description("Generates lamp room and lever puzzle in doom.")]
public static void GenLampPuzzle_OnCommand(CommandEventArgs e)
{
var eable = Map.Malas.GetItemsInRange(lp_Center, 0);
foreach (var item in eable)
{
if (item is LeverPuzzleController)
{
eable.Free();
e.Mobile.SendMessage("Lamp room puzzle already exists: please delete the existing controller first ...");
return;
}
}
eable.Free();
e.Mobile.SendMessage("Generating Lamp Room puzzle...");
NetState.FlushAll();
new LeverPuzzleController().MoveToWorld(lp_Center, Map.Malas);
e.Mobile.SendMessage(
!installed ? "There was a problem generating the puzzle." : "Lamp room puzzle successfully generated."
);
}
public static Item AddLeverPuzzlePart(int[] loc, Item newitem)
{
if (newitem?.Deleted != false)
{
installed = false;
}
else
{
newitem.MoveToWorld(new Point3D(loc[0], loc[1], loc[2]), Map.Malas);
}
return newitem;
}
public override void OnDelete()
{
KillTimers();
base.OnDelete();
}
public override void OnAfterDelete()
{
NukeItemList(m_Teles);
NukeItemList(m_Statues);
NukeItemList(m_Levers);
m_LampRoom?.Unregister();
if (m_Tiles != null)
{
foreach (var region in m_Tiles)
{
region.Unregister();
}
}
if (m_Box?.Deleted == false)
{
m_Box.Delete();
}
}
public static void NukeItemList(List<Item> list)
{
if (list?.Count > 0)
{
foreach (var item in list)
{
if (item?.Deleted == false)
{
item.Delete();
}
}
}
}
public virtual PlayerMobile GetOccupant(int index)
{
var region = m_Tiles[index];
if (region?.Occupant?.Alive == true)
{
return (PlayerMobile)region.Occupant;
}
return null;
}
public virtual LeverPuzzleStatue GetStatue(int index)
{
var statue = (LeverPuzzleStatue)m_Statues[index];
return statue?.Deleted == false ? statue : null;
}
public virtual LeverPuzzleLever GetLever(int index)
{
var lever = (LeverPuzzleLever)m_Levers[index];
return lever?.Deleted == false ? lever : null;
}
public virtual void PuzzleStatus(int message, string fstring = null)
{
for (var i = 0; i < 2; i++)
{
Item s;
if ((s = GetStatue(i)) != null)
{
s.PublicOverheadMessage(MessageType.Regular, 0x3B2, message, fstring);
}
}
}
public virtual void ResetPuzzle()
{
PuzzleStatus(1062053);
ResetLevers();
}
public virtual void ResetLevers()
{
for (var i = 0; i < 4; i++)
{
Item l;
if ((l = GetLever(i)) != null)
{
l.ItemID = 0x108E;
Effects.PlaySound(l.Location, Map, 0x3E8);
}
}
TheirKey ^= TheirKey;
}
public virtual void KillTimers()
{
_resetTimerToken.Cancel();
m_Timer?.Stop();
}
public virtual void RemoveSuccessful()
{
Successful = null;
}
public virtual void LeverPulled(ushort code)
{
var correct = 0;
KillTimers();
/* if one bit in each of the four nibbles is set, this is false */
if ((TheirKey = (ushort)(code | (TheirKey <<= 4))) < 0x0FFF)
{
Timer.StartTimer(TimeSpan.FromSeconds(30.0), ResetPuzzle, out _resetTimerToken);
return;
}
if (!CircleComplete)
{
PuzzleStatus(1050004); // The circle is the key...
}
else
{
Mobile player;
if (TheirKey == MyKey)
{
GenKey();
if ((Successful = player = GetOccupant(0)) != null)
{
SendLocationEffect(lp_Center, 0x1153, 0, 60, 1);
PlaySounds(lp_Center, cs1);
Effects.SendBoltEffect(player);
player.MoveToWorld(lr_Enter, Map.Malas);
m_Timer = new LampRoomTimer(this);
m_Timer.Start();
Enabled = false;
}
}
else
{
for (var i = 0; i < 16; i++) /* Count matching SET bits, ie correct codes */
{
if (((MyKey >> i) & 1) == 1 && ((TheirKey >> i) & 1) == 1)
{
correct++;
}
}
PuzzleStatus(Statue_Msg[correct], correct > 0 ? correct.ToString() : null);
for (var i = 0; i < 5; i++)
{
if ((player = GetOccupant(i)) != null)
{
new RockTimer(player).Start();
}
}
}
}
ResetLevers();
}
public virtual void GenKey()
{
Span<ushort> ca = stackalloc ushort[] { 1, 2, 4, 8 };
ca.Shuffle();
for (var i = 0; i < 4; i++)
{
MyKey = (ushort)(ca[i] | (MyKey <<= 4));
}
}
private static bool IsValidDamagable(Mobile m) =>
m?.Deleted == false &&
(m.Player && m.Alive ||
m is BaseCreature bc && (bc.Controlled || bc.Summoned) && !bc.IsDeadBondedPet);
public static void MoveMobileOut(Mobile m)
{
if (m != null)
{
if (m is PlayerMobile && !m.Alive && m.Corpse?.Deleted == false)
{
m.Corpse.MoveToWorld(lr_Exit, Map.Malas);
}
BaseCreature.TeleportPets(m, lr_Exit, Map.Malas);
m.Location = lr_Exit;
m.ProcessDelta();
}
}
public static bool AniSafe(Mobile m) =>
m?.BodyMod == 0 && m.Alive && !TransformationSpellHelper.UnderTransformation(m);
public static IEntity ZAdjustedIEFromMobile(Mobile m, int zDelta) => new Entity(
Serial.Zero,
new Point3D(m.X, m.Y, m.Z + zDelta),
m.Map
);
public static void DoDamage(Mobile m, int min, int max, bool poison)
{
if (m?.Deleted == false && m.Alive)
{
var damage = Utility.Random(min, max);
AOS.Damage(m, damage, poison ? 0 : 100, 0, 0, poison ? 100 : 0, 0);
}
}
public static Point3D RandomPointIn(Point3D point, int range) => RandomPointIn(
point.X - range,
point.Y - range,
range * 2,
range * 2,
point.Z
);
public static Point3D RandomPointIn(Rectangle2D rect, int z) =>
RandomPointIn(rect.X, rect.Y, rect.Height, rect.Width, z);
public static Point3D RandomPointIn(int x, int y, int x2, int y2, int z) =>
new(Utility.Random(x, x2), Utility.Random(y, y2), z);
public static void PlaySounds(Point3D location, int[] sounds)
{
foreach (var soundid in sounds)
{
Effects.PlaySound(location, Map.Malas, soundid);
}
}
private static void PlayEffect(IEntity from, IEntity to, int itemid, int speed, bool explodes)
{
Effects.SendMovingParticles(from, to, itemid, speed, 0, true, explodes, 2, 0, 0);
}
private static void SendLocationEffect(Point3D p, int itemID, int speed, int duration, int hue)
{
Effects.SendLocationEffect(p, Map.Malas, itemID, speed, duration, hue);
}
public static void PlayerSendASCII(Mobile player, int index)
{
player.NetState.SendMessage(
Serial.MinusOne,
0xFFFF,
MessageType.Label,
MsgParams[index][0],
MsgParams[index][1],
true,
null,
null,
Msgs[index]
);
}
/* I cant find any better way to send "speech" using fonts other than default */
public static void POHMessage(Mobile from, int index)
{
Span<byte> buffer = stackalloc byte[OutgoingMessagePackets.GetMaxMessageLength(Msgs[index])].InitializePacket();
var eable = from.Map.GetClientsInRange(from.Location);
foreach (var state in eable)
{
var length = OutgoingMessagePackets.CreateMessage(
buffer,
from.Serial,
from.Body,
MessageType.Regular,
MsgParams[index][0],
MsgParams[index][1],
true,
null,
from.Name,
Msgs[index]
);
if (length != buffer.Length)
{
buffer = buffer[..length]; // Adjust to the actual size
}
state.Send(buffer);
}
eable.Free();
}
public override void Serialize(IGenericWriter writer)
{
base.Serialize(writer);
writer.Write(0); // version
m_Levers.Tidy();
writer.Write(m_Levers);
m_Statues.Tidy();
writer.Write(m_Statues);
m_Teles.Tidy();
writer.Write(m_Teles);
writer.Write(m_Box);
}
public override void Deserialize(IGenericReader reader)
{
base.Deserialize(reader);
var version = reader.ReadInt();
m_Levers = reader.ReadEntityList<Item>();
m_Statues = reader.ReadEntityList<Item>();
m_Teles = reader.ReadEntityList<Item>();
m_Box = reader.ReadEntity<LampRoomBox>();
m_Tiles = new List<LeverPuzzleRegion>();
for (var i = 4; i < 9; i++)
{
m_Tiles.Add(new LeverPuzzleRegion(TA[i]));
}
m_LampRoom = new LampRoomRegion(this);
Enabled = true;
TheirKey = 0;
MyKey = 0;
GenKey();
}
public class RockTimer : Timer
{
private readonly Mobile m_Player;
private int Count;
public RockTimer(Mobile player)
: base(TimeSpan.Zero, TimeSpan.FromSeconds(.25))
{
Count = 0;
m_Player = player;
}
private int Rock() => 0x1363 + Utility.Random(0, 11);
protected override void OnTick()
{
if (m_Player == null || m_Player.Map != Map.Malas)
{
Stop();
}
else
{
Count++;
if (Count == 1) /* TODO consolidate */
{
m_Player.Paralyze(TimeSpan.FromSeconds(2));
Effects.SendTargetEffect(m_Player, 0x11B7, 20, 10);
PlayerSendASCII(m_Player, 0); // You are pinned down ...
PlaySounds(m_Player.Location, !m_Player.Female ? fs : ms);
PlayEffect(ZAdjustedIEFromMobile(m_Player, 50), m_Player, 0x11B7, 20, false);
}
else if (Count == 2)
{
DoDamage(m_Player, 80, 90, false);
Effects.SendTargetEffect(m_Player, 0x36BD, 20, 10);
PlaySounds(m_Player.Location, exp);
PlayerSendASCII(m_Player, 1); // A speeding rock ...
if (AniSafe(m_Player))
{
m_Player.Animate(21, 10, 1, true, true, 0);
}
}
else if (Count == 3)
{
Stop();
Effects.SendTargetEffect(m_Player, 0x36B0, 20, 10);
PlayerSendASCII(m_Player, 1); // A speeding rock ...
PlaySounds(m_Player.Location, !m_Player.Female ? fs2 : ms2);
var j = Utility.Random(6, 10);
for (var i = 0; i < j; i++)
{
IEntity m_IEntity = new Entity(Serial.Zero, RandomPointIn(m_Player.Location, 10), m_Player.Map);
var eable = m_IEntity.Map.GetMobilesInRange(m_IEntity.Location, 2);
var mobiles = new List<Mobile>();
mobiles.AddRange(eable);
eable.Free();
for (var k = 0; k < mobiles.Count; k++)
{
if (IsValidDamagable(mobiles[k]) && mobiles[k] != m_Player)
{
PlayEffect(m_Player, mobiles[k], Rock(), 8, true);
DoDamage(mobiles[k], 25, 30, false);
if (mobiles[k].Player)
{
POHMessage(mobiles[k], 2); // OUCH!
}
}
}
PlayEffect(m_Player, m_IEntity, Rock(), 8, false);
}
}
}
}
}
public class LampRoomKickTimer : Timer
{
private readonly Mobile m;
public LampRoomKickTimer(Mobile player)
: base(TimeSpan.FromSeconds(.25)) =>
m = player;
protected override void OnTick()
{
MoveMobileOut(m);
}
}
public class LampRoomTimer : Timer
{
public int level;
public LeverPuzzleController m_Controller;
public int ticks;
public LampRoomTimer(LeverPuzzleController controller)
: base(TimeSpan.FromSeconds(5.0), TimeSpan.FromSeconds(5.0))
{
level = 0;
ticks = 0;
m_Controller = controller;
}
protected override void OnTick()
{
ticks++;
var mobiles = m_Controller.m_LampRoom.GetMobiles();
if (ticks >= 71 || m_Controller.m_LampRoom.GetPlayerCount() == 0)
{
foreach (var mobile in mobiles)
{
if (mobile?.Deleted == false && !mobile.IsDeadBondedPet)
{
mobile.Kill();
}
}
m_Controller.Enabled = true;
Stop();
}
else
{
if (ticks % 12 == 0)
{
level++;
}
foreach (var mobile in mobiles)
{
if (IsValidDamagable(mobile))
{
if (ticks % 2 == 0 && level == 5)
{
if (mobile.Player)
{
mobile.Say(1062092);
if (AniSafe(mobile))
{
mobile.Animate(32, 5, 1, true, false, 0);
}
}
DoDamage(mobile, 15, 20, true);
}
if (Utility.Random((int)(level & ~0xfffffffc), 3) == 3)
{
mobile.ApplyPoison(mobile, PA2[level]);
}
if (ticks % 12 == 0 && level > 0 && mobile.Player)
{
mobile.SendLocalizedMessage(PA[level][0], null, PA[level][1]);
}
}
}
for (var i = 0; i <= level; i++)
{
SendLocationEffect(RandomPointIn(lr_Rect, -1), 0x36B0, Utility.Random(150, 200), 0, PA[level][2]);
}
}
}
}
}
}