mirror of
https://github.com/modernuo/ModernUO
synced 2026-08-11 22:23:06 -04:00
## Summary - Refactors the poison system to separate `Index` (globally unique ID) from `Level` (tier within a family), enabling multiple poison families (Standard, Darkglow, Parasitic) to coexist without collisions - Implements Darkglow and Parasitic poison special effects from Mondain's Legacy: Darkglow boosts damage by 10% when attacker is ranged, Parasitic heals the attacker for damage dealt in melee range - Fixes several bugs: `Register()` crashing on duplicate `Level` values across families, `IncreaseLevel()` crossing family boundaries, `InfectiousStrike` and `NinjaWeapons` stripping poison family via level-based lookups, and `ArchCure`/`CleansingWinds` using raw `Level + 1` instead of `IncreaseLevel()` ## Changes **`Projects/Server/Poison.cs`** — Adds `PoisonFamily` enum and abstract `Family` property. Adds `Index` as unique identifier. Fixes `Register()` to check `Index` uniqueness (not `Level`) and validate the new poison's name (not the existing one's). Fixes `IncreaseLevel()` to use `Index + 1`, naturally respecting family boundaries via Index gaps. Replaces linear name lookup with `Dictionary`-based `PoisonsByName`. **`Projects/UOContent/Misc/Poison.cs`** — Adds `family` parameter to `PoisonImpl`. Implements Darkglow effect (10% damage boost when `From` >1 tile, cliloc 1072850) and Parasitic effect (heals `From` for damage dealt within 1 tile, cliloc 1060203) in `PoisonTimer.OnTick()`. Renames `m_` fields to `_` convention. **`Projects/UOContent/Misc/PoisonKinds.cs`** — New file. Moves poison registration out of `PoisonImpl` into `PoisonKinds.Configure()`. Adds `PoisonFamily` to Darkglow/Parasitic registrations. Provides extension properties (`Lesser`, `Deadly`, `LesserDarkglow`, etc.), `GetPoison(int level)` (standard-only), `GetPoisonByFamilyAndLevel()`, and `IsDarkglow`/`IsParasitic` instance helpers. **`Projects/UOContent/Items/Weapons/Abilities/InfectiousStrike.cs`** — Family-aware poison scaling: Darkglow caps at Deadly (Poisoning/33.3), Parasitic caps at Lethal (Poisoning/25), Standard unchanged. Level bump uses `IncreaseLevel()` with family boundary check. **`Projects/UOContent/Items/Skill Items/Ninjitsu/NinjaWeapons.cs`** — EvilOmen level bump uses `Poison.IncreaseLevel()` instead of `Poison.GetPoison(Level + 1)`. **`Projects/UOContent/Spells/Fourth/ArchCure.cs`** and **`CleansingWindsSpell.cs`** — Replace `poison.Level + 1` with `Poison.IncreaseLevel(poison).Level` for family-safe cure chance calculation. **`Projects/Server/Serialization/SerializationExtensions.cs`** — Serializes/deserializes `Index` instead of `Level`. **`DarkglowPotion.cs`** / **`ParasiticPotion.cs`** — Point to actual Darkglow/Parasitic poisons instead of placeholder `Greater`. **`PotionKeg.cs`** / **`BasePotion.cs`** — Adds Darkglow, Parasitic, Invisibility, and FlintsPungentBrew to `PotionEffect` enum and keg label support. ## Test plan - [ ] `dotnet build` compiles cleanly (verified, 0 warnings 0 errors) - [ ] Verify `PoisonKinds.Configure()` registers all poisons without throwing (Register bug fix) - [ ] Standard poison behavior unchanged — PoisonField, PoisonSpell, SerpentArrow, SavageShaman, TrappableContainer all use `GetPoison(int level)` which now correctly filters to Standard family - [ ] Darkglow: poison tick deals +10% damage when attacker is >1 tile away, sends "Darkglow poison increases your damage!" message - [ ] Parasitic: poison tick heals attacker for damage dealt when within 1 tile, sends heal message - [ ] InfectiousStrike preserves poison family and respects family-specific skill scaling - [ ] EvilOmen + NinjaWeapons level bump stays within poison family - [ ] ArchCure/CleansingWinds cure chance calculations work correctly across all poison families - [ ] Serialization round-trips correctly using Index
191 lines
5.7 KiB
C#
191 lines
5.7 KiB
C#
/*************************************************************************
|
|
* ModernUO *
|
|
* Copyright 2019-2026 - ModernUO Development Team *
|
|
* Email: hi@modernuo.com *
|
|
* File: SerializationExtensions.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 System.Collections.Generic;
|
|
using Server.Guilds;
|
|
|
|
namespace Server;
|
|
|
|
public static class SerializationExtensions
|
|
{
|
|
private static readonly Dictionary<Type, Func<Serial, bool, ISerializable>> _directFinderTable = new();
|
|
private static readonly Dictionary<Type, Func<Serial, bool, ISerializable>> _searchTable = new();
|
|
|
|
public static void RegisterFindEntity(this Type type, Func<Serial, bool, ISerializable> func)
|
|
{
|
|
_searchTable[type] = func;
|
|
}
|
|
|
|
public static T ReadEntity<T>(this IGenericReader reader) where T : class, ISerializable
|
|
{
|
|
var serial = reader.ReadSerial();
|
|
var typeT = typeof(T);
|
|
|
|
T entity;
|
|
|
|
// Add to this list when creating new serializable types
|
|
if (typeof(BaseGuild).IsAssignableFrom(typeT))
|
|
{
|
|
// If we check for `entity.Deleted` here during deserialization then all guilds are deleted because
|
|
// Deleted -> Disbanded -> No leader, which is the case before deserialization.
|
|
// TODO: Use a deleted flag instead, and actively check for disbanded guilds properly.
|
|
return World.FindGuild(serial) as T;
|
|
}
|
|
|
|
if (typeof(IEntity).IsAssignableFrom(typeT))
|
|
{
|
|
return World.FindEntity<IEntity>(serial) as T;
|
|
}
|
|
|
|
if (_directFinderTable.TryGetValue(typeT, out var finder))
|
|
{
|
|
return finder(serial, false) as T;
|
|
}
|
|
|
|
Type type = null;
|
|
foreach (var baseType in _searchTable.Keys)
|
|
{
|
|
if (baseType.IsAssignableFrom(typeT))
|
|
{
|
|
type = baseType;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (type == null)
|
|
{
|
|
type = typeT;
|
|
while (true)
|
|
{
|
|
var baseType = type?.BaseType;
|
|
|
|
// Find the parent class with ISerializable registered. To do this we break on it's parent class (or object)
|
|
// that doesn't have ISerializable implemented.
|
|
if (baseType?.GetInterface("ISerializable") == null && type?.GetInterface("ISerializable") != null)
|
|
{
|
|
break;
|
|
}
|
|
|
|
type = baseType;
|
|
}
|
|
|
|
throw new Exception($"No FindEntity registered for '{type.FullName}'.");
|
|
}
|
|
|
|
finder = _searchTable[type];
|
|
_directFinderTable[type] = finder;
|
|
return finder(serial, false) as T;
|
|
}
|
|
|
|
public static List<T> ReadEntityList<T>(
|
|
this IGenericReader reader,
|
|
bool nullIfEmpty = false
|
|
) where T : class, ISerializable
|
|
{
|
|
var count = reader.ReadInt();
|
|
|
|
if (count == 0 && nullIfEmpty)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var list = new List<T>(count);
|
|
|
|
for (var i = 0; i < count; ++i)
|
|
{
|
|
var entity = reader.ReadEntity<T>();
|
|
if (entity != null)
|
|
{
|
|
list.Add(entity);
|
|
}
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
public static HashSet<T> ReadEntitySet<T>(
|
|
this IGenericReader reader,
|
|
bool nullIfEmpty = false
|
|
) where T : class, ISerializable
|
|
{
|
|
var count = reader.ReadInt();
|
|
|
|
if (count == 0 && nullIfEmpty)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var set = new HashSet<T>(count);
|
|
|
|
for (var i = 0; i < count; ++i)
|
|
{
|
|
var entity = reader.ReadEntity<T>();
|
|
if (entity != null)
|
|
{
|
|
set.Add(entity);
|
|
}
|
|
}
|
|
|
|
return set;
|
|
}
|
|
|
|
public static void Write(this IGenericWriter writer, ISerializable value)
|
|
{
|
|
writer.Write(value?.Deleted != false ? Serial.MinusOne : value.Serial);
|
|
}
|
|
|
|
public static void Write<T>(this IGenericWriter writer, ICollection<T> coll) where T : class, ISerializable
|
|
{
|
|
writer.Write(coll.Count);
|
|
foreach (var entry in coll)
|
|
{
|
|
writer.Write(entry);
|
|
}
|
|
}
|
|
|
|
public static void Write<T>(
|
|
this IGenericWriter writer, ICollection<T> coll, Action<IGenericWriter, T> action
|
|
) where T : class, ISerializable
|
|
{
|
|
if (coll == null)
|
|
{
|
|
writer.Write(0);
|
|
return;
|
|
}
|
|
|
|
writer.Write(coll.Count);
|
|
foreach (var entry in coll)
|
|
{
|
|
action(writer, entry);
|
|
}
|
|
}
|
|
|
|
public static void Write(this IGenericWriter writer, Poison p)
|
|
{
|
|
if (p == null)
|
|
{
|
|
writer.Write(false);
|
|
}
|
|
else
|
|
{
|
|
writer.Write(true);
|
|
writer.Write((byte)p.Index);
|
|
}
|
|
}
|
|
|
|
public static Poison ReadPoison(this IGenericReader reader) =>
|
|
reader.ReadBool() ? Poison.GetPoisonByIndex(reader.ReadByte()) : null;
|
|
}
|