ModernUO/Projects/UOContent/Engines/ConPVP/Games/CTF.cs
Kamron Batman 61e41df00c
feat: Add zero-alloc interpolation handler to ValueStringBuilder, replace all StringBuilder usage (#2387)
## Summary

- **Add a self-referencing `InterpolationHandler` to `ValueStringBuilder`** that writes directly into the builder's buffer — zero intermediate allocation, works with `stackalloc`-backed builders
- **Replace all `System.Text.StringBuilder` usage** across the codebase with `ValueStringBuilder`
- **Convert `ValueStringBuilder.Create()` to `stackalloc`** at 10 sites where output length is provably bounded
- **Convert manual `Dispose()` to `using var`** where possible, and hoist loop-scoped builders outside loops with `Reset()`
- **Convert verbose `Append()` chains to `Append($"...")`** interpolation for readability
- **Add comprehensive documentation** for string handling patterns

## InterpolationHandler Design

`ValueStringBuilder` is a `ref struct`, which creates challenges for C#'s interpolated string handler pattern:

- **`ref` fields to ref structs are not allowed** (CS9050)
- **`[InterpolatedStringHandlerArgument("")]` passes struct receivers by value**, not by ref
- **`ISelfInterpolatedStringHandler` requires boxing** ref structs into interface fields

**Solution: Copy-and-reconcile pattern.** The handler receives a value copy of the builder. The copy shares the same underlying `char` buffer (`Span` points to the same `stackalloc`/pooled memory), so writes go to the original buffer. `Append()` reconciles by `this = handler._builder`, updating `_length` and any buffer references changed by `Grow()`.

This is safe because:
- The game loop is single-threaded — no concurrent access between handler construction and reconciliation
- If `Grow()` occurs in the copy, the original's stale buffer isn't accessed until `Append()` replaces it
- `Dispose()` correctly returns the reconciled buffer to the pool

## Changes by Category

### ValueStringBuilder (`Projects/Server/Buffers/ValueStringBuilder.cs`)
- Added nested `InterpolationHandler` ref struct with copy-and-reconcile pattern
- Added `Append([InterpolatedStringHandlerArgument("")] scoped ref InterpolationHandler)` method
- Removed `RawInterpolatedStringHandler` overloads (new handler replaces them)
- All `AppendFormatted` overloads delegate to existing `Append` methods (no code duplication)
- Alignment support via direct private field access (nested type privilege)

### StringBuilder → ValueStringBuilder (15 files)
Replaced all `new StringBuilder()` with `ValueStringBuilder.Create()` or `stackalloc`:
- ConPVP games: KingOfTheHill, DoubleDom, CTF, BombingRun, TourneyMatch
- ConPVP infrastructure: Tournament, Participant, TourneyParticipant
- ConPVP gumps: ArenaGump, TournamentBracketGump, AcceptTeamGump, ConfirmSignupGump
- Commands: Handlers, Logging, Add
- Other: TownCrier, SpeechLogGump, TestCenter

Key patterns:
- `sb = new StringBuilder()` reassignment → `sb.Reset()`
- `sb.AppendFormat("{0:N0}", value)` → `sb.Append($"{value:N0}")`
- `sb.Append(x).Append(y)` chains → separate statements (VSB returns void)

### Create() → stackalloc (10 files)
Converted heap-allocated builders to stackalloc where output is bounded:
- ClientVersion (32), MapSelection (160), HouseRaffleStone (48)
- HolySense (96), UnholySense (96), ClientVerification (192)
- AcceptTeamGump (64), ConfirmSignupGump (64)
- BaseWeapon (160), BaseArmor (128)

### Loop optimizations (2 files)
Hoisted `ValueStringBuilder` creation outside loops with `Reset()` per iteration:
- TourneyMatch.cs: `using var` inside for loop → stackalloc before loop
- ArenaGump.cs: `Create()` + `Dispose()` per iteration → stackalloc before loop

### Append chain → interpolation (5 files)
Converted multi-line `Append()` chains to `Append($"...")`:
- BountyMessage.cs: title switch (6 cases), paragraph (15→1 Append), description lines, closing
- AcceptTeamGump, ConfirmSignupGump, TournamentBracketGump: tournament type strings
- AdminGump: comment/tag formatting in loops

### Documentation
- `dev-docs/string-handling.md`: Full reference — construction, interpolation, disposal, decision guide
- `dev-docs/claude-skills/modernuo-string-handling.md`: Claude skill with quick reference
- `CLAUDE.md`: Added rule 17 (no StringBuilder), dev-docs table entry, skills table entry
- `dev-docs/code-standards.md`: Updated memory management section

## Test Plan

- [x] `dotnet build` — 0 errors, 0 warnings
- [x] `dotnet test` — 940/940 tests pass
- [x] 28 ValueStringBuilder tests covering all reconciliation scenarios:
  - Stackalloc no-grow, stackalloc with grow (→pool transition)
  - Heap no-grow, heap with grow, heap double grow
  - Pre-existing content with and without grow
  - Sequential multiple `Append($"...")` calls
  - Mixed plain + interpolated Append
  - Empty interpolation, literal-only, format specifiers
  - Null string holes, ISpanFormattable types
  - Dispose after stackalloc→pool grow
2026-03-22 14:23:44 -07:00

1278 lines
33 KiB
C#

using System;
using System.Collections.Generic;
using ModernUO.Serialization;
using Server.Gumps;
using Server.Items;
using Server.Mobiles;
using Server.Network;
using Server.Targeting;
using Server.Text;
namespace Server.Engines.ConPVP;
[SerializationGenerator(0, false)]
public sealed partial class CTFBoard : Item
{
public CTFTeamInfo m_TeamInfo;
[Constructible]
public CTFBoard() : base(7774) => Movable = false;
public override string DefaultName => "scoreboard";
public override void OnDoubleClick(Mobile from)
{
if (m_TeamInfo?.Game != null)
{
from.SendGump(new CTFBoardGump(from, m_TeamInfo.Game));
}
}
}
public class CTFBoardGump : Gump
{
private const int LabelColor32 = 0xFFFFFF;
private const int BlackColor32 = 0x000000;
private CTFGame m_Game;
public override bool Singleton => true;
public CTFBoardGump(Mobile mob, CTFGame game, CTFTeamInfo section = null)
: base(60, 60)
{
m_Game = game;
var ourTeam = game.GetTeamInfo(mob);
var entries = new List<IRankedCTF>();
if (section == null)
{
for (var i = 0; i < game.Context.Participants.Count; ++i)
{
var teamInfo = game.Controller.TeamInfo[i % 8];
if (teamInfo?.Flag == null)
{
continue;
}
entries.Add(teamInfo);
}
}
else
{
foreach (var player in section.Players.Values)
{
if (player.Score > 0)
{
entries.Add(player);
}
}
}
entries.Sort((a, b) => b.Score - a.Score);
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 < entries.Count; 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, "CTF Scoreboard".Center(), 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] as CTFTeamInfo;
if (teamInfo == null)
{
continue;
}
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 borderColor = teamInfo.Color == 0x455 ? LabelColor32 : BlackColor32;
var nameColor = teamInfo.Color switch
{
0x47E => 0xFFFFFF,
0x4F2 => 0x3399FF,
0x4F7 => 0x33FF33,
0x4FC => 0xFF00FF,
0x021 => 0xFF3333,
0x01A => 0xFF66FF,
0x455 => 0x333333,
_ => LabelColor32
};
AddBorderedText(
60,
65 + i * 75,
250,
20,
$"{LadderGump.Rank(1 + i)}: {teamInfo.Name} Team",
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, "Captures:", 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);
}
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)
{
AddHtml(x, y, width, height, color == 0 ? text : text.Color(color));
}
}
[SerializationGenerator(0, false)]
public sealed partial class CTFFlag : Item
{
public Mobile m_Fragger;
public DateTime m_FragTime;
private int m_ReturnCount;
public Mobile m_Returner;
public DateTime m_ReturnTime;
private TimerExecutionToken _returnTimerToken;
public CTFTeamInfo m_TeamInfo;
[Constructible]
public CTFFlag() : base(5643) => Movable = false;
public override string DefaultName => "old people cookies";
public override void OnDoubleClick(Mobile from)
{
if (m_TeamInfo?.Game != null)
{
var ourTeam = m_TeamInfo;
var useTeam = m_TeamInfo.Game.GetTeamInfo(from);
if (ourTeam == null || useTeam == null)
{
return;
}
if (IsChildOf(from.Backpack))
{
from.BeginTarget(1, false, TargetFlags.None, Flag_OnTarget);
}
else if (!from.InRange(this, 1) || !from.InLOS(this))
{
from.LocalOverheadMessage(MessageType.Regular, 0x26, 1019045); // I can't reach that
}
else if (ourTeam == useTeam)
{
if (Location == m_TeamInfo.Origin && Map == m_TeamInfo.Game.Facet)
{
from.NetState.SendMessage(
Serial,
ItemID,
MessageType.Regular,
0x3B2,
3,
false,
"ENU",
Name,
"Touch me not for I am chaste."
);
}
else
{
var playerInfo = useTeam[from];
if (playerInfo != null)
{
playerInfo.Score += 4; // return
}
m_Returner = from;
m_ReturnTime = Core.Now;
SendHome();
from.LocalOverheadMessage(MessageType.Regular, 0x59, false, "You returned the cookies!");
m_TeamInfo.Game.Alert($"The {from.Name} cookies have been returned by {ourTeam.Name}.");
}
}
else if (!from.PlaceInBackpack(this))
{
from.LocalOverheadMessage(MessageType.Regular, 0x26, false, "I can't hold that.");
}
else
{
from.RevealingAction();
from.LocalOverheadMessage(MessageType.Regular, 0x59, false, "You stole the cookies!");
m_TeamInfo.Game.Alert($"The {ourTeam.Name} cookies have been stolen by {from.Name} ({useTeam.Name}).");
BeginCountdown(120);
}
}
}
public override void Delete()
{
if (Parent != null)
{
SendHome();
return;
}
base.Delete();
}
public void DropTo(Mobile mob, Mobile killer)
{
m_Fragger = killer;
m_FragTime = Core.Now;
if (mob != null)
{
MoveToWorld(new Point3D(mob.X, mob.Y, mob.Z + 2), mob.Map);
m_ReturnCount = Math.Min(m_ReturnCount, 10);
}
else
{
SendHome();
}
}
private void BeginCountdown(int returnCount)
{
_returnTimerToken.Cancel();
Timer.StartTimer(TimeSpan.FromSeconds(1.0), TimeSpan.FromSeconds(1.0), Countdown_OnTick, out _returnTimerToken);
m_ReturnCount = returnCount;
}
private void Countdown_OnTick()
{
var owner = RootParent as Mobile;
switch (m_ReturnCount)
{
case 60:
case 30:
case 15:
case 10:
case 5:
case 4:
case 3:
case 2:
case 1:
{
if (m_ReturnCount == 1)
{
owner?.SendMessage(0x26, $"You have {m_ReturnCount} second to capture the cookies!");
}
else
{
owner?.SendMessage(0x26, $"You have {m_ReturnCount} seconds to capture the cookies!");
}
break;
}
case 0:
{
if (owner != null)
{
owner.SendMessage(0x26, "You have taken too long to capture the cookies!");
owner.Kill();
}
SendHome();
m_TeamInfo?.Game?.Alert($"The {m_TeamInfo.Name} cookies have been returned.");
return;
}
}
--m_ReturnCount;
}
private void Flag_OnTarget(Mobile from, object obj)
{
if (m_TeamInfo == null)
{
return;
}
if (!IsChildOf(from.Backpack))
{
return;
}
var ourTeam = m_TeamInfo;
var useTeam = m_TeamInfo.Game.GetTeamInfo(from);
if (obj is CTFFlag)
{
if (obj == useTeam.Flag)
{
from.LocalOverheadMessage(MessageType.Regular, 0x59, false, "You captured the cookies!");
m_TeamInfo.Game.Alert($"{from.Name} captured the {ourTeam.Name} cookies!");
SendHome();
var playerInfo = useTeam[from];
if (playerInfo != null)
{
playerInfo.Captures += 1;
playerInfo.Score += 50; // capture
var teamFlag = useTeam.Flag;
if (teamFlag.m_Fragger != null &&
Core.Now < teamFlag.m_FragTime + TimeSpan.FromSeconds(5.0) &&
m_TeamInfo.Game.GetTeamInfo(teamFlag.m_Fragger) == useTeam)
{
var assistInfo = useTeam[teamFlag.m_Fragger];
if (assistInfo != null)
{
assistInfo.Score += 6; // frag assist
}
}
if (teamFlag.m_Returner != null &&
Core.Now < teamFlag.m_ReturnTime + TimeSpan.FromSeconds(5.0))
{
var assistInfo = useTeam[teamFlag.m_Returner];
if (assistInfo != null)
{
assistInfo.Score += 4; // return assist
}
}
}
}
else
{
from.LocalOverheadMessage(MessageType.Regular, 0x26, false, "Those are not my cookies.");
}
}
else if (obj is Mobile passTo)
{
var passTeam = m_TeamInfo.Game.GetTeamInfo(passTo);
if (passTo == from)
{
from.LocalOverheadMessage(MessageType.Regular, 0x26, false, "I can't pass to them.");
}
else if (passTeam == useTeam && passTo.PlaceInBackpack(this))
{
passTo.LocalOverheadMessage(
MessageType.Regular,
0x59,
false,
$"{from.Name} has passed you the cookies!"
);
}
else
{
from.LocalOverheadMessage(MessageType.Regular, 0x26, false, "I can't pass to them.");
}
}
}
public void SendHome()
{
_returnTimerToken.Cancel();
if (m_TeamInfo == null)
{
return;
}
MoveToWorld(m_TeamInfo.Origin, m_TeamInfo.Game.Facet);
}
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 OnAdded(IEntity parent)
{
base.OnAdded(parent);
var mob = FindOwner(parent);
if (mob != null)
{
mob.SolidHueOverride = 0x4001;
}
}
public override void OnRemoved(IEntity parent)
{
base.OnRemoved(parent);
var mob = FindOwner(parent);
if (mob != null)
{
mob.SolidHueOverride = m_TeamInfo?.Game.GetColor(mob) ?? -1;
}
}
}
public interface IRankedCTF
{
int Kills { get; }
int Captures { get; }
int Score { get; }
string Name { get; }
}
public sealed class CTFPlayerInfo : IRankedCTF
{
private readonly CTFTeamInfo m_TeamInfo;
private int m_Captures;
private int m_Kills;
private int m_Score;
public CTFPlayerInfo(CTFTeamInfo teamInfo, Mobile player)
{
m_TeamInfo = teamInfo;
Player = player;
}
public Mobile Player { get; }
string IRankedCTF.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 CTFTeamInfo : IRankedCTF
{
public CTFTeamInfo(int teamID)
{
TeamID = teamID;
Players = new Dictionary<Mobile, CTFPlayerInfo>();
}
public CTFTeamInfo(int teamID, IGenericReader ip)
{
TeamID = teamID;
Players = new Dictionary<Mobile, CTFPlayerInfo>();
var version = ip.ReadEncodedInt();
switch (version)
{
case 2:
{
Board = ip.ReadEntity<CTFBoard>();
goto case 1;
}
case 1:
{
Name = ip.ReadString();
goto case 0;
}
case 0:
{
Color = ip.ReadEncodedInt();
Flag = ip.ReadEntity<CTFFlag>();
Origin = ip.ReadPoint3D();
break;
}
}
}
public CTFGame Game { get; set; }
public int TeamID { get; }
public CTFPlayerInfo Leader { get; set; }
[CommandProperty(AccessLevel.GameMaster)]
public CTFBoard Board { get; set; }
public Dictionary<Mobile, CTFPlayerInfo> Players { get; }
public CTFPlayerInfo this[Mobile mob]
{
get
{
if (mob == null)
{
return null;
}
if (!Players.TryGetValue(mob, out var val))
{
Players[mob] = val = new CTFPlayerInfo(this, mob);
}
return val;
}
}
[CommandProperty(AccessLevel.GameMaster)]
public int Color { get; set; }
[CommandProperty(AccessLevel.GameMaster)]
public string Name { get; set; }
[CommandProperty(AccessLevel.GameMaster)]
public CTFFlag Flag { get; set; }
[CommandProperty(AccessLevel.GameMaster)]
public Point3D Origin { get; set; }
string IRankedCTF.Name => $"{Name} 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 (Flag != null)
{
Flag.m_TeamInfo = this;
Flag.Hue = Color;
Flag.SendHome();
}
if (Board != null)
{
Board.m_TeamInfo = this;
}
}
public void Serialize(IGenericWriter op)
{
op.WriteEncodedInt(2); // version
op.Write(Board);
op.Write(Name);
op.WriteEncodedInt(Color);
op.Write(Flag);
op.Write(Origin);
}
public override string ToString() => "...";
}
public sealed class CTFController : EventController
{
[Constructible]
public CTFController()
{
Visible = false;
Movable = false;
Duration = TimeSpan.FromMinutes(30.0);
TeamInfo = new CTFTeamInfo[8];
for (var i = 0; i < TeamInfo.Length; ++i)
{
TeamInfo[i] = new CTFTeamInfo(i);
}
}
public CTFController(Serial serial)
: base(serial)
{
}
public CTFTeamInfo[] TeamInfo { get; private set; }
[CommandProperty(AccessLevel.GameMaster)]
public CTFTeamInfo Team1 => TeamInfo[0];
[CommandProperty(AccessLevel.GameMaster)]
public CTFTeamInfo Team2 => TeamInfo[1];
[CommandProperty(AccessLevel.GameMaster)]
public CTFTeamInfo Team3 => TeamInfo[2];
[CommandProperty(AccessLevel.GameMaster)]
public CTFTeamInfo Team4 => TeamInfo[3];
[CommandProperty(AccessLevel.GameMaster)]
public CTFTeamInfo Team5 => TeamInfo[4];
[CommandProperty(AccessLevel.GameMaster)]
public CTFTeamInfo Team6 => TeamInfo[5];
[CommandProperty(AccessLevel.GameMaster)]
public CTFTeamInfo Team7 => TeamInfo[6];
[CommandProperty(AccessLevel.GameMaster)]
public CTFTeamInfo Team8 => TeamInfo[7];
[CommandProperty(AccessLevel.GameMaster)]
public TimeSpan Duration { get; set; }
public override string Title => "CTF";
public override string GetTeamName(int teamID) => TeamInfo[teamID % TeamInfo.Length].Name;
public override EventGame Construct(DuelContext context) => new CTFGame(this, context);
public override void Serialize(IGenericWriter writer)
{
base.Serialize(writer);
writer.Write(2);
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 2:
{
Duration = reader.ReadTimeSpan();
goto case 1;
}
case 1:
{
TeamInfo = new CTFTeamInfo[reader.ReadEncodedInt()];
for (var i = 0; i < TeamInfo.Length; ++i)
{
TeamInfo[i] = new CTFTeamInfo(i, reader);
}
break;
}
case 0:
{
TeamInfo = new CTFTeamInfo[8];
for (var i = 0; i < TeamInfo.Length; ++i)
{
TeamInfo[i] = new CTFTeamInfo(i);
}
break;
}
}
if (version < 2)
{
Duration = TimeSpan.FromMinutes(30.0);
}
}
}
public sealed class CTFGame : EventGame
{
private TimerExecutionToken _finishTimerToken;
public CTFGame(CTFController controller, DuelContext context) : base(context) => Controller = controller;
public CTFController Controller { get; }
public Map Facet
{
get
{
if (m_Context.Arena != null)
{
return m_Context.Arena.Facet;
}
return Controller.Map;
}
}
public static void Initialize()
{
for (var i = 0x7C9; i <= 0x7D0; ++i)
{
TileData.ItemTable[i].Flags |= TileFlag.NoShoot;
}
}
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 CTFTeamInfo 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 not PlayerMobile pm)
{
return -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)
{
var teamInfo = GetTeamInfo(mob);
if (teamInfo != null)
{
return teamInfo.Color;
}
return -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 hadFlag = false;
using (var queue = corpse.EnumerateItemsByType<CTFFlag>(false))
{
foreach (var flag in queue)
{
hadFlag = true;
flag.DropTo(mob, killer);
}
}
if (mob.Backpack != null)
{
using var queue = mob.Backpack.EnumerateItemsByType<CTFFlag>(false);
foreach (var flag in queue)
{
hadFlag = true;
flag.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 (hadFlag)
{
playerInfo.Score += 4; // fragged flag carrier
}
if (mob.InRange(teamInfo.Origin, 24) && mob.Map == Facet)
{
playerInfo.Score += 1; // fragged in base -- guarding
}
for (var i = 0; i < Controller.TeamInfo.Length; ++i)
{
if (Controller.TeamInfo[i] == teamInfo)
{
continue;
}
Mobile ourFlagCarrier = null;
if (Controller.TeamInfo[i].Flag != null)
{
ourFlagCarrier = Controller.TeamInfo[i].Flag.RootParent as Mobile;
}
if (ourFlagCarrier != null && GetTeamInfo(ourFlagCarrier) == teamInfo)
{
foreach (var aggr in ourFlagCarrier.Aggressors)
{
if (aggr.Defender == ourFlagCarrier && aggr.Attacker == mob)
{
playerInfo.Score += 2; // helped defend guy capturing enemy flag
break;
}
}
if (mob.Map == ourFlagCarrier.Map && ourFlagCarrier.InRange(mob, 12))
{
playerInfo.Score += 1; // helped defend guy capturing enemy flag
}
}
}
}
}
}
mob.SendGump(new CTFBoardGump(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 % 8].Color);
}
_finishTimerToken.Cancel();
Timer.StartTimer(Controller.Duration, Finish_Callback, out _finishTimerToken);
}
private void Finish_Callback()
{
var teams = new List<CTFTeamInfo>();
for (var i = 0; i < m_Context.Participants.Count; ++i)
{
var teamInfo = Controller.TeamInfo[i % 8];
if (teamInfo?.Flag == null)
{
continue;
}
teams.Add(teamInfo);
}
teams.Sort((a, b) => b.Score - a.Score);
var tourney = m_Context.m_Tournament;
using var sb = ValueStringBuilder.Create(256);
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(' ');
sb.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 = TrophyRank.Bronze;
if (i == 0)
{
rank = TrophyRank.Gold;
}
else if (i == 1)
{
rank = TrophyRank.Silver;
}
var leader = teams[i].Leader;
foreach (var pl in teams[i].Players.Values)
{
var mob = pl.Player;
if (mob == null)
{
continue;
}
// "Red v Blue CTF Champion"
sb.Reset();
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");
if (pl.Kills > 0)
{
sb.Append(", ");
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 ? " capture" : " captures");
}
}
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 {rank.ToString().ToLower()} trophy and {cash:N0}gp for your participation in this tournament."
);
}
else
{
mob.SendMessage(
$"You have been awarded a {rank.ToString().ToLower()} trophy for your participation in this tournament."
);
}
}
}
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)
{
var dp = p.Players[j];
if (dp?.Mobile != null)
{
dp.Mobile.SendGump(new CTFBoardGump(dp.Mobile, this));
}
}
if (i == winner?.TeamID)
{
continue;
}
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.Flag != null)
{
teamInfo.Flag.SendHome();
teamInfo.Flag.m_TeamInfo = null;
}
if (teamInfo.Board != null)
{
teamInfo.Board.m_TeamInfo = null;
}
teamInfo.Game = null;
}
for (var i = 0; i < m_Context.Participants.Count; ++i)
{
ApplyHues(m_Context.Participants[i], -1);
}
_finishTimerToken.Cancel();
}
}