Files
Nebula/src/main/java/emu/nebula/game/player/Player.java

969 lines
30 KiB
Java

package emu.nebula.game.player;
import java.util.Stack;
import dev.morphia.annotations.AlsoLoad;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import dev.morphia.annotations.Indexed;
import emu.nebula.GameConstants;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.account.Account;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.achievement.AchievementManager;
import emu.nebula.game.activity.ActivityManager;
import emu.nebula.game.agent.AgentManager;
import emu.nebula.game.battlepass.BattlePassManager;
import emu.nebula.game.character.CharacterStorage;
import emu.nebula.game.dating.DatingManager;
import emu.nebula.game.formation.FormationManager;
import emu.nebula.game.friends.FriendList;
import emu.nebula.game.gacha.GachaManager;
import emu.nebula.game.infinitytower.InfinityTowerManager;
import emu.nebula.game.instance.InstanceManager;
import emu.nebula.game.inventory.Inventory;
import emu.nebula.game.mail.Mailbox;
import emu.nebula.game.quest.QuestCondition;
import emu.nebula.game.quest.QuestManager;
import emu.nebula.game.scoreboss.ScoreBossManager;
import emu.nebula.game.story.StoryManager;
import emu.nebula.game.tower.StarTowerManager;
import emu.nebula.game.vampire.VampireSurvivorManager;
import emu.nebula.net.GameSession;
import emu.nebula.net.NetMsgId;
import emu.nebula.net.NetMsgPacket;
import emu.nebula.proto.PlayerData.DictionaryEntry;
import emu.nebula.proto.PlayerData.DictionaryTab;
import emu.nebula.proto.PlayerData.PlayerInfo;
import emu.nebula.proto.Public.CharShow;
import emu.nebula.proto.Public.Energy;
import emu.nebula.proto.Public.Friend;
import emu.nebula.proto.Public.HonorInfo;
import emu.nebula.proto.Public.NewbieInfo;
import emu.nebula.proto.Public.QuestType;
import emu.nebula.proto.Public.Story;
import emu.nebula.proto.Public.WorldClass;
import emu.nebula.proto.Public.WorldClassRewardState;
import emu.nebula.util.Utils;
import emu.nebula.proto.Public.Title;
import lombok.Getter;
import us.hebi.quickbuf.ProtoMessage;
import us.hebi.quickbuf.RepeatedInt;
@Getter
@Entity(value = "players", useDiscriminator = false)
public class Player implements GameDatabaseObject {
@Id private int uid;
@Indexed private String accountUid;
private transient Account account;
private transient GameSession session;
@Indexed
@AlsoLoad("playerRemoteToken")
private String remoteToken;
// Details
private String name;
private String signature;
private boolean gender;
private int headIcon;
private int skinId;
private int titlePrefix;
private int titleSuffix;
private int[] honor;
private int[] showChars;
private int[] boards;
private int level;
private int exp;
private int energy;
private long energyLastUpdate;
private long lastEpochDay;
private long lastLogin;
private long createTime;
// Managers
private final transient CharacterStorage characters;
private final transient FriendList friendList;
private final transient BattlePassManager battlePassManager;
private final transient DatingManager datingManager;
private final transient StarTowerManager starTowerManager;
private final transient InstanceManager instanceManager;
private final transient InfinityTowerManager infinityTowerManager;
private final transient VampireSurvivorManager vampireSurvivorManager;
private final transient ScoreBossManager scoreBossManager;
// Referenced data
private transient Inventory inventory;
private transient FormationManager formations;
private transient Mailbox mailbox;
private transient GachaManager gachaManager;
private transient PlayerProgress progress;
private transient StoryManager storyManager;
private transient QuestManager questManager;
private transient AchievementManager achievementManager;
private transient AgentManager agentManager;
private transient ActivityManager activityManager;
// Extra
private transient Stack<NetMsgPacket> nextPackages;
private transient boolean loaded;
@Deprecated // Morphia only
public Player() {
// Init player managers
this.characters = new CharacterStorage(this);
this.friendList = new FriendList(this);
this.battlePassManager = new BattlePassManager(this);
this.datingManager = new DatingManager(this);
this.starTowerManager = new StarTowerManager(this);
this.instanceManager = new InstanceManager(this);
this.infinityTowerManager = new InfinityTowerManager(this);
this.vampireSurvivorManager = new VampireSurvivorManager(this);
this.scoreBossManager = new ScoreBossManager(this);
// Init next packages stack
this.nextPackages = new Stack<>();
}
public Player(Account account, String name, boolean gender) {
this();
// Set uid first
if (account.getReservedPlayerUid() > 0) {
this.uid = account.getReservedPlayerUid();
} else {
this.uid = Nebula.getGameDatabase().getNextObjectId(Player.class);
}
// Set basic info
this.accountUid = account.getUid();
this.createTime = Nebula.getCurrentTime();
this.name = name;
this.signature = "";
this.gender = gender;
this.headIcon = gender ? 101 : 102;
this.skinId = 10301;
this.titlePrefix = 1;
this.titleSuffix = 2;
this.honor = new int[3];
this.showChars = new int[3];
this.boards = new int[] {410301};
this.level = 1;
this.energy = 240;
this.energyLastUpdate = this.createTime;
// Setup inventory
this.inventory = new Inventory(this);
// Add starter characters
this.getCharacters().addCharacter(103);
this.getCharacters().addCharacter(112);
this.getCharacters().addCharacter(113);
// Add starter discs
this.getCharacters().addDisc(211001);
this.getCharacters().addDisc(211005);
this.getCharacters().addDisc(211007);
this.getCharacters().addDisc(211008);
}
public Account getAccount() {
if (this.account == null) {
this.account = Nebula.getAccountDatabase().getObjectByField(Account.class, "_id", this.getAccountUid());
}
return this.account;
}
public void setSession(GameSession session) {
if (this.session != null) {
// Sanity check
if (this.session == session) {
return;
}
// Clear player from session
this.session.clearPlayer();
}
// Set session
this.session = session;
}
public void removeSession() {
this.session = null;
Nebula.getGameContext().getPlayerModule().removeFromCache(this);
}
public boolean hasSession() {
return this.session != null;
}
public void setLevel(int level) {
// Set player world class (level)
this.level = level;
// Save to database
Nebula.getGameDatabase().update(this, this.getUid(), "level", this.level);
// Trigger achievement
this.trigger(AchievementCondition.WorldClassSpecific, this.getLevel());
}
public void setExp(int exp) {
this.exp = exp;
Nebula.getGameDatabase().update(this, this.getUid(), "exp", this.exp);
}
public void setRemoteToken(String token) {
// Skip if tokens are the same
if (this.remoteToken == null) {
if (token == null) {
return;
}
} else if (this.remoteToken != null) {
if (this.remoteToken.equals(token)) {
return;
}
}
// Set remote token
this.remoteToken = token;
// Update in database
Nebula.getGameDatabase().update(this, this.getUid(), "remoteToken", this.remoteToken);
}
public boolean getGender() {
return this.gender;
}
public boolean editName(String newName) {
// Sanity check
if (newName == null || newName.isEmpty() || newName.equals(this.getName())) {
return false;
}
// Limit name length
if (newName.length() > 20) {
newName = newName.substring(0, 19);
}
// Set name
this.name = newName;
// Update in database
Nebula.getGameDatabase().update(this, this.getUid(), "name", this.getName());
// Success
return true;
}
public void editGender() {
// Set name
this.gender = !this.gender;
// Update in database
Nebula.getGameDatabase().update(this, this.getUid(), "gender", this.getGender());
}
public boolean editTitle(int prefix, int suffix) {
// Check to make sure we own these titles
if (!getInventory().getTitles().contains(prefix) || !getInventory().getTitles().contains(suffix)) {
return false;
}
// Skip if we are not changing titles
if (this.titlePrefix == prefix && this.titleSuffix == suffix) {
return true;
}
// TODO check if title is prefix or suffix
// Set
this.titlePrefix = prefix;
this.titleSuffix = suffix;
// Update in database
Nebula.getGameDatabase().update(this, this.getUid(), "titlePrefix", this.getTitlePrefix(), "titleSuffix", this.getTitleSuffix());
return true;
}
public boolean editHeadIcon(int id) {
// Skip if we are not changing head icon
if (this.headIcon == id) {
return true;
}
// Make sure we own the head icon
if (!getInventory().hasHeadIcon(id)) {
return false;
}
// Set
this.headIcon = id;
// Update in database
Nebula.getGameDatabase().update(this, this.getUid(), "headIcon", this.getHeadIcon());
// Success
return true;
}
public boolean editSignature(String signature) {
// Sanity check
if (signature == null) {
return false;
}
// Limit signature to 30 max chars
if (signature.length() > 30) {
signature = signature.substring(0, 29);
}
// Set signature
this.signature = signature;
// Update in database
Nebula.getGameDatabase().update(this, this.getUid(), "signature", this.getSignature());
// Success
return true;
}
public boolean setSkin(int skinId) {
// Skip if we are setting the same skin
if (this.skinId == skinId) {
return true;
}
// Make sure we own this skin
if (!getInventory().hasSkin(skinId)) {
return false;
}
// Set skin
this.skinId = skinId;
// Update in database
Nebula.getGameDatabase().update(this, this.getUid(), "skinId", this.getSkinId());
// Success
return false;
}
public boolean setShowChars(RepeatedInt charIds) {
// Sanity check
if (charIds.length() > this.getShowChars().length) {
return false;
}
// Verify that we have the correct characters
for (int id : charIds) {
if (id != 0 && !getCharacters().hasCharacter(id)) {
return false;
}
}
// TODO check duplicates
// Clear
this.showChars[0] = 0;
this.showChars[1] = 0;
this.showChars[2] = 0;
// Set
for (int i = 0; i < charIds.length(); i++) {
this.showChars[i] = charIds.get(i);
}
// Update in database
Nebula.getGameDatabase().update(this, this.getUid(), "showChars", this.getShowChars());
// Success
return true;
}
public boolean setHonor(RepeatedInt honorIds) {
// Sanity check
if (honorIds.length() > this.getHonor().length) {
return false;
}
// Verify that we have the honor titles
for (int id : honorIds) {
if (id != 0 && !getInventory().getHonorList().contains(id)) {
System.out.println(id);
return false;
}
}
// TODO check duplicates
// Clear
this.honor[0] = 0;
this.honor[1] = 0;
this.honor[2] = 0;
// Set
for (int i = 0; i < honorIds.length(); i++) {
this.honor[i] = honorIds.get(i);
}
// Update in database
Nebula.getGameDatabase().update(this, this.getUid(), "honor", this.getHonor());
// Success
return true;
}
public boolean setBoard(RepeatedInt ids) {
// Length check
if (ids.length() <= 0 || ids.length() > GameConstants.MAX_SHOWCASE_IDS) {
return false;
}
// Get max length
this.boards = new int[ids.length()];
// Copy ids to our boards array
for (int i = 0; i < ids.length(); i++) {
int id = ids.get(i);
this.boards[i] = id;
}
// Save to database
Nebula.getGameDatabase().update(this, this.getUid(), "boards", this.getBoards());
// Success
return true;
}
public void setNewbieInfo(int groupId, int stepId) {
// TODO
}
public int getMaxExp() {
var data = GameData.getWorldClassDataTable().get(this.level + 1);
return data != null ? data.getExp() : 0;
}
public PlayerChangeInfo addExp(int amount, PlayerChangeInfo changes) {
// Check if changes is null
if (changes == null) {
changes = new PlayerChangeInfo();
}
// Sanity
if (amount <= 0) {
return changes;
}
// Setup
int oldLevel = this.getLevel();
int oldExp = this.getExp();
int expRequired = this.getMaxExp();
// Add exp
this.exp += amount;
// Check for level ups
while (this.exp >= expRequired && expRequired > 0) {
// Add level
this.level += 1;
this.exp -= expRequired;
// Recalculate exp required
expRequired = this.getMaxExp();
// Set level reward
this.getQuestManager().getLevelRewards().setBit(this.level);
}
// Save to database
Nebula.getGameDatabase().update(
this,
this.getUid(),
"level",
this.getLevel(),
"exp",
this.getExp()
);
// Save level rewards if we changed it
if (oldLevel != this.getLevel()) {
// Update level rewards
this.getQuestManager().saveLevelRewards();
this.addNextPackage(
NetMsgId.world_class_reward_state_notify,
WorldClassRewardState.newInstance()
.setFlag(getQuestManager().getLevelRewards().toBigEndianByteArray())
);
// Trigger achievement
this.trigger(AchievementCondition.WorldClassSpecific, this.getLevel());
}
// Calculate changes
var proto = WorldClass.newInstance()
.setAddClass(this.getLevel() - oldLevel)
.setExpChange(this.getExp() - oldExp);
changes.add(proto);
return changes;
}
// Energy
public int getEnergy() {
// Cache time
long time = Nebula.getCurrentTime();
// Calculate time diff
double diff = time - this.energyLastUpdate;
long bonusEnergy = (int) Math.floor(diff / GameConstants.ENERGY_REGEN_TIME);
if (this.energy < GameConstants.MAX_ENERGY) {
this.energy = Math.min(this.energy + (int) bonusEnergy, GameConstants.MAX_ENERGY);
this.energyLastUpdate = (bonusEnergy * GameConstants.ENERGY_REGEN_TIME) + this.energyLastUpdate;
} else {
this.energyLastUpdate = time;
}
return this.energy;
}
public PlayerChangeInfo addEnergy(int amount, PlayerChangeInfo change) {
// Sanity check
if (amount <= 0) {
return change == null ? new PlayerChangeInfo() : change;
}
// Complete
return modifyEnergy(amount, change);
}
public PlayerChangeInfo consumeEnergy(int amount, PlayerChangeInfo change) {
// Sanity check
if (amount <= 0) {
return change == null ? new PlayerChangeInfo() : change;
}
// Consume energy
change = modifyEnergy(-amount, change);
// Trigger quest
this.trigger(QuestCondition.EnergyDeplete, amount);
// Complete
return change;
}
private PlayerChangeInfo modifyEnergy(int amount, PlayerChangeInfo change) {
// Check if changes is null
if (change == null) {
change = new PlayerChangeInfo();
}
// Update energy
this.getEnergy();
// Remove energy
this.energy = Math.max(this.energy + amount, 0);
// Save to database
Nebula.getGameDatabase().update(
this,
this.getUid(),
"energy",
this.getEnergy(),
"energyLastUpdate",
this.getEnergyLastUpdate()
);
// Add to change
change.add(this.getEnergyProto());
// Complete
return change;
}
// Dailies
public void checkResetDailies() {
// Sanity check to make sure daily reset isnt being triggered wrong
if (Nebula.getGameContext().getEpochDays() <= this.getLastEpochDay()) {
return;
}
// Check if week has changed (Resets on monday)
// TODO add a config option
int curWeek = Utils.getWeeks(this.getLastEpochDay());
boolean hasWeekChanged = Nebula.getGameContext().getEpochWeeks() > curWeek;
// Reset dailies
this.resetDailies(hasWeekChanged);
// Trigger quest/achievement login
this.trigger(QuestCondition.LoginTotal, 1);
// Update last epoch day
this.lastEpochDay = Nebula.getGameContext().getEpochDays();
Nebula.getGameDatabase().update(this, this.getUid(), "lastEpochDay", this.lastEpochDay);
}
public void resetDailies(boolean resetWeekly) {
// Reset daily quests
this.getQuestManager().resetDailyQuests();
this.getBattlePassManager().getBattlePass().resetDailyQuests(resetWeekly);
}
// Trigger quests + achievements
public void trigger(int condition, int progress, int param1, int param2) {
this.getQuestManager().trigger(condition, progress, param1, param2);
this.getBattlePassManager().getBattlePass().trigger(condition, progress, param1, param2);
this.getAchievementManager().trigger(condition, progress, param1, param2);
}
public void trigger(QuestCondition condition, int progress) {
this.trigger(condition.getValue(), progress, 0, 0);
}
public void trigger(QuestCondition condition, int progress, int param1) {
this.trigger(condition.getValue(), progress, param1, 0);
}
public void trigger(AchievementCondition condition, int progress) {
this.trigger(condition.getValue(), progress, 0, 0);
}
public void trigger(AchievementCondition condition, int progress, int param1, int param2) {
this.trigger(condition.getValue(), progress, param1, param2);
}
// Login
private <T extends PlayerManager> T loadManagerFromDatabase(Class<T> cls) {
var manager = Nebula.getGameDatabase().getObjectByField(cls, "_id", this.getUid());
if (manager != null) {
manager.setPlayer(this);
} else {
try {
manager = cls.getDeclaredConstructor(Player.class).newInstance(this);
} catch (Exception e) {
e.printStackTrace();
}
}
return manager;
}
/**
* Called when the player is loaded from the database
*/
public void onLoad() {
// Load from database
this.getCharacters().loadFromDatabase();
this.getFriendList().loadFromDatabase();
this.getStarTowerManager().loadFromDatabase();
this.getBattlePassManager().loadFromDatabase();
// Load inventory before referenced classes
if (this.inventory == null) {
this.inventory = this.loadManagerFromDatabase(Inventory.class);
}
this.getInventory().loadFromDatabase();
// Load referenced classes from the database
this.formations = this.loadManagerFromDatabase(FormationManager.class);
this.mailbox = this.loadManagerFromDatabase(Mailbox.class);
this.progress = this.loadManagerFromDatabase(PlayerProgress.class);
this.gachaManager = this.loadManagerFromDatabase(GachaManager.class);
this.storyManager = this.loadManagerFromDatabase(StoryManager.class);
this.questManager = this.loadManagerFromDatabase(QuestManager.class);
this.achievementManager = this.loadManagerFromDatabase(AchievementManager.class);
this.agentManager = this.loadManagerFromDatabase(AgentManager.class);
this.activityManager = this.loadManagerFromDatabase(ActivityManager.class);
// Database fixes
if (this.showChars == null) {
this.showChars = new int[3];
this.save();
}
// Init activities
this.getActivityManager().init();
// Load complete
this.loaded = true;
}
public void onLogin() {
// See if we need to reset dailies
this.checkResetDailies();
// Update last login time
this.lastLogin = System.currentTimeMillis();
Nebula.getGameDatabase().update(this, this.getUid(), "lastLogin", this.getLastLogin());
}
// Next packages
public boolean hasNextPackages() {
return this.getNextPackages().size() > 0;
}
public void addNextPackage(int msgId, ProtoMessage<?> proto) {
this.getNextPackages().add(new NetMsgPacket(msgId, proto));
}
// Misc
/**
* Called AFTER a response is sent to the client
*/
public void afterResponse() {
// Check if we need save achievements
if (this.getAchievementManager().isQueueSave()) {
this.getAchievementManager().save();
}
}
// Proto
public PlayerInfo toProto() {
PlayerInfo proto = PlayerInfo.newInstance()
.setServerTs(Nebula.getCurrentTime())
.setDailyShopRewardStatus(this.getQuestManager().hasDailyReward())
.setAchievements(new byte[64]);
var acc = proto.getMutableAcc()
.setNickName(this.getName())
.setSignature(this.getSignature())
.setGender(this.getGender())
.setId(this.getUid())
.setHeadIcon(this.getHeadIcon())
.setSkinId(this.getSkinId())
.setTitlePrefix(this.getTitlePrefix())
.setTitleSuffix(this.getTitleSuffix())
.setCreateTime(this.getCreateTime());
// Set showcase character
for (int charId : this.getShowChars()) {
var info = CharShow.newInstance();
var character = this.getCharacters().getCharacterById(charId);
if (character != null) {
info.setCharId(character.getCharId())
.setLevel(character.getLevel())
.setSkin(character.getSkin());
}
acc.addChars(info);
}
// Set honor
for (int honorId : this.getHonor()) {
var info = HonorInfo.newInstance();
if (honorId != 0) {
info.setId(honorId);
}
proto.addHonors(info);
}
this.getInventory().getHonorList().forEach(proto::addHonorList);
// Set world class
proto.getMutableWorldClass()
.setCur(this.getLevel())
.setLastExp(this.getExp());
proto.getMutableEnergy().setEnergy(this.getEnergyProto());
// Add characters/discs/res/items
for (var character : getCharacters().getCharacterCollection()) {
proto.addChars(character.toProto());
}
for (var disc : getCharacters().getDiscCollection()) {
proto.addDiscs(disc.toProto());
}
for (var item : getInventory().getItems().values()) {
proto.addItems(item.toProto());
}
for (var res : getInventory().getResources().values()) {
proto.addRes(res.toProto());
}
// Formations
var formations = proto.getMutableFormation();
for (var f : this.getFormations().getFormations().values()) {
formations.addInfo(f.toProto());
}
// Set player states
var state = proto.getMutableState()
.setStorySet(true)
.setFriend(this.getFriendList().hasPendingRequests());
state.getMutableMail()
.setNew(this.getMailbox().hasNewMail());
state.getMutableBattlePass()
.setState(1);
state.getMutableAchievement()
.setNew(this.getAchievementManager().hasNewAchievements());
state.getMutableFriendEnergy();
state.getMutableMallPackage();
state.getMutableTravelerDuelQuest()
.setType(QuestType.TravelerDuel);
state.getMutableTravelerDuelChallengeQuest()
.setType(QuestType.TravelerDuelChallenge);
state.getMutableStarTower();
state.getMutableStarTowerBook();
state.getMutableScoreBoss();
state.getMutableCharAffinityRewards();
// Force complete tutorials
for (var guide : GameData.getGuideGroupDataTable()) {
var info = NewbieInfo.newInstance()
.setGroupId(guide.getId())
.setStepId(-1);
acc.addNewbies(info);
}
acc.addNewbies(NewbieInfo.newInstance().setGroupId(GameConstants.INTRO_GUIDE_ID).setStepId(-1));
// Story
var story = proto.getMutableStory();
for (int storyId : this.getStoryManager().getCompletedStories()) {
var storyProto = Story.newInstance()
.setIdx(storyId);
story.addStories(storyProto);
}
// Add titles
for (int titleId : this.getInventory().getTitles()) {
var titleProto = Title.newInstance()
.setTitleId(titleId);
proto.addTitles(titleProto);
}
// Add board ids
for (int boardId : this.getBoards()) {
proto.addBoard(boardId);
}
// Quests
this.getQuestManager().encodeProto(proto);
// Add dictionary tabs
for (var dictionaryData : GameData.getDictionaryTabDataTable()) {
var dictionaryProto = DictionaryTab.newInstance()
.setTabId(dictionaryData.getId());
for (var entry : dictionaryData.getEntries()) {
var entryProto = DictionaryEntry.newInstance()
.setIndex(entry.getIndex())
.setStatus(2); // 2 = complete
dictionaryProto.addEntries(entryProto);
}
proto.addDictionaries(dictionaryProto);
}
// Add progress
this.getProgress().encodeProto(proto);
// Handbook
proto.addHandbook(this.getCharacters().getCharacterHandbook());
proto.addHandbook(this.getCharacters().getDiscHandbook());
// Phone
var phone = proto.getMutablePhone();
phone.setNewMessage(this.getCharacters().getNewPhoneMessageCount());
// Agent
var agentProto = proto.getMutableAgent();
for (var agent : getAgentManager().getAgents().values()) {
agentProto.addInfos(agent.toProto());
}
// Activities
for (var activity : getActivityManager().getActivities().values()) {
proto.addActivities(activity.toProto());
}
// Complete
return proto;
}
public Friend getFriendProto() {
var proto = Friend.newInstance()
.setId(this.getUid())
.setWorldClass(this.getLevel())
.setHeadIcon(this.getHeadIcon())
.setNickName(this.getName())
.setSignature(this.getSignature())
.setTitlePrefix(this.getTitlePrefix())
.setTitleSuffix(this.getTitleSuffix())
.setLastLoginTime(this.getLastLogin() * 1_000_000L);
for (int charId : this.getShowChars()) {
var info = CharShow.newInstance()
.setCharId(charId)
.setLevel(1) // TODO
.setSkin((charId * 100) + 1); // TODO
proto.addCharShows(info);
}
for (int honorId : this.getHonor()) {
var info = HonorInfo.newInstance()
.setId(honorId);
proto.addHonors(info);
}
return proto;
}
public Energy getEnergyProto() {
long nextDuration = Math.max(GameConstants.ENERGY_REGEN_TIME - (Nebula.getCurrentTime() - getEnergyLastUpdate()), 1);
var proto = Energy.newInstance()
.setUpdateTime(this.getEnergyLastUpdate())
.setNextDuration(nextDuration)
.setPrimary(this.getEnergy())
.setIsPrimary(true);
return proto;
}
}