Strip all Phase references, TODO/FUTURE roadmap notes, and internal planning comments from the codebase. Run Prettier for consistent formatting across all Java files.
407 lines
12 KiB
Java
407 lines
12 KiB
Java
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<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;
|
|
}
|
|
}
|