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

This commit is contained in:
Yostarcc
2026-05-25 18:43:02 -07:00
committed by Melledy
parent c1c09c6594
commit 57c05c8e32
45 changed files with 2171 additions and 232 deletions
+7 -4
View File
@@ -121,20 +121,23 @@ public class Config {
@Getter @Getter
public static class ServerOptions { public static class ServerOptions {
// Default permissions for accounts. By default, all commands are allowed. Reccomended to change if making a public server. // Default permissions for accounts. By default, all commands are allowed. Recommended to change if making a public server.
public Set<String> defaultPermissions = Set.of("*"); public Set<String> defaultPermissions = Set.of("*");
// Automatically creates an account when a player logs in for the first time on a new email. // Automatically creates an account when a player logs in for the first time on a new email.
public boolean autoCreateAccount = true; public boolean autoCreateAccount = true;
// Skips the intro cinematics/stage when starting a new account. // Skips the intro cinematics/stage when starting a new account.
public boolean skipIntro = false; public boolean skipIntro = false;
// Unlocks all instances (Monolith, Bounty Trials, etc) for players to enter without needing to do the previous levels. // Unlocks all instances (Monolith, Bounty Trials, etc.) for players to enter without needing to do the previous levels.
public boolean unlockInstances = true; public boolean unlockInstances = true;
// Unlocks all story CGs to use in the showcase // Unlocks all story CGs to use in the showcase
public boolean unlockAllStoryCGs = false; public boolean unlockAllStoryCGs = false;
// Unlocks all mall skins without sale window restrictions
public boolean showAllSkinInMall = true;
// How long to wait (in seconds) after the last http request from a session before removing it from the server. // How long to wait (in seconds) after the last http request from a session before removing it from the server.
public int sessionTimeout = 300; public int sessionTimeout = 300;
// The offset hour for when daily quests are refreshed in UTC. Example: "dailyResetHour = 4" means dailies will be refreshed at UTC+4 12:00 AM every day. // The local server hour when daily, weekly, and monthly reset boundaries occur.
public int dailyResetHour = 0; // Example: "dailyResetHour = 4" means all reset boundaries happen at 04:00 in the server's system time zone.
public int dailyResetHour = 4;
// Leaderboard for Boss Blitz refresh time in seconds. // Leaderboard for Boss Blitz refresh time in seconds.
public int leaderboardRefreshTime = 60; public int leaderboardRefreshTime = 60;
// The welcome mail to send when a player is created. Set to null to disable. // The welcome mail to send when a player is created. Set to null to disable.
+21 -5
View File
@@ -18,9 +18,10 @@ public class GameConstants {
public static final int DEFAULT_HONOR_ID = 111001; public static final int DEFAULT_HONOR_ID = 111001;
public static final int GOLD_ITEM_ID = 1; public static final int GOLD_ITEM_ID = 1;
public static final int GEM_ITEM_ID = 2; public static final int STELLANITE_DUST_ITEM_ID = 2;
public static final int PREM_GEM_ITEM_ID = 3; public static final int PAID_STELLANITE_LUMINA_ITEM_ID = 3;
public static final int ENERGY_BUY_ITEM_ID = GEM_ITEM_ID; public static final int FREE_STELLANITE_LUMINA_ITEM_ID = 4;
public static final int ENERGY_BUY_ITEM_ID = STELLANITE_DUST_ITEM_ID;
public static final int EXP_ITEM_ID = 21; public static final int EXP_ITEM_ID = 21;
public static final int WEEKLY_ENTRY_ITEM_ID = 28; public static final int WEEKLY_ENTRY_ITEM_ID = 28;
public static final int JOINT_DRILL_TICKET_ID = 36; public static final int JOINT_DRILL_TICKET_ID = 36;
@@ -53,12 +54,26 @@ public class GameConstants {
public static final int[] TOWER_EVENTS_IDS = new int[] { public static final int[] TOWER_EVENTS_IDS = new int[] {
101, 102, 104, 105, 106, 107, 108, 114, 115, 116, 126, 127, 128 101, 102, 104, 105, 106, 107, 108, 114, 115, 116, 126, 127, 128
}; };
public static final int REFRESH_TYPE_DAILY = 1;
public static final int REFRESH_TYPE_WEEKLY = 2;
public static final int REFRESH_TYPE_MONTHLY = 3;
public static final int CURRENCY_TYPE_CASH = 1;
// Stellanite Lumina
public static final int CURRENCY_TYPE_ITEM = 2;
public static final int CURRENCY_TYPE_FREE = 3;
public static final int TAG_SKIN = 2;
public static final int BATTLE_PASS_UNLOCK_LEVEL = 3;
public static int[][] VAMPIRE_SURVIVOR_BONUS_POWER = new int[][] { public static int[][] VAMPIRE_SURVIVOR_BONUS_POWER = new int[][] {
new int[] {100, 120}, new int[] {100, 120},
new int[] {200, 150}, new int[] {200, 150},
new int[] {300, 200} new int[] {300, 200}
}; };
public static final int UNLIMITED_STOCK = Integer.MAX_VALUE;
// Daily gifts (Custom) // Daily gifts (Custom)
@@ -70,13 +85,13 @@ public class GameConstants {
DAILY_SHOP_GIFTS.add(250, new ItemParam(GOLD_ITEM_ID, 18888)); DAILY_SHOP_GIFTS.add(250, new ItemParam(GOLD_ITEM_ID, 18888));
DAILY_SHOP_GIFTS.add(100, new ItemParam(GOLD_ITEM_ID, 28888)); DAILY_SHOP_GIFTS.add(100, new ItemParam(GOLD_ITEM_ID, 28888));
DAILY_SHOP_GIFTS.add(250, new ItemParam(33001, 10)); DAILY_SHOP_GIFTS.add(250, new ItemParam(33001, 10));
DAILY_SHOP_GIFTS.add(10, new ItemParam(GEM_ITEM_ID, 50)); // Custom DAILY_SHOP_GIFTS.add(10, new ItemParam(STELLANITE_DUST_ITEM_ID, 50)); // Custom
DAILY_MALL_GIFTS.add(100, ItemParamMap.of(GOLD_ITEM_ID, 25_000, 82007, 5)); DAILY_MALL_GIFTS.add(100, ItemParamMap.of(GOLD_ITEM_ID, 25_000, 82007, 5));
} }
// Helper functions // Helper functions
public static String getGameVersion() { public static String getGameVersion() {
// Load data version // Load data version
var region = RegionConfig.getRegion(Nebula.getConfig().getRegion()); var region = RegionConfig.getRegion(Nebula.getConfig().getRegion());
@@ -91,4 +106,5 @@ public class GameConstants {
public static int getDataVersion() { public static int getDataVersion() {
return Nebula.getConfig().getCustomDataVersion() > 0 ? Nebula.getConfig().getCustomDataVersion() : DATA_VERSION ; return Nebula.getConfig().getCustomDataVersion() > 0 ? Nebula.getConfig().getCustomDataVersion() : DATA_VERSION ;
} }
} }
+2 -1
View File
@@ -57,6 +57,7 @@ public class GameData {
@Getter private static DataTable<MallPackageDef> MallPackageDataTable = new DataTable<>(); @Getter private static DataTable<MallPackageDef> MallPackageDataTable = new DataTable<>();
@Getter private static DataTable<MallShopDef> MallShopDataTable = new DataTable<>(); @Getter private static DataTable<MallShopDef> MallShopDataTable = new DataTable<>();
@Getter private static DataTable<MallGemDef> MallGemDataTable = new DataTable<>(); @Getter private static DataTable<MallGemDef> MallGemDataTable = new DataTable<>();
@Getter private static DataTable<MonthlyCardRewardDef> MonthlyCardRewardDataTable = new DataTable<>();
@Getter private static DataTable<ResidentShopDef> ResidentShopDataTable = new DataTable<>(); @Getter private static DataTable<ResidentShopDef> ResidentShopDataTable = new DataTable<>();
@Getter private static DataTable<ResidentGoodsDef> ResidentGoodsDataTable = new DataTable<>(); @Getter private static DataTable<ResidentGoodsDef> ResidentGoodsDataTable = new DataTable<>();
@@ -171,4 +172,4 @@ public class GameData {
@Getter private static DataTable<ActivityShopDef> ActivityShopDataTable = new DataTable<>(); @Getter private static DataTable<ActivityShopDef> ActivityShopDataTable = new DataTable<>();
@Getter private static DataTable<ActivityShopControlDef> ActivityShopControlDataTable = new DataTable<>(); @Getter private static DataTable<ActivityShopControlDef> ActivityShopControlDataTable = new DataTable<>();
@Getter private static DataTable<ActivityGoodsDef> ActivityGoodsDataTable = new DataTable<>(); @Getter private static DataTable<ActivityGoodsDef> ActivityGoodsDataTable = new DataTable<>();
} }
@@ -4,13 +4,65 @@ import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType; import emu.nebula.data.ResourceType;
import lombok.Getter; import lombok.Getter;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
@Getter @Getter
@ResourceType(name = "BattlePass.json") @ResourceType(name = "BattlePass.json")
public class BattlePassDef extends BaseDef { public class BattlePassDef extends BaseDef {
private int ID; private int ID;
private String Name;
private String StartTime;
private String EndTime;
private String LuxuryProductId;
private int LuxuryPrice;
private String LuxuryShowPrice;
private int LuxuryBonusLevel;
private int LuxuryTid;
private int LuxuryQty;
private String PremiumProductId;
private int PremiumPrice;
private String PremiumShowPrice;
private String ComplementaryProductId;
private int ComplementaryPrice;
private String ComplementaryShowPrice;
private int ComplementaryTid;
private int ComplementaryQty;
private String OriginShowPrice;
private String CoverColor;
private int Cover;
private List<Integer> PremiumShowItems;
private List<Integer> LuxuryShowItems;
private int OutfitPackageShowItem;
private transient long endTimeTimestamp;
private transient long startTimeTimestamp;
@Override @Override
public int getId() { public int getId() {
return ID; return ID;
} }
@Override
public void onLoad() {
// Parse start time to timestamp
try {
var formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
var zonedDateTime = ZonedDateTime.parse(StartTime, formatter);
this.startTimeTimestamp = zonedDateTime.toInstant().toEpochMilli() / 1000;
} catch (Exception e) {
this.startTimeTimestamp = 0;
}
// Parse end time to timestamp
try {
var formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
var zonedDateTime = ZonedDateTime.parse(EndTime, formatter);
this.endTimeTimestamp = zonedDateTime.toInstant().toEpochMilli() / 1000;
} catch (Exception e) {
this.endTimeTimestamp = Long.MAX_VALUE;
}
}
} }
@@ -17,9 +17,11 @@ public class BattlePassRewardDef extends BaseDef {
private int Qty2; private int Qty2;
private int Tid3; private int Tid3;
private int Qty3; private int Qty3;
private boolean Focus;
private transient ItemParamMap basicRewards; private transient ItemParamMap basicRewards;
private transient ItemParamMap premiumRewards; private transient ItemParamMap premiumRewards;
private transient ItemParamMap luxuryRewards;
@Override @Override
public int getId() { public int getId() {
@@ -30,16 +32,26 @@ public class BattlePassRewardDef extends BaseDef {
public void onLoad() { public void onLoad() {
this.basicRewards = new ItemParamMap(); this.basicRewards = new ItemParamMap();
this.premiumRewards = new ItemParamMap(); this.premiumRewards = new ItemParamMap();
this.luxuryRewards = new ItemParamMap();
// Basic rewards (Tid1) - for all players
if (this.Tid1 > 0) { if (this.Tid1 > 0) {
this.basicRewards.add(this.Tid1, this.Qty1); this.basicRewards.add(this.Tid1, this.Qty1);
} }
// Premium rewards (Tid2) - for both 58 and 98 yuan tiers
if (this.Tid2 > 0) { if (this.Tid2 > 0) {
this.premiumRewards.add(this.Tid2, this.Qty2); this.premiumRewards.add(this.Tid2, this.Qty2);
} }
// Luxury rewards (Tid3) - ONLY for 98 yuan tier
if (this.Tid3 > 0) { if (this.Tid3 > 0) {
this.premiumRewards.add(this.Tid3, this.Qty3); this.luxuryRewards.add(this.Tid3, this.Qty3);
} }
} }
public boolean hasLuxuryRewards() {
return this.luxuryRewards != null && !this.luxuryRewards.isEmpty();
}
} }
@@ -4,6 +4,9 @@ import com.google.gson.annotations.SerializedName;
import emu.nebula.data.BaseDef; import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType; import emu.nebula.data.ResourceType;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.Player;
import emu.nebula.proto.MallGemListOuterClass.GemInfo;
import lombok.Getter; import lombok.Getter;
@Getter @Getter
@@ -11,13 +14,51 @@ import lombok.Getter;
public class MallGemDef extends BaseDef { public class MallGemDef extends BaseDef {
@SerializedName("Id") @SerializedName("Id")
private String IdString; private String IdString;
private int Stock; private int BaseItemId;
private int ItemId; private int BaseItemQty;
private int CurrencyItemId; private int ExperiencedBonusItemId;
private int ItemQty; private int ExperiencedBonusItemQty;
private int MaidenBonusItemID;
private int MaidenBonusItemQty;
private int Price;
@Override @Override
public int getId() { public int getId() {
return IdString.hashCode(); return IdString.hashCode();
} }
/**
* Builds the actual delivery payload for the current player state.
*/
public ItemParamMap buildProducts(Player player) {
var products = new ItemParamMap();
if (BaseItemId > 0 && BaseItemQty > 0) {
products.add(BaseItemId, BaseItemQty);
}
if (this.hasMaidenBonus(player)) {
if (MaidenBonusItemID > 0 && MaidenBonusItemQty > 0) {
products.add(MaidenBonusItemID, MaidenBonusItemQty);
}
return products;
}
if (ExperiencedBonusItemId > 0 && ExperiencedBonusItemQty > 0) {
products.add(ExperiencedBonusItemId, ExperiencedBonusItemQty);
}
return products;
}
public boolean hasMaidenBonus(Player player) {
return player.getInventory().hasMallGemMaidenBonus(this.IdString);
}
public GemInfo toInfo(Player player) {
return GemInfo.newInstance()
.setId(this.getIdString())
.setMaiden(this.hasMaidenBonus(player));
}
} }
@@ -3,7 +3,11 @@ package emu.nebula.data.resources;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import emu.nebula.data.BaseDef; import emu.nebula.data.BaseDef;
import emu.nebula.data.GameData;
import emu.nebula.data.ResourceType; import emu.nebula.data.ResourceType;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.Player;
import emu.nebula.proto.MallMonthlycardList.MonthlyCardInfo;
import lombok.Getter; import lombok.Getter;
@Getter @Getter
@@ -15,9 +19,81 @@ public class MallMonthlyCardDef extends BaseDef {
private int Price; private int Price;
private int BaseItemId; private int BaseItemId;
private int BaseItemQty; private int BaseItemQty;
private int MaxDays;
private transient ItemParamMap products;
private transient ItemParamMap dailyRewards;
@Override @Override
public int getId() { public int getId() {
return IdString.hashCode(); return IdString.hashCode();
} }
/**
* Monthly card valid duration(days).
*/
public int getMonthlyCardDuration() {
return 30;
}
/**
* Monthly cards can be repurchased until the remaining duration exceeds the configured cap.
*/
public boolean canPurchase(Player player) {
return this.getRemainingDays(player) <= this.MaxDays;
}
/**
* Returns the player's current remaining duration for this monthly card.
*/
public int getRemainingDays(Player player) {
return player.getMonthlyCardRemainingDays(this.getIdString());
}
/**
* Returns whether today's monthly-card reward has already been claimed.
*/
public boolean hasReceivedRewardToday(Player player) {
return player.receivedMonthlyCardRewardToday(this.getIdString());
}
public ItemParamMap getProducts() {
if (products == null) {
products = new ItemParamMap();
// Add base items (initial purchase reward)
if (BaseItemId > 0 && BaseItemQty > 0) {
products.add(BaseItemId, BaseItemQty);
}
}
return products;
}
/**
* Returns the configured daily login rewards for this monthly card.
*/
public ItemParamMap getDailyRewards() {
if (this.dailyRewards == null) {
this.dailyRewards = new ItemParamMap();
for (var rewardData : GameData.getMonthlyCardRewardDataTable()) {
if (rewardData.getCardId() != this.MonthlyCardId) {
continue;
}
this.dailyRewards.add(rewardData.getRewards());
break;
}
}
return this.dailyRewards.clone();
}
public MonthlyCardInfo toInfo(Player player) {
return MonthlyCardInfo.newInstance()
.setId(this.getIdString())
.setRemaining(this.getRemainingDays(player))
.setReceived(this.hasReceivedRewardToday(player));
}
} }
@@ -1,32 +1,121 @@
package emu.nebula.data.resources; package emu.nebula.data.resources;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import emu.nebula.GameConstants;
import emu.nebula.data.BaseDef; import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType; import emu.nebula.data.ResourceType;
import emu.nebula.Nebula;
import emu.nebula.game.inventory.ItemParamMap; import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.Player;
import emu.nebula.util.JsonUtils;
import emu.nebula.util.ResetCycle;
import emu.nebula.util.Utils;
import lombok.Getter; import lombok.Getter;
@Getter @Getter
@ResourceType(name = "MallPackage.json") @ResourceType(name = "MallPackage.json")
public class MallPackageDef extends BaseDef { public class MallPackageDef extends BaseDef {
@SerializedName("Id") @SerializedName("Id")
private String IdString; private String IdString;
private int Stock; private int Stock;
private int CurrencyType; private int CurrencyType;
private int CurrencyItemId; private int CurrencyItemId;
private int CurrencyItemQty; private int CurrencyItemQty;
private int Tag;
private String Items; private String Items;
private String ListTime;
private String DeListTime;
private int RefreshType;
private int OrderCondType;
private String OrderCondParams;
private int ListCondType;
private String ListCondParams;
private transient ItemParamMap products; private transient ItemParamMap products;
private transient long listTimeSeconds;
private transient long delistTimeSeconds;
private transient int[] orderCondParams;
private transient int[] listCondParams;
@Override @Override
public int getId() { public int getId() {
return IdString.hashCode(); return IdString.hashCode();
} }
public int getStock(Player player) {
return Math.max(Stock - player.getInventory().getMallPackagePurchaseCount(this.IdString), 0);
}
/**
* Returns whether this package is a free mall package(CurrencyType = 3).
*/
public boolean isFreePackage() {
return this.CurrencyType == GameConstants.CURRENCY_TYPE_FREE;
}
/**
* Returns whether this package is a cash mall package(CurrencyType = 1).
*/
public boolean isCashPackage() {
return this.CurrencyType == GameConstants.CURRENCY_TYPE_CASH;
}
/**
* Returns whether this package is bought with Stellanite Lumina.
*/
public boolean isItemPackage() {
return this.CurrencyType == GameConstants.CURRENCY_TYPE_ITEM;
}
/**
* Checks whether the player can purchase this package.
*/
public boolean canPurchase(Player player) {
return this.isVisible(player)
&& this.getStock(player) > 0
&& ShopCondition.matches(player, this.OrderCondType, this.orderCondParams);
}
/**
* Returns whether this package should be included in the mall package list for the player.
*/
public boolean isVisible(Player player) {
long now = Nebula.getCurrentServerTime();
if (!this.shouldIgnoreSaleWindow()) {
if (this.listTimeSeconds > 0 && now < this.listTimeSeconds) {
return false;
}
if (this.delistTimeSeconds > 0 && now >= this.delistTimeSeconds) {
return false;
}
}
return ShopCondition.matches(player, this.ListCondType, this.listCondParams);
}
/**
* Display all skins via config
*/
private boolean shouldIgnoreSaleWindow() {
return this.Tag == GameConstants.TAG_SKIN && Nebula.getConfig().getServerOptions().isShowAllSkinInMall();
}
/**
* Returns the next relevant stock refresh time for this package in epoch seconds.
*/
public long getNextRefreshTime() {
return Utils.getNextResetTimeSeconds(this.RefreshType);
}
@Override @Override
public void onLoad() { public void onLoad() {
this.products = ItemParamMap.fromJsonString(this.Items); this.products = ItemParamMap.fromJsonString(this.Items);
this.listTimeSeconds = Utils.dateToSeconds(this.ListTime);
this.delistTimeSeconds = Utils.dateToSeconds(this.DeListTime);
this.orderCondParams = JsonUtils.decode(this.OrderCondParams, int[].class);
this.listCondParams = JsonUtils.decode(this.ListCondParams, int[].class);
} }
} }
@@ -1,14 +1,18 @@
package emu.nebula.data.resources; package emu.nebula.data.resources;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import emu.nebula.GameConstants;
import emu.nebula.Nebula;
import emu.nebula.data.BaseDef; import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType; import emu.nebula.data.ResourceType;
import emu.nebula.game.inventory.ItemParamMap; import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.Player; import emu.nebula.game.player.Player;
import emu.nebula.util.Utils;
import lombok.Getter; import lombok.Getter;
import lombok.Setter;
@Getter @Getter
@Setter
@ResourceType(name = "MallShop.json") @ResourceType(name = "MallShop.json")
public class MallShopDef extends BaseDef { public class MallShopDef extends BaseDef {
@SerializedName("Id") @SerializedName("Id")
@@ -20,8 +24,13 @@ public class MallShopDef extends BaseDef {
private int ItemId; private int ItemId;
private int ItemQty; private int ItemQty;
private String ListTime;
private String DeListTime;
private int RefreshType;
private transient ItemParamMap products; private transient ItemParamMap products;
private transient long listTimeSeconds;
private transient long delistTimeSeconds;
@Override @Override
public int getId() { public int getId() {
@@ -29,15 +38,46 @@ public class MallShopDef extends BaseDef {
} }
public int getStock(Player player) { public int getStock(Player player) {
return Math.max(this.getStock() - player.getInventory().getMallBuyCount().get(this.getIdString()), 0); // Not purchase count limit.
if (this.Stock <= 0) {
return GameConstants.UNLIMITED_STOCK;
}
return Math.max(Stock - player.getInventory().getMallShopPurchaseCount(this.getIdString()), 0);
}
/**
* Returns whether the item is currently within its configured mall list window.
*/
public boolean isVisible() {
long now = Nebula.getCurrentServerTime();
return (this.listTimeSeconds <= 0 || now >= this.listTimeSeconds)
&& (this.delistTimeSeconds <= 0 || now < this.delistTimeSeconds);
}
/**
* Returns whether the player can currently purchase the requested quantity.
*/
public boolean canPurchase(Player player, int quantity) {
return quantity > 0 && this.isVisible() && this.getStock(player) >= quantity;
}
/**
* Returns the next relevant stock refresh time for this mall shop entry in epoch seconds.
*/
public long getNextRefreshTime() {
return Utils.getNextResetTimeSeconds(this.RefreshType);
} }
@Override @Override
public void onLoad() { public void onLoad() {
this.products = new ItemParamMap(); this.products = new ItemParamMap();
this.listTimeSeconds = Utils.dateToSeconds(this.ListTime);
this.delistTimeSeconds = Utils.dateToSeconds(this.DeListTime);
if (this.ItemId > 0) { if (this.ItemId > 0) {
this.products.add(this.ItemId, this.ItemQty); this.products.add(this.ItemId, this.ItemQty);
} }
} }
} }
@@ -0,0 +1,37 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import emu.nebula.game.inventory.ItemParamMap;
import lombok.Getter;
@Getter
@ResourceType(name = "MonthlyCard.json")
public class MonthlyCardRewardDef extends BaseDef {
private int Id;
private int CardId;
private int RewardId1;
private int RewardNum1;
private int RewardId2;
private int RewardNum2;
private transient ItemParamMap rewards;
@Override
public int getId() {
return this.Id;
}
@Override
public void onLoad() {
this.rewards = new ItemParamMap();
if (this.RewardId1 > 0 && this.RewardNum1 > 0) {
this.rewards.add(this.RewardId1, this.RewardNum1);
}
if (this.RewardId2 > 0 && this.RewardNum2 > 0) {
this.rewards.add(this.RewardId2, this.RewardNum2);
}
}
}
@@ -4,6 +4,7 @@ import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType; import emu.nebula.data.ResourceType;
import emu.nebula.game.inventory.ItemParamMap; import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.Player; import emu.nebula.game.player.Player;
import emu.nebula.util.JsonUtils;
import lombok.Getter; import lombok.Getter;
@Getter @Getter
@@ -18,24 +19,36 @@ public class ResidentGoodsDef extends BaseDef {
private int CurrencyItemId; private int CurrencyItemId;
private int Price; private int Price;
private int AppearCondType;
private String AppearCondParams;
private transient ItemParamMap products; private transient ItemParamMap products;
private transient int[] appearCondParams;
@Override @Override
public int getId() { public int getId() {
return Id; return Id;
} }
public int getStock(Player player) { public int getStock(Player player) {
return Math.max(this.getMaximumLimit() - player.getInventory().getMallBuyCount().getInt(this.getId()), 0); return Math.max(this.getMaximumLimit() - player.getInventory().getShopBuyCount().get(this.getId()), 0);
}
/**
* Returns whether this resident shop good should currently be visible.
*/
public boolean isVisible(Player player) {
return ShopCondition.matchesResidentGoodsAppear(player, this.AppearCondType, this.appearCondParams);
} }
@Override @Override
public void onLoad() { public void onLoad() {
this.products = new ItemParamMap(); this.products = new ItemParamMap();
this.appearCondParams = JsonUtils.decode(this.AppearCondParams, int[].class);
if (this.ItemId > 0) { if (this.ItemId > 0) {
this.products.add(this.ItemId, this.ItemQuantity); this.products.add(this.ItemId, this.ItemQuantity);
} }
} }
} }
@@ -1,16 +1,47 @@
package emu.nebula.data.resources; package emu.nebula.data.resources;
import emu.nebula.Nebula;
import emu.nebula.data.BaseDef; import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType; import emu.nebula.data.ResourceType;
import emu.nebula.util.Utils;
import lombok.Getter; import lombok.Getter;
@Getter @Getter
@ResourceType(name = "ResidentShop.json") @ResourceType(name = "ResidentShop.json")
public class ResidentShopDef extends BaseDef { public class ResidentShopDef extends BaseDef {
private int Id; private int Id;
private int RefreshTimeType;
private int RefreshInterval;
private String OpenTime;
private transient long openTimeSeconds;
@Override @Override
public int getId() { public int getId() {
return Id; return Id;
} }
/**
* Returns the next refresh time for this resident shop in epoch seconds.
*/
public long getNextRefreshTime() {
if (this.openTimeSeconds > 0 && Nebula.getCurrentServerTime() < this.openTimeSeconds) {
return this.openTimeSeconds;
}
return Utils.getNextResetTimeSeconds(this.RefreshTimeType);
}
/**
* Returns whether this resident shop should currently be visible to the client.
*/
public boolean isVisible() {
return this.openTimeSeconds <= 0 || this.openTimeSeconds <= Nebula.getCurrentServerTime();
}
@Override
public void onLoad() {
this.openTimeSeconds = Utils.dateToSeconds(this.OpenTime);
}
} }
@@ -0,0 +1,77 @@
package emu.nebula.data.resources;
import emu.nebula.data.GameData;
import emu.nebula.game.player.Player;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
public enum ShopCondition {
None(0),
WorldClassSpecific(71),
ShopPreGoodsSellOut(85),
ActivityShopPreGoodsSellOut(115);
@Getter
private final int value;
private static final Int2ObjectMap<ShopCondition> map = new Int2ObjectOpenHashMap<>();
static {
for (ShopCondition type : ShopCondition.values()) {
map.put(type.getValue(), type);
}
}
ShopCondition(int value) {
this.value = value;
}
public static ShopCondition getByValue(int value) {
return map.get(value);
}
public static boolean matches(Player player, int condType, int[] condParams) {
ShopCondition condition = ShopCondition.getByValue(condType);
if (condition == ShopCondition.None) {
return true;
}
if (condition == null) {
return false;
}
if (condition == ShopCondition.WorldClassSpecific) {
int requiredLevel = condParams != null && condParams.length > 0 ? condParams[0] : 0;
return player.getLevel() >= requiredLevel;
}
return false;
}
public static boolean matchesResidentGoodsAppear(Player player, int condType, int[] condParams) {
ShopCondition condition = ShopCondition.getByValue(condType);
if (condition == ShopCondition.None) {
return true;
}
if (condition == null) {
return false;
}
if (condition == ShopCondition.ShopPreGoodsSellOut) {
if (condParams == null || condParams.length < 2) {
return false;
}
int requiredGoodsId = condParams[1];
ResidentGoodsDef requiredGoods = GameData.getResidentGoodsDataTable().get(requiredGoodsId);
if (requiredGoods == null) {
return false;
}
int boughtCount = player.getInventory().getShopBuyCount().get(requiredGoodsId);
return boughtCount > 0 && boughtCount >= requiredGoods.getMaximumLimit();
}
return false;
}
}
+15 -21
View File
@@ -1,21 +1,20 @@
package emu.nebula.game; package emu.nebula.game;
import java.time.Instant;
import java.time.LocalDate;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import emu.nebula.GameConstants;
import emu.nebula.Nebula; import emu.nebula.Nebula;
import emu.nebula.game.activity.ActivityModule; import emu.nebula.game.activity.ActivityModule;
import emu.nebula.game.ban.BanModule; import emu.nebula.game.ban.BanModule;
import emu.nebula.game.gacha.GachaModule; import emu.nebula.game.gacha.GachaModule;
import emu.nebula.game.jointdrill.JointDrillModule; import emu.nebula.game.jointdrill.JointDrillModule;
import emu.nebula.game.mall.PayModule;
import emu.nebula.game.player.PlayerModule; import emu.nebula.game.player.PlayerModule;
import emu.nebula.game.scoreboss.ScoreBossModule; import emu.nebula.game.scoreboss.ScoreBossModule;
import emu.nebula.game.tutorial.TutorialModule; import emu.nebula.game.tutorial.TutorialModule;
import emu.nebula.net.GameSession; import emu.nebula.net.GameSession;
import emu.nebula.util.ResetCycle;
import emu.nebula.util.Utils; import emu.nebula.util.Utils;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap; import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
@@ -34,14 +33,15 @@ public class GameContext implements Runnable {
private final ScoreBossModule scoreBossModule; private final ScoreBossModule scoreBossModule;
private final JointDrillModule jointDrillModule; private final JointDrillModule jointDrillModule;
private final BanModule banModule; private final BanModule banModule;
private final PayModule payModule;
// Game loop // Game loop
private final ScheduledExecutorService scheduler; private final ScheduledExecutorService scheduler;
// Daily // Reset-period indexes aligned to the configured daily reset boundary.
private long epochDays; private long resetDays;
private int epochWeeks; private int resetWeeks;
private int epochMonths; private int resetMonths;
public GameContext() { public GameContext() {
// Create session map // Create session map
@@ -55,6 +55,7 @@ public class GameContext implements Runnable {
this.scoreBossModule = new ScoreBossModule(this); this.scoreBossModule = new ScoreBossModule(this);
this.jointDrillModule = new JointDrillModule(this); this.jointDrillModule = new JointDrillModule(this);
this.banModule = new BanModule(this); this.banModule = new BanModule(this);
this.payModule = new PayModule(this);
// Run game loop // Run game loop
this.scheduler = Executors.newScheduledThreadPool(1); this.scheduler = Executors.newScheduledThreadPool(1);
@@ -76,7 +77,7 @@ public class GameContext implements Runnable {
} }
// Generate token // Generate token
String token = null; String token;
do { do {
token = session.generateToken(); token = session.generateToken();
@@ -107,20 +108,14 @@ public class GameContext implements Runnable {
@Override @Override
public void run() { public void run() {
// Check daily - Update epoch days long lastResetDays = this.resetDays;
long offset = Nebula.getConfig().getServerOptions().getDailyResetHour() * -3600; this.resetDays = Utils.getResetEpochDay();
var instant = Instant.now().plusSeconds(offset);
var date = LocalDate.ofInstant(instant, GameConstants.UTC_ZONE);
// Update epoch days
long lastEpochDays = this.epochDays;
this.epochDays = date.toEpochDay();
// Check if the day was changed // Check if the day was changed
if (this.epochDays > lastEpochDays) { if (this.resetDays > lastResetDays) {
// Update epoch weeks/months // Update epoch weeks/months
this.epochWeeks = Utils.getWeeks(this.epochDays); this.resetWeeks = Utils.getResetPeriodIndex(ResetCycle.WEEKLY, Nebula.getCurrentServerTime());
this.epochMonths = Utils.getMonths(this.epochDays); this.resetMonths = Utils.getResetPeriodIndex(ResetCycle.MONTHLY, Nebula.getCurrentServerTime());
// Update score boss season // Update score boss season
this.getScoreBossModule().updateSeason(); this.getScoreBossModule().updateSeason();
@@ -145,8 +140,7 @@ public class GameContext implements Runnable {
if (player == null) { if (player == null) {
continue; continue;
} }
//
player.checkResetDailies(); player.checkResetDailies();
} }
} }
@@ -148,11 +148,12 @@ public enum AchievementCondition {
} }
} }
private AchievementCondition(int value) { AchievementCondition(int value) {
this.value = value; this.value = value;
} }
public static AchievementCondition getByValue(int value) { public static AchievementCondition getByValue(int value) {
return map.get(value); return map.get(value);
} }
} }
@@ -1,15 +1,11 @@
package emu.nebula.game.battlepass; package emu.nebula.game.battlepass;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import dev.morphia.annotations.Entity; import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id; import dev.morphia.annotations.Id;
import emu.nebula.GameConstants; import emu.nebula.GameConstants;
import emu.nebula.Nebula; import emu.nebula.Nebula;
import emu.nebula.data.GameData; import emu.nebula.data.GameData;
import emu.nebula.data.resources.BattlePassDef;
import emu.nebula.data.resources.BattlePassRewardDef; import emu.nebula.data.resources.BattlePassRewardDef;
import emu.nebula.database.GameDatabaseObject; import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.inventory.ItemParamMap; import emu.nebula.game.inventory.ItemParamMap;
@@ -20,10 +16,15 @@ import emu.nebula.game.quest.QuestType;
import emu.nebula.net.NetMsgId; import emu.nebula.net.NetMsgId;
import emu.nebula.proto.BattlePassInfoOuterClass.BattlePassInfo; import emu.nebula.proto.BattlePassInfoOuterClass.BattlePassInfo;
import emu.nebula.util.Bitset; import emu.nebula.util.Bitset;
import lombok.Getter; import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
@Getter @Getter
@Setter
@Entity(value = "battlepass", useDiscriminator = false) @Entity(value = "battlepass", useDiscriminator = false)
public class BattlePass implements GameDatabaseObject { public class BattlePass implements GameDatabaseObject {
@Id @Id
@@ -49,7 +50,7 @@ public class BattlePass implements GameDatabaseObject {
public BattlePass(BattlePassManager manager) { public BattlePass(BattlePassManager manager) {
this.uid = manager.getPlayerUid(); this.uid = manager.getPlayerUid();
this.manager = manager; this.manager = manager;
this.battlePassId = GameConstants.BATTLE_PASS_ID; this.battlePassId = getActiveBattlePassId();
this.basicReward = new Bitset(); this.basicReward = new Bitset();
this.premiumReward = new Bitset(); this.premiumReward = new Bitset();
@@ -63,8 +64,22 @@ public class BattlePass implements GameDatabaseObject {
this.save(); this.save();
} }
public void setManager(BattlePassManager manager) { private static int getActiveBattlePassId() {
this.manager = manager; long currentTimeSec = Nebula.getCurrentServerTime();
for (BattlePassDef data : GameData.getBattlePassDataTable()) {
if (data == null) {
continue;
}
long start = data.getStartTimeTimestamp();
long end = data.getEndTimeTimestamp();
if (currentTimeSec >= start && currentTimeSec <= end) {
return data.getId();
}
}
return GameConstants.BATTLE_PASS_ID;
} }
public Player getPlayer() { public Player getPlayer() {
@@ -116,30 +131,63 @@ public class BattlePass implements GameDatabaseObject {
} }
/** /**
* Returns true if any rewards or quests are claimable * Check claimable tasks to show battle pass red dot.
*/ */
public synchronized boolean hasNew() { public synchronized boolean hasClaimableQuest() {
// Check if any quests are complete but unclaimed if (!this.getPlayer().isBattlePassUnlocked()) {
return false;
}
var nextLevelData = GameData.getBattlePassLevelDataTable().get(this.getLevel() + 1);
if (nextLevelData == null) {
return false;
}
for (var quest : getQuests().values()) { for (var quest : getQuests().values()) {
if (quest.isComplete() && !quest.isClaimed()) { if (quest.isComplete() && !quest.isClaimed()) {
return true; return true;
} }
} }
// Check if we have any pending rewards return false;
}
/**
* Returns whether the battle pass currently has at least one claimable
* reward lane item for the player's current level/mode.
*/
public synchronized boolean hasClaimableReward() {
if (!this.getPlayer().isBattlePassUnlocked()) {
return false;
}
for (int i = 1; i <= this.getLevel(); i++) { for (int i = 1; i <= this.getLevel(); i++) {
if (!this.getBasicReward().isSet(i)) { if (!this.getBasicReward().isSet(i)) {
return true; return true;
} }
if (this.isPremium() && !this.getPremiumReward().isSet(i)) { if (this.isPremium() && !this.getPremiumReward().isSet(i)) {
return true; return true;
} }
} }
// No claimable things
return false; return false;
} }
/**
* Encodes the client battle-pass state red dot contract:
* 0 = none, 1 = quest only, 2 = reward only, 3 = both.
*/
public synchronized int getClientState() {
int state = 0;
if (this.hasClaimableQuest()) {
state |= 1;
}
if (this.hasClaimableReward()) {
state |= 2;
}
return state;
}
public synchronized void resetDailyQuests(boolean resetWeekly) { public synchronized void resetDailyQuests(boolean resetWeekly) {
// Reset daily quests // Reset daily quests
@@ -190,7 +238,7 @@ public class BattlePass implements GameDatabaseObject {
* Update this quest on the player client * Update this quest on the player client
*/ */
private void syncQuest(GameQuest quest) { private void syncQuest(GameQuest quest) {
if (!getPlayer().hasSession()) { if (!getPlayer().hasSession() || !getPlayer().isBattlePassUnlocked()) {
return; return;
} }
@@ -269,7 +317,7 @@ public class BattlePass implements GameDatabaseObject {
public PlayerChangeInfo receiveReward(boolean premium, int levelId) { public PlayerChangeInfo receiveReward(boolean premium, int levelId) {
// Get bitset // Get bitset
Bitset rewards = null; Bitset rewards;
if (premium) { if (premium) {
rewards = this.getPremiumReward(); rewards = this.getPremiumReward();
@@ -296,7 +344,13 @@ public class BattlePass implements GameDatabaseObject {
// Add items // Add items
if (premium) { if (premium) {
return getPlayer().getInventory().addItems(data.getPremiumRewards()); var premiumRewards = data.getPremiumRewards().clone();
if (this.getMode() >= 2 && data.hasLuxuryRewards()) {
premiumRewards.add(data.getLuxuryRewards());
}
return getPlayer().getInventory().addItems(premiumRewards);
} else { } else {
return getPlayer().getInventory().addItems(data.getBasicRewards()); return getPlayer().getInventory().addItems(data.getBasicRewards());
} }
@@ -308,38 +362,29 @@ public class BattlePass implements GameDatabaseObject {
// Get unclaimed rewards // Get unclaimed rewards
for (int i = 1; i <= this.getLevel(); i++) { for (int i = 1; i <= this.getLevel(); i++) {
// Cache reward data boolean claimBasic = !this.getBasicReward().isSet(i);
BattlePassRewardDef data = null; boolean claimPremium = this.isPremium() && !this.getPremiumReward().isSet(i);
// Basic reward if (!claimBasic && !claimPremium) {
if (!this.getBasicReward().isSet(i)) { continue;
// Set flag
this.getBasicReward().setBit(i);
// Get reward data if we havent already
if (data == null) {
data = this.getRewardData(i);
}
// Add basic rewards
if (data != null) {
rewards.add(data.getBasicRewards());
}
} }
// Premium reward BattlePassRewardDef data = this.getRewardData(i);
if (this.isPremium() && !this.getPremiumReward().isSet(i)) { if (data == null) {
// Set flag continue;
}
if (claimBasic) {
this.getBasicReward().setBit(i);
rewards.add(data.getBasicRewards());
}
if (claimPremium) {
this.getPremiumReward().setBit(i); this.getPremiumReward().setBit(i);
rewards.add(data.getPremiumRewards());
// Get reward data if we havent already
if (data == null) { if (this.getMode() >= 2 && data.hasLuxuryRewards()) {
data = this.getRewardData(i); rewards.add(data.getLuxuryRewards());
}
// Add basic rewards
if (data != null) {
rewards.add(data.getPremiumRewards());
} }
} }
} }
@@ -358,6 +403,20 @@ public class BattlePass implements GameDatabaseObject {
// Proto // Proto
public BattlePassInfo toProto() { 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()) {
return BattlePassInfo.newInstance()
.setId(0)
.setLevel(0)
.setMode(0)
.setExp(0)
.setExpThisWeek(0)
.setDeadline(0L)
.setBasicReward()
.setPremiumReward();
}
var proto = BattlePassInfo.newInstance() var proto = BattlePassInfo.newInstance()
.setId(this.getBattlePassId()) .setId(this.getBattlePassId())
.setLevel(this.getLevel()) .setLevel(this.getLevel())
@@ -367,10 +426,10 @@ public class BattlePass implements GameDatabaseObject {
.setDeadline(Long.MAX_VALUE) .setDeadline(Long.MAX_VALUE)
.setBasicReward(this.getBasicReward().toByteArray()) .setBasicReward(this.getBasicReward().toByteArray())
.setPremiumReward(this.getPremiumReward().toByteArray()); .setPremiumReward(this.getPremiumReward().toByteArray());
var daily = proto.getMutableDailyQuests(); var daily = proto.getMutableDailyQuests();
var weekly = proto.getMutableWeeklyQuests(); var weekly = proto.getMutableWeeklyQuests();
for (var quest : this.getQuests().values()) { for (var quest : this.getQuests().values()) {
if (quest.getType() == QuestType.BattlePassDaily) { if (quest.getType() == QuestType.BattlePassDaily) {
daily.addList(quest.toProto()); daily.addList(quest.toProto());
@@ -378,7 +437,8 @@ public class BattlePass implements GameDatabaseObject {
weekly.addList(quest.toProto()); weekly.addList(quest.toProto());
} }
} }
return proto; return proto;
} }
} }
@@ -12,10 +12,6 @@ public class BattlePassManager extends PlayerManager {
public BattlePassManager(Player player) { public BattlePassManager(Player player) {
super(player); super(player);
} }
public boolean hasNew() {
return this.getBattlePass().hasNew();
}
// Database // Database
@@ -1,43 +1,37 @@
package emu.nebula.game.inventory; package emu.nebula.game.inventory;
import java.util.List;
import com.mongodb.client.model.Filters; import com.mongodb.client.model.Filters;
import dev.morphia.annotations.Entity; import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id; import dev.morphia.annotations.Id;
import emu.nebula.GameConstants; import emu.nebula.GameConstants;
import emu.nebula.Nebula; import emu.nebula.Nebula;
import emu.nebula.data.GameData; import emu.nebula.data.GameData;
import emu.nebula.data.resources.DropPkgDef; import emu.nebula.data.resources.*;
import emu.nebula.data.resources.MallShopDef;
import emu.nebula.data.resources.ResidentGoodsDef;
import emu.nebula.database.GameDatabaseObject; import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.game.player.PlayerManager; import emu.nebula.game.player.PlayerManager;
import emu.nebula.game.quest.QuestCondition; import emu.nebula.game.quest.QuestCondition;
import emu.nebula.net.NetMsgId; import emu.nebula.net.NetMsgId;
import emu.nebula.proto.Notify.Skin; import emu.nebula.proto.Notify.Skin;
import emu.nebula.proto.Public.Honor; import emu.nebula.proto.Public.*;
import emu.nebula.proto.Public.Item;
import emu.nebula.proto.Public.Res;
import emu.nebula.proto.Public.Title;
import emu.nebula.proto.Public.UI32;
import emu.nebula.util.Utils; import emu.nebula.util.Utils;
import emu.nebula.util.ints.String2IntMap; import emu.nebula.util.ints.String2IntMap;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import it.unimi.dsi.fastutil.ints.IntCollection; import it.unimi.dsi.fastutil.ints.IntCollection;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet; import it.unimi.dsi.fastutil.ints.IntSet;
import lombok.Getter; import lombok.Getter;
import java.lang.String;
import java.util.List;
@Getter @Getter
@Entity(value = "inventory", useDiscriminator = false) @Entity(value = "inventory", useDiscriminator = false)
public class Inventory extends PlayerManager implements GameDatabaseObject { public class Inventory extends PlayerManager implements GameDatabaseObject {
@Id @Id
private int uid; private int uid;
// Items/resources // Items/resources
private ItemParamMap items; private ItemParamMap items;
private ItemParamMap resources; private ItemParamMap resources;
@@ -51,8 +45,15 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
// Buy limit // Buy limit
private ItemParamMap shopBuyCount; private ItemParamMap shopBuyCount;
private String2IntMap mallBuyCount; private String2IntMap mallBuyCount;
private String2IntMap mallPackageBuyCount;
@Deprecated /**
* Tracks whether a MallGem pack has already consumed its maiden bonus.
* This is the only persisted state MallGem recharge still needs now that
* recharge packs no longer have any purchase-limit semantics.
*/
private String2IntMap gemMaidenClaimed;
private String2IntMap monthlyCardBuyCount;
public Inventory() { public Inventory() {
// Morphia only // Morphia only
} }
@@ -73,7 +74,10 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
this.shopBuyCount = new ItemParamMap(); this.shopBuyCount = new ItemParamMap();
this.mallBuyCount = new String2IntMap(); this.mallBuyCount = new String2IntMap();
this.mallPackageBuyCount = new String2IntMap();
this.gemMaidenClaimed = new String2IntMap();
this.monthlyCardBuyCount = new String2IntMap();
// Add player heads // Add player heads
this.getHeadIcons().add(101); this.getHeadIcons().add(101);
this.getHeadIcons().add(102); this.getHeadIcons().add(102);
@@ -562,6 +566,29 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
change.add(proto); change.add(proto);
} }
} }
case HeadItem -> {
// Cannot remove head icons
if (amount <= 0) {
break;
}
// Ensure the head icon exists in data and persist ownership.
var headData = GameData.getPlayerHeadDataTable().get(id);
if (headData == null) {
break;
}
// Persist ownership and expose it in ChangeInfo as a normal item reward,
// so regular reward popup can still show the head icon entry.
if (this.addHeadIcon(id)) {
// Client head-page state/red-dot updates are driven by proto.HeadIcon
// in ChangeInfo, not by a generic proto.Item entry.
var proto = HeadIcon.newInstance()
.setTid(id);
change.add(proto);
}
}
default -> { default -> {
// Not implemented // Not implemented
} }
@@ -651,7 +678,7 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
} }
/** /**
* Checks if the player has enough quanity of this item * Checks if the player has enough quantity of this item
*/ */
public synchronized boolean hasItem(int id, int count) { public synchronized boolean hasItem(int id, int count) {
// Sanity check // Sanity check
@@ -726,7 +753,7 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
} }
// Get materials // Get materials
var materials = data.getMaterials().mulitply(num); var materials = data.getMaterials().multiply(num);
// Verify that we have the materials // Verify that we have the materials
if (!this.hasItems(materials)) { if (!this.hasItems(materials)) {
@@ -774,8 +801,7 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
public PlayerChangeInfo buyMallItem(MallShopDef data, int buyCount) { public PlayerChangeInfo buyMallItem(MallShopDef data, int buyCount) {
// Check stock // Check stock
int stock = data.getStock(this.getPlayer()); if (!data.canPurchase(this.getPlayer(), buyCount)) {
if (buyCount > stock) {
return null; return null;
} }
@@ -787,14 +813,14 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
} }
// Update purchase limit // Update purchase limit
this.getMallBuyCount().addTo(data.getIdString(), buyCount); int purchaseCount = this.getMallShopPurchaseCount(data.getIdString());
this.setMallCounterValue(this.getMallBuyCount(), data.getIdString(), purchaseCount + buyCount);
Nebula.getGameDatabase().update( Nebula.getGameDatabase().update(
this, this,
getUid(), getUid(),
"mallBuyCount." + data.getIdString(), "mallBuyCount",
getMallBuyCount().get(data.getIdString()) this.getMallBuyCount());
);
// Return // Return
return change; return change;
} }
@@ -846,7 +872,7 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
this.removeItem(currencyId, cost, change); this.removeItem(currencyId, cost, change);
// Add items // Add items
this.addItems(buyItems.mulitply(buyCount), change); this.addItems(buyItems.multiply(buyCount), change);
// Success // Success
return change.setSuccess(true); return change.setSuccess(true);
@@ -879,7 +905,7 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
switch (data.getUseAction()) { switch (data.getUseAction()) {
case 2 -> { case 2 -> {
// Add items // Add items
this.addItems(data.getUseParams().mulitply(count), change); this.addItems(data.getUseParams().multiply(count), change);
// Success // Success
success = true; success = true;
@@ -912,18 +938,18 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
return change.setSuccess(true); return change.setSuccess(true);
} }
public PlayerChangeInfo convertGems(int amount) { public PlayerChangeInfo convertStellaniteLuminaToDust(int amount) {
// Verify that we have the gems // Verify that we have enough stellanite lumina in the combined paid/free pool.
if (!this.hasItem(GameConstants.PREM_GEM_ITEM_ID, amount)) { if (!this.hasMallPackageCurrency(GameConstants.FREE_STELLANITE_LUMINA_ITEM_ID, amount)) {
return null; return null;
} }
// Create change info // Create change info
var change = new PlayerChangeInfo(); var change = new PlayerChangeInfo();
// Convert gems // Convert stellanite lumina into stellanite dust.
this.removeItem(GameConstants.PREM_GEM_ITEM_ID, amount, change); this.consumeMallPackageCurrency(GameConstants.FREE_STELLANITE_LUMINA_ITEM_ID, amount, change);
this.addItem(GameConstants.GEM_ITEM_ID, amount, change); this.addItem(GameConstants.STELLANITE_DUST_ITEM_ID, amount, change);
// Success // Success
return change.setSuccess(true); return change.setSuccess(true);
@@ -942,7 +968,207 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
Nebula.getGameDatabase().update(this, this.getUid(), "mallBuyCount", this.getMallBuyCount()); Nebula.getGameDatabase().update(this, this.getUid(), "mallBuyCount", this.getMallBuyCount());
} }
} }
public void resetWeeklyMallPackagePurchases() {
this.resetMallCounterByRefreshType(this.getMallPackageBuyCount(), "mallPackageBuyCount", GameConstants.REFRESH_TYPE_WEEKLY);
}
public void resetMonthlyMallPackagePurchases() {
this.resetMallCounterByRefreshType(this.getMallPackageBuyCount(), "mallPackageBuyCount", GameConstants.REFRESH_TYPE_MONTHLY);
}
private void resetMallCounterByRefreshType(String2IntMap counter, String fieldName, int refreshType) {
if (counter == null || counter.isEmpty()) {
return;
}
var updated = new String2IntMap();
boolean changed = false;
for (var entry : counter.object2IntEntrySet()) {
var data = GameData.getMallPackageDataTable().get(Utils.unescapeKey(entry.getKey()).hashCode());
if (data != null && data.getRefreshType() == refreshType) {
changed = true;
continue;
}
updated.put(entry.getKey(), entry.getIntValue());
}
if (!changed) {
return;
}
counter.clear();
counter.putAll(updated);
Nebula.getGameDatabase().update(this, this.getUid(), fieldName, counter);
}
private int getMallCounterValue(String2IntMap counter, String key) {
if (counter == null || key == null) {
return 0;
}
int directValue = counter.get(key);
String escapedKey = Utils.escapeKey(key);
if (escapedKey.equals(key)) {
return directValue;
}
return Math.max(directValue, counter.get(escapedKey));
}
private void setMallCounterValue(String2IntMap counter, String key, int value) {
if (counter == null || key == null) {
return;
}
String escapedKey = Utils.escapeKey(key);
counter.put(escapedKey, value);
if (!escapedKey.equals(key)) {
counter.removeInt(key);
}
}
public int getMallPackagePurchaseCount(String packageId) {
return this.getMallCounterValue(this.getMallPackageBuyCount(), packageId);
}
public int getMallShopPurchaseCount(String shopId) {
return this.getMallCounterValue(this.getMallBuyCount(), shopId);
}
public boolean hasMallGemMaidenBonus(String gemId) {
return this.getMallCounterValue(this.getGemMaidenClaimed(), gemId) <= 0;
}
/**
* Dedicated toggle for MallGem first-purchase bonus state.
* Can reset this field later to reopen maiden bonus eligibility
*/
public void setMallGemMaidenClaimed(String gemId, boolean claimed) {
if (gemId == null) {
return;
}
if (this.gemMaidenClaimed == null) {
this.gemMaidenClaimed = new String2IntMap();
}
if (claimed) {
this.setMallCounterValue(this.getGemMaidenClaimed(), gemId, 1);
} else {
this.gemMaidenClaimed.removeInt(Utils.escapeKey(gemId));
this.gemMaidenClaimed.removeInt(gemId);
}
Nebula.getGameDatabase().update(this, getUid(), "gemMaidenClaimed", this.getGemMaidenClaimed());
}
/**
* Resets all MallGem maiden-bonus flags.
* This is the intended operation hook for anniversary-style first-purchase refreshes.
*/
public void resetMallGemMaidenBonuses() {
if (this.gemMaidenClaimed == null || this.gemMaidenClaimed.isEmpty()) {
return;
}
this.gemMaidenClaimed.clear();
Nebula.getGameDatabase().update(this, getUid(), "gemMaidenClaimed", this.getGemMaidenClaimed());
}
public int getMallMonthlyCardPurchaseCount(String cardId) {
return this.getMallCounterValue(this.getMonthlyCardBuyCount(), cardId);
}
public PlayerChangeInfo buyMallPackage(MallPackageDef data) {
var player = this.getPlayer();
if (!data.canPurchase(player)) {
return null;
}
int currencyItemId = data.getCurrencyItemId();
int currencyItemQty = data.getCurrencyItemQty();
if (data.isItemPackage() && currencyItemId > 0 && currencyItemQty > 0
&& !this.hasMallPackageCurrency(currencyItemId, currencyItemQty)) {
return null;
}
var change = new PlayerChangeInfo();
if (data.isItemPackage() && currencyItemId > 0 && currencyItemQty > 0) {
this.consumeMallPackageCurrency(currencyItemId, currencyItemQty, change);
}
this.addItems(data.getProducts(), change);
String packageId = data.getIdString();
int purchaseCount = this.getMallPackagePurchaseCount(packageId);
this.setMallCounterValue(this.getMallPackageBuyCount(), packageId, purchaseCount + 1);
Nebula.getGameDatabase().update(this, getUid(), "mallPackageBuyCount", this.getMallPackageBuyCount());
return change.setSuccess(true);
}
private boolean hasMallPackageCurrency(int currencyItemId, int currencyItemQty) {
if (currencyItemQty <= 0) {
return true;
}
if (currencyItemId == GameConstants.PAID_STELLANITE_LUMINA_ITEM_ID
|| currencyItemId == GameConstants.FREE_STELLANITE_LUMINA_ITEM_ID) {
return this.getResourceCount(GameConstants.PAID_STELLANITE_LUMINA_ITEM_ID)
+ this.getResourceCount(GameConstants.FREE_STELLANITE_LUMINA_ITEM_ID) >= currencyItemQty;
}
return this.hasItem(currencyItemId, currencyItemQty);
}
private void consumeMallPackageCurrency(int currencyItemId, int currencyItemQty, PlayerChangeInfo change) {
if (currencyItemId == GameConstants.PAID_STELLANITE_LUMINA_ITEM_ID
|| currencyItemId == GameConstants.FREE_STELLANITE_LUMINA_ITEM_ID) {
// use free stellanite lumina first.
int freeOwned = this.getResourceCount(GameConstants.FREE_STELLANITE_LUMINA_ITEM_ID);
int useFree = Math.min(freeOwned, currencyItemQty);
if (useFree > 0) {
this.removeItem(GameConstants.FREE_STELLANITE_LUMINA_ITEM_ID, useFree, change);
}
int remain = currencyItemQty - useFree;
if (remain > 0) {
this.removeItem(GameConstants.PAID_STELLANITE_LUMINA_ITEM_ID, remain, change);
}
return;
}
this.removeItem(currencyItemId, currencyItemQty, change);
}
public PlayerChangeInfo buyMallGem(MallGemDef data) {
var change = new PlayerChangeInfo();
this.addItems(data.buildProducts(this.getPlayer()), change);
this.setMallGemMaidenClaimed(data.getIdString(), true);
return change.setSuccess(true);
}
public PlayerChangeInfo buyMallMonthlyCard(MallMonthlyCardDef data) {
var change = new PlayerChangeInfo();
this.addItems(data.getProducts(), change);
String cardId = data.getIdString();
int purchaseCount = this.getMallMonthlyCardPurchaseCount(cardId);
this.setMallCounterValue(this.getMonthlyCardBuyCount(), cardId, purchaseCount + 1);
Nebula.getGameDatabase().update(this, getUid(), "monthlyCardBuyCount", this.getMonthlyCardBuyCount());
return change.setSuccess(true);
}
// Database // Database
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
@@ -990,7 +1216,22 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
// Set to save inventory to database // Set to save inventory to database
save = true; save = true;
} }
if (this.mallPackageBuyCount == null) {
this.mallPackageBuyCount = new String2IntMap();
save = true;
}
if (this.gemMaidenClaimed == null) {
this.gemMaidenClaimed = new String2IntMap();
save = true;
}
if (this.monthlyCardBuyCount == null) {
this.monthlyCardBuyCount = new String2IntMap();
save = true;
}
// Update in database // Update in database
if (save) { if (save) {
this.save(); this.save();
@@ -54,10 +54,10 @@ public class ItemParamMap extends Int2IntLinkedOpenHashMap implements ObjectBidi
/** /**
* Returns a new ItemParamMap with item amounts multiplied * Returns a new ItemParamMap with item amounts multiplied
* @param mult Value to multiply all item amounts in this map by * @param multiplier Value to multiply all item amounts in this map by
* @return * @return ItemParamMap
*/ */
public ItemParamMap mulitply(int multiplier) { public ItemParamMap multiply(int multiplier) {
var params = new ItemParamMap(); var params = new ItemParamMap();
for (var entry : this.int2IntEntrySet()) { for (var entry : this.int2IntEntrySet()) {
@@ -0,0 +1,48 @@
package emu.nebula.game.mall;
import emu.nebula.proto.Public.ChangeInfo;
import lombok.Getter;
/**
* Separate real reward delta from popup display data
* Decouple settlement logic from client UI, prevent data mutation on reuse
*/
@Getter
public final class MallOrderCollectResult {
private final ChangeInfo stateChange;
private final ChangeInfo displayChange;
private MallOrderCollectResult(ChangeInfo stateChange, ChangeInfo displayChange) {
this.stateChange = copyOf(stateChange);
this.displayChange = copyOf(displayChange);
}
public static MallOrderCollectResult empty() {
return new MallOrderCollectResult(null, null);
}
public static MallOrderCollectResult of(ChangeInfo change) {
return new MallOrderCollectResult(change, change);
}
public static MallOrderCollectResult split(ChangeInfo stateChange, ChangeInfo displayChange) {
return new MallOrderCollectResult(stateChange, displayChange);
}
public boolean hasStateChange() {
return !stateChange.isEmpty();
}
public boolean hasDisplayChange() {
return !displayChange.isEmpty();
}
private static ChangeInfo copyOf(ChangeInfo change) {
if (change == null || change.isEmpty()) {
return ChangeInfo.newInstance();
}
return ChangeInfo.newInstance().copyFrom(change);
}
}
@@ -0,0 +1,338 @@
package emu.nebula.game.mall;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import emu.nebula.Nebula;
import emu.nebula.data.resources.MallPackageDef;
import emu.nebula.data.resources.MallGemDef;
import emu.nebula.data.resources.MallMonthlyCardDef;
import emu.nebula.data.resources.BattlePassDef;
import emu.nebula.data.resources.DropPkgDef;
import emu.nebula.game.GameContext;
import emu.nebula.game.GameContextModule;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.net.GameSession;
import emu.nebula.net.NetMsgId;
import emu.nebula.net.PacketHelper;
import emu.nebula.proto.MallGemOrder.OrderInfo;
import emu.nebula.proto.Public.HeadIcon;
import emu.nebula.proto.Notify.OrderStateChange;
import emu.nebula.proto.Public.Item;
import emu.nebula.proto.Public.ChangeInfo;
import emu.nebula.proto.Public.Res;
import emu.nebula.util.JsonUtils;
/**
* Mock payment, isolate mock order data
*/
public class PayModule extends GameContextModule {
public PayModule(GameContext context) {
super(context);
}
@FunctionalInterface
private interface CollectSettlement {
MallOrderCollectResult resolve(GameSession session) throws Exception;
}
private final ConcurrentHashMap<Integer, CollectSettlement> pendingCollects = new ConcurrentHashMap<>();
private volatile Map<Integer, int[]> battlePassDropPackages;
public void clearPendingCollect(int uid) {
pendingCollects.remove(uid);
}
/**
* Resolves the authoritative collect payload and clears the transient order state.
*/
public MallOrderCollectResult consumePendingCollect(GameSession session) throws Exception {
CollectSettlement settlement = pendingCollects.remove(session.getPlayer().getUid());
if (settlement == null) {
return null;
}
return settlement.resolve(session);
}
/**
* Keeps mock-order flows aligned with normal inventory updates by reusing the
* same item notify packets as the rest of the server.
*/
public void pushInventoryNotifies(GameSession session, ChangeInfo items) {
if (items == null || items.isEmpty()) {
return;
}
session.getPlayer().addNextPackage(NetMsgId.items_change_notify, items);
}
/**
* Use mock payment for gem recharge to match official mall order flow
*/
public OrderInfo createGemOrder(GameSession session, MallGemDef data) {
return this.createOrder(session, "gem", data.getIdString(), collectSession -> {
var change = collectSession.getPlayer().getInventory().buyMallGem(data);
if (change == null) {
return MallOrderCollectResult.empty();
}
return MallOrderCollectResult.of(change.toProto());
});
}
/**
* Defer monthly card settlement to maintain consistent mock-payment lifecycle with mall flow.
*/
public OrderInfo createMonthlyCardOrder(GameSession session, MallMonthlyCardDef data) {
var player = session.getPlayer();
if (data == null || !data.canPurchase(player)) {
return null;
}
int durationDays = data.getMonthlyCardDuration();
return this.createOrder(session, "monthly", data.getIdString(), collectSession -> {
var collectPlayer = collectSession.getPlayer();
var collectChange = collectPlayer.getInventory().buyMallMonthlyCard(data);
if (collectChange == null) {
return MallOrderCollectResult.empty();
}
collectPlayer.activateMonthlyCard(data.getIdString(), durationDays);
var dailyRewardChange = collectPlayer.createMonthlyCardRewardChange(data.getIdString());
if (dailyRewardChange != null) {
collectChange.add(dailyRewardChange);
}
return MallOrderCollectResult.of(collectChange.toProto());
});
}
/**
* Cash mall packages follow the same order -> paid notify -> collect lifecycle as other RMB products.
*/
public OrderInfo createPackageOrder(GameSession session, MallPackageDef data) {
if (data == null || !data.isCashPackage() || !data.canPurchase(session.getPlayer())) {
return null;
}
return this.createOrder(session, "package", data.getIdString(), collectSession -> {
var change = collectSession.getPlayer().getInventory().buyMallPackage(data);
if (change == null) {
return MallOrderCollectResult.empty();
}
return MallOrderCollectResult.of(change.toProto());
});
}
/**
* Create mock battle pass order following standard payment flow:
* order confirm -> payment notify -> reward settlement.
*/
public OrderInfo createBattlePassOrder(GameSession session, BattlePassDef data, int requestedMode) {
return this.createOrder(session, "battlepass", data.getId() + ":" + requestedMode, collectSession -> {
var player = collectSession.getPlayer();
var battlePass = player.getBattlePassManager().getBattlePass();
if (battlePass == null) {
return MallOrderCollectResult.empty();
}
int currentMode = battlePass.getMode();
if (requestedMode <= currentMode) {
return MallOrderCollectResult.empty();
}
battlePass.setMode(requestedMode);
if (requestedMode == 2 && data.getLuxuryBonusLevel() > 0) {
battlePass.setLevel(battlePass.getLevel() + data.getLuxuryBonusLevel());
}
var rewards = new ItemParamMap();
int collectItemId = 0;
int collectItemQty = 0;
// BattlePass.json semantics:
// - mode=1 (Premium) unlocks premium claim lane only, no immediate collect reward.
// - mode=2 (Luxury) grants immediate luxury reward.
// - mode=1 -> mode=2 upgrade uses complementary reward config.
if (requestedMode == 2) {
if (currentMode == 0) {
collectItemId = data.getLuxuryTid();
collectItemQty = data.getLuxuryQty();
} else if (currentMode == 1) {
collectItemId = data.getComplementaryTid();
collectItemQty = data.getComplementaryQty();
}
}
this.addBattlePassCollectRewards(rewards, collectItemId, collectItemQty);
battlePass.save();
if (rewards.isEmpty()) {
return MallOrderCollectResult.empty();
}
var change = player.getInventory().addItems(rewards);
if (change == null || change.isEmpty()) {
return MallOrderCollectResult.empty();
}
var stateChange = change.toProto();
var displayChange = this.buildBattlePassDisplayChange(change);
return MallOrderCollectResult.split(stateChange, displayChange);
});
}
/**
* Resolves battle-pass collect rewards.
* <p>
* Some battle-pass collect ids in Item.json are intermediate "drop keys" that
* should expand to concrete rewards (for example, skin/head icon), otherwise
* the client misses skin-gain specific reward presentation.
*/
private void addBattlePassCollectRewards(ItemParamMap rewards, int collectItemId, int collectItemQty) {
if (collectItemId <= 0 || collectItemQty <= 0) {
return;
}
var packageIds = this.getBattlePassDropPackages().get(collectItemId);
if (packageIds == null || packageIds.length == 0) {
rewards.add(collectItemId, collectItemQty);
return;
}
for (int i = 0; i < collectItemQty; i++) {
for (int packageId : packageIds) {
int dropItemId = DropPkgDef.getRandomDrop(packageId);
if (dropItemId > 0) {
rewards.add(dropItemId, 1);
}
}
}
}
/**
* Loads battle-pass drop-package mapping from Drop.json on first use and caches it.
* Mapping rule: DropId -> [PkgId...].
*/
private Map<Integer, int[]> getBattlePassDropPackages() {
var cache = this.battlePassDropPackages;
if (cache != null) {
return cache;
}
synchronized (this) {
cache = this.battlePassDropPackages;
if (cache != null) {
return cache;
}
cache = this.loadBattlePassDropPackages();
this.battlePassDropPackages = cache;
return cache;
}
}
private Map<Integer, int[]> loadBattlePassDropPackages() {
final String dropPath = Nebula.getConfig().resourceDir + "/bin/Drop.json";
var result = new LinkedHashMap<Integer, int[]>();
try {
var rows = JsonUtils.loadToMap(dropPath, String.class, DropRow.class);
if (rows == null || rows.isEmpty()) {
return Collections.emptyMap();
}
var aggregate = new LinkedHashMap<Integer, List<Integer>>();
for (var row : rows.values()) {
if (row == null || row.DropId <= 0 || row.PkgId <= 0) {
continue;
}
aggregate.computeIfAbsent(row.DropId, ignored -> new ArrayList<>()).add(row.PkgId);
}
for (var entry : aggregate.entrySet()) {
var values = entry.getValue();
if (values == null || values.isEmpty()) {
continue;
}
int[] packageIds = new int[values.size()];
for (int i = 0; i < values.size(); i++) {
packageIds[i] = values.get(i);
}
result.put(entry.getKey(), packageIds);
}
} catch (Exception exception) {
Nebula.getLogger().error("Failed to load battle-pass drop mapping from {}", dropPath, exception);
return Collections.emptyMap();
}
return result;
}
/**
* Minimal Drop.json row shape needed for battle-pass mapping.
*/
private static final class DropRow {
private int DropId;
private int PkgId;
}
/**
* Battle-pass collect triggers 3 client presentations:
* skin animation (notify queue), normal reward popup (CollectResp.Items), level refresh (main response).
* <p>
* Only popup-safe items are shown in reward popup to avoid duplicate skin/head display.
* Avatar rewards are converted to item format for the client's general reward popup.
*/
private ChangeInfo buildBattlePassDisplayChange(PlayerChangeInfo change) {
if (change == null || change.isEmpty()) {
return ChangeInfo.newInstance();
}
var display = new PlayerChangeInfo();
for (var any : change.getList()) {
String typeUrl = any.getTypeUrl();
if (typeUrl.endsWith(Item.class.getSimpleName()) || typeUrl.endsWith(Res.class.getSimpleName())) {
display.getList().add(any.clone());
continue;
}
if (typeUrl.endsWith(HeadIcon.class.getSimpleName())) {
try {
var headIcon = HeadIcon.parseFrom(any.getValue().toArray());
display.add(Item.newInstance().setTid(headIcon.getTid()).setQty(1));
} catch (Exception ignored) {
// ignore
}
}
}
return display.toProto();
}
private OrderInfo createOrder(GameSession session, String orderType, String payloadKey, CollectSettlement settlement) {
int uid = session.getPlayer().getUid();
String orderId = orderType + "." + uid + "." + System.currentTimeMillis();
this.pendingCollects.put(uid, settlement);
return this.buildMockOrder(orderId, orderType + ":" + payloadKey + ":" + orderId);
}
private OrderInfo buildMockOrder(String orderId, String extraData) {
var paidNotify = OrderStateChange.newInstance()
.setOrderId(orderId)
.setStore(orderId.startsWith("battlepass.") ? 3 : 1);
return OrderInfo.newInstance()
.setId(orderId)
.setExtraData(extraData)
.setNotifyUrl(String.format("http://localhost:%s/mock-pay", Nebula.getConfig().getHttpServer().getBindPort()))
.setNextPackage(PacketHelper.encodeMsg(NetMsgId.order_paid_notify, paidNotify));
}
}
+274 -35
View File
@@ -1,15 +1,13 @@
package emu.nebula.game.player; package emu.nebula.game.player;
import java.util.Stack;
import dev.morphia.annotations.AlsoLoad; import dev.morphia.annotations.AlsoLoad;
import dev.morphia.annotations.Entity; import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id; import dev.morphia.annotations.Id;
import dev.morphia.annotations.Indexed; import dev.morphia.annotations.Indexed;
import emu.nebula.GameConstants; import emu.nebula.GameConstants;
import emu.nebula.Nebula; import emu.nebula.Nebula;
import emu.nebula.data.GameData; import emu.nebula.data.GameData;
import emu.nebula.data.resources.MallMonthlyCardDef;
import emu.nebula.database.GameDatabaseObject; import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.account.Account; import emu.nebula.game.account.Account;
import emu.nebula.game.achievement.AchievementCondition; import emu.nebula.game.achievement.AchievementCondition;
@@ -35,27 +33,23 @@ import emu.nebula.game.vampire.VampireSurvivorManager;
import emu.nebula.net.GameSession; import emu.nebula.net.GameSession;
import emu.nebula.net.NetMsgId; import emu.nebula.net.NetMsgId;
import emu.nebula.net.NetMsgPacket; import emu.nebula.net.NetMsgPacket;
import emu.nebula.proto.Notify.MonthlyCardRewards;
import emu.nebula.proto.Notify.SigninRewardUpdate; import emu.nebula.proto.Notify.SigninRewardUpdate;
import emu.nebula.proto.PlayerData.DictionaryEntry; import emu.nebula.proto.PlayerData.DictionaryEntry;
import emu.nebula.proto.PlayerData.DictionaryTab; import emu.nebula.proto.PlayerData.DictionaryTab;
import emu.nebula.proto.PlayerData.PlayerInfo; import emu.nebula.proto.PlayerData.PlayerInfo;
import emu.nebula.proto.Public.CharShow; import emu.nebula.proto.Public;
import emu.nebula.proto.Public.Energy; import emu.nebula.proto.Public.*;
import emu.nebula.proto.Public.Friend; import emu.nebula.util.ResetCycle;
import emu.nebula.proto.Public.HonorInfo;
import emu.nebula.proto.Public.Item;
import emu.nebula.proto.Public.NewbieInfo;
import emu.nebula.proto.Public.QuestType;
import emu.nebula.proto.Public.Res;
import emu.nebula.proto.Public.WorldClass;
import emu.nebula.proto.Public.WorldClassRewardState;
import emu.nebula.util.Utils; import emu.nebula.util.Utils;
import emu.nebula.proto.Public.Title; import emu.nebula.util.ints.String2IntMap;
import lombok.Getter; import lombok.Getter;
import us.hebi.quickbuf.ProtoMessage; import us.hebi.quickbuf.ProtoMessage;
import us.hebi.quickbuf.RepeatedInt; import us.hebi.quickbuf.RepeatedInt;
import java.lang.String;
import java.util.Stack;
@Getter @Getter
@Entity(value = "players", useDiscriminator = false) @Entity(value = "players", useDiscriminator = false)
public class Player implements GameDatabaseObject { public class Player implements GameDatabaseObject {
@@ -92,6 +86,8 @@ public class Player implements GameDatabaseObject {
private long lastEpochDay; private long lastEpochDay;
private long lastLogin; private long lastLogin;
private long createTime; private long createTime;
private String2IntMap monthlyCardExpireDays;
private String2IntMap monthlyCardLastRewardDays;
// Managers // Managers
private final transient CharacterStorage characters; private final transient CharacterStorage characters;
@@ -165,6 +161,8 @@ public class Player implements GameDatabaseObject {
this.level = 1; this.level = 1;
this.energy = 240; this.energy = 240;
this.energyLastUpdate = this.createTime; this.energyLastUpdate = this.createTime;
this.monthlyCardExpireDays = new String2IntMap();
this.monthlyCardLastRewardDays = new String2IntMap();
// Setup inventory // Setup inventory
this.inventory = new Inventory(this); this.inventory = new Inventory(this);
@@ -217,6 +215,8 @@ public class Player implements GameDatabaseObject {
} }
public void setLevel(int level) { public void setLevel(int level) {
int oldLevel = this.level;
// Set player world class (level) // Set player world class (level)
this.level = level; this.level = level;
@@ -225,6 +225,10 @@ public class Player implements GameDatabaseObject {
// Trigger achievement // Trigger achievement
this.trigger(AchievementCondition.WorldClassSpecific, this.getLevel()); this.trigger(AchievementCondition.WorldClassSpecific, this.getLevel());
if (oldLevel != this.level) {
this.queueBattlePassUnlockNotify(oldLevel);
}
} }
public void setExp(int exp) { public void setExp(int exp) {
@@ -547,6 +551,8 @@ public class Player implements GameDatabaseObject {
// Trigger achievement // Trigger achievement
this.trigger(AchievementCondition.WorldClassSpecific, this.getLevel()); this.trigger(AchievementCondition.WorldClassSpecific, this.getLevel());
this.queueBattlePassUnlockNotify(oldLevel);
} }
// Calculate changes // Calculate changes
@@ -555,7 +561,7 @@ public class Player implements GameDatabaseObject {
.setExpChange(this.getExp() - oldExp); .setExpChange(this.getExp() - oldExp);
changes.add(proto); changes.add(proto);
return changes; return changes;
} }
@@ -637,26 +643,29 @@ public class Player implements GameDatabaseObject {
// Dailies // Dailies
public void checkResetDailies() { public void checkResetDailies() {
// Sanity check to make sure daily reset isnt being triggered wrong long currentResetDay = Utils.getResetEpochDay();
if (Nebula.getGameContext().getEpochDays() <= this.getLastEpochDay()) {
// Sanity check to make sure daily reset isn't being triggered wrong
if (currentResetDay <= this.getLastEpochDay()) {
// Fix sign-in index // Fix sign-in index
// TODO remove later // TODO remove later
if (this.getSignInIndex() <= 0) { if (this.getSignInIndex() <= 0) {
this.getSignInRewards(false); this.getSignInRewards(false);
} }
this.refreshMonthlyCardRewards(false);
// End // End
return; return;
} }
// Check if week has changed (Resets on monday) // Check if week has changed (Resets on Monday)
// TODO add a config option int curWeek = Utils.getResetPeriodIndex(ResetCycle.WEEKLY, this.getLastResetDayTimeSeconds());
int curWeek = Utils.getWeeks(this.getLastEpochDay()); boolean hasWeekChanged = Utils.getResetPeriodIndex(ResetCycle.WEEKLY, Nebula.getCurrentServerTime()) > curWeek;
boolean hasWeekChanged = Nebula.getGameContext().getEpochWeeks() > curWeek;
// Check if month was changed // Check if month was changed
int curMonth = Utils.getMonths(this.getLastEpochDay()); int curMonth = Utils.getResetPeriodIndex(ResetCycle.MONTHLY, this.getLastResetDayTimeSeconds());
boolean hasMonthChanged = Nebula.getGameContext().getEpochMonths() > curMonth; boolean hasMonthChanged = Utils.getResetPeriodIndex(ResetCycle.MONTHLY, Nebula.getCurrentServerTime()) > curMonth;
// Reset dailies // Reset dailies
this.resetDailies(hasWeekChanged, hasMonthChanged); this.resetDailies(hasWeekChanged, hasMonthChanged);
@@ -666,9 +675,10 @@ public class Player implements GameDatabaseObject {
// Give sign-in rewards // Give sign-in rewards
this.getSignInRewards(hasMonthChanged); this.getSignInRewards(hasMonthChanged);
this.refreshMonthlyCardRewards(true);
// Update last epoch day // Update last epoch day
this.lastEpochDay = Nebula.getGameContext().getEpochDays(); this.lastEpochDay = currentResetDay;
Nebula.getGameDatabase().update(this, this.getUid(), "lastEpochDay", this.lastEpochDay); Nebula.getGameDatabase().update(this, this.getUid(), "lastEpochDay", this.lastEpochDay);
} }
@@ -680,7 +690,7 @@ public class Player implements GameDatabaseObject {
// Get next sign-in index // Get next sign-in index
int nextSignIn = this.signInIndex + 1; int nextSignIn = this.signInIndex + 1;
int group = Utils.getDaysOfMonth(this.getLastEpochDay()); int group = Utils.getDaysOfMonth(Utils.getResetEpochDay());
var data = GameData.getSignInDataTable().get((group << 16) + nextSignIn); var data = GameData.getSignInDataTable().get((group << 16) + nextSignIn);
if (data == null) { if (data == null) {
@@ -725,12 +735,14 @@ public class Player implements GameDatabaseObject {
// Reset weekly tower tickets // Reset weekly tower tickets
this.getProgress().clearWeeklyTowerTicketLog(); this.getProgress().clearWeeklyTowerTicketLog();
this.getInventory().resetWeeklyMallPackagePurchases();
} }
// Check if we need to reset monthly // Check if we need to reset monthly
if (resetMonthly) { if (resetMonthly) {
// Reset monthly shop purchases // Reset monthly shop purchases
this.getInventory().resetShopPurchases(); this.getInventory().resetShopPurchases();
this.getInventory().resetMonthlyMallPackagePurchases();
} }
} }
@@ -809,6 +821,16 @@ public class Player implements GameDatabaseObject {
this.showChars = new int[3]; this.showChars = new int[3];
this.save(); this.save();
} }
if (this.monthlyCardExpireDays == null) {
this.monthlyCardExpireDays = new String2IntMap();
Nebula.getGameDatabase().update(this, this.getUid(), "monthlyCardExpireDays", this.monthlyCardExpireDays);
}
if (this.monthlyCardLastRewardDays == null) {
this.monthlyCardLastRewardDays = new String2IntMap();
Nebula.getGameDatabase().update(this, this.getUid(), "monthlyCardLastRewardDays", this.monthlyCardLastRewardDays);
}
// Init activities // Init activities
this.getActivityManager().init(); this.getActivityManager().init();
@@ -829,13 +851,225 @@ public class Player implements GameDatabaseObject {
// Fix any broken honor ids // Fix any broken honor ids
this.checkBrokenHonor(); this.checkBrokenHonor();
// Update activities
this.getActivityManager().onLogin();
// Update last login time // Update last login time
this.lastLogin = System.currentTimeMillis(); this.lastLogin = System.currentTimeMillis();
Nebula.getGameDatabase().update(this, this.getUid(), "lastLogin", this.getLastLogin()); Nebula.getGameDatabase().update(this, this.getUid(), "lastLogin", this.getLastLogin());
} }
/**
* Returns whether the player currently has at least one purchasable
* free mall package (`CurrencyType = 3`) for the mall entrance red dot.
*/
public boolean hasAvailableFreeMallPackage() {
for (var data : GameData.getMallPackageDataTable()) {
if (!data.isFreePackage()) {
continue;
}
if (data.canPurchase(this)) {
return true;
}
}
return false;
}
/**
* Queues the mall entrance red dot state for free mall packages (`CurrencyType = 3`).
* Specific package entries are still refreshed from the package list itself.
*/
public void queueMallPackageStateNotify() {
this.addNextPackage(
NetMsgId.mall_package_state_notify,
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);
}
/**
* Returns the number of future game days still covered after the current game day.
* On the last claimable day this value is 0 even though today's reward can still be claimed.
*/
public int getMonthlyCardRemainingDays(String cardId) {
if (this.monthlyCardExpireDays == null) {
return 0;
}
int endDay = this.monthlyCardExpireDays.get(cardId);
long currentDay = Utils.getResetEpochDay();
if (endDay < currentDay) {
return 0;
}
return (int) (endDay - currentDay);
}
public boolean receivedMonthlyCardRewardToday(String cardId) {
return cardId != null
&& this.monthlyCardLastRewardDays != null
&& this.monthlyCardLastRewardDays.get(cardId) >= Utils.getResetEpochDay();
}
public long getMonthlyCardEndTime(String cardId) {
if (cardId == null || this.monthlyCardExpireDays == null) {
return 0;
}
int endDay = this.monthlyCardExpireDays.get(cardId);
if (endDay <= 0) {
return 0;
}
return Utils.getResetTimeSecondsByEpochDay((long) endDay + 1);
}
private void setMonthlyCardRewardEndDay(String cardId, int endDay) {
if (this.monthlyCardExpireDays == null) {
this.monthlyCardExpireDays = new String2IntMap();
}
this.monthlyCardExpireDays.put(cardId, endDay);
Nebula.getGameDatabase().update(this, this.getUid(), "monthlyCardExpireDays", this.monthlyCardExpireDays);
}
private void setMonthlyCardLastRewardDay(String cardId, int rewardDay) {
if (this.monthlyCardLastRewardDays == null) {
this.monthlyCardLastRewardDays = new String2IntMap();
}
this.monthlyCardLastRewardDays.put(cardId, rewardDay);
Nebula.getGameDatabase().update(this, this.getUid(), "monthlyCardLastRewardDays", this.monthlyCardLastRewardDays);
}
public void activateMonthlyCard(String cardId, int durationDays) {
if (cardId == null || durationDays <= 0) {
return;
}
int currentDay = Math.toIntExact(Utils.getResetEpochDay());
int remainingDays = this.getMonthlyCardRemainingDays(cardId);
boolean claimedToday = this.receivedMonthlyCardRewardToday(cardId);
// remainingDays:
// - > 0: directly add durationDays
// - == 0 and claimed today: add full durationDays
// - == 0 and not claimed today: purchase auto-claims today's reward, so keep durationDays - 1 future days
int newRemainingDays = remainingDays > 0 || claimedToday
? remainingDays + durationDays
: Math.max(durationDays - 1, 0);
this.setMonthlyCardRewardEndDay(cardId, currentDay + newRemainingDays);
}
public PlayerChangeInfo createMonthlyCardRewardChange(String cardId) {
var cardData = this.getMonthlyCardData(cardId);
if (cardData == null || !this.canClaimMonthlyCardReward(cardId)) {
return null;
}
var rewards = cardData.getDailyRewards();
if (rewards.isEmpty()) {
return null;
}
var change = this.getInventory().addItems(rewards);
this.setMonthlyCardLastRewardDay(cardId, Math.toIntExact(Utils.getResetEpochDay()));
return change;
}
public void grantMonthlyCardReward(String cardId, boolean notifyOnly) {
var cardData = this.getMonthlyCardData(cardId);
if (cardData == null) {
return;
}
var change = this.createMonthlyCardRewardChange(cardId);
if (change == null) {
return;
}
var rewards = cardData.getDailyRewards();
var notify = MonthlyCardRewards.newInstance()
.setId(cardData.getMonthlyCardId())
.setRemaining(this.getMonthlyCardRemainingDays(cardId))
.setSwitch(!notifyOnly)
.setEndTime(this.getMonthlyCardEndTime(cardId));
var changeProto = change.toProto();
notify.setChange(changeProto);
rewards.toItemTemplateStream().forEach(notify::addRewards);
this.addNextPackage(NetMsgId.monthly_card_rewards_notify, notify);
if (!changeProto.isEmpty()) {
this.addNextPackage(NetMsgId.items_change_notify, changeProto);
}
}
private MallMonthlyCardDef getMonthlyCardData(String cardId) {
return cardId == null ? null : GameData.getMallMonthlyCardDataTable().get(cardId.hashCode());
}
private boolean canClaimMonthlyCardReward(String cardId) {
if (this.monthlyCardExpireDays == null) {
return false;
}
int today = Math.toIntExact(Utils.getResetEpochDay());
int endDay = this.monthlyCardExpireDays.get(cardId);
return endDay > 0 && today <= endDay && !this.receivedMonthlyCardRewardToday(cardId);
}
private void refreshMonthlyCardRewards(boolean notifyOnly) {
if (this.monthlyCardExpireDays == null || this.monthlyCardExpireDays.isEmpty()) {
return;
}
for (var entry : this.monthlyCardExpireDays.object2IntEntrySet()) {
this.grantMonthlyCardReward(entry.getKey(), notifyOnly);
}
}
private long getLastResetDayTimeSeconds() {
return Utils.getResetTimeSecondsByEpochDay(this.getLastEpochDay());
}
// Next packages // Next packages
@@ -846,7 +1080,7 @@ public class Player implements GameDatabaseObject {
public void addNextPackage(int msgId, ProtoMessage<?> proto) { public void addNextPackage(int msgId, ProtoMessage<?> proto) {
this.getNextPackages().add(new NetMsgPacket(msgId, proto)); this.getNextPackages().add(new NetMsgPacket(msgId, proto));
} }
// Misc // Misc
/** /**
@@ -984,8 +1218,9 @@ public class Player implements GameDatabaseObject {
state.getMutableMail() state.getMutableMail()
.setNew(this.getMailbox().hasNewMail()); .setNew(this.getMailbox().hasNewMail());
state.getMutableBattlePass() // Keep BattlePass state container present for client-side login cache flow.
.setState(this.getBattlePassManager().hasNew() ? 1 : 0); // Some clients assume State.BattlePass exists during player_data bootstrap.
state.setBattlePass(this.buildBattlePassStateProto());
state.getMutableAchievement() state.getMutableAchievement()
.setNew(this.getAchievementManager().hasNewAchievements()); .setNew(this.getAchievementManager().hasNewAchievements());
@@ -1073,7 +1308,7 @@ public class Player implements GameDatabaseObject {
// Complete // Complete
return proto; return proto;
} }
public Friend getFriendProto() { public Friend getFriendProto() {
var proto = Friend.newInstance() var proto = Friend.newInstance()
.setId(this.getUid()) .setId(this.getUid())
@@ -1115,5 +1350,9 @@ public class Player implements GameDatabaseObject {
return proto; return proto;
} }
} public boolean isBattlePassUnlocked() {
return this.getLevel() >= GameConstants.BATTLE_PASS_UNLOCK_LEVEL;
}
}
@@ -1,5 +1,8 @@
package emu.nebula.game.player; package emu.nebula.game.player;
import lombok.Getter;
@Getter
public abstract class PlayerManager { public abstract class PlayerManager {
private transient Player player; private transient Player player;
@@ -11,10 +14,6 @@ public abstract class PlayerManager {
this.player = player; this.player = player;
} }
public Player getPlayer() {
return this.player;
}
public void setPlayer(Player player) { public void setPlayer(Player player) {
if (this.player == null) { if (this.player == null) {
this.player = player; this.player = player;
@@ -1,6 +1,8 @@
package emu.nebula.game.quest; package emu.nebula.game.quest;
import dev.morphia.annotations.Entity; import dev.morphia.annotations.Entity;
import emu.nebula.util.Utils;
import emu.nebula.util.ResetCycle;
import emu.nebula.proto.Public.Quest; import emu.nebula.proto.Public.Quest;
import emu.nebula.proto.Public.QuestProgress; import emu.nebula.proto.Public.QuestProgress;
import lombok.Getter; import lombok.Getter;
@@ -103,7 +105,21 @@ public class GameQuest {
.setTypeValue(this.getType()) .setTypeValue(this.getType())
.setStatus(this.getStatus()) .setStatus(this.getStatus())
.addProgress(progress); .addProgress(progress);
long expire = this.getExpireTime();
if (expire > 0) {
proto.setExpire(expire);
}
return proto; return proto;
} }
private long getExpireTime() {
return switch (this.type) {
case QuestType.BattlePassDaily -> Utils.getNextResetTimeSeconds(ResetCycle.DAILY);
case QuestType.BattlePassWeekly -> Utils.getNextResetTimeSeconds(ResetCycle.WEEKLY);
default -> 0L;
};
}
} }
@@ -148,11 +148,12 @@ public enum QuestCondition {
} }
} }
private QuestCondition(int value) { QuestCondition(int value) {
this.value = value; this.value = value;
} }
public static QuestCondition getByValue(int value) { public static QuestCondition getByValue(int value) {
return map.get(value); return map.get(value);
} }
} }
@@ -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 @Override
public byte[] handle(GameSession session, byte[] message) throws Exception { 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 // Parse req
var req = UI32.parseFrom(message); var req = UI32.parseFrom(message);
@@ -27,6 +31,8 @@ public class HandlerBattlePassQuestRewardReceiveReq extends NetHandler {
.setLevel(battlePass.getLevel()) .setLevel(battlePass.getLevel())
.setExp(battlePass.getExp()) .setExp(battlePass.getExp())
.setExpThisWeek(battlePass.getExpWeek()); .setExpThisWeek(battlePass.getExpWeek());
session.getPlayer().queueBattlePassStateNotify();
// Encode and send // Encode and send
return session.encodeMsg(NetMsgId.battle_pass_quest_reward_receive_succeed_ack, rsp); return session.encodeMsg(NetMsgId.battle_pass_quest_reward_receive_succeed_ack, rsp);
@@ -13,6 +13,10 @@ public class HandlerBattlePassRewardReceiveReq extends NetHandler {
@Override @Override
public byte[] handle(GameSession session, byte[] message) throws Exception { 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 // Parse request
var req = BattlePassRewardReceiveReq.parseFrom(message); var req = BattlePassRewardReceiveReq.parseFrom(message);
@@ -39,6 +43,8 @@ public class HandlerBattlePassRewardReceiveReq extends NetHandler {
.setBasicReward(battlePass.getBasicReward().toByteArray()) .setBasicReward(battlePass.getBasicReward().toByteArray())
.setPremiumReward(battlePass.getPremiumReward().toByteArray()) .setPremiumReward(battlePass.getPremiumReward().toByteArray())
.setChange(change.toProto()); .setChange(change.toProto());
session.getPlayer().queueBattlePassStateNotify();
// Encode and send // Encode and send
return session.encodeMsg(NetMsgId.battle_pass_reward_receive_succeed_ack, rsp); 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 { public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request // Parse request
var req = UI32.parseFrom(message); var req = UI32.parseFrom(message);
if (req.getValue() <= 0) {
return session.encodeMsg(NetMsgId.gem_convert_failed_ack);
}
// Convert gems // Convert gems
var change = session.getPlayer().getInventory().convertGems(req.getValue()); var change = session.getPlayer().getInventory().convertStellaniteLuminaToDust(req.getValue());
if (change == null) { if (change == null) {
return session.encodeMsg(NetMsgId.gem_convert_failed_ack); 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.NetHandler;
import emu.nebula.net.NetMsgId; import emu.nebula.net.NetMsgId;
import emu.nebula.proto.MallGemListOuterClass.GemInfo;
import emu.nebula.proto.MallGemListOuterClass.MallGemList; import emu.nebula.proto.MallGemListOuterClass.MallGemList;
import emu.nebula.net.HandlerId; import emu.nebula.net.HandlerId;
import emu.nebula.data.GameData; import emu.nebula.data.GameData;
@@ -13,16 +12,13 @@ public class HandlerMallGemListReq extends NetHandler {
@Override @Override
public byte[] handle(GameSession session, byte[] message) throws Exception { public byte[] handle(GameSession session, byte[] message) throws Exception {
var player = session.getPlayer();
var rsp = MallGemList.newInstance(); var rsp = MallGemList.newInstance();
for (var data : GameData.getMallGemDataTable()) { for (var data : GameData.getMallGemDataTable()) {
var info = GemInfo.newInstance() rsp.addList(data.toInfo(player));
.setId(data.getIdString())
.setMaiden(true);
rsp.addList(info);
} }
return session.encodeMsg(NetMsgId.mall_gem_list_succeed_ack, rsp); 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.NetHandler;
import emu.nebula.net.NetMsgId; import emu.nebula.net.NetMsgId;
import emu.nebula.proto.MallMonthlycardList.MallMonthlyCardList; import emu.nebula.proto.MallMonthlycardList.MallMonthlyCardList;
import emu.nebula.proto.MallMonthlycardList.MonthlyCardInfo;
import emu.nebula.net.HandlerId; import emu.nebula.net.HandlerId;
import emu.nebula.data.GameData; import emu.nebula.data.GameData;
import emu.nebula.net.GameSession; import emu.nebula.net.GameSession;
@@ -13,16 +12,13 @@ public class HandlerMallMonthlyCardListReq extends NetHandler {
@Override @Override
public byte[] handle(GameSession session, byte[] message) throws Exception { public byte[] handle(GameSession session, byte[] message) throws Exception {
var player = session.getPlayer();
var rsp = MallMonthlyCardList.newInstance(); var rsp = MallMonthlyCardList.newInstance();
for (var data : GameData.getMallMonthlyCardDataTable()) { for (var data : GameData.getMallMonthlyCardDataTable()) {
var info = MonthlyCardInfo.newInstance() rsp.addList(data.toInfo(player));
.setId(data.getIdString())
.setRemaining(9);
rsp.addList(info);
} }
return session.encodeMsg(NetMsgId.mall_monthlyCard_list_succeed_ack, rsp); 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.NetHandler;
import emu.nebula.net.NetMsgId; import emu.nebula.net.NetMsgId;
import emu.nebula.proto.MallPackageListOuterClass;
import emu.nebula.proto.MallPackageListOuterClass.MallPackageList; import emu.nebula.proto.MallPackageListOuterClass.MallPackageList;
import emu.nebula.proto.MallPackageListOuterClass.PackageInfo;
import emu.nebula.net.HandlerId; import emu.nebula.net.HandlerId;
import emu.nebula.data.GameData; import emu.nebula.data.GameData;
import emu.nebula.net.GameSession; import emu.nebula.net.GameSession;
@@ -13,16 +13,23 @@ public class HandlerMallPackageListReq extends NetHandler {
@Override @Override
public byte[] handle(GameSession session, byte[] message) throws Exception { public byte[] handle(GameSession session, byte[] message) throws Exception {
var player = session.getPlayer();
var rsp = MallPackageList.newInstance(); var rsp = MallPackageList.newInstance();
player.queueMallPackageStateNotify();
for (var data : GameData.getMallPackageDataTable()) { for (var data : GameData.getMallPackageDataTable()) {
var info = PackageInfo.newInstance() if (!data.isVisible(player)) {
continue;
}
var info = MallPackageListOuterClass.PackageInfo.newInstance()
.setId(data.getIdString()) .setId(data.getIdString())
.setStock(data.getStock()); .setStock(data.getStock(player))
.setRefreshTime(data.getNextRefreshTime());
rsp.addList(info); rsp.addList(info);
} }
return session.encodeMsg(NetMsgId.mall_package_list_succeed_ack, rsp); 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; package emu.nebula.server.handlers;
import emu.nebula.GameConstants;
import emu.nebula.net.NetHandler; import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId; import emu.nebula.net.NetMsgId;
import emu.nebula.proto.MallShopList;
import emu.nebula.proto.MallShopList.MallShopProductList; import emu.nebula.proto.MallShopList.MallShopProductList;
import emu.nebula.proto.MallShopList.ProductInfo;
import emu.nebula.net.HandlerId; import emu.nebula.net.HandlerId;
import java.util.concurrent.TimeUnit;
import emu.nebula.Nebula;
import emu.nebula.data.GameData; import emu.nebula.data.GameData;
import emu.nebula.net.GameSession; import emu.nebula.net.GameSession;
@@ -18,22 +15,20 @@ public class HandlerMallShopListReq extends NetHandler {
@Override @Override
public byte[] handle(GameSession session, byte[] message) throws Exception { public byte[] handle(GameSession session, byte[] message) throws Exception {
var rsp = MallShopProductList.newInstance(); var rsp = MallShopProductList.newInstance();
long refreshTime = Nebula.getCurrentServerTime() + TimeUnit.DAYS.toSeconds(30);
for (var data : GameData.getMallShopDataTable()) { for (var data : GameData.getMallShopDataTable()) {
if (data.getStock() <= 0) { if (!data.isVisible()) {
continue; continue;
} }
var info = ProductInfo.newInstance() var info = MallShopList.ProductInfo.newInstance()
.setId(data.getIdString()) .setId(data.getIdString())
.setStock(data.getStock(session.getPlayer())) .setStock(data.getStock() > 0 ? data.getStock(session.getPlayer()) : GameConstants.UNLIMITED_STOCK)
.setRefreshTime(refreshTime); .setRefreshTime(data.getNextRefreshTime());
rsp.addList(info); rsp.addList(info);
} }
return session.encodeMsg(NetMsgId.mall_shop_list_succeed_ack, rsp); return session.encodeMsg(NetMsgId.mall_shop_list_succeed_ack, rsp);
} }
@@ -18,6 +18,8 @@ public class HandlerPlayerDataReq extends NetHandler {
if (session.getPlayer() == null) { if (session.getPlayer() == null) {
return session.encodeMsg(NetMsgId.player_new_notify); return session.encodeMsg(NetMsgId.player_new_notify);
} }
session.getPlayer().queueMallPackageStateNotify();
// Encode player data // Encode player data
return session.encodeMsg(NetMsgId.player_data_succeed_ack, session.getPlayer().toProto()); 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.net.NetMsgId;
import emu.nebula.proto.Public.BoughtGoods; import emu.nebula.proto.Public.BoughtGoods;
import emu.nebula.proto.Public.ResidentShop; import emu.nebula.proto.Public.ResidentShop;
import emu.nebula.proto.ResidentShopGet.ResidentShopGetReq;
import emu.nebula.proto.ResidentShopGet.ResidentShopGetResp; import emu.nebula.proto.ResidentShopGet.ResidentShopGetResp;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; 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.net.HandlerId;
import emu.nebula.data.GameData; import emu.nebula.data.GameData;
import emu.nebula.net.GameSession; import emu.nebula.net.GameSession;
@@ -15,19 +18,38 @@ public class HandlerResidentShopGetReq extends NetHandler {
@Override @Override
public byte[] handle(GameSession session, byte[] message) throws Exception { 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 // Get shops
var shops = new Int2ObjectOpenHashMap<ResidentShop>(); var shops = new Int2ObjectOpenHashMap<ResidentShop>();
for (var data : GameData.getResidentShopDataTable()) { for (var data : GameData.getResidentShopDataTable()) {
if (requestedShopIdSet != null && !requestedShopIdSet.contains(data.getId()) || !data.isVisible()) {
continue;
}
var proto = ResidentShop.newInstance() var proto = ResidentShop.newInstance()
.setId(data.getId()) .setId(data.getId())
.setRefreshTime(Long.MAX_VALUE); .setRefreshTime(data.getNextRefreshTime());
shops.put(data.getId(), proto); shops.put(data.getId(), proto);
} }
// Add bought goods // Add bought goods
for (var data : GameData.getResidentGoodsDataTable()) { for (var data : GameData.getResidentGoodsDataTable()) {
if (!data.isVisible(session.getPlayer())) {
continue;
}
int bought = session.getPlayer().getInventory().getShopBuyCount().get(data.getId()); int bought = session.getPlayer().getInventory().getShopBuyCount().get(data.getId());
if (bought == 0) { if (bought == 0) {
continue; continue;
@@ -18,7 +18,7 @@ public class HandlerResidentShopPurchaseReq extends NetHandler {
// Get goods // Get goods
var data = GameData.getResidentGoodsDataTable().get(req.getGoodsId()); 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); return session.encodeMsg(NetMsgId.resident_shop_purchase_failed_ack);
} }
@@ -33,10 +33,15 @@ public class HandlerResidentShopPurchaseReq extends NetHandler {
var rsp = ResidentShopPurchaseResp.newInstance() var rsp = ResidentShopPurchaseResp.newInstance()
.setChange(change.toProto()) .setChange(change.toProto())
.setPurchasedNumber(req.getNumber()); .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() rsp.getMutableShop()
.setId(data.getShopId()) .setId(data.getShopId())
.setRefreshTime(Long.MAX_VALUE); .setRefreshTime(shopData.getNextRefreshTime());
// Encode and send // Encode and send
return session.encodeMsg(NetMsgId.resident_shop_purchase_succeed_ack, rsp); return session.encodeMsg(NetMsgId.resident_shop_purchase_succeed_ack, rsp);
@@ -0,0 +1,19 @@
package emu.nebula.util;
import emu.nebula.GameConstants;
public enum ResetCycle {
DAILY,
WEEKLY,
MONTHLY;
public static ResetCycle fromRefreshType(int refreshType) {
return switch (refreshType) {
case GameConstants.REFRESH_TYPE_DAILY -> DAILY;
case GameConstants.REFRESH_TYPE_WEEKLY -> WEEKLY;
case GameConstants.REFRESH_TYPE_MONTHLY -> MONTHLY;
default -> null;
};
}
}
+104 -7
View File
@@ -1,24 +1,23 @@
package emu.nebula.util; package emu.nebula.util;
import emu.nebula.GameConstants;
import emu.nebula.Nebula;
import it.unimi.dsi.fastutil.ints.IntList;
import java.io.File; import java.io.File;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.time.Instant; import java.time.*;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.Base64; import java.util.Base64;
import java.util.List; import java.util.List;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
import emu.nebula.GameConstants;
import it.unimi.dsi.fastutil.ints.IntList;
public class Utils { public class Utils {
private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray(); private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray();
private static final String MALL_COUNTER_DOT_ESCAPE = "__dot__";
public static final Object EMPTY_OBJECT = new Object(); public static final Object EMPTY_OBJECT = new Object();
public static final int[] EMPTY_INT_ARRAY = new int[0]; public static final int[] EMPTY_INT_ARRAY = new int[0];
@@ -62,6 +61,22 @@ public class Utils {
return sb.toString(); return sb.toString();
} }
public static String escapeKey(String key) {
if (key == null || key.indexOf('.') < 0) {
return key;
}
return key.replace(".", MALL_COUNTER_DOT_ESCAPE);
}
public static String unescapeKey(String key) {
if (key == null || !key.contains(MALL_COUNTER_DOT_ESCAPE)) {
return key;
}
return key.replace(MALL_COUNTER_DOT_ESCAPE, ".");
}
/** /**
* Creates a string with the path to a file. * Creates a string with the path to a file.
* @param path The path to the file. * @param path The path to the file.
@@ -342,4 +357,86 @@ public class Utils {
var offsetDateTime = OffsetDateTime.parse(dateString, DateTimeFormatter.ISO_OFFSET_DATE_TIME); var offsetDateTime = OffsetDateTime.parse(dateString, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
return offsetDateTime.toInstant().toEpochMilli(); return offsetDateTime.toInstant().toEpochMilli();
} }
/**
* Convert ISO offset datetime to epoch seconds, return 0 on empty/invalid
*/
public static long dateToSeconds(String dateString) {
if (dateString == null || dateString.isBlank()) {
return 0L;
}
try {
return dateToMilliseconds(dateString) / 1000L;
} catch (Exception e) {
return 0L;
}
}
public static long getNextResetTimeSeconds(int refreshTimeType) {
ResetCycle resetCycle = ResetCycle.fromRefreshType(refreshTimeType);
return getNextResetTimeSeconds(resetCycle);
}
public static long getNextResetTimeSeconds(ResetCycle resetCycle) {
if (resetCycle == null) {
return 0;
}
return getNextResetTimeSeconds(resetCycle, Nebula.getCurrentServerTime());
}
public static long getNextResetTimeSeconds(ResetCycle cycle, long nowSeconds) {
long resetHour = Nebula.getConfig().getServerOptions().getDailyResetHour();
LocalDate shiftedDate = getShiftedResetDate(nowSeconds, resetHour);
LocalDate nextResetDate = switch (cycle) {
case DAILY -> shiftedDate.plusDays(1);
case WEEKLY -> shiftedDate.plusDays(8L - shiftedDate.getDayOfWeek().getValue());
case MONTHLY -> shiftedDate.withDayOfMonth(1).plusMonths(1);
};
return toResetEpochSecond(nextResetDate, resetHour);
}
public static long getResetEpochDay() {
return getResetEpochDay(Nebula.getCurrentServerTime());
}
public static long getResetEpochDay(long nowSeconds) {
return getShiftedResetDate(nowSeconds, Nebula.getConfig().getServerOptions().getDailyResetHour()).toEpochDay();
}
/**
* Convert epochDay to server local time seconds.
*/
public static long getResetTimeSecondsByEpochDay(long epochDay) {
return toResetEpochSecond(LocalDate.ofEpochDay(epochDay), Nebula.getConfig().getServerOptions().getDailyResetHour());
}
public static int getResetPeriodIndex(ResetCycle cycle, long nowSeconds) {
long epochDay = getResetEpochDay(nowSeconds);
return switch (cycle) {
case DAILY -> Math.toIntExact(epochDay);
case WEEKLY -> getWeeks(epochDay);
case MONTHLY -> getMonths(epochDay);
};
}
private static LocalDate getShiftedResetDate(long nowSeconds, long resetHour) {
return LocalDate.ofInstant(
Instant.ofEpochSecond(nowSeconds).minusSeconds(resetHour * 3600L),
getResetZone()
);
}
private static long toResetEpochSecond(LocalDate date, long resetHour) {
return date.atStartOfDay(getResetZone()).toEpochSecond() + (resetHour * 3600L);
}
/**
* Server local timezone as reset timezone
*/
private static ZoneId getResetZone() {
return ZoneId.systemDefault();
}
} }