diff --git a/src/main/java/emu/nebula/GameConstants.java b/src/main/java/emu/nebula/GameConstants.java index 30ccce3..f7a31d5 100644 --- a/src/main/java/emu/nebula/GameConstants.java +++ b/src/main/java/emu/nebula/GameConstants.java @@ -51,6 +51,8 @@ public class GameConstants { public static final int MAX_PRESETS = 50; public static final int MAX_VITA_PROJECTS = 5; + public static final int MAX_GACHA_HISTORY_SAVE_RETRIES = 2; + public static final int MAX_FRIENDSHIPS = 50; public static final int MAX_PENDING_FRIENDSHIPS = 30; diff --git a/src/main/java/emu/nebula/game/battlepass/BattlePass.java b/src/main/java/emu/nebula/game/battlepass/BattlePass.java index bbda243..59fa3ce 100644 --- a/src/main/java/emu/nebula/game/battlepass/BattlePass.java +++ b/src/main/java/emu/nebula/game/battlepass/BattlePass.java @@ -2,6 +2,7 @@ package emu.nebula.game.battlepass; import dev.morphia.annotations.Entity; import dev.morphia.annotations.Id; + import emu.nebula.GameConstants; import emu.nebula.Nebula; import emu.nebula.data.GameData; @@ -16,6 +17,7 @@ import emu.nebula.game.quest.QuestType; import emu.nebula.net.NetMsgId; import emu.nebula.proto.BattlePassInfoOuterClass.BattlePassInfo; import emu.nebula.util.Bitset; + import lombok.Getter; import lombok.Setter; @@ -134,7 +136,7 @@ public class BattlePass implements GameDatabaseObject { * Check claimable tasks to show battle pass red dot. */ public synchronized boolean hasClaimableQuest() { - if (!this.getPlayer().isBattlePassUnlocked()) { + if (!this.getManager().isUnlocked()) { return false; } @@ -157,7 +159,7 @@ public class BattlePass implements GameDatabaseObject { * reward lane item for the player's current level/mode. */ public synchronized boolean hasClaimableReward() { - if (!this.getPlayer().isBattlePassUnlocked()) { + if (!this.getManager().isUnlocked()) { return false; } @@ -238,7 +240,7 @@ public class BattlePass implements GameDatabaseObject { * Update this quest on the player client */ private void syncQuest(GameQuest quest) { - if (!getPlayer().hasSession() || !getPlayer().isBattlePassUnlocked()) { + if (!getPlayer().hasSession() || !getManager().isUnlocked()) { return; } @@ -399,13 +401,72 @@ public class BattlePass implements GameDatabaseObject { // Add rewards return getPlayer().getInventory().addItems(rewards); } + + public PlayerChangeInfo buyLevels(int levels) { + // Sanity check + if (levels < 1) { + return null; + } + + // Setup + int costItemId = 2; // Gems + int currentLevel = this.getLevel(); + int totalGemCost = 0; + int totalExp = 0; + int affordableLevels = 0; + + var inventory = getPlayer().getInventory(); + + // Calculate total cost and exp for the levels we can afford + for (int i = 1; i <= levels; i++) { + // Get level data + var data = GameData.getBattlePassLevelDataTable().get(currentLevel + i); + + // Stop if max level reached + if (data == null) { + break; + } + + // Stop if not enough gems for this level + int cost = data.getQty(); + if (inventory.getResourceCount(costItemId) < totalGemCost + cost) { + break; + } + + totalGemCost += cost; + totalExp += data.getExp(); + affordableLevels++; + } + + // Make sure we can buy at least one level + if (affordableLevels == 0) { + return null; + } + + // Prepare change info + var change = new PlayerChangeInfo(); + + // Deduct gems + if (totalGemCost > 0) { + change.add(inventory.addItem(costItemId, -totalGemCost)); + } + + // Add all exp at once + this.addExp(totalExp); + + // Save to database + this.save(); + + // Success + return change; + } // Proto public BattlePassInfo toProto() { // Return a locked/empty snapshot until the battle pass feature is unlocked, // so the client cannot derive local quest or reward red dots from battle pass data. - if (!this.getPlayer().isBattlePassUnlocked()) { + if (!this.getManager().isUnlocked()) { return BattlePassInfo.newInstance() .setId(0) .setLevel(0) diff --git a/src/main/java/emu/nebula/game/battlepass/BattlePassManager.java b/src/main/java/emu/nebula/game/battlepass/BattlePassManager.java index 0e92ed8..27893b6 100644 --- a/src/main/java/emu/nebula/game/battlepass/BattlePassManager.java +++ b/src/main/java/emu/nebula/game/battlepass/BattlePassManager.java @@ -1,8 +1,12 @@ package emu.nebula.game.battlepass; +import emu.nebula.GameConstants; import emu.nebula.Nebula; import emu.nebula.game.player.Player; import emu.nebula.game.player.PlayerManager; +import emu.nebula.net.NetMsgId; +import emu.nebula.proto.Public.BattlePassState; + import lombok.Getter; @Getter @@ -13,6 +17,51 @@ public class BattlePassManager extends PlayerManager { super(player); } + public boolean isUnlocked() { + return this.getPlayer().getLevel() >= GameConstants.BATTLE_PASS_UNLOCK_LEVEL; + } + + /** + * Updates the battle pass state on the client + */ + public void updateState() { + this.getPlayer().addNextPackage( + NetMsgId.battle_pass_state_notify, + this.getStateProto() + ); + } + + /** + * Pushes a full battle-pass snapshot for clients that only refresh their + * local battle-pass cache/red dots from the page-info payload. + */ + public void queueBattlePassInfoNotify() { + this.getPlayer().addNextPackage( + NetMsgId.battle_pass_info_succeed_ack, + this.getBattlePass().toProto() + ); + } + + /** + * Battle-pass unlock is a special case: clients need the full page snapshot + * to initialize local battle-pass cache and refresh the main-entry red dot. + */ + public void queueUnlockNotify(int oldLevel) { + if (oldLevel < GameConstants.BATTLE_PASS_UNLOCK_LEVEL && this.isUnlocked()) { + this.queueBattlePassInfoNotify(); + } + } + + // Proto + + public BattlePassState getStateProto() { + int state = this.isUnlocked() + ? this.getBattlePass().getClientState() + : 0; + + return BattlePassState.newInstance().setState(state); + } + // Database public void loadFromDatabase() { diff --git a/src/main/java/emu/nebula/game/gacha/GachaManager.java b/src/main/java/emu/nebula/game/gacha/GachaManager.java index d04a9c0..8522259 100644 --- a/src/main/java/emu/nebula/game/gacha/GachaManager.java +++ b/src/main/java/emu/nebula/game/gacha/GachaManager.java @@ -2,6 +2,7 @@ package emu.nebula.game.gacha; import dev.morphia.annotations.Entity; import dev.morphia.annotations.Id; +import emu.nebula.GameConstants; import emu.nebula.Nebula; import emu.nebula.data.resources.GachaDef; import emu.nebula.data.resources.GachaNewbieDef; @@ -19,19 +20,20 @@ import java.util.Set; @Getter @Entity(value = "gacha", useDiscriminator = false) public class GachaManager extends PlayerManager implements GameDatabaseObject { - private static final String PATH_BANNERS = "banners."; - private static final String PATH_PITY_STATES = "pityStates."; - private static final String PATH_NEWBIE_STATES = "newbieStates."; - private static final int HISTORY_SAVE_RETRIES = 2; - @Id private int uid; private Map banners = new HashMap<>(); private Map pityStates = new HashMap<>(); private Map newbieStates = new HashMap<>(); + + // Not saved to the database private transient Set lockedNewbieObtainIds = new HashSet<>(); + private static final String PATH_BANNERS = "banners."; + private static final String PATH_PITY_STATES = "pityStates."; + private static final String PATH_NEWBIE_STATES = "newbieStates."; + @Deprecated // Morphia only public GachaManager() { @@ -91,7 +93,7 @@ public class GachaManager extends PlayerManager implements GameDatabaseObject { var log = new GachaHistoryLog(this.getPlayerUid(), storageId, gachaId, results); Exception lastError = null; - for (int attempt = 1; attempt <= HISTORY_SAVE_RETRIES + 1; attempt++) { + for (int attempt = 1; attempt <= GameConstants.MAX_GACHA_HISTORY_SAVE_RETRIES + 1; attempt++) { try { Nebula.getGameDatabase().save(log); return; diff --git a/src/main/java/emu/nebula/game/player/Player.java b/src/main/java/emu/nebula/game/player/Player.java index c0628d7..00632ba 100644 --- a/src/main/java/emu/nebula/game/player/Player.java +++ b/src/main/java/emu/nebula/game/player/Player.java @@ -4,6 +4,7 @@ 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; @@ -40,12 +41,13 @@ import emu.nebula.proto.Notify.SigninRewardUpdate; import emu.nebula.proto.PlayerData.DictionaryEntry; import emu.nebula.proto.PlayerData.DictionaryTab; import emu.nebula.proto.PlayerData.PlayerInfo; -import emu.nebula.proto.Public; import emu.nebula.proto.Public.*; import emu.nebula.util.ResetCycle; import emu.nebula.util.Utils; import emu.nebula.util.ints.String2IntMap; + import lombok.Getter; + import us.hebi.quickbuf.ProtoMessage; import us.hebi.quickbuf.RepeatedInt; @@ -231,7 +233,7 @@ public class Player implements GameDatabaseObject { this.trigger(AchievementCondition.WorldClassSpecific, this.getLevel()); if (oldLevel != this.level) { - this.queueBattlePassUnlockNotify(oldLevel); + this.getBattlePassManager().queueUnlockNotify(oldLevel); } } @@ -556,7 +558,8 @@ public class Player implements GameDatabaseObject { // Trigger achievement this.trigger(AchievementCondition.WorldClassSpecific, this.getLevel()); - this.queueBattlePassUnlockNotify(oldLevel); + // Send battle pass unlock notify packet + this.getBattlePassManager().queueUnlockNotify(oldLevel); } // Calculate changes @@ -759,6 +762,10 @@ public class Player implements GameDatabaseObject { } } + private long getLastResetDayTimeSeconds() { + return Utils.getResetTimeSecondsByEpochDay(this.getLastEpochDay()); + } + // Trigger quests + achievements public void trigger(int condition, int progress, int param1, int param2) { @@ -879,6 +886,8 @@ public class Player implements GameDatabaseObject { Nebula.getGameDatabase().update(this, this.getUid(), "lastLogin", this.getLastLogin()); } + // Mall + /** * Returns whether the player currently has at least one purchasable * free mall package (`CurrencyType = 3`) for the mall entrance red dot. @@ -907,42 +916,8 @@ public class Player implements GameDatabaseObject { MallPackageState.newInstance().setNew(this.hasAvailableFreeMallPackage()) ); } - - public void queueBattlePassStateNotify() { - this.addNextPackage( - NetMsgId.battle_pass_state_notify, - this.buildBattlePassStateProto() - ); - } - - /** - * Pushes a full battle-pass snapshot for clients that only refresh their - * local battle-pass cache/red dots from the page-info payload. - */ - public void queueBattlePassInfoNotify() { - this.addNextPackage( - NetMsgId.battle_pass_info_succeed_ack, - this.getBattlePassManager().getBattlePass().toProto() - ); - } - - /** - * Battle-pass unlock is a special case: clients need the full page snapshot - * to initialize local battle-pass cache and refresh the main-entry red dot. - */ - private void queueBattlePassUnlockNotify(int oldLevel) { - if (oldLevel < GameConstants.BATTLE_PASS_UNLOCK_LEVEL && this.isBattlePassUnlocked()) { - this.queueBattlePassInfoNotify(); - } - } - - private Public.BattlePassState buildBattlePassStateProto() { - int state = this.isBattlePassUnlocked() - ? this.getBattlePassManager().getBattlePass().getClientState() - : 0; - - return Public.BattlePassState.newInstance().setState(state); - } + + // Monthly Card /** * Returns the number of future game days still covered after the current game day. @@ -1089,10 +1064,6 @@ public class Player implements GameDatabaseObject { this.grantMonthlyCardReward(entry.getKey(), notifyOnly); } } - - private long getLastResetDayTimeSeconds() { - return Utils.getResetTimeSecondsByEpochDay(this.getLastEpochDay()); - } // Next packages @@ -1263,7 +1234,7 @@ public class Player implements GameDatabaseObject { // Keep BattlePass state container present for client-side login cache flow. // Some clients assume State.BattlePass exists during player_data bootstrap. - state.setBattlePass(this.buildBattlePassStateProto()); + state.setBattlePass(this.getBattlePassManager().getStateProto()); state.getMutableAchievement() .setNew(this.getAchievementManager().hasNewAchievements()); @@ -1415,8 +1386,4 @@ public class Player implements GameDatabaseObject { return proto; } - public boolean isBattlePassUnlocked() { - return this.getLevel() >= GameConstants.BATTLE_PASS_UNLOCK_LEVEL; - } - } diff --git a/src/main/java/emu/nebula/server/handlers/HandlerBattlePassLevelBuyReq.java b/src/main/java/emu/nebula/server/handlers/HandlerBattlePassLevelBuyReq.java index a0e6817..7c8aadd 100644 --- a/src/main/java/emu/nebula/server/handlers/HandlerBattlePassLevelBuyReq.java +++ b/src/main/java/emu/nebula/server/handlers/HandlerBattlePassLevelBuyReq.java @@ -1,13 +1,10 @@ package emu.nebula.server.handlers; -import emu.nebula.data.GameData; -import emu.nebula.game.player.PlayerChangeInfo; import emu.nebula.net.GameSession; import emu.nebula.net.HandlerId; import emu.nebula.net.NetHandler; import emu.nebula.net.NetMsgId; import emu.nebula.proto.BattlePassLevelBuy.BattlePassLevelBuyResp; -import emu.nebula.proto.Public.ChangeInfo; import emu.nebula.proto.Public.UI32; @HandlerId(NetMsgId.battle_pass_level_buy_req) @@ -15,85 +12,31 @@ public class HandlerBattlePassLevelBuyReq extends NetHandler { @Override public byte[] handle(GameSession session, byte[] message) throws Exception { - if (!session.getPlayer().isBattlePassUnlocked()) { + // Sanity check to make sure battlepass is unlocked + if (!session.getPlayer().getBattlePassManager().isUnlocked()) { return session.encodeMsg(NetMsgId.battle_pass_level_buy_failed_ack); } - // Parse request using UI32 (standard uint32 protobuf type) + // Parse request var req = UI32.parseFrom(message); - int levelsToBuy = req.getValue(); - if (levelsToBuy < 1) { - levelsToBuy = 1; - } - - // Get battle pass + // Get battlepass var battlePass = session.getPlayer().getBattlePassManager().getBattlePass(); - // Calculate total cost and total exp - int currentLevel = battlePass.getLevel(); - int totalGemCost = 0; - int totalExpToAdd = 0; - int affordableLevels = 0; - int costItemId = 2; // Gems + // Buy levels + var change = battlePass.buyLevels(req.getValue()); - for (int i = 1; i <= levelsToBuy; i++) { - int targetLevel = currentLevel + i; - var levelData = GameData.getBattlePassLevelDataTable().get(targetLevel); - - if (levelData == null) { - // Max level reached - break; - } - - int cost = levelData.getQty(); - int currentGems = session.getPlayer().getInventory().getResourceCount(costItemId); - - if (currentGems < totalGemCost + cost) { - // Not enough gems for all levels - break; - } - - totalGemCost += cost; - totalExpToAdd += levelData.getExp(); - affordableLevels++; - } - - if (affordableLevels == 0) { + // Check + if (change == null) { return session.encodeMsg(NetMsgId.battle_pass_level_buy_failed_ack); } - // Prepare change info - var change = new PlayerChangeInfo(); - - // Deduct gems (total cost) - if (totalGemCost > 0) { - var gemCost = session.getPlayer().getInventory().addItem(costItemId, -totalGemCost); - if (gemCost != null) { - change.add(gemCost); - } - } - - // Add all exp at once (preserves current exp) - battlePass.addExp(totalExpToAdd); - - // Save to database - battlePass.save(); - // Build response var rsp = BattlePassLevelBuyResp.newInstance() - .setLevel(battlePass.getLevel()); - - // Add change info (gem cost) - if (!change.isEmpty()) { - var changeProto = ChangeInfo.newInstance(); - for (var any : change.getList()) { - changeProto.addProps(any); - } - rsp.setChange(changeProto); - } + .setLevel(battlePass.getLevel()) + .setChange(change.toProto()); - session.getPlayer().queueBattlePassStateNotify(); + session.getPlayer().getBattlePassManager().updateState(); // Encode and send return session.encodeMsg(NetMsgId.battle_pass_level_buy_succeed_ack, rsp); diff --git a/src/main/java/emu/nebula/server/handlers/HandlerBattlePassOrderCollectReq.java b/src/main/java/emu/nebula/server/handlers/HandlerBattlePassOrderCollectReq.java index 19ead7a..02281a2 100644 --- a/src/main/java/emu/nebula/server/handlers/HandlerBattlePassOrderCollectReq.java +++ b/src/main/java/emu/nebula/server/handlers/HandlerBattlePassOrderCollectReq.java @@ -1,6 +1,7 @@ package emu.nebula.server.handlers; import emu.nebula.Nebula; +import emu.nebula.game.mall.MallOrderCollectResult; import emu.nebula.net.NetHandler; import emu.nebula.net.NetMsgId; import emu.nebula.proto.BattlePassOrderCollect.BattlePassOrderCollectResp; @@ -13,7 +14,8 @@ public class HandlerBattlePassOrderCollectReq extends NetHandler { @Override public byte[] handle(GameSession session, byte[] message) throws Exception { - if (!session.getPlayer().isBattlePassUnlocked()) { + // Sanity check to make sure battlepass is unlocked + if (!session.getPlayer().getBattlePassManager().isUnlocked()) { return session.encodeMsg(NetMsgId.battle_pass_order_collect_failed_ack); } @@ -26,7 +28,7 @@ public class HandlerBattlePassOrderCollectReq extends NetHandler { if (!battlePass.isPremium()) { return session.encodeMsg(NetMsgId.battle_pass_order_collect_failed_ack); } - collectResult = emu.nebula.game.mall.MallOrderCollectResult.empty(); + collectResult = MallOrderCollectResult.empty(); } // Build collect response diff --git a/src/main/java/emu/nebula/server/handlers/HandlerBattlePassOrderReq.java b/src/main/java/emu/nebula/server/handlers/HandlerBattlePassOrderReq.java index 8faabf3..0b40033 100644 --- a/src/main/java/emu/nebula/server/handlers/HandlerBattlePassOrderReq.java +++ b/src/main/java/emu/nebula/server/handlers/HandlerBattlePassOrderReq.java @@ -15,9 +15,10 @@ public class HandlerBattlePassOrderReq extends NetHandler { public byte[] handle(GameSession session, byte[] message) throws Exception { // Parse request var req = BattlePassOrderReq.parseFrom(message); + // Validate mode (1 = Premium, 2 = Luxury) int requestedMode = req.getMode(); - if (!session.getPlayer().isBattlePassUnlocked() || requestedMode < 1 || requestedMode > 2) { + if (!session.getPlayer().getBattlePassManager().isUnlocked() || requestedMode < 1 || requestedMode > 2) { return session.encodeMsg(NetMsgId.battle_pass_order_failed_ack); } diff --git a/src/main/java/emu/nebula/server/handlers/HandlerBattlePassQuestRewardReceiveReq.java b/src/main/java/emu/nebula/server/handlers/HandlerBattlePassQuestRewardReceiveReq.java index 7a4b27d..7c0063c 100644 --- a/src/main/java/emu/nebula/server/handlers/HandlerBattlePassQuestRewardReceiveReq.java +++ b/src/main/java/emu/nebula/server/handlers/HandlerBattlePassQuestRewardReceiveReq.java @@ -12,7 +12,7 @@ public class HandlerBattlePassQuestRewardReceiveReq extends NetHandler { @Override public byte[] handle(GameSession session, byte[] message) throws Exception { - if (!session.getPlayer().isBattlePassUnlocked()) { + if (!session.getPlayer().getBattlePassManager().isUnlocked()) { return session.encodeMsg(NetMsgId.battle_pass_quest_reward_receive_failed_ack); } @@ -32,7 +32,7 @@ public class HandlerBattlePassQuestRewardReceiveReq extends NetHandler { .setExp(battlePass.getExp()) .setExpThisWeek(battlePass.getExpWeek()); - session.getPlayer().queueBattlePassStateNotify(); + session.getPlayer().getBattlePassManager().updateState(); // Encode and send return session.encodeMsg(NetMsgId.battle_pass_quest_reward_receive_succeed_ack, rsp); diff --git a/src/main/java/emu/nebula/server/handlers/HandlerBattlePassRewardReceiveReq.java b/src/main/java/emu/nebula/server/handlers/HandlerBattlePassRewardReceiveReq.java index 1062beb..c2abfe1 100644 --- a/src/main/java/emu/nebula/server/handlers/HandlerBattlePassRewardReceiveReq.java +++ b/src/main/java/emu/nebula/server/handlers/HandlerBattlePassRewardReceiveReq.java @@ -13,7 +13,7 @@ public class HandlerBattlePassRewardReceiveReq extends NetHandler { @Override public byte[] handle(GameSession session, byte[] message) throws Exception { - if (!session.getPlayer().isBattlePassUnlocked()) { + if (!session.getPlayer().getBattlePassManager().isUnlocked()) { return session.encodeMsg(NetMsgId.battle_pass_reward_receive_failed_ack); } @@ -44,7 +44,7 @@ public class HandlerBattlePassRewardReceiveReq extends NetHandler { .setPremiumReward(battlePass.getPremiumReward().toByteArray()) .setChange(change.toProto()); - session.getPlayer().queueBattlePassStateNotify(); + session.getPlayer().getBattlePassManager().updateState(); // Encode and send return session.encodeMsg(NetMsgId.battle_pass_reward_receive_succeed_ack, rsp);