implement the music game

This commit is contained in:
Akka 2022-06-26 20:16:50 +08:00 committed by Melledy
parent 977f1ca2ea
commit 12146ff09c
32 changed files with 780 additions and 80 deletions

View File

@ -101,6 +101,7 @@ public class GameData {
@Getter private static final Int2ObjectMap<ActivityData> activityDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<ActivityWatcherData> activityWatcherDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<MusicGameBasicData> musicGameBasicDataMap = new Int2ObjectOpenHashMap<>();
// Cache
private static Map<Integer, List<Integer>> fetters = new HashMap<>();

View File

@ -2,6 +2,7 @@ package emu.grasscutter.data.excels;
import emu.grasscutter.data.GameResource;
import emu.grasscutter.data.ResourceType;
import emu.grasscutter.game.props.WatcherTriggerType;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.experimental.FieldDefaults;
@ -24,6 +25,7 @@ public class ActivityWatcherData extends GameResource {
@Override
public void onLoad() {
triggerConfig.paramList = triggerConfig.paramList.stream().filter(x -> !x.isBlank()).toList();
triggerConfig.watcherTriggerType = WatcherTriggerType.getTypeByName(triggerConfig.triggerType);
}
@Getter
@ -31,6 +33,8 @@ public class ActivityWatcherData extends GameResource {
public static class WatcherTrigger{
String triggerType;
List<String> paramList;
transient WatcherTriggerType watcherTriggerType;
}
}

View File

@ -0,0 +1,21 @@
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;
@ResourceType(name = "MusicGameBasicConfigData.json")
@Getter
@FieldDefaults(level = AccessLevel.PRIVATE)
public class MusicGameBasicData extends GameResource {
int id;
int musicID;
int musicLevel;
@Override
public int getId() {
return this.id;
}
}

View File

@ -11,6 +11,7 @@ import emu.grasscutter.GameConstants;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.Account;
import emu.grasscutter.game.activity.PlayerActivityData;
import emu.grasscutter.game.activity.musicgame.MusicGameBeatmap;
import emu.grasscutter.game.avatar.Avatar;
import emu.grasscutter.game.battlepass.BattlePassManager;
import emu.grasscutter.game.friends.Friendship;
@ -337,4 +338,13 @@ public final class DatabaseHelper {
public static void savePlayerActivityData(PlayerActivityData playerActivityData) {
DatabaseManager.getGameDatastore().save(playerActivityData);
}
public static MusicGameBeatmap getMusicGameBeatmap(long musicShareId) {
return DatabaseManager.getGameDatastore().find(MusicGameBeatmap.class)
.filter(Filters.eq("musicShareId", musicShareId))
.first();
}
public static void saveMusicGameBeatmap(MusicGameBeatmap musicGameBeatmap) {
DatabaseManager.getGameDatastore().save(musicGameBeatmap);
}
}

View File

@ -14,6 +14,7 @@ import emu.grasscutter.Grasscutter;
import emu.grasscutter.Grasscutter.ServerRunMode;
import emu.grasscutter.game.Account;
import emu.grasscutter.game.activity.PlayerActivityData;
import emu.grasscutter.game.activity.musicgame.MusicGameBeatmap;
import emu.grasscutter.game.avatar.Avatar;
import emu.grasscutter.game.battlepass.BattlePassManager;
import emu.grasscutter.game.friends.Friendship;
@ -33,12 +34,14 @@ 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, PlayerActivityData.class
GachaRecord.class, Mail.class, GameMainQuest.class, GameHome.class, BattlePassManager.class,
PlayerActivityData.class, MusicGameBeatmap.class
};
public static Datastore getGameDatastore() {
return gameDatastore;
}
public static MongoDatabase getGameDatabase() {
return getGameDatastore().getDatabase();
}

View File

@ -26,12 +26,15 @@ public abstract class ActivityHandler {
ActivityData activityData;
Map<WatcherTriggerType, List<ActivityWatcher>> watchersMap = new HashMap<>();
public void initWatchers(HashMap<String, ConstructorAccess<?>> activityWatcherTypeMap){
abstract public void onProtoBuild(PlayerActivityData playerActivityData, ActivityInfoOuterClass.ActivityInfo.Builder activityInfo);
abstract public void onInitPlayerActivityData(PlayerActivityData playerActivityData);
public void initWatchers(Map<WatcherTriggerType, 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());
var watcherType = activityWatcherTypeMap.get(watcherData.getTriggerConfig().getWatcherTriggerType());
ActivityWatcher watcher;
if(watcherType != null){
watcher = (ActivityWatcher) watcherType.newInstance();
@ -42,8 +45,8 @@ public abstract class ActivityHandler {
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);
watchersMap.computeIfAbsent(watcherData.getTriggerConfig().getWatcherTriggerType(), k -> new ArrayList<>());
watchersMap.get(watcherData.getTriggerConfig().getWatcherTriggerType()).add(watcher);
});
}
@ -55,16 +58,19 @@ public abstract class ActivityHandler {
}
public PlayerActivityData initPlayerActivityData(Player player){
return PlayerActivityData.of()
PlayerActivityData playerActivityData = PlayerActivityData.of()
.activityId(activityConfigItem.getActivityId())
.uid(player.getUid())
.watcherInfoMap(initWatchersDataForPlayer())
.build();
onInitPlayerActivityData(playerActivityData);
return playerActivityData;
}
public void buildProto(PlayerActivityData playerActivityData, ActivityInfoOuterClass.ActivityInfo.Builder activityInfo){
activityInfo.setActivityId(activityConfigItem.getActivityId())
public ActivityInfoOuterClass.ActivityInfo toProto(PlayerActivityData playerActivityData){
var proto = ActivityInfoOuterClass.ActivityInfo.newBuilder();
proto.setActivityId(activityConfigItem.getActivityId())
.setActivityType(activityConfigItem.getActivityType())
.setScheduleId(activityConfigItem.getScheduleId())
.setBeginTime(DateHelper.getUnixTime(activityConfigItem.getBeginTime()))
@ -73,9 +79,12 @@ public abstract class ActivityHandler {
.addAllMeetCondList(activityConfigItem.getMeetCondList());
if (playerActivityData != null){
activityInfo.addAllWatcherInfoList(playerActivityData.getAllWatcherInfoList());
proto.addAllWatcherInfoList(playerActivityData.getAllWatcherInfoList());
}
onProtoBuild(playerActivityData, proto);
return proto.build();
}
}

View File

@ -6,6 +6,7 @@ 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.ActivityType;
import emu.grasscutter.game.props.WatcherTriggerType;
import emu.grasscutter.net.proto.ActivityInfoOuterClass;
import emu.grasscutter.server.packet.send.PacketActivityScheduleInfoNotify;
@ -25,41 +26,22 @@ public class ActivityManager {
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 activityHandlerTypeMap = new HashMap<ActivityType, ConstructorAccess<?>>();
var activityWatcherTypeMap = new HashMap<WatcherTriggerType, ConstructorAccess<?>>();
var reflections = new Reflections(ActivityManager.class.getPackage().getName());
reflections.getSubTypesOf(ActivityHandler.class).forEach(item -> {
var typeName = item.getAnnotation(ActivityType.class);
var typeName = item.getAnnotation(GameActivity.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));
var typeName = item.getAnnotation(ActivityWatcherType.class);
activityWatcherTypeMap.put(typeName.value(), ConstructorAccess.get(item));
});
try(InputStream is = DataLoader.load("ActivityConfig.json"); InputStreamReader isr = new InputStreamReader(is)) {
@ -74,39 +56,49 @@ public class ActivityManager {
Grasscutter.getLogger().warn("activity {} not exist.", item.getActivityId());
return;
}
var activityHandlerType = activityHandlerTypeMap.get(activityData.getActivityType());
var activityHandlerType = activityHandlerTypeMap.get(ActivityType.getTypeByName(activityData.getActivityType()));
ActivityHandler activityHandler;
if(activityHandlerType != null) {
var activityHandler = (ActivityHandler) activityHandlerType.newInstance();
activityHandler.setActivityConfigItem(item);
activityHandler.initWatchers(activityWatcherTypeMap);
item.setActivityHandler(activityHandler);
activityHandler = (ActivityHandler) activityHandlerType.newInstance();
}else{
activityHandler = new DefaultActivityHandler();
}
activityHandler.setActivityConfigItem(item);
activityHandler.initWatchers(activityWatcherTypeMap);
item.setActivityHandler(activityHandler);
activityConfigItemMap.putIfAbsent(item.getActivityId(), item);
});
Grasscutter.getLogger().error("Enable {} activities.", activityConfigItemMap.size());
Grasscutter.getLogger().info("Enable {} activities.", activityConfigItemMap.size());
} catch (Exception e) {
Grasscutter.getLogger().error("Unable to load chest reward config.", e);
Grasscutter.getLogger().error("Unable to load activities config.", e);
}
}
public ActivityInfoOuterClass.ActivityInfo getInfoProto(int activityId){
var activityHandler = activityConfigItemMap.get(activityId).getActivityHandler();
var activityData = playerActivityDataMap.get(activityId);
public ActivityManager(Player player){
this.player = player;
var proto = ActivityInfoOuterClass.ActivityInfo.newBuilder();
activityHandler.buildProto(activityData, proto);
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);
data.setActivityHandler(item.getActivityHandler());
playerActivityDataMap.put(item.getActivityId(), data);
});
return proto.build();
player.sendPacket(new PacketActivityScheduleInfoNotify(activityConfigItemMap.values()));
}
/**
* trigger activity watcher
* @param watcherTriggerType
* @param params
*/
public void triggerWatcher(WatcherTriggerType watcherTriggerType, String... params) {
var watchers = activityConfigItemMap.values().stream()
@ -122,4 +114,37 @@ public class ActivityManager {
playerActivityDataMap.get(watcher.getActivityHandler().getActivityConfigItem().getActivityId()),
params));
}
public ActivityInfoOuterClass.ActivityInfo getInfoProtoByActivityId(int activityId){
var activityHandler = activityConfigItemMap.get(activityId).getActivityHandler();
var activityData = playerActivityDataMap.get(activityId);
return activityHandler.toProto(activityData);
}
public Optional<ActivityHandler> getActivityHandler(ActivityType type){
return activityConfigItemMap.values().stream()
.map(ActivityConfigItem::getActivityHandler)
.filter(x -> type == x.getClass().getAnnotation(GameActivity.class).value())
.findFirst();
}
public <T extends ActivityHandler> Optional<T> getActivityHandlerAs(ActivityType type, Class<T> clazz){
return getActivityHandler(type).map(x -> (T)x);
}
public Optional<Integer> getActivityIdByActivityType(ActivityType type){
return getActivityHandler(type)
.map(ActivityHandler::getActivityConfigItem)
.map(ActivityConfigItem::getActivityId);
}
public Optional<PlayerActivityData> getPlayerActivityDataByActivityType(ActivityType type){
return getActivityIdByActivityType(type)
.map(playerActivityDataMap::get);
}
public Optional<ActivityInfoOuterClass.ActivityInfo> getInfoProtoByActivityType(ActivityType type){
return getActivityIdByActivityType(type)
.map(this::getInfoProtoByActivityId);
}
}

View File

@ -9,6 +9,6 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface WatcherType {
public @interface ActivityWatcherType {
WatcherTriggerType value();
}

View File

@ -0,0 +1,17 @@
package emu.grasscutter.game.activity;
import emu.grasscutter.game.props.ActivityType;
import emu.grasscutter.net.proto.ActivityInfoOuterClass;
@GameActivity(ActivityType.NONE)
public class DefaultActivityHandler extends ActivityHandler{
@Override
public void onProtoBuild(PlayerActivityData playerActivityData, ActivityInfoOuterClass.ActivityInfo.Builder activityInfo) {
}
@Override
public void onInitPlayerActivityData(PlayerActivityData playerActivityData) {
}
}

View File

@ -2,7 +2,7 @@ package emu.grasscutter.game.activity;
import emu.grasscutter.game.props.WatcherTriggerType;
@WatcherType(WatcherTriggerType.TRIGGER_NONE)
@ActivityWatcherType(WatcherTriggerType.TRIGGER_NONE)
public class DefaultWatcher extends ActivityWatcher{
@Override
protected boolean isMeet(String... param) {

View File

@ -1,5 +1,7 @@
package emu.grasscutter.game.activity;
import emu.grasscutter.game.props.ActivityType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ -7,6 +9,6 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ActivityType {
String value();
public @interface GameActivity {
ActivityType value();
}

View File

@ -3,8 +3,14 @@ package emu.grasscutter.game.activity;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import dev.morphia.annotations.Transient;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.common.ItemParamData;
import emu.grasscutter.data.excels.ActivityWatcherData;
import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.inventory.GameItem;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.ActionReason;
import emu.grasscutter.net.proto.ActivityWatcherInfoOuterClass;
import emu.grasscutter.server.packet.send.PacketActivityUpdateWatcherNotify;
import lombok.AccessLevel;
@ -12,8 +18,10 @@ import lombok.Builder;
import lombok.Data;
import lombok.experimental.FieldDefaults;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Entity("activities")
@Data
@ -25,9 +33,12 @@ public class PlayerActivityData {
int uid;
int activityId;
Map<Integer, WatcherInfo> watcherInfoMap;
/**
* the detail data of each type of activity (Json format)
*/
String detail;
@Transient Player player;
@Transient ActivityHandler activityHandler;
public void save(){
DatabaseHelper.savePlayerActivityData(this);
}
@ -56,6 +67,35 @@ public class PlayerActivityData {
.toList();
}
public void setDetail(Object detail){
this.detail = Grasscutter.getGsonFactory().toJson(detail);
}
public void takeWatcherReward(int watcherId) {
var watcher = watcherInfoMap.get(watcherId);
if(watcher == null || watcher.isTakenReward()){
return;
}
var reward = Optional.of(watcher)
.map(WatcherInfo::getMetadata)
.map(ActivityWatcherData::getRewardID)
.map(id -> GameData.getRewardDataMap().get(id.intValue()));
if(reward.isEmpty()){
return;
}
List<GameItem> rewards = new ArrayList<>();
for (ItemParamData param : reward.get().getRewardItemList()) {
rewards.add(new GameItem(param.getId(), Math.max(param.getCount(), 1)));
}
player.getInventory().addItems(rewards, ActionReason.ActivityWatcher);
watcher.setTakenReward(true);
save();
}
@Entity
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
@ -66,6 +106,10 @@ public class PlayerActivityData {
int curProgress;
boolean isTakenReward;
public ActivityWatcherData getMetadata(){
return GameData.getActivityWatcherDataMap().get(watcherId);
}
public static WatcherInfo init(ActivityWatcher watcher){
return WatcherInfo.of()
.watcherId(watcher.getWatcherId())

View File

@ -1,17 +1,85 @@
package emu.grasscutter.game.activity.musicgame;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.activity.ActivityHandler;
import emu.grasscutter.game.activity.ActivityType;
import emu.grasscutter.game.activity.GameActivity;
import emu.grasscutter.game.activity.PlayerActivityData;
import emu.grasscutter.game.props.ActivityType;
import emu.grasscutter.net.proto.ActivityInfoOuterClass;
import emu.grasscutter.net.proto.MusicBriefInfoOuterClass;
import emu.grasscutter.net.proto.MusicGameActivityDetailInfoOuterClass;
@ActivityType("NEW_ACTIVITY_MUSIC_GAME")
import java.util.stream.Collectors;
@GameActivity(ActivityType.NEW_ACTIVITY_MUSIC_GAME)
public class MusicGameActivityHandler extends ActivityHandler {
@Override
public void buildProto(PlayerActivityData playerActivityData, ActivityInfoOuterClass.ActivityInfo.Builder activityInfo) {
super.buildProto(playerActivityData, activityInfo);
public void onInitPlayerActivityData(PlayerActivityData playerActivityData) {
var musicGamePlayerData = MusicGamePlayerData.create();
playerActivityData.setDetail(musicGamePlayerData);
}
@Override
public void onProtoBuild(PlayerActivityData playerActivityData, ActivityInfoOuterClass.ActivityInfo.Builder activityInfo) {
MusicGamePlayerData musicGamePlayerData = getMusicGameRecord(playerActivityData);
activityInfo.setMusicGameInfo(MusicGameActivityDetailInfoOuterClass.MusicGameActivityDetailInfo.newBuilder()
.putAllMusicGameRecordMap(
musicGamePlayerData.getMusicGameRecord().values().stream()
.collect(Collectors.toMap(MusicGamePlayerData.MusicGameRecord::getMusicId, MusicGamePlayerData.MusicGameRecord::toProto)))
.addAllPersonCustomBeatmap(musicGamePlayerData.getPersonalCustomBeatmapRecord().values().stream()
.map(MusicGamePlayerData.CustomBeatmapRecord::toProto)
.map(MusicBriefInfoOuterClass.MusicBriefInfo.Builder::build)
.toList())
.addAllPersonCustomBeatmap(musicGamePlayerData.getOthersCustomBeatmapRecord().values().stream()
.map(MusicGamePlayerData.CustomBeatmapRecord::toProto)
.map(MusicBriefInfoOuterClass.MusicBriefInfo.Builder::build)
.toList())
.build());
}
public MusicGamePlayerData getMusicGameRecord(PlayerActivityData playerActivityData){
if(playerActivityData.getDetail() == null || playerActivityData.getDetail().isBlank()){
onInitPlayerActivityData(playerActivityData);
playerActivityData.save();
}
return Grasscutter.getGsonFactory().fromJson(playerActivityData.getDetail(),
MusicGamePlayerData.class);
}
public boolean setMusicGameRecord(PlayerActivityData playerActivityData, MusicGamePlayerData.MusicGameRecord newRecord){
var musicGamePlayerData = getMusicGameRecord(playerActivityData);
var saveRecord = musicGamePlayerData.getMusicGameRecord().get(newRecord.getMusicId());
saveRecord.setMaxCombo(Math.max(newRecord.getMaxCombo(), saveRecord.getMaxCombo()));
saveRecord.setMaxScore(Math.max(newRecord.getMaxScore(), saveRecord.getMaxScore()));
playerActivityData.setDetail(musicGamePlayerData);
playerActivityData.save();
return newRecord.getMaxScore() > saveRecord.getMaxScore();
}
public void setMusicGameCustomBeatmapRecord(PlayerActivityData playerActivityData, MusicGamePlayerData.CustomBeatmapRecord newRecord){
var musicGamePlayerData = getMusicGameRecord(playerActivityData);
musicGamePlayerData.getOthersCustomBeatmapRecord().put(newRecord.getMusicShareId(), newRecord);
playerActivityData.setDetail(musicGamePlayerData);
playerActivityData.save();
}
public void addPersonalBeatmap(PlayerActivityData playerActivityData, MusicGameBeatmap musicGameBeatmap) {
var musicGamePlayerData = getMusicGameRecord(playerActivityData);
musicGamePlayerData.getPersonalCustomBeatmapRecord().put(musicGameBeatmap.getMusicShareId(),
MusicGamePlayerData.CustomBeatmapRecord.of()
.musicShareId(musicGameBeatmap.getMusicShareId())
.build());
playerActivityData.setDetail(musicGamePlayerData);
playerActivityData.save();
}
}

View File

@ -0,0 +1,109 @@
package emu.grasscutter.game.activity.musicgame;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.net.proto.MusicBeatmapListOuterClass;
import emu.grasscutter.net.proto.MusicBeatmapNoteOuterClass;
import emu.grasscutter.net.proto.MusicBeatmapOuterClass;
import emu.grasscutter.net.proto.MusicBriefInfoOuterClass;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Data;
import lombok.experimental.FieldDefaults;
import java.util.List;
import java.util.Random;
@Entity("music_game_beatmaps")
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
@Builder(builderMethodName = "of")
public class MusicGameBeatmap {
@Id
long musicShareId;
int authorUid;
int musicId;
int musicNoteCount;
int savePosition;
int maxScore;
int createTime;
List<List<BeatmapNote>> beatmap;
public static MusicGameBeatmap getByShareId(long musicShareId){
return DatabaseHelper.getMusicGameBeatmap(musicShareId);
}
public void save(){
if(musicShareId == 0){
musicShareId = new Random().nextLong(100000000000000L,999999999999999L);
}
DatabaseHelper.saveMusicGameBeatmap(this);
}
public static List<List<BeatmapNote>> parse(List<MusicBeatmapListOuterClass.MusicBeatmapList> beatmapItemListList) {
return beatmapItemListList.stream()
.map(item -> item.getBeatmapNoteListList().stream()
.map(BeatmapNote::parse)
.toList())
.toList();
}
public MusicBeatmapOuterClass.MusicBeatmap toProto(){
return MusicBeatmapOuterClass.MusicBeatmap.newBuilder()
.setMusicId(musicId)
.addAllBeatmapItemList(beatmap.stream()
.map(this::musicBeatmapListToProto)
.toList())
.build();
}
public MusicBriefInfoOuterClass.MusicBriefInfo.Builder toBriefProto(){
var player = DatabaseHelper.getPlayerByUid(authorUid);
return MusicBriefInfoOuterClass.MusicBriefInfo.newBuilder()
.setCanShare(true)
.setMusicId(musicId)
.setMusicNoteCount(musicNoteCount)
.setMusicShareId(musicShareId)
.setMaxScore(maxScore)
.setCreateTime(createTime)
.setShareTime(createTime)
.setAuthorNickname(player.getNickname())
.setVersion(1)
;
}
private MusicBeatmapListOuterClass.MusicBeatmapList musicBeatmapListToProto(List<BeatmapNote> beatmapNoteList){
return MusicBeatmapListOuterClass.MusicBeatmapList.newBuilder()
.addAllBeatmapNoteList(beatmapNoteList.stream()
.map(BeatmapNote::toProto)
.toList())
.build();
}
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
@Builder(builderMethodName = "of")
@Entity
public static class BeatmapNote{
int startTime;
int endTime;
public static BeatmapNote parse(MusicBeatmapNoteOuterClass.MusicBeatmapNote note){
return BeatmapNote.of()
.startTime(note.getStartTime())
.endTime(note.getEndTime())
.build();
}
public MusicBeatmapNoteOuterClass.MusicBeatmapNote toProto(){
return MusicBeatmapNoteOuterClass.MusicBeatmapNote.newBuilder()
.setStartTime(startTime)
.setEndTime(endTime)
.build();
}
}
}

View File

@ -0,0 +1,76 @@
package emu.grasscutter.game.activity.musicgame;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.MusicGameBasicData;
import emu.grasscutter.net.proto.MusicBriefInfoOuterClass;
import emu.grasscutter.net.proto.MusicGameRecordOuterClass;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Data;
import lombok.experimental.FieldDefaults;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
@Builder(builderMethodName = "of")
public class MusicGamePlayerData {
Map<Integer, MusicGameRecord> musicGameRecord;
Map<Long, CustomBeatmapRecord> personalCustomBeatmapRecord;
Map<Long, CustomBeatmapRecord> othersCustomBeatmapRecord;
public static MusicGamePlayerData create(){
return MusicGamePlayerData.of()
.musicGameRecord(GameData.getMusicGameBasicDataMap().values().stream()
.collect(Collectors.toMap(MusicGameBasicData::getId, MusicGamePlayerData.MusicGameRecord::create)))
.personalCustomBeatmapRecord(new HashMap<>())
.othersCustomBeatmapRecord(new HashMap<>())
.build();
}
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
@Builder(builderMethodName = "of")
public static class MusicGameRecord {
int musicId;
int maxCombo;
int maxScore;
public static MusicGameRecord create(MusicGameBasicData musicGameBasicData){
return MusicGameRecord.of()
.musicId(musicGameBasicData.getId())
.build();
}
public MusicGameRecordOuterClass.MusicGameRecord toProto(){
return MusicGameRecordOuterClass.MusicGameRecord.newBuilder()
.setIsUnlock(true)
.setMaxCombo(maxCombo)
.setMaxScore(maxScore)
.build();
}
}
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
@Builder(builderMethodName = "of")
public static class CustomBeatmapRecord {
long musicShareId;
int score;
boolean settle;
public MusicBriefInfoOuterClass.MusicBriefInfo.Builder toProto(){
var musicGameBeatmap = MusicGameBeatmap.getByShareId(musicShareId);
return musicGameBeatmap.toBriefProto()
.setScore(score)
.setSettle(settle)
;
}
}
}

View File

@ -1,10 +1,10 @@
package emu.grasscutter.game.activity.musicgame;
import emu.grasscutter.game.activity.ActivityWatcher;
import emu.grasscutter.game.activity.WatcherType;
import emu.grasscutter.game.activity.ActivityWatcherType;
import emu.grasscutter.game.props.WatcherTriggerType;
@WatcherType(WatcherTriggerType.TRIGGER_FLEUR_FAIR_MUSIC_GAME_REACH_SCORE)
@ActivityWatcherType(WatcherTriggerType.TRIGGER_FLEUR_FAIR_MUSIC_GAME_REACH_SCORE)
public class MusicGameScoreTrigger extends ActivityWatcher {
@Override
protected boolean isMeet(String... param) {

View File

@ -0,0 +1,38 @@
package emu.grasscutter.game.props;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;
@Getter
@AllArgsConstructor
public enum ActivityType {
NONE(0),
NEW_ACTIVITY_MUSIC_GAME(2202),
;
private final int value;
private static final Int2ObjectMap<ActivityType> map = new Int2ObjectOpenHashMap<>();
private static final Map<String, ActivityType> stringMap = new HashMap<>();
static {
Stream.of(values()).forEach(e -> {
map.put(e.getValue(), e);
stringMap.put(e.name(), e);
});
}
public static ActivityType getTypeByValue(int value) {
return map.getOrDefault(value, NONE);
}
public static ActivityType getTypeByName(String name) {
return stringMap.getOrDefault(name, NONE);
}
}

View File

@ -899,6 +899,12 @@ public class PacketOpcodes {
public static final int MultistagePlayInfoNotify = 5309;
public static final int MultistagePlaySettleNotify = 5314;
public static final int MultistagePlayStageEndNotify = 5340;
public static final int MusicGameCreateBeatmapReq = 6326;
public static final int MusicGameCreateBeatmapRsp = 6347;
public static final int MusicGameGetBeatmapReq = 6318;
public static final int MusicGameGetBeatmapRsp = 6309;
public static final int MusicGameSearchBeatmapReq = 6343;
public static final int MusicGameSearchBeatmapRsp = 6304;
public static final int MusicGameSettleReq = 8745;
public static final int MusicGameSettleRsp = 8288;
public static final int MusicGameStartReq = 8927;

View File

@ -0,0 +1,25 @@
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.ActivityTakeWatcherRewardReqOuterClass;
import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.server.packet.send.PacketActivityTakeWatcherRewardRsp;
import java.util.Optional;
@Opcodes(PacketOpcodes.ActivityTakeWatcherRewardReq)
public class HandlerActivityTakeWatcherRewardReq extends PacketHandler {
@Override
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
var req = ActivityTakeWatcherRewardReqOuterClass.ActivityTakeWatcherRewardReq.parseFrom(payload);
Optional.ofNullable(session.getPlayer().getActivityManager().getPlayerActivityDataMap().get(req.getActivityId()))
.ifPresent(x -> x.takeWatcherReward(req.getWatcherId()));
session.send(new PacketActivityTakeWatcherRewardRsp(req.getActivityId(), req.getWatcherId()));
}
}

View File

@ -0,0 +1,47 @@
package emu.grasscutter.server.packet.recv;
import emu.grasscutter.game.activity.musicgame.MusicGameActivityHandler;
import emu.grasscutter.game.activity.musicgame.MusicGameBeatmap;
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.MusicGameCreateBeatmapReqOuterClass;
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;
@Opcodes(PacketOpcodes.MusicGameCreateBeatmapReq)
public class HandlerMusicGameCreateBeatmapReq extends PacketHandler {
@Override
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
var req = MusicGameCreateBeatmapReqOuterClass.MusicGameCreateBeatmapReq.parseFrom(payload);
var musicGameBeatmap = MusicGameBeatmap.of()
.musicId(req.getMusicBriefInfo().getMusicId())
.musicNoteCount(req.getMusicBriefInfo().getMusicNoteCount())
.savePosition(req.getMusicBriefInfo().getSavePosition())
.maxScore(req.getMusicBriefInfo().getMaxScore())
.authorUid(session.getPlayer().getUid())
.beatmap(MusicGameBeatmap.parse(req.getMusicRecord().getBeatmapItemListList()))
.createTime(Utils.getCurrentSeconds())
.build();
// TODO avoid player save too much to make server down
musicGameBeatmap.save();
var playerData = session.getPlayer().getActivityManager().getPlayerActivityDataByActivityType(ActivityType.NEW_ACTIVITY_MUSIC_GAME);
if(playerData.isEmpty()){
return;
}
var handler = (MusicGameActivityHandler) playerData.get().getActivityHandler();
handler.addPersonalBeatmap(playerData.get(), musicGameBeatmap);
session.send(new PacketActivityInfoNotify(handler.toProto(playerData.get())));
session.send(new PacketMusicGameCreateBeatmapRsp(musicGameBeatmap.getMusicShareId(), req.getUnknownEnum1()));
}
}

View File

@ -0,0 +1,31 @@
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.MusicGameGetBeatmapReqOuterClass;
import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.server.packet.send.PacketMusicGameGetBeatmapRsp;
@Opcodes(PacketOpcodes.MusicGameGetBeatmapReq)
public class HandlerMusicGameGetBeatmapReq extends PacketHandler {
@Override
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
var req = MusicGameGetBeatmapReqOuterClass.MusicGameGetBeatmapReq.parseFrom(payload);
var musicGameBeatmap = MusicGameBeatmap.getByShareId(req.getMusicShareId());
if(musicGameBeatmap == null){
return;
}
session.send(new PacketMusicGameGetBeatmapRsp(
musicGameBeatmap.toBriefProto().build(),
musicGameBeatmap.toProto(),
req
));
}
}

View File

@ -0,0 +1,28 @@
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.MusicGameSearchBeatmapReqOuterClass;
import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.server.packet.send.PacketMusicGameSearchBeatmapRsp;
@Opcodes(PacketOpcodes.MusicGameSearchBeatmapReq)
public class HandlerMusicGameSearchBeatmapReq extends PacketHandler {
@Override
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
var req = MusicGameSearchBeatmapReqOuterClass.MusicGameSearchBeatmapReq.parseFrom(payload);
var musicGameBeatmap = MusicGameBeatmap.getByShareId(req.getMusicShareId());
if(musicGameBeatmap == null){
session.send(new PacketMusicGameSearchBeatmapRsp(11153, req.getUnknownEnum1()));
return;
}
session.send(new PacketMusicGameSearchBeatmapRsp(musicGameBeatmap.toBriefProto().build(), req.getUnknownEnum1()));
}
}

View File

@ -1,11 +1,15 @@
package emu.grasscutter.server.packet.recv;
import emu.grasscutter.game.activity.musicgame.MusicGameActivityHandler;
import emu.grasscutter.game.activity.musicgame.MusicGamePlayerData;
import emu.grasscutter.game.props.ActivityType;
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.PacketActivityInfoNotify;
import emu.grasscutter.server.packet.send.PacketMusicGameSettleRsp;
@Opcodes(PacketOpcodes.MusicGameSettleReq)
@ -15,15 +19,40 @@ public class HandlerMusicGameSettleReq extends PacketHandler {
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())
var playerData = session.getPlayer().getActivityManager().getPlayerActivityDataByActivityType(ActivityType.NEW_ACTIVITY_MUSIC_GAME);
if(playerData.isEmpty()){
return;
}
var handler = (MusicGameActivityHandler) playerData.get().getActivityHandler();
boolean isNewRecord = false;
// check if custom beatmap
if(req.getMusicShareId() == 0){
session.getPlayer().getActivityManager().triggerWatcher(
WatcherTriggerType.TRIGGER_FLEUR_FAIR_MUSIC_GAME_REACH_SCORE,
String.valueOf(req.getMusicBasicId()),
String.valueOf(req.getScore())
);
isNewRecord = handler.setMusicGameRecord(playerData.get(),
MusicGamePlayerData.MusicGameRecord.of()
.musicId(req.getMusicBasicId())
.maxCombo(req.getMaxCombo())
.maxScore(req.getScore())
.build());
//session.send(new PacketMusicGameSettleRsp(req.getMusicBasicId()));
session.send(new PacketMusicGameSettleRsp(req.getMusicBasicId()));
// update activity info
session.send(new PacketActivityInfoNotify(handler.toProto(playerData.get())));
}else{
handler.setMusicGameCustomBeatmapRecord(playerData.get(),
MusicGamePlayerData.CustomBeatmapRecord.of()
.musicShareId(req.getMusicShareId())
.score(req.getMaxCombo())
.settle(req.getSuccess())
.build());
}
session.send(new PacketMusicGameSettleRsp(req.getMusicBasicId(), req.getMusicShareId(), isNewRecord));
}
}

View File

@ -9,12 +9,12 @@ 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()));
session.send(new PacketMusicGameStartRsp(req.getMusicBasicId(), req.getMusicShareId()));
}
}

View File

@ -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.ActivityTakeWatcherRewardRspOuterClass;
public class PacketActivityTakeWatcherRewardRsp extends BasePacket {
public PacketActivityTakeWatcherRewardRsp(int activityId, int watcherId) {
super(PacketOpcodes.ActivityTakeWatcherRewardRsp);
var proto = ActivityTakeWatcherRewardRspOuterClass.ActivityTakeWatcherRewardRsp.newBuilder();
proto.setActivityId(activityId)
.setWatcherId(watcherId);
this.setData(proto);
}
}

View File

@ -14,7 +14,7 @@ public class PacketGetActivityInfoRsp extends BasePacket {
var proto = GetActivityInfoRsp.newBuilder();
activityIdList.stream()
.map(activityManager::getInfoProto)
.map(activityManager::getInfoProtoByActivityId)
.forEach(proto::addActivityInfoList);
this.setData(proto);

View File

@ -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.MusicGameCreateBeatmapRspOuterClass;
import emu.grasscutter.net.proto.MusicGameUnknown1EnumOuterClass;
public class PacketMusicGameCreateBeatmapRsp extends BasePacket {
public PacketMusicGameCreateBeatmapRsp(long musicShareId, MusicGameUnknown1EnumOuterClass.MusicGameUnknown1Enum unknownEnum1) {
super(PacketOpcodes.MusicGameCreateBeatmapRsp);
var proto = MusicGameCreateBeatmapRspOuterClass.MusicGameCreateBeatmapRsp.newBuilder();
proto.setMusicShareId(musicShareId)
.setUnknownEnum1(unknownEnum1);
this.setData(proto);
}
}

View File

@ -0,0 +1,27 @@
package emu.grasscutter.server.packet.send;
import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.MusicBeatmapOuterClass;
import emu.grasscutter.net.proto.MusicBriefInfoOuterClass;
import emu.grasscutter.net.proto.MusicGameGetBeatmapReqOuterClass;
import emu.grasscutter.net.proto.MusicGameGetBeatmapRspOuterClass;
public class PacketMusicGameGetBeatmapRsp extends BasePacket {
public PacketMusicGameGetBeatmapRsp(MusicBriefInfoOuterClass.MusicBriefInfo briefInfo, MusicBeatmapOuterClass.MusicBeatmap musicRecord, MusicGameGetBeatmapReqOuterClass.MusicGameGetBeatmapReq req) {
super(PacketOpcodes.MusicGameGetBeatmapRsp);
var proto = MusicGameGetBeatmapRspOuterClass.MusicGameGetBeatmapRsp.newBuilder();
proto.setMusicBriefInfo(briefInfo)
.setMusicShareId(briefInfo.getMusicShareId())
.setMusicRecord(musicRecord)
.setUnknownEnum1(req.getUnknownEnum1())
.setReqType(req.getReqType())
;
this.setData(proto);
}
}

View File

@ -0,0 +1,34 @@
package emu.grasscutter.server.packet.send;
import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.MusicBriefInfoOuterClass;
import emu.grasscutter.net.proto.MusicGameSearchBeatmapRspOuterClass;
import emu.grasscutter.net.proto.MusicGameUnknown1EnumOuterClass;
public class PacketMusicGameSearchBeatmapRsp extends BasePacket {
public PacketMusicGameSearchBeatmapRsp(int ret, MusicGameUnknown1EnumOuterClass.MusicGameUnknown1Enum unknownEnum1) {
super(PacketOpcodes.MusicGameSearchBeatmapRsp);
var proto = MusicGameSearchBeatmapRspOuterClass.MusicGameSearchBeatmapRsp.newBuilder();
proto.setRetcode(ret)
.setUnknownEnum1(unknownEnum1);
this.setData(proto);
}
public PacketMusicGameSearchBeatmapRsp(MusicBriefInfoOuterClass.MusicBriefInfo briefInfo, MusicGameUnknown1EnumOuterClass.MusicGameUnknown1Enum unknownEnum1) {
super(PacketOpcodes.MusicGameSearchBeatmapRsp);
var proto = MusicGameSearchBeatmapRspOuterClass.MusicGameSearchBeatmapRsp.newBuilder();
proto.setMusicBriefInfo(briefInfo)
.setUnknownEnum1(unknownEnum1);
this.setData(proto);
}
}

View File

@ -6,14 +6,15 @@ import emu.grasscutter.net.proto.MusicGameSettleRspOuterClass;
public class PacketMusicGameSettleRsp extends BasePacket {
public PacketMusicGameSettleRsp(int musicBasicId) {
super(PacketOpcodes.MusicGameSettleRsp);
public PacketMusicGameSettleRsp(int musicBasicId, long musicShareId, boolean isNewRecord) {
super(PacketOpcodes.MusicGameSettleRsp);
var proto = MusicGameSettleRspOuterClass.MusicGameSettleRsp.newBuilder();
var proto = MusicGameSettleRspOuterClass.MusicGameSettleRsp.newBuilder();
proto.setMusicBasicId(musicBasicId)
.setIsNewRecord(true);
proto.setMusicBasicId(musicBasicId)
.setMusicShareId(musicShareId)
.setIsNewRecord(isNewRecord);
this.setData(proto);
}
this.setData(proto);
}
}

View File

@ -6,12 +6,13 @@ import emu.grasscutter.net.proto.MusicGameStartRspOuterClass;
public class PacketMusicGameStartRsp extends BasePacket {
public PacketMusicGameStartRsp(int musicBasicId) {
public PacketMusicGameStartRsp(int musicBasicId, long musicShareId) {
super(PacketOpcodes.MusicGameStartRsp);
var proto = MusicGameStartRspOuterClass.MusicGameStartRsp.newBuilder();
proto.setMusicBasicId(musicBasicId);
proto.setMusicBasicId(musicBasicId)
.setMusicShareId(musicShareId);
this.setData(proto);
}

View File

@ -2,6 +2,7 @@
{
"activityId" : 5072,
"activityType" : 2202,
"scheduleId": 5072001,
"meetCondList" : [
5072001,
5072002,
@ -9,7 +10,10 @@
5072004,
5072005,
5072006,
5072007
5072007,
5072008,
5072009,
5072013
],
"beginTime" : "2022-05-01T00:00:00+08:00",
"endTime" : "2023-05-01T00:00:00+08:00"