using SoulHarvest.Common;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using Terraria;
using Terraria.DataStructures;
using Terraria.ID;
using Terraria.ModLoader;
namespace SoulHarvest.Projectiles;
/// <summary>
/// Zero-damage, server-authored presentation cue for a Void Reaper direct hit.
/// It only transports the authoritative action snapshot and impact location;
/// clients own all drawing and the cue can never create damage or move an NPC.
/// </summary>
public sealed class ReaperVoidHitCueProjectile : ModProjectile
{
private const int SeedSalt = 0x564F4944;
private const int MaximumActiveCues = 96;
private int ownerIndex = -1;
private ReaperStage stage;
private int actionId;
private int targetIndex = -1;
private Vector2 origin;
private Vector2 direction;
private int seed;
private bool configured;
private bool presented;
public override string Texture => "Terraria/Images/Projectile_0";
public static int Spawn(
IEntitySource source,
int owner,
ReaperStage stage,
int actionId,
int targetIndex,
Vector2 origin,
Vector2 direction)
{
if (Main.netMode == NetmodeID.MultiplayerClient
|| owner < 0 || owner >= Main.maxPlayers
|| stage is < ReaperStage.StageI or > ReaperStage.StageIII
|| targetIndex is < -1 || targetIndex >= Main.maxNPCs
|| !IsFinite(origin)
|| !IsFinite(direction))
{
return -1;
}
direction = direction.SafeNormalize(Vector2.UnitX);
int projectileType = ModContent.ProjectileType<ReaperVoidHitCueProjectile>();
if (!HasTransportCapacity(projectileType)
|| !ReaperVoidHitCueReservationSystem.TryReserve(owner, actionId, targetIndex))
{
return -1;
}
// The server owns the transport projectile. Using the releasing player's
// owner slot here would let the server's local identity collide with a
// different player-owned projectile identity on that player's client.
int transportOwner = Main.netMode == NetmodeID.Server ? Main.maxPlayers : owner;
int projectileIndex = Projectile.NewProjectile(source, origin, Vector2.Zero,
projectileType, 0, 0f, transportOwner);
if (projectileIndex < 0 || projectileIndex >= Main.maxProjectiles)
{
ReaperVoidHitCueReservationSystem.Release(owner, actionId, targetIndex);
return -1;
}
Projectile projectile = Main.projectile[projectileIndex];
if (projectile.ModProjectile is not ReaperVoidHitCueProjectile cue)
{
ReaperVoidHitCueReservationSystem.Release(owner, actionId, targetIndex);
projectile.Kill();
return -1;
}
cue.ownerIndex = owner;
cue.stage = stage;
cue.actionId = actionId;
cue.targetIndex = targetIndex;
cue.origin = origin;
cue.direction = direction;
cue.seed = ReaperSkillCueProjectile.StableSeed(
owner, actionId, targetIndex, (int)stage, SeedSalt);
cue.configured = true;
projectile.Center = origin;
projectile.timeLeft = 30;
ReaperProjectileHelper.SyncNewProjectile(projectile);
return projectileIndex;
}
public override void SetDefaults()
{
Projectile.width = 2;
Projectile.height = 2;
Projectile.friendly = false;
Projectile.hostile = false;
Projectile.tileCollide = false;
Projectile.ignoreWater = true;
Projectile.penetrate = -1;
Projectile.timeLeft = 30;
Projectile.hide = true;
Projectile.netImportant = true;
}
public override bool ShouldUpdatePosition() => false;
public override bool? CanDamage() => false;
public override void AI()
{
if (!configured || presented || Main.netMode == NetmodeID.Server)
return;
presented = true;
ReaperVfxDirector.Submit(new ReaperVisualEvent(
ownerIndex,
ReaperFormId.Void,
stage,
ReaperVisualActionKind.NormalAttack,
Skill: -1,
SkillLevel: 0,
actionId,
Target: targetIndex,
Origin: origin,
Direction: direction,
Seed: seed,
TimelinePhase: 0,
NormalizedTime: 0f));
}
public override bool PreDraw(ref Color lightColor) => false;
public override void SendExtraAI(BinaryWriter writer)
{
writer.Write(configured);
if (!configured)
return;
writer.Write((byte)ownerIndex);
writer.Write((byte)stage);
writer.Write(actionId);
writer.Write(targetIndex);
writer.WriteVector2(origin);
writer.WriteVector2(direction);
writer.Write(seed);
}
public override void ReceiveExtraAI(BinaryReader reader)
{
bool incomingConfigured = reader.ReadBoolean();
if (!incomingConfigured)
{
if (Main.netMode != NetmodeID.Server)
configured = false;
return;
}
int incomingOwner = reader.ReadByte();
ReaperStage incomingStage = (ReaperStage)reader.ReadByte();
int incomingActionId = reader.ReadInt32();
int incomingTargetIndex = reader.ReadInt32();
Vector2 incomingOrigin = reader.ReadVector2();
Vector2 incomingDirection = reader.ReadVector2();
int incomingSeed = reader.ReadInt32();
if (Main.netMode == NetmodeID.Server)
return;
if (incomingOwner < 0 || incomingOwner >= Main.maxPlayers
|| incomingStage is < ReaperStage.StageI or > ReaperStage.StageIII
|| incomingTargetIndex is < -1 || incomingTargetIndex >= Main.maxNPCs
|| !IsFinite(incomingOrigin)
|| !IsFinite(incomingDirection))
{
configured = false;
return;
}
ownerIndex = incomingOwner;
stage = incomingStage;
actionId = incomingActionId;
targetIndex = incomingTargetIndex;
origin = incomingOrigin;
direction = incomingDirection.SafeNormalize(Vector2.UnitX);
seed = incomingSeed;
configured = true;
Projectile.Center = origin;
}
private static bool IsFinite(Vector2 value)
=> float.IsFinite(value.X) && float.IsFinite(value.Y);
private static bool HasTransportCapacity(int projectileType)
{
bool hasFreeSlot = false;
int activeCues = 0;
for (int index = 0; index < Main.maxProjectiles; index++)
{
Projectile projectile = Main.projectile[index];
if (!projectile.active)
{
hasFreeSlot = true;
continue;
}
if (projectile.type == projectileType && ++activeCues >= MaximumActiveCues)
return false;
}
return hasFreeSlot;
}
}
using SoulHarvest.Common;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using Terraria;
using Terraria.DataStructures;
using Terraria.ID;
using Terraria.ModLoader;
namespace SoulHarvest.Projectiles;
/// <summary>
/// Zero-damage, server-authored presentation cue for a Void Reaper direct hit.
/// It only transports the authoritative action snapshot and impact location;
/// clients own all drawing and the cue can never create damage or move an NPC.
/// </summary>
public sealed class ReaperVoidHitCueProjectile : ModProjectile
{
private const int SeedSalt = 0x564F4944;
private const int MaximumActiveCues = 96;
private int ownerIndex = -1;
private ReaperStage stage;
private int actionId;
private int targetIndex = -1;
private Vector2 origin;
private Vector2 direction;
private int seed;
private bool configured;
private bool presented;
public override string Texture => "Terraria/Images/Projectile_0";
public static int Spawn(
IEntitySource source,
int owner,
ReaperStage stage,
int actionId,
int targetIndex,
Vector2 origin,
Vector2 direction)
{
if (Main.netMode == NetmodeID.MultiplayerClient
|| owner < 0 || owner >= Main.maxPlayers
|| stage is < ReaperStage.StageI or > ReaperStage.StageIII
|| targetIndex is < -1 || targetIndex >= Main.maxNPCs
|| !IsFinite(origin)
|| !IsFinite(direction))
{
return -1;
}
direction = direction.SafeNormalize(Vector2.UnitX);
int projectileType = ModContent.ProjectileType<ReaperVoidHitCueProjectile>();
if (!HasTransportCapacity(projectileType)
|| !ReaperVoidHitCueReservationSystem.TryReserve(owner, actionId, targetIndex))
{
return -1;
}
// The server owns the transport projectile. Using the releasing player's
// owner slot here would let the server's local identity collide with a
// different player-owned projectile identity on that player's client.
int transportOwner = Main.netMode == NetmodeID.Server ? Main.maxPlayers : owner;
int projectileIndex = Projectile.NewProjectile(source, origin, Vector2.Zero,
projectileType, 0, 0f, transportOwner);
if (projectileIndex < 0 || projectileIndex >= Main.maxProjectiles)
{
ReaperVoidHitCueReservationSystem.Release(owner, actionId, targetIndex);
return -1;
}
Projectile projectile = Main.projectile[projectileIndex];
if (projectile.ModProjectile is not ReaperVoidHitCueProjectile cue)
{
ReaperVoidHitCueReservationSystem.Release(owner, actionId, targetIndex);
projectile.Kill();
return -1;
}
cue.ownerIndex = owner;
cue.stage = stage;
cue.actionId = actionId;
cue.targetIndex = targetIndex;
cue.origin = origin;
cue.direction = direction;
cue.seed = ReaperSkillCueProjectile.StableSeed(
owner, actionId, targetIndex, (int)stage, SeedSalt);
cue.configured = true;
projectile.Center = origin;
projectile.timeLeft = 30;
ReaperProjectileHelper.SyncNewProjectile(projectile);
return projectileIndex;
}
public override void SetDefaults()
{
Projectile.width = 2;
Projectile.height = 2;
Projectile.friendly = false;
Projectile.hostile = false;
Projectile.tileCollide = false;
Projectile.ignoreWater = true;
Projectile.penetrate = -1;
Projectile.timeLeft = 30;
Projectile.hide = true;
Projectile.netImportant = true;
}
public override bool ShouldUpdatePosition() => false;
public override bool? CanDamage() => false;
public override void AI()
{
if (!configured || presented || Main.netMode == NetmodeID.Server)
return;
presented = true;
ReaperVfxDirector.Submit(new ReaperVisualEvent(
ownerIndex,
ReaperFormId.Void,
stage,
ReaperVisualActionKind.NormalAttack,
Skill: -1,
SkillLevel: 0,
actionId,
Target: targetIndex,
Origin: origin,
Direction: direction,
Seed: seed,
TimelinePhase: 0,
NormalizedTime: 0f));
}
public override bool PreDraw(ref Color lightColor) => false;
public override void SendExtraAI(BinaryWriter writer)
{
writer.Write(configured);
if (!configured)
return;
writer.Write((byte)ownerIndex);
writer.Write((byte)stage);
writer.Write(actionId);
writer.Write(targetIndex);
writer.WriteVector2(origin);
writer.WriteVector2(direction);
writer.Write(seed);
}
public override void ReceiveExtraAI(BinaryReader reader)
{
bool incomingConfigured = reader.ReadBoolean();
if (!incomingConfigured)
{
if (Main.netMode != NetmodeID.Server)
configured = false;
return;
}
int incomingOwner = reader.ReadByte();
ReaperStage incomingStage = (ReaperStage)reader.ReadByte();
int incomingActionId = reader.ReadInt32();
int incomingTargetIndex = reader.ReadInt32();
Vector2 incomingOrigin = reader.ReadVector2();
Vector2 incomingDirection = reader.ReadVector2();
int incomingSeed = reader.ReadInt32();
if (Main.netMode == NetmodeID.Server)
return;
if (incomingOwner < 0 || incomingOwner >= Main.maxPlayers
|| incomingStage is < ReaperStage.StageI or > ReaperStage.StageIII
|| incomingTargetIndex is < -1 || incomingTargetIndex >= Main.maxNPCs
|| !IsFinite(incomingOrigin)
|| !IsFinite(incomingDirection))
{
configured = false;
return;
}
ownerIndex = incomingOwner;
stage = incomingStage;
actionId = incomingActionId;
targetIndex = incomingTargetIndex;
origin = incomingOrigin;
direction = incomingDirection.SafeNormalize(Vector2.UnitX);
seed = incomingSeed;
configured = true;
Projectile.Center = origin;
}
private static bool IsFinite(Vector2 value)
=> float.IsFinite(value.X) && float.IsFinite(value.Y);
private static bool HasTransportCapacity(int projectileType)
{
bool hasFreeSlot = false;
int activeCues = 0;
for (int index = 0; index < Main.maxProjectiles; index++)
{
Projectile projectile = Main.projectile[index];
if (!projectile.active)
{
hasFreeSlot = true;
continue;
}
if (projectile.type == projectileType && ++activeCues >= MaximumActiveCues)
return false;
}
return hasFreeSlot;
}
}