Simply implement achievement system and add achievement command (#2068)

* Implement achievement system

* Update src/main/java/emu/grasscutter/command/commands/AchievementCommand.java

Co-authored-by: Der Chien <b03902015@ntu.edu.tw>

* fix: redundant codes

* fix: redundant codes

* Update language files

---------

Co-authored-by: Der Chien <b03902015@ntu.edu.tw>
This commit is contained in:
hamusuke
2023-02-26 14:14:27 +09:00
committed by GitHub
parent 51479e2abd
commit 3ab3d5bc04
35 changed files with 7418 additions and 1 deletions

View File

@@ -0,0 +1,38 @@
package emu.grasscutter.game.achievement;
import dev.morphia.annotations.Entity;
import emu.grasscutter.net.proto.AchievementOuterClass;
import emu.grasscutter.net.proto.StatusOuterClass;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
public class Achievement {
@Setter
private StatusOuterClass.Status status;
private int id;
private int totalProgress;
@Setter
private int curProgress;
@Setter
private int finishTimestampSec;
public Achievement(StatusOuterClass.Status status, int id, int totalProgress, int curProgress, int finishTimestampSec) {
this.status = status;
this.id = id;
this.totalProgress = totalProgress;
this.curProgress = curProgress;
this.finishTimestampSec = finishTimestampSec;
}
public AchievementOuterClass.Achievement toProto() {
return AchievementOuterClass.Achievement.newBuilder()
.setStatus(this.getStatus())
.setId(this.getId())
.setTotalProgress(this.getTotalProgress())
.setCurProgress(this.getCurProgress())
.setFinishTimestamp(this.getFinishTimestampSec())
.build();
}
}

View File

@@ -0,0 +1,48 @@
package emu.grasscutter.game.achievement;
import lombok.Getter;
@Getter
public class AchievementControlReturns {
private final Return ret;
private final int changedAchievementStatusNum;
private AchievementControlReturns(Return ret) {
this(ret, 0);
}
private AchievementControlReturns(Return ret, int changedAchievementStatusNum) {
this.ret = ret;
this.changedAchievementStatusNum = changedAchievementStatusNum;
}
public static AchievementControlReturns success(int changedAchievementStatusNum) {
return new AchievementControlReturns(Return.SUCCESS, changedAchievementStatusNum);
}
public static AchievementControlReturns achievementNotFound() {
return new AchievementControlReturns(Return.ACHIEVEMENT_NOT_FOUND);
}
public static AchievementControlReturns alreadyAchieved() {
return new AchievementControlReturns(Return.ALREADY_ACHIEVED);
}
public static AchievementControlReturns notYetAchieved() {
return new AchievementControlReturns(Return.NOT_YET_ACHIEVED);
}
public enum Return {
SUCCESS("commands.achievement.success."),
ACHIEVEMENT_NOT_FOUND("commands.achievement.fail.achievement_not_found"),
ALREADY_ACHIEVED("commands.achievement.fail.already_achieved"),
NOT_YET_ACHIEVED("commands.achievement.fail.not_yet_achieved");
@Getter
private final String key;
Return(String key) {
this.key = key;
}
}
}

View File

@@ -0,0 +1,290 @@
package emu.grasscutter.game.achievement;
import com.github.davidmoten.guavamini.Lists;
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.excels.AchievementData;
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.StatusOuterClass;
import emu.grasscutter.server.packet.send.PacketAchievementAllDataNotify;
import emu.grasscutter.server.packet.send.PacketAchievementUpdateNotify;
import emu.grasscutter.server.packet.send.PacketTakeAchievementGoalRewardRsp;
import emu.grasscutter.server.packet.send.PacketTakeAchievementRewardRsp;
import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import org.bson.types.ObjectId;
import javax.annotation.Nullable;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.IntSupplier;
@Entity("achievements")
@Data
@Builder(builderMethodName = "of")
public class Achievements {
private static final IntSupplier currentTimeSecs = () -> (int) (System.currentTimeMillis() / 1000L);
private static final Achievement INVALID = new Achievement(StatusOuterClass.Status.STATUS_INVALID, -1, 0, 0, 0);
@Id
private ObjectId id;
private int uid;
@Transient
private Player player;
private Map<Integer, Achievement> achievementList;
@Getter
private int finishedAchievementNum;
private List<Integer> takenGoalRewardIdList;
public static Achievements getByPlayer(Player player) {
var achievements = player.getAchievements() == null ? DatabaseHelper.getAchievementData(player.getUid()) : player.getAchievements();
if (achievements == null) {
achievements = create(player.getUid());
}
return achievements;
}
public static Achievements create(int uid) {
var newAchievement = Achievements.of()
.uid(uid)
.achievementList(init())
.finishedAchievementNum(0)
.takenGoalRewardIdList(Lists.newArrayList())
.build();
newAchievement.save();
return newAchievement;
}
private static Map<Integer, Achievement> init() {
Map<Integer, Achievement> map = new HashMap<>();
GameData.getAchievementDataMap().values().stream()
.filter(AchievementData::isUsed)
.forEach(a -> {
map.put(a.getId(), new Achievement(StatusOuterClass.Status.STATUS_UNFINISHED, a.getId(), a.getProgress(), 0, 0));
});
return map;
}
public AchievementControlReturns grant(int achievementId) {
var a = this.getAchievement(achievementId);
if (a == null || this.isFinished(achievementId)) {
return a == null ? AchievementControlReturns.achievementNotFound() : AchievementControlReturns.alreadyAchieved();
}
return this.progress(achievementId, a.getTotalProgress());
}
public AchievementControlReturns revoke(int achievementId) {
var a = this.getAchievement(achievementId);
if (a == null || !this.isFinished(achievementId)) {
return a == null ? AchievementControlReturns.achievementNotFound() : AchievementControlReturns.notYetAchieved();
}
return this.progress(achievementId, 0);
}
public AchievementControlReturns progress(int achievementId, int progress) {
var a = this.getAchievement(achievementId);
if (a == null) {
return AchievementControlReturns.achievementNotFound();
}
a.setCurProgress(progress);
return AchievementControlReturns.success(this.notifyOtherAchievements(a));
}
private int notifyOtherAchievements(Achievement a) {
var changedNum = new AtomicInteger();
changedNum.addAndGet(this.update(a) ? 1 : 0);
GameData.getAchievementDataMap().get(a.getId())
.getExcludedGroupAchievementIdList().stream()
.map(this::getAchievement)
.filter(Objects::nonNull)
.forEach(other -> {
other.setCurProgress(a.getCurProgress());
changedNum.addAndGet(this.update(other) ? 1 : 0);
});
this.computeFinishedAchievementNum();
this.save();
this.sendUpdatePacket(a);
return changedNum.intValue();
}
private boolean update(Achievement a) {
if (a.getStatus() == StatusOuterClass.Status.STATUS_UNFINISHED && a.getCurProgress() >= a.getTotalProgress()) {
a.setStatus(StatusOuterClass.Status.STATUS_FINISHED);
a.setFinishTimestampSec(currentTimeSecs.getAsInt());
return true;
} else if (this.isFinished(a.getId()) && a.getCurProgress() < a.getTotalProgress()) {
a.setStatus(StatusOuterClass.Status.STATUS_UNFINISHED);
a.setFinishTimestampSec(0);
return true;
}
return false;
}
private void computeFinishedAchievementNum() {
this.finishedAchievementNum = GameData.getAchievementDataMap().values().stream()
.filter(a -> this.isFinished(a.getId()))
.mapToInt(value -> 1)
.sum();
}
private void sendUpdatePacket(Achievement achievement) {
List<Achievement> achievements = Lists.newArrayList(achievement);
achievements.addAll(GameData.getAchievementDataMap().get(achievement.getId())
.getExcludedGroupAchievementIdList()
.stream().map(this::getAchievement)
.filter(Objects::nonNull)
.toList()
);
this.sendUpdatePacket(achievements);
}
private void sendUpdatePacket(List<Achievement> achievement) {
if (this.isPacketSendable()) {
this.player.sendPacket(new PacketAchievementUpdateNotify(achievement));
}
}
@Nullable
public Achievement getAchievement(int achievementId) {
if (this.isInvalid(achievementId)) {
return null;
}
return this.getAchievementList().computeIfAbsent(achievementId, id -> {
return new Achievement(StatusOuterClass.Status.STATUS_UNFINISHED, id, GameData.getAchievementDataMap().get(id.intValue()).getProgress(), 0, 0);
});
}
public boolean isInvalid(int achievementId) {
var data = GameData.getAchievementDataMap().get(achievementId);
return data == null || data.isDisuse();
}
public StatusOuterClass.Status getStatus(int achievementId) {
return this.getAchievementList().getOrDefault(achievementId, INVALID).getStatus();
}
public boolean isFinished(int achievementId) {
var status = this.getStatus(achievementId);
return status == StatusOuterClass.Status.STATUS_FINISHED || status == StatusOuterClass.Status.STATUS_REWARD_TAKEN;
}
public void takeReward(List<Integer> ids) {
List<GameItem> rewards = Lists.newArrayList();
for (int i : ids) {
var target = GameData.getAchievementDataMap().get(i);
if (target == null) {
Grasscutter.getLogger().warn("null returned while taking reward!");
return;
}
if (this.isRewardTaken(i)) {
this.player.sendPacket(new PacketTakeAchievementRewardRsp());
return;
}
var data = GameData.getRewardDataMap().get(target.getFinishRewardId());
if (data == null) {
Grasscutter.getLogger().warn("null returned while getting reward data!");
continue;
}
data.getRewardItemList().forEach(itemParamData -> {
var itemData = GameData.getItemDataMap().get(itemParamData.getId());
if (itemData == null) {
Grasscutter.getLogger().warn("itemData == null!");
return;
}
rewards.add(new GameItem(itemData, itemParamData.getCount()));
});
var a = this.getAchievement(i);
a.setStatus(StatusOuterClass.Status.STATUS_REWARD_TAKEN);
this.save();
this.sendUpdatePacket(a);
}
this.player.getInventory().addItems(rewards, ActionReason.AchievementReward);
this.player.sendPacket(new PacketTakeAchievementRewardRsp(ids, rewards.stream().map(GameItem::toItemParam).toList()));
}
public void takeGoalReward(List<Integer> ids) {
List<GameItem> rewards = Lists.newArrayList();
for (int i : ids) {
if (this.takenGoalRewardIdList.contains(i)) {
this.player.sendPacket(new PacketTakeAchievementGoalRewardRsp());
}
var goalData = GameData.getAchievementGoalDataMap().get(i);
if (goalData == null) {
Grasscutter.getLogger().warn("null returned while getting goal reward data!");
continue;
}
var data = GameData.getRewardDataMap().get(goalData.getFinishRewardId());
if (data == null) {
Grasscutter.getLogger().warn("null returned while getting reward data!");
continue;
}
data.getRewardItemList().forEach(itemParamData -> {
var itemData = GameData.getItemDataMap().get(itemParamData.getId());
if (itemData == null) {
Grasscutter.getLogger().warn("itemData == null!");
return;
}
rewards.add(new GameItem(itemData, itemParamData.getCount()));
});
this.takenGoalRewardIdList.add(i);
this.save();
}
this.player.getInventory().addItems(rewards, ActionReason.AchievementGoalReward);
this.player.sendPacket(new PacketTakeAchievementGoalRewardRsp(ids, rewards.stream().map(GameItem::toItemParam).toList()));
}
public boolean isRewardTaken(int achievementId) {
return this.getStatus(achievementId) == StatusOuterClass.Status.STATUS_REWARD_TAKEN;
}
public boolean isRewardLeft(int achievementId) {
return this.getStatus(achievementId) == StatusOuterClass.Status.STATUS_FINISHED;
}
private boolean isPacketSendable() {
return this.player != null;
}
public void save() {
DatabaseHelper.saveAchievementData(this);
}
public void onLogin(Player player) {
if (this.player == null) {
this.player = player;
}
this.player.sendPacket(new PacketAchievementAllDataNotify(this.player));
}
}

View File

@@ -10,6 +10,7 @@ import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.Account;
import emu.grasscutter.game.CoopRequest;
import emu.grasscutter.game.ability.AbilityManager;
import emu.grasscutter.game.achievement.Achievements;
import emu.grasscutter.game.activity.ActivityManager;
import emu.grasscutter.game.avatar.Avatar;
import emu.grasscutter.game.avatar.AvatarStorage;
@@ -168,6 +169,7 @@ public class Player {
@Getter private transient SatiationManager satiationManager;
// Manager data (Save-able to the database)
@Getter private transient Achievements achievements;
private PlayerProfile playerProfile; // Getter has null-check
@Getter private TeamManager teamManager;
private TowerData towerData; // Getter has null-check
@@ -973,11 +975,15 @@ public class Player {
.setIsShowAvatar(this.isShowAvatars())
.addAllShowAvatarInfoList(socialShowAvatarInfoList)
.addAllShowNameCardIdList(this.getShowNameCardInfoList())
.setFinishAchievementNum(0)
.setFinishAchievementNum(this.getFinishedAchievementNum())
.setFriendEnterHomeOptionValue(this.getHome() == null ? 0 : this.getHome().getEnterHomeOption());
return social;
}
public int getFinishedAchievementNum() {
return Achievements.getByPlayer(this).getFinishedAchievementNum();
}
public List<ShowAvatarInfoOuterClass.ShowAvatarInfo> getShowAvatarInfoList() {
List<ShowAvatarInfoOuterClass.ShowAvatarInfo> showAvatarInfoList = new ArrayList<>();
@@ -1172,6 +1178,7 @@ public class Player {
}
// Load from db
this.achievements = Achievements.getByPlayer(this);
this.getAvatars().loadFromDatabase();
this.getInventory().loadFromDatabase();
this.loadBattlePassManager(); // Call before avatar postLoad to avoid null pointer
@@ -1224,6 +1231,10 @@ public class Player {
session.send(new PacketQuestListNotify(this));
session.send(new PacketCodexDataFullNotify(this));
session.send(new PacketAllWidgetDataNotify(this));
//Achievements
this.achievements.onLogin(this);
session.send(new PacketWidgetGadgetAllDataNotify());
session.send(new PacketCombineDataNotify(this.unlockedCombines));
session.send(new PacketGetChatEmojiCollectionRsp(this.getChatEmojiIdList()));