From c4d564abb62ee1a2f56693ccc0993974d41c2573 Mon Sep 17 00:00:00 2001 From: Melledy <121644117+Melledy@users.noreply.github.com> Date: Fri, 14 Nov 2025 08:08:02 -0800 Subject: [PATCH] Implement shop purchase limits --- .../nebula/data/resources/MallShopDef.java | 5 ++ .../data/resources/ResidentGoodsDef.java | 5 ++ .../emu/nebula/database/DatabaseManager.java | 3 +- .../database/codecs/String2IntMapCodec.java | 42 ++++++++++ .../emu/nebula/game/inventory/Inventory.java | 78 ++++++++++++++++--- .../handlers/HandlerMallShopListReq.java | 2 +- .../handlers/HandlerMallShopOrderReq.java | 7 +- .../handlers/HandlerResidentShopGetReq.java | 34 +++++++- .../HandlerResidentShopPurchaseReq.java | 13 ++-- .../java/emu/nebula/util/String2IntMap.java | 15 ++++ 10 files changed, 176 insertions(+), 28 deletions(-) create mode 100644 src/main/java/emu/nebula/database/codecs/String2IntMapCodec.java create mode 100644 src/main/java/emu/nebula/util/String2IntMap.java diff --git a/src/main/java/emu/nebula/data/resources/MallShopDef.java b/src/main/java/emu/nebula/data/resources/MallShopDef.java index b2bb621..a620999 100644 --- a/src/main/java/emu/nebula/data/resources/MallShopDef.java +++ b/src/main/java/emu/nebula/data/resources/MallShopDef.java @@ -5,6 +5,7 @@ import com.google.gson.annotations.SerializedName; import emu.nebula.data.BaseDef; import emu.nebula.data.ResourceType; import emu.nebula.game.inventory.ItemParamMap; +import emu.nebula.game.player.Player; import lombok.Getter; @Getter @@ -27,6 +28,10 @@ public class MallShopDef extends BaseDef { return IdString.hashCode(); } + public int getStock(Player player) { + return Math.max(this.getStock() - player.getInventory().getMallBuyCount().get(this.getIdString()), 0); + } + @Override public void onLoad() { this.products = new ItemParamMap(); diff --git a/src/main/java/emu/nebula/data/resources/ResidentGoodsDef.java b/src/main/java/emu/nebula/data/resources/ResidentGoodsDef.java index 6ce79a4..856dd02 100644 --- a/src/main/java/emu/nebula/data/resources/ResidentGoodsDef.java +++ b/src/main/java/emu/nebula/data/resources/ResidentGoodsDef.java @@ -3,6 +3,7 @@ package emu.nebula.data.resources; import emu.nebula.data.BaseDef; import emu.nebula.data.ResourceType; import emu.nebula.game.inventory.ItemParamMap; +import emu.nebula.game.player.Player; import lombok.Getter; @Getter @@ -24,6 +25,10 @@ public class ResidentGoodsDef extends BaseDef { public int getId() { return Id; } + + public int getStock(Player player) { + return Math.max(this.getMaximumLimit() - player.getInventory().getMallBuyCount().getInt(this.getId()), 0); + } @Override public void onLoad() { diff --git a/src/main/java/emu/nebula/database/DatabaseManager.java b/src/main/java/emu/nebula/database/DatabaseManager.java index 8599650..6f40e0c 100644 --- a/src/main/java/emu/nebula/database/DatabaseManager.java +++ b/src/main/java/emu/nebula/database/DatabaseManager.java @@ -64,7 +64,8 @@ public final class DatabaseManager { // Add our custom fastutil codecs var codecProvider = CodecRegistries.fromCodecs( - new IntSetCodec(), new IntListCodec(), new Int2IntMapCodec(), new ItemParamMapCodec(), new BitsetCodec() + new IntSetCodec(), new IntListCodec(), new Int2IntMapCodec(), + new ItemParamMapCodec(), new String2IntMapCodec(), new BitsetCodec() ); // Set mapper options diff --git a/src/main/java/emu/nebula/database/codecs/String2IntMapCodec.java b/src/main/java/emu/nebula/database/codecs/String2IntMapCodec.java new file mode 100644 index 0000000..27fd9bb --- /dev/null +++ b/src/main/java/emu/nebula/database/codecs/String2IntMapCodec.java @@ -0,0 +1,42 @@ +package emu.nebula.database.codecs; + +import org.bson.BsonReader; +import org.bson.BsonType; +import org.bson.BsonWriter; +import org.bson.codecs.Codec; +import org.bson.codecs.DecoderContext; +import org.bson.codecs.EncoderContext; + +import emu.nebula.util.String2IntMap; + +/** + * Custom mongodb codec for encoding/decoding fastutil int2int maps. + */ +public class String2IntMapCodec implements Codec { + + @Override + public Class getEncoderClass() { + return String2IntMap.class; + } + + @Override + public void encode(BsonWriter writer, String2IntMap collection, EncoderContext encoderContext) { + writer.writeStartDocument(); + for (var entry : collection.object2IntEntrySet()) { + writer.writeName(entry.getKey()); + writer.writeInt32(entry.getIntValue()); + } + writer.writeEndDocument(); + } + + @Override + public String2IntMap decode(BsonReader reader, DecoderContext decoderContext) { + String2IntMap collection = new String2IntMap(); + reader.readStartDocument(); + while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) { + collection.put(reader.readName(), reader.readInt32()); + } + reader.readEndDocument(); + return collection; + } +} \ No newline at end of file diff --git a/src/main/java/emu/nebula/game/inventory/Inventory.java b/src/main/java/emu/nebula/game/inventory/Inventory.java index 696fbb7..b042a57 100644 --- a/src/main/java/emu/nebula/game/inventory/Inventory.java +++ b/src/main/java/emu/nebula/game/inventory/Inventory.java @@ -7,6 +7,8 @@ import dev.morphia.annotations.Id; import emu.nebula.GameConstants; import emu.nebula.Nebula; import emu.nebula.data.GameData; +import emu.nebula.data.resources.MallShopDef; +import emu.nebula.data.resources.ResidentGoodsDef; import emu.nebula.database.GameDatabaseObject; import emu.nebula.game.player.PlayerManager; import emu.nebula.game.quest.QuestCondType; @@ -17,6 +19,7 @@ import emu.nebula.proto.Public.Item; import emu.nebula.proto.Public.Res; import emu.nebula.proto.Public.Title; import emu.nebula.proto.Public.UI32; +import emu.nebula.util.String2IntMap; import emu.nebula.game.player.Player; import emu.nebula.game.player.PlayerChangeInfo; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; @@ -38,6 +41,10 @@ public class Inventory extends PlayerManager implements GameDatabaseObject { private IntSet titles; private IntSet honorList; + // Buy limit + private ItemParamMap shopBuyCount; + private String2IntMap mallBuyCount; + // Items/resources private transient Int2ObjectMap resources; private transient Int2ObjectMap items; @@ -58,6 +65,9 @@ public class Inventory extends PlayerManager implements GameDatabaseObject { this.titles = new IntOpenHashSet(); this.honorList = new IntOpenHashSet(); + this.shopBuyCount = new ItemParamMap(); + this.mallBuyCount = new String2IntMap(); + // Add titles directly this.getTitles().add(player.getTitlePrefix()); this.getTitles().add(player.getTitleSuffix()); @@ -640,27 +650,75 @@ public class Inventory extends PlayerManager implements GameDatabaseObject { return change; } - public PlayerChangeInfo buyItem(int currencyId, int currencyCount, ItemParamMap buyItems, int buyCount) { - return this.buyItem(currencyId, currencyCount, buyItems, buyCount, null); - } - - public PlayerChangeInfo buyItem(int currencyId, int currencyCount, ItemParamMap buyItems, int buyCount, PlayerChangeInfo change) { - // Player change info - if (change == null) { - change = new PlayerChangeInfo(); + public PlayerChangeInfo buyMallItem(MallShopDef data, int buyCount) { + // Check stock + int stock = data.getStock(this.getPlayer()); + if (buyCount > stock) { + return null; } + // Buy item + var change = this.buyItem(data.getExchangeItemId(), data.getExchangeItemQty(), data.getProducts(), buyCount); + + if (change == null) { + return null; + } + + // Update purchase limit + this.getMallBuyCount().addTo(data.getIdString(), buyCount); + Nebula.getGameDatabase().update( + this, + getUid(), + "mallBuyCount." + data.getIdString(), + getMallBuyCount().get(data.getIdString()) + ); + + // Return + return change; + } + + public PlayerChangeInfo buyShopItem(ResidentGoodsDef data, int buyCount) { + // Check stock + int stock = data.getStock(this.getPlayer()); + if (buyCount > stock) { + return null; + } + + // Buy item + var change = this.buyItem(data.getCurrencyItemId(), data.getPrice(), data.getProducts(), buyCount); + + if (change == null) { + return null; + } + + // Update purchase limit + this.getShopBuyCount().add(data.getId(), buyCount); + Nebula.getGameDatabase().update( + this, + getUid(), + "shopBuyCount." + data.getId(), + getShopBuyCount().get(data.getId()) + ); + + // Return + return change; + } + + public PlayerChangeInfo buyItem(int currencyId, int currencyCount, ItemParamMap buyItems, int buyCount) { // Sanity check if (buyCount <= 0) { - return change; + return null; } // Make sure we have the currency int cost = buyCount * currencyCount; if (!this.hasItem(currencyId, cost)) { - return change; + return null; } + + // Player change info + var change = new PlayerChangeInfo(); // Remove currency item this.removeItem(currencyId, cost, change); diff --git a/src/main/java/emu/nebula/server/handlers/HandlerMallShopListReq.java b/src/main/java/emu/nebula/server/handlers/HandlerMallShopListReq.java index 014abe2..8169bc4 100644 --- a/src/main/java/emu/nebula/server/handlers/HandlerMallShopListReq.java +++ b/src/main/java/emu/nebula/server/handlers/HandlerMallShopListReq.java @@ -28,7 +28,7 @@ public class HandlerMallShopListReq extends NetHandler { var info = ProductInfo.newInstance() .setId(data.getIdString()) - .setStock(data.getStock()) + .setStock(data.getStock(session.getPlayer())) .setRefreshTime(refreshTime); rsp.addList(info); diff --git a/src/main/java/emu/nebula/server/handlers/HandlerMallShopOrderReq.java b/src/main/java/emu/nebula/server/handlers/HandlerMallShopOrderReq.java index 35c18bb..977b8fd 100644 --- a/src/main/java/emu/nebula/server/handlers/HandlerMallShopOrderReq.java +++ b/src/main/java/emu/nebula/server/handlers/HandlerMallShopOrderReq.java @@ -22,12 +22,7 @@ public class HandlerMallShopOrderReq extends NetHandler { } // Buy items - var change = session.getPlayer().getInventory().buyItem( - data.getExchangeItemId(), - data.getExchangeItemQty(), - data.getProducts(), - req.getQty() - ); + var change = session.getPlayer().getInventory().buyMallItem(data, req.getQty()); if (change == null) { return session.encodeMsg(NetMsgId.mall_shop_order_failed_ack); diff --git a/src/main/java/emu/nebula/server/handlers/HandlerResidentShopGetReq.java b/src/main/java/emu/nebula/server/handlers/HandlerResidentShopGetReq.java index 09079a4..fa2a6f6 100644 --- a/src/main/java/emu/nebula/server/handlers/HandlerResidentShopGetReq.java +++ b/src/main/java/emu/nebula/server/handlers/HandlerResidentShopGetReq.java @@ -2,8 +2,10 @@ package emu.nebula.server.handlers; import emu.nebula.net.NetHandler; import emu.nebula.net.NetMsgId; +import emu.nebula.proto.Public.BoughtGoods; import emu.nebula.proto.Public.ResidentShop; import emu.nebula.proto.ResidentShopGet.ResidentShopGetResp; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import emu.nebula.net.HandlerId; import emu.nebula.data.GameData; import emu.nebula.net.GameSession; @@ -13,15 +15,41 @@ public class HandlerResidentShopGetReq extends NetHandler { @Override public byte[] handle(GameSession session, byte[] message) throws Exception { - // Build response - var rsp = ResidentShopGetResp.newInstance(); + // Get shops + var shops = new Int2ObjectOpenHashMap(); for (var data : GameData.getResidentShopDataTable()) { var proto = ResidentShop.newInstance() .setId(data.getId()) .setRefreshTime(Long.MAX_VALUE); - rsp.addShops(proto); + shops.put(data.getId(), proto); + } + + // Add bought goods + for (var data : GameData.getResidentGoodsDataTable()) { + int bought = session.getPlayer().getInventory().getShopBuyCount().get(data.getId()); + if (bought == 0) { + continue; + } + + var shop = shops.get(data.getShopId()); + if (shop == null) { + continue; + } + + var info = BoughtGoods.newInstance() + .setId(data.getId()) + .setNumber(bought); + + shop.addInfos(info); + } + + // Build response + var rsp = ResidentShopGetResp.newInstance(); + + for (var shop : shops.values()) { + rsp.addShops(shop); } // Encode and send diff --git a/src/main/java/emu/nebula/server/handlers/HandlerResidentShopPurchaseReq.java b/src/main/java/emu/nebula/server/handlers/HandlerResidentShopPurchaseReq.java index 3551d65..62a8750 100644 --- a/src/main/java/emu/nebula/server/handlers/HandlerResidentShopPurchaseReq.java +++ b/src/main/java/emu/nebula/server/handlers/HandlerResidentShopPurchaseReq.java @@ -23,17 +23,16 @@ public class HandlerResidentShopPurchaseReq extends NetHandler { } // Buy - var change = session.getPlayer().getInventory().buyItem( - data.getCurrencyItemId(), - data.getPrice(), - data.getProducts(), - req.getNumber() - ); + var change = session.getPlayer().getInventory().buyShopItem(data, req.getNumber()); + + if (change == null) { + return session.encodeMsg(NetMsgId.resident_shop_purchase_failed_ack); + } // Build response var rsp = ResidentShopPurchaseResp.newInstance() .setChange(change.toProto()) - .setPurchasedNumber(0); // Prevent avaliable item count from decreasing + .setPurchasedNumber(req.getNumber()); rsp.getMutableShop() .setId(data.getShopId()) diff --git a/src/main/java/emu/nebula/util/String2IntMap.java b/src/main/java/emu/nebula/util/String2IntMap.java new file mode 100644 index 0000000..e5ad365 --- /dev/null +++ b/src/main/java/emu/nebula/util/String2IntMap.java @@ -0,0 +1,15 @@ +package emu.nebula.util; + +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; + +public class String2IntMap extends Object2IntOpenHashMap { + private static final long serialVersionUID = -7301945177198000055L; + + public int get(String key) { + return super.getInt(key); + } + + public FastEntrySet entries() { + return super.object2IntEntrySet(); + } +}