Fix mall, shop, mall/shop refresh logic and battle pass flows

This commit is contained in:
Yostarcc authored and Melledy committed 2026-05-25 18:43:02 -07:00
1 parent c1c09c6594
commit 57c05c8e32
45 files changed
+2171 -232

No files matched your search

@@ -0,0 +1,102 @@
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)
public class HandlerBattlePassLevelBuyReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
if (!session.getPlayer().isBattlePassUnlocked()) {
return session.encodeMsg(NetMsgId.battle_pass_level_buy_failed_ack);
}
// Parse request using UI32 (standard uint32 protobuf type)
var req = UI32.parseFrom(message);
int levelsToBuy = req.getValue();
if (levelsToBuy < 1) {
levelsToBuy = 1;
}
// Get battle pass
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
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) {
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);
}
session.getPlayer().queueBattlePassStateNotify();
// Encode and send
return session.encodeMsg(NetMsgId.battle_pass_level_buy_succeed_ack, rsp);
}
}
@@ -0,0 +1,55 @@
package emu.nebula.server.handlers;
import emu.nebula.Nebula;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.BattlePassOrderCollect.BattlePassOrderCollectResp;
import emu.nebula.proto.Public.CollectResp;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.battle_pass_order_collect_req)
public class HandlerBattlePassOrderCollectReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
if (!session.getPlayer().isBattlePassUnlocked()) {
return session.encodeMsg(NetMsgId.battle_pass_order_collect_failed_ack);
}
// Get battle pass
var battlePass = session.getPlayer().getBattlePassManager().getBattlePass();
var payModule = Nebula.getGameContext().getPayModule();
var collectResult = payModule.consumePendingCollect(session);
if (collectResult == null) {
if (!battlePass.isPremium()) {
return session.encodeMsg(NetMsgId.battle_pass_order_collect_failed_ack);
}
collectResult = emu.nebula.game.mall.MallOrderCollectResult.empty();
}
// Build collect response
var collectResp = CollectResp.newInstance()
.setStatusValue(1); // Success
if (collectResult.hasDisplayChange()) {
collectResp.setItems(collectResult.getDisplayChange());
}
if (collectResult.hasStateChange()) {
payModule.pushInventoryNotifies(session, collectResult.getStateChange());
}
// Build response
var rsp = BattlePassOrderCollectResp.newInstance()
.setMode(battlePass.getMode())
.setLevel(battlePass.getLevel())
.setVersion(battlePass.getBattlePassId())
.setCollectResp(collectResp);
// Encode and send
return session.encodeMsg(NetMsgId.battle_pass_order_collect_succeed_ack, rsp);
}
}
@@ -0,0 +1,47 @@
package emu.nebula.server.handlers;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.net.GameSession;
import emu.nebula.net.HandlerId;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.BattlePassOrder.BattlePassOrderReq;
import emu.nebula.proto.MallGemOrder.OrderInfo;
@HandlerId(NetMsgId.battle_pass_order_req)
public class HandlerBattlePassOrderReq extends NetHandler {
@Override
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) {
return session.encodeMsg(NetMsgId.battle_pass_order_failed_ack);
}
// Get battle pass
var battlePass = session.getPlayer().getBattlePassManager().getBattlePass();
// Allow: 0->1 (free to premium), 0->2 (free to luxury), 1->2 (premium to luxury)
int currentMode = battlePass.getMode();
if (requestedMode <= currentMode) {
return session.encodeMsg(NetMsgId.battle_pass_order_failed_ack);
}
var data = GameData.getBattlePassDataTable().get(battlePass.getBattlePassId());
if (data == null) {
return session.encodeMsg(NetMsgId.battle_pass_order_failed_ack);
}
// Follow the same fake-payment lifecycle as mall orders:
// order ack -> paid notify -> order collect -> rewards popup.
return session.encodeMsg(
NetMsgId.battle_pass_order_succeed_ack,
Nebula.getGameContext().getPayModule().createBattlePassOrder(session, data, requestedMode)
);
}
}
@@ -12,6 +12,10 @@ public class HandlerBattlePassQuestRewardReceiveReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
if (!session.getPlayer().isBattlePassUnlocked()) {
return session.encodeMsg(NetMsgId.battle_pass_quest_reward_receive_failed_ack);
}
// Parse req
var req = UI32.parseFrom(message);
@@ -27,6 +31,8 @@ public class HandlerBattlePassQuestRewardReceiveReq extends NetHandler {
.setLevel(battlePass.getLevel())
.setExp(battlePass.getExp())
.setExpThisWeek(battlePass.getExpWeek());
session.getPlayer().queueBattlePassStateNotify();
// Encode and send
return session.encodeMsg(NetMsgId.battle_pass_quest_reward_receive_succeed_ack, rsp);
@@ -13,6 +13,10 @@ public class HandlerBattlePassRewardReceiveReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
if (!session.getPlayer().isBattlePassUnlocked()) {
return session.encodeMsg(NetMsgId.battle_pass_reward_receive_failed_ack);
}
// Parse request
var req = BattlePassRewardReceiveReq.parseFrom(message);
@@ -39,6 +43,8 @@ public class HandlerBattlePassRewardReceiveReq extends NetHandler {
.setBasicReward(battlePass.getBasicReward().toByteArray())
.setPremiumReward(battlePass.getPremiumReward().toByteArray())
.setChange(change.toProto());
session.getPlayer().queueBattlePassStateNotify();
// Encode and send
return session.encodeMsg(NetMsgId.battle_pass_reward_receive_succeed_ack, rsp);
@@ -13,9 +13,12 @@ public class HandlerGemConvertReq extends NetHandler {
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request
var req = UI32.parseFrom(message);
if (req.getValue() <= 0) {
return session.encodeMsg(NetMsgId.gem_convert_failed_ack);
}
// Convert gems
var change = session.getPlayer().getInventory().convertGems(req.getValue());
var change = session.getPlayer().getInventory().convertStellaniteLuminaToDust(req.getValue());
if (change == null) {
return session.encodeMsg(NetMsgId.gem_convert_failed_ack);
@@ -2,7 +2,6 @@ package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.MallGemListOuterClass.GemInfo;
import emu.nebula.proto.MallGemListOuterClass.MallGemList;
import emu.nebula.net.HandlerId;
import emu.nebula.data.GameData;
@@ -13,16 +12,13 @@ public class HandlerMallGemListReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
var player = session.getPlayer();
var rsp = MallGemList.newInstance();
for (var data : GameData.getMallGemDataTable()) {
var info = GemInfo.newInstance()
.setId(data.getIdString())
.setMaiden(true);
rsp.addList(info);
rsp.addList(data.toInfo(player));
}
return session.encodeMsg(NetMsgId.mall_gem_list_succeed_ack, rsp);
}
@@ -0,0 +1,30 @@
package emu.nebula.server.handlers;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.net.GameSession;
import emu.nebula.net.HandlerId;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.MallGemOrder.OrderInfo;
@HandlerId(NetMsgId.mall_gem_order_req)
public class HandlerMallGemOrderReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
var req = OrderInfo.parseFrom(message);
var data = GameData.getMallGemDataTable().get(req.getId().hashCode());
if (data == null) {
return session.encodeMsg(NetMsgId.mall_gem_order_failed_ack);
}
var order = Nebula.getGameContext().getPayModule().createGemOrder(session, data);
if (order == null) {
return session.encodeMsg(NetMsgId.mall_gem_order_failed_ack);
}
return session.encodeMsg(NetMsgId.mall_gem_order_succeed_ack, order);
}
}
@@ -3,7 +3,6 @@ package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.MallMonthlycardList.MallMonthlyCardList;
import emu.nebula.proto.MallMonthlycardList.MonthlyCardInfo;
import emu.nebula.net.HandlerId;
import emu.nebula.data.GameData;
import emu.nebula.net.GameSession;
@@ -13,16 +12,13 @@ public class HandlerMallMonthlyCardListReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
var player = session.getPlayer();
var rsp = MallMonthlyCardList.newInstance();
for (var data : GameData.getMallMonthlyCardDataTable()) {
var info = MonthlyCardInfo.newInstance()
.setId(data.getIdString())
.setRemaining(9);
rsp.addList(info);
rsp.addList(data.toInfo(player));
}
return session.encodeMsg(NetMsgId.mall_monthlyCard_list_succeed_ack, rsp);
}
@@ -0,0 +1,26 @@
package emu.nebula.server.handlers;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
import emu.nebula.proto.MallMonthlycardList.MonthlyCardInfo;
@HandlerId(NetMsgId.mall_monthlyCard_order_req)
public class HandlerMallMonthlyCardOrderReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
var req = MonthlyCardInfo.parseFrom(message);
var data = GameData.getMallMonthlyCardDataTable().get(req.getId().hashCode());
var order = Nebula.getGameContext().getPayModule().createMonthlyCardOrder(session, data);
if (order == null) {
return session.encodeMsg(NetMsgId.mall_monthlyCard_order_failed_ack);
}
return session.encodeMsg(NetMsgId.mall_monthlyCard_order_succeed_ack, order);
}
}
@@ -0,0 +1,19 @@
package emu.nebula.server.handlers;
import emu.nebula.Nebula;
import emu.nebula.net.GameSession;
import emu.nebula.net.HandlerId;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
@HandlerId(NetMsgId.mall_order_cancel_req)
public class HandlerMallOrderCancelReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
var payModule = Nebula.getGameContext().getPayModule();
payModule.clearPendingCollect(session.getPlayer().getUid());
return session.encodeMsg(NetMsgId.mall_order_cancel_succeed_ack);
}
}
@@ -0,0 +1,30 @@
package emu.nebula.server.handlers;
import emu.nebula.Nebula;
import emu.nebula.net.GameSession;
import emu.nebula.net.HandlerId;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.Public.CollectResp;
@HandlerId(NetMsgId.mall_order_collect_req)
public class HandlerMallOrderCollectReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
var rsp = CollectResp.newInstance().setStatusValue(1);
var payModule = Nebula.getGameContext().getPayModule();
var collectResult = payModule.consumePendingCollect(session);
if (collectResult != null && collectResult.hasDisplayChange()) {
rsp.setItems(collectResult.getDisplayChange());
}
if (collectResult != null && collectResult.hasStateChange()) {
payModule.pushInventoryNotifies(session, collectResult.getStateChange());
}
return session.encodeMsg(NetMsgId.mall_order_collect_succeed_ack, rsp);
}
}
@@ -2,8 +2,8 @@ package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.MallPackageListOuterClass;
import emu.nebula.proto.MallPackageListOuterClass.MallPackageList;
import emu.nebula.proto.MallPackageListOuterClass.PackageInfo;
import emu.nebula.net.HandlerId;
import emu.nebula.data.GameData;
import emu.nebula.net.GameSession;
@@ -13,16 +13,23 @@ public class HandlerMallPackageListReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
var player = session.getPlayer();
var rsp = MallPackageList.newInstance();
player.queueMallPackageStateNotify();
for (var data : GameData.getMallPackageDataTable()) {
var info = PackageInfo.newInstance()
if (!data.isVisible(player)) {
continue;
}
var info = MallPackageListOuterClass.PackageInfo.newInstance()
.setId(data.getIdString())
.setStock(data.getStock());
.setStock(data.getStock(player))
.setRefreshTime(data.getNextRefreshTime());
rsp.addList(info);
}
return session.encodeMsg(NetMsgId.mall_package_list_succeed_ack, rsp);
}
@@ -0,0 +1,55 @@
package emu.nebula.server.handlers;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
import emu.nebula.proto.MallGemOrder.OrderInfo;
import emu.nebula.proto.MallPackageOrderOuterClass.MallPackageOrder;
@HandlerId(NetMsgId.mall_package_order_req)
public class HandlerMallPackageOrderReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
var req = OrderInfo.parseFrom(message);
var data = GameData.getMallPackageDataTable().get(req.getId().hashCode());
if (data == null) {
return session.encodeMsg(NetMsgId.mall_package_order_failed_ack);
}
if (data.isCashPackage()) {
var order = Nebula.getGameContext().getPayModule().createPackageOrder(session, data);
if (order == null) {
return session.encodeMsg(NetMsgId.mall_package_order_failed_ack);
}
var rsp = MallPackageOrder.newInstance().setOrder(order);
if (order.hasNextPackage()) {
rsp.setNextPackage(order.getNextPackage().toArray());
}
return session.encodeMsg(
NetMsgId.mall_package_order_succeed_ack,
rsp
);
}
var change = session.getPlayer().getInventory().buyMallPackage(data);
if (change == null) {
return session.encodeMsg(NetMsgId.mall_package_order_failed_ack);
}
if (data.isFreePackage()) {
session.getPlayer().queueMallPackageStateNotify();
}
return session.encodeMsg(
NetMsgId.mall_package_order_succeed_ack,
MallPackageOrder.newInstance().setChange(change.toProto())
);
}
}
@@ -1,14 +1,11 @@
package emu.nebula.server.handlers;
import emu.nebula.GameConstants;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.MallShopList;
import emu.nebula.proto.MallShopList.MallShopProductList;
import emu.nebula.proto.MallShopList.ProductInfo;
import emu.nebula.net.HandlerId;
import java.util.concurrent.TimeUnit;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.net.GameSession;
@@ -18,22 +15,20 @@ public class HandlerMallShopListReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
var rsp = MallShopProductList.newInstance();
long refreshTime = Nebula.getCurrentServerTime() + TimeUnit.DAYS.toSeconds(30);
for (var data : GameData.getMallShopDataTable()) {
if (data.getStock() <= 0) {
if (!data.isVisible()) {
continue;
}
var info = ProductInfo.newInstance()
var info = MallShopList.ProductInfo.newInstance()
.setId(data.getIdString())
.setStock(data.getStock(session.getPlayer()))
.setRefreshTime(refreshTime);
.setStock(data.getStock() > 0 ? data.getStock(session.getPlayer()) : GameConstants.UNLIMITED_STOCK)
.setRefreshTime(data.getNextRefreshTime());
rsp.addList(info);
}
return session.encodeMsg(NetMsgId.mall_shop_list_succeed_ack, rsp);
}
@@ -18,6 +18,8 @@ public class HandlerPlayerDataReq extends NetHandler {
if (session.getPlayer() == null) {
return session.encodeMsg(NetMsgId.player_new_notify);
}
session.getPlayer().queueMallPackageStateNotify();
// Encode player data
return session.encodeMsg(NetMsgId.player_data_succeed_ack, session.getPlayer().toProto());
@@ -4,8 +4,11 @@ import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.Public.BoughtGoods;
import emu.nebula.proto.Public.ResidentShop;
import emu.nebula.proto.ResidentShopGet.ResidentShopGetReq;
import emu.nebula.proto.ResidentShopGet.ResidentShopGetResp;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import emu.nebula.net.HandlerId;
import emu.nebula.data.GameData;
import emu.nebula.net.GameSession;
@@ -15,19 +18,38 @@ public class HandlerResidentShopGetReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
var req = ResidentShopGetReq.parseFrom(message);
var requestedShopIds = req.getShopIds();
IntSet requestedShopIdSet = null;
if (requestedShopIds.length() > 0) {
requestedShopIdSet = new IntOpenHashSet(requestedShopIds.length());
for (int shopId : requestedShopIds) {
requestedShopIdSet.add(shopId);
}
}
// Get shops
var shops = new Int2ObjectOpenHashMap<ResidentShop>();
for (var data : GameData.getResidentShopDataTable()) {
if (requestedShopIdSet != null && !requestedShopIdSet.contains(data.getId()) || !data.isVisible()) {
continue;
}
var proto = ResidentShop.newInstance()
.setId(data.getId())
.setRefreshTime(Long.MAX_VALUE);
.setRefreshTime(data.getNextRefreshTime());
shops.put(data.getId(), proto);
}
// Add bought goods
for (var data : GameData.getResidentGoodsDataTable()) {
if (!data.isVisible(session.getPlayer())) {
continue;
}
int bought = session.getPlayer().getInventory().getShopBuyCount().get(data.getId());
if (bought == 0) {
continue;
@@ -18,7 +18,7 @@ public class HandlerResidentShopPurchaseReq extends NetHandler {
// Get goods
var data = GameData.getResidentGoodsDataTable().get(req.getGoodsId());
if (data == null) {
if (data == null || !data.isVisible(session.getPlayer())) {
return session.encodeMsg(NetMsgId.resident_shop_purchase_failed_ack);
}
@@ -33,10 +33,15 @@ public class HandlerResidentShopPurchaseReq extends NetHandler {
var rsp = ResidentShopPurchaseResp.newInstance()
.setChange(change.toProto())
.setPurchasedNumber(req.getNumber());
var shopData = GameData.getResidentShopDataTable().get(data.getShopId());
if (shopData == null || !shopData.isVisible()) {
return session.encodeMsg(NetMsgId.resident_shop_purchase_failed_ack);
}
rsp.getMutableShop()
.setId(data.getShopId())
.setRefreshTime(Long.MAX_VALUE);
.setRefreshTime(shopData.getNextRefreshTime());
// Encode and send
return session.encodeMsg(NetMsgId.resident_shop_purchase_succeed_ack, rsp);