namespace Aeshnidae.SkillMastery;
///
/// Two jobs: make the bonus real on the server, and make it visible on the client.
/// Nothing here writes mastery into a skill - see Mastery for why that matters.
///
[HarmonyPatch]
public static class Patches
{
/// Load a character's mastery into the cache as they enter the world.
[HarmonyPostfix]
[HarmonyPatch(typeof(Player), nameof(Player.PlayerEnterWorld))]
public static void PostPlayerEnterWorld(Player __instance)
{
try
{
Mastery.LoadInto(__instance);
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] failed to load mastery on login: {ex}", ModManager.LogLevel.Error);
}
}
///
/// The bonus itself. Base and Current are what every combat, magic and defence
/// formula reads, so adding it here is what makes mastery mean anything - and
/// because it is added on the way out rather than stored, nothing ACE does to the
/// skill can disturb it.
///
[HarmonyPostfix]
[HarmonyPatch(typeof(CreatureSkill), nameof(CreatureSkill.Base), MethodType.Getter)]
public static void PostBase(CreatureSkill __instance, ref uint __result)
{
try
{
__result += (uint)Mastery.BonusFor(__instance);
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] mastery bonus failed on Base: {ex}", ModManager.LogLevel.Error);
}
}
///
/// Current has already had enchantments and vitae applied by the time we see it,
/// so the bonus is scaled to match rather than added flat - see ScaledBonusFor.
///
[HarmonyPostfix]
[HarmonyPatch(typeof(CreatureSkill), nameof(CreatureSkill.Current), MethodType.Getter)]
public static void PostCurrent(CreatureSkill __instance, ref uint __result)
{
try
{
__result += (uint)Mastery.ScaledBonusFor(__instance);
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] mastery bonus failed on Current: {ex}", ModManager.LogLevel.Error);
}
}
///
/// Display, part one: incremental skill updates.
///
/// The client renders a skill as attributeFormula + InitLevel + Ranks, so the only
/// way to show the bonus without a dat change is to include it in the InitLevel we
/// transmit. The value is inflated for the duration of the constructor and put
/// straight back - the message carries the bonus, the stored skill never does.
///
[HarmonyPrefix]
[HarmonyPatch(typeof(GameMessagePrivateUpdateSkill), MethodType.Constructor,
new[] { typeof(WorldObject), typeof(CreatureSkill) })]
public static void PreUpdateSkill(CreatureSkill creatureSkill, out uint __state)
{
__state = creatureSkill?.InitLevel ?? 0;
if (creatureSkill is null)
return;
try
{
var bonus = Mastery.BonusFor(creatureSkill);
if (bonus > 0)
creatureSkill.InitLevel = __state + (uint)bonus;
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] mastery display failed on skill update: {ex}", ModManager.LogLevel.Error);
}
}
[HarmonyPostfix]
[HarmonyPatch(typeof(GameMessagePrivateUpdateSkill), MethodType.Constructor,
new[] { typeof(WorldObject), typeof(CreatureSkill) })]
public static void PostUpdateSkill(CreatureSkill creatureSkill, uint __state)
{
if (creatureSkill is not null)
creatureSkill.InitLevel = __state;
}
///
/// Display, part two: the full description sent at login, which writes every
/// skill's InitLevel in one pass. Same trick, applied to all of them at once.
///
[HarmonyPrefix]
[HarmonyPatch(typeof(GameEventPlayerDescription), MethodType.Constructor, new[] { typeof(Session) })]
public static void PrePlayerDescription(Session session, out Dictionary? __state)
{
__state = null;
try
{
var player = session?.Player;
if (player is null)
return;
var saved = new Dictionary();
foreach (Skill skill in Enum.GetValues(typeof(Skill)))
{
var creatureSkill = player.GetCreatureSkill(skill, false);
if (creatureSkill is null)
continue;
var bonus = Mastery.BonusFor(creatureSkill);
if (bonus <= 0)
continue;
saved[skill] = creatureSkill.InitLevel;
creatureSkill.InitLevel += (uint)bonus;
}
if (saved.Count > 0)
__state = saved;
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] mastery display failed on player description: {ex}",
ModManager.LogLevel.Error);
}
}
[HarmonyPostfix]
[HarmonyPatch(typeof(GameEventPlayerDescription), MethodType.Constructor, new[] { typeof(Session) })]
public static void PostPlayerDescription(Session session, Dictionary? __state)
{
if (__state is null || session?.Player is null)
return;
foreach (var (skill, initLevel) in __state)
{
var creatureSkill = session.Player.GetCreatureSkill(skill, false);
if (creatureSkill is not null)
creatureSkill.InitLevel = initLevel;
}
}
}