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(); 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().TryExtractEssence(); break; case MessageType.RequestAbsorbEssence: if (Main.netMode == NetmodeID.Server && whoAmI >= 0 && whoAmI < Main.maxPlayers && Main.player[whoAmI].active) Main.player[whoAmI].GetModPlayer().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(); 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() .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(); 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().ReceiveReaperCombat(reader); break; } case MessageType.SyncBloodShieldConsumption: { uint revision = reader.ReadUInt32(); int absorbed = reader.ReadUInt16(); if (TryGetRequestPlayer(whoAmI, out Player player)) { player.GetModPlayer() .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() .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().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().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().ServerCharacterStateReady) { MyPlayer modPlayer = Main.player[whoAmI].GetModPlayer(); 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().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().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().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().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().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().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().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().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().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().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().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().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().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(); 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().GetPacket(); upload.Write((byte)MessageType.UploadReaperCharacterState); MyPlayer modPlayer = player.GetModPlayer(); 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().GetPacket(); privatePacket.Write((byte)MessageType.SyncReaperProgression); privatePacket.Write((byte)player.whoAmI); MyPlayer modPlayer = player.GetModPlayer(); modPlayer.WriteDeathDomainProgression(privatePacket); modPlayer.WriteReaperProgression(privatePacket); privatePacket.Send(player.whoAmI, fromWho); } SendReaperPublicState(player, toWho, fromWho); } /// /// 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. /// internal static void SendReaperProgressionTransaction(Player player, IEnumerable 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().GetPacket(); packet.Write((byte)MessageType.SyncReaperTransaction); packet.Write((byte)player.whoAmI); MyPlayer modPlayer = player.GetModPlayer(); 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().ReaperProgression; ReaperFormId form = ReaperCombatRegistry.ResolveUsableForm(progression); ModPacket packet = ModContent.GetInstance().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().GetPacket(); packet.Write((byte)MessageType.SyncReaperCombat); packet.Write((byte)player.whoAmI); player.GetModPlayer().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().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().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().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().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().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().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() : ModContent.ProjectileType(); 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().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().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); } }