9 Commits

Author SHA1 Message Date
Melledy
e5ce16d6ea Fix more Monolith achievements 2025-12-08 01:12:01 -08:00
Melledy
b92319b4c5 Update sessionTimeout default value to 5 minutes 2025-12-07 23:58:18 -08:00
Melledy
1b0b6873b9 Update player session removal 2025-12-07 23:52:23 -08:00
Melledy
f542ea7cb4 Fix gift boxes not giving gifts when being bought from the shop 2025-12-07 23:15:25 -08:00
Melledy
fa08bcebae Give crescendo/talent material if we are adding duplicate discs/characters 2025-12-07 23:01:46 -08:00
Melledy
209ce83fc9 Reset shop purchases each month 2025-12-07 21:06:53 -08:00
HongchengQ
8e7ef038ea Add HTTP debugging log configuration option 2025-12-07 20:31:03 -08:00
Melledy
90f4be862f Implement daily check in 2025-12-07 20:00:22 -08:00
HongchengQ
05e74f4d12 docs(readme): Update supported regions and domain list
- Added new supported region CN in README
2025-12-07 18:53:42 -08:00
17 changed files with 497 additions and 163 deletions

View File

@@ -33,7 +33,7 @@ For any extra support, questions, or discussions, check out our [Discord](https:
Nebula supports the global PC client by default. If you want to switch regions, you need to change the `region` field in the Nebula config.
Current supported regions (PC): `GLOBAL`, `KR`, `JP`, `TW`
Current supported regions (PC): `GLOBAL`, `KR`, `JP`, `TW`, `CN`
You may need to change the data version when switching regions. The `customDataVersion` field should match the the data version of your client, which is usually the last number of your client's version string (top left of your login screen). Example: 1.0.0.42 = data version 42.
@@ -66,7 +66,8 @@ class Handlers
".stellasora.global",
".stellasora.kr",
".stellasora.jp",
".stargazer-games.com"
".stargazer-games.com",
".yostar.cn"
];
static function OnBeforeRequest(oS: Session) {

View File

@@ -101,14 +101,21 @@ 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.
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 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.
public boolean unlockInstances = true;
public int sessionTimeout = 600; // How long to wait (in seconds) after the last http request from a session
// before removing it from the server
// How long to wait (in seconds) after the last http request from a session before removing it from the server.
public int sessionTimeout = 300;
// 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;
public int leaderboardRefreshTime = 60; // Leaderboard refresh time in seconds
// 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.
public WelcomeMail welcomeMail = new WelcomeMail();
}
@@ -121,6 +128,7 @@ public class Config {
public static class LogOptions {
public boolean commands = true;
public boolean packets = false;
public boolean httpDebug = false;
}
@Getter

View File

@@ -18,7 +18,7 @@ import lombok.Getter;
@SuppressWarnings("unused")
public class GameData {
// Characters
// ===== Characters =====
@Getter private static DataTable<CharacterDef> CharacterDataTable = new DataTable<>();
@Getter private static DataTable<CharacterAdvanceDef> CharacterAdvanceDataTable = new DataTable<>();
@Getter private static DataTable<CharacterSkillUpgradeDef> CharacterSkillUpgradeDataTable = new DataTable<>();
@@ -28,40 +28,43 @@ public class GameData {
@Getter private static DataTable<TalentGroupDef> TalentGroupDataTable = new DataTable<>();
@Getter private static DataTable<TalentDef> TalentDataTable = new DataTable<>();
// Character emblems
// Characters: Emblems
@Getter private static DataTable<CharGemDef> CharGemDataTable = new DataTable<>();
@Getter private static DataTable<CharGemSlotControlDef> CharGemSlotControlDataTable = new DataTable<>();
@Getter private static DataTable<CharGemAttrGroupDef> CharGemAttrGroupDataTable = new DataTable<>();
@Getter private static DataTable<CharGemAttrValueDef> CharGemAttrValueDataTable = new DataTable<>();
// Character affinity
// Characters: Affinity
@Getter private static DataTable<AffinityLevelDef> AffinityLevelDataTable = new DataTable<>();
@Getter private static DataTable<AffinityGiftDef> AffinityGiftDataTable = new DataTable<>();
@Getter private static DataTable<PlotDef> PlotDataTable = new DataTable<>();
// Characters: Phone
@Getter private static DataTable<ChatDef> ChatDataTable = new DataTable<>();
// Characters: Dating
@Getter private static DataTable<DatingLandmarkDef> DatingLandmarkDataTable = new DataTable<>();
@Getter private static DataTable<DatingLandmarkEventDef> DatingLandmarkEventDataTable = new DataTable<>();
@Getter private static DataTable<DatingCharacterEventDef> DatingCharacterEventDataTable = new DataTable<>();
// Discs
// ===== Discs =====
@Getter private static DataTable<DiscDef> DiscDataTable = new DataTable<>();
@Getter private static DataTable<DiscStrengthenDef> DiscStrengthenDataTable = new DataTable<>();
@Getter private static DataTable<DiscItemExpDef> DiscItemExpDataTable = new DataTable<>();
@Getter private static DataTable<DiscPromoteDef> DiscPromoteDataTable = new DataTable<>();
@Getter private static DataTable<DiscPromoteLimitDef> DiscPromoteLimitDataTable = new DataTable<>();
// Discs: Melody items
@Getter private static DataTable<SecondarySkillDef> SecondarySkillDataTable = new DataTable<>();
// Items
// ===== Items =====
@Getter private static DataTable<ItemDef> ItemDataTable = new DataTable<>();
@Getter private static DataTable<ProductionDef> ProductionDataTable = new DataTable<>();
@Getter private static DataTable<PlayerHeadDef> PlayerHeadDataTable = new DataTable<>();
@Getter private static DataTable<TitleDef> titleDataTable = new DataTable<>();
@Getter private static DataTable<HonorDef> honorDataTable = new DataTable<>();
// Shops
// ===== Shops =====
@Getter private static DataTable<MallMonthlyCardDef> MallMonthlyCardDataTable = new DataTable<>();
@Getter private static DataTable<MallPackageDef> MallPackageDataTable = new DataTable<>();
@Getter private static DataTable<MallShopDef> MallShopDataTable = new DataTable<>();
@@ -70,20 +73,38 @@ public class GameData {
@Getter private static DataTable<ResidentShopDef> ResidentShopDataTable = new DataTable<>();
@Getter private static DataTable<ResidentGoodsDef> ResidentGoodsDataTable = new DataTable<>();
// Battle Pass
// ===== Battle Pass =====
@Getter private static DataTable<BattlePassDef> BattlePassDataTable = new DataTable<>();
@Getter private static DataTable<BattlePassLevelDef> BattlePassLevelDataTable = new DataTable<>();
@Getter private static DataTable<BattlePassQuestDef> BattlePassQuestDataTable = new DataTable<>();
@Getter private static DataTable<BattlePassRewardDef> BattlePassRewardDataTable = new DataTable<>();
// Commissions
// ===== Commissions =====
@Getter private static DataTable<AgentDef> AgentDataTable = new DataTable<>();
// Dictionary
// ===== Dictionary =====
@Getter private static DataTable<DictionaryTabDef> DictionaryTabDataTable = new DataTable<>();
@Getter private static DataTable<DictionaryEntryDef> DictionaryEntryDataTable = new DataTable<>();
// Instances
// ===== Gacha =====
@Getter private static DataTable<GachaDef> GachaDataTable = new DataTable<>();
@Getter private static DataTable<GachaStorageDef> GachaStorageDataTable = new DataTable<>();
// ===== Story =====
@Getter private static DataTable<StoryDef> StoryDataTable = new DataTable<>();
@Getter private static DataTable<StorySetSectionDef> StorySetSectionDataTable = new DataTable<>();
// ===== Daily Quests =====
@Getter private static DataTable<DailyQuestDef> DailyQuestDataTable = new DataTable<>();
@Getter private static DataTable<DailyQuestActiveDef> DailyQuestActiveDataTable = new DataTable<>();
// ===== Achievements =====
@Getter private static DataTable<AchievementDef> AchievementDataTable = new DataTable<>();
// ===== Tutorials =====
@Getter private static DataTable<TutorialLevelDef> TutorialLevelDataTable = new DataTable<>();
// ===== Instances =====
@Getter private static DataTable<DailyInstanceDef> DailyInstanceDataTable = new DataTable<>();
@Getter private static DataTable<DailyInstanceRewardGroupDef> DailyInstanceRewardGroupDataTable = new DataTable<>();
@Getter private static DataTable<RegionBossLevelDef> RegionBossLevelDataTable = new DataTable<>();
@@ -91,26 +112,7 @@ public class GameData {
@Getter private static DataTable<CharGemInstanceDef> CharGemInstanceDataTable = new DataTable<>();
@Getter private static DataTable<WeekBossLevelDef> WeekBossLevelDataTable = new DataTable<>();
@Getter private static DataTable<GachaDef> GachaDataTable = new DataTable<>();
@Getter private static DataTable<GachaStorageDef> GachaStorageDataTable = new DataTable<>();
@Getter private static DataTable<WorldClassDef> WorldClassDataTable = new DataTable<>();
@Getter private static DataTable<GuideGroupDef> GuideGroupDataTable = new DataTable<>();
@Getter private static DataTable<HandbookDef> HandbookDataTable = new DataTable<>();
@Getter private static DataTable<StoryDef> StoryDataTable = new DataTable<>();
@Getter private static DataTable<StorySetSectionDef> StorySetSectionDataTable = new DataTable<>();
// Daily quests
@Getter private static DataTable<DailyQuestDef> DailyQuestDataTable = new DataTable<>();
@Getter private static DataTable<DailyQuestActiveDef> DailyQuestActiveDataTable = new DataTable<>();
// Achievements
@Getter private static DataTable<AchievementDef> AchievementDataTable = new DataTable<>();
// Tutorial
@Getter private static DataTable<TutorialLevelDef> TutorialLevelDataTable = new DataTable<>();
// Star tower
// ===== Star Tower =====
@Getter private static DataTable<StarTowerDef> StarTowerDataTable = new DataTable<>();
@Getter private static DataTable<StarTowerStageDef> StarTowerStageDataTable = new DataTable<>();
@Getter private static DataTable<StarTowerGrowthNodeDef> StarTowerGrowthNodeDataTable = new DataTable<>();
@@ -126,24 +128,31 @@ public class GameData {
@Getter private static DataTable<StarTowerBookFateCardQuestDef> StarTowerBookFateCardQuestDataTable = new DataTable<>();
@Getter private static DataTable<StarTowerBookFateCardDef> StarTowerBookFateCardDataTable = new DataTable<>();
@Getter private static DataTable<FateCardDef> FateCardDataTable = new DataTable<>();
// Infinity Tower
// ===== Infinity Tower =====
@Getter private static DataTable<InfinityTowerLevelDef> InfinityTowerLevelDataTable = new DataTable<>();
// Vampire survivor
// ===== Vampire Survivor =====
@Getter private static DataTable<VampireSurvivorDef> VampireSurvivorDataTable = new DataTable<>();
@Getter private static DataTable<VampireTalentDef> VampireTalentDataTable = new DataTable<>();
// Score boss
// ===== Score Boss =====
@Getter private static DataTable<ScoreBossControlDef> ScoreBossControlDataTable = new DataTable<>();
@Getter private static DataTable<ScoreBossRewardDef> ScoreBossRewardDataTable = new DataTable<>();
// Activity
// ===== Misc =====
@Getter private static DataTable<WorldClassDef> WorldClassDataTable = new DataTable<>();
@Getter private static DataTable<GuideGroupDef> GuideGroupDataTable = new DataTable<>();
@Getter private static DataTable<HandbookDef> HandbookDataTable = new DataTable<>();
@Getter private static DataTable<SignInDef> SignInDataTable = new DataTable<>();
// ===== Activity =====
@Getter private static DataTable<ActivityDef> ActivityDataTable = new DataTable<>();
// Tower defense
// Activity: Tower Defense
@Getter private static DataTable<TowerDefenseLevelDef> TowerDefenseLevelDataTable = new DataTable<>();
// Activity: Trials
@Getter private static DataTable<TrialControlDef> TrialControlDataTable = new DataTable<>();
@Getter private static DataTable<TrialGroupDef> TrialGroupDataTable = new DataTable<>();
}

View File

@@ -0,0 +1,40 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import emu.nebula.util.Utils;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import lombok.Getter;
@Getter
@ResourceType(name = "DropPkg.json")
public class DropPkgDef extends BaseDef {
private int PkgId;
private int ItemId;
private static Int2ObjectMap<IntList> PACKAGES = new Int2ObjectOpenHashMap<>();
@Override
public int getId() {
return PkgId;
}
@Override
public void onLoad() {
var packageList = PACKAGES.computeIfAbsent(this.PkgId, i -> new IntArrayList());
packageList.add(this.ItemId);
}
public static int getRandomDrop(int packageId) {
var packageList = PACKAGES.get(packageId);
if (packageList == null) {
return 0;
}
return Utils.randomElement(packageList);
}
}

View File

@@ -0,0 +1,20 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "SignIn.json")
public class SignInDef extends BaseDef {
private int Group;
private int Day;
private int ItemId;
private int ItemQty;
@Override
public int getId() {
return (this.Group << 16) + this.Day;
}
}

View File

@@ -37,6 +37,7 @@ public class GameContext implements Runnable {
// Daily
private long epochDays;
private int epochWeeks;
private int epochMonths;
public GameContext() {
this.sessions = new Object2ObjectOpenHashMap<>();
@@ -105,12 +106,17 @@ public class GameContext implements Runnable {
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();
this.epochWeeks = Utils.getWeeks(this.epochDays);
// Check if the day was changed
if (this.epochDays > lastEpochDays) {
// Update epoch weeks/months
this.epochWeeks = Utils.getWeeks(this.epochDays);
this.epochMonths = Utils.getMonths(this.epochDays);
// Reset dailies for players
this.resetDailies();
}

View File

@@ -53,11 +53,45 @@ public class AchievementHelper {
}
private static void fixParams() {
// Star Tower TODO
addParam(78, 0, 2);
addParam(79, 0, 4);
// Clear "Misstep On One"
addParam(56, 401, 0); // Custom trigger
// Clear "Currents and Shadows"
addParam(57, 102, 0);
addParam(58, 103, 0);
addParam(59, 104, 0);
addParam(60, 105, 0);
addParam(61, 106, 0);
addParam(62, 107, 0);
addParam(63, 108, 0);
// Clear "Dust and Flames"
addParam(64, 202, 0);
addParam(65, 203, 0);
addParam(66, 204, 0);
addParam(67, 205, 0);
addParam(68, 206, 0);
addParam(69, 207, 0);
addParam(70, 208, 0);
// Clear "Storm and Thunder"
addParam(71, 302, 0);
addParam(72, 303, 0);
addParam(73, 304, 0);
addParam(74, 305, 0);
addParam(75, 306, 0);
addParam(76, 307, 0);
addParam(77, 308, 0);
// First Ascension
addParam(498, 0, 1);
// Monolith Conqueror
addParam(78, 2, 0);
addParam(79, 4, 0);
addParam(80, 6, 0);
addParam(81, 7, 0);
// Money
addParam(25, GameConstants.GOLD_ITEM_ID, 0);
addParam(26, GameConstants.GOLD_ITEM_ID, 0);

View File

@@ -5,6 +5,7 @@ import java.util.Map;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.AchievementDef;
@@ -16,8 +17,8 @@ import emu.nebula.game.player.PlayerManager;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.Public.Achievements;
import emu.nebula.proto.Public.Events;
import lombok.Getter;
import lombok.Setter;
import us.hebi.quickbuf.RepeatedInt;
@Getter
@@ -29,8 +30,9 @@ public class AchievementManager extends PlayerManager implements GameDatabaseObj
// Achievement data
private Map<Integer, GameAchievement> achievements;
@Setter
// Flags
private transient boolean queueSave;
private transient boolean hasCompleted;
@Deprecated // Morphia only
public AchievementManager() {
@@ -79,9 +81,6 @@ public class AchievementManager extends PlayerManager implements GameDatabaseObj
}
public synchronized void handleClientEvents(Events events) {
//
boolean hasCompleted = false;
// Parse events
for (var event : events.getList()) {
// Check id
@@ -117,23 +116,15 @@ public class AchievementManager extends PlayerManager implements GameDatabaseObj
// Update achievement
boolean changed = achievement.trigger(true, progress, 0, 0);
// Only save/update on client if achievement was changed
// Sync with client if achievement was changed
if (changed) {
// Sync
this.syncAchievement(achievement);
// Set save flag
this.queueSave = true;
// Check if achievement was completed
if (achievement.isComplete()) {
hasCompleted = true;
}
}
}
// Trigger update
if (hasCompleted) {
// Update total achievements
if (this.hasCompleted) {
this.hasCompleted = false;
this.getPlayer().trigger(AchievementCondition.AchievementTotal, this.getCompletedAchievementsCount());
}
}
@@ -166,8 +157,7 @@ public class AchievementManager extends PlayerManager implements GameDatabaseObj
// Check what type of achievement condition this is
boolean isTotal = AchievementHelper.isIncrementalAchievement(condition);
boolean hasCompleted = false;
// Parse achievements
for (var data : triggerList) {
// Get achievement
@@ -176,23 +166,41 @@ public class AchievementManager extends PlayerManager implements GameDatabaseObj
// Update achievement
boolean changed = achievement.trigger(isTotal, progress, param1, param2);
// Only save/update on client if achievement was changed
// Sync with client if achievement was changed
if (changed) {
// Sync
this.syncAchievement(achievement);
// Set save flag
this.queueSave = true;
// Check if achievement was completed
if (achievement.isComplete()) {
hasCompleted = true;
}
}
}
// Trigger update
if (hasCompleted) {
// Update total achievements
if (this.hasCompleted) {
this.hasCompleted = false;
this.getPlayer().trigger(AchievementCondition.AchievementTotal, this.getCompletedAchievementsCount());
}
}
public synchronized void triggerOne(int id, int progress, int param1, int param2) {
// Get achievement data
var data = GameData.getAchievementDataTable().get(id);
if (data == null) return;
// Get achievement
var achievement = this.getAchievement(data);
// Check what type of achievement condition this is
boolean isTotal = AchievementHelper.isIncrementalAchievement(data.getCompleteCond());
// Update achievement
boolean changed = achievement.trigger(isTotal, progress, param1, param2);
// Sync with client if achievement was changed
if (changed) {
this.syncAchievement(achievement);
}
// Update total achievements
if (this.hasCompleted) {
this.hasCompleted = false;
this.getPlayer().trigger(AchievementCondition.AchievementTotal, this.getCompletedAchievementsCount());
}
}
@@ -205,6 +213,15 @@ public class AchievementManager extends PlayerManager implements GameDatabaseObj
return;
}
// Set save flag
this.queueSave = true;
// Check if achievement was completed
if (achievement.isComplete()) {
this.hasCompleted = true;
}
// Send update to player
getPlayer().addNextPackage(
NetMsgId.achievement_change_notify,
achievement.toProto()

View File

@@ -58,10 +58,15 @@ public class CharacterStorage extends PlayerManager {
return null;
}
return this.addCharacter(GameData.getCharacterDataTable().get(charId));
// Get data
var data = GameData.getCharacterDataTable().get(charId);
if (data == null) return null;
// Add character
return this.addCharacter(data);
}
private GameCharacter addCharacter(CharacterDef data) {
public GameCharacter addCharacter(CharacterDef data) {
// Sanity check to make sure we dont have this character already
if (this.hasCharacter(data.getId())) {
return null;
@@ -132,10 +137,15 @@ public class CharacterStorage extends PlayerManager {
return null;
}
return this.addDisc(GameData.getDiscDataTable().get(discId));
// Get data
var data = GameData.getDiscDataTable().get(discId);
if (data == null) return null;
// Add disc
return this.addDisc(data);
}
private GameDisc addDisc(DiscDef data) {
public GameDisc addDisc(DiscDef data) {
// Sanity check to make sure we dont have this disc already
if (this.hasDisc(data.getId())) {
return null;

View File

@@ -7,6 +7,7 @@ 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.database.GameDatabaseObject;
@@ -284,8 +285,8 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
change = new PlayerChangeInfo();
}
// Sanity
if (count == 0) {
// Sanity check
if (id <= 0 || count == 0) {
return change;
}
@@ -339,11 +340,32 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
}
}
case Item -> {
// Check if item is a random package
if (data.getItemSubType() == ItemSubType.RandomPackage && data.getUseParams() != null) {
// Cannot remove packages
if (count <= 0) break;
// Add random packages
for (var entry : data.getUseParams()) {
int pkgId = entry.getIntKey();
int pkgCount = entry.getIntValue() * count;
for (int i = 0; i < pkgCount; i++) {
int pkgDropId = DropPkgDef.getRandomDrop(pkgId);
this.addItem(pkgDropId, 1, change);
}
}
// End early
break;
}
// Get item
var item = this.items.get(id);
int diff = 0;
if (amount > 0) {
// Add resource
// Add item
if (item == null) {
item = new GameItem(this.getPlayer(), id, amount);
this.items.put(item.getItemId(), item);
@@ -377,12 +399,23 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
}
}
case Disc -> {
if (amount <= 0) {
// Cannot remove discs
if (amount <= 0) break;
// Get disc data
var discData = GameData.getDiscDataTable().get(id);
if (discData == null) break;
// Add transform item instead if we already have this disc
if (getPlayer().getCharacters().hasDisc(id)) {
this.addItem(discData.getTransformItemId(), amount, change);
break;
}
var disc = getPlayer().getCharacters().addDisc(id);
// Add disc
var disc = getPlayer().getCharacters().addDisc(discData);
// Add to change info
if (disc != null) {
change.add(disc.toProto());
} else {
@@ -390,12 +423,23 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
}
}
case Char -> {
if (amount <= 0) {
// Cannot remove characters
if (amount <= 0) break;
// Get character data
var charData = GameData.getCharacterDataTable().get(id);
if (charData == null) break;
// Add transform item instead if we already have this character
if (getPlayer().getCharacters().hasCharacter(id)) {
this.addItem(charData.getFragmentsId(), charData.getTransformQty(), change);
break;
}
var character = getPlayer().getCharacters().addCharacter(id);
// Add character
var character = getPlayer().getCharacters().addCharacter(charData);
// Add to change info
if (character != null) {
change.add(character.toProto());
} else {
@@ -829,6 +873,20 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
return change.setSuccess(true);
}
public void resetShopPurchases() {
// Clear shop purchases if it's not empty
if (!this.getShopBuyCount().isEmpty()) {
this.getShopBuyCount().clear();
Nebula.getGameDatabase().update(this, this.getUid(), "shopBuyCount", this.getShopBuyCount());
}
// Clear mall purchases if it's not empty
if (!this.getMallBuyCount().isEmpty()) {
this.getMallBuyCount().clear();
Nebula.getGameDatabase().update(this, this.getUid(), "mallBuyCount", this.getMallBuyCount());
}
}
// Database
public void loadFromDatabase() {

View File

@@ -1,7 +1,6 @@
package emu.nebula.game.player;
import java.util.Stack;
import java.util.concurrent.TimeUnit;
import dev.morphia.annotations.AlsoLoad;
import dev.morphia.annotations.Entity;
@@ -36,6 +35,7 @@ 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.SigninRewardUpdate;
import emu.nebula.proto.PlayerData.DictionaryEntry;
import emu.nebula.proto.PlayerData.DictionaryTab;
import emu.nebula.proto.PlayerData.PlayerInfo;
@@ -85,6 +85,8 @@ public class Player implements GameDatabaseObject {
private int energy;
private long energyLastUpdate;
private int signInIndex;
private long lastEpochDay;
private long lastLogin;
private long createTime;
@@ -186,30 +188,26 @@ public class Player implements GameDatabaseObject {
}
public void setSession(GameSession session) {
int time = Nebula.getConfig().getServerOptions().sessionTimeout;
long timeout = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(time);
if (this.session == null) {
// Set session
this.session = session;
// Don't set session if it's the same session
if (this.session == session) {
return;
}
// 1. Sanity check
// 2. Prevent incorrect deletion of players when re-logging into the game
if (this.session == session || this.lastLogin > timeout) {
return;
}
// Clear player from session
this.session.clearPlayer();
// Cache previous session
var prevSession = this.session;
// Set session
this.session = session;
}
public void removeSession() {
this.session = null;
Nebula.getGameContext().getPlayerModule().removeFromCache(this);
// Clear player reference from the previous session
if (prevSession != null) {
prevSession.clearPlayer();
}
// We cleared session, now remove player from cache
if (this.session == null) {
Nebula.getGameContext().getPlayerModule().removeFromCache(this);
}
}
public boolean hasSession() {
@@ -234,14 +232,12 @@ public class Player implements GameDatabaseObject {
public void setRemoteToken(String token) {
// Skip if tokens are the same
if (this.remoteToken == null) {
if (this.getRemoteToken() == null) {
if (token == null) {
return;
}
} else if (this.remoteToken != null) {
if (this.remoteToken.equals(token)) {
return;
}
} else if (this.getRemoteToken().equals(token)) {
return;
}
// Set remote token
@@ -613,6 +609,13 @@ public class Player implements GameDatabaseObject {
public void checkResetDailies() {
// Sanity check to make sure daily reset isnt being triggered wrong
if (Nebula.getGameContext().getEpochDays() <= this.getLastEpochDay()) {
// Fix sign-in index
// TODO remove later
if (this.getSignInIndex() <= 0) {
this.getSignInRewards(false);
}
// End
return;
}
@@ -621,8 +624,12 @@ public class Player implements GameDatabaseObject {
int curWeek = Utils.getWeeks(this.getLastEpochDay());
boolean hasWeekChanged = Nebula.getGameContext().getEpochWeeks() > curWeek;
// Check if month was changed
int curMonth = Utils.getMonths(this.getLastEpochDay());
boolean hasMonthChanged = Nebula.getGameContext().getEpochMonths() > curMonth;
// Reset dailies
this.resetDailies(hasWeekChanged);
this.resetDailies(hasWeekChanged, hasMonthChanged);
// Trigger quest/achievement login
this.trigger(QuestCondition.LoginTotal, 1);
@@ -633,15 +640,55 @@ public class Player implements GameDatabaseObject {
this.getInventory().addItem(GameConstants.WEEKLY_ENTRY_ITEM_ID, 3 - entries);
}
// Give sign-in rewards
this.getSignInRewards(hasMonthChanged);
// Update last epoch day
this.lastEpochDay = Nebula.getGameContext().getEpochDays();
Nebula.getGameDatabase().update(this, this.getUid(), "lastEpochDay", this.lastEpochDay);
}
private void getSignInRewards(boolean resetMonthly) {
// Check monthly reset
if (resetMonthly) {
this.signInIndex = 0;
}
// Get next sign-in index
int nextSignIn = this.signInIndex + 1;
int group = Utils.getDaysOfMonth(this.getLastEpochDay());
var data = GameData.getSignInDataTable().get((group << 16) + nextSignIn);
if (data == null) {
return;
}
// Add rewards
var change = this.getInventory().addItem(data.getItemId(), data.getItemQty());
// Add package
this.addNextPackage(
NetMsgId.signin_reward_change_notify,
SigninRewardUpdate.newInstance()
.setIndex(nextSignIn)
.setSwitch(resetMonthly)
.setChange(change.toProto())
);
// Update sign-in index
this.signInIndex = nextSignIn;
Nebula.getGameDatabase().update(this, this.getUid(), "signInIndex", this.signInIndex);
}
public void resetDailies(boolean resetWeekly) {
public void resetDailies(boolean resetWeekly, boolean resetMonthly) {
// Reset daily quests
this.getQuestManager().resetDailyQuests();
this.getBattlePassManager().getBattlePass().resetDailyQuests(resetWeekly);
// Reset monthly shop purchases
if (resetMonthly) {
this.getInventory().resetShopPurchases();
}
}
// Trigger quests + achievements
@@ -767,6 +814,7 @@ public class Player implements GameDatabaseObject {
public PlayerInfo toProto() {
PlayerInfo proto = PlayerInfo.newInstance()
.setServerTs(Nebula.getCurrentTime())
.setSigninIndex(this.getSignInIndex())
.setDailyShopRewardStatus(this.getQuestManager().hasDailyReward())
.setAchievements(new byte[64]);

View File

@@ -769,32 +769,7 @@ public class StarTowerGame {
}
// Refresh secondary skills
var newSecondarySkills = SecondarySkillDef.calculateSecondarySkills(this.getDiscIds(), this.getItems());
// Add any new secondary skills to the data proto
for (int id : newSecondarySkills) {
if (!this.getSecondarySkills().contains(id)) {
var info = ActiveSecondaryChange.newInstance()
.setSecondaryId(id)
.setActive(true);
data.addSecondaries(info);
}
}
// Inform the client that these skills are no longer active
for (int id : this.getSecondarySkills()) {
if (!newSecondarySkills.contains(id)) {
var info = ActiveSecondaryChange.newInstance()
.setSecondaryId(id)
.setActive(false);
data.addSecondaries(info);
}
}
// Set new secondary skills
this.secondarySkills = newSecondarySkills;
this.refreshSecondarySkills(data);
// Clear new infos
this.getNewInfos().clear();
@@ -826,6 +801,50 @@ public class StarTowerGame {
return rsp;
}
// Etc
private void refreshSecondarySkills(TowerChangeData data) {
// Init
var newSecondarySkills = SecondarySkillDef.calculateSecondarySkills(this.getDiscIds(), this.getItems());
int newSecondaryCount = 0;
// Add any new secondary skills to the data proto
for (int id : newSecondarySkills) {
if (!this.getSecondarySkills().contains(id)) {
var info = ActiveSecondaryChange.newInstance()
.setSecondaryId(id)
.setActive(true);
data.addSecondaries(info);
// Counter
newSecondaryCount++;
}
}
// Inform the client that these skills are no longer active
for (int id : this.getSecondarySkills()) {
if (!newSecondarySkills.contains(id)) {
var info = ActiveSecondaryChange.newInstance()
.setSecondaryId(id)
.setActive(false);
data.addSecondaries(info);
}
}
// Set new secondary skills
this.secondarySkills = newSecondarySkills;
// Achievement trigger
if (newSecondaryCount > 0) {
this.getAchievementManager().trigger(
AchievementCondition.TowerSpecificSecondarySkillActivateTotal,
newSecondaryCount
);
}
}
// Proto
public StarTowerInfo toProto() {

View File

@@ -12,6 +12,7 @@ import emu.nebula.game.quest.QuestCondition;
import emu.nebula.proto.StarTowerApply.StarTowerApplyReq;
import emu.nebula.util.Utils;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntSet;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
@@ -34,6 +35,10 @@ public class StarTowerManager extends PlayerManager {
return this.getPlayer().getProgress();
}
public IntSet getStarTowerLog() {
return this.getProgress().getStarTowerLog();
}
// Growth nodes (talents/research)
public boolean hasGrowthNode(int id) {
@@ -240,23 +245,20 @@ public class StarTowerManager extends PlayerManager {
var achievements = this.getPlayer().getAchievementManager();
achievements.trigger(AchievementCondition.TowerClearTotal, 1);
achievements.trigger(
AchievementCondition.TowerClearSpecificGroupIdAndDifficulty,
1,
game.getData().getGroupId(),
game.getData().getDifficulty()
);
achievements.trigger(
AchievementCondition.TowerClearSpecificLevelWithDifficultyAndTotal,
1,
game.getData().getId(),
game.getData().getDifficulty()
0
);
var elementType = game.getTeamElement();
if (elementType != null) {
achievements.trigger(AchievementCondition.TowerClearSpecificCharacterTypeWithTotal, 1, elementType.getValue(), 0);
}
// Update tower group achievements
this.updateTowerGroupAchievements(game);
}
// Return game
@@ -290,6 +292,35 @@ public class StarTowerManager extends PlayerManager {
return change;
}
// Achievements
private void updateTowerGroupAchievements(StarTowerGame game) {
// Update "First Ascension" achievement
boolean firstAscension = this.getStarTowerLog().contains(401) && this.getStarTowerLog().size() >= 2;
if (firstAscension) {
this.getPlayer().getAchievementManager().triggerOne(498, 1, 0, 1);
}
// Get total clears on this difficulty
int diff = game.getDifficulty();
int totalDiffClears = 0;
for (int i = 1; i <= 3; i++) {
int towerId = (i * 100) + 1 + diff;
if (this.getStarTowerLog().contains(towerId)) {
totalDiffClears++;
}
}
// Update "Monolith Conqueror" achievements
this.getPlayer().getAchievementManager().trigger(
AchievementCondition.TowerClearSpecificGroupIdAndDifficulty,
totalDiffClears,
diff,
0
);
}
// Build
private PlayerChangeInfo dismantleBuild(StarTowerBuild build, PlayerChangeInfo change) {

View File

@@ -209,9 +209,13 @@ public class StarTowerHawkerCase extends StarTowerBaseCase {
// Remove coins
this.getGame().addItem(GameConstants.TOWER_COIN_ITEM_ID, -price, change);
// Achievement
// Achievements
this.getGame().getAchievementManager().trigger(AchievementCondition.TowerSpecificDifficultyShopBuyTimes, 1);
if (goods.hasDiscount()) {
this.getGame().getAchievementManager().trigger(AchievementCondition.TowerSpecificShopBuyDiscountTotal, 1);
}
// Set change info
rsp.setChange(change.toProto());
}

View File

@@ -60,10 +60,12 @@ public class GameSession {
var player = this.player;
this.player = null;
// Remove session from player
player.removeSession();
// Remove session reference from player ONLY if their session wasn't replaced yet
if (player.getSession() == this) {
player.setSession(null);
}
// Set remove flag
// Set session removal flag
this.remove = true;
}

View File

@@ -1,14 +1,7 @@
package emu.nebula.server;
import java.io.File;
import java.io.FileReader;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import emu.nebula.Config.HttpServerConfig;
import emu.nebula.GameConstants;
import emu.nebula.Nebula;
@@ -21,6 +14,15 @@ import io.javalin.http.ContentType;
import io.javalin.http.Context;
import io.javalin.http.staticfiles.Location;
import lombok.Getter;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileReader;
@Getter
public class HttpServer {
@@ -39,6 +41,11 @@ public class HttpServer {
if (staticFilesDir.exists()) {
javalinConfig.staticFiles.add(staticFilesDir.getPath(), Location.EXTERNAL);
}
if (Nebula.getConfig().getLogOptions().httpDebug) {
javalinConfig.plugins.enableDevLogging();
((Logger) LoggerFactory.getLogger("io.javalin")).setLevel(Level.DEBUG);
}
});
this.loadPatchList();

View File

@@ -4,6 +4,9 @@ import java.io.File;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
@@ -294,11 +297,28 @@ public class Utils {
}
/**
* Get amount weeks since this epoch day. Each week starts on monday.
* Get amount of weeks since this epoch day. Each week starts on monday.
* @param epochDays
* @return
*/
public static int getWeeks(long epochDays) {
return (int) Math.floor((epochDays + 3) / 7D);
}
/**
* Get amount of months since this epoch day.
* @param epochDays
* @return
*/
public static int getMonths(long epochDays) {
var begin = LocalDate.ofEpochDay(0);
var date = LocalDate.ofEpochDay(epochDays);
return (int) ChronoUnit.MONTHS.between(begin, date);
}
public static int getDaysOfMonth(long epochDays) {
var date = LocalDate.ofEpochDay(epochDays);
var month = YearMonth.of(date.getYear(), date.getMonthValue());
return month.lengthOfMonth();
}
}