fix: Overhauls champion titles & codegen champion system (#1430)

## MAJOR CHANGE

Added a champion title system to facilitate the existing champion titles. This should make it easier to extend or create other related game content. Champion titles will be saved in a folder called _ChampionTitles_.

### Motivation

The motivation to refactor was two-folder, but mostly related to performance in two ways.
First, every player had a ChampionTitleInfo object with an array of ChamptionTitleInfo. We want to eliminate the need for this information unless a player actually uses it. This should save a considerable amount of memory.

Second, to facilitate the atrophy mechanic, the champion titles would run atrophy post-world save, adding to the time that the server is frozen. Eliminating this post-world save side effect unlocks our ability to further optimize the world save process since there are no direct side effects.


### Bugs fixed

- [X] Fixed titles getting cut off on the paperdoll
- [X] Fixed champion title not displaying overhead (OPL)

### Screenshots
<img width="216" alt="image" src="https://github.com/modernuo/ModernUO/assets/3953314/8916f895-8d68-4fb0-892e-108a0c43be90">
This commit is contained in:
Kamron Batman 2023-07-27 21:44:06 -07:00 committed by GitHub
parent 4dddc8d3f1
commit 51ecee6caa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 3028 additions and 2785 deletions

View file

@ -40,9 +40,6 @@ public abstract class BaseGuild : ISerializable
public abstract GuildType Type { get; set; }
public abstract bool Disbanded { get; }
public abstract bool ShouldExecuteAfterSerialize { get; }
public abstract void AfterSerialize();
public abstract void Delete();
public bool Deleted => Disbanded;

View file

@ -112,10 +112,4 @@ public class Entity : IEntity
public void Serialize(IGenericWriter writer)
{
}
public bool ShouldExecuteAfterSerialize => false;
public void AfterSerialize()
{
}
}

View file

@ -1041,12 +1041,6 @@ public class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropertyListEnt
}
}
public virtual bool ShouldExecuteAfterSerialize => false;
public virtual void AfterSerialize()
{
}
public void MoveToWorld(WorldLocation worldLocation)
{
MoveToWorld(worldLocation.Location, worldLocation.Map);

View file

@ -126,8 +126,6 @@ public static class Core
private static long _tickCount;
// Don't access this from other threads than the game thread.
// Persistence accesses this via AfterSerialize or Serialize in other threads, but the value is set and won't change
// since the game loop is frozen at that moment.
private static DateTime _now;
// For Unix Stopwatch.Frequency is normalized to 1ns

View file

@ -2380,12 +2380,6 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
writer.Write((byte)m_IntLock);
}
public virtual bool ShouldExecuteAfterSerialize => false;
public virtual void AfterSerialize()
{
}
public bool Deleted { get; private set; }
public virtual void Delete()

View file

@ -25,7 +25,6 @@ public static class GenericPersistence
string name,
Action<IGenericWriter> serializer,
Action<IGenericReader> deserializer,
Action afterSerialize = null,
int priority = Persistence.DefaultPriority
)
{
@ -49,6 +48,6 @@ public static class GenericPersistence
void Deserialize(string savePath, Dictionary<ulong, string> typesDb) =>
AdhocPersistence.Deserialize(Path.Combine(savePath, name, $"{name}.bin"), deserializer);
Persistence.Register(name, Serialize, WriteSnapshot, Deserialize, afterSerialize, priority);
Persistence.Register(name, Serialize, WriteSnapshot, Deserialize, priority);
}
}

View file

@ -34,12 +34,6 @@ public interface ISerializable
void Deserialize(IGenericReader reader);
void Serialize(IGenericWriter writer);
// Determines if AfterSerialize should execute. This is checked on a worker thread.
bool ShouldExecuteAfterSerialize { get; }
// Executes after serialization if ShouldExecuteAfterSerialize is true. This is run on the game thread synchronously.
void AfterSerialize();
bool Deleted { get; }
void Delete();
@ -60,13 +54,6 @@ public interface ISerializable
{
SaveBuffer ??= new BufferWriter(true, types);
// Queue for post serialization if this entity has it enabled
// This will run AfterSerialize in the main game thread after the world is done saving
if (ShouldExecuteAfterSerialize)
{
World.EnqueueAfterSerialization(this);
}
// Clean, don't bother serializing
if (SavePosition > -1)
{

View file

@ -32,7 +32,6 @@ public static class Persistence
Action serializer,
Action<string> snapshotWriter,
Action<string, Dictionary<ulong, string>> deserializer,
Action afterSerialize = null,
int priority = DefaultPriority
)
{
@ -42,7 +41,6 @@ public static class Persistence
Name = name,
Priority = priority,
Serialize = serializer,
AfterSerialize = afterSerialize,
WriteSnapshot = snapshotWriter,
Deserialize = deserializer
}
@ -91,12 +89,6 @@ public static class Persistence
public static void Serialize()
{
Parallel.ForEach(_registry, entry => entry.Serialize());
// Synchronously run the AfterSerialize on the main game thread
foreach (var entry in _registry)
{
entry.AfterSerialize?.Invoke();
}
}
public static void WriteSnapshot(string path, ConcurrentQueue<Type> types)
@ -141,7 +133,6 @@ public static class Persistence
// Serializes to memory buffers and run in parallel
public Action Serialize { get; init; }
public Action AfterSerialize { get; init; }
public Action<string> WriteSnapshot { get; init; }
public Action<string, Dictionary<ulong, string>> Deserialize { get; init; }
}

View file

@ -44,7 +44,6 @@ public static class World
private static Dictionary<Serial, IEntity> _pendingAdd = new();
private static Dictionary<Serial, IEntity> _pendingDelete = new();
private static ConcurrentQueue<Item> _decayQueue = new();
private static ConcurrentQueue<ISerializable> _afterSerializeEntities = new();
private static string _tempSavePath; // Path to the temporary folder for the save
private static bool _enableSaveStats;
@ -166,7 +165,7 @@ public static class World
_enableSaveStats = ServerConfiguration.GetOrUpdateSetting("world.enableSaveStats", false);
// Mobiles & Items
Persistence.Register("Mobiles & Items", SaveEntities, WriteEntities, LoadEntities, AfterSerialize, 1);
Persistence.Register("Mobiles & Items", SaveEntities, WriteEntities, LoadEntities, 1);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -187,9 +186,6 @@ public static class World
_decayQueue.Enqueue(item);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void EnqueueAfterSerialization(ISerializable entity) => _afterSerializeEntities.Enqueue(entity);
public static void Broadcast(int hue, bool ascii, string text)
{
var length = OutgoingMessagePackets.GetMaxMessageLength(text);
@ -519,14 +515,6 @@ public static class World
*/
public static ConcurrentQueue<Type> SerializedTypes { get; } = new();
private static void AfterSerialize()
{
while (_afterSerializeEntities.TryDequeue(out var entity))
{
entity.AfterSerialize();
}
}
private static void SaveEntities()
{
_serializationStart = DateTime.UtcNow;

View file

@ -333,12 +333,6 @@ namespace Server.Accounting
}
}
public bool ShouldExecuteAfterSerialize => false;
public void AfterSerialize()
{
}
/// <summary>
/// Deletes the account, all characters of the account, and all houses of those characters
/// </summary>

View file

@ -13,56 +13,31 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using ModernUO.Serialization;
using Server.Items;
namespace Server.Engines.CannedEvil
namespace Server.Engines.CannedEvil;
[SerializationGenerator(0, false)]
public partial class ChampionAltar : PentagramAddon
{
public class ChampionAltar : PentagramAddon
[SerializableField(0, setter: "private")]
private ChampionSpawn _spawn;
public ChampionAltar(ChampionSpawn spawn) => _spawn = spawn;
public override void OnAfterDelete()
{
public ChampionSpawn Spawn { get; private set; }
base.OnAfterDelete();
Spawn?.Delete();
}
public ChampionAltar(ChampionSpawn spawn) => Spawn = spawn;
public override void OnAfterDelete()
[AfterDeserialization(false)]
private void AfterDeserialization()
{
if (Spawn == null)
{
base.OnAfterDelete();
Spawn?.Delete();
}
public ChampionAltar(Serial serial) : base(serial)
{
}
public override void Serialize(IGenericWriter writer)
{
base.Serialize(writer);
writer.Write(0); // version
writer.Write(Spawn);
}
public override void Deserialize(IGenericReader reader)
{
base.Deserialize(reader);
var version = reader.ReadInt();
switch (version)
{
case 0:
{
Spawn = reader.ReadEntity<ChampionSpawn>();
if (Spawn == null)
{
Delete();
}
break;
}
}
Delete();
}
}
}

View file

@ -13,95 +13,70 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using ModernUO.Serialization;
using Server.Items;
namespace Server.Engines.CannedEvil
namespace Server.Engines.CannedEvil;
[SerializationGenerator(0, false)]
public partial class ChampionPlatform : BaseAddon
{
public class ChampionPlatform : BaseAddon
[SerializableField(0, setter: "private")]
private ChampionSpawn _spawn;
public ChampionPlatform(ChampionSpawn spawn)
{
public ChampionSpawn Spawn { get; private set; }
_spawn = spawn;
public ChampionPlatform(ChampionSpawn spawn)
for (var x = -2; x <= 2; ++x)
{
Spawn = spawn;
for (var x = -2; x <= 2; ++x)
for (var y = -2; y <= 2; ++y)
{
for (var y = -2; y <= 2; ++y)
{
AddComponent(0x750, x, y, -5);
}
AddComponent(0x750, x, y, -5);
}
}
for (var x = -1; x <= 1; ++x)
for (var x = -1; x <= 1; ++x)
{
for (var y = -1; y <= 1; ++y)
{
for (var y = -1; y <= 1; ++y)
{
AddComponent(0x750, x, y, 0);
}
AddComponent(0x750, x, y, 0);
}
for (var i = -1; i <= 1; ++i)
{
AddComponent(0x751, i, 2, 0);
AddComponent(0x752, 2, i, 0);
AddComponent(0x753, i, -2, 0);
AddComponent(0x754, -2, i, 0);
}
AddComponent(0x759, -2, -2, 0);
AddComponent(0x75A, 2, 2, 0);
AddComponent(0x75B, -2, 2, 0);
AddComponent(0x75C, 2, -2, 0);
}
public void AddComponent(int id, int x, int y, int z)
for (var i = -1; i <= 1; ++i)
{
AddonComponent ac = new AddonComponent(id) { Hue = 0x497 };
AddComponent(ac, x, y, z);
AddComponent(0x751, i, 2, 0);
AddComponent(0x752, 2, i, 0);
AddComponent(0x753, i, -2, 0);
AddComponent(0x754, -2, i, 0);
}
public override void OnAfterDelete()
AddComponent(0x759, -2, -2, 0);
AddComponent(0x75A, 2, 2, 0);
AddComponent(0x75B, -2, 2, 0);
AddComponent(0x75C, 2, -2, 0);
}
public void AddComponent(int id, int x, int y, int z)
{
AddonComponent ac = new AddonComponent(id) { Hue = 0x497 };
AddComponent(ac, x, y, z);
}
public override void OnAfterDelete()
{
base.OnAfterDelete();
Spawn?.Delete();
}
[AfterDeserialization(false)]
private void AfterDeserialization()
{
if (Spawn == null)
{
base.OnAfterDelete();
Spawn?.Delete();
}
public ChampionPlatform(Serial serial) : base(serial)
{
}
public override void Serialize(IGenericWriter writer)
{
base.Serialize(writer);
writer.Write(0); // version
writer.Write(Spawn);
}
public override void Deserialize(IGenericReader reader)
{
base.Deserialize(reader);
var version = reader.ReadInt();
switch (version)
{
case 0:
{
Spawn = reader.ReadEntity<ChampionSpawn>();
if (Spawn == null)
{
Delete();
}
break;
}
}
Delete();
}
}
}

View file

@ -13,86 +13,41 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using ModernUO.Serialization;
using Server.Engines.CannedEvil;
namespace Server.Items
namespace Server.Items;
[SerializationGenerator(2, false)]
public partial class ChampionSkull : Item
{
public class ChampionSkull : Item
[InvalidateProperties]
[SerializableField(0)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private ChampionSkullType _type;
public override int LabelNumber => 1049479 + (int)_type;
[Constructible]
public ChampionSkull(ChampionSkullType type) : base(0x1AE1)
{
private ChampionSkullType m_Type;
_type = type;
LootType = LootType.Cursed;
[CommandProperty(AccessLevel.GameMaster)]
public ChampionSkullType Type
// TODO: All hue values
Hue = type switch
{
get => m_Type;
set
{
m_Type = value;
InvalidateProperties();
}
}
ChampionSkullType.Power => 0x159,
ChampionSkullType.Venom => 0x172,
ChampionSkullType.Greed => 0x1EE,
ChampionSkullType.Death => 0x025,
ChampionSkullType.Pain => 0x035,
_ => Hue
};
}
public override int LabelNumber => 1049479 + (int)m_Type;
[Constructible]
public ChampionSkull(ChampionSkullType type) : base(0x1AE1)
{
m_Type = type;
LootType = LootType.Cursed;
// TODO: All hue values
Hue = type switch
{
ChampionSkullType.Power => 0x159,
ChampionSkullType.Venom => 0x172,
ChampionSkullType.Greed => 0x1EE,
ChampionSkullType.Death => 0x025,
ChampionSkullType.Pain => 0x035,
_ => Hue
};
}
public ChampionSkull(Serial serial) : base(serial)
{
}
public override void Serialize(IGenericWriter writer)
{
base.Serialize(writer);
writer.Write(1); // version
writer.Write((int)m_Type);
}
public override void Deserialize(IGenericReader reader)
{
base.Deserialize(reader);
var version = reader.ReadInt();
switch (version)
{
case 1:
case 0:
{
m_Type = (ChampionSkullType)reader.ReadInt();
break;
}
}
if (version == 0)
{
if (LootType != LootType.Cursed)
{
LootType = LootType.Cursed;
}
if (Insured)
{
Insured = false;
}
}
}
private void Deserialize(IGenericReader reader, int version)
{
_type = (ChampionSkullType)reader.ReadInt();
}
}

View file

@ -13,194 +13,156 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using ModernUO.Serialization;
using Server.Items;
using Server.Targeting;
using Server.Mobiles;
namespace Server.Engines.CannedEvil
namespace Server.Engines.CannedEvil;
[SerializationGenerator(1, false)]
public partial class ChampionSkullBrazier : AddonComponent
{
public class ChampionSkullBrazier : AddonComponent
[InvalidateProperties]
[SerializableField(0)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private ChampionSkullType _type;
[InvalidateProperties]
[SerializableField(1)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private ChampionSkullPlatform _platform;
[SerializableProperty(2)]
[CommandProperty(AccessLevel.GameMaster)]
public Item Skull
{
private ChampionSkullType m_Type;
private Item m_Skull;
[CommandProperty(AccessLevel.GameMaster)]
public ChampionSkullPlatform Platform { get; private set; }
[CommandProperty(AccessLevel.GameMaster)]
public ChampionSkullType Type
get => _skull;
set
{
get => m_Type;
set
{
m_Type = value;
InvalidateProperties();
}
_skull = value;
this.MarkDirty();
_platform?.Validate();
}
}
public override int LabelNumber => 1049489 + (int)_type;
public ChampionSkullBrazier(ChampionSkullPlatform platform, ChampionSkullType type) : base(0x19BB)
{
Hue = 0x455;
Light = LightType.Circle300;
_platform = platform;
_type = type;
}
public override void OnDoubleClick(Mobile from)
{
_platform?.Validate();
BeginSacrifice(from);
}
public void BeginSacrifice(Mobile from)
{
if (Deleted)
{
return;
}
[CommandProperty(AccessLevel.GameMaster)]
public Item Skull
if (_skull is { Deleted: true })
{
get => m_Skull;
set
{
m_Skull = value;
Platform?.Validate();
}
Skull = null;
}
public override int LabelNumber => 1049489 + (int)m_Type;
public ChampionSkullBrazier(ChampionSkullPlatform platform, ChampionSkullType type) : base(0x19BB)
if (from.Map != Map || !from.InRange(GetWorldLocation(), 3))
{
Hue = 0x455;
Light = LightType.Circle300;
from.SendLocalizedMessage(500446); // That is too far away.
}
else if (!Harrower.CanSpawn)
{
from.SendMessage("The harrower has already been spawned.");
}
else if (_skull == null)
{
from.SendLocalizedMessage(1049485); // What would you like to sacrifice?
from.Target = new SacrificeTarget(this);
}
else
{
SendLocalizedMessageTo(from, 1049487); // I already have my champions awakening skull!
}
}
Platform = platform;
m_Type = type;
public void EndSacrifice(Mobile from, ChampionSkull skull)
{
if (Deleted)
{
return;
}
public ChampionSkullBrazier(Serial serial) : base(serial)
if (_skull is { Deleted: true })
{
Skull = null;
}
public override void OnDoubleClick(Mobile from)
if (from.Map != Map || !from.InRange(GetWorldLocation(), 3))
{
Platform?.Validate();
BeginSacrifice(from);
from.SendLocalizedMessage(500446); // That is too far away.
}
public void BeginSacrifice(Mobile from)
else if (!Harrower.CanSpawn)
{
if (Deleted)
{
return;
}
if (m_Skull is { Deleted: true })
{
Skull = null;
}
if (from.Map != Map || !from.InRange(GetWorldLocation(), 3))
{
from.SendLocalizedMessage(500446); // That is too far away.
}
else if (!Harrower.CanSpawn)
{
from.SendMessage("The harrower has already been spawned.");
}
else if (m_Skull == null)
{
from.SendLocalizedMessage(1049485); // What would you like to sacrifice?
from.Target = new SacrificeTarget(this);
}
else
{
SendLocalizedMessageTo(from, 1049487); // I already have my champions awakening skull!
}
from.SendMessage("The harrower has already been spawned.");
}
public void EndSacrifice(Mobile from, ChampionSkull skull)
else if (skull == null)
{
if (Deleted)
{
return;
}
if (m_Skull is { Deleted: true })
{
Skull = null;
}
if (from.Map != Map || !from.InRange(GetWorldLocation(), 3))
{
from.SendLocalizedMessage(500446); // That is too far away.
}
else if (!Harrower.CanSpawn)
{
from.SendMessage("The harrower has already been spawned.");
}
else if (skull == null)
{
SendLocalizedMessageTo(from, 1049488); // That is not my champions awakening skull!
}
else if (m_Skull != null)
{
SendLocalizedMessageTo(from, 1049487); // I already have my champions awakening skull!
}
else if (!skull.IsChildOf(from.Backpack))
{
from.SendLocalizedMessage(1049486); // You can only sacrifice items that are in your backpack!
}
else if (skull.Type == Type)
{
skull.Movable = false;
skull.MoveToWorld(GetWorldTop(), Map);
Skull = skull;
}
else
{
SendLocalizedMessageTo(from, 1049488); // That is not my champions awakening skull!
}
SendLocalizedMessageTo(from, 1049488); // That is not my champions awakening skull!
}
private class SacrificeTarget : Target
else if (_skull != null)
{
private readonly ChampionSkullBrazier m_Brazier;
public SacrificeTarget(ChampionSkullBrazier brazier) : base(12, false, TargetFlags.None) =>
m_Brazier = brazier;
protected override void OnTarget(Mobile from, object targeted) =>
m_Brazier.EndSacrifice(from, targeted as ChampionSkull);
SendLocalizedMessageTo(from, 1049487); // I already have my champions awakening skull!
}
public override void Serialize(IGenericWriter writer)
else if (!skull.IsChildOf(from.Backpack))
{
base.Serialize(writer);
writer.Write(0); // version
writer.Write((int)m_Type);
writer.Write(Platform);
writer.Write(m_Skull);
from.SendLocalizedMessage(1049486); // You can only sacrifice items that are in your backpack!
}
public override void Deserialize(IGenericReader reader)
else if (skull.Type == Type)
{
base.Deserialize(reader);
skull.Movable = false;
skull.MoveToWorld(GetWorldTop(), Map);
var version = reader.ReadInt();
Skull = skull;
}
else
{
SendLocalizedMessageTo(from, 1049488); // That is not my champions awakening skull!
}
}
switch (version)
{
case 0:
{
m_Type = (ChampionSkullType)reader.ReadInt();
Platform = reader.ReadEntity<ChampionSkullPlatform>();
m_Skull = reader.ReadEntity<Item>();
private class SacrificeTarget : Target
{
private readonly ChampionSkullBrazier _brazier;
if (Platform == null)
{
Delete();
}
public SacrificeTarget(ChampionSkullBrazier brazier) : base(12, false, TargetFlags.None) =>
_brazier = brazier;
break;
}
}
protected override void OnTarget(Mobile from, object targeted) =>
_brazier.EndSacrifice(from, targeted as ChampionSkull);
}
if (Hue == 0x497)
{
Hue = 0x455;
}
private void Deserialize(IGenericReader reader, int version)
{
_type = (ChampionSkullType)reader.ReadInt();
_platform = reader.ReadEntity<ChampionSkullPlatform>();
_skull = reader.ReadEntity<Item>();
}
if (Light != LightType.Circle300)
{
Light = LightType.Circle300;
}
[AfterDeserialization(false)]
private void AfterDeserialization()
{
if (_platform == null)
{
Delete();
}
}
}

View file

@ -13,127 +13,104 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using ModernUO.Serialization;
using Server.Items;
using Server.Mobiles;
namespace Server.Engines.CannedEvil
namespace Server.Engines.CannedEvil;
[SerializationGenerator(0, false)]
public partial class ChampionSkullPlatform : BaseAddon
{
public class ChampionSkullPlatform : BaseAddon
[SerializableField(0)]
private ChampionSkullBrazier _power;
[SerializableField(1)]
private ChampionSkullBrazier _enlightenment;
[SerializableField(2)]
private ChampionSkullBrazier _venom;
[SerializableField(3)]
private ChampionSkullBrazier _pain;
[SerializableField(4)]
private ChampionSkullBrazier _greed;
[SerializableField(5)]
private ChampionSkullBrazier _death;
[Constructible]
public ChampionSkullPlatform()
{
private ChampionSkullBrazier m_Power, m_Enlightenment, m_Venom, m_Pain, m_Greed, m_Death;
AddComponent(new AddonComponent(0x71A), -1, -1, -1);
AddComponent(new AddonComponent(0x709), 0, -1, -1);
AddComponent(new AddonComponent(0x709), 1, -1, -1);
AddComponent(new AddonComponent(0x709), -1, 0, -1);
AddComponent(new AddonComponent(0x709), 0, 0, -1);
AddComponent(new AddonComponent(0x709), 1, 0, -1);
AddComponent(new AddonComponent(0x709), -1, 1, -1);
AddComponent(new AddonComponent(0x709), 0, 1, -1);
AddComponent(new AddonComponent(0x71B), 1, 1, -1);
[Constructible]
public ChampionSkullPlatform()
AddComponent(new AddonComponent(0x50F), 0, -1, 4);
AddComponent(_power = new ChampionSkullBrazier(this, ChampionSkullType.Power), 0, -1, 5);
AddComponent(new AddonComponent(0x50F), 1, -1, 4);
AddComponent(_enlightenment = new ChampionSkullBrazier(this, ChampionSkullType.Enlightenment), 1, -1, 5);
AddComponent(new AddonComponent(0x50F), -1, 0, 4);
AddComponent(_venom = new ChampionSkullBrazier(this, ChampionSkullType.Venom), -1, 0, 5);
AddComponent(new AddonComponent(0x50F), 1, 0, 4);
AddComponent(_pain = new ChampionSkullBrazier(this, ChampionSkullType.Pain), 1, 0, 5);
AddComponent(new AddonComponent(0x50F), -1, 1, 4);
AddComponent(_greed = new ChampionSkullBrazier(this, ChampionSkullType.Greed), -1, 1, 5);
AddComponent(new AddonComponent(0x50F), 0, 1, 4);
AddComponent(_death = new ChampionSkullBrazier(this, ChampionSkullType.Death), 0, 1, 5);
AddonComponent comp = new LocalizedAddonComponent(0x20D2, 1049495) { Hue = 0x482 };
AddComponent(comp, 0, 0, 5);
comp = new LocalizedAddonComponent(0x0BCF, 1049496) { Hue = 0x482 };
AddComponent(comp, 0, 2, -7);
comp = new LocalizedAddonComponent(0x0BD0, 1049497) { Hue = 0x482 };
AddComponent(comp, 2, 0, -7);
}
public void Validate()
{
if (Validate(_power) && Validate(_enlightenment) && Validate(_venom) && Validate(_pain) &&
Validate(_greed) && Validate(_death))
{
AddComponent(new AddonComponent(0x71A), -1, -1, -1);
AddComponent(new AddonComponent(0x709), 0, -1, -1);
AddComponent(new AddonComponent(0x709), 1, -1, -1);
AddComponent(new AddonComponent(0x709), -1, 0, -1);
AddComponent(new AddonComponent(0x709), 0, 0, -1);
AddComponent(new AddonComponent(0x709), 1, 0, -1);
AddComponent(new AddonComponent(0x709), -1, 1, -1);
AddComponent(new AddonComponent(0x709), 0, 1, -1);
AddComponent(new AddonComponent(0x71B), 1, 1, -1);
Mobile harrower = Harrower.Spawn(new Point3D(X, Y, Z + 6), Map);
AddComponent(new AddonComponent(0x50F), 0, -1, 4);
AddComponent(m_Power = new ChampionSkullBrazier(this, ChampionSkullType.Power), 0, -1, 5);
AddComponent(new AddonComponent(0x50F), 1, -1, 4);
AddComponent(m_Enlightenment = new ChampionSkullBrazier(this, ChampionSkullType.Enlightenment), 1, -1, 5);
AddComponent(new AddonComponent(0x50F), -1, 0, 4);
AddComponent(m_Venom = new ChampionSkullBrazier(this, ChampionSkullType.Venom), -1, 0, 5);
AddComponent(new AddonComponent(0x50F), 1, 0, 4);
AddComponent(m_Pain = new ChampionSkullBrazier(this, ChampionSkullType.Pain), 1, 0, 5);
AddComponent(new AddonComponent(0x50F), -1, 1, 4);
AddComponent(m_Greed = new ChampionSkullBrazier(this, ChampionSkullType.Greed), -1, 1, 5);
AddComponent(new AddonComponent(0x50F), 0, 1, 4);
AddComponent(m_Death = new ChampionSkullBrazier(this, ChampionSkullType.Death), 0, 1, 5);
AddonComponent comp = new LocalizedAddonComponent(0x20D2, 1049495) { Hue = 0x482 };
AddComponent(comp, 0, 0, 5);
comp = new LocalizedAddonComponent(0x0BCF, 1049496) { Hue = 0x482 };
AddComponent(comp, 0, 2, -7);
comp = new LocalizedAddonComponent(0x0BD0, 1049497) { Hue = 0x482 };
AddComponent(comp, 2, 0, -7);
}
public void Validate()
{
if (Validate(m_Power) && Validate(m_Enlightenment) && Validate(m_Venom) && Validate(m_Pain) &&
Validate(m_Greed) && Validate(m_Death))
if (harrower == null)
{
Mobile harrower = Harrower.Spawn(new Point3D(X, Y, Z + 6), Map);
if (harrower == null)
{
return;
}
Clear(m_Power);
Clear(m_Enlightenment);
Clear(m_Venom);
Clear(m_Pain);
Clear(m_Greed);
Clear(m_Death);
return;
}
}
public void Clear(ChampionSkullBrazier brazier)
{
if (brazier != null)
{
Effects.SendBoltEffect(brazier);
brazier.Skull?.Delete();
}
}
public bool Validate(ChampionSkullBrazier brazier) => brazier is { Skull: { Deleted: false } };
public ChampionSkullPlatform(Serial serial) : base(serial)
{
}
public override void Serialize(IGenericWriter writer)
{
base.Serialize(writer);
writer.Write(0); // version
writer.Write(m_Power);
writer.Write(m_Enlightenment);
writer.Write(m_Venom);
writer.Write(m_Pain);
writer.Write(m_Greed);
writer.Write(m_Death);
}
public override void Deserialize(IGenericReader reader)
{
base.Deserialize(reader);
var version = reader.ReadInt();
switch (version)
{
case 0:
{
m_Power = reader.ReadEntity<ChampionSkullBrazier>();
m_Enlightenment = reader.ReadEntity<ChampionSkullBrazier>();
m_Venom = reader.ReadEntity<ChampionSkullBrazier>();
m_Pain = reader.ReadEntity<ChampionSkullBrazier>();
m_Greed = reader.ReadEntity<ChampionSkullBrazier>();
m_Death = reader.ReadEntity<ChampionSkullBrazier>();
break;
}
}
Clear(_power);
Clear(_enlightenment);
Clear(_venom);
Clear(_pain);
Clear(_greed);
Clear(_death);
}
}
public static void Clear(ChampionSkullBrazier brazier)
{
if (brazier != null)
{
Effects.SendBoltEffect(brazier);
brazier.Skull?.Delete();
}
}
public static bool Validate(ChampionSkullBrazier brazier) => brazier is { Skull: { Deleted: false } };
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,150 @@
using System;
using Server.Mobiles;
namespace Server.Engines.CannedEvil;
public enum ChampionSpawnType
{
Abyss,
Arachnid,
ColdBlood,
ForestLord,
VerminHorde,
UnholyTerror,
SleepingDragon,
Glade,
Corrupt,
// Unliving,
// Pit,
// DragonTurtle,
// Khaldun
}
public class ChampionSpawnInfo
{
public ChampionSpawnType Type { get; }
public Type Champion { get; }
public Type[][] SpawnTypes { get; }
public TextDefinition[] LevelNames { get; }
public ChampionSpawnInfo(ChampionSpawnType type, Type champion, TextDefinition[] levelNames, Type[][] spawnTypes)
{
Type = type;
Champion = champion;
LevelNames = levelNames;
SpawnTypes = spawnTypes;
}
// After updating this table, make sure to update ChampionTitleContext.
public static ChampionSpawnInfo[] Table { get; } = {
new(ChampionSpawnType.Abyss, typeof(Semidar), new TextDefinition[]{ 1113118, 1113107, 1113096 }, new[]
{
new[]{ typeof(GreaterMongbat), typeof(Imp) }, // Level 1
new[]{ typeof(Gargoyle), typeof(Harpy) }, // Level 2
new[]{ typeof(FireGargoyle), typeof(StoneGargoyle) }, // Level 3
new[]{ typeof(Daemon), typeof(Succubus) } // Level 4
}),
new(ChampionSpawnType.Arachnid, typeof(Mephitis), new TextDefinition[]{ 1113117, 1113106, 1113095 }, new[]
{
new[]{ typeof(Scorpion), typeof(GiantSpider) }, // Level 1
new[]{ typeof(TerathanDrone), typeof(TerathanWarrior) }, // Level 2
new[]{ typeof(DreadSpider), typeof(TerathanMatriarch) }, // Level 3
new[]{ typeof(PoisonElemental), typeof(TerathanAvenger) } // Level 4
}),
new(ChampionSpawnType.ColdBlood, typeof(Rikktor), new TextDefinition[]{ 1113115, 1113104, 1113093 }, new[]
{
new[]{ typeof(Lizardman), typeof(GiantSerpent) }, // Level 1
new[]{ typeof(LavaLizard), typeof(OphidianWarrior) }, // Level 2
new[]{ typeof(Drake), typeof(OphidianArchmage) }, // Level 3
new[]{ typeof(Dragon), typeof(OphidianKnight) } // Level 4
}),
new(ChampionSpawnType.ForestLord, typeof(LordOaks), new TextDefinition[]{ 1113116, 1113105, 1113094 }, new[]
{
new[]{ typeof(Pixie), typeof(ShadowWisp) }, // Level 1
new[]{ typeof(Kirin), typeof(Wisp) }, // Level 2
new[]{ typeof(Centaur), typeof(Unicorn) }, // Level 3
new[]{ typeof(EtherealWarrior), typeof(SerpentineDragon) } // Level 4
}),
new(ChampionSpawnType.VerminHorde, typeof(Barracoon), new TextDefinition[]{ 1113119, 1113108, 1113097 }, new[]
{
new[]{ typeof(GiantRat), typeof(Slime) }, // Level 1
new[]{ typeof(DireWolf), typeof(Ratman) }, // Level 2
new[]{ typeof(HellHound), typeof(RatmanMage) }, // Level 3
new[]{ typeof(RatmanArcher), typeof(SilverSerpent) } // Level 4
}),
new(ChampionSpawnType.UnholyTerror, typeof(Neira), new TextDefinition[]{ 1113120, 1113109, 1113098 }, new[]
{
Core.AOS ? // Level 1
new[]{ typeof(Bogle), typeof(Ghoul), typeof(Shade), typeof(Spectre), typeof(Wraith) }
: new[]{ typeof(Ghoul), typeof(Shade), typeof(Spectre), typeof(Wraith) },
new[]{ typeof(BoneMagi), typeof(Mummy), typeof(SkeletalMage) }, // Level 2
new[]{ typeof(BoneKnight), typeof(Lich), typeof(SkeletalKnight) }, // Level 3
new[]{ typeof(LichLord), typeof(RottingCorpse) } // Level 4
}),
new(ChampionSpawnType.SleepingDragon, typeof(Serado), new TextDefinition[]{ 1113121, 1113110, 1113099 }, new[]
{
new[]{ typeof(DeathwatchBeetleHatchling), typeof(Lizardman) }, // Level 1
new[]{ typeof(DeathwatchBeetle), typeof(Kappa) }, // Level 2
new[]{ typeof(LesserHiryu), typeof(RevenantLion) }, // Level 3
new[]{ typeof(Hiryu), typeof(Oni) } // Level 4
}),
new(ChampionSpawnType.Glade, typeof(Twaulo), new TextDefinition[]{ 1113123, 1113112, 1113101 }, new[]
{
new[]{ typeof(Pixie), typeof(ShadowWisp) }, // Level 1
new[]{ typeof(Centaur), typeof(MLDryad) }, // Level 2
new[]{ typeof(Satyr), typeof(CuSidhe) }, // Level 3
new[]{ typeof(FeralTreefellow), typeof(RagingGrizzlyBear) } // Level 4
}),
new(ChampionSpawnType.Corrupt, typeof(Ilhenir), new TextDefinition[]{ 1113122, 1113111, 1113100 }, new[]
{
new[]{ typeof(PlagueSpawn), typeof(Bogling) }, // Level 1
new[]{ typeof(PlagueBeast), typeof(BogThing) }, // Level 2
new[]{ typeof(PlagueBeastLord), typeof(InterredGrizzle) }, // Level 3
new[]{ typeof(FetidEssence), typeof(PestilentBandage) } // Level 4
}),
// new(ChampionSpawnType.Unliving, typeof(PrimevalLich), new TextDefinition[]{ 1113124, 1113113, 1113102 }, new[]
// {
// new[]{ typeof(GoreFiend), typeof(VampireBat) },
// new[]{ typeof(FleshGolem), typeof(DarkWisp) },
// new[]{ typeof(UndeadGargoyle), typeof(Wight) },
// new[]{ typeof(SkeletalDrake), typeof(DreamWraith) }
// }),
// new(ChampionSpawnType.Pit, typeof(AbyssalInfernal), new TextDefinition[]{ 1113125, 1113114, 1113103 }, new[]
// {
// new[]{ typeof(HordeMinion), typeof(ChaosDaemon) },
// new[]{ typeof(StoneHarpy), typeof(ArcaneDaemon) },
// new[]{ typeof(PitFiend), typeof(Moloch) },
// new[]{ typeof(ArchDaemon), typeof(AbyssalAbomination) }
// }),
// new(ChampionSpawnType.Valley, typeof(DragonTurtle), new TextDefinition[]{ "Explorer", "Huntsman", "Msafiri" }, new[]
// {
// new[]{ typeof(MyrmidexDrone), typeof(MyrmidexLarvae) },
// new[]{ typeof(SilverbackGorilla), typeof(WildTiger) },
// new[]{ typeof(GreaterPhoenix ), typeof(Infernus) },
// new[]{ typeof(Dimetrosaur), typeof(Allosaurus) }
// }),
// new(ChampionSpawnType.Khaldun, typeof(KhalAnkur), new TextDefinition[]{ "Banisher", "Enforcer", "Eradicator" }, new[]
// {
// new[]{ typeof(SkelementalKnight), typeof(KhaldunBlood) },
// new[]{ typeof(SkelementalMage), typeof(Viscera) },
// new[]{ typeof(CultistAmbusher), typeof(ShadowFiend) },
// new[]{ typeof(KhalAnkurWarriors) }
// })
};
public static ChampionSpawnInfo GetInfo(ChampionSpawnType type)
{
var v = (int)type;
if (v < 0 || v >= Table.Length)
{
v = 0;
}
return Table[v];
}
}

View file

@ -1,134 +0,0 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2022 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: ChampionSpawnType.cs *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
using Server.Mobiles;
namespace Server.Engines.CannedEvil
{
public enum ChampionSpawnType
{
Abyss,
Arachnid,
ColdBlood,
ForestLord,
VerminHorde,
UnholyTerror,
SleepingDragon,
Glade,
Pestilence,
Graveyard
}
public class ChampionSpawnInfo
{
public string Name { get; }
public Type Champion { get; }
public Type[][] SpawnTypes { get; }
public string[] LevelNames { get; }
public ChampionSpawnInfo(string name, Type champion, string[] levelNames, Type[][] spawnTypes)
{
Name = name;
Champion = champion;
LevelNames = levelNames;
SpawnTypes = spawnTypes;
}
public static ChampionSpawnInfo[] Table { get; } = {
new("Abyss", typeof(Semidar), new[]{ "Foe", "Assassin", "Conqueror" }, new[]
{
new[]{ typeof(GreaterMongbat), typeof(Imp) }, // Level 1
new[]{ typeof(Gargoyle), typeof(Harpy) }, // Level 2
new[]{ typeof(FireGargoyle), typeof(StoneGargoyle) }, // Level 3
new[]{ typeof(Daemon), typeof(Succubus) } // Level 4
}),
new("Arachnid", typeof(Mephitis), new[]{ "Bane", "Killer", "Vanquisher" }, new[]
{
new[]{ typeof(Scorpion), typeof(GiantSpider) }, // Level 1
new[]{ typeof(TerathanDrone), typeof(TerathanWarrior) }, // Level 2
new[]{ typeof(DreadSpider), typeof(TerathanMatriarch) }, // Level 3
new[]{ typeof(PoisonElemental), typeof(TerathanAvenger) } // Level 4
}),
new("Cold Blood", typeof(Rikktor), new[]{ "Blight", "Slayer", "Destroyer" }, new[]
{
new[]{ typeof(Lizardman), typeof(GiantSerpent) }, // Level 1
new[]{ typeof(LavaLizard), typeof(OphidianWarrior) }, // Level 2
new[]{ typeof(Drake), typeof(OphidianArchmage) }, // Level 3
new[]{ typeof(Dragon), typeof(OphidianKnight) } // Level 4
}),
new("Forest Lord", typeof(LordOaks), new[]{ "Enemy", "Curse", "Slaughterer" }, new[]
{
new[]{ typeof(Pixie), typeof(ShadowWisp) }, // Level 1
new[]{ typeof(Kirin), typeof(Wisp) }, // Level 2
new[]{ typeof(Centaur), typeof(Unicorn) }, // Level 3
new[]{ typeof(EtherealWarrior), typeof(SerpentineDragon) } // Level 4
}),
new("Vermin Horde", typeof(Barracoon), new[]{ "Adversary", "Subjugator", "Eradicator" }, new[]
{
new[]{ typeof(GiantRat), typeof(Slime) }, // Level 1
new[]{ typeof(DireWolf), typeof(Ratman) }, // Level 2
new[]{ typeof(HellHound), typeof(RatmanMage) }, // Level 3
new[]{ typeof(RatmanArcher), typeof(SilverSerpent) } // Level 4
}),
new("Unholy Terror", typeof(Neira), new[]{ "Scourge", "Punisher", "Nemesis" }, new[]
{
Core.AOS ? // Level 1
new[]{ typeof(Bogle), typeof(Ghoul), typeof(Shade), typeof(Spectre), typeof(Wraith) }
: new[]{ typeof(Ghoul), typeof(Shade), typeof(Spectre), typeof(Wraith) },
new[]{ typeof(BoneMagi), typeof(Mummy), typeof(SkeletalMage) }, // Level 2
new[]{ typeof(BoneKnight), typeof(Lich), typeof(SkeletalKnight) }, // Level 3
new[]{ typeof(LichLord), typeof(RottingCorpse) } // Level 4
}),
new("Sleeping Dragon", typeof(Serado), new[]{ "Rival", "Challenger", "Antagonist" } , new[]
{
new[]{ typeof(DeathwatchBeetleHatchling), typeof(Lizardman) }, // Level 1
new[]{ typeof(DeathwatchBeetle), typeof(Kappa) }, // Level 2
new[]{ typeof(LesserHiryu), typeof(RevenantLion) }, // Level 3
new[]{ typeof(Hiryu), typeof(Oni) } // Level 4
}),
new("Glade", typeof(Twaulo), new[]{ "Banisher", "Enforcer", "Eradicator" } , new[]
{
new[]{ typeof(Pixie), typeof(ShadowWisp) }, // Level 1
new[]{ typeof(Centaur), typeof(MLDryad) }, // Level 2
new[]{ typeof(Satyr), typeof(CuSidhe) }, // Level 3
new[]{ typeof(FeralTreefellow), typeof(RagingGrizzlyBear) } // Level 4
}),
new("The Corrupt", typeof(Ilhenir), new[]{ "Cleanser", "Expunger", "Depurator" } , new[]
{
new[]{ typeof(PlagueSpawn), typeof(Bogling) }, // Level 1
new[]{ typeof(PlagueBeast), typeof(BogThing) }, // Level 2
new[]{ typeof(PlagueBeastLord), typeof(InterredGrizzle) }, // Level 3
new[]{ typeof(FetidEssence), typeof(PestilentBandage) } // Level 4
}),
};
public static ChampionSpawnInfo GetInfo(ChampionSpawnType type)
{
var v = (int)type;
if (v < 0 || v >= Table.Length)
{
v = 0;
}
return Table[v];
}
}
}

View file

@ -0,0 +1,30 @@
using System;
using ModernUO.Serialization;
namespace Server.Engines.CannedEvil;
[SerializationGenerator(0)]
public partial class ChampionTitle
{
[EncodedInt]
[SerializableField(0)]
private int _value;
// TODO: Change to delta time
[SerializableField(1)]
private DateTime _lastDecay;
public bool Atrophy(int value)
{
var before = Value;
Value -= Math.Min(value, before);
if (before != Value)
{
LastDecay = Core.Now;
}
return Value > 0;
}
}

View file

@ -0,0 +1,379 @@
using System;
using System.Runtime.CompilerServices;
using ModernUO.Serialization;
using Server.Mobiles;
namespace Server.Engines.CannedEvil;
[PropertyObject]
[SerializationGenerator(1)]
public partial class ChampionTitleContext
{
private const int LossAmount = 90;
private static readonly TimeSpan LossDelay = TimeSpan.FromDays(1.0);
[SerializableField(0)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _harrower;
private PlayerMobile _player;
public PlayerMobile Player => _player;
public ChampionTitleContext(PlayerMobile player) => _player = player;
// Visible to PublicMobile for migrations
internal void Deserialize(IGenericReader reader, int version)
{
_harrower = reader.ReadEncodedInt();
var length = reader.ReadEncodedInt();
if (length == 0)
{
return;
}
// If the list/enum for ChampionSpawnInfo changes, then this has to change for migrations
for (var i = 0; i < length; i++)
{
var type = (ChampionSpawnType)i;
ref var title = ref GetTitleValueRef(type);
if (Unsafe.IsNullRef(ref title))
{
throw new NotImplementedException($"Cannot find ChampionSpawnType value {type}.");
}
title = new ChampionTitle();
title.Deserialize(reader);
}
}
[SerializableField(1)]
private ChampionTitle _abyss;
[SerializableFieldSaveFlag(1)]
private bool ShouldSerializeAbyss() => _abyss != null;
[CommandProperty(AccessLevel.GameMaster)]
public int AbyssValue
{
get => GetTitle(ChampionSpawnType.Abyss)?.Value ?? 0;
set => SetValue(ChampionSpawnType.Abyss, value);
}
[CommandProperty(AccessLevel.GameMaster)]
public DateTime AbyssLastDecay
{
get => GetTitle(ChampionSpawnType.Abyss)?.LastDecay ?? DateTime.MinValue;
set => SetLastDecay(ChampionSpawnType.Abyss, value);
}
[SerializableField(2)]
private ChampionTitle _arachnid;
[SerializableFieldSaveFlag(2)]
private bool ShouldSerializeArachnid() => _arachnid != null;
[CommandProperty(AccessLevel.GameMaster)]
public int ArachnidValue
{
get => GetTitle(ChampionSpawnType.Arachnid)?.Value ?? 0;
set => SetValue(ChampionSpawnType.Arachnid, value);
}
[CommandProperty(AccessLevel.GameMaster)]
public DateTime ArachnidLastDecay
{
get => GetTitle(ChampionSpawnType.Arachnid)?.LastDecay ?? DateTime.MinValue;
set => SetLastDecay(ChampionSpawnType.Arachnid, value);
}
[SerializableField(3)]
private ChampionTitle _coldBlood;
[SerializableFieldSaveFlag(3)]
private bool ShouldSerializeColdBlood() => _coldBlood != null;
[CommandProperty(AccessLevel.GameMaster)]
public int ColdBloodValue
{
get => GetTitle(ChampionSpawnType.ColdBlood)?.Value ?? 0;
set => SetValue(ChampionSpawnType.ColdBlood, value);
}
[CommandProperty(AccessLevel.GameMaster)]
public DateTime ColdBloodLastDecay
{
get => GetTitle(ChampionSpawnType.ColdBlood)?.LastDecay ?? DateTime.MinValue;
set => SetLastDecay(ChampionSpawnType.ColdBlood, value);
}
[SerializableField(4)]
private ChampionTitle _forestLord;
[SerializableFieldSaveFlag(4)]
private bool ShouldSerializeForestLord() => _forestLord != null;
[CommandProperty(AccessLevel.GameMaster)]
public int ForestLordValue
{
get => GetTitle(ChampionSpawnType.ForestLord)?.Value ?? 0;
set => SetValue(ChampionSpawnType.ForestLord, value);
}
[CommandProperty(AccessLevel.GameMaster)]
public DateTime ForestLordLastDecay
{
get => GetTitle(ChampionSpawnType.ForestLord)?.LastDecay ?? DateTime.MinValue;
set => SetLastDecay(ChampionSpawnType.ForestLord, value);
}
[SerializableField(5)]
private ChampionTitle _verminHorde;
[SerializableFieldSaveFlag(5)]
private bool ShouldSerializeVerminHorde() => _verminHorde != null;
[CommandProperty(AccessLevel.GameMaster)]
public int VerminHordeValue
{
get => GetTitle(ChampionSpawnType.VerminHorde)?.Value ?? 0;
set => SetValue(ChampionSpawnType.VerminHorde, value);
}
[CommandProperty(AccessLevel.GameMaster)]
public DateTime VerminHordeLastDecay
{
get => GetTitle(ChampionSpawnType.VerminHorde)?.LastDecay ?? DateTime.MinValue;
set => SetLastDecay(ChampionSpawnType.VerminHorde, value);
}
[SerializableField(6)]
private ChampionTitle _unholyTerror;
[SerializableFieldSaveFlag(6)]
private bool ShouldSerializeUnholyTerror() => _unholyTerror != null;
[CommandProperty(AccessLevel.GameMaster)]
public int UnholyTerrorValue
{
get => GetTitle(ChampionSpawnType.UnholyTerror)?.Value ?? 0;
set => SetValue(ChampionSpawnType.UnholyTerror, value);
}
[CommandProperty(AccessLevel.GameMaster)]
public DateTime UnholyTerrorLastDecay
{
get => GetTitle(ChampionSpawnType.UnholyTerror)?.LastDecay ?? DateTime.MinValue;
set => SetLastDecay(ChampionSpawnType.UnholyTerror, value);
}
[SerializableField(7)]
private ChampionTitle _sleepingDragon;
[SerializableFieldSaveFlag(7)]
private bool ShouldSerializeSleepingDragon() => _sleepingDragon != null;
[CommandProperty(AccessLevel.GameMaster)]
public int SleepingDragonValue
{
get => GetTitle(ChampionSpawnType.SleepingDragon)?.Value ?? 0;
set => SetValue(ChampionSpawnType.SleepingDragon, value);
}
[CommandProperty(AccessLevel.GameMaster)]
public DateTime SleepingDragonLastDecay
{
get => GetTitle(ChampionSpawnType.SleepingDragon)?.LastDecay ?? DateTime.MinValue;
set => SetLastDecay(ChampionSpawnType.SleepingDragon, value);
}
[SerializableField(8)]
private ChampionTitle _corrupt;
[SerializableFieldSaveFlag(8)]
private bool ShouldSerializeCorrupt() => _corrupt != null;
[CommandProperty(AccessLevel.GameMaster)]
public int CorruptValue
{
get => GetTitle(ChampionSpawnType.Corrupt)?.Value ?? 0;
set => SetValue(ChampionSpawnType.Corrupt, value);
}
[CommandProperty(AccessLevel.GameMaster)]
public DateTime CorruptLastDecay
{
get => GetTitle(ChampionSpawnType.Corrupt)?.LastDecay ?? DateTime.MinValue;
set => SetLastDecay(ChampionSpawnType.Corrupt, value);
}
[SerializableField(9)]
private ChampionTitle _glade;
[SerializableFieldSaveFlag(9)]
private bool ShouldSerializeGlade() => _glade != null;
[CommandProperty(AccessLevel.GameMaster)]
public int GladeValue
{
get => GetTitle(ChampionSpawnType.Glade)?.Value ?? 0;
set => SetValue(ChampionSpawnType.Glade, value);
}
[CommandProperty(AccessLevel.GameMaster)]
public DateTime GladeLastDecay
{
get => GetTitle(ChampionSpawnType.Glade)?.LastDecay ?? DateTime.MinValue;
set => SetLastDecay(ChampionSpawnType.Glade, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ChampionTitle GetTitle(ChampionSpawnType type) => GetTitleValueRef(type);
// Get a pointer to the title so we can manipulate it internally such as nullifying it.
// Using this technique instead of an array decouples the context from changes to ChampionSpawnInfo.Table indexes
private ref ChampionTitle GetTitleValueRef(ChampionSpawnType type)
{
switch (type)
{
case ChampionSpawnType.Abyss:
{
return ref _abyss;
}
case ChampionSpawnType.Arachnid:
{
return ref _arachnid;
}
case ChampionSpawnType.ColdBlood:
{
return ref _coldBlood;
}
case ChampionSpawnType.ForestLord:
{
return ref _forestLord;
}
case ChampionSpawnType.VerminHorde:
{
return ref _verminHorde;
}
case ChampionSpawnType.UnholyTerror:
{
return ref _unholyTerror;
}
case ChampionSpawnType.SleepingDragon:
{
return ref _sleepingDragon;
}
case ChampionSpawnType.Glade:
{
return ref _glade;
}
case ChampionSpawnType.Corrupt:
{
return ref _corrupt;
}
default:
{
return ref Unsafe.NullRef<ChampionTitle>();
}
}
}
public ChampionTitle TryGetOrCreateTitle(ChampionSpawnType type)
{
ref var title = ref GetTitleValueRef(type);
if (Unsafe.IsNullRef(ref title))
{
return null;
}
return title ??= new ChampionTitle();
}
public void SetValue(ChampionSpawnType type, int value)
{
ref var title = ref GetTitleValueRef(type);
if (Unsafe.IsNullRef(ref title))
{
return;
}
if (title != null)
{
if (value <= 0)
{
title = null;
_player.InvalidateProperties();
return;
}
}
else
{
title = new ChampionTitle();
}
title.Value = value;
title.LastDecay = Core.Now;
_player.InvalidateProperties();
}
public void SetLastDecay(ChampionSpawnType type, DateTime value)
{
var title = TryGetOrCreateTitle(type);
if (title != null)
{
title.LastDecay = value;
}
}
public void Award(ChampionSpawnType type, int value)
{
var title = TryGetOrCreateTitle(type);
if (title == null)
{
return;
}
title.Value += value;
if (title.LastDecay == DateTime.MinValue)
{
title.LastDecay = Core.Now;
}
}
public bool CheckAtrophy()
{
var valuesUsed = _harrower > 0;
for (var i = 0; i < ChampionSpawnInfo.Table.Length; i++)
{
ref var title = ref GetTitleValueRef(ChampionSpawnInfo.Table[i].Type);
if (Unsafe.IsNullRef(ref title))
{
continue;
}
var decay = title.LastDecay;
if (decay > DateTime.MinValue && decay + LossDelay < Core.Now)
{
// Use bitwise-or to force-execute the Atrophy function
var isUsed = title.Atrophy(LossAmount);
if (!isUsed)
{
title = null;
}
valuesUsed |= isUsed;
}
}
return valuesUsed;
}
public override string ToString() => "...";
}

View file

@ -0,0 +1,172 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Server.Collections;
using Server.Mobiles;
namespace Server.Engines.CannedEvil;
public static class ChampionTitleSystem
{
// All of the players with murders
private static readonly Dictionary<PlayerMobile, ChampionTitleContext> _championTitleContexts = new();
private static readonly Timer _championTitleTimer = new ChampionTitleTimer();
public static void Configure()
{
GenericPersistence.Register("ChampionTitles", Serialize, Deserialize);
}
public static void Initialize()
{
EventSink.PlayerDeleted += OnPlayerDeleted;
_championTitleTimer.Start();
}
private static void OnPlayerDeleted(Mobile m)
{
if (m is PlayerMobile pm)
{
_championTitleContexts.Remove(pm);
}
}
private static void Deserialize(IGenericReader reader)
{
var version = reader.ReadEncodedInt();
var count = reader.ReadEncodedInt();
for (var i = 0; i < count; ++i)
{
var context = new ChampionTitleContext(reader.ReadEntity<PlayerMobile>());
context.Deserialize(reader);
_championTitleContexts.Add(context.Player, context);
}
}
private static void Serialize(IGenericWriter writer)
{
writer.WriteEncodedInt(0); // version
writer.WriteEncodedInt(_championTitleContexts.Count);
foreach (var (m, context) in _championTitleContexts)
{
writer.Write(m);
context.Serialize(writer);
}
}
public static bool GetChampionTitleContext(this PlayerMobile player, out ChampionTitleContext context) =>
_championTitleContexts.TryGetValue(player, out context);
public static ChampionTitleContext GetOrCreateChampionTitleContext(this PlayerMobile player)
{
ref var context = ref CollectionsMarshal.GetValueRefOrAddDefault(_championTitleContexts, player, out var exists);
if (!exists)
{
context = new ChampionTitleContext(player);
}
return context;
}
// Called when killing a harrower. Will give a minimum of 1 point.
public static void AwardHarrowerTitle(PlayerMobile pm)
{
var context = pm.GetOrCreateChampionTitleContext();
var count = 1;
for (var i = 0; i < ChampionSpawnInfo.Table.Length; i++)
{
var title = context.GetTitle(ChampionSpawnInfo.Table[i].Type);
if (title?.Value > 900)
{
count++;
}
}
context.Harrower = Math.Max(count, context.Harrower); // Harrower titles never decay.
}
public static int GetChampionTitleLabel(this PlayerMobile player)
{
if (!player.GetChampionTitleContext(out var context))
{
return 0;
}
if (context.Harrower > 0)
{
return 1113082 + Math.Min(context.Harrower, 10);
}
var highestValue = 0;
var highestType = 0;
for (var i = 0; i < ChampionSpawnInfo.Table.Length; i++)
{
var t = context.GetTitle(ChampionSpawnInfo.Table[i].Type);
if (t == null)
{
continue;
}
var v = t.Value;
if (v > highestValue)
{
highestValue = v;
highestType = i;
}
}
var offset = highestValue switch
{
> 800 => 3,
> 300 => highestValue / 300,
_ => 0
};
if (offset > 0)
{
var champInfo = ChampionSpawnInfo.Table[highestType];
var championLevelName = champInfo.LevelNames[Math.Min(offset, champInfo.LevelNames.Length) - 1];
return championLevelName.Number;
}
return 0;
}
private class ChampionTitleTimer : Timer
{
public ChampionTitleTimer() : base(TimeSpan.FromMinutes(5.0), TimeSpan.FromMinutes(5.0))
{
}
protected override void OnTick()
{
if (_championTitleContexts.Count == 0)
{
return;
}
using var queue = PooledRefQueue<Mobile>.Create();
foreach (var context in _championTitleContexts.Values)
{
if (!context.CheckAtrophy())
{
queue.Enqueue(context.Player);
}
}
while (queue.Count > 0)
{
_championTitleContexts.Remove((PlayerMobile)queue.Dequeue());
}
}
}
}

View file

@ -13,42 +13,30 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
namespace Server.Engines.CannedEvil
using ModernUO.Serialization;
namespace Server.Engines.CannedEvil;
[SerializationGenerator(0)]
public partial class DungeonChampionSpawn : ChampionSpawn
{
public class DungeonChampionSpawn : ChampionSpawn
[Constructible]
public DungeonChampionSpawn()
{
[Constructible]
public DungeonChampionSpawn()
{
CannedEvilTimer.AddSpawn(this);
}
CannedEvilTimer.AddSpawn(this);
}
public DungeonChampionSpawn(Serial serial) : base(serial)
{
CannedEvilTimer.AddSpawn(this);
}
public DungeonChampionSpawn(Serial serial)
{
CannedEvilTimer.AddSpawn(this);
}
public override bool ProximitySpawn => true;
public override bool AlwaysActive => false;
public override bool ProximitySpawn => true;
public override bool AlwaysActive => false;
public override void Serialize(IGenericWriter writer)
{
base.Serialize(writer);
writer.WriteEncodedInt(0); //version
}
public override void Deserialize(IGenericReader reader)
{
base.Deserialize(reader);
int version = reader.ReadEncodedInt();
}
public override void OnAfterDelete()
{
base.OnAfterDelete();
CannedEvilTimer.RemoveSpawn(this);
}
public override void OnAfterDelete()
{
base.OnAfterDelete();
CannedEvilTimer.RemoveSpawn(this);
}
}

View file

@ -1,60 +1,32 @@
namespace Server.Items
using ModernUO.Serialization;
namespace Server.Items;
[SerializationGenerator(0, false)]
public partial class HarrowerGate : Moongate
{
public class HarrowerGate : Moongate
[SerializableField(0)]
private Mobile _harrower;
public HarrowerGate(Mobile harrower, Point3D loc, Map map, Point3D targLoc, Map targMap) : base(targLoc, targMap)
{
private Mobile m_Harrower;
_harrower = harrower;
public HarrowerGate(Mobile harrower, Point3D loc, Map map, Point3D targLoc, Map targMap) : base(targLoc, targMap)
Dispellable = false;
ItemID = 0x1FD4;
Light = LightType.Circle300;
MoveToWorld(loc, map);
}
public override int LabelNumber => 1049498; // dark moongate
[AfterDeserialization(false)]
private void AfterDeserialization()
{
if (_harrower == null)
{
m_Harrower = harrower;
Dispellable = false;
ItemID = 0x1FD4;
Light = LightType.Circle300;
MoveToWorld(loc, map);
}
public HarrowerGate(Serial serial) : base(serial)
{
}
public override int LabelNumber => 1049498; // dark moongate
public override void Serialize(IGenericWriter writer)
{
base.Serialize(writer);
writer.Write(0); // version
writer.Write(m_Harrower);
}
public override void Deserialize(IGenericReader reader)
{
base.Deserialize(reader);
var version = reader.ReadInt();
switch (version)
{
case 0:
{
m_Harrower = reader.ReadEntity<Mobile>();
if (m_Harrower == null)
{
Delete();
}
break;
}
}
if (Light != LightType.Circle300)
{
Light = LightType.Circle300;
}
Delete();
}
}
}

View file

@ -13,43 +13,31 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
namespace Server.Engines.CannedEvil
using ModernUO.Serialization;
namespace Server.Engines.CannedEvil;
[SerializationGenerator(0)]
public partial class LLChampionSpawn : ChampionSpawn
{
public class LLChampionSpawn : ChampionSpawn
public override bool HasStarRoomGate => false;
[Constructible]
public LLChampionSpawn()
{
public override bool HasStarRoomGate => false;
CannedEvilTimer.AddSpawn(this);
}
[Constructible]
public LLChampionSpawn()
{
CannedEvilTimer.AddSpawn(this);
}
public LLChampionSpawn(Serial serial)
{
CannedEvilTimer.AddSpawn(this);
}
public LLChampionSpawn(Serial serial) : base(serial)
{
}
public override bool AlwaysActive => false;
public override bool AlwaysActive => false;
public override void Serialize(IGenericWriter writer)
{
base.Serialize(writer);
writer.WriteEncodedInt(0); //version
}
public override void Deserialize(IGenericReader reader)
{
base.Deserialize(reader);
int version = reader.ReadEncodedInt();
CannedEvilTimer.AddSpawn(this);
}
public override void OnAfterDelete()
{
base.OnAfterDelete();
CannedEvilTimer.RemoveSpawn(this);
}
public override void OnAfterDelete()
{
base.OnAfterDelete();
CannedEvilTimer.RemoveSpawn(this);
}
}

View file

@ -1,98 +1,68 @@
using System;
using ModernUO.Serialization;
namespace Server.Items
namespace Server.Items;
[SerializationGenerator(1, false)]
public partial class StarRoomGate : Moongate
{
public class StarRoomGate : Moongate
private static TimeSpan GateDuration = TimeSpan.FromMinutes(2.0);
[SerializableField(0)]
private bool _decays;
[DeltaDateTime]
[SerializableField(1)]
private DateTime _decayTime;
private Timer _timer;
[Constructible]
public StarRoomGate(Point3D loc, Map map, bool decays) : this(decays)
{
private bool m_Decays;
private DateTime m_DecayTime;
private Timer m_Timer;
MoveToWorld(loc, map);
Effects.PlaySound(loc, map, 0x20E);
}
[Constructible]
public StarRoomGate(Point3D loc, Map map, bool decays) : this(decays)
[Constructible]
public StarRoomGate(bool decays = false) : base(new Point3D(5143, 1774, 0), Map.Felucca)
{
Dispellable = false;
ItemID = 0x1FD4;
if (decays)
{
MoveToWorld(loc, map);
Effects.PlaySound(loc, map, 0x20E);
_decays = true;
_decayTime = Core.Now + GateDuration;
_timer = Timer.DelayCall(GateDuration, Delete);
}
}
[Constructible]
public StarRoomGate(bool decays = false) : base(new Point3D(5143, 1774, 0), Map.Felucca)
public override int LabelNumber => 1049498; // dark moongate
public override void OnAfterDelete()
{
_timer?.Stop();
base.OnAfterDelete();
}
private void Deserialize(IGenericReader reader, int version)
{
_decays = reader.ReadBool();
if (_decays)
{
Dispellable = false;
ItemID = 0x1FD4;
if (decays)
{
m_Decays = true;
m_DecayTime = Core.Now + TimeSpan.FromMinutes(2.0);
m_Timer = new InternalTimer(this, m_DecayTime);
m_Timer.Start();
}
_decayTime = reader.ReadDeltaTime();
}
}
public StarRoomGate(Serial serial) : base(serial)
[AfterDeserialization]
private void AfterDeserialization()
{
if (_decays)
{
}
public override int LabelNumber => 1049498; // dark moongate
public override void OnAfterDelete()
{
m_Timer?.Stop();
base.OnAfterDelete();
}
public override void Serialize(IGenericWriter writer)
{
base.Serialize(writer);
writer.Write(0); // version
writer.Write(m_Decays);
if (m_Decays)
{
writer.WriteDeltaTime(m_DecayTime);
}
}
public override void Deserialize(IGenericReader reader)
{
base.Deserialize(reader);
var version = reader.ReadInt();
switch (version)
{
case 0:
{
m_Decays = reader.ReadBool();
if (m_Decays)
{
m_DecayTime = reader.ReadDeltaTime();
m_Timer = new InternalTimer(this, m_DecayTime);
m_Timer.Start();
}
break;
}
}
}
private class InternalTimer : Timer
{
private readonly Item m_Item;
public InternalTimer(Item item, DateTime end) : base(end - Core.Now) => m_Item = item;
protected override void OnTick()
{
m_Item.Delete();
}
_timer = Timer.DelayCall(_decayTime - Core.Now, Delete);
}
}
}

View file

@ -58,9 +58,9 @@ public static class ValorVirtue
}
else if (idol.Spawn.Active)
{
if (idol.Spawn.Champion != null) // TODO: Message?
if (idol.Spawn.Champion != null)
{
return;
from.SendLocalizedMessage(1112470); // You may not use Valor on this Champion Idol. The Champion has already spawned.
}
int needed, consumed;

View file

@ -0,0 +1,11 @@
{
"version": 0,
"type": "Server.Engines.CannedEvil.ChampionAltar",
"properties": [
{
"name": "Spawn",
"type": "Server.Engines.CannedEvil.ChampionSpawn",
"rule": "SerializableInterfaceMigrationRule"
}
]
}

View file

@ -0,0 +1,11 @@
{
"version": 0,
"type": "Server.Engines.CannedEvil.ChampionPlatform",
"properties": [
{
"name": "Spawn",
"type": "Server.Engines.CannedEvil.ChampionSpawn",
"rule": "SerializableInterfaceMigrationRule"
}
]
}

View file

@ -0,0 +1,21 @@
{
"version": 1,
"type": "Server.Engines.CannedEvil.ChampionSkullBrazier",
"properties": [
{
"name": "Type",
"type": "Server.Engines.CannedEvil.ChampionSkullType",
"rule": "EnumMigrationRule"
},
{
"name": "Platform",
"type": "Server.Engines.CannedEvil.ChampionSkullPlatform",
"rule": "SerializableInterfaceMigrationRule"
},
{
"name": "Skull",
"type": "Server.Item",
"rule": "SerializableInterfaceMigrationRule"
}
]
}

View file

@ -0,0 +1,36 @@
{
"version": 0,
"type": "Server.Engines.CannedEvil.ChampionSkullPlatform",
"properties": [
{
"name": "Power",
"type": "Server.Engines.CannedEvil.ChampionSkullBrazier",
"rule": "SerializableInterfaceMigrationRule"
},
{
"name": "Enlightenment",
"type": "Server.Engines.CannedEvil.ChampionSkullBrazier",
"rule": "SerializableInterfaceMigrationRule"
},
{
"name": "Venom",
"type": "Server.Engines.CannedEvil.ChampionSkullBrazier",
"rule": "SerializableInterfaceMigrationRule"
},
{
"name": "Pain",
"type": "Server.Engines.CannedEvil.ChampionSkullBrazier",
"rule": "SerializableInterfaceMigrationRule"
},
{
"name": "Greed",
"type": "Server.Engines.CannedEvil.ChampionSkullBrazier",
"rule": "SerializableInterfaceMigrationRule"
},
{
"name": "Death",
"type": "Server.Engines.CannedEvil.ChampionSkullBrazier",
"rule": "SerializableInterfaceMigrationRule"
}
]
}

View file

@ -0,0 +1,186 @@
{
"version": 10,
"type": "Server.Engines.CannedEvil.ChampionSpawn",
"properties": [
{
"name": "Level",
"type": "int",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "ActivatedByProximity",
"type": "bool",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "NextProximityTime",
"type": "System.DateTime",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"DeltaTime"
]
},
{
"name": "MaxLevel",
"type": "int",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "ActivatedByValor",
"type": "bool",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "DamageEntries",
"type": "System.Collections.Generic.Dictionary\u003CServer.Mobile, int\u003E",
"rule": "DictionaryMigrationRule",
"ruleArguments": [
"Server.Mobile",
"SerializableInterfaceMigrationRule",
"0",
"int",
"PrimitiveTypeMigrationRule",
"1",
""
]
},
{
"name": "ConfinedRoaming",
"type": "bool",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "Idol",
"type": "Server.Engines.CannedEvil.IdolOfTheChampion",
"rule": "SerializableInterfaceMigrationRule"
},
{
"name": "HasBeenAdvanced",
"type": "bool",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "SpawnArea",
"type": "Server.Rectangle2D",
"rule": "PrimitiveUOTypeMigrationRule",
"ruleArguments": [
"Rect2D"
]
},
{
"name": "RandomizeType",
"type": "bool",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "Kills",
"type": "int",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "Active",
"type": "bool",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "Type",
"type": "Server.Engines.CannedEvil.ChampionSpawnType",
"rule": "EnumMigrationRule"
},
{
"name": "Creatures",
"type": "System.Collections.Generic.List\u003CServer.Mobile\u003E",
"rule": "ListMigrationRule",
"ruleArguments": [
"Server.Mobile",
"SerializableInterfaceMigrationRule"
]
},
{
"name": "RedSkulls",
"type": "System.Collections.Generic.List\u003CServer.Item\u003E",
"rule": "ListMigrationRule",
"ruleArguments": [
"Server.Item",
"SerializableInterfaceMigrationRule"
]
},
{
"name": "WhiteSkulls",
"type": "System.Collections.Generic.List\u003CServer.Item\u003E",
"rule": "ListMigrationRule",
"ruleArguments": [
"Server.Item",
"SerializableInterfaceMigrationRule"
]
},
{
"name": "Platform",
"type": "Server.Engines.CannedEvil.ChampionPlatform",
"rule": "SerializableInterfaceMigrationRule"
},
{
"name": "Altar",
"type": "Server.Engines.CannedEvil.ChampionAltar",
"rule": "SerializableInterfaceMigrationRule"
},
{
"name": "ExpireDelay",
"type": "System.TimeSpan",
"rule": "PrimitiveTypeMigrationRule"
},
{
"name": "ExpireTime",
"type": "System.DateTime",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"DeltaTime"
]
},
{
"name": "Champion",
"type": "Server.Mobile",
"rule": "SerializableInterfaceMigrationRule"
},
{
"name": "RestartDelay",
"type": "System.TimeSpan",
"rule": "PrimitiveTypeMigrationRule"
},
{
"name": "RestartTime",
"type": "System.DateTime",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"DeltaTime"
]
}
]
}

View file

@ -0,0 +1,22 @@
{
"version": 0,
"type": "Server.Engines.CannedEvil.ChampionTitle",
"properties": [
{
"name": "Value",
"type": "int",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "LastDecay",
"type": "System.DateTime",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
}
]
}

View file

@ -0,0 +1,95 @@
{
"version": 1,
"type": "Server.Engines.CannedEvil.ChampionTitleContext",
"properties": [
{
"name": "Harrower",
"type": "int",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "Abyss",
"type": "Server.Engines.CannedEvil.ChampionTitle",
"usesSaveFlag": true,
"rule": "RawSerializableMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "Arachnid",
"type": "Server.Engines.CannedEvil.ChampionTitle",
"usesSaveFlag": true,
"rule": "RawSerializableMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "ColdBlood",
"type": "Server.Engines.CannedEvil.ChampionTitle",
"usesSaveFlag": true,
"rule": "RawSerializableMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "ForestLord",
"type": "Server.Engines.CannedEvil.ChampionTitle",
"usesSaveFlag": true,
"rule": "RawSerializableMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "VerminHorde",
"type": "Server.Engines.CannedEvil.ChampionTitle",
"usesSaveFlag": true,
"rule": "RawSerializableMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "UnholyTerror",
"type": "Server.Engines.CannedEvil.ChampionTitle",
"usesSaveFlag": true,
"rule": "RawSerializableMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "SleepingDragon",
"type": "Server.Engines.CannedEvil.ChampionTitle",
"usesSaveFlag": true,
"rule": "RawSerializableMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "Corrupt",
"type": "Server.Engines.CannedEvil.ChampionTitle",
"usesSaveFlag": true,
"rule": "RawSerializableMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "Glade",
"type": "Server.Engines.CannedEvil.ChampionTitle",
"usesSaveFlag": true,
"rule": "RawSerializableMigrationRule",
"ruleArguments": [
""
]
}
]
}

View file

@ -0,0 +1,4 @@
{
"version": 0,
"type": "Server.Engines.CannedEvil.DungeonChampionSpawn"
}

View file

@ -0,0 +1,11 @@
{
"version": 0,
"type": "Server.Engines.CannedEvil.IdolOfTheChampion",
"properties": [
{
"name": "Spawn",
"type": "Server.Engines.CannedEvil.ChampionSpawn",
"rule": "SerializableInterfaceMigrationRule"
}
]
}

View file

@ -0,0 +1,4 @@
{
"version": 0,
"type": "Server.Engines.CannedEvil.LLChampionSpawn"
}

View file

@ -0,0 +1,11 @@
{
"version": 2,
"type": "Server.Items.ChampionSkull",
"properties": [
{
"name": "Type",
"type": "Server.Engines.CannedEvil.ChampionSkullType",
"rule": "EnumMigrationRule"
}
]
}

View file

@ -0,0 +1,11 @@
{
"version": 0,
"type": "Server.Items.HarrowerGate",
"properties": [
{
"name": "Harrower",
"type": "Server.Mobile",
"rule": "SerializableInterfaceMigrationRule"
}
]
}

View file

@ -0,0 +1,22 @@
{
"version": 1,
"type": "Server.Items.StarRoomGate",
"properties": [
{
"name": "Decays",
"type": "bool",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "DecayTime",
"type": "System.DateTime",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"DeltaTime"
]
}
]
}

View file

@ -1207,12 +1207,6 @@ namespace Server.Guilds
writer.Write(Website);
}
public override bool ShouldExecuteAfterSerialize => false;
public override void AfterSerialize()
{
}
public override void Delete()
{
World.RemoveGuild(this);

View file

@ -1,5 +1,6 @@
using System;
using System.Text;
using Server.Buffers;
using Server.Engines.CannedEvil;
using Server.Mobiles;
@ -13,12 +14,6 @@ namespace Server.Misc
public const int MinKarma = -15000;
public const int MaxKarma = 15000;
public static string[] HarrowerTitles =
{
"Spite", "Opponent", "Hunter", "Venom", "Executioner",
"Annihilator", "Champion", "Assailant", "Purifier", "Nullifier"
};
private static readonly string[,] m_Levels =
{
{ "Neophyte", "Neophyte", "Neophyte" },
@ -39,85 +34,85 @@ namespace Server.Misc
1249,
new[]
{
new KarmaEntry(-10000, "The Outcast {0}"),
new KarmaEntry(-5000, "The Despicable {0}"),
new KarmaEntry(-2500, "The Scoundrel {0}"),
new KarmaEntry(-1250, "The Unsavory {0}"),
new KarmaEntry(-625, "The Rude {0}"),
new KarmaEntry(624, "{0}"),
new KarmaEntry(1249, "The Fair {0}"),
new KarmaEntry(2499, "The Kind {0}"),
new KarmaEntry(4999, "The Good {0}"),
new KarmaEntry(9999, "The Honest {0}"),
new KarmaEntry(10000, "The Trustworthy {0}")
new KarmaEntry(-10000, "The Outcast "),
new KarmaEntry(-5000, "The Despicable "),
new KarmaEntry(-2500, "The Scoundrel "),
new KarmaEntry(-1250, "The Unsavory "),
new KarmaEntry(-625, "The Rude "),
new KarmaEntry(624, ""),
new KarmaEntry(1249, "The Fair "),
new KarmaEntry(2499, "The Kind "),
new KarmaEntry(4999, "The Good "),
new KarmaEntry(9999, "The Honest "),
new KarmaEntry(10000, "The Trustworthy ")
}
),
new(
2499,
new[]
{
new KarmaEntry(-10000, "The Wretched {0}"),
new KarmaEntry(-5000, "The Dastardly {0}"),
new KarmaEntry(-2500, "The Malicious {0}"),
new KarmaEntry(-1250, "The Dishonorable {0}"),
new KarmaEntry(-625, "The Disreputable {0}"),
new KarmaEntry(624, "The Notable {0}"),
new KarmaEntry(1249, "The Upstanding {0}"),
new KarmaEntry(2499, "The Respectable {0}"),
new KarmaEntry(4999, "The Honorable {0}"),
new KarmaEntry(9999, "The Commendable {0}"),
new KarmaEntry(10000, "The Estimable {0}")
new KarmaEntry(-10000, "The Wretched "),
new KarmaEntry(-5000, "The Dastardly "),
new KarmaEntry(-2500, "The Malicious "),
new KarmaEntry(-1250, "The Dishonorable "),
new KarmaEntry(-625, "The Disreputable "),
new KarmaEntry(624, "The Notable "),
new KarmaEntry(1249, "The Upstanding "),
new KarmaEntry(2499, "The Respectable "),
new KarmaEntry(4999, "The Honorable "),
new KarmaEntry(9999, "The Commendable "),
new KarmaEntry(10000, "The Estimable ")
}
),
new(
4999,
new[]
{
new KarmaEntry(-10000, "The Nefarious {0}"),
new KarmaEntry(-5000, "The Wicked {0}"),
new KarmaEntry(-2500, "The Vile {0}"),
new KarmaEntry(-1250, "The Ignoble {0}"),
new KarmaEntry(-625, "The Notorious {0}"),
new KarmaEntry(624, "The Prominent {0}"),
new KarmaEntry(1249, "The Reputable {0}"),
new KarmaEntry(2499, "The Proper {0}"),
new KarmaEntry(4999, "The Admirable {0}"),
new KarmaEntry(9999, "The Famed {0}"),
new KarmaEntry(10000, "The Great {0}")
new KarmaEntry(-10000, "The Nefarious "),
new KarmaEntry(-5000, "The Wicked "),
new KarmaEntry(-2500, "The Vile "),
new KarmaEntry(-1250, "The Ignoble "),
new KarmaEntry(-625, "The Notorious "),
new KarmaEntry(624, "The Prominent "),
new KarmaEntry(1249, "The Reputable "),
new KarmaEntry(2499, "The Proper "),
new KarmaEntry(4999, "The Admirable "),
new KarmaEntry(9999, "The Famed "),
new KarmaEntry(10000, "The Great ")
}
),
new(
9999,
new[]
{
new KarmaEntry(-10000, "The Dread {0}"),
new KarmaEntry(-5000, "The Evil {0}"),
new KarmaEntry(-2500, "The Villainous {0}"),
new KarmaEntry(-1250, "The Sinister {0}"),
new KarmaEntry(-625, "The Infamous {0}"),
new KarmaEntry(624, "The Renowned {0}"),
new KarmaEntry(1249, "The Distinguished {0}"),
new KarmaEntry(2499, "The Eminent {0}"),
new KarmaEntry(4999, "The Noble {0}"),
new KarmaEntry(9999, "The Illustrious {0}"),
new KarmaEntry(10000, "The Glorious {0}")
new KarmaEntry(-10000, "The Dread "),
new KarmaEntry(-5000, "The Evil "),
new KarmaEntry(-2500, "The Villainous "),
new KarmaEntry(-1250, "The Sinister "),
new KarmaEntry(-625, "The Infamous "),
new KarmaEntry(624, "The Renowned "),
new KarmaEntry(1249, "The Distinguished "),
new KarmaEntry(2499, "The Eminent "),
new KarmaEntry(4999, "The Noble "),
new KarmaEntry(9999, "The Illustrious "),
new KarmaEntry(10000, "The Glorious ")
}
),
new(
10000,
new[]
{
new KarmaEntry(-10000, "The Dread {1} {0}"),
new KarmaEntry(-5000, "The Evil {1} {0}"),
new KarmaEntry(-2500, "The Dark {1} {0}"),
new KarmaEntry(-1250, "The Sinister {1} {0}"),
new KarmaEntry(-625, "The Dishonored {1} {0}"),
new KarmaEntry(624, "{1} {0}"),
new KarmaEntry(1249, "The Distinguished {1} {0}"),
new KarmaEntry(2499, "The Eminent {1} {0}"),
new KarmaEntry(4999, "The Noble {1} {0}"),
new KarmaEntry(9999, "The Illustrious {1} {0}"),
new KarmaEntry(10000, "The Glorious {1} {0}")
new KarmaEntry(-10000, "The Dread "),
new KarmaEntry(-5000, "The Evil "),
new KarmaEntry(-2500, "The Dark "),
new KarmaEntry(-1250, "The Sinister "),
new KarmaEntry(-625, "The Dishonored "),
new KarmaEntry(624, ""),
new KarmaEntry(1249, "The Distinguished "),
new KarmaEntry(2499, "The Eminent "),
new KarmaEntry(4999, "The Noble "),
new KarmaEntry(9999, "The Illustrious "),
new KarmaEntry(10000, "The Glorious ")
}
)
};
@ -274,7 +269,7 @@ namespace Server.Misc
public static string ComputeTitle(Mobile beholder, Mobile beheld)
{
var title = new StringBuilder();
using var title = ValueStringBuilder.Create();
var fame = beheld.Fame;
var karma = beheld.Karma;
@ -297,7 +292,21 @@ namespace Server.Misc
if (karma <= ke.m_Karma || j == karmaEntries.Length - 1)
{
title.AppendFormat(ke.m_Title, beheld.Name, beheld.Female ? "Lady" : "Lord");
if (karma >= 10000)
{
if (beheld.Female)
{
title.Append($"{ke.m_Title}Lady {beheld.Name}");
}
else
{
title.Append($"{ke.m_Title}Lord {beheld.Name}");
}
}
else
{
title.Append($"{ke.m_Title}{beheld.Name}");
}
break;
}
}
@ -313,40 +322,14 @@ namespace Server.Misc
if (beheld is PlayerMobile mobile && mobile.DisplayChampionTitle)
{
var info = mobile.ChampionTitles;
if (info.Harrower > 0)
var titleLabel = mobile.GetChampionTitleLabel();
if (titleLabel > 0)
{
title.Append($": {HarrowerTitles[Math.Min(HarrowerTitles.Length, info.Harrower) - 1]} of Evil");
}
else
{
int highestValue = 0, highestType = 0;
for (var i = 0; i < ChampionSpawnInfo.Table.Length; i++)
{
var v = info.GetValue(i);
// Should this be translated to the receivers language? Prefix titles aren't?
title.Append($": {Localization.GetText(titleLabel)}");
if (v > highestValue)
{
highestValue = v;
highestType = i;
}
}
var offset = highestValue switch
{
> 800 => 3,
> 300 => highestValue / 300,
_ => 0
};
if (offset > 0)
{
var champInfo = ChampionSpawnInfo.GetInfo((ChampionSpawnType)highestType);
title.Append(
$": {champInfo.LevelNames[Math.Min(offset, champInfo.LevelNames.Length) - 1]} of the {champInfo.Name}"
);
}
// Do not display the skills title
return title.ToString();
}
}

View file

@ -1,339 +0,0 @@
using System;
using System.Runtime.CompilerServices;
using Server.Engines.CannedEvil;
namespace Server.Mobiles
{
[PropertyObject]
public class ChampionTitleInfo
{
private const int LossAmount = 90;
private static TimeSpan LossDelay = TimeSpan.FromDays(1.0);
private TitleInfo[] m_Values;
public ChampionTitleInfo()
{
}
public ChampionTitleInfo(IGenericReader reader)
{
var version = reader.ReadEncodedInt();
switch (version)
{
case 0:
{
Harrower = reader.ReadEncodedInt();
var length = reader.ReadEncodedInt();
if (length == 0)
{
break;
}
m_Values = new TitleInfo[length];
for (var i = 0; i < length; i++)
{
m_Values[i] = new TitleInfo(reader);
}
if (m_Values.Length != ChampionSpawnInfo.Table.Length)
{
var oldValues = m_Values;
m_Values = new TitleInfo[ChampionSpawnInfo.Table.Length];
for (var i = 0; i < m_Values.Length && i < oldValues.Length; i++)
{
m_Values[i] = oldValues[i];
}
}
break;
}
}
}
[CommandProperty(AccessLevel.GameMaster)]
public int Pestilence
{
get => GetValue(ChampionSpawnType.Pestilence);
set => SetValue(ChampionSpawnType.Pestilence, value);
}
[CommandProperty(AccessLevel.GameMaster)]
public int Abyss
{
get => GetValue(ChampionSpawnType.Abyss);
set => SetValue(ChampionSpawnType.Abyss, value);
}
[CommandProperty(AccessLevel.GameMaster)]
public int Arachnid
{
get => GetValue(ChampionSpawnType.Arachnid);
set => SetValue(ChampionSpawnType.Arachnid, value);
}
[CommandProperty(AccessLevel.GameMaster)]
public int ColdBlood
{
get => GetValue(ChampionSpawnType.ColdBlood);
set => SetValue(ChampionSpawnType.ColdBlood, value);
}
[CommandProperty(AccessLevel.GameMaster)]
public int ForestLord
{
get => GetValue(ChampionSpawnType.ForestLord);
set => SetValue(ChampionSpawnType.ForestLord, value);
}
[CommandProperty(AccessLevel.GameMaster)]
public int SleepingDragon
{
get => GetValue(ChampionSpawnType.SleepingDragon);
set => SetValue(ChampionSpawnType.SleepingDragon, value);
}
[CommandProperty(AccessLevel.GameMaster)]
public int UnholyTerror
{
get => GetValue(ChampionSpawnType.UnholyTerror);
set => SetValue(ChampionSpawnType.UnholyTerror, value);
}
[CommandProperty(AccessLevel.GameMaster)]
public int VerminHorde
{
get => GetValue(ChampionSpawnType.VerminHorde);
set => SetValue(ChampionSpawnType.VerminHorde, value);
}
[CommandProperty(AccessLevel.GameMaster)]
public int Harrower { get; set; }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetValue(ChampionSpawnType type) => GetValue((int)type);
public int GetValue(int index)
{
if (index < 0 || index >= m_Values?.Length)
{
return 0;
}
return m_Values?[index]?.Value ?? 0;
}
public DateTime GetLastDecay(int index)
{
if (index < 0 || index >= m_Values?.Length)
{
return DateTime.MinValue;
}
return m_Values?[index]?.LastDecay ?? DateTime.MinValue;
}
public void SetValue(ChampionSpawnType type, int value)
{
var index = (int)type;
if (index < 0 || index >= ChampionSpawnInfo.Table.Length)
{
return;
}
m_Values ??= new TitleInfo[ChampionSpawnInfo.Table.Length];
var title = m_Values[index];
if (title == null)
{
if (value > 0)
{
m_Values[index] = new TitleInfo(value, Core.Now);
}
return;
}
title.Value = Math.Max(value, 0);
title.LastDecay = title.Value == 0 ? DateTime.MinValue : Core.Now;
}
public void Award(ChampionSpawnType type, int value)
{
var index = (int)type;
if (value <= 0 || index < 0 || index >= ChampionSpawnInfo.Table.Length)
{
return;
}
m_Values ??= new TitleInfo[ChampionSpawnInfo.Table.Length];
var title = m_Values[index];
if (title == null)
{
m_Values[index] = new TitleInfo(value, Core.Now);
return;
}
title.Value += value;
if (title.LastDecay == DateTime.MinValue)
{
title.LastDecay = Core.Now;
}
}
public void Atrophy(int index, int value)
{
if (value <= 0 || index < 0 || index >= ChampionSpawnInfo.Table.Length)
{
return;
}
var title = m_Values?[index];
if (title == null)
{
return;
}
var before = title.Value;
title.Value -= Math.Min(value, title.Value);
if (before != title.Value)
{
title.LastDecay = Core.Now;
}
}
public override string ToString() => "...";
public static void Serialize(IGenericWriter writer, ChampionTitleInfo titles)
{
writer.WriteEncodedInt(0); // version
writer.WriteEncodedInt(titles.Harrower);
var length = titles.m_Values?.Length ?? 0;
writer.WriteEncodedInt(length);
for (var i = 0; i < length; i++)
{
titles.m_Values![i] ??= new TitleInfo();
TitleInfo.Serialize(writer, titles.m_Values[i]);
}
}
public static bool ShouldAtrophy(PlayerMobile pm)
{
var t = pm.ChampionTitles;
if (t?.m_Values == null)
{
return false;
}
for (var i = 0; i < t.m_Values.Length; i++)
{
var decay = t.GetLastDecay(i);
if (decay > DateTime.MinValue && decay + LossDelay < Core.Now)
{
return true;
}
}
return false;
}
public static void CheckAtrophy(PlayerMobile pm)
{
var t = pm.ChampionTitles;
if (t?.m_Values == null)
{
return;
}
for (var i = 0; i < t.m_Values.Length; i++)
{
var decay = t.GetLastDecay(i);
if (decay > DateTime.MinValue && decay + LossDelay < Core.Now)
{
t.Atrophy(i, LossAmount);
}
}
}
// Called when killing a harrower. Will give a minimum of 1 point.
public static void AwardHarrowerTitle(PlayerMobile pm)
{
var t = pm.ChampionTitles;
if (t == null)
{
return;
}
if (t.m_Values == null)
{
if (t.Harrower == 0)
{
t.Harrower = 1;
}
return;
}
t.m_Values ??= new TitleInfo[ChampionSpawnInfo.Table.Length];
var count = 1;
for (var i = 0; i < t.m_Values.Length; i++)
{
if (t.m_Values[i].Value > 900)
{
count++;
}
}
t.Harrower = Math.Max(count, t.Harrower); // Harrower titles never decay.
}
private class TitleInfo
{
public TitleInfo()
{
}
public TitleInfo(int value, DateTime lastDecay)
{
Value = value;
LastDecay = lastDecay;
}
public TitleInfo(IGenericReader reader)
{
var version = reader.ReadEncodedInt();
switch (version)
{
case 0:
{
Value = reader.ReadEncodedInt();
LastDecay = reader.ReadDateTime();
break;
}
}
}
public int Value { get; set; }
public DateTime LastDecay { get; set; }
public static void Serialize(IGenericWriter writer, TitleInfo info)
{
writer.WriteEncodedInt(0); // version
writer.WriteEncodedInt(info.Value);
writer.Write(info.LastDecay);
}
}
}
}

View file

@ -4,6 +4,7 @@ using Server.Accounting;
using Server.Collections;
using Server.ContextMenus;
using Server.Engines.BulkOrders;
using Server.Engines.CannedEvil;
using Server.Engines.ConPVP;
using Server.Engines.Craft;
using Server.Engines.Help;
@ -206,8 +207,6 @@ namespace Server.Mobiles
m_GameTime = TimeSpan.Zero;
m_GuildRank = RankDefinition.Lowest;
ChampionTitles = new ChampionTitleInfo();
}
public PlayerMobile(Serial s) : base(s)
@ -693,8 +692,8 @@ namespace Server.Mobiles
set => SetFlag(PlayerFlag.DisplayChampionTitle, value);
}
[CommandProperty(AccessLevel.GameMaster)]
public ChampionTitleInfo ChampionTitles { get; private set; }
[CommandProperty(AccessLevel.GameMaster, canModify: true)]
public ChampionTitleContext ChampionTitles => this.GetOrCreateChampionTitleContext();
[CommandProperty(AccessLevel.GameMaster)]
public int ShortTermMurders
@ -1859,21 +1858,13 @@ namespace Server.Mobiles
{
if (AutoRenewInsurance)
{
list.Add(
new CallbackEntry(
6202,
CancelRenewInventoryInsurance
)
); // Cancel Renewing Inventory Insurance
// Cancel Renewing Inventory Insurance
list.Add(new CallbackEntry(6202, CancelRenewInventoryInsurance));
}
else
{
list.Add(
new CallbackEntry(
6200,
AutoRenewInventoryInsurance
)
); // Auto Renew Inventory Insurance
// Auto Renew Inventory Insurance
list.Add(new CallbackEntry(6200, AutoRenewInventoryInsurance));
}
}
}
@ -2855,6 +2846,7 @@ namespace Server.Mobiles
switch (version)
{
case 33: // Removes champion title
case 32: // Removes virtue properties
case 31: // Removed Short/Long Term Elapse
case 30:
@ -2928,7 +2920,11 @@ namespace Server.Mobiles
}
case 23:
{
ChampionTitles = new ChampionTitleInfo(reader);
if (version < 33)
{
ChampionTitles.Deserialize(reader);
}
goto case 22;
}
case 22:
@ -3165,8 +3161,6 @@ namespace Server.Mobiles
LastOnline = ((Account)Account).LastLogin;
}
ChampionTitles ??= new ChampionTitleInfo();
if (AccessLevel > AccessLevel.Player)
{
IgnoreMobiles = true;
@ -3184,8 +3178,6 @@ namespace Server.Mobiles
}
}
CheckAtrophies();
if (Hidden) // Hiding is the only buff where it has an effect that's serialized.
{
AddBuff(new BuffInfo(BuffIcon.HidingAndOrStealth, 1075655));
@ -3196,7 +3188,7 @@ namespace Server.Mobiles
{
base.Serialize(writer);
writer.Write(32); // version
writer.Write(33); // version
if (Stabled == null)
{
@ -3251,8 +3243,6 @@ namespace Server.Mobiles
}
}
ChampionTitleInfo.Serialize(writer, ChampionTitles);
writer.WriteEncodedInt(ToTItemsTurnedIn);
writer.Write(ToTTotalMonsterFame); // This ain't going to be a small #.
@ -3317,22 +3307,6 @@ namespace Server.Mobiles
writer.Write(GameTime);
}
// Do we need to run an after serialize?
public override bool ShouldExecuteAfterSerialize => ShouldAtrophy();
public override void AfterSerialize()
{
base.AfterSerialize();
CheckAtrophies();
}
public bool ShouldAtrophy() => ChampionTitleInfo.ShouldAtrophy(this);
public void CheckAtrophies()
{
ChampionTitleInfo.CheckAtrophy(this);
}
public override bool CanSee(Mobile m)
{
if (m is CharacterStatue statue)
@ -3439,6 +3413,17 @@ namespace Server.Mobiles
}
}
// TODO: Add the Titles Menu for later Eras:
// https://uo.com/wiki/ultima-online-wiki/player/skill-titles-order/
if (DisplayChampionTitle)
{
var titleLabel = this.GetChampionTitleLabel();
if (titleLabel > 0)
{
list.Add(titleLabel);
}
}
if (Core.ML && AllFollowers != null)
{
foreach (var follower in AllFollowers)
@ -4457,6 +4442,7 @@ namespace Server.Mobiles
}
DisplayChampionTitle = !DisplayChampionTitle;
InvalidateProperties();
}
public virtual bool HasRecipe(Recipe r) => r != null && HasRecipe(r.ID);

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Server.Engines.CannedEvil;
using Server.Engines.Virtues;
using Server.Items;
using Server.Spells;
@ -325,7 +326,7 @@ namespace Server.Mobiles
if (ds.m_HasRight && ds.m_Mobile is PlayerMobile mobile)
{
ChampionTitleInfo.AwardHarrowerTitle(mobile);
ChampionTitleSystem.AwardHarrowerTitle(mobile);
}
}