## 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
991 lines
34 KiB
C#
991 lines
34 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using Server.Factions;
|
|
using Server.Items;
|
|
using Server.Regions;
|
|
using Server.Text;
|
|
|
|
namespace Server.Engines.ConPVP
|
|
{
|
|
public enum TournamentStage
|
|
{
|
|
Inactive,
|
|
Signup,
|
|
Fighting
|
|
}
|
|
|
|
public enum GroupingType
|
|
{
|
|
HighVsLow,
|
|
Nearest,
|
|
Random
|
|
}
|
|
|
|
public enum TieType
|
|
{
|
|
Random,
|
|
Highest,
|
|
Lowest,
|
|
FullElimination,
|
|
FullAdvancement
|
|
}
|
|
|
|
public enum TourneyType
|
|
{
|
|
Standard,
|
|
FreeForAll,
|
|
RandomTeam,
|
|
RedVsBlue,
|
|
Faction
|
|
}
|
|
|
|
[PropertyObject]
|
|
public class Tournament
|
|
{
|
|
private static readonly TimeSpan SliceInterval = TimeSpan.FromSeconds(12.0);
|
|
private int m_ParticipantsPerMatch;
|
|
private int m_PlayersPerParticipant;
|
|
|
|
public Tournament(IGenericReader reader)
|
|
{
|
|
var version = reader.ReadEncodedInt();
|
|
|
|
switch (version)
|
|
{
|
|
case 5:
|
|
{
|
|
FactionRestricted = reader.ReadBool();
|
|
|
|
goto case 4;
|
|
}
|
|
case 4:
|
|
{
|
|
EventController = reader.ReadEntity<EventController>();
|
|
|
|
goto case 3;
|
|
}
|
|
case 3:
|
|
{
|
|
SuddenDeathRounds = reader.ReadEncodedInt();
|
|
|
|
goto case 2;
|
|
}
|
|
case 2:
|
|
{
|
|
TourneyType = (TourneyType)reader.ReadEncodedInt();
|
|
|
|
goto case 1;
|
|
}
|
|
case 1:
|
|
{
|
|
GroupType = (GroupingType)reader.ReadEncodedInt();
|
|
TieType = (TieType)reader.ReadEncodedInt();
|
|
SignupPeriod = reader.ReadTimeSpan();
|
|
|
|
goto case 0;
|
|
}
|
|
case 0:
|
|
{
|
|
if (version < 3)
|
|
{
|
|
SuddenDeathRounds = 3;
|
|
}
|
|
|
|
m_ParticipantsPerMatch = reader.ReadEncodedInt();
|
|
m_PlayersPerParticipant = reader.ReadEncodedInt();
|
|
SignupPeriod = reader.ReadTimeSpan();
|
|
CurrentStage = TournamentStage.Inactive;
|
|
Pyramid = new TourneyPyramid();
|
|
Ruleset = new Ruleset(RulesetLayout.Root);
|
|
Ruleset.ApplyDefault(Ruleset.Layout.Defaults[0]);
|
|
Participants = new List<TourneyParticipant>();
|
|
Undefeated = new List<TourneyParticipant>();
|
|
Arenas = new List<Arena>();
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
Timer.StartTimer(SliceInterval, SliceInterval, Slice);
|
|
}
|
|
|
|
public Tournament()
|
|
{
|
|
m_ParticipantsPerMatch = 2;
|
|
m_PlayersPerParticipant = 1;
|
|
Pyramid = new TourneyPyramid();
|
|
Ruleset = new Ruleset(RulesetLayout.Root);
|
|
Ruleset.ApplyDefault(Ruleset.Layout.Defaults[0]);
|
|
Participants = new List<TourneyParticipant>();
|
|
Undefeated = new List<TourneyParticipant>();
|
|
Arenas = new List<Arena>();
|
|
SignupPeriod = TimeSpan.FromMinutes(10.0);
|
|
|
|
Timer.StartTimer(SliceInterval, SliceInterval, Slice);
|
|
}
|
|
|
|
public bool IsNotoRestricted => TourneyType != TourneyType.Standard;
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public EventController EventController { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int SuddenDeathRounds { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public TourneyType TourneyType { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public GroupingType GroupType { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public TieType TieType { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public TimeSpan SuddenDeath { get; set; }
|
|
|
|
public Ruleset Ruleset { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int ParticipantsPerMatch
|
|
{
|
|
get => m_ParticipantsPerMatch;
|
|
set => m_ParticipantsPerMatch = Math.Clamp(value, 2, 10);
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int PlayersPerParticipant
|
|
{
|
|
get => m_PlayersPerParticipant;
|
|
set => m_PlayersPerParticipant = Math.Clamp(value, 1, 10);
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int LevelRequirement { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool FactionRestricted { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public TimeSpan SignupPeriod { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public DateTime SignupStart { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public TournamentStage CurrentStage { get; private set; }
|
|
|
|
public TournamentStage Stage
|
|
{
|
|
get => CurrentStage;
|
|
set => CurrentStage = value;
|
|
}
|
|
|
|
public TourneyPyramid Pyramid { get; set; }
|
|
|
|
public List<Arena> Arenas { get; set; }
|
|
|
|
public List<TourneyParticipant> Participants { get; set; }
|
|
|
|
public List<TourneyParticipant> Undefeated { get; set; }
|
|
|
|
public bool IsFactionRestricted => FactionRestricted || TourneyType == TourneyType.Faction;
|
|
|
|
public bool HasParticipant(Mobile mob)
|
|
{
|
|
for (var i = 0; i < Participants.Count; ++i)
|
|
{
|
|
if (Participants[i].Players.Contains(mob))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public void Serialize(IGenericWriter writer)
|
|
{
|
|
writer.WriteEncodedInt(5); // version
|
|
|
|
writer.Write(FactionRestricted);
|
|
|
|
writer.Write(EventController);
|
|
|
|
writer.WriteEncodedInt(SuddenDeathRounds);
|
|
|
|
writer.WriteEncodedInt((int)TourneyType);
|
|
|
|
writer.WriteEncodedInt((int)GroupType);
|
|
writer.WriteEncodedInt((int)TieType);
|
|
writer.Write(SuddenDeath);
|
|
|
|
writer.WriteEncodedInt(m_ParticipantsPerMatch);
|
|
writer.WriteEncodedInt(m_PlayersPerParticipant);
|
|
writer.Write(SignupPeriod);
|
|
}
|
|
|
|
public void HandleTie(Arena arena, TourneyMatch match, List<TourneyParticipant> remaining)
|
|
{
|
|
if (remaining.Count == 1)
|
|
{
|
|
HandleWon(arena, match, remaining[0]);
|
|
}
|
|
|
|
if (remaining.Count < 2)
|
|
{
|
|
return;
|
|
}
|
|
|
|
using var sb = ValueStringBuilder.Create(512);
|
|
|
|
sb.Append("The match has ended in a tie ");
|
|
|
|
sb.Append(remaining.Count == 2 ? "between " : "among ");
|
|
|
|
sb.Append(remaining.Count);
|
|
|
|
sb.Append(remaining[0].Players.Count == 1 ? " players: " : " teams: ");
|
|
|
|
var hasAppended = false;
|
|
|
|
for (var j = 0; j < match.Participants.Count; ++j)
|
|
{
|
|
var part = match.Participants[j];
|
|
|
|
if (remaining.Contains(part))
|
|
{
|
|
if (hasAppended)
|
|
{
|
|
sb.Append(", ");
|
|
}
|
|
|
|
sb.Append(part.NameList);
|
|
hasAppended = true;
|
|
}
|
|
else
|
|
{
|
|
Undefeated.Remove(part);
|
|
}
|
|
}
|
|
|
|
sb.Append(". ");
|
|
|
|
var whole = remaining.Count == 2 ? "both" : "all";
|
|
|
|
var tieType = TieType;
|
|
|
|
if (tieType == TieType.FullElimination && remaining.Count >= Undefeated.Count)
|
|
{
|
|
tieType = TieType.FullAdvancement;
|
|
}
|
|
|
|
switch (tieType)
|
|
{
|
|
case TieType.FullAdvancement:
|
|
{
|
|
sb.Append($"In accordance with the rules, {whole} parties are advanced.");
|
|
break;
|
|
}
|
|
case TieType.FullElimination:
|
|
{
|
|
for (var j = 0; j < remaining.Count; ++j)
|
|
{
|
|
Undefeated.Remove(remaining[j]);
|
|
}
|
|
|
|
sb.Append($"In accordance with the rules, {whole} parties are eliminated.");
|
|
break;
|
|
}
|
|
case TieType.Random:
|
|
{
|
|
var advanced = remaining.RandomElement();
|
|
|
|
for (var i = 0; i < remaining.Count; ++i)
|
|
{
|
|
if (remaining[i] != advanced)
|
|
{
|
|
Undefeated.Remove(remaining[i]);
|
|
}
|
|
}
|
|
|
|
if (advanced != null)
|
|
{
|
|
sb.Append($"In accordance with the rules, {advanced.NameList} {(advanced.Players.Count == 1 ? "is" : "are")} advanced.");
|
|
}
|
|
|
|
break;
|
|
}
|
|
case TieType.Highest:
|
|
{
|
|
TourneyParticipant advanced = null;
|
|
|
|
for (var i = 0; i < remaining.Count; ++i)
|
|
{
|
|
var part = remaining[i];
|
|
|
|
if (advanced == null || part.TotalLadderXP > advanced.TotalLadderXP)
|
|
{
|
|
advanced = part;
|
|
}
|
|
}
|
|
|
|
for (var i = 0; i < remaining.Count; ++i)
|
|
{
|
|
if (remaining[i] != advanced)
|
|
{
|
|
Undefeated.Remove(remaining[i]);
|
|
}
|
|
}
|
|
|
|
if (advanced != null)
|
|
{
|
|
sb.Append($"In accordance with the rules, {advanced.NameList} {(advanced.Players.Count == 1 ? "is" : "are")} advanced.");
|
|
}
|
|
|
|
break;
|
|
}
|
|
case TieType.Lowest:
|
|
{
|
|
TourneyParticipant advanced = null;
|
|
|
|
for (var i = 0; i < remaining.Count; ++i)
|
|
{
|
|
var part = remaining[i];
|
|
|
|
if (advanced == null || part.TotalLadderXP < advanced.TotalLadderXP)
|
|
{
|
|
advanced = part;
|
|
}
|
|
}
|
|
|
|
for (var i = 0; i < remaining.Count; ++i)
|
|
{
|
|
if (remaining[i] != advanced)
|
|
{
|
|
Undefeated.Remove(remaining[i]);
|
|
}
|
|
}
|
|
|
|
if (advanced != null)
|
|
{
|
|
sb.Append($"In accordance with the rules, {advanced.NameList} {(advanced.Players.Count == 1 ? "is" : "are")} advanced.");
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
Alert(arena, sb.ToString());
|
|
}
|
|
|
|
public void OnEliminated(DuelPlayer player)
|
|
{
|
|
var part = player.Participant;
|
|
|
|
if (!part.Eliminated)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (TourneyType == TourneyType.FreeForAll)
|
|
{
|
|
var rem = 0;
|
|
|
|
for (var i = 0; i < part.Context.Participants.Count; ++i)
|
|
{
|
|
if (part.Context.Participants[i]?.Eliminated == false)
|
|
{
|
|
++rem;
|
|
}
|
|
}
|
|
|
|
var tp = part.TourneyPart;
|
|
|
|
if (tp == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (rem == 1)
|
|
{
|
|
GiveAwards(tp.Players, TrophyRank.Silver, ComputeCashAward() / 2);
|
|
}
|
|
else if (rem == 2)
|
|
{
|
|
GiveAwards(tp.Players, TrophyRank.Bronze, ComputeCashAward() / 4);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void HandleWon(Arena arena, TourneyMatch match, TourneyParticipant winner)
|
|
{
|
|
using var sb = ValueStringBuilder.Create(512);
|
|
|
|
sb.Append("The match is complete. ");
|
|
sb.Append(winner.NameList);
|
|
|
|
if (winner.Players.Count > 1)
|
|
{
|
|
sb.Append(" have bested ");
|
|
}
|
|
else
|
|
{
|
|
sb.Append(" has bested ");
|
|
}
|
|
|
|
if (match.Participants.Count > 2)
|
|
{
|
|
sb.Append($"{match.Participants.Count - 1} other {(winner.Players.Count == 1 ? "players" : "teams")}: ");
|
|
}
|
|
|
|
var hasAppended = false;
|
|
|
|
for (var j = 0; j < match.Participants.Count; ++j)
|
|
{
|
|
var part = match.Participants[j];
|
|
|
|
if (part == winner)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Undefeated.Remove(part);
|
|
|
|
if (hasAppended)
|
|
{
|
|
sb.Append(", ");
|
|
}
|
|
|
|
sb.Append(part.NameList);
|
|
hasAppended = true;
|
|
}
|
|
|
|
sb.Append('.');
|
|
|
|
if (TourneyType == TourneyType.Standard)
|
|
{
|
|
Alert(arena, sb.ToString());
|
|
}
|
|
}
|
|
|
|
private int ComputeCashAward() => Participants.Count * m_PlayersPerParticipant * 2500;
|
|
|
|
private void GiveAwards()
|
|
{
|
|
switch (TourneyType)
|
|
{
|
|
case TourneyType.FreeForAll:
|
|
{
|
|
if (Pyramid.Levels.Count < 1)
|
|
{
|
|
break;
|
|
}
|
|
|
|
var top = Pyramid.Levels[^1];
|
|
|
|
if (top.FreeAdvance != null || top.Matches.Count != 1)
|
|
{
|
|
break;
|
|
}
|
|
|
|
var match = top.Matches[0];
|
|
var winner = match.Winner;
|
|
|
|
if (winner != null)
|
|
{
|
|
GiveAwards(winner.Players, TrophyRank.Gold, ComputeCashAward());
|
|
}
|
|
|
|
break;
|
|
}
|
|
case TourneyType.Standard:
|
|
{
|
|
if (Pyramid.Levels.Count < 2)
|
|
{
|
|
break;
|
|
}
|
|
|
|
var top = Pyramid.Levels[^1];
|
|
|
|
if (top.FreeAdvance != null || top.Matches.Count != 1)
|
|
{
|
|
break;
|
|
}
|
|
|
|
var cash = ComputeCashAward();
|
|
|
|
var match = top.Matches[0];
|
|
var winner = match.Winner;
|
|
|
|
for (var i = 0; i < match.Participants.Count; ++i)
|
|
{
|
|
var part = match.Participants[i];
|
|
|
|
if (part == winner)
|
|
{
|
|
GiveAwards(part.Players, TrophyRank.Gold, cash);
|
|
}
|
|
else
|
|
{
|
|
GiveAwards(part.Players, TrophyRank.Silver, cash / 2);
|
|
}
|
|
}
|
|
|
|
var next = Pyramid.Levels[^2];
|
|
|
|
if (next.Matches.Count > 2)
|
|
{
|
|
break;
|
|
}
|
|
|
|
for (var i = 0; i < next.Matches.Count; ++i)
|
|
{
|
|
match = next.Matches[i];
|
|
winner = match.Winner;
|
|
|
|
for (var j = 0; j < match.Participants.Count; ++j)
|
|
{
|
|
var part = match.Participants[j];
|
|
|
|
if (part != winner)
|
|
{
|
|
GiveAwards(part.Players, TrophyRank.Bronze, cash / 4);
|
|
}
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void GiveAwards(List<Mobile> players, TrophyRank rank, int cash)
|
|
{
|
|
if (players.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (players.Count > 1)
|
|
{
|
|
cash /= players.Count - 1;
|
|
}
|
|
|
|
cash += 500;
|
|
cash /= 1000;
|
|
cash *= 1000;
|
|
|
|
using var sb = ValueStringBuilder.Create(256);
|
|
|
|
if (TourneyType == TourneyType.FreeForAll)
|
|
{
|
|
sb.Append(Participants.Count * m_PlayersPerParticipant);
|
|
sb.Append("-man FFA");
|
|
}
|
|
else if (TourneyType == TourneyType.RandomTeam)
|
|
{
|
|
sb.Append(m_ParticipantsPerMatch);
|
|
sb.Append("-Team");
|
|
}
|
|
else if (TourneyType == TourneyType.Faction)
|
|
{
|
|
sb.Append(m_ParticipantsPerMatch);
|
|
sb.Append("-Team Faction");
|
|
}
|
|
else if (TourneyType == TourneyType.RedVsBlue)
|
|
{
|
|
sb.Append("Red v Blue");
|
|
}
|
|
else
|
|
{
|
|
for (var i = 0; i < m_ParticipantsPerMatch; ++i)
|
|
{
|
|
if (sb.Length > 0)
|
|
{
|
|
sb.Append('v');
|
|
}
|
|
|
|
sb.Append(m_PlayersPerParticipant);
|
|
}
|
|
}
|
|
|
|
if (EventController != null)
|
|
{
|
|
sb.Append(' ');
|
|
sb.Append(EventController.Title);
|
|
}
|
|
|
|
sb.Append(" Champion");
|
|
|
|
var title = sb.ToString();
|
|
|
|
for (var i = 0; i < players.Count; ++i)
|
|
{
|
|
var mob = players[i];
|
|
|
|
if (mob?.Deleted != false)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Item item = new Trophy(title, rank);
|
|
|
|
if (!mob.PlaceInBackpack(item))
|
|
{
|
|
mob.BankBox.DropItem(item);
|
|
}
|
|
|
|
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."
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Slice()
|
|
{
|
|
if (CurrentStage == TournamentStage.Signup)
|
|
{
|
|
var until = SignupStart + SignupPeriod - Core.Now;
|
|
|
|
if (until <= TimeSpan.Zero)
|
|
{
|
|
for (var i = Participants.Count - 1; i >= 0; --i)
|
|
{
|
|
var part = Participants[i];
|
|
var bad = false;
|
|
|
|
for (var j = 0; j < part.Players.Count; ++j)
|
|
{
|
|
var check = part.Players[j];
|
|
|
|
if (check.Deleted || check.Map == null || check.Map == Map.Internal || !check.Alive ||
|
|
Sigil.ExistsOn(check) || check.Region.IsPartOf<JailRegion>())
|
|
{
|
|
bad = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (bad)
|
|
{
|
|
for (var j = 0; j < part.Players.Count; ++j)
|
|
{
|
|
part.Players[j].SendMessage("You have been disqualified from the tournament.");
|
|
}
|
|
|
|
Participants.RemoveAt(i);
|
|
}
|
|
}
|
|
|
|
if (Participants.Count >= 2)
|
|
{
|
|
CurrentStage = TournamentStage.Fighting;
|
|
|
|
Undefeated.Clear();
|
|
|
|
Pyramid.Levels.Clear();
|
|
Pyramid.AddLevel(m_ParticipantsPerMatch, Participants, GroupType, TourneyType);
|
|
|
|
var level = Pyramid.Levels[0];
|
|
|
|
if (level.FreeAdvance != null)
|
|
{
|
|
Undefeated.Add(level.FreeAdvance);
|
|
}
|
|
|
|
for (var i = 0; i < level.Matches.Count; ++i)
|
|
{
|
|
var match = level.Matches[i];
|
|
|
|
Undefeated.AddRange(match.Participants);
|
|
}
|
|
|
|
Alert("Hear ye! Hear ye!", "The tournament will begin shortly.");
|
|
}
|
|
else
|
|
{
|
|
/*Alert( "Is this all?", "Pitiful. Signup extended." );
|
|
m_SignupStart = Core.Now;*/
|
|
|
|
Alert("Is this all?", "Pitiful. Tournament cancelled.");
|
|
CurrentStage = TournamentStage.Inactive;
|
|
}
|
|
}
|
|
else if (Math.Abs(until.TotalSeconds - TimeSpan.FromMinutes(1.0).TotalSeconds) <
|
|
SliceInterval.TotalSeconds / 2)
|
|
{
|
|
Alert("Last call!", "If you wish to enter the tournament, sign up with the registrar now.");
|
|
}
|
|
else if (Math.Abs(until.TotalSeconds - TimeSpan.FromMinutes(5.0).TotalSeconds) <
|
|
SliceInterval.TotalSeconds / 2)
|
|
{
|
|
Alert("The tournament will begin in 5 minutes.", "Sign up now before it's too late.");
|
|
}
|
|
}
|
|
else if (CurrentStage == TournamentStage.Fighting)
|
|
{
|
|
if (Undefeated.Count == 1)
|
|
{
|
|
var winner = Undefeated[0];
|
|
|
|
try
|
|
{
|
|
if (EventController != null)
|
|
{
|
|
Alert(
|
|
"The tournament has completed!",
|
|
$"Team {EventController.GetTeamName(Pyramid.Levels[0].Matches[0].Participants.IndexOf(winner))} has won!"
|
|
);
|
|
}
|
|
else if (TourneyType == TourneyType.RandomTeam)
|
|
{
|
|
Alert(
|
|
"The tournament has completed!",
|
|
$"Team {Pyramid.Levels[0].Matches[0].Participants.IndexOf(winner) + 1} has won!"
|
|
);
|
|
}
|
|
else if (TourneyType == TourneyType.Faction)
|
|
{
|
|
if (m_ParticipantsPerMatch == 4)
|
|
{
|
|
var name = Pyramid.Levels[0].Matches[0].Participants.IndexOf(winner) switch
|
|
{
|
|
0 => "Minax",
|
|
1 => "Council of Mages",
|
|
2 => "True Britannians",
|
|
3 => "Shadowlords",
|
|
_ => "(null)"
|
|
};
|
|
|
|
Alert("The tournament has completed!", $"The {name} team has won!");
|
|
}
|
|
else if (m_ParticipantsPerMatch == 2)
|
|
{
|
|
Alert(
|
|
"The tournament has completed!",
|
|
$"The {(Pyramid.Levels[0].Matches[0].Participants.IndexOf(winner) == 0 ? "Evil" : "Hero")} team has won!"
|
|
);
|
|
}
|
|
else
|
|
{
|
|
Alert(
|
|
"The tournament has completed!",
|
|
$"Team {Pyramid.Levels[0].Matches[0].Participants.IndexOf(winner) + 1} has won!"
|
|
);
|
|
}
|
|
}
|
|
else if (TourneyType == TourneyType.RedVsBlue)
|
|
{
|
|
Alert(
|
|
"The tournament has completed!",
|
|
$"Team {(Pyramid.Levels[0].Matches[0].Participants.IndexOf(winner) == 0 ? "Red" : "Blue")} has won!"
|
|
);
|
|
}
|
|
else
|
|
{
|
|
Alert(
|
|
"The tournament has completed!",
|
|
$"{winner.NameList} {(winner.Players.Count > 1 ? "are" : "is")} the champion{(winner.Players.Count == 1 ? "" : "s")}."
|
|
);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignored
|
|
}
|
|
|
|
GiveAwards();
|
|
|
|
CurrentStage = TournamentStage.Inactive;
|
|
Undefeated.Clear();
|
|
}
|
|
else if (Pyramid.Levels.Count > 0)
|
|
{
|
|
var activeLevel = Pyramid.Levels[^1];
|
|
var stillGoing = false;
|
|
|
|
for (var i = 0; i < activeLevel.Matches.Count; ++i)
|
|
{
|
|
var match = activeLevel.Matches[i];
|
|
|
|
if (match.Winner == null)
|
|
{
|
|
stillGoing = true;
|
|
|
|
if (!match.InProgress)
|
|
{
|
|
for (var j = 0; j < Arenas.Count; ++j)
|
|
{
|
|
var arena = Arenas[j];
|
|
|
|
if (!arena.IsOccupied)
|
|
{
|
|
match.Start(arena, this);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!stillGoing)
|
|
{
|
|
for (var i = Undefeated.Count - 1; i >= 0; --i)
|
|
{
|
|
var part = Undefeated[i];
|
|
var bad = false;
|
|
|
|
for (var j = 0; j < part.Players.Count; ++j)
|
|
{
|
|
var check = part.Players[j];
|
|
|
|
if (check.Deleted || check.Map == null || check.Map == Map.Internal || !check.Alive ||
|
|
Sigil.ExistsOn(check) || check.Region.IsPartOf<JailRegion>())
|
|
{
|
|
bad = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!bad)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
for (var j = 0; j < part.Players.Count; ++j)
|
|
{
|
|
part.Players[j].SendMessage("You have been disqualified from the tournament.");
|
|
}
|
|
|
|
Undefeated.RemoveAt(i);
|
|
|
|
if (Undefeated.Count == 1)
|
|
{
|
|
var winner = Undefeated[0];
|
|
|
|
try
|
|
{
|
|
if (EventController != null)
|
|
{
|
|
Alert(
|
|
"The tournament has completed!",
|
|
$"Team {EventController.GetTeamName(Pyramid.Levels[0].Matches[0].Participants.IndexOf(winner))} has won"
|
|
);
|
|
}
|
|
else if (TourneyType == TourneyType.RandomTeam)
|
|
{
|
|
Alert(
|
|
"The tournament has completed!",
|
|
$"Team {Pyramid.Levels[0].Matches[0].Participants.IndexOf(winner) + 1} has won!"
|
|
);
|
|
}
|
|
else if (TourneyType == TourneyType.Faction)
|
|
{
|
|
if (m_ParticipantsPerMatch == 4)
|
|
{
|
|
var name = Pyramid.Levels[0].Matches[0].Participants.IndexOf(winner) switch
|
|
{
|
|
0 => "Minax",
|
|
1 => "Council of Mages",
|
|
2 => "True Britannians",
|
|
3 => "Shadowlords",
|
|
_ => "(null)"
|
|
};
|
|
|
|
Alert("The tournament has completed!", $"The {name} team has won!");
|
|
}
|
|
else if (m_ParticipantsPerMatch == 2)
|
|
{
|
|
Alert(
|
|
"The tournament has completed!",
|
|
$"The {(Pyramid.Levels[0].Matches[0].Participants.IndexOf(winner) == 0 ? "Evil" : "Hero")} team has won!"
|
|
);
|
|
}
|
|
else
|
|
{
|
|
Alert(
|
|
"The tournament has completed!",
|
|
$"Team {Pyramid.Levels[0].Matches[0].Participants.IndexOf(winner) + 1} has won!"
|
|
);
|
|
}
|
|
}
|
|
else if (TourneyType == TourneyType.RedVsBlue)
|
|
{
|
|
Alert(
|
|
"The tournament has completed!",
|
|
$"Team {(Pyramid.Levels[0].Matches[0].Participants.IndexOf(winner) == 0 ? "Red" : "Blue")} has won!"
|
|
);
|
|
}
|
|
else
|
|
{
|
|
Alert(
|
|
"The tournament has completed!",
|
|
$"{winner.NameList} {(winner.Players.Count > 1 ? "are" : "is")} the champion{(winner.Players.Count == 1 ? "" : "s")}."
|
|
);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignored
|
|
}
|
|
|
|
GiveAwards();
|
|
|
|
CurrentStage = TournamentStage.Inactive;
|
|
Undefeated.Clear();
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (Undefeated.Count > 1)
|
|
{
|
|
Pyramid.AddLevel(m_ParticipantsPerMatch, Undefeated, GroupType, TourneyType);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Alert(params string[] alerts)
|
|
{
|
|
for (var i = 0; i < Arenas.Count; ++i)
|
|
{
|
|
Alert(Arenas[i], alerts);
|
|
}
|
|
}
|
|
|
|
public void Alert(Arena arena, params string[] alerts)
|
|
{
|
|
if (arena?.Announcer == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var count = 0;
|
|
|
|
Timer.StartTimer(TimeSpan.FromSeconds(0.5), alerts.Length,
|
|
() =>
|
|
{
|
|
arena.Announcer.PublicOverheadMessage(MessageType.Regular, 0x35, false, alerts[count++]);
|
|
}
|
|
);
|
|
}
|
|
}
|
|
}
|