From 275fcc7dd6e6a1b23e4e2b2c454d423454bd8ca2 Mon Sep 17 00:00:00 2001 From: Kengxxiao <11478651+Kengxxiao@users.noreply.github.com> Date: Fri, 29 Apr 2022 03:02:54 +0800 Subject: [PATCH] shop improvement --- src/main/java/emu/grasscutter/Config.java | 1 + .../commands/ResetShopLimitCommand.java | 31 ++++++++++++ .../grasscutter/data/def/ShopGoodsData.java | 28 +++++++++++ .../emu/grasscutter/game/player/Player.java | 32 ++++++------- .../emu/grasscutter/game/shop/ShopInfo.java | 48 +++++++++++++++---- .../emu/grasscutter/game/shop/ShopLimit.java | 18 +++++++ .../grasscutter/game/shop/ShopManager.java | 13 +++++ .../packet/recv/HandlerBuyGoodsReq.java | 23 ++++++++- .../server/packet/send/PacketGetShopRsp.java | 24 ++++++++-- .../java/emu/grasscutter/utils/Utils.java | 38 +++++++++++++++ 10 files changed, 224 insertions(+), 32 deletions(-) create mode 100644 src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java diff --git a/src/main/java/emu/grasscutter/Config.java b/src/main/java/emu/grasscutter/Config.java index c50a21f58..46f3bdc02 100644 --- a/src/main/java/emu/grasscutter/Config.java +++ b/src/main/java/emu/grasscutter/Config.java @@ -72,6 +72,7 @@ public final class Config { public boolean WatchGacha = false; public int[] WelcomeEmotes = {2007, 1002, 4010}; public String WelcomeMotd = "Welcome to Grasscutter emu"; + public boolean EnableOfficialShop = true; public GameRates Game = new GameRates(); diff --git a/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java b/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java new file mode 100644 index 000000000..add382b10 --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/ResetShopLimitCommand.java @@ -0,0 +1,31 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.game.player.Player; + +import java.util.List; + +@Command(label = "resetshop", usage = "resetshop", + description = "Reset target player's shop refresh time.", permission = "server.resetshop") +public final class ResetShopLimitCommand implements CommandHandler { + @Override + public void execute(Player sender, List args) { + if (args.size() < 1) { + CommandHandler.sendMessage(sender,"Usage: /resetshop "); + return; + } + + int target = Integer.parseInt(args.get(0)); + Player targetPlayer = Grasscutter.getGameServer().getPlayerByUid(target); + if (targetPlayer == null) { + CommandHandler.sendMessage(sender, "Player not found."); + return; + } + + targetPlayer.getShopLimit().forEach(x -> x.setNextRefreshTime(0)); + targetPlayer.save(); + CommandHandler.sendMessage(sender, "Success"); + } +} diff --git a/src/main/java/emu/grasscutter/data/def/ShopGoodsData.java b/src/main/java/emu/grasscutter/data/def/ShopGoodsData.java index 3ba2021f8..1a4168637 100644 --- a/src/main/java/emu/grasscutter/data/def/ShopGoodsData.java +++ b/src/main/java/emu/grasscutter/data/def/ShopGoodsData.java @@ -3,6 +3,7 @@ package emu.grasscutter.data.def; import emu.grasscutter.data.GameResource; import emu.grasscutter.data.ResourceType; import emu.grasscutter.data.common.ItemParamData; +import emu.grasscutter.game.shop.ShopInfo; import java.util.List; @@ -25,6 +26,25 @@ public class ShopGoodsData extends GameResource { private int BuyLimit; private int SubTabId; + private String RefreshType; + private transient ShopInfo.ShopRefreshType RefreshTypeEnum; + + private int RefreshParam; + + @Override + public void onLoad() { + if (this.RefreshType == null) + this.RefreshTypeEnum = ShopInfo.ShopRefreshType.NONE; + else { + this.RefreshTypeEnum = switch (this.RefreshType) { + case "SHOP_REFRESH_DAILY" -> ShopInfo.ShopRefreshType.SHOP_REFRESH_DAILY; + case "SHOP_REFRESH_WEEKLY" -> ShopInfo.ShopRefreshType.SHOP_REFRESH_WEEKLY; + case "SHOP_REFRESH_MONTHLY" -> ShopInfo.ShopRefreshType.SHOP_REFRESH_MONTHLY; + default -> ShopInfo.ShopRefreshType.NONE; + }; + } + } + @Override public int getId() { return getGoodsId(); @@ -77,4 +97,12 @@ public class ShopGoodsData extends GameResource { public int getSubTabId() { return SubTabId; } + + public ShopInfo.ShopRefreshType getRefreshType() { + return RefreshTypeEnum; + } + + public int getRefreshParam() { + return RefreshParam; + } } diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index 75c9c21ad..f440fcb1b 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -21,6 +21,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.ShopInfo; import emu.grasscutter.game.shop.ShopLimit; import emu.grasscutter.game.world.Scene; import emu.grasscutter.game.world.World; @@ -621,27 +622,26 @@ public class Player { return shopLimit; } - public int getGoodsLimitNum(int goodsId) { - for (ShopLimit sl : getShopLimit()) { - if (sl.getShopGoodId() == goodsId) - return sl.getHasBought(); - } - return 0; + public ShopLimit getGoodsLimit(int goodsId) { + Optional shopLimit = this.shopLimit.stream().filter(x -> x.getShopGoodId() == goodsId).findFirst(); + if (shopLimit.isEmpty()) + return null; + return shopLimit.get(); } - 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) { + public void addShopLimit(int goodsId, int boughtCount, int nextRefreshTime) { + ShopLimit target = getGoodsLimit(goodsId); + if (target != null) { + target.setHasBought(target.getHasBought() + boughtCount); + target.setHasBoughtInPeriod(target.getHasBoughtInPeriod() + boughtCount); + target.setNextRefreshTime(nextRefreshTime); + } else { ShopLimit sl = new ShopLimit(); sl.setShopGoodId(goodsId); sl.setHasBought(boughtCount); - shopLimit.add(sl); + sl.setHasBoughtInPeriod(boughtCount); + sl.setNextRefreshTime(nextRefreshTime); + getShopLimit().add(sl); } this.save(); } diff --git a/src/main/java/emu/grasscutter/game/shop/ShopInfo.java b/src/main/java/emu/grasscutter/game/shop/ShopInfo.java index 2b10c2005..c0a2a3cf8 100644 --- a/src/main/java/emu/grasscutter/game/shop/ShopInfo.java +++ b/src/main/java/emu/grasscutter/game/shop/ShopInfo.java @@ -15,7 +15,6 @@ public class ShopInfo { private int buyLimit = 0; private int beginTime = 0; private int endTime = 1924992000; - private int nextRefreshTime = 1924992000; private int minLevel = 0; private int maxLevel = 61; private List preGoodsIdList = new ArrayList<>(); @@ -24,6 +23,25 @@ public class ShopInfo { private int disableType = 0; private int secondarySheetId = 0; + public enum ShopRefreshType { + NONE(0), + SHOP_REFRESH_DAILY(1), + SHOP_REFRESH_WEEKLY(2), + SHOP_REFRESH_MONTHLY(3); + + private final int value; + ShopRefreshType(int value) { + this.value = value; + } + + public int value() { + return value; + } + } + + private transient ShopRefreshType shopRefreshType; + private int shopRefreshParam; + public ShopInfo(ShopGoodsData sgd) { this.goodsId = sgd.getGoodsId(); this.goodsItem = new ItemParamData(sgd.getItemId(), sgd.getItemCount()); @@ -36,6 +54,8 @@ public class ShopInfo { this.maxLevel = sgd.getMaxPlayerLevel(); this.costItemList = sgd.getCostItems().stream().filter(x -> x.getId() != 0).map(x -> new ItemParamData(x.getId(), x.getCount())).toList(); this.secondarySheetId = sgd.getSubTabId(); + this.shopRefreshType = sgd.getRefreshType(); + this.shopRefreshParam = sgd.getRefreshParam(); } public int getHcoin() { @@ -142,14 +162,6 @@ public class ShopInfo { this.endTime = endTime; } - public int getNextRefreshTime() { - return nextRefreshTime; - } - - public void setNextRefreshTime(int nextRefreshTime) { - this.nextRefreshTime = nextRefreshTime; - } - public int getMinLevel() { return minLevel; } @@ -165,4 +177,22 @@ public class ShopInfo { public void setMaxLevel(int maxLevel) { this.maxLevel = maxLevel; } + + public ShopRefreshType getShopRefreshType() { + if (shopRefreshType == null) + return ShopRefreshType.NONE; + return shopRefreshType; + } + + public void setShopRefreshType(ShopRefreshType shopRefreshType) { + this.shopRefreshType = shopRefreshType; + } + + public int getShopRefreshParam() { + return shopRefreshParam; + } + + public void setShopRefreshParam(int shopRefreshParam) { + this.shopRefreshParam = shopRefreshParam; + } } diff --git a/src/main/java/emu/grasscutter/game/shop/ShopLimit.java b/src/main/java/emu/grasscutter/game/shop/ShopLimit.java index ded87102e..296179d3f 100644 --- a/src/main/java/emu/grasscutter/game/shop/ShopLimit.java +++ b/src/main/java/emu/grasscutter/game/shop/ShopLimit.java @@ -20,6 +20,24 @@ public class ShopLimit { this.hasBought = hasBought; } + public int getNextRefreshTime() { + return nextRefreshTime; + } + + public void setNextRefreshTime(int nextRefreshTime) { + this.nextRefreshTime = nextRefreshTime; + } + + public int getHasBoughtInPeriod() { + return hasBoughtInPeriod; + } + + public void setHasBoughtInPeriod(int hasBoughtInPeriod) { + this.hasBoughtInPeriod = hasBoughtInPeriod; + } + private int shopGoodId; private int hasBought; + private int hasBoughtInPeriod = 0; + private int nextRefreshTime = 0; } diff --git a/src/main/java/emu/grasscutter/game/shop/ShopManager.java b/src/main/java/emu/grasscutter/game/shop/ShopManager.java index 4154b79bd..96dd932a1 100644 --- a/src/main/java/emu/grasscutter/game/shop/ShopManager.java +++ b/src/main/java/emu/grasscutter/game/shop/ShopManager.java @@ -8,6 +8,7 @@ import emu.grasscutter.data.def.ShopGoodsData; import emu.grasscutter.net.proto.ItemParamOuterClass; import emu.grasscutter.net.proto.ShopGoodsOuterClass; import emu.grasscutter.server.game.GameServer; +import emu.grasscutter.utils.Utils; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; @@ -32,6 +33,18 @@ public class ShopManager { this.load(); } + private static final int REFRESH_HOUR = 4; // In GMT+8 server + private static final String TIME_ZONE = "Asia/Shanghai"; // GMT+8 Timezone + + public static int getShopNextRefreshTime(ShopInfo shopInfo) { + return switch (shopInfo.getShopRefreshType()) { + case SHOP_REFRESH_DAILY -> Utils.GetNextTimestampOfThisHour(REFRESH_HOUR, TIME_ZONE, shopInfo.getShopRefreshParam()); + case SHOP_REFRESH_WEEKLY -> Utils.GetNextTimestampOfThisHourInNextWeek(REFRESH_HOUR, TIME_ZONE, shopInfo.getShopRefreshParam()); + case SHOP_REFRESH_MONTHLY -> Utils.GetNextTimestampOfThisHourInNextMonth(REFRESH_HOUR, TIME_ZONE, shopInfo.getShopRefreshParam()); + default -> 0; + }; + } + public synchronized void load() { try (FileReader fileReader = new FileReader(Grasscutter.getConfig().DATA_FOLDER + "Shop.json")) { getShopData().clear(); diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerBuyGoodsReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerBuyGoodsReq.java index 79e0f0fc3..e0257c4c8 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerBuyGoodsReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerBuyGoodsReq.java @@ -6,6 +6,8 @@ import emu.grasscutter.game.inventory.GameItem; import emu.grasscutter.game.props.ActionReason; import emu.grasscutter.game.props.PlayerProperty; import emu.grasscutter.game.shop.ShopInfo; +import emu.grasscutter.game.shop.ShopLimit; +import emu.grasscutter.game.shop.ShopManager; import emu.grasscutter.net.packet.Opcodes; import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.packet.PacketOpcodes; @@ -15,6 +17,7 @@ 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 emu.grasscutter.utils.Utils; import java.util.ArrayList; import java.util.HashMap; @@ -39,6 +42,22 @@ public class HandlerBuyGoodsReq extends PacketHandler { continue; ShopInfo sg = sg2.get(); + int currentTs = Utils.getCurrentSeconds(); + ShopLimit shopLimit = session.getPlayer().getGoodsLimit(sg.getGoodsId()); + int bought = 0; + if (shopLimit != null) { + if (currentTs > shopLimit.getNextRefreshTime()) { + shopLimit.setNextRefreshTime(ShopManager.getShopNextRefreshTime(sg)); + } else { + bought = shopLimit.getHasBoughtInPeriod(); + } + session.getPlayer().save(); + } + + if (bought + buyGoodsReq.getBoughtNum() > sg.getBuyLimit()) { + return; + } + if (sg.getScoin() > 0 && session.getPlayer().getMora() < buyGoodsReq.getBoughtNum() * sg.getScoin()) { return; } @@ -70,11 +89,11 @@ public class HandlerBuyGoodsReq extends PacketHandler { itemsCache.clear(); } - session.getPlayer().addShopLimit(sg.getGoodsId(), buyGoodsReq.getBoughtNum()); + session.getPlayer().addShopLimit(sg.getGoodsId(), buyGoodsReq.getBoughtNum(), ShopManager.getShopNextRefreshTime(sg)); GameItem item = new GameItem(GameData.getItemDataMap().get(sg.getGoodsItem().getId())); 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()), buyGoodsReq.getGoodsListList().stream().filter(x -> x.getGoodsId() == goodsId).findFirst().get())); + session.send(new PacketBuyGoodsRsp(buyGoodsReq.getShopType(), session.getPlayer().getGoodsLimit(sg.getGoodsId()).getHasBoughtInPeriod(), buyGoodsReq.getGoodsListList().stream().filter(x -> x.getGoodsId() == goodsId).findFirst().get())); } session.getPlayer().save(); 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 10e12d666..f76f3d198 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketGetShopRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketGetShopRsp.java @@ -1,10 +1,9 @@ package emu.grasscutter.server.packet.send; import emu.grasscutter.Grasscutter; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.def.ShopGoodsData; import emu.grasscutter.game.player.Player; import emu.grasscutter.game.shop.ShopInfo; +import emu.grasscutter.game.shop.ShopLimit; import emu.grasscutter.game.shop.ShopManager; import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.PacketOpcodes; @@ -12,13 +11,13 @@ 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 emu.grasscutter.utils.Utils; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; public class PacketGetShopRsp extends BasePacket { - public PacketGetShopRsp(Player inv, int shopType) { super(PacketOpcodes.GetShopRsp); @@ -38,11 +37,9 @@ public class PacketGetShopRsp extends BasePacket { .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()) .setMcoin(info.getMcoin()) @@ -54,11 +51,28 @@ public class PacketGetShopRsp extends BasePacket { if (info.getPreGoodsIdList() != null) { goods.addAllPreGoodsIdList(info.getPreGoodsIdList()); } + + int currentTs = Utils.getCurrentSeconds(); + ShopLimit currentShopLimit = inv.getGoodsLimit(info.getGoodsId()); + int nextRefreshTime = ShopManager.getShopNextRefreshTime(info); + if (currentShopLimit != null) { + if (currentShopLimit.getNextRefreshTime() < currentTs) { // second game day + currentShopLimit.setHasBoughtInPeriod(0); + currentShopLimit.setNextRefreshTime(nextRefreshTime); + } + goods.setBoughtNum(currentShopLimit.getHasBoughtInPeriod()); + goods.setNextRefreshTime(currentShopLimit.getNextRefreshTime()); + } else { + inv.addShopLimit(goods.getGoodsId(), 0, nextRefreshTime); // save generated refresh time + goods.setNextRefreshTime(nextRefreshTime); + } + goodsList.add(goods.build()); } shop.addAllGoodsList(goodsList); } + inv.save(); this.setData(GetShopRspOuterClass.GetShopRsp.newBuilder().setShop(shop).build()); } } diff --git a/src/main/java/emu/grasscutter/utils/Utils.java b/src/main/java/emu/grasscutter/utils/Utils.java index 8129a1188..fcc35b1d4 100644 --- a/src/main/java/emu/grasscutter/utils/Utils.java +++ b/src/main/java/emu/grasscutter/utils/Utils.java @@ -3,6 +3,8 @@ package emu.grasscutter.utils; import java.io.*; import java.nio.file.Files; import java.nio.file.StandardCopyOption; +import java.time.*; +import java.time.temporal.TemporalAdjusters; import java.util.Random; import emu.grasscutter.Config; @@ -191,4 +193,40 @@ public final class Utils { if(exit) System.exit(1); } + + public static int GetNextTimestampOfThisHour(int hour, String timeZone, int param) { + ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of(timeZone)); + for (int i = 0; i < param; i ++){ + if (zonedDateTime.getHour() < hour) { + zonedDateTime = zonedDateTime.withHour(hour).withMinute(0).withSecond(0); + } else { + zonedDateTime = zonedDateTime.plusDays(1).withHour(hour).withMinute(0).withSecond(0); + } + } + return (int)zonedDateTime.toInstant().atZone(ZoneOffset.UTC).toEpochSecond(); + } + + public static int GetNextTimestampOfThisHourInNextWeek(int hour, String timeZone, int param) { + ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of(timeZone)); + for (int i = 0; i < param; i++) { + if (zonedDateTime.getDayOfWeek() == DayOfWeek.MONDAY && zonedDateTime.getHour() < hour) { + zonedDateTime = ZonedDateTime.now(ZoneId.of(timeZone)).withHour(hour).withMinute(0).withSecond(0); + } else { + zonedDateTime = zonedDateTime.with(TemporalAdjusters.next(DayOfWeek.MONDAY)).withHour(hour).withMinute(0).withSecond(0); + } + } + return (int)zonedDateTime.toInstant().atZone(ZoneOffset.UTC).toEpochSecond(); + } + + public static int GetNextTimestampOfThisHourInNextMonth(int hour, String timeZone, int param) { + ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of(timeZone)); + for (int i = 0; i < param; i++) { + if (zonedDateTime.getDayOfMonth() == 1 && zonedDateTime.getHour() < hour) { + zonedDateTime = ZonedDateTime.now(ZoneId.of(timeZone)).withHour(hour).withMinute(0).withSecond(0); + } else { + zonedDateTime = zonedDateTime.with(TemporalAdjusters.firstDayOfNextMonth()).withHour(hour).withMinute(0).withSecond(0); + } + } + return (int)zonedDateTime.toInstant().atZone(ZoneOffset.UTC).toEpochSecond(); + } }