This commit is contained in:
gamer147
2025-11-24 21:12:49 -05:00
parent 76400a6cae
commit 16bfe74417
10 changed files with 395 additions and 77 deletions

View File

@@ -1,3 +1,6 @@
import org.gradle.api.tasks.bundling.Jar
import org.gradle.api.tasks.bundling.Zip
plugins {
id("java")
}
@@ -15,3 +18,14 @@ repositories {
dependencies {
implementation("org.gotti.wurmunlimited:server-modlauncher:0.46")
}
tasks.register<Zip>("dist") {
into("mods") {
into(project.name) {
from(tasks.named<Jar>("jar"))
}
from(fileTree("mods") { include("*") })
}
archiveFileName.set("${project.name}.zip")
}

View File

@@ -2,6 +2,18 @@ classname=mod.treestar.shopmod.ShopMod
classpath=ShopMod.jar
sharedClassLoader=true
# Activates the json item provider and loads items from the given configuration
jsonItemProvider=true
jsonItemProviderItems=mods/shop_items.json
# Display name shown in the shop window
shopName=Server Shop
# Allow opening the shop from settlement tokens
enableTokenAccess=true
# Allow opening the shop from mailboxes
enableMailboxAccess=false
# JSON config files for categories and items
categoryJsonPath=mods/shop_categories.json
itemJsonPath=mods/shop_items.json
# java.util.logging level for this mod (e.g., INFO, FINE)
logLevel=INFO

View File

@@ -1 +1 @@
rootProject.name = "SilverShop"
rootProject.name = "ShopMod"

View File

@@ -6,13 +6,17 @@ import com.wurmonline.server.players.Player;
import mod.treestar.shopmod.ShopService;
import mod.treestar.shopmod.datamodels.ShopCategory;
import mod.treestar.shopmod.datamodels.ShopItem;
import mod.treestar.shopmod.util.BmlForm;
import java.util.List;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
public class ShopQuestion extends Question {
private static final int SHOP_QUESTION_ID = 90001;
private static final Logger logger = Logger.getLogger(ShopQuestion.class.getName());
private ShopService shopService;
private int selectedCategoryId = -1;
@@ -28,49 +32,66 @@ public class ShopQuestion extends Question {
@Override
public void answer(Properties properties) {
super.answer(properties);
logger.log(Level.INFO, "ShopQuestion.answer: received properties {0}", properties);
List<ShopCategory> categories = shopService.getCategories();
// Update category selection if present
String categoryIndex = properties.getProperty("category");
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 });
}
} catch (NumberFormatException ignored) { }
} catch (NumberFormatException ignored) {
logger.log(Level.INFO, "ShopQuestion.answer: invalid category index {0}", categoryIndex);
}
}
// If purchase not requested, refresh UI with selected category
if (!properties.containsKey("submit")) {
String buyKey = properties.stringPropertyNames().stream()
.filter(k -> k.startsWith("buy_"))
.findFirst()
.orElse(null);
if (properties.containsKey("close")) {
logger.log(Level.INFO, "ShopQuestion.answer: close requested");
return; // user closed the window
}
if (properties.containsKey("refresh")) {
logger.log(Level.INFO, "ShopQuestion.answer: refresh requested");
sendQuestion();
return;
}
String itemIdStr = properties.getProperty("itemid");
if (itemIdStr == null) {
getResponder().getCommunicator().sendNormalServerMessage("No item selected.");
if (buyKey == null) { // category change or refresh
logger.log(Level.INFO, "ShopQuestion.answer: no buy key present, refreshing UI");
sendQuestion();
return;
}
int itemId;
try {
itemId = Integer.parseInt(itemIdStr);
int itemId = Integer.parseInt(buyKey.substring("buy_".length()));
logger.log(Level.INFO, "ShopQuestion.answer: attempting purchase itemId={0}", itemId);
ShopService.PurchaseResult result = shopService.purchaseItem((Player) getResponder(), itemId);
if (result.isSuccess()) {
logger.log(Level.INFO, "ShopQuestion.answer: purchase success itemId={0}", itemId);
getResponder().getCommunicator().sendSafeServerMessage(result.getMessage());
} else {
logger.log(Level.INFO, "ShopQuestion.answer: purchase failed itemId={0}, message={1}", new Object[] { itemId, result.getMessage() });
getResponder().getCommunicator().sendAlertServerMessage(result.getMessage());
}
} catch (NumberFormatException e) {
logger.log(Level.INFO, "ShopQuestion.answer: invalid buy key {0}", buyKey);
getResponder().getCommunicator().sendNormalServerMessage("Invalid item selected.");
return;
}
ShopService.PurchaseResult result = shopService.purchaseItem((Player)getResponder(), itemId);
if (result.isSuccess()) {
getResponder().getCommunicator().sendSafeServerMessage(result.getMessage());
} else {
getResponder().getCommunicator().sendNormalServerMessage(result.getMessage());
}
}
@Override
public void sendQuestion() {
List<ShopCategory> categories = shopService.getCategories();
categories = categories.stream()
.filter(c -> shopService.getItems().stream().anyMatch(i -> i.getCategoryId() == c.getId()))
.collect(Collectors.toList());
if (categories.isEmpty()) {
getResponder().getCommunicator().sendNormalServerMessage("No shop categories are available.");
return;
@@ -78,13 +99,6 @@ public class ShopQuestion extends Question {
if (selectedCategoryId == -1 || categories.stream().noneMatch(c -> c.getId() == selectedCategoryId)) {
selectedCategoryId = categories.get(0).getId();
}
int defaultCategoryIndex = 0;
for (int i = 0; i < categories.size(); i++) {
if (categories.get(i).getId() == selectedCategoryId) {
defaultCategoryIndex = i;
break;
}
}
List<ShopItem> items = shopService.getItems().stream()
.filter(i -> i.getCategoryId() == selectedCategoryId)
@@ -93,24 +107,42 @@ public class ShopQuestion extends Question {
getResponder().getCommunicator().sendNormalServerMessage("No shop items are available.");
return;
}
StringBuilder bml = new StringBuilder();
bml.append("border{center{heading{text=\"").append(escape(title)).append("\"}}");
BmlForm form = new BmlForm(title);
form.addHidden("id", String.valueOf(this.id));
// Category selector with refresh button
if (categories.size() > 1) {
bml.append("harray{label{text=\"Category\"};dropdown{id=\"category\";options=\"")
.append(categories.stream().map(c -> escape(c.getName())).collect(Collectors.joining(",")))
.append("\";default=\"").append(defaultCategoryIndex).append("\"}}");
String options = categories.stream().map(c -> escape(c.getName())).collect(Collectors.joining(","));
int defaultIndex = 0;
for (int i = 0; i < categories.size(); i++) {
if (categories.get(i).getId() == selectedCategoryId) {
defaultIndex = i;
break;
}
}
form.beginHorizontalFlow();
form.addLabel("Category");
form.addRaw("dropdown{id=\"" + escape("category") + "\";options=\"" + options + "\";default=\"" + defaultIndex + "\"}");
form.addButton("Show", "refresh");
form.endHorizontalFlow();
}
bml.append("center{scroll{table{rows=\"");
// Items list (one row per item)
for (ShopItem item : items) {
bml.append("label{text=\"").append(escape(item.getName())).append("\"};");
bml.append("label{text=\"").append(escape(item.getDescription())).append("\"};");
bml.append("label{text=\"").append(escape(item.getPriceDisplay())).append("\"};");
bml.append("radio{group=\"itemid\";id=\"").append(item.getId()).append("\"};");
form.beginHorizontalFlow();
form.addLabel(escape(item.getName()));
form.addLabel(escape(item.getDescription()));
form.addLabel(escape(item.getPriceDisplay()));
form.addButton("+1", "buy_" + item.getId());
form.endHorizontalFlow();
}
bml.append("\"}}}");
bml.append("harray{button{id=\"submit\";text=\"Purchase\"};button{id=\"cancel\";text=\"Cancel\"}}");
bml.append("}");
getResponder().getCommunicator().sendBml(400, 400, true, true, bml.toString(), 200, 200, 200, title);
form.beginHorizontalFlow();
form.addButton("Close", "close");
form.endHorizontalFlow();
String bml = form.toString();
logger.log(Level.INFO, "ShopQuestion BML: {0}", bml);
getResponder().getCommunicator().sendBml(400, 400, true, true, bml, 200, 200, 200, title);
}
public ShopService getShopService() {
@@ -122,6 +154,9 @@ public class ShopQuestion extends Question {
}
private String escape(String value) {
return value == null ? "" : value.replace("\"", "''");
if (value == null) {
return "";
}
return value.replace("\"", "''").replace("'", "''");
}
}

View File

@@ -5,6 +5,7 @@ import mod.treestar.shopmod.itemprovider.JsonShopItemProvider;
import mod.treestar.shopmod.ShopOpenAction;
import org.gotti.wurmunlimited.modloader.interfaces.Configurable;
import org.gotti.wurmunlimited.modloader.interfaces.Initable;
import org.gotti.wurmunlimited.modloader.interfaces.ServerStartedListener;
import org.gotti.wurmunlimited.modloader.interfaces.WurmServerMod;
import org.gotti.wurmunlimited.modsupport.actions.ModActions;
@@ -12,7 +13,7 @@ import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
public class ShopMod implements WurmServerMod, Initable, Configurable {
public class ShopMod implements WurmServerMod, Initable, Configurable, ServerStartedListener {
private static final Logger logger = Logger.getLogger(ShopMod.class.getName());
private String shopName = "Server Shop";
@@ -21,6 +22,7 @@ public class ShopMod implements WurmServerMod, Initable, Configurable {
private String categoryJsonPath = "mods/shop/categories.json";
private String itemJsonPath = "mods/shop/items.json";
private Level logLevel = Level.INFO;
private ShopService shopService;
@Override
public void configure(Properties properties) {
@@ -37,11 +39,13 @@ public class ShopMod implements WurmServerMod, Initable, Configurable {
public void init() {
ModActions.init();
ShopService shopService = ShopService.getInstance();
shopService = ShopService.getInstance();
shopService.registerCategoryProvider(new JsonShopCategoryProvider(categoryJsonPath));
shopService.registerItemProvider(new JsonShopItemProvider(itemJsonPath));
}
@Override
public void onServerStarted() {
new ShopOpenAction(shopService, shopName, enableTokenAccess, enableMailboxAccess);
logger.log(Level.INFO, String.format("Initialized shop '%s' (token access: %s, mailbox access: %s)", shopName, enableTokenAccess, enableMailboxAccess));

View File

@@ -66,6 +66,11 @@ public class ShopOpenAction implements ActionPerformer, BehaviourProvider {
return false; // not supported
}
@Override
public boolean action(Action action, Creature performer, Item source, Item target, short actionId, float counter) {
return action(action, performer, target, actionId, counter);
}
@Override
public List<ActionEntry> getBehavioursFor(Creature performer, Item target) {
if (isValidTarget(target)) {
@@ -74,6 +79,11 @@ public class ShopOpenAction implements ActionPerformer, BehaviourProvider {
return null;
}
@Override
public List<ActionEntry> getBehavioursFor(Creature performer, Item source, Item target) {
return getBehavioursFor(performer, target);
}
@Override
public List<ActionEntry> getBehavioursFor(Creature performer, Creature target) {
return null;

View File

@@ -32,9 +32,13 @@ public class ShopService {
public List<ShopCategory> getCategories() {
List<ShopCategory> categories = new ArrayList<>();
for (ShopCategoryProvider provider : categoryProviders) {
List<ShopCategory> provided = provider.getCategories();
if (provided != null) {
categories.addAll(provided);
try {
List<ShopCategory> provided = provider.getCategories();
if (provided != null) {
categories.addAll(provided);
}
} catch (Exception e) {
logger.log(Level.WARNING, "Category provider threw an exception", e);
}
}
return categories;
@@ -43,9 +47,13 @@ public class ShopService {
public List<ShopItem> getItems() {
List<ShopItem> items = new ArrayList<>();
for (ShopItemProvider provider : itemProviders) {
List<ShopItem> provided = provider.getItems();
if (provided != null) {
items.addAll(provided);
try {
List<ShopItem> provided = provider.getItems();
if (provided != null) {
items.addAll(provided);
}
} catch (Exception e) {
logger.log(Level.WARNING, "Item provider threw an exception", e);
}
}
return items;
@@ -66,12 +74,17 @@ public class ShopService {
if (item.getPurchaseHandler() == null) {
return PurchaseResult.failure("Item has no purchase handler configured.");
}
if (!item.getCurrency().canPlayerAfford(player)) {
return PurchaseResult.failure("You cannot afford this item.");
}
boolean charged = item.getCurrency().chargePlayer(player);
if (!charged) {
return PurchaseResult.failure("Charging failed; purchase canceled.");
try {
if (!item.getCurrency().canPlayerAfford(player)) {
return PurchaseResult.failure("You cannot afford this item.");
}
boolean charged = item.getCurrency().chargePlayer(player);
if (!charged) {
return PurchaseResult.failure("Charging failed; purchase canceled.");
}
} catch (Exception e) {
logger.log(Level.WARNING, "Currency check/charge failed for item " + itemId + " for player " + player.getName(), e);
return PurchaseResult.failure("Payment failed; purchase canceled.");
}
try {
item.getPurchaseHandler().onPurchase(player);

View File

@@ -50,6 +50,8 @@ public class JsonShopCategoryProvider implements ShopCategoryProvider {
JSONArray typeArray = new JSONArray(tokenizer);
cachedCategories = parseCategories(typeArray);
lastModified = file.lastModified();
} catch (Exception e) {
logger.log(Level.WARNING, "Failed to parse categories JSON; keeping previous categories", e);
}
}

View File

@@ -60,6 +60,8 @@ public class JsonShopItemProvider implements ShopItemProvider {
JSONArray typeArray = new JSONArray(tokenizer);
cachedItems = parseItems(typeArray);
lastModified = file.lastModified();
} catch (Exception e) {
logger.log(Level.WARNING, "Failed to parse items JSON; keeping previous items", e);
}
}
@@ -95,9 +97,9 @@ 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", "");
String type = currencyObj.optString("type", "").toLowerCase(Locale.ROOT);
switch (type) {
case "WurmBankCurrency":
case "wurmbankcurrency":
long ironAmount = currencyObj.optLong("ironAmount", currencyObj.optLong("priceIron", 0L));
return new WurmBankCurrency(ironAmount);
default:
@@ -111,23 +113,23 @@ 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", "");
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");
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);
case "shopwurmitempurchaseeffect":
int templateId = handlerObj.optInt("itemTemplateId", -1);
if (templateId <= 0) {
logger.log(Level.WARNING, "Handler for item at index " + index + " missing valid itemTemplateId");
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);
return null;
}
}
}

View File

@@ -0,0 +1,226 @@
package mod.treestar.shopmod.util;
import java.util.logging.Level;
import java.util.logging.Logger;
public class BmlForm {
private static Logger logger = Logger.getLogger(BmlForm.class.getName());
private final StringBuffer buf = new StringBuffer();
private int openBorders = 0;
private int openCenters = 0;
private int openVarrays = 0;
private int openScrolls = 0;
private int openHarrays = 0;
private int openTrees = 0;
private int openRows = 0;
private int openColumns = 0;
private int openTables = 0;
private int indentNum = 0;
private boolean beautify = false;
private boolean closeDefault = false;
public BmlForm() {
}
public BmlForm(String formTitle) {
this.addDefaultHeader(formTitle);
}
public void addDefaultHeader(String formTitle) {
if (this.closeDefault) {
return;
}
this.beginBorder();
this.beginCenter();
this.addBoldText(formTitle, new String[0]);
this.endCenter();
this.beginScroll();
this.beginVerticalFlow();
this.closeDefault = true;
}
public void beginBorder() {
this.buf.append(this.indent("border{"));
++this.indentNum;
++this.openBorders;
}
public void endBorder() {
--this.indentNum;
this.buf.append(this.indent("}"));
--this.openBorders;
}
public void beginCenter() {
this.buf.append(this.indent("center{"));
++this.indentNum;
++this.openCenters;
}
public void endCenter() {
--this.indentNum;
this.buf.append(this.indent("};null;"));
--this.openCenters;
}
public void beginVerticalFlow() {
this.buf.append(this.indent("varray{rescale=\"true\";"));
++this.indentNum;
++this.openVarrays;
}
public void endVerticalFlow() {
--this.indentNum;
this.buf.append(this.indent("}"));
--this.openVarrays;
}
public void beginScroll() {
this.buf.append(this.indent("scroll{vertical=\"true\";horizontal=\"false\";"));
++this.indentNum;
++this.openScrolls;
}
public void endScroll() {
--this.indentNum;
this.buf.append(this.indent("};null;null;"));
--this.openScrolls;
}
public void beginHorizontalFlow() {
this.buf.append(this.indent("harray {"));
++this.indentNum;
++this.openHarrays;
}
public void endHorizontalFlow() {
--this.indentNum;
this.buf.append(this.indent("}"));
--this.openHarrays;
}
public void beginTable(int rowCount, String[] columns) {
this.buf.append(this.indent("table {rows=\"" + rowCount + "\"; cols=\"" + columns.length + "\";"));
++this.indentNum;
String[] arrstring = columns;
int n = arrstring.length;
int n2 = 0;
while (n2 < n) {
String c = arrstring[n2];
this.addLabel(c);
++n2;
}
--this.indentNum;
++this.indentNum;
++this.openTables;
}
public void endTable() {
--this.indentNum;
this.buf.append(this.indent("}"));
--this.openTables;
}
public void addHidden(String name, String val) {
this.buf.append(this.indent("passthrough{id=\"" + name + "\";text=\"" + val + "\"}"));
}
private String indent(String s) {
return this.beautify ? String.valueOf(this.getIndentation()) + s + "\r\n" : s;
}
private String getIndentation() {
if (this.indentNum > 0) {
return "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t".substring(0, this.indentNum);
}
return "";
}
public void addRaw(String s) {
this.buf.append(s);
}
public void addImage(String url, int height, int width) {
this.addImage(url, height, width, "");
}
public void addImage(String url, int height, int width, String tooltip) {
this.buf.append("image{src=\"");
this.buf.append(url);
this.buf.append("\";size=\"");
this.buf.append(String.valueOf(height) + "," + width);
this.buf.append("\";text=\"" + tooltip + "\"}");
}
public void addLabel(String text) {
this.buf.append("label{text='" + text + "'};");
}
public void addInput(String id, int maxChars, String defaultText) {
this.buf.append("input{id='" + id + "';maxchars='" + maxChars + "';text=\"" + defaultText + "\"};");
}
public void addColoredText(String text, int r, int g, int b, String ... args){
this.addText(text, "", r, g, b, args);
}
public void addBoldText(String text, String ... args) {
this.addText(text, "bold", args);
}
public void addBoldColoredText(String text, int r, int g, int b, String ... args){
this.addText(text, "bold", r, g, b, args);
}
public void addText(String text, String ... args) {
this.addText(text, "", args);
}
private void addText(String text, String type, String ... args){
this.addText(text, type, -10, -10, -10, args);
}
private void addText(String text, String type, int r, int g, int b, String ... args) {
String[] lines;
String[] arrstring = lines = text.split("\n");
int n = arrstring.length;
int n2 = 0;
while (n2 < n) {
String l = arrstring[n2];
if (this.beautify) {
this.buf.append(this.getIndentation());
}
this.buf.append("text{");
if (!type.equals("")) {
this.buf.append("type='").append(type).append("';");
}
if(r >= 0 && g >= 0 && b >= 0 && r <= 255 && g <= 255 && b <= 255){
this.buf.append("color='").append(r).append(",").append(g).append(",").append(b).append("';");
}
this.buf.append("text=\"");
this.buf.append(String.format(l, (Object[]) args));
this.buf.append("\"}");
if (this.beautify) {
this.buf.append("\r\n");
}
++n2;
}
}
public void addButton(String name, String id) {
this.buf.append(this.indent("button{text=' " + name + " ';id='" + id + "'}"));
}
public String toString() {
if (this.closeDefault) {
this.endVerticalFlow();
this.endScroll();
this.endBorder();
this.closeDefault = false;
}
if (this.openCenters != 0 || this.openVarrays != 0 || this.openScrolls != 0 || this.openHarrays != 0 || this.openBorders != 0 || this.openTrees != 0 || this.openRows != 0 || this.openColumns != 0 || this.openTables != 0) {
logger.log(Level.SEVERE, "While finalizing BML unclosed (or too many closed) blocks were found (this will likely mean the BML will not work!): center: " + this.openCenters + " vert-flows: " + this.openVarrays + " scroll: " + this.openScrolls + " horiz-flows: " + this.openHarrays + " border: " + this.openBorders + " trees: " + this.openTrees + " rows: " + this.openRows + " columns: " + this.openColumns + " tables: " + this.openTables);
}
return this.buf.toString();
}
}