XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

DeathMod

【Terraria】DeathMod (now aka Soul Harvest)

公开
关注 0 Fork 0 Star 0
UTF-8
using SoulHarvest.Common;
using SoulHarvest.Items;
using SoulHarvest.Projectiles;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using Terraria;
using Terraria.ID;
using Terraria.Localization;
using Terraria.ModLoader;
using Terraria.ModLoader.IO;

namespace SoulHarvest;

public class SoulHarvest : Mod
{
    internal enum MessageType : byte
    {
        SyncSoulPlayer,
        RequestExtractEssence,
        RequestAbsorbEssence,
        RequestAltarUpgrade,
        OperationResult,
        SickleHitRequest,
        SyncInventorySlot,
        SyncDeathMark,
        SyncAltarItem,
        SyncDeathDomainState,
        SyncDeathDomainVisual,
        SyncReaperProgression,
        SyncReaperPublicState,
        SyncReaperCombat,
        RequestReaperStageUpgrade,
        RequestReaperSkillUpgrade,
        RequestReaperCommonUpgrade,
        RequestReaperFormSwitch,
        RequestReaperAction,
        SyncReaperTransaction,
        SyncBloodShieldConsumption,
        SyncReaperMovement,
        AcknowledgeReaperMovement,
        AcknowledgeReaperCharacterState,
        UploadReaperCharacterState
    }

    internal enum DeathDomainVisualAction : byte
    {
        SpawnTelegraph,
        SpawnHarvestSlash,
        ClearTelegraph
    }

    internal enum ReaperActionRequest : byte
    {
        SpecialStart,
        Ultimate,
        SpecialRelease,
        SpecialAimUpdate,
        PrimaryRelease
    }

    internal enum OperationResult : byte
    {
        EssenceExtracted,
        EssenceAbsorbed,
        NotEnoughSouls,
        UpgradeSucceeded,
        SickleRegressed,
        NotEnoughEssence,
        NotEnoughUpgradeSouls,
        HoldSickle,
        TooFarFromAltar,
        UpgradeMaxed,
        ReaperStageUnlocked,
        ReaperSkillUpgraded,
        ReaperCommonUpgraded,
        ReaperFormSwitched,
        ReaperProgressLocked,
        ReaperInvalidForm,
        ReaperActionUnavailable,
        InventoryFull,
        ReaperFormSwitchStarted
    }

    public static SoulHarvest? Current { get; private set; }
    public static ModKeybind? ToggleDeathDomainKeybind { get; private set; }
    public static ModKeybind? SpecialAttackKeybind { get; private set; }
    public static ModKeybind? UltimateKeybind { get; private set; }
    public static ModKeybind? FormWheelKeybind { get; private set; }

    public SoulHarvest()
    {
        Current = this;
    }

    public override void Load()
    {
        if (!Main.dedServ)
        {
            ToggleDeathDomainKeybind = KeybindLoader.RegisterKeybind(this, "ToggleDeathDomain", "O");
            SpecialAttackKeybind = KeybindLoader.RegisterKeybind(this, "SpecialAttack", "Mouse2");
            UltimateKeybind = KeybindLoader.RegisterKeybind(this, "Ultimate", "Mouse3");
            FormWheelKeybind = KeybindLoader.RegisterKeybind(this, "FormWheel", "V");
        }
    }

    public override void Unload()
    {
        ToggleDeathDomainKeybind = null;
        SpecialAttackKeybind = null;
        UltimateKeybind = null;
        FormWheelKeybind = null;
        Current = null;
    }

    public override void HandlePacket(BinaryReader reader, int whoAmI)
    {
        MessageType messageType = (MessageType)reader.ReadByte();
        switch (messageType)
        {
            case MessageType.SyncSoulPlayer:
            {
                byte playerIndex = reader.ReadByte();
                int souls = reader.ReadInt32();
                bool deathDomainEnabled = reader.ReadBoolean();
                bool deathDomainDescended = reader.ReadBoolean();
                if (Main.netMode == NetmodeID.MultiplayerClient && playerIndex < Main.maxPlayers)
                {
                    MyPlayer modPlayer = Main.player[playerIndex].GetModPlayer<MyPlayer>();
                    modPlayer.ReceiveSouls(souls);
                    modPlayer.ReceiveDeathDomainState(deathDomainEnabled, deathDomainDescended);
                    modPlayer.ReceiveDeathDomainProgression(reader);
                }
                break;
            }
            case MessageType.RequestExtractEssence:
                if (Main.netMode == NetmodeID.Server && whoAmI >= 0 && whoAmI < Main.maxPlayers && Main.player[whoAmI].active)
                    Main.player[whoAmI].GetModPlayer<MyPlayer>().TryExtractEssence();
                break;
            case MessageType.RequestAbsorbEssence:
                if (Main.netMode == NetmodeID.Server && whoAmI >= 0 && whoAmI < Main.maxPlayers && Main.player[whoAmI].active)
                    Main.player[whoAmI].GetModPlayer<MyPlayer>().TryAbsorbEssence();
                break;
            case MessageType.RequestAltarUpgrade:
            {
                DeathAltarUpgradeType upgradeType = (DeathAltarUpgradeType)reader.ReadByte();
                DeathAltarItemStorage storage = (DeathAltarItemStorage)reader.ReadByte();
                byte slot = reader.ReadByte();
                int expectedItemType = reader.ReadInt32();
                short tileX = reader.ReadInt16();
                short tileY = reader.ReadInt16();
                if (Main.netMode == NetmodeID.Server && whoAmI >= 0 && whoAmI < Main.maxPlayers && Main.player[whoAmI].active)
                {
                    DeathAltarUpgradeService.TryUpgrade(Main.player[whoAmI], upgradeType,
                        new DeathAltarItemReference(storage, slot, expectedItemType), tileX, tileY);
                }
                break;
            }
            case MessageType.SyncReaperProgression:
            {
                byte playerIndex = reader.ReadByte();
                if (Main.netMode == NetmodeID.MultiplayerClient && playerIndex < Main.maxPlayers)
                {
                    MyPlayer modPlayer = Main.player[playerIndex].GetModPlayer<MyPlayer>();
                    modPlayer.ReceiveDeathDomainProgression(reader);
                    modPlayer.ReceiveReaperProgression(reader);
                }
                else if (Main.netMode == NetmodeID.Server)
                {
                    // Compatibility path for the old upload format. Never gate
                    // reading on Player.active: SyncPlayer is sent during the join
                    // handshake, before the server marks the slot active. Leaving
                    // the body unread aborts HandlePacket and prevents every later
                    // server-authoritative Reaper action for this character.
                    ReceiveReaperCharacterStateUpload(reader, whoAmI);
                }
                break;
            }
            case MessageType.UploadReaperCharacterState:
            {
                if (Main.netMode == NetmodeID.Server)
                    ReceiveReaperCharacterStateUpload(reader, whoAmI);
                break;
            }
            case MessageType.AcknowledgeReaperCharacterState:
            {
                byte playerIndex = reader.ReadByte();
                if (Main.netMode == NetmodeID.MultiplayerClient
                    && playerIndex == Main.myPlayer
                    && playerIndex < Main.maxPlayers)
                {
                    Main.player[playerIndex].GetModPlayer<MyPlayer>()
                        .AcknowledgeServerCharacterState();
                }
                break;
            }
            case MessageType.SyncReaperTransaction:
            {
                byte playerIndex = reader.ReadByte();
                if (Main.netMode != NetmodeID.MultiplayerClient
                    || playerIndex >= Main.maxPlayers
                    || playerIndex != Main.myPlayer)
                {
                    break;
                }

                MyPlayer modPlayer = Main.player[playerIndex].GetModPlayer<MyPlayer>();
                modPlayer.ReceiveSouls(reader.ReadInt32());
                modPlayer.ReceiveReaperProgression(reader);
                int changedSlotCount = reader.ReadByte();
                for (int index = 0; index < changedSlotCount; index++)
                {
                    int slot = reader.ReadByte();
                    Item item = ItemIO.Receive(reader, readStack: true, readFavorite: true);
                    if (slot >= 0 && slot < 58)
                        Main.player[playerIndex].inventory[slot] = item;
                }
                Recipe.FindRecipes(canDelayCheck: true);
                break;
            }
            case MessageType.SyncReaperCombat:
            {
                byte playerIndex = reader.ReadByte();
                if (Main.netMode == NetmodeID.MultiplayerClient && playerIndex < Main.maxPlayers)
                    Main.player[playerIndex].GetModPlayer<MyPlayer>().ReceiveReaperCombat(reader);
                break;
            }
            case MessageType.SyncBloodShieldConsumption:
            {
                uint revision = reader.ReadUInt32();
                int absorbed = reader.ReadUInt16();
                if (TryGetRequestPlayer(whoAmI, out Player player))
                {
                    player.GetModPlayer<ReaperCombatPlayer>()
                        .AcceptOwnerBloodShieldConsumption(revision, absorbed);
                }
                break;
            }
            case MessageType.SyncReaperMovement:
            {
                byte playerIndex = reader.ReadByte();
                uint sequence = reader.ReadUInt32();
                Vector2 position = reader.ReadVector2();
                Vector2 velocity = reader.ReadVector2();
                if (Main.netMode == NetmodeID.MultiplayerClient
                    && playerIndex < Main.maxPlayers
                    && Main.player[playerIndex].active
                    && float.IsFinite(position.X)
                    && float.IsFinite(position.Y)
                    && float.IsFinite(velocity.X)
                    && float.IsFinite(velocity.Y))
                {
                    Main.player[playerIndex].position = position;
                    Main.player[playerIndex].velocity = velocity;
                    if (playerIndex == Main.myPlayer && sequence != 0)
                        SendReaperMovementAcknowledgement(sequence);
                }
                break;
            }
            case MessageType.AcknowledgeReaperMovement:
            {
                uint sequence = reader.ReadUInt32();
                if (TryGetRequestPlayer(whoAmI, out Player player))
                {
                    player.GetModPlayer<ReaperMovementAuthorityPlayer>()
                        .ConfirmAuthoritativeMovement(sequence);
                }
                break;
            }
            case MessageType.SyncReaperPublicState:
            {
                byte playerIndex = reader.ReadByte();
                ReaperFormId form = (ReaperFormId)reader.ReadByte();
                ReaperStage stage = (ReaperStage)reader.ReadByte();
                if (Main.netMode == NetmodeID.MultiplayerClient && playerIndex < Main.maxPlayers)
                    Main.player[playerIndex].GetModPlayer<MyPlayer>().ReceiveReaperPublicState(form, stage);
                break;
            }
            case MessageType.RequestReaperStageUpgrade:
            {
                ReaperFormId form = (ReaperFormId)reader.ReadByte();
                ReaperStage expectedTargetStage = (ReaperStage)reader.ReadByte();
                if (TryGetRequestPlayer(whoAmI, out Player player))
                {
                    bool success = ReaperProgressionService.TryUnlockNextStage(
                        player, form, expectedTargetStage, out ReaperProgressionFailure failure);
                    SendOperationResult(player, success ? OperationResult.ReaperStageUnlocked : MapReaperFailure(failure));
                }
                break;
            }
            case MessageType.RequestReaperSkillUpgrade:
            {
                ReaperFormId form = (ReaperFormId)reader.ReadByte();
                int skillIndex = reader.ReadByte();
                int expectedTargetLevel = reader.ReadByte();
                if (TryGetRequestPlayer(whoAmI, out Player player))
                {
                    bool success = ReaperProgressionService.TryUpgradeSkill(
                        player, form, skillIndex, expectedTargetLevel, out ReaperProgressionFailure failure);
                    SendOperationResult(player, success ? OperationResult.ReaperSkillUpgraded : MapReaperFailure(failure));
                }
                break;
            }
            case MessageType.RequestReaperCommonUpgrade:
            {
                ReaperCommonNode node = (ReaperCommonNode)reader.ReadByte();
                int expectedTargetLevel = reader.ReadInt32();
                if (TryGetRequestPlayer(whoAmI, out Player player))
                {
                    bool success = ReaperProgressionService.TryUpgradeCommonNode(
                        player, node, expectedTargetLevel, out ReaperProgressionFailure failure);
                    SendOperationResult(player, success ? OperationResult.ReaperCommonUpgraded : MapReaperFailure(failure));
                }
                break;
            }
            case MessageType.RequestReaperFormSwitch:
            {
                ReaperFormId form = (ReaperFormId)reader.ReadByte();
                if (TryGetRequestPlayer(whoAmI, out Player player))
                {
                    bool success = ReaperProgressionService.TrySwitchForm(player, form, out ReaperProgressionFailure failure);
                    SendOperationResult(player, success
                        ? OperationResult.ReaperFormSwitchStarted
                        : MapReaperFailure(failure));
                }
                break;
            }
            case MessageType.RequestReaperAction:
            {
                ReaperActionRequest action = (ReaperActionRequest)reader.ReadByte();
                Vector2 aim = new(reader.ReadSingle(), reader.ReadSingle());
                if (TryGetRequestPlayer(whoAmI, out Player player))
                {
                    bool success;
                    if (action == ReaperActionRequest.SpecialRelease)
                    {
                        success = ReaperCombatService.TryReleaseSpecial(player);
                    }
                    else if (action == ReaperActionRequest.PrimaryRelease)
                    {
                        ReaperCombatService.ReleaseDeathPrimaryHold(player);
                        success = true;
                    }
                    else if (action == ReaperActionRequest.SpecialAimUpdate
                        && float.IsFinite(aim.X) && float.IsFinite(aim.Y))
                    {
                        success = ReaperCombatService.TryUpdateSpecialTarget(player, aim);
                    }
                    else if (action is ReaperActionRequest.SpecialStart or ReaperActionRequest.Ultimate
                        && float.IsFinite(aim.X) && float.IsFinite(aim.Y))
                    {
                        success = action == ReaperActionRequest.Ultimate
                            ? ReaperCombatService.TryStartUltimate(player, aim)
                            : ReaperCombatService.TryUpdateSpecialTarget(player,
                                aim)
                                || ReaperCombatService.TryStartSpecial(player,
                                    aim);
                    }
                    else
                    {
                        success = false;
                    }

                    // Late and duplicate release requests are harmless and intentionally silent.
                    if (!success && action is not ReaperActionRequest.SpecialRelease
                        and not ReaperActionRequest.SpecialAimUpdate)
                        SendOperationResult(player, OperationResult.ReaperActionUnavailable);
                }
                break;
            }
            case MessageType.OperationResult:
            {
                OperationResult result = (OperationResult)reader.ReadByte();
                if (Main.netMode == NetmodeID.MultiplayerClient)
                    ShowOperationResult(result);
                break;
            }
            case MessageType.SickleHitRequest:
            {
                int projectileIdentity = reader.ReadInt32();
                short npcIndex = reader.ReadInt16();
                if (Main.netMode == NetmodeID.Server)
                    HandleSickleHitRequest(whoAmI, projectileIdentity, npcIndex);
                break;
            }
            case MessageType.SyncInventorySlot:
            {
                if (Main.netMode != NetmodeID.MultiplayerClient)
                    break;

                byte slot = reader.ReadByte();
                Item item = ItemIO.Receive(reader, readStack: true, readFavorite: true);
                if (slot < 58)
                {
                    Main.LocalPlayer.inventory[slot] = item;
                    Recipe.FindRecipes(canDelayCheck: true);
                }
                break;
            }
            case MessageType.SyncDeathMark:
            {
                short npcIndex = reader.ReadInt16();
                bool marked = reader.ReadBoolean();
                if (Main.netMode == NetmodeID.MultiplayerClient
                    && npcIndex >= 0
                    && npcIndex < Main.maxNPCs
                    && Main.npc[npcIndex].active)
                {
                    Main.npc[npcIndex].GetGlobalNPC<MyGlobalNPC>().ReceiveDeathMarkState(marked);
                }
                break;
            }
            case MessageType.SyncAltarItem:
            {
                if (Main.netMode != NetmodeID.MultiplayerClient)
                    break;

                DeathAltarItemStorage storage = (DeathAltarItemStorage)reader.ReadByte();
                byte slot = reader.ReadByte();
                Item item = ItemIO.Receive(reader, readStack: true, readFavorite: true);
                Item[] container = storage == DeathAltarItemStorage.Inventory
                    ? Main.LocalPlayer.inventory
                    : Main.LocalPlayer.armor;
                if (slot < container.Length)
                {
                    container[slot] = item;
                    Recipe.FindRecipes(canDelayCheck: true);
                }
                break;
            }
            case MessageType.SyncDeathDomainState:
            {
                if (Main.netMode == NetmodeID.Server)
                {
                    bool enabled = reader.ReadBoolean();
                    bool descended = reader.ReadBoolean();
                    if (whoAmI >= 0
                        && whoAmI < Main.maxPlayers
                        && Main.player[whoAmI].active
                        && Main.player[whoAmI].GetModPlayer<MyPlayer>().ServerCharacterStateReady)
                    {
                        MyPlayer modPlayer = Main.player[whoAmI].GetModPlayer<MyPlayer>();
                        DeathNecklace? necklace = modPlayer.GetEquippedDeathNecklace();
                        enabled = enabled && necklace is not null;
                        descended = enabled
                            && descended
                            && necklace?.DeathDescentUnlocked == true;
                        modPlayer.ReceiveDeathDomainState(enabled, descended);
                        BroadcastDeathDomainState(whoAmI, enabled, descended);
                    }
                }
                else if (Main.netMode == NetmodeID.MultiplayerClient)
                {
                    byte playerIndex = reader.ReadByte();
                    bool enabled = reader.ReadBoolean();
                    bool descended = reader.ReadBoolean();
                    if (playerIndex < Main.maxPlayers)
                        Main.player[playerIndex].GetModPlayer<MyPlayer>().ReceiveDeathDomainState(enabled, descended);
                }
                break;
            }
            case MessageType.SyncDeathDomainVisual:
            {
                DeathDomainVisualAction action = (DeathDomainVisualAction)reader.ReadByte();
                byte playerIndex = reader.ReadByte();
                short npcIndex = reader.ReadInt16();
                Vector2 position = new(reader.ReadSingle(), reader.ReadSingle());
                float rotation = reader.ReadSingle();
                float scale = reader.ReadSingle();
                byte slashCount = reader.ReadByte();
                float mastery = reader.ReadSingle();
                short duration = reader.ReadInt16();
                bool requiem = reader.ReadBoolean();
                if (Main.netMode == NetmodeID.MultiplayerClient)
                {
                    if (action == DeathDomainVisualAction.ClearTelegraph)
                    {
                        ClearClientDeathDomainTelegraph(playerIndex, npcIndex);
                    }
                    else
                    {
                        SpawnClientDeathDomainVisual(
                            action,
                            playerIndex,
                            npcIndex,
                            position,
                            rotation,
                            scale,
                            slashCount,
                            mastery,
                            duration,
                            requiem);
                    }
                }
                break;
            }
            default:
                Logger.Warn($"Unknown SoulHarvest packet: {messageType}");
                break;
        }
    }

    public static void RequestReaperStageUpgrade(ReaperFormId form, ReaperStage expectedTargetStage)
    {
        Player player = Main.LocalPlayer;
        if (Main.netMode == NetmodeID.SinglePlayer)
        {
            bool success = ReaperProgressionService.TryUnlockNextStage(
                player, form, expectedTargetStage, out ReaperProgressionFailure failure);
            ShowOperationResult(success ? OperationResult.ReaperStageUnlocked : MapReaperFailure(failure));
            return;
        }

        if (Main.netMode != NetmodeID.MultiplayerClient)
            return;
        ModPacket packet = ModContent.GetInstance<SoulHarvest>().GetPacket();
        packet.Write((byte)MessageType.RequestReaperStageUpgrade);
        packet.Write((byte)form);
        packet.Write((byte)expectedTargetStage);
        packet.Send();
    }

    public static void RequestReaperSkillUpgrade(ReaperFormId form, int skillIndex, int expectedTargetLevel)
    {
        Player player = Main.LocalPlayer;
        if (Main.netMode == NetmodeID.SinglePlayer)
        {
            bool success = ReaperProgressionService.TryUpgradeSkill(
                player, form, skillIndex, expectedTargetLevel, out ReaperProgressionFailure failure);
            ShowOperationResult(success ? OperationResult.ReaperSkillUpgraded : MapReaperFailure(failure));
            return;
        }

        if (Main.netMode != NetmodeID.MultiplayerClient)
            return;
        ModPacket packet = ModContent.GetInstance<SoulHarvest>().GetPacket();
        packet.Write((byte)MessageType.RequestReaperSkillUpgrade);
        packet.Write((byte)form);
        packet.Write((byte)Math.Clamp(skillIndex, 0, ReaperDefinitions.SkillsPerBranch - 1));
        packet.Write((byte)Math.Clamp(expectedTargetLevel, 0, byte.MaxValue));
        packet.Send();
    }

    public static void RequestReaperCommonUpgrade(ReaperCommonNode node, int expectedTargetLevel)
    {
        Player player = Main.LocalPlayer;
        if (Main.netMode == NetmodeID.SinglePlayer)
        {
            bool success = ReaperProgressionService.TryUpgradeCommonNode(
                player, node, expectedTargetLevel, out ReaperProgressionFailure failure);
            ShowOperationResult(success ? OperationResult.ReaperCommonUpgraded : MapReaperFailure(failure));
            return;
        }

        if (Main.netMode != NetmodeID.MultiplayerClient)
            return;
        ModPacket packet = ModContent.GetInstance<SoulHarvest>().GetPacket();
        packet.Write((byte)MessageType.RequestReaperCommonUpgrade);
        packet.Write((byte)node);
        packet.Write(Math.Max(0, expectedTargetLevel));
        packet.Send();
    }

    public static void RequestReaperFormSwitch(ReaperFormId form)
    {
        Player player = Main.LocalPlayer;
        if (Main.netMode == NetmodeID.SinglePlayer)
        {
            bool success = ReaperProgressionService.TrySwitchForm(player, form, out ReaperProgressionFailure failure);
            if (!success)
                ShowOperationResult(MapReaperFailure(failure));
            return;
        }

        if (Main.netMode != NetmodeID.MultiplayerClient)
            return;
        ModPacket packet = ModContent.GetInstance<SoulHarvest>().GetPacket();
        packet.Write((byte)MessageType.RequestReaperFormSwitch);
        packet.Write((byte)form);
        packet.Send();
    }

    internal static void SendReaperAction(Player player, bool ultimate, Vector2 aim)
    {
        if (Main.netMode != NetmodeID.MultiplayerClient || player.whoAmI != Main.myPlayer)
            return;
        ModPacket packet = ModContent.GetInstance<SoulHarvest>().GetPacket();
        packet.Write((byte)MessageType.RequestReaperAction);
        packet.Write((byte)(ultimate ? ReaperActionRequest.Ultimate : ReaperActionRequest.SpecialStart));
        packet.Write(aim.X);
        packet.Write(aim.Y);
        packet.Send();
    }

    internal static void SendReaperSpecialRelease(Player player)
    {
        if (Main.netMode != NetmodeID.MultiplayerClient || player.whoAmI != Main.myPlayer)
            return;
        ModPacket packet = ModContent.GetInstance<SoulHarvest>().GetPacket();
        packet.Write((byte)MessageType.RequestReaperAction);
        packet.Write((byte)ReaperActionRequest.SpecialRelease);
        packet.Write(0f);
        packet.Write(0f);
        packet.Send();
    }

    internal static void SendReaperSpecialAim(Player player, Vector2 cursorOffset)
    {
        if (Main.netMode != NetmodeID.MultiplayerClient || player.whoAmI != Main.myPlayer)
            return;
        ModPacket packet = ModContent.GetInstance<SoulHarvest>().GetPacket();
        packet.Write((byte)MessageType.RequestReaperAction);
        packet.Write((byte)ReaperActionRequest.SpecialAimUpdate);
        packet.Write(cursorOffset.X);
        packet.Write(cursorOffset.Y);
        packet.Send();
    }

    internal static void SendBloodShieldConsumption(Player player, uint revision, int absorbed)
    {
        if (Main.netMode != NetmodeID.MultiplayerClient || player.whoAmI != Main.myPlayer)
            return;
        ModPacket packet = ModContent.GetInstance<SoulHarvest>().GetPacket();
        packet.Write((byte)MessageType.SyncBloodShieldConsumption);
        packet.Write(revision);
        packet.Write((ushort)Math.Clamp(absorbed, 0, ushort.MaxValue));
        packet.Send();
    }

    internal static void BroadcastReaperMovement(Player player, uint sequence)
    {
        if (Main.netMode != NetmodeID.Server)
            return;
        ModPacket packet = ModContent.GetInstance<SoulHarvest>().GetPacket();
        packet.Write((byte)MessageType.SyncReaperMovement);
        packet.Write((byte)player.whoAmI);
        packet.Write(sequence);
        packet.WriteVector2(player.position);
        packet.WriteVector2(player.velocity);
        packet.Send();
    }

    private static void SendReaperMovementAcknowledgement(uint sequence)
    {
        if (Main.netMode != NetmodeID.MultiplayerClient || sequence == 0)
            return;
        ModPacket packet = ModContent.GetInstance<SoulHarvest>().GetPacket();
        packet.Write((byte)MessageType.AcknowledgeReaperMovement);
        packet.Write(sequence);
        packet.Send();
    }

    internal static void SendReaperPrimaryRelease(Player player)
    {
        if (Main.netMode != NetmodeID.MultiplayerClient
            || player.whoAmI != Main.myPlayer)
        {
            return;
        }
        ModPacket packet = ModContent.GetInstance<SoulHarvest>().GetPacket();
        packet.Write((byte)MessageType.RequestReaperAction);
        packet.Write((byte)ReaperActionRequest.PrimaryRelease);
        packet.Write(0f);
        packet.Write(0f);
        packet.Send();
    }

    private static void SendReaperCharacterStateAcknowledgement(Player player)
    {
        if (Main.netMode != NetmodeID.Server
            || player.whoAmI < 0 || player.whoAmI >= Main.maxPlayers)
            return;

        ModPacket packet = ModContent.GetInstance<SoulHarvest>().GetPacket();
        packet.Write((byte)MessageType.AcknowledgeReaperCharacterState);
        packet.Write((byte)player.whoAmI);
        packet.Send(player.whoAmI);
    }

    private static void ReceiveReaperCharacterStateUpload(BinaryReader reader, int sender)
    {
        int souls = reader.ReadInt32();
        bool deathDomainEnabled = reader.ReadBoolean();
        bool deathDomainDescended = reader.ReadBoolean();

        if (sender < 0 || sender >= Main.maxPlayers)
        {
            // Consume the complete body even for an invalid sender so tModLoader
            // never observes an under-read mod packet.
            DeathDomainProgression discardedDomain = new();
            ReaperProgressionState discardedProgression = new();
            discardedDomain.NetReceive(reader);
            discardedProgression.NetReceive(reader);
            return;
        }

        Player player = Main.player[sender];
        MyPlayer modPlayer = player.GetModPlayer<MyPlayer>();
        bool initialized = modPlayer.TryInitializeServerCharacterState(
            souls,
            deathDomainEnabled,
            deathDomainDescended,
            reader);
        // A disconnect can race the final response after the upload has already
        // been dequeued. Keep the accepted state, but do not address a dead slot.
        bool canRespond = Netplay.Clients[sender].IsActive;
        if (initialized && canRespond)
            modPlayer.SyncPlayer(-1, -1, false);
        if (modPlayer.ServerCharacterStateReady && canRespond)
            SendReaperCharacterStateAcknowledgement(player);
    }

    internal static void SendReaperProgression(Player player, int toWho = -1, int fromWho = -1)
    {
        if (Main.netMode == NetmodeID.SinglePlayer)
            return;
        if (Main.netMode == NetmodeID.MultiplayerClient)
        {
            if (player.whoAmI != Main.myPlayer)
                return;
            ModPacket upload = ModContent.GetInstance<SoulHarvest>().GetPacket();
            upload.Write((byte)MessageType.UploadReaperCharacterState);
            MyPlayer modPlayer = player.GetModPlayer<MyPlayer>();
            upload.Write(modPlayer.Souls);
            upload.Write(modPlayer.DeathDomainEnabled);
            upload.Write(modPlayer.DeathDomainDescended);
            modPlayer.WriteDeathDomainProgression(upload);
            modPlayer.WriteReaperProgression(upload);
            upload.Send();
            return;
        }

        // The full tree is private character data. A targeted SyncPlayer call for
        // another client receives only the public active form below.
        bool ownerConnected = player.whoAmI >= 0
            && player.whoAmI < Main.maxPlayers
            && Netplay.Clients[player.whoAmI].IsActive;
        if (ownerConnected && (toWho < 0 || toWho == player.whoAmI))
        {
            ModPacket privatePacket = ModContent.GetInstance<SoulHarvest>().GetPacket();
            privatePacket.Write((byte)MessageType.SyncReaperProgression);
            privatePacket.Write((byte)player.whoAmI);
            MyPlayer modPlayer = player.GetModPlayer<MyPlayer>();
            modPlayer.WriteDeathDomainProgression(privatePacket);
            modPlayer.WriteReaperProgression(privatePacket);
            privatePacket.Send(player.whoAmI, fromWho);
        }
        SendReaperPublicState(player, toWho, fromWho);
    }

    /// <summary>
    /// Commits an altar purchase to its owner in one framed message. Keeping the
    /// soul balance, complete character tree and every consumed inventory slot in
    /// the same packet prevents a disconnect from persisting only half a purchase.
    /// </summary>
    internal static void SendReaperProgressionTransaction(Player player, IEnumerable<int> changedSlots)
    {
        if (Main.netMode != NetmodeID.Server)
            return;

        bool[] includedSlots = new bool[58];
        int changedSlotCount = 0;
        foreach (int slot in changedSlots)
        {
            if (slot < 0 || slot >= 58 || includedSlots[slot])
                continue;
            includedSlots[slot] = true;
            changedSlotCount++;
        }

        ModPacket packet = ModContent.GetInstance<SoulHarvest>().GetPacket();
        packet.Write((byte)MessageType.SyncReaperTransaction);
        packet.Write((byte)player.whoAmI);
        MyPlayer modPlayer = player.GetModPlayer<MyPlayer>();
        packet.Write(modPlayer.Souls);
        modPlayer.WriteReaperProgression(packet);
        packet.Write((byte)changedSlotCount);
        for (int slot = 0; slot < includedSlots.Length; slot++)
        {
            if (!includedSlots[slot])
                continue;
            packet.Write((byte)slot);
            ItemIO.Send(player.inventory[slot], packet, writeStack: true, writeFavorite: true);
        }
        packet.Send(player.whoAmI);
        SendReaperPublicState(player);
    }

    internal static void SendReaperPublicState(Player player, int toWho = -1, int fromWho = -1)
    {
        if (Main.netMode != NetmodeID.Server)
            return;
        ReaperProgressionState progression = player.GetModPlayer<MyPlayer>().ReaperProgression;
        ReaperFormId form = ReaperCombatRegistry.ResolveUsableForm(progression);
        ModPacket packet = ModContent.GetInstance<SoulHarvest>().GetPacket();
        packet.Write((byte)MessageType.SyncReaperPublicState);
        packet.Write((byte)player.whoAmI);
        packet.Write((byte)form);
        packet.Write((byte)progression.GetStage(form));
        packet.Send(toWho, fromWho);
    }

    internal static void SendReaperCombat(Player player, int toWho = -1, int fromWho = -1)
    {
        if (Main.netMode != NetmodeID.Server)
            return;
        if (toWho >= 0 && toWho != player.whoAmI)
            return;
        if (player.whoAmI < 0
            || player.whoAmI >= Main.maxPlayers
            || !Netplay.Clients[player.whoAmI].IsActive)
        {
            return;
        }
        ModPacket packet = ModContent.GetInstance<SoulHarvest>().GetPacket();
        packet.Write((byte)MessageType.SyncReaperCombat);
        packet.Write((byte)player.whoAmI);
        player.GetModPlayer<MyPlayer>().WriteReaperCombat(packet);
        packet.Send(player.whoAmI, fromWho);
    }

    private static bool TryGetRequestPlayer(int whoAmI, out Player player)
    {
        player = new Player();
        if (Main.netMode != NetmodeID.Server
            || whoAmI < 0
            || whoAmI >= Main.maxPlayers
            || !Main.player[whoAmI].active)
        {
            return false;
        }

        player = Main.player[whoAmI];
        return true;
    }

    private static OperationResult MapReaperFailure(ReaperProgressionFailure failure)
    {
        return failure switch
        {
            ReaperProgressionFailure.NotEnoughSouls => OperationResult.NotEnoughUpgradeSouls,
            ReaperProgressionFailure.NotEnoughEssence => OperationResult.NotEnoughEssence,
            ReaperProgressionFailure.NotNearAltar => OperationResult.TooFarFromAltar,
            ReaperProgressionFailure.InvalidForm => OperationResult.ReaperInvalidForm,
            ReaperProgressionFailure.Maxed => OperationResult.UpgradeMaxed,
            _ => OperationResult.ReaperProgressLocked
        };
    }

    internal static void SendOperationResult(Player player, OperationResult result)
    {
        if (Main.netMode == NetmodeID.SinglePlayer)
        {
            ShowOperationResult(result);
            return;
        }

        if (Main.netMode != NetmodeID.Server)
            return;

        ModPacket packet = ModContent.GetInstance<SoulHarvest>().GetPacket();
        packet.Write((byte)MessageType.OperationResult);
        packet.Write((byte)result);
        packet.Send(player.whoAmI);
    }

    internal static void SendInventorySlot(Player player, int slot)
    {
        if (Main.netMode != NetmodeID.Server || slot < 0 || slot >= 58)
            return;

        ModPacket packet = ModContent.GetInstance<SoulHarvest>().GetPacket();
        packet.Write((byte)MessageType.SyncInventorySlot);
        packet.Write((byte)slot);
        ItemIO.Send(player.inventory[slot], packet, writeStack: true, writeFavorite: true);
        packet.Send(player.whoAmI);
    }

    internal static void SendAltarItem(Player player, DeathAltarItemReference itemReference)
    {
        if (Main.netMode != NetmodeID.Server || !itemReference.TryGetSlotItem(player, out Item item))
            return;

        ModPacket packet = ModContent.GetInstance<SoulHarvest>().GetPacket();
        packet.Write((byte)MessageType.SyncAltarItem);
        packet.Write((byte)itemReference.Storage);
        packet.Write(itemReference.Slot);
        ItemIO.Send(item, packet, writeStack: true, writeFavorite: true);
        packet.Send(player.whoAmI);
    }

    internal static void SendDeathDomainState(Player player, bool enabled, bool descended)
    {
        if (Main.netMode != NetmodeID.MultiplayerClient || player.whoAmI != Main.myPlayer)
            return;
        ModPacket packet = ModContent.GetInstance<SoulHarvest>().GetPacket();
        packet.Write((byte)MessageType.SyncDeathDomainState);
        packet.Write(enabled);
        packet.Write(descended);
        packet.Send();
    }

    internal static void BroadcastDeathDomainState(int playerIndex, bool enabled, bool descended)
    {
        if (Main.netMode != NetmodeID.Server)
            return;
        ModPacket packet = ModContent.GetInstance<SoulHarvest>().GetPacket();
        packet.Write((byte)MessageType.SyncDeathDomainState);
        packet.Write((byte)playerIndex);
        packet.Write(enabled);
        packet.Write(descended);
        packet.Send();
    }

    internal static void BroadcastDeathDomainTelegraph(
        Player player,
        NPC target,
        int duration,
        int slashCount,
        float mastery,
        float rotation,
        float scale)
    {
        BroadcastDeathDomainVisual(
            DeathDomainVisualAction.SpawnTelegraph,
            player.whoAmI,
            target.whoAmI,
            target.Center,
            rotation,
            scale,
            slashCount,
            mastery,
            duration,
            requiem: false);
    }

    internal static void BroadcastDeathDomainHarvestSlash(
        Player player,
        NPC target,
        int slashCount,
        float mastery,
        bool requiem,
        float rotation,
        float scale)
    {
        BroadcastDeathDomainVisual(
            DeathDomainVisualAction.SpawnHarvestSlash,
            player.whoAmI,
            target.whoAmI,
            target.Center,
            rotation,
            scale,
            slashCount,
            mastery,
            duration: 0,
            requiem);
    }

    internal static void BroadcastDeathDomainTelegraphClear(Player player, int npcIndex)
    {
        BroadcastDeathDomainVisual(
            DeathDomainVisualAction.ClearTelegraph,
            player.whoAmI,
            npcIndex,
            Vector2.Zero,
            0f,
            1f,
            1,
            0f,
            0,
            requiem: false);
    }

    private static void BroadcastDeathDomainVisual(
        DeathDomainVisualAction action,
        int playerIndex,
        int npcIndex,
        Vector2 position,
        float rotation,
        float scale,
        int slashCount,
        float mastery,
        int duration,
        bool requiem)
    {
        if (Main.netMode != NetmodeID.Server
            || playerIndex < 0
            || playerIndex >= Main.maxPlayers
            || npcIndex < 0
            || npcIndex >= Main.maxNPCs)
        {
            return;
        }

        ModPacket packet = ModContent.GetInstance<SoulHarvest>().GetPacket();
        packet.Write((byte)MessageType.SyncDeathDomainVisual);
        packet.Write((byte)action);
        packet.Write((byte)playerIndex);
        packet.Write((short)npcIndex);
        packet.Write(position.X);
        packet.Write(position.Y);
        packet.Write(rotation);
        packet.Write(scale);
        packet.Write((byte)Math.Clamp(slashCount, 1, 5));
        packet.Write(MathHelper.Clamp(mastery, 0f, 1f));
        packet.Write((short)Math.Clamp(duration, 0, short.MaxValue));
        packet.Write(requiem);
        packet.Send();
    }

    private static void SpawnClientDeathDomainVisual(
        DeathDomainVisualAction action,
        int playerIndex,
        int npcIndex,
        Vector2 position,
        float rotation,
        float scale,
        int slashCount,
        float mastery,
        int duration,
        bool requiem)
    {
        if (playerIndex < 0 || playerIndex >= Main.maxPlayers
            || npcIndex < 0 || npcIndex >= Main.maxNPCs
            || action is not (DeathDomainVisualAction.SpawnTelegraph or DeathDomainVisualAction.SpawnHarvestSlash))
        {
            return;
        }

        int projectileType = action == DeathDomainVisualAction.SpawnTelegraph
            ? ModContent.ProjectileType<DeathDomainHarvestTelegraphProjectile>()
            : ModContent.ProjectileType<DeathDomainHarvestSlashProjectile>();
        float ai1 = action == DeathDomainVisualAction.SpawnTelegraph
            ? Math.Max(1, duration)
            : Math.Clamp(slashCount, 1, 5);
        float ai2 = action == DeathDomainVisualAction.SpawnTelegraph
            ? Math.Clamp(slashCount, 1, 5) + MathHelper.Clamp(mastery, 0f, 1f) * 0.1f
            : MathHelper.Clamp(mastery, 0f, 1f) + (requiem ? 2f : 0f);
        int projectileIndex = Projectile.NewProjectile(
            Main.LocalPlayer.GetSource_Misc("SoulHarvest:NetworkDomainVisual"),
            position,
            Vector2.Zero,
            projectileType,
            0,
            0f,
            Main.myPlayer,
            npcIndex,
            ai1,
            ai2);
        if (projectileIndex < 0 || projectileIndex >= Main.maxProjectiles)
            return;

        Projectile visual = Main.projectile[projectileIndex];
        visual.rotation = rotation;
        visual.scale = MathHelper.Clamp(scale, 0.5f, 3f);
        visual.localAI[1] = playerIndex + 1;
        visual.netUpdate = false;
        visual.netImportant = false;
    }

    private static void ClearClientDeathDomainTelegraph(int playerIndex, int npcIndex)
    {
        float visualOwnerTag = playerIndex + 1;
        foreach (Projectile projectile in Main.ActiveProjectiles)
        {
            if (projectile.ModProjectile is DeathDomainHarvestTelegraphProjectile
                && (int)projectile.ai[0] == npcIndex
                && Math.Abs(projectile.localAI[1] - visualOwnerTag) < 0.1f)
            {
                projectile.active = false;
            }
        }
    }

    internal static void BroadcastDeathMark(NPC npc, bool marked)
    {
        if (Main.netMode != NetmodeID.Server || npc.whoAmI < 0 || npc.whoAmI >= Main.maxNPCs)
            return;

        ModPacket packet = ModContent.GetInstance<SoulHarvest>().GetPacket();
        packet.Write((byte)MessageType.SyncDeathMark);
        packet.Write((short)npc.whoAmI);
        packet.Write(marked);
        packet.Send();
    }

    internal static void SendSickleHit(Projectile projectile, NPC target)
    {
        if (Main.netMode != NetmodeID.MultiplayerClient || projectile.owner != Main.myPlayer)
            return;

        SendSickleHit(projectile.identity, target);
    }

    internal static void SendSickleItemHit(NPC target)
    {
        if (Main.netMode != NetmodeID.MultiplayerClient)
            return;

        SendSickleHit(-1, target);
    }

    private static void SendSickleHit(int projectileIdentity, NPC target)
    {
        if (!target.active || target.whoAmI < 0 || target.whoAmI >= Main.maxNPCs)
            return;

        ModPacket packet = ModContent.GetInstance<SoulHarvest>().GetPacket();
        packet.Write((byte)MessageType.SickleHitRequest);
        packet.Write(projectileIdentity);
        packet.Write((short)target.whoAmI);
        packet.Send();
    }

    private static void HandleSickleHitRequest(int playerIndex, int projectileIdentity, int npcIndex)
    {
        // 3.0 combat is resolved entirely by server projectile collision. The old
        // client-reported hit packet remains in the enum for wire compatibility,
        // but can no longer mutate marks, healing, debuffs, or participation.
    }

    private static void ShowOperationResult(OperationResult result)
    {
        string key = $"Mods.SoulHarvest.Messages.{result}";
        Color color = result is OperationResult.EssenceExtracted
            or OperationResult.EssenceAbsorbed
            or OperationResult.UpgradeSucceeded
            or OperationResult.SickleRegressed
            or OperationResult.ReaperStageUnlocked
            or OperationResult.ReaperSkillUpgraded
            or OperationResult.ReaperCommonUpgraded
            or OperationResult.ReaperFormSwitched
            or OperationResult.ReaperFormSwitchStarted
            ? new Color(90, 235, 255)
            : new Color(255, 105, 130);
        Main.NewText(Language.GetTextValue(key), color);
    }
}