From f71b29c00a8d7894802ff8be7c415dd25aadb00a Mon Sep 17 00:00:00 2001 From: dragon <52032586+eternalcomet@users.noreply.github.com> Date: Fri, 14 Apr 2023 10:28:54 +0800 Subject: [PATCH] Implement a new item drops system (#2112) * 1 * Delete .gitattributes * implement * Update PacketDropHintNotify.java * Update PacketWorldChestOpenNotify.java * Update FileUtils.java * Update PacketDropHintNotify.java * Update WorldDataSystem.java --------- Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com> --- .gitattributes | 7 - .../command/commands/ReloadCommand.java | 1 - .../java/emu/grasscutter/data/GameData.java | 11 +- .../grasscutter/data/common/DropItemData.java | 12 + .../data/excels/DropMaterialData.java | 18 ++ .../data/excels/DropTableData.java | 25 ++ .../grasscutter/game/drop/BaseDropData.java | 11 + .../grasscutter/game/drop/ChestDropData.java | 9 + .../emu/grasscutter/game/drop/DropData.java | 1 + .../emu/grasscutter/game/drop/DropSystem.java | 302 +++++++++++++----- .../game/drop/DropSystemLegacy.java | 106 ++++++ .../game/entity/gadget/GadgetChest.java | 94 ++++-- .../grasscutter/game/inventory/Inventory.java | 2 +- .../emu/grasscutter/game/world/Scene.java | 35 +- .../game/world/WorldDataSystem.java | 3 +- .../grasscutter/scripts/data/SceneGadget.java | 18 ++ .../scripts/data/SceneMonster.java | 3 + .../grasscutter/server/game/GameServer.java | 16 +- .../packet/send/PacketDropHintNotify.java | 27 ++ .../send/PacketWorldChestOpenNotify.java | 17 + .../java/emu/grasscutter/utils/FileUtils.java | 3 +- 21 files changed, 590 insertions(+), 131 deletions(-) delete mode 100644 .gitattributes create mode 100644 src/main/java/emu/grasscutter/data/common/DropItemData.java create mode 100644 src/main/java/emu/grasscutter/data/excels/DropMaterialData.java create mode 100644 src/main/java/emu/grasscutter/data/excels/DropTableData.java create mode 100644 src/main/java/emu/grasscutter/game/drop/BaseDropData.java create mode 100644 src/main/java/emu/grasscutter/game/drop/ChestDropData.java create mode 100644 src/main/java/emu/grasscutter/game/drop/DropSystemLegacy.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketDropHintNotify.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketWorldChestOpenNotify.java diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index bd1e95beb..000000000 --- a/.gitattributes +++ /dev/null @@ -1,7 +0,0 @@ -*.java text=lf -*.json text=lf -*.md text=lf -*.properties text=lf -*.py text=lf -*.sh text=lf -* text eol=lf diff --git a/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java b/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java index e1a0c3352..752859d1f 100644 --- a/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java @@ -21,7 +21,6 @@ public final class ReloadCommand implements CommandHandler { Grasscutter.loadConfig(); Grasscutter.loadLanguage(); Grasscutter.getGameServer().getGachaSystem().load(); - Grasscutter.getGameServer().getDropSystem().load(); Grasscutter.getGameServer().getShopSystem().load(); CommandHandler.sendMessage(sender, translate(sender, "commands.reload.reload_done")); diff --git a/src/main/java/emu/grasscutter/data/GameData.java b/src/main/java/emu/grasscutter/data/GameData.java index f72576002..2a4a359df 100644 --- a/src/main/java/emu/grasscutter/data/GameData.java +++ b/src/main/java/emu/grasscutter/data/GameData.java @@ -189,25 +189,26 @@ public final class GameData { @Getter private static final Int2ObjectMap cookRecipeDataMap = - new Int2ObjectOpenHashMap<>(); + new Int2ObjectOpenHashMap<>(); @Getter private static final Int2ObjectMap compoundDataMap = new Int2ObjectOpenHashMap<>(); @Getter private static final Int2ObjectMap dailyDungeonDataMap = - new Int2ObjectOpenHashMap<>(); - + new Int2ObjectOpenHashMap<>(); + @Getter private static final Int2ObjectMap dropTableDataMap = new Int2ObjectOpenHashMap<>(); + @Getter private static final Int2ObjectMap dropMaterialDataMap = new Int2ObjectOpenHashMap<>(); @Getter private static final Int2ObjectMap dungeonDataMap = new Int2ObjectOpenHashMap<>(); @Getter private static final Int2ObjectMap dungeonEntryDataMap = - new Int2ObjectOpenHashMap<>(); + new Int2ObjectOpenHashMap<>(); @Getter private static final Int2ObjectMap envAnimalGatherConfigDataMap = - new Int2ObjectOpenHashMap<>(); + new Int2ObjectOpenHashMap<>(); @Getter private static final Int2ObjectMap equipAffixDataMap = diff --git a/src/main/java/emu/grasscutter/data/common/DropItemData.java b/src/main/java/emu/grasscutter/data/common/DropItemData.java new file mode 100644 index 000000000..6c3842350 --- /dev/null +++ b/src/main/java/emu/grasscutter/data/common/DropItemData.java @@ -0,0 +1,12 @@ +package emu.grasscutter.data.common; + +import com.google.gson.annotations.SerializedName; +import lombok.Getter; + +@Getter +public class DropItemData { + @SerializedName(value="itemId") + private int id; + private String countRange; + private int weight; +} diff --git a/src/main/java/emu/grasscutter/data/excels/DropMaterialData.java b/src/main/java/emu/grasscutter/data/excels/DropMaterialData.java new file mode 100644 index 000000000..f3cfacc8d --- /dev/null +++ b/src/main/java/emu/grasscutter/data/excels/DropMaterialData.java @@ -0,0 +1,18 @@ +package emu.grasscutter.data.excels; + +import emu.grasscutter.data.GameResource; +import emu.grasscutter.data.ResourceType; +import lombok.Getter; + +@ResourceType(name = "DropMaterialExcelConfigData.json") +@Getter +public class DropMaterialData extends GameResource { + @Getter(onMethod_ = @Override) + private int id; + private boolean useOnGain; + private boolean disableFirstGainHint; + private boolean autoPick; + private boolean dropSeparately; + private int groupId; + private boolean forceGainHint; +} diff --git a/src/main/java/emu/grasscutter/data/excels/DropTableData.java b/src/main/java/emu/grasscutter/data/excels/DropTableData.java new file mode 100644 index 000000000..31e4d7468 --- /dev/null +++ b/src/main/java/emu/grasscutter/data/excels/DropTableData.java @@ -0,0 +1,25 @@ +package emu.grasscutter.data.excels; + +import emu.grasscutter.data.GameResource; +import emu.grasscutter.data.ResourceType; +import emu.grasscutter.data.ResourceType.LoadPriority; +import emu.grasscutter.data.common.DropItemData; +import lombok.Getter; + +import java.util.List; + +@ResourceType(name ={"DropTableExcelConfigData.json","DropSubTableExcelConfigData.json"} , loadPriority = LoadPriority.HIGH) +@Getter +public class DropTableData extends GameResource { + @Getter(onMethod_ = @Override) + private int id; + private int randomType; + private int dropLevel; + private List dropVec; + private int nodeType; + private boolean fallToGround; + private int sourceType; + private int everydayLimit; + private int historyLimit; + private int activityLimit; +} diff --git a/src/main/java/emu/grasscutter/game/drop/BaseDropData.java b/src/main/java/emu/grasscutter/game/drop/BaseDropData.java new file mode 100644 index 000000000..021e2f47b --- /dev/null +++ b/src/main/java/emu/grasscutter/game/drop/BaseDropData.java @@ -0,0 +1,11 @@ +package emu.grasscutter.game.drop; + +import lombok.Getter; + +@Getter +public class BaseDropData { + private int minLevel; + private String index; + private int dropId; + private int dropCount; +} diff --git a/src/main/java/emu/grasscutter/game/drop/ChestDropData.java b/src/main/java/emu/grasscutter/game/drop/ChestDropData.java new file mode 100644 index 000000000..0ba263b70 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/drop/ChestDropData.java @@ -0,0 +1,9 @@ +package emu.grasscutter.game.drop; + +import lombok.Getter; + +@Getter +public class ChestDropData extends BaseDropData { + private int sourceType; + private String type; +} diff --git a/src/main/java/emu/grasscutter/game/drop/DropData.java b/src/main/java/emu/grasscutter/game/drop/DropData.java index 213007d94..e82681f59 100644 --- a/src/main/java/emu/grasscutter/game/drop/DropData.java +++ b/src/main/java/emu/grasscutter/game/drop/DropData.java @@ -1,5 +1,6 @@ package emu.grasscutter.game.drop; +@Deprecated public class DropData { private int minWeight; private int maxWeight; diff --git a/src/main/java/emu/grasscutter/game/drop/DropSystem.java b/src/main/java/emu/grasscutter/game/drop/DropSystem.java index 34d92beb2..144244678 100644 --- a/src/main/java/emu/grasscutter/game/drop/DropSystem.java +++ b/src/main/java/emu/grasscutter/game/drop/DropSystem.java @@ -3,109 +3,257 @@ package emu.grasscutter.game.drop; import emu.grasscutter.Grasscutter; import emu.grasscutter.data.DataLoader; import emu.grasscutter.data.GameData; -import emu.grasscutter.data.excels.ItemData; -import emu.grasscutter.game.entity.EntityItem; +import emu.grasscutter.data.common.DropItemData; +import emu.grasscutter.data.excels.DropMaterialData; +import emu.grasscutter.data.excels.DropTableData; import emu.grasscutter.game.entity.EntityMonster; +import emu.grasscutter.game.entity.GameEntity; import emu.grasscutter.game.inventory.GameItem; import emu.grasscutter.game.inventory.ItemType; import emu.grasscutter.game.player.Player; import emu.grasscutter.game.props.ActionReason; -import emu.grasscutter.game.world.Scene; +import emu.grasscutter.scripts.data.SceneMonster; import emu.grasscutter.server.game.BaseGameSystem; import emu.grasscutter.server.game.GameServer; -import emu.grasscutter.utils.Position; -import emu.grasscutter.utils.Utils; +import emu.grasscutter.server.packet.send.PacketDropHintNotify; +import emu.grasscutter.server.packet.send.PacketGadgetAutoPickDropInfoNotify; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import java.util.List; + +import java.util.*; public class DropSystem extends BaseGameSystem { - private final Int2ObjectMap> dropData; + private final Int2ObjectMap dropTable; + private final Map> chestReward; + private final Map> monsterDrop; + private final Random rand; + //TODO:don't know how to determine boss level.Have to hard-code the data from wiki. + private final int[] bossLevel = {36, 37, 41, 50, 62, 72, 83, 91, 93}; public DropSystem(GameServer server) { super(server); - this.dropData = new Int2ObjectOpenHashMap<>(); - this.load(); - } - - public Int2ObjectMap> getDropData() { - return dropData; - } - - public synchronized void load() { - getDropData().clear(); + rand = new Random(); + dropTable = GameData.getDropTableDataMap(); + chestReward = new HashMap<>(); + monsterDrop = new HashMap<>(); try { - List banners = DataLoader.loadList("Drop.json", DropInfo.class); - if (banners.size() > 0) { - for (DropInfo di : banners) { - getDropData().put(di.getMonsterId(), di.getDropDataList()); + List dataList = DataLoader.loadList("ChestDrop.json", ChestDropData.class); + for (var i : dataList) { + if (!chestReward.containsKey(i.getIndex())) { + chestReward.put(i.getIndex(), new ArrayList<>()); } - Grasscutter.getLogger().debug("Drop data successfully loaded."); - } else { - Grasscutter.getLogger().error("Unable to load drop data. Drop data size is 0."); + chestReward.get(i.getIndex()).add(i); } } catch (Exception e) { - Grasscutter.getLogger().error("Unable to load drop data.", e); + Grasscutter.getLogger().error("Unable to load chest drop data.Please place ChestDrop.json in data folder."); } - } - - private void addDropEntity( - DropData dd, Scene dropScene, ItemData itemData, Position pos, int num, Player target) { - if (!dd.isGive() - && (itemData.getItemType() != ItemType.ITEM_VIRTUAL || itemData.getGadgetId() != 0)) { - EntityItem entity = new EntityItem(dropScene, target, itemData, pos, num, dd.isShare()); - if (!dd.isShare()) dropScene.addEntityToSingleClient(target, entity); - else dropScene.addEntity(entity); - } else { - if (target != null) { - target.getInventory().addItem(new GameItem(itemData, num), ActionReason.SubfieldDrop, true); - } else { - // target is null if items will be added are shared. no one could pick it up because of the - // combination(give + shared) - // so it will be sent to all players' inventories directly. - dropScene - .getPlayers() - .forEach( - x -> - x.getInventory() - .addItem(new GameItem(itemData, num), ActionReason.SubfieldDrop, true)); - } - } - } - - private void processDrop(DropData dd, EntityMonster em, Player gp) { - int target = Utils.randomRange(1, 10000); - if (target >= dd.getMinWeight() && target < dd.getMaxWeight()) { - ItemData itemData = GameData.getItemDataMap().get(dd.getItemId()); - int num = Utils.randomRange(dd.getMinCount(), dd.getMaxCount()); - - if (itemData == null) { - return; - } - if (itemData.isEquip()) { - for (int i = 0; i < num; i++) { - float range = (2.5f + (.05f * num)); - Position pos = em.getPosition().nearby2d(range).addY(3f); - addDropEntity(dd, em.getScene(), itemData, pos, num, gp); + try { + List dataList = DataLoader.loadList("MonsterDrop.json", BaseDropData.class); + for (var i : dataList) { + if (!monsterDrop.containsKey(i.getIndex())) { + monsterDrop.put(i.getIndex(), new ArrayList<>()); } - } else { - Position pos = em.getPosition().clone().addY(3f); - addDropEntity(dd, em.getScene(), itemData, pos, num, gp); + monsterDrop.get(i.getIndex()).add(i); } + } catch (Exception e) { + Grasscutter.getLogger().error("Unable to load monster drop data.Please place MonsterDrop.json in data folder."); } } - public void callDrop(EntityMonster em) { - int id = em.getMonsterData().getId(); - if (getDropData().containsKey(id)) { - for (DropData dd : getDropData().get(id)) { - if (dd.isShare()) processDrop(dd, em, null); - else { - for (Player gp : em.getScene().getPlayers()) { - processDrop(dd, em, gp); + private int queryDropData(String dropTag, int level, Map> rewards) { + if (!rewards.containsKey(dropTag)) return 0; + var rewardList = rewards.get(dropTag); + BaseDropData dropData = null; + int minLevel = 0; + for (var i : rewardList) { + if (level >= i.getMinLevel() && i.getMinLevel() > minLevel) { + minLevel = i.getMinLevel(); + dropData = i; + } + } + if (dropData == null) return 0; + return dropData.getDropId(); + } + + public boolean handleMonsterDrop(EntityMonster monster) { + int dropId; + int level = monster.getLevel(); + SceneMonster sceneMonster = monster.getMetaMonster(); + if (sceneMonster != null) { + if (sceneMonster.drop_tag != null) { + dropId = queryDropData(sceneMonster.drop_tag, level, monsterDrop); + } else { + dropId = sceneMonster.drop_id; + } + } else { + dropId = monster.getMonsterData().getKillDropId(); + } + if (!dropTable.containsKey(dropId)) return false; + var dropData = dropTable.get(dropId); + List items = new ArrayList<>(); + processDrop(dropData, 1, items); + if (dropData.isFallToGround()) { + dropItems(items, ActionReason.MonsterDie, monster, monster.getScene().getPlayers().get(0), true); + } else { + for (Player p : monster.getScene().getPlayers()) { + p.getInventory().addItems(items, ActionReason.MonsterDie); + } + } + return true; + } + + public boolean handleChestDrop(int chestDropId, int dropCount, GameEntity bornFrom) { + if (!dropTable.containsKey(chestDropId)) return false; + var dropData = dropTable.get(chestDropId); + List items = new ArrayList<>(); + processDrop(dropData, dropCount, items); + if (dropData.isFallToGround()) { + dropItems(items, ActionReason.OpenChest, bornFrom, bornFrom.getWorld().getHost(), false); + } else { + bornFrom.getWorld().getHost().getInventory().addItems(items, ActionReason.OpenChest); + } + return true; + } + + public boolean handleChestDrop(String dropTag, int level, GameEntity bornFrom) { + int dropId = queryDropData(dropTag, level, chestReward); + if (dropId == 0) return false; + return handleChestDrop(dropId, 1, bornFrom); + } + + public boolean handleBossChestDrop(String dropTag, Player player) { + int dropId = queryDropData(dropTag, bossLevel[player.getWorldLevel()], chestReward); + if (!dropTable.containsKey(dropId)) return false; + var dropData = dropTable.get(dropId); + List items = new ArrayList<>(); + processDrop(dropData, 1, items); + player.getInventory().addItems(items, ActionReason.OpenWorldBossChest); + player.sendPacket(new PacketGadgetAutoPickDropInfoNotify(items)); + return true; + } + + private void processDrop(DropTableData dropData, int count, List items) { + //TODO:Not clear on the meaning of some fields,like "dropLevel".Will ignore them. + //TODO:solve drop limits,like everydayLimit. + if (count > 1) { + for (int i = 0; i < count; i++) processDrop(dropData, 1, items); + return; + } + if (dropData.getRandomType() == 0) { + int weightSum = 0; + for (var i : dropData.getDropVec()) { + int id = i.getId(); + if (id == 0) continue; + weightSum += i.getWeight(); + } + if (weightSum == 0) return; + int weight = rand.nextInt(weightSum); + int sum = 0; + for (var i : dropData.getDropVec()) { + int id = i.getId(); + if (id == 0) continue; + sum += i.getWeight(); + if (weight < sum) { + //win the item + int amount = calculateDropAmount(i) * count; + if (amount <= 0) break; + if (dropTable.containsKey(id)) { + processDrop(dropTable.get(id), amount, items); + } else { + boolean flag = true; + for (var j : items) { + if (j.getItemId() == id) { + j.setCount(j.getCount() + amount); + flag = false; + break; + } + } + if (flag) items.add(new GameItem(id, amount)); + } + break; + } + } + } else if (dropData.getRandomType() == 1) { + for (var i : dropData.getDropVec()) { + int id = i.getId(); + if (id == 0) continue; + if (rand.nextInt(10000) < i.getWeight()) { + int amount = calculateDropAmount(i) * count; + if (amount <= 0) continue; + if (dropTable.containsKey(id)) { + processDrop(dropTable.get(id), amount, items); + } else { + boolean flag = true; + for (var j : items) { + if (j.getItemId() == id) { + j.setCount(j.getCount() + amount); + flag = false; + break; + } + } + if (flag) items.add(new GameItem(id, amount)); } } } } } + + private int calculateDropAmount(DropItemData i) { + int amount; + if (i.getCountRange().contains(";")) { + String[] ranges = i.getCountRange().split(";"); + amount = rand.nextInt(Integer.parseInt(ranges[0]), Integer.parseInt(ranges[1]) + 1); + } else if (i.getCountRange().contains(".")) { + double expectAmount = Double.parseDouble(i.getCountRange()); + amount = (int) expectAmount; + if (rand.nextDouble() < expectAmount - amount) amount++; + } else { + amount = Integer.parseInt(i.getCountRange()); + } + return amount; + } + + /** + * @param share Whether other players in the scene could see the drop items. + */ + private void dropItem(GameItem item, ActionReason reason, Player player, GameEntity bornFrom, boolean share) { + DropMaterialData drop = GameData.getDropMaterialDataMap().get(item.getItemId()); + if ((drop != null && drop.isAutoPick()) || (item.getItemData().getItemType() == ItemType.ITEM_VIRTUAL && item.getItemData().getGadgetId() == 0)) { + giveItem(item, reason, player, share); + } else { + //TODO:solve share problem + player.getScene().addDropEntity(item, bornFrom, player, share); + } + } + + private void dropItems(List items, ActionReason reason, GameEntity bornFrom, Player player, boolean share) { + for (var i : items) { + dropItem(i, reason, player, bornFrom, share); + } + } + + private void giveItem(GameItem item, ActionReason reason, Player player, boolean share) { + if (share) { + for (var p : player.getScene().getPlayers()) { + p.getInventory().addItem(item, reason); + p.sendPacket(new PacketDropHintNotify(item.getItemId(), player.getPosition().toProto())); + } + } else { + player.getInventory().addItem(item, reason); + player.sendPacket(new PacketDropHintNotify(item.getItemId(), player.getPosition().toProto())); + } + } + + private void giveItems(List items, ActionReason reason, Player player, boolean share) { + //don't know whether we need PacketDropHintNotify. + if (share) { + for (var p : player.getScene().getPlayers()) { + p.getInventory().addItems(items, reason); + p.sendPacket(new PacketDropHintNotify(items, player.getPosition().toProto())); + } + } else { + player.getInventory().addItems(items, reason); + player.sendPacket(new PacketDropHintNotify(items, player.getPosition().toProto())); + } + } + } diff --git a/src/main/java/emu/grasscutter/game/drop/DropSystemLegacy.java b/src/main/java/emu/grasscutter/game/drop/DropSystemLegacy.java new file mode 100644 index 000000000..93c88a7b5 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/drop/DropSystemLegacy.java @@ -0,0 +1,106 @@ +package emu.grasscutter.game.drop; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.DataLoader; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.excels.ItemData; +import emu.grasscutter.game.entity.EntityItem; +import emu.grasscutter.game.entity.EntityMonster; +import emu.grasscutter.game.inventory.GameItem; +import emu.grasscutter.game.inventory.ItemType; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.ActionReason; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.server.game.BaseGameSystem; +import emu.grasscutter.server.game.GameServer; +import emu.grasscutter.utils.Position; +import emu.grasscutter.utils.Utils; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; + +import java.util.List; + +public class DropSystemLegacy extends BaseGameSystem { + private final Int2ObjectMap> dropData; + + public DropSystemLegacy(GameServer server) { + super(server); + this.dropData = new Int2ObjectOpenHashMap<>(); + this.load(); + } + + public Int2ObjectMap> getDropData() { + return dropData; + } + + public synchronized void load() { + getDropData().clear(); + try { + List banners = DataLoader.loadList("Drop.json", DropInfo.class); + if (banners.size() > 0) { + for (DropInfo di : banners) { + getDropData().put(di.getMonsterId(), di.getDropDataList()); + } + Grasscutter.getLogger().debug("Drop data successfully loaded."); + } else { + Grasscutter.getLogger().error("Unable to load drop data. Drop data size is 0."); + } + } catch (Exception e) { + Grasscutter.getLogger().error("Unable to load drop data.", e); + } + } + private void addDropEntity(DropData dd, Scene dropScene, ItemData itemData, Position pos, int num, Player target) { + if (!dd.isGive() && (itemData.getItemType() != ItemType.ITEM_VIRTUAL || itemData.getGadgetId() != 0)) { + EntityItem entity = new EntityItem(dropScene, target, itemData, pos, num, dd.isShare()); + if (!dd.isShare()) + dropScene.addEntityToSingleClient(target, entity); + else + dropScene.addEntity(entity); + } else { + if (target != null) { + target.getInventory().addItem(new GameItem(itemData, num), ActionReason.SubfieldDrop, true); + } else { + // target is null if items will be added are shared. no one could pick it up because of the combination(give + shared) + // so it will be sent to all players' inventories directly. + dropScene.getPlayers().forEach(x -> x.getInventory().addItem(new GameItem(itemData, num), ActionReason.SubfieldDrop, true)); + } + } + } + + private void processDrop(DropData dd, EntityMonster em, Player gp) { + int target = Utils.randomRange(1, 10000); + if (target >= dd.getMinWeight() && target < dd.getMaxWeight()) { + ItemData itemData = GameData.getItemDataMap().get(dd.getItemId()); + int num = Utils.randomRange(dd.getMinCount(), dd.getMaxCount()); + + if (itemData == null) { + return; + } + if (itemData.isEquip()) { + for (int i = 0; i < num; i++) { + float range = (2.5f + (.05f * num)); + Position pos = em.getPosition().nearby2d(range).addY(3f); + addDropEntity(dd, em.getScene(), itemData, pos, num, gp); + } + } else { + Position pos = em.getPosition().clone().addY(3f); + addDropEntity(dd, em.getScene(), itemData, pos, num, gp); + } + } + } + + public void callDrop(EntityMonster em) { + int id = em.getMonsterData().getId(); + if (getDropData().containsKey(id)) { + for (DropData dd : getDropData().get(id)) { + if (dd.isShare()) + processDrop(dd, em, null); + else { + for (Player gp : em.getScene().getPlayers()) { + processDrop(dd, em, gp); + } + } + } + } + } +} diff --git a/src/main/java/emu/grasscutter/game/entity/gadget/GadgetChest.java b/src/main/java/emu/grasscutter/game/entity/gadget/GadgetChest.java index 8b7041466..b881d65fe 100644 --- a/src/main/java/emu/grasscutter/game/entity/gadget/GadgetChest.java +++ b/src/main/java/emu/grasscutter/game/entity/gadget/GadgetChest.java @@ -1,9 +1,11 @@ package emu.grasscutter.game.entity.gadget; import emu.grasscutter.Grasscutter; +import emu.grasscutter.game.drop.DropSystem; import emu.grasscutter.game.entity.EntityGadget; import emu.grasscutter.game.entity.gadget.chest.BossChestInteractHandler; import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.WatcherTriggerType; import emu.grasscutter.net.proto.BossChestInfoOuterClass.BossChestInfo; import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq; import emu.grasscutter.net.proto.InterOpTypeOuterClass.InterOpType; @@ -12,7 +14,9 @@ import emu.grasscutter.net.proto.InteractTypeOuterClass.InteractType; import emu.grasscutter.net.proto.ResinCostTypeOuterClass; import emu.grasscutter.net.proto.SceneGadgetInfoOuterClass.SceneGadgetInfo; import emu.grasscutter.scripts.constants.ScriptGadgetState; +import emu.grasscutter.scripts.data.SceneGadget; import emu.grasscutter.server.packet.send.PacketGadgetInteractRsp; +import emu.grasscutter.server.packet.send.PacketWorldChestOpenNotify; public class GadgetChest extends GadgetContent { @@ -20,37 +24,69 @@ public class GadgetChest extends GadgetContent { super(gadget); } + /** + * @return Whether we should remove the gadget. + */ public boolean onInteract(Player player, GadgetInteractReq req) { - var chestInteractHandlerMap = - getGadget() - .getScene() - .getWorld() - .getServer() - .getWorldDataSystem() - .getChestInteractHandlerMap(); + //If bigWorldScript enabled,use new drop system. + if (Grasscutter.getConfig().server.game.enableScriptInBigWorld) { + SceneGadget chest = getGadget().getMetaGadget(); + DropSystem dropSystem = player.getServer().getDropSystem(); + if (chest.boss_chest != null && chest.drop_tag != null) { + //Boss chest drop + //TODO:check for blossom chests + if (req.getOpType() == InterOpType.INTER_OP_TYPE_START) { + //Two steps + player.sendPacket(new PacketGadgetInteractRsp(getGadget(), InteractType.INTERACT_TYPE_OPEN_CHEST, InterOpType.INTER_OP_TYPE_START)); + return false; + } + //TODO:check for take_num.(some boss rewards can only be claimed once a week.). Handle boss respawn. + //TODO:should return Retcode.RET_RESIN_NOT_ENOUGH ? + if (player.getResinManager().useResin(chest.boss_chest.resin) && dropSystem.handleBossChestDrop(chest.drop_tag, player)) { + //Is it correct? + player.getBattlePassManager().triggerMission(WatcherTriggerType.TRIGGER_WORLD_BOSS_REWARD, chest.boss_chest.monster_config_id, 1); + getGadget().updateState(ScriptGadgetState.ChestOpened); + player.sendPacket(new PacketGadgetInteractRsp(this.getGadget(), InteractTypeOuterClass.InteractType.INTERACT_TYPE_OPEN_CHEST, InterOpType.INTER_OP_TYPE_FINISH)); + return true; + } + //if failed,fallback to legacy drop system. + } else { + //Normal chest drop + //only the owner of the world can open chests. + if (player != player.getWorld().getHost()) return false; + boolean status = false; + if (chest.drop_tag != null) { + status = dropSystem.handleChestDrop(chest.drop_tag, chest.level, getGadget()); + } else if (chest.chest_drop_id != 0) { + status = dropSystem.handleChestDrop(chest.chest_drop_id, chest.drop_count, getGadget()); + } + if (status) { + getGadget().updateState(ScriptGadgetState.ChestOpened); + player.sendPacket(new PacketGadgetInteractRsp(getGadget(), InteractType.INTERACT_TYPE_OPEN_CHEST, InterOpType.INTER_OP_TYPE_FINISH)); + player.sendPacket(new PacketWorldChestOpenNotify(getGadget().getGroupId(), player.getSceneId(), chest.config_id)); + return true; + } + //if failed,fallback to legacy drop system. + } + Grasscutter.getLogger().warn("Can not solve chest drop: chest_drop_id = {} , drop_tag = {}.Fallback to legacy drop system.", chest.chest_drop_id, chest.drop_tag); + } + + //Legacy chest drop system + var chestInteractHandlerMap = getGadget().getScene().getWorld().getServer().getWorldDataSystem().getChestInteractHandlerMap(); var handler = chestInteractHandlerMap.get(getGadget().getGadgetData().getJsonName()); if (handler == null) { - Grasscutter.getLogger() - .warn( - "Could not found the handler of this type of Chests {}", - getGadget().getGadgetData().getJsonName()); + Grasscutter.getLogger().warn("Could not found the handler of this type of Chests {}", getGadget().getGadgetData().getJsonName()); return false; } if (req.getOpType() == InterOpType.INTER_OP_TYPE_START && handler.isTwoStep()) { - player.sendPacket( - new PacketGadgetInteractRsp( - getGadget(), InteractType.INTERACT_TYPE_OPEN_CHEST, InterOpType.INTER_OP_TYPE_START)); + player.sendPacket(new PacketGadgetInteractRsp(getGadget(), InteractType.INTERACT_TYPE_OPEN_CHEST, InterOpType.INTER_OP_TYPE_START)); return false; } else { boolean success; if (handler instanceof BossChestInteractHandler bossChestInteractHandler) { - success = - bossChestInteractHandler.onInteract( - this, - player, - req.getResinCostType() - == ResinCostTypeOuterClass.ResinCostType.RESIN_COST_TYPE_CONDENSE); + success = bossChestInteractHandler.onInteract(this, player, + req.getResinCostType() == ResinCostTypeOuterClass.ResinCostType.RESIN_COST_TYPE_CONDENSE); } else { success = handler.onInteract(this, player); } @@ -59,9 +95,7 @@ public class GadgetChest extends GadgetContent { } getGadget().updateState(ScriptGadgetState.ChestOpened); - player.sendPacket( - new PacketGadgetInteractRsp( - this.getGadget(), InteractTypeOuterClass.InteractType.INTERACT_TYPE_OPEN_CHEST)); + player.sendPacket(new PacketGadgetInteractRsp(this.getGadget(), InteractTypeOuterClass.InteractType.INTERACT_TYPE_OPEN_CHEST, InterOpType.INTER_OP_TYPE_FINISH)); return true; } @@ -76,13 +110,13 @@ public class GadgetChest extends GadgetContent { if (bossChest != null) { var players = getGadget().getScene().getPlayers().stream().map(Player::getUid).toList(); - gadgetInfo.setBossChest( - BossChestInfo.newBuilder() - .setMonsterConfigId(bossChest.monster_config_id) - .setResin(bossChest.resin) - .addAllQualifyUidList(players) - .addAllRemainUidList(players) - .build()); + gadgetInfo.setBossChest(BossChestInfo.newBuilder() + .setMonsterConfigId(bossChest.monster_config_id) + .setResin(bossChest.resin) + .addAllQualifyUidList(players) + .addAllRemainUidList(players) + .build()); } + } } diff --git a/src/main/java/emu/grasscutter/game/inventory/Inventory.java b/src/main/java/emu/grasscutter/game/inventory/Inventory.java index 79618fc29..52a60abfd 100644 --- a/src/main/java/emu/grasscutter/game/inventory/Inventory.java +++ b/src/main/java/emu/grasscutter/game/inventory/Inventory.java @@ -142,7 +142,7 @@ public class Inventory extends BasePlayerManager implements Iterable { if (item.getItemId() == 0) continue; GameItem result = null; try { - // putItem might throws exception + // putItem might throw exception // ignore that exception and continue result = putItem(item); } catch (Exception e) { diff --git a/src/main/java/emu/grasscutter/game/world/Scene.java b/src/main/java/emu/grasscutter/game/world/Scene.java index 505a388fc..b83a0365f 100644 --- a/src/main/java/emu/grasscutter/game/world/Scene.java +++ b/src/main/java/emu/grasscutter/game/world/Scene.java @@ -17,6 +17,7 @@ import emu.grasscutter.game.dungeons.challenge.WorldChallenge; import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType; import emu.grasscutter.game.entity.*; import emu.grasscutter.game.entity.gadget.GadgetWorktop; +import emu.grasscutter.game.inventory.GameItem; import emu.grasscutter.game.managers.blossom.BlossomManager; import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.TeamInfo; @@ -39,11 +40,17 @@ import emu.grasscutter.server.packet.send.*; import emu.grasscutter.utils.KahnsSort; import emu.grasscutter.utils.Position; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import lombok.Getter; +import lombok.Setter; +import lombok.val; + +import javax.annotation.Nullable; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.stream.Collectors; import javax.annotation.Nullable; + import lombok.Getter; import lombok.Setter; import lombok.val; @@ -300,12 +307,33 @@ public final class Scene { player.sendPacket(new PacketSceneEntityAppearNotify(entity)); } + public void addDropEntity(GameItem item, GameEntity bornForm, Player player, boolean share) { + // TODO:optimize EntityItem.java. Maybe we should make other players can't see + // the ItemEntity. + ItemData itemData = GameData.getItemDataMap().get(item.getItemId()); + if (itemData == null) + return; + if (itemData.isEquip()) { + float range = (1.5f + (.05f * item.getCount())); + for (int j = 0; j < item.getCount(); j++) { + Position pos = bornForm.getPosition().nearby2d(range).addY(0.5f); + EntityItem entity = new EntityItem(this, player, itemData, pos, item.getCount(), share); + addEntity(entity); + } + } else { + EntityItem entity = new EntityItem(this, player, itemData, bornForm.getPosition().clone().addY(0.5f), + item.getCount(), share); + addEntity(entity); + } + + } + public void addEntities(Collection entities) { addEntities(entities, VisionType.VISION_TYPE_BORN); } public synchronized void addEntities( - Collection entities, VisionType visionType) { + Collection entities, VisionType visionType) { if (entities == null || entities.isEmpty()) { return; } @@ -419,7 +447,10 @@ public final class Scene { // Reward drop if (target instanceof EntityMonster && this.getSceneType() != SceneType.SCENE_DUNGEON) { - getWorld().getServer().getDropSystem().callDrop((EntityMonster) target); + if (!getWorld().getServer().getDropSystem().handleMonsterDrop((EntityMonster) target)) { + Grasscutter.getLogger().warn("Can not solve monster drop: drop_id = {} , drop_tag = {}.Fallback to legacy drop system.", ((EntityMonster) target).getMetaMonster().drop_id, ((EntityMonster) target).getMetaMonster().drop_tag); + getWorld().getServer().getDropSystemLegacy().callDrop((EntityMonster) target); + } } // Remove entity from world diff --git a/src/main/java/emu/grasscutter/game/world/WorldDataSystem.java b/src/main/java/emu/grasscutter/game/world/WorldDataSystem.java index 9cdcf7958..010122df1 100644 --- a/src/main/java/emu/grasscutter/game/world/WorldDataSystem.java +++ b/src/main/java/emu/grasscutter/game/world/WorldDataSystem.java @@ -57,6 +57,7 @@ public class WorldDataSystem extends BaseGameSystem { return chestInteractHandlerMap; } + @Deprecated public RewardPreviewData getRewardByBossId(int monsterId) { var investigationMonsterData = GameData.getInvestigationMonsterDataMap().values().parallelStream() @@ -92,7 +93,7 @@ public class WorldDataSystem extends BaseGameSystem { WorldLevelData worldLevelData = GameData.getWorldLevelDataMap().get(world.getWorldLevel()); if (worldLevelData != null) { - level = worldLevelData.getMonsterLevel(); + level = Math.max(level, worldLevelData.getMonsterLevel()); } return level; } diff --git a/src/main/java/emu/grasscutter/scripts/data/SceneGadget.java b/src/main/java/emu/grasscutter/scripts/data/SceneGadget.java index d299db196..46ba562e9 100644 --- a/src/main/java/emu/grasscutter/scripts/data/SceneGadget.java +++ b/src/main/java/emu/grasscutter/scripts/data/SceneGadget.java @@ -6,17 +6,35 @@ import lombok.ToString; @ToString @Setter public class SceneGadget extends SceneObject { + public int config_id; public int gadget_id; + public int level; + public int chest_drop_id; + public int drop_count; + public String drop_tag; + boolean showcutscene; + boolean persistence; public int state; + public int point_type; + public int owner; public SceneBossChest boss_chest; public int interact_id; + /** + * Note: this field indicates whether the gadget should disappear permanently. + * For example, if isOneOff=true, like most chests, it will disappear permanently after interacted. + * If isOneOff=false, like investigation points, it will disappear temporarily, and appear again in next big world resource refresh routine. + */ public boolean isOneoff; + public int area_id; public int draft_id; public int route_id; public boolean start_route = true; public boolean is_use_point_array = false; public boolean persistent = false; + public int mark_flag; + public Explore explore; + public int trigger_count; public void setIsOneoff(boolean isOneoff) { this.isOneoff = isOneoff; diff --git a/src/main/java/emu/grasscutter/scripts/data/SceneMonster.java b/src/main/java/emu/grasscutter/scripts/data/SceneMonster.java index 4ee0a55ba..87c8913ea 100644 --- a/src/main/java/emu/grasscutter/scripts/data/SceneMonster.java +++ b/src/main/java/emu/grasscutter/scripts/data/SceneMonster.java @@ -6,10 +6,13 @@ import lombok.ToString; @ToString @Setter public class SceneMonster extends SceneObject { + public int config_id; public int monster_id; public int pose_id; + public int level; public int drop_id; public boolean disableWander; public int title_id; public int special_name_id; + public String drop_tag; } diff --git a/src/main/java/emu/grasscutter/server/game/GameServer.java b/src/main/java/emu/grasscutter/server/game/GameServer.java index 397d2d918..c69d1bf77 100644 --- a/src/main/java/emu/grasscutter/server/game/GameServer.java +++ b/src/main/java/emu/grasscutter/server/game/GameServer.java @@ -1,8 +1,5 @@ package emu.grasscutter.server.game; -import static emu.grasscutter.config.Configuration.GAME_INFO; -import static emu.grasscutter.utils.Language.translate; - import emu.grasscutter.GameConstants; import emu.grasscutter.Grasscutter; import emu.grasscutter.database.DatabaseHelper; @@ -12,6 +9,7 @@ import emu.grasscutter.game.chat.ChatSystem; import emu.grasscutter.game.chat.ChatSystemHandler; import emu.grasscutter.game.combine.CombineManger; import emu.grasscutter.game.drop.DropSystem; +import emu.grasscutter.game.drop.DropSystemLegacy; import emu.grasscutter.game.dungeons.DungeonSystem; import emu.grasscutter.game.expedition.ExpeditionSystem; import emu.grasscutter.game.gacha.GachaSystem; @@ -36,14 +34,18 @@ import emu.grasscutter.server.event.internal.ServerStopEvent; import emu.grasscutter.server.event.types.ServerEvent; import emu.grasscutter.server.scheduler.ServerTaskScheduler; import emu.grasscutter.task.TaskMap; +import kcp.highway.ChannelConfig; +import kcp.highway.KcpServer; +import lombok.Getter; + import java.net.InetSocketAddress; import java.time.Instant; import java.time.OffsetDateTime; import java.util.*; import java.util.concurrent.ConcurrentHashMap; -import kcp.highway.ChannelConfig; -import kcp.highway.KcpServer; -import lombok.Getter; + +import static emu.grasscutter.config.Configuration.GAME_INFO; +import static emu.grasscutter.utils.Language.translate; @Getter public final class GameServer extends KcpServer { @@ -61,6 +63,7 @@ public final class GameServer extends KcpServer { private final DungeonSystem dungeonSystem; private final ExpeditionSystem expeditionSystem; private final DropSystem dropSystem; + private final DropSystemLegacy dropSystemLegacy; private final WorldDataSystem worldDataSystem; private final BattlePassSystem battlePassSystem; private final CombineManger combineSystem; @@ -113,6 +116,7 @@ public final class GameServer extends KcpServer { this.multiplayerSystem = new MultiplayerSystem(this); this.dungeonSystem = new DungeonSystem(this); this.dropSystem = new DropSystem(this); + this.dropSystemLegacy = new DropSystemLegacy(this); this.expeditionSystem = new ExpeditionSystem(this); this.combineSystem = new CombineManger(this); this.towerSystem = new TowerSystem(this); diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketDropHintNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketDropHintNotify.java new file mode 100644 index 000000000..f72be0df6 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketDropHintNotify.java @@ -0,0 +1,27 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.game.inventory.GameItem; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.DropHintNotifyOuterClass.DropHintNotify; +import emu.grasscutter.net.proto.VectorOuterClass.Vector; + +public class PacketDropHintNotify extends BasePacket { + public PacketDropHintNotify(int itemId, Vector position) { + super(PacketOpcodes.DropHintNotify); + + var proto = DropHintNotify.newBuilder() + .addItemIdList(itemId) + .setPosition(position); + this.setData(proto.build()); + } + + public PacketDropHintNotify(Iterable items, Vector position) { + super(PacketOpcodes.DropHintNotify); + + var proto = DropHintNotify.newBuilder(); + items.forEach(i -> proto.addItemIdList(i.getItemId())); + proto.setPosition(position); + this.setData(proto.build()); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketWorldChestOpenNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketWorldChestOpenNotify.java new file mode 100644 index 000000000..6b9ae3403 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketWorldChestOpenNotify.java @@ -0,0 +1,17 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.WorldChestOpenNotifyOuterClass.WorldChestOpenNotify; + +public class PacketWorldChestOpenNotify extends BasePacket { + public PacketWorldChestOpenNotify(int groupId,int sceneId,int configId){ + super(PacketOpcodes.WorldChestOpenNotify); + + this.setData(WorldChestOpenNotify.newBuilder() + .setGroupId(groupId) + .setSceneId(sceneId) + .setConfigId(configId) + .build()); + } +} diff --git a/src/main/java/emu/grasscutter/utils/FileUtils.java b/src/main/java/emu/grasscutter/utils/FileUtils.java index e15782ba4..9a6ca895f 100644 --- a/src/main/java/emu/grasscutter/utils/FileUtils.java +++ b/src/main/java/emu/grasscutter/utils/FileUtils.java @@ -151,7 +151,8 @@ public final class FileUtils { } public static Path getExcelPath(String filename) { - return getTsjJsonTsv(RESOURCES_PATH.resolve("ExcelBinOutput"), filename); + Path p = getTsjJsonTsv(RESOURCES_PATH.resolve("Server"), filename); + return Files.exists(p) ? p : getTsjJsonTsv(RESOURCES_PATH.resolve("ExcelBinOutput"), filename); } // Gets path of a resource.