Merge branch 'talking' into dev-4.0

This commit is contained in:
KingRainbow44 2023-08-15 20:46:38 -04:00
commit 70eb0f2b16
No known key found for this signature in database
GPG Key ID: FC2CB64B00D257BE
38 changed files with 1213 additions and 164 deletions

View File

@ -0,0 +1,11 @@
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.
}
}

View File

@ -4,58 +4,33 @@ import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.binout.*;
import emu.grasscutter.data.binout.config.*;
import emu.grasscutter.data.binout.routes.Route;
import emu.grasscutter.data.custom.TrialAvatarActivityCustomData;
import emu.grasscutter.data.custom.TrialAvatarCustomData;
import emu.grasscutter.data.custom.*;
import emu.grasscutter.data.excels.*;
import emu.grasscutter.data.excels.achievement.AchievementData;
import emu.grasscutter.data.excels.achievement.AchievementGoalData;
import emu.grasscutter.data.excels.activity.ActivityCondExcelConfigData;
import emu.grasscutter.data.excels.activity.ActivityData;
import emu.grasscutter.data.excels.activity.ActivityShopData;
import emu.grasscutter.data.excels.activity.ActivityWatcherData;
import emu.grasscutter.data.excels.achievement.*;
import emu.grasscutter.data.excels.activity.*;
import emu.grasscutter.data.excels.avatar.*;
import emu.grasscutter.data.excels.codex.*;
import emu.grasscutter.data.excels.dungeon.*;
import emu.grasscutter.data.excels.monster.MonsterCurveData;
import emu.grasscutter.data.excels.monster.MonsterData;
import emu.grasscutter.data.excels.monster.MonsterDescribeData;
import emu.grasscutter.data.excels.monster.MonsterSpecialNameData;
import emu.grasscutter.data.excels.quest.QuestData;
import emu.grasscutter.data.excels.quest.QuestGlobalVarData;
import emu.grasscutter.data.excels.reliquary.ReliquaryAffixData;
import emu.grasscutter.data.excels.reliquary.ReliquaryLevelData;
import emu.grasscutter.data.excels.reliquary.ReliquaryMainPropData;
import emu.grasscutter.data.excels.reliquary.ReliquarySetData;
import emu.grasscutter.data.excels.tower.TowerFloorData;
import emu.grasscutter.data.excels.tower.TowerLevelData;
import emu.grasscutter.data.excels.tower.TowerScheduleData;
import emu.grasscutter.data.excels.giving.*;
import emu.grasscutter.data.excels.monster.*;
import emu.grasscutter.data.excels.quest.*;
import emu.grasscutter.data.excels.reliquary.*;
import emu.grasscutter.data.excels.tower.*;
import emu.grasscutter.data.excels.trial.*;
import emu.grasscutter.data.excels.weapon.WeaponCurveData;
import emu.grasscutter.data.excels.weapon.WeaponLevelData;
import emu.grasscutter.data.excels.weapon.WeaponPromoteData;
import emu.grasscutter.data.excels.world.WeatherData;
import emu.grasscutter.data.excels.world.WorldAreaData;
import emu.grasscutter.data.excels.world.WorldLevelData;
import emu.grasscutter.data.server.ActivityCondGroup;
import emu.grasscutter.data.server.DropSubfieldMapping;
import emu.grasscutter.data.server.DropTableExcelConfigData;
import emu.grasscutter.data.server.GadgetMapping;
import emu.grasscutter.data.server.MonsterMapping;
import emu.grasscutter.data.server.SubfieldMapping;
import emu.grasscutter.data.excels.weapon.*;
import emu.grasscutter.data.excels.world.*;
import emu.grasscutter.data.server.*;
import emu.grasscutter.game.dungeons.DungeonDropEntry;
import emu.grasscutter.game.quest.QuestEncryptionKey;
import emu.grasscutter.game.quest.RewindData;
import emu.grasscutter.game.quest.TeleportData;
import emu.grasscutter.game.quest.*;
import emu.grasscutter.game.quest.enums.QuestCond;
import emu.grasscutter.game.world.GroupReplacementData;
import emu.grasscutter.utils.Utils;
import it.unimi.dsi.fastutil.ints.*;
import lombok.*;
import javax.annotation.Nullable;
import java.lang.reflect.Field;
import java.util.*;
import javax.annotation.Nullable;
import lombok.Getter;
import lombok.Setter;
import lombok.val;
@SuppressWarnings({"unused", "MismatchedQueryAndUpdateOfCollection"})
public final class GameData {
@ -165,6 +140,10 @@ public final class GameData {
private static final Int2ObjectMap<AvatarTalentData> avatarTalentDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<BargainData> bargainDataMap
= new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<BattlePassMissionData> battlePassMissionDataMap =
new Int2ObjectOpenHashMap<>();
@ -266,6 +245,12 @@ public final class GameData {
@Getter
private static final Int2ObjectMap<GatherData> gatherDataMap = new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<GivingData> givingDataMap = new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<GivingGroupData> givingGroupDataMap = new Int2ObjectOpenHashMap<>();
@Getter
@Deprecated // This is to prevent people from using this map. This is for the resource loader
// only!

View File

@ -0,0 +1,49 @@
package emu.grasscutter.data.excels;
import emu.grasscutter.data.*;
import lombok.Getter;
import java.util.List;
@Getter
@ResourceType(name = "BargainExcelConfigData.json")
public final class BargainData extends GameResource {
@Getter private int id;
private int questId;
private List<Integer> dialogId;
/**
* This is a list of 2 integers.
* The first integer is the minimum value of the bargain.
* The second integer is the maximum value of the bargain.
*/
private List<Integer> expectedValue;
private int space;
private List<Integer> successTalkId;
private int failTalkId;
private int moodNpcId;
/**
* This is a list of 2 integers.
* The first integer is the minimum value of the mood.
* The second integer is the maximum value of the mood.
*/
private List<Integer> randomMood;
private int moodAlertLimit;
private int moodLowLimit;
private int singleFailMoodDeduction;
private long moodLowLimitTextTextMapHash;
private long titleTextTextMapHash;
private long affordTextTextMapHash;
private long storageTextTextMapHash;
private long moodHintTextTextMapHash;
private long moodDescTextTextMapHash;
private List<Integer> singleFailTalkId;
private boolean deleteItem;
private int itemId;
}

View File

@ -0,0 +1,46 @@
package emu.grasscutter.data.excels.giving;
import com.google.gson.annotations.SerializedName;
import emu.grasscutter.data.*;
import emu.grasscutter.data.common.ItemParamData;
import emu.grasscutter.game.inventory.BagTab;
import lombok.*;
import java.util.List;
@Data
@EqualsAndHashCode(callSuper = false)
@ResourceType(name = "GivingExcelConfigData.json")
public final class GivingData extends GameResource {
@SerializedName(value = "id", alternate = "Id")
private int id;
private int talkId;
private int mistakeTalkId;
private BagTab tab;
private GiveMethod givingMethod;
private List<ItemParamData> exactItems;
private int exactFinishTalkId;
private List<Integer> givingGroupIds;
private int givingGroupCount;
private boolean isRemoveItem;
private GiveType giveType;
public enum GiveMethod {
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 {
@SerializedName("GIVING_TYPE_QUEST") QUEST,
@SerializedName("GIVING_TYPE_GROUP") GROUP
}
}

View File

@ -0,0 +1,21 @@
package emu.grasscutter.data.excels.giving;
import com.google.gson.annotations.SerializedName;
import emu.grasscutter.data.*;
import lombok.*;
import java.util.List;
@Data
@EqualsAndHashCode(callSuper = false)
@ResourceType(name = "GivingGroupExcelConfigData.json")
public final class GivingGroupData extends GameResource {
@SerializedName(value = "id", alternate = "Id")
private int id;
@SerializedName(value = "itemIds", alternate = "ItemIds")
private List<Integer> itemIds;
private int finishTalkId;
private int mistakeTalkId;
}

View File

@ -1,7 +1,7 @@
package emu.grasscutter.game.ability;
import com.google.protobuf.*;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.*;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.binout.*;
import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction;
@ -19,13 +19,13 @@ import emu.grasscutter.net.proto.AbilityScalarTypeOuterClass.AbilityScalarType;
import emu.grasscutter.net.proto.AbilityScalarValueEntryOuterClass.AbilityScalarValueEntry;
import emu.grasscutter.net.proto.ModifierActionOuterClass.ModifierAction;
import io.netty.util.concurrent.FastThreadLocalThread;
import java.util.HashMap;
import java.util.concurrent.*;
import lombok.Getter;
import org.reflections.Reflections;
public final class AbilityManager extends BasePlayerManager {
import java.util.HashMap;
import java.util.concurrent.*;
public final class AbilityManager extends BasePlayerManager {
private static final HashMap<AbilityModifierAction.Type, AbilityActionHandler> actionHandlers =
new HashMap<>();
private static final HashMap<AbilityMixinData.Type, AbilityMixinHandler> mixinHandlers =
@ -91,8 +91,11 @@ public final class AbilityManager extends BasePlayerManager {
Ability ability, AbilityModifierAction action, ByteString abilityData, GameEntity target) {
var handler = actionHandlers.get(action.type);
if (handler == null || ability == null) {
Grasscutter.getLogger()
if (DebugConstants.LOG_ABILITIES) {
Grasscutter.getLogger()
.debug("Could not execute ability action {} at {}", action.type, ability);
}
return;
}
@ -148,7 +151,7 @@ public final class AbilityManager extends BasePlayerManager {
invoke.getArgumentType(),
invoke.getArgumentTypeValue(),
entity.getId());
} else {
} else if (DebugConstants.LOG_ABILITIES) {
Grasscutter.getLogger()
.debug(
"Invoke type of {} ({}) has no entity. (referring to {})",
@ -375,7 +378,10 @@ public final class AbilityManager extends BasePlayerManager {
var entity = this.player.getScene().getEntityById(invoke.getEntityId());
if (entity == null) {
Grasscutter.getLogger().debug("Entity not found: {}", invoke.getEntityId());
if (DebugConstants.LOG_ABILITIES) {
Grasscutter.getLogger().debug("Entity not found: {}", invoke.getEntityId());
}
return;
}

View File

@ -2,7 +2,7 @@ package emu.grasscutter.game.ability.actions;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.*;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction;
import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction.DropType;
@ -81,8 +81,11 @@ public final class ActionGenerateElemBall extends AbilityActionHandler {
return false;
}
Grasscutter.getLogger()
.debug("Generating {} of {} element balls", amountGenerated, action.configID);
if (DebugConstants.LOG_ABILITIES) {
Grasscutter.getLogger()
.debug("Generating {} of {} element balls", amountGenerated, action.configID);
}
for (int i = 0; i < amountGenerated; i++) {
EntityItem energyBall =
new EntityItem(

View File

@ -1,7 +1,7 @@
package emu.grasscutter.game.ability.actions;
import com.google.protobuf.ByteString;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.*;
import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction;
import emu.grasscutter.game.ability.Ability;
import emu.grasscutter.game.entity.EntityClientGadget;
@ -22,12 +22,14 @@ public final class ActionHealHP extends AbilityActionHandler {
ownerGadget
.getScene()
.getEntityById(ownerGadget.getOwnerEntityId()); // Caster for EntityClientGadget
Grasscutter.getLogger()
.debug(
"Owner {} has top owner {}: {}",
ability.getOwner(),
ownerGadget.getOwnerEntityId(),
owner);
if (DebugConstants.LOG_ABILITIES) {
Grasscutter.getLogger()
.debug(
"Owner {} has top owner {}: {}",
ability.getOwner(),
ownerGadget.getOwnerEntityId(),
owner);
}
}
if (owner == null) return false;
@ -76,7 +78,7 @@ public final class ActionHealHP extends AbilityActionHandler {
amountToRegenerate +=
amountByTargetMaxHPRatio * target.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
Grasscutter.getLogger().debug(">>> Healing {} without ratios", amountToRegenerate);
Grasscutter.getLogger().trace("Healing {} without ratios", amountToRegenerate);
target.heal(
amountToRegenerate * abilityRatio * action.healRatio.get(ability, 1f),
action.muteHealEffect);

View File

@ -0,0 +1,55 @@
package emu.grasscutter.game.inventory;
import it.unimi.dsi.fastutil.ints.*;
import lombok.*;
import java.util.*;
import java.util.stream.Stream;
@RequiredArgsConstructor
public enum BagTab {
TAB_NONE(0),
TAB_WEAPON(1),
TAB_EQUIP(2),
TAB_AVATAR(3),
TAB_FOOD(4),
TAB_MATERIAL(5),
TAB_QUEST(6),
TAB_CONSUME(7),
TAB_WIDGET(8),
TAB_HOMEWORLD(9);
private static final Int2ObjectMap<BagTab> map = new Int2ObjectOpenHashMap<>();
private static final Map<String, BagTab> stringMap = new HashMap<>();
static {
Stream.of(BagTab.values())
.forEach(
entry -> {
map.put(entry.getValue(), entry);
stringMap.put(entry.name(), entry);
});
}
@Getter private final int value;
/**
* Fetches the bag tab by its value.
*
* @param value The name of the bag tab.
* @return The bag tab.
*/
public static BagTab getTypeByValue(int value) {
return map.getOrDefault(value, TAB_NONE);
}
/**
* Fetches the bag tab by its name.
*
* @param name The name of the bag tab.
* @return The bag tab.
*/
public static BagTab getTypeByName(String name) {
return stringMap.getOrDefault(name, TAB_NONE);
}
}

View File

@ -1,7 +1,5 @@
package emu.grasscutter.game.inventory;
import static emu.grasscutter.config.Configuration.INVENTORY_LIMITS;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.common.ItemParamData;
@ -17,10 +15,13 @@ import emu.grasscutter.server.packet.send.*;
import emu.grasscutter.utils.Utils;
import it.unimi.dsi.fastutil.ints.*;
import it.unimi.dsi.fastutil.longs.*;
import java.util.*;
import javax.annotation.Nullable;
import lombok.val;
import static emu.grasscutter.config.Configuration.INVENTORY_LIMITS;
public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
private final Long2ObjectMap<GameItem> store;
private final Int2ObjectMap<InventoryTab> inventoryTypes;
@ -60,6 +61,19 @@ public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
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);
}
@ -170,6 +184,52 @@ public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
getPlayer().sendPacket(new PacketStoreItemChangeNotify(changedItems));
}
/**
* 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 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(int itemId, int minCount) {
return hasItem(itemId, minCount, false);
}
/**
* Checks to see if the player has the item in their inventory.
*
* @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(int itemId, int count, boolean enforce) {
var item = this.getFirstItem(itemId);
if (item == null) return false;
return enforce ?
item.getCount() == count :
item.getCount() >= count;
}
/**
* Checks to see if the player has the item in their inventory.
* This is exact.
*
* @param items A map of item game IDs to their count.
* @return True if the player has the items, false otherwise.
*/
public boolean hasAllItems(Collection<ItemParam> items) {
for (var item : items) {
if (!this.hasItem(item.getItemId(), item.getCount(), true))
return false;
}
return true;
}
private void triggerAddItemEvents(GameItem result) {
try {
getPlayer()
@ -454,10 +514,40 @@ public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
}
}
/**
* Performs a bulk delete of items.
*
* @param items A map of item game IDs to the amount of items to remove.
*/
public void removeItems(Collection<ItemParam> items) {
for (var entry : items) {
this.removeItem(entry.getItemId(), entry.getCount());
}
}
public boolean removeItem(long guid) {
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);
@ -476,10 +566,14 @@ public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
* @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) {

View File

@ -160,7 +160,7 @@ public class Player implements PlayerHook, FieldFetch {
@Getter private transient FriendsList friendsList;
@Getter private transient MailHandler mailHandler;
@Getter private transient AbilityManager abilityManager;
@Getter @Setter private transient QuestManager questManager;
@Getter private transient QuestManager questManager;
@Getter private transient TowerManager towerManager;
@Getter private transient SotSManager sotsManager;
@Getter private transient MapMarksManager mapMarksManager;

View File

@ -1,40 +1,42 @@
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.*;
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<Integer, ItemEntry> itemHistory;
@Setter @Transient private Player player;
private Map<Integer, ItemEntry> 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<String, Integer> questProgressCountMap;
private Map<Integer, ItemGiveRecord> itemGivings;
private Map<Integer, BargainRecord> bargains;
public PlayerProgress() {
this.questProgressCountMap = new ConcurrentHashMap<>();
this.completedDungeons = new IntArrayList();
this.itemHistory = new Int2ObjectOpenHashMap<>();
this.itemGivings = new Int2ObjectOpenHashMap<>();
this.bargains = new Int2ObjectOpenHashMap<>();
}
/**

View File

@ -0,0 +1,109 @@
package emu.grasscutter.game.quest;
import dev.morphia.annotations.Entity;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.BargainData;
import emu.grasscutter.net.proto.BargainResultTypeOuterClass.BargainResultType;
import emu.grasscutter.net.proto.BargainSnapshotOuterClass.BargainSnapshot;
import emu.grasscutter.utils.Utils;
import lombok.*;
@Data
@Entity
@Builder
public final class BargainRecord {
/**
* Provides an instance of a bargain record.
* Uses information from game resources.
*
* @param bargainId The ID of the bargain.
* @return An instance of a bargain record.
*/
public static BargainRecord resolve(int bargainId) {
var bargainData = GameData.getBargainDataMap().get(bargainId);
if (bargainData == null) throw new RuntimeException("No bargain data found for " + bargainId + ".");
return BargainRecord.builder()
.bargainId(bargainId)
.build()
.determineBase(bargainData);
}
private int bargainId;
private int lowestPrice;
private int expectedPrice;
private int currentMood;
private boolean finished;
private BargainResultType result;
/**
* Determines the price of the bargain.
*/
public BargainRecord determineBase(BargainData data) {
// Set the expected price.
var price = data.getExpectedValue();
this.setExpectedPrice(Utils.randomRange(
price.get(0), price.get(1)));
// Set the lowest price.
this.setLowestPrice(price.get(0));
// Set the base mood.
var mood = data.getRandomMood();
this.setCurrentMood(Utils.randomRange(
mood.get(0), mood.get(1)));
return this;
}
/**
* Computes an offer's validity.
*
* @param offer The offer to compute.
* @return The result of the offer.
*/
public BargainResultType applyOffer(int offer) {
if (offer < this.getLowestPrice()) {
// Decrease the mood.
this.currentMood -= Utils.randomRange(1, 3);
// Return a failure.
return this.result = BargainResultType.BARGAIN_SINGLE_FAIL;
}
if (offer > this.getExpectedPrice()) {
// Complete the bargain.
this.setFinished(true);
// Return a success.
return this.result = BargainResultType.BARGAIN_COMPLETE_SUCC;
}
// Compare the offer against the mood & expected price.
// The mood is out of 100; 1 mood should decrease the price by 100.
var moodAdjustment = (int) Math.floor(this.getCurrentMood() / 100.0);
var expectedPrice = this.getExpectedPrice() - moodAdjustment;
if (offer < expectedPrice) {
// Decrease the mood.
this.currentMood -= Utils.randomRange(1, 3);
// Return a failure.
return this.result = BargainResultType.BARGAIN_SINGLE_FAIL;
} else {
// Complete the bargain.
this.setFinished(true);
// Return a success.
return this.result = BargainResultType.BARGAIN_COMPLETE_SUCC;
}
}
/**
* @return A snapshot of this bargain record.
*/
public BargainSnapshot toSnapshot() {
return BargainSnapshot.newBuilder()
.setBargainId(this.getBargainId())
.setCurMood(this.getCurrentMood())
.setPJHMEHGELGC(this.getExpectedPrice())
.setHADMOPEJFIC(this.getLowestPrice())
.build();
}
}

View File

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

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

@ -1,84 +1,41 @@
package emu.grasscutter.game.quest;
import dev.morphia.annotations.Transient;
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 class QuestManager extends BasePlayerManager {
public final class QuestManager extends BasePlayerManager {
@Getter private final Player player;
@Getter private final Int2ObjectMap<GameMainQuest> mainQuests;
@Transient @Getter private final List<Integer> loggedQuests;
@Getter private final List<Integer> loggedQuests;
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<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 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);
@ -134,20 +91,146 @@ public 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<GivingRecord> getGivingRecords() {
return this.getPlayer().getPlayerProgress()
.getItemGivings().values().stream()
.map(ItemGiveRecord::toProto)
.toList();
}
/**
* Attempts to start the bargain.
*
* @param bargainId The bargain ID.
*/
public void startBargain(int bargainId) {
var progress = this.player.getPlayerProgress();
var bargains = progress.getBargains();
// Check if the bargain is already present.
if (bargains.containsKey(bargainId)) {
throw new IllegalStateException("Bargain " + bargainId + " is already active.");
}
// Add the action.
var bargain = BargainRecord.resolve(bargainId);
bargains.put(bargainId, bargain);
// Save the bargains.
this.player.save();
// Send the player the start packet.
this.player.sendPacket(new PacketBargainStartNotify(bargain));
}
/**
* Attempts to stop the bargain.
*
* @param bargainId The bargain ID.
*/
public void stopBargain(int bargainId) {
var progress = this.player.getPlayerProgress();
var bargains = progress.getBargains();
// Check if the bargain is already present.
if (!bargains.containsKey(bargainId)) {
throw new IllegalStateException("Bargain " + bargainId + " is not active.");
}
// Remove the action.
bargains.remove(bargainId);
// Save the bargains.
this.player.save();
// Send the player the stop packet.
this.player.sendPacket(new PacketBargainTerminateNotify(bargainId));
}
/**
* Sends the giving records to the player.
*/
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() {

View File

@ -0,0 +1,15 @@
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;
import emu.grasscutter.net.proto.BargainResultTypeOuterClass.BargainResultType;
@QuestValueContent(QuestContent.QUEST_CONTENT_BARGAIN_FAIL)
public final class ContentBargainFail extends BaseContent {
@Override
public boolean execute(GameQuest quest, QuestData.QuestContentCondition condition, String paramStr, int... params) {
return condition.getParam()[0] == params[0] &&
condition.getParam()[1] == params[1];
}
}

View File

@ -0,0 +1,15 @@
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;
import emu.grasscutter.net.proto.BargainResultTypeOuterClass.BargainResultType;
@QuestValueContent(QuestContent.QUEST_CONTENT_ITEM_LESS_THAN_BARGAIN)
public final class ContentBargainLessThan extends BaseContent {
@Override
public boolean execute(GameQuest quest, QuestData.QuestContentCondition condition, String paramStr, int... params) {
return condition.getParam()[0] == params[0] &&
condition.getParam()[1] == params[1];
}
}

View File

@ -0,0 +1,14 @@
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_BARGAIN_SUCC)
public final class ContentBargainSuccess extends BaseContent {
@Override
public boolean execute(GameQuest quest, QuestData.QuestContentCondition condition, String paramStr, int... params) {
return condition.getParam()[0] == params[0] &&
condition.getParam()[1] == params[1];
}
}

View File

@ -0,0 +1,14 @@
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) {
return condition.getParam()[0] == params[0] &&
condition.getParam()[1] == params[1];
}
}

View File

@ -34,7 +34,7 @@ public enum QuestContent implements QuestTrigger {
QUEST_CONTENT_ADD_QUEST_PROGRESS(24),
QUEST_CONTENT_INTERACT_GADGET(25),
QUEST_CONTENT_DAILY_TASK_COMP_FINISH(26), // missing, currently unused
QUEST_CONTENT_FINISH_ITEM_GIVING(27), // missing, finish
QUEST_CONTENT_FINISH_ITEM_GIVING(27),
QUEST_CONTENT_SKILL(107),
QUEST_CONTENT_CITY_LEVEL_UP(109), // missing, finish
QUEST_CONTENT_PATTERN_GROUP_CLEAR_MONSTER(110), // missing, finish, for random quests
@ -51,9 +51,9 @@ public enum QuestContent implements QuestTrigger {
QUEST_CONTENT_QUEST_VAR_LESS(121),
QUEST_CONTENT_OBTAIN_VARIOUS_ITEM(122), // missing, finish
QUEST_CONTENT_FINISH_TOWER_LEVEL(123), // missing, currently unused
QUEST_CONTENT_BARGAIN_SUCC(124), // missing, finish
QUEST_CONTENT_BARGAIN_FAIL(125), // missing, fail
QUEST_CONTENT_ITEM_LESS_THAN_BARGAIN(126), // missing, fail
QUEST_CONTENT_BARGAIN_SUCC(124),
QUEST_CONTENT_BARGAIN_FAIL(125),
QUEST_CONTENT_ITEM_LESS_THAN_BARGAIN(126),
QUEST_CONTENT_ACTIVITY_TRIGGER_FAILED(127), // missing, fail
QUEST_CONTENT_MAIN_COOP_ENTER_SAVE_POINT(128), // missing, finish
QUEST_CONTENT_ANY_MANUAL_TRANSPORT(129),

View File

@ -35,7 +35,7 @@ public enum QuestExec implements QuestTrigger {
QUEST_EXEC_CREATE_PATTERN_GROUP(25), // missing, used for random quests
QUEST_EXEC_REMOVE_PATTERN_GROUP(26), // missing, used for random quests
QUEST_EXEC_REFRESH_GROUP_SUITE_RANDOM(27), // missing
QUEST_EXEC_ACTIVE_ITEM_GIVING(28), // missing
QUEST_EXEC_ACTIVE_ITEM_GIVING(28),
QUEST_EXEC_DEL_ALL_SPECIFIC_PACK_ITEM(29), // missing
QUEST_EXEC_ROLLBACK_PARENT_QUEST(30),
QUEST_EXEC_LOCK_AVATAR_TEAM(31), // missing
@ -47,8 +47,8 @@ public enum QuestExec implements QuestTrigger {
QUEST_EXEC_ACTIVE_ACTIVITY_COND_STATE(37), // missing
QUEST_EXEC_INACTIVE_ACTIVITY_COND_STATE(38), // missing
QUEST_EXEC_ADD_CUR_AVATAR_ENERGY(39),
QUEST_EXEC_START_BARGAIN(41), // missing
QUEST_EXEC_STOP_BARGAIN(42), // missing
QUEST_EXEC_START_BARGAIN(41),
QUEST_EXEC_STOP_BARGAIN(42),
QUEST_EXEC_SET_QUEST_GLOBAL_VAR(43),
QUEST_EXEC_INC_QUEST_GLOBAL_VAR(44),
QUEST_EXEC_DEC_QUEST_GLOBAL_VAR(45),
@ -71,7 +71,7 @@ public enum QuestExec implements QuestTrigger {
QUEST_EXEC_MODIFY_CLIMATE_AREA(60), // missing
QUEST_EXEC_GRANT_TRIAL_AVATAR_AND_LOCK_TEAM(61), // missing
QUEST_EXEC_CHANGE_MAP_AREA_STATE(62), // missing
QUEST_EXEC_DEACTIVE_ITEM_GIVING(63), // missing
QUEST_EXEC_DEACTIVE_ITEM_GIVING(63),
QUEST_EXEC_CHANGE_SCENE_LEVEL_TAG(64), // missing
QUEST_EXEC_UNLOCK_PLAYER_WORLD_SCENE(65), // missing
QUEST_EXEC_LOCK_PLAYER_WORLD_SCENE(66), // missing

View File

@ -0,0 +1,29 @@
package emu.grasscutter.game.quest.exec;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.excels.quest.QuestData.QuestExecParam;
import emu.grasscutter.game.quest.*;
import emu.grasscutter.game.quest.enums.QuestExec;
import emu.grasscutter.game.quest.handlers.QuestExecHandler;
@QuestValueExec(QuestExec.QUEST_EXEC_ACTIVE_ITEM_GIVING)
public final class ExecActiveItemGiving extends QuestExecHandler {
@Override
public boolean execute(GameQuest quest, QuestExecParam condition, String... paramStr) {
var player = quest.getOwner();
var questManager = player.getQuestManager();
var givingId = Integer.parseInt(condition.getParam()[0]);
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;
}
}
}

View File

@ -0,0 +1,25 @@
package emu.grasscutter.game.quest.exec;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.excels.quest.QuestData;
import emu.grasscutter.game.quest.*;
import emu.grasscutter.game.quest.enums.QuestExec;
import emu.grasscutter.game.quest.handlers.QuestExecHandler;
@QuestValueExec(QuestExec.QUEST_EXEC_DEACTIVE_ITEM_GIVING)
public final class ExecDeactivateItemGiving extends QuestExecHandler {
@Override
public boolean execute(GameQuest quest, QuestData.QuestExecParam condition, String... paramStr) {
var questManager = quest.getOwner().getQuestManager();
var givingId = Integer.parseInt(condition.getParam()[0]);
try {
questManager.removeGivingItemAction(givingId);
return true;
} catch (IllegalStateException ignored) {
Grasscutter.getLogger().warn("Quest {} attempted to remove give action {} twice.",
quest.getSubQuestId(), givingId);
return false;
}
}
}

View File

@ -0,0 +1,27 @@
package emu.grasscutter.game.quest.exec;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.excels.quest.QuestData;
import emu.grasscutter.game.quest.*;
import emu.grasscutter.game.quest.enums.QuestExec;
import emu.grasscutter.game.quest.handlers.QuestExecHandler;
@QuestValueExec(QuestExec.QUEST_EXEC_START_BARGAIN)
public final class ExecStartBargain extends QuestExecHandler {
@Override
public boolean execute(GameQuest quest, QuestData.QuestExecParam condition, String... paramStr) {
// Get the bargain data from the quest parameters.
var bargainId = Integer.parseInt(condition.getParam()[0]);
try {
// Start the bargain.
quest.getOwner().getQuestManager()
.startBargain(bargainId);
Grasscutter.getLogger().debug("Bargain {} started.", bargainId);
return true;
} catch (RuntimeException ignored) {
Grasscutter.getLogger().debug("Bargain {} does not exist.", bargainId);
return false;
}
}
}

View File

@ -0,0 +1,27 @@
package emu.grasscutter.game.quest.exec;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.excels.quest.QuestData;
import emu.grasscutter.game.quest.*;
import emu.grasscutter.game.quest.enums.QuestExec;
import emu.grasscutter.game.quest.handlers.QuestExecHandler;
@QuestValueExec(QuestExec.QUEST_EXEC_STOP_BARGAIN)
public final class ExecStopBargain extends QuestExecHandler {
@Override
public boolean execute(GameQuest quest, QuestData.QuestExecParam condition, String... paramStr) {
// Get the bargain data from the quest parameters.
var bargainId = Integer.parseInt(condition.getParam()[0]);
try {
// Start the bargain.
quest.getOwner().getQuestManager()
.stopBargain(bargainId);
Grasscutter.getLogger().debug("Bargain {} stopped.", bargainId);
return true;
} catch (RuntimeException ignored) {
Grasscutter.getLogger().debug("Bargain {} does not exist.", bargainId);
return false;
}
}
}

View File

@ -1,14 +1,12 @@
package emu.grasscutter.scripts.data.controller;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.*;
import emu.grasscutter.game.entity.GameEntity;
import emu.grasscutter.game.props.ElementType;
import emu.grasscutter.scripts.ScriptLib;
import emu.grasscutter.scripts.ScriptLoader;
import javax.script.Bindings;
import javax.script.CompiledScript;
import org.luaj.vm2.LuaError;
import org.luaj.vm2.LuaValue;
import emu.grasscutter.scripts.*;
import org.luaj.vm2.*;
import javax.script.*;
public class EntityController {
private transient CompiledScript entityController;
@ -38,9 +36,11 @@ public class EntityController {
}
public int onClientExecuteRequest(GameEntity entity, int param1, int param2, int param3) {
Grasscutter.getLogger()
if (DebugConstants.LOG_LUA_SCRIPTS) {
Grasscutter.getLogger()
.debug(
"Request on {}, {}: {}", entity.getGroupId(), param1, entity.getPosition().toString());
"Request on {}, {}: {}", entity.getGroupId(), param1, entity.getPosition().toString());
}
LuaValue value =
callControllerScriptFunc(
entity,

View File

@ -0,0 +1,43 @@
package emu.grasscutter.server.packet.recv;
import emu.grasscutter.game.quest.enums.QuestContent;
import emu.grasscutter.net.packet.*;
import emu.grasscutter.net.proto.BargainOfferPriceReqOuterClass.BargainOfferPriceReq;
import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.server.packet.send.PacketBargainOfferPriceRsp;
@Opcodes(PacketOpcodes.BargainOfferPriceReq)
public final class HandlerBargainOfferPriceReq extends PacketHandler {
@Override
public void handle(GameSession session, byte[] header, byte[] payload)
throws Exception {
var packet = BargainOfferPriceReq.parseFrom(payload);
var player = session.getPlayer();
// Fetch the active bargain.
var bargainId = packet.getBargainId();
var progress = player.getPlayerProgress();
var bargain = progress.getBargains().get(bargainId);
if (bargain == null) return;
// Apply the offer.
var result = bargain.applyOffer(packet.getPrice());
// Queue the quest content event.
var questManager = player.getQuestManager();
switch (result) {
case BARGAIN_COMPLETE_SUCC -> questManager.queueEvent(
QuestContent.QUEST_CONTENT_BARGAIN_SUCC,
bargainId, 0);
case BARGAIN_SINGLE_FAIL -> questManager.queueEvent(
QuestContent.QUEST_CONTENT_ITEM_LESS_THAN_BARGAIN,
bargainId, 0);
case BARGAIN_COMPLETE_FAIL -> questManager.queueEvent(
QuestContent.QUEST_CONTENT_BARGAIN_FAIL,
bargainId, 0);
}
// Return the resulting packet.
session.send(new PacketBargainOfferPriceRsp(result, bargain));
}
}

View File

@ -0,0 +1,17 @@
package emu.grasscutter.server.packet.recv;
import emu.grasscutter.net.packet.*;
import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.server.packet.send.PacketGetAllActivatedBargainDataRsp;
@Opcodes(PacketOpcodes.GetAllActivatedBargainDataReq)
public final class HandlerGetAllActivatedBargainDataReq extends PacketHandler {
@Override
public void handle(GameSession session, byte[] header, byte[] payload) {
session.send(new PacketGetAllActivatedBargainDataRsp(
session.getPlayer()
.getPlayerProgress()
.getBargains()
.values()));
}
}

View File

@ -0,0 +1,29 @@
package emu.grasscutter.server.packet.recv;
import emu.grasscutter.net.packet.*;
import emu.grasscutter.net.proto.GetBargainDataReqOuterClass.GetBargainDataReq;
import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.server.packet.send.PacketGetBargainDataRsp;
@Opcodes(PacketOpcodes.GetBargainDataReq)
public final class HandlerGetBargainDataReq extends PacketHandler {
@Override
public void handle(GameSession session, byte[] header, byte[] payload)
throws Exception {
var packet = GetBargainDataReq.parseFrom(payload);
var bargainId = packet.getBargainId();
var bargain = session.getPlayer()
.getPlayerProgress()
.getBargains()
.get(bargainId);
if (bargain == null) {
session.send(new PacketGetBargainDataRsp(
Retcode.RET_BARGAIN_NOT_ACTIVATED));
return;
}
session.send(new PacketGetBargainDataRsp(bargain));
}
}

View File

@ -0,0 +1,109 @@
package emu.grasscutter.server.packet.recv;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData;
import emu.grasscutter.game.quest.enums.QuestContent;
import emu.grasscutter.net.packet.*;
import emu.grasscutter.net.proto.ItemGivingReqOuterClass.ItemGivingReq;
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
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
var req = ItemGivingReq.parseFrom(payload);
var player = session.getPlayer();
var inventory = player.getInventory();
var giveId = req.getGivingId();
var items = req.getItemParamListList();
switch (req.getItemGivingType()) {
case QUEST -> {
var questManager = player.getQuestManager();
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 + ".");
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);
}
// 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<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());
} 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 -> {
Grasscutter.getLogger().debug("Unimplemented gadget giving was executed for {}.", giveId);
player.sendPacket(new PacketItemGivingRsp());
}
}
}
}

View File

@ -0,0 +1,21 @@
package emu.grasscutter.server.packet.send;
import emu.grasscutter.game.quest.BargainRecord;
import emu.grasscutter.net.packet.*;
import emu.grasscutter.net.proto.BargainOfferPriceRspOuterClass.BargainOfferPriceRsp;
import emu.grasscutter.net.proto.BargainResultTypeOuterClass.BargainResultType;
import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
public final class PacketBargainOfferPriceRsp extends BasePacket {
public PacketBargainOfferPriceRsp(BargainResultType result, BargainRecord record) {
super(PacketOpcodes.BargainOfferPriceRsp);
this.setData(BargainOfferPriceRsp.newBuilder()
.setRetcode(record.isFinished() ?
Retcode.RET_BARGAIN_FINISHED.getNumber() :
Retcode.RET_BARGAIN_NOT_ACTIVATED.getNumber())
.setCurMood(record.getCurrentMood())
.setBargainResult(result)
.setResultParam(0));
}
}

View File

@ -0,0 +1,15 @@
package emu.grasscutter.server.packet.send;
import emu.grasscutter.game.quest.BargainRecord;
import emu.grasscutter.net.packet.*;
import emu.grasscutter.net.proto.BargainStartNotifyOuterClass.BargainStartNotify;
public final class PacketBargainStartNotify extends BasePacket {
public PacketBargainStartNotify(BargainRecord record) {
super(PacketOpcodes.BargainStartNotify);
this.setData(BargainStartNotify.newBuilder()
.setBargainId(record.getBargainId())
.setSnapshot(record.toSnapshot()));
}
}

View File

@ -0,0 +1,13 @@
package emu.grasscutter.server.packet.send;
import emu.grasscutter.net.packet.*;
import emu.grasscutter.net.proto.BargainTerminateNotifyOuterClass.BargainTerminateNotify;
public final class PacketBargainTerminateNotify extends BasePacket {
public PacketBargainTerminateNotify(int bargainId) {
super(PacketOpcodes.BargainTerminateNotify);
this.setData(BargainTerminateNotify.newBuilder()
.setBargainId(bargainId));
}
}

View File

@ -0,0 +1,20 @@
package emu.grasscutter.server.packet.send;
import emu.grasscutter.game.quest.BargainRecord;
import emu.grasscutter.net.packet.*;
import emu.grasscutter.net.proto.GetAllActivatedBargainDataRspOuterClass.GetAllActivatedBargainDataRsp;
import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
import java.util.Collection;
public final class PacketGetAllActivatedBargainDataRsp extends BasePacket {
public PacketGetAllActivatedBargainDataRsp(Collection<BargainRecord> records) {
super(PacketOpcodes.GetAllActivatedBargainDataRsp);
this.setData(GetAllActivatedBargainDataRsp.newBuilder()
.setRetcode(Retcode.RET_SUCC.getNumber())
.addAllSnapshotList(records.stream()
.map(BargainRecord::toSnapshot)
.toList()));
}
}

View File

@ -0,0 +1,24 @@
package emu.grasscutter.server.packet.send;
import emu.grasscutter.game.quest.BargainRecord;
import emu.grasscutter.net.packet.*;
import emu.grasscutter.net.proto.GetBargainDataRspOuterClass.GetBargainDataRsp;
import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
public final class PacketGetBargainDataRsp extends BasePacket {
public PacketGetBargainDataRsp(Retcode retcode) {
super(PacketOpcodes.GetBargainDataRsp);
this.setData(GetBargainDataRsp.newBuilder()
.setRetcode(retcode.getNumber()));
}
public PacketGetBargainDataRsp(BargainRecord record) {
super(PacketOpcodes.GetBargainDataRsp);
this.setData(GetBargainDataRsp.newBuilder()
.setRetcode(Retcode.RET_SUCC.getNumber())
.setBargainId(record.getBargainId())
.setSnapshot(record.toSnapshot()));
}
}

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

View File

@ -0,0 +1,30 @@
package emu.grasscutter.server.packet.send;
import emu.grasscutter.net.packet.*;
import emu.grasscutter.net.proto.ItemGivingRspOuterClass.ItemGivingRsp;
public final class PacketItemGivingRsp extends BasePacket {
public PacketItemGivingRsp() {
this(0, Mode.FAILURE);
}
public PacketItemGivingRsp(int value, Mode mode) {
super(PacketOpcodes.ItemGivingRsp);
var packet = ItemGivingRsp.newBuilder()
.setRetcode(mode == Mode.FAILURE ? 1 : 0);
if (mode == Mode.EXACT_SUCCESS) {
packet.setGivingId(value);
} else if (mode == Mode.GROUP_SUCCESS) {
packet.setGivingGroupId(value);
}
this.setData(packet);
}
public enum Mode {
GROUP_SUCCESS,
EXACT_SUCCESS,
FAILURE
}
}