ModernUO/Projects/UOContent/Engines/ConPVP/DuelContext.cs
Kamron Batman f4a87a8629
fix: Fixes adding items with ambiguous type lookup (#2307)
Fix [add command failing with ambiguous type names + refactor for performance

### Problem

[add blight would fail with "No type with that name was found" because multiple types contain "blight" (e.g., Server.Items.Blight, Server.Ethics.Evil.Blight, Server.Items.BlightGrippedLongbow, Server.Items.QuiverOfBlight). The old code only succeeded when exactly one type matched the search regardless of constructability and inheriting Mobi les/Items.

### Solution

Exact match takes priority: If a type's name exactly equals the search string (case-insensitive), use it directly. Otherwise, show the AddGump with all partial matches.

- [add blight → Creates Blight (exact name match)
- [add bligh → Shows gump with Blight, BlightGrippedLongbow, etc.

### Refactoring

- CommandEventArgs context: Added GetContext<T>/SetContext<T> to pass resolved type through the command chain without method signature changes
- Removed TrySetupTarget duplication: Validation now happens only in ValidateArgs, eliminating redundant code paths
- Split type matching:
  - ExactMatch(string) → Returns Type for exact name match (used by [add)
  - MatchEmptyCtor(string) → Returns ConstructorInfo[] for gump display (empty-callable constructors only)

### Memory & Performance Improvements

| Optimization                  | Benefit                                                                                      |
|-------------------------------|----------------------------------------------------------------------------------------------|
| _mobileItemTypes cache        | Filters Mobile/Item types once per assembly, reused on all subsequent searches               |
| ReadOnlySpan<string> for args | Avoids string[] heap allocations when slicing arguments                                      |
| ValueStringBuilder            | Stack-allocated string building, avoids StringBuilder heap allocation                        |
| Single type resolution        | Type resolved once in ValidateArgs, passed via context to Execute (was resolved 2-3x before) |
2026-01-06 21:21:29 -08:00

2828 lines
83 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using ModernUO.CodeGeneratedEvents;
using Server.Engines.PartySystem;
using Server.Factions;
using Server.Gumps;
using Server.Items;
using Server.Mobiles;
using Server.Regions;
using Server.Spells;
using Server.Spells.Bushido;
using Server.Spells.Chivalry;
using Server.Spells.Fourth;
using Server.Spells.Necromancy;
using Server.Spells.Ninjitsu;
using Server.Spells.Second;
using Server.Spells.Seventh;
using Server.Spells.Spellweaving;
using Server.Targeting;
namespace Server.Engines.ConPVP
{
public delegate void CountdownCallback(int count);
public class DuelContext
{
private static readonly TimeSpan CombatDelay = TimeSpan.FromSeconds(30.0);
private static readonly TimeSpan AutoTieDelay = TimeSpan.FromMinutes(15.0);
private readonly List<Item> m_Walls = new();
private TimerExecutionToken _autoTieTimerToken;
private TimerExecutionToken _countdownTimerToken;
private TimerExecutionToken _SdWarnTimerToken;
private TimerExecutionToken _SdActivateTimerToken;
public EventGame m_EventGame;
private Map m_GateFacet;
private Point3D m_GatePoint;
public TourneyMatch m_Match;
public Arena m_OverrideArena;
public Tournament m_Tournament;
private bool m_Yielding;
public DuelContext(Mobile initiator, RulesetLayout layout, bool addNew = true)
{
Initiator = initiator;
Participants = new List<Participant>();
Ruleset = new Ruleset(layout);
Ruleset.ApplyDefault(layout.Defaults[0]);
if (addNew)
{
Participants.Add(new Participant(this, 1));
Participants.Add(new Participant(this, 1));
Participants[0].Add(initiator);
}
}
public bool Rematch { get; private set; }
public bool ReadyWait { get; private set; }
public int ReadyCount { get; private set; }
public bool Registered { get; private set; } = true;
public bool Finished { get; private set; }
public bool Started { get; private set; }
public Mobile Initiator { get; }
public List<Participant> Participants { get; }
public Ruleset Ruleset { get; private set; }
public Arena Arena { get; private set; }
public bool Tied { get; private set; }
public bool IsSuddenDeath { get; set; }
public bool IsOneVsOne => Participants.Count == 2 && Participants[0].Players.Length == 1 &&
Participants[1].Players.Length == 1;
public bool StartedBeginCountdown { get; private set; }
public bool StartedReadyCountdown { get; private set; }
public Tournament Tournament => m_Tournament;
private bool CantDoAnything(Mobile mob) => m_EventGame?.CantDoAnything(mob) == true;
public static bool IsFreeConsume(Mobile mob)
{
if (mob is not PlayerMobile pm || pm.DuelContext?.m_EventGame == null)
{
return false;
}
return pm.DuelContext.m_EventGame.FreeConsume;
}
public void DelayBounce(TimeSpan ts, Mobile mob, Container corpse)
{
Timer.StartTimer(ts, () => DelayBounce_Callback(mob, corpse));
}
public static bool AllowSpecialMove(Mobile from, string name, SpecialMove move) =>
(from as PlayerMobile)?.DuelContext?.InstAllowSpecialMove(from, name, move) != false;
public bool InstAllowSpecialMove(Mobile from, string name, SpecialMove move)
{
if (!StartedBeginCountdown)
{
return true;
}
var pl = Find(from);
if (pl?.Eliminated != false)
{
return true;
}
if (CantDoAnything(from))
{
return false;
}
var title = move switch
{
NinjaMove => "Bushido",
SamuraiMove => "Ninjitsu",
_ => null
};
if (title == null || name == null || Ruleset.GetOption(title, name))
{
return true;
}
from.SendMessage("The dueling ruleset prevents you from using this move.");
return false;
}
public bool AllowSpellCast(Mobile from, Spell spell)
{
if (!StartedBeginCountdown)
{
return true;
}
if (Find(from)?.Eliminated != false)
{
return true;
}
if (CantDoAnything(from))
{
return false;
}
if (spell is RecallSpell)
{
from.SendMessage("You may not cast this spell.");
}
string title;
string option;
switch (spell)
{
case ArcanistSpell _:
title = "Spellweaving";
option = spell.Name;
break;
case PaladinSpell _:
title = "Chivalry";
option = spell.Name;
break;
case NecromancerSpell _:
title = "Necromancy";
option = spell.Name;
break;
case NinjaSpell _:
title = "Ninjitsu";
option = spell.Name;
break;
case SamuraiSpell _:
title = "Bushido";
option = spell.Name;
break;
case MagerySpell magerySpell:
title = magerySpell.Circle switch
{
SpellCircle.First => "1st Circle",
SpellCircle.Second => "2nd Circle",
SpellCircle.Third => "3rd Circle",
SpellCircle.Fourth => "4th Circle",
SpellCircle.Fifth => "5th Circle",
SpellCircle.Sixth => "6th Circle",
SpellCircle.Seventh => "7th Circle",
SpellCircle.Eighth => "8th Circle",
_ => null
};
option = magerySpell.Name;
break;
default:
title = "Other Spell";
option = spell.Name;
break;
}
if (title == null || option == null || Ruleset.GetOption(title, option))
{
return true;
}
from.SendMessage("The dueling ruleset prevents you from casting this spell.");
return false;
}
public bool AllowItemEquip(Mobile from, Item item)
{
if (!StartedBeginCountdown)
{
return true;
}
var pl = Find(from);
if (pl?.Eliminated != false)
{
return true;
}
if (item is Dagger || CheckItemEquip(from, item))
{
return true;
}
from.SendMessage("The dueling ruleset prevents you from equipping this item.");
return false;
}
public static bool AllowSpecialAbility(Mobile from, string name, bool message)
{
if (from is not PlayerMobile pm)
{
return true;
}
var dc = pm.DuelContext;
// No DuelContext or InstAllowSpecialAbility
return dc?.InstAllowSpecialAbility(from, name, message) != false;
}
public bool InstAllowSpecialAbility(Mobile from, string name, bool message)
{
if (!StartedBeginCountdown)
{
return true;
}
var pl = Find(from);
if (pl?.Eliminated != false)
{
return true;
}
if (CantDoAnything(from))
{
return false;
}
if (Ruleset.GetOption("Combat Abilities", name))
{
return true;
}
if (message)
{
from.SendMessage("The dueling ruleset prevents you from using this combat ability.");
}
return false;
}
public bool CheckItemEquip(Mobile from, Item item)
{
if (item is Fists)
{
if (!Ruleset.GetOption("Weapons", "Wrestling"))
{
return false;
}
}
else if (item is BaseArmor armor)
{
if (armor.ProtectionLevel > ArmorProtectionLevel.Regular && !Ruleset.GetOption("Armor", "Magical"))
{
return false;
}
if (!Core.AOS && armor.Resource != armor.DefaultResource && !Ruleset.GetOption("Armor", "Colored"))
{
return false;
}
if (armor is BaseShield && !Ruleset.GetOption("Armor", "Shields"))
{
return false;
}
}
else if (item is BaseWeapon weapon)
{
if ((weapon.DamageLevel > WeaponDamageLevel.Regular || weapon.AccuracyLevel > WeaponAccuracyLevel.Regular) &&
!Ruleset.GetOption("Weapons", "Magical"))
{
return false;
}
if (!Core.AOS && weapon.Resource != CraftResource.Iron && weapon.Resource != CraftResource.None &&
!Ruleset.GetOption("Weapons", "Runics"))
{
return false;
}
if (weapon is BaseRanged && !Ruleset.GetOption("Weapons", "Ranged"))
{
return false;
}
if (weapon is not BaseRanged && !Ruleset.GetOption("Weapons", "Melee"))
{
return false;
}
if (weapon.PoisonCharges > 0 && weapon.Poison != null && !Ruleset.GetOption("Weapons", "Poisoned"))
{
return false;
}
if (weapon is BaseWand && !Ruleset.GetOption("Items", "Wands"))
{
return false;
}
}
return true;
}
public bool AllowSkillUse(Mobile from, SkillName skill)
{
if (!StartedBeginCountdown)
{
return true;
}
var pl = Find(from);
if (pl?.Eliminated != false)
{
return true;
}
if (CantDoAnything(from))
{
return false;
}
var id = (int)skill;
if (id >= 0 && id < SkillInfo.Table.Length)
{
if (Ruleset.GetOption("Skills", SkillInfo.Table[id].Name))
{
return true;
}
}
from.SendMessage("The dueling ruleset prevents you from using this skill.");
return false;
}
public bool AllowItemUse(Mobile from, Item item)
{
if (!StartedBeginCountdown)
{
return true;
}
var pl = Find(from);
if (pl?.Eliminated != false)
{
return true;
}
if (item is not BaseRefreshPotion)
{
if (CantDoAnything(from))
{
return false;
}
}
string title = null, option = null;
if (item is BasePotion)
{
title = "Potions";
if (item is BaseAgilityPotion)
{
option = "Agility";
}
else if (item is BaseCurePotion)
{
option = "Cure";
}
else if (item is BaseHealPotion)
{
option = "Heal";
}
else if (item is NightSightPotion)
{
option = "Nightsight";
}
else if (item is BasePoisonPotion)
{
option = "Poison";
}
else if (item is BaseStrengthPotion)
{
option = "Strength";
}
else if (item is BaseExplosionPotion)
{
option = "Explosion";
}
else if (item is BaseRefreshPotion)
{
option = "Refresh";
}
}
else if (item is Bandage)
{
title = "Items";
option = "Bandages";
}
else if (item is TrappableContainer container)
{
if (container.TrapType != TrapType.None)
{
title = "Items";
option = "Trapped Containers";
}
}
else if (item is Bola)
{
title = "Items";
option = "Bolas";
}
else if (item is OrangePetals)
{
title = "Items";
option = "Orange Petals";
}
else if (item is EtherealMount || item.Layer == Layer.Mount)
{
title = "Items";
option = "Mounts";
}
else if (item is LeatherNinjaBelt)
{
title = "Items";
option = "Shurikens";
}
else if (item is Fukiya)
{
title = "Items";
option = "Fukiya Darts";
}
else if (item is FireHorn)
{
title = "Items";
option = "Fire Horns";
}
else if (item is BaseWand)
{
title = "Items";
option = "Wands";
}
if (title != null && option != null && StartedBeginCountdown && !Started)
{
from.SendMessage("You may not use this item before the duel begins.");
return false;
}
if (item is BasePotion && item is not BaseExplosionPotion && item is not BaseRefreshPotion && IsSuddenDeath)
{
from.SendMessage(0x22, "You may not drink potions in sudden death.");
return false;
}
if (item is Bandage && IsSuddenDeath)
{
from.SendMessage(0x22, "You may not use bandages in sudden death.");
return false;
}
if (title == null || option == null || Ruleset.GetOption(title, option))
{
return true;
}
from.SendMessage("The dueling ruleset prevents you from using this item.");
return false;
}
private void DelayBounce_Callback(Mobile mob, Container corpse)
{
RemoveAggressions(mob);
SendOutside(mob);
Refresh(mob, corpse);
Debuff(mob);
CancelSpell(mob);
mob.Frozen = false;
}
public void OnMapChanged(Mobile mob)
{
OnLocationChanged(mob);
}
public void OnLocationChanged(Mobile mob)
{
if (!Registered || !StartedBeginCountdown || Finished)
{
return;
}
var arena = Arena;
if (arena == null)
{
return;
}
if (mob.Map == arena.Facet && arena.Bounds.Contains(mob.Location))
{
return;
}
var pl = Find(mob);
if (pl?.Eliminated != false)
{
return;
}
if (mob.Map == Map.Internal)
{
if (mob.LogoutMap == arena.Facet && arena.Bounds.Contains(mob.LogoutLocation))
{
mob.LogoutLocation = arena.Outside;
}
}
pl.Eliminated = true;
mob.LocalOverheadMessage(MessageType.Regular, 0x22, false, "You have forfeited your position in the duel.");
mob.NonlocalOverheadMessage(
MessageType.Regular,
0x22,
false,
$"{mob.Name} has forfeited by leaving the dueling arena."
);
var winner = CheckCompletion();
if (winner != null)
{
Finish(winner);
}
}
[OnEvent(nameof(PlayerMobile.PlayerDeathEvent))]
public static void OnPlayerDeath(PlayerMobile m) => m.DuelContext?.OnDeath(m, m.Corpse);
public void OnDeath(Mobile mob, Container corpse)
{
if (!Registered || !Started)
{
return;
}
var pl = Find(mob);
if (pl?.Eliminated != false || m_EventGame?.OnDeath(mob, corpse) == false)
{
return;
}
pl.Eliminated = true;
if (mob.Poison != null)
{
mob.Poison = null;
}
Requip(mob, corpse);
DelayBounce(TimeSpan.FromSeconds(4.0), mob, corpse);
var winner = CheckCompletion();
if (winner != null)
{
Finish(winner);
}
else if (!m_Yielding)
{
mob.LocalOverheadMessage(MessageType.Regular, 0x22, false, "You have been defeated.");
mob.NonlocalOverheadMessage(MessageType.Regular, 0x22, false, $"{mob.Name} has been defeated.");
}
}
public bool CheckFull()
{
for (var i = 0; i < Participants.Count; ++i)
{
var p = Participants[i];
if (p.HasOpenSlot)
{
return false;
}
}
return true;
}
public void Requip(Mobile from, Container cont)
{
if (cont is not Corpse corpse)
{
return;
}
var items = new List<Item>(corpse.Items);
var didntFit = false;
var pack = from.Backpack;
for (var i = 0; !didntFit && i < items.Count; ++i)
{
var item = items[i];
if (item.Layer is Layer.Hair or Layer.FacialHair || !item.Movable)
{
continue;
}
if (pack != null)
{
pack.DropItem(item);
}
else
{
didntFit = true;
}
}
corpse.Carved = true;
if (corpse.ItemID == 0x2006)
{
corpse.ProcessDelta();
corpse.SendRemovePacket();
corpse.ItemID = Utility.Random(0xECA, 9); // bone graphic
corpse.Hue = 0;
corpse.ProcessDelta();
var killer = from.FindMostRecentDamager(false);
if (killer?.Player == true)
{
killer.AddToBackpack(new Head(m_Tournament == null ? HeadType.Duel : HeadType.Tournament, from.Name));
}
}
from.PlaySound(0x3E3);
if (didntFit)
{
from.SendLocalizedMessage(1062472); // You gather some of your belongings. The rest remain on the corpse.
}
else
{
from.SendLocalizedMessage(1062471); // You quickly gather all of your belongings.
}
}
public void Refresh(Mobile mob, Container cont)
{
if (!mob.Alive)
{
mob.Resurrect();
mob.FindItemOnLayer<DeathRobe>(Layer.OuterTorso)?.Delete();
if (cont is Corpse corpse)
{
for (var i = 0; i < corpse.EquipItems.Count; ++i)
{
var item = corpse.EquipItems[i];
if (item.Movable && item.Layer != Layer.Hair && item.Layer != Layer.FacialHair &&
item.IsChildOf(mob.Backpack))
{
mob.EquipItem(item);
}
}
}
}
mob.Hits = mob.HitsMax;
mob.Stam = mob.StamMax;
mob.Mana = mob.ManaMax;
mob.Poison = null;
}
public void SendOutside(Mobile mob)
{
if (Arena == null)
{
return;
}
mob.Combatant = null;
mob.MoveToWorld(Arena.Outside, Arena.Facet);
}
public void Finish(Participant winner)
{
if (Finished)
{
return;
}
_autoTieTimerToken.Cancel();
StopSDTimers();
Finished = true;
for (var i = 0; i < winner.Players.Length; ++i)
{
var pl = winner.Players[i];
if (pl?.Eliminated == false)
{
DelayBounce(TimeSpan.FromSeconds(8.0), pl.Mobile, null);
}
}
winner.Broadcast(
0x59,
null,
winner.Players.Length == 1 ? "{0} has won the duel." : "{0} and {1} team have won the duel.",
winner.Players.Length == 1 ? "You have won the duel." : "Your team has won the duel."
);
if (m_Tournament != null && winner.TourneyPart != null)
{
m_Match.Winner = winner.TourneyPart;
winner.TourneyPart.WonMatch(m_Match);
m_Tournament.HandleWon(Arena, m_Match, winner.TourneyPart);
}
for (var i = 0; i < Participants.Count; ++i)
{
var loser = Participants[i];
if (loser != winner)
{
loser.Broadcast(
0x22,
null,
loser.Players.Length == 1 ? "{0} has lost the duel." : "{0} and {1} team have lost the duel.",
loser.Players.Length == 1 ? "You have lost the duel." : "Your team has lost the duel."
);
if (m_Tournament != null)
{
loser.TourneyPart?.LostMatch(m_Match);
}
}
for (var j = 0; j < loser.Players.Length; ++j)
{
if (loser.Players[j] != null)
{
RemoveAggressions(loser.Players[j].Mobile);
loser.Players[j].Mobile.Delta(MobileDelta.Noto);
loser.Players[j].Mobile.CloseGump<BeginGump>();
if (m_Tournament != null)
{
loser.Players[j].Mobile.SendEverything();
}
}
}
}
if (IsOneVsOne)
{
var dp1 = Participants[0].Players[0];
var dp2 = Participants[1].Players[0];
if (dp1 != null && dp2 != null)
{
Award(dp1.Mobile, dp2.Mobile, dp1.Participant == winner);
Award(dp2.Mobile, dp1.Mobile, dp2.Participant == winner);
}
}
m_EventGame?.OnStop();
Timer.StartTimer(TimeSpan.FromSeconds(9.0), UnregisterRematch);
}
public void Award(Mobile us, Mobile them, bool won)
{
var ladder = Arena == null ? Ladder.Instance : Arena.AcquireLadder();
if (ladder == null)
{
return;
}
var ourEntry = ladder.Find(us);
var theirEntry = ladder.Find(them);
if (ourEntry == null || theirEntry == null)
{
return;
}
var xpGain = Ladder.GetExperienceGain(ourEntry, theirEntry, won);
if (xpGain == 0)
{
return;
}
if (m_Tournament != null)
{
xpGain *= xpGain > 0 ? 5 : 2;
}
if (won)
{
++ourEntry.Wins;
}
else
{
++ourEntry.Losses;
}
var oldLevel = Ladder.GetLevel(ourEntry.Experience);
ourEntry.Experience += xpGain;
if (ourEntry.Experience < 0)
{
ourEntry.Experience = 0;
}
ladder.UpdateEntry(ourEntry);
var newLevel = Ladder.GetLevel(ourEntry.Experience);
if (newLevel > oldLevel)
{
us.SendMessage(0x59, $"You have achieved level {newLevel}!");
}
else if (newLevel < oldLevel)
{
us.SendMessage(0x22, $"You have lost a level. You are now at {newLevel}.");
}
}
public void UnregisterRematch()
{
Unregister(true);
}
public void Unregister()
{
Unregister(false);
}
public void Unregister(bool queryRematch)
{
DestroyWall();
if (!Registered)
{
return;
}
Registered = false;
Arena?.Evict();
StopSDTimers();
for (var i = 0; i < Participants.Count; ++i)
{
var p = Participants[i];
for (var j = 0; j < p.Players.Length; ++j)
{
var pl = p.Players[j];
if (pl == null)
{
continue;
}
if (pl.Mobile is PlayerMobile mobile)
{
mobile.DuelPlayer = null;
}
CloseAllGumps(pl);
}
}
if (queryRematch && m_Tournament == null)
{
QueryRematch();
}
}
public void QueryRematch()
{
var dc = new DuelContext(Initiator, Ruleset.Layout, false);
dc.Ruleset = Ruleset;
dc.Rematch = true;
dc.Participants.Clear();
for (var i = 0; i < Participants.Count; ++i)
{
var oldPart = Participants[i];
var newPart = new Participant(dc, oldPart.Players.Length);
for (var j = 0; j < oldPart.Players.Length; ++j)
{
var oldPlayer = oldPart.Players[j];
if (oldPlayer != null)
{
newPart.Players[j] = new DuelPlayer(oldPlayer.Mobile, newPart);
}
}
dc.Participants.Add(newPart);
}
dc.CloseAllGumps();
dc.SendReadyUpGump();
}
public DuelPlayer Find(Mobile mob)
{
if (mob is PlayerMobile pm)
{
if (pm.DuelContext == this)
{
return pm.DuelPlayer;
}
return null;
}
for (var i = 0; i < Participants.Count; ++i)
{
var p = Participants[i];
var pl = p.Find(mob);
if (pl != null)
{
return pl;
}
}
return null;
}
public bool IsAlly(Mobile m1, Mobile m2)
{
var pl1 = Find(m1);
var pl2 = Find(m2);
return pl1 != null && pl1.Participant == pl2?.Participant;
}
public Participant CheckCompletion()
{
Participant winner = null;
var hasWinner = false;
var eliminated = 0;
for (var i = 0; i < Participants.Count; ++i)
{
var p = Participants[i];
if (p.Eliminated)
{
++eliminated;
if (eliminated == Participants.Count - 1)
{
hasWinner = true;
}
}
else
{
winner = p;
}
}
return hasWinner ? winner ?? Participants[0] : null;
}
public void StartCountdown(int count, CountdownCallback cb)
{
Timer.StartTimer(
TimeSpan.FromSeconds(1.0),
count,
() => Countdown_Callback(cb),
out _countdownTimerToken
);
}
public void StopCountdown() => _countdownTimerToken.Cancel();
private void Countdown_Callback(CountdownCallback cb)
{
var count = _countdownTimerToken.RemainingCount;
if (count == 0)
{
StopCountdown();
}
cb(count);
}
public void StopSDTimers()
{
_SdWarnTimerToken.Cancel();
_SdActivateTimerToken.Cancel();
}
public void StartSuddenDeath(TimeSpan timeUntilActive)
{
_SdWarnTimerToken.Cancel();
Timer.StartTimer(TimeSpan.FromMinutes(timeUntilActive.TotalMinutes * 0.9), WarnSuddenDeath, out _SdWarnTimerToken);
_SdActivateTimerToken.Cancel();
Timer.StartTimer(timeUntilActive, ActivateSuddenDeath, out _SdActivateTimerToken);
}
public void WarnSuddenDeath()
{
for (var i = 0; i < Participants.Count; ++i)
{
var p = Participants[i];
for (var j = 0; j < p.Players.Length; ++j)
{
var pl = p.Players[j];
if (pl?.Eliminated != false)
{
continue;
}
pl.Mobile.SendSound(0x1E1);
pl.Mobile.SendMessage(0x22, "Warning! Warning! Warning!");
pl.Mobile.SendMessage(0x22, "Sudden death will be active soon!");
}
}
m_Tournament?.Alert(Arena, "Sudden death will be active soon!");
_SdWarnTimerToken.Cancel();
}
public static bool CheckSuddenDeath(Mobile mob) => mob is PlayerMobile pm && pm.DuelPlayer?.Eliminated == false &&
pm.DuelContext?.IsSuddenDeath == true;
public void ActivateSuddenDeath()
{
for (var i = 0; i < Participants.Count; ++i)
{
var p = Participants[i];
for (var j = 0; j < p.Players.Length; ++j)
{
var pl = p.Players[j];
if (pl?.Eliminated != false)
{
continue;
}
pl.Mobile.SendSound(0x1E1);
pl.Mobile.SendMessage(0x22, "Warning! Warning! Warning!");
pl.Mobile.SendMessage(
0x22,
"Sudden death has ACTIVATED. You are now unable to perform any beneficial actions."
);
}
}
m_Tournament?.Alert(Arena, "Sudden death has been activated!");
IsSuddenDeath = true;
_SdActivateTimerToken.Cancel();
}
public void BeginAutoTie()
{
var ts = m_Tournament == null || m_Tournament.TourneyType == TourneyType.Standard
? AutoTieDelay
: TimeSpan.FromMinutes(90.0);
_autoTieTimerToken.Cancel();
Timer.StartTimer(ts, InvokeAutoTie, out _autoTieTimerToken);
}
public void InvokeAutoTie()
{
_autoTieTimerToken.Cancel();
if (!Started || Finished)
{
return;
}
Tied = true;
Finished = true;
StopSDTimers();
var remaining = new List<TourneyParticipant>();
for (var i = 0; i < Participants.Count; ++i)
{
var p = Participants[i];
if (p.Eliminated)
{
p.Broadcast(
0x22,
null,
p.Players.Length == 1 ? "{0} has lost the duel." : "{0} and {1} team have lost the duel.",
p.Players.Length == 1 ? "You have lost the duel." : "Your team has lost the duel."
);
}
else
{
p.Broadcast(
0x59,
null,
p.Players.Length == 1
? "{0} has tied the duel due to time expiration."
: "{0} and {1} team have tied the duel due to time expiration.",
p.Players.Length == 1
? "You have tied the duel due to time expiration."
: "Your team has tied the duel due to time expiration."
);
for (var j = 0; j < p.Players.Length; ++j)
{
var pl = p.Players[j];
if (pl?.Eliminated == false)
{
DelayBounce(TimeSpan.FromSeconds(8.0), pl.Mobile, null);
}
}
if (p.TourneyPart != null)
{
remaining.Add(p.TourneyPart);
}
}
for (var j = 0; j < p.Players.Length; ++j)
{
var pl = p.Players[j];
if (pl != null)
{
pl.Mobile.Delta(MobileDelta.Noto);
pl.Mobile.SendEverything();
}
}
}
m_Tournament?.HandleTie(Arena, m_Match, remaining);
Timer.StartTimer(TimeSpan.FromSeconds(10.0), Unregister);
}
public static void Configure()
{
EventSink.Speech += EventSink_Speech;
CommandSystem.Register("vli", AccessLevel.GameMaster, vli_oc);
}
[Usage("vli")]
[Aliases("ViewLadderInfo")]
[Description("View ladder information.")]
private static void vli_oc(CommandEventArgs e)
{
e.Mobile.BeginTarget(-1, false, TargetFlags.None, vli_ot);
}
private static void vli_ot(Mobile from, object obj)
{
if (obj is PlayerMobile pm)
{
var ladder = Ladder.Instance;
if (ladder == null)
{
return;
}
var entry = ladder.Find(pm);
if (entry != null)
{
from.SendGump(new PropertiesGump(from, entry));
}
}
}
public static bool CheckCombat(Mobile m)
{
foreach (var info in m.Aggressed)
{
if (info.Defender.Player && Core.Now - info.LastCombatTime < CombatDelay)
{
return true;
}
}
foreach (var info in m.Aggressors)
{
if (info.Attacker.Player && Core.Now - info.LastCombatTime < CombatDelay)
{
return true;
}
}
return false;
}
[OnEvent(nameof(PlayerMobile.PlayerLoginEvent))]
public static void OnLogin(PlayerMobile pm)
{
var dc = pm.DuelContext;
if (dc == null)
{
return;
}
if (dc.ReadyWait && pm.DuelPlayer.Ready && !dc.Started && !dc.StartedBeginCountdown && !dc.Finished)
{
if (dc.m_Tournament == null)
{
pm.SendGump(new ReadyGump(pm, dc, dc.ReadyCount));
}
}
else if (dc.ReadyWait && !dc.StartedBeginCountdown && !dc.Started && !dc.Finished)
{
if (dc.m_Tournament == null)
{
pm.SendGump(new ReadyUpGump(pm, dc));
}
}
else if (dc.Initiator == pm && !dc.ReadyWait && !dc.StartedBeginCountdown && !dc.Started && !dc.Finished)
{
pm.SendGump(new DuelContextGump(pm, dc));
}
}
private static void ViewLadder_OnTarget(Mobile from, object obj, Ladder ladder)
{
if (obj is PlayerMobile pm)
{
var entry = ladder.Find(pm);
if (entry == null)
{
return; // sanity
}
var text =
$"{{0}} are ranked {LadderGump.Rank(entry.Index + 1)} at level {Ladder.GetLevel(entry.Experience)}.";
pm.PrivateOverheadMessage(
MessageType.Regular,
pm.SpeechHue,
true,
string.Format(text, from == pm ? "You" : "They"),
from.NetState
);
}
else if (obj is Mobile mob)
{
if (mob.Body.IsHuman)
{
mob.PrivateOverheadMessage(
MessageType.Regular,
mob.SpeechHue,
false,
"I'm not a duelist, and quite frankly, I resent the implication.",
from.NetState
);
}
else
{
mob.PrivateOverheadMessage(
MessageType.Regular,
0x3B2,
true,
"It's probably better than you.",
from.NetState
);
}
}
else
{
from.SendMessage("That's not a player.");
}
}
private static void EventSink_Speech(SpeechEventArgs e)
{
if (e.Handled)
{
return;
}
if (e.Mobile is not PlayerMobile pm)
{
return;
}
if (e.Speech.InsensitiveContains("i wish to duel"))
{
if (!pm.CheckAlive())
{
}
else if (pm.Region.IsPartOf<JailRegion>())
{
}
else if (CheckCombat(pm))
{
e.Mobile.SendMessage(
0x22,
"You have recently been in combat with another player and must wait before starting a duel."
);
}
else if (pm.DuelContext != null)
{
if (pm.DuelContext.Initiator == pm)
{
e.Mobile.SendMessage(0x22, "You have already started a duel.");
}
else
{
e.Mobile.SendMessage(0x22, "You have already been challenged in a duel.");
}
}
else if (TournamentController.IsActive)
{
e.Mobile.SendMessage(0x22, "You may not start a duel while a tournament is active.");
}
else
{
pm.SendGump(new DuelContextGump(pm, new DuelContext(pm, RulesetLayout.Root)));
e.Handled = true;
}
}
else if (e.Speech.InsensitiveEquals("change arena preferences"))
{
if (!pm.CheckAlive())
{
}
else
{
var prefs = Preferences.Instance;
if (prefs != null)
{
e.Mobile.SendGump(new PreferencesGump(e.Mobile, prefs));
}
}
}
else if (e.Speech.InsensitiveEquals("showladder"))
{
e.Blocked = true;
if (!pm.CheckAlive())
{
}
else
{
var instance = Ladder.Instance;
if (instance == null)
{
// pm.SendMessage( "Ladder not yet initialized." );
}
else
{
var entry = instance.Find(pm);
if (entry == null)
{
return; // sanity
}
var text =
$"{{0}} {{1}} ranked {LadderGump.Rank(entry.Index + 1)} at level {Ladder.GetLevel(entry.Experience)}.";
pm.LocalOverheadMessage(MessageType.Regular, pm.SpeechHue, true, string.Format(text, "You", "are"));
pm.NonlocalOverheadMessage(
MessageType.Regular,
pm.SpeechHue,
true,
string.Format(text, pm.Name, "is")
);
// pm.PublicOverheadMessage( MessageType.Regular, pm.SpeechHue, true, String.Format( "Level {0} with {1} win{2} and {3} loss{4}.", Ladder.GetLevel( entry.Experience ), entry.Wins, entry.Wins==1?"":"s", entry.Losses, entry.Losses==1?"":"es" ) );
// pm.PublicOverheadMessage( MessageType.Regular, pm.SpeechHue, true, String.Format( "Level {0} with {1} win{2} and {3} loss{4}.", Ladder.GetLevel( entry.Experience ), entry.Wins, entry.Wins==1?"":"s", entry.Losses, entry.Losses==1?"":"es" ) );
}
}
}
else if (e.Speech.InsensitiveEquals("viewladder"))
{
e.Blocked = true;
if (!pm.CheckAlive())
{
}
else
{
var instance = Ladder.Instance;
if (instance == null)
{
// pm.SendMessage( "Ladder not yet initialized." );
}
else
{
pm.SendMessage("Target a player to view their ranking and level.");
pm.BeginTarget(16, false, TargetFlags.None, ViewLadder_OnTarget, instance);
}
}
}
else if (e.Speech.InsensitiveContains("i yield"))
{
if (!pm.CheckAlive())
{
}
else if (pm.DuelContext == null)
{
}
else if (pm.DuelContext.Finished)
{
e.Mobile.SendMessage(0x22, "The duel is already finished.");
}
else if (!pm.DuelContext.Started)
{
var dc = pm.DuelContext;
var init = dc.Initiator;
if (pm.DuelContext.StartedBeginCountdown)
{
e.Mobile.SendMessage(0x22, "The duel has not yet started.");
}
else
{
var pl = pm.DuelContext.Find(pm);
if (pl == null)
{
return;
}
var p = pl.Participant;
if (!pm.DuelContext.ReadyWait) // still setting stuff up
{
p.Broadcast(0x22, null, "{0} has yielded.", "You have yielded.");
if (init == pm)
{
dc.Unregister();
}
else
{
p.Nullify(pl);
pm.DuelPlayer = null;
foreach (var g in init.GetGumps())
{
if (g is ParticipantGump pg && pg.Participant == p)
{
init.SendGump(new ParticipantGump(init, dc, p));
break;
}
if (g is DuelContextGump dcg && dcg.Context == dc)
{
init.SendGump(new DuelContextGump(init, dc));
break;
}
}
}
}
else if (!pm.DuelContext.StartedReadyCountdown) // at ready stage
{
p.Broadcast(0x22, null, "{0} has yielded.", "You have yielded.");
dc.m_Yielding = true;
dc.RejectReady(pm, null);
dc.m_Yielding = false;
if (init == pm)
{
dc.Unregister();
}
else if (dc.Registered)
{
p.Nullify(pl);
pm.DuelPlayer = null;
var send = true;
foreach (var g in init.GetGumps())
{
if (g is ParticipantGump pg && pg.Participant == p)
{
init.SendGump(new ParticipantGump(init, dc, p));
send = false;
break;
}
if (g is DuelContextGump dcg && dcg.Context == dc)
{
init.SendGump(new DuelContextGump(init, dc));
send = false;
break;
}
}
if (send)
{
init.SendGump(new DuelContextGump(init, dc));
}
}
}
else
{
pm.DuelContext.StopCountdown();
pm.DuelContext.StartedReadyCountdown = false;
p.Broadcast(0x22, null, "{0} has yielded.", "You have yielded.");
dc.m_Yielding = true;
dc.RejectReady(pm, null);
dc.m_Yielding = false;
if (init == pm)
{
dc.Unregister();
}
else if (dc.Registered)
{
p.Nullify(pl);
pm.DuelPlayer = null;
var send = true;
foreach (var g in init.GetGumps())
{
if (g is ParticipantGump pg && pg.Participant == p)
{
init.SendGump(new ParticipantGump(init, dc, p));
send = false;
break;
}
if (g is DuelContextGump dcg && dcg.Context == dc)
{
init.SendGump(new DuelContextGump(init, dc));
send = false;
break;
}
}
if (send)
{
init.SendGump(new DuelContextGump(init, dc));
}
}
}
}
}
else
{
var pl = pm.DuelContext.Find(pm);
if (pl != null)
{
if (pm.DuelContext.IsOneVsOne)
{
e.Mobile.SendMessage(0x22, "You may not yield a 1 on 1 match.");
}
else if (pl.Eliminated)
{
e.Mobile.SendMessage(0x22, "You have already been eliminated.");
}
else
{
pm.LocalOverheadMessage(MessageType.Regular, 0x22, false, "You have yielded.");
pm.NonlocalOverheadMessage(MessageType.Regular, 0x22, false, $"{pm.Name} has yielded.");
pm.DuelContext.m_Yielding = true;
pm.Kill();
pm.DuelContext.m_Yielding = false;
if (pm.Alive) // invul, ...
{
pl.Eliminated = true;
pm.DuelContext.RemoveAggressions(pm);
pm.DuelContext.SendOutside(pm);
pm.DuelContext.Refresh(pm, null);
Debuff(pm);
CancelSpell(pm);
pm.Frozen = false;
var winner = pm.DuelContext.CheckCompletion();
if (winner != null)
{
pm.DuelContext.Finish(winner);
}
}
}
}
else
{
e.Mobile.SendMessage(0x22, "BUG: Unable to find duel context.");
}
}
}
}
public void CloseAllGumps(DuelPlayer pl)
{
var gumps = pl.Mobile.GetGumps();
gumps.Close<BeginGump>();
gumps.Close<DuelContextGump>();
gumps.Close<ParticipantGump>();
gumps.Close<PickRulesetGump>();
gumps.Close<ReadyGump>();
gumps.Close<ReadyUpGump>();
gumps.Close<RulesetGump>();
}
public void CloseAllGumps()
{
for (var i = 0; i < Participants.Count; ++i)
{
var p = Participants[i];
for (var j = 0; j < p.Players.Length; ++j)
{
var pl = p.Players[j];
if (pl != null)
{
CloseAllGumps(pl);
}
}
}
}
public void RejectReady(Mobile rejector, string page)
{
if (StartedReadyCountdown)
{
return; // sanity
}
for (var i = 0; i < Participants.Count; ++i)
{
var p = Participants[i];
for (var j = 0; j < p.Players.Length; ++j)
{
var pl = p.Players[j];
if (pl == null)
{
continue;
}
pl.Ready = false;
var mob = pl.Mobile;
if (page == null) // yield
{
if (mob != rejector)
{
mob.SendMessage(0x22, $"{rejector.Name} has yielded.");
}
}
else
{
if (mob == rejector)
{
if (Rematch)
{
mob.SendMessage(0x22, "You have rejected the rematch.");
}
else
{
mob.SendMessage(0x22, $"You have rejected the {page}.");
}
}
else
{
if (Rematch)
{
mob.SendMessage(0x22, $"{rejector.Name} has rejected the rematch.");
}
else
{
mob.SendMessage(0x22, $"{rejector.Name} has rejected the {page}.");
}
}
}
// Close all of them?
var gumps = mob.GetGumps();
gumps.Close<DuelContextGump>();
gumps.Close<ReadyUpGump>();
gumps.Close<ReadyGump>();
}
}
if (Rematch)
{
Unregister();
}
else if (!m_Yielding)
{
Initiator.SendGump(new DuelContextGump(Initiator, this));
}
ReadyWait = false;
ReadyCount = 0;
}
public void SendReadyGump()
{
SendReadyGump(-1);
}
public static void Debuff(Mobile mob)
{
mob.RemoveStatMod("[Magic] Str Buff");
mob.RemoveStatMod("[Magic] Dex Buff");
mob.RemoveStatMod("[Magic] Int Buff");
mob.RemoveStatMod("[Magic] Str Curse");
mob.RemoveStatMod("[Magic] Dex Curse");
mob.RemoveStatMod("[Magic] Int Curse");
mob.RemoveStatMod("Concussion");
mob.RemoveStatMod("blood-rose");
mob.RemoveStatMod("clarity-potion");
mob.RemoveStatMod("RoseOfTrinsicPetal");
mob.RemoveStatMod("Holy Bless");
mob.RemoveStatMod("Holy Curse");
OrangePetals.RemoveEffect(mob);
mob.Paralyzed = false;
mob.Hidden = false;
if (!Core.AOS)
{
mob.MagicDamageAbsorb = 0;
mob.MeleeDamageAbsorb = 0;
ProtectionSpell.Registry.Remove(mob);
ArchProtectionSpell.RemoveEntry(mob);
mob.EndAction<DefensiveSpell>();
}
TransformationSpellHelper.RemoveContext(mob, true);
AnimalForm.RemoveContext(mob);
DisguisePersistence.StopTimer(mob);
if (!mob.CanBeginAction<PolymorphSpell>())
{
mob.BodyMod = 0;
mob.HueMod = -1;
mob.EndAction<PolymorphSpell>();
}
BaseArmor.ValidateMobile(mob);
BaseClothing.ValidateMobile(mob);
mob.Hits = mob.HitsMax;
mob.Stam = mob.StamMax;
mob.Mana = mob.ManaMax;
mob.Poison = null;
}
public static void CancelSpell(Mobile mob)
{
if (mob.Spell is Spell spell)
{
spell.Disturb(DisturbType.Kill);
}
Target.Cancel(mob);
}
public void DestroyWall()
{
for (var i = 0; i < m_Walls.Count; ++i)
{
m_Walls[i].Delete();
}
m_Walls.Clear();
}
public void CreateWall()
{
if (Arena == null)
{
return;
}
var start = Arena.Points.EdgeWest;
var wall = Arena.Wall;
var dx = start.X - wall.X;
var dy = start.Y - wall.Y;
var rx = dx - dy;
var ry = dx + dy;
var eastToWest = rx == 0 && ry >= 0 || rx >= 0 && ry == 0;
Effects.PlaySound(wall, Arena.Facet, 0x1F6);
for (var i = -1; i <= 1; ++i)
{
var loc = new Point3D(eastToWest ? wall.X + i : wall.X, eastToWest ? wall.Y : wall.Y + i, wall.Z);
var created = new InternalWall();
created.Appear(loc, Arena.Facet);
m_Walls.Add(created);
}
}
public void BuildParties()
{
for (var i = 0; i < Participants.Count; ++i)
{
var p = Participants[i];
if (p.Players.Length > 1)
{
var players = new List<Mobile>();
for (var j = 0; j < p.Players.Length; ++j)
{
var dp = p.Players[j];
if (dp == null)
{
continue;
}
players.Add(dp.Mobile);
}
if (players.Count > 1)
{
for (var leaderIndex = 0; leaderIndex + 1 < players.Count; leaderIndex += Party.Capacity)
{
var leader = players[leaderIndex];
var party = Party.Get(leader);
if (party == null)
{
leader.Party = party = new Party(leader);
}
else if (party.Leader != leader)
{
party.SendPublicMessage(leader, "I leave this party to fight in a duel.");
party.Remove(leader);
leader.Party = party = new Party(leader);
}
for (var j = leaderIndex + 1; j < players.Count && j < leaderIndex + Party.Capacity; ++j)
{
var player = players[j];
var existing = Party.Get(player);
if (existing == party)
{
continue;
}
if (party.Members.Count + party.Candidates.Count >= Party.Capacity)
{
player.SendMessage(
"You could not be added to the team party because it is at full capacity."
);
leader.SendMessage(
"{0} could not be added to the team party because it is at full capacity."
);
}
else
{
if (existing != null)
{
existing.SendPublicMessage(player, "I leave this party to fight in a duel.");
existing.Remove(player);
}
party.OnAccept(player, true);
}
}
}
}
}
}
}
public void ClearIllegalItems()
{
for (var i = 0; i < Participants.Count; ++i)
{
var p = Participants[i];
for (var j = 0; j < p.Players.Length; ++j)
{
var pl = p.Players[j];
if (pl == null)
{
continue;
}
ClearIllegalItems(pl.Mobile);
}
}
}
public void ClearIllegalItems(Mobile mob)
{
if (mob.StunReady && !AllowSpecialAbility(mob, "Stun", false))
{
mob.StunReady = false;
}
if (mob.DisarmReady && !AllowSpecialAbility(mob, "Disarm", false))
{
mob.DisarmReady = false;
}
var pack = mob.Backpack;
if (pack == null)
{
return;
}
for (var i = mob.Items.Count - 1; i >= 0; --i)
{
if (i >= mob.Items.Count)
{
continue; // sanity
}
var item = mob.Items[i];
if (!CheckItemEquip(mob, item))
{
pack.DropItem(item);
// TODO: Use Layer instead of Type
var number = item switch
{
BaseWeapon _ => 1062001, // You can no longer wield your ~1_WEAPON~
not BaseShield when item is BaseArmor or BaseClothing => 1062002, // You can no longer wear your ~1_ARMOR~
_ => 1062003 // You can no longer equip your ~1_SHIELD~
};
mob.SendLocalizedMessage(number, item.Name ?? $"#{item.LabelNumber}");
}
}
var inHand = mob.Holding;
if (inHand != null && !CheckItemEquip(mob, inHand))
{
mob.Holding = null;
var bi = inHand.GetBounce();
if (bi.Parent == mob)
{
pack.DropItem(inHand);
}
else
{
inHand.Bounce(mob);
}
inHand.ClearBounce();
}
}
private void MessageRuleset(Mobile mob)
{
if (Ruleset == null)
{
return;
}
var ruleset = Ruleset;
var basedef = ruleset.Base;
mob.SendMessage($"Ruleset: {basedef.Title}");
BitArray defs;
if (ruleset.Flavors.Count > 0)
{
defs = new BitArray(basedef.Options);
for (var i = 0; i < ruleset.Flavors.Count; ++i)
{
defs.Or(ruleset.Flavors[i].Options);
mob.SendMessage($" + {ruleset.Flavors[i].Title}");
}
}
else
{
defs = basedef.Options;
}
var changes = 0;
var opts = ruleset.Options;
for (var i = 0; i < opts.Length; ++i)
{
if (defs[i] != opts[i])
{
var name = ruleset.Layout.FindByIndex(i);
if (name != null) // sanity
{
++changes;
if (changes == 1)
{
mob.SendMessage("Modifications:");
}
if (opts[i])
{
mob.SendMessage($"{name}: enabled");
}
else
{
mob.SendMessage($"{name}: disabled");
}
}
}
}
}
public void SendBeginGump(int count)
{
if (!Registered || Finished)
{
return;
}
if (count == 10)
{
CreateWall();
BuildParties();
ClearIllegalItems();
}
else if (count == 0)
{
DestroyWall();
}
StartedBeginCountdown = true;
if (count == 0)
{
Started = true;
BeginAutoTie();
}
for (var i = 0; i < Participants.Count; ++i)
{
var p = Participants[i];
for (var j = 0; j < p.Players.Length; ++j)
{
var pl = p.Players[j];
if (pl == null)
{
continue;
}
var mob = pl.Mobile;
if (count > 0)
{
if (count == 10)
{
var gumps = mob.GetGumps();
gumps.Close<ReadyGump>();
gumps.Close<ReadyUpGump>();
gumps.Close<BeginGump>();
gumps.Send(new BeginGump(count));
}
mob.Frozen = true;
}
else
{
mob.CloseGump<BeginGump>();
mob.Frozen = false;
}
}
}
}
public void RemoveAggressions(Mobile mob)
{
for (var i = 0; i < Participants.Count; ++i)
{
var p = Participants[i];
for (var j = 0; j < p.Players.Length; ++j)
{
var dp = p.Players[j];
if (dp == null || dp.Mobile == mob)
{
continue;
}
mob.RemoveAggressed(dp.Mobile);
mob.RemoveAggressor(dp.Mobile);
dp.Mobile.RemoveAggressed(mob);
dp.Mobile.RemoveAggressor(mob);
}
}
}
public void SendReadyUpGump()
{
if (!Registered)
{
return;
}
ReadyWait = true;
ReadyCount = -1;
for (var i = 0; i < Participants.Count; ++i)
{
var p = Participants[i];
for (var j = 0; j < p.Players.Length; ++j)
{
var pl = p.Players[j];
var mob = pl?.Mobile;
if (mob != null && m_Tournament == null)
{
mob.SendGump(new ReadyUpGump(mob, this), true);
}
}
}
}
public string ValidateStart()
{
if (m_Tournament == null && TournamentController.IsActive)
{
return "a tournament is active";
}
for (var i = 0; i < Participants.Count; ++i)
{
var p = Participants[i];
for (var j = 0; j < p.Players.Length; ++j)
{
var dp = p.Players[j];
if (dp == null)
{
return "a slot is empty";
}
if (dp.Mobile.Region.IsPartOf<JailRegion>())
{
return $"{dp.Mobile.Name} is in jail";
}
if (Sigil.ExistsOn(dp.Mobile))
{
return $"{dp.Mobile.Name} is holding a sigil";
}
if (!dp.Mobile.Alive)
{
if (m_Tournament == null)
{
return $"{dp.Mobile.Name} is dead";
}
dp.Mobile.Resurrect();
}
if (m_Tournament == null && CheckCombat(dp.Mobile))
{
return $"{dp.Mobile.Name} is in combat";
}
if (dp.Mobile.Mounted)
{
var mount = dp.Mobile.Mount;
if (m_Tournament != null && mount != null)
{
mount.Rider = null;
}
else
{
return $"{dp.Mobile.Name} is mounted";
}
}
}
}
return null;
}
public void SendReadyGump(int count)
{
if (!Registered)
{
return;
}
if (count != -1)
{
StartedReadyCountdown = true;
}
ReadyCount = count;
if (count == 0)
{
var error = ValidateStart();
if (error != null)
{
for (var i = 0; i < Participants.Count; ++i)
{
var p = Participants[i];
for (var j = 0; j < p.Players.Length; ++j)
{
var dp = p.Players[j];
dp?.Mobile.SendMessage($"The duel could not be started because {error}.");
}
}
StartCountdown(10, SendReadyGump);
return;
}
ReadyWait = false;
var players = new List<Mobile>();
for (var i = 0; i < Participants.Count; ++i)
{
var p = Participants[i];
for (var j = 0; j < p.Players.Length; ++j)
{
var dp = p.Players[j];
if (dp != null)
{
players.Add(dp.Mobile);
}
}
}
var arena = m_OverrideArena ?? Arena.FindArena(players);
if (arena == null)
{
for (var i = 0; i < Participants.Count; ++i)
{
var p = Participants[i];
for (var j = 0; j < p.Players.Length; ++j)
{
var dp = p.Players[j];
dp?.Mobile.SendMessage(
"The duel could not be started because there are no arenas. If you want to stop waiting for a free arena, yield the duel."
);
}
}
StartCountdown(10, SendReadyGump);
return;
}
if (!arena.IsOccupied)
{
Arena = arena;
if (Initiator.Map == Map.Internal)
{
m_GatePoint = Initiator.LogoutLocation;
m_GateFacet = Initiator.LogoutMap;
}
else
{
m_GatePoint = Initiator.Location;
m_GateFacet = Initiator.Map;
}
if (arena.Teleporter is not ExitTeleporter tp)
{
arena.Teleporter = tp = new ExitTeleporter();
tp.MoveToWorld(arena.GateOut == Point3D.Zero ? arena.Outside : arena.GateOut, arena.Facet);
}
var mg = new ArenaMoongate(
arena.GateIn == Point3D.Zero ? arena.Outside : arena.GateIn,
arena.Facet,
tp
);
StartedBeginCountdown = true;
for (var i = 0; i < Participants.Count; ++i)
{
var p = Participants[i];
for (var j = 0; j < p.Players.Length; ++j)
{
var pl = p.Players[j];
if (pl == null)
{
continue;
}
tp.Register(pl.Mobile);
pl.Mobile.Frozen = false; // reset timer just in case
pl.Mobile.Frozen = true;
Debuff(pl.Mobile);
CancelSpell(pl.Mobile);
pl.Mobile.Delta(MobileDelta.Noto);
}
arena.MoveInside(p.Players, i);
}
m_EventGame?.OnStart();
StartCountdown(10, SendBeginGump);
mg.Appear(m_GatePoint, m_GateFacet);
}
else
{
for (var i = 0; i < Participants.Count; ++i)
{
var p = Participants[i];
for (var j = 0; j < p.Players.Length; ++j)
{
var dp = p.Players[j];
dp?.Mobile.SendMessage(
"The duel could not be started because all arenas are full. If you want to stop waiting for a free arena, yield the duel."
);
}
}
StartCountdown(10, SendReadyGump);
}
return;
}
ReadyWait = true;
var isAllReady = true;
for (var i = 0; i < Participants.Count; ++i)
{
var p = Participants[i];
for (var j = 0; j < p.Players.Length; ++j)
{
var pl = p.Players[j];
if (pl == null)
{
continue;
}
var mob = pl.Mobile;
if (pl.Ready)
{
if (m_Tournament == null)
{
mob.SendGump(new ReadyGump(mob, this, count), true);
}
}
else
{
isAllReady = false;
}
}
}
if (count == -1 && isAllReady)
{
StartCountdown(3, SendReadyGump);
}
}
private class InternalWall : Item
{
public InternalWall() : base(0x80) => Movable = false;
public InternalWall(Serial serial) : base(serial)
{
}
public void Appear(Point3D loc, Map map)
{
MoveToWorld(loc, map);
Effects.SendLocationParticles(this, 0x376A, 9, 10, 5025);
}
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();
Delete();
}
}
private class ReturnEntry
{
private DateTime m_Expire;
public ReturnEntry(Mobile mob)
{
Mobile = mob;
Update();
}
public ReturnEntry(Mobile mob, Point3D loc, Map facet)
{
Mobile = mob;
Location = loc;
Facet = facet;
m_Expire = Core.Now + TimeSpan.FromMinutes(30.0);
}
public Mobile Mobile { get; }
public Point3D Location { get; private set; }
public Map Facet { get; private set; }
public bool Expired => Core.Now >= m_Expire;
public void Return()
{
if (Facet == Map.Internal || Facet == null)
{
return;
}
if (Mobile.Map == Map.Internal)
{
Mobile.LogoutLocation = Location;
Mobile.LogoutMap = Facet;
}
else
{
Mobile.Location = Location;
Mobile.Map = Facet;
}
}
public void Update()
{
m_Expire = Core.Now + TimeSpan.FromMinutes(30.0);
if (Mobile.Map == Map.Internal)
{
Facet = Mobile.LogoutMap;
Location = Mobile.LogoutLocation;
}
else
{
Facet = Mobile.Map;
Location = Mobile.Location;
}
}
}
private class ExitTeleporter : Item
{
private List<ReturnEntry> m_Entries;
public ExitTeleporter() : base(0x1822)
{
m_Entries = new List<ReturnEntry>();
Hue = 0x482;
Movable = false;
}
public ExitTeleporter(Serial serial) : base(serial)
{
}
public override string DefaultName => "return teleporter";
public void Register(Mobile mob)
{
var entry = Find(mob);
if (entry != null)
{
entry.Update();
return;
}
m_Entries.Add(new ReturnEntry(mob));
}
private ReturnEntry Find(Mobile mob)
{
for (var i = 0; i < m_Entries.Count; ++i)
{
var entry = m_Entries[i];
if (entry.Mobile == mob)
{
return entry;
}
if (entry.Expired)
{
m_Entries.RemoveAt(i--);
}
}
return null;
}
public override bool OnMoveOver(Mobile m)
{
if (!base.OnMoveOver(m))
{
return false;
}
var entry = Find(m);
if (entry != null)
{
entry.Return();
Effects.PlaySound(GetWorldLocation(), Map, 0x1FE);
Effects.PlaySound(m.Location, m.Map, 0x1FE);
m_Entries.Remove(entry);
return false;
}
m.SendLocalizedMessage(1049383); // The teleporter doesn't seem to work for you.
return true;
}
public override void Serialize(IGenericWriter writer)
{
base.Serialize(writer);
writer.Write(0);
writer.WriteEncodedInt(m_Entries.Count);
for (var i = 0; i < m_Entries.Count; ++i)
{
var entry = m_Entries[i];
writer.Write(entry.Mobile);
writer.Write(entry.Location);
writer.Write(entry.Facet);
if (entry.Expired)
{
m_Entries.RemoveAt(i--);
}
}
}
public override void Deserialize(IGenericReader reader)
{
base.Deserialize(reader);
var version = reader.ReadInt();
switch (version)
{
case 0:
{
var count = reader.ReadEncodedInt();
m_Entries = new List<ReturnEntry>(count);
for (var i = 0; i < count; ++i)
{
var mob = reader.ReadEntity<Mobile>();
var loc = reader.ReadPoint3D();
var map = reader.ReadMap();
m_Entries.Add(new ReturnEntry(mob, loc, map));
}
break;
}
}
}
}
private class ArenaMoongate : ConfirmationMoongate
{
private readonly ExitTeleporter m_Teleporter;
public ArenaMoongate(Point3D target, Map map, ExitTeleporter tp) : base(target, map)
{
m_Teleporter = tp;
ItemID = 0x1FD4;
Dispellable = false;
GumpWidth = 300;
GumpHeight = 150;
MessageColor = 0xFFC000;
Message = "Are you sure you wish to spectate this duel?";
TitleColor = 0x7800;
TitleNumber = 1062051; // Gate Warning
Timer.StartTimer(TimeSpan.FromSeconds(10.0), Delete);
}
public ArenaMoongate(Serial serial) : base(serial)
{
}
public override string DefaultName => "spectator moongate";
public override void CheckGate(Mobile m, int range)
{
if (CheckCombat(m))
{
m.SendMessage(
0x22,
"You have recently been in combat with another player and cannot use this moongate."
);
}
else
{
base.CheckGate(m, range);
}
}
public override void UseGate(Mobile m)
{
if (CheckCombat(m))
{
m.SendMessage(
0x22,
"You have recently been in combat with another player and cannot use this moongate."
);
}
else
{
if (m_Teleporter?.Deleted == false)
{
m_Teleporter.Register(m);
}
base.UseGate(m);
}
}
public void Appear(Point3D loc, Map map)
{
Effects.PlaySound(loc, map, 0x20E);
MoveToWorld(loc, map);
}
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();
Delete();
}
}
}
}