result = new Int2ObjectRBTreeMap<>();
+
+ activityCondMap
+ .int2ObjectEntrySet()
+ .stream()
+ .map(entry -> new BasicEntry<>(entry.getIntKey(), getPlayerActivityDataByCondId(entry.getIntKey())))
+ .filter(entry -> entry.getValue() != null)
+ .forEach(entry -> result.put(entry.getIntKey(), entry.getValue()));
+
+ return result;
+ }
+
+ private PlayerActivityData getPlayerActivityDataByCondId(Integer key) {
+ return playerActivityDataMap.get(detectActivityDataIdByCondId(key));
+ }
+
+ /**
+ * Detect activity data id by cond id. Cond id comes from condId field from NewActivityCondExcelConfigData.json.
+ * See {@link ActivityCondExcelConfigData} for condId.
+ *
+ * Generally, there are 3 cases:
+ *
+ * - Activity data id >= 5003. Then cond id will be activity data id plus 3 additional digits.
+ * For example: activity data id = 5087, cond id = 5087xxx (x - any digit)
+ * - Activity data id = 5001. Then cond id will be activity data id plus 2 additional digits.
+ * For example: activity data id = 5001, cond id = 5001xx (x - any digit)
+ * - Activity data id one of [1001]. Then cond id will be activity data id plus 2 additional digits.
+ * This also applied to activity data id = 1002. For example: activity data id = 1001, cond id = 1001x (x - any digit>
+ *
+ *
+ * @param key cond id for which activity data id should be defined
+ * @return activity data for given cond id. Returns -1 if activity was not found.
+ */
+ private Integer detectActivityDataIdByCondId(Integer key) {
+ if (key / 10 == 1001 || key / 10 == 1002) {
+ return 1001;
+ } else if (key / 100 == 5001) {
+ return key / 100;
+ } else {
+ return key / 1000;
+ }
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/activity/condition/all/DayLess.java b/src/main/java/emu/grasscutter/game/activity/condition/all/DayLess.java
new file mode 100644
index 000000000..ef6c8263f
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/activity/condition/all/DayLess.java
@@ -0,0 +1,16 @@
+package emu.grasscutter.game.activity.condition.all;
+
+import emu.grasscutter.game.activity.ActivityConfigItem;
+import emu.grasscutter.game.activity.PlayerActivityData;
+import emu.grasscutter.game.activity.condition.ActivityCondition;
+import emu.grasscutter.game.activity.condition.ActivityConditionBaseHandler;
+
+import static emu.grasscutter.game.activity.condition.ActivityConditions.NEW_ACTIVITY_COND_DAYS_LESS;
+
+@ActivityCondition(NEW_ACTIVITY_COND_DAYS_LESS)
+public class DayLess extends ActivityConditionBaseHandler {
+ @Override
+ public boolean execute(PlayerActivityData activityData, ActivityConfigItem activityConfig, int... params) {
+ return true; //TODO implement this and add possibility to always return true
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/activity/condition/all/DaysGreatEqual.java b/src/main/java/emu/grasscutter/game/activity/condition/all/DaysGreatEqual.java
new file mode 100644
index 000000000..0377372ce
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/activity/condition/all/DaysGreatEqual.java
@@ -0,0 +1,21 @@
+package emu.grasscutter.game.activity.condition.all;
+
+import emu.grasscutter.game.activity.ActivityConfigItem;
+import emu.grasscutter.game.activity.PlayerActivityData;
+import emu.grasscutter.game.activity.condition.ActivityCondition;
+import emu.grasscutter.game.activity.condition.ActivityConditionBaseHandler;
+
+import java.util.Date;
+
+import static emu.grasscutter.game.activity.condition.ActivityConditions.NEW_ACTIVITY_COND_DAYS_GREAT_EQUAL;
+
+@ActivityCondition(NEW_ACTIVITY_COND_DAYS_GREAT_EQUAL)
+public class DaysGreatEqual extends ActivityConditionBaseHandler {
+ @Override
+ public boolean execute(PlayerActivityData activityData, ActivityConfigItem activityConfig, int... params) {
+ Date activityBeginTime = activityConfig.getBeginTime();
+ long timeDiff = System.currentTimeMillis() - activityBeginTime.getTime();
+ int days = (int) (timeDiff / (1000 * 60 * 60 * 24L));
+ return days + 1 >= params[0];
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/activity/condition/all/FinishWatcher.java b/src/main/java/emu/grasscutter/game/activity/condition/all/FinishWatcher.java
new file mode 100644
index 000000000..5b775f4a9
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/activity/condition/all/FinishWatcher.java
@@ -0,0 +1,24 @@
+package emu.grasscutter.game.activity.condition.all;
+
+import emu.grasscutter.game.activity.ActivityConfigItem;
+import emu.grasscutter.game.activity.PlayerActivityData;
+import emu.grasscutter.game.activity.condition.ActivityCondition;
+import emu.grasscutter.game.activity.condition.ActivityConditionBaseHandler;
+import emu.grasscutter.game.activity.condition.ActivityConditions;
+import lombok.val;
+
+@ActivityCondition(ActivityConditions.NEW_ACTIVITY_COND_FINISH_WATCHER)
+public class FinishWatcher extends ActivityConditionBaseHandler {
+
+ @Override
+ public boolean execute(PlayerActivityData activityData, ActivityConfigItem activityConfig, int... params) {
+ val watcherMap = activityData.getWatcherInfoMap();
+ for (int param : params) {
+ val watcher = watcherMap.get(param);
+ if(watcher == null || !watcher.isFinished()){
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/activity/condition/all/NotFinishTalk.java b/src/main/java/emu/grasscutter/game/activity/condition/all/NotFinishTalk.java
new file mode 100644
index 000000000..b863b3885
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/activity/condition/all/NotFinishTalk.java
@@ -0,0 +1,22 @@
+package emu.grasscutter.game.activity.condition.all;
+
+import emu.grasscutter.game.activity.ActivityConfigItem;
+import emu.grasscutter.game.activity.PlayerActivityData;
+import emu.grasscutter.game.activity.condition.ActivityCondition;
+import emu.grasscutter.game.activity.condition.ActivityConditionBaseHandler;
+
+import static emu.grasscutter.game.activity.condition.ActivityConditions.NEW_ACTIVITY_COND_NOT_FINISH_TALK;
+
+@ActivityCondition(NEW_ACTIVITY_COND_NOT_FINISH_TALK)
+public class NotFinishTalk extends ActivityConditionBaseHandler {
+ @Override
+ public boolean execute(PlayerActivityData activityData, ActivityConfigItem activityConfig, int... params) {
+ return activityData
+ .getPlayer()
+ .getQuestManager()
+ .getMainQuests()
+ .int2ObjectEntrySet()
+ .stream()
+ .noneMatch(q -> q.getValue().getTalks().get(params[0]) != null); //FIXME taken from ContentCompleteTalk
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/activity/condition/all/PlayerLevelGreatEqualActivityActivityCondition.java b/src/main/java/emu/grasscutter/game/activity/condition/all/PlayerLevelGreatEqualActivityActivityCondition.java
new file mode 100644
index 000000000..9d03da1a1
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/activity/condition/all/PlayerLevelGreatEqualActivityActivityCondition.java
@@ -0,0 +1,17 @@
+package emu.grasscutter.game.activity.condition.all;
+
+import emu.grasscutter.game.activity.ActivityConfigItem;
+import emu.grasscutter.game.activity.PlayerActivityData;
+import emu.grasscutter.game.activity.condition.ActivityCondition;
+import emu.grasscutter.game.activity.condition.ActivityConditionBaseHandler;
+
+import static emu.grasscutter.game.activity.condition.ActivityConditions.NEW_ACTIVITY_COND_PLAYER_LEVEL_GREAT_EQUAL;
+
+@ActivityCondition(NEW_ACTIVITY_COND_PLAYER_LEVEL_GREAT_EQUAL)
+public class PlayerLevelGreatEqualActivityActivityCondition extends ActivityConditionBaseHandler {
+
+ @Override
+ public boolean execute(PlayerActivityData activityData, ActivityConfigItem activityConfig, int... params) {
+ return activityData.getPlayer().getLevel() >= params[0];
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/activity/condition/all/QuestFinished.java b/src/main/java/emu/grasscutter/game/activity/condition/all/QuestFinished.java
new file mode 100644
index 000000000..cb2bea414
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/activity/condition/all/QuestFinished.java
@@ -0,0 +1,23 @@
+package emu.grasscutter.game.activity.condition.all;
+
+import emu.grasscutter.game.activity.ActivityConfigItem;
+import emu.grasscutter.game.activity.PlayerActivityData;
+import emu.grasscutter.game.activity.condition.ActivityCondition;
+import emu.grasscutter.game.activity.condition.ActivityConditionBaseHandler;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.enums.QuestState;
+
+import static emu.grasscutter.game.activity.condition.ActivityConditions.NEW_ACTIVITY_COND_QUEST_FINISH;
+
+@ActivityCondition(NEW_ACTIVITY_COND_QUEST_FINISH)
+public class QuestFinished extends ActivityConditionBaseHandler {
+ @Override
+ public boolean execute(PlayerActivityData activityData, ActivityConfigItem activityConfig, int... params) {
+ GameQuest quest = activityData
+ .getPlayer()
+ .getQuestManager()
+ .getQuestById(params[0]);
+
+ return quest != null && quest.getState() == QuestState.QUEST_STATE_FINISHED;
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/activity/condition/all/SalesmanCanDeliver.java b/src/main/java/emu/grasscutter/game/activity/condition/all/SalesmanCanDeliver.java
new file mode 100644
index 000000000..52e24f7d2
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/activity/condition/all/SalesmanCanDeliver.java
@@ -0,0 +1,19 @@
+package emu.grasscutter.game.activity.condition.all;
+
+import emu.grasscutter.game.activity.ActivityConfigItem;
+import emu.grasscutter.game.activity.PlayerActivityData;
+import emu.grasscutter.game.activity.condition.ActivityCondition;
+import emu.grasscutter.game.activity.condition.ActivityConditionBaseHandler;
+
+import static emu.grasscutter.game.activity.condition.ActivityConditions.NEW_ACTIVITY_COND_SALESMAN_CAN_DELIVER;
+
+@ActivityCondition(NEW_ACTIVITY_COND_SALESMAN_CAN_DELIVER)
+public class SalesmanCanDeliver extends ActivityConditionBaseHandler {
+ @Override
+ public boolean execute(PlayerActivityData activityData, ActivityConfigItem activityConfig, int... params) {
+ //TODO need to reverse engineer this logic.
+ //This condition appears only in one condition "condId": 5003001
+ //and this condition accept no params. I have no idea how to implement it
+ return false;
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/activity/condition/all/UnknownActivityConditionHandler.java b/src/main/java/emu/grasscutter/game/activity/condition/all/UnknownActivityConditionHandler.java
new file mode 100644
index 000000000..740cabf59
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/activity/condition/all/UnknownActivityConditionHandler.java
@@ -0,0 +1,18 @@
+package emu.grasscutter.game.activity.condition.all;
+
+import emu.grasscutter.Grasscutter;
+import emu.grasscutter.game.activity.ActivityConfigItem;
+import emu.grasscutter.game.activity.PlayerActivityData;
+import emu.grasscutter.game.activity.condition.ActivityConditionBaseHandler;
+
+/**
+ * This class is used when condition was not found
+ */
+public class UnknownActivityConditionHandler extends ActivityConditionBaseHandler {
+
+ @Override
+ public boolean execute(PlayerActivityData activityData, ActivityConfigItem activityConfig, int... params) {
+ Grasscutter.getLogger().error("Called unknown condition handler");
+ return false;
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/activity/trialavatar/TrialAvatarActivityChallengeTrigger.java b/src/main/java/emu/grasscutter/game/activity/trialavatar/TrialAvatarActivityChallengeTrigger.java
new file mode 100644
index 000000000..b470b046d
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/activity/trialavatar/TrialAvatarActivityChallengeTrigger.java
@@ -0,0 +1,36 @@
+package emu.grasscutter.game.activity.trialavatar;
+
+import emu.grasscutter.game.activity.ActivityWatcher;
+import emu.grasscutter.game.activity.ActivityWatcherType;
+import emu.grasscutter.game.activity.PlayerActivityData;
+import emu.grasscutter.game.props.WatcherTriggerType;
+
+import lombok.val;
+import java.util.stream.Stream;
+
+@ActivityWatcherType(WatcherTriggerType.TRIGGER_FINISH_CHALLENGE)
+public class TrialAvatarActivityChallengeTrigger extends ActivityWatcher {
+ @Override
+ protected boolean isMeet(String... param) {
+ if(param.length < 3) return false;
+
+ val handler = (TrialAvatarActivityHandler) getActivityHandler();
+ if(handler == null) return false;
+
+ val paramList = handler.getTriggerParamList();
+ if(paramList.isEmpty()) return false;
+
+ val paramCond = Stream.of(paramList.get(0).split(",")).toList();
+ return Stream.of(param).allMatch(x -> paramCond.contains(x));
+ }
+
+ @Override
+ public void trigger(PlayerActivityData playerActivityData, String... param) {
+ if (!isMeet(param)) return;
+
+ val handler = (TrialAvatarActivityHandler) getActivityHandler();
+ if(handler == null) return;
+
+ handler.setPassDungeon(playerActivityData);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/activity/trialavatar/TrialAvatarActivityHandler.java b/src/main/java/emu/grasscutter/game/activity/trialavatar/TrialAvatarActivityHandler.java
new file mode 100644
index 000000000..1c20991a4
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/activity/trialavatar/TrialAvatarActivityHandler.java
@@ -0,0 +1,144 @@
+package emu.grasscutter.game.activity.trialavatar;
+
+import com.esotericsoftware.reflectasm.ConstructorAccess;
+import emu.grasscutter.Grasscutter;
+import emu.grasscutter.data.GameData;
+import emu.grasscutter.data.excels.RewardData;
+import emu.grasscutter.game.activity.ActivityWatcher;
+import emu.grasscutter.game.activity.DefaultWatcher;
+import emu.grasscutter.game.dungeons.DungeonTrialTeam;
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.game.props.ActionReason;
+import emu.grasscutter.game.props.WatcherTriggerType;
+import emu.grasscutter.game.activity.ActivityHandler;
+import emu.grasscutter.game.activity.GameActivity;
+import emu.grasscutter.game.activity.PlayerActivityData;
+import emu.grasscutter.game.props.ActivityType;
+import emu.grasscutter.net.proto.ActivityInfoOuterClass.ActivityInfo;
+import emu.grasscutter.net.proto.TrialAvatarGrantRecordOuterClass.TrialAvatarGrantRecord.GrantReason;
+import emu.grasscutter.server.packet.send.PacketActivityInfoNotify;
+import emu.grasscutter.server.packet.send.PacketScenePlayerLocationNotify;
+import emu.grasscutter.utils.JsonUtils;
+
+import java.util.*;
+import java.util.stream.*;
+import lombok.*;
+
+@GameActivity(ActivityType.NEW_ACTIVITY_TRIAL_AVATAR)
+public class TrialAvatarActivityHandler extends ActivityHandler {
+ @Getter @Setter private int selectedTrialAvatarIndex;
+
+ @Override
+ public void onInitPlayerActivityData(PlayerActivityData playerActivityData) {
+ TrialAvatarPlayerData trialAvatarPlayerData = TrialAvatarPlayerData.create(getActivityConfigItem().getScheduleId());
+
+ playerActivityData.setDetail(trialAvatarPlayerData);
+ }
+
+ @Override
+ public void onProtoBuild(PlayerActivityData playerActivityData, ActivityInfo.Builder activityInfo) {
+ TrialAvatarPlayerData trialAvatarPlayerData = getTrialAvatarPlayerData(playerActivityData);
+
+ activityInfo.setTrialAvatarInfo(trialAvatarPlayerData.toProto());
+ }
+
+ @Override
+ public void initWatchers(Map> activityWatcherTypeMap) {
+ var watcherType = activityWatcherTypeMap.get(WatcherTriggerType.TRIGGER_FINISH_CHALLENGE);
+ ActivityWatcher watcher;
+ if(watcherType != null){
+ watcher = (ActivityWatcher) watcherType.newInstance();
+ }else{
+ watcher = new DefaultWatcher();
+ }
+
+ watcher.setActivityHandler(this);
+ getWatchersMap().computeIfAbsent(WatcherTriggerType.TRIGGER_FINISH_CHALLENGE, k -> new ArrayList<>());
+ getWatchersMap().get(WatcherTriggerType.TRIGGER_FINISH_CHALLENGE).add(watcher);
+ }
+
+ public TrialAvatarPlayerData getTrialAvatarPlayerData(PlayerActivityData playerActivityData) {
+ if (playerActivityData.getDetail() == null || playerActivityData.getDetail().isBlank()) {
+ onInitPlayerActivityData(playerActivityData);
+ playerActivityData.save();
+ }
+
+ return JsonUtils.decode(playerActivityData.getDetail(), TrialAvatarPlayerData.class);
+ }
+
+ public int getTrialActivityDungeonId(int trialAvatarIndexId) {
+ val data = GameData.getTrialAvatarActivityDataByAvatarIndex(trialAvatarIndexId);
+ return data!=null ? data.getDungeonId() : -1;
+ }
+
+ public List getTriggerParamList() {
+ val data = GameData.getTrialAvatarActivityDataByAvatarIndex(getSelectedTrialAvatarIndex());
+ return data!=null ? data.getTriggerConfig().getParamList() : Collections.emptyList();
+ }
+
+ public boolean enterTrialDungeon(Player player, int trialAvatarIndexId, int enterPointId) {
+ // TODO, not sure if this will cause problem in MP, since we are entering trial activity dungeon
+ player.sendPacket(new PacketScenePlayerLocationNotify(player.getScene())); // official does send this
+
+ if (!player.getServer().getDungeonSystem().enterDungeon(
+ player,
+ enterPointId,
+ getTrialActivityDungeonId(trialAvatarIndexId))) return false;
+
+ setSelectedTrialAvatarIndex(trialAvatarIndexId);
+
+ return true;
+ }
+
+ public List getBattleAvatarsList() {
+ val activityData = GameData.getTrialAvatarActivityDataByAvatarIndex(getSelectedTrialAvatarIndex());
+ if (activityData == null || activityData.getBattleAvatarsList().isBlank()) return List.of();
+ return Stream.of(activityData.getBattleAvatarsList().split(",")).map(Integer::parseInt).toList();
+ }
+
+ public DungeonTrialTeam getTrialAvatarDungeonTeam(){
+ List battleAvatarsList = getBattleAvatarsList();
+ if (battleAvatarsList.isEmpty()) return null;
+
+ return new DungeonTrialTeam(battleAvatarsList, GrantReason.GRANT_REASON_BY_TRIAL_AVATAR_ACTIVITY);
+ }
+
+ public void unsetTrialAvatarTeam(Player player) {
+ if (getSelectedTrialAvatarIndex() <= 0) return;
+ player.removeTrialAvatarForActivity();
+ setSelectedTrialAvatarIndex(0);
+ }
+
+ public boolean getReward(Player player, int trialAvatarIndexId) {
+ val playerActivityData = player.getActivityManager().getPlayerActivityDataByActivityType(ActivityType.NEW_ACTIVITY_TRIAL_AVATAR);
+
+ if(playerActivityData.isEmpty()){
+ return false;
+ }
+
+ TrialAvatarPlayerData trialAvatarPlayerData = getTrialAvatarPlayerData(playerActivityData.get());
+ TrialAvatarPlayerData.RewardInfoItem rewardInfo = trialAvatarPlayerData.getRewardInfo(trialAvatarIndexId);
+ if (rewardInfo == null) return false;
+
+ RewardData rewardParam = GameData.getRewardDataMap().get(rewardInfo.getRewardId());
+ if (rewardParam == null) return false;
+
+ player.getInventory().addItemParamDatas(rewardParam.getRewardItemList(), ActionReason.TrialAvatarActivityFirstPassReward);
+ rewardInfo.setReceivedReward(true);
+ playerActivityData.get().setDetail(trialAvatarPlayerData);
+ playerActivityData.get().save();
+ return true;
+ }
+
+ public void setPassDungeon(PlayerActivityData playerActivityData) {
+ TrialAvatarPlayerData trialAvatarPlayerData = getTrialAvatarPlayerData(playerActivityData);
+ TrialAvatarPlayerData.RewardInfoItem rewardInfo = trialAvatarPlayerData.getRewardInfo(getSelectedTrialAvatarIndex());
+ if (rewardInfo == null) return;
+
+ rewardInfo.setPassedDungeon(true);
+ playerActivityData.setDetail(trialAvatarPlayerData);
+ playerActivityData.save();
+ Player player = Grasscutter.getGameServer().getPlayerByUid(playerActivityData.getUid());
+ player.sendPacket(new PacketActivityInfoNotify(toProto(playerActivityData, player.getActivityManager().getConditionExecutor())));
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/activity/trialavatar/TrialAvatarPlayerData.java b/src/main/java/emu/grasscutter/game/activity/trialavatar/TrialAvatarPlayerData.java
new file mode 100644
index 000000000..1001d1858
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/activity/trialavatar/TrialAvatarPlayerData.java
@@ -0,0 +1,90 @@
+package emu.grasscutter.game.activity.trialavatar;
+
+import emu.grasscutter.data.GameData;
+import emu.grasscutter.data.common.BaseTrialActivityData;
+import emu.grasscutter.net.proto.TrialAvatarActivityDetailInfoOuterClass.TrialAvatarActivityDetailInfo;
+import emu.grasscutter.net.proto.TrialAvatarActivityRewardDetailInfoOuterClass.TrialAvatarActivityRewardDetailInfo;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Data;
+import lombok.experimental.FieldDefaults;
+import lombok.val;
+
+import java.util.List;
+import java.util.stream.*;
+
+@Data
+@FieldDefaults(level = AccessLevel.PRIVATE)
+@Builder(builderMethodName = "of")
+public class TrialAvatarPlayerData {
+ List rewardInfoList;
+
+ private static BaseTrialActivityData getActivityData(int scheduleId){
+ // prefer custom data over official data
+ return GameData.getTrialAvatarActivityCustomData().isEmpty() ? GameData.getTrialAvatarActivityDataMap().get(scheduleId)
+ : GameData.getTrialAvatarActivityCustomData().get(scheduleId);
+ }
+
+ public static List getAvatarIdList(int scheduleId) {
+ val activityData = getActivityData(scheduleId);
+ return activityData != null ? activityData.getAvatarIndexIdList() : List.of();
+ }
+
+ public static List getRewardIdList(int scheduleId) {
+ val activityData = getActivityData(scheduleId);
+ return activityData != null ? activityData.getRewardIdList() : List.of();
+ }
+
+ public static TrialAvatarPlayerData create(int scheduleId) {
+ List avatarIds = getAvatarIdList(scheduleId);
+ List rewardIds = getRewardIdList(scheduleId);
+ return TrialAvatarPlayerData.of()
+ .rewardInfoList(IntStream.range(0, avatarIds.size())
+ .filter(i -> avatarIds.get(i) > 0 && rewardIds.get(i) > 0)
+ .mapToObj(i -> RewardInfoItem.create(
+ avatarIds.get(i),
+ rewardIds.get(i)))
+ .collect(Collectors.toList()))
+ .build();
+ }
+
+ public TrialAvatarActivityDetailInfo toProto() {
+ return TrialAvatarActivityDetailInfo.newBuilder()
+ .addAllRewardInfoList(getRewardInfoList().stream()
+ .map(RewardInfoItem::toProto).toList())
+ .build();
+ }
+
+ public RewardInfoItem getRewardInfo(int trialAvatarIndexId) {
+ return getRewardInfoList().stream().filter(x -> x.getTrialAvatarIndexId() == trialAvatarIndexId)
+ .findFirst().orElse(null);
+ }
+
+ @Data
+ @FieldDefaults(level = AccessLevel.PRIVATE)
+ @Builder(builderMethodName = "of")
+ public static class RewardInfoItem {
+ int trialAvatarIndexId;
+ int rewardId;
+ boolean passedDungeon;
+ boolean receivedReward;
+
+ public static RewardInfoItem create(int trialAvatarIndexId, int rewardId) {
+ return RewardInfoItem.of()
+ .trialAvatarIndexId(trialAvatarIndexId)
+ .rewardId(rewardId)
+ .passedDungeon(false)
+ .receivedReward(false)
+ .build();
+ }
+
+ public TrialAvatarActivityRewardDetailInfo toProto() {
+ return TrialAvatarActivityRewardDetailInfo.newBuilder()
+ .setTrialAvatarIndexId(getTrialAvatarIndexId())
+ .setRewardId(getRewardId())
+ .setPassedDungeon(isPassedDungeon())
+ .setReceivedReward(isReceivedReward())
+ .build();
+ }
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/DungeonEndStats.java b/src/main/java/emu/grasscutter/game/dungeons/DungeonEndStats.java
new file mode 100644
index 000000000..6943ab5a3
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/DungeonEndStats.java
@@ -0,0 +1,18 @@
+package emu.grasscutter.game.dungeons;
+
+import emu.grasscutter.game.dungeons.dungeon_results.BaseDungeonResult;
+import lombok.Getter;
+
+public class DungeonEndStats {
+ @Getter private int killedMonsters;
+ @Getter private int timeTaken;
+ @Getter private int openChestCount;
+ @Getter private BaseDungeonResult.DungeonEndReason dungeonResult;
+
+ public DungeonEndStats(int killedMonsters, int timeTaken, int openChestCount, BaseDungeonResult.DungeonEndReason dungeonResult){
+ this.killedMonsters = killedMonsters;
+ this.timeTaken = timeTaken;
+ this.dungeonResult = dungeonResult;
+ this.openChestCount = openChestCount;
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/DungeonManager.java b/src/main/java/emu/grasscutter/game/dungeons/DungeonManager.java
new file mode 100644
index 000000000..c6ba88b02
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/DungeonManager.java
@@ -0,0 +1,315 @@
+package emu.grasscutter.game.dungeons;
+
+import emu.grasscutter.Grasscutter;
+import emu.grasscutter.data.GameData;
+import emu.grasscutter.data.common.ItemParamData;
+import emu.grasscutter.data.excels.DungeonData;
+import emu.grasscutter.data.excels.DungeonPassConfigData;
+import emu.grasscutter.game.activity.trialavatar.TrialAvatarActivityHandler;
+import emu.grasscutter.game.dungeons.dungeon_results.BaseDungeonResult;
+import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType;
+import emu.grasscutter.game.inventory.GameItem;
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.game.props.ActionReason;
+import emu.grasscutter.game.props.ActivityType;
+import emu.grasscutter.game.props.WatcherTriggerType;
+import emu.grasscutter.game.quest.enums.LogicType;
+import emu.grasscutter.game.quest.enums.QuestContent;
+import emu.grasscutter.game.world.Scene;
+import emu.grasscutter.scripts.constants.EventType;
+import emu.grasscutter.scripts.data.ScriptArgs;
+import emu.grasscutter.server.packet.send.PacketDungeonWayPointNotify;
+import emu.grasscutter.server.packet.send.PacketGadgetAutoPickDropInfoNotify;
+import emu.grasscutter.utils.Position;
+import emu.grasscutter.utils.Utils;
+import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
+import it.unimi.dsi.fastutil.ints.IntSet;
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.val;
+
+import javax.annotation.Nullable;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+/**
+ * TODO handle time limits
+ * TODO handle respawn points
+ * TODO handle team wipes and respawns
+ * TODO check monster level and levelConfigMap
+ */
+public class DungeonManager {
+
+ @Getter private final Scene scene;
+ @Getter private final DungeonData dungeonData;
+ @Getter private final DungeonPassConfigData passConfigData;
+
+ @Getter private final int[] finishedConditions;
+ private final IntSet rewardedPlayers = new IntOpenHashSet();
+ private final Set activeDungeonWayPoints = new HashSet<>();
+ private boolean ended = false;
+ private int newestWayPoint = 0;
+ @Getter private int startSceneTime = 0;
+
+ DungeonTrialTeam trialTeam = null;
+
+ public DungeonManager(@NonNull Scene scene, @NonNull DungeonData dungeonData) {
+ this.scene = scene;
+ this.dungeonData = dungeonData;
+ this.passConfigData = GameData.getDungeonPassConfigDataMap().get(dungeonData.getPassCond());
+ this.finishedConditions = new int[passConfigData.getConds().size()];
+ this.scene.setDungeonManager(this);
+ }
+
+ public void triggerEvent(DungeonPassConditionType conditionType, int... params) {
+ if (ended) {
+ return;
+ }
+ for (int i = 0; i < passConfigData.getConds().size(); i++) {
+ var cond = passConfigData.getConds().get(i);
+ if (conditionType == cond.getCondType()) {
+ if (getScene().getWorld().getServer().getDungeonSystem().triggerCondition(cond, params)) {
+ finishedConditions[i] = 1;
+ }
+
+ }
+ }
+
+ if (isFinishedSuccessfully()) {
+ finishDungeon();
+ }
+
+ }
+
+ public boolean isFinishedSuccessfully() {
+ return LogicType.calculate(passConfigData.getLogicType(), finishedConditions);
+ }
+
+ public int getLevelForMonster(int id) {
+ //TODO should use levelConfigMap? and how?
+ return dungeonData.getShowLevel();
+ }
+
+ public boolean activateRespawnPoint(int pointId) {
+ val respawnPoint = GameData.getScenePointEntryById(scene.getId(), pointId);
+
+ if (respawnPoint == null) {
+ Grasscutter.getLogger().warn("trying to activate unknown respawn point {}", pointId);
+ return false;
+ }
+
+ scene.broadcastPacket(new PacketDungeonWayPointNotify(activeDungeonWayPoints.add(pointId), activeDungeonWayPoints));
+ newestWayPoint = pointId;
+
+ Grasscutter.getLogger().debug("[unimplemented respawn] activated respawn point {}", pointId);
+ return true;
+ }
+
+ @Nullable
+ public Position getRespawnLocation() {
+ if (newestWayPoint == 0) { // validity is checked before setting it, so if != 0 its always valid
+ return null;
+ }
+ val pointData = GameData.getScenePointEntryById(scene.getId(), newestWayPoint).getPointData();
+ return pointData.getTranPos() != null ? pointData.getTranPos() : pointData.getPos();
+ }
+
+ public Position getRespawnRotation() {
+ if (newestWayPoint == 0) { // validity is checked before setting it, so if != 0 its always valid
+ return null;
+ }
+ val pointData = GameData.getScenePointEntryById(scene.getId(), newestWayPoint).getPointData();
+ return pointData.getRot() != null ? pointData.getRot() : null;
+ }
+
+ public boolean getStatueDrops(Player player, boolean useCondensed, int groupId) {
+ if (!isFinishedSuccessfully() || dungeonData.getRewardPreviewData() == null || dungeonData.getRewardPreviewData().getPreviewItems().length == 0) {
+ return false;
+ }
+
+ // Already rewarded
+ if (rewardedPlayers.contains(player.getUid())) {
+ return false;
+ }
+
+
+ if (!handleCost(player, useCondensed)) {
+ return false;
+ }
+
+ // Get and roll rewards.
+ List rewards = new ArrayList<>(this.rollRewards(useCondensed));
+ // Add rewards to player and send notification.
+ player.getInventory().addItems(rewards, ActionReason.DungeonStatueDrop);
+ player.sendPacket(new PacketGadgetAutoPickDropInfoNotify(rewards));
+
+ rewardedPlayers.add(player.getUid());
+
+ scene.getScriptManager().callEvent(new ScriptArgs(groupId, EventType.EVENT_DUNGEON_REWARD_GET));
+ return true;
+ }
+
+ public boolean handleCost(Player player, boolean useCondensed) {
+ int resinCost = dungeonData.getStatueCostCount() != 0 ? dungeonData.getStatueCostCount() : 20;
+ if (resinCost == 0) {
+ return true;
+ }
+ if (useCondensed) {
+ // Check if condensed resin is usable here.
+ // For this, we use the following logic for now:
+ // The normal resin cost of the dungeon has to be 20.
+ if (resinCost != 20) {
+ return false;
+ }
+
+ // Spend the condensed resin and only proceed if the transaction succeeds.
+ return player.getResinManager().useCondensedResin(1);
+ } else if (dungeonData.getStatueCostID() == 106) {
+ // Spend the resin and only proceed if the transaction succeeds.
+ return player.getResinManager().useResin(resinCost);
+ }
+ return true;
+ }
+
+ private List rollRewards(boolean useCondensed) {
+ List rewards = new ArrayList<>();
+ int dungeonId = this.dungeonData.getId();
+ // If we have specific drop data for this dungeon, we use it.
+ if (GameData.getDungeonDropDataMap().containsKey(dungeonId)) {
+ List dropEntries = GameData.getDungeonDropDataMap().get(dungeonId);
+
+ // Roll for each drop group.
+ for (var entry : dropEntries) {
+ // Determine the number of drops we get for this entry.
+ int start = entry.getCounts().get(0);
+ int end = entry.getCounts().get(entry.getCounts().size() - 1);
+ var candidateAmounts = IntStream.range(start, end + 1).boxed().collect(Collectors.toList());
+
+ int amount = Utils.drawRandomListElement(candidateAmounts, entry.getProbabilities());
+
+ if (useCondensed) {
+ amount += Utils.drawRandomListElement(candidateAmounts, entry.getProbabilities());
+ }
+
+ // Double rewards in multiplay mode, if specified.
+ if (entry.isMpDouble() && this.getScene().getPlayerCount() > 1) {
+ amount *= 2;
+ }
+
+ // Roll items for this group.
+ // Here, we have to handle stacking, or the client will not display results correctly.
+ // For now, we use the following logic: If the possible drop item are a list of multiple items,
+ // we roll them separately. If not, we stack them. This should work out in practice, at least
+ // for the currently existing set of dungeons.
+ if (entry.getItems().size() == 1) {
+ rewards.add(new GameItem(entry.getItems().get(0), amount));
+ } else {
+ for (int i = 0; i < amount; i++) {
+ // int itemIndex = ThreadLocalRandom.current().nextInt(0, entry.getItems().size());
+ // int itemId = entry.getItems().get(itemIndex);
+ int itemId = Utils.drawRandomListElement(entry.getItems(), entry.getItemProbabilities());
+ rewards.add(new GameItem(itemId, 1));
+ }
+ }
+ }
+ }
+ // Otherwise, we fall back to the preview data.
+ else {
+ Grasscutter.getLogger().info("No drop data found or dungeon {}, falling back to preview data ...", dungeonId);
+ for (ItemParamData param : dungeonData.getRewardPreviewData().getPreviewItems()) {
+ rewards.add(new GameItem(param.getId(), Math.max(param.getCount(), 1)));
+ }
+ }
+
+ return rewards;
+ }
+
+ public void applyTrialTeam(Player player) {
+ if (getDungeonData() == null) return;
+
+ switch (getDungeonData().getType()) {
+ // case DUNGEON_PLOT is handled by quest execs
+ case DUNGEON_ACTIVITY -> {
+ switch (getDungeonData().getPlayType()) {
+ case DUNGEON_PLAY_TYPE_TRIAL_AVATAR -> {
+ val activityHandler = player.getActivityManager()
+ .getActivityHandlerAs(ActivityType.NEW_ACTIVITY_TRIAL_AVATAR, TrialAvatarActivityHandler.class);
+ activityHandler.ifPresent(trialAvatarActivityHandler ->
+ this.trialTeam = trialAvatarActivityHandler.getTrialAvatarDungeonTeam());
+ }
+ }
+ }
+ case DUNGEON_ELEMENT_CHALLENGE -> {} // TODO
+ }
+ if(this.trialTeam != null) {
+ player.addTrialAvatarsForActivity(trialTeam.trialAvatarIds);
+ }
+ }
+
+ public void unsetTrialTeam(Player player){
+ if(this.trialTeam==null){
+ return;
+ }
+ player.removeTrialAvatarForActivity();
+ this.trialTeam = null;
+ }
+
+ public void startDungeon() {
+ this.startSceneTime = scene.getSceneTimeSeconds();
+ scene.getPlayers().forEach(p -> {
+ p.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_ENTER_DUNGEON, dungeonData.getId());
+ applyTrialTeam(p);
+ });
+ }
+
+ public void finishDungeon() {
+ notifyEndDungeon(true);
+ endDungeon(BaseDungeonResult.DungeonEndReason.COMPLETED);
+ }
+
+ public void notifyEndDungeon(boolean successfully) {
+ scene.getPlayers().forEach(p -> {
+ // Quest trigger
+ p.getQuestManager().queueEvent(successfully ?
+ QuestContent.QUEST_CONTENT_FINISH_DUNGEON : QuestContent.QUEST_CONTENT_FAIL_DUNGEON,
+ dungeonData.getId());
+
+ // Battle pass trigger
+ if (dungeonData.getType().isCountsToBattlepass() && successfully) {
+ p.getBattlePassManager().triggerMission(WatcherTriggerType.TRIGGER_FINISH_DUNGEON);
+ }
+ });
+ scene.getScriptManager().callEvent(new ScriptArgs(0, EventType.EVENT_DUNGEON_SETTLE, successfully ? 1 : 0));
+ }
+
+ public void quitDungeon() {
+ notifyEndDungeon(false);
+ endDungeon(BaseDungeonResult.DungeonEndReason.QUIT);
+ }
+
+ public void failDungeon() {
+ notifyEndDungeon(false);
+ endDungeon(BaseDungeonResult.DungeonEndReason.FAILED);
+ }
+
+ public void endDungeon(BaseDungeonResult.DungeonEndReason endReason) {
+ if (scene.getDungeonSettleListeners() != null) {
+ scene.getDungeonSettleListeners().forEach(o -> o.onDungeonSettle(this, endReason));
+ }
+ ended = true;
+ }
+
+ public void restartDungeon() {
+ this.scene.setKilledMonsterCount(0);
+ this.rewardedPlayers.clear();
+ Arrays.fill(finishedConditions, 0);
+ this.ended = false;
+ this.activeDungeonWayPoints.clear();
+ }
+
+ public void cleanUpScene() {
+ this.scene.setDungeonManager(null);
+ this.scene.setKilledMonsterCount(0);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/DungeonTrialTeam.java b/src/main/java/emu/grasscutter/game/dungeons/DungeonTrialTeam.java
new file mode 100644
index 000000000..74f1bea47
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/DungeonTrialTeam.java
@@ -0,0 +1,14 @@
+package emu.grasscutter.game.dungeons;
+
+import emu.grasscutter.net.proto.TrialAvatarGrantRecordOuterClass.TrialAvatarGrantRecord;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+@AllArgsConstructor
+public class DungeonTrialTeam {
+ List trialAvatarIds;
+ TrialAvatarGrantRecord.GrantReason grantReason;
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/DungeonValue.java b/src/main/java/emu/grasscutter/game/dungeons/DungeonValue.java
new file mode 100644
index 000000000..d9788cc41
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/DungeonValue.java
@@ -0,0 +1,11 @@
+package emu.grasscutter.game.dungeons;
+
+import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.RUNTIME)
+public @interface DungeonValue {
+ DungeonPassConditionType value();
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/enums/ChallengeCondType.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/enums/ChallengeCondType.java
new file mode 100644
index 000000000..fa5c38adc
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/enums/ChallengeCondType.java
@@ -0,0 +1,19 @@
+package emu.grasscutter.game.dungeons.challenge.enums;
+
+public enum ChallengeCondType {
+ CHALLENGE_COND_NONE, //00
+ CHALLENGE_COND_IN_TIME, //01
+ CHALLENGE_COND_ALL_TIME, //02
+ CHALLENGE_COND_KILL_COUNT, //03
+ CHALLENGE_COND_SURVIVE, //04
+ CHALLENGE_COND_TIME_INC, //05
+ CHALLENGE_COND_KILL_FAST, //06
+ CHALLENGE_COND_DOWN_LESS, //07
+ CHALLENGE_COND_BEATEN_LESS , //08
+ CHALLENGE_COND_UNNATURAL_COUNT , //09
+ CHALLENGE_COND_FROZEN_LESS , //10
+ CHALLENGE_COND_KILL_MONSTER , //11
+ CHALLENGE_COND_TRIGGER , //12
+ CHALLENGE_COND_GUARD_HP , //13
+ CHALLENGE_COND_TIME_DEC , //14
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/enums/ChallengeEventMarkType.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/enums/ChallengeEventMarkType.java
new file mode 100644
index 000000000..2674df680
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/enums/ChallengeEventMarkType.java
@@ -0,0 +1,9 @@
+package emu.grasscutter.game.dungeons.challenge.enums;
+
+public enum ChallengeEventMarkType {
+ CHALLENGE_EVENT_NONE,
+ FLIGHT_TIME,
+ FLIGHT_GATHER_POINT,
+ SUMMER_TIME_SPRINT_BOAT_TIME,
+ SUMMER_TIME_SPRINT_BOAT_GATHER_POINT,
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/enums/ChallengeRecordType.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/enums/ChallengeRecordType.java
new file mode 100644
index 000000000..c763903b1
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/enums/ChallengeRecordType.java
@@ -0,0 +1,6 @@
+package emu.grasscutter.game.dungeons.challenge.enums;
+
+public enum ChallengeRecordType {
+ CHALLENGE_RECORD_TYPE_NONE,
+ CHALLENGE_RECORD_TYPE_IN_TIME
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/enums/ChallengeType.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/enums/ChallengeType.java
new file mode 100644
index 000000000..c59dbd240
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/enums/ChallengeType.java
@@ -0,0 +1,27 @@
+package emu.grasscutter.game.dungeons.challenge.enums;
+
+public enum ChallengeType {
+ CHALLENGE_NONE, //00
+ CHALLENGE_KILL_COUNT, //01
+ CHALLENGE_KILL_COUNT_IN_TIME, //02
+ CHALLENGE_SURVIVE, //03
+ CHALLENGE_TIME_FLY, //04
+ CHALLENGE_KILL_COUNT_FAST, //05
+ CHALLENGE_KILL_COUNT_FROZEN_LESS, //06
+ CHALLENGE_KILL_MONSTER_IN_TIME, //07
+ CHALLENGE_TRIGGER_IN_TIME, //08
+ CHALLENGE_GUARD_HP, //09
+ CHALLENGE_KILL_COUNT_GUARD_HP, //10
+ CHALLENGE_TRIGGER_IN_TIME_FLY , //11
+ //unknown if position and time match from here on
+ CHALLENGE_TRIGGER2_AVOID_TRIGGER1,
+ CHALLENGE_FATHER_SUCC_IN_TIME,
+ CHALLENGE_MONSTER_DAMAGE_COUNT,
+ CHALLENGE_ELEMENT_REACTION_COUNT,
+ CHALLENGE_FREEZE_ENEMY_IN_TIME,
+ CHALLENGE_CRYSTAL_ELEMENT_REACTION_COUNT,
+ CHALLENGE_SHEILD_ABSORB_DAMAGE_COUNT,
+ CHALLENGE_SWIRL_ELEMENT_REACTION_COUNT,
+ CHALLENGE_DIE_LESS_IN_TIME,
+ CHALLENGE_TRIGGER_COUNT,
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/enums/FatherChallengeProperty.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/enums/FatherChallengeProperty.java
new file mode 100644
index 000000000..8a4763831
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/enums/FatherChallengeProperty.java
@@ -0,0 +1,9 @@
+package emu.grasscutter.game.dungeons.challenge.enums;
+
+public enum FatherChallengeProperty {
+ DURATION,
+ CUR_SUCC,
+ CUR_FAIL,
+ SUM_SUCC,
+ SUM_FAIL
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillAndGuardChallengeFactoryHandler.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillAndGuardChallengeFactoryHandler.java
new file mode 100644
index 000000000..69857f42e
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillAndGuardChallengeFactoryHandler.java
@@ -0,0 +1,34 @@
+package emu.grasscutter.game.dungeons.challenge.factory;
+
+import emu.grasscutter.game.dungeons.challenge.WorldChallenge;
+import emu.grasscutter.game.dungeons.challenge.enums.ChallengeType;
+import emu.grasscutter.game.dungeons.challenge.trigger.GuardTrigger;
+import emu.grasscutter.game.dungeons.challenge.trigger.KillMonsterCountTrigger;
+import emu.grasscutter.game.world.Scene;
+import emu.grasscutter.scripts.data.SceneGroup;
+import lombok.val;
+
+import java.util.List;
+
+import static emu.grasscutter.game.dungeons.challenge.enums.ChallengeType.CHALLENGE_KILL_COUNT_GUARD_HP;
+
+public class KillAndGuardChallengeFactoryHandler implements ChallengeFactoryHandler{
+ @Override
+ public boolean isThisType(ChallengeType challengeType) {
+ // ActiveChallenge with 1,188,234101003,12,3030,0
+ return challengeType == CHALLENGE_KILL_COUNT_GUARD_HP;
+ }
+
+ @Override /*TODO check param4 == monstesToKill*/
+ public WorldChallenge build(int challengeIndex, int challengeId, int groupId, int monstersToKill, int gadgetCFGId, int unused, Scene scene, SceneGroup group) {
+ val realGroup = scene.getScriptManager().getGroupById(groupId);
+ return new WorldChallenge(
+ scene, realGroup,
+ challengeId, // Id
+ challengeIndex, // Index
+ List.of(monstersToKill, 0),
+ 0, // Limit
+ monstersToKill, // Goal
+ List.of(new KillMonsterCountTrigger(), new GuardTrigger(gadgetCFGId)));
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillMonsterCountChallengeFactoryHandler.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillMonsterCountChallengeFactoryHandler.java
new file mode 100644
index 000000000..0866a38d7
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillMonsterCountChallengeFactoryHandler.java
@@ -0,0 +1,32 @@
+package emu.grasscutter.game.dungeons.challenge.factory;
+
+import emu.grasscutter.game.dungeons.challenge.WorldChallenge;
+import emu.grasscutter.game.dungeons.challenge.enums.ChallengeType;
+import emu.grasscutter.game.dungeons.challenge.trigger.KillMonsterCountTrigger;
+import emu.grasscutter.game.world.Scene;
+import emu.grasscutter.scripts.data.SceneGroup;
+import lombok.val;
+
+import java.util.List;
+
+public class KillMonsterCountChallengeFactoryHandler implements ChallengeFactoryHandler{
+ @Override
+ public boolean isThisType(ChallengeType challengeType) {
+ // ActiveChallenge with 1, 1, 241033003, 15, 0, 0
+ return challengeType == ChallengeType.CHALLENGE_KILL_COUNT;
+ }
+
+ @Override
+ public WorldChallenge build(int challengeIndex, int challengeId, int groupId, int goal, int param5, int param6, Scene scene, SceneGroup group) {
+ val realGroup = scene.getScriptManager().getGroupById(groupId);
+ return new WorldChallenge(
+ scene, realGroup,
+ challengeId, // Id
+ challengeIndex, // Index
+ List.of(goal, groupId),
+ 0, // Limit
+ goal, // Goal
+ List.of(new KillMonsterCountTrigger())
+ );
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillMonsterInTimeChallengeFactoryHandler.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillMonsterInTimeChallengeFactoryHandler.java
new file mode 100644
index 000000000..38b617ebb
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillMonsterInTimeChallengeFactoryHandler.java
@@ -0,0 +1,33 @@
+package emu.grasscutter.game.dungeons.challenge.factory;
+
+import emu.grasscutter.game.dungeons.challenge.WorldChallenge;
+import emu.grasscutter.game.dungeons.challenge.enums.ChallengeType;
+import emu.grasscutter.game.dungeons.challenge.trigger.InTimeTrigger;
+import emu.grasscutter.game.dungeons.challenge.trigger.KillMonsterTrigger;
+import emu.grasscutter.game.world.Scene;
+import emu.grasscutter.scripts.data.SceneGroup;
+import lombok.val;
+
+import java.util.List;
+
+public class KillMonsterInTimeChallengeFactoryHandler implements ChallengeFactoryHandler{
+ @Override
+ public boolean isThisType(ChallengeType challengeType) {
+ // ActiveChallenge with 180, 72, 240, 133220161, 133220161, 0
+ return challengeType == ChallengeType.CHALLENGE_KILL_MONSTER_IN_TIME;
+ }
+
+ @Override
+ public WorldChallenge build(int challengeIndex, int challengeId, int timeLimit, int groupId, int targetCfgId, int param6, Scene scene, SceneGroup group) {
+ val realGroup = scene.getScriptManager().getGroupById(groupId);
+ return new WorldChallenge(
+ scene, realGroup,
+ challengeId, // Id
+ challengeIndex, // Index
+ List.of(timeLimit),
+ timeLimit, // Limit
+ 0, // Goal
+ List.of(new KillMonsterTrigger(targetCfgId), new InTimeTrigger())
+ );
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillMonsterTimeChallengeFactoryHandler.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillMonsterTimeChallengeFactoryHandler.java
new file mode 100644
index 000000000..8ca87518b
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/KillMonsterTimeChallengeFactoryHandler.java
@@ -0,0 +1,35 @@
+package emu.grasscutter.game.dungeons.challenge.factory;
+
+import emu.grasscutter.game.dungeons.challenge.WorldChallenge;
+import emu.grasscutter.game.dungeons.challenge.enums.ChallengeType;
+import emu.grasscutter.game.dungeons.challenge.trigger.InTimeTrigger;
+import emu.grasscutter.game.dungeons.challenge.trigger.KillMonsterCountTrigger;
+import emu.grasscutter.game.world.Scene;
+import emu.grasscutter.scripts.data.SceneGroup;
+import lombok.val;
+
+import java.util.List;
+
+public class KillMonsterTimeChallengeFactoryHandler implements ChallengeFactoryHandler{
+ @Override
+ public boolean isThisType(ChallengeType challengeType) {
+ // ActiveChallenge with 180,180,45,133108061,1,0
+ // ActiveChallenge Fast with 1001, 5, 15, 240004005, 10, 0
+ return challengeType == ChallengeType.CHALLENGE_KILL_COUNT_IN_TIME ||
+ challengeType == ChallengeType.CHALLENGE_KILL_COUNT_FAST;
+ }
+
+ @Override
+ public WorldChallenge build(int challengeIndex, int challengeId, int timeLimit, int groupId, int targetCount, int param6, Scene scene, SceneGroup group) {
+ val realGroup = scene.getScriptManager().getGroupById(groupId);
+ return new WorldChallenge(
+ scene, realGroup,
+ challengeId, // Id
+ challengeIndex, // Index
+ List.of(targetCount, timeLimit),
+ timeLimit, // Limit
+ targetCount, // Goal
+ List.of(new KillMonsterCountTrigger(), new InTimeTrigger())
+ );
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/SurviveChallengeFactoryHandler.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/SurviveChallengeFactoryHandler.java
new file mode 100644
index 000000000..5a1934ff5
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/SurviveChallengeFactoryHandler.java
@@ -0,0 +1,33 @@
+package emu.grasscutter.game.dungeons.challenge.factory;
+
+import emu.grasscutter.game.dungeons.challenge.WorldChallenge;
+import emu.grasscutter.game.dungeons.challenge.enums.ChallengeType;
+import emu.grasscutter.game.dungeons.challenge.trigger.ForTimeTrigger;
+import emu.grasscutter.game.world.Scene;
+import emu.grasscutter.scripts.data.SceneGroup;
+
+import java.util.List;
+
+import static emu.grasscutter.game.dungeons.challenge.enums.ChallengeType.CHALLENGE_SURVIVE;
+
+public class SurviveChallengeFactoryHandler implements ChallengeFactoryHandler {
+ @Override
+ public boolean isThisType(ChallengeType challengeType) {
+ // grp 201055005
+ // ActiveChallenge with 100, 56, 60, 0, 0, 0
+ return challengeType == CHALLENGE_SURVIVE;
+ }
+
+ @Override
+ public WorldChallenge build(int challengeIndex, int challengeId, int timeToSurvive, int unused4, int unused5, int unused6, Scene scene, SceneGroup group) {
+ return new WorldChallenge(
+ scene, group,
+ challengeId, // Id
+ challengeIndex, // Index
+ List.of(timeToSurvive),
+ timeToSurvive, // Limit
+ 0, // Goal
+ List.of(new ForTimeTrigger())
+ );
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/TriggerInTimeChallengeFactoryHandler.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/TriggerInTimeChallengeFactoryHandler.java
new file mode 100644
index 000000000..15aeccbec
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/factory/TriggerInTimeChallengeFactoryHandler.java
@@ -0,0 +1,36 @@
+package emu.grasscutter.game.dungeons.challenge.factory;
+
+import emu.grasscutter.game.dungeons.challenge.WorldChallenge;
+import emu.grasscutter.game.dungeons.challenge.enums.ChallengeType;
+import emu.grasscutter.game.dungeons.challenge.trigger.InTimeTrigger;
+import emu.grasscutter.game.dungeons.challenge.trigger.TriggerGroupTriggerTrigger;
+import emu.grasscutter.game.world.Scene;
+import emu.grasscutter.scripts.data.SceneGroup;
+
+import java.util.List;
+
+import static emu.grasscutter.game.dungeons.challenge.enums.ChallengeType.CHALLENGE_TRIGGER_IN_TIME;
+
+public class TriggerInTimeChallengeFactoryHandler implements ChallengeFactoryHandler {
+ @Override
+ public boolean isThisType(ChallengeType challengeType) {
+ // kill gadgets(explosive barrel) in time
+ // ActiveChallenge with 56,201,20,2,201,4
+ // open chest in time
+ // ActiveChallenge with 666,202,30,7,202,1
+ return challengeType == CHALLENGE_TRIGGER_IN_TIME;
+ }
+
+ @Override
+ public WorldChallenge build(int challengeIndex, int challengeId, int timeLimit, int param4, int triggerTag, int triggerCount, Scene scene, SceneGroup group) {
+ return new WorldChallenge(
+ scene, group,
+ challengeId, // Id
+ challengeIndex, // Index
+ List.of(timeLimit, triggerCount),
+ timeLimit, // Limit
+ triggerCount, // Goal
+ List.of(new InTimeTrigger(), new TriggerGroupTriggerTrigger(Integer.toString(triggerTag)))
+ );
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/ForTimeTrigger.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/ForTimeTrigger.java
new file mode 100644
index 000000000..6b272c34b
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/ForTimeTrigger.java
@@ -0,0 +1,13 @@
+package emu.grasscutter.game.dungeons.challenge.trigger;
+
+import emu.grasscutter.game.dungeons.challenge.WorldChallenge;
+
+public class ForTimeTrigger extends ChallengeTrigger{
+ @Override
+ public void onCheckTimeout(WorldChallenge challenge) {
+ var current = challenge.getScene().getSceneTimeSeconds();
+ if(current - challenge.getStartedAt() > challenge.getTimeLimit()){
+ challenge.done();
+ }
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/KillMonsterCountTrigger.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/KillMonsterCountTrigger.java
new file mode 100644
index 000000000..cc1b02cf0
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/KillMonsterCountTrigger.java
@@ -0,0 +1,23 @@
+package emu.grasscutter.game.dungeons.challenge.trigger;
+
+import emu.grasscutter.game.dungeons.challenge.WorldChallenge;
+import emu.grasscutter.game.entity.EntityMonster;
+import emu.grasscutter.server.packet.send.PacketChallengeDataNotify;
+
+public class KillMonsterCountTrigger extends ChallengeTrigger{
+ @Override
+ public void onBegin(WorldChallenge challenge) {
+ challenge.getScene().broadcastPacket(new PacketChallengeDataNotify(challenge, 1, challenge.getScore().get()));
+ }
+
+ @Override
+ public void onMonsterDeath(WorldChallenge challenge, EntityMonster monster) {
+ var newScore = challenge.increaseScore();
+ challenge.getScene().broadcastPacket(new PacketChallengeDataNotify(challenge, 1, newScore));
+
+ if(newScore >= challenge.getGoal()){
+ challenge.done();
+ }
+
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/TriggerGroupTriggerTrigger.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/TriggerGroupTriggerTrigger.java
new file mode 100644
index 000000000..82aae8140
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/trigger/TriggerGroupTriggerTrigger.java
@@ -0,0 +1,30 @@
+package emu.grasscutter.game.dungeons.challenge.trigger;
+
+import emu.grasscutter.game.dungeons.challenge.WorldChallenge;
+import emu.grasscutter.game.entity.EntityGadget;
+import emu.grasscutter.scripts.data.SceneTrigger;
+import emu.grasscutter.server.packet.send.PacketChallengeDataNotify;
+import lombok.AllArgsConstructor;
+
+@AllArgsConstructor
+public class TriggerGroupTriggerTrigger extends ChallengeTrigger{
+ String triggerTag;
+
+ @Override
+ public void onBegin(WorldChallenge challenge) {
+ challenge.getScene().broadcastPacket(new PacketChallengeDataNotify(challenge, 2, challenge.getScore().get()));
+ }
+
+ @Override
+ public void onGroupTrigger(WorldChallenge challenge, SceneTrigger trigger) {
+ if(!triggerTag.equals(trigger.getTag())) {
+ return;
+ }
+
+ var newScore = challenge.increaseScore();
+ challenge.getScene().broadcastPacket(new PacketChallengeDataNotify(challenge, 2, newScore));
+ if(newScore >= challenge.getGoal()){
+ challenge.done();
+ }
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/dungeon_results/BaseDungeonResult.java b/src/main/java/emu/grasscutter/game/dungeons/dungeon_results/BaseDungeonResult.java
new file mode 100644
index 000000000..9cf616159
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/dungeon_results/BaseDungeonResult.java
@@ -0,0 +1,74 @@
+package emu.grasscutter.game.dungeons.dungeon_results;
+
+import emu.grasscutter.data.excels.DungeonData;
+import emu.grasscutter.game.dungeons.DungeonEndStats;
+import emu.grasscutter.net.proto.DungeonSettleNotifyOuterClass.DungeonSettleNotify;
+import emu.grasscutter.net.proto.ParamListOuterClass;
+import emu.grasscutter.utils.Utils;
+import lombok.Getter;
+
+public class BaseDungeonResult {
+ @Getter DungeonData dungeonData;
+ @Getter
+ DungeonEndStats dungeonStats;
+
+ public BaseDungeonResult(DungeonData dungeonData, DungeonEndStats dungeonStats){
+ this.dungeonData = dungeonData;
+ this.dungeonStats = dungeonStats;
+ }
+
+ protected void onProto(DungeonSettleNotify.Builder builder){ }
+
+ public final DungeonSettleNotify.Builder getProto(){
+ var success = dungeonStats.getDungeonResult().isSuccess();
+ var builder = DungeonSettleNotify.newBuilder()
+ .setDungeonId(dungeonData.getId())
+ .setIsSuccess(success)
+ .setCloseTime(getCloseTime())
+ .setResult(success ? 1 : 0);
+
+ // TODO check
+ if(dungeonData.getSettleShows()!=null) {
+ for (int i = 0; i < dungeonData.getSettleShows().size(); i++) {
+ var settle = dungeonData.getSettleShows().get(i);
+ builder.putSettleShow(i + 1,switch (settle) {
+ case SETTLE_SHOW_TIME_COST -> ParamListOuterClass.ParamList.newBuilder()
+ .addParamList(settle.getId())
+ .addParamList(dungeonStats.getTimeTaken())
+ .build();
+ case SETTLE_SHOW_KILL_MONSTER_COUNT -> ParamListOuterClass.ParamList.newBuilder()
+ .addParamList(settle.getId())
+ .addParamList(dungeonStats.getKilledMonsters())
+ .build();
+ default -> ParamListOuterClass.ParamList.newBuilder()
+ .addParamList(settle.getId())
+ .build();
+ });
+ }
+ }
+
+ //TODO handle settle show
+
+ onProto(builder);
+
+ return builder;
+ }
+
+ public int getCloseTime(){
+ return Utils.getCurrentSeconds() + switch (dungeonStats.getDungeonResult()){
+ case COMPLETED -> dungeonData.getSettleCountdownTime();
+ case FAILED -> dungeonData.getFailSettleCountdownTime();
+ case QUIT -> dungeonData.getQuitSettleCountdownTime();
+ };
+ }
+
+ public enum DungeonEndReason{
+ COMPLETED,
+ FAILED,
+ QUIT;
+
+ public boolean isSuccess(){
+ return this == COMPLETED;
+ }
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/dungeon_results/TowerResult.java b/src/main/java/emu/grasscutter/game/dungeons/dungeon_results/TowerResult.java
new file mode 100644
index 000000000..46543a9bd
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/dungeon_results/TowerResult.java
@@ -0,0 +1,50 @@
+package emu.grasscutter.game.dungeons.dungeon_results;
+
+import emu.grasscutter.data.excels.DungeonData;
+import emu.grasscutter.game.dungeons.DungeonEndStats;
+import emu.grasscutter.game.dungeons.challenge.WorldChallenge;
+import emu.grasscutter.game.tower.TowerManager;
+import emu.grasscutter.net.proto.DungeonSettleExhibitionInfoOuterClass;
+import emu.grasscutter.net.proto.DungeonSettleNotifyOuterClass;
+import emu.grasscutter.net.proto.ItemParamOuterClass;
+import emu.grasscutter.net.proto.TowerLevelEndNotifyOuterClass.TowerLevelEndNotify.ContinueStateType;
+import emu.grasscutter.net.proto.TowerLevelEndNotifyOuterClass.TowerLevelEndNotify;
+
+public class TowerResult extends BaseDungeonResult{
+ WorldChallenge challenge;
+ boolean canJump;
+ boolean hasNextLevel;
+ int nextFloorId;
+ public TowerResult(DungeonData dungeonData, DungeonEndStats dungeonStats, TowerManager towerManager, WorldChallenge challenge) {
+ super(dungeonData, dungeonStats);
+ this.challenge = challenge;
+ this.canJump = towerManager.hasNextFloor();
+ this.hasNextLevel = towerManager.hasNextLevel();
+ this.nextFloorId = hasNextLevel ? 0 : towerManager.getNextFloorId();
+ }
+
+ @Override
+ protected void onProto(DungeonSettleNotifyOuterClass.DungeonSettleNotify.Builder builder) {
+ var continueStatus = ContinueStateType.CONTINUE_STATE_TYPE_CAN_NOT_CONTINUE_VALUE;
+ if(challenge.isSuccess() && canJump){
+ continueStatus = hasNextLevel ? ContinueStateType.CONTINUE_STATE_TYPE_CAN_ENTER_NEXT_LEVEL_VALUE
+ : ContinueStateType.CONTINUE_STATE_TYPE_CAN_ENTER_NEXT_FLOOR_VALUE;
+ }
+
+ var towerLevelEndNotify = TowerLevelEndNotify.newBuilder()
+ .setIsSuccess(challenge.isSuccess())
+ .setContinueState(continueStatus)
+ .addFinishedStarCondList(1)
+ .addFinishedStarCondList(2)
+ .addFinishedStarCondList(3)
+ .addRewardItemList(ItemParamOuterClass.ItemParam.newBuilder()
+ .setItemId(201)
+ .setCount(1000)
+ .build())
+ ;
+ if(nextFloorId > 0 && canJump){
+ towerLevelEndNotify.setNextFloorId(nextFloorId);
+ }
+ builder.setTowerLevelEndNotify(towerLevelEndNotify);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/dungeon_results/TrialAvatarDungeonResult.java b/src/main/java/emu/grasscutter/game/dungeons/dungeon_results/TrialAvatarDungeonResult.java
new file mode 100644
index 000000000..298925a8f
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/dungeon_results/TrialAvatarDungeonResult.java
@@ -0,0 +1,23 @@
+package emu.grasscutter.game.dungeons.dungeon_results;
+
+import emu.grasscutter.data.excels.DungeonData;
+import emu.grasscutter.game.dungeons.DungeonEndStats;
+import emu.grasscutter.net.proto.DungeonSettleNotifyOuterClass;
+import emu.grasscutter.net.proto.TrialAvatarFirstPassDungeonNotifyOuterClass.TrialAvatarFirstPassDungeonNotify;
+
+public class TrialAvatarDungeonResult extends BaseDungeonResult {
+ int trialCharacterIndexId;
+
+ public TrialAvatarDungeonResult(DungeonData dungeonData, DungeonEndStats dungeonStats, int trialCharacterIndexId) {
+ super(dungeonData, dungeonStats);
+ this.trialCharacterIndexId = trialCharacterIndexId;
+ }
+
+ @Override
+ protected void onProto(DungeonSettleNotifyOuterClass.DungeonSettleNotify.Builder builder) {
+ if (dungeonStats.getDungeonResult() == DungeonEndReason.COMPLETED) { //TODO check if its the first pass(?)
+ builder.setTrialAvatarFirstPassDungeonNotify(TrialAvatarFirstPassDungeonNotify.newBuilder()
+ .setTrialAvatarIndexId(trialCharacterIndexId));
+ }
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/enums/DungeonEntrySatisfiedConditionType.java b/src/main/java/emu/grasscutter/game/dungeons/enums/DungeonEntrySatisfiedConditionType.java
new file mode 100644
index 000000000..fce7da285
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/enums/DungeonEntrySatisfiedConditionType.java
@@ -0,0 +1,7 @@
+package emu.grasscutter.game.dungeons.enums;
+
+public enum DungeonEntrySatisfiedConditionType {
+ DUNGEON_ENTRY_CONDITION_NONE,
+ DUNGEON_ENTRY_CONDITION_LEVEL,
+ DUNGEON_ENTRY_CONDITION_QUEST
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/enums/DungeonInvolveType.java b/src/main/java/emu/grasscutter/game/dungeons/enums/DungeonInvolveType.java
new file mode 100644
index 000000000..2e32e3496
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/enums/DungeonInvolveType.java
@@ -0,0 +1,7 @@
+package emu.grasscutter.game.dungeons.enums;
+
+public enum DungeonInvolveType {
+ INVOLVE_NONE,
+ INVOLVE_ONLY_SINGLE,
+ INVOLVE_SINGLE_MULTIPLE
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/enums/DungeonPassConditionType.java b/src/main/java/emu/grasscutter/game/dungeons/enums/DungeonPassConditionType.java
new file mode 100644
index 000000000..33bac9fd3
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/enums/DungeonPassConditionType.java
@@ -0,0 +1,27 @@
+package emu.grasscutter.game.dungeons.enums;
+
+import emu.grasscutter.scripts.constants.IntValueEnum;
+import lombok.Getter;
+
+public enum DungeonPassConditionType implements IntValueEnum {
+ DUNGEON_COND_NONE(0),
+ DUNGEON_COND_KILL_MONSTER(3),
+ DUNGEON_COND_KILL_GROUP_MONSTER(5),
+ DUNGEON_COND_KILL_TYPE_MONSTER(7),
+ DUNGEON_COND_FINISH_QUEST(9),
+ DUNGEON_COND_KILL_MONSTER_COUNT(11), // TODO handle count
+ DUNGEON_COND_IN_TIME(13), // Missing triggers and tracking
+ DUNGEON_COND_FINISH_CHALLENGE(14),
+ DUNGEON_COND_END_MULTISTAGE_PLAY(15) // Missing
+ ;
+
+ @Getter private final int id;
+ DungeonPassConditionType(int id){
+ this.id = id;
+ }
+
+ @Override
+ public int getValue() {
+ return id;
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/enums/DungeonPlayType.java b/src/main/java/emu/grasscutter/game/dungeons/enums/DungeonPlayType.java
new file mode 100644
index 000000000..e2d287894
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/enums/DungeonPlayType.java
@@ -0,0 +1,7 @@
+package emu.grasscutter.game.dungeons.enums;
+public enum DungeonPlayType {
+ DUNGEON_PLAY_TYPE_NONE,
+ DUNGEON_PLAY_TYPE_FOGGY_MAZE,
+ DUNGEON_PLAY_TYPE_MIST_TRIAL,
+ DUNGEON_PLAY_TYPE_TRIAL_AVATAR
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/enums/DungeonSubType.java b/src/main/java/emu/grasscutter/game/dungeons/enums/DungeonSubType.java
new file mode 100644
index 000000000..111b9e2f3
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/enums/DungeonSubType.java
@@ -0,0 +1,9 @@
+package emu.grasscutter.game.dungeons.enums;
+
+public enum DungeonSubType {
+ DUNGEON_SUB_NONE,
+ DUNGEON_SUB_BOSS,
+ DUNGEON_SUB_TALENT,
+ DUNGEON_SUB_WEAPON,
+ DUNGEON_SUB_RELIQUARY
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/enums/DungeonType.java b/src/main/java/emu/grasscutter/game/dungeons/enums/DungeonType.java
new file mode 100644
index 000000000..0864c2dce
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/enums/DungeonType.java
@@ -0,0 +1,49 @@
+package emu.grasscutter.game.dungeons.enums;
+
+import lombok.Getter;
+
+public enum DungeonType {
+ DUNGEON_NONE(false),
+ DUNGEON_PLOT(true),
+ DUNGEON_FIGHT(true),
+ DUNGEON_DAILY_FIGHT(false),
+ DUNGEON_WEEKLY_FIGHT(true),
+ DUNGEON_DISCARDED(false),
+ DUNGEON_TOWER(false),
+ DUNGEON_BOSS(true),
+ DUNGEON_ACTIVITY(false),
+ DUNGEON_EFFIGY(false),
+ DUNGEON_ELEMENT_CHALLENGE(true),
+ DUNGEON_THEATRE_MECHANICUS(false),
+ DUNGEON_FLEUR_FAIR(false),
+ DUNGEON_CHANNELLER_SLAB_LOOP(false),
+ DUNGEON_CHANNELLER_SLAB_ONE_OFF(false),
+ DUNGEON_BLITZ_RUSH(true),
+ DUNGEON_CHESS(false),
+ DUNGEON_SUMO_COMBAT(false),
+ DUNGEON_ROGUELIKE(false),
+ DUNGEON_HACHI(false),
+ DUNGEON_POTION(false),
+ DUNGEON_MINI_ELDRITCH(false),
+ DUNGEON_UGC(false),
+ DUNGEON_GCG(false),
+ DUNGEON_CRYSTAL_LINK(false),
+ DUNGEON_IRODORI_CHESS(false),
+ DUNGEON_ROGUE_DIARY(false),
+ DUNGEON_DREAMLAND(false),
+ DUNGEON_SUMMER_V2(true),
+ DUNGEON_MUQADAS_POTION(false),
+ DUNGEON_INSTABLE_SPRAY(false),
+ DUNGEON_WIND_FIELD(false),
+ DUNGEON_BIGWORLD_MIRROR(false),
+ DUNGEON_FUNGUS_FIGHTER_TRAINING(false),
+ DUNGEON_FUNGUS_FIGHTER_PLOT(false),
+ DUNGEON_EFFIGY_CHALLENGE_V2(false),
+ DUNGEON_CHAR_AMUSEMENT(false);
+
+ @Getter private final boolean countsToBattlepass;
+
+ DungeonType(boolean countsToBattlepass){
+ this.countsToBattlepass = countsToBattlepass;
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/enums/DungunEntryType.java b/src/main/java/emu/grasscutter/game/dungeons/enums/DungunEntryType.java
new file mode 100644
index 000000000..5b7295c58
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/enums/DungunEntryType.java
@@ -0,0 +1,12 @@
+package emu.grasscutter.game.dungeons.enums;
+
+public enum DungunEntryType {
+ DUNGEN_ENTRY_TYPE_NONE ,
+ DUNGEN_ENTRY_TYPE_AVATAR_EXP ,
+ DUNGEN_ENTRY_TYPE_WEAPON_PROMOTE,
+ DUNGEN_ENTRY_TYPE_AVATAR_TALENT ,
+ DUNGEN_ENTRY_TYPE_RELIQUARY ,
+ DUNGEN_ENTRY_TYPE_SCOIN ,
+ DUNGEON_ENTRY_TYPE_OBSCURAE ,
+ DUNGEON_ENTRY_TYPE_NORMAL
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/enums/SettleShowType.java b/src/main/java/emu/grasscutter/game/dungeons/enums/SettleShowType.java
new file mode 100644
index 000000000..3d5af54a8
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/enums/SettleShowType.java
@@ -0,0 +1,17 @@
+package emu.grasscutter.game.dungeons.enums;
+
+import lombok.Getter;
+
+public enum SettleShowType {
+ SETTLE_SHOW_NONE(0),
+ SETTLE_SHOW_TIME_COST(1),
+ SETTLE_SHOW_OPEN_CHEST_COUNT(2),
+ SETTLE_SHOW_KILL_MONSTER_COUNT(3),
+ SETTLE_SHOW_BLACKSCREEN(4);
+
+ @Getter private final int id;
+
+ SettleShowType(int id){
+ this.id = id;
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/handlers/DungeonBaseHandler.java b/src/main/java/emu/grasscutter/game/dungeons/handlers/DungeonBaseHandler.java
new file mode 100644
index 000000000..7c02b6199
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/handlers/DungeonBaseHandler.java
@@ -0,0 +1,9 @@
+package emu.grasscutter.game.dungeons.handlers;
+
+import emu.grasscutter.data.excels.DungeonPassConfigData;
+
+public abstract class DungeonBaseHandler {
+
+ public abstract boolean execute(DungeonPassConfigData.DungeonPassCondition condition, int... params);
+
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/pass_condition/BaseCondition.java b/src/main/java/emu/grasscutter/game/dungeons/pass_condition/BaseCondition.java
new file mode 100644
index 000000000..edc795d41
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/pass_condition/BaseCondition.java
@@ -0,0 +1,17 @@
+package emu.grasscutter.game.dungeons.pass_condition;
+
+import emu.grasscutter.data.excels.DungeonPassConfigData;
+import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType;
+import emu.grasscutter.game.dungeons.DungeonValue;
+import emu.grasscutter.game.dungeons.handlers.DungeonBaseHandler;
+
+@DungeonValue(DungeonPassConditionType.DUNGEON_COND_NONE)
+public class BaseCondition extends DungeonBaseHandler {
+
+ @Override
+ public boolean execute(DungeonPassConfigData.DungeonPassCondition condition, int... params) {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/pass_condition/ConditionFinishChallenge.java b/src/main/java/emu/grasscutter/game/dungeons/pass_condition/ConditionFinishChallenge.java
new file mode 100644
index 000000000..40dd40e29
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/pass_condition/ConditionFinishChallenge.java
@@ -0,0 +1,15 @@
+package emu.grasscutter.game.dungeons.pass_condition;
+
+import emu.grasscutter.data.excels.DungeonPassConfigData;
+import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType;
+import emu.grasscutter.game.dungeons.DungeonValue;
+import emu.grasscutter.game.dungeons.handlers.DungeonBaseHandler;
+
+@DungeonValue(DungeonPassConditionType.DUNGEON_COND_FINISH_CHALLENGE)
+public class ConditionFinishChallenge extends DungeonBaseHandler {
+
+ @Override
+ public boolean execute(DungeonPassConfigData.DungeonPassCondition condition, int... params) {
+ return params[0] == condition.getParam()[0] || params[1] == condition.getParam()[0];
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/pass_condition/ConditionFinishQuest.java b/src/main/java/emu/grasscutter/game/dungeons/pass_condition/ConditionFinishQuest.java
new file mode 100644
index 000000000..0e18dddeb
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/pass_condition/ConditionFinishQuest.java
@@ -0,0 +1,15 @@
+package emu.grasscutter.game.dungeons.pass_condition;
+
+import emu.grasscutter.data.excels.DungeonPassConfigData;
+import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType;
+import emu.grasscutter.game.dungeons.DungeonValue;
+import emu.grasscutter.game.dungeons.handlers.DungeonBaseHandler;
+
+@DungeonValue(DungeonPassConditionType.DUNGEON_COND_FINISH_QUEST)
+public class ConditionFinishQuest extends DungeonBaseHandler {
+
+ @Override
+ public boolean execute(DungeonPassConfigData.DungeonPassCondition condition, int... params) {
+ return params[0] == condition.getParam()[0];
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/pass_condition/ConditionInTime.java b/src/main/java/emu/grasscutter/game/dungeons/pass_condition/ConditionInTime.java
new file mode 100644
index 000000000..b387cde26
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/pass_condition/ConditionInTime.java
@@ -0,0 +1,15 @@
+package emu.grasscutter.game.dungeons.pass_condition;
+
+import emu.grasscutter.data.excels.DungeonPassConfigData;
+import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType;
+import emu.grasscutter.game.dungeons.DungeonValue;
+import emu.grasscutter.game.dungeons.handlers.DungeonBaseHandler;
+
+@DungeonValue(DungeonPassConditionType.DUNGEON_COND_IN_TIME)
+public class ConditionInTime extends DungeonBaseHandler {
+
+ @Override
+ public boolean execute(DungeonPassConfigData.DungeonPassCondition condition, int... params) {
+ return params[0] <= condition.getParam()[0];
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/pass_condition/ConditionKillGroupMonster.java b/src/main/java/emu/grasscutter/game/dungeons/pass_condition/ConditionKillGroupMonster.java
new file mode 100644
index 000000000..a72eef9da
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/pass_condition/ConditionKillGroupMonster.java
@@ -0,0 +1,15 @@
+package emu.grasscutter.game.dungeons.pass_condition;
+
+import emu.grasscutter.data.excels.DungeonPassConfigData;
+import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType;
+import emu.grasscutter.game.dungeons.DungeonValue;
+import emu.grasscutter.game.dungeons.handlers.DungeonBaseHandler;
+
+@DungeonValue(DungeonPassConditionType.DUNGEON_COND_KILL_GROUP_MONSTER)
+public class ConditionKillGroupMonster extends DungeonBaseHandler {
+
+ @Override
+ public boolean execute(DungeonPassConfigData.DungeonPassCondition condition, int... params) {
+ return params[0] == condition.getParam()[0];
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/pass_condition/ConditionKillMonster.java b/src/main/java/emu/grasscutter/game/dungeons/pass_condition/ConditionKillMonster.java
new file mode 100644
index 000000000..8faea3740
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/pass_condition/ConditionKillMonster.java
@@ -0,0 +1,15 @@
+package emu.grasscutter.game.dungeons.pass_condition;
+
+import emu.grasscutter.data.excels.DungeonPassConfigData;
+import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType;
+import emu.grasscutter.game.dungeons.DungeonValue;
+import emu.grasscutter.game.dungeons.handlers.DungeonBaseHandler;
+
+@DungeonValue(DungeonPassConditionType.DUNGEON_COND_KILL_MONSTER)
+public class ConditionKillMonster extends DungeonBaseHandler {
+
+ @Override
+ public boolean execute(DungeonPassConfigData.DungeonPassCondition condition, int... params) {
+ return params[0] == condition.getParam()[0];
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/pass_condition/ConditionKillMonsterCount.java b/src/main/java/emu/grasscutter/game/dungeons/pass_condition/ConditionKillMonsterCount.java
new file mode 100644
index 000000000..e12342a52
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/pass_condition/ConditionKillMonsterCount.java
@@ -0,0 +1,15 @@
+package emu.grasscutter.game.dungeons.pass_condition;
+
+import emu.grasscutter.data.excels.DungeonPassConfigData;
+import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType;
+import emu.grasscutter.game.dungeons.DungeonValue;
+import emu.grasscutter.game.dungeons.handlers.DungeonBaseHandler;
+
+@DungeonValue(DungeonPassConditionType.DUNGEON_COND_KILL_MONSTER_COUNT)
+public class ConditionKillMonsterCount extends DungeonBaseHandler {
+
+ @Override
+ public boolean execute(DungeonPassConfigData.DungeonPassCondition condition, int... params) {
+ return params[0] >= condition.getParam()[0];
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/dungeons/pass_condition/ConditionKillTypeMonster.java b/src/main/java/emu/grasscutter/game/dungeons/pass_condition/ConditionKillTypeMonster.java
new file mode 100644
index 000000000..c5a834528
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/dungeons/pass_condition/ConditionKillTypeMonster.java
@@ -0,0 +1,15 @@
+package emu.grasscutter.game.dungeons.pass_condition;
+
+import emu.grasscutter.data.excels.DungeonPassConfigData;
+import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType;
+import emu.grasscutter.game.dungeons.DungeonValue;
+import emu.grasscutter.game.dungeons.handlers.DungeonBaseHandler;
+
+@DungeonValue(DungeonPassConditionType.DUNGEON_COND_KILL_TYPE_MONSTER)
+public class ConditionKillTypeMonster extends DungeonBaseHandler {
+
+ @Override
+ public boolean execute(DungeonPassConfigData.DungeonPassCondition condition, int... params) {
+ return params[0] == condition.getParam()[0];
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/entity/gadget/GadgetAbility.java b/src/main/java/emu/grasscutter/game/entity/gadget/GadgetAbility.java
new file mode 100644
index 000000000..30f03e574
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/entity/gadget/GadgetAbility.java
@@ -0,0 +1,39 @@
+package emu.grasscutter.game.entity.gadget;
+
+import java.util.Arrays;
+
+import emu.grasscutter.game.entity.EntityClientGadget;
+import emu.grasscutter.game.entity.EntityGadget;
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.net.proto.AbilityGadgetInfoOuterClass;
+import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq;
+import emu.grasscutter.net.proto.SceneGadgetInfoOuterClass.SceneGadgetInfo;
+import lombok.val;
+
+public class GadgetAbility extends GadgetContent {
+ private EntityClientGadget parent;
+
+ public GadgetAbility(EntityGadget gadget, EntityClientGadget parent) {
+ super(gadget);
+ this.parent = parent;
+ }
+
+ public boolean onInteract(Player player, GadgetInteractReq req) {
+ return false;
+ }
+
+ public void onBuildProto(SceneGadgetInfo.Builder gadgetInfo) {
+ if (this.parent == null) {
+ return;
+ }
+
+ val abilityGadgetInfo = AbilityGadgetInfoOuterClass.AbilityGadgetInfo.newBuilder()
+ .setCampId(parent.getCampId())
+ .setCampTargetType(parent.getCampType())
+ .setTargetEntityId(parent.getId())
+ .build();
+
+ gadgetInfo.setAbilityGadget(abilityGadgetInfo);
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/game/entity/gadget/platform/AbilityRoute.java b/src/main/java/emu/grasscutter/game/entity/gadget/platform/AbilityRoute.java
new file mode 100644
index 000000000..44f07cb0b
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/entity/gadget/platform/AbilityRoute.java
@@ -0,0 +1,28 @@
+package emu.grasscutter.game.entity.gadget.platform;
+
+import emu.grasscutter.net.proto.MathQuaternionOuterClass.MathQuaternion;
+import emu.grasscutter.net.proto.MovingPlatformTypeOuterClass;
+import emu.grasscutter.net.proto.PlatformInfoOuterClass;
+import emu.grasscutter.utils.Position;
+
+/**
+ * TODO mostly hardcoded for EntitySolarIsotomaElevatorPlatform, should be more generic
+ */
+public class AbilityRoute extends BaseRoute {
+
+ private final Position basePosition;
+
+ public AbilityRoute(Position startRot, boolean startRoute, boolean isActive, Position basePosition) {
+ super(startRot, startRoute, isActive);
+ this.basePosition = basePosition;
+ }
+
+ @Override
+ public PlatformInfoOuterClass.PlatformInfo.Builder toProto() {
+ return super.toProto()
+ .setStartRot(MathQuaternion.newBuilder().setW(1.0F))
+ .setPosOffset(basePosition.toProto())
+ .setRotOffset(MathQuaternion.newBuilder().setW(1.0F))
+ .setMovingPlatformType(MovingPlatformTypeOuterClass.MovingPlatformType.MOVING_PLATFORM_TYPE_ABILITY);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/entity/gadget/platform/BaseRoute.java b/src/main/java/emu/grasscutter/game/entity/gadget/platform/BaseRoute.java
new file mode 100644
index 000000000..6ea1ddd58
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/entity/gadget/platform/BaseRoute.java
@@ -0,0 +1,84 @@
+package emu.grasscutter.game.entity.gadget.platform;
+
+import emu.grasscutter.game.world.Scene;
+import emu.grasscutter.net.proto.MathQuaternionOuterClass.MathQuaternion;
+import emu.grasscutter.net.proto.PlatformInfoOuterClass.PlatformInfo;
+import emu.grasscutter.scripts.data.SceneGadget;
+import emu.grasscutter.utils.Position;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.val;
+
+public abstract class BaseRoute {
+ @Getter @Setter private boolean isStarted;
+ @Getter @Setter private boolean isActive;
+ @Getter @Setter private Position startRot;
+ @Getter @Setter private int startSceneTime;
+ @Getter @Setter private int stopSceneTime;
+
+ BaseRoute(Position startRot, boolean isStarted, boolean isActive) {
+ this.startRot = startRot;
+ this.isStarted = isStarted;
+ this.isActive = isActive;
+ }
+
+ BaseRoute(SceneGadget gadget) {
+ this.startRot = gadget.rot;
+ this.isStarted = gadget.start_route;
+ this.isActive = gadget.start_route;
+ }
+
+ public static BaseRoute fromSceneGadget(SceneGadget sceneGadget) {
+ if (sceneGadget.route_id != 0) {
+ return new ConfigRoute(sceneGadget);
+ } else if (sceneGadget.is_use_point_array) {
+ return new PointArrayRoute(sceneGadget);
+ }
+ return null;
+ }
+
+ public boolean startRoute(Scene scene) {
+ if (this.isStarted) {
+ return false;
+ }
+ this.isStarted = true;
+ this.isActive = true;
+ this.startSceneTime = scene.getSceneTime()+300;
+
+ return true;
+ }
+
+ public boolean stopRoute(Scene scene) {
+ if (!this.isStarted) {
+ return false;
+ }
+ this.isStarted = false;
+ this.isActive = false;
+ this.startSceneTime = scene.getSceneTime();
+ this.stopSceneTime = scene.getSceneTime();
+
+ return true;
+ }
+
+ private MathQuaternion.Builder rotAsMathQuaternion() {
+ val result = MathQuaternion.newBuilder();
+ if (startRot != null) {
+ result.setX(startRot.getX())
+ .setY(startRot.getY())
+ .setZ(startRot.getZ());
+ }
+ return result;
+ }
+
+ public PlatformInfo.Builder toProto() {
+ val result = PlatformInfo.newBuilder()
+ .setIsStarted(isStarted)
+ .setIsActive(isActive)
+ .setStartRot(rotAsMathQuaternion())
+ .setStartSceneTime(startSceneTime);
+ if (!isStarted) {
+ result.setStopSceneTime(stopSceneTime);
+ }
+ return result;
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/entity/gadget/platform/ConfigRoute.java b/src/main/java/emu/grasscutter/game/entity/gadget/platform/ConfigRoute.java
new file mode 100644
index 000000000..60659e2c3
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/entity/gadget/platform/ConfigRoute.java
@@ -0,0 +1,30 @@
+package emu.grasscutter.game.entity.gadget.platform;
+
+import emu.grasscutter.net.proto.MovingPlatformTypeOuterClass;
+import emu.grasscutter.net.proto.PlatformInfoOuterClass;
+import emu.grasscutter.scripts.data.SceneGadget;
+import emu.grasscutter.utils.Position;
+import lombok.Getter;
+import lombok.Setter;
+
+public class ConfigRoute extends BaseRoute {
+
+ @Getter @Setter private int routeId;
+
+ public ConfigRoute(SceneGadget gadget) {
+ super(gadget);
+ this.routeId = gadget.route_id;
+ }
+
+ public ConfigRoute(Position startRot, boolean startRoute, boolean isActive, int routeId) {
+ super(startRot, startRoute, isActive);
+ this.routeId = routeId;
+ }
+
+ @Override
+ public PlatformInfoOuterClass.PlatformInfo.Builder toProto() {
+ return super.toProto()
+ .setRouteId(routeId)
+ .setMovingPlatformType(MovingPlatformTypeOuterClass.MovingPlatformType.MOVING_PLATFORM_TYPE_USE_CONFIG);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/entity/gadget/platform/PointArrayRoute.java b/src/main/java/emu/grasscutter/game/entity/gadget/platform/PointArrayRoute.java
new file mode 100644
index 000000000..84b1c6620
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/entity/gadget/platform/PointArrayRoute.java
@@ -0,0 +1,32 @@
+package emu.grasscutter.game.entity.gadget.platform;
+
+import emu.grasscutter.net.proto.MovingPlatformTypeOuterClass;
+import emu.grasscutter.net.proto.PlatformInfoOuterClass;
+import emu.grasscutter.scripts.data.SceneGadget;
+import emu.grasscutter.utils.Position;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * TODO implement point array routes, read from missing resources
+ */
+public class PointArrayRoute extends BaseRoute {
+
+ @Getter @Setter int currentPoint;
+ @Getter @Setter int pointArrayId;
+
+ public PointArrayRoute(SceneGadget gadget) {
+ super(gadget);
+ }
+
+ public PointArrayRoute(Position startRot, boolean startRoute, boolean isActive, int pointArrayId) {
+ super(startRot, startRoute, isActive);
+ this.pointArrayId = pointArrayId;
+ }
+
+ @Override
+ public PlatformInfoOuterClass.PlatformInfo.Builder toProto() {
+ return super.toProto()
+ .setMovingPlatformType(MovingPlatformTypeOuterClass.MovingPlatformType.MOVING_PLATFORM_TYPE_ROUTE);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/player/PlayerProgress.java b/src/main/java/emu/grasscutter/game/player/PlayerProgress.java
new file mode 100644
index 000000000..ee08959dd
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/player/PlayerProgress.java
@@ -0,0 +1,63 @@
+package emu.grasscutter.game.player;
+
+import dev.morphia.annotations.Entity;
+import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.val;
+
+import java.util.Map;
+
+/**
+ * Tracks progress the player made in the world, like obtained items, seen characters and more
+ */
+@Entity
+public class PlayerProgress {
+
+ @Getter private Map itemHistory;
+
+ // 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;
+
+ public PlayerProgress(){
+ this.questProgressCountMap = new Int2IntOpenHashMap();
+ this.itemHistory = new Int2ObjectOpenHashMap<>();
+ }
+
+ public boolean hasPlayerObtainedItemHistorically(int itemId){
+ return itemHistory.containsKey(itemId);
+ }
+
+ public int addToItemHistory(int itemId, int count){
+ val itemEntry = itemHistory.computeIfAbsent(itemId, (key) -> new ItemEntry(itemId));
+ return itemEntry.addToObtainedCount(count);
+ }
+
+ public int getCurrentProgress(int progressId){
+ return questProgressCountMap.getOrDefault(progressId, -1);
+ }
+
+ public int addToCurrentProgress(int progressId, int count){
+ return questProgressCountMap.merge(progressId, count, Integer::sum);
+ }
+
+ @Entity
+ @NoArgsConstructor
+ public static class ItemEntry{
+ @Getter private int itemId;
+ @Getter @Setter private int obtainedCount;
+
+ ItemEntry(int itemId){
+ this.itemId = itemId;
+ }
+
+ int addToObtainedCount(int amount){
+ this.obtainedCount+=amount;
+ return this.obtainedCount;
+ }
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/props/RefreshType.java b/src/main/java/emu/grasscutter/game/props/RefreshType.java
new file mode 100644
index 000000000..cb88dbba3
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/props/RefreshType.java
@@ -0,0 +1,43 @@
+package emu.grasscutter.game.props;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
+
+public enum RefreshType {
+ REFRESH_NONE (0),
+ REFRESH_INTERVAL (1),
+ REFRESH_DAILY (2),
+ REFRESH_WEEKlY (3),
+ REFRESH_DAYBEGIN_INTERVAL (4);
+
+ private final int value;
+ private static final Int2ObjectMap map = new Int2ObjectOpenHashMap<>();
+ private static final Map stringMap = new HashMap<>();
+
+ static {
+ Stream.of(values()).forEach(e -> {
+ map.put(e.getValue(), e);
+ stringMap.put(e.name(), e);
+ });
+ }
+
+ private RefreshType(int value) {
+ this.value = value;
+ }
+
+ public int getValue() {
+ return value;
+ }
+
+ public static RefreshType getTypeByValue(int value) {
+ return map.getOrDefault(value, REFRESH_NONE);
+ }
+
+ public static RefreshType getTypeByName(String name) {
+ return stringMap.getOrDefault(name, REFRESH_NONE);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/QuestValueCond.java b/src/main/java/emu/grasscutter/game/quest/QuestValueCond.java
new file mode 100644
index 000000000..3701a9fa0
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/QuestValueCond.java
@@ -0,0 +1,11 @@
+package emu.grasscutter.game.quest;
+
+import emu.grasscutter.game.quest.enums.QuestCond;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.RUNTIME)
+public @interface QuestValueCond {
+ QuestCond value();
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/QuestValueContent.java b/src/main/java/emu/grasscutter/game/quest/QuestValueContent.java
new file mode 100644
index 000000000..751db4a55
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/QuestValueContent.java
@@ -0,0 +1,11 @@
+package emu.grasscutter.game.quest;
+
+import emu.grasscutter.game.quest.enums.QuestContent;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.RUNTIME)
+public @interface QuestValueContent {
+ QuestContent value();
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/QuestValueExec.java b/src/main/java/emu/grasscutter/game/quest/QuestValueExec.java
new file mode 100644
index 000000000..bd5f0ffcc
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/QuestValueExec.java
@@ -0,0 +1,11 @@
+package emu.grasscutter.game.quest;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import emu.grasscutter.game.quest.enums.QuestExec;
+
+@Retention(RetentionPolicy.RUNTIME)
+public @interface QuestValueExec {
+ QuestExec value();
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/TeleportData.java b/src/main/java/emu/grasscutter/game/quest/TeleportData.java
new file mode 100644
index 000000000..83c143f5c
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/TeleportData.java
@@ -0,0 +1,35 @@
+package emu.grasscutter.game.quest;
+
+import java.util.List;
+import lombok.Data;
+
+@Data
+public class TeleportData {
+ List transmit_points;
+ List npcs;
+ List gadgets;
+
+ @Data
+ public static class TransmitPoint {
+ private int point_id;
+ private int scene_id;
+ private String pos;
+ }
+
+ @Data
+ public static class Npc {
+ private int data_index;
+ private int room_id;
+ private int scene_id;
+ private int id;
+ private String alias;
+ private String script;
+ private String pos;
+ }
+
+ @Data
+ public static class Gadget {
+ private int id;
+ private String pos;
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/conditions/BaseConditionQuestVar.java b/src/main/java/emu/grasscutter/game/quest/conditions/BaseConditionQuestVar.java
new file mode 100644
index 000000000..9d94317c7
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/conditions/BaseConditionQuestVar.java
@@ -0,0 +1,40 @@
+package emu.grasscutter.game.quest.conditions;
+
+import emu.grasscutter.Grasscutter;
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.player.Player;
+import lombok.val;
+
+public abstract class BaseConditionQuestVar extends BaseCondition {
+
+ protected abstract boolean doCompare(int variable, int cond);
+
+ @Override
+ public boolean execute(Player owner, QuestData questData, QuestData.QuestAcceptCondition condition, String paramStr, int... params) {
+ val index = condition.getParam()[0];
+ val targetValue = condition.getParam()[1];
+ val questVarValue = getQuestVar(owner, questData, index);
+
+ Grasscutter.getLogger().debug("questVar {} : {}", index, questVarValue);
+
+ if (questVarValue < 0) {
+ return false;
+ }
+ return doCompare(questVarValue, targetValue);
+ }
+
+ protected int getQuestVar(Player owner, QuestData questData, int index) {
+ val mainQuest = owner.getQuestManager().getMainQuestById(questData.getMainId());
+ if (mainQuest == null) {
+ Grasscutter.getLogger().debug("mainQuest for quest var not available yet");
+ return -1;
+ }
+ val questVars = mainQuest.getQuestVars();
+ if (index >= questVars.length) {
+ Grasscutter.getLogger().error("questVar out of bounds for {} index {} size {}", questData.getSubId(), index, questVars.length);
+ return -2;
+ }
+ return questVars[index];
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/conditions/ConditionActivityCond.java b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionActivityCond.java
new file mode 100644
index 000000000..4ec0603d5
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionActivityCond.java
@@ -0,0 +1,20 @@
+package emu.grasscutter.game.quest.conditions;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.game.quest.QuestValueCond;
+import lombok.val;
+
+import static emu.grasscutter.game.quest.enums.QuestCond.QUEST_COND_ACTIVITY_COND;
+
+@QuestValueCond(QUEST_COND_ACTIVITY_COND)
+public class ConditionActivityCond extends BaseCondition {
+
+ @Override
+ public boolean execute(Player owner, QuestData questData, QuestData.QuestAcceptCondition condition, String paramStr, int... params) {
+ val activityCondId = condition.getParam()[0];
+ val targetState = condition.getParam()[1]; // only 1 for now
+ return owner.getActivityManager().meetsCondition(activityCondId) == (targetState == 1);
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/conditions/ConditionActivityEnd.java b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionActivityEnd.java
new file mode 100644
index 000000000..34c31be7f
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionActivityEnd.java
@@ -0,0 +1,19 @@
+package emu.grasscutter.game.quest.conditions;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.game.quest.QuestValueCond;
+import lombok.val;
+
+import static emu.grasscutter.game.quest.enums.QuestCond.QUEST_COND_ACTIVITY_END;
+
+@QuestValueCond(QUEST_COND_ACTIVITY_END)
+public class ConditionActivityEnd extends BaseCondition {
+
+ @Override
+ public boolean execute(Player owner, QuestData questData, QuestData.QuestAcceptCondition condition, String paramStr, int... params) {
+ val activityId = condition.getParam()[0];
+ return owner.getActivityManager().hasActivityEnded(activityId);
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/conditions/ConditionActivityOpen.java b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionActivityOpen.java
new file mode 100644
index 000000000..10d857261
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionActivityOpen.java
@@ -0,0 +1,19 @@
+package emu.grasscutter.game.quest.conditions;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.game.quest.QuestValueCond;
+import lombok.val;
+
+import static emu.grasscutter.game.quest.enums.QuestCond.QUEST_COND_ACTIVITY_OPEN;
+
+@QuestValueCond(QUEST_COND_ACTIVITY_OPEN)
+public class ConditionActivityOpen extends BaseCondition {
+
+ @Override
+ public boolean execute(Player owner, QuestData questData, QuestData.QuestAcceptCondition condition, String paramStr, int... params) {
+ val activityId = condition.getParam()[0];
+ return owner.getActivityManager().isActivityActive(activityId);
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/conditions/ConditionHistoryGotAnyItem.java b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionHistoryGotAnyItem.java
new file mode 100644
index 000000000..bc8997326
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionHistoryGotAnyItem.java
@@ -0,0 +1,18 @@
+package emu.grasscutter.game.quest.conditions;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.game.quest.QuestValueCond;
+import emu.grasscutter.game.quest.enums.QuestCond;
+import lombok.val;
+
+@QuestValueCond(QuestCond.QUEST_COND_HISTORY_GOT_ANY_ITEM)
+public class ConditionHistoryGotAnyItem extends BaseCondition {
+
+ @Override
+ public boolean execute(Player owner, QuestData questData, QuestData.QuestAcceptCondition condition, String paramStr, int... params) {
+ val itemId = condition.getParam()[0];
+ return owner.getPlayerProgress().hasPlayerObtainedItemHistorically(itemId);
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/conditions/ConditionIsDaytime.java b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionIsDaytime.java
new file mode 100644
index 000000000..c51c9398d
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionIsDaytime.java
@@ -0,0 +1,20 @@
+package emu.grasscutter.game.quest.conditions;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.game.quest.QuestValueCond;
+import emu.grasscutter.game.quest.enums.QuestCond;
+import lombok.val;
+
+@QuestValueCond(QuestCond.QUEST_COND_IS_DAYTIME)
+public class ConditionIsDaytime extends BaseCondition{
+
+ @Override
+ public boolean execute(Player owner, QuestData questData, QuestData.QuestAcceptCondition condition, String paramStr, int... params) {
+ val daytime = condition.getParam()[0] == 1;
+ val currentTime = owner.getWorld().getGameTimeHours();
+ // TODO is this the real timeframe?
+ return (currentTime >=6 && currentTime<=18) == daytime;
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/conditions/ConditionItemNumLessThan.java b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionItemNumLessThan.java
new file mode 100644
index 000000000..3c4d6b7c2
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionItemNumLessThan.java
@@ -0,0 +1,21 @@
+package emu.grasscutter.game.quest.conditions;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.game.quest.QuestValueCond;
+import lombok.val;
+
+import static emu.grasscutter.game.quest.enums.QuestCond.QUEST_COND_ITEM_NUM_LESS_THAN;
+
+@QuestValueCond(QUEST_COND_ITEM_NUM_LESS_THAN)
+public class ConditionItemNumLessThan extends BaseCondition {
+
+ @Override
+ public boolean execute(Player owner, QuestData questData, QuestData.QuestAcceptCondition condition, String paramStr, int... params) {
+ val itemId = condition.getParam()[0];
+ val amount = condition.getParam()[1];
+ val checkItem = owner.getInventory().getItemByGuid(itemId);
+ return checkItem == null || checkItem.getCount() < amount;
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/conditions/ConditionNone.java b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionNone.java
new file mode 100644
index 000000000..23dd4a07a
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionNone.java
@@ -0,0 +1,16 @@
+package emu.grasscutter.game.quest.conditions;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.game.quest.QuestValueCond;
+import emu.grasscutter.game.quest.enums.QuestCond;
+
+@QuestValueCond(QuestCond.QUEST_COND_NONE)
+public class ConditionNone extends BaseCondition{
+
+ @Override
+ public boolean execute(Player owner, QuestData questData, QuestData.QuestAcceptCondition condition, String paramStr, int... params) {
+ return true;
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/conditions/ConditionOpenStateEqual.java b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionOpenStateEqual.java
new file mode 100644
index 000000000..1869f7e48
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionOpenStateEqual.java
@@ -0,0 +1,19 @@
+package emu.grasscutter.game.quest.conditions;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.game.quest.QuestValueCond;
+import emu.grasscutter.game.quest.enums.QuestCond;
+import lombok.val;
+
+@QuestValueCond(QuestCond.QUEST_COND_OPEN_STATE_EQUAL)
+public class ConditionOpenStateEqual extends BaseCondition {
+
+ @Override
+ public boolean execute(Player owner, QuestData questData, QuestData.QuestAcceptCondition condition, String paramStr, int... params) {
+ val openStateId = condition.getParam()[0];
+ val requiredState = condition.getParam()[1];
+ return owner.getProgressManager().getOpenState(openStateId) == requiredState;
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/conditions/ConditionPackHaveItem.java b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionPackHaveItem.java
new file mode 100644
index 000000000..86e40bf33
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionPackHaveItem.java
@@ -0,0 +1,21 @@
+package emu.grasscutter.game.quest.conditions;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.game.quest.QuestValueCond;
+import lombok.val;
+
+import static emu.grasscutter.game.quest.enums.QuestCond.QUEST_COND_PACK_HAVE_ITEM;
+
+@QuestValueCond(QUEST_COND_PACK_HAVE_ITEM)
+public class ConditionPackHaveItem extends BaseCondition {
+
+ @Override
+ public boolean execute(Player owner, QuestData questData, QuestData.QuestAcceptCondition condition, String paramStr, int... params) {
+ val itemId = condition.getParam()[0];
+ val targetAmount = condition.getParam()[1];
+ val checkItem = owner.getInventory().getItemByGuid(itemId);
+ return checkItem != null && checkItem.getCount() >= targetAmount;
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/conditions/ConditionPersonalLineUnlock.java b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionPersonalLineUnlock.java
new file mode 100644
index 000000000..739420cb2
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionPersonalLineUnlock.java
@@ -0,0 +1,18 @@
+package emu.grasscutter.game.quest.conditions;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.game.quest.QuestValueCond;
+import emu.grasscutter.game.quest.enums.QuestCond;
+import lombok.val;
+
+@QuestValueCond(QuestCond.QUEST_COND_PERSONAL_LINE_UNLOCK)
+public class ConditionPersonalLineUnlock extends BaseCondition {
+
+ @Override
+ public boolean execute(Player owner, QuestData questData, QuestData.QuestAcceptCondition condition, String paramStr, int... params) {
+ val personalLineId = condition.getParam()[0];
+ return owner.getPersonalLineList().contains(personalLineId);
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/conditions/ConditionTimeVarGreaterOrEqual.java b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionTimeVarGreaterOrEqual.java
new file mode 100644
index 000000000..8484a42af
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionTimeVarGreaterOrEqual.java
@@ -0,0 +1,25 @@
+package emu.grasscutter.game.quest.conditions;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.game.quest.QuestValueCond;
+import emu.grasscutter.game.quest.enums.QuestCond;
+import lombok.val;
+
+@QuestValueCond(QuestCond.QUEST_COND_TIME_VAR_GT_EQ)
+public class ConditionTimeVarGreaterOrEqual extends BaseCondition{
+ @Override
+ public boolean execute(Player owner, QuestData questData, QuestData.QuestAcceptCondition condition, String paramStr, int... params) {
+ val mainQuestId = condition.getParam()[0];
+ val timeVarIndex = condition.getParam()[1];
+ val minTime = condition.getParam()[2];
+
+ val mainQuest = owner.getQuestManager().getMainQuestById(mainQuestId);
+
+ if(mainQuest == null){
+ return false;
+ }
+
+ return mainQuest.getHoursSinceTimeVar(timeVarIndex) >= minTime;
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/conditions/ConditionTimeVarPassDay.java b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionTimeVarPassDay.java
new file mode 100644
index 000000000..5dbd75cc6
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/conditions/ConditionTimeVarPassDay.java
@@ -0,0 +1,30 @@
+package emu.grasscutter.game.quest.conditions;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.game.quest.QuestValueCond;
+import emu.grasscutter.game.quest.enums.QuestCond;
+import lombok.val;
+
+@QuestValueCond(QuestCond.QUEST_COND_TIME_VAR_PASS_DAY)
+public class ConditionTimeVarPassDay extends BaseCondition{
+ @Override
+ public boolean execute(Player owner, QuestData questData, QuestData.QuestAcceptCondition condition, String paramStr, int... params) {
+ val mainQuestId = condition.getParam()[0];
+ val timeVarIndex = condition.getParam()[1];
+ val minDays = condition.getParam()[2];
+
+ val mainQuest = owner.getQuestManager().getMainQuestById(mainQuestId);
+
+ if(mainQuest == null){
+ return false;
+ }
+
+ val daysSinceTimeVar = mainQuest.getDaysSinceTimeVar(timeVarIndex);
+ if(daysSinceTimeVar == -1){
+ return false;
+ }
+
+ return daysSinceTimeVar >= minDays;
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/content/ContentAnyManualTransport.java b/src/main/java/emu/grasscutter/game/quest/content/ContentAnyManualTransport.java
new file mode 100644
index 000000000..9da4fcb41
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/content/ContentAnyManualTransport.java
@@ -0,0 +1,15 @@
+package emu.grasscutter.game.quest.content;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueContent;
+
+import static emu.grasscutter.game.quest.enums.QuestContent.QUEST_CONTENT_ANY_MANUAL_TRANSPORT;
+
+@QuestValueContent(QUEST_CONTENT_ANY_MANUAL_TRANSPORT)
+public class ContentAnyManualTransport extends BaseContent {
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestContentCondition condition, String paramStr, int... params) {
+ return true;
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/content/ContentClearGroupMonster.java b/src/main/java/emu/grasscutter/game/quest/content/ContentClearGroupMonster.java
new file mode 100644
index 000000000..b9ca961a7
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/content/ContentClearGroupMonster.java
@@ -0,0 +1,19 @@
+package emu.grasscutter.game.quest.content;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueContent;
+import lombok.val;
+
+import static emu.grasscutter.game.quest.enums.QuestContent.QUEST_CONTENT_CLEAR_GROUP_MONSTER;
+
+@QuestValueContent(QUEST_CONTENT_CLEAR_GROUP_MONSTER)
+public class ContentClearGroupMonster extends BaseContent {
+
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestContentCondition condition, String paramStr, int... params) {
+ val groupId = condition.getParam()[0];
+
+ return quest.getOwner().getScene().getScriptManager().isClearedGroupMonsters(groupId);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/content/ContentDestroyGadget.java b/src/main/java/emu/grasscutter/game/quest/content/ContentDestroyGadget.java
new file mode 100644
index 000000000..61981436f
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/content/ContentDestroyGadget.java
@@ -0,0 +1,15 @@
+package emu.grasscutter.game.quest.content;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueContent;
+
+import static emu.grasscutter.game.quest.enums.QuestContent.QUEST_CONTENT_DESTROY_GADGET;
+
+@QuestValueContent(QUEST_CONTENT_DESTROY_GADGET)
+public class ContentDestroyGadget extends BaseContent {
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestContentCondition condition, String paramStr, int... params) {
+ return condition.getParam()[0] == params[0];
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/content/ContentEnterMyWorld.java b/src/main/java/emu/grasscutter/game/quest/content/ContentEnterMyWorld.java
new file mode 100644
index 000000000..d214f926e
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/content/ContentEnterMyWorld.java
@@ -0,0 +1,17 @@
+package emu.grasscutter.game.quest.content;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueContent;
+
+import static emu.grasscutter.game.quest.enums.QuestContent.QUEST_CONTENT_ENTER_MY_WORLD;
+
+@QuestValueContent(QUEST_CONTENT_ENTER_MY_WORLD)
+public class ContentEnterMyWorld extends BaseContent {
+ // params[0] scene ID
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestContentCondition condition, String paramStr, int... params) {
+ return condition.getParam()[0] == params[0];
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/content/ContentEnterMyWorldScene.java b/src/main/java/emu/grasscutter/game/quest/content/ContentEnterMyWorldScene.java
new file mode 100644
index 000000000..43a06e425
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/content/ContentEnterMyWorldScene.java
@@ -0,0 +1,17 @@
+package emu.grasscutter.game.quest.content;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueContent;
+
+import static emu.grasscutter.game.quest.enums.QuestContent.QUEST_CONTENT_ENTER_MY_WORLD_SCENE;
+
+@QuestValueContent(QUEST_CONTENT_ENTER_MY_WORLD_SCENE)
+public class ContentEnterMyWorldScene extends BaseContent {
+ // params[0] scene ID
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestContentCondition condition, String paramStr, int... params) {
+ return condition.getParam()[0] == params[0];
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/content/ContentEnterVehicle.java b/src/main/java/emu/grasscutter/game/quest/content/ContentEnterVehicle.java
new file mode 100644
index 000000000..e027ec2dc
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/content/ContentEnterVehicle.java
@@ -0,0 +1,15 @@
+package emu.grasscutter.game.quest.content;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueContent;
+
+import static emu.grasscutter.game.quest.enums.QuestContent.QUEST_CONTENT_ENTER_VEHICLE;
+
+@QuestValueContent(QUEST_CONTENT_ENTER_VEHICLE)
+public class ContentEnterVehicle extends BaseContent {
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestContentCondition condition, String paramStr, int... params) {
+ return condition.getParam()[0] == params[0];
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/content/ContentFailDungeon.java b/src/main/java/emu/grasscutter/game/quest/content/ContentFailDungeon.java
new file mode 100644
index 000000000..29da1d0fb
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/content/ContentFailDungeon.java
@@ -0,0 +1,18 @@
+package emu.grasscutter.game.quest.content;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueContent;
+
+import static emu.grasscutter.game.quest.enums.QuestContent.QUEST_CONTENT_FAIL_DUNGEON;
+
+@QuestValueContent(QUEST_CONTENT_FAIL_DUNGEON)
+public class ContentFailDungeon extends BaseContent {
+
+ // params[0] dungeon ID
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestContentCondition condition, String paramStr, int... params) {
+ return condition.getParam()[0] == params[0];
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/content/ContentFinishDungeon.java b/src/main/java/emu/grasscutter/game/quest/content/ContentFinishDungeon.java
new file mode 100644
index 000000000..a0a787c09
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/content/ContentFinishDungeon.java
@@ -0,0 +1,18 @@
+package emu.grasscutter.game.quest.content;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueContent;
+
+import static emu.grasscutter.game.quest.enums.QuestContent.QUEST_CONTENT_FINISH_DUNGEON;
+
+@QuestValueContent(QUEST_CONTENT_FINISH_DUNGEON)
+public class ContentFinishDungeon extends BaseContent {
+
+ // params[0] dungeon ID, params[1] unknown
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestContentCondition condition, String paramStr, int... params) {
+ return condition.getParam()[0] == params[0];
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/content/ContentItemLessThan.java b/src/main/java/emu/grasscutter/game/quest/content/ContentItemLessThan.java
new file mode 100644
index 000000000..9384252d3
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/content/ContentItemLessThan.java
@@ -0,0 +1,15 @@
+package emu.grasscutter.game.quest.content;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueContent;
+
+import static emu.grasscutter.game.quest.enums.QuestContent.QUEST_CONTENT_ITEM_LESS_THAN;
+
+@QuestValueContent(QUEST_CONTENT_ITEM_LESS_THAN)
+public class ContentItemLessThan extends BaseContent {
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestContentCondition condition, String paramStr, int... params) {
+ return condition.getParam()[0] == params[0] && condition.getCount() > params[1];
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/content/ContentKillMonster.java b/src/main/java/emu/grasscutter/game/quest/content/ContentKillMonster.java
new file mode 100644
index 000000000..7739be1e9
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/content/ContentKillMonster.java
@@ -0,0 +1,15 @@
+package emu.grasscutter.game.quest.content;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueContent;
+
+import static emu.grasscutter.game.quest.enums.QuestContent.QUEST_CONTENT_KILL_MONSTER;
+
+@QuestValueContent(QUEST_CONTENT_KILL_MONSTER)
+public class ContentKillMonster extends BaseContent {
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestContentCondition condition, String paramStr, int... params) {
+ return condition.getParam()[0] == params[0];
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/content/ContentMonsterDie.java b/src/main/java/emu/grasscutter/game/quest/content/ContentMonsterDie.java
new file mode 100644
index 000000000..72de7f4c0
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/content/ContentMonsterDie.java
@@ -0,0 +1,15 @@
+package emu.grasscutter.game.quest.content;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueContent;
+
+import static emu.grasscutter.game.quest.enums.QuestContent.QUEST_CONTENT_MONSTER_DIE;
+
+@QuestValueContent(QUEST_CONTENT_MONSTER_DIE)
+public class ContentMonsterDie extends BaseContent {
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestContentCondition condition, String paramStr, int... params) {
+ return condition.getParam()[0] == params[0];
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/content/ContentObtainItem.java b/src/main/java/emu/grasscutter/game/quest/content/ContentObtainItem.java
new file mode 100644
index 000000000..93c3ea2ac
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/content/ContentObtainItem.java
@@ -0,0 +1,19 @@
+package emu.grasscutter.game.quest.content;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueContent;
+
+import static emu.grasscutter.game.quest.enums.QuestContent.QUEST_CONTENT_OBTAIN_ITEM;
+
+@QuestValueContent(QUEST_CONTENT_OBTAIN_ITEM)
+public class ContentObtainItem extends BaseContent {
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestContentCondition condition, String paramStr, int... params) {
+ var targetCount = condition.getCount();
+ if (targetCount == 0) {
+ targetCount = 1;
+ }
+ return condition.getParam()[0] == params[0] && targetCount <= params[1];
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/content/ContentPlayerLevelUp.java b/src/main/java/emu/grasscutter/game/quest/content/ContentPlayerLevelUp.java
new file mode 100644
index 000000000..f9ae864be
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/content/ContentPlayerLevelUp.java
@@ -0,0 +1,15 @@
+package emu.grasscutter.game.quest.content;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueContent;
+
+import static emu.grasscutter.game.quest.enums.QuestContent.QUEST_CONTENT_PLAYER_LEVEL_UP;
+
+@QuestValueContent(QUEST_CONTENT_PLAYER_LEVEL_UP)
+public class ContentPlayerLevelUp extends BaseContent {
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestContentCondition condition, String paramStr, int... params) {
+ return quest.getOwner().getLevel() >= condition.getCount();
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/content/ContentTimeVarMoreOrEqual.java b/src/main/java/emu/grasscutter/game/quest/content/ContentTimeVarMoreOrEqual.java
new file mode 100644
index 000000000..cd3b330da
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/content/ContentTimeVarMoreOrEqual.java
@@ -0,0 +1,25 @@
+package emu.grasscutter.game.quest.content;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueContent;
+import emu.grasscutter.game.quest.enums.QuestContent;
+import lombok.val;
+
+@QuestValueContent(QuestContent.QUEST_CONTENT_TIME_VAR_GT_EQ)
+public class ContentTimeVarMoreOrEqual extends BaseContent{
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestContentCondition condition, String paramStr, int... params) {
+ val mainQuestId = condition.getParam()[0];
+ val timeVarIndex = condition.getParam()[1];
+ val minTime = Integer.parseInt(condition.getParamStr());
+
+ val mainQuest = quest.getOwner().getQuestManager().getMainQuestById(mainQuestId);
+
+ if(mainQuest == null){
+ return false;
+ }
+
+ return mainQuest.getHoursSinceTimeVar(timeVarIndex) >= minTime;
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/content/ContentTimeVarPassDay.java b/src/main/java/emu/grasscutter/game/quest/content/ContentTimeVarPassDay.java
new file mode 100644
index 000000000..e0b814820
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/content/ContentTimeVarPassDay.java
@@ -0,0 +1,30 @@
+package emu.grasscutter.game.quest.content;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueContent;
+import emu.grasscutter.game.quest.enums.QuestContent;
+import lombok.val;
+
+@QuestValueContent(QuestContent.QUEST_CONTENT_TIME_VAR_PASS_DAY)
+public class ContentTimeVarPassDay extends BaseContent{
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestContentCondition condition, String paramStr, int... params) {
+ val mainQuestId = condition.getParam()[0];
+ val timeVarIndex = condition.getParam()[1];
+ val minDays = Integer.parseInt(condition.getParamStr());
+
+ val mainQuest = quest.getOwner().getQuestManager().getMainQuestById(mainQuestId);
+
+ if(mainQuest == null){
+ return false;
+ }
+
+ val daysSinceTimeVar = mainQuest.getDaysSinceTimeVar(timeVarIndex);
+ if(daysSinceTimeVar == -1){
+ return false;
+ }
+
+ return daysSinceTimeVar >= minDays;
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/content/ContentUnlockArea.java b/src/main/java/emu/grasscutter/game/quest/content/ContentUnlockArea.java
new file mode 100644
index 000000000..60f245b6f
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/content/ContentUnlockArea.java
@@ -0,0 +1,15 @@
+package emu.grasscutter.game.quest.content;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueContent;
+
+import static emu.grasscutter.game.quest.enums.QuestContent.QUEST_CONTENT_UNLOCK_AREA;
+
+@QuestValueContent(QUEST_CONTENT_UNLOCK_AREA)
+public class ContentUnlockArea 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];
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/content/ContentUseItem.java b/src/main/java/emu/grasscutter/game/quest/content/ContentUseItem.java
new file mode 100644
index 000000000..e0d68fd9b
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/content/ContentUseItem.java
@@ -0,0 +1,15 @@
+package emu.grasscutter.game.quest.content;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueContent;
+
+import static emu.grasscutter.game.quest.enums.QuestContent.QUEST_CONTENT_USE_ITEM;
+
+@QuestValueContent(QUEST_CONTENT_USE_ITEM)
+public class ContentUseItem extends BaseContent {
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestContentCondition condition, String paramStr, int... params) {
+ return condition.getParam()[0] == params[0];
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/content/ContentWorktopSelect.java b/src/main/java/emu/grasscutter/game/quest/content/ContentWorktopSelect.java
new file mode 100644
index 000000000..3a5a8b202
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/content/ContentWorktopSelect.java
@@ -0,0 +1,15 @@
+package emu.grasscutter.game.quest.content;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueContent;
+
+import static emu.grasscutter.game.quest.enums.QuestContent.QUEST_CONTENT_WORKTOP_SELECT;
+
+@QuestValueContent(QUEST_CONTENT_WORKTOP_SELECT)
+public class ContentWorktopSelect 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];
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/enums/QuestCond.java b/src/main/java/emu/grasscutter/game/quest/enums/QuestCond.java
new file mode 100644
index 000000000..95c5c955d
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/enums/QuestCond.java
@@ -0,0 +1,121 @@
+package emu.grasscutter.game.quest.enums;
+
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Stream;
+
+public enum QuestCond implements QuestTrigger {
+ QUEST_COND_NONE (0),
+ QUEST_COND_STATE_EQUAL (1),
+ QUEST_COND_STATE_NOT_EQUAL (2),
+ QUEST_COND_PACK_HAVE_ITEM (3),
+ QUEST_COND_AVATAR_ELEMENT_EQUAL (4), // missing, currently unused
+ QUEST_COND_AVATAR_ELEMENT_NOT_EQUAL (5), // missing, only NPC groups
+ QUEST_COND_AVATAR_CAN_CHANGE_ELEMENT (6), // missing, only NPC groups
+ QUEST_COND_CITY_LEVEL_EQUAL_GREATER (7), // missing, currently unused
+ QUEST_COND_ITEM_NUM_LESS_THAN (8),
+ QUEST_COND_DAILY_TASK_START (9), // missing
+ QUEST_COND_OPEN_STATE_EQUAL (10),
+ QUEST_COND_DAILY_TASK_OPEN (11), // missing, only NPC groups
+ QUEST_COND_DAILY_TASK_REWARD_CAN_GET (12), // missing, only NPC groups/talks
+ QUEST_COND_DAILY_TASK_REWARD_RECEIVED (13), // missing, only NPC groups/talks
+ QUEST_COND_PLAYER_LEVEL_REWARD_CAN_GET (14), // missing, only NPC groups/talks
+ QUEST_COND_EXPLORATION_REWARD_CAN_GET (15), // missing, only NPC groups/talks
+ QUEST_COND_IS_WORLD_OWNER (16), // missing, only NPC groups/talks
+ QUEST_COND_PLAYER_LEVEL_EQUAL_GREATER (17),
+ QUEST_COND_SCENE_AREA_UNLOCKED (18), // missing, only NPC groups/talks
+ QUEST_COND_ITEM_GIVING_ACTIVED (19), // missing
+ QUEST_COND_ITEM_GIVING_FINISHED (20), // missing
+ QUEST_COND_IS_DAYTIME (21), // only NPC groups
+ QUEST_COND_CURRENT_AVATAR (22), // missing
+ QUEST_COND_CURRENT_AREA (23), // missing
+ QUEST_COND_QUEST_VAR_EQUAL (24),
+ QUEST_COND_QUEST_VAR_GREATER (25),
+ QUEST_COND_QUEST_VAR_LESS (26),
+ QUEST_COND_FORGE_HAVE_FINISH (27), // missing, only NPC groups
+ QUEST_COND_DAILY_TASK_IN_PROGRESS (28), // missing
+ QUEST_COND_DAILY_TASK_FINISHED (29), // missing, currently unused
+ QUEST_COND_ACTIVITY_COND (30),
+ QUEST_COND_ACTIVITY_OPEN (31),
+ QUEST_COND_DAILY_TASK_VAR_GT (32), // missing
+ QUEST_COND_DAILY_TASK_VAR_EQ (33), // missing
+ QUEST_COND_DAILY_TASK_VAR_LT (34), // missing
+ QUEST_COND_BARGAIN_ITEM_GT (35), // missing, currently unused
+ QUEST_COND_BARGAIN_ITEM_EQ (36), // missing, currently unused
+ QUEST_COND_BARGAIN_ITEM_LT (37), // missing, currently unused
+ QUEST_COND_COMPLETE_TALK (38),
+ QUEST_COND_NOT_HAVE_BLOSSOM_TALK (39), // missing, only NPC groups
+ QUEST_COND_IS_CUR_BLOSSOM_TALK (40), // missing, only Blossom groups
+ QUEST_COND_QUEST_NOT_RECEIVE (41), // missing
+ QUEST_COND_QUEST_SERVER_COND_VALID (42), // missing, only NPC groups
+ QUEST_COND_ACTIVITY_CLIENT_COND (43), // missing, only NPC and Activity groups
+ QUEST_COND_QUEST_GLOBAL_VAR_EQUAL (44),
+ QUEST_COND_QUEST_GLOBAL_VAR_GREATER (45),
+ QUEST_COND_QUEST_GLOBAL_VAR_LESS (46),
+ QUEST_COND_PERSONAL_LINE_UNLOCK (47),
+ QUEST_COND_CITY_REPUTATION_REQUEST (48), // missing
+ QUEST_COND_MAIN_COOP_START (49), // missing
+ QUEST_COND_MAIN_COOP_ENTER_SAVE_POINT (50), // missing
+ QUEST_COND_CITY_REPUTATION_LEVEL (51), // missing, only NPC groups
+ QUEST_COND_CITY_REPUTATION_UNLOCK (52), // missing, currently unused
+ QUEST_COND_LUA_NOTIFY (53),
+ QUEST_COND_CUR_CLIMATE (54),
+ QUEST_COND_ACTIVITY_END (55),
+ QUEST_COND_COOP_POINT_RUNNING (56), // missing, currently unused
+ QUEST_COND_GADGET_TALK_STATE_EQUAL (57), // missing, only Gadget groups
+ QUEST_COND_AVATAR_FETTER_GT (58), // missing, only NPC groups/talks
+ QUEST_COND_AVATAR_FETTER_EQ (59), // missing, only talks
+ QUEST_COND_AVATAR_FETTER_LT (60), // missing, only talks
+ QUEST_COND_NEW_HOMEWORLD_MOUDLE_UNLOCK (61), // missing, only Gadget groups
+ QUEST_COND_NEW_HOMEWORLD_LEVEL_REWARD (62), // missing, only Gadget groups
+ QUEST_COND_NEW_HOMEWORLD_MAKE_FINISH (63), // missing, only Gadget groups
+ QUEST_COND_HOMEWORLD_NPC_EVENT (64), // missing, only NPC groups
+ QUEST_COND_TIME_VAR_GT_EQ (65), // currently unused
+ QUEST_COND_TIME_VAR_PASS_DAY (66),
+ QUEST_COND_HOMEWORLD_NPC_NEW_TALK (67), // missing, only NPC groups
+ QUEST_COND_PLAYER_CHOOSE_MALE (68), // missing, only talks
+ QUEST_COND_HISTORY_GOT_ANY_ITEM (69),
+ QUEST_COND_LEARNED_RECIPE (70), // missing, currently unused
+ QUEST_COND_LUNARITE_REGION_UNLOCKED (71), // missing, only NPC groups
+ QUEST_COND_LUNARITE_HAS_REGION_HINT_COUNT (72), // missing, only NPC groups
+ QUEST_COND_LUNARITE_COLLECT_FINISH (73), // missing, only NPC groups
+ QUEST_COND_LUNARITE_MARK_ALL_FINISH (74), // missing, only NPC groups
+ QUEST_COND_NEW_HOMEWORLD_SHOP_ITEM (75), // missing, only Gadget groups
+ QUEST_COND_SCENE_POINT_UNLOCK (76), // missing, only NPC groups
+ QUEST_COND_SCENE_LEVEL_TAG_EQ (77), // missing
+ QUEST_COND_PLAYER_ENTER_REGION (78), // missing
+ QUEST_COND_UNKNOWN (9999);
+
+ private final int value;
+
+ QuestCond(int id) {
+ this.value = id;
+ }
+
+ public int getValue() {
+ return value;
+ }
+
+
+ private static final Int2ObjectMap contentMap = new Int2ObjectOpenHashMap<>();
+ private static final Map contentStringMap = new HashMap<>();
+
+ static {
+ Stream.of(values())
+ .forEach(e -> {
+ contentMap.put(e.getValue(), e);
+ contentStringMap.put(e.name(), e);
+ });
+ }
+
+ public static QuestCond getContentTriggerByValue(int value) {
+ return contentMap.getOrDefault(value, QUEST_COND_NONE);
+ }
+
+ public static QuestCond getContentTriggerByName(String name) {
+ return contentStringMap.getOrDefault(name, QUEST_COND_NONE);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/enums/QuestContent.java b/src/main/java/emu/grasscutter/game/quest/enums/QuestContent.java
new file mode 100644
index 000000000..32d67bd55
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/enums/QuestContent.java
@@ -0,0 +1,117 @@
+package emu.grasscutter.game.quest.enums;
+
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Stream;
+
+public enum QuestContent implements QuestTrigger {
+ QUEST_CONTENT_NONE (0),
+ QUEST_CONTENT_KILL_MONSTER (1), // currently unused
+ QUEST_CONTENT_COMPLETE_TALK (2),
+ QUEST_CONTENT_MONSTER_DIE (3),
+ QUEST_CONTENT_FINISH_PLOT (4),
+ QUEST_CONTENT_OBTAIN_ITEM (5),
+ QUEST_CONTENT_TRIGGER_FIRE (6),
+ QUEST_CONTENT_CLEAR_GROUP_MONSTER (7),
+ QUEST_CONTENT_NOT_FINISH_PLOT (8), // missing triggers, fail
+ QUEST_CONTENT_ENTER_DUNGEON (9),
+ QUEST_CONTENT_ENTER_MY_WORLD (10),
+ QUEST_CONTENT_FINISH_DUNGEON (11),
+ QUEST_CONTENT_DESTROY_GADGET (12),
+ QUEST_CONTENT_OBTAIN_MATERIAL_WITH_SUBTYPE (13), // missing, finish
+ QUEST_CONTENT_NICK_NAME (14), // missing, currently unused
+ QUEST_CONTENT_WORKTOP_SELECT (15), // currently unused
+ QUEST_CONTENT_SEAL_BATTLE_RESULT (16), // missing, currently unused
+ QUEST_CONTENT_ENTER_ROOM (17),
+ QUEST_CONTENT_GAME_TIME_TICK (18),
+ QUEST_CONTENT_FAIL_DUNGEON (19),
+ QUEST_CONTENT_LUA_NOTIFY (20),
+ QUEST_CONTENT_TEAM_DEAD (21), // missing, fail
+ QUEST_CONTENT_COMPLETE_ANY_TALK (22),
+ QUEST_CONTENT_UNLOCK_TRANS_POINT (23),
+ 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_SKILL (107),
+ QUEST_CONTENT_CITY_LEVEL_UP (109), // missing, finish
+ QUEST_CONTENT_PATTERN_GROUP_CLEAR_MONSTER (110), // missing, finish, for random quests
+ QUEST_CONTENT_ITEM_LESS_THAN (111),
+ QUEST_CONTENT_PLAYER_LEVEL_UP (112),
+ QUEST_CONTENT_DUNGEON_OPEN_STATUE (113), // missing, currently unused
+ QUEST_CONTENT_UNLOCK_AREA (114), // currently unused
+ QUEST_CONTENT_OPEN_CHEST_WITH_GADGET_ID (115), // missing, currently unused
+ QUEST_CONTENT_UNLOCK_TRANS_POINT_WITH_TYPE (116), // missing, currently unused
+ QUEST_CONTENT_FINISH_DAILY_DUNGEON (117), // missing, currently unused
+ QUEST_CONTENT_FINISH_WEEKLY_DUNGEON (118), // missing, currently unused
+ QUEST_CONTENT_QUEST_VAR_EQUAL (119),
+ QUEST_CONTENT_QUEST_VAR_GREATER (120),
+ 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_ACTIVITY_TRIGGER_FAILED (127),// missing, fail
+ QUEST_CONTENT_MAIN_COOP_ENTER_SAVE_POINT (128),// missing, finish
+ QUEST_CONTENT_ANY_MANUAL_TRANSPORT (129),
+ QUEST_CONTENT_USE_ITEM (130),
+ QUEST_CONTENT_MAIN_COOP_ENTER_ANY_SAVE_POINT (131),// missing, finish and fail
+ QUEST_CONTENT_ENTER_MY_HOME_WORLD (132),// missing, finish and fail
+ QUEST_CONTENT_ENTER_MY_WORLD_SCENE (133),// missing, finish
+ QUEST_CONTENT_TIME_VAR_GT_EQ (134),
+ QUEST_CONTENT_TIME_VAR_PASS_DAY (135),
+ QUEST_CONTENT_QUEST_STATE_EQUAL (136),
+ QUEST_CONTENT_QUEST_STATE_NOT_EQUAL (137),
+ QUEST_CONTENT_UNLOCKED_RECIPE (138),// missing, finish
+ QUEST_CONTENT_NOT_UNLOCKED_RECIPE (139),// missing, finish
+ QUEST_CONTENT_FISHING_SUCC (140),// missing, finish
+ QUEST_CONTENT_ENTER_ROGUE_DUNGEON (141),// missing, finish
+ QUEST_CONTENT_USE_WIDGET (142),// missing, finish, only in unreleased quest
+ QUEST_CONTENT_CAPTURE_SUCC (143),// missing, currently unused
+ QUEST_CONTENT_CAPTURE_USE_CAPTURETAG_LIST (144),// missing, currently unused
+ QUEST_CONTENT_CAPTURE_USE_MATERIAL_LIST (145),// missing, finish
+ QUEST_CONTENT_ENTER_VEHICLE (147),
+ QUEST_CONTENT_SCENE_LEVEL_TAG_EQ (148),// missing, finish
+ QUEST_CONTENT_LEAVE_SCENE (149),
+ QUEST_CONTENT_LEAVE_SCENE_RANGE (150),// missing, fail
+ QUEST_CONTENT_IRODORI_FINISH_FLOWER_COMBINATION (151),// missing, finish
+ QUEST_CONTENT_IRODORI_POETRY_REACH_MIN_PROGRESS (152),// missing, finish
+ QUEST_CONTENT_IRODORI_POETRY_FINISH_FILL_POETRY (153),// missing, finish
+ QUEST_CONTENT_ACTIVITY_TRIGGER_UPDATE(154), // missing
+ QUEST_CONTENT_GADGET_STATE_CHANGE(155), // missing
+ QUEST_CONTENT_UNKNOWN (9999);
+
+ private final int value;
+
+ QuestContent(int id) {
+ this.value = id;
+ }
+
+ public int getValue() {
+ return value;
+ }
+
+
+ private static final Int2ObjectMap contentMap = new Int2ObjectOpenHashMap<>();
+ private static final Map contentStringMap = new HashMap<>();
+
+ static {
+ Stream.of(values())
+ .forEach(e -> {
+ contentMap.put(e.getValue(), e);
+ contentStringMap.put(e.name(), e);
+ });
+ }
+
+ public static QuestContent getContentTriggerByValue(int value) {
+ return contentMap.getOrDefault(value, QUEST_CONTENT_NONE);
+ }
+
+ public static QuestContent getContentTriggerByName(String name) {
+ return contentStringMap.getOrDefault(name, QUEST_CONTENT_NONE);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/enums/QuestExec.java b/src/main/java/emu/grasscutter/game/quest/enums/QuestExec.java
new file mode 100644
index 000000000..0e67b0030
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/enums/QuestExec.java
@@ -0,0 +1,112 @@
+package emu.grasscutter.game.quest.enums;
+
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Stream;
+
+public enum QuestExec implements QuestTrigger {
+ QUEST_EXEC_NONE (0),
+ QUEST_EXEC_DEL_PACK_ITEM (1),
+ QUEST_EXEC_UNLOCK_POINT (2),
+ QUEST_EXEC_UNLOCK_AREA (3),
+ QUEST_EXEC_UNLOCK_FORCE (4), // missing, currently unused
+ QUEST_EXEC_LOCK_FORCE (5), // missing, currently unused
+ QUEST_EXEC_CHANGE_AVATAR_ELEMET (6),
+ QUEST_EXEC_REFRESH_GROUP_MONSTER (7),
+ QUEST_EXEC_SET_IS_FLYABLE (8), // missing, maybe gives glider
+ QUEST_EXEC_SET_IS_WEATHER_LOCKED (9), // missing
+ QUEST_EXEC_SET_IS_GAME_TIME_LOCKED (10), // missing
+ QUEST_EXEC_SET_IS_TRANSFERABLE (11), // missing, currently unused
+ QUEST_EXEC_GRANT_TRIAL_AVATAR (12),
+ QUEST_EXEC_OPEN_BORED (13), // missing, currently unused
+ QUEST_EXEC_ROLLBACK_QUEST (14),
+ QUEST_EXEC_NOTIFY_GROUP_LUA (15),
+ QUEST_EXEC_SET_OPEN_STATE (16),
+ QUEST_EXEC_LOCK_POINT (17), // missing
+ QUEST_EXEC_DEL_PACK_ITEM_BATCH (18),
+ QUEST_EXEC_REFRESH_GROUP_SUITE (19),
+ QUEST_EXEC_REMOVE_TRIAL_AVATAR (20),
+ QUEST_EXEC_SET_GAME_TIME (21), // missing
+ QUEST_EXEC_SET_WEATHER_GADGET (22), // missing
+ QUEST_EXEC_ADD_QUEST_PROGRESS (23),
+ QUEST_EXEC_NOTIFY_DAILY_TASK (24), // missing
+ 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_DEL_ALL_SPECIFIC_PACK_ITEM (29), // missing
+ QUEST_EXEC_ROLLBACK_PARENT_QUEST (30),
+ QUEST_EXEC_LOCK_AVATAR_TEAM (31), // missing
+ QUEST_EXEC_UNLOCK_AVATAR_TEAM (32), // missing
+ QUEST_EXEC_UPDATE_PARENT_QUEST_REWARD_INDEX (33), // missing
+ QUEST_EXEC_SET_DAILY_TASK_VAR (34), // missing
+ QUEST_EXEC_INC_DAILY_TASK_VAR (35), // missing
+ QUEST_EXEC_DEC_DAILY_TASK_VAR (36), // missing, currently unused
+ 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_SET_QUEST_GLOBAL_VAR (43),
+ QUEST_EXEC_INC_QUEST_GLOBAL_VAR (44),
+ QUEST_EXEC_DEC_QUEST_GLOBAL_VAR (45),
+ QUEST_EXEC_REGISTER_DYNAMIC_GROUP (46), // test, maybe the dynamic should be saved on a list and when you enter the view range this loads it again
+ QUEST_EXEC_UNREGISTER_DYNAMIC_GROUP (47), // test, same for this
+ QUEST_EXEC_SET_QUEST_VAR (48),
+ QUEST_EXEC_INC_QUEST_VAR (49),
+ QUEST_EXEC_DEC_QUEST_VAR (50),
+ QUEST_EXEC_RANDOM_QUEST_VAR (51), // missing
+ QUEST_EXEC_ACTIVATE_SCANNING_PIC (52), // missing, currently unused
+ QUEST_EXEC_RELOAD_SCENE_TAG (53), // missing
+ QUEST_EXEC_REGISTER_DYNAMIC_GROUP_ONLY (54), // missing
+ QUEST_EXEC_CHANGE_SKILL_DEPOT (55), // missing
+ QUEST_EXEC_ADD_SCENE_TAG (56), // missing
+ QUEST_EXEC_DEL_SCENE_TAG (57), // missing
+ QUEST_EXEC_INIT_TIME_VAR (58),
+ QUEST_EXEC_CLEAR_TIME_VAR (59),
+ 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_CHANGE_SCENE_LEVEL_TAG (64), // missing
+ QUEST_EXEC_UNLOCK_PLAYER_WORLD_SCENE (65), // missing
+ QUEST_EXEC_LOCK_PLAYER_WORLD_SCENE (66), // missing
+ QUEST_EXEC_FAIL_MAINCOOP (67), // missing
+ QUEST_EXEC_MODIFY_WEATHER_AREA (68), // missing
+ QUEST_EXEC_MODIFY_ARANARA_COLLECTION_STATE (69), // missing
+ QUEST_EXEC_GRANT_TRIAL_AVATAR_BATCH_AND_LOCK_TEAM (70), // missing
+ QUEST_EXEC_UNKNOWN (9999);
+
+ private final int value;
+
+ QuestExec(int id) {
+ this.value = id;
+ }
+
+ public int getValue() {
+ return value;
+ }
+
+ private static final Int2ObjectMap contentMap = new Int2ObjectOpenHashMap<>();
+ private static final Map contentStringMap = new HashMap<>();
+
+ static {
+ Stream.of(values())
+ .filter(e -> e.name().startsWith("QUEST_CONTENT_"))
+ .forEach(e -> {
+ contentMap.put(e.getValue(), e);
+ contentStringMap.put(e.name(), e);
+ });
+ }
+
+ public static QuestExec getContentTriggerByValue(int value) {
+ return contentMap.getOrDefault(value, QUEST_EXEC_NONE);
+ }
+
+ public static QuestExec getContentTriggerByName(String name) {
+ return contentStringMap.getOrDefault(name, QUEST_EXEC_NONE);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/exec/ExecAddCurAvatarEnergy.java b/src/main/java/emu/grasscutter/game/quest/exec/ExecAddCurAvatarEnergy.java
new file mode 100644
index 000000000..45ba3d572
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/exec/ExecAddCurAvatarEnergy.java
@@ -0,0 +1,17 @@
+package emu.grasscutter.game.quest.exec;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueExec;
+import emu.grasscutter.game.quest.enums.QuestExec;
+import emu.grasscutter.game.quest.handlers.QuestExecHandler;
+import emu.grasscutter.Grasscutter;
+
+@QuestValueExec(QuestExec.QUEST_EXEC_ADD_CUR_AVATAR_ENERGY)
+public class ExecAddCurAvatarEnergy extends QuestExecHandler {
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestExecParam condition, String... paramStr) {
+ Grasscutter.getLogger().info("Energy refilled");
+ return quest.getOwner().getEnergyManager().refillEntityAvatarEnergy();
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/exec/ExecChangeAvatarElemet.java b/src/main/java/emu/grasscutter/game/quest/exec/ExecChangeAvatarElemet.java
new file mode 100644
index 000000000..803722b89
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/exec/ExecChangeAvatarElemet.java
@@ -0,0 +1,31 @@
+package emu.grasscutter.game.quest.exec;
+
+import emu.grasscutter.Grasscutter;
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.props.ElementType;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueExec;
+import emu.grasscutter.game.quest.enums.QuestExec;
+import emu.grasscutter.game.quest.handlers.QuestExecHandler;
+import lombok.val;
+
+/**
+ * Changes the main avatar's element. First parameter is the elementType id
+ */
+@QuestValueExec(QuestExec.QUEST_EXEC_CHANGE_AVATAR_ELEMET)
+public class ExecChangeAvatarElemet extends QuestExecHandler {
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestExecParam condition, String... paramStr) {
+ val targetElement = ElementType.getTypeByValue(Integer.parseInt(paramStr[0]));
+ val owner = quest.getOwner();
+ val mainAvatar = owner.getAvatars().getAvatarById(owner.getMainCharacterId());
+
+ if(mainAvatar == null){
+ Grasscutter.getLogger().error("Failed to get main avatar for use {}", quest.getOwner().getUid());
+ return false;
+ }
+
+ Grasscutter.getLogger().info("Changing avatar element to {} for quest {}", targetElement.name(), quest.getSubQuestId());
+ return mainAvatar.changeElement(targetElement);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/exec/ExecClearTimeVar.java b/src/main/java/emu/grasscutter/game/quest/exec/ExecClearTimeVar.java
new file mode 100644
index 000000000..e84c61bf5
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/exec/ExecClearTimeVar.java
@@ -0,0 +1,19 @@
+package emu.grasscutter.game.quest.exec;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueExec;
+import emu.grasscutter.game.quest.enums.QuestExec;
+import emu.grasscutter.game.quest.handlers.QuestExecHandler;
+import lombok.val;
+
+@QuestValueExec(QuestExec.QUEST_EXEC_CLEAR_TIME_VAR)
+public class ExecClearTimeVar extends QuestExecHandler {
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestExecParam condition, String... paramStr) {
+ val mainQuestId = Integer.parseInt(condition.getParam()[0]);
+ val timeVarId = Integer.parseInt(condition.getParam()[1]);
+ val mainQuest = quest.getOwner().getQuestManager().getMainQuestById(mainQuestId);
+ return mainQuest.clearTimeVar(timeVarId);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/exec/ExecDelPackItem.java b/src/main/java/emu/grasscutter/game/quest/exec/ExecDelPackItem.java
new file mode 100644
index 000000000..cd2286d1f
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/exec/ExecDelPackItem.java
@@ -0,0 +1,17 @@
+package emu.grasscutter.game.quest.exec;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueExec;
+import emu.grasscutter.game.quest.enums.QuestExec;
+import emu.grasscutter.game.quest.handlers.QuestExecHandler;
+
+@QuestValueExec(QuestExec.QUEST_EXEC_DEL_PACK_ITEM)
+public class ExecDelPackItem extends QuestExecHandler {
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestExecParam condition, String... paramStr) {
+ int itemId = Integer.parseInt(paramStr[0]);
+ int amount = Integer.parseInt(paramStr[1]);
+ return quest.getOwner().getInventory().removeItem(itemId, amount);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/exec/ExecDelPackItemBatch.java b/src/main/java/emu/grasscutter/game/quest/exec/ExecDelPackItemBatch.java
new file mode 100644
index 000000000..f06b52fe3
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/exec/ExecDelPackItemBatch.java
@@ -0,0 +1,26 @@
+package emu.grasscutter.game.quest.exec;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueExec;
+import emu.grasscutter.game.quest.enums.QuestExec;
+import emu.grasscutter.game.quest.handlers.QuestExecHandler;
+
+@QuestValueExec(QuestExec.QUEST_EXEC_DEL_PACK_ITEM_BATCH)
+public class ExecDelPackItemBatch extends QuestExecHandler {
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestExecParam condition, String... paramStr) {
+ // input is like this: "100497:999,100498:999,100499:999"
+ var items = paramStr[0].split(",");
+ boolean success = true;
+ for (var itemString : items){
+ var itemFields = itemString.split(":");
+ var itemId = Integer.parseInt(itemFields[0]);
+ var amount = Integer.parseInt(itemFields[1]);
+ if(!quest.getOwner().getInventory().removeItem(itemId, amount)){
+ success = false;
+ }
+ }
+ return success;
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/exec/ExecGrantTrialAvatar.java b/src/main/java/emu/grasscutter/game/quest/exec/ExecGrantTrialAvatar.java
new file mode 100644
index 000000000..6b88e7333
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/exec/ExecGrantTrialAvatar.java
@@ -0,0 +1,21 @@
+package emu.grasscutter.game.quest.exec;
+
+import emu.grasscutter.Grasscutter;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueExec;
+import emu.grasscutter.game.quest.enums.QuestExec;
+import emu.grasscutter.game.quest.handlers.QuestExecHandler;
+
+@QuestValueExec(QuestExec.QUEST_EXEC_GRANT_TRIAL_AVATAR)
+public class ExecGrantTrialAvatar extends QuestExecHandler {
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestExecParam condition, String... paramStr) {
+ if (quest.getOwner().addTrialAvatarForQuest(Integer.parseInt(paramStr[0]), quest.getMainQuestId())) {
+ Grasscutter.getLogger().info("Added trial avatar to team for quest {}", quest.getSubQuestId());
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/exec/ExecInitTimeVar.java b/src/main/java/emu/grasscutter/game/quest/exec/ExecInitTimeVar.java
new file mode 100644
index 000000000..476ea4326
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/exec/ExecInitTimeVar.java
@@ -0,0 +1,18 @@
+package emu.grasscutter.game.quest.exec;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueExec;
+import emu.grasscutter.game.quest.enums.QuestExec;
+import emu.grasscutter.game.quest.handlers.QuestExecHandler;
+import lombok.val;
+
+@QuestValueExec(QuestExec.QUEST_EXEC_INIT_TIME_VAR)
+public class ExecInitTimeVar extends QuestExecHandler {
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestExecParam condition, String... paramStr) {
+ val timeVarId = Integer.parseInt(condition.getParam()[0]);
+ val mainQuest = quest.getMainQuest();
+ return mainQuest.initTimeVar(timeVarId);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/exec/ExecRefreshGroupMonster.java b/src/main/java/emu/grasscutter/game/quest/exec/ExecRefreshGroupMonster.java
new file mode 100644
index 000000000..b0cf0bf1e
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/exec/ExecRefreshGroupMonster.java
@@ -0,0 +1,19 @@
+package emu.grasscutter.game.quest.exec;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueExec;
+import emu.grasscutter.game.quest.enums.QuestExec;
+import emu.grasscutter.game.quest.handlers.QuestExecHandler;
+
+@QuestValueExec(QuestExec.QUEST_EXEC_REFRESH_GROUP_MONSTER)
+public class ExecRefreshGroupMonster extends QuestExecHandler {
+
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestExecParam condition, String... paramStr) {
+ var groupId = Integer.parseInt(paramStr[0]);
+
+ return quest.getOwner().getScene().getScriptManager().refreshGroupMonster(groupId);
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/exec/ExecRegisterDynamicGroup.java b/src/main/java/emu/grasscutter/game/quest/exec/ExecRegisterDynamicGroup.java
new file mode 100644
index 000000000..37beb9550
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/exec/ExecRegisterDynamicGroup.java
@@ -0,0 +1,39 @@
+package emu.grasscutter.game.quest.exec;
+
+import emu.grasscutter.Grasscutter;
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestGroupSuite;
+import emu.grasscutter.game.quest.QuestValueExec;
+import emu.grasscutter.game.quest.enums.QuestExec;
+import emu.grasscutter.game.quest.handlers.QuestExecHandler;
+import emu.grasscutter.game.world.Scene;
+
+@QuestValueExec(QuestExec.QUEST_EXEC_REGISTER_DYNAMIC_GROUP)
+public class ExecRegisterDynamicGroup extends QuestExecHandler {
+
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestExecParam condition, String... paramStr) {
+ var sceneId = Integer.parseInt(paramStr[0]);
+ var groupId = Integer.parseInt(paramStr[1]);
+
+ Grasscutter.getLogger().warn("Registering group {}", groupId);
+
+ Scene scene = quest.getOwner().getWorld().getSceneById(sceneId);
+ if(scene == null) return false;
+
+ int suiteId = scene.loadDynamicGroup(groupId);
+ if(suiteId == -1) return false;
+
+ quest.getMainQuest().getQuestGroupSuites().add(QuestGroupSuite.of()
+ .scene(sceneId)
+ .group(groupId)
+ .suite(suiteId)
+ .build());
+
+ Grasscutter.getLogger().warn("Registered group {}, suite {} in scene {}", groupId, suiteId, scene.getId());
+
+ return true;
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/exec/ExecRemoveTrialAvatar.java b/src/main/java/emu/grasscutter/game/quest/exec/ExecRemoveTrialAvatar.java
new file mode 100644
index 000000000..afa425406
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/exec/ExecRemoveTrialAvatar.java
@@ -0,0 +1,21 @@
+package emu.grasscutter.game.quest.exec;
+
+import emu.grasscutter.Grasscutter;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueExec;
+import emu.grasscutter.game.quest.enums.QuestExec;
+import emu.grasscutter.game.quest.handlers.QuestExecHandler;
+
+@QuestValueExec(QuestExec.QUEST_EXEC_REMOVE_TRIAL_AVATAR)
+public class ExecRemoveTrialAvatar extends QuestExecHandler {
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestExecParam condition, String... paramStr) {
+ if (quest.getOwner().removeTrialAvatarForQuest(Integer.parseInt(paramStr[0]))) {
+ Grasscutter.getLogger().info("Removed trial avatar from team for quest {}", quest.getSubQuestId());
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/exec/ExecRollbackParentQuest.java b/src/main/java/emu/grasscutter/game/quest/exec/ExecRollbackParentQuest.java
new file mode 100644
index 000000000..55622b5fd
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/exec/ExecRollbackParentQuest.java
@@ -0,0 +1,24 @@
+package emu.grasscutter.game.quest.exec;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueExec;
+import emu.grasscutter.game.quest.enums.QuestExec;
+import emu.grasscutter.game.quest.handlers.QuestExecHandler;
+import emu.grasscutter.server.packet.send.PacketScenePlayerLocationNotify;
+
+@QuestValueExec(QuestExec.QUEST_EXEC_ROLLBACK_PARENT_QUEST)
+public class ExecRollbackParentQuest extends QuestExecHandler {
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestExecParam condition, String... paramStr) {
+ var targetPosition = quest.getMainQuest().rewind();
+ if(targetPosition == null){
+ return false;
+ }
+ quest.getOwner().getPosition().set(targetPosition.get(0));
+ quest.getOwner().getRotation().set(targetPosition.get(1));
+ quest.getOwner().sendPacket(new PacketScenePlayerLocationNotify(quest.getOwner().getScene()));
+ // todo proper reset and warp
+ return true;
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/exec/ExecRollbackQuest.java b/src/main/java/emu/grasscutter/game/quest/exec/ExecRollbackQuest.java
new file mode 100644
index 000000000..109010e63
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/exec/ExecRollbackQuest.java
@@ -0,0 +1,26 @@
+package emu.grasscutter.game.quest.exec;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueExec;
+import emu.grasscutter.game.quest.enums.QuestExec;
+import emu.grasscutter.game.quest.handlers.QuestExecHandler;
+import emu.grasscutter.server.packet.send.PacketScenePlayerLocationNotify;
+
+@QuestValueExec(QuestExec.QUEST_EXEC_ROLLBACK_QUEST)
+public class ExecRollbackQuest extends QuestExecHandler {
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestExecParam condition, String... paramStr) {
+ var targetQuestId = Integer.parseInt(paramStr[0]);
+ var targetQuest = quest.getOwner().getQuestManager().getQuestById(targetQuestId);
+ var targetPosition = targetQuest.getMainQuest().rewindTo(targetQuest, true);
+ if(targetPosition == null){
+ return false;
+ }
+ quest.getOwner().getPosition().set(targetPosition.get(0));
+ quest.getOwner().getRotation().set(targetPosition.get(1));
+ quest.getOwner().sendPacket(new PacketScenePlayerLocationNotify(quest.getOwner().getScene()));
+ // todo proper reset and warp
+ return true;
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/exec/ExecSetOpenState.java b/src/main/java/emu/grasscutter/game/quest/exec/ExecSetOpenState.java
new file mode 100644
index 000000000..5c2ca5005
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/exec/ExecSetOpenState.java
@@ -0,0 +1,24 @@
+package emu.grasscutter.game.quest.exec;
+
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueExec;
+import emu.grasscutter.game.quest.enums.QuestExec;
+import emu.grasscutter.game.quest.handlers.QuestExecHandler;
+import lombok.val;
+
+import java.util.Arrays;
+
+@QuestValueExec(QuestExec.QUEST_EXEC_SET_OPEN_STATE)
+public class ExecSetOpenState extends QuestExecHandler {
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestExecParam condition, String... paramStr) {
+ val param = Arrays.stream(paramStr)
+ .filter(i -> !i.isBlank())
+ .mapToInt(Integer::parseInt)
+ .toArray();
+
+ quest.getOwner().getProgressManager().forceSetOpenState(param[0], param[1]);
+ return true;
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/quest/exec/ExecUnregisterDynamicGroup.java b/src/main/java/emu/grasscutter/game/quest/exec/ExecUnregisterDynamicGroup.java
new file mode 100644
index 000000000..6c5ebc180
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/quest/exec/ExecUnregisterDynamicGroup.java
@@ -0,0 +1,37 @@
+package emu.grasscutter.game.quest.exec;
+
+import emu.grasscutter.Grasscutter;
+import emu.grasscutter.data.excels.QuestData;
+import emu.grasscutter.game.quest.GameQuest;
+import emu.grasscutter.game.quest.QuestValueExec;
+import emu.grasscutter.game.quest.enums.QuestExec;
+import emu.grasscutter.game.quest.handlers.QuestExecHandler;
+import emu.grasscutter.game.world.Scene;
+import emu.grasscutter.scripts.data.SceneBlock;
+import emu.grasscutter.scripts.data.SceneGroup;
+import lombok.val;
+
+@QuestValueExec(QuestExec.QUEST_EXEC_UNREGISTER_DYNAMIC_GROUP)
+public class ExecUnregisterDynamicGroup extends QuestExecHandler {
+
+ @Override
+ public boolean execute(GameQuest quest, QuestData.QuestExecParam condition, String... paramStr) {
+ val groupId = Integer.parseInt(paramStr[0]);
+ val unknownParam = Integer.parseInt(paramStr[1]); //TODO: Goes from 0 to 1, maybe is a boolean. Investigate
+ val scene = quest.getOwner().getScene();
+
+ Grasscutter.getLogger().warn("Unregistering group {}", groupId);
+
+ if(!scene.unregisterDynamicGroup(groupId)){
+ return false;
+ }
+
+ //Remove suites if they are registered
+ quest.getMainQuest().getQuestGroupSuites().removeIf(gs -> gs.getGroup() == groupId && gs.getScene() == scene.getId());
+
+ Grasscutter.getLogger().warn("Unregistered group {} in scene {}", groupId, scene.getId());
+
+ return true;
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/game/world/GroupReplacementData.java b/src/main/java/emu/grasscutter/game/world/GroupReplacementData.java
new file mode 100644
index 000000000..13fd157de
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/world/GroupReplacementData.java
@@ -0,0 +1,10 @@
+package emu.grasscutter.game.world;
+
+import java.util.List;
+import lombok.Data;
+
+@Data
+public class GroupReplacementData {
+ int id;
+ List replace_groups;
+}
diff --git a/src/main/java/emu/grasscutter/game/world/SceneGroupInstance.java b/src/main/java/emu/grasscutter/game/world/SceneGroupInstance.java
new file mode 100644
index 000000000..18fc40ff8
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/world/SceneGroupInstance.java
@@ -0,0 +1,86 @@
+package emu.grasscutter.game.world;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.bson.types.ObjectId;
+
+import dev.morphia.annotations.Entity;
+import dev.morphia.annotations.Id;
+import dev.morphia.annotations.Indexed;
+import emu.grasscutter.database.DatabaseHelper;
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.scripts.data.SceneGadget;
+import emu.grasscutter.scripts.data.SceneGroup;
+import lombok.Getter;
+import lombok.Setter;
+
+@Entity(value = "group_instances", useDiscriminator = false)
+public class SceneGroupInstance {
+ @Id private ObjectId id;
+
+ @Indexed private int ownerUid; //This group is owned by the host player
+ @Getter private int groupId;
+
+ @Getter private transient SceneGroup luaGroup;
+ @Getter @Setter private int targetSuiteId;
+ @Getter @Setter private int activeSuiteId;
+ @Getter private Set deadEntities; //Config_ids
+ private boolean isCached;
+
+ @Getter private Map cachedGadgetStates;
+ @Getter private Map cachedVariables;
+
+ @Getter @Setter private int lastTimeRefreshed;
+
+ public SceneGroupInstance(SceneGroup group, Player owner) {
+ this.luaGroup = group;
+ this.groupId = group.id;
+ this.targetSuiteId = 0;
+ this.activeSuiteId = 0;
+ this.lastTimeRefreshed = 0;
+ this.ownerUid = owner.getUid();
+ this.deadEntities = new HashSet<>();
+ this.cachedGadgetStates = new ConcurrentHashMap<>();
+ this.cachedVariables = new ConcurrentHashMap<>();
+
+ this.isCached = false; //This is true when the group is not loaded on scene but caches suite data
+ }
+
+ @Deprecated // Morphia only!
+ SceneGroupInstance(){
+ this.cachedVariables = new ConcurrentHashMap<>();
+ this.deadEntities = new HashSet<>();
+ this.cachedGadgetStates = new ConcurrentHashMap<>();
+ }
+
+ public void setLuaGroup(SceneGroup group) {
+ this.luaGroup = group;
+ this.groupId = group.id;
+ }
+
+ public boolean isCached() {
+ return this.isCached;
+ }
+
+ public void setCached(boolean value) {
+ this.isCached = value;
+ save(); //Save each time a group is registered or unregistered
+ }
+
+ public void cacheGadgetState(SceneGadget g, int state) {
+ if(g.persistent) //Only cache when is persistent
+ cachedGadgetStates.put(g.config_id, state);
+ }
+
+ public int getCachedGadgetState(SceneGadget g) {
+ Integer state = cachedGadgetStates.getOrDefault(g.config_id, null);
+ return (state == null) ? g.state : state;
+ }
+
+ public void save() {
+ DatabaseHelper.saveGroupInstance(this);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/game/world/data/TeleportProperties.java b/src/main/java/emu/grasscutter/game/world/data/TeleportProperties.java
new file mode 100644
index 000000000..283651120
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/world/data/TeleportProperties.java
@@ -0,0 +1,19 @@
+package emu.grasscutter.game.world.data;
+
+import emu.grasscutter.game.props.EnterReason;
+import emu.grasscutter.net.proto.EnterTypeOuterClass;
+import emu.grasscutter.server.event.player.PlayerTeleportEvent;
+import emu.grasscutter.utils.Position;
+import lombok.Builder;
+import lombok.Data;
+
+@Data
+@Builder
+public class TeleportProperties {
+ private final int sceneId;
+ private final PlayerTeleportEvent.TeleportType teleportType;
+ private final EnterReason enterReason;
+ private Position teleportTo;
+ private Position teleportRot;
+ private EnterTypeOuterClass.EnterType enterType;
+}
diff --git a/src/main/java/emu/grasscutter/scripts/EntityControllerScriptManager.java b/src/main/java/emu/grasscutter/scripts/EntityControllerScriptManager.java
new file mode 100644
index 000000000..fa3321bc0
--- /dev/null
+++ b/src/main/java/emu/grasscutter/scripts/EntityControllerScriptManager.java
@@ -0,0 +1,53 @@
+package emu.grasscutter.scripts;
+
+import emu.grasscutter.Grasscutter;
+import emu.grasscutter.scripts.data.controller.EntityController;
+import lombok.val;
+
+import javax.script.Bindings;
+import javax.script.CompiledScript;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static emu.grasscutter.utils.FileUtils.getScriptPath;
+
+public class EntityControllerScriptManager {
+ private static final Map gadgetController = new ConcurrentHashMap<>();
+
+ public static void load(){
+ cacheGadgetControllers();
+ }
+
+ private static void cacheGadgetControllers(){
+ try {
+ Files.newDirectoryStream(getScriptPath("Gadget/"), "*.lua").forEach(path -> {
+ val fileName = path.getFileName().toString();
+
+ if(!fileName.endsWith(".lua")) return;
+
+ val controllerName = fileName.substring(0, fileName.length()-4);
+ CompiledScript cs = ScriptLoader.getScript("Gadget/"+fileName);
+ Bindings bindings = ScriptLoader.getEngine().createBindings();
+ if (cs == null) return;
+
+ try{
+ cs.eval(bindings);
+ gadgetController.put(controllerName, new EntityController(cs, bindings));
+ } catch (Throwable e){
+ Grasscutter.getLogger().error("Error while loading gadget controller: {}", fileName);
+ }
+ });
+
+ Grasscutter.getLogger().info("Loaded {} gadget controllers", gadgetController.size());
+ } catch (IOException e) {
+ Grasscutter.getLogger().error("Error loading gadget controller luas");
+ }
+ }
+
+
+ public static EntityController getGadgetController(String name) {
+ return gadgetController.get(name);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/scripts/constants/GroupKillPolicy.java b/src/main/java/emu/grasscutter/scripts/constants/GroupKillPolicy.java
new file mode 100644
index 000000000..adc6485c9
--- /dev/null
+++ b/src/main/java/emu/grasscutter/scripts/constants/GroupKillPolicy.java
@@ -0,0 +1,9 @@
+package emu.grasscutter.scripts.constants;
+
+public enum GroupKillPolicy {
+ GROUP_KILL_NONE,
+ GROUP_KILL_ALL,
+ GROUP_KILL_MONSTER,
+ GROUP_KILL_GADGET,
+ GROUP_KILL_NPC
+}
diff --git a/src/main/java/emu/grasscutter/scripts/constants/IntValueEnum.java b/src/main/java/emu/grasscutter/scripts/constants/IntValueEnum.java
new file mode 100644
index 000000000..065465129
--- /dev/null
+++ b/src/main/java/emu/grasscutter/scripts/constants/IntValueEnum.java
@@ -0,0 +1,5 @@
+package emu.grasscutter.scripts.constants;
+
+public interface IntValueEnum {
+ int getValue();
+}
diff --git a/src/main/java/emu/grasscutter/scripts/constants/SealBattleType.java b/src/main/java/emu/grasscutter/scripts/constants/SealBattleType.java
new file mode 100644
index 000000000..49af43c6b
--- /dev/null
+++ b/src/main/java/emu/grasscutter/scripts/constants/SealBattleType.java
@@ -0,0 +1,7 @@
+package emu.grasscutter.scripts.constants;
+
+public enum SealBattleType {
+ NONE,
+ ENERGY_CHARGE,
+ KILL_MONSTER
+}
diff --git a/src/main/java/emu/grasscutter/scripts/data/SceneReplaceable.java b/src/main/java/emu/grasscutter/scripts/data/SceneReplaceable.java
new file mode 100644
index 000000000..1ffa1623e
--- /dev/null
+++ b/src/main/java/emu/grasscutter/scripts/data/SceneReplaceable.java
@@ -0,0 +1,12 @@
+package emu.grasscutter.scripts.data;
+
+import lombok.Setter;
+import lombok.ToString;
+
+@ToString
+@Setter
+public class SceneReplaceable {
+ public boolean value;
+ public int version;
+ public boolean new_bin_only;
+}
diff --git a/src/main/java/emu/grasscutter/scripts/data/controller/EntityController.java b/src/main/java/emu/grasscutter/scripts/data/controller/EntityController.java
new file mode 100644
index 000000000..e48831f94
--- /dev/null
+++ b/src/main/java/emu/grasscutter/scripts/data/controller/EntityController.java
@@ -0,0 +1,67 @@
+package emu.grasscutter.scripts.data.controller;
+
+import emu.grasscutter.Grasscutter;
+import emu.grasscutter.game.entity.GameEntity;
+import emu.grasscutter.game.props.ElementType;
+import emu.grasscutter.scripts.ScriptLib;
+import emu.grasscutter.scripts.ScriptLoader;
+import org.luaj.vm2.LuaError;
+import org.luaj.vm2.LuaValue;
+
+import javax.script.Bindings;
+import javax.script.CompiledScript;
+
+public class EntityController {
+ private transient CompiledScript entityController;
+ private transient Bindings entityControllerBindings;
+
+ public EntityController(CompiledScript entityController, Bindings entityControllerBindings){
+ this.entityController = entityController;
+ this.entityControllerBindings = entityControllerBindings;
+ }
+
+ public void onBeHurt(GameEntity entity, ElementType elementType, boolean isHost) {
+ callControllerScriptFunc(entity, "OnBeHurt", LuaValue.valueOf(elementType.getValue()), LuaValue.valueOf(0), LuaValue.valueOf(isHost));
+ }
+
+ public void onDie(GameEntity entity, ElementType elementType) {
+ callControllerScriptFunc(entity, "OnDie", LuaValue.valueOf(elementType.getValue()), LuaValue.valueOf(0));
+ }
+
+ public void onTimer(GameEntity entity, int now) {
+ callControllerScriptFunc(entity, "OnTimer", LuaValue.valueOf(now));
+ }
+
+ public int onClientExecuteRequest(GameEntity entity, int param1, int param2, int param3) {
+ Grasscutter.getLogger().debug("Request on {}, {}: {}", entity.getGroupId(), param1, entity.getPosition().toString());
+ LuaValue value = callControllerScriptFunc(entity, "OnClientExecuteReq", LuaValue.valueOf(param1), LuaValue.valueOf(param2), LuaValue.valueOf(param3));
+ if(value.isint() && value.toint() == 1) return 1;
+
+ return 0;
+ }
+
+ // TODO actual execution should probably be handle by EntityControllerScriptManager
+ private LuaValue callControllerScriptFunc(GameEntity entity, String funcName, LuaValue arg1) { return callControllerScriptFunc(entity, funcName, arg1, LuaValue.NIL, LuaValue.NIL); }
+ private LuaValue callControllerScriptFunc(GameEntity entity, String funcName, LuaValue arg1, LuaValue arg2) { return callControllerScriptFunc(entity, funcName, arg1, arg2, LuaValue.NIL); }
+ private LuaValue callControllerScriptFunc(GameEntity entity, String funcName, LuaValue arg1, LuaValue arg2, LuaValue arg3) {
+ LuaValue funcLua = null;
+ if (funcName != null && !funcName.isEmpty()) {
+ funcLua = (LuaValue) entityControllerBindings.get(funcName);
+ }
+
+ LuaValue ret = LuaValue.ONE;
+
+ if (funcLua != null) {
+ try {
+ ScriptLoader.getScriptLib().setCurrentEntity(entity);
+ ret = funcLua.invoke(new LuaValue[]{ScriptLoader.getScriptLibLua(), arg1, arg2, arg3}).arg1();
+ }catch (LuaError error) {
+ ScriptLib.logger.error("[LUA] call function failed in gadget {} with {} {} {},{}", entity.getEntityTypeId(), funcName, arg1, arg2, arg3, error);
+ ret = LuaValue.valueOf(-1);
+ }
+ } else if(funcName != null && !funcName.equals("OnTimer")) {
+ ScriptLib.logger.error("[LUA] unknown func in gadget {} with {} {} {} {}", entity.getEntityTypeId(), funcName, arg1, arg2, arg3);
+ }
+ return ret;
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerAddCustomTeamReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerAddCustomTeamReq.java
new file mode 100644
index 000000000..1f056ec0f
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerAddCustomTeamReq.java
@@ -0,0 +1,14 @@
+package emu.grasscutter.server.packet.recv;
+
+import emu.grasscutter.net.packet.Opcodes;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.packet.PacketHandler;
+import emu.grasscutter.server.game.GameSession;
+
+@Opcodes(PacketOpcodes.AddCustomTeamReq)
+public class HandlerAddCustomTeamReq extends PacketHandler {
+ @Override
+ public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
+ session.getPlayer().getTeamManager().addNewCustomTeam();
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerChangeHomeBgmReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerChangeHomeBgmReq.java
new file mode 100644
index 000000000..0ce35064e
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerChangeHomeBgmReq.java
@@ -0,0 +1,26 @@
+package emu.grasscutter.server.packet.recv;
+
+import emu.grasscutter.net.packet.Opcodes;
+import emu.grasscutter.net.packet.PacketHandler;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.Unk2700BEDLIGJANCJClientReq;
+import emu.grasscutter.server.game.GameSession;
+import emu.grasscutter.server.packet.send.PacketChangeHomeBgmNotify;
+import emu.grasscutter.server.packet.send.PacketChangeHomeBgmRsp;
+
+@Opcodes(PacketOpcodes.Unk2700_BEDLIGJANCJ_ClientReq)
+public class HandlerChangeHomeBgmReq extends PacketHandler {
+ @Override
+ public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
+ var req = Unk2700BEDLIGJANCJClientReq.Unk2700_BEDLIGJANCJ_ClientReq.parseFrom(payload);
+
+ int homeBgmId = req.getUnk2700BJHAMKKECEI();
+ var home = session.getPlayer().getHome();
+
+ home.getHomeSceneItem(session.getPlayer().getSceneId()).setHomeBgmId(homeBgmId);
+ home.save();
+
+ session.send(new PacketChangeHomeBgmNotify(homeBgmId));
+ session.send(new PacketChangeHomeBgmRsp());
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerCheckUgcStateReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerCheckUgcStateReq.java
new file mode 100644
index 000000000..174a022d9
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerCheckUgcStateReq.java
@@ -0,0 +1,23 @@
+package emu.grasscutter.server.packet.recv;
+
+import emu.grasscutter.net.packet.Opcodes;
+import emu.grasscutter.net.packet.PacketHandler;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.CheckUgcStateReqOuterClass.CheckUgcStateReq;
+import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
+import emu.grasscutter.server.game.GameSession;
+import emu.grasscutter.server.packet.send.PacketCheckUgcStateRsp;
+import lombok.val;
+
+@Opcodes(PacketOpcodes.CheckUgcStateReq)
+public class HandlerCheckUgcStateReq extends PacketHandler {
+
+ @Override
+ public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
+ val req = CheckUgcStateReq.parseFrom(payload);
+
+ session.send(new PacketCheckUgcStateRsp(Retcode.RET_SUCC));
+
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerCheckUgcUpdateReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerCheckUgcUpdateReq.java
new file mode 100644
index 000000000..59f034180
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerCheckUgcUpdateReq.java
@@ -0,0 +1,20 @@
+package emu.grasscutter.server.packet.recv;
+
+import emu.grasscutter.net.packet.Opcodes;
+import emu.grasscutter.net.packet.PacketHandler;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.CheckUgcUpdateReqOuterClass.CheckUgcUpdateReq;
+import emu.grasscutter.server.game.GameSession;
+import emu.grasscutter.server.packet.send.PacketCheckUgcUpdateRsp;
+
+@Opcodes(PacketOpcodes.CheckUgcUpdateReq)
+public class HandlerCheckUgcUpdateReq extends PacketHandler {
+
+ @Override
+ public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
+ var req = CheckUgcUpdateReq.parseFrom(payload);
+
+ session.send(new PacketCheckUgcUpdateRsp(req.getUgcType()));
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerDungeonPlayerDieReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerDungeonPlayerDieReq.java
new file mode 100644
index 000000000..03996439b
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerDungeonPlayerDieReq.java
@@ -0,0 +1,28 @@
+
+package emu.grasscutter.server.packet.recv;
+
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.net.packet.Opcodes;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.DungeonPlayerDieReqOuterClass.DungeonPlayerDieReq;
+import emu.grasscutter.net.packet.PacketHandler;
+import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
+import emu.grasscutter.server.game.GameSession;
+import emu.grasscutter.server.packet.send.PacketDungeonPlayerDieRsp;
+import lombok.val;
+
+@Opcodes(PacketOpcodes.DungeonPlayerDieReq)
+public class HandlerDungeonPlayerDieReq extends PacketHandler {
+
+ @Override
+ public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
+ DungeonPlayerDieReq req = DungeonPlayerDieReq.parseFrom(payload);
+
+ Player player = session.getPlayer();
+
+ boolean result = player.getScene().respawnPlayer(player);
+
+ player.sendPacket(new PacketDungeonPlayerDieRsp(result ? Retcode.RET_SUCC : Retcode.RET_FAIL));
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerDungeonSlipRevivePointActivateReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerDungeonSlipRevivePointActivateReq.java
new file mode 100644
index 000000000..211029fcf
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerDungeonSlipRevivePointActivateReq.java
@@ -0,0 +1,26 @@
+package emu.grasscutter.server.packet.recv;
+
+import emu.grasscutter.net.packet.Opcodes;
+import emu.grasscutter.net.packet.PacketHandler;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.DungeonSlipRevivePointActivateReqOuterClass.DungeonSlipRevivePointActivateReq;
+import emu.grasscutter.server.game.GameSession;
+import emu.grasscutter.server.packet.send.PacketDungeonSlipRevivePointActivateRsp;
+
+@Opcodes(PacketOpcodes.DungeonSlipRevivePointActivateReq)
+public class HandlerDungeonSlipRevivePointActivateReq extends PacketHandler {
+
+ @Override
+ public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
+ var req = DungeonSlipRevivePointActivateReq.parseFrom(payload);
+ var dungeonManager = session.getPlayer().getScene().getDungeonManager();
+
+ boolean success = false;
+ if (dungeonManager != null) {
+ success = dungeonManager.activateRespawnPoint(req.getSlipRevivePointId());
+ }
+
+ session.send(new PacketDungeonSlipRevivePointActivateRsp(success, req.getSlipRevivePointId()));
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerDungeonWayPointActivateReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerDungeonWayPointActivateReq.java
new file mode 100644
index 000000000..cbea5854d
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerDungeonWayPointActivateReq.java
@@ -0,0 +1,26 @@
+package emu.grasscutter.server.packet.recv;
+
+import emu.grasscutter.net.packet.Opcodes;
+import emu.grasscutter.net.packet.PacketHandler;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.DungeonWayPointActivateReqOuterClass.DungeonWayPointActivateReq;
+import emu.grasscutter.server.game.GameSession;
+import emu.grasscutter.server.packet.send.PacketDungeonWayPointActivateRsp;
+
+@Opcodes(PacketOpcodes.DungeonWayPointActivateReq)
+public class HandlerDungeonWayPointActivateReq extends PacketHandler {
+
+ @Override
+ public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
+ var req = DungeonWayPointActivateReq.parseFrom(payload);
+ var dungeonManager = session.getPlayer().getScene().getDungeonManager();
+
+ boolean success = false;
+ if(dungeonManager != null){
+ success = dungeonManager.activateRespawnPoint(req.getWayPointId());
+ }
+
+ session.send(new PacketDungeonWayPointActivateRsp(success, req.getWayPointId()));
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerEnterTrialAvatarActivityDungeonReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerEnterTrialAvatarActivityDungeonReq.java
new file mode 100644
index 000000000..c93f9c651
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerEnterTrialAvatarActivityDungeonReq.java
@@ -0,0 +1,33 @@
+package emu.grasscutter.server.packet.recv;
+
+import emu.grasscutter.game.activity.trialavatar.TrialAvatarActivityHandler;
+import emu.grasscutter.game.props.ActivityType;
+import emu.grasscutter.net.packet.Opcodes;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.packet.PacketHandler;
+import emu.grasscutter.server.packet.send.PacketEnterTrialAvatarActivityDungeonRsp;
+import emu.grasscutter.server.packet.send.PacketScenePlayerLocationNotify;
+import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
+import emu.grasscutter.net.proto.EnterTrialAvatarActivityDungeonReqOuterClass.EnterTrialAvatarActivityDungeonReq;
+import emu.grasscutter.server.game.GameSession;
+import lombok.val;
+
+@Opcodes(PacketOpcodes.EnterTrialAvatarActivityDungeonReq)
+public class HandlerEnterTrialAvatarActivityDungeonReq extends PacketHandler {
+
+ @Override
+ public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
+ val req = EnterTrialAvatarActivityDungeonReq.parseFrom(payload);
+
+ val handler = session.getPlayer().getActivityManager()
+ .getActivityHandlerAs(ActivityType.NEW_ACTIVITY_TRIAL_AVATAR, TrialAvatarActivityHandler.class);
+
+ boolean result = handler.isPresent() && handler.get().enterTrialDungeon(session.getPlayer(), req.getTrialAvatarIndexId(), req.getEnterPointId());
+
+ session.getPlayer().sendPacket(new PacketEnterTrialAvatarActivityDungeonRsp(
+ req.getActivityId(),
+ req.getTrialAvatarIndexId(),
+ result));
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerExecuteGadgetLuaReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerExecuteGadgetLuaReq.java
new file mode 100644
index 000000000..fcaccbdf6
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerExecuteGadgetLuaReq.java
@@ -0,0 +1,30 @@
+package emu.grasscutter.server.packet.recv;
+
+import emu.grasscutter.Grasscutter;
+import emu.grasscutter.game.entity.EntityGadget;
+import emu.grasscutter.game.entity.GameEntity;
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.net.packet.Opcodes;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.ExecuteGadgetLuaReqOuterClass.ExecuteGadgetLuaReq;
+import emu.grasscutter.net.packet.PacketHandler;
+import emu.grasscutter.server.game.GameSession;
+import emu.grasscutter.server.packet.send.PacketExecuteGadgetLuaRsp;
+
+@Opcodes(PacketOpcodes.ExecuteGadgetLuaReq)
+public class HandlerExecuteGadgetLuaReq extends PacketHandler {
+
+ @Override
+ public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
+ ExecuteGadgetLuaReq req = ExecuteGadgetLuaReq.parseFrom(payload);
+
+ Player player = session.getPlayer();
+ GameEntity entity = player.getScene().getEntities().get(req.getSourceEntityId());
+
+ int result = 1;
+ if(entity instanceof EntityGadget gadget) result = gadget.onClientExecuteRequest(req.getParam1(), req.getParam2(), req.getParam3());
+
+ player.sendPacket(new PacketExecuteGadgetLuaRsp(result));
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerFireWorkReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerFireWorkReq.java
new file mode 100644
index 000000000..aa7ea26e2
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerFireWorkReq.java
@@ -0,0 +1,21 @@
+package emu.grasscutter.server.packet.recv;
+
+import emu.grasscutter.net.packet.Opcodes;
+import emu.grasscutter.net.packet.PacketHandler;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.FireWorkReqOuterClass;
+import emu.grasscutter.server.game.GameSession;
+import emu.grasscutter.server.packet.send.PacketFireworkNotify;
+import emu.grasscutter.server.packet.send.PacketFireworkRsp;
+
+@Opcodes(PacketOpcodes.FireworkReq)
+public class HandlerFireWorkReq extends PacketHandler {
+ @Override
+ public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
+
+ var req
+ = FireWorkReqOuterClass.FireWorkReq.parseFrom(payload);
+ session.send(new PacketFireworkNotify(req.getFireWorkData()));
+ session.send(new PacketFireworkRsp());
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerFireworkSetReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerFireworkSetReq.java
new file mode 100644
index 000000000..ade4f4ced
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerFireworkSetReq.java
@@ -0,0 +1,23 @@
+package emu.grasscutter.server.packet.recv;
+
+import emu.grasscutter.net.packet.Opcodes;
+import emu.grasscutter.net.packet.PacketHandler;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.FireworkSetReqOuterClass;
+import emu.grasscutter.server.game.GameSession;
+import emu.grasscutter.server.packet.send.PacketFireworkSetNotify;
+import emu.grasscutter.server.packet.send.PacketFireworkSetRsp;
+
+@Opcodes(PacketOpcodes.FireworkSetReq)
+public class HandlerFireworkSetReq extends PacketHandler {
+ @Override
+ public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
+
+ var req
+ = FireworkSetReqOuterClass.FireworkSetReq.parseFrom(payload);
+
+
+ session.send(new PacketFireworkSetNotify(req.getData()));
+ session.send(new PacketFireworkSetRsp());
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetUgcBriefInfoReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetUgcBriefInfoReq.java
new file mode 100644
index 000000000..4d7656ea7
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetUgcBriefInfoReq.java
@@ -0,0 +1,33 @@
+package emu.grasscutter.server.packet.recv;
+
+import emu.grasscutter.game.activity.musicgame.MusicGameBeatmap;
+import emu.grasscutter.net.packet.Opcodes;
+import emu.grasscutter.net.packet.PacketHandler;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.GetUgcBriefInfoReqOuterClass;
+import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
+import emu.grasscutter.net.proto.UgcTypeOuterClass;
+import emu.grasscutter.server.game.GameSession;
+import emu.grasscutter.server.packet.send.PacketGetUgcBriefInfoRsp;
+
+@Opcodes(PacketOpcodes.GetUgcBriefInfoReq)
+public class HandlerGetUgcBriefInfoReq extends PacketHandler {
+
+ @Override
+ public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
+ var req = GetUgcBriefInfoReqOuterClass.GetUgcBriefInfoReq.parseFrom(payload);
+
+ if(req.getUgcType() == UgcTypeOuterClass.UgcType.UGC_TYPE_MUSIC_GAME){
+ var musicGameBeatmap = MusicGameBeatmap.getByShareId(req.getUgcGuid());
+
+ if(musicGameBeatmap != null){
+ session.send(new PacketGetUgcBriefInfoRsp(musicGameBeatmap.toBriefProto().build(), req.getUgcType()));
+ return;
+ }
+ }
+
+ session.send(new PacketGetUgcBriefInfoRsp(Retcode.RET_UGC_BRIEF_NOT_FOUND, req.getUgcType()));
+
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetUgcReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetUgcReq.java
new file mode 100644
index 000000000..ff6ddd02b
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetUgcReq.java
@@ -0,0 +1,48 @@
+package emu.grasscutter.server.packet.recv;
+
+import emu.grasscutter.game.activity.musicgame.MusicGameBeatmap;
+import emu.grasscutter.net.packet.Opcodes;
+import emu.grasscutter.net.packet.PacketHandler;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.RetcodeOuterClass;
+import emu.grasscutter.net.proto.UgcTypeOuterClass;
+import emu.grasscutter.server.game.GameSession;
+import emu.grasscutter.server.packet.send.PacketGetUgcRsp;
+import emu.grasscutter.net.proto.GetUgcReqOuterClass.GetUgcReq;
+import lombok.val;
+
+@Opcodes(PacketOpcodes.GetUgcReq)
+public class HandlerGetUgcReq extends PacketHandler {
+
+ @Override
+ public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
+ val req = GetUgcReq.parseFrom(payload);
+
+ PacketGetUgcRsp rsp = null;
+
+ if(req.getUgcType() == UgcTypeOuterClass.UgcType.UGC_TYPE_MUSIC_GAME) {
+ val musicGameBeatmap = MusicGameBeatmap.getByShareId(req.getUgcGuid());
+
+ if (musicGameBeatmap != null) {
+ rsp = new PacketGetUgcRsp(
+ musicGameBeatmap.toBriefProto().build(),
+ musicGameBeatmap.toProto(),
+ req
+ );
+ } else {
+ rsp = new PacketGetUgcRsp(
+ RetcodeOuterClass.Retcode.RET_UGC_DATA_NOT_FOUND,
+ req
+ );
+ }
+ }else {
+ rsp = new PacketGetUgcRsp(
+ RetcodeOuterClass.Retcode.RET_UGC_DISABLED,
+ req
+ );
+ }
+
+ session.send(rsp);
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeUnknown2Req.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeUnknown2Req.java
new file mode 100644
index 000000000..467449067
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerHomeUnknown2Req.java
@@ -0,0 +1,20 @@
+package emu.grasscutter.server.packet.recv;
+
+import emu.grasscutter.net.packet.Opcodes;
+import emu.grasscutter.net.packet.PacketHandler;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.server.game.GameSession;
+import emu.grasscutter.server.packet.send.PacketHomeUnknown2Rsp;
+
+@Opcodes(PacketOpcodes.Unk2700_ACILPONNGGK_ClientReq)
+public class HandlerHomeUnknown2Req extends PacketHandler {
+
+ @Override
+ public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
+ /*
+ * This packet is about the edit mode
+ */
+ session.send(new PacketHomeUnknown2Rsp());
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerQuestCreateEntityReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerQuestCreateEntityReq.java
new file mode 100644
index 000000000..ff2c93e04
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerQuestCreateEntityReq.java
@@ -0,0 +1,63 @@
+package emu.grasscutter.server.packet.recv;
+
+import emu.grasscutter.data.GameData;
+import emu.grasscutter.data.excels.GadgetData;
+import emu.grasscutter.data.excels.ItemData;
+import emu.grasscutter.data.excels.MonsterData;
+import emu.grasscutter.game.entity.*;
+import emu.grasscutter.net.packet.Opcodes;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.packet.PacketHandler;
+import emu.grasscutter.server.game.GameSession;
+import emu.grasscutter.server.packet.send.PacketQuestCreateEntityRsp;
+import emu.grasscutter.utils.Position;
+import lombok.val;
+import emu.grasscutter.net.proto.QuestCreateEntityReqOuterClass.QuestCreateEntityReq;
+
+@Opcodes(PacketOpcodes.QuestCreateEntityReq)
+public class HandlerQuestCreateEntityReq extends PacketHandler {
+
+ @Override
+ public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
+ val req = QuestCreateEntityReq.parseFrom(payload);
+ val entity = req.getEntity();
+ val scene = session.getPlayer().getWorld().getSceneById(entity.getSceneId());
+
+ val pos = new Position(entity.getPos());
+ val rot = new Position(entity.getRot());
+ GameEntity gameEntity = null;
+ switch (entity.getEntityCase()){
+ case GADGET_ID -> {
+ val gadgetId = entity.getGadgetId();
+ val gadgetInfo = entity.getGadget();
+ GadgetData gadgetData = GameData.getGadgetDataMap().get(gadgetId);
+ gameEntity = switch (gadgetData.getType()){
+ case Vehicle -> new EntityVehicle(scene, session.getPlayer(), gadgetId, 0, pos, rot);
+ default -> new EntityGadget(scene, gadgetId, pos, rot);
+ };
+ }
+ case ITEM_ID -> {
+ val itemId = entity.getItemId();
+ ItemData itemData = GameData.getItemDataMap().get(itemId);
+ gameEntity = new EntityItem(scene, null, itemData, pos, 1, true);
+ }
+ case MONSTER_ID -> {
+ val monsterId = entity.getMonsterId();
+ val level = entity.getLevel();
+ MonsterData monsterData = GameData.getMonsterDataMap().get(monsterId);
+ gameEntity = new EntityMonster(scene, monsterData, pos, level);
+ }
+ case NPC_ID -> {
+ }
+ }
+
+ if(gameEntity != null){
+ scene.addEntity(gameEntity);
+ }
+
+ val createdEntityId = gameEntity!=null ? gameEntity.getId() : -1;
+
+ session.send(new PacketQuestCreateEntityRsp(createdEntityId, req));
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerQuestDestroyEntityReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerQuestDestroyEntityReq.java
new file mode 100644
index 000000000..1e0f9b703
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerQuestDestroyEntityReq.java
@@ -0,0 +1,27 @@
+package emu.grasscutter.server.packet.recv;
+
+import emu.grasscutter.net.packet.Opcodes;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.packet.PacketHandler;
+import emu.grasscutter.server.game.GameSession;
+import emu.grasscutter.server.packet.send.PacketQuestDestroyEntityRsp;
+import lombok.val;
+import emu.grasscutter.net.proto.QuestDestroyEntityReqOuterClass.QuestDestroyEntityReq;
+
+@Opcodes(PacketOpcodes.QuestDestroyEntityReq)
+public class HandlerQuestDestroyEntityReq extends PacketHandler {
+
+ @Override
+ public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
+ val req = QuestDestroyEntityReq.parseFrom(payload);
+ val scene = session.getPlayer().getWorld().getSceneById(req.getSceneId());
+ val entity = scene.getEntityById(req.getEntityId());
+
+ if(entity!=null){
+ scene.removeEntity(entity);
+ }
+
+ session.send(new PacketQuestDestroyEntityRsp(entity!=null, req));
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerQuestDestroyNpcReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerQuestDestroyNpcReq.java
new file mode 100644
index 000000000..4e334724c
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerQuestDestroyNpcReq.java
@@ -0,0 +1,20 @@
+package emu.grasscutter.server.packet.recv;
+
+import emu.grasscutter.game.entity.EntityNPC;
+import emu.grasscutter.net.packet.Opcodes;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.packet.PacketHandler;
+import emu.grasscutter.server.game.GameSession;
+import emu.grasscutter.server.packet.send.PacketQuestDestroyNpcRsp;
+import lombok.val;
+import emu.grasscutter.net.proto.QuestDestroyNpcReqOuterClass.QuestDestroyNpcReq;
+
+@Opcodes(PacketOpcodes.QuestDestroyNpcReq)
+public class HandlerQuestDestroyNpcReq extends PacketHandler {
+ @Override
+ public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
+ val req = QuestDestroyNpcReq.parseFrom(payload);
+
+ session.send(new PacketQuestDestroyNpcRsp(req.getNpcId(), req.getParentQuestId(), 0));
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerQuestTransmitReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerQuestTransmitReq.java
new file mode 100644
index 000000000..67979a344
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerQuestTransmitReq.java
@@ -0,0 +1,32 @@
+package emu.grasscutter.server.packet.recv;
+
+import emu.grasscutter.data.GameData;
+import emu.grasscutter.game.quest.GameMainQuest;
+import emu.grasscutter.net.packet.Opcodes;
+import emu.grasscutter.net.packet.PacketHandler;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.server.game.GameSession;
+import emu.grasscutter.server.packet.send.PacketQuestTransmitRsp;
+import emu.grasscutter.utils.Position;
+import emu.grasscutter.net.proto.QuestTransmitReqOuterClass.QuestTransmitReq;
+
+import java.util.List;
+import java.util.ArrayList;
+import lombok.val;
+
+@Opcodes(PacketOpcodes.QuestTransmitReq)
+public class HandlerQuestTransmitReq extends PacketHandler {
+
+ @Override
+ public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
+ val req = QuestTransmitReq.parseFrom(payload);
+ GameMainQuest mainQuest = session.getPlayer().getQuestManager().getMainQuestById(req.getQuestId() / 100);
+ List posAndRot = new ArrayList<>();
+ boolean result = false;
+ if(mainQuest.hasTeleportPostion(req.getQuestId(), posAndRot)){
+ int sceneId = GameData.getTeleportDataMap().get(req.getQuestId()).getTransmit_points().get(0).getScene_id();
+ result = session.getPlayer().getWorld().transferPlayerToScene(session.getPlayer(), sceneId, posAndRot.get(0));
+ }
+ session.send(new PacketQuestTransmitRsp(result, req));
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerReceivedTrialAvatarActivityRewardReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerReceivedTrialAvatarActivityRewardReq.java
new file mode 100644
index 000000000..b0ce377e0
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerReceivedTrialAvatarActivityRewardReq.java
@@ -0,0 +1,32 @@
+package emu.grasscutter.server.packet.recv;
+
+import emu.grasscutter.game.activity.trialavatar.TrialAvatarActivityHandler;
+import emu.grasscutter.game.props.ActivityType;
+import emu.grasscutter.net.packet.Opcodes;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.packet.PacketHandler;
+import emu.grasscutter.server.game.GameSession;
+import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
+import emu.grasscutter.net.proto.ReceivedTrialAvatarActivityRewardReqOuterClass.ReceivedTrialAvatarActivityRewardReq;
+import emu.grasscutter.server.packet.send.PacketReceivedTrialAvatarActivityRewardRsp;
+
+import lombok.val;
+
+@Opcodes(PacketOpcodes.ReceivedTrialAvatarActivityRewardReq)
+public class HandlerReceivedTrialAvatarActivityRewardReq extends PacketHandler {
+
+ @Override
+ public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
+ val req = ReceivedTrialAvatarActivityRewardReq.parseFrom(payload);
+ val player = session.getPlayer();
+ val handler = player.getActivityManager().getActivityHandlerAs(ActivityType.NEW_ACTIVITY_TRIAL_AVATAR, TrialAvatarActivityHandler.class);
+
+ boolean result = handler.isPresent() && handler.get().getReward(player, req.getTrialAvatarIndexId());
+
+ session.getPlayer().sendPacket(new PacketReceivedTrialAvatarActivityRewardRsp(
+ 5002, // trial activity id
+ req.getTrialAvatarIndexId(),
+ result));
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerRemoveCustomTeamReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerRemoveCustomTeamReq.java
new file mode 100644
index 000000000..9ea1fe4c0
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerRemoveCustomTeamReq.java
@@ -0,0 +1,16 @@
+package emu.grasscutter.server.packet.recv;
+
+import emu.grasscutter.net.packet.Opcodes;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.RemoveCustomTeamReqOuterClass.RemoveCustomTeamReq;
+import emu.grasscutter.net.packet.PacketHandler;
+import emu.grasscutter.server.game.GameSession;
+
+@Opcodes(PacketOpcodes.RemoveCustomTeamReq)
+public class HandlerRemoveCustomTeamReq extends PacketHandler {
+ @Override
+ public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
+ RemoveCustomTeamReq req = RemoveCustomTeamReq.parseFrom(payload);
+ session.getPlayer().getTeamManager().removeCustomTeam(req.getId());
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerSaveUgcReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerSaveUgcReq.java
new file mode 100644
index 000000000..619394a58
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerSaveUgcReq.java
@@ -0,0 +1,84 @@
+package emu.grasscutter.server.packet.recv;
+
+import emu.grasscutter.database.DatabaseHelper;
+import emu.grasscutter.game.activity.musicgame.MusicGameActivityHandler;
+import emu.grasscutter.game.activity.musicgame.MusicGameBeatmap;
+import emu.grasscutter.game.activity.musicgame.MusicGamePlayerData;
+import emu.grasscutter.game.props.ActivityType;
+import emu.grasscutter.net.packet.Opcodes;
+import emu.grasscutter.net.packet.PacketHandler;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.RetcodeOuterClass;
+import emu.grasscutter.net.proto.SaveUgcReqOuterClass;
+import emu.grasscutter.net.proto.UgcTypeOuterClass;
+import emu.grasscutter.server.game.GameSession;
+import emu.grasscutter.server.packet.send.PacketActivityInfoNotify;
+import emu.grasscutter.server.packet.send.PacketMusicGameCreateBeatmapRsp;
+import emu.grasscutter.utils.Utils;
+import lombok.val;
+
+import java.util.Objects;
+
+@Opcodes(PacketOpcodes.SaveUgcReq)
+public class HandlerSaveUgcReq extends PacketHandler {
+
+ @Override
+ public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
+ val req = SaveUgcReqOuterClass.SaveUgcReq.parseFrom(payload);
+
+ // We only support music game user generated content
+ if(req.getUgcType() != UgcTypeOuterClass.UgcType.UGC_TYPE_MUSIC_GAME){
+ session.send(new PacketMusicGameCreateBeatmapRsp(RetcodeOuterClass.Retcode.RET_UGC_DISABLED, req.getUgcType()));
+ return;
+ }
+ val briefInfo = req.getMusicBriefInfo();
+
+ val musicGameBeatmap = MusicGameBeatmap.of()
+ .musicId(briefInfo.getMusicId())
+ .musicNoteCount(briefInfo.getNoteCount())
+ .savePosition(briefInfo.getSaveIdx())
+ .savePageType(briefInfo.getSavePageType())
+ .version(briefInfo.getVersion())
+ .afterNoteList(briefInfo.getAfterNoteListList())
+ .beforeNoteList(briefInfo.getBeforeNoteListList())
+ .timeLineEditTime(briefInfo.getTimeLineEditTime())
+ .publishTime(briefInfo.getPublishTime())
+ .realTimeEditTime(briefInfo.getRealTimeEditTime())
+ .maxScore(briefInfo.getMaxScore())
+ .authorUid(session.getPlayer().getUid())
+ .beatmap(MusicGameBeatmap.parse(req.getMusicRecord().getMusicTrackListList()))
+ .createTime(Utils.getCurrentSeconds())
+ .build();
+
+ musicGameBeatmap.save();
+
+ val playerData = session.getPlayer().getActivityManager().getPlayerActivityDataByActivityType(ActivityType.NEW_ACTIVITY_MUSIC_GAME);
+ if(playerData.isEmpty()){
+ session.send(new PacketMusicGameCreateBeatmapRsp(RetcodeOuterClass.Retcode.RET_UGC_DATA_NOT_FOUND, req.getUgcType()));
+ return;
+ }
+
+ val handler = (MusicGameActivityHandler) playerData.get().getActivityHandler();
+ val musicGamePlayerData = handler.getMusicGamePlayerData(playerData.get());
+
+ val oldBeatmap = musicGamePlayerData.getPersonalCustomBeatmapRecord().values().stream()
+ .map(MusicGamePlayerData.CustomBeatmapRecord::getMusicShareId)
+ .map(DatabaseHelper::getMusicGameBeatmap)
+ .filter(Objects::nonNull)
+ .filter(item -> item.getAuthorUid() == session.getPlayer().getUid())
+ .filter(item -> item.getMusicId() == req.getMusicBriefInfo().getMusicId())
+ .filter(item -> item.getSavePosition() == req.getMusicBriefInfo().getSaveIdx())
+ .findFirst();
+
+ // delete old beatmap for player
+ // the old beatmap is still in database so that others can still play.
+ oldBeatmap.ifPresent(i -> handler.removePersonalBeatmap(playerData.get(), i));
+
+ // link this beatmap to player's personal data
+ handler.addPersonalBeatmap(playerData.get(), musicGameBeatmap);
+
+ session.send(new PacketActivityInfoNotify(handler.toProto(playerData.get(), session.getPlayer().getActivityManager().getConditionExecutor())));
+ session.send(new PacketMusicGameCreateBeatmapRsp(musicGameBeatmap.getMusicShareId(), req.getUgcType()));
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketAddCustomTeamRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketAddCustomTeamRsp.java
new file mode 100644
index 000000000..54cc022c9
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketAddCustomTeamRsp.java
@@ -0,0 +1,22 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
+import emu.grasscutter.net.proto.AddCustomTeamRspOuterClass.AddCustomTeamRsp;
+
+public class PacketAddCustomTeamRsp extends BasePacket {
+ public PacketAddCustomTeamRsp(Retcode retcode) {
+ super(PacketOpcodes.AddCustomTeamRsp);
+
+ AddCustomTeamRsp proto = AddCustomTeamRsp.newBuilder()
+ .setRetcode(retcode.getNumber())
+ .build();
+
+ this.setData(proto);
+ }
+
+ public PacketAddCustomTeamRsp() {
+ this(Retcode.RET_SUCC);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketAddNoGachaAvatarCardNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketAddNoGachaAvatarCardNotify.java
new file mode 100644
index 000000000..f2b73af96
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketAddNoGachaAvatarCardNotify.java
@@ -0,0 +1,40 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.game.avatar.Avatar;
+import emu.grasscutter.game.inventory.GameItem;
+import emu.grasscutter.game.props.ActionReason;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.AddNoGachaAvatarCardNotifyOuterClass.AddNoGachaAvatarCardNotify;
+
+public class PacketAddNoGachaAvatarCardNotify extends BasePacket {
+
+ public PacketAddNoGachaAvatarCardNotify(Avatar avatar, ActionReason reason, GameItem item) {
+ super(PacketOpcodes.AddNoGachaAvatarCardNotify, true);
+
+ AddNoGachaAvatarCardNotify proto = AddNoGachaAvatarCardNotify.newBuilder()
+ .setAvatarId(avatar.getAvatarId())
+ .setReason(reason.getValue())
+ .setInitialLevel(1)
+ .setItemId(item.getItemId())
+ .setInitialPromoteLevel(0)
+ .build();
+
+ this.setData(proto);
+ }
+
+ public PacketAddNoGachaAvatarCardNotify(int avatarId, ActionReason reason, GameItem item) {
+ super(PacketOpcodes.AddNoGachaAvatarCardNotify, true);
+
+ AddNoGachaAvatarCardNotify proto = AddNoGachaAvatarCardNotify.newBuilder()
+ .setAvatarId(avatarId)
+ .setReason(reason.getValue())
+ .setInitialLevel(1)
+ .setItemId(item.getItemId())
+ .setInitialPromoteLevel(0)
+ .build();
+
+ this.setData(proto);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketAvatarDelNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketAvatarDelNotify.java
new file mode 100644
index 000000000..1c4229161
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketAvatarDelNotify.java
@@ -0,0 +1,20 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.AvatarDelNotifyOuterClass.AvatarDelNotify;
+
+import java.util.List;
+
+public class PacketAvatarDelNotify extends BasePacket {
+
+ public PacketAvatarDelNotify(List avatarGuidList) {
+ super(PacketOpcodes.AvatarDelNotify);
+
+ AvatarDelNotify proto = AvatarDelNotify.newBuilder()
+ .addAllAvatarGuidList(avatarGuidList)
+ .build();
+
+ this.setData(proto);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketBeginCameraSceneLookNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketBeginCameraSceneLookNotify.java
new file mode 100644
index 000000000..c1e2133fb
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketBeginCameraSceneLookNotify.java
@@ -0,0 +1,61 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.BeginCameraSceneLookNotifyOuterClass.BeginCameraSceneLookNotify;
+import emu.grasscutter.utils.Position;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.val;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+public class PacketBeginCameraSceneLookNotify extends BasePacket {
+
+ public PacketBeginCameraSceneLookNotify(CameraSceneLookNotify parameters) {
+ super(PacketOpcodes.BeginCameraSceneLookNotify);
+ val builder = BeginCameraSceneLookNotify.newBuilder()
+ .setLookPos(parameters.lookPos.toProto())
+ .setFollowPos(parameters.followPos.toProto())
+ .setDuration(parameters.duration)
+ .setIsAllowInput(parameters.isAllowInput)
+ .setIsSetFollowPos(parameters.setFollowPos)
+ .setIsSetScreenXy(parameters.isScreenXY)
+ .setIsRecoverKeepCurrent(parameters.recoverKeepCurrent)
+ .setIsChangePlayMode(parameters.isChangePlayMode)
+ .setScreenY(parameters.screenY)
+ .setScreenX(parameters.screenX)
+ .setIsForce(parameters.isForce)
+ .setIsForce(parameters.isForceWalk)
+ .setEntityId(parameters.entityId)
+ .addAllOtherParams(parameters.otherParams);
+ this.setData(builder);
+ }
+
+ // TODO check default values
+ // todo find missing field usages:
+ // enum Unk2700_HIAKNNCKHJB (Unk2700_LNCHDDOOECD)
+ // Unk3000_MNLLCJMPMNH (uint32)
+ // Unk2700_DHAHEKOGHBJ (float)
+ // Unk3000_IEFIKMHCKDH (uint32)
+ // Unk3000_OGCLMFFADBD (float)
+
+ @Data @NoArgsConstructor
+ public static class CameraSceneLookNotify{
+ Position lookPos = new Position();
+ Position followPos = new Position();
+ float duration = 0.0f;
+ boolean isAllowInput = true;
+ boolean setFollowPos = false;
+ boolean isScreenXY = false;
+ boolean recoverKeepCurrent = true;
+ boolean isForceWalk = false;
+ boolean isForce = false;
+ boolean isChangePlayMode = false;
+ float screenY = 0.0f;
+ float screenX = 0.0f;
+ int entityId = 0;
+ Collection otherParams = new ArrayList<>(0);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketChangeHomeBgmNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketChangeHomeBgmNotify.java
new file mode 100644
index 000000000..f61b05d25
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketChangeHomeBgmNotify.java
@@ -0,0 +1,17 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.Unk2700FJEHHCPCBLGServerNotify;
+
+public class PacketChangeHomeBgmNotify extends BasePacket {
+ public PacketChangeHomeBgmNotify(int homeBgmId) {
+ super(PacketOpcodes.Unk2700_FJEHHCPCBLG_ServerNotify);
+
+ var notify = Unk2700FJEHHCPCBLGServerNotify.Unk2700_FJEHHCPCBLG_ServerNotify.newBuilder()
+ .setUnk2700BJHAMKKECEI(homeBgmId)
+ .build();
+
+ this.setData(notify);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketChangeHomeBgmRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketChangeHomeBgmRsp.java
new file mode 100644
index 000000000..19fb43c92
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketChangeHomeBgmRsp.java
@@ -0,0 +1,17 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.Unk2700OGHMHELMBNNServerRsp;
+
+public class PacketChangeHomeBgmRsp extends BasePacket {
+ public PacketChangeHomeBgmRsp() {
+ super(PacketOpcodes.Unk2700_OGHMHELMBNN_ServerRsp);
+
+ var rsp = Unk2700OGHMHELMBNNServerRsp.Unk2700_OGHMHELMBNN_ServerRsp.newBuilder()
+ .setRetcode(0)
+ .build();
+
+ this.setData(rsp);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketCheckUgcStateRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketCheckUgcStateRsp.java
new file mode 100644
index 000000000..3b9884ea3
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketCheckUgcStateRsp.java
@@ -0,0 +1,18 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.CheckUgcStateRspOuterClass.CheckUgcStateRsp;
+import emu.grasscutter.net.proto.RetcodeOuterClass;
+
+public class PacketCheckUgcStateRsp extends BasePacket {
+
+ public PacketCheckUgcStateRsp(RetcodeOuterClass.Retcode ret) {
+ super(PacketOpcodes.CheckUgcStateRsp);
+
+ this.setData(CheckUgcStateRsp.newBuilder()
+ .setRetcode(ret.getNumber())
+ );
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketCheckUgcUpdateRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketCheckUgcUpdateRsp.java
new file mode 100644
index 000000000..87b491df4
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketCheckUgcUpdateRsp.java
@@ -0,0 +1,19 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.CheckUgcUpdateRspOuterClass.CheckUgcUpdateRsp;
+import emu.grasscutter.net.proto.UgcTypeOuterClass.UgcType;
+
+public class PacketCheckUgcUpdateRsp extends BasePacket {
+
+ public PacketCheckUgcUpdateRsp(UgcType ugcType) {
+ super(PacketOpcodes.CheckUgcUpdateRsp);
+
+ var proto = CheckUgcUpdateRsp.newBuilder();
+
+ proto.setUgcType(ugcType);
+
+ this.setData(proto);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketCloseCommonTipsNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketCloseCommonTipsNotify.java
new file mode 100644
index 000000000..663aeb314
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketCloseCommonTipsNotify.java
@@ -0,0 +1,13 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.CloseCommonTipsNotifyOuterClass.CloseCommonTipsNotify;
+
+public class PacketCloseCommonTipsNotify extends BasePacket {
+
+ public PacketCloseCommonTipsNotify() {
+ super(PacketOpcodes.CloseCommonTipsNotify);
+ this.setData(CloseCommonTipsNotify.newBuilder().build());
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketCustomTeamListNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketCustomTeamListNotify.java
new file mode 100644
index 000000000..182adb0fb
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketCustomTeamListNotify.java
@@ -0,0 +1,26 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.CustomTeamListNotifyOuterClass.CustomTeamListNotify;
+
+public class PacketCustomTeamListNotify extends BasePacket {
+ public PacketCustomTeamListNotify(Player player) {
+ super(PacketOpcodes.CustomTeamListNotify);
+
+ CustomTeamListNotify.Builder proto = CustomTeamListNotify.newBuilder();
+
+ // Add the id list for custom teams.
+ for (int id : player.getTeamManager().getTeams().keySet()) {
+ if (id > 4) {
+ proto.addCustomTeamIds(id);
+ }
+ }
+
+ // Add the avatar lists for all the teams the player has.
+ player.getTeamManager().getTeams().forEach((id, teamInfo) -> proto.putAvatarTeamMap(id, teamInfo.toProto(player)));
+
+ this.setData(proto);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketCutsceneBeginNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketCutsceneBeginNotify.java
new file mode 100644
index 000000000..2ccb7032a
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketCutsceneBeginNotify.java
@@ -0,0 +1,16 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.CutSceneBeginNotifyOuterClass.CutSceneBeginNotify;
+
+public class PacketCutsceneBeginNotify extends BasePacket {
+
+ public PacketCutsceneBeginNotify(int cutsceneId) {
+ super(PacketOpcodes.CutSceneBeginNotify);
+
+ setData(CutSceneBeginNotify.newBuilder()
+ .setCutsceneId(cutsceneId));
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketDelQuestNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketDelQuestNotify.java
new file mode 100644
index 000000000..c11a10b6c
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketDelQuestNotify.java
@@ -0,0 +1,21 @@
+package emu.grasscutter.server.packet.send;
+
+import java.util.List;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.QuestDelNotifyOuterClass.QuestDelNotify;
+
+public class PacketDelQuestNotify extends BasePacket {
+
+ public PacketDelQuestNotify(int questId) {
+ super(PacketOpcodes.QuestDelNotify);
+
+ QuestDelNotify proto = QuestDelNotify.newBuilder()
+ .setQuestId(questId)
+ .build();
+
+ this.setData(proto);
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonPlayerDieNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonPlayerDieNotify.java
new file mode 100644
index 000000000..d98e576f6
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonPlayerDieNotify.java
@@ -0,0 +1,25 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType;
+import emu.grasscutter.net.proto.DungeonPlayerDieNotifyOuterClass.DungeonPlayerDieNotify;
+
+public class PacketDungeonPlayerDieNotify extends BasePacket {
+
+ public PacketDungeonPlayerDieNotify(PlayerDieType playerDieType, int killerId, int dungeonId, int waitTime, int reviveCount, boolean isGadget) {
+ super(PacketOpcodes.DungeonPlayerDieNotify);
+
+ DungeonPlayerDieNotify.Builder proto = DungeonPlayerDieNotify.newBuilder()
+ .setDieType(playerDieType)
+ .setReviveCount(reviveCount)
+ .setWaitTime(waitTime)
+ .setDungeonId(dungeonId)
+ .setMurdererEntityId(killerId);
+
+ if(isGadget) proto.setGadgetId(killerId);
+ else proto.setMonsterId(killerId);
+
+ this.setData(proto.build());
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonPlayerDieRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonPlayerDieRsp.java
new file mode 100644
index 000000000..44cf6c641
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonPlayerDieRsp.java
@@ -0,0 +1,19 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.DungeonPlayerDieRspOuterClass.DungeonPlayerDieRsp;
+import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
+
+public class PacketDungeonPlayerDieRsp extends BasePacket {
+
+ public PacketDungeonPlayerDieRsp(Retcode retcode) {
+ super(PacketOpcodes.DungeonPlayerDieRsp);
+
+ DungeonPlayerDieRsp proto = DungeonPlayerDieRsp.newBuilder()
+ .setRetcode(retcode.getNumber())
+ .build();
+
+ this.setData(proto);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonSlipRevivePointActivateRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonSlipRevivePointActivateRsp.java
new file mode 100644
index 000000000..2cd679963
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonSlipRevivePointActivateRsp.java
@@ -0,0 +1,16 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.DungeonSlipRevivePointActivateRspOuterClass.DungeonSlipRevivePointActivateRsp;
+import emu.grasscutter.net.proto.RetcodeOuterClass;
+
+public class PacketDungeonSlipRevivePointActivateRsp extends BasePacket {
+ public PacketDungeonSlipRevivePointActivateRsp(boolean success, int pointId) {
+ super(PacketOpcodes.DungeonSlipRevivePointActivateRsp);
+
+ this.setData(DungeonSlipRevivePointActivateRsp.newBuilder()
+ .setSlipRevivePointId(pointId)
+ .setRetcode(success ? RetcodeOuterClass.Retcode.RET_SUCC_VALUE : RetcodeOuterClass.Retcode.RET_FAIL_VALUE));
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonWayPointActivateRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonWayPointActivateRsp.java
new file mode 100644
index 000000000..7ec40a92c
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonWayPointActivateRsp.java
@@ -0,0 +1,16 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.DungeonWayPointActivateRspOuterClass;
+import emu.grasscutter.net.proto.RetcodeOuterClass;
+
+public class PacketDungeonWayPointActivateRsp extends BasePacket {
+ public PacketDungeonWayPointActivateRsp(boolean success, int pointId) {
+ super(PacketOpcodes.DungeonWayPointActivateRsp);
+
+ this.setData(DungeonWayPointActivateRspOuterClass.DungeonWayPointActivateRsp.newBuilder()
+ .setWayPointId(pointId)
+ .setRetcode(success ? RetcodeOuterClass.Retcode.RET_SUCC_VALUE : RetcodeOuterClass.Retcode.RET_FAIL_VALUE));
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonWayPointNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonWayPointNotify.java
new file mode 100644
index 000000000..29bb640fa
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketDungeonWayPointNotify.java
@@ -0,0 +1,19 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.DungeonWayPointActivateRspOuterClass;
+import emu.grasscutter.net.proto.DungeonWayPointNotifyOuterClass;
+import emu.grasscutter.net.proto.RetcodeOuterClass;
+
+import java.util.Set;
+
+public class PacketDungeonWayPointNotify extends BasePacket {
+ public PacketDungeonWayPointNotify(boolean added, Set activePointIds) {
+ super(PacketOpcodes.DungeonWayPointNotify);
+
+ this.setData(DungeonWayPointNotifyOuterClass.DungeonWayPointNotify.newBuilder()
+ .addAllActiveWayPointList(activePointIds)
+ .setIsAdd(added));
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketEndCameraSceneLookNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketEndCameraSceneLookNotify.java
new file mode 100644
index 000000000..de8914154
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketEndCameraSceneLookNotify.java
@@ -0,0 +1,14 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.EndCameraSceneLookNotifyOuterClass.EndCameraSceneLookNotify;
+
+public class PacketEndCameraSceneLookNotify extends BasePacket {
+
+ public PacketEndCameraSceneLookNotify() {
+ super(PacketOpcodes.EndCameraSceneLookNotify);
+
+ this.setData(EndCameraSceneLookNotify.newBuilder());
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketEnterTrialAvatarActivityDungeonRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketEnterTrialAvatarActivityDungeonRsp.java
new file mode 100644
index 000000000..1a5744635
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketEnterTrialAvatarActivityDungeonRsp.java
@@ -0,0 +1,20 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.EnterTrialAvatarActivityDungeonRspOuterClass.EnterTrialAvatarActivityDungeonRsp;
+import emu.grasscutter.net.proto.RetcodeOuterClass;
+
+public class PacketEnterTrialAvatarActivityDungeonRsp extends BasePacket {
+
+ public PacketEnterTrialAvatarActivityDungeonRsp(int activityId, int trialAvatarIndexId, boolean success) {
+ this(activityId, trialAvatarIndexId, success ? RetcodeOuterClass.Retcode.RET_SUCC_VALUE : RetcodeOuterClass.Retcode.RET_FAIL_VALUE);
+ }
+ public PacketEnterTrialAvatarActivityDungeonRsp(int activityId, int trialAvatarIndexId, int retcodeVal) {
+ super(PacketOpcodes.EnterTrialAvatarActivityDungeonRsp);
+ this.setData(EnterTrialAvatarActivityDungeonRsp.newBuilder()
+ .setActivityId(activityId)
+ .setTrialAvatarIndexId(trialAvatarIndexId)
+ .setRetcode(retcodeVal));
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketExecuteGadgetLuaRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketExecuteGadgetLuaRsp.java
new file mode 100644
index 000000000..3ae98e150
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketExecuteGadgetLuaRsp.java
@@ -0,0 +1,19 @@
+
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.ExecuteGadgetLuaRspOuterClass.ExecuteGadgetLuaRsp;
+
+public class PacketExecuteGadgetLuaRsp extends BasePacket {
+
+ public PacketExecuteGadgetLuaRsp(int result) {
+ super(PacketOpcodes.ExecuteGadgetLuaRsp, true);
+
+ ExecuteGadgetLuaRsp proto = ExecuteGadgetLuaRsp.newBuilder()
+ .setRetcode(result)
+ .build();
+
+ this.setData(proto);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketFireworkNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketFireworkNotify.java
new file mode 100644
index 000000000..059194b02
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketFireworkNotify.java
@@ -0,0 +1,21 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.FireWorkNotifyOuterClass;
+import emu.grasscutter.net.proto.FireWorkDataOuterClass;
+
+public class PacketFireworkNotify extends BasePacket {
+
+ public PacketFireworkNotify(FireWorkDataOuterClass.FireWorkData pinfo) {
+ super(PacketOpcodes.FireworkNotify);
+
+ var proto
+ = FireWorkNotifyOuterClass.FireWorkNotify.newBuilder();
+
+ proto.addFireWorkData(pinfo);
+
+ setData(proto.build());
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketFireworkRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketFireworkRsp.java
new file mode 100644
index 000000000..e5c7b3498
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketFireworkRsp.java
@@ -0,0 +1,12 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+
+public class PacketFireworkRsp extends BasePacket {
+
+ public PacketFireworkRsp() {
+ super(PacketOpcodes.FireworkRsp);
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketFireworkSetNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketFireworkSetNotify.java
new file mode 100644
index 000000000..678573380
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketFireworkSetNotify.java
@@ -0,0 +1,21 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.FireworkSetNotifyOuterClass;
+import emu.grasscutter.net.proto.FireworkSetDataOuterClass;
+
+public class PacketFireworkSetNotify extends BasePacket {
+
+ public PacketFireworkSetNotify(FireworkSetDataOuterClass.FireworkSetData notify) {
+ super(PacketOpcodes.FireworkSetNotify);
+
+ var proto
+ = FireworkSetNotifyOuterClass.FireworkSetNotify.newBuilder();
+
+ proto.setCode(1).addData(notify);
+
+ setData(proto.build());
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketFireworkSetRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketFireworkSetRsp.java
new file mode 100644
index 000000000..aad309164
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketFireworkSetRsp.java
@@ -0,0 +1,13 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+
+public class PacketFireworkSetRsp extends BasePacket {
+
+ public PacketFireworkSetRsp() {
+ super(PacketOpcodes.FireworkSetRsp);
+
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketGetUgcBriefInfoRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketGetUgcBriefInfoRsp.java
new file mode 100644
index 000000000..a884be7a6
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketGetUgcBriefInfoRsp.java
@@ -0,0 +1,35 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.GetUgcBriefInfoRspOuterClass.GetUgcBriefInfoRsp;
+import emu.grasscutter.net.proto.RetcodeOuterClass;
+import emu.grasscutter.net.proto.UgcMusicBriefInfoOuterClass.UgcMusicBriefInfo;
+import emu.grasscutter.net.proto.UgcTypeOuterClass.UgcType;
+
+public class PacketGetUgcBriefInfoRsp extends BasePacket {
+
+ public PacketGetUgcBriefInfoRsp(RetcodeOuterClass.Retcode ret, UgcType unknownEnum1) {
+ super(PacketOpcodes.GetUgcBriefInfoRsp);
+
+ var proto = GetUgcBriefInfoRsp.newBuilder();
+
+ proto.setRetcode(ret.getNumber())
+ .setUgcType(unknownEnum1);
+
+ this.setData(proto);
+ }
+
+ public PacketGetUgcBriefInfoRsp(UgcMusicBriefInfo briefInfo, UgcType ugcType) {
+ super(PacketOpcodes.GetUgcBriefInfoRsp);
+
+ var proto = GetUgcBriefInfoRsp.newBuilder();
+
+ proto.setMusicBriefInfo(briefInfo)
+ .setUgcType(ugcType);
+
+ this.setData(proto);
+ }
+
+
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketGetUgcRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketGetUgcRsp.java
new file mode 100644
index 000000000..6db0890ec
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketGetUgcRsp.java
@@ -0,0 +1,40 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.GetUgcReqOuterClass.GetUgcReq;
+import emu.grasscutter.net.proto.GetUgcRspOuterClass.GetUgcRsp;
+import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
+import emu.grasscutter.net.proto.UgcMusicBriefInfoOuterClass.UgcMusicBriefInfo;
+import emu.grasscutter.net.proto.UgcMusicRecordOuterClass.UgcMusicRecord;
+
+public class PacketGetUgcRsp extends BasePacket {
+
+ public PacketGetUgcRsp(UgcMusicBriefInfo briefInfo, UgcMusicRecord musicRecord, GetUgcReq req) {
+ super(PacketOpcodes.GetUgcRsp);
+
+ var proto = GetUgcRsp.newBuilder();
+
+ proto
+ .setUgcGuid(briefInfo.getUgcGuid())
+ .setUgcType(req.getUgcType())
+ .setUgcRecordUsageValue(req.getUgcRecordUsageValue())
+ .setMusicRecord(musicRecord)
+ .setMusicBriefInfo(briefInfo);
+
+ this.setData(proto);
+ }
+ public PacketGetUgcRsp(Retcode errorCode, GetUgcReq req) {
+ super(PacketOpcodes.GetUgcRsp);
+
+ var proto = GetUgcRsp.newBuilder();
+
+ proto
+ .setUgcGuid(req.getUgcGuid())
+ .setUgcType(req.getUgcType())
+ .setUgcRecordUsageValue(req.getUgcRecordUsageValue())
+ .setRetcode(errorCode.getNumber());
+
+ this.setData(proto);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketHomeUnknown1Notify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketHomeUnknown1Notify.java
new file mode 100644
index 000000000..8fdaca533
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketHomeUnknown1Notify.java
@@ -0,0 +1,18 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.HomeUnknown1NotifyOuterClass;
+
+public class PacketHomeUnknown1Notify extends BasePacket {
+
+ public PacketHomeUnknown1Notify(boolean isEnterEditMode) {
+ super(PacketOpcodes.Unk2700_JDMPECKFGIG_ServerNotify);
+
+ var proto = HomeUnknown1NotifyOuterClass.HomeUnknown1Notify.newBuilder();
+
+ proto.setIsEnterEditMode(isEnterEditMode);
+
+ this.setData(proto);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketHomeUnknown2Rsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketHomeUnknown2Rsp.java
new file mode 100644
index 000000000..1aac1f721
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketHomeUnknown2Rsp.java
@@ -0,0 +1,12 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+
+public class PacketHomeUnknown2Rsp extends BasePacket {
+
+ public PacketHomeUnknown2Rsp() {
+ super(PacketOpcodes.Unk2700_KIIOGMKFNNP_ServerRsp);
+
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketPlatformChangeRouteNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketPlatformChangeRouteNotify.java
new file mode 100644
index 000000000..c27275b8f
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketPlatformChangeRouteNotify.java
@@ -0,0 +1,23 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.game.entity.EntityGadget;
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.PlatformChangeRouteNotifyOuterClass.PlatformChangeRouteNotify;
+import lombok.val;
+
+public class PacketPlatformChangeRouteNotify extends BasePacket {
+
+ public PacketPlatformChangeRouteNotify(EntityGadget gadgetEntity) {
+ super(PacketOpcodes.PlatformChangeRouteNotify);
+
+ val proto = PlatformChangeRouteNotify.newBuilder()
+ .setEntityId(gadgetEntity.getId())
+ .setSceneTime(gadgetEntity.getScene().getSceneTime())
+ .setPlatform(gadgetEntity.getPlatformInfo())
+ .build();
+
+ this.setData(proto);
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketQuestCreateEntityRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketQuestCreateEntityRsp.java
new file mode 100644
index 000000000..e9dd110fc
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketQuestCreateEntityRsp.java
@@ -0,0 +1,23 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
+import emu.grasscutter.net.proto.QuestCreateEntityReqOuterClass.QuestCreateEntityReq;
+import emu.grasscutter.net.proto.QuestCreateEntityRspOuterClass.QuestCreateEntityRsp;
+
+public class PacketQuestCreateEntityRsp extends BasePacket {
+
+ public PacketQuestCreateEntityRsp(int entityId, QuestCreateEntityReq req) {
+ super(PacketOpcodes.QuestCreateEntityRsp);
+
+ this.setData(QuestCreateEntityRsp.newBuilder()
+ .setQuestId(req.getQuestId())
+ .setEntity(req.getEntity())
+ .setParentQuestId(req.getParentQuestId())
+ .setIsRewind(req.getIsRewind())
+ .setEntityId(entityId).setRetcode(
+ entityId!=-1 ? Retcode.RET_SUCC_VALUE : Retcode.RET_FAIL_VALUE));
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketQuestDestroyEntityRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketQuestDestroyEntityRsp.java
new file mode 100644
index 000000000..7fad0edb1
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketQuestDestroyEntityRsp.java
@@ -0,0 +1,21 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
+import emu.grasscutter.net.proto.QuestDestroyEntityRspOuterClass.QuestDestroyEntityRsp;
+import emu.grasscutter.net.proto.QuestDestroyEntityReqOuterClass.QuestDestroyEntityReq;
+
+public class PacketQuestDestroyEntityRsp extends BasePacket {
+
+ public PacketQuestDestroyEntityRsp(boolean success, QuestDestroyEntityReq req) {
+ super(PacketOpcodes.QuestDestroyEntityRsp);
+
+ this.setData(QuestDestroyEntityRsp.newBuilder()
+ .setQuestId(req.getQuestId())
+ .setEntityId(req.getEntityId())
+ .setSceneId(req.getSceneId())
+ .setRetcode(success ? Retcode.RET_SUCC_VALUE : Retcode.RET_FAIL_VALUE));
+ }
+
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketQuestDestroyNpcRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketQuestDestroyNpcRsp.java
new file mode 100644
index 000000000..c0c9d1c4a
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketQuestDestroyNpcRsp.java
@@ -0,0 +1,20 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.QuestDestroyNpcRspOuterClass.QuestDestroyNpcRsp;
+
+public class PacketQuestDestroyNpcRsp extends BasePacket {
+
+ public PacketQuestDestroyNpcRsp(int npcId, int parentQuestId, int retCode) {
+ super(PacketOpcodes.QuestDestroyNpcRsp, true);
+
+ QuestDestroyNpcRsp proto = QuestDestroyNpcRsp.newBuilder()
+ .setNpcId(npcId)
+ .setParentQuestId(parentQuestId)
+ .setRetcode(retCode)
+ .build();
+
+ this.setData(proto);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketQuestTransmitRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketQuestTransmitRsp.java
new file mode 100644
index 000000000..4a47e1b78
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketQuestTransmitRsp.java
@@ -0,0 +1,18 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
+import emu.grasscutter.net.proto.QuestTransmitRspOuterClass.QuestTransmitRsp;
+import emu.grasscutter.net.proto.QuestTransmitReqOuterClass.QuestTransmitReq;
+
+public class PacketQuestTransmitRsp extends BasePacket {
+
+ public PacketQuestTransmitRsp(boolean result, QuestTransmitReq req) {
+ super(PacketOpcodes.QuestTransmitRsp);
+ this.setData(QuestTransmitRsp.newBuilder()
+ .setQuestId(req.getQuestId())
+ .setPointId(req.getPointId())
+ .setRetcode(result ? Retcode.RET_SUCC_VALUE : Retcode.RET_FAIL_VALUE));
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketReceivedTrialAvatarActivityRewardRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketReceivedTrialAvatarActivityRewardRsp.java
new file mode 100644
index 000000000..783fa25f6
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketReceivedTrialAvatarActivityRewardRsp.java
@@ -0,0 +1,21 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
+import emu.grasscutter.net.proto.ReceivedTrialAvatarActivityRewardRspOuterClass.ReceivedTrialAvatarActivityRewardRsp;
+
+public class PacketReceivedTrialAvatarActivityRewardRsp extends BasePacket {
+
+ public PacketReceivedTrialAvatarActivityRewardRsp(int activityId, int trialAvatarId, boolean success) {
+ this(activityId, trialAvatarId, success ? Retcode.RET_SUCC_VALUE : Retcode.RET_FAIL_VALUE);
+ }
+ public PacketReceivedTrialAvatarActivityRewardRsp(int activityId, int trialAvatarId, int retcodeVal) {
+ super(PacketOpcodes.ReceivedTrialAvatarActivityRewardRsp);
+ this.setData(ReceivedTrialAvatarActivityRewardRsp.newBuilder()
+ .setActivityId(activityId)
+ .setTrialAvatarIndexId(trialAvatarId)
+ .setRetcode(retcodeVal)
+ .build());
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketRemoveCustomTeamRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketRemoveCustomTeamRsp.java
new file mode 100644
index 000000000..bbfa4b429
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketRemoveCustomTeamRsp.java
@@ -0,0 +1,23 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
+import emu.grasscutter.net.proto.RemoveCustomTeamRspOuterClass.RemoveCustomTeamRsp;
+
+public class PacketRemoveCustomTeamRsp extends BasePacket {
+ public PacketRemoveCustomTeamRsp(Retcode retcode, int id) {
+ super(PacketOpcodes.RemoveCustomTeamRsp);
+
+ RemoveCustomTeamRsp proto = RemoveCustomTeamRsp.newBuilder()
+ .setRetcode(retcode.getNumber())
+ .setId(id)
+ .build();
+
+ this.setData(proto);
+ }
+
+ public PacketRemoveCustomTeamRsp(int id) {
+ this(Retcode.RET_SUCC, id);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketSceneForceLockNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketSceneForceLockNotify.java
new file mode 100644
index 000000000..790517a65
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketSceneForceLockNotify.java
@@ -0,0 +1,25 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.SceneForceLockNotifyOuterClass.SceneForceLockNotify;
+import lombok.val;
+
+import java.util.Collection;
+
+public class PacketSceneForceLockNotify extends BasePacket {
+ public PacketSceneForceLockNotify(Collection locked) {
+ super(PacketOpcodes.SceneForceLockNotify);
+ val builder = SceneForceLockNotify.newBuilder()
+ .addAllForceIdList(locked);
+
+ this.setData(builder);
+ }
+ public PacketSceneForceLockNotify(int locked) {
+ super(PacketOpcodes.SceneForceLockNotify);
+ val builder = SceneForceLockNotify.newBuilder()
+ .addForceIdList(locked);
+
+ this.setData(builder);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketSceneForceUnlockNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketSceneForceUnlockNotify.java
new file mode 100644
index 000000000..d5be59986
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketSceneForceUnlockNotify.java
@@ -0,0 +1,30 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.SceneForceUnlockNotifyOuterClass.SceneForceUnlockNotify;
+import lombok.val;
+
+import java.util.Collection;
+
+public class PacketSceneForceUnlockNotify extends BasePacket {
+ public PacketSceneForceUnlockNotify(Collection unlocked, boolean isAdd) {
+ super(PacketOpcodes.SceneForceUnlockNotify);
+
+ val builder = SceneForceUnlockNotify.newBuilder()
+ .addAllForceIdList(unlocked)
+ .setIsAdd(isAdd);
+
+ this.setData(builder);
+ }
+
+ public PacketSceneForceUnlockNotify(int unlocked, boolean isAdd) {
+ super(PacketOpcodes.SceneForceUnlockNotify);
+
+ val builder = SceneForceUnlockNotify.newBuilder()
+ .addForceIdList(unlocked)
+ .setIsAdd(isAdd);
+
+ this.setData(builder);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketScenePlayerSoundNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketScenePlayerSoundNotify.java
new file mode 100644
index 000000000..f32807c79
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketScenePlayerSoundNotify.java
@@ -0,0 +1,31 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.utils.Position;
+import emu.grasscutter.net.proto.ScenePlayerSoundNotifyOuterClass.ScenePlayerSoundNotify;
+import emu.grasscutter.net.proto.ScenePlayerSoundNotifyOuterClass.ScenePlayerSoundNotify.PlaySoundType;
+import emu.grasscutter.net.proto.VectorOuterClass.Vector;
+import java.util.Objects;
+
+public class PacketScenePlayerSoundNotify extends BasePacket {
+
+ public PacketScenePlayerSoundNotify(Position playPosition, String soundName, int playType) {
+ super(PacketOpcodes.ScenePlayerSoundNotify, true);
+
+ ScenePlayerSoundNotify.Builder proto = ScenePlayerSoundNotify.newBuilder();
+ if (!Objects.equals(playPosition, null)) {
+ proto.setPlayPos(Vector.newBuilder()
+ .setX(playPosition.getX())
+ .setY(playPosition.getY())
+ .setZ(playPosition.getZ())
+ .build());
+ }
+ if (!Objects.equals(soundName, null)) {
+ proto.setSoundName(soundName);
+ }
+ proto.setPlayType(PlaySoundType.forNumber(playType));
+
+ this.setData(proto.build());
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketShowClientGuideNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketShowClientGuideNotify.java
new file mode 100644
index 000000000..68db81974
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketShowClientGuideNotify.java
@@ -0,0 +1,17 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.ShowClientGuideNotifyOuterClass.ShowClientGuideNotify;
+
+public class PacketShowClientGuideNotify extends BasePacket {
+
+ public PacketShowClientGuideNotify(String guideName) {
+ super(PacketOpcodes.ShowClientGuideNotify, true);
+
+ ShowClientGuideNotify proto = ShowClientGuideNotify.newBuilder()
+ .setGuideName(guideName)
+ .build();
+ this.setData(proto);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketShowCommonTipsNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketShowCommonTipsNotify.java
new file mode 100644
index 000000000..7cf96abd0
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketShowCommonTipsNotify.java
@@ -0,0 +1,17 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.ShowCommonTipsNotifyOuterClass.ShowCommonTipsNotify;
+
+public class PacketShowCommonTipsNotify extends BasePacket {
+
+ public PacketShowCommonTipsNotify(String title, String content, int closeTime) {
+ super(PacketOpcodes.ShowCommonTipsNotify);
+ this.setData(ShowCommonTipsNotify.newBuilder()
+ .setTitle(title)
+ .setContent(content)
+ .setCloseTime(closeTime)
+ .build());
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketUnlockHomeBgmNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketUnlockHomeBgmNotify.java
new file mode 100644
index 000000000..2bd5a7868
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketUnlockHomeBgmNotify.java
@@ -0,0 +1,17 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.Unk2700MEBFPBDNPGOServerNotify;
+
+public class PacketUnlockHomeBgmNotify extends BasePacket {
+ public PacketUnlockHomeBgmNotify(int homeBgmId) {
+ super(PacketOpcodes.Unk2700_MEBFPBDNPGO_ServerNotify);
+
+ var notify = Unk2700MEBFPBDNPGOServerNotify.Unk2700_MEBFPBDNPGO_ServerNotify.newBuilder()
+ .addUnk2700ELJPLMIHNIP(homeBgmId)
+ .build();
+
+ this.setData(notify);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketUnlockedHomeBgmNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketUnlockedHomeBgmNotify.java
new file mode 100644
index 000000000..ad8c1e7c7
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketUnlockedHomeBgmNotify.java
@@ -0,0 +1,24 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.Unk2700LOHBMOKOPLHServerNotify;
+
+public class PacketUnlockedHomeBgmNotify extends BasePacket {
+ public PacketUnlockedHomeBgmNotify(Player player) {
+ super(PacketOpcodes.Unk2700_LOHBMOKOPLH_ServerNotify);
+
+ if (player.getRealmList() == null) {
+ return;
+ }
+
+ var unlocked = player.getHome().getUnlockedHomeBgmList();
+
+ var notify = Unk2700LOHBMOKOPLHServerNotify.Unk2700_LOHBMOKOPLH_ServerNotify.newBuilder()
+ .addAllUnk2700KMEKMNONMGE(unlocked)
+ .build();
+
+ this.setData(notify);
+ }
+}
diff --git a/src/main/java/emu/grasscutter/utils/GridPosition.java b/src/main/java/emu/grasscutter/utils/GridPosition.java
new file mode 100644
index 000000000..a2ba3c41d
--- /dev/null
+++ b/src/main/java/emu/grasscutter/utils/GridPosition.java
@@ -0,0 +1,118 @@
+package emu.grasscutter.utils;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.List;
+
+import dev.morphia.annotations.Entity;
+import lombok.Getter;
+import lombok.Setter;
+
+@Entity
+public class GridPosition implements Serializable {
+ private static final long serialVersionUID = -2001232300615923575L;
+
+ @Getter @Setter private int x;
+
+ @Getter @Setter private int z;
+
+ @Getter @Setter private int width;
+
+ public GridPosition() {}
+
+ public GridPosition(int x, int y, int width) {
+ set(x, y, width);
+ }
+
+ public GridPosition(GridPosition pos) {
+ this.set(pos);
+ }
+
+ public GridPosition(Position pos, int width) {
+ this.set((int)(pos.getX() / width), (int)(pos.getZ() / width), width);
+ }
+
+ public GridPosition(List xzwidth) {
+ this.width = xzwidth.get(2);
+ this.z = xzwidth.get(1);
+ this.x = xzwidth.get(0);
+ }
+
+ public GridPosition(String str) throws IOException {
+ String[] listOfParams = str.replace(" ", "").replace("(", "").replace(")", "").split(",");
+ if(listOfParams.length != 3)
+ throw new IOException("invalid size on GridPosition definition - ");
+ try {
+ this.x = Integer.parseInt(listOfParams[0]);
+ this.z = Integer.parseInt(listOfParams[1]);
+ this.width = Integer.parseInt(listOfParams[2]);
+ } catch(NumberFormatException ignored) {
+ throw new IOException("invalid number on GridPosition definition - ");
+ }
+ }
+
+ public GridPosition set(int x, int z) {
+ this.x = x;
+ this.z = z;
+ return this;
+ }
+
+ public GridPosition set(int x, int z, int width) {
+ this.x = x;
+ this.z = z;
+ this.width = width;
+ return this;
+ }
+
+ // Deep copy
+ public GridPosition set(GridPosition pos) {
+ return this.set(pos.getX(), pos.getZ(), pos.getWidth());
+ }
+
+ public GridPosition addClone(int x, int z) {
+ GridPosition pos = clone();
+ pos.x += x;
+ pos.z += z;
+ return pos;
+ }
+
+ @Override
+ public GridPosition clone() {
+ return new GridPosition(x, z, width);
+ }
+
+ @Override
+ public String toString() {
+ return "(" + this.getX() + ", " + this.getZ() + ", " + this.getWidth() + ")";
+ }
+
+ public int[] toIntArray() {
+ return new int[]{ x, z, width };
+ }
+
+ public int[] toXZIntArray() {
+ return new int[]{ x, z };
+ }
+
+ @Override
+ public int hashCode()
+ {
+ int result = (int) (x ^ (x >>> 32));
+ result = 31 * result + (int) (z ^ (z >>> 32));
+ result = 31 * result + (int) (width ^ (width >>> 32));
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null)
+ return false;
+ if (getClass() != o.getClass())
+ return false;
+ GridPosition pos = (GridPosition) o;
+ // field comparison
+ return pos.x == x && pos.z == z && pos.width == width;
+ }
+}
diff --git a/src/main/java/emu/grasscutter/utils/KahnsSort.java b/src/main/java/emu/grasscutter/utils/KahnsSort.java
new file mode 100644
index 000000000..7ee71bd76
--- /dev/null
+++ b/src/main/java/emu/grasscutter/utils/KahnsSort.java
@@ -0,0 +1,66 @@
+package emu.grasscutter.utils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Stack;
+
+public class KahnsSort {
+ public static class Node {
+ int source, dest; //Dest is a value, and source too
+
+ public Node(int source, int dest) {
+ this.source = source;
+ this.dest = dest;
+ }
+ }
+
+ public static class Graph {
+ Map> mainList;
+ Map degreeList;
+
+ List nodeList;
+
+ public Graph(List nodes, List nodeList) {
+ mainList = new HashMap<>();
+ this.nodeList = nodeList;
+
+ for(int i = 0; i < nodeList.size(); i++) mainList.put(nodeList.get(i), new ArrayList<>());
+
+ degreeList = new HashMap<>();
+ for(int i = 0; i < nodeList.size(); i++) degreeList.put(nodeList.get(i), 0);
+
+ for(Node node : nodes) {
+ mainList.get(node.source).add(node.dest);
+ degreeList.replace(node.dest, degreeList.get(node.dest) + 1);
+ }
+ }
+ }
+
+ public static List doSort(Graph graph) {
+ List orderedList = new ArrayList<>();
+ Map degreeList = graph.degreeList;
+
+ Stack zeroStack = new Stack<>();
+ degreeList.forEach((key, value) -> {
+ if(value == 0) zeroStack.add(key);
+ });
+
+ while(!zeroStack.isEmpty()) {
+ int element = zeroStack.pop();
+
+ //If the list is empty then this node
+ if(!graph.mainList.get(element).isEmpty()) orderedList.add(element);
+ for(int topElement : graph.mainList.get(element)) {
+ degreeList.replace(topElement, degreeList.get(topElement) - 1);
+
+ if(degreeList.get(topElement) == 0) zeroStack.add(topElement);
+ }
+ }
+
+ if(degreeList.values().stream().filter(value -> value != 0).count() != 0) return null; //Loop found
+
+ return orderedList;
+ }
+}