diff --git a/data/Shop.json b/data/Shop.json new file mode 100644 index 000000000..85d6ad9e8 --- /dev/null +++ b/data/Shop.json @@ -0,0 +1,86 @@ +[ + { + "shopId": 1004, + "goodsId": 1004202, + "goodsItem": { + "Id": 202, + "Count": 1000000 + }, + "scoin": 1, + "buyLimit": 500, + "beginTime": 1575129600, + "endTime": 2051193600, + "minLevel": 1, + "maxLevel": 99 + }, + { + "shopId": 1004, + "goodsId": 10048006, + "goodsItem": { + "Id": 108006, + "Count": 20 + }, + "scoin": 1, + "buyLimit": 50000, + "beginTime": 1575129600, + "endTime": 2051193600, + "minLevel": 1, + "maxLevel": 99 + }, + { + "shopId": 1004, + "goodsId": 10048033, + "goodsItem": { + "Id": 108033, + "Count": 20 + }, + "scoin": 1, + "buyLimit": 50000, + "beginTime": 1575129600, + "endTime": 2051193600, + "minLevel": 1, + "maxLevel": 99 + }, + { + "shopId": 1004, + "goodsId": 10040008, + "goodsItem": { + "Id": 220008, + "Count": 1 + }, + "scoin": 1, + "buyLimit": 1, + "beginTime": 1575129600, + "endTime": 2051193600, + "minLevel": 1, + "maxLevel": 99 + }, + { + "shopId": 1004, + "goodsId": 10044003, + "goodsItem": { + "Id": 104003, + "Count": 200 + }, + "scoin": 1, + "buyLimit": 50000, + "beginTime": 1575129600, + "endTime": 2051193600, + "minLevel": 1, + "maxLevel": 99 + }, + { + "shopId": 1004, + "goodsId": 10044013, + "goodsItem": { + "Id": 104013, + "Count": 200 + }, + "scoin": 1, + "buyLimit": 50000, + "beginTime": 1575129600, + "endTime": 2051193600, + "minLevel": 1, + "maxLevel": 99 + } +] \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java b/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java index 26dbb903b..bad97a20a 100644 --- a/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java @@ -16,6 +16,7 @@ public final class ReloadCommand implements CommandHandler { CommandHandler.sendMessage(sender, "Reloading config."); Grasscutter.loadConfig(); Grasscutter.getGameServer().getGachaManager().load(); + Grasscutter.getGameServer().getShopManager().load(); Grasscutter.getDispatchServer().loadQueries(); CommandHandler.sendMessage(sender, "Reload complete."); } diff --git a/src/main/java/emu/grasscutter/game/inventory/Inventory.java b/src/main/java/emu/grasscutter/game/inventory/Inventory.java index bb8dfad46..d407e077b 100644 --- a/src/main/java/emu/grasscutter/game/inventory/Inventory.java +++ b/src/main/java/emu/grasscutter/game/inventory/Inventory.java @@ -109,6 +109,16 @@ public class Inventory implements Iterable { return result; } + + public boolean addItem(GameItem item, ActionReason reason, boolean forceNotify) { + boolean result = addItem(item); + + if (reason != null && (forceNotify || result)) { + getPlayer().sendPacket(new PacketItemAddHintNotify(item, reason)); + } + + return result; + } public void addItems(Collection items) { this.addItems(items, null); diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index 5970c3f13..059292497 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -25,6 +25,7 @@ import emu.grasscutter.game.inventory.Inventory; import emu.grasscutter.game.mail.Mail; import emu.grasscutter.game.props.ActionReason; import emu.grasscutter.game.props.PlayerProperty; +import emu.grasscutter.game.shop.ShopLimit; import emu.grasscutter.game.world.Scene; import emu.grasscutter.game.world.World; import emu.grasscutter.net.packet.BasePacket; @@ -84,6 +85,7 @@ public class Player { private ArrayList shownAvatars; private Set rewardedLevels; private ArrayList mail; + private ArrayList shopLimit; private int sceneId; private int regionId; @@ -141,6 +143,8 @@ public class Player { this.birthday = new PlayerBirthday(); this.rewardedLevels = new HashSet<>(); this.moonCardGetTimes = new HashSet<>(); + + this.shopLimit = new ArrayList<>(); } // On player creation @@ -592,6 +596,35 @@ public class Player { session.send(new PacketCardProductRewardNotify(getMoonCardRemainDays())); } + public List getShopLimit() { + return shopLimit; + } + + public int getGoodsLimitNum(int goodsId) { + for (ShopLimit sl : getShopLimit()) { + if (sl.getShopGoodId() == goodsId) + return sl.getHasBought(); + } + return 0; + } + + public void addShopLimit(int goodsId, int boughtCount) { + boolean found = false; + for (ShopLimit sl : getShopLimit()) { + if (sl.getShopGoodId() == goodsId){ + sl.setHasBought(sl.getHasBought() + boughtCount); + found = true; + } + } + if (!found) { + ShopLimit sl = new ShopLimit(); + sl.setShopGoodId(goodsId); + sl.setHasBought(boughtCount); + shopLimit.add(sl); + } + this.save(); + } + public boolean inGodmode() { return godmode; } diff --git a/src/main/java/emu/grasscutter/game/shop/ShopInfo.java b/src/main/java/emu/grasscutter/game/shop/ShopInfo.java index 768a3a7ca..510f0d479 100644 --- a/src/main/java/emu/grasscutter/game/shop/ShopInfo.java +++ b/src/main/java/emu/grasscutter/game/shop/ShopInfo.java @@ -1,5 +1,162 @@ package emu.grasscutter.game.shop; -public class ShopInfo { +import emu.grasscutter.data.common.ItemParamData; +import java.util.ArrayList; +import java.util.List; + +public class ShopInfo { + public int shopId = 1004; + public int goodsId = 0; + public ItemParamData goodsItem; + public int scoin = 0; + public List costItemList; + public int boughtNum = 0; + public int buyLimit = 0; + public int beginTime = 0; + public int endTime = 1924992000; + public int nextRefreshTime = 1924992000; + public int minLevel = 0; + public int maxLevel = 61; + public List preGoodsIdList = new ArrayList<>(); + public int mcoin = 0; + public int hcoin = 0; + public int disableType = 0; + public int secondarySheetId = 0; + + public int getHcoin() { + return hcoin; + } + + public void setHcoin(int hcoin) { + this.hcoin = hcoin; + } + + public List getPreGoodsIdList() { + return preGoodsIdList; + } + + public void setPreGoodsIdList(List preGoodsIdList) { + this.preGoodsIdList = preGoodsIdList; + } + + public int getMcoin() { + return mcoin; + } + + public void setMcoin(int mcoin) { + this.mcoin = mcoin; + } + + public int getDisableType() { + return disableType; + } + + public void setDisableType(int disableType) { + this.disableType = disableType; + } + + public int getSecondarySheetId() { + return secondarySheetId; + } + + public void setSecondarySheetId(int secondarySheetId) { + this.secondarySheetId = secondarySheetId; + } + + public int getShopId() { + return shopId; + } + + public void setShopId(int shopId) { + this.shopId = shopId; + } + + public int getGoodsId() { + return goodsId; + } + + public void setGoodsId(int goodsId) { + this.goodsId = goodsId; + } + + public ItemParamData getGoodsItem() { + return goodsItem; + } + + public void setGoodsItem(ItemParamData goodsItem) { + this.goodsItem = goodsItem; + } + + public int getScoin() { + return scoin; + } + + public void setScoin(int scoin) { + this.scoin = scoin; + } + + public List getCostItemList() { + return costItemList; + } + + public void setCostItemList(List costItemList) { + this.costItemList = costItemList; + } + + public int getBoughtNum() { + return boughtNum; + } + + public void setBoughtNum(int boughtNum) { + this.boughtNum = boughtNum; + } + + public int getBuyLimit() { + return buyLimit; + } + + public void setBuyLimit(int buyLimit) { + this.buyLimit = buyLimit; + } + + public int getBeginTime() { + return beginTime; + } + + public void setBeginTime(int beginTime) { + this.beginTime = beginTime; + } + + public int getEndTime() { + return endTime; + } + + public void setEndTime(int endTime) { + this.endTime = endTime; + } + + public int getNextRefreshTime() { + return nextRefreshTime; + } + + public void setNextRefreshTime(int nextRefreshTime) { + this.nextRefreshTime = nextRefreshTime; + } + + public int getMinLevel() { + return minLevel; + } + + public void setMinLevel(int minLevel) { + this.minLevel = minLevel; + } + + public int getMaxLevel() { + return maxLevel; + } + + public void setMaxLevel(int maxLevel) { + this.maxLevel = maxLevel; + } } diff --git a/src/main/java/emu/grasscutter/game/shop/ShopLimit.java b/src/main/java/emu/grasscutter/game/shop/ShopLimit.java new file mode 100644 index 000000000..ded87102e --- /dev/null +++ b/src/main/java/emu/grasscutter/game/shop/ShopLimit.java @@ -0,0 +1,25 @@ +package emu.grasscutter.game.shop; + +import dev.morphia.annotations.Entity; + +@Entity +public class ShopLimit { + public int getShopGoodId() { + return shopGoodId; + } + + public void setShopGoodId(int shopGoodId) { + this.shopGoodId = shopGoodId; + } + + public int getHasBought() { + return hasBought; + } + + public void setHasBought(int hasBought) { + this.hasBought = hasBought; + } + + private int shopGoodId; + private int hasBought; +} diff --git a/src/main/java/emu/grasscutter/game/shop/ShopManager.java b/src/main/java/emu/grasscutter/game/shop/ShopManager.java index a21888f25..e591aa65e 100644 --- a/src/main/java/emu/grasscutter/game/shop/ShopManager.java +++ b/src/main/java/emu/grasscutter/game/shop/ShopManager.java @@ -1,12 +1,50 @@ package emu.grasscutter.game.shop; +import com.google.gson.reflect.TypeToken; +import emu.grasscutter.Grasscutter; import emu.grasscutter.server.game.GameServer; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; + +import java.io.FileReader; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; public class ShopManager { private final GameServer server; + + public Int2ObjectMap> getShopData() { + return shopData; + } + + private final Int2ObjectMap> shopData; public ShopManager(GameServer server) { this.server = server; + this.shopData = new Int2ObjectOpenHashMap<>(); + this.load(); + } + + public synchronized void load() { + try (FileReader fileReader = new FileReader(Grasscutter.getConfig().DATA_FOLDER + "Shop.json")) { + getShopData().clear(); + List banners = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, ShopInfo.class).getType()); + if(banners.size() > 0) { + for (ShopInfo shopInfo : banners) { + if (!getShopData().containsKey(shopInfo.getShopId())) + getShopData().put(shopInfo.getShopId(), new ArrayList<>()); + getShopData().get(shopInfo.getShopId()).add(shopInfo); + Grasscutter.getLogger().info(String.format("Shop add: id [%d], data [%d*%d]", shopInfo.getShopId(), shopInfo.getGoodsItem().getId(), shopInfo.getGoodsItem().getCount())); + } + Grasscutter.getLogger().info("Shop data successfully loaded."); + } else { + Grasscutter.getLogger().error("Unable to load shop data. Shop data size is 0."); + } + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } } public GameServer getServer() { diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerBuyGoodsReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerBuyGoodsReq.java new file mode 100644 index 000000000..c83808ada --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerBuyGoodsReq.java @@ -0,0 +1,67 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.data.GameData; +import emu.grasscutter.game.inventory.GameItem; +import emu.grasscutter.game.props.ActionReason; +import emu.grasscutter.game.props.PlayerProperty; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.BuyGoodsReqOuterClass; +import emu.grasscutter.net.proto.ItemParamOuterClass; +import emu.grasscutter.net.proto.ShopGoodsOuterClass; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketBuyGoodsRsp; +import emu.grasscutter.server.packet.send.PacketStoreItemChangeNotify; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Optional; + +@Opcodes(PacketOpcodes.BuyGoodsReq) +public class HandlerBuyGoodsReq extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + BuyGoodsReqOuterClass.BuyGoodsReq buyGoodsReq = BuyGoodsReqOuterClass.BuyGoodsReq.parseFrom(payload); + + for (ShopGoodsOuterClass.ShopGoods sg : buyGoodsReq.getGoodsListList()) { + if (sg.getScoin() > 0 && session.getPlayer().getMora() < buyGoodsReq.getBoughtNum() * sg.getScoin()) { + return; + } + if (sg.getHcoin() > 0 && session.getPlayer().getPrimogems() < buyGoodsReq.getBoughtNum() * sg.getHcoin()) { + return; + } + if (sg.getMcoin() > 0 && session.getPlayer().getProperty(PlayerProperty.PROP_PLAYER_MCOIN) < buyGoodsReq.getBoughtNum() * sg.getMcoin()) { + return; + } + + HashMap itemsCache = new HashMap<>(); + for (ItemParamOuterClass.ItemParam p : sg.getCostItemListList()) { + Optional invItem = session.getPlayer().getInventory().getItems().values().stream().filter(x -> x.getItemId() == p.getItemId()).findFirst(); + if (invItem.isEmpty() || invItem.get().getCount() < p.getCount()) + return; + itemsCache.put(invItem.get(), p.getCount() * buyGoodsReq.getBoughtNum()); + } + + session.getPlayer().setMora(session.getPlayer().getMora() - buyGoodsReq.getBoughtNum() * sg.getScoin()); + session.getPlayer().setPrimogems(session.getPlayer().getPrimogems() - buyGoodsReq.getBoughtNum() * sg.getHcoin()); + session.getPlayer().setProperty(PlayerProperty.PROP_PLAYER_MCOIN, session.getPlayer().getProperty(PlayerProperty.PROP_PLAYER_MCOIN) - buyGoodsReq.getBoughtNum() * sg.getMcoin()); + + if (!itemsCache.isEmpty()) { + for (GameItem gi : itemsCache.keySet()) { + session.getPlayer().getInventory().removeItem(gi, itemsCache.get(gi)); + } + itemsCache.clear(); + } + + session.getPlayer().addShopLimit(sg.getGoodsId(), buyGoodsReq.getBoughtNum()); + GameItem item = new GameItem(GameData.getItemDataMap().get(sg.getGoodsItem().getItemId())); + item.setCount(buyGoodsReq.getBoughtNum() * sg.getGoodsItem().getCount()); + session.getPlayer().getInventory().addItem(item, ActionReason.Shop, true); // fix: not notify when got virtual item from shop + session.send(new PacketBuyGoodsRsp(buyGoodsReq.getShopType(), session.getPlayer().getGoodsLimitNum(sg.getGoodsId()), sg)); + } + + session.getPlayer().save(); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetShopReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetShopReq.java index 9b7c6e96d..1dbe1c3ff 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetShopReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetShopReq.java @@ -1,9 +1,9 @@ package emu.grasscutter.server.packet.recv; import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.proto.GetShopReqOuterClass.GetShopReq; -import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.packet.send.PacketGetShopRsp; @@ -12,8 +12,7 @@ public class HandlerGetShopReq extends PacketHandler { @Override public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { GetShopReq req = GetShopReq.parseFrom(payload); - - // TODO - session.send(new PacketGetShopRsp(req.getShopType())); + + session.send(new PacketGetShopRsp(session.getPlayer(), req.getShopType())); } } diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketBuyGoodsRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketBuyGoodsRsp.java new file mode 100644 index 000000000..07371440d --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketBuyGoodsRsp.java @@ -0,0 +1,22 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.BuyGoodsRspOuterClass; +import emu.grasscutter.net.proto.ShopGoodsOuterClass; + +public class PacketBuyGoodsRsp extends BasePacket { + public PacketBuyGoodsRsp(int shopType, int boughtNum, ShopGoodsOuterClass.ShopGoods sg) { + super(PacketOpcodes.BuyGoodsRsp); + + BuyGoodsRspOuterClass.BuyGoodsRsp buyGoodsRsp = BuyGoodsRspOuterClass.BuyGoodsRsp.newBuilder() + .setShopType(shopType) + .setBoughtNum(boughtNum) + .addGoodsList(ShopGoodsOuterClass.ShopGoods.newBuilder() + .mergeFrom(sg) + .setBoughtNum(boughtNum) + ).build(); + + this.setData(buyGoodsRsp); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketGetShopRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketGetShopRsp.java index 64d415763..4f57c72e6 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketGetShopRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketGetShopRsp.java @@ -1,19 +1,60 @@ package emu.grasscutter.server.packet.send; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.shop.ShopInfo; +import emu.grasscutter.game.shop.ShopManager; import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.GetShopRspOuterClass.GetShopRsp; +import emu.grasscutter.net.proto.GetShopRspOuterClass; +import emu.grasscutter.net.proto.ItemParamOuterClass; +import emu.grasscutter.net.proto.ShopGoodsOuterClass.ShopGoods; import emu.grasscutter.net.proto.ShopOuterClass.Shop; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + public class PacketGetShopRsp extends BasePacket { - - public PacketGetShopRsp(int shopType) { + + public PacketGetShopRsp(Player inv, int shopType) { super(PacketOpcodes.GetShopRsp); - GetShopRsp proto = GetShopRsp.newBuilder() - .setShop(Shop.newBuilder().setShopType(shopType)) - .build(); - - this.setData(proto); + // TODO: CityReputationLevel + Shop.Builder shop = Shop.newBuilder() + .setShopType(shopType) + .setCityId(1) //mock + .setCityReputationLevel(10); //mock + + ShopManager manager = Grasscutter.getGameServer().getShopManager(); + if (manager.getShopData().get(shopType) != null) { + List list = manager.getShopData().get(shopType); + List goodsList = new ArrayList<>(); + for (ShopInfo info : list) { + ShopGoods.Builder goods = ShopGoods.newBuilder() + .setGoodsId(info.getGoodsId()) + .setGoodsItem(ItemParamOuterClass.ItemParam.newBuilder().setItemId(info.getGoodsItem().getId()).setCount(info.getGoodsItem().getCount()).build()) + .setScoin(info.getScoin()) + .setHcoin(info.getHcoin()) + .setBoughtNum(inv.getGoodsLimitNum(info.getGoodsId())) + .setBuyLimit(info.getBuyLimit()) + .setBeginTime(info.getBeginTime()) + .setEndTime(info.getEndTime()) + .setNextRefreshTime(info.getNextRefreshTime()) + .setMinLevel(info.getMinLevel()) + .setMaxLevel(info.getMaxLevel()) + .addAllPreGoodsIdList(info.getPreGoodsIdList()) + .setMcoin(info.getMcoin()) + .setDisableType(info.getDisableType()) + .setSecondarySheetId(info.getSecondarySheetId()); + if (info.getCostItemList() != null) { + goods.addAllCostItemList(info.getCostItemList().stream().map(x -> ItemParamOuterClass.ItemParam.newBuilder().setItemId(x.getId()).setCount(x.getCount()).build()).collect(Collectors.toList())); + } + goodsList.add(goods.build()); + } + shop.addAllGoodsList(goodsList); + } + + this.setData(GetShopRspOuterClass.GetShopRsp.newBuilder().setShop(shop).build()); } }