diff --git a/build.gradle b/build.gradle index c0cd353c9..f82134542 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ plugins { id 'idea' } -version = "1.11-beta8.1" +version = "1.11-beta9" group= "kamkeel.customnpc-plus" archivesBaseName = "CustomNPC-Plus" diff --git a/gradle-plugins b/gradle-plugins index f1c82c10c..34cbba98f 160000 --- a/gradle-plugins +++ b/gradle-plugins @@ -1 +1 @@ -Subproject commit f1c82c10c8c92eaaf55044cf931da5e1c7a2668f +Subproject commit 34cbba98f1d3edf84e8c7d67216fe02a520a5cbc diff --git a/src/api b/src/api index 3c063d8bc..253ef7329 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 3c063d8bc1c27d06107488768fa89357e80bb40d +Subproject commit 253ef7329814f37578a771363e4bde0b61bc6fcd diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/effect/AbilityPotionEffect.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/effect/AbilityPotionEffect.java index 7fe6422f0..52a05a8af 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/effect/AbilityPotionEffect.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/effect/AbilityPotionEffect.java @@ -14,7 +14,7 @@ public class AbilityPotionEffect { private EnumPotionType type = EnumPotionType.None; private int manualPotionId = 0; private int durationTicks = 60; - private int amplifier = 0; // 0-10 + private int amplifier = 0; // 0-255 (Manual), 0-10 (preset) public AbilityPotionEffect() { } @@ -22,7 +22,7 @@ public class AbilityPotionEffect { public AbilityPotionEffect(EnumPotionType type, int durationTicks, int amplifier) { this.type = type; this.durationTicks = Math.max(1, durationTicks); - this.amplifier = Math.max(0, Math.min(10, amplifier)); + this.amplifier = Math.max(0, Math.min(255, amplifier)); } /** @@ -120,7 +120,7 @@ public class AbilityPotionEffect { } public void setAmplifier(int amplifier) { - this.amplifier = Math.max(0, Math.min(10, amplifier)); + this.amplifier = Math.max(0, Math.min(255, amplifier)); } /** diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldBuilder.java b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldBuilder.java index 0a05bc3d6..6438f6ba1 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldBuilder.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldBuilder.java @@ -112,11 +112,22 @@ public class AbilityFieldBuilder extends GuiFieldBuilder { widgetId++; if (effect.getType() != EnumPotionType.Fire) { - GuiNpcButton ampBtn = new GuiNpcButton(widgetId, colLLabel + 158, y, 40, 20, ampValues, effect.getAmplifier()); - sw.addButton(ampBtn); - buttonFieldMap.put(widgetId, def); - effectWidgetMeta.put(widgetId, new int[]{e, 2}); - widgetId++; + if (effect.getType() == EnumPotionType.Manual) { + GuiNpcTextField ampField = new GuiNpcTextField(widgetId, parent, fontRenderer, + colLLabel + 158, y, 40, 20, String.valueOf(effect.getAmplifier())); + ampField.setIntegersOnly(); + ampField.setMinMaxDefault(0, 255, 0); + sw.addTextField(ampField); + textFieldMap.put(widgetId, def); + effectWidgetMeta.put(widgetId, new int[]{e, 2}); + widgetId++; + } else { + GuiNpcButton ampBtn = new GuiNpcButton(widgetId, colLLabel + 158, y, 40, 20, ampValues, effect.getAmplifier()); + sw.addButton(ampBtn); + buttonFieldMap.put(widgetId, def); + effectWidgetMeta.put(widgetId, new int[]{e, 2}); + widgetId++; + } } else { widgetId++; } @@ -367,8 +378,14 @@ public class AbilityFieldBuilder extends GuiFieldBuilder { if (effects == null) return false; switch (action) { case 0: - if (idx < effects.size()) - effects.get(idx).setType(EnumPotionType.fromIndexNoNone(((GuiNpcButton) button).getValue())); + if (idx < effects.size()) { + EnumPotionType oldType = effects.get(idx).getType(); + EnumPotionType newType = EnumPotionType.fromIndexNoNone(((GuiNpcButton) button).getValue()); + if (oldType == EnumPotionType.Manual && newType != EnumPotionType.Manual) { + effects.get(idx).setAmplifier(0); + } + effects.get(idx).setType(newType); + } return true; case 2: if (idx < effects.size()) @@ -471,6 +488,7 @@ public class AbilityFieldBuilder extends GuiFieldBuilder { List effects = (List) def.getValue(); if (effects != null && idx < effects.size()) { if (action == 1) effects.get(idx).setDurationTicks(field.getInteger()); + else if (action == 2) effects.get(idx).setAmplifier(field.getInteger()); else if (action == 5) effects.get(idx).setManualPotionId(field.getInteger()); } return true; diff --git a/src/main/java/kamkeel/npcs/network/packets/data/ability/AbilityCooldownSyncPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/ability/AbilityCooldownSyncPacket.java index 484cc602c..aa7dd62aa 100644 --- a/src/main/java/kamkeel/npcs/network/packets/data/ability/AbilityCooldownSyncPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/data/ability/AbilityCooldownSyncPacket.java @@ -10,6 +10,7 @@ import kamkeel.npcs.network.enums.EnumDataPacket; import kamkeel.npcs.util.ByteBufUtils; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; +import noppes.npcs.client.ClientCacheHandler; import noppes.npcs.controllers.PlayerDataController; import noppes.npcs.controllers.data.PlayerData; @@ -86,7 +87,7 @@ public final class AbilityCooldownSyncPacket extends AbstractPacket { perDurations.put(key, duration); } - PlayerData data = PlayerData.get(player); + PlayerData data = ClientCacheHandler.playerData; if (data != null && data.abilityData != null) { data.abilityData.applyCooldownSync(globalEndTime, globalDuration, perEndTimes, perDurations); } diff --git a/src/main/java/kamkeel/npcs/network/packets/data/ability/PlayerAbilitySyncPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/ability/PlayerAbilitySyncPacket.java index 08df836aa..2353623b2 100644 --- a/src/main/java/kamkeel/npcs/network/packets/data/ability/PlayerAbilitySyncPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/data/ability/PlayerAbilitySyncPacket.java @@ -11,6 +11,7 @@ import kamkeel.npcs.util.ByteBufUtils; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.client.ClientCacheHandler; import noppes.npcs.controllers.PlayerDataController; import noppes.npcs.controllers.data.PlayerData; @@ -53,7 +54,7 @@ public final class PlayerAbilitySyncPacket extends AbstractPacket { NBTTagCompound nbt = ByteBufUtils.readNBT(in); if (nbt == null) return; - PlayerData data = PlayerData.get(player); + PlayerData data = ClientCacheHandler.playerData; if (data != null) { data.abilityData.readFromNBT(nbt); } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/auction/ManageAuctionPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/auction/ManageAuctionPacket.java index 10b4504b9..f2ad4c961 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/auction/ManageAuctionPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/auction/ManageAuctionPacket.java @@ -366,8 +366,9 @@ public class ManageAuctionPacket extends AbstractPacket { String result = controller.createGlobalListing(player, sellerName, item, startingPrice, buyoutPrice, durationHours); if (result != null) { // Failed create, return item (or drop to avoid loss). - if (!player.inventory.addItemStackToInventory(item.copy())) { - player.entityDropItem(item.copy(), 0.5f); + // Use same reference so partial add mutates stackSize, then drop only the remainder. + if (!player.inventory.addItemStackToInventory(item)) { + player.entityDropItem(item, 0.5f); } } if (result == null) { diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 0759fcc57..2f6d65a7c 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -122,7 +122,7 @@ import java.util.HashSet; import java.util.Set; import java.util.function.Consumer; -@Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.11-beta8.1") +@Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.11-beta9") public class CustomNpcs { @SidedProxy(clientSide = "noppes.npcs.client.ClientProxy", serverSide = "noppes.npcs.CommonProxy") diff --git a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java index 05d6b0889..3da3ccc56 100644 --- a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java @@ -147,10 +147,7 @@ public class ScriptPlayerEventHandler { EntityPlayerMP mp = (EntityPlayerMP) player; SyncController.syncEffects(mp); SyncController.syncAbilityCooldowns(mp); - if (playerData.abilityData.needsSync) { - PlayerAbilitySyncPacket.sendToPlayer(mp); - playerData.abilityData.needsSync = false; - } + PlayerAbilitySyncPacket.sendToPlayer(mp); playerData.abilityData.ensureLockStateClear(); } diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index 170d706cf..cd889fa5b 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -417,7 +417,8 @@ public class ClientProxy extends CommonProxy { @Override public PlayerData getPlayerData(EntityPlayer player) { - if (player.getUniqueID() == Minecraft.getMinecraft().thePlayer.getUniqueID()) { + EntityPlayer local = Minecraft.getMinecraft().thePlayer; + if (local != null && player.getUniqueID().equals(local.getUniqueID())) { if (ClientCacheHandler.playerData != null) { if (ClientCacheHandler.playerData.player != player) { ClientCacheHandler.playerData.player = player; diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiNpcMeleeProperties.java b/src/main/java/noppes/npcs/client/gui/SubGuiNpcMeleeProperties.java index 36cbfff37..c40c009fe 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiNpcMeleeProperties.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiNpcMeleeProperties.java @@ -58,7 +58,13 @@ public class SubGuiNpcMeleeProperties extends SubGuiInterface implements ITextfi if (stats.potionType != EnumPotionType.Fire) { y += 30; addLabel(new GuiNpcLabel(7, "stats.amplify", guiLeft + 5, y + 5)); - addButton(new GuiButtonBiDirectional(7, guiLeft + 85, y, 52, 20, new String[]{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}, stats.potionAmp)); + if (stats.potionType == EnumPotionType.Manual) { + addTextField(new GuiNpcTextField(7, this, fontRendererObj, guiLeft + 85, y, 52, 18, stats.potionAmp + "")); + getTextField(7).integersOnly = true; + getTextField(7).setMinMaxDefault(0, 255, 0); + } else { + addButton(new GuiButtonBiDirectional(7, guiLeft + 85, y, 52, 20, new String[]{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}, stats.potionAmp)); + } } } @@ -76,6 +82,8 @@ public class SubGuiNpcMeleeProperties extends SubGuiInterface implements ITextfi stats.knockback = textfield.getInteger(); } else if (textfield.id == 6) { stats.potionDuration = textfield.getInteger(); + } else if (textfield.id == 7) { + stats.potionAmp = textfield.getInteger(); } else if (textfield.id == 8) { stats.potionManualId = textfield.getInteger(); } @@ -84,7 +92,11 @@ public class SubGuiNpcMeleeProperties extends SubGuiInterface implements ITextfi protected void actionPerformed(GuiButton guibutton) { GuiNpcButton button = (GuiNpcButton) guibutton; if (button.id == 5) { - stats.potionType = EnumPotionType.fromOrdinal(button.getValue()); + EnumPotionType newType = EnumPotionType.fromOrdinal(button.getValue()); + if (stats.potionType == EnumPotionType.Manual && newType != EnumPotionType.Manual) { + stats.potionAmp = 0; + } + stats.potionType = newType; initGui(); } if (button.id == 7) { diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiNpcProjectiles.java b/src/main/java/noppes/npcs/client/gui/SubGuiNpcProjectiles.java index e0806a538..eee99a12e 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiNpcProjectiles.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiNpcProjectiles.java @@ -97,7 +97,13 @@ public class SubGuiNpcProjectiles extends SubGuiInterface implements ITextfieldL } else { internalY += 30; addLabel(new GuiNpcLabel(70, "stats.amplify", guiLeft + 210, internalY + 5)); - addButton(new GuiButtonBiDirectional(10, guiLeft + 280, internalY, 52, 20, new String[]{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}, stats.pEffAmp)); + if (stats.pEffect == EnumPotionType.Manual) { + addTextField(new GuiNpcTextField(10, this, fontRendererObj, guiLeft + 280, internalY, 52, 20, stats.pEffAmp + "")); + getTextField(10).integersOnly = true; + getTextField(10).setMinMaxDefault(0, 255, 0); + } else { + addButton(new GuiButtonBiDirectional(10, guiLeft + 280, internalY, 52, 20, new String[]{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}, stats.pEffAmp)); + } } } @@ -132,6 +138,8 @@ public class SubGuiNpcProjectiles extends SubGuiInterface implements ITextfieldL stats.pSpeed = textfield.getInteger(); } else if (textfield.id == 5) { stats.pDur = textfield.getInteger(); + } else if (textfield.id == 10) { + stats.pEffAmp = textfield.getInteger(); } else if (textfield.id == 11) { stats.pManualId = textfield.getInteger(); } @@ -154,7 +162,11 @@ public class SubGuiNpcProjectiles extends SubGuiInterface implements ITextfieldL stats.pArea = button.getValue(); } if (button.id == 4) { - stats.pEffect = EnumPotionType.fromOrdinal(button.getValue()); + EnumPotionType newType = EnumPotionType.fromOrdinal(button.getValue()); + if (stats.pEffect == EnumPotionType.Manual && newType != EnumPotionType.Manual) { + stats.pEffAmp = 0; + } + stats.pEffect = newType; initGui(); } if (button.id == 5) { diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java b/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java index 71b5ed611..b527663d6 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java @@ -63,6 +63,9 @@ public class GuiNpcTextField extends GuiTextField { if (integersOnly && getText().isEmpty() && c == '-') return true; + if ((floatsOnly || doublesOnly) && getText().isEmpty() && c == '-') + return true; + for (int j : allowedSpecialChars) if (j == i) return true; diff --git a/src/main/java/noppes/npcs/controllers/AuctionController.java b/src/main/java/noppes/npcs/controllers/AuctionController.java index 1928b1b81..b754accac 100644 --- a/src/main/java/noppes/npcs/controllers/AuctionController.java +++ b/src/main/java/noppes/npcs/controllers/AuctionController.java @@ -289,7 +289,15 @@ public class AuctionController implements IAuctionHandler { // Global claims are not tied to a single player. claim.playerUUID = null; claim.playerName = "Global"; - globalClaims.add(claim); + synchronized (globalClaims) { + // Prevent duplicate claim IDs + for (AuctionClaim existing : globalClaims) { + if (existing.id.equals(claim.id)) { + return; + } + } + globalClaims.add(claim); + } markDirty(); } @@ -305,19 +313,23 @@ public class AuctionController implements IAuctionHandler { private AuctionClaim getGlobalClaimInternal(String claimId) { if (claimId == null || claimId.isEmpty()) return null; - for (AuctionClaim claim : globalClaims) { - if (claim != null && !claim.claimed && claimId.equals(claim.id)) { - return claim; + synchronized (globalClaims) { + for (AuctionClaim claim : globalClaims) { + if (claim != null && !claim.claimed && claimId.equals(claim.id)) { + return claim; + } } } return null; } private boolean claimAndRemoveGlobal(String claimId) { - AuctionClaim claim = getGlobalClaimInternal(claimId); - if (claim == null) return false; - claim.claimed = true; - return globalClaims.remove(claim); + synchronized (globalClaims) { + AuctionClaim claim = getGlobalClaimInternal(claimId); + if (claim == null) return false; + claim.claimed = true; + return globalClaims.remove(claim); + } } /** @@ -358,15 +370,27 @@ public class AuctionController implements IAuctionHandler { return "This item cannot be listed on the Auction House."; } - // Validate minimum price + // Validate prices + if (startingPrice <= 0) { + return "Starting price must be positive."; + } if (startingPrice < ConfigMarket.MinimumListingPrice) { return "Starting price must be at least " + ConfigMarket.MinimumListingPrice + " " + ConfigMarket.CurrencyName + "."; } + if (startingPrice > ConfigMarket.MaxBalance) { + return "Starting price exceeds maximum allowed value."; + } // Validate buyout price if set + if (buyoutPrice < 0) { + return "Buyout price cannot be negative."; + } if (buyoutPrice > 0 && buyoutPrice < startingPrice) { return "Buyout price must be higher than starting price."; } + if (buyoutPrice > ConfigMarket.MaxBalance) { + return "Buyout price exceeds maximum allowed value."; + } UUID playerUUID = player.getUniqueID(); String playerName = player.getCommandSenderName(); @@ -386,7 +410,7 @@ public class AuctionController implements IAuctionHandler { PlayerTradeData currency = playerData.tradeData; long fee = ConfigMarket.ListingFee; - if (fee > 0 && !currency.canAfford(fee)) { + if (!currency.canAfford(fee)) { return "You cannot afford the listing fee (" + fee + " " + ConfigMarket.CurrencyName + ")."; } @@ -400,7 +424,7 @@ public class AuctionController implements IAuctionHandler { } // Deduct fee - if (fee > 0 && !currency.withdraw(fee)) { + if (!currency.withdraw(fee)) { return "Failed to deduct listing fee."; } @@ -435,9 +459,16 @@ public class AuctionController implements IAuctionHandler { return "No item provided."; } + // Validate prices + if (startingPrice <= 0) { + return "Starting price must be positive."; + } if (startingPrice < ConfigMarket.MinimumListingPrice) { return "Starting price must be at least " + ConfigMarket.MinimumListingPrice + " " + ConfigMarket.CurrencyName + "."; } + if (buyoutPrice < 0) { + return "Buyout price cannot be negative."; + } if (buyoutPrice > 0 && buyoutPrice < startingPrice) { return "Buyout price must be higher than starting price."; } @@ -510,22 +541,23 @@ public class AuctionController implements IAuctionHandler { listing.sellerUUID, listing.sellerName, listing.id, listing.item); addClaimToPlayer(listing.sellerUUID, itemClaim); - // Deduct cancellation penalty from seller (take what they can afford) - long actualPenalty = 0; + // Deduct cancellation penalty from seller if (penalty > 0) { PlayerData sellerData = PlayerDataController.Instance.getData(listing.sellerUUID); if (sellerData != null) { - long sellerBalance = sellerData.tradeData.getBalance(); - actualPenalty = Math.min(penalty, sellerBalance); - if (actualPenalty > 0) { - sellerData.tradeData.withdraw(actualPenalty); + if (!sellerData.tradeData.withdraw(penalty)) { + // Withdraw whatever they have if they can't cover the full penalty + long available = sellerData.tradeData.getBalance(); + if (available > 0) { + sellerData.tradeData.withdraw(available); + } } sellerData.save(); } } logAuction(EnumAuctionLogAction.CANCELLED, listing.sellerName, listing.item.getDisplayName(), listing.currentBid, - "Penalty: " + actualPenalty + "/" + penalty + ", Bidder refunded: " + listing.highBidderName + + "Penalty: " + penalty + ", Bidder refunded: " + listing.highBidderName + (isAdmin ? ", Cancelled by admin: " + player.getCommandSenderName() : "")); } else { // No bids - just return item @@ -556,6 +588,11 @@ public class AuctionController implements IAuctionHandler { return "Auction is disabled."; } + // Validate bid amount + if (bidAmount <= 0) { + return "Bid amount must be positive."; + } + UUID playerUUID = player.getUniqueID(); String playerName = player.getCommandSenderName(); @@ -647,10 +684,10 @@ public class AuctionController implements IAuctionHandler { listing.currentBid = bidAmount; listing.bidCount++; - // Add to new bidder's active bids (inside sync to prevent trade slot count race) + // Add to new bidder's active bids addToPlayerBids(playerUUID, listing.id); - // Snipe protection (inside sync to prevent stale endTime reads) + // Snipe protection (must be inside sync to prevent race with endAuction) listing.extendForSnipeProtection(ConfigMarket.SnipeProtectionMinutes); } @@ -839,30 +876,36 @@ public class AuctionController implements IAuctionHandler { } public String claimGlobalItem(String claimId, EntityPlayer player) { - AuctionClaim claim = getGlobalClaimInternal(claimId); - if (claim == null) { - return "Global claim not found."; - } - if (!claim.type.isItem()) { - return "This global claim is not an item claim."; - } - if (claim.item == null) { - return "Item data is missing."; - } + String listingId; + String itemName; + synchronized (globalClaims) { + AuctionClaim claim = getGlobalClaimInternal(claimId); + if (claim == null) { + return "Global claim not found."; + } + if (!claim.type.isItem()) { + return "This global claim is not an item claim."; + } + if (claim.item == null) { + return "Item data is missing."; + } - // Pre-check inventory space to prevent partial-add dupe - if (!canInventoryHold(player, claim.item)) { - return "Your inventory is full."; + // Check inventory space before adding (addItemStackToInventory mutates even on partial add) + if (!canFitInInventory(player, claim.item)) { + return "Not enough inventory space to claim this item."; + } + + // Add to inventory (safe - we verified space above) + player.inventory.addItemStackToInventory(claim.item.copy()); + + listingId = claim.listingId; + itemName = claim.item.getDisplayName(); + claimAndRemoveGlobal(claim.id); } - - // Safe to add - inventory has enough space for the full stack - player.inventory.addItemStackToInventory(claim.item.copy()); - - claimAndRemoveGlobal(claim.id); - cleanupListingIfComplete(claim.listingId); + cleanupListingIfComplete(listingId); markDirty(); - logAuction(EnumAuctionLogAction.CLAIMED, player.getCommandSenderName(), claim.item.getDisplayName(), 0, + logAuction(EnumAuctionLogAction.CLAIMED, player.getCommandSenderName(), itemName, 0, "Global item claim"); return null; } @@ -871,19 +914,25 @@ public class AuctionController implements IAuctionHandler { * Global currency claims are acknowledged/cleared and do not alter player balances. */ public String claimGlobalCurrency(String claimId, EntityPlayer player) { - AuctionClaim claim = getGlobalClaimInternal(claimId); - if (claim == null) { - return "Global claim not found."; - } - if (!claim.type.isCurrency()) { - return "This global claim is not a currency claim."; - } + String listingId; + long currency; + synchronized (globalClaims) { + AuctionClaim claim = getGlobalClaimInternal(claimId); + if (claim == null) { + return "Global claim not found."; + } + if (!claim.type.isCurrency()) { + return "This global claim is not a currency claim."; + } - claimAndRemoveGlobal(claim.id); - cleanupListingIfComplete(claim.listingId); + listingId = claim.listingId; + currency = claim.currency; + claimAndRemoveGlobal(claim.id); + } + cleanupListingIfComplete(listingId); markDirty(); - logAuction(EnumAuctionLogAction.CLAIMED, player.getCommandSenderName(), ConfigMarket.CurrencyName, claim.currency, + logAuction(EnumAuctionLogAction.CLAIMED, player.getCommandSenderName(), ConfigMarket.CurrencyName, currency, "Global currency claim acknowledged (no balance change)"); return null; } @@ -919,12 +968,12 @@ public class AuctionController implements IAuctionHandler { return "Claim was prevented."; } - // Pre-check inventory space to prevent partial-add dupe - if (!canInventoryHold(player, claim.item)) { - return "Your inventory is full."; + // Check inventory space before adding (addItemStackToInventory mutates even on partial add) + if (!canFitInInventory(player, claim.item)) { + return "Not enough inventory space to claim this item."; } - // Safe to add - inventory has enough space for the full stack + // Add to inventory (safe - we verified space above) player.inventory.addItemStackToInventory(claim.item.copy()); // Remove claim from player's auction data @@ -986,26 +1035,30 @@ public class AuctionController implements IAuctionHandler { } /** - * Pre-check if the player's inventory can fully hold an ItemStack. - * Unlike addItemStackToInventory, this does NOT modify the inventory. - * Prevents partial-add dupe exploits. + * Check if a player's inventory can fit the entire ItemStack without partial adds. + * Unlike addItemStackToInventory, this does NOT mutate the inventory. */ - private boolean canInventoryHold(EntityPlayer player, ItemStack item) { - if (item == null || item.stackSize <= 0) return true; + private boolean canFitInInventory(EntityPlayer player, ItemStack stack) { + int remaining = stack.stackSize; + int maxStack = stack.getMaxStackSize(); + ItemStack[] mainInventory = player.inventory.mainInventory; - int remaining = item.stackSize; - int maxStack = Math.min(player.inventory.getInventoryStackLimit(), item.getMaxStackSize()); + for (int i = 0; i < mainInventory.length; i++) { + if (remaining <= 0) return true; - for (int i = 0; i < player.inventory.mainInventory.length && remaining > 0; i++) { - ItemStack slot = player.inventory.mainInventory[i]; + ItemStack slot = mainInventory[i]; if (slot == null) { - remaining -= maxStack; - } else if (slot.getItem() == item.getItem() - && slot.getItemDamage() == item.getItemDamage() - && ItemStack.areItemStackTagsEqual(slot, item)) { - remaining -= (maxStack - slot.stackSize); + remaining -= Math.min(remaining, maxStack); + } else if (slot.getItem() == stack.getItem() + && slot.getItemDamage() == stack.getItemDamage() + && ItemStack.areItemStackTagsEqual(slot, stack)) { + int spaceInSlot = maxStack - slot.stackSize; + if (spaceInSlot > 0) { + remaining -= Math.min(remaining, spaceInSlot); + } } } + return remaining <= 0; } @@ -1055,7 +1108,6 @@ public class AuctionController implements IAuctionHandler { for (AuctionListing listing : listings.values()) { if (listing.status != EnumAuctionStatus.ACTIVE) continue; - if (listing.isExpired()) continue; // Apply search filter (searches item name AND seller name) if (filter.hasSearchText()) { @@ -1106,7 +1158,6 @@ public class AuctionController implements IAuctionHandler { int count = 0; for (AuctionListing listing : listings.values()) { if (listing.status != EnumAuctionStatus.ACTIVE) continue; - if (listing.isExpired()) continue; if (filter.hasSearchText()) { String itemName = listing.item != null ? listing.item.getDisplayName() : ""; String sellerName = listing.sellerName != null ? listing.sellerName : ""; @@ -1793,12 +1844,20 @@ public class AuctionController implements IAuctionHandler { String itemName = listing.item != null ? listing.item.getDisplayName() : "Unknown Item"; - // Return active highest bid - always refund to the actual bidder + // Return active highest bid. if (listing.hasBids() && listing.highBidderUUID != null) { - AuctionClaim refundClaim = AuctionClaim.createRefundClaim( - listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid, itemName, - listing.sellerName, null); - addClaimToPlayer(listing.highBidderUUID, refundClaim); + AuctionClaim refundClaim; + if (cancelCompletely) { + refundClaim = AuctionClaim.createRefundClaim( + null, "Global", listing.id, listing.currentBid, itemName, + listing.highBidderName, null); + addGlobalClaim(refundClaim); + } else { + refundClaim = AuctionClaim.createRefundClaim( + listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid, itemName, + listing.sellerName, null); + addClaimToPlayer(listing.highBidderUUID, refundClaim); + } removeFromPlayerBids(listing.highBidderUUID, listing.id); } diff --git a/src/main/java/noppes/npcs/controllers/LinkedItemController.java b/src/main/java/noppes/npcs/controllers/LinkedItemController.java index 78534a82f..efba64ff5 100644 --- a/src/main/java/noppes/npcs/controllers/LinkedItemController.java +++ b/src/main/java/noppes/npcs/controllers/LinkedItemController.java @@ -5,6 +5,7 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; +import noppes.npcs.api.handler.ILinkedItemHandler; import noppes.npcs.api.handler.data.ILinkedItem; import noppes.npcs.api.item.IItemStack; import noppes.npcs.controllers.data.IScriptHandler; @@ -21,7 +22,7 @@ import java.io.IOException; import java.util.HashMap; import java.util.zip.GZIPInputStream; -public class LinkedItemController { +public class LinkedItemController implements ILinkedItemHandler { private static LinkedItemController Instance; private int lastUsedID = 0; @@ -40,25 +41,28 @@ public class LinkedItemController { return Instance; } - public LinkedItem createItem(String name) { + @Override + public ILinkedItem createItem(String name) { return new LinkedItem(name); } + @Override public IItemStack createItemStack(int id) { - LinkedItem linkedItem = this.get(id); + LinkedItem linkedItem = (LinkedItem) this.get(id); if (linkedItem != null) { return linkedItem.createStack(); } return null; } - public void add(LinkedItem linkedItem) { + @Override + public void add(ILinkedItem linkedItem) { if (linkedItem != null) { String name = linkedItem.getName(); if (name != null && !name.isEmpty()) { int linkedItemId = linkedItem.getId(); int id = this.linkedItems.containsKey(linkedItemId) ? linkedItemId : this.getUnusedId(); - this.linkedItems.put(id, linkedItem); + this.linkedItems.put(id, (LinkedItem) linkedItem); linkedItem.setId(id); this.addScript(id); } @@ -69,6 +73,7 @@ public class LinkedItemController { this.linkedItemsScripts.put(id, new LinkedItemScript()); } + @Override public LinkedItem remove(int id) { this.removeScript(id); return this.linkedItems.remove(id); @@ -78,16 +83,19 @@ public class LinkedItemController { this.linkedItemsScripts.remove(id); } - public LinkedItem get(int id) { - return this.linkedItems.get(id); + @Override + public ILinkedItem get(int id) { + return (LinkedItem) this.linkedItems.get(id); } + @Override public boolean contains(int id) { return this.linkedItems.containsKey(id); } - public boolean contains(LinkedItem linkedItem) { - return this.linkedItems.containsValue(linkedItem); + @Override + public boolean contains(ILinkedItem linkedItem) { + return this.linkedItems.containsValue((LinkedItem) linkedItem); } public IScriptHandler getScriptHandler(int id) { @@ -219,7 +227,7 @@ public class LinkedItemController { } public void delete(int id) { - LinkedItem linkedItem = get(id); + LinkedItem linkedItem = (LinkedItem) get(id); if (linkedItem != null) { LinkedItem foundItem = remove(id); if (foundItem != null && foundItem.name != null) { diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index 2230e18c4..f5a36e836 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -91,14 +91,6 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA */ private transient byte lastSyncedFlags = 0; - /** - * Dirty flag: set when ability data changes (unlock/lock/select/toggle). - * Checked by the periodic 10-tick sync handler, which sends an ability - * data packet and clears the flag. Avoids scattering sync calls across - * every mutation method. - */ - public transient boolean needsSync = false; - /** * World time when the last ability was activated (for double-press cancel detection). */ @@ -497,7 +489,6 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA @Override protected void onToggleStateChanged(String key, boolean active, int state) { - needsSync = true; } @Override @@ -534,20 +525,17 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA public void setSelectedIndex(int index) { if (index == -1 || (index >= 0 && index < unlockedAbilities.size())) { this.selectedIndex = index; - needsSync = true; } } public void selectNext() { if (unlockedAbilities.isEmpty()) return; selectedIndex = (selectedIndex + 1) % unlockedAbilities.size(); - needsSync = true; } public void selectPrevious() { if (unlockedAbilities.isEmpty()) return; selectedIndex = (selectedIndex - 1 + unlockedAbilities.size()) % unlockedAbilities.size(); - needsSync = true; } public String getSelectedAbilityKey() { @@ -576,13 +564,11 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA if (selectedIndex >= unlockedAbilities.size()) { selectedIndex = Math.max(0, unlockedAbilities.size() - 1); } - needsSync = true; } public void unlockAbility(String key) { if (key != null && !key.isEmpty() && !unlockedAbilities.contains(key)) { unlockedAbilities.add(key); - needsSync = true; } } @@ -591,7 +577,6 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA if (selectedIndex >= unlockedAbilities.size()) { selectedIndex = Math.max(0, unlockedAbilities.size() - 1); } - needsSync = true; } public boolean hasUnlockedAbility(String key) { @@ -837,7 +822,6 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA // Reset sync tracking so next sync sends fresh state lastSyncedFlags = 0; - needsSync = true; } /** diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerTradeData.java b/src/main/java/noppes/npcs/controllers/data/PlayerTradeData.java index 2850ddb90..f2c0d19d6 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerTradeData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerTradeData.java @@ -245,8 +245,14 @@ public class PlayerTradeData implements IPlayerTradeData { * Add a claim to this player's pending claims. * Thread-safe - can be called from any thread. */ - public void addClaim(AuctionClaim claim) { + public synchronized void addClaim(AuctionClaim claim) { if (claim != null && !claim.claimed) { + // Prevent duplicate claim IDs + for (AuctionClaim existing : claims) { + if (existing.id.equals(claim.id)) { + return; + } + } claims.add(claim); } } @@ -276,8 +282,9 @@ public class PlayerTradeData implements IPlayerTradeData { /** * Get a specific claim by ID (internal use). + * Synchronized to pair with claimAndRemove for thread safety. */ - public AuctionClaim getClaimInternal(String claimId) { + public synchronized AuctionClaim getClaimInternal(String claimId) { for (AuctionClaim claim : claims) { if (claim.id.equals(claimId) && !claim.claimed) { return claim; @@ -296,8 +303,9 @@ public class PlayerTradeData implements IPlayerTradeData { /** * Mark a claim as claimed and remove it. + * Synchronized to prevent double-claim from concurrent threads. */ - public boolean claimAndRemove(String claimId) { + public synchronized boolean claimAndRemove(String claimId) { for (AuctionClaim claim : claims) { if (claim.id.equals(claimId) && !claim.claimed) { claim.claimed = true; diff --git a/src/main/java/noppes/npcs/scripted/NpcAPI.java b/src/main/java/noppes/npcs/scripted/NpcAPI.java index 8cb554c3e..09c294e2f 100644 --- a/src/main/java/noppes/npcs/scripted/NpcAPI.java +++ b/src/main/java/noppes/npcs/scripted/NpcAPI.java @@ -71,24 +71,7 @@ import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.api.entity.IEntity; import noppes.npcs.api.entity.IPlayer; import noppes.npcs.api.gui.ICustomGui; -import noppes.npcs.api.handler.IAbilityHandler; -import noppes.npcs.api.handler.IActionManager; -import noppes.npcs.api.handler.IAnimationHandler; -import noppes.npcs.api.handler.IAttributeHandler; -import noppes.npcs.api.handler.IAuctionHandler; -import noppes.npcs.api.handler.ICloneHandler; -import noppes.npcs.api.handler.ICustomEffectHandler; -import noppes.npcs.api.handler.IDialogHandler; -import noppes.npcs.api.handler.IFactionHandler; -import noppes.npcs.api.handler.IMagicHandler; -import noppes.npcs.api.handler.INaturalSpawnsHandler; -import noppes.npcs.api.handler.IPartyHandler; -import noppes.npcs.api.handler.IProfileHandler; -import noppes.npcs.api.handler.IQuestHandler; -import noppes.npcs.api.handler.IRecipeHandler; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.api.handler.ITelegraphHandler; -import noppes.npcs.api.handler.ITransportHandler; +import noppes.npcs.api.handler.*; import noppes.npcs.api.handler.data.IAnimation; import noppes.npcs.api.handler.data.IFrame; import noppes.npcs.api.handler.data.IFramePart; @@ -99,22 +82,7 @@ import noppes.npcs.compat.PixelmonHelper; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumAnimationPart; import noppes.npcs.containers.ContainerNpcInterface; -import noppes.npcs.controllers.AnimationController; -import noppes.npcs.controllers.AuctionController; -import noppes.npcs.controllers.ChunkController; -import noppes.npcs.controllers.CustomEffectController; -import noppes.npcs.controllers.DialogController; -import noppes.npcs.controllers.FactionController; -import noppes.npcs.controllers.MagicController; -import noppes.npcs.controllers.PartyController; -import noppes.npcs.controllers.QuestController; -import noppes.npcs.controllers.RecipeController; -import noppes.npcs.controllers.ScriptController; -import noppes.npcs.controllers.ScriptEntityData; -import noppes.npcs.controllers.ScriptHookController; -import noppes.npcs.controllers.ServerCloneController; -import noppes.npcs.controllers.SpawnController; -import noppes.npcs.controllers.TransportController; +import noppes.npcs.controllers.*; import noppes.npcs.controllers.data.Animation; import noppes.npcs.controllers.data.Frame; import noppes.npcs.controllers.data.FramePart; @@ -383,6 +351,10 @@ public class NpcAPI extends AbstractNpcAPI { return AnimationController.Instance; } + public ILinkedItemHandler getLinkedItems() { + return LinkedItemController.getInstance(); + } + public IScriptHookHandler getScriptHooks() { return ScriptHookController.Instance; }