From bf277bbdfecde9d2fd8b3eb7854b52d5a463910d Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 24 Feb 2026 17:46:52 -0500 Subject: [PATCH 01/11] Auction House Space Check Fix --- .../request/auction/ManageAuctionPacket.java | 5 +- .../npcs/controllers/AuctionController.java | 179 +++++++++++++----- .../controllers/data/PlayerTradeData.java | 14 +- 3 files changed, 148 insertions(+), 50 deletions(-) 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/controllers/AuctionController.java b/src/main/java/noppes/npcs/controllers/AuctionController.java index 8bf06343f..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(); @@ -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."; } @@ -514,7 +545,13 @@ public class AuctionController implements IAuctionHandler { if (penalty > 0) { PlayerData sellerData = PlayerDataController.Instance.getData(listing.sellerUUID); if (sellerData != null) { - sellerData.tradeData.withdraw(penalty); + 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(); } } @@ -551,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(); @@ -641,14 +683,14 @@ public class AuctionController implements IAuctionHandler { listing.highBidderName = playerName; listing.currentBid = bidAmount; listing.bidCount++; + + // Add to new bidder's active bids + addToPlayerBids(playerUUID, listing.id); + + // Snipe protection (must be inside sync to prevent race with endAuction) + listing.extendForSnipeProtection(ConfigMarket.SnipeProtectionMinutes); } - // Add to new bidder's active bids - addToPlayerBids(playerUUID, listing.id); - - // Snipe protection - listing.extendForSnipeProtection(ConfigMarket.SnipeProtectionMinutes); - markDirty(); logAuction(EnumAuctionLogAction.BID, playerName, listing.item.getDisplayName(), bidAmount, @@ -834,26 +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."; + } - if (!player.inventory.addItemStackToInventory(claim.item.copy())) { - 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."; + } - claimAndRemoveGlobal(claim.id); - cleanupListingIfComplete(claim.listingId); + // Add to inventory (safe - we verified space above) + player.inventory.addItemStackToInventory(claim.item.copy()); + + listingId = claim.listingId; + itemName = claim.item.getDisplayName(); + claimAndRemoveGlobal(claim.id); + } + cleanupListingIfComplete(listingId); markDirty(); - logAuction(EnumAuctionLogAction.CLAIMED, player.getCommandSenderName(), claim.item.getDisplayName(), 0, + logAuction(EnumAuctionLogAction.CLAIMED, player.getCommandSenderName(), itemName, 0, "Global item claim"); return null; } @@ -862,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; } @@ -910,11 +968,14 @@ public class AuctionController implements IAuctionHandler { return "Claim was prevented."; } - // Check inventory space - if (!player.inventory.addItemStackToInventory(claim.item.copy())) { - 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()); + // Remove claim from player's auction data playerData.tradeData.claimAndRemove(claimId); playerData.updateClient = true; @@ -973,6 +1034,34 @@ public class AuctionController implements IAuctionHandler { return null; // Success } + /** + * Check if a player's inventory can fit the entire ItemStack without partial adds. + * Unlike addItemStackToInventory, this does NOT mutate the inventory. + */ + private boolean canFitInInventory(EntityPlayer player, ItemStack stack) { + int remaining = stack.stackSize; + int maxStack = stack.getMaxStackSize(); + ItemStack[] mainInventory = player.inventory.mainInventory; + + for (int i = 0; i < mainInventory.length; i++) { + if (remaining <= 0) return true; + + ItemStack slot = mainInventory[i]; + if (slot == null) { + 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; + } + /** * Clean up a listing if it's ended and no longer needed. * Now just removes ended listings after some delay since claims are in PlayerData. 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; From a023f3ec82e2ccd19022138cfd75e3f995fa8afb Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 24 Feb 2026 18:20:57 -0500 Subject: [PATCH 02/11] Better Ability Data Syncing --- .../data/ability/AbilityCooldownSyncPacket.java | 3 ++- .../data/ability/PlayerAbilitySyncPacket.java | 3 ++- .../noppes/npcs/ScriptPlayerEventHandler.java | 5 +---- .../java/noppes/npcs/client/ClientProxy.java | 3 ++- .../npcs/controllers/data/PlayerAbilityData.java | 16 ---------------- 5 files changed, 7 insertions(+), 23 deletions(-) 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/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/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; } /** From 36f76f9cbebf531503f27d03c6c0126f1449be14 Mon Sep 17 00:00:00 2001 From: Vidal Date: Tue, 24 Feb 2026 20:23:44 -0300 Subject: [PATCH 03/11] Linked Item API methods --- gradle-plugins | 2 +- src/api | 2 +- .../controllers/LinkedItemController.java | 28 ++++++++----- .../java/noppes/npcs/scripted/NpcAPI.java | 40 +++---------------- 4 files changed, 26 insertions(+), 46 deletions(-) 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/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/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; } From aa186d8b7d317dcca1221d6209cb43570658d09e Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 24 Feb 2026 18:24:44 -0500 Subject: [PATCH 04/11] Smaller Auction System Fixes --- .../packets/player/AuctionActionPacket.java | 4 + .../npcs/controllers/AuctionController.java | 84 +++++++++++++------ 2 files changed, 61 insertions(+), 27 deletions(-) diff --git a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java index 5eed2aa93..cf0fb3a36 100644 --- a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java @@ -311,6 +311,10 @@ public class AuctionActionPacket extends AbstractPacket { ItemStack item = ItemStack.loadItemStackFromNBT(itemNBT); if (item == null) return "Invalid item data."; + // Validate stack size + if (item.stackSize <= 0) return "Invalid item amount."; + if (item.stackSize > item.getMaxStackSize()) return "Stack size exceeds item maximum."; + // Remove item from inventory first if (!removeItemFromInventory(player, item)) { return "Item not found in inventory."; diff --git a/src/main/java/noppes/npcs/controllers/AuctionController.java b/src/main/java/noppes/npcs/controllers/AuctionController.java index 8bf06343f..1928b1b81 100644 --- a/src/main/java/noppes/npcs/controllers/AuctionController.java +++ b/src/main/java/noppes/npcs/controllers/AuctionController.java @@ -386,7 +386,7 @@ public class AuctionController implements IAuctionHandler { PlayerTradeData currency = playerData.tradeData; long fee = ConfigMarket.ListingFee; - if (!currency.canAfford(fee)) { + if (fee > 0 && !currency.canAfford(fee)) { return "You cannot afford the listing fee (" + fee + " " + ConfigMarket.CurrencyName + ")."; } @@ -400,7 +400,7 @@ public class AuctionController implements IAuctionHandler { } // Deduct fee - if (!currency.withdraw(fee)) { + if (fee > 0 && !currency.withdraw(fee)) { return "Failed to deduct listing fee."; } @@ -510,17 +510,22 @@ public class AuctionController implements IAuctionHandler { listing.sellerUUID, listing.sellerName, listing.id, listing.item); addClaimToPlayer(listing.sellerUUID, itemClaim); - // Deduct cancellation penalty from seller + // Deduct cancellation penalty from seller (take what they can afford) + long actualPenalty = 0; if (penalty > 0) { PlayerData sellerData = PlayerDataController.Instance.getData(listing.sellerUUID); if (sellerData != null) { - sellerData.tradeData.withdraw(penalty); + long sellerBalance = sellerData.tradeData.getBalance(); + actualPenalty = Math.min(penalty, sellerBalance); + if (actualPenalty > 0) { + sellerData.tradeData.withdraw(actualPenalty); + } sellerData.save(); } } logAuction(EnumAuctionLogAction.CANCELLED, listing.sellerName, listing.item.getDisplayName(), listing.currentBid, - "Penalty: " + penalty + ", Bidder refunded: " + listing.highBidderName + + "Penalty: " + actualPenalty + "/" + penalty + ", Bidder refunded: " + listing.highBidderName + (isAdmin ? ", Cancelled by admin: " + player.getCommandSenderName() : "")); } else { // No bids - just return item @@ -641,14 +646,14 @@ public class AuctionController implements IAuctionHandler { listing.highBidderName = playerName; listing.currentBid = bidAmount; listing.bidCount++; + + // Add to new bidder's active bids (inside sync to prevent trade slot count race) + addToPlayerBids(playerUUID, listing.id); + + // Snipe protection (inside sync to prevent stale endTime reads) + listing.extendForSnipeProtection(ConfigMarket.SnipeProtectionMinutes); } - // Add to new bidder's active bids - addToPlayerBids(playerUUID, listing.id); - - // Snipe protection - listing.extendForSnipeProtection(ConfigMarket.SnipeProtectionMinutes); - markDirty(); logAuction(EnumAuctionLogAction.BID, playerName, listing.item.getDisplayName(), bidAmount, @@ -845,10 +850,14 @@ public class AuctionController implements IAuctionHandler { return "Item data is missing."; } - if (!player.inventory.addItemStackToInventory(claim.item.copy())) { + // Pre-check inventory space to prevent partial-add dupe + if (!canInventoryHold(player, claim.item)) { return "Your inventory is full."; } + // Safe to add - inventory has enough space for the full stack + player.inventory.addItemStackToInventory(claim.item.copy()); + claimAndRemoveGlobal(claim.id); cleanupListingIfComplete(claim.listingId); markDirty(); @@ -910,11 +919,14 @@ public class AuctionController implements IAuctionHandler { return "Claim was prevented."; } - // Check inventory space - if (!player.inventory.addItemStackToInventory(claim.item.copy())) { + // Pre-check inventory space to prevent partial-add dupe + if (!canInventoryHold(player, claim.item)) { return "Your inventory is full."; } + // Safe to add - inventory has enough space for the full stack + player.inventory.addItemStackToInventory(claim.item.copy()); + // Remove claim from player's auction data playerData.tradeData.claimAndRemove(claimId); playerData.updateClient = true; @@ -973,6 +985,30 @@ public class AuctionController implements IAuctionHandler { return null; // Success } + /** + * 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. + */ + private boolean canInventoryHold(EntityPlayer player, ItemStack item) { + if (item == null || item.stackSize <= 0) return true; + + int remaining = item.stackSize; + int maxStack = Math.min(player.inventory.getInventoryStackLimit(), item.getMaxStackSize()); + + for (int i = 0; i < player.inventory.mainInventory.length && remaining > 0; i++) { + ItemStack slot = player.inventory.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); + } + } + return remaining <= 0; + } + /** * Clean up a listing if it's ended and no longer needed. * Now just removes ended listings after some delay since claims are in PlayerData. @@ -1019,6 +1055,7 @@ 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()) { @@ -1069,6 +1106,7 @@ 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 : ""; @@ -1755,20 +1793,12 @@ public class AuctionController implements IAuctionHandler { String itemName = listing.item != null ? listing.item.getDisplayName() : "Unknown Item"; - // Return active highest bid. + // Return active highest bid - always refund to the actual bidder if (listing.hasBids() && listing.highBidderUUID != null) { - 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); - } + AuctionClaim refundClaim = AuctionClaim.createRefundClaim( + listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid, itemName, + listing.sellerName, null); + addClaimToPlayer(listing.highBidderUUID, refundClaim); removeFromPlayerBids(listing.highBidderUUID, listing.id); } From c492e16cc83990888e956a87d144bcaeb017da42 Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 24 Feb 2026 18:34:10 -0500 Subject: [PATCH 05/11] Smaller Auction System Fixes --- .../npcs/controllers/AuctionController.java | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/main/java/noppes/npcs/controllers/AuctionController.java b/src/main/java/noppes/npcs/controllers/AuctionController.java index b754accac..e51c07a5d 100644 --- a/src/main/java/noppes/npcs/controllers/AuctionController.java +++ b/src/main/java/noppes/npcs/controllers/AuctionController.java @@ -410,7 +410,7 @@ public class AuctionController implements IAuctionHandler { PlayerTradeData currency = playerData.tradeData; long fee = ConfigMarket.ListingFee; - if (!currency.canAfford(fee)) { + if (fee > 0 && !currency.canAfford(fee)) { return "You cannot afford the listing fee (" + fee + " " + ConfigMarket.CurrencyName + ")."; } @@ -424,7 +424,7 @@ public class AuctionController implements IAuctionHandler { } // Deduct fee - if (!currency.withdraw(fee)) { + if (fee > 0 && !currency.withdraw(fee)) { return "Failed to deduct listing fee."; } @@ -1108,6 +1108,7 @@ 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()) { @@ -1158,6 +1159,7 @@ 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 : ""; @@ -1844,20 +1846,12 @@ public class AuctionController implements IAuctionHandler { String itemName = listing.item != null ? listing.item.getDisplayName() : "Unknown Item"; - // Return active highest bid. + // Return active highest bid - always refund to the actual bidder if (listing.hasBids() && listing.highBidderUUID != null) { - 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); - } + AuctionClaim refundClaim = AuctionClaim.createRefundClaim( + listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid, itemName, + listing.sellerName, null); + addClaimToPlayer(listing.highBidderUUID, refundClaim); removeFromPlayerBids(listing.highBidderUUID, listing.id); } From d36ec5c2597e28d8695a5a56de65abb1d267173c Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 24 Feb 2026 18:35:40 -0500 Subject: [PATCH 06/11] Small Linked Item Building Fix --- src/api | 2 +- src/main/java/noppes/npcs/controllers/LinkedItemController.java | 2 +- src/main/java/noppes/npcs/items/ItemLinked.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api b/src/api index 253ef7329..7f95b90fc 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 253ef7329814f37578a771363e4bde0b61bc6fcd +Subproject commit 7f95b90fc2ff1238df3e902fd55c804ffbdaacbc diff --git a/src/main/java/noppes/npcs/controllers/LinkedItemController.java b/src/main/java/noppes/npcs/controllers/LinkedItemController.java index efba64ff5..f318fb279 100644 --- a/src/main/java/noppes/npcs/controllers/LinkedItemController.java +++ b/src/main/java/noppes/npcs/controllers/LinkedItemController.java @@ -84,7 +84,7 @@ public class LinkedItemController implements ILinkedItemHandler { } @Override - public ILinkedItem get(int id) { + public LinkedItem get(int id) { return (LinkedItem) this.linkedItems.get(id); } diff --git a/src/main/java/noppes/npcs/items/ItemLinked.java b/src/main/java/noppes/npcs/items/ItemLinked.java index 8392beaf6..161547526 100644 --- a/src/main/java/noppes/npcs/items/ItemLinked.java +++ b/src/main/java/noppes/npcs/items/ItemLinked.java @@ -33,7 +33,7 @@ public class ItemLinked extends ItemCustomizable { IItemStack itemStack = NpcAPI.Instance().getIItemStack(stack); if (itemStack instanceof ScriptLinkedItem) { ScriptLinkedItem scriptLinkedItem = (ScriptLinkedItem) itemStack; - LinkedItem linkedItem = LinkedItemController.getInstance().get(scriptLinkedItem.linkedItem.getId()); + LinkedItem linkedItem = (LinkedItem) LinkedItemController.getInstance().get(scriptLinkedItem.linkedItem.getId()); int prevVersion = scriptLinkedItem.linkedVersion; if (linkedItem != null && scriptLinkedItem.linkedVersion != linkedItem.version) { scriptLinkedItem.linkedItem = linkedItem.clone(); From b2f89035b90f35d07d0c45e18cb295e5147cf49c Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 24 Feb 2026 18:55:52 -0500 Subject: [PATCH 07/11] MENU BG for Everything!!!! --- src/main/java/noppes/npcs/client/gui/GuiNpcPather.java | 2 +- src/main/java/noppes/npcs/client/gui/SubGuiAnimationFrame.java | 2 +- .../java/noppes/npcs/client/gui/SubGuiAnimationOptions.java | 2 +- src/main/java/noppes/npcs/client/gui/SubGuiEditText.java | 2 +- .../java/noppes/npcs/client/gui/player/GuiQuestCompletion.java | 2 +- .../noppes/npcs/client/gui/player/GuiTransportSelection.java | 2 +- .../java/noppes/npcs/client/gui/script/GuiScriptGlobal.java | 2 +- .../noppes/npcs/client/gui/util/SubGuiNpcCooldownPicker.java | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/GuiNpcPather.java b/src/main/java/noppes/npcs/client/gui/GuiNpcPather.java index 2a23e474a..eee981bdf 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiNpcPather.java +++ b/src/main/java/noppes/npcs/client/gui/GuiNpcPather.java @@ -28,7 +28,7 @@ public class GuiNpcPather extends GuiNPCInterface implements IGuiData { drawDefaultBackground = false; xSize = 176; title = "Npc Pather"; - setBackground("smallbg.png"); + setBackground("menubg.png"); ai = npc.ais; PacketClient.sendClient(new MovingPathGetPacket()); } diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiAnimationFrame.java b/src/main/java/noppes/npcs/client/gui/SubGuiAnimationFrame.java index e5594bf52..68eb9398b 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiAnimationFrame.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiAnimationFrame.java @@ -13,7 +13,7 @@ public class SubGuiAnimationFrame extends SubGuiInterface implements ITextfieldL public SubGuiAnimationFrame(Frame editingFrame) { this.editingFrame = editingFrame; - setBackground("smallbg.png"); + setBackground("menubg.png"); } @Override diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiAnimationOptions.java b/src/main/java/noppes/npcs/client/gui/SubGuiAnimationOptions.java index 27921388b..3d42c0cc0 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiAnimationOptions.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiAnimationOptions.java @@ -11,7 +11,7 @@ public class SubGuiAnimationOptions extends SubGuiInterface { public SubGuiAnimationOptions(Animation animation) { this.animation = animation; - setBackground("smallbg.png"); + setBackground("menubg.png"); } @Override diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiEditText.java b/src/main/java/noppes/npcs/client/gui/SubGuiEditText.java index 5fe7ffe71..1f6fc1bc6 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiEditText.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiEditText.java @@ -11,7 +11,7 @@ public class SubGuiEditText extends SubGuiInterface { public SubGuiEditText(String text) { this.text = text; - setBackground("extrasmallbg.png"); + setBackground("menubg.png"); closeOnEsc = true; xSize = 176; ySize = 71; diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiQuestCompletion.java b/src/main/java/noppes/npcs/client/gui/player/GuiQuestCompletion.java index 66767e7cf..6f488c492 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiQuestCompletion.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiQuestCompletion.java @@ -15,7 +15,7 @@ import org.lwjgl.opengl.GL11; public class GuiQuestCompletion extends GuiNPCInterface implements ITopButtonListener { private Quest quest; - private final ResourceLocation resource = new ResourceLocation("customnpcs", "textures/gui/smallbg.png"); + private final ResourceLocation resource = new ResourceLocation("customnpcs", "textures/gui/menubg.png"); public GuiQuestCompletion(Quest quest) { super(); diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiTransportSelection.java b/src/main/java/noppes/npcs/client/gui/player/GuiTransportSelection.java index f266059a6..e86769131 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiTransportSelection.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiTransportSelection.java @@ -20,7 +20,7 @@ import java.util.Vector; public class GuiTransportSelection extends GuiNPCInterface implements ITopButtonListener, IScrollData { - private final ResourceLocation resource = new ResourceLocation("customnpcs", "textures/gui/smallbg.png"); + private final ResourceLocation resource = new ResourceLocation("customnpcs", "textures/gui/menubg.png"); protected int xSize; protected int guiLeft; protected int guiTop; diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptGlobal.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptGlobal.java index 52ab86b24..88770f1e0 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptGlobal.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptGlobal.java @@ -16,7 +16,7 @@ import org.lwjgl.opengl.GL11; import java.util.HashMap; public class GuiScriptGlobal extends GuiNPCInterface implements IGuiData { - private final ResourceLocation resource = new ResourceLocation("customnpcs", "textures/gui/smallbg.png"); + private final ResourceLocation resource = new ResourceLocation("customnpcs", "textures/gui/menubg.png"); private HashMap> scriptGuiClasses = new HashMap<>(); public GuiScriptGlobal() { diff --git a/src/main/java/noppes/npcs/client/gui/util/SubGuiNpcCooldownPicker.java b/src/main/java/noppes/npcs/client/gui/util/SubGuiNpcCooldownPicker.java index 36d902f58..330b631bf 100644 --- a/src/main/java/noppes/npcs/client/gui/util/SubGuiNpcCooldownPicker.java +++ b/src/main/java/noppes/npcs/client/gui/util/SubGuiNpcCooldownPicker.java @@ -15,7 +15,7 @@ public class SubGuiNpcCooldownPicker extends SubGuiInterface implements ITextfie public SubGuiNpcCooldownPicker(boolean isMCCustom, long currentCooldown) { this.isMCCustom = isMCCustom; this.cooldownValue = currentCooldown; - setBackground("smallbg.png"); + setBackground("menubg.png"); xSize = 220; ySize = 222; closeOnEsc = true; From 9eadeaa6113e719ed79b9c1fdd7ba15e965286a7 Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 24 Feb 2026 19:03:26 -0500 Subject: [PATCH 08/11] Dialog and Quest Saving Bug --- .../packets/request/dialog/DialogSavePacket.java | 10 ++++++++++ .../network/packets/request/quest/QuestSavePacket.java | 7 +++++++ 2 files changed, 17 insertions(+) diff --git a/src/main/java/kamkeel/npcs/network/packets/request/dialog/DialogSavePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/dialog/DialogSavePacket.java index 716a53feb..87206c3c9 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/dialog/DialogSavePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/dialog/DialogSavePacket.java @@ -13,10 +13,13 @@ import kamkeel.npcs.util.ByteBufUtils; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; +import kamkeel.npcs.network.packets.data.large.GuiDataPacket; import noppes.npcs.CustomNpcsPermissions; import noppes.npcs.NoppesUtilServer; import noppes.npcs.controllers.DialogController; +import noppes.npcs.controllers.QuestController; import noppes.npcs.controllers.data.Dialog; +import noppes.npcs.controllers.data.Quest; import java.io.IOException; @@ -80,6 +83,13 @@ public final class DialogSavePacket extends AbstractPacket { } else { NoppesUtilServer.sendDialogData((EntityPlayerMP) player, dialog.category); } + + // Send full dialog data back so the client doesn't need a second round-trip + NBTTagCompound dialogData = dialog.writeToNBT(new NBTTagCompound()); + Quest quest = QuestController.Instance.quests.get(dialog.quest); + if (quest != null) + dialogData.setString("DialogQuestName", quest.title); + GuiDataPacket.sendGuiData((EntityPlayerMP) player, dialogData); } } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/quest/QuestSavePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/quest/QuestSavePacket.java index 9fdfbb78e..1eafb6c93 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/quest/QuestSavePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/quest/QuestSavePacket.java @@ -13,6 +13,7 @@ import kamkeel.npcs.util.ByteBufUtils; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; +import kamkeel.npcs.network.packets.data.large.GuiDataPacket; import noppes.npcs.CustomNpcsPermissions; import noppes.npcs.NoppesUtilServer; import noppes.npcs.controllers.QuestController; @@ -79,6 +80,12 @@ public final class QuestSavePacket extends AbstractPacket { } else { NoppesUtilServer.sendQuestData((EntityPlayerMP) player, quest.category); } + + // Send full quest data back so the client doesn't need a second round-trip + NBTTagCompound questData = new NBTTagCompound(); + if (quest.hasNewQuest()) + questData.setString("NextQuestTitle", quest.getNextQuest().title); + GuiDataPacket.sendGuiData((EntityPlayerMP) player, quest.writeToNBT(questData)); } } } From 930b7ed4234292e5f5bf2e1315e62627d995babc Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 24 Feb 2026 19:05:08 -0500 Subject: [PATCH 09/11] Better Energy Charging Syncing --- .../type/energy/AbilityEnergyProjectile.java | 15 +++ .../energycharge/EnergyChargeTracker.java | 112 ++++++++++++++++++ .../npcs/entity/EntityEnergyProjectile.java | 13 +- src/main/java/noppes/npcs/CustomNpcs.java | 5 +- .../java/noppes/npcs/ServerEventsHandler.java | 16 ++- 5 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/controllers/data/energycharge/EnergyChargeTracker.java diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java index 6054f3a41..fdf2d827e 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java @@ -10,6 +10,7 @@ import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyHomingData; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLifespanData; import kamkeel.npcs.controllers.data.ability.data.ProjectileData; +import kamkeel.npcs.controllers.data.energycharge.EnergyChargeTracker; import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; @@ -287,6 +288,15 @@ public abstract class AbilityEnergyProjectile chargeVisualIds[index] = id; chargeVisualCaster = caster; EnergyChargeSpawnPacket.sendToTracking(id, previewEntity, caster); + + // Register with server-side tracker for late-join support + EnergyChargeTracker.Instance.add(new EnergyChargeTracker.ChargeEntry( + id, + previewEntity.getClass().getName(), + previewEntity.exportSpawnNBT(), + caster.getEntityId(), + (int) caster.worldObj.getTotalWorldTime() + )); } protected void removeChargeVisual(EntityLivingBase caster, int index) { @@ -296,6 +306,7 @@ public abstract class AbilityEnergyProjectile String id = chargeVisualIds[index]; if (id != null && !id.isEmpty()) { EnergyChargeRemovePacket.sendToTracking(id, caster); + EnergyChargeTracker.Instance.remove(id, caster.getEntityId()); chargeVisualIds[index] = null; } } @@ -305,6 +316,10 @@ public abstract class AbilityEnergyProjectile for (int i = 0; i < chargeVisualIds.length; i++) { removeChargeVisual(caster, i); } + // Safety net: ensure tracker is fully cleaned for this caster + if (!isPreview() && caster.worldObj != null && !caster.worldObj.isRemote) { + EnergyChargeTracker.Instance.removeAllForCaster(caster.getEntityId()); + } } // ==================== LIFECYCLE ==================== diff --git a/src/main/java/kamkeel/npcs/controllers/data/energycharge/EnergyChargeTracker.java b/src/main/java/kamkeel/npcs/controllers/data/energycharge/EnergyChargeTracker.java new file mode 100644 index 000000000..606efb2fc --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/energycharge/EnergyChargeTracker.java @@ -0,0 +1,112 @@ +package kamkeel.npcs.controllers.data.energycharge; + +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.packets.data.energycharge.EnergyChargeSpawnPacket; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Server-side tracker for active charging preview visuals. + * Stores enough data to resend {@link EnergyChargeSpawnPacket} to late-joining players + * who enter tracking range after the initial broadcast. + *

+ * Keyed by caster entity ID so {@code PlayerEvent.StartTracking} can look up + * active charges for a newly tracked entity. + */ +public class EnergyChargeTracker { + public static final EnergyChargeTracker Instance = new EnergyChargeTracker(); + + public static class ChargeEntry { + public final String instanceId; + public final String entityClassName; + public final NBTTagCompound spawnNbt; + public final int casterEntityId; + public final int startTick; + + public ChargeEntry(String instanceId, String entityClassName, + NBTTagCompound spawnNbt, int casterEntityId, int startTick) { + this.instanceId = instanceId; + this.entityClassName = entityClassName; + this.spawnNbt = spawnNbt; + this.casterEntityId = casterEntityId; + this.startTick = startTick; + } + } + + private final Map> activeCharges = new HashMap<>(); + + public void add(ChargeEntry entry) { + List list = activeCharges.get(entry.casterEntityId); + if (list == null) { + list = new ArrayList<>(); + activeCharges.put(entry.casterEntityId, list); + } + list.add(entry); + } + + public void remove(String instanceId, int casterEntityId) { + List list = activeCharges.get(casterEntityId); + if (list == null) return; + Iterator it = list.iterator(); + while (it.hasNext()) { + if (it.next().instanceId.equals(instanceId)) { + it.remove(); + break; + } + } + if (list.isEmpty()) { + activeCharges.remove(casterEntityId); + } + } + + public void removeAllForCaster(int casterEntityId) { + activeCharges.remove(casterEntityId); + } + + /** + * Send all active charges for a caster to a specific player. + * Adjusts ChargeTick in the NBT to reflect elapsed time so the + * late-joining client starts at the correct visual progress. + */ + public void sendToPlayer(int casterEntityId, EntityPlayerMP player, int currentWorldTick) { + List entries = activeCharges.get(casterEntityId); + if (entries == null || entries.isEmpty()) return; + + for (ChargeEntry entry : entries) { + int elapsed = currentWorldTick - entry.startTick; + int chargeDuration = entry.spawnNbt.hasKey("ChargeDuration") + ? entry.spawnNbt.getInteger("ChargeDuration") : 0; + + // Charge already complete — server remove packet should follow shortly + if (chargeDuration > 0 && elapsed >= chargeDuration) { + continue; + } + + NBTTagCompound adjustedNbt = (NBTTagCompound) entry.spawnNbt.copy(); + if (elapsed > 0) { + adjustedNbt.setInteger("ChargeTick", elapsed); + } + + PacketHandler.Instance.sendToPlayer( + new EnergyChargeSpawnPacket(entry.instanceId, entry.entityClassName, adjustedNbt), + player + ); + } + } + + public boolean hasCharges(int casterEntityId) { + List list = activeCharges.get(casterEntityId); + return list != null && !list.isEmpty(); + } + + public void clear() { + activeCharges.clear(); + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java index a5f892f69..ff7a66c5e 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java @@ -221,6 +221,10 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { if (ticksExisted == 1 && !worldObj.isRemote) { trackProjectile(this); } + } else { + // Preview entities skip super.onUpdate() but still need ticksExisted + // for renderer pulsing effects (e.g., sin(ticksExisted + partialTicks)). + this.ticksExisted++; } if (!previewMode) { @@ -230,8 +234,13 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { // Update rotation updateRotation(); - // Lerp render size toward actual size - this.renderCurrentSize = this.renderCurrentSize + (this.size - this.renderCurrentSize) * 0.15f; + // Update render size: during charging, track directly (size already grows smoothly). + // Post-charging, lerp for smooth visual transitions. + if (isCharging()) { + this.renderCurrentSize = this.size; + } else { + this.renderCurrentSize = this.renderCurrentSize + (this.size - this.renderCurrentSize) * 0.15f; + } // Server-synced visual timer used for brief barrier impact lightning. if (!previewMode && !worldObj.isRemote) { diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 2f6d65a7c..4087880ce 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -26,6 +26,7 @@ import kamkeel.npcs.controllers.AttributeController; import kamkeel.npcs.controllers.ProfileController; import kamkeel.npcs.controllers.SyncController; import kamkeel.npcs.controllers.TelegraphController; +import kamkeel.npcs.controllers.data.energycharge.EnergyChargeTracker; import kamkeel.npcs.controllers.data.profile.CNPCData; import kamkeel.npcs.developer.Developer; import kamkeel.npcs.entity.EntityAbilityBarrier; @@ -37,6 +38,7 @@ import kamkeel.npcs.entity.EntityAbilityPanel; import kamkeel.npcs.entity.EntityAbilitySweeper; import kamkeel.npcs.entity.EntityAbilityZone; import kamkeel.npcs.entity.EntityAbilityDome; +import kamkeel.npcs.entity.EntityEnergyProjectile; import kamkeel.npcs.entity.EntityEnergySlicer; import kamkeel.npcs.network.PacketHandler; import kamkeel.npcs.util.BukkitUtil; @@ -430,8 +432,9 @@ public class CustomNpcs { AuctionController.Instance.save(); } - kamkeel.npcs.entity.EntityEnergyProjectile.clearAllProjectiles(); + EntityEnergyProjectile.clearAllProjectiles(); EntityAbilityBarrier.clearAllBarriers(); + EnergyChargeTracker.Instance.clear(); if (FMLCommonHandler.instance().getSide().isClient()) clientJaninoCompiler = null; diff --git a/src/main/java/noppes/npcs/ServerEventsHandler.java b/src/main/java/noppes/npcs/ServerEventsHandler.java index 43d78e6eb..9cbede79a 100644 --- a/src/main/java/noppes/npcs/ServerEventsHandler.java +++ b/src/main/java/noppes/npcs/ServerEventsHandler.java @@ -4,6 +4,7 @@ import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.eventhandler.SubscribeEvent; import cpw.mods.fml.relauncher.Side; import kamkeel.npcs.controllers.SyncController; +import kamkeel.npcs.controllers.data.energycharge.EnergyChargeTracker; import kamkeel.npcs.entity.EntityAbilityBarrier; import kamkeel.npcs.entity.EntityAbilityDome; import kamkeel.npcs.network.PacketHandler; @@ -300,6 +301,7 @@ public class ServerEventsHandler { public void invoke(LivingDeathEvent event) { if (event.entityLiving.worldObj.isRemote) return; + EnergyChargeTracker.Instance.removeAllForCaster(event.entityLiving.getEntityId()); if (event.source.getEntity() != null) { if (event.source.getEntity() instanceof EntityPlayer) { doExcalibur((EntityPlayer) event.source.getEntity(), event.entityLiving); @@ -535,7 +537,19 @@ public class ServerEventsHandler { @SubscribeEvent public void playerTracking(PlayerEvent.StartTracking event) { - if (!(event.target instanceof EntityPlayerMP || event.target instanceof EntityNPCInterface) || event.target.worldObj.isRemote) + if (event.target.worldObj.isRemote) return; + + // Energy charge visuals: send active charges for newly tracked entity + if (event.target instanceof EntityLivingBase + && event.entityPlayer instanceof EntityPlayerMP) { + EnergyChargeTracker.Instance.sendToPlayer( + event.target.getEntityId(), + (EntityPlayerMP) event.entityPlayer, + (int) event.target.worldObj.getTotalWorldTime() + ); + } + + if (!(event.target instanceof EntityPlayerMP || event.target instanceof EntityNPCInterface)) return; AnimationData animationData = AnimationData.getData(event.target); From dfd016d10c72effd564da74dd8a85853c9de035a Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 24 Feb 2026 19:06:48 -0500 Subject: [PATCH 10/11] Update CNPCData.java --- .../java/kamkeel/npcs/controllers/data/profile/CNPCData.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java b/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java index e4eb04dfe..f7d6ded84 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java +++ b/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java @@ -63,6 +63,9 @@ public class CNPCData implements IProfileData { if (playerData.partyUUID != null) return ProfileOperation.error("Cannot switch while in Party"); + if (playerData.abilityData.isExecutingAbility()) + return ProfileOperation.error("Cannot switch while performing an Ability"); + return ProfileOperation.success(""); } From edc3fa55c8c79026994fad31a0fd97dc4567c2c4 Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 24 Feb 2026 19:11:49 -0500 Subject: [PATCH 11/11] Fix Defaults --- .../java/noppes/npcs/client/gui/builder/GuiFieldBuilder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/builder/GuiFieldBuilder.java b/src/main/java/noppes/npcs/client/gui/builder/GuiFieldBuilder.java index 64b81624d..6e6a48ab1 100644 --- a/src/main/java/noppes/npcs/client/gui/builder/GuiFieldBuilder.java +++ b/src/main/java/noppes/npcs/client/gui/builder/GuiFieldBuilder.java @@ -220,7 +220,7 @@ public class GuiFieldBuilder { float fVal = def.getValue() instanceof Number ? ((Number) def.getValue()).floatValue() : 0f; GuiNpcTextField tf = new GuiNpcTextField(widgetId, parent, fontRenderer, fieldX, y, fieldW, 20, String.valueOf(fVal)); tf.setFloatsOnly(); - if (def.hasRange()) tf.setMinMaxDefaultFloat(def.getMin(), def.getMax(), fVal); + tf.setMinMaxDefaultFloat(def.getMin(), def.getMax(), fVal); if (!def.isEnabled()) tf.setEnabled(false); if (hover != null) tf.setHoverText(hover); sw.addTextField(tf); @@ -233,7 +233,7 @@ public class GuiFieldBuilder { int iVal = def.getValue() instanceof Number ? ((Number) def.getValue()).intValue() : 0; GuiNpcTextField tf = new GuiNpcTextField(widgetId, parent, fontRenderer, fieldX, y, fieldW, 20, String.valueOf(iVal)); tf.setIntegersOnly(); - if (def.hasRange()) tf.setMinMaxDefault((int) def.getMin(), (int) def.getMax(), iVal); + tf.setMinMaxDefault((int) def.getMin(), (int) def.getMax(), iVal); if (!def.isEnabled()) tf.setEnabled(false); if (hover != null) tf.setHoverText(hover); sw.addTextField(tf);