namespace Aeshnidae.Enlightenment; /// /// Makes a character eligible to enlighten, for testing. It does not enlighten them. /// /// The distinction is the whole point of this file. The Font is the only thing that /// may grant an enlightenment (see Commands.cs and Guard.cs for why), and nothing /// here touches PropertyInt.Enlightenment. What this does is stand a test character /// at the door with every prerequisite met: the level the Font asks for, every aura /// at its cap, and Master rank in the Celestial Hand. They still have to walk up to /// the Font, get the prompt, and say yes - which is exactly the path being tested. /// /// Every call is logged with who ran it and on whom. It is Admin-only and it is loud. /// public static class Prep { public static string Run(Player admin, Player target) { var settings = Mod.Settings; var sb = new StringBuilder(); sb.AppendLine($"Preparing {target.Name} for enlightenment #{target.Enlightenment + 1}:"); // ---- level ---------------------------------------------------------- var requiredLevel = Requirements.LevelFor(target); var level = target.Level ?? 1; if (level < requiredLevel) { var table = DatManager.PortalDat.XpTable.CharacterLevelXPList; if (requiredLevel >= table.Count) { sb.AppendLine($" level : cannot raise to {requiredLevel} - the XP table stops at {table.Count - 1}. " + "Raise Aeshnidae.MaxLevel or lower the requirement."); } else { var delta = (long)table[requiredLevel] - (target.TotalExperience ?? 0); if (delta > 0) RaiseLevel(target, delta); sb.AppendLine($" level : {level} -> {requiredLevel} (+{delta:N0} xp, admin type, unscaled)"); } } else { sb.AppendLine($" level : {level}, already >= {requiredLevel}"); } // ---- auras ---------------------------------------------------------- if (settings.RequireAllLuminanceAuras) { var raised = new List(); foreach (var (name, property, _, max) in Requirements.Auras) { var current = target.GetProperty(property) ?? 0; if (current >= max) continue; target.SetProperty(property, max); target.Session.Network.EnqueueSend(new GameMessagePrivateUpdatePropertyInt(target, property, max)); raised.Add($"{name} {current}->{max}"); } sb.AppendLine(raised.Count == 0 ? " auras : all already at cap" : $" auras : {string.Join(", ", raised)}"); } // ---- society -------------------------------------------------------- if (settings.RequireSocietyMaster) { if (Requirements.IsSocietyMaster(target)) { sb.AppendLine(" society : already a Master"); } else { MakeCelestialHandMaster(target); sb.AppendLine(" society : Celestial Hand Master (rank 1001)"); } } // ---- the one thing this cannot do --------------------------------------- if (settings.RequiredFreeInventorySlots > 0) { var free = target.GetFreeInventorySlots(); sb.AppendLine(free >= settings.RequiredFreeInventorySlots ? $" pack : {free} free slots, ok" : $" pack : {free} free slots, NEEDS {settings.RequiredFreeInventorySlots} - clear some space by hand"); } target.SaveBiotaToDatabase(); ModManager.Log($"[{Mod.Name}] PREP by {admin.Name} ({admin.Guid.Full:X8}) on {target.Name} ({target.Guid.Full:X8}): " + $"level {level}->{Math.Max(level, requiredLevel)}, auras capped, society master", ModManager.LogLevel.Warn); sb.AppendLine(); sb.AppendLine("Prerequisites applied. Enlightenment itself is not - use the Font for that."); return sb.ToString().TrimEnd(); } /// /// Straight into ACE's private UpdateXpAndLevel rather than through GrantXP. /// /// GrantXP is where Aeshnidae.QuestBonus applies its multiplier, to every XpType /// including Admin, so an exact delta through it lands somewhere above the target /// level. UpdateXpAndLevel is the step after that prefix: it adds the xp, runs /// CheckForLevelup, grants skill credits per level, and sends the client updates - /// everything a real level-up does, minus the bonus. /// /// Run on the player's own action queue, as GrantXP itself does. /// private static void RaiseLevel(Player target, long delta) { var method = AccessTools.Method(typeof(Player), "UpdateXpAndLevel", new[] { typeof(long), typeof(XpType) }); if (method is null) throw new MissingMethodException("Player.UpdateXpAndLevel(long, XpType) not found - ACE changed; fix Prep.RaiseLevel"); target.EnqueueAction(new ActionEventDelegate(() => method.Invoke(target, new object[] { delta, XpType.Admin }))); } /// /// The same sequence ACE's own /faction ch 5 runs, so the character is a Master /// by every test the world applies - the rank property, the faction bits, and /// the quest flags society NPCs branch on. /// private static void MakeCelestialHandMaster(Player target) { target.Faction1Bits = FactionBits.CelestialHand; target.SocietyRankCelhan = 1001; target.SocietyRankEldweb = null; target.SocietyRankRadblo = null; target.QuestManager.SetQuestBits("SocietyMember", (int)FactionBits.CelestialHand, true); target.QuestManager.SetQuestBits("SocietyFlag", (int)FactionBits.CelestialHand, true); target.QuestManager.SetQuestBits("SocietyMember", (int)(FactionBits.EldrytchWeb | FactionBits.RadiantBlood), false); target.QuestManager.SetQuestBits("SocietyFlag", (int)(FactionBits.EldrytchWeb | FactionBits.RadiantBlood), false); target.QuestManager.Stamp("CelestialHandMember"); target.QuestManager.Erase("EldrytchWebMember"); target.QuestManager.Erase("RadiantBloodMember"); target.Session.Network.EnqueueSend( new GameMessagePrivateUpdatePropertyInt(target, PropertyInt.Faction1Bits, (int)FactionBits.CelestialHand), new GameMessagePrivateUpdatePropertyInt(target, PropertyInt.SocietyRankCelhan, 1001), new GameMessagePrivateUpdatePropertyInt(target, PropertyInt.SocietyRankEldweb, 0), new GameMessagePrivateUpdatePropertyInt(target, PropertyInt.SocietyRankRadblo, 0)); } }