## The bug
Any property getter reached from `GetProperties` that calls `InvalidateProperties` takes the tooltip build down with it:
```
System.ArgumentNullException: Value cannot be null. (Parameter 'array')
at Server.ObjectPropertyList.AppendStringDirect(String value)
at Server.Mobiles.PlayerMobile.GetProperties(IPropertyList list)
```
`InvalidateProperties` rebuilds **in place** — `Reset()`, then `GetProperties()` again on the same instance — and `Reset()` does two destructive things to a build already in flight:
1. **It returns the pooled interpolation buffer.** The compiler rents it in the handler ctor and returns it in the closing `Add`, so *every hole is evaluated while it is live*:
```csharp
var handler = new InterpolatedStringHandler(1, 2, list); // InitializeInterpolation() RENTS
handler.AppendFormatted(pl.Rank.Title); // <-- getter runs HERE
handler.AppendLiteral("\t");
handler.AppendFormatted(faction.Definition.PropName);
list.Add(1060776, ref handler); // consumes span, RETURNS
```
```
GetProperties(list)
├─ InitializeInterpolation() -> _arrayToReturnToPool = Rent(256) buffer LIVE
├─ « hole 1: pl.Rank.Title »
│ └─ PlayerState.Rank.get (lazy recompute)
│ └─ Invalidate() -> InvalidateProperties() -> m_PropertyList.Reset()
│ └─ Dispose(): Return(buf); _arrayToReturnToPool = null buffer GONE
└─ handler.AppendFormatted("Knight")
└─ _arrayToReturnToPool.AsSpan(_pos..)
└─ ArgumentNullException (Parameter 'array')
```
It surfaces as `ArgumentNullException` rather than `NullReferenceException` because the `Range` overload of `AsSpan` must read `array.Length`, so the BCL null-checks and names the parameter `array`.
2. **It rewinds the packet cursor**, so properties already written are overwritten by the nested pass — a silently corrupted tooltip even where the buffer survives.
## The fix: refuse, don't recover
There is no correct recovery, and retrying the build would only hide the defect. A nested invalidation now logs an error with a stack trace, **throws in `DEBUG`** so it gets found and fixed, and in `RELEASE` returns without touching the list — a possibly stale tooltip, but no crash, no corrupted packet, and nothing leaked back to the pool. Getters that genuinely must invalidate should defer:
```csharp
Timer.DelayCall(InvalidateProperties);
```
The guard flag lives on the `ObjectPropertyList`, not the entity: it is that list's own lifecycle, it costs nothing (both `Item` and `ObjectPropertyList` absorb it in existing padding, and the list is allocated lazily), and it stays correct when builds for different entities nest.
Base instance sizes are unchanged from `main`: Item 128 B, Mobile 792 B, ObjectPropertyList 72 B, PlayerMobile 1216 B.
`PropertyList` also publishes the list into `m_PropertyList` **before** building it rather than assigning through `??=` afterwards, so a nested `InvalidateProperties` sees the build in progress instead of recursing into a second throwaway list whose work is discarded.
`ObjectPropertyList` re-rents its scratch buffer instead of spanning a null array, so a stray `Reset()` from any other caller degrades rather than aborting `GetProperties`.
## Factions `PlayerState`: maintained, not lazily computed
The getter that surfaced this is now a plain field read — the whole `if (m_InvalidateRank)` block and the flag itself are gone:
```csharp
public RankDefinition Rank => m_Rank;
```
`UpdateRank()` recomputes at each point an input actually changes:
| Site | Why |
|---|---|
| `RankIndex` setter | this player's index changed |
| end of `KillPoints` setter | two paths write `m_RankIndex` directly, bypassing the setter; runs once the swap bookkeeping and `ZeroRankOffset` have settled |
| `Faction.AddMember` | *after* the insert — the member count is not settled during the ctor |
| `FactionState` load | once ordering and `ZeroRankOffset` are final |
Supporting fixes this forced out:
- **Both ctors seed the lowest rank.** Nothing recomputes on read any more, so `Rank` has to be usable immediately — including for members that never get a `RankIndex` assigned, which is *every member with no kill points*. Without this, `Rank.Title` NREs.
- **`Rank` always resolves.** Ranks are ordered by `Required` descending ending at `0`, so a *negative* percent (`RankIndex` out of sync with `ZeroRankOffset`) matched nothing and left `m_Rank` null. It no longer divides by a zero `ZeroRankOffset` either.
- **A pre-existing staleness bug.** The `KillPoints` setter writes `m_RankIndex` directly in two places, so the cached rank was never refreshed when a player crossed zero kill points.
All six readers of `Rank` were checked; none relied on the old side effect.
One behaviour change worth flagging: rank refreshes are now **eager** where they used to be lazy, so a `KillPoints` change invalidates each swapped player as it happens. The swap loops break as soon as ordering is satisfied — typically 0–2 swaps — but it is on the path that runs on every faction kill.
## Documentation
The rule is written down so it is enforceable rather than folklore:
- **CLAUDE.md** audit rule 19
- **`dev-docs/property-lists.md`** — new "Never Invalidate From Inside `GetProperties`" section with the failing/passing pattern
- **`dev-docs/claude-skills/modernuo-property-lists.md`** — key rule + anti-pattern
- **`dev-docs/claude-skills/modernuo-code-audit.md`** — rule 19, ERROR severity
## Tests
- `ObjectPropertyListReentrancyTests` — `Reset()` and `Dispose()` re-entered mid-hole (both red against `main` with the exact exception above), nesting behaviour, and the new contract: `DEBUG` throws, `RELEASE` survives, and the build is never retried into a loop.
- `FactionRankTests` — `Rank` is populated before anything reads it, tracks `RankIndex` without a read, is stable across reads, and still resolves when `RankIndex` is out of sync with `ZeroRankOffset`. Red-verified: removing the ctor seed fails the first one.
793/793 `Server.Tests` and 608/608 `UOContent.Tests` pass.
## Noted, not addressed here
`~ObjectPropertyList()` returns the rented array to `STArrayPool<char>.Shared` from the **finalizer thread**, and that pool is single-threaded by design. Left alone as a separate concern.
1567 lines
44 KiB
C#
1567 lines
44 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.CompilerServices;
|
|
using ModernUO.CodeGeneratedEvents;
|
|
using Server.Accounting;
|
|
using Server.Commands.Generic;
|
|
using Server.Engines.ConPVP;
|
|
using Server.Ethics;
|
|
using Server.Guilds;
|
|
using Server.Gumps;
|
|
using Server.Items;
|
|
using Server.Mobiles;
|
|
using Server.Prompts;
|
|
using Server.Targeting;
|
|
|
|
namespace Server.Factions;
|
|
|
|
[CustomEnum(["Minax", "Council of Mages", "True Britannians", "Shadowlords"])]
|
|
public abstract class Faction : IComparable<Faction>, ISpanParsable<Faction>
|
|
{
|
|
public const int StabilityFactor = 300; // 300% greater (3 times) than smallest faction
|
|
public const int StabilityActivation = 200; // Stability code goes into effect when largest faction has > 200 people
|
|
|
|
public const double SkillLossFactor = 1.0 / 3;
|
|
|
|
public static readonly TimeSpan LeavePeriod = TimeSpan.FromDays(3.0);
|
|
|
|
public static readonly Map Facet = Map.Felucca;
|
|
public static readonly TimeSpan SkillLossPeriod = TimeSpan.FromMinutes(20.0);
|
|
|
|
private static readonly Dictionary<Mobile, SkillLossContext> m_SkillLoss = new();
|
|
|
|
private FactionDefinition m_Definition;
|
|
public int ZeroRankOffset;
|
|
|
|
public Faction() => State = new FactionState(this);
|
|
|
|
public StrongholdRegion StrongholdRegion { get; set; }
|
|
|
|
public FactionDefinition Definition
|
|
{
|
|
get => m_Definition;
|
|
set
|
|
{
|
|
m_Definition = value;
|
|
StrongholdRegion = new StrongholdRegion(this);
|
|
}
|
|
}
|
|
|
|
public FactionState State { get; set; }
|
|
|
|
public Election Election
|
|
{
|
|
get => State.Election;
|
|
set => State.Election = value;
|
|
}
|
|
|
|
public Mobile Commander
|
|
{
|
|
get => State.Commander;
|
|
set => State.Commander = value;
|
|
}
|
|
|
|
public int Tithe
|
|
{
|
|
get => State.Tithe;
|
|
set => State.Tithe = value;
|
|
}
|
|
|
|
public int Silver
|
|
{
|
|
get => State.Silver;
|
|
set => State.Silver = value;
|
|
}
|
|
|
|
public List<PlayerState> Members
|
|
{
|
|
get => State.Members;
|
|
set => State.Members = value;
|
|
}
|
|
|
|
public bool FactionMessageReady => State.FactionMessageReady;
|
|
|
|
public virtual int MaximumTraps => 15;
|
|
|
|
public List<BaseFactionTrap> Traps
|
|
{
|
|
get => State.Traps;
|
|
set => State.Traps = value;
|
|
}
|
|
|
|
public static List<Faction> Factions => Reflector.Factions;
|
|
|
|
public int CompareTo(Faction f) => m_Definition.Sort - (f?.m_Definition.Sort ?? 0);
|
|
|
|
public void Broadcast(string text)
|
|
{
|
|
Broadcast(0x3B2, text);
|
|
}
|
|
|
|
public void Broadcast(int hue, string text)
|
|
{
|
|
var members = Members;
|
|
|
|
for (var i = 0; i < members.Count; ++i)
|
|
{
|
|
members[i].Mobile.SendMessage(hue, text);
|
|
}
|
|
}
|
|
|
|
public void Broadcast(int number)
|
|
{
|
|
var members = Members;
|
|
|
|
for (var i = 0; i < members.Count; ++i)
|
|
{
|
|
members[i].Mobile.SendLocalizedMessage(number);
|
|
}
|
|
}
|
|
|
|
public void BeginBroadcast(Mobile from)
|
|
{
|
|
from.SendLocalizedMessage(1010265); // Enter Faction Message
|
|
from.Prompt = new BroadcastPrompt(this);
|
|
}
|
|
|
|
public void EndBroadcast(Mobile from, string text)
|
|
{
|
|
if (from.AccessLevel == AccessLevel.Player)
|
|
{
|
|
State.RegisterBroadcast();
|
|
}
|
|
|
|
Broadcast(Definition.HueBroadcast, $"{from.Name} [Commander] {Definition.FriendlyName} : {text}");
|
|
}
|
|
|
|
public static void HandleAtrophy()
|
|
{
|
|
foreach (var f in Factions)
|
|
{
|
|
if (!f.State.IsAtrophyReady)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
var activePlayers = new List<PlayerState>();
|
|
|
|
foreach (var f in Factions)
|
|
{
|
|
foreach (var ps in f.Members)
|
|
{
|
|
if (ps.KillPoints > 0 && ps.IsActive)
|
|
{
|
|
activePlayers.Add(ps);
|
|
}
|
|
}
|
|
}
|
|
|
|
var distrib = 0;
|
|
|
|
foreach (var f in Factions)
|
|
{
|
|
distrib += f.State.CheckAtrophy();
|
|
}
|
|
|
|
if (activePlayers.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (var i = 0; i < distrib; ++i)
|
|
{
|
|
activePlayers.RandomElement().KillPoints++;
|
|
}
|
|
}
|
|
|
|
public static void DistributePoints(int distrib)
|
|
{
|
|
var activePlayers = new List<PlayerState>();
|
|
|
|
foreach (var f in Factions)
|
|
{
|
|
foreach (var ps in f.Members)
|
|
{
|
|
if (ps.KillPoints > 0 && ps.IsActive)
|
|
{
|
|
activePlayers.Add(ps);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (activePlayers.Count > 0)
|
|
{
|
|
for (var i = 0; i < distrib; ++i)
|
|
{
|
|
activePlayers.RandomElement().KillPoints++;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void BeginHonorLeadership(Mobile from)
|
|
{
|
|
from.SendLocalizedMessage(502090); // Click on the player whom you wish to honor.
|
|
from.BeginTarget(12, false, TargetFlags.None, HonorLeadership_OnTarget);
|
|
}
|
|
|
|
private static void HonorLeadership_OnTarget(Mobile from, object obj)
|
|
{
|
|
if (obj is not Mobile recv)
|
|
{
|
|
from.SendLocalizedMessage(1042496); // You may only honor another player.
|
|
return;
|
|
}
|
|
|
|
var giveState = PlayerState.Find(from);
|
|
var recvState = PlayerState.Find(recv);
|
|
|
|
if (giveState == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (recvState == null || recvState.Faction != giveState.Faction)
|
|
{
|
|
from.SendLocalizedMessage(1042497); // Only faction mates can be honored this way.
|
|
}
|
|
else if (giveState.KillPoints < 5)
|
|
{
|
|
from.SendLocalizedMessage(1042499); // You must have at least five kill points to honor them.
|
|
}
|
|
else
|
|
{
|
|
recvState.LastHonorTime = Core.Now;
|
|
giveState.KillPoints -= 5;
|
|
recvState.KillPoints += 4;
|
|
|
|
// TODO: Confirm no message sent to giver
|
|
recv.SendLocalizedMessage(1042500); // You have been honored with four kill points.
|
|
}
|
|
}
|
|
|
|
public virtual void AddMember(Mobile mob)
|
|
{
|
|
var state = new PlayerState(mob, this, Members);
|
|
Members.Insert(ZeroRankOffset, state);
|
|
|
|
// Ranked after the insert: the ctor ran while Owner was still short a member.
|
|
state.UpdateRank();
|
|
|
|
mob.AddToBackpack(FactionItem.Imbue(new Robe(), this, false, Definition.HuePrimary));
|
|
mob.SendLocalizedMessage(1010374); // You have been granted a robe which signifies your faction
|
|
|
|
mob.InvalidateProperties();
|
|
mob.Delta(MobileDelta.Noto);
|
|
|
|
mob.FixedEffect(0x373A, 10, 30);
|
|
mob.PlaySound(0x209);
|
|
}
|
|
|
|
public static bool IsNearType(Mobile mob, Type type, int range)
|
|
{
|
|
if (type.IsAssignableTo(typeof(Mobile)))
|
|
{
|
|
foreach (var obj in mob.Map.GetMobilesInRange(mob.Location, range))
|
|
{
|
|
if (type.IsInstanceOfType(obj))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (type.IsAssignableTo(typeof(Item)))
|
|
{
|
|
foreach (var item in mob.Map.GetItemsInRange(mob.Location, range))
|
|
{
|
|
if (type.IsInstanceOfType(item))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public static bool IsNearType(Mobile mob, Type[] types, int range)
|
|
{
|
|
var mobs = false;
|
|
var items = false;
|
|
for (var i = 0; !(mobs && items) && i < types.Length; i++)
|
|
{
|
|
var type = types[i];
|
|
if (type.IsAssignableTo(typeof(Mobile)))
|
|
{
|
|
mobs = true;
|
|
}
|
|
|
|
if (type.IsAssignableTo(typeof(Item)))
|
|
{
|
|
items = true;
|
|
}
|
|
}
|
|
|
|
if (mobs)
|
|
{
|
|
foreach (var m in mob.Map.GetMobilesInRange(mob.Location, range))
|
|
{
|
|
if (m.InTypeList(types))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (items)
|
|
{
|
|
foreach (var item in mob.Map.GetItemsInRange(mob.Location, range))
|
|
{
|
|
if (item.InTypeList(types))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public void RemovePlayerState(PlayerState pl)
|
|
{
|
|
if (pl == null || !Members.Contains(pl))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var killPoints = pl.KillPoints;
|
|
|
|
if (pl.RankIndex != -1)
|
|
{
|
|
while (pl.RankIndex + 1 < ZeroRankOffset)
|
|
{
|
|
var pNext = Members[pl.RankIndex + 1];
|
|
Members[pl.RankIndex + 1] = pl;
|
|
Members[pl.RankIndex] = pNext;
|
|
pl.RankIndex++;
|
|
pNext.RankIndex--;
|
|
}
|
|
|
|
ZeroRankOffset--;
|
|
}
|
|
|
|
Members.Remove(pl);
|
|
|
|
var pm = (PlayerMobile)pl.Mobile;
|
|
if (pm == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var mob = pl.Mobile;
|
|
if (pm.FactionPlayerState == pl)
|
|
{
|
|
pm.FactionPlayerState = null;
|
|
|
|
mob.InvalidateProperties();
|
|
mob.Delta(MobileDelta.Noto);
|
|
|
|
if (Election.IsCandidate(mob))
|
|
{
|
|
Election.RemoveCandidate(mob);
|
|
}
|
|
|
|
if (pl.Finance != null)
|
|
{
|
|
pl.Finance.Finance = null;
|
|
}
|
|
|
|
if (pl.Sheriff != null)
|
|
{
|
|
pl.Sheriff.Sheriff = null;
|
|
}
|
|
|
|
Election.RemoveVoter(mob);
|
|
|
|
if (Commander == mob)
|
|
{
|
|
Commander = null;
|
|
}
|
|
|
|
pm.ValidateEquipment();
|
|
}
|
|
|
|
if (killPoints > 0)
|
|
{
|
|
DistributePoints(killPoints);
|
|
}
|
|
}
|
|
|
|
public void RemoveMember(Mobile mob)
|
|
{
|
|
var pl = PlayerState.Find(mob);
|
|
|
|
if (pl == null || !Members.Contains(pl))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var killPoints = pl.KillPoints;
|
|
|
|
// Ordinarily, through normal faction removal, this will never find any sigils.
|
|
// Only with a leave delay less than the ReturnPeriod or a Faction Kick/Ban, will this ever do anything
|
|
|
|
if (mob.Backpack != null)
|
|
{
|
|
using var queue = mob.Backpack.EnumerateItemsByType<Sigil>();
|
|
foreach (var sigil in queue)
|
|
{
|
|
sigil.ReturnHome();
|
|
}
|
|
}
|
|
|
|
if (pl.RankIndex != -1)
|
|
{
|
|
while (pl.RankIndex + 1 < ZeroRankOffset)
|
|
{
|
|
var pNext = Members[pl.RankIndex + 1];
|
|
Members[pl.RankIndex + 1] = pl;
|
|
Members[pl.RankIndex] = pNext;
|
|
pl.RankIndex++;
|
|
pNext.RankIndex--;
|
|
}
|
|
|
|
ZeroRankOffset--;
|
|
}
|
|
|
|
Members.Remove(pl);
|
|
|
|
if (mob is PlayerMobile mobile)
|
|
{
|
|
mobile.FactionPlayerState = null;
|
|
}
|
|
|
|
mob.InvalidateProperties();
|
|
mob.Delta(MobileDelta.Noto);
|
|
|
|
if (Election.IsCandidate(mob))
|
|
{
|
|
Election.RemoveCandidate(mob);
|
|
}
|
|
|
|
Election.RemoveVoter(mob);
|
|
|
|
if (pl.Finance != null)
|
|
{
|
|
pl.Finance.Finance = null;
|
|
}
|
|
|
|
if (pl.Sheriff != null)
|
|
{
|
|
pl.Sheriff.Sheriff = null;
|
|
}
|
|
|
|
if (Commander == mob)
|
|
{
|
|
Commander = null;
|
|
}
|
|
|
|
if (mob is PlayerMobile playerMobile)
|
|
{
|
|
playerMobile.ValidateEquipment();
|
|
}
|
|
|
|
if (killPoints > 0)
|
|
{
|
|
DistributePoints(killPoints);
|
|
}
|
|
}
|
|
|
|
public void JoinGuilded(PlayerMobile mob, Guild guild)
|
|
{
|
|
if (mob.Young)
|
|
{
|
|
guild.RemoveMember(mob);
|
|
// You have been kicked out of your guild!
|
|
// Young players may not remain in a guild which is allied with a faction.
|
|
mob.SendLocalizedMessage(1042283);
|
|
}
|
|
else if (AlreadyHasCharInFaction(mob))
|
|
{
|
|
guild.RemoveMember(mob);
|
|
mob.SendLocalizedMessage(1005281); // You have been kicked out of your guild due to factional overlap
|
|
}
|
|
else if (IsFactionBanned(mob))
|
|
{
|
|
guild.RemoveMember(mob);
|
|
mob.SendLocalizedMessage(1005052); // You are currently banned from the faction system
|
|
}
|
|
else
|
|
{
|
|
AddMember(mob);
|
|
// You are now joining a faction:
|
|
mob.SendLocalizedMessage(1042756, true, $" {m_Definition.FriendlyName}");
|
|
}
|
|
}
|
|
|
|
public void JoinAlone(Mobile mob)
|
|
{
|
|
AddMember(mob);
|
|
mob.SendLocalizedMessage(1005058); // You have joined the faction
|
|
}
|
|
|
|
private static bool AlreadyHasCharInFaction(Mobile mob)
|
|
{
|
|
if (mob.Account is Account acct)
|
|
{
|
|
for (var i = 0; i < acct.Length; ++i)
|
|
{
|
|
var c = acct[i];
|
|
|
|
if (Find(c) != null)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public static bool IsFactionBanned(Mobile mob)
|
|
{
|
|
if (mob.Account is not Account acct)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return acct.GetTag("FactionBanned") != null;
|
|
}
|
|
|
|
public void OnJoinAccepted(Mobile mob)
|
|
{
|
|
if (mob is not PlayerMobile pm)
|
|
{
|
|
return; // sanity
|
|
}
|
|
|
|
var pl = PlayerState.Find(pm);
|
|
|
|
if (pm.Young)
|
|
{
|
|
pm.SendLocalizedMessage(1010104); // You cannot join a faction as a young player
|
|
}
|
|
else if (pl?.IsLeaving == true)
|
|
{
|
|
// You cannot use the faction stone until you have finished quitting your current faction
|
|
pm.SendLocalizedMessage(1005051);
|
|
}
|
|
else if (AlreadyHasCharInFaction(pm))
|
|
{
|
|
// You cannot join a faction because you already declared your allegiance with another character
|
|
pm.SendLocalizedMessage(1005059);
|
|
}
|
|
else if (IsFactionBanned(mob))
|
|
{
|
|
pm.SendLocalizedMessage(1005052); // You are currently banned from the faction system
|
|
}
|
|
else if (pm.Guild is not Guild guild)
|
|
{
|
|
if (!CanHandleInflux(1))
|
|
{
|
|
// In the interest of faction stability, this faction declines to accept new members for now.
|
|
pm.SendLocalizedMessage(1018031);
|
|
}
|
|
else
|
|
{
|
|
JoinAlone(mob);
|
|
}
|
|
}
|
|
else if (guild?.Leader != pm)
|
|
{
|
|
// You cannot join a faction because you are in a guild and not the guildmaster
|
|
pm.SendLocalizedMessage(1005057);
|
|
}
|
|
else if (guild.Type != GuildType.Regular)
|
|
{
|
|
// You cannot join a faction because your guild is an Order or Chaos type.
|
|
pm.SendLocalizedMessage(1042161);
|
|
}
|
|
else if (!Guild.NewGuildSystem && guild.Enemies?.Count > 0) // CAN join w/wars in new system
|
|
{
|
|
pm.SendLocalizedMessage(1005056); // You cannot join a faction with active Wars
|
|
}
|
|
else if (Guild.NewGuildSystem && guild.Alliance != null)
|
|
{
|
|
// Your guild cannot join a faction while in alliance with non-factioned guilds.
|
|
pm.SendLocalizedMessage(1080454);
|
|
}
|
|
else if (!CanHandleInflux(guild.Members.Count))
|
|
{
|
|
// In the interest of faction stability, this faction declines to accept new members for now.
|
|
pm.SendLocalizedMessage(1018031);
|
|
}
|
|
else
|
|
{
|
|
var members = new List<Mobile>(guild.Members);
|
|
|
|
for (var i = 0; i < members.Count; ++i)
|
|
{
|
|
if (members[i] is not PlayerMobile member)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
JoinGuilded(member, guild);
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool IsCommander(Mobile mob) => mob?.AccessLevel >= AccessLevel.GameMaster || mob == Commander;
|
|
|
|
public override string ToString() => m_Definition.FriendlyName;
|
|
|
|
public static bool CheckLeaveTimer(Mobile mob)
|
|
{
|
|
var pl = PlayerState.Find(mob);
|
|
|
|
if (pl?.IsLeaving != true)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (pl.Leaving + LeavePeriod >= Core.Now)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
mob.SendLocalizedMessage(1005163); // You have now quit your faction
|
|
|
|
pl.Faction.RemoveMember(mob);
|
|
|
|
return true;
|
|
}
|
|
|
|
public static void Configure()
|
|
{
|
|
EventSink.Logout += EventSink_Logout;
|
|
|
|
CommandSystem.Register("FactionElection", AccessLevel.GameMaster, FactionElection_OnCommand);
|
|
CommandSystem.Register("FactionCommander", AccessLevel.Administrator, FactionCommander_OnCommand);
|
|
CommandSystem.Register("FactionItemReset", AccessLevel.Administrator, FactionItemReset_OnCommand);
|
|
CommandSystem.Register("FactionReset", AccessLevel.Administrator, FactionReset_OnCommand);
|
|
CommandSystem.Register("FactionTownReset", AccessLevel.Administrator, FactionTownReset_OnCommand);
|
|
}
|
|
|
|
public static void Initialize()
|
|
{
|
|
Timer.DelayCall(TimeSpan.FromMinutes(1.0), TimeSpan.FromMinutes(10.0), HandleAtrophy);
|
|
Timer.DelayCall(TimeSpan.FromSeconds(30.0), TimeSpan.FromSeconds(30.0), ProcessTick);
|
|
}
|
|
|
|
[Usage("FactionTownReset")]
|
|
[Description("Resets all faction town data in the world.")]
|
|
public static void FactionTownReset_OnCommand(CommandEventArgs e)
|
|
{
|
|
var monoliths = BaseMonolith.Monoliths;
|
|
|
|
for (var i = 0; i < monoliths.Count; ++i)
|
|
{
|
|
monoliths[i].Sigil = null;
|
|
}
|
|
|
|
var towns = Town.Towns;
|
|
|
|
for (var i = 0; i < towns.Count; ++i)
|
|
{
|
|
var town = towns[i];
|
|
town.Silver = 0;
|
|
town.Sheriff = null;
|
|
town.Finance = null;
|
|
town.Tax = 0;
|
|
town.Owner = null;
|
|
}
|
|
|
|
var sigils = Sigil.Sigils;
|
|
|
|
for (var i = 0; i < sigils.Count; ++i)
|
|
{
|
|
var sigil = sigils[i];
|
|
sigil.Corrupted = null;
|
|
sigil.Corrupting = null;
|
|
sigil.LastStolen = DateTime.MinValue;
|
|
sigil.GraceStart = DateTime.MinValue;
|
|
sigil.CorruptionStart = DateTime.MinValue;
|
|
sigil.PurificationStart = DateTime.MinValue;
|
|
sigil.LastMonolith = null;
|
|
sigil.ReturnHome();
|
|
}
|
|
|
|
var factions = Factions;
|
|
|
|
for (var i = 0; i < factions.Count; ++i)
|
|
{
|
|
var f = factions[i];
|
|
|
|
var list = new List<FactionItem>(f.State.FactionItems);
|
|
|
|
for (var j = 0; j < list.Count; ++j)
|
|
{
|
|
var fi = list[j];
|
|
|
|
if (fi.Expiration == DateTime.MinValue)
|
|
{
|
|
fi.Item.Delete();
|
|
}
|
|
else
|
|
{
|
|
fi.Detach();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
[Usage("FactionReset")]
|
|
[Description("Resets all faction data in the world.")]
|
|
public static void FactionReset_OnCommand(CommandEventArgs e)
|
|
{
|
|
var monoliths = BaseMonolith.Monoliths;
|
|
|
|
for (var i = 0; i < monoliths.Count; ++i)
|
|
{
|
|
monoliths[i].Sigil = null;
|
|
}
|
|
|
|
var towns = Town.Towns;
|
|
|
|
for (var i = 0; i < towns.Count; ++i)
|
|
{
|
|
var town = towns[i];
|
|
town.Silver = 0;
|
|
town.Sheriff = null;
|
|
town.Finance = null;
|
|
town.Tax = 0;
|
|
town.Owner = null;
|
|
}
|
|
|
|
var sigils = Sigil.Sigils;
|
|
|
|
for (var i = 0; i < sigils.Count; ++i)
|
|
{
|
|
var sigil = sigils[i];
|
|
sigil.Corrupted = null;
|
|
sigil.Corrupting = null;
|
|
sigil.LastStolen = DateTime.MinValue;
|
|
sigil.GraceStart = DateTime.MinValue;
|
|
sigil.CorruptionStart = DateTime.MinValue;
|
|
sigil.PurificationStart = DateTime.MinValue;
|
|
sigil.LastMonolith = null;
|
|
sigil.ReturnHome();
|
|
}
|
|
|
|
var factions = Factions;
|
|
|
|
for (var i = 0; i < factions.Count; ++i)
|
|
{
|
|
var f = factions[i];
|
|
|
|
var playerStateList = new List<PlayerState>(f.Members);
|
|
|
|
for (var j = 0; j < playerStateList.Count; ++j)
|
|
{
|
|
f.RemoveMember(playerStateList[j].Mobile);
|
|
}
|
|
|
|
var factionItemList = new List<FactionItem>(f.State.FactionItems);
|
|
|
|
for (var j = 0; j < factionItemList.Count; ++j)
|
|
{
|
|
var fi = factionItemList[j];
|
|
|
|
if (fi.Expiration == DateTime.MinValue)
|
|
{
|
|
fi.Item.Delete();
|
|
}
|
|
else
|
|
{
|
|
fi.Detach();
|
|
}
|
|
}
|
|
|
|
var factionTrapList = new List<BaseFactionTrap>(f.Traps);
|
|
|
|
for (var j = 0; j < factionTrapList.Count; ++j)
|
|
{
|
|
factionTrapList[j].Delete();
|
|
}
|
|
}
|
|
}
|
|
|
|
[Usage("FactionItemReset")]
|
|
[Description("Resets all faction items in the world.")]
|
|
public static void FactionItemReset_OnCommand(CommandEventArgs e)
|
|
{
|
|
var items = new List<Item>();
|
|
|
|
foreach (var item in World.Items.Values)
|
|
{
|
|
if (item is IFactionItem && item is not HoodedShroudOfShadows)
|
|
{
|
|
items.Add(item);
|
|
}
|
|
}
|
|
|
|
var hues = new int[Factions.Count * 2];
|
|
|
|
for (var i = 0; i < Factions.Count; ++i)
|
|
{
|
|
var faction = Factions[i];
|
|
hues[0 + i * 2] = faction.Definition.HuePrimary;
|
|
hues[1 + i * 2] = faction.Definition.HueSecondary;
|
|
}
|
|
|
|
var count = 0;
|
|
|
|
for (var i = 0; i < items.Count; ++i)
|
|
{
|
|
var item = items[i];
|
|
var fci = (IFactionItem)item;
|
|
|
|
if (fci.FactionItemState != null || item.LootType != LootType.Blessed)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var isHued = false;
|
|
|
|
for (var j = 0; j < hues.Length; ++j)
|
|
{
|
|
if (item.Hue == hues[j])
|
|
{
|
|
isHued = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (isHued)
|
|
{
|
|
fci.FactionItemState = null;
|
|
++count;
|
|
}
|
|
}
|
|
|
|
e.Mobile.SendMessage($"{count} items reset");
|
|
}
|
|
|
|
[Usage("FactionCommander")]
|
|
[Description("Sets the targeted player as the faction commander.")]
|
|
public static void FactionCommander_OnCommand(CommandEventArgs e)
|
|
{
|
|
e.Mobile.SendMessage("Target a player to make them the faction commander.");
|
|
e.Mobile.BeginTarget(-1, false, TargetFlags.None, FactionCommander_OnTarget);
|
|
}
|
|
|
|
public static void FactionCommander_OnTarget(Mobile from, object obj)
|
|
{
|
|
if (obj is PlayerMobile mobile)
|
|
{
|
|
Mobile targ = mobile;
|
|
var pl = PlayerState.Find(targ);
|
|
|
|
if (pl != null)
|
|
{
|
|
pl.Faction.Commander = targ;
|
|
from.SendMessage("You have appointed them as the faction commander.");
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage("They are not in a faction.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage("That is not a player.");
|
|
}
|
|
}
|
|
|
|
[Usage("FactionElection")]
|
|
[Description("Opens the election properties for the targeted faction stone.")]
|
|
public static void FactionElection_OnCommand(CommandEventArgs e)
|
|
{
|
|
e.Mobile.SendMessage("Target a faction stone to open its election properties.");
|
|
e.Mobile.BeginTarget(-1, false, TargetFlags.None, FactionElection_OnTarget);
|
|
}
|
|
|
|
public static void FactionElection_OnTarget(Mobile from, object obj)
|
|
{
|
|
if (obj is FactionStone stone)
|
|
{
|
|
var faction = stone.Faction;
|
|
|
|
if (faction != null)
|
|
{
|
|
from.SendGump(new ElectionManagementGump(faction.Election));
|
|
}
|
|
// from.SendGump( new Gumps.PropertiesGump( from, faction.Election ) );
|
|
else
|
|
{
|
|
from.SendMessage("That stone has no faction assigned.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage("That is not a faction stone.");
|
|
}
|
|
}
|
|
|
|
public static void FactionKick_OnCommand(CommandEventArgs e)
|
|
{
|
|
e.Mobile.SendMessage("Target a player to remove them from their faction.");
|
|
e.Mobile.BeginTarget(-1, false, TargetFlags.None, FactionKick_OnTarget);
|
|
}
|
|
|
|
public static void FactionKick_OnTarget(Mobile from, object obj)
|
|
{
|
|
if (obj is Mobile mob)
|
|
{
|
|
var pl = PlayerState.Find(mob);
|
|
|
|
if (pl != null)
|
|
{
|
|
pl.Faction.RemoveMember(mob);
|
|
|
|
mob.SendMessage("You have been kicked from your faction.");
|
|
from.SendMessage("They have been kicked from their faction.");
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage("They are not in a faction.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage("That is not a player.");
|
|
}
|
|
}
|
|
|
|
public static void ProcessTick()
|
|
{
|
|
var sigils = Sigil.Sigils;
|
|
|
|
for (var i = 0; i < sigils.Count; ++i)
|
|
{
|
|
var sigil = sigils[i];
|
|
|
|
if (!sigil.IsBeingCorrupted && sigil.GraceStart != DateTime.MinValue &&
|
|
sigil.GraceStart + Sigil.CorruptionGrace < Core.Now)
|
|
{
|
|
if (sigil.LastMonolith is StrongholdMonolith &&
|
|
(sigil.Corrupted == null || sigil.LastMonolith.Faction != sigil.Corrupted))
|
|
{
|
|
sigil.Corrupting = sigil.LastMonolith.Faction;
|
|
sigil.CorruptionStart = Core.Now;
|
|
}
|
|
else
|
|
{
|
|
sigil.Corrupting = null;
|
|
sigil.CorruptionStart = DateTime.MinValue;
|
|
}
|
|
|
|
sigil.GraceStart = DateTime.MinValue;
|
|
}
|
|
|
|
if (sigil.LastMonolith?.Sigil == null)
|
|
{
|
|
if (sigil.LastStolen + Sigil.ReturnPeriod < Core.Now)
|
|
{
|
|
sigil.ReturnHome();
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (sigil.IsBeingCorrupted && sigil.CorruptionStart + Sigil.CorruptionPeriod < Core.Now)
|
|
{
|
|
sigil.Corrupted = sigil.Corrupting;
|
|
}
|
|
else if (sigil.IsPurifying && sigil.PurificationStart + Sigil.PurificationPeriod < Core.Now)
|
|
{
|
|
sigil.PurificationStart = DateTime.MinValue;
|
|
}
|
|
|
|
sigil.Corrupting = null;
|
|
sigil.CorruptionStart = DateTime.MinValue;
|
|
sigil.GraceStart = DateTime.MinValue;
|
|
}
|
|
}
|
|
|
|
public static void HandleDeath(Mobile mob)
|
|
{
|
|
HandleDeath(mob, null);
|
|
}
|
|
|
|
public int AwardSilver(Mobile mob, int silver)
|
|
{
|
|
if (silver <= 0)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
var tithed = silver * Tithe / 100;
|
|
|
|
Silver += tithed;
|
|
|
|
silver = silver - tithed;
|
|
|
|
if (silver > 0)
|
|
{
|
|
mob.AddToBackpack(new Silver(silver));
|
|
}
|
|
|
|
return silver;
|
|
}
|
|
|
|
public static Faction FindSmallestFaction()
|
|
{
|
|
var factions = Factions;
|
|
Faction smallest = null;
|
|
|
|
for (var i = 0; i < factions.Count; ++i)
|
|
{
|
|
var faction = factions[i];
|
|
|
|
if (smallest == null || faction.Members.Count < smallest.Members.Count)
|
|
{
|
|
smallest = faction;
|
|
}
|
|
}
|
|
|
|
return smallest;
|
|
}
|
|
|
|
public static bool StabilityActive()
|
|
{
|
|
var factions = Factions;
|
|
|
|
for (var i = 0; i < factions.Count; ++i)
|
|
{
|
|
var faction = factions[i];
|
|
|
|
if (faction.Members.Count > StabilityActivation)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public bool CanHandleInflux(int influx)
|
|
{
|
|
if (!StabilityActive())
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var smallest = FindSmallestFaction();
|
|
|
|
return smallest == null || (Members.Count + influx) * 100 / StabilityFactor <= smallest.Members.Count;
|
|
}
|
|
|
|
public static void HandleDeath(Mobile victim, Mobile killer)
|
|
{
|
|
killer ??= victim.FindMostRecentDamager(true);
|
|
|
|
var killerState = PlayerState.Find(killer);
|
|
var killerPack = killer?.Backpack;
|
|
|
|
if (victim.Backpack != null)
|
|
{
|
|
using var queue = victim.Backpack.EnumerateItemsByType<Sigil>();
|
|
foreach (var sigil in queue)
|
|
{
|
|
if (killerState == null || killerPack == null)
|
|
{
|
|
sigil.ReturnHome();
|
|
}
|
|
else if (killer?.GetDistanceToSqrt(victim) > 64)
|
|
{
|
|
sigil.ReturnHome();
|
|
killer.SendLocalizedMessage(1042230); // The sigil has gone back to its home location.
|
|
}
|
|
else if (Sigil.ExistsOn(killer))
|
|
{
|
|
sigil.ReturnHome();
|
|
// The sigil has gone back to its home location because you already have a sigil.
|
|
killer?.SendLocalizedMessage(1010258);
|
|
}
|
|
else if (!killerPack.TryDropItem(killer, sigil, false))
|
|
{
|
|
sigil.ReturnHome();
|
|
// The sigil has gone home because your backpack is full.
|
|
killer?.SendLocalizedMessage(1010259);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (killerState == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (victim is BaseCreature bc)
|
|
{
|
|
var victimFaction = bc.FactionAllegiance;
|
|
|
|
if (bc.Map == Facet && victimFaction != null && killerState.Faction != victimFaction)
|
|
{
|
|
var silver = killerState.Faction.AwardSilver(killer, bc.FactionSilverWorth);
|
|
|
|
if (silver > 0)
|
|
{
|
|
// Thou hast earned ~1_AMOUNT~ silver for vanquishing the vile creature.
|
|
killer?.SendLocalizedMessage(1042748, silver.ToString("N0"));
|
|
}
|
|
}
|
|
|
|
if (bc.Map == Facet && bc.GetEthicAllegiance(killer) == BaseCreature.Allegiance.Enemy)
|
|
{
|
|
var killerEPL = Player.Find(killer);
|
|
|
|
if (killerEPL != null && 100 - killerEPL.Power > Utility.Random(100))
|
|
{
|
|
++killerEPL.Power;
|
|
++killerEPL.History;
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
var victimState = PlayerState.Find(victim);
|
|
|
|
if (victimState == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (victim.Region.IsPartOf<SafeZone>())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (killer == victim || killerState.Faction != victimState.Faction)
|
|
{
|
|
ApplySkillLoss(victim);
|
|
}
|
|
|
|
if (killerState.Faction != victimState.Faction)
|
|
{
|
|
if (victimState.KillPoints <= -6)
|
|
{
|
|
killer?.SendLocalizedMessage(501693); // This victim is not worth enough to get kill points from.
|
|
|
|
var killerEPL = Player.Find(killer);
|
|
var victimEPL = Player.Find(victim);
|
|
|
|
if (killerEPL != null && victimEPL?.Power > 0 && victimState.CanGiveSilverTo(killer))
|
|
{
|
|
// Not using Math.Clamp because min/max values could be reversed.
|
|
var powerTransfer = Math.Min(Math.Max(1, victimEPL.Power / 5), 100 - killerEPL.Power);
|
|
|
|
if (powerTransfer > 0)
|
|
{
|
|
victimEPL.Power -= (powerTransfer + 1) / 2;
|
|
killerEPL.Power += powerTransfer;
|
|
|
|
killerEPL.History += powerTransfer;
|
|
victimState.OnGivenSilverTo(killer);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var award = Math.Max(victimState.KillPoints / 10, 1);
|
|
|
|
if (award > 40)
|
|
{
|
|
award = 40;
|
|
}
|
|
|
|
if (victimState.CanGiveSilverTo(killer))
|
|
{
|
|
PowerFactionItem.CheckSpawn(killer, victim);
|
|
|
|
if (victimState.KillPoints > 0)
|
|
{
|
|
victimState.IsActive = true;
|
|
|
|
if (Utility.Random(3) < 1)
|
|
{
|
|
killerState.IsActive = true;
|
|
}
|
|
|
|
var silver = killerState.Faction.AwardSilver(killer, award * 40);
|
|
|
|
if (silver > 0)
|
|
{
|
|
// You have earned ~1_SILVER_AMOUNT~ pieces for vanquishing ~2_PLAYER_NAME~!
|
|
killer?.SendLocalizedMessage(1042736, $"{silver:N0} silver\t{victim.Name}");
|
|
}
|
|
}
|
|
|
|
victimState.KillPoints -= award;
|
|
killerState.KillPoints += award;
|
|
|
|
var offset = award != 1 ? 0 : 2; // for pluralization
|
|
|
|
var args = $"{award}\t{victim.Name}\t{killer?.Name}";
|
|
|
|
// Thou hast been honored with ~1_KILL_POINTS~ kill point(s) for vanquishing ~2_DEAD_PLAYER~!
|
|
killer?.SendLocalizedMessage(1042737 + offset, args);
|
|
|
|
// Thou has lost ~1_KILL_POINTS~ kill point(s) to ~3_ATTACKER_NAME~ for being vanquished!
|
|
victim.SendLocalizedMessage(1042738 + offset, args);
|
|
|
|
var killerEPL = Player.Find(killer);
|
|
var victimEPL = Player.Find(victim);
|
|
|
|
if (killerEPL != null && victimEPL?.Power > 0)
|
|
{
|
|
var powerTransfer = Math.Max(1, victimEPL.Power / 5);
|
|
|
|
if (powerTransfer > 100 - killerEPL.Power)
|
|
{
|
|
powerTransfer = 100 - killerEPL.Power;
|
|
}
|
|
|
|
if (powerTransfer > 0)
|
|
{
|
|
victimEPL.Power -= (powerTransfer + 1) / 2;
|
|
killerEPL.Power += powerTransfer;
|
|
|
|
killerEPL.History += powerTransfer;
|
|
}
|
|
}
|
|
|
|
victimState.OnGivenSilverTo(killer);
|
|
}
|
|
else
|
|
{
|
|
// You have recently defeated this enemy and thus their death brings you no honor.
|
|
killer?.SendLocalizedMessage(1042231);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void EventSink_Logout(Mobile m)
|
|
{
|
|
if (m.Backpack != null)
|
|
{
|
|
using var queue = m.Backpack.EnumerateItemsByType<Sigil>();
|
|
foreach (var sigil in queue)
|
|
{
|
|
sigil.ReturnHome();
|
|
}
|
|
}
|
|
}
|
|
|
|
[OnEvent(nameof(PlayerMobile.PlayerLoginEvent))]
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static void OnLogin(PlayerMobile pm) => CheckLeaveTimer(pm);
|
|
|
|
public static void WriteReference(IGenericWriter writer, Faction fact)
|
|
{
|
|
var idx = Factions.IndexOf(fact);
|
|
|
|
writer.WriteEncodedInt(idx + 1);
|
|
}
|
|
|
|
public static Faction ReadReference(IGenericReader reader)
|
|
{
|
|
var idx = reader.ReadEncodedInt() - 1;
|
|
|
|
return idx >= 0 && idx < Factions.Count ? Factions[idx] : null;
|
|
}
|
|
|
|
public static Faction Find(Mobile mob, bool inherit = false, bool creatureAllegiances = false)
|
|
{
|
|
var pl = PlayerState.Find(mob);
|
|
|
|
if (pl != null)
|
|
{
|
|
return pl.Faction;
|
|
}
|
|
|
|
if (inherit && mob is BaseCreature bc)
|
|
{
|
|
var master = bc.GetMaster();
|
|
if (master != null)
|
|
{
|
|
return Find(master);
|
|
}
|
|
|
|
if (creatureAllegiances && bc is BaseFactionGuard guard)
|
|
{
|
|
return guard.Faction;
|
|
}
|
|
|
|
if (creatureAllegiances)
|
|
{
|
|
return bc.FactionAllegiance;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static Faction Parse(string s) => Parse(s, null);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static Faction Parse(string s, IFormatProvider provider) => Parse(s.AsSpan(), provider);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static bool TryParse(string s, IFormatProvider provider, out Faction result) =>
|
|
TryParse(s.AsSpan(), provider, out result);
|
|
|
|
public static Faction Parse(ReadOnlySpan<char> s, IFormatProvider provider)
|
|
{
|
|
if (TryParse(s, provider, out var result))
|
|
{
|
|
return result;
|
|
}
|
|
|
|
throw new FormatException($"The input string '{s}' was not in a correct format.");
|
|
}
|
|
|
|
public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider provider, out Faction result)
|
|
{
|
|
var factions = Factions;
|
|
|
|
for (var i = 0; i < factions.Count; ++i)
|
|
{
|
|
var faction = factions[i];
|
|
|
|
if (s.InsensitiveEquals(faction.Definition.FriendlyName))
|
|
{
|
|
result = faction;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
result = null;
|
|
return false;
|
|
}
|
|
|
|
public static bool InSkillLoss(Mobile mob) => m_SkillLoss.ContainsKey(mob);
|
|
|
|
public static void ApplySkillLoss(Mobile mob)
|
|
{
|
|
if (InSkillLoss(mob))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var context = new SkillLossContext();
|
|
m_SkillLoss[mob] = context;
|
|
|
|
var mods = context.m_Mods = [];
|
|
|
|
for (var i = 0; i < mob.Skills.Length; ++i)
|
|
{
|
|
var sk = mob.Skills[i];
|
|
var baseValue = sk.Base;
|
|
|
|
if (baseValue > 0)
|
|
{
|
|
SkillMod mod = new DefaultSkillMod(
|
|
sk.SkillName,
|
|
$"{sk.Name}FactionSkillLoss",
|
|
true,
|
|
-(baseValue * SkillLossFactor)
|
|
);
|
|
|
|
mods.Add(mod);
|
|
mob.AddSkillMod(mod);
|
|
}
|
|
}
|
|
|
|
Timer.StartTimer(SkillLossPeriod, () => ClearSkillLoss_Event(mob), out context._timerToken);
|
|
}
|
|
|
|
private static void ClearSkillLoss_Event(Mobile mob) => ClearSkillLoss(mob);
|
|
|
|
public static bool ClearSkillLoss(Mobile mob)
|
|
{
|
|
if (!m_SkillLoss.Remove(mob, out var context))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var mods = context.m_Mods;
|
|
|
|
foreach (var mod in mods)
|
|
{
|
|
mod.Remove();
|
|
}
|
|
|
|
context.m_Mods = null;
|
|
context._timerToken.Cancel();
|
|
|
|
return true;
|
|
}
|
|
|
|
private class BroadcastPrompt : Prompt
|
|
{
|
|
private readonly Faction m_Faction;
|
|
|
|
public BroadcastPrompt(Faction faction) => m_Faction = faction;
|
|
|
|
public override void OnResponse(Mobile from, string text)
|
|
{
|
|
m_Faction.EndBroadcast(from, text);
|
|
}
|
|
}
|
|
|
|
private class SkillLossContext
|
|
{
|
|
public HashSet<SkillMod> m_Mods;
|
|
public TimerExecutionToken _timerToken;
|
|
}
|
|
}
|
|
|
|
public enum FactionKickType
|
|
{
|
|
Kick,
|
|
Ban,
|
|
Unban
|
|
}
|
|
|
|
public class FactionKickCommand : BaseCommand
|
|
{
|
|
private readonly FactionKickType m_KickType;
|
|
|
|
public FactionKickCommand(FactionKickType kickType)
|
|
{
|
|
m_KickType = kickType;
|
|
|
|
AccessLevel = AccessLevel.GameMaster;
|
|
Supports = CommandSupport.AllMobiles;
|
|
ObjectTypes = ObjectTypes.Mobiles;
|
|
|
|
switch (m_KickType)
|
|
{
|
|
case FactionKickType.Kick:
|
|
{
|
|
Commands = ["FactionKick"];
|
|
Usage = "FactionKick";
|
|
Description =
|
|
"Kicks the targeted player out of his current faction. This does not prevent them from rejoining.";
|
|
break;
|
|
}
|
|
case FactionKickType.Ban:
|
|
{
|
|
Commands = ["FactionBan"];
|
|
Usage = "FactionBan";
|
|
Description =
|
|
"Bans the account of a targeted player from joining factions. All players on the account are removed from their current faction, if any.";
|
|
break;
|
|
}
|
|
case FactionKickType.Unban:
|
|
{
|
|
Commands = ["FactionUnban"];
|
|
Usage = "FactionUnban";
|
|
Description = "Unbans the account of a targeted player from joining factions.";
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void Execute(CommandEventArgs e, object obj)
|
|
{
|
|
var mob = (Mobile)obj;
|
|
|
|
switch (m_KickType)
|
|
{
|
|
case FactionKickType.Kick:
|
|
{
|
|
var pl = PlayerState.Find(mob);
|
|
|
|
if (pl != null)
|
|
{
|
|
pl.Faction.RemoveMember(mob);
|
|
mob.SendMessage("You have been kicked from your faction.");
|
|
AddResponse("They have been kicked from their faction.");
|
|
}
|
|
else
|
|
{
|
|
LogFailure("They are not in a faction.");
|
|
}
|
|
|
|
break;
|
|
}
|
|
case FactionKickType.Ban:
|
|
{
|
|
if (mob.Account is Account acct)
|
|
{
|
|
if (acct.GetTag("FactionBanned") == null)
|
|
{
|
|
acct.SetTag("FactionBanned", "true");
|
|
AddResponse("The account has been banned from joining factions.");
|
|
}
|
|
else
|
|
{
|
|
AddResponse("The account is already banned from joining factions.");
|
|
}
|
|
|
|
for (var i = 0; i < acct.Length; ++i)
|
|
{
|
|
mob = acct[i];
|
|
|
|
if (mob != null)
|
|
{
|
|
var pl = PlayerState.Find(mob);
|
|
|
|
if (pl != null)
|
|
{
|
|
pl.Faction.RemoveMember(mob);
|
|
mob.SendMessage("You have been kicked from your faction.");
|
|
AddResponse("They have been kicked from their faction.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
LogFailure("They have no assigned account.");
|
|
}
|
|
|
|
break;
|
|
}
|
|
case FactionKickType.Unban:
|
|
{
|
|
if (mob.Account is Account acct)
|
|
{
|
|
if (acct.GetTag("FactionBanned") == null)
|
|
{
|
|
AddResponse("The account is not already banned from joining factions.");
|
|
}
|
|
else
|
|
{
|
|
acct.RemoveTag("FactionBanned");
|
|
AddResponse("The account may now freely join factions.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
LogFailure("They have no assigned account.");
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|