namespace Aeshnidae.Awakens; /// /// Three patches. /// /// DatManager.Initialize (postfix): ModManager starts mods before the dats are /// loaded, so the XP table is grown the moment it exists (LevelCap.Extend). /// /// Player.UpdateXpAndLevel and Player.CheckForLevelup (prefixes that replace the /// originals): ACE's bodies, line for line, with the character's own cap /// (LevelCap.Of) where they read the table's end, and no skill credits paid on /// the climb for an awakened character. Replacing rather than wrapping because /// the cap is read inside a loop and at the top of each method, where a /// prefix or postfix cannot reach. When the mod is off both fall through to ACE. /// /// The credit rule: CheckForLevelup pays skill credits at every credit level as /// a character climbs; an awakened character kept every credit from the first /// climb, so the second must not pay them again (NoLevelCreditsAfterAwakening). /// [HarmonyPatch] public static class Patches { [HarmonyPostfix] [HarmonyPatch(typeof(DatManager), nameof(DatManager.Initialize), new[] { typeof(string), typeof(bool), typeof(bool) })] public static void PostDatManagerInitialize() => LevelCap.Extend("dats loaded"); private static readonly Action UpdateXpVitae = AccessTools.MethodDelegate>(AccessTools.Method(typeof(Player), "UpdateXpVitae")); /// Player.UpdateXpAndLevel, stopping at the character's cap rather than the table's end. [HarmonyPrefix] [HarmonyPatch(typeof(Player), "UpdateXpAndLevel")] public static bool PreUpdateXpAndLevel(Player __instance, long amount, XpType xpType) { if (!Mod.Settings.Enabled) return true; try { var cap = LevelCap.Of(__instance); var capXp = LevelCap.XpAt(cap); if (__instance.Level != cap) { var addAmount = amount; var amountLeftToEnd = (long)capXp - __instance.TotalExperience ?? 0; if (amount > amountLeftToEnd) addAmount = amountLeftToEnd; __instance.AvailableExperience += addAmount; __instance.TotalExperience += addAmount; __instance.Session.Network.EnqueueSend( new GameMessagePrivateUpdatePropertyInt64(__instance, PropertyInt64.TotalExperience, __instance.TotalExperience ?? 0), new GameMessagePrivateUpdatePropertyInt64(__instance, PropertyInt64.AvailableExperience, __instance.AvailableExperience ?? 0)); CheckForLevelup(__instance, cap); } if (xpType == XpType.Quest) __instance.Session.Network.EnqueueSend(new GameMessageSystemChat($"You've earned {amount:N0} experience.", ChatMessageType.Broadcast)); if (__instance.HasVitae && xpType != XpType.Allegiance) UpdateXpVitae(__instance, amount); return false; } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] UpdateXpAndLevel for {__instance?.Name} failed, falling back to ACE: {ex.Message}", ModManager.LogLevel.Error); return true; } } /// Player.CheckForLevelup, for the odd caller that is not UpdateXpAndLevel (admin level commands). [HarmonyPrefix] [HarmonyPatch(typeof(Player), "CheckForLevelup")] public static bool PreCheckForLevelup(Player __instance) { if (!Mod.Settings.Enabled) return true; try { CheckForLevelup(__instance, LevelCap.Of(__instance)); return false; } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] CheckForLevelup for {__instance?.Name} failed, falling back to ACE: {ex.Message}", ModManager.LogLevel.Error); return true; } } /// ACE's CheckForLevelup with `cap` for `maxLevel`, and the credit rule. private static void CheckForLevelup(Player player, int cap) { var xpTable = DatManager.PortalDat.XpTable; if (player.Level >= cap) return; // Credits are withheld from an awakened character only when the awakening let // them keep the credits; the reset takes them, and the climb must pay them again. var payCredits = Mod.Settings.ResetSkillsAndAttributes || !Mod.Settings.NoLevelCreditsAfterAwakening || Awakening.CountOf(player) <= 0; var startingLevel = player.Level; var creditEarned = false; // increases until the correct level is found while ((ulong)(player.TotalExperience ?? 0) >= xpTable.CharacterLevelXPList[(player.Level ?? 0) + 1]) { player.Level++; // increase the skill credits if the chart allows this level to grant a credit if (payCredits && xpTable.CharacterLevelSkillCreditList[player.Level ?? 0] > 0) { player.AvailableSkillCredits += (int)xpTable.CharacterLevelSkillCreditList[player.Level ?? 0]; player.TotalSkillCredits += (int)xpTable.CharacterLevelSkillCreditList[player.Level ?? 0]; creditEarned = true; } // break if we reach max if (player.Level == cap) { player.PlayParticleEffect(PlayScript.WeddingBliss, player.Guid); break; } } if (player.Level <= startingLevel) return; var message = (player.Level == cap) ? $"You have reached the maximum level of {player.Level}!" : $"You are now level {player.Level}!"; message += (player.AvailableSkillCredits > 0) ? $"\nYou have {player.AvailableExperience:#,###0} experience points and {player.AvailableSkillCredits} skill credits available to raise skills and attributes." : $"\nYou have {player.AvailableExperience:#,###0} experience points available to raise skills and attributes."; var levelUp = new GameMessagePrivateUpdatePropertyInt(player, PropertyInt.Level, player.Level ?? 1); var currentCredits = new GameMessagePrivateUpdatePropertyInt(player, PropertyInt.AvailableSkillCredits, player.AvailableSkillCredits ?? 0); if (player.Level != cap && !creditEarned && payCredits) { var nextLevelWithCredits = 0; for (var i = (player.Level ?? 0) + 1; i <= cap; i++) { if (xpTable.CharacterLevelSkillCreditList[i] > 0) { nextLevelWithCredits = i; break; } } if (nextLevelWithCredits > 0) message += $"\nYou will earn another skill credit at level {nextLevelWithCredits}."; } if (player.Fellowship != null) player.Fellowship.OnFellowLevelUp(player); if (player.AllegianceNode != null) player.AllegianceNode.OnLevelUp(); player.Session.Network.EnqueueSend(levelUp); player.SetMaxVitals(); // play level up effect player.PlayParticleEffect(PlayScript.LevelUp, player.Guid); player.Session.Network.EnqueueSend(new GameMessageSystemChat(message, ChatMessageType.Advancement), currentCredits); } }