From 977f1ca2ea4017e776bfe1d88a3395983131ed59 Mon Sep 17 00:00:00 2001
From: Akka <104902222+Akka0@users.noreply.github.com>
Date: Sat, 25 Jun 2022 11:27:45 +0800
Subject: [PATCH] implement the activity system

---
 .../java/emu/grasscutter/data/GameData.java   |   4 +
 .../grasscutter/data/excels/ActivityData.java |  34 +++++
 .../data/excels/ActivityWatcherData.java      |  36 +++++
 .../grasscutter/database/DatabaseHelper.java  |  11 ++
 .../grasscutter/database/DatabaseManager.java |   8 +-
 .../game/activity/ActivityConfigItem.java     |  21 +++
 .../game/activity/ActivityHandler.java        |  81 ++++++++++++
 .../game/activity/ActivityManager.java        | 125 ++++++++++++++++++
 .../game/activity/ActivityType.java           |  12 ++
 .../game/activity/ActivityWatcher.java        |  26 ++++
 .../game/activity/DefaultWatcher.java         |  11 ++
 .../game/activity/PlayerActivityData.java     |  86 ++++++++++++
 .../game/activity/WatcherType.java            |  14 ++
 .../musicgame/MusicGameActivityHandler.java   |  17 +++
 .../musicgame/MusicGameScoreTrigger.java      |  23 ++++
 .../emu/grasscutter/game/player/Player.java   |   6 +-
 .../recv/HandlerGetActivityInfoReq.java       |   9 +-
 .../recv/HandlerMusicGameSettleReq.java       |  29 ++++
 .../packet/recv/HandlerMusicGameStartReq.java |  20 +++
 .../packet/send/PacketActivityInfoNotify.java |  19 +++
 .../PacketActivityScheduleInfoNotify.java     |  31 +++++
 .../PacketActivityUpdateWatcherNotify.java    |  20 +++
 .../packet/send/PacketGetActivityInfoRsp.java |  15 ++-
 .../packet/send/PacketMusicGameSettleRsp.java |  19 +++
 .../packet/send/PacketMusicGameStartRsp.java  |  18 +++
 .../defaults/data/ActivityConfig.json         |  17 +++
 26 files changed, 702 insertions(+), 10 deletions(-)
 create mode 100644 src/main/java/emu/grasscutter/data/excels/ActivityData.java
 create mode 100644 src/main/java/emu/grasscutter/data/excels/ActivityWatcherData.java
 create mode 100644 src/main/java/emu/grasscutter/game/activity/ActivityConfigItem.java
 create mode 100644 src/main/java/emu/grasscutter/game/activity/ActivityHandler.java
 create mode 100644 src/main/java/emu/grasscutter/game/activity/ActivityManager.java
 create mode 100644 src/main/java/emu/grasscutter/game/activity/ActivityType.java
 create mode 100644 src/main/java/emu/grasscutter/game/activity/ActivityWatcher.java
 create mode 100644 src/main/java/emu/grasscutter/game/activity/DefaultWatcher.java
 create mode 100644 src/main/java/emu/grasscutter/game/activity/PlayerActivityData.java
 create mode 100644 src/main/java/emu/grasscutter/game/activity/WatcherType.java
 create mode 100644 src/main/java/emu/grasscutter/game/activity/musicgame/MusicGameActivityHandler.java
 create mode 100644 src/main/java/emu/grasscutter/game/activity/musicgame/MusicGameScoreTrigger.java
 create mode 100644 src/main/java/emu/grasscutter/server/packet/recv/HandlerMusicGameSettleReq.java
 create mode 100644 src/main/java/emu/grasscutter/server/packet/recv/HandlerMusicGameStartReq.java
 create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketActivityInfoNotify.java
 create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketActivityScheduleInfoNotify.java
 create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketActivityUpdateWatcherNotify.java
 create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketMusicGameSettleRsp.java
 create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketMusicGameStartRsp.java
 create mode 100644 src/main/resources/defaults/data/ActivityConfig.json

diff --git a/src/main/java/emu/grasscutter/data/GameData.java b/src/main/java/emu/grasscutter/data/GameData.java
index 59b7c494..780564cb 100644
--- a/src/main/java/emu/grasscutter/data/GameData.java
+++ b/src/main/java/emu/grasscutter/data/GameData.java
@@ -15,6 +15,7 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
 import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
 import it.unimi.dsi.fastutil.ints.IntArrayList;
 import it.unimi.dsi.fastutil.ints.IntList;
+import lombok.Getter;
 
 public class GameData {
 	// BinOutputs
@@ -98,6 +99,9 @@ public class GameData {
 	private static final Int2ObjectMap<BattlePassMissionData> battlePassMissionDataMap = new Int2ObjectOpenHashMap<>();
 	private static final Int2ObjectMap<BattlePassRewardData> battlePassRewardDataMap = new Int2ObjectOpenHashMap<>();
 
+	@Getter private static final Int2ObjectMap<ActivityData> activityDataMap = new Int2ObjectOpenHashMap<>();
+    @Getter private static final Int2ObjectMap<ActivityWatcherData> activityWatcherDataMap = new Int2ObjectOpenHashMap<>();
+
 	// Cache
 	private static Map<Integer, List<Integer>> fetters = new HashMap<>();
 	private static Map<Integer, List<ShopGoodsData>> shopGoods = new HashMap<>();
diff --git a/src/main/java/emu/grasscutter/data/excels/ActivityData.java b/src/main/java/emu/grasscutter/data/excels/ActivityData.java
new file mode 100644
index 00000000..74b56967
--- /dev/null
+++ b/src/main/java/emu/grasscutter/data/excels/ActivityData.java
@@ -0,0 +1,34 @@
+package emu.grasscutter.data.excels;
+
+import emu.grasscutter.data.GameData;
+import emu.grasscutter.data.GameResource;
+import emu.grasscutter.data.ResourceType;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.experimental.FieldDefaults;
+
+import java.util.List;
+import java.util.Objects;
+
+@ResourceType(name = "NewActivityExcelConfigData.json", loadPriority = ResourceType.LoadPriority.LOW)
+@Getter
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public class ActivityData extends GameResource {
+    int activityId;
+    String activityType;
+    List<Integer> condGroupId;
+    List<Integer> watcherId;
+    List<ActivityWatcherData> watcherDataList;
+
+    @Override
+    public int getId() {
+        return this.activityId;
+    }
+    @Override
+    public void onLoad() {
+        this.watcherDataList = watcherId.stream().map(item -> GameData.getActivityWatcherDataMap().get(item.intValue()))
+            .filter(Objects::nonNull)
+            .toList();
+    }
+
+}
diff --git a/src/main/java/emu/grasscutter/data/excels/ActivityWatcherData.java b/src/main/java/emu/grasscutter/data/excels/ActivityWatcherData.java
new file mode 100644
index 00000000..e784693f
--- /dev/null
+++ b/src/main/java/emu/grasscutter/data/excels/ActivityWatcherData.java
@@ -0,0 +1,36 @@
+package emu.grasscutter.data.excels;
+
+import emu.grasscutter.data.GameResource;
+import emu.grasscutter.data.ResourceType;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.experimental.FieldDefaults;
+
+import java.util.List;
+
+@ResourceType(name = "NewActivityWatcherConfigData.json", loadPriority = ResourceType.LoadPriority.HIGH)
+@Getter
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public class ActivityWatcherData extends GameResource {
+    int id;
+    int rewardID;
+    int progress;
+    WatcherTrigger triggerConfig;
+
+    @Override
+    public int getId() {
+        return this.id;
+    }
+    @Override
+    public void onLoad() {
+        triggerConfig.paramList = triggerConfig.paramList.stream().filter(x -> !x.isBlank()).toList();
+    }
+
+    @Getter
+    @FieldDefaults(level = AccessLevel.PRIVATE)
+    public static class WatcherTrigger{
+        String triggerType;
+        List<String> paramList;
+    }
+
+}
diff --git a/src/main/java/emu/grasscutter/database/DatabaseHelper.java b/src/main/java/emu/grasscutter/database/DatabaseHelper.java
index d4592b00..55afc435 100644
--- a/src/main/java/emu/grasscutter/database/DatabaseHelper.java
+++ b/src/main/java/emu/grasscutter/database/DatabaseHelper.java
@@ -10,6 +10,7 @@ import dev.morphia.query.experimental.filters.Filters;
 import emu.grasscutter.GameConstants;
 import emu.grasscutter.Grasscutter;
 import emu.grasscutter.game.Account;
+import emu.grasscutter.game.activity.PlayerActivityData;
 import emu.grasscutter.game.avatar.Avatar;
 import emu.grasscutter.game.battlepass.BattlePassManager;
 import emu.grasscutter.game.friends.Friendship;
@@ -326,4 +327,14 @@ public final class DatabaseHelper {
 	public static void saveBattlePass(BattlePassManager manager) {
 		DatabaseManager.getGameDatastore().save(manager);
 	}
+
+    public static PlayerActivityData getPlayerActivityData(int uid, int activityId) {
+        return DatabaseManager.getGameDatastore().find(PlayerActivityData.class)
+            .filter(Filters.and(Filters.eq("uid", uid),Filters.eq("activityId", activityId)))
+            .first();
+    }
+
+    public static void savePlayerActivityData(PlayerActivityData playerActivityData) {
+        DatabaseManager.getGameDatastore().save(playerActivityData);
+    }
 }
diff --git a/src/main/java/emu/grasscutter/database/DatabaseManager.java b/src/main/java/emu/grasscutter/database/DatabaseManager.java
index 565d7681..19618c27 100644
--- a/src/main/java/emu/grasscutter/database/DatabaseManager.java
+++ b/src/main/java/emu/grasscutter/database/DatabaseManager.java
@@ -13,6 +13,7 @@ import dev.morphia.query.experimental.filters.Filters;
 import emu.grasscutter.Grasscutter;
 import emu.grasscutter.Grasscutter.ServerRunMode;
 import emu.grasscutter.game.Account;
+import emu.grasscutter.game.activity.PlayerActivityData;
 import emu.grasscutter.game.avatar.Avatar;
 import emu.grasscutter.game.battlepass.BattlePassManager;
 import emu.grasscutter.game.friends.Friendship;
@@ -29,12 +30,11 @@ import static emu.grasscutter.Configuration.*;
 public final class DatabaseManager {
 	private static Datastore gameDatastore;
 	private static Datastore dispatchDatastore;
-	
+
 	private static final Class<?>[] mappedClasses = new Class<?>[] {
-		DatabaseCounter.class, Account.class, Player.class, Avatar.class, GameItem.class, Friendship.class, 
-		GachaRecord.class, Mail.class, GameMainQuest.class, GameHome.class, BattlePassManager.class
+		DatabaseCounter.class, Account.class, Player.class, Avatar.class, GameItem.class, Friendship.class,
+		GachaRecord.class, Mail.class, GameMainQuest.class, GameHome.class, BattlePassManager.class, PlayerActivityData.class
 	};
-    
     public static Datastore getGameDatastore() {
     	return gameDatastore;
     }
diff --git a/src/main/java/emu/grasscutter/game/activity/ActivityConfigItem.java b/src/main/java/emu/grasscutter/game/activity/ActivityConfigItem.java
new file mode 100644
index 00000000..a8f17cdc
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/activity/ActivityConfigItem.java
@@ -0,0 +1,21 @@
+package emu.grasscutter.game.activity;
+
+import lombok.AccessLevel;
+import lombok.Data;
+import lombok.experimental.FieldDefaults;
+
+import java.util.Date;
+import java.util.List;
+
+@Data
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public class ActivityConfigItem {
+    int activityId;
+    int activityType;
+    int scheduleId;
+    List<Integer> meetCondList;
+    Date beginTime;
+    Date endTime;
+
+    transient ActivityHandler activityHandler;
+}
diff --git a/src/main/java/emu/grasscutter/game/activity/ActivityHandler.java b/src/main/java/emu/grasscutter/game/activity/ActivityHandler.java
new file mode 100644
index 00000000..386533d5
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/activity/ActivityHandler.java
@@ -0,0 +1,81 @@
+package emu.grasscutter.game.activity;
+
+import com.esotericsoftware.reflectasm.ConstructorAccess;
+import emu.grasscutter.data.GameData;
+import emu.grasscutter.data.excels.ActivityData;
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.game.props.WatcherTriggerType;
+import emu.grasscutter.net.proto.ActivityInfoOuterClass;
+import emu.grasscutter.utils.DateHelper;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.experimental.FieldDefaults;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Getter
+@Setter
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public abstract class ActivityHandler {
+    /**
+     * Must set before initWatchers
+     */
+    ActivityConfigItem activityConfigItem;
+    ActivityData activityData;
+    Map<WatcherTriggerType, List<ActivityWatcher>> watchersMap = new HashMap<>();
+
+    public void initWatchers(HashMap<String, ConstructorAccess<?>> activityWatcherTypeMap){
+        activityData = GameData.getActivityDataMap().get(activityConfigItem.getActivityId());
+
+        // add watcher to map by id
+        activityData.getWatcherDataList().forEach(watcherData -> {
+            var watcherType = activityWatcherTypeMap.get(watcherData.getTriggerConfig().getTriggerType());
+            ActivityWatcher watcher;
+            if(watcherType != null){
+                watcher = (ActivityWatcher) watcherType.newInstance();
+            }else{
+                watcher = new DefaultWatcher();
+            }
+
+            watcher.setWatcherId(watcherData.getId());
+            watcher.setActivityHandler(this);
+            watcher.setActivityWatcherData(watcherData);
+            watchersMap.computeIfAbsent(WatcherTriggerType.getTypeByName(watcherData.getTriggerConfig().getTriggerType()), k -> new ArrayList<>());
+            watchersMap.get(WatcherTriggerType.getTypeByName(watcherData.getTriggerConfig().getTriggerType())).add(watcher);
+        });
+    }
+
+    private Map<Integer, PlayerActivityData.WatcherInfo> initWatchersDataForPlayer(){
+        return watchersMap.values().stream()
+            .flatMap(Collection::stream)
+            .map(PlayerActivityData.WatcherInfo::init)
+            .collect(Collectors.toMap(PlayerActivityData.WatcherInfo::getWatcherId, y -> y));
+    }
+
+    public PlayerActivityData initPlayerActivityData(Player player){
+        return PlayerActivityData.of()
+            .activityId(activityConfigItem.getActivityId())
+            .uid(player.getUid())
+            .watcherInfoMap(initWatchersDataForPlayer())
+            .build();
+    }
+
+
+    public void buildProto(PlayerActivityData playerActivityData, ActivityInfoOuterClass.ActivityInfo.Builder activityInfo){
+        activityInfo.setActivityId(activityConfigItem.getActivityId())
+            .setActivityType(activityConfigItem.getActivityType())
+            .setScheduleId(activityConfigItem.getScheduleId())
+            .setBeginTime(DateHelper.getUnixTime(activityConfigItem.getBeginTime()))
+            .setFirstDayStartTime(DateHelper.getUnixTime(activityConfigItem.getBeginTime()))
+            .setEndTime(DateHelper.getUnixTime(activityConfigItem.getEndTime()))
+            .addAllMeetCondList(activityConfigItem.getMeetCondList());
+
+        if (playerActivityData != null){
+            activityInfo.addAllWatcherInfoList(playerActivityData.getAllWatcherInfoList());
+        }
+
+    }
+
+}
diff --git a/src/main/java/emu/grasscutter/game/activity/ActivityManager.java b/src/main/java/emu/grasscutter/game/activity/ActivityManager.java
new file mode 100644
index 00000000..b8b4db16
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/activity/ActivityManager.java
@@ -0,0 +1,125 @@
+package emu.grasscutter.game.activity;
+
+import com.esotericsoftware.reflectasm.ConstructorAccess;
+import com.google.gson.reflect.TypeToken;
+import emu.grasscutter.Grasscutter;
+import emu.grasscutter.data.DataLoader;
+import emu.grasscutter.data.GameData;
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.game.props.WatcherTriggerType;
+import emu.grasscutter.net.proto.ActivityInfoOuterClass;
+import emu.grasscutter.server.packet.send.PacketActivityScheduleInfoNotify;
+import lombok.Getter;
+import org.reflections.Reflections;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Getter
+public class ActivityManager {
+    private static final Map<Integer, ActivityConfigItem> activityConfigItemMap;
+    private final Player player;
+    private final Map<Integer, PlayerActivityData> playerActivityDataMap;
+
+    static {
+        activityConfigItemMap = new HashMap<>();
+
+        loadActivityConfigData();
+    }
+
+    public ActivityManager(Player player){
+        this.player = player;
+
+        playerActivityDataMap = new ConcurrentHashMap<>();
+        // load data for player
+        activityConfigItemMap.values().forEach(item -> {
+            var data = PlayerActivityData.getByPlayer(player, item.getActivityId());
+            if(data == null){
+                data = item.getActivityHandler().initPlayerActivityData(player);
+                data.save();
+            }
+            data.setPlayer(player);
+            playerActivityDataMap.put(item.getActivityId(), data);
+        });
+
+        player.sendPacket(new PacketActivityScheduleInfoNotify(activityConfigItemMap.values()));
+    }
+
+    private static void loadActivityConfigData() {
+        // scan activity type handler & watcher type
+        var activityHandlerTypeMap = new HashMap<String, ConstructorAccess<?>>();
+        var activityWatcherTypeMap = new HashMap<String, ConstructorAccess<?>>();
+        var reflections = new Reflections(ActivityManager.class.getPackage().getName());
+
+        reflections.getSubTypesOf(ActivityHandler.class).forEach(item -> {
+            var typeName = item.getAnnotation(ActivityType.class);
+            activityHandlerTypeMap.put(typeName.value(), ConstructorAccess.get(item));
+        });
+        reflections.getSubTypesOf(ActivityWatcher.class).forEach(item -> {
+            var typeName = item.getAnnotation(WatcherType.class);
+            activityWatcherTypeMap.put(typeName.value().name(), ConstructorAccess.get(item));
+        });
+
+        try(InputStream is = DataLoader.load("ActivityConfig.json"); InputStreamReader isr = new InputStreamReader(is)) {
+            List<ActivityConfigItem> activities = Grasscutter.getGsonFactory().fromJson(
+                isr,
+                TypeToken.getParameterized(List.class, ActivityConfigItem.class).getType());
+
+
+            activities.forEach(item -> {
+                var activityData = GameData.getActivityDataMap().get(item.getActivityId());
+                if(activityData == null){
+                    Grasscutter.getLogger().warn("activity {} not exist.", item.getActivityId());
+                    return;
+                }
+                var activityHandlerType = activityHandlerTypeMap.get(activityData.getActivityType());
+
+                if(activityHandlerType != null) {
+                    var activityHandler = (ActivityHandler) activityHandlerType.newInstance();
+                    activityHandler.setActivityConfigItem(item);
+                    activityHandler.initWatchers(activityWatcherTypeMap);
+                    item.setActivityHandler(activityHandler);
+                }
+
+                activityConfigItemMap.putIfAbsent(item.getActivityId(), item);
+            });
+
+            Grasscutter.getLogger().error("Enable {} activities.", activityConfigItemMap.size());
+        } catch (Exception e) {
+            Grasscutter.getLogger().error("Unable to load chest reward config.", e);
+        }
+
+    }
+
+    public ActivityInfoOuterClass.ActivityInfo getInfoProto(int activityId){
+        var activityHandler = activityConfigItemMap.get(activityId).getActivityHandler();
+        var activityData = playerActivityDataMap.get(activityId);
+
+        var proto = ActivityInfoOuterClass.ActivityInfo.newBuilder();
+        activityHandler.buildProto(activityData, proto);
+
+        return proto.build();
+    }
+
+    /**
+     * trigger activity watcher
+     * @param watcherTriggerType
+     * @param params
+     */
+    public void triggerWatcher(WatcherTriggerType watcherTriggerType, String... params) {
+        var watchers = activityConfigItemMap.values().stream()
+            .map(ActivityConfigItem::getActivityHandler)
+            .filter(Objects::nonNull)
+            .map(ActivityHandler::getWatchersMap)
+            .map(map -> map.get(watcherTriggerType))
+            .filter(Objects::nonNull)
+            .flatMap(Collection::stream)
+            .toList();
+
+        watchers.forEach(watcher -> watcher.trigger(
+            playerActivityDataMap.get(watcher.getActivityHandler().getActivityConfigItem().getActivityId()),
+            params));
+    }
+}
diff --git a/src/main/java/emu/grasscutter/game/activity/ActivityType.java b/src/main/java/emu/grasscutter/game/activity/ActivityType.java
new file mode 100644
index 00000000..da57637e
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/activity/ActivityType.java
@@ -0,0 +1,12 @@
+package emu.grasscutter.game.activity;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface ActivityType {
+    String value();
+}
diff --git a/src/main/java/emu/grasscutter/game/activity/ActivityWatcher.java b/src/main/java/emu/grasscutter/game/activity/ActivityWatcher.java
new file mode 100644
index 00000000..eeb86420
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/activity/ActivityWatcher.java
@@ -0,0 +1,26 @@
+package emu.grasscutter.game.activity;
+
+import emu.grasscutter.data.excels.ActivityWatcherData;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.experimental.FieldDefaults;
+
+@Getter
+@Setter
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public abstract class ActivityWatcher {
+    int watcherId;
+    ActivityWatcherData activityWatcherData;
+    ActivityHandler activityHandler;
+
+    protected abstract boolean isMeet(String... param);
+
+    public void trigger(PlayerActivityData playerActivityData, String... param){
+        if(isMeet(param)){
+            playerActivityData.addWatcherProgress(watcherId);
+            playerActivityData.save();
+        }
+    }
+
+}
diff --git a/src/main/java/emu/grasscutter/game/activity/DefaultWatcher.java b/src/main/java/emu/grasscutter/game/activity/DefaultWatcher.java
new file mode 100644
index 00000000..8c6e2464
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/activity/DefaultWatcher.java
@@ -0,0 +1,11 @@
+package emu.grasscutter.game.activity;
+
+import emu.grasscutter.game.props.WatcherTriggerType;
+
+@WatcherType(WatcherTriggerType.TRIGGER_NONE)
+public class DefaultWatcher extends ActivityWatcher{
+    @Override
+    protected boolean isMeet(String... param) {
+        return false;
+    }
+}
diff --git a/src/main/java/emu/grasscutter/game/activity/PlayerActivityData.java b/src/main/java/emu/grasscutter/game/activity/PlayerActivityData.java
new file mode 100644
index 00000000..4a251a62
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/activity/PlayerActivityData.java
@@ -0,0 +1,86 @@
+package emu.grasscutter.game.activity;
+
+import dev.morphia.annotations.Entity;
+import dev.morphia.annotations.Id;
+import dev.morphia.annotations.Transient;
+import emu.grasscutter.database.DatabaseHelper;
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.net.proto.ActivityWatcherInfoOuterClass;
+import emu.grasscutter.server.packet.send.PacketActivityUpdateWatcherNotify;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Data;
+import lombok.experimental.FieldDefaults;
+
+import java.util.List;
+import java.util.Map;
+
+@Entity("activities")
+@Data
+@FieldDefaults(level = AccessLevel.PRIVATE)
+@Builder(builderMethodName = "of")
+public class PlayerActivityData {
+    @Id
+    String id;
+    int uid;
+    int activityId;
+    Map<Integer, WatcherInfo> watcherInfoMap;
+    String detail;
+    @Transient Player player;
+
+    public void save(){
+        DatabaseHelper.savePlayerActivityData(this);
+    }
+
+    public static PlayerActivityData getByPlayer(Player player, int activityId){
+        return DatabaseHelper.getPlayerActivityData(player.getUid(), activityId);
+    }
+
+    public synchronized void addWatcherProgress(int watcherId){
+        var watcherInfo = watcherInfoMap.get(watcherId);
+        if(watcherInfo == null){
+            return;
+        }
+
+        if(watcherInfo.curProgress >= watcherInfo.totalProgress){
+            return;
+        }
+
+        watcherInfo.curProgress++;
+        getPlayer().sendPacket(new PacketActivityUpdateWatcherNotify(activityId, watcherInfo));
+    }
+
+    public List<ActivityWatcherInfoOuterClass.ActivityWatcherInfo> getAllWatcherInfoList() {
+        return watcherInfoMap.values().stream()
+            .map(WatcherInfo::toProto)
+            .toList();
+    }
+
+    @Entity
+    @Data
+    @FieldDefaults(level = AccessLevel.PRIVATE)
+    @Builder(builderMethodName = "of")
+    public static class WatcherInfo{
+        int watcherId;
+        int totalProgress;
+        int curProgress;
+        boolean isTakenReward;
+
+        public static WatcherInfo init(ActivityWatcher watcher){
+            return WatcherInfo.of()
+                .watcherId(watcher.getWatcherId())
+                .totalProgress(watcher.getActivityWatcherData().getProgress())
+                .isTakenReward(false)
+                .build();
+        }
+
+        public ActivityWatcherInfoOuterClass.ActivityWatcherInfo toProto(){
+            return ActivityWatcherInfoOuterClass.ActivityWatcherInfo.newBuilder()
+                .setWatcherId(watcherId)
+                .setCurProgress(curProgress)
+                .setTotalProgress(totalProgress)
+                .setIsTakenReward(isTakenReward)
+                .build();
+        }
+    }
+}
diff --git a/src/main/java/emu/grasscutter/game/activity/WatcherType.java b/src/main/java/emu/grasscutter/game/activity/WatcherType.java
new file mode 100644
index 00000000..7d1f9a26
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/activity/WatcherType.java
@@ -0,0 +1,14 @@
+package emu.grasscutter.game.activity;
+
+import emu.grasscutter.game.props.WatcherTriggerType;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface WatcherType {
+    WatcherTriggerType value();
+}
diff --git a/src/main/java/emu/grasscutter/game/activity/musicgame/MusicGameActivityHandler.java b/src/main/java/emu/grasscutter/game/activity/musicgame/MusicGameActivityHandler.java
new file mode 100644
index 00000000..c3d5ad53
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/activity/musicgame/MusicGameActivityHandler.java
@@ -0,0 +1,17 @@
+package emu.grasscutter.game.activity.musicgame;
+
+import emu.grasscutter.game.activity.ActivityHandler;
+import emu.grasscutter.game.activity.ActivityType;
+import emu.grasscutter.game.activity.PlayerActivityData;
+import emu.grasscutter.net.proto.ActivityInfoOuterClass;
+
+@ActivityType("NEW_ACTIVITY_MUSIC_GAME")
+public class MusicGameActivityHandler extends ActivityHandler {
+
+    @Override
+    public void buildProto(PlayerActivityData playerActivityData, ActivityInfoOuterClass.ActivityInfo.Builder activityInfo) {
+        super.buildProto(playerActivityData, activityInfo);
+
+
+    }
+}
diff --git a/src/main/java/emu/grasscutter/game/activity/musicgame/MusicGameScoreTrigger.java b/src/main/java/emu/grasscutter/game/activity/musicgame/MusicGameScoreTrigger.java
new file mode 100644
index 00000000..8ee92027
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/activity/musicgame/MusicGameScoreTrigger.java
@@ -0,0 +1,23 @@
+package emu.grasscutter.game.activity.musicgame;
+
+import emu.grasscutter.game.activity.ActivityWatcher;
+import emu.grasscutter.game.activity.WatcherType;
+import emu.grasscutter.game.props.WatcherTriggerType;
+
+@WatcherType(WatcherTriggerType.TRIGGER_FLEUR_FAIR_MUSIC_GAME_REACH_SCORE)
+public class MusicGameScoreTrigger extends ActivityWatcher {
+    @Override
+    protected boolean isMeet(String... param) {
+        if(param.length != 2){
+            return false;
+        }
+        var paramList = getActivityWatcherData().getTriggerConfig().getParamList();
+        if(!paramList.get(0).equals(param[0])){
+            return false;
+        }
+
+        var score = Integer.parseInt(param[1]);
+        var target = Integer.parseInt(paramList.get(1));
+        return score >= target;
+    }
+}
diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java
index e01b0abc..59c00194 100644
--- a/src/main/java/emu/grasscutter/game/player/Player.java
+++ b/src/main/java/emu/grasscutter/game/player/Player.java
@@ -11,6 +11,7 @@ import emu.grasscutter.database.DatabaseManager;
 import emu.grasscutter.game.Account;
 import emu.grasscutter.game.CoopRequest;
 import emu.grasscutter.game.ability.AbilityManager;
+import emu.grasscutter.game.activity.ActivityManager;
 import emu.grasscutter.game.avatar.Avatar;
 import emu.grasscutter.game.avatar.AvatarProfileData;
 import emu.grasscutter.game.avatar.AvatarStorage;
@@ -183,6 +184,7 @@ public class Player {
 	@Transient private GameHome home;
 	@Transient private FurnitureManager furnitureManager;
 	@Transient private BattlePassManager battlePassManager;
+	@Getter @Transient private ActivityManager activityManager;
 
 	@Transient private CollectionManager collectionManager;
 	private CollectionRecordStore collectionRecordStore;
@@ -1508,11 +1510,13 @@ public class Player {
 
 		// Battle Pass trigger
 		this.getBattlePassManager().triggerMission(WatcherTriggerType.TRIGGER_LOGIN);
-		
+
 		this.furnitureManager.onLogin();
 		// Home
 		home = GameHome.getByUid(getUid());
 		home.onOwnerLogin(this);
+        // Activity
+        activityManager = new ActivityManager(this);
 
 		session.send(new PacketPlayerEnterSceneNotify(this)); // Enter game world
 		session.send(new PacketPlayerLevelRewardUpdateNotify(rewardedLevels));
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetActivityInfoReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetActivityInfoReq.java
index bf2cf749..8ecad351 100644
--- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetActivityInfoReq.java
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetActivityInfoReq.java
@@ -3,13 +3,20 @@ 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.net.proto.GetActivityInfoReqOuterClass;
 import emu.grasscutter.server.game.GameSession;
 import emu.grasscutter.server.packet.send.PacketGetActivityInfoRsp;
 
+import java.util.HashSet;
+
 @Opcodes(PacketOpcodes.GetActivityInfoReq)
 public class HandlerGetActivityInfoReq extends PacketHandler {
 	@Override
 	public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
-		session.send(new PacketGetActivityInfoRsp());
+        var req = GetActivityInfoReqOuterClass.GetActivityInfoReq.parseFrom(payload);
+
+		session.send(new PacketGetActivityInfoRsp(
+            new HashSet<>(req.getActivityIdListList()),
+            session.getPlayer().getActivityManager()));
 	}
 }
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerMusicGameSettleReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerMusicGameSettleReq.java
new file mode 100644
index 00000000..e4e83950
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerMusicGameSettleReq.java
@@ -0,0 +1,29 @@
+package emu.grasscutter.server.packet.recv;
+
+import emu.grasscutter.game.props.WatcherTriggerType;
+import emu.grasscutter.net.packet.Opcodes;
+import emu.grasscutter.net.packet.PacketHandler;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.MusicGameSettleReqOuterClass;
+import emu.grasscutter.server.game.GameSession;
+import emu.grasscutter.server.packet.send.PacketMusicGameSettleRsp;
+
+@Opcodes(PacketOpcodes.MusicGameSettleReq)
+public class HandlerMusicGameSettleReq extends PacketHandler {
+
+	@Override
+	public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
+		var req = MusicGameSettleReqOuterClass.MusicGameSettleReq.parseFrom(payload);
+
+        session.getPlayer().getActivityManager().triggerWatcher(
+            WatcherTriggerType.TRIGGER_FLEUR_FAIR_MUSIC_GAME_REACH_SCORE,
+            String.valueOf(req.getMusicBasicId()),
+            String.valueOf(req.getScore())
+            );
+
+
+		//session.send(new PacketMusicGameSettleRsp(req.getMusicBasicId()));
+		session.send(new PacketMusicGameSettleRsp(req.getMusicBasicId()));
+	}
+
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerMusicGameStartReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerMusicGameStartReq.java
new file mode 100644
index 00000000..a7bb22cf
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerMusicGameStartReq.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.MusicGameStartReqOuterClass;
+import emu.grasscutter.server.game.GameSession;
+import emu.grasscutter.server.packet.send.PacketMusicGameStartRsp;
+
+@Opcodes(PacketOpcodes.MusicGameStartReq)
+public class HandlerMusicGameStartReq extends PacketHandler {
+	
+	@Override
+	public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
+		var req = MusicGameStartReqOuterClass.MusicGameStartReq.parseFrom(payload);
+
+		session.send(new PacketMusicGameStartRsp(req.getMusicBasicId()));
+	}
+
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketActivityInfoNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketActivityInfoNotify.java
new file mode 100644
index 00000000..cf7342ec
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketActivityInfoNotify.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.ActivityInfoNotifyOuterClass;
+import emu.grasscutter.net.proto.ActivityInfoOuterClass;
+
+public class PacketActivityInfoNotify extends BasePacket {
+
+	public PacketActivityInfoNotify(ActivityInfoOuterClass.ActivityInfo activityInfo) {
+		super(PacketOpcodes.ActivityInfoNotify);
+
+        var proto = ActivityInfoNotifyOuterClass.ActivityInfoNotify.newBuilder();
+
+        proto.setActivityInfo(activityInfo);
+
+        this.setData(proto);
+	}
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketActivityScheduleInfoNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketActivityScheduleInfoNotify.java
new file mode 100644
index 00000000..f7048ffb
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketActivityScheduleInfoNotify.java
@@ -0,0 +1,31 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.game.activity.ActivityConfigItem;
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.ActivityScheduleInfoNotifyOuterClass;
+import emu.grasscutter.net.proto.ActivityScheduleInfoOuterClass;
+import emu.grasscutter.utils.DateHelper;
+
+import java.util.Collection;
+
+public class PacketActivityScheduleInfoNotify extends BasePacket {
+
+	public PacketActivityScheduleInfoNotify(Collection<ActivityConfigItem> activityConfigItemList) {
+		super(PacketOpcodes.ActivityScheduleInfoNotify);
+
+		var proto = ActivityScheduleInfoNotifyOuterClass.ActivityScheduleInfoNotify.newBuilder();
+
+        activityConfigItemList.forEach(item -> {
+            proto.addActivityScheduleList(ActivityScheduleInfoOuterClass.ActivityScheduleInfo.newBuilder()
+                .setActivityId(item.getActivityId())
+                .setScheduleId(item.getScheduleId())
+                .setIsOpen(true)
+                .setBeginTime(DateHelper.getUnixTime(item.getBeginTime()))
+                .setEndTime(DateHelper.getUnixTime(item.getEndTime()))
+                .build());
+        });
+
+		this.setData(proto);
+	}
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketActivityUpdateWatcherNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketActivityUpdateWatcherNotify.java
new file mode 100644
index 00000000..68ced92a
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketActivityUpdateWatcherNotify.java
@@ -0,0 +1,20 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.game.activity.PlayerActivityData;
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.ActivityUpdateWatcherNotifyOuterClass;
+
+public class PacketActivityUpdateWatcherNotify extends BasePacket {
+
+	public PacketActivityUpdateWatcherNotify(int activityId, PlayerActivityData.WatcherInfo watcherInfo) {
+		super(PacketOpcodes.ActivityUpdateWatcherNotify);
+
+        var proto = ActivityUpdateWatcherNotifyOuterClass.ActivityUpdateWatcherNotify.newBuilder();
+
+        proto.setActivityId(activityId)
+            .setWatcherInfo(watcherInfo.toProto());
+
+        this.setData(proto);
+	}
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketGetActivityInfoRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketGetActivityInfoRsp.java
index 1808391c..36e35dcb 100644
--- a/src/main/java/emu/grasscutter/server/packet/send/PacketGetActivityInfoRsp.java
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketGetActivityInfoRsp.java
@@ -1,15 +1,22 @@
 package emu.grasscutter.server.packet.send;
 
+import emu.grasscutter.game.activity.ActivityManager;
 import emu.grasscutter.net.packet.BasePacket;
 import emu.grasscutter.net.packet.PacketOpcodes;
 import emu.grasscutter.net.proto.GetActivityInfoRspOuterClass.GetActivityInfoRsp;
 
+import java.util.Set;
+
 public class PacketGetActivityInfoRsp extends BasePacket {
-	public PacketGetActivityInfoRsp() {
+	public PacketGetActivityInfoRsp(Set<Integer> activityIdList, ActivityManager activityManager) {
 		super(PacketOpcodes.GetActivityInfoRsp);
-		
-		GetActivityInfoRsp proto = GetActivityInfoRsp.newBuilder().build();
-		
+
+		var proto = GetActivityInfoRsp.newBuilder();
+
+        activityIdList.stream()
+            .map(activityManager::getInfoProto)
+            .forEach(proto::addActivityInfoList);
+
 		this.setData(proto);
 	}
 }
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketMusicGameSettleRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketMusicGameSettleRsp.java
new file mode 100644
index 00000000..c64e44f7
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketMusicGameSettleRsp.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.MusicGameSettleRspOuterClass;
+
+public class PacketMusicGameSettleRsp extends BasePacket {
+
+	public PacketMusicGameSettleRsp(int musicBasicId) {
+		super(PacketOpcodes.MusicGameSettleRsp);
+
+		var proto = MusicGameSettleRspOuterClass.MusicGameSettleRsp.newBuilder();
+
+		proto.setMusicBasicId(musicBasicId)
+				.setIsNewRecord(true);
+
+		this.setData(proto);
+	}
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketMusicGameStartRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketMusicGameStartRsp.java
new file mode 100644
index 00000000..2dce1ba7
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketMusicGameStartRsp.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.MusicGameStartRspOuterClass;
+
+public class PacketMusicGameStartRsp extends BasePacket {
+
+	public PacketMusicGameStartRsp(int musicBasicId) {
+		super(PacketOpcodes.MusicGameStartRsp);
+
+		var proto = MusicGameStartRspOuterClass.MusicGameStartRsp.newBuilder();
+
+		proto.setMusicBasicId(musicBasicId);
+
+		this.setData(proto);
+	}
+}
diff --git a/src/main/resources/defaults/data/ActivityConfig.json b/src/main/resources/defaults/data/ActivityConfig.json
new file mode 100644
index 00000000..75314fdb
--- /dev/null
+++ b/src/main/resources/defaults/data/ActivityConfig.json
@@ -0,0 +1,17 @@
+[
+    {
+        "activityId" : 5072,
+        "activityType" : 2202,
+        "meetCondList" : [
+            5072001,
+            5072002,
+            5072003,
+            5072004,
+            5072005,
+            5072006,
+            5072007
+        ],
+        "beginTime" : "2022-05-01T00:00:00+08:00",
+        "endTime" : "2023-05-01T00:00:00+08:00"
+    }
+]
\ No newline at end of file
-- 
GitLab