ModernUO/Projects/UOContent/Engines/ConPVP/Games/BombingRun.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

2042 lines
57 KiB
C#

using System;
using System.Collections.Generic;
using System.Text;
using Server.Gumps;
using Server.Items;
using Server.Mobiles;
using Server.Network;
using Server.Targeting;
namespace Server.Engines.ConPVP
{
public class BRBomb : Item
{
private readonly BRGame m_Game;
private readonly List<Mobile> m_Helpers;
private readonly Point3DList m_Path = new();
private readonly EffectTimer m_Timer;
private bool m_Flying;
private int m_PathIdx;
public BRBomb(BRGame game) :
base(0x103C) // 0x103C = bread, 0x1042 = pie, 0x1364 = rock, 0x13a8 = pillow, 0x2256 = bagball
{
Movable = false;
Hue = 0x35;
m_Game = game;
m_Helpers = new List<Mobile>();
m_Timer = new EffectTimer(this);
m_Timer.Start();
}
public BRBomb(Serial serial) : base(serial)
{
}
public override string DefaultName => "da bomb";
public Mobile Thrower { get; private set; }
private Mobile FindOwner(IEntity parent)
{
if (parent is Item item)
{
return item.RootParent as Mobile;
}
if (parent is Mobile mobile)
{
return mobile;
}
return null;
}
public override void Deserialize(IGenericReader reader)
{
base.Deserialize(reader);
var version = reader.ReadInt();
switch (version)
{
case 0:
{
break;
}
}
Timer.StartTimer(Delete); // delete this after the world loads
}
public override void Serialize(IGenericWriter writer)
{
base.Serialize(writer);
writer.Write(0); // version
}
public override void OnAdded(IEntity parent)
{
base.OnAdded(parent);
var mob = FindOwner(parent);
if (mob != null)
{
mob.SolidHueOverride = 0x0499;
}
}
public override void OnRemoved(IEntity parent)
{
base.OnRemoved(parent);
var mob = FindOwner(parent);
if (mob != null && m_Game != null)
{
mob.SolidHueOverride = m_Game.GetColor(mob);
}
}
public void DropTo(Mobile mob, Mobile killer)
{
if (mob?.Deleted == false)
{
MoveToWorld(mob.Location, mob.Map);
}
else if (killer?.Deleted == false)
{
MoveToWorld(killer.Location, killer.Map);
}
else
{
m_Game?.ReturnBomb();
}
}
public override bool OnMoveOver(Mobile m)
{
if (m_Flying || !Visible || m_Game == null || m?.Alive != true)
{
return true;
}
var useTeam = m_Game.GetTeamInfo(m);
return useTeam == null || TakeBomb(m, useTeam, "picked up");
}
public override void OnLocationChange(Point3D oldLocation)
{
base.OnLocationChange(oldLocation);
if (m_Flying || !Visible || m_Game == null || Parent != null)
{
return;
}
var eable = GetClientsInRange(0);
foreach (var ns in eable)
{
var m = ns.Mobile;
if (m?.Player != true || !m.Alive)
{
continue;
}
var useTeam = m_Game.GetTeamInfo(m);
if (useTeam != null)
{
TakeBomb(m, useTeam, "got");
}
}
}
public override void OnAfterDelete()
{
base.OnAfterDelete();
m_Timer?.Stop();
}
public override void OnDoubleClick(Mobile m)
{
if (m_Game == null || !Visible || m?.Alive != true)
{
return;
}
if (!m_Flying && IsChildOf(m.Backpack))
{
m.Target = new BombTarget(this, m);
}
else if (Parent == null)
{
if (m.InRange(Location, 1) && m.Location.Z != Z)
{
var useTeam = m_Game.GetTeamInfo(m);
if (useTeam == null)
{
return;
}
TakeBomb(m, useTeam, "grabbed");
}
}
}
private bool OnBombTarget(Mobile from, object obj)
{
if (m_Game == null)
{
return true;
}
if (!IsChildOf(from.Backpack))
{
return true;
}
// don't let them throw it to themselves
if (obj == from)
{
return false;
}
if (!(obj is IPoint3D))
{
return false;
}
var pt = new Point3D((IPoint3D)obj);
if (obj is Mobile)
{
pt.Z += 10;
}
else if (obj is Item item)
{
pt.Z += item.ItemData.CalcHeight + 1;
}
m_Flying = true;
Visible = false;
Thrower = from;
MoveToWorld(GetWorldLocation(), from.Map);
BeginFlight(pt);
return true;
}
private void HitObject(Point3D ballLoc, int objZ, int objHeight)
{
DoAnim(GetWorldLocation(), ballLoc, Map);
MoveToWorld(ballLoc);
m_Path.Clear();
m_PathIdx = 0;
Timer.StartTimer(TimeSpan.FromSeconds(0.05), ContinueFlight);
}
private bool CheckCatch(Mobile m, Point3D myLoc)
{
if (m?.Alive != true || !m.Player || m_Game == null)
{
return false;
}
if (m_Game.GetTeamInfo(m) == null)
{
return false;
}
var zdiff = myLoc.Z - m.Z;
return zdiff >= 0 && (zdiff < 12 || zdiff < 16 && Utility.RandomBool());
}
private void DoAnim(Point3D start, Point3D end, Map map)
{
Effects.SendMovingEffect(
new Entity(Serial.Zero, start, map),
new Entity(Serial.Zero, end, map),
ItemID,
15,
0,
false,
false,
Hue
);
}
private void DoCatch(Mobile m)
{
m_Flying = false;
Visible = true;
if (m?.Alive != true || !m.Player || m_Game == null)
{
return;
}
var useTeam = m_Game.GetTeamInfo(m);
if (useTeam == null)
{
return;
}
DoAnim(GetWorldLocation(), m.Location, m.Map);
var verb = "caught";
if (Thrower != null && m_Game.GetTeamInfo(Thrower) != useTeam)
{
verb = "intercepted";
}
if (!TakeBomb(m, useTeam, verb))
{
MoveToWorld(m.Location, m.Map);
}
}
private void BeginFlight(Point3D dest)
{
var org = GetWorldLocation();
org.Z += 10; // always add 10 at the start cause we're coming from a mobile's eye level
/*if (org.X > dest.X || ( org.X == dest.X && org.Y > dest.Y ) || ( org.X == dest.X && org.Y == dest.Y && org.Z > dest.Z ))
{
Point3D swap = org;
org = dest;
dest = swap;
}*/
var list = new List<Point3D>();
var xd = dest.X - org.X;
var yd = dest.Y - org.Y;
var zd = dest.Z - org.Z;
var dist2d = Math.Sqrt(xd * xd + yd * yd);
var dist3d = zd == 0 ? dist2d : Math.Sqrt(dist2d * dist2d + zd * zd);
var rise = yd / dist3d;
var run = xd / dist3d;
var zslp = zd / dist3d;
double x = org.X;
double y = org.Y;
double z = org.Z;
while (Utility.NumberBetween(x, dest.X, org.X, 0.5) && Utility.NumberBetween(y, dest.Y, org.Y, 0.5) &&
Utility.NumberBetween(z, dest.Z, org.Z, 0.5))
{
var ix = (int)Math.Round(x);
var iy = (int)Math.Round(y);
var iz = (int)Math.Round(z);
if (list.Count > 0)
{
var p = list[^1];
if (p.X != ix || p.Y != iy || p.Z != iz)
{
list.Add(new Point3D(ix, iy, iz));
}
}
else
{
list.Add(new Point3D(ix, iy, iz));
}
x += run;
y += rise;
z += zslp;
}
if (list.Count > 0 && list[^1] != dest)
{
list.Add(dest);
}
/*if (dist3d > 4 && ( dest.X != org.X || dest.Y != org.Y ))
{
int count = list.Count;
int i;
int climb = count / 2;
if (climb > 3)
climb = 3;
for ( i = 0; i < climb; i++ )
{
p = ((Point3D)list[i]);
p.Z += (i+1) * 4;
list[i] = p;
}
for ( ; i < count - climb; i++ )
{
p = ((Point3D)list[i]);
p.Z += 16;
list[i] = p;
}
for ( i = climb; i > 0; i-- )
{
p = ((Point3D)list[i]);
p.Z += i * 4;
list[i] = p;
}
}*/
if (dist2d > 1)
{
var count = list.Count;
var height = count * 2 * (Utility.RandomDouble() * 0.40 + 0.10); // 10 - 50%
var coeff = -height / (count * count / 4.0);
for (var i = 0; i < count; i++)
{
var p = list[i];
var xp = i - count / 2;
p.Z += (int)Math.Ceiling(coeff * xp * xp + height);
list[i] = p;
}
}
m_Path.Clear();
for (var i = 0; i < list.Count; i++)
{
m_Path.Add(list[i]);
}
m_PathIdx = 0;
ContinueFlight();
}
private void ContinueFlight()
{
int height;
var found = false;
if (m_PathIdx < m_Path.Count && Map?.Tiles != null && Map != Map.Internal)
{
var pathCheckEnd = m_PathIdx + 5;
if (m_Path.Count < pathCheckEnd)
{
pathCheckEnd = m_Path.Count;
}
Visible = false;
if (m_PathIdx > 0) // move to the next location
{
MoveToWorld(m_Path[m_PathIdx - 1]);
}
Point3D pTop = new(GetWorldLocation()), pBottom = new(m_Path[pathCheckEnd - 1]);
Utility.FixPoints(ref pTop, ref pBottom);
for (var i = m_PathIdx; i < pathCheckEnd; i++)
{
var point = m_Path[i];
var landTile = Map.Tiles.GetLandTile(point.X, point.Y);
Map.GetAverageZ(point.X, point.Y, out var landZ, out _, out var landTop);
if (landZ <= point.Z && landTop >= point.Z && !landTile.Ignored)
{
HitObject(point, landTop, 0);
return;
}
var statics = Map.Tiles.GetStaticTiles(point.X, point.Y, true);
if (landTile.ID == 0x244 && statics.Length == 0) // 0x244 = invalid land tile
{
var eable = Map.GetItemsInRange(point, 0);
var empty = true;
foreach (var item in eable)
{
if (item != this)
{
empty = false;
break;
}
}
eable.Free();
if (empty)
{
HitObject(point, landTop, 0);
return;
}
}
for (var j = 0; j < statics.Length; j++)
{
var t = statics[j];
var id = TileData.ItemTable[t.ID & TileData.MaxItemValue];
height = id.CalcHeight;
if (t.Z <= point.Z && t.Z + height >= point.Z &&
(id.Flags & (TileFlag.Impassable | TileFlag.Wall | TileFlag.NoShoot)) != 0)
{
if (i > m_PathIdx)
{
point = m_Path[i - 1];
}
else
{
point = GetWorldLocation();
}
HitObject(point, t.Z, height);
return;
}
}
}
var rect = new Rectangle2D(pTop.X, pTop.Y, pBottom.X - pTop.X + 1, pBottom.Y - pTop.Y + 1);
var area = Map.GetItemsInBounds(rect);
foreach (var i in area)
{
if (i == this || i.ItemID >= 0x4000)
{
continue;
}
if (i is BRGoal)
{
height = 17;
}
else if (i is Blocker)
{
height = 20;
}
else
{
var id = i.ItemData;
if ((id.Flags & (TileFlag.Impassable | TileFlag.Wall | TileFlag.NoShoot)) == 0)
{
continue;
}
height = id.CalcHeight;
}
var point = i.Location;
var loc = i.Location;
for (var j = m_PathIdx; j < pathCheckEnd; j++)
{
point = m_Path[j];
if (loc.X == point.X && loc.Y == point.Y &&
(i is Blocker || loc.Z <= point.Z && loc.Z + height >= point.Z))
{
found = true;
if (j > m_PathIdx)
{
point = m_Path[j - 1];
}
else
{
point = GetWorldLocation();
}
break;
}
}
if (!found)
{
continue;
}
area.Free();
if (i is BRGoal goal)
{
var oldLoc = new Point3D(GetWorldLocation());
if (CheckScore(goal, Thrower, 3))
{
DoAnim(oldLoc, point, Map);
}
else
{
HitObject(point, loc.Z, height);
}
}
else
{
HitObject(point, loc.Z, height);
}
return;
}
area.Free();
var clients = Map.GetClientsInBounds(rect);
foreach (var ns in clients)
{
var m = ns.Mobile;
if (m == null || m == Thrower)
{
continue;
}
Point3D point;
var loc = m.Location;
for (var j = m_PathIdx; j < pathCheckEnd && !found; j++)
{
point = m_Path[j];
if (loc.X == point.X && loc.Y == point.Y &&
loc.Z <= point.Z && loc.Z + 16 >= point.Z)
{
found = CheckCatch(m, point);
}
}
if (!found)
{
continue;
}
clients.Free();
// TODO: probably need to change this a lot...
DoCatch(m);
return;
}
clients.Free();
m_PathIdx = pathCheckEnd;
if (m_PathIdx > 0 && m_PathIdx - 1 < m_Path.Count)
{
DoAnim(GetWorldLocation(), m_Path[m_PathIdx - 1], Map);
}
Timer.StartTimer(TimeSpan.FromSeconds(0.1), ContinueFlight);
}
else
{
if (m_PathIdx > 0 && m_PathIdx - 1 < m_Path.Count)
{
MoveToWorld(m_Path[m_PathIdx - 1]);
}
else if (m_Path.Count > 0)
{
MoveToWorld(m_Path.Last);
}
var myZ = Map?.GetAverageZ(X, Y) ?? 0;
var statics = Map?.Tiles?.GetStaticTiles(X, Y, true);
if (statics != null)
{
for (var j = 0; j < statics.Length; j++)
{
var t = statics[j];
var id = TileData.ItemTable[t.ID & TileData.MaxItemValue];
height = id.CalcHeight;
if (t.Z + height > myZ && t.Z + height <= Z)
{
myZ = t.Z + height;
}
}
}
var eable = GetItemsInRange(0);
foreach (var item in eable)
{
if (item.Visible && item != this)
{
height = item.ItemData.CalcHeight;
if (item.Z + height > myZ && item.Z + height <= Z)
{
myZ = item.Z + height;
}
}
}
eable.Free();
Z = myZ;
m_Flying = false;
Visible = true;
m_Path.Clear();
m_PathIdx = 0;
}
}
public bool CheckScore(BRGoal goal, Mobile m, int points)
{
if (m_Game == null || m == null || goal == null)
{
return false;
}
var team = m_Game.GetTeamInfo(m);
if (team == null || goal.Team == null || team == goal.Team)
{
return false;
}
if (points > 3)
{
m_Game.Alert("Touchdown {0} ({1})!", team.Name, m.Name);
}
else
{
m_Game.Alert("Field goal {0} ({1})!", team.Name, m.Name);
}
for (var i = m_Helpers.Count - 1; i >= 0; i--)
{
var mob = m_Helpers[i];
var pi = team[mob];
if (pi != null)
{
if (mob == m)
{
pi.Captures += points;
}
pi.Score += points + 1;
points /= 2;
}
}
m_Game.ReturnBomb();
m_Flying = false;
Visible = true;
m_Path.Clear();
m_PathIdx = 0;
Target.Cancel(m);
return true;
}
private bool TakeBomb(Mobile m, BRTeamInfo team, string verb)
{
if (!m.Player || !m.Alive || m.NetState == null)
{
return false;
}
if (m.PlaceInBackpack(this))
{
m.RevealingAction();
m.LocalOverheadMessage(MessageType.Regular, 0x59, false, "You got the bomb!");
m_Game.Alert("{1} ({2}) {0} the bomb!", verb, m.Name, team.Name);
m.Target = new BombTarget(this, m);
if (m_Helpers.Contains(m))
{
m_Helpers.Remove(m);
}
if (m_Helpers.Count > 0)
{
var last = m_Helpers[0];
if (m_Game.GetTeamInfo(last) != team)
{
m_Helpers.Clear();
}
}
m_Helpers.Add(m);
return true;
}
return false;
}
private class EffectTimer : Timer
{
private readonly BRBomb m_Bomb;
private int m_Count;
public EffectTimer(BRBomb bomb) : base(TimeSpan.Zero, TimeSpan.FromSeconds(1.0))
{
m_Bomb = bomb;
m_Count = 0;
}
protected override void OnTick()
{
if (m_Bomb.Parent == null && m_Bomb.m_Game?.Controller != null)
{
if (!m_Bomb.m_Flying && m_Bomb.Map != Map.Internal)
{
Effects.SendLocationEffect(m_Bomb.GetWorldLocation(), m_Bomb.Map, 0x377A, 16, 10, m_Bomb.Hue);
}
if (m_Bomb.Location != m_Bomb.m_Game.Controller.BombHome)
{
if (++m_Count >= 30)
{
m_Bomb.m_Game.ReturnBomb();
m_Bomb.m_Game.Alert("The bomb has been returned to it's starting point.");
m_Count = 0;
m_Bomb.m_Helpers.Clear();
}
}
else
{
m_Count = 0;
}
}
else
{
m_Count = 0;
}
}
}
private class BombTarget : Target
{
private readonly BRBomb m_Bomb;
private readonly Mobile m_Mob;
private bool m_Resend = true;
public BombTarget(BRBomb bomb, Mobile from) : base(10, true, TargetFlags.None)
{
CheckLOS = false;
m_Bomb = bomb;
m_Mob = from;
m_Mob.SendMessage(0x26, "Where do you want to throw it?");
}
protected override void OnTarget(Mobile from, object targeted)
{
m_Resend = !m_Bomb.OnBombTarget(from, targeted);
}
protected override void OnTargetUntargetable(Mobile from, object targeted)
{
m_Resend = !m_Bomb.OnBombTarget(from, targeted);
}
protected override void OnTargetFinish(Mobile from)
{
base.OnTargetFinish(from);
// has to be delayed in case some other target canceled us...
if (m_Resend)
{
Timer.StartTimer(ResendBombTarget);
}
}
private void ResendBombTarget()
{
// Make sure they still have the bomb, then give them the target back
if (m_Bomb?.Deleted == false && m_Mob?.Deleted == false && m_Mob.Alive && m_Bomb.IsChildOf(m_Mob))
{
m_Mob.Target = new BombTarget(m_Bomb, m_Mob);
}
}
}
}
public class BRGoal : BaseAddon
{
private bool m_North;
private BRTeamInfo m_Team;
[Constructible]
public BRGoal()
{
ItemID = 0x51D;
Hue = 0x84C;
Visible = true;
Remake();
}
public BRGoal(Serial serial) : base(serial)
{
}
[CommandProperty(AccessLevel.GameMaster)]
public bool North
{
get => m_North;
set
{
m_North = value;
Remake();
}
}
public override string DefaultName => "Bombing Run Goal";
public override bool ShareHue => false;
public BRTeamInfo Team
{
get => m_Team;
set
{
m_Team = value;
if (m_Team != null && m_Team.Color != 0)
{
Hue = m_Team.Color;
}
else
{
Hue = 0x84C;
}
}
}
private static AddonComponent SetHue(AddonComponent ac, int hue)
{
ac.Hue = hue;
return ac;
}
private void Remake()
{
foreach (var ac in Components)
{
ac.Addon = null;
ac.Delete();
}
Components.Clear();
// stairs
AddComponent(new AddonComponent(0x74D), -1, +1, -5);
AddComponent(new AddonComponent(0x71F), 0, +1, -5);
AddComponent(new AddonComponent(0x74B), +1, +1, -5);
AddComponent(new AddonComponent(0x736), +1, 0, -5);
AddComponent(new AddonComponent(0x74C), +1, -1, -5);
AddComponent(new AddonComponent(0x737), 0, -1, -5);
AddComponent(new AddonComponent(0x74A), -1, -1, -5);
AddComponent(new AddonComponent(0x749), -1, 0, -5);
// Center Sparkle
AddComponent(new AddonComponent(0x375A), 0, 0, -1);
if (!m_North)
{
// Pillars
AddComponent(new AddonComponent(0x0CE), 0, +1, -2);
AddComponent(new AddonComponent(0x0CC), 0, -1, -2);
AddComponent(new AddonComponent(0x0D0), 0, 0, -2);
// Yellow parts
AddComponent(SetHue(new AddonComponent(0x0DF), 0x499), 0, +1, 7);
AddComponent(SetHue(new AddonComponent(0x0DF), 0x499), 0, 0, 16);
AddComponent(SetHue(new AddonComponent(0x0DF), 0x499), 0, -1, 7);
// Blue Sparkles
AddComponent(SetHue(new AddonComponent(0x377A), 0x84C), 0, +1, 12);
AddComponent(SetHue(new AddonComponent(0x377A), 0x84C), 0, +1, -1);
AddComponent(SetHue(new AddonComponent(0x377A), 0x84C), 0, -1, 12);
AddComponent(SetHue(new AddonComponent(0x377A), 0x84C), 0, -1, -1);
}
else
{
// Pillars
AddComponent(new AddonComponent(0x0CF), +1, 0, -2);
AddComponent(new AddonComponent(0x0CC), -1, 0, -2);
AddComponent(new AddonComponent(0x0D1), 0, 0, -2);
// Yellow parts
AddComponent(SetHue(new AddonComponent(0x0DF), 0x499), +1, 0, 7);
AddComponent(SetHue(new AddonComponent(0x0DF), 0x499), 0, 0, 16);
AddComponent(SetHue(new AddonComponent(0x0DF), 0x499), -1, 0, 7);
// Blue Sparkles
AddComponent(SetHue(new AddonComponent(0x377A), 0x84C), +1, 0, 12);
AddComponent(SetHue(new AddonComponent(0x377A), 0x84C), +1, 0, -1);
AddComponent(SetHue(new AddonComponent(0x377A), 0x84C), -1, 0, 12);
AddComponent(SetHue(new AddonComponent(0x377A), 0x84C), -1, 0, -1);
}
}
public override void Serialize(IGenericWriter writer)
{
base.Serialize(writer);
writer.Write(1); // version
writer.Write(m_North);
}
public override void Deserialize(IGenericReader reader)
{
base.Deserialize(reader);
var version = reader.ReadInt();
switch (version)
{
case 1:
{
m_North = reader.ReadBool();
goto case 0;
}
case 0:
{
break;
}
}
Hue = 0x84C;
}
public override bool OnMoveOver(Mobile m)
{
if (!Visible)
{
return true;
}
if (m?.Player != true || !m.Alive || m.Backpack == null || m_Team?.Game == null)
{
return true;
}
if (!base.OnMoveOver(m))
{
return false;
}
if (m_Team != null && m_Team.Color != 0)
{
Hue = m_Team.Color;
}
else
{
Hue = 0x84C;
}
m.Backpack.FindItemByType<BRBomb>()?.CheckScore(this, m, 7);
return true;
}
}
public sealed class BRBoard : Item
{
public BRTeamInfo m_TeamInfo;
[Constructible]
public BRBoard()
: base(7774) =>
Movable = false;
public BRBoard(Serial serial)
: base(serial)
{
}
public override string DefaultName => "Scoreboard";
public override void OnDoubleClick(Mobile from)
{
if (m_TeamInfo?.Game != null)
{
from.CloseGump<BRBoardGump>();
from.SendGump(new BRBoardGump(from, m_TeamInfo.Game));
}
}
public override void Serialize(IGenericWriter writer)
{
base.Serialize(writer);
writer.Write(0);
}
public override void Deserialize(IGenericReader reader)
{
base.Deserialize(reader);
var version = reader.ReadInt();
}
}
public class BRBoardGump : Gump
{
private const int LabelColor32 = 0xFFFFFF;
private const int BlackColor32 = 0x000000;
// private BRGame m_Game;
public BRBoardGump(Mobile mob, BRGame game, BRTeamInfo section = null) : base(60, 60)
{
// m_Game = game;
var ourTeam = game.GetTeamInfo(mob);
var entries = new List<BRTeamInfo>();
var total = 0;
if (section == null)
{
for (var i = 0; i < game.Context.Participants.Count; ++i)
{
var teamInfo = game.Controller.TeamInfo[i % game.Controller.TeamInfo.Length];
if (teamInfo == null)
{
continue;
}
entries.Add(teamInfo);
}
total = entries.Count;
}
else
{
foreach (var player in section.Players.Values)
{
if (player.Score > 0)
{
total++;
}
}
}
entries.Sort();
var height = 0;
if (section == null)
{
height = 73 + entries.Count * 75 + 28;
}
Closable = false;
AddPage(0);
AddBackground(1, 1, 398, height, 3600);
AddImageTiled(16, 15, 369, height - 29, 3604);
for (var i = 0; i < total; i += 1)
{
AddImageTiled(22, 58 + i * 75, 357, 70, 0x2430);
}
AddAlphaRegion(16, 15, 369, height - 29);
AddImage(215, -45, 0xEE40);
// AddImage( 330, 141, 0x8BA );
AddBorderedText(22, 22, 294, 20, Center("BR Scoreboard"), LabelColor32, BlackColor32);
AddImageTiled(32, 50, 264, 1, 9107);
AddImageTiled(42, 52, 264, 1, 9157);
if (section == null)
{
for (var i = 0; i < entries.Count; ++i)
{
var teamInfo = entries[i];
AddImage(30, 70 + i * 75, 10152);
AddImage(30, 85 + i * 75, 10151);
AddImage(30, 100 + i * 75, 10151);
AddImage(30, 106 + i * 75, 10154);
AddImage(24, 60 + i * 75, teamInfo == ourTeam ? 9730 : 9727, teamInfo.Color - 1);
var nameColor = LabelColor32;
var borderColor = BlackColor32;
switch (teamInfo.Color)
{
case 0x47E:
nameColor = 0xFFFFFF;
break;
case 0x4F2:
nameColor = 0x3399FF;
break;
case 0x4F7:
nameColor = 0x33FF33;
break;
case 0x4FC:
nameColor = 0xFF00FF;
break;
case 0x021:
nameColor = 0xFF3333;
break;
case 0x01A:
nameColor = 0xFF66FF;
break;
case 0x455:
nameColor = 0x333333;
borderColor = 0xFFFFFF;
break;
}
AddBorderedText(
60,
65 + i * 75,
250,
20,
$"{LadderGump.Rank(1 + i)}: {teamInfo.Name}",
nameColor,
borderColor
);
AddBorderedText(50 + 10, 85 + i * 75, 100, 20, "Score:", 0xFFC000, BlackColor32);
AddBorderedText(50 + 15, 105 + i * 75, 100, 20, teamInfo.Score.ToString("N0"), 0xFFC000, BlackColor32);
AddBorderedText(110 + 10, 85 + i * 75, 100, 20, "Kills:", 0xFFC000, BlackColor32);
AddBorderedText(110 + 15, 105 + i * 75, 100, 20, teamInfo.Kills.ToString("N0"), 0xFFC000, BlackColor32);
AddBorderedText(160 + 10, 85 + i * 75, 100, 20, "Points:", 0xFFC000, BlackColor32);
AddBorderedText(
160 + 15,
105 + i * 75,
100,
20,
teamInfo.Captures.ToString("N0"),
0xFFC000,
BlackColor32
);
var pl = teamInfo.Leader;
AddBorderedText(235 + 10, 85 + i * 75, 250, 20, "Leader:", 0xFFC000, BlackColor32);
if (pl != null)
{
AddBorderedText(235 + 15, 105 + i * 75, 250, 20, pl.Player.Name, 0xFFC000, BlackColor32);
}
}
}
AddButton(314, height - 42, 247, 248, 1);
}
public string Center(string text) => $"<CENTER>{text}</CENTER>";
public string Color(string text, int color) => $"<BASEFONT COLOR=#{color:X6}>{text}</BASEFONT>";
private void AddBorderedText(int x, int y, int width, int height, string text, int color, int borderColor)
{
AddColoredText(x - 1, y - 1, width, height, text, borderColor);
AddColoredText(x - 1, y + 1, width, height, text, borderColor);
AddColoredText(x + 1, y - 1, width, height, text, borderColor);
AddColoredText(x + 1, y + 1, width, height, text, borderColor);
AddColoredText(x, y, width, height, text, color);
}
private void AddColoredText(int x, int y, int width, int height, string text, int color)
{
if (color == 0)
{
AddHtml(x, y, width, height, text);
}
else
{
AddHtml(x, y, width, height, Color(text, color));
}
}
}
public sealed class BRPlayerInfo : IRankedCTF, IComparable<BRPlayerInfo>
{
private readonly BRTeamInfo m_TeamInfo;
private int m_Captures;
private int m_Kills;
private int m_Score;
public BRPlayerInfo(BRTeamInfo teamInfo, Mobile player)
{
m_TeamInfo = teamInfo;
Player = player;
}
public Mobile Player { get; }
public int CompareTo(BRPlayerInfo pi)
{
var res = pi.Captures.CompareTo(Captures);
if (res != 0)
{
return res;
}
res = pi.Score.CompareTo(Score);
if (res == 0)
{
res = pi.Kills.CompareTo(Kills);
}
return res;
}
public string Name => Player.Name;
public int Kills
{
get => m_Kills;
set
{
m_TeamInfo.Kills += value - m_Kills;
m_Kills = value;
}
}
public int Captures
{
get => m_Captures;
set
{
m_TeamInfo.Captures += value - m_Captures;
m_Captures = value;
}
}
public int Score
{
get => m_Score;
set
{
m_TeamInfo.Score += value - m_Score;
m_Score = value;
if (m_TeamInfo.Leader == null || m_Score > m_TeamInfo.Leader.Score)
{
m_TeamInfo.Leader = this;
}
}
}
}
[PropertyObject]
public sealed class BRTeamInfo : IRankedCTF, IComparable<BRTeamInfo>
{
private BRGoal m_Goal;
public BRTeamInfo(int teamID)
{
TeamID = teamID;
Players = new Dictionary<Mobile, BRPlayerInfo>();
}
public BRTeamInfo(int teamID, IGenericReader ip)
{
TeamID = teamID;
Players = new Dictionary<Mobile, BRPlayerInfo>();
var version = ip.ReadEncodedInt();
switch (version)
{
case 0:
{
Board = ip.ReadEntity<BRBoard>();
TeamName = ip.ReadString();
Color = ip.ReadEncodedInt();
m_Goal = ip.ReadEntity<BRGoal>();
break;
}
}
}
public BRGame Game { get; set; }
public int TeamID { get; }
public BRPlayerInfo Leader { get; set; }
[CommandProperty(AccessLevel.GameMaster)]
public BRBoard Board { get; set; }
public Dictionary<Mobile, BRPlayerInfo> Players { get; }
public BRPlayerInfo this[Mobile mob]
{
get
{
if (mob == null)
{
return null;
}
if (!Players.TryGetValue(mob, out var val))
{
Players[mob] = val = new BRPlayerInfo(this, mob);
}
return val;
}
}
[CommandProperty(AccessLevel.GameMaster)]
public int Color { get; set; }
[CommandProperty(AccessLevel.GameMaster)]
public string TeamName { get; set; }
[CommandProperty(AccessLevel.GameMaster)]
public BRGoal Goal
{
get => m_Goal;
set
{
m_Goal = value;
if (m_Goal != null)
{
m_Goal.Team = this;
}
}
}
public int CompareTo(BRTeamInfo ti)
{
var res = ti.Captures.CompareTo(Captures);
if (res == 0)
{
res = ti.Score.CompareTo(Score);
if (res == 0)
{
res = ti.Kills.CompareTo(Kills);
}
}
return res;
}
public string Name => $"{TeamName} Team";
public int Kills { get; set; }
public int Captures { get; set; }
public int Score { get; set; }
public void Reset()
{
Kills = 0;
Captures = 0;
Score = 0;
Leader = null;
Players.Clear();
if (Board != null)
{
Board.m_TeamInfo = this;
}
if (m_Goal != null)
{
m_Goal.Team = this;
}
}
public void Serialize(IGenericWriter op)
{
op.WriteEncodedInt(0); // version
op.Write(Board);
op.Write(TeamName);
op.WriteEncodedInt(Color);
op.Write(m_Goal);
}
public override string ToString()
{
if (TeamName != null)
{
return $"({Name}) ...";
}
return "...";
}
}
public sealed class BRController : EventController
{
[Constructible]
public BRController()
{
Visible = false;
Movable = false;
Duration = TimeSpan.FromMinutes(30.0);
BombHome = Point3D.Zero;
TeamInfo = new BRTeamInfo[4];
for (var i = 0; i < TeamInfo.Length; ++i)
{
TeamInfo[i] = new BRTeamInfo(i);
}
}
public BRController(Serial serial)
: base(serial)
{
}
public BRTeamInfo[] TeamInfo { get; private set; }
[CommandProperty(AccessLevel.GameMaster)]
public BRTeamInfo Team1 => TeamInfo[0];
[CommandProperty(AccessLevel.GameMaster)]
public BRTeamInfo Team2 => TeamInfo[1];
[CommandProperty(AccessLevel.GameMaster)]
public BRTeamInfo Team3 => TeamInfo[2];
[CommandProperty(AccessLevel.GameMaster)]
public BRTeamInfo Team4 => TeamInfo[3];
[CommandProperty(AccessLevel.GameMaster)]
public TimeSpan Duration { get; set; }
[CommandProperty(AccessLevel.GameMaster)]
public Point3D BombHome { get; set; }
public override string Title => "Bombing Run";
public override string DefaultName => "Bombing Run Controller";
public override string GetTeamName(int teamID) => TeamInfo[teamID % TeamInfo.Length].Name;
public override EventGame Construct(DuelContext context) => new BRGame(this, context);
public override void Serialize(IGenericWriter writer)
{
base.Serialize(writer);
writer.Write(0);
writer.Write(BombHome);
writer.Write(Duration);
writer.WriteEncodedInt(TeamInfo.Length);
for (var i = 0; i < TeamInfo.Length; ++i)
{
TeamInfo[i].Serialize(writer);
}
}
public override void Deserialize(IGenericReader reader)
{
base.Deserialize(reader);
var version = reader.ReadInt();
switch (version)
{
case 0:
{
BombHome = reader.ReadPoint3D();
Duration = reader.ReadTimeSpan();
TeamInfo = new BRTeamInfo[reader.ReadEncodedInt()];
for (var i = 0; i < TeamInfo.Length; ++i)
{
TeamInfo[i] = new BRTeamInfo(i, reader);
}
break;
}
}
}
}
public sealed class BRGame : EventGame
{
private BRBomb m_Bomb;
private TimerExecutionToken _finishTimerToken;
public BRGame(BRController controller, DuelContext context) : base(context) => Controller = controller;
public BRController Controller { get; }
public Map Facet => m_Context.Arena != null ? m_Context.Arena.Facet : Controller.Map;
public override bool CantDoAnything(Mobile mob) =>
mob.Backpack?.FindItemByType<BRBomb>() != null && GetTeamInfo(mob) != null;
public void ReturnBomb()
{
if (m_Bomb != null && Controller != null)
{
m_Bomb.Visible = false;
m_Bomb.MoveToWorld(Controller.BombHome, Controller.Map);
Timer.StartTimer(TimeSpan.FromSeconds(Utility.RandomMinMax(5, 15)), UnhideBomb);
}
}
private void UnhideBomb()
{
if (m_Bomb != null)
{
m_Bomb.Visible = true;
m_Bomb.OnLocationChange(m_Bomb.Location);
}
}
public void Alert(string text)
{
m_Context.m_Tournament?.Alert(text);
for (var i = 0; i < m_Context.Participants.Count; ++i)
{
var p = m_Context.Participants[i];
for (var j = 0; j < p.Players.Length; ++j)
{
if (p.Players[j] != null)
{
p.Players[j].Mobile.SendMessage(0x35, text);
}
}
}
}
public void Alert(string format, params object[] args)
{
Alert(string.Format(format, args));
}
public BRTeamInfo GetTeamInfo(Mobile mob)
{
var teamID = GetTeamID(mob);
if (teamID >= 0)
{
return Controller.TeamInfo[teamID % Controller.TeamInfo.Length];
}
return null;
}
public int GetTeamID(Mobile mob)
{
if (!(mob is PlayerMobile pm))
{
return mob is BaseCreature creature ? creature.Team - 1 : -1;
}
if (pm.DuelContext == null || pm.DuelContext != m_Context)
{
return -1;
}
if (pm.DuelPlayer?.Eliminated != false)
{
return -1;
}
return pm.DuelContext.Participants.IndexOf(pm.DuelPlayer.Participant);
}
public int GetColor(Mobile mob) => GetTeamInfo(mob)?.Color ?? -1;
private void ApplyHues(Participant p, int hueOverride)
{
for (var i = 0; i < p.Players.Length; ++i)
{
if (p.Players[i] != null)
{
p.Players[i].Mobile.SolidHueOverride = hueOverride;
}
}
}
public void DelayBounce(TimeSpan ts, Mobile mob, Container corpse)
{
Timer.StartTimer(ts, () => DelayBounce_Callback(mob, corpse));
}
private void DelayBounce_Callback(Mobile mob, Container corpse)
{
var dp = (mob as PlayerMobile)?.DuelPlayer;
m_Context.RemoveAggressions(mob);
if (dp?.Eliminated == false)
{
mob.MoveToWorld(m_Context.Arena.GetBaseStartPoint(GetTeamID(mob)), Facet);
}
else
{
m_Context.SendOutside(mob);
}
m_Context.Refresh(mob, corpse);
DuelContext.Debuff(mob);
DuelContext.CancelSpell(mob);
mob.Frozen = false;
}
public override bool OnDeath(Mobile mob, Container corpse)
{
var killer = mob.FindMostRecentDamager(false);
var hadBomb = false;
corpse.FindItemsByType<BRBomb>(false)
.ForEach(
bomb =>
{
hadBomb = true;
bomb.DropTo(mob, killer);
}
);
mob.Backpack?.FindItemsByType<BRBomb>(false)
.ForEach(
bomb =>
{
hadBomb = true;
bomb.DropTo(mob, killer);
}
);
if (killer?.Player == true)
{
var teamInfo = GetTeamInfo(killer);
var victInfo = GetTeamInfo(mob);
if (teamInfo != null && teamInfo != victInfo)
{
var playerInfo = teamInfo[killer];
if (playerInfo != null)
{
playerInfo.Kills += 1;
playerInfo.Score += 1; // base frag
if (hadBomb)
{
playerInfo.Score += 4; // fragged bomb carrier
}
}
}
}
mob.CloseGump<BRBoardGump>();
mob.SendGump(new BRBoardGump(mob, this));
m_Context.Requip(mob, corpse);
DelayBounce(TimeSpan.FromSeconds(30.0), mob, corpse);
return false;
}
public override void OnStart()
{
for (var i = 0; i < Controller.TeamInfo.Length; ++i)
{
var teamInfo = Controller.TeamInfo[i];
teamInfo.Game = this;
teamInfo.Reset();
}
for (var i = 0; i < m_Context.Participants.Count; ++i)
{
ApplyHues(
m_Context.Participants[i],
Controller.TeamInfo[i % Controller.TeamInfo.Length].Color
);
}
_finishTimerToken.Cancel();
m_Bomb = new BRBomb(this);
ReturnBomb();
Timer.StartTimer(Controller.Duration, Finish_Callback, out _finishTimerToken);
}
private void Finish_Callback()
{
var teams = new List<BRTeamInfo>();
for (var i = 0; i < m_Context.Participants.Count; ++i)
{
var teamInfo = Controller.TeamInfo[i % Controller.TeamInfo.Length];
if (teamInfo != null)
{
teams.Add(teamInfo);
}
}
teams.Sort();
var tourney = m_Context.m_Tournament;
var sb = new StringBuilder();
if (tourney != null)
{
if (tourney.TourneyType == TourneyType.FreeForAll)
{
sb.Append(m_Context.Participants.Count * tourney.PlayersPerParticipant);
sb.Append("-man FFA");
}
else if (tourney.TourneyType == TourneyType.RandomTeam)
{
sb.Append(tourney.ParticipantsPerMatch);
sb.Append("-team");
}
else if (tourney.TourneyType == TourneyType.RedVsBlue)
{
sb.Append("Red v Blue");
}
else if (tourney.TourneyType == TourneyType.Faction)
{
sb.Append(tourney.ParticipantsPerMatch);
sb.Append("-team Faction");
}
else
{
for (var i = 0; i < tourney.ParticipantsPerMatch; ++i)
{
if (sb.Length > 0)
{
sb.Append('v');
}
sb.Append(tourney.PlayersPerParticipant);
}
}
}
if (Controller != null)
{
sb.Append(' ').Append(Controller.Title);
}
var title = sb.ToString();
var winner = teams.Count > 0 ? teams[0] : null;
for (var i = 0; i < teams.Count; ++i)
{
var rank = i switch
{
0 => TrophyRank.Gold,
1 => TrophyRank.Silver,
_ => TrophyRank.Bronze
};
var leader = teams[i].Leader;
foreach (var pl in teams[i].Players.Values)
{
var mob = pl.Player;
if (mob == null)
{
continue;
}
sb = new StringBuilder();
sb.Append(title);
if (pl == leader)
{
sb.Append(" Leader");
}
if (pl.Score > 0)
{
sb.Append(": ");
// sb.Append( pl.Score.ToString( "N0" ) );
// sb.Append( pl.Score == 1 ? " point" : " points" );
sb.Append(pl.Kills.ToString("N0"));
sb.Append(pl.Kills == 1 ? " kill" : " kills");
if (pl.Captures > 0)
{
sb.Append(", ");
sb.Append(pl.Captures.ToString("N0"));
sb.Append(pl.Captures == 1 ? " point" : " points");
}
}
Item item = new Trophy(sb.ToString(), rank);
if (pl == leader)
{
item.ItemID = 4810;
}
item.Name = $"{item.Name}, {teams[i].Name.ToLower()} team";
if (!mob.PlaceInBackpack(item))
{
mob.BankBox.DropItem(item);
}
var cash = pl.Score * 250;
if (cash > 0)
{
item = new BankCheck(cash);
if (!mob.PlaceInBackpack(item))
{
mob.BankBox.DropItem(item);
}
mob.SendMessage(
"You have been awarded a {0} trophy and {1:N0}gp for your participation in this tournament.",
rank.ToString().ToLower(),
cash
);
}
else
{
mob.SendMessage(
"You have been awarded a {0} trophy for your participation in this tournament.",
rank.ToString().ToLower()
);
}
}
}
for (var i = 0; i < m_Context.Participants.Count; ++i)
{
var p = m_Context.Participants[i];
if (p?.Players == null)
{
continue;
}
for (var j = 0; j < p.Players.Length; ++j)
{
var dp = p.Players[j];
if (dp?.Mobile != null)
{
dp.Mobile.CloseGump<BRBoardGump>();
dp.Mobile.SendGump(new BRBoardGump(dp.Mobile, this));
}
}
if (i == winner?.TeamID)
{
continue;
}
if (p.Players != null)
{
for (var j = 0; j < p.Players.Length; ++j)
{
if (p.Players[j] != null)
{
p.Players[j].Eliminated = true;
}
}
}
}
if (winner != null)
{
m_Context.Finish(m_Context.Participants[winner.TeamID]);
}
}
public override void OnStop()
{
for (var i = 0; i < Controller.TeamInfo.Length; ++i)
{
var teamInfo = Controller.TeamInfo[i];
if (teamInfo.Board != null)
{
teamInfo.Board.m_TeamInfo = null;
}
teamInfo.Game = null;
}
ReturnBomb();
m_Bomb?.Delete();
for (var i = 0; i < m_Context.Participants.Count; ++i)
{
ApplyHues(m_Context.Participants[i], -1);
}
_finishTimerToken.Cancel();
}
}
}