using System.Runtime.CompilerServices; namespace Aeshnidae.AwakenedRewards; /// /// Tells each player's own client the difficulty tier of the place they are standing in: /// PropertyInt 9051 on the player's own object, sent as a PRIVATE update (only that player /// receives it), never written to the character (2026-10-03). /// /// Why: awakened worlds and scaled dungeon copies hold the same monsters as real Dereth, /// with the same names and weenie ids, scaled 5% a tier. A client-side helper that learns /// monsters by weenie id (RynthAi's kill prediction, the damage panel) cannot tell a world-10 /// Drudge from a real one, so a fight in a world spoiled what it had learned for Dereth. With /// the tier on the player it can keep each tier's numbers apart. /// /// What the tier is: of the player's current landblock - the /// copy's tier by InstancesNoDat's own rule (Scaling.TierFor: a personal copy by whoever /// opened it, an awakened world by its number), 0 for real Dereth and for every copy that is /// not scaled. Every monster in one copy or world shares that tier. /// /// When it is sent: on login, on every finished teleport, on arrival in a copy (InstancesNoDat's /// arrival hook), and from the heartbeat whenever the answer differs from the last one sent - /// which covers walking from one landblock into another. A value is only re-sent when it /// changes, except that login always sends one, so a client that has seen the property at all /// knows where it stands. Tier 0 is sent too: "seen, and unscaled". /// /// Not written to the character on purpose: it describes the place, not the character, and a /// saved value would be stale at the next login until the first send corrected it. The client /// keeps it in its own copy of the player's properties for the session. /// [HarmonyPatch] public static class TierFeed { /// PropertyInt 9051: the difficulty tier where the player stands (0 = unscaled). Sent, never saved. public const int PropertyId = 9051; public static readonly PropertyInt Property = (PropertyInt)PropertyId; private sealed class Sent { public int Tier; } /// The last tier sent to each player, kept with the player object (gone with it). private static ConditionalWeakTable _sent = new(); /// Sends since load, for the console readout. public static int Sends; /// Forget what was sent (the mod unloading): the next load re-sends to everyone. public static void Reset() { _sent = new ConditionalWeakTable(); Sends = 0; } /// The tier the player's client was last told, or null if nothing was sent this load. public static int? LastSent(Player player) => player is not null && _sent.TryGetValue(player, out var s) ? s.Tier : null; /// The tier where the player stands right now (0 = real Dereth or unscaled). public static int TierHere(Player player) => Instances.TierOf(player?.CurrentLandblock); /// /// Sends the tier if it differs from the last one sent to this player (or always, with /// ). Never throws. /// public static void Check(Player? player, string why, bool force = false) { try { if (player?.Session is null || !Mod.Settings.SendTierToClient) return; // Not placed yet (a login still loading): the heartbeat comes back for them. if (player.CurrentLandblock is null) return; Send(player, TierHere(player), why, force); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] tier feed for {player?.Name} ({why}) failed: {ex.Message}", ModManager.LogLevel.Warn); } } /// /// The arrival hook's form: InstancesNoDat names the copy's tier itself, from the relocation /// INTO the copy - a prefix, so the player's CurrentLandblock may still be the place they left. /// public static void Arrived(Player? player, int tier) { try { if (player?.Session is null || !Mod.Settings.SendTierToClient) return; Send(player, Math.Max(0, tier), "arrival", force: false); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] tier feed for {player?.Name} (arrival) failed: {ex.Message}", ModManager.LogLevel.Warn); } } private static void Send(Player player, int tier, string why, bool force) { if (!force && _sent.TryGetValue(player, out var last) && last.Tier == tier) return; player.Session.Network.EnqueueSend(new GameMessagePrivateUpdatePropertyInt(player, Property, tier)); _sent.AddOrUpdate(player, new Sent { Tier = tier }); Sends++; if (Mod.Settings.LogTierFeed) ModManager.Log($"[{Mod.Name}] tier feed: {player.Name} told tier {tier} ({why})"); } /// Everyone online, now (the mod loading: what a previous load sent is forgotten). public static void CheckAllOnline(string why) { try { foreach (var player in PlayerManager.GetAllOnline()) Check(player, why); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] tier feed sweep ({why}) failed: {ex.Message}", ModManager.LogLevel.Warn); } } [HarmonyPostfix] [HarmonyPatch(typeof(Player), nameof(Player.PlayerEnterWorld))] public static void PostEnterWorld(Player __instance) => Check(__instance, "login", force: true); [HarmonyPostfix] [HarmonyPatch(typeof(Player), nameof(Player.OnTeleportComplete))] public static void PostTeleportComplete(Player __instance) => Check(__instance, "teleport"); [HarmonyPostfix] [HarmonyPatch(typeof(Player), nameof(Player.Heartbeat))] public static void PostHeartbeat(Player __instance) => Check(__instance, "heartbeat"); }