Files
TiedUp-/src/main/java/com/tiedup/remake/bounty/BountyManager.java
NotEvil f6466360b6 Clean repo for open source release
Remove build artifacts, dev tool configs, unused dependencies,
and third-party source dumps. Add proper README, update .gitignore,
clean up Makefile.
2026-04-12 00:51:22 +02:00

408 lines
12 KiB
Java

package com.tiedup.remake.bounty;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.core.SettingsAccessor;
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.
*
* Phase 17: Bounty System
*
* 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<Bounty> bounties = new ArrayList<>();
// Bounties for offline players (to return reward when they log in)
// Stored with timestamp via Bounty.creationTime + durationSeconds
private final List<Bounty> 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<Bounty> getBounties(ServerLevel level) {
// Clean up expired bounties
Iterator<Bounty> 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<Bounty> 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<Bounty> 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;
}
}