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