Commit 977f1ca2 authored by Akka's avatar Akka Committed by Melledy
Browse files

implement the activity system

parent 5d35cb49
......@@ -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<>();
......
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();
}
}
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;
}
}
......@@ -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);
}
}
......@@ -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;
......@@ -32,9 +33,8 @@ public final class DatabaseManager {
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
GachaRecord.class, Mail.class, GameMainQuest.class, GameHome.class, BattlePassManager.class, PlayerActivityData.class
};
public static Datastore getGameDatastore() {
return gameDatastore;
}
......
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;
}
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());
}
}
}
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));
}
}
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();
}
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();
}
}
}
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;
}
}
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();
}
}
}
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();
}
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);
}
}
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;
}
}
......@@ -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;
......@@ -1513,6 +1515,8 @@ public class Player {
// 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));
......
......@@ -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()));
}
}
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()));
}
}
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()));
}
}
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);
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment