Use reflection for JSON parsing and fix question state issue

This commit is contained in:
gamer147
2025-11-25 20:53:54 -05:00
parent 9898b2ed07
commit 96e47515b1
2 changed files with 143 additions and 30 deletions

View File

@@ -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<ShopCategory> 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<ShopCategory> 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<ShopCategory> categories = shopService.getCategories();

View File

@@ -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<ShopItem> cachedItems = Collections.emptyList();
// Cache for class lookups to avoid repeated reflection
private final Map<String, Class<?>> currencyClassCache = new HashMap<>();
private final Map<String, Class<?>> 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<String> 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;
}
}