mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-13 18:24:10 +00:00
Closing the setup GUI saved the market from the server''s stale role and then re-read that market over the edits it had just applied, so stock and currency cost changes never survived. The role now saves first and the market is written from it, and relinking to another market no longer pushes pre-link data over it.
469 lines
16 KiB
Java
469 lines
16 KiB
Java
package noppes.npcs.roles;
|
|
|
|
import foxz.utils.Market;
|
|
import kamkeel.npcs.network.packets.data.large.GuiDataPacket;
|
|
import net.minecraft.entity.player.EntityPlayer;
|
|
import net.minecraft.entity.player.EntityPlayerMP;
|
|
import net.minecraft.item.ItemStack;
|
|
import net.minecraft.nbt.NBTTagCompound;
|
|
import net.minecraft.nbt.NBTTagList;
|
|
import noppes.npcs.NBTTags;
|
|
import noppes.npcs.NoppesUtilPlayer;
|
|
import noppes.npcs.NoppesUtilServer;
|
|
import noppes.npcs.NpcMiscInventory;
|
|
import noppes.npcs.constants.EnumGuiType;
|
|
import noppes.npcs.controllers.MarketRegistry;
|
|
import noppes.npcs.controllers.data.PlayerData;
|
|
import noppes.npcs.controllers.data.TraderStock;
|
|
import noppes.npcs.entity.EntityNPCInterface;
|
|
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.Set;
|
|
import java.util.logging.Level;
|
|
import java.util.logging.Logger;
|
|
|
|
public class RoleTrader extends RoleInterface {
|
|
|
|
public String marketName = "";
|
|
public NpcMiscInventory inventoryCurrency;
|
|
public NpcMiscInventory inventorySold;
|
|
|
|
public boolean ignoreDamage = false;
|
|
public boolean ignoreNBT = false;
|
|
public boolean recordHistory = false;
|
|
|
|
public int[] purchases;
|
|
public int[] disableSlot;
|
|
public HashMap<String, int[]> playerPurchases;
|
|
public HashMap<String, int[]> playerDisableSlot;
|
|
|
|
// Stock system
|
|
public TraderStock stock = new TraderStock();
|
|
|
|
// Currency cost per trade slot (ADDITIVE to item costs, 0 = no currency required)
|
|
public long[] currencyCost = new long[18];
|
|
|
|
// Local viewer tracking for non-linked traders (per-NPC sync)
|
|
private final Set<EntityPlayerMP> localViewers = new HashSet<>();
|
|
|
|
public RoleTrader(EntityNPCInterface npc) {
|
|
super(npc);
|
|
inventoryCurrency = new NpcMiscInventory(36);
|
|
inventorySold = new NpcMiscInventory(18);
|
|
purchases = new int[18];
|
|
disableSlot = new int[18];
|
|
playerPurchases = new HashMap<String, int[]>();
|
|
playerDisableSlot = new HashMap<String, int[]>();
|
|
}
|
|
|
|
@Override
|
|
public NBTTagCompound writeToNBT(NBTTagCompound nbttagcompound) {
|
|
nbttagcompound.setString("TraderMarket", marketName);
|
|
writeNBT(nbttagcompound);
|
|
return nbttagcompound;
|
|
}
|
|
|
|
public NBTTagCompound writeNBT(NBTTagCompound nbttagcompound) {
|
|
nbttagcompound.setTag("TraderCurrency", inventoryCurrency.getToNBT());
|
|
nbttagcompound.setTag("TraderSold", inventorySold.getToNBT());
|
|
nbttagcompound.setBoolean("TraderIgnoreDamage", ignoreDamage);
|
|
nbttagcompound.setBoolean("TraderIgnoreNBT", ignoreNBT);
|
|
nbttagcompound.setBoolean("RecordHistory", recordHistory);
|
|
nbttagcompound.setIntArray("DisableSlot", disableSlot);
|
|
nbttagcompound.setTag("PlayerDisableSlot", NBTTags.nbtStringIntegerArrayMap(playerDisableSlot));
|
|
if (recordHistory) {
|
|
nbttagcompound.setIntArray("Purchases", purchases);
|
|
nbttagcompound.setTag("PlayerPurchases", NBTTags.nbtStringIntegerArrayMap(playerPurchases));
|
|
}
|
|
|
|
// Stock system
|
|
nbttagcompound.setTag("Stock", stock.writeToNBT(new NBTTagCompound()));
|
|
|
|
// Currency cost per slot (additive to item costs)
|
|
NBTTagList currencyList = new NBTTagList();
|
|
for (long cost : currencyCost) {
|
|
NBTTagCompound costTag = new NBTTagCompound();
|
|
costTag.setLong("Cost", cost);
|
|
currencyList.appendTag(costTag);
|
|
}
|
|
nbttagcompound.setTag("CurrencyCost", currencyList);
|
|
|
|
return nbttagcompound;
|
|
}
|
|
|
|
@Override
|
|
public void readFromNBT(NBTTagCompound nbttagcompound) {
|
|
readFromNBT(nbttagcompound, true);
|
|
}
|
|
|
|
/**
|
|
* @param loadFromMarket pull the linked market's stored data over the compound just read.
|
|
* False when the compound is an admin edit, which is authoritative and
|
|
* would otherwise be overwritten by the market it is about to be saved to.
|
|
*/
|
|
public void readFromNBT(NBTTagCompound nbttagcompound, boolean loadFromMarket) {
|
|
String oldMarket = marketName;
|
|
marketName = nbttagcompound.getString("TraderMarket");
|
|
readNBT(nbttagcompound);
|
|
|
|
// Register with MarketRegistry for linked market sync
|
|
MarketRegistry.updateTraderMarket(oldMarket, marketName, this);
|
|
|
|
if (!loadFromMarket)
|
|
return;
|
|
|
|
try {
|
|
Market.getMarket(this, marketName);
|
|
} catch (Exception ex) {
|
|
Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, ex);
|
|
}
|
|
}
|
|
|
|
public void readNBT(NBTTagCompound nbttagcompound) {
|
|
inventoryCurrency.setFromNBT(nbttagcompound.getCompoundTag("TraderCurrency"));
|
|
inventorySold.setFromNBT(nbttagcompound.getCompoundTag("TraderSold"));
|
|
ignoreDamage = nbttagcompound.getBoolean("TraderIgnoreDamage");
|
|
ignoreNBT = nbttagcompound.getBoolean("TraderIgnoreNBT");
|
|
recordHistory = nbttagcompound.getBoolean("RecordHistory");
|
|
disableSlot = nbttagcompound.getIntArray("DisableSlot");
|
|
playerDisableSlot = NBTTags.getStringIntegerArrayMap(nbttagcompound.getTagList("PlayerDisableSlot", 10), 18);
|
|
if (recordHistory) {
|
|
purchases = nbttagcompound.getIntArray("Purchases");
|
|
playerPurchases = NBTTags.getStringIntegerArrayMap(nbttagcompound.getTagList("PlayerPurchases", 10), 18);
|
|
}
|
|
if (purchases == null || purchases.length != 18) {
|
|
purchases = new int[18];
|
|
for (int i = 0; i < purchases.length; ++i) purchases[i] = 0;
|
|
}
|
|
if (disableSlot == null || disableSlot.length != 18) {
|
|
disableSlot = new int[18];
|
|
for (int i = 0; i < disableSlot.length; ++i) disableSlot[i] = 0;
|
|
}
|
|
|
|
// Stock system
|
|
if (nbttagcompound.hasKey("Stock")) {
|
|
stock.readFromNBT(nbttagcompound.getCompoundTag("Stock"));
|
|
}
|
|
|
|
// Currency cost per slot (additive to item costs)
|
|
if (nbttagcompound.hasKey("CurrencyCost")) {
|
|
NBTTagList currencyList = nbttagcompound.getTagList("CurrencyCost", 10);
|
|
for (int i = 0; i < currencyList.tagCount() && i < 18; i++) {
|
|
currencyCost[i] = currencyList.getCompoundTagAt(i).getLong("Cost");
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void interact(EntityPlayer player) {
|
|
npc.say(player, npc.advanced.getInteractLine());
|
|
|
|
// Load from market FIRST to get latest shared data
|
|
try {
|
|
Market.getMarket(this, marketName);
|
|
} catch (Exception ex) {
|
|
Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, ex);
|
|
}
|
|
|
|
// THEN check for stock reset (after loading latest market data)
|
|
if (stock.enableStock) {
|
|
long currentTime = stock.resetType.isRealTime()
|
|
? System.currentTimeMillis()
|
|
: player.worldObj.getTotalWorldTime();
|
|
|
|
boolean needsSave = false;
|
|
|
|
if (stock.shouldReset(currentTime)) {
|
|
stock.resetStock(currentTime);
|
|
needsSave = true;
|
|
}
|
|
|
|
// Validate stock levels (clamp current to max if exceeded)
|
|
if (stock.validateStock()) {
|
|
needsSave = true;
|
|
}
|
|
|
|
// Save to market if using shared market and changes were made
|
|
if (needsSave && !marketName.isEmpty()) {
|
|
Market.save(this, marketName);
|
|
}
|
|
}
|
|
|
|
NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerTrader, npc);
|
|
}
|
|
|
|
public boolean hasCurrency(ItemStack itemstack) {
|
|
if (itemstack == null)
|
|
return false;
|
|
for (ItemStack item : inventoryCurrency.items.values()) {
|
|
if (item != null && NoppesUtilPlayer.compareItems(item, itemstack, ignoreDamage, ignoreNBT)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public void addPurchase(int slot, String playerName) {
|
|
if (slot >= 18 || slot < 0) return;
|
|
++purchases[slot];
|
|
++getArrayByName(playerName, playerPurchases)[slot];
|
|
|
|
if (recordHistory)
|
|
Market.save(this, marketName);
|
|
}
|
|
|
|
public boolean isSlotEnabled(int slot, String playerName) {
|
|
if (slot >= 18 || slot < 0) return false;
|
|
if (disableSlot[slot] > 0) return false;
|
|
return getArrayByName(playerName, playerDisableSlot)[slot] <= 0;
|
|
}
|
|
|
|
public int[] getArrayByName(String name, HashMap<String, int[]> map) {
|
|
map.computeIfAbsent(name, k -> new int[18]);
|
|
return map.get(name);
|
|
}
|
|
|
|
// ==================== Stock System Methods ====================
|
|
|
|
/**
|
|
* Check if there is stock available for a slot
|
|
*/
|
|
public boolean hasStock(int slot, String playerName, int amount) {
|
|
if (!stock.enableStock) {
|
|
return true; // Stock system disabled, always available
|
|
}
|
|
return stock.hasStock(slot, playerName, amount);
|
|
}
|
|
|
|
/**
|
|
* Get available stock for a slot
|
|
*/
|
|
public int getAvailableStock(int slot, String playerName) {
|
|
if (!stock.enableStock) {
|
|
return Integer.MAX_VALUE; // Unlimited
|
|
}
|
|
return stock.getAvailableStock(slot, playerName);
|
|
}
|
|
|
|
/**
|
|
* Consume stock after purchase.
|
|
* For shared stock, syncs all viewers (market-wide for linked markets, per-NPC otherwise).
|
|
*
|
|
* @param slot The slot purchased from
|
|
* @param playerName The player making the purchase
|
|
* @param amount Amount to consume
|
|
* @return true if stock was consumed successfully
|
|
*/
|
|
public boolean consumeStock(int slot, String playerName, int amount) {
|
|
if (!stock.enableStock) {
|
|
return true; // Stock system disabled
|
|
}
|
|
boolean consumed = stock.consumeStock(slot, playerName, amount);
|
|
if (consumed) {
|
|
// Save to market if linked
|
|
if (!marketName.isEmpty()) {
|
|
Market.save(this, marketName);
|
|
}
|
|
|
|
// Sync viewers for shared stock (not per-player)
|
|
if (!stock.perPlayer) {
|
|
if (!marketName.isEmpty()) {
|
|
// Linked market: sync all traders and viewers market-wide
|
|
MarketRegistry.syncMarket(marketName);
|
|
} else {
|
|
// No linked market: sync only this NPC's viewers
|
|
syncLocalViewers();
|
|
}
|
|
}
|
|
}
|
|
return consumed;
|
|
}
|
|
|
|
/**
|
|
* Get time until stock reset formatted for display
|
|
*/
|
|
public String getTimeUntilResetFormatted() {
|
|
if (!stock.enableStock || stock.resetType == noppes.npcs.constants.EnumStockReset.NONE) {
|
|
return "";
|
|
}
|
|
long currentTime = stock.resetType.isRealTime()
|
|
? System.currentTimeMillis()
|
|
: (npc != null && npc.worldObj != null ? npc.worldObj.getTotalWorldTime() : 0);
|
|
return stock.getTimeUntilResetFormatted(currentTime);
|
|
}
|
|
|
|
/**
|
|
* Get time until stock reset in real-time milliseconds.
|
|
* Converts MC ticks to approximate real-time for client countdown display.
|
|
*
|
|
* @return Remaining time in milliseconds, or -1 if no reset scheduled
|
|
*/
|
|
public long getResetTimeRemainingMillis() {
|
|
if (!stock.enableStock || stock.resetType == noppes.npcs.constants.EnumStockReset.NONE) {
|
|
return -1;
|
|
}
|
|
|
|
long currentTime;
|
|
long remaining;
|
|
|
|
if (stock.resetType.isRealTime()) {
|
|
// Real time - already in milliseconds
|
|
currentTime = System.currentTimeMillis();
|
|
remaining = stock.getTimeUntilReset(currentTime);
|
|
} else {
|
|
// MC time - convert ticks to real milliseconds (1 tick = 50ms)
|
|
currentTime = (npc != null && npc.worldObj != null ? npc.worldObj.getTotalWorldTime() : 0);
|
|
long remainingTicks = stock.getTimeUntilReset(currentTime);
|
|
remaining = remainingTicks * 50L;
|
|
}
|
|
|
|
return remaining;
|
|
}
|
|
|
|
/**
|
|
* Reset the stock cooldown timer (for admin use)
|
|
*/
|
|
public void resetCooldown() {
|
|
stock.lastResetTime = 0;
|
|
}
|
|
|
|
// ==================== Currency Cost Methods ====================
|
|
|
|
/**
|
|
* Get the currency cost for a slot (additive to item costs)
|
|
*/
|
|
public long getCurrencyCost(int slot) {
|
|
if (slot < 0 || slot >= 18) return 0;
|
|
return currencyCost[slot];
|
|
}
|
|
|
|
/**
|
|
* Set the currency cost for a slot (additive to item costs)
|
|
*/
|
|
public void setCurrencyCost(int slot, long cost) {
|
|
if (slot >= 0 && slot < 18) {
|
|
currencyCost[slot] = Math.max(0, cost);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a slot has a currency cost (greater than 0)
|
|
*/
|
|
public boolean hasCurrencyCost(int slot) {
|
|
return slot >= 0 && slot < 18 && currencyCost[slot] > 0;
|
|
}
|
|
|
|
// ==================== Viewer Tracking Methods ====================
|
|
|
|
/**
|
|
* Register a player as viewing this trader's GUI.
|
|
* - For linked markets: uses MarketRegistry (market-wide sync)
|
|
* - For normal traders: uses local viewer set (per-NPC sync)
|
|
*/
|
|
public void registerViewer(EntityPlayerMP player) {
|
|
if (player == null) return;
|
|
|
|
if (!marketName.isEmpty()) {
|
|
// Linked market: use MarketRegistry for market-wide tracking
|
|
MarketRegistry.registerViewer(marketName, player);
|
|
} else {
|
|
// No linked market: use local viewer set
|
|
localViewers.add(player);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unregister a player from viewing this trader's GUI.
|
|
* - For linked markets: uses MarketRegistry
|
|
* - For normal traders: uses local viewer set
|
|
*/
|
|
public void unregisterViewer(EntityPlayerMP player) {
|
|
if (player == null) return;
|
|
|
|
if (!marketName.isEmpty()) {
|
|
MarketRegistry.unregisterViewer(marketName, player);
|
|
} else {
|
|
localViewers.remove(player);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sync trader data to all players viewing this trader.
|
|
* - For linked markets: syncs ALL viewers across ALL NPCs sharing the market
|
|
* - For normal traders: syncs only viewers of THIS specific NPC
|
|
*/
|
|
public void syncAllViewers() {
|
|
if (!marketName.isEmpty()) {
|
|
// Linked market: sync all viewers across all NPCs with this market
|
|
MarketRegistry.syncMarketViewers(marketName);
|
|
} else {
|
|
// No linked market: sync only local viewers
|
|
syncLocalViewers();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sync to all players viewing this specific NPC (for non-linked traders).
|
|
*/
|
|
private void syncLocalViewers() {
|
|
// Remove disconnected players
|
|
localViewers.removeIf(player -> player.playerNetServerHandler == null);
|
|
|
|
for (EntityPlayerMP viewer : localViewers) {
|
|
syncToPlayer(viewer);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when this trader is unloaded (NPC death, chunk unload, etc.)
|
|
* Cleans up MarketRegistry registration.
|
|
*/
|
|
public void onUnload() {
|
|
if (!marketName.isEmpty()) {
|
|
MarketRegistry.unregisterTrader(marketName, this);
|
|
}
|
|
localViewers.clear();
|
|
}
|
|
|
|
/**
|
|
* Send trader data to a specific player
|
|
*/
|
|
public void syncToPlayer(EntityPlayerMP player) {
|
|
if (player == null || player.playerNetServerHandler == null) {
|
|
return;
|
|
}
|
|
|
|
NBTTagCompound compound = new NBTTagCompound();
|
|
|
|
// Player balance
|
|
PlayerData data = PlayerData.get(player);
|
|
compound.setLong("Balance", data.tradeData.getBalance());
|
|
|
|
// Stock info
|
|
compound.setBoolean("StockEnabled", stock.enableStock);
|
|
compound.setString("ResetTime", getTimeUntilResetFormatted());
|
|
|
|
// Available stock per slot (player-specific if perPlayer mode)
|
|
String playerName = player.getCommandSenderName();
|
|
int[] stockArr = new int[18];
|
|
for (int i = 0; i < 18; i++) {
|
|
stockArr[i] = getAvailableStock(i, playerName);
|
|
}
|
|
compound.setIntArray("Stock", stockArr);
|
|
|
|
// Currency cost per slot
|
|
for (int i = 0; i < 18; i++) {
|
|
compound.setLong("Cost" + i, getCurrencyCost(i));
|
|
}
|
|
|
|
GuiDataPacket.sendGuiData(player, compound);
|
|
}
|
|
|
|
@Override
|
|
public void delete() {
|
|
onUnload();
|
|
super.delete();
|
|
}
|
|
}
|