using SoulHarvest.Items;
using System;
using System.Collections.Generic;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;
namespace SoulHarvest.Common;
/// <summary>
/// Issues character-bound seal entitlements and mirrors them with inventory items.
/// The ledger is authoritative; the item is only a visible receipt and cannot be
/// copied or traded to create progression.
/// </summary>
public static class ReaperProgressionSoulSealService
{
private const int InventorySlotCount = 58;
public static bool TryGrant(Player player, ReaperStage stage)
{
if (Main.netMode == NetmodeID.MultiplayerClient
|| player is null
|| !player.active
|| !player.GetModPlayer<MyPlayer>().ServerCharacterStateReady
|| stage is < ReaperStage.StageI or > ReaperStage.StageIII)
{
return false;
}
MyPlayer modPlayer = player.GetModPlayer<MyPlayer>();
ReaperProgressionState progression = modPlayer.ReaperProgression;
if (!progression.CanIssueSoulSeal(stage))
return false;
bool hasInventorySpace = TryFindInsertionSlot(player, stage, out int slot);
if (!progression.TryRecordSoulSealIssued(stage, pending: !hasInventorySpace))
return false;
if (hasInventorySpace)
{
InsertToken(player, stage, slot);
}
SoulHarvest.SendReaperProgressionTransaction(
player,
hasInventorySpace ? new[] { slot } : Array.Empty<int>());
return true;
}
/// <summary>
/// Delivers as many pending seals as currently fit. Call from the owning
/// ModPlayer's server-side update loop; it performs no work when the ledger is
/// empty or the inventory is still full.
/// </summary>
public static bool TryDeliverPending(Player player)
{
if (Main.netMode == NetmodeID.MultiplayerClient
|| player is null
|| !player.active
|| !player.GetModPlayer<MyPlayer>().ServerCharacterStateReady)
return false;
ReaperProgressionState progression = player.GetModPlayer<MyPlayer>().ReaperProgression;
bool hasOutstandingSeal = false;
for (ReaperStage stage = ReaperStage.StageI; stage <= ReaperStage.StageIII; stage++)
{
if (progression.GetSoulSealIssued(stage) > progression.GetSoulSealSpent(stage))
{
hasOutstandingSeal = true;
break;
}
}
if (!hasOutstandingSeal)
return false;
bool ledgerChanged = ReconcileMissingReceipts(player, progression);
bool deliveredAny = false;
HashSet<int> changedSlots = [];
for (ReaperStage stage = ReaperStage.StageI; stage <= ReaperStage.StageIII; stage++)
{
while (progression.GetSoulSealPending(stage) > 0
&& TryFindInsertionSlot(player, stage, out int slot))
{
InsertToken(player, stage, slot);
progression.TryMarkPendingSoulSealDelivered(stage);
changedSlots.Add(slot);
deliveredAny = true;
}
}
if (deliveredAny || ledgerChanged)
SoulHarvest.SendReaperProgressionTransaction(player, changedSlots);
return deliveredAny;
}
private static bool ReconcileMissingReceipts(Player player, ReaperProgressionState progression)
{
bool changed = false;
for (ReaperStage stage = ReaperStage.StageI; stage <= ReaperStage.StageIII; stage++)
{
int outstanding = progression.GetSoulSealIssued(stage) - progression.GetSoulSealSpent(stage);
if (outstanding <= progression.GetSoulSealPending(stage))
continue;
int itemType = ReaperSoulSealCatalog.GetItemType(stage);
int physicalCount = CountReceipts(player.inventory, Math.Min(InventorySlotCount, player.inventory.Length), itemType)
+ CountReceipts(player.bank.item, player.bank.item.Length, itemType)
+ CountReceipts(player.bank2.item, player.bank2.item.Length, itemType)
+ CountReceipts(player.bank3.item, player.bank3.item.Length, itemType)
+ CountReceipts(player.bank4.item, player.bank4.item.Length, itemType);
int missing = Math.Max(0, outstanding - progression.GetSoulSealPending(stage) - physicalCount);
changed |= progression.MarkMissingSoulSealsPending(stage, missing) > 0;
}
return changed;
}
private static int CountReceipts(Item[] items, int count, int itemType)
{
int total = 0;
for (int slot = 0; slot < count; slot++)
{
Item item = items[slot];
if (item.type == itemType && item.stack > 0)
total += item.stack;
}
return total;
}
private static bool TryFindInsertionSlot(Player player, ReaperStage stage, out int slot)
{
slot = -1;
int itemType = ReaperSoulSealCatalog.GetItemType(stage);
if (itemType <= ItemID.None)
return false;
int emptySlot = -1;
int slotCount = Math.Min(InventorySlotCount, player.inventory.Length);
for (int index = 0; index < slotCount; index++)
{
Item item = player.inventory[index];
if (item.type == itemType && item.stack > 0 && item.stack < item.maxStack)
{
slot = index;
return true;
}
if (emptySlot < 0 && item.IsAir)
emptySlot = index;
}
slot = emptySlot;
return slot >= 0;
}
private static void InsertToken(Player player, ReaperStage stage, int slot)
{
int itemType = ReaperSoulSealCatalog.GetItemType(stage);
Item item = player.inventory[slot];
if (item.type == itemType && item.stack > 0)
{
item.stack++;
return;
}
item.SetDefaults(itemType);
item.stack = 1;
}
}
using SoulHarvest.Items;
using System;
using System.Collections.Generic;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;
namespace SoulHarvest.Common;
/// <summary>
/// Issues character-bound seal entitlements and mirrors them with inventory items.
/// The ledger is authoritative; the item is only a visible receipt and cannot be
/// copied or traded to create progression.
/// </summary>
public static class ReaperProgressionSoulSealService
{
private const int InventorySlotCount = 58;
public static bool TryGrant(Player player, ReaperStage stage)
{
if (Main.netMode == NetmodeID.MultiplayerClient
|| player is null
|| !player.active
|| !player.GetModPlayer<MyPlayer>().ServerCharacterStateReady
|| stage is < ReaperStage.StageI or > ReaperStage.StageIII)
{
return false;
}
MyPlayer modPlayer = player.GetModPlayer<MyPlayer>();
ReaperProgressionState progression = modPlayer.ReaperProgression;
if (!progression.CanIssueSoulSeal(stage))
return false;
bool hasInventorySpace = TryFindInsertionSlot(player, stage, out int slot);
if (!progression.TryRecordSoulSealIssued(stage, pending: !hasInventorySpace))
return false;
if (hasInventorySpace)
{
InsertToken(player, stage, slot);
}
SoulHarvest.SendReaperProgressionTransaction(
player,
hasInventorySpace ? new[] { slot } : Array.Empty<int>());
return true;
}
/// <summary>
/// Delivers as many pending seals as currently fit. Call from the owning
/// ModPlayer's server-side update loop; it performs no work when the ledger is
/// empty or the inventory is still full.
/// </summary>
public static bool TryDeliverPending(Player player)
{
if (Main.netMode == NetmodeID.MultiplayerClient
|| player is null
|| !player.active
|| !player.GetModPlayer<MyPlayer>().ServerCharacterStateReady)
return false;
ReaperProgressionState progression = player.GetModPlayer<MyPlayer>().ReaperProgression;
bool hasOutstandingSeal = false;
for (ReaperStage stage = ReaperStage.StageI; stage <= ReaperStage.StageIII; stage++)
{
if (progression.GetSoulSealIssued(stage) > progression.GetSoulSealSpent(stage))
{
hasOutstandingSeal = true;
break;
}
}
if (!hasOutstandingSeal)
return false;
bool ledgerChanged = ReconcileMissingReceipts(player, progression);
bool deliveredAny = false;
HashSet<int> changedSlots = [];
for (ReaperStage stage = ReaperStage.StageI; stage <= ReaperStage.StageIII; stage++)
{
while (progression.GetSoulSealPending(stage) > 0
&& TryFindInsertionSlot(player, stage, out int slot))
{
InsertToken(player, stage, slot);
progression.TryMarkPendingSoulSealDelivered(stage);
changedSlots.Add(slot);
deliveredAny = true;
}
}
if (deliveredAny || ledgerChanged)
SoulHarvest.SendReaperProgressionTransaction(player, changedSlots);
return deliveredAny;
}
private static bool ReconcileMissingReceipts(Player player, ReaperProgressionState progression)
{
bool changed = false;
for (ReaperStage stage = ReaperStage.StageI; stage <= ReaperStage.StageIII; stage++)
{
int outstanding = progression.GetSoulSealIssued(stage) - progression.GetSoulSealSpent(stage);
if (outstanding <= progression.GetSoulSealPending(stage))
continue;
int itemType = ReaperSoulSealCatalog.GetItemType(stage);
int physicalCount = CountReceipts(player.inventory, Math.Min(InventorySlotCount, player.inventory.Length), itemType)
+ CountReceipts(player.bank.item, player.bank.item.Length, itemType)
+ CountReceipts(player.bank2.item, player.bank2.item.Length, itemType)
+ CountReceipts(player.bank3.item, player.bank3.item.Length, itemType)
+ CountReceipts(player.bank4.item, player.bank4.item.Length, itemType);
int missing = Math.Max(0, outstanding - progression.GetSoulSealPending(stage) - physicalCount);
changed |= progression.MarkMissingSoulSealsPending(stage, missing) > 0;
}
return changed;
}
private static int CountReceipts(Item[] items, int count, int itemType)
{
int total = 0;
for (int slot = 0; slot < count; slot++)
{
Item item = items[slot];
if (item.type == itemType && item.stack > 0)
total += item.stack;
}
return total;
}
private static bool TryFindInsertionSlot(Player player, ReaperStage stage, out int slot)
{
slot = -1;
int itemType = ReaperSoulSealCatalog.GetItemType(stage);
if (itemType <= ItemID.None)
return false;
int emptySlot = -1;
int slotCount = Math.Min(InventorySlotCount, player.inventory.Length);
for (int index = 0; index < slotCount; index++)
{
Item item = player.inventory[index];
if (item.type == itemType && item.stack > 0 && item.stack < item.maxStack)
{
slot = index;
return true;
}
if (emptySlot < 0 && item.IsAir)
emptySlot = index;
}
slot = emptySlot;
return slot >= 0;
}
private static void InsertToken(Player player, ReaperStage stage, int slot)
{
int itemType = ReaperSoulSealCatalog.GetItemType(stage);
Item item = player.inventory[slot];
if (item.type == itemType && item.stack > 0)
{
item.stack++;
return;
}
item.SetDefaults(itemType);
item.stack = 1;
}
}