From 40bbfd90e1c94d8916c2d1e056b878d50f225aa8 Mon Sep 17 00:00:00 2001 From: KingRainbow44 Date: Sun, 13 Aug 2023 00:32:02 -0400 Subject: [PATCH] Implement group-based item giving & Add content handler for item giving --- .../java/emu/grasscutter/DebugConstants.java | 1 + .../data/excels/giving/GivingData.java | 12 +- .../grasscutter/game/inventory/Inventory.java | 64 +++++-- .../game/player/PlayerProgress.java | 24 +-- .../emu/grasscutter/game/quest/GameQuest.java | 12 +- .../game/quest/ItemGiveRecord.java | 75 ++++++++ .../grasscutter/game/quest/QuestManager.java | 169 +++++++++++------- .../content/ContentFinishGivingItem.java | 17 ++ .../game/quest/exec/ExecActiveItemGiving.java | 17 +- .../quest/exec/ExecDeactivateItemGiving.java | 11 +- .../packet/recv/HandlerItemGivingReq.java | 90 +++++++--- .../packet/send/PacketGivingRecordNotify.java | 17 ++ 12 files changed, 371 insertions(+), 138 deletions(-) create mode 100644 src/main/java/emu/grasscutter/game/quest/ItemGiveRecord.java create mode 100644 src/main/java/emu/grasscutter/game/quest/content/ContentFinishGivingItem.java create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketGivingRecordNotify.java diff --git a/src/main/java/emu/grasscutter/DebugConstants.java b/src/main/java/emu/grasscutter/DebugConstants.java index c496ab053..647e58bf7 100644 --- a/src/main/java/emu/grasscutter/DebugConstants.java +++ b/src/main/java/emu/grasscutter/DebugConstants.java @@ -3,6 +3,7 @@ package emu.grasscutter; public final class DebugConstants { public static boolean LOG_ABILITIES = false; public static boolean LOG_LUA_SCRIPTS = false; + public static boolean LOG_QUEST_START = false; private DebugConstants() { // Prevent instantiation. diff --git a/src/main/java/emu/grasscutter/data/excels/giving/GivingData.java b/src/main/java/emu/grasscutter/data/excels/giving/GivingData.java index ba42760dd..560be60e6 100644 --- a/src/main/java/emu/grasscutter/data/excels/giving/GivingData.java +++ b/src/main/java/emu/grasscutter/data/excels/giving/GivingData.java @@ -30,11 +30,13 @@ public final class GivingData extends GameResource { private GiveType giveType; public enum GiveMethod { - @SerializedName("GIVING_METHOD_NONE") NONE, - @SerializedName("GIVING_METHOD_EXACT") EXACT, - @SerializedName("GIVING_METHOD_GROUP") GROUP, - @SerializedName("GIVING_METHOD_VAGUE_GROUP") GROUP_VAGUE, - @SerializedName("GIVING_METHOD_ANY_NO_FINISH") ANY + GIVING_METHOD_NONE, + /** All items are required to succeed. */ + GIVING_METHOD_EXACT, + GIVING_METHOD_GROUP, + /** One in the group is required to succeed. */ + GIVING_METHOD_VAGUE_GROUP, + GIVING_METHOD_ANY_NO_FINISH } public enum GiveType { diff --git a/src/main/java/emu/grasscutter/game/inventory/Inventory.java b/src/main/java/emu/grasscutter/game/inventory/Inventory.java index a49ee4081..d467ae586 100644 --- a/src/main/java/emu/grasscutter/game/inventory/Inventory.java +++ b/src/main/java/emu/grasscutter/game/inventory/Inventory.java @@ -59,6 +59,19 @@ public class Inventory extends BasePlayerManager implements Iterable { this.getInventoryTypes().put(type.getValue(), tab); } + /** + * Finds the first item in the inventory with the given item id. + * + * @param itemId The item id to search for. + * @return The first item found with the given item id, or null if no item was + */ + public GameItem getFirstItem(int itemId) { + return this.getItems().values().stream() + .filter(item -> item.getItemId() == itemId) + .findFirst() + .orElse(null); + } + public GameItem getItemByGuid(long id) { return this.getItems().get(id); } @@ -155,25 +168,25 @@ public class Inventory extends BasePlayerManager implements Iterable { * Checks to see if the player has the item in their inventory. * This will succeed if the player has at least the minimum count of the item. * - * @param itemGuid The item id to check for. + * @param itemId The item id to check for. * @param minCount The minimum count of the item to check for. * @return True if the player has the item, false otherwise. */ - public boolean hasItem(long itemGuid, int minCount) { - return hasItem(itemGuid, minCount, false); + public boolean hasItem(int itemId, int minCount) { + return hasItem(itemId, minCount, false); } /** * Checks to see if the player has the item in their inventory. * - * @param itemGuid The item id to check for. + * @param itemId The item id to check for. * @param count The count of the item to check for. * @param enforce If true, the player must have the exact amount. * If false, the player must have at least the amount. * @return True if the player has the item, false otherwise. */ - public boolean hasItem(long itemGuid, int count, boolean enforce) { - var item = this.getItemByGuid(itemGuid); + public boolean hasItem(int itemId, int count, boolean enforce) { + var item = this.getFirstItem(itemId); if (item == null) return false; return enforce ? @@ -188,9 +201,9 @@ public class Inventory extends BasePlayerManager implements Iterable { * @param items A map of item game IDs to their count. * @return True if the player has the items, false otherwise. */ - public boolean hasAllItems(Map items) { - for (var item : items.entrySet()) { - if (!this.hasItem(item.getKey(), item.getValue(), true)) + public boolean hasAllItems(Collection items) { + for (var item : items) { + if (!this.hasItem(item.getItemId(), item.getCount(), true)) return false; } @@ -486,9 +499,9 @@ public class Inventory extends BasePlayerManager implements Iterable { * * @param items A map of item game IDs to the amount of items to remove. */ - public void removeItems(Map items) { - for (var entry : items.entrySet()) { - this.removeItem(entry.getKey(), entry.getValue()); + public void removeItems(Collection items) { + for (var entry : items) { + this.removeItem(entry.getItemId(), entry.getCount()); } } @@ -496,6 +509,25 @@ public class Inventory extends BasePlayerManager implements Iterable { return removeItem(guid, 1); } + /** + * Removes an item from the player's inventory. + * This uses the item ID to find the first stack of the item's type. + * + * @param itemId The ID of the item to remove. + * @param count The amount of items to remove. + * @return True if the item was removed, false otherwise. + */ + public synchronized boolean removeItem(int itemId, int count) { + var item = this.getItems().values().stream() + .filter(i -> i.getItemId() == itemId) + .findFirst(); + + // Check if the item is in the player's inventory. + return item + .filter(gameItem -> this.removeItem(gameItem, count)) + .isPresent(); + } + public synchronized boolean removeItem(long guid, int count) { var item = this.getItemByGuid(guid); @@ -514,10 +546,14 @@ public class Inventory extends BasePlayerManager implements Iterable { * @return True if the item was removed, false otherwise. */ public synchronized boolean removeItemById(int itemId, int count) { - var item = this.getItems().values().stream().filter(i -> i.getItemId() == itemId).findFirst(); + var item = this.getItems().values().stream() + .filter(i -> i.getItemId() == itemId) + .findFirst(); // Check if the item is in the player's inventory. - return item.filter(gameItem -> this.removeItem(gameItem, count)).isPresent(); + return item + .filter(gameItem -> this.removeItem(gameItem, count)) + .isPresent(); } public synchronized boolean removeItem(GameItem item) { diff --git a/src/main/java/emu/grasscutter/game/player/PlayerProgress.java b/src/main/java/emu/grasscutter/game/player/PlayerProgress.java index 5ac575f5a..0c34eaedb 100644 --- a/src/main/java/emu/grasscutter/game/player/PlayerProgress.java +++ b/src/main/java/emu/grasscutter/game/player/PlayerProgress.java @@ -1,40 +1,40 @@ package emu.grasscutter.game.player; -import dev.morphia.annotations.Entity; -import dev.morphia.annotations.Transient; +import dev.morphia.annotations.*; import emu.grasscutter.Grasscutter; +import emu.grasscutter.game.quest.ItemGiveRecord; import emu.grasscutter.game.quest.enums.QuestContent; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.*; +import lombok.*; + import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.val; /** Tracks progress the player made in the world, like obtained items, seen characters and more */ +@Getter @Entity public class PlayerProgress { - @Getter @Setter @Transient private Player player; - - @Getter private Map itemHistory; + @Setter @Transient private Player player; + private Map itemHistory; /* * A list of dungeon IDs which have been completed. * This only applies to one-time dungeons. */ - @Getter private IntArrayList completedDungeons; + private IntArrayList completedDungeons; // keep track of EXEC_ADD_QUEST_PROGRESS count, will be used in CONTENT_ADD_QUEST_PROGRESS // not sure where to put this, this should be saved to DB but not to individual quest, since // it will be hard to loop and compare private Map questProgressCountMap; + private Map itemGivings; + public PlayerProgress() { this.questProgressCountMap = new ConcurrentHashMap<>(); this.completedDungeons = new IntArrayList(); this.itemHistory = new Int2ObjectOpenHashMap<>(); + this.itemGivings = new Int2ObjectOpenHashMap<>(); } /** diff --git a/src/main/java/emu/grasscutter/game/quest/GameQuest.java b/src/main/java/emu/grasscutter/game/quest/GameQuest.java index 904a7b404..364b4449d 100644 --- a/src/main/java/emu/grasscutter/game/quest/GameQuest.java +++ b/src/main/java/emu/grasscutter/game/quest/GameQuest.java @@ -1,7 +1,7 @@ package emu.grasscutter.game.quest; import dev.morphia.annotations.*; -import emu.grasscutter.Grasscutter; +import emu.grasscutter.*; import emu.grasscutter.data.GameData; import emu.grasscutter.data.excels.*; import emu.grasscutter.data.excels.quest.QuestData; @@ -15,10 +15,11 @@ import emu.grasscutter.scripts.data.SceneGroup; import emu.grasscutter.server.packet.send.*; import emu.grasscutter.utils.Utils; import it.unimi.dsi.fastutil.ints.IntIntImmutablePair; -import java.util.*; -import javax.script.Bindings; import lombok.*; +import javax.script.Bindings; +import java.util.*; + @Entity public class GameQuest { @Transient @Getter @Setter private GameMainQuest mainQuest; @@ -104,7 +105,10 @@ public class GameQuest { .forEach(e -> getOwner().getServer().getQuestSystem().triggerExec(this, e, e.getParam())); this.getOwner().getQuestManager().checkQuestAlreadyFulfilled(this); - Grasscutter.getLogger().debug("Quest {} is started", subQuestId); + if (DebugConstants.LOG_QUEST_START) { + Grasscutter.getLogger().debug("Quest {} is started", subQuestId); + } + this.save(); } diff --git a/src/main/java/emu/grasscutter/game/quest/ItemGiveRecord.java b/src/main/java/emu/grasscutter/game/quest/ItemGiveRecord.java new file mode 100644 index 000000000..1000eac82 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/quest/ItemGiveRecord.java @@ -0,0 +1,75 @@ +package emu.grasscutter.game.quest; + +import dev.morphia.annotations.Entity; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.excels.giving.GivingData.GiveMethod; +import emu.grasscutter.net.proto.GivingRecordOuterClass.GivingRecord; +import lombok.*; + +import java.util.*; + +@Data +@Entity +@Builder +public final class ItemGiveRecord { + /** + * Provides a builder for an item give record. + * Uses information from game resources. + * + * @param givingId The ID of the giving action. + * @return A builder for an item give record. + */ + public static ItemGiveRecord resolve(int givingId) { + var givingData = GameData.getGivingDataMap().get(givingId); + if (givingData == null) throw new RuntimeException("No giving data found for " + givingId + "."); + + var builder = ItemGiveRecord.builder() + .givingId(givingId) + .finished(false); + + // Create a map. + var givenItems = new HashMap(); + if (givingData.getGivingMethod() == GiveMethod.GIVING_METHOD_EXACT) { + givingData.getExactItems().forEach(item -> + givenItems.put(item.getItemId(), 0)); + } else { + givingData.getGivingGroupIds().forEach(groupId -> { + var groupData = GameData.getGivingGroupDataMap().get((int) groupId); + if (groupData == null) return; + + // Add all items in the group. + groupData.getItemIds().forEach(itemId -> + givenItems.put(itemId, 0)); + builder.groupId(groupId); + }); + } + + return builder + .givenItems(givenItems) + .build(); + } + + private int givingId; + private int configId; + + private int groupId; + private int lastGroupId; + + private boolean finished; + private Map givenItems; + + /** + * @return A serialized protobuf object. + */ + public GivingRecord toProto() { + return GivingRecord.newBuilder() + .setGivingId(this.getGivingId()) + .setConfigId(this.getConfigId()) + .setGroupId(this.getGroupId()) + .setLastGroupId(this.getLastGroupId()) + .setIsFinished(this.isFinished()) + .setIsGadgetGiving(false) + .putAllMaterialCntMap(this.getGivenItems()) + .build(); + } +} diff --git a/src/main/java/emu/grasscutter/game/quest/QuestManager.java b/src/main/java/emu/grasscutter/game/quest/QuestManager.java index ca37d9f8a..b382b1436 100644 --- a/src/main/java/emu/grasscutter/game/quest/QuestManager.java +++ b/src/main/java/emu/grasscutter/game/quest/QuestManager.java @@ -2,33 +2,26 @@ package emu.grasscutter.game.quest; import emu.grasscutter.Grasscutter; import emu.grasscutter.data.GameData; -import emu.grasscutter.data.binout.MainQuestData; -import emu.grasscutter.data.binout.ScenePointEntry; +import emu.grasscutter.data.binout.*; import emu.grasscutter.data.excels.quest.QuestData; import emu.grasscutter.database.DatabaseHelper; -import emu.grasscutter.game.player.BasePlayerManager; -import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.player.*; import emu.grasscutter.game.quest.enums.*; -import emu.grasscutter.server.packet.send.PacketFinishedParentQuestUpdateNotify; -import emu.grasscutter.server.packet.send.PacketQuestGlobalVarNotify; import emu.grasscutter.game.world.Position; +import emu.grasscutter.net.proto.GivingRecordOuterClass.GivingRecord; +import emu.grasscutter.server.packet.send.*; import io.netty.util.concurrent.FastThreadLocalThread; import it.unimi.dsi.fastutil.ints.*; -import lombok.Getter; -import lombok.val; +import lombok.*; import javax.annotation.Nonnull; import java.util.*; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.LinkedBlockingDeque; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; import java.util.function.Consumer; import java.util.stream.Collectors; import static emu.grasscutter.GameConstants.DEBUG; -import static emu.grasscutter.config.Configuration.GAME_OPTIONS; -import static emu.grasscutter.config.Configuration.SERVER; +import static emu.grasscutter.config.Configuration.*; public final class QuestManager extends BasePlayerManager { @Getter private final Player player; @@ -36,52 +29,13 @@ public final class QuestManager extends BasePlayerManager { @Getter private final Int2ObjectMap mainQuests; @Getter private final List loggedQuests; - @Getter private final List activeGivings = new ArrayList<>(); - private long lastHourCheck = 0; private long lastDayCheck = 0; - public static final ExecutorService eventExecutor; - static { - eventExecutor = new ThreadPoolExecutor(4, 4, - 60, TimeUnit.SECONDS, new LinkedBlockingDeque<>(1000), - FastThreadLocalThread::new, new ThreadPoolExecutor.AbortPolicy()); - } - - /* - On SetPlayerBornDataReq, the server sends FinishedParentQuestNotify, with this exact - parentQuestList. Captured on Game version 2.7 - Note: quest 40063 is already set to finished, with childQuest 4006406's state set to 3 - */ - - private static Set newPlayerMainQuests = Set.of(303,318,348,349,350,351,416,500, - 501,502,503,504,505,506,507,508,509,20000,20507,20509,21004,21005,21010,21011,21016,21017, - 21020,21021,21025,40063,70121,70124,70511,71010,71012,71013,71015,71016,71017,71555); - - /* - On SetPlayerBornDataReq, the server sends ServerCondMeetQuestListUpdateNotify, with this exact - addQuestIdList. Captured on Game version 2.7 - Total of 161... - */ - /* - private static Set newPlayerServerCondMeetQuestListUpdateNotify = Set.of(3100101, 7104405, 2201601, - 7100801, 1907002, 7293301, 7193801, 7293401, 7193901, 7091001, 7190501, 7090901, 7190401, 7090801, 7190301, - 7195301, 7294801, 7195201, 7293001, 7094001, 7193501, 7293501, 7194001, 7293701, 7194201, 7194301, 7293801, - 7194901, 7194101, 7195001, 7294501, 7294101, 7194601, 7294301, 7194801, 7091301, 7290301, 2102401, 7216801, - 7190201, 7090701, 7093801, 7193301, 7292801, 7227828, 7093901, 7193401, 7292901, 7093701, 7193201, 7292701, - 7082402, 7093601, 7292601, 7193101, 2102301, 7093501, 7292501, 7193001, 7093401, 7292401, 7192901, 7093301, - 7292301, 7192801, 7294201, 7194701, 2100301, 7093201, 7212402, 7292201, 7192701, 7280001, 7293901, 7194401, - 7093101, 7212302, 7292101, 7192601, 7093001, 7292001, 7192501, 7216001, 7195101, 7294601, 2100900, 7092901, - 7291901, 7192401, 7092801, 7291801, 7192301, 2101501, 7092701, 7291701, 7192201, 7106401, 2100716, 7091801, - 7290801, 7191301, 7293201, 7193701, 7094201, 7294001, 7194501, 2102290, 7227829, 7193601, 7094101, 7091401, - 7290401, 7190901, 7106605, 7291601, 7192101, 7092601, 7291501, 7192001, 7092501, 7291401, 7191901, 7092401, - 7291301, 7191801, 7092301, 7211402, 7291201, 7191701, 7092201, 7291101, 7191601, 7092101, 7291001, 7191501, - 7092001, 7290901, 7191401, 7091901, 7290701, 7191201, 7091701, 7290601, 7191101, 7091601, 7290501, 7191001, - 7091501, 7290201, 7190701, 7091201, 7190601, 7091101, 7190101, 7090601, 7090501, 7090401, 7010701, 7090301, - 7090201, 7010103, 7090101 - ); - - */ + public static final ExecutorService eventExecutor + = new ThreadPoolExecutor(4, 4, + 60, TimeUnit.SECONDS, new LinkedBlockingDeque<>(1000), + FastThreadLocalThread::new, new ThreadPoolExecutor.AbortPolicy()); public static long getQuestKey(int mainQuestId) { QuestEncryptionKey questEncryptionKey = GameData.getMainQuestEncryptionMap().get(mainQuestId); @@ -137,20 +91,99 @@ public final class QuestManager extends BasePlayerManager { return GAME_OPTIONS.questing.enabled; } - public void onPlayerBorn() { - // TODO scan the quest and start the quest with acceptCond fulfilled - // The off send 3 request in that order: - // 1. FinishedParentQuestNotify - // 2. QuestListNotify - // 3. ServerCondMeetQuestListUpdateNotify + /** + * Attempts to add the giving action. + * + * @param givingId The giving action ID. + * @throws IllegalStateException If the giving action is already active. + */ + public void addGiveItemAction(int givingId) + throws IllegalStateException { + var progress = this.player.getPlayerProgress(); + var givings = progress.getItemGivings(); - if (this.isQuestingEnabled()) { - this.enableQuests(); + // Check if the action is already present. + if (givings.containsKey(givingId)) { + throw new IllegalStateException("Giving action " + givingId + " is already active."); } - // this.getPlayer().sendPacket(new PacketFinishedParentQuestUpdateNotify(newQuests)); - // this.getPlayer().sendPacket(new PacketQuestListNotify(subQuests)); - // this.getPlayer().sendPacket(new PacketServerCondMeetQuestListUpdateNotify(newPlayerServerCondMeetQuestListUpdateNotify)); + // Add the action. + givings.put(givingId, ItemGiveRecord.resolve(givingId)); + // Save the givings. + player.save(); + + this.sendGivingRecords(); + } + + /** + * Marks a giving action as completed. + * + * @param givingId The giving action ID. + */ + public void markCompleted(int givingId) { + var progress = this.player.getPlayerProgress(); + var givings = progress.getItemGivings(); + + // Check if the action is already present. + if (!givings.containsKey(givingId)) { + throw new IllegalStateException("Giving action " + givingId + " is not active."); + } + + // Mark the action as finished. + givings.get(givingId).setFinished(true); + // Save the givings. + player.save(); + + this.sendGivingRecords(); + } + + /** + * Attempts to remove the giving action. + * + * @param givingId The giving action ID. + */ + public void removeGivingItemAction(int givingId) { + var progress = this.player.getPlayerProgress(); + var givings = progress.getItemGivings(); + + // Check if the action is already present. + if (!givings.containsKey(givingId)) { + throw new IllegalStateException("Giving action " + givingId + " is not active."); + } + + // Remove the action. + givings.remove(givingId); + // Save the givings. + player.save(); + + this.sendGivingRecords(); + } + + /** + * @return Serialized giving records to be used in a packet. + */ + public Collection getGivingRecords() { + return this.getPlayer().getPlayerProgress() + .getItemGivings().values().stream() + .map(ItemGiveRecord::toProto) + .toList(); + } + + /** + * Attempts to remove the giving action. + */ + public void sendGivingRecords() { + // Send the record to the player. + this.player.sendPacket( + new PacketGivingRecordNotify( + this.getGivingRecords())); + } + + public void onPlayerBorn() { + if (this.isQuestingEnabled()) { + this.enableQuests(); + this.sendGivingRecords(); + } } public void onLogin() { diff --git a/src/main/java/emu/grasscutter/game/quest/content/ContentFinishGivingItem.java b/src/main/java/emu/grasscutter/game/quest/content/ContentFinishGivingItem.java new file mode 100644 index 000000000..88f2a7491 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/quest/content/ContentFinishGivingItem.java @@ -0,0 +1,17 @@ +package emu.grasscutter.game.quest.content; + +import emu.grasscutter.data.excels.quest.QuestData; +import emu.grasscutter.game.quest.*; +import emu.grasscutter.game.quest.enums.QuestContent; + +@QuestValueContent(QuestContent.QUEST_CONTENT_FINISH_ITEM_GIVING) +public final class ContentFinishGivingItem extends BaseContent { + @Override + public boolean execute(GameQuest quest, QuestData.QuestContentCondition condition, String paramStr, int... params) { + var giveAction = quest.getOwner() + .getPlayerProgress() + .getItemGivings() + .get(condition.getParam()[0]); + return giveAction != null && giveAction.isFinished(); + } +} diff --git a/src/main/java/emu/grasscutter/game/quest/exec/ExecActiveItemGiving.java b/src/main/java/emu/grasscutter/game/quest/exec/ExecActiveItemGiving.java index 6b426aa30..c2f073967 100644 --- a/src/main/java/emu/grasscutter/game/quest/exec/ExecActiveItemGiving.java +++ b/src/main/java/emu/grasscutter/game/quest/exec/ExecActiveItemGiving.java @@ -10,17 +10,20 @@ import emu.grasscutter.game.quest.handlers.QuestExecHandler; public final class ExecActiveItemGiving extends QuestExecHandler { @Override public boolean execute(GameQuest quest, QuestExecParam condition, String... paramStr) { - var questManager = quest.getOwner().getQuestManager(); - var activeGivings = questManager.getActiveGivings(); + var player = quest.getOwner(); + var questManager = player.getQuestManager(); var givingId = Integer.parseInt(condition.getParam()[0]); - if (activeGivings.contains(givingId)) { - Grasscutter.getLogger().debug("Quest {} attempted to add give action {} twice.", + try { + questManager.addGiveItemAction(givingId); + + Grasscutter.getLogger().debug("Quest {} added give action {}.", + quest.getSubQuestId(), givingId); + return true; + } catch (IllegalStateException ignored) { + Grasscutter.getLogger().warn("Quest {} attempted to add give action {} twice.", quest.getSubQuestId(), givingId); return false; - } else { - activeGivings.add(givingId); - return true; } } } diff --git a/src/main/java/emu/grasscutter/game/quest/exec/ExecDeactivateItemGiving.java b/src/main/java/emu/grasscutter/game/quest/exec/ExecDeactivateItemGiving.java index 2298a6570..9f9f8d5ba 100644 --- a/src/main/java/emu/grasscutter/game/quest/exec/ExecDeactivateItemGiving.java +++ b/src/main/java/emu/grasscutter/game/quest/exec/ExecDeactivateItemGiving.java @@ -11,16 +11,15 @@ public final class ExecDeactivateItemGiving extends QuestExecHandler { @Override public boolean execute(GameQuest quest, QuestData.QuestExecParam condition, String... paramStr) { var questManager = quest.getOwner().getQuestManager(); - var activeGivings = questManager.getActiveGivings(); var givingId = Integer.parseInt(condition.getParam()[0]); - if (!activeGivings.contains(givingId)) { - Grasscutter.getLogger().debug("Quest {} attempted to remove give action {} when it isn't active.", + try { + questManager.removeGivingItemAction(givingId); + return true; + } catch (IllegalStateException ignored) { + Grasscutter.getLogger().warn("Quest {} attempted to remove give action {} twice.", quest.getSubQuestId(), givingId); return false; - } else { - activeGivings.remove(givingId); - return true; } } } diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerItemGivingReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerItemGivingReq.java index 8c9deac7f..d80846dda 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerItemGivingReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerItemGivingReq.java @@ -2,7 +2,6 @@ package emu.grasscutter.server.packet.recv; import emu.grasscutter.Grasscutter; import emu.grasscutter.data.GameData; -import emu.grasscutter.data.excels.giving.GivingData.GiveMethod; import emu.grasscutter.game.quest.enums.QuestContent; import emu.grasscutter.net.packet.*; import emu.grasscutter.net.proto.ItemGivingReqOuterClass.ItemGivingReq; @@ -10,6 +9,8 @@ import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.packet.send.PacketItemGivingRsp; import emu.grasscutter.server.packet.send.PacketItemGivingRsp.Mode; +import java.util.*; + @Opcodes(PacketOpcodes.ItemGivingReq) public final class HandlerItemGivingReq extends PacketHandler { @Override @@ -20,38 +21,83 @@ public final class HandlerItemGivingReq extends PacketHandler { var inventory = player.getInventory(); var giveId = req.getGivingId(); - var items = req.getItemGuidCountMapMap(); + var items = req.getItemParamListList(); switch (req.getItemGivingType()) { case QUEST -> { var questManager = player.getQuestManager(); - var activeGivings = questManager.getActiveGivings(); - if (!activeGivings.contains(giveId)) return; + var activeGivings = player.getPlayerProgress().getItemGivings(); + if (!activeGivings.containsKey(giveId)) return; // Check the items against the resources. var data = GameData.getGivingDataMap().get(giveId); if (data == null) throw new IllegalArgumentException("No giving data found for " + giveId + "."); - if (data.getGivingMethod() == GiveMethod.EXACT) { - if (!inventory.hasAllItems(items)) { - player.sendPacket(new PacketItemGivingRsp()); - return; - } + switch (data.getGivingMethod()) { + case GIVING_METHOD_EXACT -> { + // Check if the player has all the items. + if (!inventory.hasAllItems(items)) { + player.sendPacket(new PacketItemGivingRsp()); + return; + } - // Remove the items if the quest specifies. - if (data.isRemoveItem()) { - inventory.removeItems(items); - } + // Remove the items if the quest specifies. + if (data.isRemoveItem()) { + inventory.removeItems(items); + } - // Send the response packet. - player.sendPacket(new PacketItemGivingRsp(giveId, Mode.EXACT_SUCCESS)); - // Remove the action from the active givings. - activeGivings.remove(giveId); - // Queue the content action. - questManager.queueEvent(QuestContent.QUEST_CONTENT_FINISH_ITEM_GIVING, giveId); - } else { - // TODO: Handle group givings. - player.sendPacket(new PacketItemGivingRsp()); + // Send the response packet. + player.sendPacket(new PacketItemGivingRsp(giveId, Mode.EXACT_SUCCESS)); + // Remove the action from the active givings. + questManager.removeGivingItemAction(giveId); + // Queue the content action. + questManager.queueEvent(QuestContent.QUEST_CONTENT_FINISH_ITEM_GIVING, giveId, 0); + } + case GIVING_METHOD_VAGUE_GROUP -> { + var matchedGroups = new ArrayList(); + var givenItems = new HashMap(); + + // Resolve potential item IDs. + var groupData = GameData.getGivingGroupDataMap(); + data.getGivingGroupIds().stream() + .map(groupId -> groupData.get((int) groupId)) + .filter(Objects::nonNull) + .forEach(group -> { + var itemIds = group.getItemIds(); + + // Match item stacks to the group items. + items.forEach(param -> { + // Get the item instance. + var itemInstance = inventory.getFirstItem(param.getItemId()); + if (itemInstance == null) return; + + // Get the item ID. + var itemId = itemInstance.getItemId(); + if (!itemIds.contains(itemId)) return; + + // Add the item to the given items. + givenItems.put(itemId, param.getCount()); + matchedGroups.add(group.getId()); + }); + }); + + // Check if the player has any items. + if (givenItems.isEmpty() && matchedGroups.isEmpty()) { + player.sendPacket(new PacketItemGivingRsp()); + } else { + // Remove the items if the quest specifies. + if (data.isRemoveItem()) { + inventory.removeItems(items); + } + + // Send the response packet. + player.sendPacket(new PacketItemGivingRsp(matchedGroups.get(0), Mode.GROUP_SUCCESS)); + // Mark the giving action as completed. + questManager.markCompleted(giveId); + // Queue the content action. + questManager.queueEvent(QuestContent.QUEST_CONTENT_FINISH_ITEM_GIVING, giveId, 0); + } + } } } case GADGET -> { diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketGivingRecordNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketGivingRecordNotify.java new file mode 100644 index 000000000..ffbe29e3c --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketGivingRecordNotify.java @@ -0,0 +1,17 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.*; +import emu.grasscutter.net.proto.GivingRecordNotifyOuterClass.GivingRecordNotify; +import emu.grasscutter.net.proto.GivingRecordOuterClass.GivingRecord; + +import java.util.Collection; + +public final class PacketGivingRecordNotify extends BasePacket { + public PacketGivingRecordNotify(Collection records) { + super(PacketOpcodes.GivingRecordNotify); + + this.setData(GivingRecordNotify.newBuilder() + .addAllGivingRecordList(records) + .build()); + } +}