Implement group-based item giving & Add content handler for item giving

This commit is contained in:
KingRainbow44 2023-08-13 00:32:02 -04:00
parent afc5841596
commit 40bbfd90e1
No known key found for this signature in database
GPG Key ID: FC2CB64B00D257BE
12 changed files with 371 additions and 138 deletions

View File

@ -3,6 +3,7 @@ package emu.grasscutter;
public final class DebugConstants { public final class DebugConstants {
public static boolean LOG_ABILITIES = false; public static boolean LOG_ABILITIES = false;
public static boolean LOG_LUA_SCRIPTS = false; public static boolean LOG_LUA_SCRIPTS = false;
public static boolean LOG_QUEST_START = false;
private DebugConstants() { private DebugConstants() {
// Prevent instantiation. // Prevent instantiation.

View File

@ -30,11 +30,13 @@ public final class GivingData extends GameResource {
private GiveType giveType; private GiveType giveType;
public enum GiveMethod { public enum GiveMethod {
@SerializedName("GIVING_METHOD_NONE") NONE, GIVING_METHOD_NONE,
@SerializedName("GIVING_METHOD_EXACT") EXACT, /** All items are required to succeed. */
@SerializedName("GIVING_METHOD_GROUP") GROUP, GIVING_METHOD_EXACT,
@SerializedName("GIVING_METHOD_VAGUE_GROUP") GROUP_VAGUE, GIVING_METHOD_GROUP,
@SerializedName("GIVING_METHOD_ANY_NO_FINISH") ANY /** One in the group is required to succeed. */
GIVING_METHOD_VAGUE_GROUP,
GIVING_METHOD_ANY_NO_FINISH
} }
public enum GiveType { public enum GiveType {

View File

@ -59,6 +59,19 @@ public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
this.getInventoryTypes().put(type.getValue(), tab); 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) { public GameItem getItemByGuid(long id) {
return this.getItems().get(id); return this.getItems().get(id);
} }
@ -155,25 +168,25 @@ public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
* Checks to see if the player has the item in their inventory. * 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. * 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. * @param minCount The minimum count of the item to check for.
* @return True if the player has the item, false otherwise. * @return True if the player has the item, false otherwise.
*/ */
public boolean hasItem(long itemGuid, int minCount) { public boolean hasItem(int itemId, int minCount) {
return hasItem(itemGuid, minCount, false); return hasItem(itemId, minCount, false);
} }
/** /**
* Checks to see if the player has the item in their inventory. * 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 count The count of the item to check for.
* @param enforce If true, the player must have the exact amount. * @param enforce If true, the player must have the exact amount.
* If false, the player must have at least the amount. * If false, the player must have at least the amount.
* @return True if the player has the item, false otherwise. * @return True if the player has the item, false otherwise.
*/ */
public boolean hasItem(long itemGuid, int count, boolean enforce) { public boolean hasItem(int itemId, int count, boolean enforce) {
var item = this.getItemByGuid(itemGuid); var item = this.getFirstItem(itemId);
if (item == null) return false; if (item == null) return false;
return enforce ? return enforce ?
@ -188,9 +201,9 @@ public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
* @param items A map of item game IDs to their count. * @param items A map of item game IDs to their count.
* @return True if the player has the items, false otherwise. * @return True if the player has the items, false otherwise.
*/ */
public boolean hasAllItems(Map<Long, Integer> items) { public boolean hasAllItems(Collection<ItemParam> items) {
for (var item : items.entrySet()) { for (var item : items) {
if (!this.hasItem(item.getKey(), item.getValue(), true)) if (!this.hasItem(item.getItemId(), item.getCount(), true))
return false; return false;
} }
@ -486,9 +499,9 @@ public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
* *
* @param items A map of item game IDs to the amount of items to remove. * @param items A map of item game IDs to the amount of items to remove.
*/ */
public void removeItems(Map<Long, Integer> items) { public void removeItems(Collection<ItemParam> items) {
for (var entry : items.entrySet()) { for (var entry : items) {
this.removeItem(entry.getKey(), entry.getValue()); this.removeItem(entry.getItemId(), entry.getCount());
} }
} }
@ -496,6 +509,25 @@ public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
return removeItem(guid, 1); 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) { public synchronized boolean removeItem(long guid, int count) {
var item = this.getItemByGuid(guid); var item = this.getItemByGuid(guid);
@ -514,10 +546,14 @@ public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
* @return True if the item was removed, false otherwise. * @return True if the item was removed, false otherwise.
*/ */
public synchronized boolean removeItemById(int itemId, int count) { 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. // 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) { public synchronized boolean removeItem(GameItem item) {

View File

@ -1,40 +1,40 @@
package emu.grasscutter.game.player; package emu.grasscutter.game.player;
import dev.morphia.annotations.Entity; import dev.morphia.annotations.*;
import dev.morphia.annotations.Transient;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.quest.ItemGiveRecord;
import emu.grasscutter.game.quest.enums.QuestContent; import emu.grasscutter.game.quest.enums.QuestContent;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.*;
import it.unimi.dsi.fastutil.ints.IntArrayList; import lombok.*;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; 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 */ /** Tracks progress the player made in the world, like obtained items, seen characters and more */
@Getter
@Entity @Entity
public class PlayerProgress { public class PlayerProgress {
@Getter @Setter @Transient private Player player; @Setter @Transient private Player player;
private Map<Integer, ItemEntry> itemHistory;
@Getter private Map<Integer, ItemEntry> itemHistory;
/* /*
* A list of dungeon IDs which have been completed. * A list of dungeon IDs which have been completed.
* This only applies to one-time dungeons. * 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 // 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 // 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 // it will be hard to loop and compare
private Map<String, Integer> questProgressCountMap; private Map<String, Integer> questProgressCountMap;
private Map<Integer, ItemGiveRecord> itemGivings;
public PlayerProgress() { public PlayerProgress() {
this.questProgressCountMap = new ConcurrentHashMap<>(); this.questProgressCountMap = new ConcurrentHashMap<>();
this.completedDungeons = new IntArrayList(); this.completedDungeons = new IntArrayList();
this.itemHistory = new Int2ObjectOpenHashMap<>(); this.itemHistory = new Int2ObjectOpenHashMap<>();
this.itemGivings = new Int2ObjectOpenHashMap<>();
} }
/** /**

View File

@ -1,7 +1,7 @@
package emu.grasscutter.game.quest; package emu.grasscutter.game.quest;
import dev.morphia.annotations.*; import dev.morphia.annotations.*;
import emu.grasscutter.Grasscutter; import emu.grasscutter.*;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.*; import emu.grasscutter.data.excels.*;
import emu.grasscutter.data.excels.quest.QuestData; 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.server.packet.send.*;
import emu.grasscutter.utils.Utils; import emu.grasscutter.utils.Utils;
import it.unimi.dsi.fastutil.ints.IntIntImmutablePair; import it.unimi.dsi.fastutil.ints.IntIntImmutablePair;
import java.util.*;
import javax.script.Bindings;
import lombok.*; import lombok.*;
import javax.script.Bindings;
import java.util.*;
@Entity @Entity
public class GameQuest { public class GameQuest {
@Transient @Getter @Setter private GameMainQuest mainQuest; @Transient @Getter @Setter private GameMainQuest mainQuest;
@ -104,7 +105,10 @@ public class GameQuest {
.forEach(e -> getOwner().getServer().getQuestSystem().triggerExec(this, e, e.getParam())); .forEach(e -> getOwner().getServer().getQuestSystem().triggerExec(this, e, e.getParam()));
this.getOwner().getQuestManager().checkQuestAlreadyFulfilled(this); this.getOwner().getQuestManager().checkQuestAlreadyFulfilled(this);
if (DebugConstants.LOG_QUEST_START) {
Grasscutter.getLogger().debug("Quest {} is started", subQuestId); Grasscutter.getLogger().debug("Quest {} is started", subQuestId);
}
this.save(); this.save();
} }

View File

@ -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<Integer, Integer>();
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<Integer, Integer> 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();
}
}

View File

@ -2,33 +2,26 @@ package emu.grasscutter.game.quest;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.binout.MainQuestData; import emu.grasscutter.data.binout.*;
import emu.grasscutter.data.binout.ScenePointEntry;
import emu.grasscutter.data.excels.quest.QuestData; import emu.grasscutter.data.excels.quest.QuestData;
import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.player.BasePlayerManager; import emu.grasscutter.game.player.*;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.quest.enums.*; 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.game.world.Position;
import emu.grasscutter.net.proto.GivingRecordOuterClass.GivingRecord;
import emu.grasscutter.server.packet.send.*;
import io.netty.util.concurrent.FastThreadLocalThread; import io.netty.util.concurrent.FastThreadLocalThread;
import it.unimi.dsi.fastutil.ints.*; import it.unimi.dsi.fastutil.ints.*;
import lombok.Getter; import lombok.*;
import lombok.val;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import java.util.*; import java.util.*;
import java.util.concurrent.ExecutorService; import java.util.concurrent.*;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static emu.grasscutter.GameConstants.DEBUG; import static emu.grasscutter.GameConstants.DEBUG;
import static emu.grasscutter.config.Configuration.GAME_OPTIONS; import static emu.grasscutter.config.Configuration.*;
import static emu.grasscutter.config.Configuration.SERVER;
public final class QuestManager extends BasePlayerManager { public final class QuestManager extends BasePlayerManager {
@Getter private final Player player; @Getter private final Player player;
@ -36,52 +29,13 @@ public final class QuestManager extends BasePlayerManager {
@Getter private final Int2ObjectMap<GameMainQuest> mainQuests; @Getter private final Int2ObjectMap<GameMainQuest> mainQuests;
@Getter private final List<Integer> loggedQuests; @Getter private final List<Integer> loggedQuests;
@Getter private final List<Integer> activeGivings = new ArrayList<>();
private long lastHourCheck = 0; private long lastHourCheck = 0;
private long lastDayCheck = 0; private long lastDayCheck = 0;
public static final ExecutorService eventExecutor; public static final ExecutorService eventExecutor
static { = new ThreadPoolExecutor(4, 4,
eventExecutor = new ThreadPoolExecutor(4, 4,
60, TimeUnit.SECONDS, new LinkedBlockingDeque<>(1000), 60, TimeUnit.SECONDS, new LinkedBlockingDeque<>(1000),
FastThreadLocalThread::new, new ThreadPoolExecutor.AbortPolicy()); 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<Integer> 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<Integer> 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 long getQuestKey(int mainQuestId) { public static long getQuestKey(int mainQuestId) {
QuestEncryptionKey questEncryptionKey = GameData.getMainQuestEncryptionMap().get(mainQuestId); QuestEncryptionKey questEncryptionKey = GameData.getMainQuestEncryptionMap().get(mainQuestId);
@ -137,20 +91,99 @@ public final class QuestManager extends BasePlayerManager {
return GAME_OPTIONS.questing.enabled; return GAME_OPTIONS.questing.enabled;
} }
public void onPlayerBorn() { /**
// TODO scan the quest and start the quest with acceptCond fulfilled * Attempts to add the giving action.
// The off send 3 request in that order: *
// 1. FinishedParentQuestNotify * @param givingId The giving action ID.
// 2. QuestListNotify * @throws IllegalStateException If the giving action is already active.
// 3. ServerCondMeetQuestListUpdateNotify */
public void addGiveItemAction(int givingId)
throws IllegalStateException {
var progress = this.player.getPlayerProgress();
var givings = progress.getItemGivings();
if (this.isQuestingEnabled()) { // Check if the action is already present.
this.enableQuests(); if (givings.containsKey(givingId)) {
throw new IllegalStateException("Giving action " + givingId + " is already active.");
} }
// this.getPlayer().sendPacket(new PacketFinishedParentQuestUpdateNotify(newQuests)); // Add the action.
// this.getPlayer().sendPacket(new PacketQuestListNotify(subQuests)); givings.put(givingId, ItemGiveRecord.resolve(givingId));
// this.getPlayer().sendPacket(new PacketServerCondMeetQuestListUpdateNotify(newPlayerServerCondMeetQuestListUpdateNotify)); // 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<GivingRecord> 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() { public void onLogin() {

View File

@ -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();
}
}

View File

@ -10,17 +10,20 @@ import emu.grasscutter.game.quest.handlers.QuestExecHandler;
public final class ExecActiveItemGiving extends QuestExecHandler { public final class ExecActiveItemGiving extends QuestExecHandler {
@Override @Override
public boolean execute(GameQuest quest, QuestExecParam condition, String... paramStr) { public boolean execute(GameQuest quest, QuestExecParam condition, String... paramStr) {
var questManager = quest.getOwner().getQuestManager(); var player = quest.getOwner();
var activeGivings = questManager.getActiveGivings(); var questManager = player.getQuestManager();
var givingId = Integer.parseInt(condition.getParam()[0]); var givingId = Integer.parseInt(condition.getParam()[0]);
if (activeGivings.contains(givingId)) { try {
Grasscutter.getLogger().debug("Quest {} attempted to add give action {} twice.", 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); quest.getSubQuestId(), givingId);
return false; return false;
} else {
activeGivings.add(givingId);
return true;
} }
} }
} }

View File

@ -11,16 +11,15 @@ public final class ExecDeactivateItemGiving extends QuestExecHandler {
@Override @Override
public boolean execute(GameQuest quest, QuestData.QuestExecParam condition, String... paramStr) { public boolean execute(GameQuest quest, QuestData.QuestExecParam condition, String... paramStr) {
var questManager = quest.getOwner().getQuestManager(); var questManager = quest.getOwner().getQuestManager();
var activeGivings = questManager.getActiveGivings();
var givingId = Integer.parseInt(condition.getParam()[0]); var givingId = Integer.parseInt(condition.getParam()[0]);
if (!activeGivings.contains(givingId)) { try {
Grasscutter.getLogger().debug("Quest {} attempted to remove give action {} when it isn't active.", questManager.removeGivingItemAction(givingId);
return true;
} catch (IllegalStateException ignored) {
Grasscutter.getLogger().warn("Quest {} attempted to remove give action {} twice.",
quest.getSubQuestId(), givingId); quest.getSubQuestId(), givingId);
return false; return false;
} else {
activeGivings.remove(givingId);
return true;
} }
} }
} }

View File

@ -2,7 +2,6 @@ package emu.grasscutter.server.packet.recv;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.giving.GivingData.GiveMethod;
import emu.grasscutter.game.quest.enums.QuestContent; import emu.grasscutter.game.quest.enums.QuestContent;
import emu.grasscutter.net.packet.*; import emu.grasscutter.net.packet.*;
import emu.grasscutter.net.proto.ItemGivingReqOuterClass.ItemGivingReq; 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;
import emu.grasscutter.server.packet.send.PacketItemGivingRsp.Mode; import emu.grasscutter.server.packet.send.PacketItemGivingRsp.Mode;
import java.util.*;
@Opcodes(PacketOpcodes.ItemGivingReq) @Opcodes(PacketOpcodes.ItemGivingReq)
public final class HandlerItemGivingReq extends PacketHandler { public final class HandlerItemGivingReq extends PacketHandler {
@Override @Override
@ -20,19 +21,21 @@ public final class HandlerItemGivingReq extends PacketHandler {
var inventory = player.getInventory(); var inventory = player.getInventory();
var giveId = req.getGivingId(); var giveId = req.getGivingId();
var items = req.getItemGuidCountMapMap(); var items = req.getItemParamListList();
switch (req.getItemGivingType()) { switch (req.getItemGivingType()) {
case QUEST -> { case QUEST -> {
var questManager = player.getQuestManager(); var questManager = player.getQuestManager();
var activeGivings = questManager.getActiveGivings(); var activeGivings = player.getPlayerProgress().getItemGivings();
if (!activeGivings.contains(giveId)) return; if (!activeGivings.containsKey(giveId)) return;
// Check the items against the resources. // Check the items against the resources.
var data = GameData.getGivingDataMap().get(giveId); var data = GameData.getGivingDataMap().get(giveId);
if (data == null) throw new IllegalArgumentException("No giving data found for " + giveId + "."); if (data == null) throw new IllegalArgumentException("No giving data found for " + giveId + ".");
if (data.getGivingMethod() == GiveMethod.EXACT) { switch (data.getGivingMethod()) {
case GIVING_METHOD_EXACT -> {
// Check if the player has all the items.
if (!inventory.hasAllItems(items)) { if (!inventory.hasAllItems(items)) {
player.sendPacket(new PacketItemGivingRsp()); player.sendPacket(new PacketItemGivingRsp());
return; return;
@ -46,12 +49,55 @@ public final class HandlerItemGivingReq extends PacketHandler {
// Send the response packet. // Send the response packet.
player.sendPacket(new PacketItemGivingRsp(giveId, Mode.EXACT_SUCCESS)); player.sendPacket(new PacketItemGivingRsp(giveId, Mode.EXACT_SUCCESS));
// Remove the action from the active givings. // Remove the action from the active givings.
activeGivings.remove(giveId); questManager.removeGivingItemAction(giveId);
// Queue the content action. // Queue the content action.
questManager.queueEvent(QuestContent.QUEST_CONTENT_FINISH_ITEM_GIVING, giveId); questManager.queueEvent(QuestContent.QUEST_CONTENT_FINISH_ITEM_GIVING, giveId, 0);
} else { }
// TODO: Handle group givings. case GIVING_METHOD_VAGUE_GROUP -> {
var matchedGroups = new ArrayList<Integer>();
var givenItems = new HashMap<Integer, Integer>();
// 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()); 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 -> { case GADGET -> {

View File

@ -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<GivingRecord> records) {
super(PacketOpcodes.GivingRecordNotify);
this.setData(GivingRecordNotify.newBuilder()
.addAllGivingRecordList(records)
.build());
}
}