Remove build artifacts, dev tool configs, unused dependencies, and third-party source dumps. Add proper README, update .gitignore, clean up Makefile.
408 lines
12 KiB
Java
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;
|
|
}
|
|
}
|