using SoulHarvest.Items; using System; using System.Collections.Generic; using Terraria; using Terraria.ID; using Terraria.ModLoader; namespace SoulHarvest.Common; /// /// 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. /// 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().ServerCharacterStateReady || stage is < ReaperStage.StageI or > ReaperStage.StageIII) { return false; } MyPlayer modPlayer = player.GetModPlayer(); 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()); return true; } /// /// 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. /// public static bool TryDeliverPending(Player player) { if (Main.netMode == NetmodeID.MultiplayerClient || player is null || !player.active || !player.GetModPlayer().ServerCharacterStateReady) return false; ReaperProgressionState progression = player.GetModPlayer().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 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; } }