diff --git a/src/main/java/com/wurmonline/server/questions/ShopQuestion.java b/src/main/java/com/wurmonline/server/questions/ShopQuestion.java index 54e080e..689ebff 100644 --- a/src/main/java/com/wurmonline/server/questions/ShopQuestion.java +++ b/src/main/java/com/wurmonline/server/questions/ShopQuestion.java @@ -22,11 +22,17 @@ public class ShopQuestion extends Question { private int selectedCategoryId = -1; public ShopQuestion(long responderId, String shopName, ShopService shopService) throws NoSuchPlayerException { + this(responderId, shopName, shopService, -1); + } + + public ShopQuestion(long responderId, String shopName, ShopService shopService, int selectedCategoryId) throws NoSuchPlayerException { super(Players.getInstance().getPlayer(responderId), shopName, null, SHOP_QUESTION_ID, responderId); this.shopService = shopService; List categories = shopService.getCategories(); - if (!categories.isEmpty()) { - selectedCategoryId = categories.get(0).getId(); + if (selectedCategoryId >= 0 && categories.stream().anyMatch(c -> c.getId() == selectedCategoryId)) { + this.selectedCategoryId = selectedCategoryId; + } else if (!categories.isEmpty()) { + this.selectedCategoryId = categories.get(0).getId(); } } @@ -35,12 +41,13 @@ public class ShopQuestion extends Question { logger.log(Level.INFO, "ShopQuestion.answer: received properties {0}", properties); List categories = shopService.getCategories(); String categoryIndex = properties.getProperty("category"); + int newCategoryId = selectedCategoryId; if (categoryIndex != null && !categories.isEmpty()) { try { int idx = Integer.parseInt(categoryIndex); if (idx >= 0 && idx < categories.size()) { - selectedCategoryId = categories.get(idx).getId(); - logger.log(Level.INFO, "ShopQuestion.answer: category set to index {0}, id {1}", new Object[] { idx, selectedCategoryId }); + newCategoryId = categories.get(idx).getId(); + logger.log(Level.INFO, "ShopQuestion.answer: category set to index {0}, id {1}", new Object[] { idx, newCategoryId }); } } catch (NumberFormatException ignored) { logger.log(Level.INFO, "ShopQuestion.answer: invalid category index {0}", categoryIndex); @@ -59,13 +66,13 @@ public class ShopQuestion extends Question { if (properties.containsKey("refresh")) { logger.log(Level.INFO, "ShopQuestion.answer: refresh requested"); - sendQuestion(); + createNewQuestion(newCategoryId); return; } if (buyKey == null) { // category change or refresh logger.log(Level.INFO, "ShopQuestion.answer: no buy key present, refreshing UI"); - sendQuestion(); + createNewQuestion(newCategoryId); return; } @@ -80,12 +87,23 @@ public class ShopQuestion extends Question { logger.log(Level.INFO, "ShopQuestion.answer: purchase failed itemId={0}, message={1}", new Object[] { itemId, result.getMessage() }); getResponder().getCommunicator().sendAlertServerMessage(result.getMessage()); } + // Reopen the shop after purchase + createNewQuestion(newCategoryId); } catch (NumberFormatException e) { logger.log(Level.INFO, "ShopQuestion.answer: invalid buy key {0}", buyKey); getResponder().getCommunicator().sendNormalServerMessage("Invalid item selected."); } } + private void createNewQuestion(int categoryId) { + try { + ShopQuestion newQuestion = new ShopQuestion(getResponder().getWurmId(), title, shopService, categoryId); + newQuestion.sendQuestion(); + } catch (NoSuchPlayerException e) { + logger.log(Level.WARNING, "Failed to create new ShopQuestion", e); + } + } + @Override public void sendQuestion() { List categories = shopService.getCategories(); diff --git a/src/main/java/mod/treestar/shopmod/itemprovider/JsonShopItemProvider.java b/src/main/java/mod/treestar/shopmod/itemprovider/JsonShopItemProvider.java index c570314..b748b8c 100644 --- a/src/main/java/mod/treestar/shopmod/itemprovider/JsonShopItemProvider.java +++ b/src/main/java/mod/treestar/shopmod/itemprovider/JsonShopItemProvider.java @@ -4,18 +4,19 @@ import com.wurmonline.server.support.JSONArray; import com.wurmonline.server.support.JSONObject; import com.wurmonline.server.support.JSONTokener; import mod.treestar.shopmod.currencies.ShopCurrency; -import mod.treestar.shopmod.currencies.WurmBankCurrency; import mod.treestar.shopmod.datamodels.ShopItem; import mod.treestar.shopmod.purchasehandlers.ShopItemPurchaseEffect; -import mod.treestar.shopmod.purchasehandlers.ShopWurmItemPurchaseEffect; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; import java.util.List; -import java.util.Locale; +import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; @@ -27,10 +28,17 @@ import java.util.logging.Logger; public class JsonShopItemProvider implements ShopItemProvider { private static final Logger logger = Logger.getLogger(JsonShopItemProvider.class.getName()); + private static final String CURRENCIES_PACKAGE = "mod.treestar.shopmod.currencies."; + private static final String HANDLERS_PACKAGE = "mod.treestar.shopmod.purchasehandlers."; + private final String itemJsonPath; private long lastModified = -1L; private List cachedItems = Collections.emptyList(); + // Cache for class lookups to avoid repeated reflection + private final Map> currencyClassCache = new HashMap<>(); + private final Map> handlerClassCache = new HashMap<>(); + public JsonShopItemProvider(String itemJsonPath) { this.itemJsonPath = itemJsonPath; } @@ -97,14 +105,29 @@ public class JsonShopItemProvider implements ShopItemProvider { logger.log(Level.WARNING, "Item at index " + index + " missing currency; item will be unusable"); return null; } - String type = currencyObj.optString("type", "").toLowerCase(Locale.ROOT); - switch (type) { - case "wurmbankcurrency": - long ironAmount = currencyObj.optLong("ironAmount", currencyObj.optLong("priceIron", 0L)); - return new WurmBankCurrency(ironAmount); - default: - logger.log(Level.WARNING, "Unknown currency type '" + type + "' for item at index " + index); + String type = currencyObj.optString("type", ""); + if (type.isEmpty()) { + logger.log(Level.WARNING, "Item at index " + index + " missing currency type"); + return null; + } + try { + Class clazz = currencyClassCache.computeIfAbsent(type, t -> { + try { + return Class.forName(CURRENCIES_PACKAGE + t); + } catch (ClassNotFoundException e) { + return null; + } + }); + if (clazz == null || !ShopCurrency.class.isAssignableFrom(clazz)) { + logger.log(Level.WARNING, "Unknown or invalid currency type '" + type + "' for item at index " + index); return null; + } + Object instance = clazz.getDeclaredConstructor().newInstance(); + applyJsonFields(instance, currencyObj, index); + return (ShopCurrency) instance; + } catch (Exception e) { + logger.log(Level.WARNING, "Failed to instantiate currency '" + type + "' for item at index " + index, e); + return null; } } @@ -113,23 +136,95 @@ public class JsonShopItemProvider implements ShopItemProvider { logger.log(Level.WARNING, "Item at index " + index + " missing handler; item will be unusable"); return null; } - String type = handlerObj.optString("type", "").toLowerCase(Locale.ROOT); - switch (type) { - case "shopwurmitempurchaseeffect": - int templateId = handlerObj.optInt("itemTemplateId", -1); - if (templateId <= 0) { - logger.log(Level.WARNING, "Handler for item at index " + index + " missing valid itemTemplateId"); + String type = handlerObj.optString("type", ""); + if (type.isEmpty()) { + logger.log(Level.WARNING, "Item at index " + index + " missing handler type"); + return null; + } + try { + Class clazz = handlerClassCache.computeIfAbsent(type, t -> { + try { + return Class.forName(HANDLERS_PACKAGE + t); + } catch (ClassNotFoundException e) { + return null; + } + }); + if (clazz == null || !ShopItemPurchaseEffect.class.isAssignableFrom(clazz)) { + logger.log(Level.WARNING, "Unknown or invalid handler type '" + type + "' for item at index " + index); return null; } - ShopWurmItemPurchaseEffect effect = new ShopWurmItemPurchaseEffect(); - effect.setItemTemplateId(templateId); - effect.setQl((float) handlerObj.optDouble("ql", 50)); - effect.setRandomQl(handlerObj.optBoolean("randomQl", false)); - effect.setRarity((byte) handlerObj.optInt("rarity", 0)); - return effect; - default: - logger.log(Level.WARNING, "Unknown handler type '" + type + "' for item at index " + index); + Object instance = clazz.getDeclaredConstructor().newInstance(); + applyJsonFields(instance, handlerObj, index); + return (ShopItemPurchaseEffect) instance; + } catch (Exception e) { + logger.log(Level.WARNING, "Failed to instantiate handler '" + type + "' for item at index " + index, e); return null; } } + + @SuppressWarnings("unchecked") + private void applyJsonFields(Object instance, JSONObject json, int index) { + Class clazz = instance.getClass(); + Iterator keys = json.keys(); + while (keys.hasNext()) { + String key = keys.next(); + if ("type".equals(key)) { + continue; // skip the type field, it's used for class lookup + } + String setterName = "set" + Character.toUpperCase(key.charAt(0)) + key.substring(1); + Object value = json.get(key); + Method setter = findSetter(clazz, setterName); + if (setter == null) { + logger.log(Level.FINE, "No setter '" + setterName + "' found on " + clazz.getSimpleName() + " for item at index " + index); + continue; + } + try { + Class paramType = setter.getParameterTypes()[0]; + Object convertedValue = convertValue(value, paramType); + if (convertedValue != null) { + setter.invoke(instance, convertedValue); + } + } catch (Exception e) { + logger.log(Level.WARNING, "Failed to set field '" + key + "' on " + clazz.getSimpleName() + " for item at index " + index, e); + } + } + } + + private Method findSetter(Class clazz, String setterName) { + for (Method method : clazz.getMethods()) { + if (method.getName().equals(setterName) && method.getParameterCount() == 1) { + return method; + } + } + return null; + } + + private Object convertValue(Object value, Class targetType) { + if (value == null) { + return null; + } + // Handle primitive types and their wrappers + if (targetType == int.class || targetType == Integer.class) { + return ((Number) value).intValue(); + } else if (targetType == long.class || targetType == Long.class) { + return ((Number) value).longValue(); + } else if (targetType == float.class || targetType == Float.class) { + return ((Number) value).floatValue(); + } else if (targetType == double.class || targetType == Double.class) { + return ((Number) value).doubleValue(); + } else if (targetType == byte.class || targetType == Byte.class) { + return ((Number) value).byteValue(); + } else if (targetType == short.class || targetType == Short.class) { + return ((Number) value).shortValue(); + } else if (targetType == boolean.class || targetType == Boolean.class) { + return value; + } else if (targetType == String.class) { + return value.toString(); + } + // If no conversion needed or type matches + if (targetType.isInstance(value)) { + return value; + } + return null; + } }