diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 4b5750c6a..422be3e61 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -58,6 +58,8 @@ import noppes.npcs.controllers.GlobalDataController; import noppes.npcs.controllers.LinkedItemController; import noppes.npcs.controllers.LinkedNpcController; import noppes.npcs.controllers.MagicController; +import noppes.npcs.controllers.VaultHelper; +import noppes.npcs.controllers.CurrencyController; import noppes.npcs.controllers.PlayerDataController; import noppes.npcs.controllers.QuestController; import noppes.npcs.controllers.RecipeController; @@ -304,6 +306,8 @@ public class CustomNpcs { //Loading items in the about to start event was corrupting items with a damage value @EventHandler public void started(FMLServerStartedEvent event) { + new VaultHelper(); + new CurrencyController(); RecipeController.Instance.load(); new BankController(); DialogController.Instance.load(); diff --git a/src/main/java/noppes/npcs/config/ConfigMarket.java b/src/main/java/noppes/npcs/config/ConfigMarket.java new file mode 100644 index 000000000..9c5791929 --- /dev/null +++ b/src/main/java/noppes/npcs/config/ConfigMarket.java @@ -0,0 +1,217 @@ +package noppes.npcs.config; + +import cpw.mods.fml.common.FMLLog; +import net.minecraftforge.common.config.Configuration; +import net.minecraftforge.common.config.Property; +import org.apache.logging.log4j.Level; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; + +public class ConfigMarket { + public static Configuration config; + + public final static String MARKET = "Market"; + public final static String CURRENCY = "Market.Currency"; + public final static String AUCTION = "Market.Auction"; + public final static String TRADER = "Market.Trader"; + + // ========================================= + // Market General + // ========================================= + public static boolean MarketEnabled = true; + + // ========================================= + // Currency Settings + // ========================================= + public static boolean UseVaultIfAvailable = true; + public static String CurrencyName = "Gold"; + public static String FallbackCurrencyItem = "customnpcs:npcCoinGold"; + public static long FallbackItemRatio = 1; + public static long StartingBalance = 1000; + public static long MaxBalance = 999999999999L; + + // ========================================= + // Auction Settings + // ========================================= + public static boolean AuctionEnabled = true; + public static int DefaultMaxListings = 5; + public static double ListingFeePercent = 0.0; + public static double SalesTaxPercent = 0.05; + public static boolean RequireAuctioneerNPC = true; + public static String AvailableDurations = "2,12,24,48"; + public static int DefaultDurationHours = 24; + public static int ClaimExpirationDays = 30; + public static double MinBidIncrementPercent = 0.05; + public static int SnipeProtectionMinutes = 5; + public static int SnipeProtectionThreshold = 5; + + // ========================================= + // Trader Settings + // ========================================= + public static boolean EnableStockByDefault = false; + public static String DefaultResetType = "NONE"; + + public static void init(File configFile) { + config = new Configuration(configFile); + + try { + config.load(); + + // ========================================= + // Market General + // ========================================= + MarketEnabled = config.get(MARKET, "Enable Market System", true, + "Enable the market/economy system").getBoolean(true); + + // ========================================= + // Currency Settings + // ========================================= + config.setCategoryComment(CURRENCY, "Currency system settings for the virtual economy"); + + UseVaultIfAvailable = config.get(CURRENCY, "Use Vault If Available", true, + "Use VaultAPI if available on Bukkit hybrid servers").getBoolean(true); + + CurrencyName = config.get(CURRENCY, "Currency Name", "Gold", + "Display name for the currency").getString(); + + FallbackCurrencyItem = config.get(CURRENCY, "Fallback Currency Item", "customnpcs:npcCoinGold", + "Item registry name to use as currency when Vault is unavailable. Leave empty to disable.").getString(); + + FallbackItemRatio = config.get(CURRENCY, "Fallback Item Ratio", 1, + "Conversion ratio: 1 fallback item = X virtual currency").getInt(1); + + StartingBalance = config.get(CURRENCY, "Starting Balance", 1000, + "Starting balance for new players (only used when Vault is not available)").getInt(1000); + + MaxBalance = config.get(CURRENCY, "Maximum Balance", 999999999999L, + "Maximum balance a player can have (only used when Vault is not available)").getLong(999999999999L); + + // ========================================= + // Auction Settings + // ========================================= + config.setCategoryComment(AUCTION, "Auction house settings"); + + AuctionEnabled = config.get(AUCTION, "Enable Auction House", true, + "Enable/disable the auction house system").getBoolean(true); + + DefaultMaxListings = config.get(AUCTION, "Default Max Listings", 5, + "Default maximum auction listings per player (without permission override)").getInt(5); + + ListingFeePercent = config.get(AUCTION, "Listing Fee Percent", 0.0, + "Listing fee as percentage of starting price (0.0 = no fee)").getDouble(0.0); + + SalesTaxPercent = config.get(AUCTION, "Sales Tax Percent", 0.05, + "Sales tax as percentage taken from final sale price (0.05 = 5%)").getDouble(0.05); + + RequireAuctioneerNPC = config.get(AUCTION, "Require Auctioneer NPC", true, + "Require an Auctioneer NPC to access auction house").getBoolean(true); + + AvailableDurations = config.get(AUCTION, "Available Durations", "2,12,24,48", + "Available auction durations in hours (comma-separated)").getString(); + + DefaultDurationHours = config.get(AUCTION, "Default Duration Hours", 24, + "Default auction duration in hours").getInt(24); + + ClaimExpirationDays = config.get(AUCTION, "Claim Expiration Days", 30, + "Claim expiration in days (claims auto-expire after this period, 0 = never)").getInt(30); + + MinBidIncrementPercent = config.get(AUCTION, "Min Bid Increment Percent", 0.05, + "Minimum bid increment as percentage (0.05 = 5% minimum increase)").getDouble(0.05); + + SnipeProtectionMinutes = config.get(AUCTION, "Snipe Protection Minutes", 5, + "Extend auction TO this many minutes when bid placed near end. Set to 0 to disable.").getInt(5); + + SnipeProtectionThreshold = config.get(AUCTION, "Snipe Protection Threshold", 5, + "How many minutes before end triggers snipe protection").getInt(5); + + // ========================================= + // Trader Settings + // ========================================= + config.setCategoryComment(TRADER, "Trader stock system settings"); + + EnableStockByDefault = config.get(TRADER, "Enable Stock By Default", false, + "Enable stock system by default for new traders").getBoolean(false); + + DefaultResetType = config.get(TRADER, "Default Reset Type", "NONE", + "Default stock reset type (NONE, MCDAILY, MCWEEKLY, RLDAILY, RLWEEKLY)").getString(); + + // Set category order + config.setCategoryPropertyOrder(MARKET, new ArrayList<>(Arrays.asList( + "Enable Market System" + ))); + + config.setCategoryPropertyOrder(CURRENCY, new ArrayList<>(Arrays.asList( + "Use Vault If Available", + "Currency Name", + "Fallback Currency Item", + "Fallback Item Ratio", + "Starting Balance", + "Maximum Balance" + ))); + + config.setCategoryPropertyOrder(AUCTION, new ArrayList<>(Arrays.asList( + "Enable Auction House", + "Default Max Listings", + "Listing Fee Percent", + "Sales Tax Percent", + "Require Auctioneer NPC", + "Available Durations", + "Default Duration Hours", + "Claim Expiration Days", + "Min Bid Increment Percent", + "Snipe Protection Minutes", + "Snipe Protection Threshold" + ))); + + config.setCategoryPropertyOrder(TRADER, new ArrayList<>(Arrays.asList( + "Enable Stock By Default", + "Default Reset Type" + ))); + + // Validation + if (DefaultMaxListings < 1) { + DefaultMaxListings = 1; + } + if (ClaimExpirationDays < 0) { + ClaimExpirationDays = 0; + } + if (SnipeProtectionMinutes < 0) { + SnipeProtectionMinutes = 0; + } + if (SnipeProtectionThreshold < 0) { + SnipeProtectionThreshold = 0; + } + if (StartingBalance < 0) { + StartingBalance = 0; + } + if (MaxBalance < 1) { + MaxBalance = 1; + } + + } catch (Exception e) { + FMLLog.log(Level.ERROR, e, "CNPC+ has had a problem loading its market configuration"); + } finally { + if (config.hasChanged()) { + config.save(); + } + } + } + + /** + * Parse available durations from config string + */ + public static int[] getAvailableDurationsArray() { + String[] parts = AvailableDurations.split(","); + int[] durations = new int[parts.length]; + for (int i = 0; i < parts.length; i++) { + try { + durations[i] = Integer.parseInt(parts[i].trim()); + } catch (NumberFormatException e) { + durations[i] = 24; // Default to 24 hours if parsing fails + } + } + return durations; + } +} diff --git a/src/main/java/noppes/npcs/config/LoadConfiguration.java b/src/main/java/noppes/npcs/config/LoadConfiguration.java index 19fad65c2..dfac43d90 100644 --- a/src/main/java/noppes/npcs/config/LoadConfiguration.java +++ b/src/main/java/noppes/npcs/config/LoadConfiguration.java @@ -9,6 +9,7 @@ public class LoadConfiguration { public static File debugConfigFile; public static File clientConfigFile; public static File experimentalConfigFile; + public static File marketConfigFile; public static void init(String configpath) { mainConfigFile = new File(configpath + "main.cfg"); @@ -17,6 +18,7 @@ public class LoadConfiguration { scriptConfigFile = new File(configpath + "script.cfg"); debugConfigFile = new File(configpath + "debug.cfg"); experimentalConfigFile = new File(configpath + "experimental.cfg"); + marketConfigFile = new File(configpath + "market.cfg"); ConfigMain.init(mainConfigFile); ConfigItem.init(itemConfigFile); @@ -24,5 +26,6 @@ public class LoadConfiguration { ConfigScript.init(scriptConfigFile); ConfigDebug.init(debugConfigFile); ConfigExperimental.init(experimentalConfigFile); + ConfigMarket.init(marketConfigFile); } } diff --git a/src/main/java/noppes/npcs/controllers/CurrencyController.java b/src/main/java/noppes/npcs/controllers/CurrencyController.java new file mode 100644 index 000000000..16d6c99f4 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/CurrencyController.java @@ -0,0 +1,400 @@ +package noppes.npcs.controllers; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.nbt.CompressedStreamTools; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.CustomNpcs; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.controllers.data.PlayerData; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.*; + +/** + * Controller for managing currency operations including: + * - Claims system (for auction refunds, NPC payments, etc.) + * - Transaction history + * - Currency administration + * + * Delegates actual balance operations to VaultHelper. + */ +public class CurrencyController { + private static final Logger logger = LogManager.getLogger(CustomNpcs.class); + + public static CurrencyController Instance; + + // Pending claims per player UUID - claims that can be collected + private Map> pendingClaims = new HashMap<>(); + + // File path for persistence + private String filePath = ""; + + public CurrencyController() { + Instance = this; + load(); + } + + /** + * Get player's current balance + */ + public long getBalance(EntityPlayer player) { + return VaultHelper.Instance.getBalance(player); + } + + /** + * Deposit currency directly to player's balance + */ + public boolean deposit(EntityPlayer player, long amount) { + return VaultHelper.Instance.deposit(player, amount); + } + + /** + * Withdraw currency from player's balance + */ + public boolean withdraw(EntityPlayer player, long amount) { + return VaultHelper.Instance.withdraw(player, amount); + } + + /** + * Check if player can afford an amount + */ + public boolean canAfford(EntityPlayer player, long amount) { + return VaultHelper.Instance.canAfford(player, amount); + } + + /** + * Transfer currency between players + */ + public boolean transfer(EntityPlayer from, EntityPlayer to, long amount) { + return VaultHelper.Instance.transfer(from, to, amount); + } + + /** + * Format amount for display + */ + public String formatAmount(long amount) { + return VaultHelper.Instance.formatAmount(amount); + } + + // ==================== CLAIMS SYSTEM ==================== + + /** + * Add a claim for a player (e.g., auction refund, NPC payment) + * Claims can be collected later by the player. + * + * @param playerUUID The player's UUID + * @param amount The amount to claim + * @param source Description of where this claim came from + * @param sourceId Optional ID of the source (e.g., auction ID) + */ + public void addClaim(UUID playerUUID, long amount, String source, String sourceId) { + if (amount <= 0) { + return; + } + + CurrencyClaim claim = new CurrencyClaim(); + claim.amount = amount; + claim.source = source; + claim.sourceId = sourceId; + claim.timestamp = System.currentTimeMillis(); + claim.expiresAt = claim.timestamp + (ConfigMarket.ClaimExpirationDays * 24L * 60L * 60L * 1000L); + + List claims = pendingClaims.computeIfAbsent(playerUUID, k -> new ArrayList<>()); + claims.add(claim); + + save(); + logger.info("Added claim of {} for player {} from {}", amount, playerUUID, source); + } + + /** + * Get all pending claims for a player + */ + public List getPendingClaims(EntityPlayer player) { + return getPendingClaims(player.getUniqueID()); + } + + /** + * Get all pending claims for a player by UUID + */ + public List getPendingClaims(UUID playerUUID) { + List claims = pendingClaims.get(playerUUID); + if (claims == null) { + return new ArrayList<>(); + } + + // Filter out expired claims + long now = System.currentTimeMillis(); + claims.removeIf(claim -> claim.expiresAt > 0 && claim.expiresAt < now); + + return new ArrayList<>(claims); + } + + /** + * Get total pending claim amount for a player + */ + public long getTotalPendingClaims(EntityPlayer player) { + return getTotalPendingClaims(player.getUniqueID()); + } + + /** + * Get total pending claim amount for a player by UUID + */ + public long getTotalPendingClaims(UUID playerUUID) { + List claims = getPendingClaims(playerUUID); + long total = 0; + for (CurrencyClaim claim : claims) { + total += claim.amount; + } + return total; + } + + /** + * Claim all pending claims for a player + * @return Amount successfully claimed + */ + public long claimAll(EntityPlayer player) { + List claims = getPendingClaims(player); + if (claims.isEmpty()) { + return 0; + } + + long totalClaimed = 0; + + for (CurrencyClaim claim : claims) { + if (deposit(player, claim.amount)) { + totalClaimed += claim.amount; + } + } + + // Clear claims after successful deposit + pendingClaims.remove(player.getUniqueID()); + save(); + + return totalClaimed; + } + + /** + * Claim a specific claim by index + * @return true if successful + */ + public boolean claimSingle(EntityPlayer player, int claimIndex) { + List claims = pendingClaims.get(player.getUniqueID()); + if (claims == null || claimIndex < 0 || claimIndex >= claims.size()) { + return false; + } + + CurrencyClaim claim = claims.get(claimIndex); + if (deposit(player, claim.amount)) { + claims.remove(claimIndex); + if (claims.isEmpty()) { + pendingClaims.remove(player.getUniqueID()); + } + save(); + return true; + } + + return false; + } + + /** + * Check if player has any pending claims + */ + public boolean hasPendingClaims(EntityPlayer player) { + return !getPendingClaims(player).isEmpty(); + } + + // ==================== PERSISTENCE ==================== + + public void load() { + File saveDir = CustomNpcs.getWorldSaveDirectory(); + if (saveDir == null) { + return; + } + + filePath = saveDir.getAbsolutePath(); + pendingClaims.clear(); + + try { + File file = new File(saveDir, "currency_claims.dat"); + if (file.exists()) { + loadFromFile(file); + } + } catch (Exception e) { + logger.error("Error loading currency claims", e); + try { + File file = new File(saveDir, "currency_claims.dat_old"); + if (file.exists()) { + loadFromFile(file); + } + } catch (Exception ee) { + logger.error("Error loading backup currency claims", ee); + } + } + + // Clean expired claims + cleanExpiredClaims(); + } + + private void loadFromFile(File file) throws Exception { + FileInputStream fis = new FileInputStream(file); + NBTTagCompound compound = CompressedStreamTools.readCompressed(fis); + fis.close(); + + NBTTagList claimsList = compound.getTagList("Claims", 10); + for (int i = 0; i < claimsList.tagCount(); i++) { + NBTTagCompound playerCompound = claimsList.getCompoundTagAt(i); + UUID playerUUID = UUID.fromString(playerCompound.getString("UUID")); + + List playerClaims = new ArrayList<>(); + NBTTagList playerClaimsList = playerCompound.getTagList("PlayerClaims", 10); + for (int j = 0; j < playerClaimsList.tagCount(); j++) { + NBTTagCompound claimCompound = playerClaimsList.getCompoundTagAt(j); + CurrencyClaim claim = new CurrencyClaim(); + claim.readFromNBT(claimCompound); + playerClaims.add(claim); + } + + if (!playerClaims.isEmpty()) { + pendingClaims.put(playerUUID, playerClaims); + } + } + + logger.info("Loaded {} player claim records", pendingClaims.size()); + } + + public void save() { + File saveDir = CustomNpcs.getWorldSaveDirectory(); + if (saveDir == null) { + return; + } + + try { + File file = new File(saveDir, "currency_claims.dat"); + File backup = new File(saveDir, "currency_claims.dat_old"); + + if (file.exists()) { + if (backup.exists()) { + backup.delete(); + } + file.renameTo(backup); + } + + NBTTagCompound compound = new NBTTagCompound(); + NBTTagList claimsList = new NBTTagList(); + + for (Map.Entry> entry : pendingClaims.entrySet()) { + if (entry.getValue().isEmpty()) { + continue; + } + + NBTTagCompound playerCompound = new NBTTagCompound(); + playerCompound.setString("UUID", entry.getKey().toString()); + + NBTTagList playerClaimsList = new NBTTagList(); + for (CurrencyClaim claim : entry.getValue()) { + NBTTagCompound claimCompound = new NBTTagCompound(); + claim.writeToNBT(claimCompound); + playerClaimsList.appendTag(claimCompound); + } + playerCompound.setTag("PlayerClaims", playerClaimsList); + + claimsList.appendTag(playerCompound); + } + + compound.setTag("Claims", claimsList); + + file = new File(saveDir, "currency_claims.dat"); + FileOutputStream fos = new FileOutputStream(file); + CompressedStreamTools.writeCompressed(compound, fos); + fos.close(); + } catch (Exception e) { + logger.error("Error saving currency claims", e); + } + } + + /** + * Clean up expired claims + */ + public void cleanExpiredClaims() { + long now = System.currentTimeMillis(); + boolean changed = false; + + Iterator>> it = pendingClaims.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry> entry = it.next(); + List claims = entry.getValue(); + + int before = claims.size(); + claims.removeIf(claim -> claim.expiresAt > 0 && claim.expiresAt < now); + + if (claims.size() != before) { + changed = true; + } + + if (claims.isEmpty()) { + it.remove(); + } + } + + if (changed) { + save(); + } + } + + // ==================== CLAIM DATA CLASS ==================== + + public static class CurrencyClaim { + public long amount; + public String source; // e.g., "Auction Refund", "NPC Payment", "Quest Reward" + public String sourceId; // e.g., auction ID + public long timestamp; // When the claim was created + public long expiresAt; // When the claim expires (0 = never) + + public void writeToNBT(NBTTagCompound compound) { + compound.setLong("Amount", amount); + compound.setString("Source", source != null ? source : ""); + compound.setString("SourceId", sourceId != null ? sourceId : ""); + compound.setLong("Timestamp", timestamp); + compound.setLong("ExpiresAt", expiresAt); + } + + public void readFromNBT(NBTTagCompound compound) { + amount = compound.getLong("Amount"); + source = compound.getString("Source"); + sourceId = compound.getString("SourceId"); + timestamp = compound.getLong("Timestamp"); + expiresAt = compound.getLong("ExpiresAt"); + } + + /** + * Get formatted time remaining until expiration + */ + public String getTimeRemaining() { + if (expiresAt <= 0) { + return "Never"; + } + + long remaining = expiresAt - System.currentTimeMillis(); + if (remaining <= 0) { + return "Expired"; + } + + long days = remaining / (24L * 60L * 60L * 1000L); + long hours = (remaining % (24L * 60L * 60L * 1000L)) / (60L * 60L * 1000L); + + if (days > 0) { + return days + " days, " + hours + " hours"; + } else if (hours > 0) { + return hours + " hours"; + } else { + long minutes = remaining / (60L * 1000L); + return minutes + " minutes"; + } + } + } +} diff --git a/src/main/java/noppes/npcs/controllers/VaultHelper.java b/src/main/java/noppes/npcs/controllers/VaultHelper.java new file mode 100644 index 000000000..ae2c6f97a --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/VaultHelper.java @@ -0,0 +1,272 @@ +package noppes.npcs.controllers; + +import net.minecraft.entity.player.EntityPlayer; +import noppes.npcs.CustomNpcs; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.controllers.data.PlayerDataController; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.lang.reflect.Method; + +/** + * Helper class for VaultAPI integration on Bukkit hybrid servers. + * Uses reflection to integrate with Vault when available. + * Falls back to internal currency system when Vault is not present. + */ +public class VaultHelper { + private static final Logger logger = LogManager.getLogger(CustomNpcs.class); + + public static VaultHelper Instance; + + // Vault reflection references + private boolean vaultAvailable = false; + private Object economy = null; + private Method getBalance; + private Method depositPlayer; + private Method withdrawPlayer; + private Method hasAccount; + private Method createPlayerAccount; + private Method responseSuccess; + + // Bukkit references for getting offline player + private Class bukkitClass; + private Method getOfflinePlayer; + + public VaultHelper() { + Instance = this; + + if (!ConfigMarket.UseVaultIfAvailable) { + logger.info("Vault integration disabled by config"); + return; + } + + try { + // Check for Bukkit first + bukkitClass = Class.forName("org.bukkit.Bukkit"); + getOfflinePlayer = bukkitClass.getMethod("getOfflinePlayer", String.class); + + // Check for Vault + Class vaultClass = Class.forName("net.milkbowl.vault.economy.Economy"); + + // Get the Economy service from Bukkit's ServicesManager + Method getServicesManager = bukkitClass.getMethod("getServicesManager"); + Object servicesManager = getServicesManager.invoke(null); + + Method getRegistration = servicesManager.getClass().getMethod("getRegistration", Class.class); + Object registration = getRegistration.invoke(servicesManager, vaultClass); + + if (registration != null) { + Method getProvider = registration.getClass().getMethod("getProvider"); + economy = getProvider.invoke(registration); + + if (economy != null) { + // Get Economy methods - using OfflinePlayer versions for broader compatibility + Class offlinePlayerClass = Class.forName("org.bukkit.OfflinePlayer"); + getBalance = economy.getClass().getMethod("getBalance", offlinePlayerClass); + depositPlayer = economy.getClass().getMethod("depositPlayer", offlinePlayerClass, double.class); + withdrawPlayer = economy.getClass().getMethod("withdrawPlayer", offlinePlayerClass, double.class); + hasAccount = economy.getClass().getMethod("hasAccount", offlinePlayerClass); + createPlayerAccount = economy.getClass().getMethod("createPlayerAccount", offlinePlayerClass); + + // EconomyResponse.transactionSuccess() method + Class economyResponseClass = Class.forName("net.milkbowl.vault.economy.EconomyResponse"); + responseSuccess = economyResponseClass.getField("transactionSuccess").getClass().getMethod("booleanValue"); + + vaultAvailable = true; + logger.info("Vault Economy integration enabled - using external economy"); + } + } + + if (!vaultAvailable) { + logger.info("Vault found but no Economy provider registered - using internal currency"); + } + } catch (ClassNotFoundException e) { + // Vault not present - use internal currency + logger.info("Vault not found - using internal currency system"); + } catch (Exception e) { + logger.warn("Error initializing Vault integration - using internal currency: " + e.getMessage()); + } + } + + /** + * Check if Vault economy is available and enabled + */ + public boolean isVaultEnabled() { + return vaultAvailable && ConfigMarket.UseVaultIfAvailable; + } + + /** + * Get player's balance + * Uses Vault if available, otherwise internal currency + */ + public long getBalance(EntityPlayer player) { + if (isVaultEnabled()) { + try { + Object offlinePlayer = getOfflinePlayer.invoke(null, player.getCommandSenderName()); + Double balance = (Double) getBalance.invoke(economy, offlinePlayer); + return balance.longValue(); + } catch (Exception e) { + logger.warn("Vault getBalance failed, falling back to internal: " + e.getMessage()); + } + } + + // Fall back to internal currency + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + return data.currencyData.getBalance(); + } + + /** + * Deposit currency to player's balance + * Uses Vault if available, otherwise internal currency + * @return true if successful + */ + public boolean deposit(EntityPlayer player, long amount) { + if (amount <= 0) { + return false; + } + + if (isVaultEnabled()) { + try { + Object offlinePlayer = getOfflinePlayer.invoke(null, player.getCommandSenderName()); + + // Ensure account exists + Boolean hasAcc = (Boolean) hasAccount.invoke(economy, offlinePlayer); + if (!hasAcc) { + createPlayerAccount.invoke(economy, offlinePlayer); + } + + Object response = depositPlayer.invoke(economy, offlinePlayer, (double) amount); + // Check if transaction succeeded by checking the transactionSuccess field + Boolean success = (Boolean) response.getClass().getField("transactionSuccess").get(response); + + if (success) { + // Also update internal tracking for statistics + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + data.currencyData.setBalance(getBalance(player)); + return true; + } + return false; + } catch (Exception e) { + logger.warn("Vault deposit failed, falling back to internal: " + e.getMessage()); + } + } + + // Fall back to internal currency + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + boolean success = data.currencyData.deposit(amount); + if (success) { + data.saveCurrency(); + } + return success; + } + + /** + * Withdraw currency from player's balance + * Uses Vault if available, otherwise internal currency + * @return true if successful + */ + public boolean withdraw(EntityPlayer player, long amount) { + if (amount <= 0) { + return false; + } + + if (isVaultEnabled()) { + try { + Object offlinePlayer = getOfflinePlayer.invoke(null, player.getCommandSenderName()); + + // Check if player has enough + Double currentBalance = (Double) getBalance.invoke(economy, offlinePlayer); + if (currentBalance < amount) { + return false; + } + + Object response = withdrawPlayer.invoke(economy, offlinePlayer, (double) amount); + Boolean success = (Boolean) response.getClass().getField("transactionSuccess").get(response); + + if (success) { + // Also update internal tracking for statistics + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + data.currencyData.setBalance(getBalance(player)); + return true; + } + return false; + } catch (Exception e) { + logger.warn("Vault withdraw failed, falling back to internal: " + e.getMessage()); + } + } + + // Fall back to internal currency + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + boolean success = data.currencyData.withdraw(amount); + if (success) { + data.saveCurrency(); + } + return success; + } + + /** + * Check if player can afford an amount + */ + public boolean canAfford(EntityPlayer player, long amount) { + return getBalance(player) >= amount; + } + + /** + * Transfer currency from one player to another + * @return true if successful + */ + public boolean transfer(EntityPlayer from, EntityPlayer to, long amount) { + if (amount <= 0) { + return false; + } + + if (!canAfford(from, amount)) { + return false; + } + + // Withdraw from sender first + if (!withdraw(from, amount)) { + return false; + } + + // Deposit to receiver + if (!deposit(to, amount)) { + // Rollback - deposit back to sender + deposit(from, amount); + return false; + } + + return true; + } + + /** + * Set player's balance directly (admin operation) + * Only works with internal currency - Vault manages its own balances + */ + public void setBalance(EntityPlayer player, long amount) { + if (isVaultEnabled()) { + logger.warn("Cannot set balance directly when using Vault - use Vault's economy commands"); + return; + } + + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + data.currencyData.setBalance(amount); + data.saveCurrency(); + } + + /** + * Format an amount for display with currency name + */ + public String formatAmount(long amount) { + return String.format("%,d %s", amount, ConfigMarket.CurrencyName); + } + + /** + * Get the currency name + */ + public String getCurrencyName() { + return ConfigMarket.CurrencyName; + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java b/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java new file mode 100644 index 000000000..511321dbb --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java @@ -0,0 +1,131 @@ +package noppes.npcs.controllers.data; + +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.config.ConfigMarket; + +/** + * Stores player currency balance. + * This data is NOT profile slot bound - all profile slots share the same currency pool. + * When Vault API is available, this serves as a cache/fallback. + */ +public class PlayerCurrencyData { + private long balance; + private long lifetimeEarned; + private long lifetimeSpent; + + public PlayerCurrencyData() { + this.balance = ConfigMarket.StartingBalance; + this.lifetimeEarned = 0; + this.lifetimeSpent = 0; + } + + public void readFromNBT(NBTTagCompound compound) { + if (compound.hasKey("CurrencyBalance")) { + this.balance = compound.getLong("CurrencyBalance"); + } else { + this.balance = ConfigMarket.StartingBalance; + } + this.lifetimeEarned = compound.getLong("CurrencyLifetimeEarned"); + this.lifetimeSpent = compound.getLong("CurrencyLifetimeSpent"); + } + + public void writeToNBT(NBTTagCompound compound) { + compound.setLong("CurrencyBalance", this.balance); + compound.setLong("CurrencyLifetimeEarned", this.lifetimeEarned); + compound.setLong("CurrencyLifetimeSpent", this.lifetimeSpent); + } + + /** + * Get the current balance + */ + public long getBalance() { + return balance; + } + + /** + * Set the balance directly (use with caution) + */ + public void setBalance(long balance) { + if (balance < 0) { + balance = 0; + } + if (balance > ConfigMarket.MaxBalance) { + balance = ConfigMarket.MaxBalance; + } + this.balance = balance; + } + + /** + * Add currency to balance (deposit) + * @param amount Amount to add + * @return true if successful, false if would exceed max balance + */ + public boolean deposit(long amount) { + if (amount <= 0) { + return false; + } + long newBalance = this.balance + amount; + if (newBalance > ConfigMarket.MaxBalance) { + return false; + } + if (newBalance < this.balance) { + // Overflow protection + return false; + } + this.balance = newBalance; + this.lifetimeEarned += amount; + return true; + } + + /** + * Remove currency from balance (withdraw) + * @param amount Amount to remove + * @return true if successful, false if insufficient funds + */ + public boolean withdraw(long amount) { + if (amount <= 0) { + return false; + } + if (this.balance < amount) { + return false; + } + this.balance -= amount; + this.lifetimeSpent += amount; + return true; + } + + /** + * Check if player can afford an amount + */ + public boolean canAfford(long amount) { + return this.balance >= amount; + } + + /** + * Get lifetime earned currency + */ + public long getLifetimeEarned() { + return lifetimeEarned; + } + + /** + * Get lifetime spent currency + */ + public long getLifetimeSpent() { + return lifetimeSpent; + } + + /** + * Format the balance for display + */ + public String formatBalance() { + return formatAmount(this.balance); + } + + /** + * Format an amount for display + */ + public static String formatAmount(long amount) { + return String.format("%,d %s", amount, ConfigMarket.CurrencyName); + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerData.java b/src/main/java/noppes/npcs/controllers/data/PlayerData.java index 599f81ff1..25d4c81c8 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerData.java @@ -59,6 +59,9 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { public DataSkinOverlays skinOverlays = new DataSkinOverlays(this); public MagicData magicData = new MagicData(); + // Currency data - NOT slot-bound, shared across all profile slots + public PlayerCurrencyData currencyData = new PlayerCurrencyData(); + public ActionManager actionManager = new ActionManager(); public PlayerDataScript scriptData; @@ -443,6 +446,69 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { data = getNBT(); } setNBT(data); + + // Load currency data separately (not slot-bound) + loadCurrency(); + } + + /** + * Load currency data from separate file (shared across profile slots) + */ + public void loadCurrency() { + if (player == null) return; + + File saveDir = PlayerDataController.Instance.getSaveDir(); + String filename = player.getPersistentID().toString() + "_currency"; + + try { + File file = new File(saveDir, filename + ".json"); + if (file.exists()) { + NBTTagCompound compound = NBTJsonUtil.LoadFile(file); + currencyData.readFromNBT(compound); + return; + } + + // Try dat format + file = new File(saveDir, filename + ".dat"); + if (file.exists()) { + NBTTagCompound compound = NBTJsonUtil.loadNBTData(file); + currencyData.readFromNBT(compound); + return; + } + + // No currency file exists, use defaults (new player) + currencyData = new PlayerCurrencyData(); + } catch (Exception e) { + LogWriter.error("Error loading currency data for " + player.getCommandSenderName(), e); + currencyData = new PlayerCurrencyData(); + } + } + + /** + * Save currency data to separate file (shared across profile slots) + */ + public synchronized void saveCurrency() { + if (uuid == null || uuid.isEmpty()) return; + + final NBTTagCompound compound = new NBTTagCompound(); + currencyData.writeToNBT(compound); + + final String filename = uuid + "_currency.json"; + + CustomNPCsThreader.customNPCThread.execute(() -> { + try { + File saveDir = PlayerDataController.Instance.getSaveDir(); + File file = new File(saveDir, filename + "_new"); + File file1 = new File(saveDir, filename); + NBTJsonUtil.SaveFile(file, compound); + if (file1.exists()) { + file1.delete(); + } + file.renameTo(file1); + } catch (Exception e) { + LogWriter.except(e); + } + }); } public static PlayerData get(EntityPlayer player) { diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java index 6206cfbac..5bfaba816 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java @@ -49,11 +49,13 @@ import noppes.npcs.compat.PixelmonHelper; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumQuestType; import noppes.npcs.containers.ContainerCustomGui; +import noppes.npcs.controllers.CurrencyController; import noppes.npcs.controllers.CustomGuiController; import noppes.npcs.controllers.DialogController; import noppes.npcs.controllers.PartyController; import noppes.npcs.controllers.PlayerQuestController; import noppes.npcs.controllers.QuestController; +import noppes.npcs.controllers.VaultHelper; import noppes.npcs.controllers.data.Dialog; import noppes.npcs.controllers.data.Party; import noppes.npcs.controllers.data.PlayerData; @@ -895,4 +897,120 @@ public class ScriptPlayer extends ScriptLivingBase } return (IPlayer[]) list.toArray(new IPlayer[list.size()]); } + + // ==================== CURRENCY METHODS ==================== + + /** + * Get the player's current currency balance. + * Uses Vault if available, otherwise uses internal currency system. + * @return The player's currency balance + * @since 1.11 + */ + public long getCurrencyBalance() { + if (CurrencyController.Instance == null) { + return 0; + } + return CurrencyController.Instance.getBalance(player); + } + + /** + * Add currency to the player's balance. + * @param amount The amount to add (must be positive) + * @return true if successful, false if failed (e.g., would exceed max balance) + * @since 1.11 + */ + public boolean addCurrency(long amount) { + if (CurrencyController.Instance == null) { + return false; + } + return CurrencyController.Instance.deposit(player, amount); + } + + /** + * Remove currency from the player's balance. + * @param amount The amount to remove (must be positive) + * @return true if successful, false if insufficient funds + * @since 1.11 + */ + public boolean removeCurrency(long amount) { + if (CurrencyController.Instance == null) { + return false; + } + return CurrencyController.Instance.withdraw(player, amount); + } + + /** + * Check if the player can afford a specific amount. + * @param amount The amount to check + * @return true if player has sufficient balance + * @since 1.11 + */ + public boolean canAffordCurrency(long amount) { + if (CurrencyController.Instance == null) { + return false; + } + return CurrencyController.Instance.canAfford(player, amount); + } + + /** + * Get the configured currency name (e.g., "Gold"). + * @return The currency name + * @since 1.11 + */ + public String getCurrencyName() { + if (VaultHelper.Instance == null) { + return "Currency"; + } + return VaultHelper.Instance.getCurrencyName(); + } + + /** + * Format a currency amount for display (e.g., "1,000 Gold"). + * @param amount The amount to format + * @return Formatted string + * @since 1.11 + */ + public String formatCurrency(long amount) { + if (CurrencyController.Instance == null) { + return String.valueOf(amount); + } + return CurrencyController.Instance.formatAmount(amount); + } + + /** + * Check if the player has pending currency claims. + * Claims are created by auction refunds, NPC payments, etc. + * @return true if there are pending claims + * @since 1.11 + */ + public boolean hasPendingClaims() { + if (CurrencyController.Instance == null) { + return false; + } + return CurrencyController.Instance.hasPendingClaims(player); + } + + /** + * Get the total amount of pending currency claims. + * @return Total pending claim amount + * @since 1.11 + */ + public long getPendingClaimsTotal() { + if (CurrencyController.Instance == null) { + return 0; + } + return CurrencyController.Instance.getTotalPendingClaims(player); + } + + /** + * Claim all pending currency claims and add them to balance. + * @return The total amount claimed + * @since 1.11 + */ + public long claimAllCurrency() { + if (CurrencyController.Instance == null) { + return 0; + } + return CurrencyController.Instance.claimAll(player); + } }