mirror of
https://github.com/Melledy/Nebula.git
synced 2026-09-19 17:19:58 +02:00
Fix mall, shop, mall/shop refresh logic and battle pass flows
This commit is contained in:
@@ -121,20 +121,23 @@ public class Config {
|
||||
|
||||
@Getter
|
||||
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("*");
|
||||
// Automatically creates an account when a player logs in for the first time on a new email.
|
||||
public boolean autoCreateAccount = true;
|
||||
// Skips the intro cinematics/stage when starting a new account.
|
||||
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;
|
||||
// Unlocks all story CGs to use in the showcase
|
||||
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.
|
||||
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.
|
||||
public int dailyResetHour = 0;
|
||||
// The local server hour when daily, weekly, and monthly reset boundaries occur.
|
||||
// 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.
|
||||
public int leaderboardRefreshTime = 60;
|
||||
// The welcome mail to send when a player is created. Set to null to disable.
|
||||
|
||||
@@ -18,9 +18,10 @@ public class GameConstants {
|
||||
public static final int DEFAULT_HONOR_ID = 111001;
|
||||
|
||||
public static final int GOLD_ITEM_ID = 1;
|
||||
public static final int GEM_ITEM_ID = 2;
|
||||
public static final int PREM_GEM_ITEM_ID = 3;
|
||||
public static final int ENERGY_BUY_ITEM_ID = GEM_ITEM_ID;
|
||||
public static final int STELLANITE_DUST_ITEM_ID = 2;
|
||||
public static final int PAID_STELLANITE_LUMINA_ITEM_ID = 3;
|
||||
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 WEEKLY_ENTRY_ITEM_ID = 28;
|
||||
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[] {
|
||||
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[][] {
|
||||
new int[] {100, 120},
|
||||
new int[] {200, 150},
|
||||
new int[] {300, 200}
|
||||
};
|
||||
|
||||
public static final int UNLIMITED_STOCK = Integer.MAX_VALUE;
|
||||
|
||||
// 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(100, new ItemParam(GOLD_ITEM_ID, 28888));
|
||||
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));
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
|
||||
public static String getGameVersion() {
|
||||
// Load data version
|
||||
var region = RegionConfig.getRegion(Nebula.getConfig().getRegion());
|
||||
@@ -91,4 +106,5 @@ public class GameConstants {
|
||||
public static int getDataVersion() {
|
||||
return Nebula.getConfig().getCustomDataVersion() > 0 ? Nebula.getConfig().getCustomDataVersion() : DATA_VERSION ;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ public class GameData {
|
||||
@Getter private static DataTable<MallPackageDef> MallPackageDataTable = new DataTable<>();
|
||||
@Getter private static DataTable<MallShopDef> MallShopDataTable = 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<ResidentGoodsDef> ResidentGoodsDataTable = new DataTable<>();
|
||||
@@ -171,4 +172,4 @@ public class GameData {
|
||||
@Getter private static DataTable<ActivityShopDef> ActivityShopDataTable = new DataTable<>();
|
||||
@Getter private static DataTable<ActivityShopControlDef> ActivityShopControlDataTable = 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 lombok.Getter;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
@ResourceType(name = "BattlePass.json")
|
||||
public class BattlePassDef extends BaseDef {
|
||||
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
|
||||
public int getId() {
|
||||
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 Tid3;
|
||||
private int Qty3;
|
||||
private boolean Focus;
|
||||
|
||||
private transient ItemParamMap basicRewards;
|
||||
private transient ItemParamMap premiumRewards;
|
||||
private transient ItemParamMap luxuryRewards;
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
@@ -30,16 +32,26 @@ public class BattlePassRewardDef extends BaseDef {
|
||||
public void onLoad() {
|
||||
this.basicRewards = new ItemParamMap();
|
||||
this.premiumRewards = new ItemParamMap();
|
||||
this.luxuryRewards = new ItemParamMap();
|
||||
|
||||
// Basic rewards (Tid1) - for all players
|
||||
if (this.Tid1 > 0) {
|
||||
this.basicRewards.add(this.Tid1, this.Qty1);
|
||||
}
|
||||
|
||||
// Premium rewards (Tid2) - for both 58 and 98 yuan tiers
|
||||
if (this.Tid2 > 0) {
|
||||
this.premiumRewards.add(this.Tid2, this.Qty2);
|
||||
}
|
||||
|
||||
// Luxury rewards (Tid3) - ONLY for 98 yuan tier
|
||||
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.ResourceType;
|
||||
import emu.nebula.game.inventory.ItemParamMap;
|
||||
import emu.nebula.game.player.Player;
|
||||
import emu.nebula.proto.MallGemListOuterClass.GemInfo;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@@ -11,13 +14,51 @@ import lombok.Getter;
|
||||
public class MallGemDef extends BaseDef {
|
||||
@SerializedName("Id")
|
||||
private String IdString;
|
||||
private int Stock;
|
||||
private int ItemId;
|
||||
private int CurrencyItemId;
|
||||
private int ItemQty;
|
||||
|
||||
private int BaseItemId;
|
||||
private int BaseItemQty;
|
||||
private int ExperiencedBonusItemId;
|
||||
private int ExperiencedBonusItemQty;
|
||||
private int MaidenBonusItemID;
|
||||
private int MaidenBonusItemQty;
|
||||
private int Price;
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
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 emu.nebula.data.BaseDef;
|
||||
import emu.nebula.data.GameData;
|
||||
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;
|
||||
|
||||
@Getter
|
||||
@@ -15,9 +19,81 @@ public class MallMonthlyCardDef extends BaseDef {
|
||||
private int Price;
|
||||
private int BaseItemId;
|
||||
private int BaseItemQty;
|
||||
private int MaxDays;
|
||||
|
||||
private transient ItemParamMap products;
|
||||
private transient ItemParamMap dailyRewards;
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
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;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import emu.nebula.GameConstants;
|
||||
import emu.nebula.data.BaseDef;
|
||||
import emu.nebula.data.ResourceType;
|
||||
import emu.nebula.Nebula;
|
||||
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;
|
||||
|
||||
@Getter
|
||||
@ResourceType(name = "MallPackage.json")
|
||||
public class MallPackageDef extends BaseDef {
|
||||
|
||||
@SerializedName("Id")
|
||||
private String IdString;
|
||||
private int Stock;
|
||||
private int CurrencyType;
|
||||
private int CurrencyItemId;
|
||||
private int CurrencyItemQty;
|
||||
private int Tag;
|
||||
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 long listTimeSeconds;
|
||||
private transient long delistTimeSeconds;
|
||||
private transient int[] orderCondParams;
|
||||
private transient int[] listCondParams;
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
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
|
||||
public void onLoad() {
|
||||
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;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import emu.nebula.GameConstants;
|
||||
import emu.nebula.Nebula;
|
||||
import emu.nebula.data.BaseDef;
|
||||
import emu.nebula.data.ResourceType;
|
||||
import emu.nebula.game.inventory.ItemParamMap;
|
||||
import emu.nebula.game.player.Player;
|
||||
import emu.nebula.util.Utils;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@ResourceType(name = "MallShop.json")
|
||||
public class MallShopDef extends BaseDef {
|
||||
@SerializedName("Id")
|
||||
@@ -20,8 +24,13 @@ public class MallShopDef extends BaseDef {
|
||||
|
||||
private int ItemId;
|
||||
private int ItemQty;
|
||||
private String ListTime;
|
||||
private String DeListTime;
|
||||
private int RefreshType;
|
||||
|
||||
private transient ItemParamMap products;
|
||||
private transient long listTimeSeconds;
|
||||
private transient long delistTimeSeconds;
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
@@ -29,15 +38,46 @@ public class MallShopDef extends BaseDef {
|
||||
}
|
||||
|
||||
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
|
||||
public void onLoad() {
|
||||
this.products = new ItemParamMap();
|
||||
this.listTimeSeconds = Utils.dateToSeconds(this.ListTime);
|
||||
this.delistTimeSeconds = Utils.dateToSeconds(this.DeListTime);
|
||||
|
||||
if (this.ItemId > 0) {
|
||||
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.game.inventory.ItemParamMap;
|
||||
import emu.nebula.game.player.Player;
|
||||
import emu.nebula.util.JsonUtils;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@@ -18,24 +19,36 @@ public class ResidentGoodsDef extends BaseDef {
|
||||
|
||||
private int CurrencyItemId;
|
||||
private int Price;
|
||||
|
||||
private int AppearCondType;
|
||||
private String AppearCondParams;
|
||||
|
||||
private transient ItemParamMap products;
|
||||
|
||||
private transient int[] appearCondParams;
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
return Id;
|
||||
}
|
||||
|
||||
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
|
||||
public void onLoad() {
|
||||
this.products = new ItemParamMap();
|
||||
this.appearCondParams = JsonUtils.decode(this.AppearCondParams, int[].class);
|
||||
|
||||
if (this.ItemId > 0) {
|
||||
this.products.add(this.ItemId, this.ItemQuantity);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,16 +1,47 @@
|
||||
package emu.nebula.data.resources;
|
||||
|
||||
import emu.nebula.Nebula;
|
||||
import emu.nebula.data.BaseDef;
|
||||
import emu.nebula.data.ResourceType;
|
||||
import emu.nebula.util.Utils;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@ResourceType(name = "ResidentShop.json")
|
||||
public class ResidentShopDef extends BaseDef {
|
||||
private int Id;
|
||||
|
||||
private int RefreshTimeType;
|
||||
private int RefreshInterval;
|
||||
private String OpenTime;
|
||||
|
||||
private transient long openTimeSeconds;
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,21 +1,20 @@
|
||||
package emu.nebula.game;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import emu.nebula.GameConstants;
|
||||
import emu.nebula.Nebula;
|
||||
import emu.nebula.game.activity.ActivityModule;
|
||||
import emu.nebula.game.ban.BanModule;
|
||||
import emu.nebula.game.gacha.GachaModule;
|
||||
import emu.nebula.game.jointdrill.JointDrillModule;
|
||||
import emu.nebula.game.mall.PayModule;
|
||||
import emu.nebula.game.player.PlayerModule;
|
||||
import emu.nebula.game.scoreboss.ScoreBossModule;
|
||||
import emu.nebula.game.tutorial.TutorialModule;
|
||||
import emu.nebula.net.GameSession;
|
||||
import emu.nebula.util.ResetCycle;
|
||||
import emu.nebula.util.Utils;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||
@@ -34,14 +33,15 @@ public class GameContext implements Runnable {
|
||||
private final ScoreBossModule scoreBossModule;
|
||||
private final JointDrillModule jointDrillModule;
|
||||
private final BanModule banModule;
|
||||
private final PayModule payModule;
|
||||
|
||||
// Game loop
|
||||
private final ScheduledExecutorService scheduler;
|
||||
|
||||
// Daily
|
||||
private long epochDays;
|
||||
private int epochWeeks;
|
||||
private int epochMonths;
|
||||
// Reset-period indexes aligned to the configured daily reset boundary.
|
||||
private long resetDays;
|
||||
private int resetWeeks;
|
||||
private int resetMonths;
|
||||
|
||||
public GameContext() {
|
||||
// Create session map
|
||||
@@ -55,6 +55,7 @@ public class GameContext implements Runnable {
|
||||
this.scoreBossModule = new ScoreBossModule(this);
|
||||
this.jointDrillModule = new JointDrillModule(this);
|
||||
this.banModule = new BanModule(this);
|
||||
this.payModule = new PayModule(this);
|
||||
|
||||
// Run game loop
|
||||
this.scheduler = Executors.newScheduledThreadPool(1);
|
||||
@@ -76,7 +77,7 @@ public class GameContext implements Runnable {
|
||||
}
|
||||
|
||||
// Generate token
|
||||
String token = null;
|
||||
String token;
|
||||
|
||||
do {
|
||||
token = session.generateToken();
|
||||
@@ -107,20 +108,14 @@ public class GameContext implements Runnable {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// Check daily - Update epoch days
|
||||
long offset = Nebula.getConfig().getServerOptions().getDailyResetHour() * -3600;
|
||||
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();
|
||||
long lastResetDays = this.resetDays;
|
||||
this.resetDays = Utils.getResetEpochDay();
|
||||
|
||||
// Check if the day was changed
|
||||
if (this.epochDays > lastEpochDays) {
|
||||
if (this.resetDays > lastResetDays) {
|
||||
// Update epoch weeks/months
|
||||
this.epochWeeks = Utils.getWeeks(this.epochDays);
|
||||
this.epochMonths = Utils.getMonths(this.epochDays);
|
||||
this.resetWeeks = Utils.getResetPeriodIndex(ResetCycle.WEEKLY, Nebula.getCurrentServerTime());
|
||||
this.resetMonths = Utils.getResetPeriodIndex(ResetCycle.MONTHLY, Nebula.getCurrentServerTime());
|
||||
|
||||
// Update score boss season
|
||||
this.getScoreBossModule().updateSeason();
|
||||
@@ -145,8 +140,7 @@ public class GameContext implements Runnable {
|
||||
if (player == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
player.checkResetDailies();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,11 +148,12 @@ public enum AchievementCondition {
|
||||
}
|
||||
}
|
||||
|
||||
private AchievementCondition(int value) {
|
||||
AchievementCondition(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static AchievementCondition getByValue(int value) {
|
||||
return map.get(value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
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.Id;
|
||||
|
||||
import emu.nebula.GameConstants;
|
||||
import emu.nebula.Nebula;
|
||||
import emu.nebula.data.GameData;
|
||||
import emu.nebula.data.resources.BattlePassDef;
|
||||
import emu.nebula.data.resources.BattlePassRewardDef;
|
||||
import emu.nebula.database.GameDatabaseObject;
|
||||
import emu.nebula.game.inventory.ItemParamMap;
|
||||
@@ -20,10 +16,15 @@ import emu.nebula.game.quest.QuestType;
|
||||
import emu.nebula.net.NetMsgId;
|
||||
import emu.nebula.proto.BattlePassInfoOuterClass.BattlePassInfo;
|
||||
import emu.nebula.util.Bitset;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity(value = "battlepass", useDiscriminator = false)
|
||||
public class BattlePass implements GameDatabaseObject {
|
||||
@Id
|
||||
@@ -49,7 +50,7 @@ public class BattlePass implements GameDatabaseObject {
|
||||
public BattlePass(BattlePassManager manager) {
|
||||
this.uid = manager.getPlayerUid();
|
||||
this.manager = manager;
|
||||
this.battlePassId = GameConstants.BATTLE_PASS_ID;
|
||||
this.battlePassId = getActiveBattlePassId();
|
||||
this.basicReward = new Bitset();
|
||||
this.premiumReward = new Bitset();
|
||||
|
||||
@@ -63,8 +64,22 @@ public class BattlePass implements GameDatabaseObject {
|
||||
this.save();
|
||||
}
|
||||
|
||||
public void setManager(BattlePassManager manager) {
|
||||
this.manager = manager;
|
||||
private static int getActiveBattlePassId() {
|
||||
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() {
|
||||
@@ -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() {
|
||||
// Check if any quests are complete but unclaimed
|
||||
public synchronized boolean hasClaimableQuest() {
|
||||
if (!this.getPlayer().isBattlePassUnlocked()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var nextLevelData = GameData.getBattlePassLevelDataTable().get(this.getLevel() + 1);
|
||||
if (nextLevelData == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var quest : getQuests().values()) {
|
||||
if (quest.isComplete() && !quest.isClaimed()) {
|
||||
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++) {
|
||||
if (!this.getBasicReward().isSet(i)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
if (this.isPremium() && !this.getPremiumReward().isSet(i)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// No claimable things
|
||||
|
||||
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) {
|
||||
// Reset daily quests
|
||||
@@ -190,7 +238,7 @@ public class BattlePass implements GameDatabaseObject {
|
||||
* Update this quest on the player client
|
||||
*/
|
||||
private void syncQuest(GameQuest quest) {
|
||||
if (!getPlayer().hasSession()) {
|
||||
if (!getPlayer().hasSession() || !getPlayer().isBattlePassUnlocked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -269,7 +317,7 @@ public class BattlePass implements GameDatabaseObject {
|
||||
|
||||
public PlayerChangeInfo receiveReward(boolean premium, int levelId) {
|
||||
// Get bitset
|
||||
Bitset rewards = null;
|
||||
Bitset rewards;
|
||||
|
||||
if (premium) {
|
||||
rewards = this.getPremiumReward();
|
||||
@@ -296,7 +344,13 @@ public class BattlePass implements GameDatabaseObject {
|
||||
|
||||
// Add items
|
||||
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 {
|
||||
return getPlayer().getInventory().addItems(data.getBasicRewards());
|
||||
}
|
||||
@@ -308,38 +362,29 @@ public class BattlePass implements GameDatabaseObject {
|
||||
|
||||
// Get unclaimed rewards
|
||||
for (int i = 1; i <= this.getLevel(); i++) {
|
||||
// Cache reward data
|
||||
BattlePassRewardDef data = null;
|
||||
|
||||
// Basic reward
|
||||
if (!this.getBasicReward().isSet(i)) {
|
||||
// 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());
|
||||
}
|
||||
boolean claimBasic = !this.getBasicReward().isSet(i);
|
||||
boolean claimPremium = this.isPremium() && !this.getPremiumReward().isSet(i);
|
||||
|
||||
if (!claimBasic && !claimPremium) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Premium reward
|
||||
if (this.isPremium() && !this.getPremiumReward().isSet(i)) {
|
||||
// Set flag
|
||||
|
||||
BattlePassRewardDef data = this.getRewardData(i);
|
||||
if (data == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (claimBasic) {
|
||||
this.getBasicReward().setBit(i);
|
||||
rewards.add(data.getBasicRewards());
|
||||
}
|
||||
|
||||
if (claimPremium) {
|
||||
this.getPremiumReward().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.getPremiumRewards());
|
||||
rewards.add(data.getPremiumRewards());
|
||||
|
||||
if (this.getMode() >= 2 && data.hasLuxuryRewards()) {
|
||||
rewards.add(data.getLuxuryRewards());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -358,6 +403,20 @@ public class BattlePass implements GameDatabaseObject {
|
||||
// Proto
|
||||
|
||||
public BattlePassInfo toProto() {
|
||||
// Return a locked/empty snapshot until the battle pass feature is unlocked,
|
||||
// so the client cannot derive local quest or reward red dots from battle pass data.
|
||||
if (!this.getPlayer().isBattlePassUnlocked()) {
|
||||
return BattlePassInfo.newInstance()
|
||||
.setId(0)
|
||||
.setLevel(0)
|
||||
.setMode(0)
|
||||
.setExp(0)
|
||||
.setExpThisWeek(0)
|
||||
.setDeadline(0L)
|
||||
.setBasicReward()
|
||||
.setPremiumReward();
|
||||
}
|
||||
|
||||
var proto = BattlePassInfo.newInstance()
|
||||
.setId(this.getBattlePassId())
|
||||
.setLevel(this.getLevel())
|
||||
@@ -367,10 +426,10 @@ public class BattlePass implements GameDatabaseObject {
|
||||
.setDeadline(Long.MAX_VALUE)
|
||||
.setBasicReward(this.getBasicReward().toByteArray())
|
||||
.setPremiumReward(this.getPremiumReward().toByteArray());
|
||||
|
||||
|
||||
var daily = proto.getMutableDailyQuests();
|
||||
var weekly = proto.getMutableWeeklyQuests();
|
||||
|
||||
|
||||
for (var quest : this.getQuests().values()) {
|
||||
if (quest.getType() == QuestType.BattlePassDaily) {
|
||||
daily.addList(quest.toProto());
|
||||
@@ -378,7 +437,8 @@ public class BattlePass implements GameDatabaseObject {
|
||||
weekly.addList(quest.toProto());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return proto;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,10 +12,6 @@ public class BattlePassManager extends PlayerManager {
|
||||
public BattlePassManager(Player player) {
|
||||
super(player);
|
||||
}
|
||||
|
||||
public boolean hasNew() {
|
||||
return this.getBattlePass().hasNew();
|
||||
}
|
||||
|
||||
// Database
|
||||
|
||||
|
||||
@@ -1,43 +1,37 @@
|
||||
package emu.nebula.game.inventory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.mongodb.client.model.Filters;
|
||||
|
||||
import dev.morphia.annotations.Entity;
|
||||
import dev.morphia.annotations.Id;
|
||||
import emu.nebula.GameConstants;
|
||||
import emu.nebula.Nebula;
|
||||
import emu.nebula.data.GameData;
|
||||
import emu.nebula.data.resources.DropPkgDef;
|
||||
import emu.nebula.data.resources.MallShopDef;
|
||||
import emu.nebula.data.resources.ResidentGoodsDef;
|
||||
import emu.nebula.data.resources.*;
|
||||
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.quest.QuestCondition;
|
||||
import emu.nebula.net.NetMsgId;
|
||||
import emu.nebula.proto.Notify.Skin;
|
||||
import emu.nebula.proto.Public.Honor;
|
||||
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.proto.Public.*;
|
||||
import emu.nebula.util.Utils;
|
||||
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.IntOpenHashSet;
|
||||
import it.unimi.dsi.fastutil.ints.IntSet;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.lang.String;
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
@Entity(value = "inventory", useDiscriminator = false)
|
||||
public class Inventory extends PlayerManager implements GameDatabaseObject {
|
||||
@Id
|
||||
private int uid;
|
||||
|
||||
|
||||
// Items/resources
|
||||
private ItemParamMap items;
|
||||
private ItemParamMap resources;
|
||||
@@ -51,8 +45,15 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
|
||||
// Buy limit
|
||||
private ItemParamMap shopBuyCount;
|
||||
private String2IntMap mallBuyCount;
|
||||
|
||||
@Deprecated
|
||||
private String2IntMap mallPackageBuyCount;
|
||||
/**
|
||||
* 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() {
|
||||
// Morphia only
|
||||
}
|
||||
@@ -73,7 +74,10 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
|
||||
|
||||
this.shopBuyCount = new ItemParamMap();
|
||||
this.mallBuyCount = new String2IntMap();
|
||||
|
||||
this.mallPackageBuyCount = new String2IntMap();
|
||||
this.gemMaidenClaimed = new String2IntMap();
|
||||
this.monthlyCardBuyCount = new String2IntMap();
|
||||
|
||||
// Add player heads
|
||||
this.getHeadIcons().add(101);
|
||||
this.getHeadIcons().add(102);
|
||||
@@ -562,6 +566,29 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
|
||||
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 -> {
|
||||
// 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) {
|
||||
// Sanity check
|
||||
@@ -726,7 +753,7 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
|
||||
}
|
||||
|
||||
// Get materials
|
||||
var materials = data.getMaterials().mulitply(num);
|
||||
var materials = data.getMaterials().multiply(num);
|
||||
|
||||
// Verify that we have the materials
|
||||
if (!this.hasItems(materials)) {
|
||||
@@ -774,8 +801,7 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
|
||||
|
||||
public PlayerChangeInfo buyMallItem(MallShopDef data, int buyCount) {
|
||||
// Check stock
|
||||
int stock = data.getStock(this.getPlayer());
|
||||
if (buyCount > stock) {
|
||||
if (!data.canPurchase(this.getPlayer(), buyCount)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -787,14 +813,14 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
|
||||
}
|
||||
|
||||
// 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(
|
||||
this,
|
||||
getUid(),
|
||||
"mallBuyCount." + data.getIdString(),
|
||||
getMallBuyCount().get(data.getIdString())
|
||||
);
|
||||
|
||||
this,
|
||||
getUid(),
|
||||
"mallBuyCount",
|
||||
this.getMallBuyCount());
|
||||
|
||||
// Return
|
||||
return change;
|
||||
}
|
||||
@@ -846,7 +872,7 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
|
||||
this.removeItem(currencyId, cost, change);
|
||||
|
||||
// Add items
|
||||
this.addItems(buyItems.mulitply(buyCount), change);
|
||||
this.addItems(buyItems.multiply(buyCount), change);
|
||||
|
||||
// Success
|
||||
return change.setSuccess(true);
|
||||
@@ -879,7 +905,7 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
|
||||
switch (data.getUseAction()) {
|
||||
case 2 -> {
|
||||
// Add items
|
||||
this.addItems(data.getUseParams().mulitply(count), change);
|
||||
this.addItems(data.getUseParams().multiply(count), change);
|
||||
|
||||
// Success
|
||||
success = true;
|
||||
@@ -912,18 +938,18 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
|
||||
return change.setSuccess(true);
|
||||
}
|
||||
|
||||
public PlayerChangeInfo convertGems(int amount) {
|
||||
// Verify that we have the gems
|
||||
if (!this.hasItem(GameConstants.PREM_GEM_ITEM_ID, amount)) {
|
||||
public PlayerChangeInfo convertStellaniteLuminaToDust(int amount) {
|
||||
// Verify that we have enough stellanite lumina in the combined paid/free pool.
|
||||
if (!this.hasMallPackageCurrency(GameConstants.FREE_STELLANITE_LUMINA_ITEM_ID, amount)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create change info
|
||||
var change = new PlayerChangeInfo();
|
||||
|
||||
// Convert gems
|
||||
this.removeItem(GameConstants.PREM_GEM_ITEM_ID, amount, change);
|
||||
this.addItem(GameConstants.GEM_ITEM_ID, amount, change);
|
||||
// Convert stellanite lumina into stellanite dust.
|
||||
this.consumeMallPackageCurrency(GameConstants.FREE_STELLANITE_LUMINA_ITEM_ID, amount, change);
|
||||
this.addItem(GameConstants.STELLANITE_DUST_ITEM_ID, amount, change);
|
||||
|
||||
// Success
|
||||
return change.setSuccess(true);
|
||||
@@ -942,7 +968,207 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
|
||||
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
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@@ -990,7 +1216,22 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
|
||||
// Set to save inventory to database
|
||||
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
|
||||
if (save) {
|
||||
this.save();
|
||||
|
||||
@@ -54,10 +54,10 @@ public class ItemParamMap extends Int2IntLinkedOpenHashMap implements ObjectBidi
|
||||
|
||||
/**
|
||||
* Returns a new ItemParamMap with item amounts multiplied
|
||||
* @param mult Value to multiply all item amounts in this map by
|
||||
* @return
|
||||
* @param multiplier Value to multiply all item amounts in this map by
|
||||
* @return ItemParamMap
|
||||
*/
|
||||
public ItemParamMap mulitply(int multiplier) {
|
||||
public ItemParamMap multiply(int multiplier) {
|
||||
var params = new ItemParamMap();
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,15 +1,13 @@
|
||||
package emu.nebula.game.player;
|
||||
|
||||
import java.util.Stack;
|
||||
|
||||
import dev.morphia.annotations.AlsoLoad;
|
||||
import dev.morphia.annotations.Entity;
|
||||
import dev.morphia.annotations.Id;
|
||||
import dev.morphia.annotations.Indexed;
|
||||
|
||||
import emu.nebula.GameConstants;
|
||||
import emu.nebula.Nebula;
|
||||
import emu.nebula.data.GameData;
|
||||
import emu.nebula.data.resources.MallMonthlyCardDef;
|
||||
import emu.nebula.database.GameDatabaseObject;
|
||||
import emu.nebula.game.account.Account;
|
||||
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.NetMsgId;
|
||||
import emu.nebula.net.NetMsgPacket;
|
||||
import emu.nebula.proto.Notify.MonthlyCardRewards;
|
||||
import emu.nebula.proto.Notify.SigninRewardUpdate;
|
||||
import emu.nebula.proto.PlayerData.DictionaryEntry;
|
||||
import emu.nebula.proto.PlayerData.DictionaryTab;
|
||||
import emu.nebula.proto.PlayerData.PlayerInfo;
|
||||
import emu.nebula.proto.Public.CharShow;
|
||||
import emu.nebula.proto.Public.Energy;
|
||||
import emu.nebula.proto.Public.Friend;
|
||||
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.proto.Public;
|
||||
import emu.nebula.proto.Public.*;
|
||||
import emu.nebula.util.ResetCycle;
|
||||
import emu.nebula.util.Utils;
|
||||
import emu.nebula.proto.Public.Title;
|
||||
|
||||
import emu.nebula.util.ints.String2IntMap;
|
||||
import lombok.Getter;
|
||||
import us.hebi.quickbuf.ProtoMessage;
|
||||
import us.hebi.quickbuf.RepeatedInt;
|
||||
|
||||
import java.lang.String;
|
||||
import java.util.Stack;
|
||||
|
||||
@Getter
|
||||
@Entity(value = "players", useDiscriminator = false)
|
||||
public class Player implements GameDatabaseObject {
|
||||
@@ -92,6 +86,8 @@ public class Player implements GameDatabaseObject {
|
||||
private long lastEpochDay;
|
||||
private long lastLogin;
|
||||
private long createTime;
|
||||
private String2IntMap monthlyCardExpireDays;
|
||||
private String2IntMap monthlyCardLastRewardDays;
|
||||
|
||||
// Managers
|
||||
private final transient CharacterStorage characters;
|
||||
@@ -165,6 +161,8 @@ public class Player implements GameDatabaseObject {
|
||||
this.level = 1;
|
||||
this.energy = 240;
|
||||
this.energyLastUpdate = this.createTime;
|
||||
this.monthlyCardExpireDays = new String2IntMap();
|
||||
this.monthlyCardLastRewardDays = new String2IntMap();
|
||||
|
||||
// Setup inventory
|
||||
this.inventory = new Inventory(this);
|
||||
@@ -217,6 +215,8 @@ public class Player implements GameDatabaseObject {
|
||||
}
|
||||
|
||||
public void setLevel(int level) {
|
||||
int oldLevel = this.level;
|
||||
|
||||
// Set player world class (level)
|
||||
this.level = level;
|
||||
|
||||
@@ -225,6 +225,10 @@ public class Player implements GameDatabaseObject {
|
||||
|
||||
// Trigger achievement
|
||||
this.trigger(AchievementCondition.WorldClassSpecific, this.getLevel());
|
||||
|
||||
if (oldLevel != this.level) {
|
||||
this.queueBattlePassUnlockNotify(oldLevel);
|
||||
}
|
||||
}
|
||||
|
||||
public void setExp(int exp) {
|
||||
@@ -547,6 +551,8 @@ public class Player implements GameDatabaseObject {
|
||||
|
||||
// Trigger achievement
|
||||
this.trigger(AchievementCondition.WorldClassSpecific, this.getLevel());
|
||||
|
||||
this.queueBattlePassUnlockNotify(oldLevel);
|
||||
}
|
||||
|
||||
// Calculate changes
|
||||
@@ -555,7 +561,7 @@ public class Player implements GameDatabaseObject {
|
||||
.setExpChange(this.getExp() - oldExp);
|
||||
|
||||
changes.add(proto);
|
||||
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
@@ -637,26 +643,29 @@ public class Player implements GameDatabaseObject {
|
||||
// Dailies
|
||||
|
||||
public void checkResetDailies() {
|
||||
// Sanity check to make sure daily reset isnt being triggered wrong
|
||||
if (Nebula.getGameContext().getEpochDays() <= this.getLastEpochDay()) {
|
||||
long currentResetDay = Utils.getResetEpochDay();
|
||||
|
||||
// Sanity check to make sure daily reset isn't being triggered wrong
|
||||
if (currentResetDay <= this.getLastEpochDay()) {
|
||||
// Fix sign-in index
|
||||
// TODO remove later
|
||||
if (this.getSignInIndex() <= 0) {
|
||||
this.getSignInRewards(false);
|
||||
}
|
||||
|
||||
this.refreshMonthlyCardRewards(false);
|
||||
|
||||
// End
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if week has changed (Resets on monday)
|
||||
// TODO add a config option
|
||||
int curWeek = Utils.getWeeks(this.getLastEpochDay());
|
||||
boolean hasWeekChanged = Nebula.getGameContext().getEpochWeeks() > curWeek;
|
||||
// Check if week has changed (Resets on Monday)
|
||||
int curWeek = Utils.getResetPeriodIndex(ResetCycle.WEEKLY, this.getLastResetDayTimeSeconds());
|
||||
boolean hasWeekChanged = Utils.getResetPeriodIndex(ResetCycle.WEEKLY, Nebula.getCurrentServerTime()) > curWeek;
|
||||
|
||||
// Check if month was changed
|
||||
int curMonth = Utils.getMonths(this.getLastEpochDay());
|
||||
boolean hasMonthChanged = Nebula.getGameContext().getEpochMonths() > curMonth;
|
||||
int curMonth = Utils.getResetPeriodIndex(ResetCycle.MONTHLY, this.getLastResetDayTimeSeconds());
|
||||
boolean hasMonthChanged = Utils.getResetPeriodIndex(ResetCycle.MONTHLY, Nebula.getCurrentServerTime()) > curMonth;
|
||||
|
||||
// Reset dailies
|
||||
this.resetDailies(hasWeekChanged, hasMonthChanged);
|
||||
@@ -666,9 +675,10 @@ public class Player implements GameDatabaseObject {
|
||||
|
||||
// Give sign-in rewards
|
||||
this.getSignInRewards(hasMonthChanged);
|
||||
this.refreshMonthlyCardRewards(true);
|
||||
|
||||
// Update last epoch day
|
||||
this.lastEpochDay = Nebula.getGameContext().getEpochDays();
|
||||
this.lastEpochDay = currentResetDay;
|
||||
Nebula.getGameDatabase().update(this, this.getUid(), "lastEpochDay", this.lastEpochDay);
|
||||
}
|
||||
|
||||
@@ -680,7 +690,7 @@ public class Player implements GameDatabaseObject {
|
||||
|
||||
// Get next sign-in index
|
||||
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);
|
||||
if (data == null) {
|
||||
@@ -725,12 +735,14 @@ public class Player implements GameDatabaseObject {
|
||||
|
||||
// Reset weekly tower tickets
|
||||
this.getProgress().clearWeeklyTowerTicketLog();
|
||||
this.getInventory().resetWeeklyMallPackagePurchases();
|
||||
}
|
||||
|
||||
// Check if we need to reset monthly
|
||||
if (resetMonthly) {
|
||||
// Reset monthly shop purchases
|
||||
this.getInventory().resetShopPurchases();
|
||||
this.getInventory().resetMonthlyMallPackagePurchases();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -809,6 +821,16 @@ public class Player implements GameDatabaseObject {
|
||||
this.showChars = new int[3];
|
||||
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
|
||||
this.getActivityManager().init();
|
||||
@@ -829,13 +851,225 @@ public class Player implements GameDatabaseObject {
|
||||
// Fix any broken honor ids
|
||||
this.checkBrokenHonor();
|
||||
|
||||
// Update activities
|
||||
this.getActivityManager().onLogin();
|
||||
|
||||
// Update last login time
|
||||
this.lastLogin = System.currentTimeMillis();
|
||||
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
|
||||
|
||||
@@ -846,7 +1080,7 @@ public class Player implements GameDatabaseObject {
|
||||
public void addNextPackage(int msgId, ProtoMessage<?> proto) {
|
||||
this.getNextPackages().add(new NetMsgPacket(msgId, proto));
|
||||
}
|
||||
|
||||
|
||||
// Misc
|
||||
|
||||
/**
|
||||
@@ -984,8 +1218,9 @@ public class Player implements GameDatabaseObject {
|
||||
state.getMutableMail()
|
||||
.setNew(this.getMailbox().hasNewMail());
|
||||
|
||||
state.getMutableBattlePass()
|
||||
.setState(this.getBattlePassManager().hasNew() ? 1 : 0);
|
||||
// Keep BattlePass state container present for client-side login cache flow.
|
||||
// Some clients assume State.BattlePass exists during player_data bootstrap.
|
||||
state.setBattlePass(this.buildBattlePassStateProto());
|
||||
|
||||
state.getMutableAchievement()
|
||||
.setNew(this.getAchievementManager().hasNewAchievements());
|
||||
@@ -1073,7 +1308,7 @@ public class Player implements GameDatabaseObject {
|
||||
// Complete
|
||||
return proto;
|
||||
}
|
||||
|
||||
|
||||
public Friend getFriendProto() {
|
||||
var proto = Friend.newInstance()
|
||||
.setId(this.getUid())
|
||||
@@ -1115,5 +1350,9 @@ public class Player implements GameDatabaseObject {
|
||||
|
||||
return proto;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public boolean isBattlePassUnlocked() {
|
||||
return this.getLevel() >= GameConstants.BATTLE_PASS_UNLOCK_LEVEL;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package emu.nebula.game.player;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public abstract class PlayerManager {
|
||||
private transient Player player;
|
||||
|
||||
@@ -11,10 +14,6 @@ public abstract class PlayerManager {
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
public Player getPlayer() {
|
||||
return this.player;
|
||||
}
|
||||
|
||||
public void setPlayer(Player player) {
|
||||
if (this.player == null) {
|
||||
this.player = player;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package emu.nebula.game.quest;
|
||||
|
||||
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.QuestProgress;
|
||||
import lombok.Getter;
|
||||
@@ -103,7 +105,21 @@ public class GameQuest {
|
||||
.setTypeValue(this.getType())
|
||||
.setStatus(this.getStatus())
|
||||
.addProgress(progress);
|
||||
|
||||
long expire = this.getExpireTime();
|
||||
if (expire > 0) {
|
||||
proto.setExpire(expire);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public static QuestCondition getByValue(int 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
|
||||
public byte[] handle(GameSession session, byte[] message) throws Exception {
|
||||
if (!session.getPlayer().isBattlePassUnlocked()) {
|
||||
return session.encodeMsg(NetMsgId.battle_pass_quest_reward_receive_failed_ack);
|
||||
}
|
||||
|
||||
// Parse req
|
||||
var req = UI32.parseFrom(message);
|
||||
|
||||
@@ -27,6 +31,8 @@ public class HandlerBattlePassQuestRewardReceiveReq extends NetHandler {
|
||||
.setLevel(battlePass.getLevel())
|
||||
.setExp(battlePass.getExp())
|
||||
.setExpThisWeek(battlePass.getExpWeek());
|
||||
|
||||
session.getPlayer().queueBattlePassStateNotify();
|
||||
|
||||
// Encode and send
|
||||
return session.encodeMsg(NetMsgId.battle_pass_quest_reward_receive_succeed_ack, rsp);
|
||||
|
||||
@@ -13,6 +13,10 @@ public class HandlerBattlePassRewardReceiveReq extends NetHandler {
|
||||
|
||||
@Override
|
||||
public byte[] handle(GameSession session, byte[] message) throws Exception {
|
||||
if (!session.getPlayer().isBattlePassUnlocked()) {
|
||||
return session.encodeMsg(NetMsgId.battle_pass_reward_receive_failed_ack);
|
||||
}
|
||||
|
||||
// Parse request
|
||||
var req = BattlePassRewardReceiveReq.parseFrom(message);
|
||||
|
||||
@@ -39,6 +43,8 @@ public class HandlerBattlePassRewardReceiveReq extends NetHandler {
|
||||
.setBasicReward(battlePass.getBasicReward().toByteArray())
|
||||
.setPremiumReward(battlePass.getPremiumReward().toByteArray())
|
||||
.setChange(change.toProto());
|
||||
|
||||
session.getPlayer().queueBattlePassStateNotify();
|
||||
|
||||
// Encode and send
|
||||
return session.encodeMsg(NetMsgId.battle_pass_reward_receive_succeed_ack, rsp);
|
||||
|
||||
@@ -13,9 +13,12 @@ public class HandlerGemConvertReq extends NetHandler {
|
||||
public byte[] handle(GameSession session, byte[] message) throws Exception {
|
||||
// Parse request
|
||||
var req = UI32.parseFrom(message);
|
||||
if (req.getValue() <= 0) {
|
||||
return session.encodeMsg(NetMsgId.gem_convert_failed_ack);
|
||||
}
|
||||
|
||||
// Convert gems
|
||||
var change = session.getPlayer().getInventory().convertGems(req.getValue());
|
||||
var change = session.getPlayer().getInventory().convertStellaniteLuminaToDust(req.getValue());
|
||||
|
||||
if (change == null) {
|
||||
return session.encodeMsg(NetMsgId.gem_convert_failed_ack);
|
||||
|
||||
@@ -2,7 +2,6 @@ package emu.nebula.server.handlers;
|
||||
|
||||
import emu.nebula.net.NetHandler;
|
||||
import emu.nebula.net.NetMsgId;
|
||||
import emu.nebula.proto.MallGemListOuterClass.GemInfo;
|
||||
import emu.nebula.proto.MallGemListOuterClass.MallGemList;
|
||||
import emu.nebula.net.HandlerId;
|
||||
import emu.nebula.data.GameData;
|
||||
@@ -13,16 +12,13 @@ public class HandlerMallGemListReq extends NetHandler {
|
||||
|
||||
@Override
|
||||
public byte[] handle(GameSession session, byte[] message) throws Exception {
|
||||
var player = session.getPlayer();
|
||||
var rsp = MallGemList.newInstance();
|
||||
|
||||
|
||||
for (var data : GameData.getMallGemDataTable()) {
|
||||
var info = GemInfo.newInstance()
|
||||
.setId(data.getIdString())
|
||||
.setMaiden(true);
|
||||
|
||||
rsp.addList(info);
|
||||
rsp.addList(data.toInfo(player));
|
||||
}
|
||||
|
||||
|
||||
return session.encodeMsg(NetMsgId.mall_gem_list_succeed_ack, rsp);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package emu.nebula.server.handlers;
|
||||
|
||||
import emu.nebula.Nebula;
|
||||
import emu.nebula.data.GameData;
|
||||
import emu.nebula.net.GameSession;
|
||||
import emu.nebula.net.HandlerId;
|
||||
import emu.nebula.net.NetHandler;
|
||||
import emu.nebula.net.NetMsgId;
|
||||
import emu.nebula.proto.MallGemOrder.OrderInfo;
|
||||
|
||||
@HandlerId(NetMsgId.mall_gem_order_req)
|
||||
public class HandlerMallGemOrderReq extends NetHandler {
|
||||
|
||||
@Override
|
||||
public byte[] handle(GameSession session, byte[] message) throws Exception {
|
||||
var req = OrderInfo.parseFrom(message);
|
||||
var data = GameData.getMallGemDataTable().get(req.getId().hashCode());
|
||||
if (data == null) {
|
||||
return session.encodeMsg(NetMsgId.mall_gem_order_failed_ack);
|
||||
}
|
||||
|
||||
var order = Nebula.getGameContext().getPayModule().createGemOrder(session, data);
|
||||
if (order == null) {
|
||||
return session.encodeMsg(NetMsgId.mall_gem_order_failed_ack);
|
||||
}
|
||||
|
||||
return session.encodeMsg(NetMsgId.mall_gem_order_succeed_ack, order);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package emu.nebula.server.handlers;
|
||||
import emu.nebula.net.NetHandler;
|
||||
import emu.nebula.net.NetMsgId;
|
||||
import emu.nebula.proto.MallMonthlycardList.MallMonthlyCardList;
|
||||
import emu.nebula.proto.MallMonthlycardList.MonthlyCardInfo;
|
||||
import emu.nebula.net.HandlerId;
|
||||
import emu.nebula.data.GameData;
|
||||
import emu.nebula.net.GameSession;
|
||||
@@ -13,16 +12,13 @@ public class HandlerMallMonthlyCardListReq extends NetHandler {
|
||||
|
||||
@Override
|
||||
public byte[] handle(GameSession session, byte[] message) throws Exception {
|
||||
var player = session.getPlayer();
|
||||
var rsp = MallMonthlyCardList.newInstance();
|
||||
|
||||
|
||||
for (var data : GameData.getMallMonthlyCardDataTable()) {
|
||||
var info = MonthlyCardInfo.newInstance()
|
||||
.setId(data.getIdString())
|
||||
.setRemaining(9);
|
||||
|
||||
rsp.addList(info);
|
||||
rsp.addList(data.toInfo(player));
|
||||
}
|
||||
|
||||
|
||||
return session.encodeMsg(NetMsgId.mall_monthlyCard_list_succeed_ack, rsp);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package emu.nebula.server.handlers;
|
||||
|
||||
import emu.nebula.Nebula;
|
||||
import emu.nebula.data.GameData;
|
||||
import emu.nebula.net.NetHandler;
|
||||
import emu.nebula.net.NetMsgId;
|
||||
import emu.nebula.net.HandlerId;
|
||||
import emu.nebula.net.GameSession;
|
||||
import emu.nebula.proto.MallMonthlycardList.MonthlyCardInfo;
|
||||
|
||||
@HandlerId(NetMsgId.mall_monthlyCard_order_req)
|
||||
public class HandlerMallMonthlyCardOrderReq extends NetHandler {
|
||||
|
||||
@Override
|
||||
public byte[] handle(GameSession session, byte[] message) throws Exception {
|
||||
var req = MonthlyCardInfo.parseFrom(message);
|
||||
var data = GameData.getMallMonthlyCardDataTable().get(req.getId().hashCode());
|
||||
var order = Nebula.getGameContext().getPayModule().createMonthlyCardOrder(session, data);
|
||||
if (order == null) {
|
||||
return session.encodeMsg(NetMsgId.mall_monthlyCard_order_failed_ack);
|
||||
}
|
||||
|
||||
return session.encodeMsg(NetMsgId.mall_monthlyCard_order_succeed_ack, order);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package emu.nebula.server.handlers;
|
||||
|
||||
import emu.nebula.Nebula;
|
||||
import emu.nebula.net.GameSession;
|
||||
import emu.nebula.net.HandlerId;
|
||||
import emu.nebula.net.NetHandler;
|
||||
import emu.nebula.net.NetMsgId;
|
||||
|
||||
@HandlerId(NetMsgId.mall_order_cancel_req)
|
||||
public class HandlerMallOrderCancelReq extends NetHandler {
|
||||
|
||||
@Override
|
||||
public byte[] handle(GameSession session, byte[] message) throws Exception {
|
||||
var payModule = Nebula.getGameContext().getPayModule();
|
||||
payModule.clearPendingCollect(session.getPlayer().getUid());
|
||||
return session.encodeMsg(NetMsgId.mall_order_cancel_succeed_ack);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package emu.nebula.server.handlers;
|
||||
|
||||
import emu.nebula.Nebula;
|
||||
import emu.nebula.net.GameSession;
|
||||
import emu.nebula.net.HandlerId;
|
||||
import emu.nebula.net.NetHandler;
|
||||
import emu.nebula.net.NetMsgId;
|
||||
import emu.nebula.proto.Public.CollectResp;
|
||||
|
||||
@HandlerId(NetMsgId.mall_order_collect_req)
|
||||
public class HandlerMallOrderCollectReq extends NetHandler {
|
||||
|
||||
@Override
|
||||
public byte[] handle(GameSession session, byte[] message) throws Exception {
|
||||
var rsp = CollectResp.newInstance().setStatusValue(1);
|
||||
var payModule = Nebula.getGameContext().getPayModule();
|
||||
var collectResult = payModule.consumePendingCollect(session);
|
||||
|
||||
if (collectResult != null && collectResult.hasDisplayChange()) {
|
||||
rsp.setItems(collectResult.getDisplayChange());
|
||||
}
|
||||
|
||||
if (collectResult != null && collectResult.hasStateChange()) {
|
||||
payModule.pushInventoryNotifies(session, collectResult.getStateChange());
|
||||
}
|
||||
|
||||
return session.encodeMsg(NetMsgId.mall_order_collect_succeed_ack, rsp);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,8 +2,8 @@ package emu.nebula.server.handlers;
|
||||
|
||||
import emu.nebula.net.NetHandler;
|
||||
import emu.nebula.net.NetMsgId;
|
||||
import emu.nebula.proto.MallPackageListOuterClass;
|
||||
import emu.nebula.proto.MallPackageListOuterClass.MallPackageList;
|
||||
import emu.nebula.proto.MallPackageListOuterClass.PackageInfo;
|
||||
import emu.nebula.net.HandlerId;
|
||||
import emu.nebula.data.GameData;
|
||||
import emu.nebula.net.GameSession;
|
||||
@@ -13,16 +13,23 @@ public class HandlerMallPackageListReq extends NetHandler {
|
||||
|
||||
@Override
|
||||
public byte[] handle(GameSession session, byte[] message) throws Exception {
|
||||
var player = session.getPlayer();
|
||||
var rsp = MallPackageList.newInstance();
|
||||
|
||||
player.queueMallPackageStateNotify();
|
||||
|
||||
for (var data : GameData.getMallPackageDataTable()) {
|
||||
var info = PackageInfo.newInstance()
|
||||
if (!data.isVisible(player)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var info = MallPackageListOuterClass.PackageInfo.newInstance()
|
||||
.setId(data.getIdString())
|
||||
.setStock(data.getStock());
|
||||
|
||||
.setStock(data.getStock(player))
|
||||
.setRefreshTime(data.getNextRefreshTime());
|
||||
|
||||
rsp.addList(info);
|
||||
}
|
||||
|
||||
|
||||
return session.encodeMsg(NetMsgId.mall_package_list_succeed_ack, rsp);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package emu.nebula.server.handlers;
|
||||
|
||||
import emu.nebula.Nebula;
|
||||
import emu.nebula.data.GameData;
|
||||
import emu.nebula.net.NetHandler;
|
||||
import emu.nebula.net.NetMsgId;
|
||||
import emu.nebula.net.HandlerId;
|
||||
import emu.nebula.net.GameSession;
|
||||
import emu.nebula.proto.MallGemOrder.OrderInfo;
|
||||
import emu.nebula.proto.MallPackageOrderOuterClass.MallPackageOrder;
|
||||
|
||||
@HandlerId(NetMsgId.mall_package_order_req)
|
||||
public class HandlerMallPackageOrderReq extends NetHandler {
|
||||
|
||||
@Override
|
||||
public byte[] handle(GameSession session, byte[] message) throws Exception {
|
||||
var req = OrderInfo.parseFrom(message);
|
||||
var data = GameData.getMallPackageDataTable().get(req.getId().hashCode());
|
||||
if (data == null) {
|
||||
return session.encodeMsg(NetMsgId.mall_package_order_failed_ack);
|
||||
}
|
||||
|
||||
if (data.isCashPackage()) {
|
||||
var order = Nebula.getGameContext().getPayModule().createPackageOrder(session, data);
|
||||
if (order == null) {
|
||||
return session.encodeMsg(NetMsgId.mall_package_order_failed_ack);
|
||||
}
|
||||
|
||||
var rsp = MallPackageOrder.newInstance().setOrder(order);
|
||||
if (order.hasNextPackage()) {
|
||||
rsp.setNextPackage(order.getNextPackage().toArray());
|
||||
}
|
||||
|
||||
return session.encodeMsg(
|
||||
NetMsgId.mall_package_order_succeed_ack,
|
||||
rsp
|
||||
);
|
||||
}
|
||||
|
||||
var change = session.getPlayer().getInventory().buyMallPackage(data);
|
||||
if (change == null) {
|
||||
return session.encodeMsg(NetMsgId.mall_package_order_failed_ack);
|
||||
}
|
||||
|
||||
if (data.isFreePackage()) {
|
||||
session.getPlayer().queueMallPackageStateNotify();
|
||||
}
|
||||
|
||||
return session.encodeMsg(
|
||||
NetMsgId.mall_package_order_succeed_ack,
|
||||
MallPackageOrder.newInstance().setChange(change.toProto())
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,14 +1,11 @@
|
||||
package emu.nebula.server.handlers;
|
||||
|
||||
import emu.nebula.GameConstants;
|
||||
import emu.nebula.net.NetHandler;
|
||||
import emu.nebula.net.NetMsgId;
|
||||
import emu.nebula.proto.MallShopList;
|
||||
import emu.nebula.proto.MallShopList.MallShopProductList;
|
||||
import emu.nebula.proto.MallShopList.ProductInfo;
|
||||
import emu.nebula.net.HandlerId;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import emu.nebula.Nebula;
|
||||
import emu.nebula.data.GameData;
|
||||
import emu.nebula.net.GameSession;
|
||||
|
||||
@@ -18,22 +15,20 @@ public class HandlerMallShopListReq extends NetHandler {
|
||||
@Override
|
||||
public byte[] handle(GameSession session, byte[] message) throws Exception {
|
||||
var rsp = MallShopProductList.newInstance();
|
||||
|
||||
long refreshTime = Nebula.getCurrentServerTime() + TimeUnit.DAYS.toSeconds(30);
|
||||
|
||||
|
||||
for (var data : GameData.getMallShopDataTable()) {
|
||||
if (data.getStock() <= 0) {
|
||||
if (!data.isVisible()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var info = ProductInfo.newInstance()
|
||||
|
||||
var info = MallShopList.ProductInfo.newInstance()
|
||||
.setId(data.getIdString())
|
||||
.setStock(data.getStock(session.getPlayer()))
|
||||
.setRefreshTime(refreshTime);
|
||||
|
||||
.setStock(data.getStock() > 0 ? data.getStock(session.getPlayer()) : GameConstants.UNLIMITED_STOCK)
|
||||
.setRefreshTime(data.getNextRefreshTime());
|
||||
|
||||
rsp.addList(info);
|
||||
}
|
||||
|
||||
|
||||
return session.encodeMsg(NetMsgId.mall_shop_list_succeed_ack, rsp);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ public class HandlerPlayerDataReq extends NetHandler {
|
||||
if (session.getPlayer() == null) {
|
||||
return session.encodeMsg(NetMsgId.player_new_notify);
|
||||
}
|
||||
|
||||
session.getPlayer().queueMallPackageStateNotify();
|
||||
|
||||
// Encode player data
|
||||
return session.encodeMsg(NetMsgId.player_data_succeed_ack, session.getPlayer().toProto());
|
||||
|
||||
@@ -4,8 +4,11 @@ import emu.nebula.net.NetHandler;
|
||||
import emu.nebula.net.NetMsgId;
|
||||
import emu.nebula.proto.Public.BoughtGoods;
|
||||
import emu.nebula.proto.Public.ResidentShop;
|
||||
import emu.nebula.proto.ResidentShopGet.ResidentShopGetReq;
|
||||
import emu.nebula.proto.ResidentShopGet.ResidentShopGetResp;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
|
||||
import it.unimi.dsi.fastutil.ints.IntSet;
|
||||
import emu.nebula.net.HandlerId;
|
||||
import emu.nebula.data.GameData;
|
||||
import emu.nebula.net.GameSession;
|
||||
@@ -15,19 +18,38 @@ public class HandlerResidentShopGetReq extends NetHandler {
|
||||
|
||||
@Override
|
||||
public byte[] handle(GameSession session, byte[] message) throws Exception {
|
||||
var req = ResidentShopGetReq.parseFrom(message);
|
||||
var requestedShopIds = req.getShopIds();
|
||||
IntSet requestedShopIdSet = null;
|
||||
|
||||
if (requestedShopIds.length() > 0) {
|
||||
requestedShopIdSet = new IntOpenHashSet(requestedShopIds.length());
|
||||
for (int shopId : requestedShopIds) {
|
||||
requestedShopIdSet.add(shopId);
|
||||
}
|
||||
}
|
||||
|
||||
// Get shops
|
||||
var shops = new Int2ObjectOpenHashMap<ResidentShop>();
|
||||
|
||||
for (var data : GameData.getResidentShopDataTable()) {
|
||||
if (requestedShopIdSet != null && !requestedShopIdSet.contains(data.getId()) || !data.isVisible()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var proto = ResidentShop.newInstance()
|
||||
.setId(data.getId())
|
||||
.setRefreshTime(Long.MAX_VALUE);
|
||||
.setRefreshTime(data.getNextRefreshTime());
|
||||
|
||||
shops.put(data.getId(), proto);
|
||||
}
|
||||
|
||||
// Add bought goods
|
||||
for (var data : GameData.getResidentGoodsDataTable()) {
|
||||
if (!data.isVisible(session.getPlayer())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int bought = session.getPlayer().getInventory().getShopBuyCount().get(data.getId());
|
||||
if (bought == 0) {
|
||||
continue;
|
||||
|
||||
@@ -18,7 +18,7 @@ public class HandlerResidentShopPurchaseReq extends NetHandler {
|
||||
|
||||
// Get goods
|
||||
var data = GameData.getResidentGoodsDataTable().get(req.getGoodsId());
|
||||
if (data == null) {
|
||||
if (data == null || !data.isVisible(session.getPlayer())) {
|
||||
return session.encodeMsg(NetMsgId.resident_shop_purchase_failed_ack);
|
||||
}
|
||||
|
||||
@@ -33,10 +33,15 @@ public class HandlerResidentShopPurchaseReq extends NetHandler {
|
||||
var rsp = ResidentShopPurchaseResp.newInstance()
|
||||
.setChange(change.toProto())
|
||||
.setPurchasedNumber(req.getNumber());
|
||||
|
||||
var shopData = GameData.getResidentShopDataTable().get(data.getShopId());
|
||||
if (shopData == null || !shopData.isVisible()) {
|
||||
return session.encodeMsg(NetMsgId.resident_shop_purchase_failed_ack);
|
||||
}
|
||||
|
||||
rsp.getMutableShop()
|
||||
.setId(data.getShopId())
|
||||
.setRefreshTime(Long.MAX_VALUE);
|
||||
.setRefreshTime(shopData.getNextRefreshTime());
|
||||
|
||||
// Encode and send
|
||||
return session.encodeMsg(NetMsgId.resident_shop_purchase_succeed_ack, rsp);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,24 +1,23 @@
|
||||
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.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.YearMonth;
|
||||
import java.time.*;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
import emu.nebula.GameConstants;
|
||||
import it.unimi.dsi.fastutil.ints.IntList;
|
||||
|
||||
public class Utils {
|
||||
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 int[] EMPTY_INT_ARRAY = new int[0];
|
||||
@@ -62,6 +61,22 @@ public class Utils {
|
||||
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.
|
||||
* @param path The path to the file.
|
||||
@@ -342,4 +357,86 @@ public class Utils {
|
||||
var offsetDateTime = OffsetDateTime.parse(dateString, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user