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