package com.tiedup.remake.bounty; import com.tiedup.remake.core.SettingsAccessor; import com.tiedup.remake.core.SystemMessageManager; import com.tiedup.remake.core.TiedUpMod; import java.util.*; import javax.annotation.Nullable; import net.minecraft.ChatFormatting; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.Tag; import net.minecraft.network.chat.Component; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.saveddata.SavedData; /** * World-saved data manager for bounties. * * * Manages all active bounties, handles expiration, delivery rewards, * and stores bounties for offline players. */ public class BountyManager extends SavedData { private static final String DATA_NAME = "tiedup_bounties"; /** Pending rewards expire after 30 real days (in milliseconds). */ private static final long PENDING_REWARD_EXPIRATION_MS = 30L * 24 * 60 * 60 * 1000; // Active bounties private final List bounties = new ArrayList<>(); // Bounties for offline players (to return reward when they log in) // Stored with timestamp via Bounty.creationTime + durationSeconds private final List pendingRewards = new ArrayList<>(); // ==================== CONSTRUCTION ==================== public BountyManager() {} public static BountyManager create() { return new BountyManager(); } public static BountyManager load(CompoundTag tag) { BountyManager manager = new BountyManager(); // Load active bounties ListTag bountiesTag = tag.getList("bounties", Tag.TAG_COMPOUND); for (int i = 0; i < bountiesTag.size(); i++) { manager.bounties.add(Bounty.load(bountiesTag.getCompound(i))); } // Load pending rewards (with expiration cleanup) ListTag pendingTag = tag.getList("pendingRewards", Tag.TAG_COMPOUND); int expiredCount = 0; for (int i = 0; i < pendingTag.size(); i++) { Bounty bounty = Bounty.load(pendingTag.getCompound(i)); if (!isPendingRewardExpired(bounty)) { manager.pendingRewards.add(bounty); } else { expiredCount++; } } if (expiredCount > 0) { TiedUpMod.LOGGER.info( "[BOUNTY] Cleaned up {} expired pending rewards (>30 days)", expiredCount ); } TiedUpMod.LOGGER.info( "[BOUNTY] Loaded {} active bounties, {} pending rewards", manager.bounties.size(), manager.pendingRewards.size() ); return manager; } @Override public CompoundTag save(CompoundTag tag) { // Save active bounties ListTag bountiesTag = new ListTag(); for (Bounty bounty : bounties) { bountiesTag.add(bounty.save()); } tag.put("bounties", bountiesTag); // Save pending rewards ListTag pendingTag = new ListTag(); for (Bounty bounty : pendingRewards) { pendingTag.add(bounty.save()); } tag.put("pendingRewards", pendingTag); return tag; } // ==================== ACCESS ==================== /** * Get the BountyManager for a world. */ public static BountyManager get(ServerLevel level) { return level .getDataStorage() .computeIfAbsent( BountyManager::load, BountyManager::create, DATA_NAME ); } /** * Get the BountyManager from a server. */ public static BountyManager get(MinecraftServer server) { ServerLevel overworld = server.overworld(); return get(overworld); } // ==================== BOUNTY MANAGEMENT ==================== /** * Get all active bounties (removes expired ones). */ public List getBounties(ServerLevel level) { // Clean up expired bounties Iterator it = bounties.iterator(); while (it.hasNext()) { Bounty bounty = it.next(); if (bounty.isExpired()) { it.remove(); onBountyExpired(level, bounty); } } setDirty(); return new ArrayList<>(bounties); } /** * Add a new bounty. */ public void addBounty(Bounty bounty) { bounties.add(bounty); setDirty(); TiedUpMod.LOGGER.info( "[BOUNTY] New bounty: {} on {} by {}", bounty.getId(), bounty.getTargetName(), bounty.getClientName() ); } /** * Get a bounty by ID. */ @Nullable public Bounty getBountyById(String id) { for (Bounty bounty : bounties) { if (bounty.getId().equals(id)) { return bounty; } } return null; } /** * Cancel a bounty. * Only the client or an admin can cancel. * If client cancels, they get their reward back. */ public boolean cancelBounty(ServerPlayer player, String bountyId) { Bounty bounty = getBountyById(bountyId); if (bounty == null) { return false; } boolean isAdmin = player.hasPermissions(2); boolean isClient = bounty.isClient(player.getUUID()); if (!isClient && !isAdmin) { return false; } bounties.remove(bounty); setDirty(); // Return reward to client (or drop if admin cancelled) if (isClient) { giveReward(player, bounty); broadcastMessage( player.server, player.getName().getString() + " cancelled their bounty on " + bounty.getTargetName() ); } else { onBountyExpired(player.serverLevel(), bounty); broadcastMessage( player.server, player.getName().getString() + " (admin) cancelled bounty on " + bounty.getTargetName() ); } return true; } // ==================== DELIVERY ==================== /** * Try to deliver a captive to a client. * Called when a hunter brings a captive near the bounty client. * * @param hunter The player delivering the captive * @param client The bounty client receiving the captive * @param target The captive being delivered * @return true if bounty was fulfilled */ public boolean tryDeliverCaptive( ServerPlayer hunter, ServerPlayer client, ServerPlayer target ) { boolean delivered = false; Iterator it = bounties.iterator(); while (it.hasNext()) { Bounty bounty = it.next(); // Skip expired if (bounty.isExpired()) { continue; } // Check if this bounty matches if (bounty.matches(client.getUUID(), target.getUUID())) { it.remove(); setDirty(); // Give reward to hunter giveReward(hunter, bounty); delivered = true; broadcastMessage( hunter.server, hunter.getName().getString() + " delivered " + target.getName().getString() + " to " + client.getName().getString() + " for " + bounty.getRewardDescription() + "!" ); TiedUpMod.LOGGER.info( "[BOUNTY] Delivered: {} brought {} to {}", hunter.getName().getString(), target.getName().getString(), client.getName().getString() ); } } return delivered; } // ==================== EXPIRATION ==================== /** * Handle bounty expiration. * Returns reward to client if online, otherwise stores for later. */ private void onBountyExpired(ServerLevel level, Bounty bounty) { ServerPlayer client = level .getServer() .getPlayerList() .getPlayer(bounty.getClientId()); if (client != null) { // Client is online - return reward giveReward(client, bounty); SystemMessageManager.sendChatToPlayer( client, "Your bounty on " + bounty.getTargetName() + " has expired. Reward returned.", ChatFormatting.YELLOW ); } else { // Client is offline - store for later pendingRewards.add(bounty); setDirty(); } TiedUpMod.LOGGER.info( "[BOUNTY] Expired: {} on {}", bounty.getClientName(), bounty.getTargetName() ); } /** * Check for pending rewards when a player joins. */ public void onPlayerJoin(ServerPlayer player) { Iterator it = pendingRewards.iterator(); while (it.hasNext()) { Bounty bounty = it.next(); if (bounty.isClient(player.getUUID())) { giveReward(player, bounty); it.remove(); setDirty(); SystemMessageManager.sendChatToPlayer( player, "Your expired bounty reward has been returned: " + bounty.getRewardDescription(), ChatFormatting.YELLOW ); } } } // ==================== VALIDATION ==================== /** * Check if a player can create a new bounty. */ public boolean canCreateBounty(ServerPlayer player, ServerLevel level) { if (player.hasPermissions(2)) { return true; // Admins bypass limit } int count = 0; for (Bounty bounty : bounties) { if (bounty.isClient(player.getUUID())) { count++; } } int max = SettingsAccessor.getMaxBounties(level.getGameRules()); return count < max; } /** * Get the number of active bounties for a player. */ public int getBountyCount(UUID playerId) { int count = 0; for (Bounty bounty : bounties) { if (bounty.isClient(playerId)) { count++; } } return count; } // ==================== HELPERS ==================== private void giveReward(ServerPlayer player, Bounty bounty) { ItemStack reward = bounty.getReward(); if (!reward.isEmpty()) { if (!player.getInventory().add(reward)) { // Inventory full - drop at feet player.drop(reward, false); } } } private void broadcastMessage(MinecraftServer server, String message) { server .getPlayerList() .broadcastSystemMessage( Component.literal("[Bounty] " + message).withStyle( ChatFormatting.GOLD ), false ); } /** * Check if a pending reward has been waiting too long (>30 days). * Uses the bounty's original expiration time as baseline. */ private static boolean isPendingRewardExpired(Bounty bounty) { // Calculate when the bounty originally expired // creationTime is in milliseconds, durationSeconds needs conversion long expirationTime = bounty.getCreationTime() + (bounty.getDurationSeconds() * 1000L); long now = System.currentTimeMillis(); // Check if it's been more than 30 days since expiration return (now - expirationTime) > PENDING_REWARD_EXPIRATION_MS; } }