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);
}
}