93 Commits
v1.1.1 ... main

Author SHA1 Message Date
Melledy
4d14099f11 Fix Menace Arena not saving properly 2025-12-13 01:20:06 -08:00
Melledy
9599877005 Clear current player level ids when settling 2025-12-13 00:49:55 -08:00
Melledy
31e56ae17f Fix potential count when giving up a Monolith run 2025-12-13 00:33:27 -08:00
Melledy
ab1f84e8db Fix Ocean of Souls research node 2025-12-13 00:26:19 -08:00
Melledy
588522df88 Fix lucky potential effect in selector 2025-12-13 00:17:18 -08:00
Melledy
660d5a3cf7 Fix emblems for on certain characters 2025-12-13 00:14:29 -08:00
Melledy
1aa2129bba Add platform string for game session 2025-12-13 00:14:07 -08:00
Melledy
968880c320 Show errors properly if emblem creation/reroll fails 2025-12-12 02:06:11 -08:00
Melledy
353d7d97b6 Add nightly link to readme 2025-12-12 01:45:34 -08:00
Melledy
c51268bcb8 Rework how story is handled, fixes choices not saving 2025-12-12 01:31:31 -08:00
Melledy
11ea526a35 Fix null pointer error when logging into a new account 2025-12-11 23:25:49 -08:00
Melledy
5668ba9cea Update data versions 2025-12-11 23:24:45 -08:00
阁主
fef25496fa Implement !status command 2025-12-11 21:21:01 -08:00
Melledy
5d9ff6e1af Fix !build command potential count and not saving to the database 2025-12-10 20:38:05 -08:00
HongchengQ
3a6387c2bd Implement ban functionality
- Added ban module
- Added Ban and Unban commands, supporting banning and unbanning players through various command parameters.
- Added timestamp formatting method in Utils utility class for displaying ban expiration times
- Introduced PlayerErrorCode enum defining various error codes including ErrBan
- Added dual ban checking for both IP and user during player login
- Optimized login failure responses to provide specific error reasons and parameters
2025-12-10 16:33:41 -08:00
Melledy
6483e8a5a7 Implement weekly journey ticket limit 2025-12-09 23:49:48 -08:00
Melledy
5d797fb9d3 Fix weekly boss entry tickets resetting every day 2025-12-09 23:28:24 -08:00
Melledy
0ad87dd751 Improve random potential selector generator 2025-12-09 22:37:04 -08:00
Melledy
5b7adc8fa4 Separate custom data defs into their own category 2025-12-09 21:57:35 -08:00
Melledy
6f832bcdfe Fix potential count calculations when creating a record 2025-12-09 21:33:34 -08:00
Melledy
bf5fe3912f Fix discs surging to max crescendo regardless of materials used 2025-12-09 21:28:53 -08:00
Melledy
c5f339c8be Implement new event 2025-12-08 22:28:47 -08:00
Melledy
80a181680b Fix !giveall not saving characters/discs properties to the database 2025-12-08 20:12:19 -08:00
Melledy
f582444679 Update data versions 2025-12-08 20:07:26 -08:00
Melledy
2af715477e Implement random npc event chance in Monoliths 2025-12-08 18:01:44 -08:00
Melledy
9d77005da6 Implement more Monolith research nodes 2025-12-08 14:47:59 -08:00
Melledy
78d88a87cd Bump server version to 1.1.3
Also updated readme and command descriptions
2025-12-08 01:28:10 -08:00
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
Melledy
005f138599 Implement Boss Blitz star rewards 2025-12-07 16:31:19 -08:00
Melledy
be5a6709fd Improve Boss Blitz UI 2025-12-07 16:02:44 -08:00
Melledy
c6ac09f112 Fix weekly boss entry tickets and rewards 2025-12-07 15:45:19 -08:00
Melledy
7f0bdb1824 Update player delete function to remove achievements and activities
Also cleaned up some stuff
2025-12-07 15:07:48 -08:00
HongchengQ
f8bd7d5db2 fix CN Client cannot modify nickname through /user/set-info 2025-12-07 13:08:00 -08:00
HongchengQ
a7eddd2ed0 Support CN Client
- Add Chinese region server key configuration
- Rename UserLoginEntity to OverseaUserLoginEntity and adjust package path
- Add ChinaUserLoginEntity entity class to support Chinese region user data structure
- Modify CommonConfigHandler to distinguish between Chinese and overseas configuration returns
- Update HandlerPlayerLoginReq to be compatible with Chinese region login token parsing
- Add /user/set-info and /user/send-sms interfaces in HttpServer
- Modify UserLoginHandler to support both Chinese and overseas user login logic
- Add mobile phone number + verification code login method support
- Add channel recognition logic to return corresponding regional user data
2025-12-07 03:38:38 -08:00
Melledy
467b7443f3 Add journey tickets after finishing a tower run 2025-12-06 19:40:15 -08:00
Melledy
2a7817df95 Prevent players from giving themselves unavailable characters/discs 2025-12-06 17:52:13 -08:00
Melledy
dfb93cae4b Implement some tower achievements 2025-12-06 02:01:57 -08:00
Fishia
5707c1c919 feat(tower_defense): implement first clear rewards 2025-12-06 01:21:28 -08:00
Fishia
51f6db9803 fix(tower_defense): level id is key, not value 2025-12-06 01:21:28 -08:00
Fishia
3df873e385 feat: tower defense activity
Bare minimum work done.
2025-12-06 01:21:28 -08:00
HongchengQ
f44262f427 Fix abnormal player online status on duplicate login
- Prevent incorrect player deletion on duplicate login
2025-12-06 00:54:58 -08:00
Melledy
cf63bc0b7e Implement starting musical notes 2025-12-05 23:24:10 -08:00
Melledy
b7bf1fcdeb Implement potential rerolling 2025-12-05 23:01:37 -08:00
Melledy
198d3aac4f Fix story red dot (untested) 2025-12-05 22:34:47 -08:00
Melledy
810427a028 Fix battle pass weekly exp not resetting 2025-12-05 21:05:21 -08:00
Melledy
70c7c849df Implement !battlepass command
Examples:
`!battlepass premium` = Activates elite grant
`!battlepass lv40` = Unlocks the rewards up to level 40
2025-12-05 21:03:09 -08:00
Melledy
0b7f1ae3a2 Fix battle pass red dot when logging in 2025-12-05 20:32:06 -08:00
Melledy
5182e94db7 Fix item count on star tower shop goods 2025-12-05 19:25:20 -08:00
Melledy
426e5bce63 Add a notification when getting the wrong answer in a npc event 2025-12-05 19:11:39 -08:00
HongchengQ
b9c4a174f8 Improve remote command execution response results
- Ensure that commands return specific messages instead of fixed strings after execution
2025-12-05 13:38:48 -08:00
Melledy
6974631601 Implement bonus potential level monolith talents 2025-12-05 02:12:29 -08:00
Melledy
880f0d1d7d Fix max level of potentials in !build being 3 2025-12-05 01:26:12 -08:00
Melledy
86c607c0b3 Implement some star tower npc events 2025-12-04 23:40:24 -08:00
Melledy
3710f0a697 Implement secondary skills in star tower too 2025-12-04 19:32:23 -08:00
Melledy
c19aa5d0a1 Fix disc secondary skills 2025-12-04 16:25:18 -08:00
Melledy
be84e0f406 Remove max level potentials from potential selectors 2025-12-04 00:01:48 -08:00
Melledy
e5cb842fdd Implement monolith shop discounts and improvements 2025-12-03 23:53:18 -08:00
Melledy
15618414a6 Implement monolith shop discounts 2025-12-03 23:15:25 -08:00
Melledy
71de6184b9 Optimize star tower case handling 2025-12-03 22:52:49 -08:00
Melledy
e887d5eb4c Update data versions 2025-12-03 22:45:42 -08:00
Melledy
211e012c42 Update patchlist handler 2025-12-03 22:44:12 -08:00
Melledy
9c87d74ad7 Add /client-code endpoint 2025-12-03 22:34:29 -08:00
Melledy
357d12779b Implement monolith shop refresh 2025-12-03 22:23:41 -08:00
Melledy
c8a7db75aa Implement bonus max potential level 2025-12-03 21:28:47 -08:00
Melledy
ef8846445c Implement monolith enhancement machines 2025-12-03 19:36:47 -08:00
Melledy
7ef7490c37 Support JP and TW clients 2025-12-03 14:08:38 -08:00
Melledy
2c1e1ae2fb Spawn the shop npc at the end of a monolith run 2025-12-03 12:07:05 -08:00
Melledy
e3d34bfa48 Spawn recovery npc after each battle in monolith 2025-12-03 00:20:51 -08:00
Melledy
65250b07bf Don't send door case after every shop purchase 2025-12-02 23:56:57 -08:00
Melledy
b38f4f0957 Fix duplicate potentials in potential selector 2025-12-02 23:50:57 -08:00
Melledy
e4dc85a50f Fix wrong potentials in star tower potential selector 2025-12-02 23:44:45 -08:00
Melledy
893b23b50d Rework star tower 2025-12-02 23:15:31 -08:00
Melledy
33b1cf55d4 Update data version to 60 2025-12-01 21:43:06 -08:00
Melledy
aecea6ab03 Implement !build command for creating records 2025-12-01 13:33:34 -08:00
Melledy
e8e7df7d50 Fix incorrect element type for wind/aqua 2025-12-01 13:21:42 -08:00
Melledy
9188d3b53a Improve command arg handling 2025-12-01 13:16:24 -08:00
Melledy
30f565d0d6 Bump version to 1.1.2 2025-11-30 23:49:50 -08:00
Melledy
faa4ea0780 Only show boss blitz leaderboard for the current season 2025-11-30 23:48:24 -08:00
Melledy
354f390c3b Reset score entry if control id doesn't match 2025-11-30 23:42:34 -08:00
Melledy
3171bce3cc Update boss blitz control id 2025-11-30 23:35:04 -08:00
Melledy
56b4d3b66d Fix monolith crash at floor 20 2025-11-30 23:33:05 -08:00
Melledy
585734c2f3 Handle more achievements 2025-11-30 23:16:37 -08:00
Melledy
6f7a92725a Rework how quest/achievement conditions are handled 2025-11-30 17:06:08 -08:00
Melledy
a04f3354f7 Implement trial activities 2025-11-30 15:19:35 -08:00
Melledy
f53bdaba32 Implement achievements properly 2025-11-29 00:29:04 -08:00
177 changed files with 8843 additions and 1153 deletions

View File

@@ -1,9 +1,11 @@
# Nebula
A work in progress game server emulator for a certain anime game.
A work in progress game server emulator for a certain anime game. Most features are implemented.
For any extra support, questions, or discussions, check out our [Discord](https://discord.gg/cskCWBqdJk).
Latest nightly compiled server jar: [Nightly](https://nightly.link/Melledy/Nebula/workflows/gradle/main/Nebula-Nightly.zip)
### Notable features
- Basic profile features
- Character system
@@ -15,19 +17,25 @@ For any extra support, questions, or discussions, check out our [Discord](https:
- Battle pass
- Gacha
- Friend system (sending energy not implemented)
- Shop (using only in-game currency)
- Shop (only in-game currency supported)
- Commissions
- Heartlink
- Monoliths (completeable but many other features missing)
- Achievements
- Monoliths (research quests not implemented)
- Bounty Trials
- Menance Arena
- Proving grounds
- Proving Grounds
- Catacylsm Survivor (talents not fully working)
- Boss Blitz
- Events (Only tower defense and trials)
### Not implemented
- Achievements
- Events
### Supported regions
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`, `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.
# Running the server and client
@@ -38,7 +46,7 @@ For any extra support, questions, or discussions, check out our [Discord](https:
* [MongoDB 4.0+](https://www.mongodb.com/try/download/community)
### Compiling the server
1. Open your system terminal, and compile the server with `./gradlew jar`
1. Open your system terminal, and compile the server with `./gradlew jar` (If you downloaded the server jar from the Nightly link, you can skip this step)
2. Create a folder named `resources` in your server directory
3. Download the `bin`, `language` folders from a repository with [datamined game data](https://github.com/Hiro420/StellaSoraData) and place them into your resources folder.
4. Run the server with `java -jar Nebula.jar` from your system terminal. This server comes with a built-in internal MongoDB server for its database, so no Mongodb installation is required. However, it is highly recommended to install Mongodb anyway.
@@ -49,17 +57,25 @@ For any extra support, questions, or discussions, check out our [Discord](https:
3. Copy and paste the following code into the Fiddlerscript tab of Fiddler Classic. Remember to save the fiddler script after you copy and paste it:
```
import System;
import System.Windows.Forms;
import Fiddler;
import System.Text.RegularExpressions;
class Handlers
{
static var list = [
".yostarplat.com",
".stellasora.global",
".stellasora.kr",
".stellasora.jp",
".stargazer-games.com",
".yostar.cn"
];
static function OnBeforeRequest(oS: Session) {
if (oS.host.EndsWith(".yostarplat.com") || oS.host.EndsWith(".stellasora.global")) {
oS.oRequest.headers.UriScheme = "http";
oS.host = "localhost"; // This can also be replaced with another IP address.
for (var i = 0; i < list.length; i++) {
if (oS.host.EndsWith(list[i])) {
oS.oRequest.headers.UriScheme = "http";
oS.host = "localhost"; // This can also be replaced with another IP address
}
}
}
};
@@ -68,25 +84,20 @@ class Handlers
4. If `autoCreateAccount` is set to true in the config, then you can skip this step. Otherwise, type `/account create [account email]` in the server console to create an account.
5. Login with your account email, the code field is ignored by the server and can be set to anything.
If you are not on the global client, `.stellasora.global` in the fiddlerscript may need to be changed to match the endpoint your client connects to.
### Supported regions
Nebula supports the global client by default. If you want to switch regions, you need to change the `customDataVersion` and `region` fields in the Nebula config. 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.
Current supported regions: `global`, `kr`
### Server commands
Server commands need to be run in the server console OR in the signature edit menu of your profile.
```
!account {create | delete} [email] (reserved player uid) = Creates or deletes an account.
!battlepass [free | premium] lv(level) = Modifies the targeted player's battle pass
!char [all | {characterId}] lv(level) a(ascension) s(skill level) t(talent level) f(affinity level) = Changes the properties of the targeted characters.
!clean [all | {id} ...] [items|resources] = Removes items/resources from the targeted player.
!disc [all | {discId}] lv(level) a(ascension) c(crescendo level) = Changes the properties of the targeted discs.
!give [item id] x[amount] = Gives the targeted player an item through the mail.
!giveall [characters | discs | materials] = Gives the targeted player items.
!help = Displays a list of available commands. (Very spammy in-game)
!level (level) = Sets the player level
!mail = Sends the targeted player a system mail.
!mail "subject" "body" [itemId xQty | itemId:qty ...] = Sends the targeted player a system mail.
!reload = Reloads the server config.
!remote = Creates a player token for remote api usage
```

View File

@@ -29,7 +29,7 @@ java {
}
}
version = '1.1.1'
version = '1.1.3'
var shouldGenerateProto = System.getenv("GENERATE_PROTO") == "true"
System.out.println(shouldGenerateProto ? "Generating proto files" : "Skipping proto generation")

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/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.
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

@@ -6,8 +6,26 @@ import emu.nebula.game.inventory.ItemParam;
import emu.nebula.util.WeightedList;
public class GameConstants {
private static final int DATA_VERSION = 54;
private static final String VERSION = "1.2.0";
public static final String VERSION = "1.3.0";
public static int DATA_VERSION = 0;
// Set data versions for each region
static {
RegionConfig.getRegion("global")
.setDataVersion(69);
RegionConfig.getRegion("kr")
.setDataVersion(76);
RegionConfig.getRegion("jp")
.setDataVersion(72);
RegionConfig.getRegion("tw")
.setDataVersion(70);
RegionConfig.getRegion("cn")
.setDataVersion(70);
}
public static final ZoneId UTC_ZONE = ZoneId.of("UTC");
@@ -19,8 +37,8 @@ public class GameConstants {
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 STAR_TOWER_GOLD_ITEM_ID = 11;
public static final int EXP_ITEM_ID = 21;
public static final int WEEKLY_ENTRY_ITEM_ID = 28;
public static final int MAX_ENERGY = 240;
public static final int ENERGY_REGEN_TIME = 360; // Seconds
@@ -37,6 +55,14 @@ public class GameConstants {
public static final int MAX_FRIENDSHIPS = 50;
public static final int MAX_PENDING_FRIENDSHIPS = 30;
public static final int TOWER_COIN_ITEM_ID = 11;
public static final int[] TOWER_COMMON_SUB_NOTE_SKILLS = new int[] {
90011, 90012, 90013, 90014, 90015, 90016, 90017
};
public static final int[] TOWER_EVENTS_IDS = new int[] {
101, 102, 104, 105, 106, 107, 108, 114, 115, 116, 126, 127, 128
};
public static int[][] VAMPIRE_SURVIVOR_BONUS_POWER = new int[][] {
new int[] {100, 120},
new int[] {200, 150},
@@ -57,7 +83,14 @@ public class GameConstants {
// Helper functions
public static String getGameVersion() {
return VERSION + "." + getDataVersion() + " (" + Nebula.getConfig().getRegion().toUpperCase() + ")";
// Load data version
var region = RegionConfig.getRegion(Nebula.getConfig().getRegion());
// Set data version from region
GameConstants.DATA_VERSION = region.getDataVersion();
// Init game version string
return VERSION + "." + getDataVersion() + " (" + region.getName().toUpperCase() + ")";
}
public static int getDataVersion() {

View File

@@ -43,9 +43,8 @@ public class Nebula {
@Getter private static PluginManager pluginManager;
public static void main(String[] args) {
// Load config + keys first
// Load config first
Nebula.loadConfig();
AeadHelper.loadKeys();
// Start Server
Nebula.getLogger().info("Starting Nebula " + getJarVersion());
@@ -54,6 +53,9 @@ public class Nebula {
boolean generateHandbook = true;
// Load keys
AeadHelper.loadKeys();
// Load plugin manager
Nebula.pluginManager = new PluginManager();

View File

@@ -0,0 +1,42 @@
package emu.nebula;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import lombok.Getter;
import lombok.Setter;
@Getter @Setter
public class RegionConfig {
private String name;
private int dataVersion;
private String serverMetaKey;
private String serverGarbleKey;
private static Object2ObjectMap<String, RegionConfig> REGIONS = new Object2ObjectOpenHashMap<>();
public RegionConfig(String name) {
this.name = name;
this.serverMetaKey = "";
this.serverGarbleKey = "";
}
public RegionConfig setDataVersion(int i) {
this.dataVersion = i;
return this;
}
public RegionConfig setServerMetaKey(String key) {
this.serverMetaKey = key;
return this;
}
public RegionConfig setServerGarbleKey(String key) {
this.serverGarbleKey = key;
return this;
}
public static RegionConfig getRegion(String name) {
String regionName = name.toLowerCase();
return REGIONS.computeIfAbsent(regionName, r -> new RegionConfig(regionName));
}
}

View File

@@ -7,8 +7,8 @@ import emu.nebula.game.character.GameCharacter;
import emu.nebula.game.character.GameDisc;
import emu.nebula.game.player.Player;
import emu.nebula.util.Utils;
import it.unimi.dsi.fastutil.ints.Int2IntLinkedOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import it.unimi.dsi.fastutil.objects.ObjectSet;
import lombok.Getter;
@@ -53,6 +53,9 @@ public class CommandArgs {
} else if (arg.startsWith("lv")) { // Level
this.level = Utils.parseSafeInt(arg.substring(2));
it.remove();
} else if (arg.startsWith("lvl")) { // Level
this.level = Utils.parseSafeInt(arg.substring(3));
it.remove();
} else if (arg.startsWith("a")) { // Advance
this.advance = Utils.parseSafeInt(arg.substring(1));
it.remove();
@@ -76,7 +79,7 @@ public class CommandArgs {
int key = Integer.parseInt(split[0]);
int value = Integer.parseInt(split[1]);
if (this.map == null) this.map = new Int2IntOpenHashMap();
if (this.map == null) this.map = new Int2IntLinkedOpenHashMap();
this.map.put(key, value);
it.remove();

View File

@@ -6,7 +6,7 @@ import emu.nebula.command.CommandHandler;
import emu.nebula.game.account.AccountHelper;
import emu.nebula.util.Utils;
@Command(label = "account", permission = "admin.account", desc = "/account {create | delete} [email] (reserved player uid). Creates or deletes an account.")
@Command(label = "account", permission = "admin.account", desc = "!account {create | delete} [email] (reserved player uid). Creates or deletes an account.")
public class AccountCommand implements CommandHandler {
@Override

View File

@@ -0,0 +1,115 @@
package emu.nebula.command.commands;
import emu.nebula.Nebula;
import emu.nebula.command.Command;
import emu.nebula.command.CommandArgs;
import emu.nebula.command.CommandHandler;
import emu.nebula.game.player.Player;
import emu.nebula.util.Utils;
import java.util.Locale;
@Command(label = "ban",
permission = "admin.ban",
desc = """
!ban {all | ip | uid} [player uid | ip] (end timestamp) (reason) - Ban a player\
- all mode bans both the player object and their IP address by UID, so the next parameter should be UID instead of IP
- ip mode can only ban IP addresses, so the next parameter should be an IP
- uid mode can only ban UIDs, so the next parameter should be a UID
- If you don't fill in the end timestamp, it will be permanently banned by default\
""")
public class BanCommand implements CommandHandler {
@Override
public String execute(CommandArgs args) {
if (args.size() < 2) {
return "Invalid amount of args";
}
int bannedUid = 0;
long banEndTime = 0;
String bannedIp = null;
String reason = null;
String mode = args.get(0).toLowerCase(Locale.ROOT);
switch (args.size()) {
case 4:
reason = args.get(3);
case 3: {
try {
banEndTime = Long.parseLong(args.get(2));
} catch (NumberFormatException ignored) {
return "Unable to parse timestamp.";
}
}
case 2: {
if (mode.equals("ip")) {
bannedIp = args.get(1);
} else {
try {
bannedUid = Integer.parseInt(args.get(1));
} catch (NumberFormatException ignored) {
return "Unable to parse uid.";
}
}
}
case 1: {
if (!mode.equals("all") && !mode.equals("uid") && !mode.equals("ip"))
return "Unable to parse mode.";
}
default: break;
}
if (banEndTime != 0 && banEndTime < System.currentTimeMillis()) {
return "Failed, the end timestamp must be greater than the current time";
}
var banModule = Nebula.getGameContext().getBanModule();
Player player;
if (!mode.equals("ip")) {
player = Nebula.getGameContext().getPlayerModule().getPlayer(bannedUid);
if (player == null) {
return "Failed, player not found.";
}
}
switch (mode) {
case "all" -> {
banModule.banPlayer(
bannedUid,
banEndTime,
reason,
true,
args.getSender() != null ? String.valueOf(args.getSender().getUid()) : "Console");
return "Banned player all mode " + bannedUid + " until " + Utils.formatTimestamp(banEndTime) +
(reason != null ? " (" + reason + ")" : "");
}
case "ip" -> {
banModule.banIp(
bannedIp,
banEndTime,
reason,
args.getSender() != null ? String.valueOf(args.getSender().getUid()) : "Console");
return "Banned ip " + bannedIp + " until " + Utils.formatTimestamp(banEndTime) +
(reason != null ? " (" + reason + ")" : "");
}
case "uid" -> {
banModule.banPlayer(
bannedUid,
banEndTime,
reason,
false,
args.getSender() != null ? String.valueOf(args.getSender().getUid()) : "Console");
return "Banned player " + bannedUid + " until " + Utils.formatTimestamp(banEndTime) +
(reason != null ? " (" + reason + ")" : "");
}
default -> {
return "Ban sub command not found";
}
}
}
}

View File

@@ -0,0 +1,61 @@
package emu.nebula.command.commands;
import emu.nebula.command.Command;
import emu.nebula.command.CommandArgs;
import emu.nebula.command.CommandHandler;
import emu.nebula.net.NetMsgId;
@Command(label = "battlepass", aliases = {"bp"}, permission = "player.battlepass", desc = "!battlepass [free | premium] lv(level) = Modifies the targeted player's battle pass")
public class BattlePassCommand implements CommandHandler {
@Override
public String execute(CommandArgs args) {
// Get target
var target = args.getTarget();
var battlepass = target.getBattlePassManager().getBattlePass();
boolean changed = false;
// Check if we are changing premium status
int mode = -1;
for (var arg : args.getList()) {
if (arg.equalsIgnoreCase("free")) {
mode = 0;
} else if (arg.equalsIgnoreCase("premium")) {
mode = 2;
}
}
if (mode >= 0 && battlepass.getMode() != mode) {
battlepass.setMode(mode);
changed = true;
}
// Set level
int level = Math.min(args.getLevel(), 50);
if (level >= 0 && battlepass.getLevel() != level) {
battlepass.setLevel(level);
changed = true;
}
// Check if we have made any changes
if (changed) {
// Save battle pass to the database
battlepass.save();
// Send package to notify the client that the battle pass needs updating
target.addNextPackage(
NetMsgId.battle_pass_info_succeed_ack,
battlepass.toProto()
);
// Success message
return "Changed the battle pass successfully.";
}
// Result message
return "No changes were made to the battle pass.";
}
}

View File

@@ -0,0 +1,182 @@
package emu.nebula.command.commands;
import java.util.ArrayList;
import java.util.List;
import emu.nebula.command.Command;
import emu.nebula.command.CommandArgs;
import emu.nebula.command.CommandHandler;
import emu.nebula.data.GameData;
import emu.nebula.game.character.GameCharacter;
import emu.nebula.game.character.GameDisc;
import emu.nebula.game.player.Player;
import emu.nebula.game.tower.StarTowerBuild;
import emu.nebula.net.NetMsgId;
import emu.nebula.util.Utils;
import lombok.Getter;
@Command(
label = "build",
aliases = {"b", "record", "r"},
permission = "player.build",
requireTarget = true,
desc = "!build [char ids...] [disc ids...] [potential ids...] [melody ids...]"
)
public class BuildCommand implements CommandHandler {
@Override
public String execute(CommandArgs args) {
// Create record
var target = args.getTarget();
var builder = new StarTowerBuildData(target);
// Parse items
for (String arg : args.getList()) {
int id = Utils.parseSafeInt(arg);
int count = 1;
this.parseItem(builder, id, count);
}
if (args.getMap() != null) {
for (var entry : args.getMap().int2IntEntrySet()) {
int id = entry.getIntKey();
int count = entry.getIntValue();
this.parseItem(builder, id, count);
}
}
// Check if build is valid
if (builder.getCharacters().size() != 3) {
return "Record must have 3 different characters";
}
if (builder.getDiscs().size() < 3 || builder.getDiscs().size() > 6) {
return "Record must have 3-6 different discs";
}
// Create record
var build = builder.toBuild();
// Add to star tower manager
target.getStarTowerManager().getBuilds().put(build.getUid(), build);
// Save to database
build.save();
// Send package to player
target.addNextPackage(NetMsgId.st_import_build_notify, build.toProto());
// Send result to player
return "Created record for " + target.getName() + " (This command make take time to update on the client)";
}
private void parseItem(StarTowerBuildData builder, int id, int count) {
// Get item data
var itemData = GameData.getItemDataTable().get(id);
if (itemData == null) {
return;
}
// Clamp
count = Math.max(count, 1);
// Parse by item id
switch (itemData.getItemSubType()) {
case Char -> {
var character = builder.getPlayer().getCharacters().getCharacterById(id);
if (character == null || !character.getData().isAvailable()) {
break;
}
builder.addCharacter(character);
}
case Disc -> {
var disc = builder.getPlayer().getCharacters().getDiscById(id);
if (disc == null || !disc.getData().isAvailable()) {
break;
}
builder.addDisc(disc);
}
case Potential, SpecificPotential -> {
var potentialData = GameData.getPotentialDataTable().get(id);
if (potentialData == null) break;
int level = Math.min(count, potentialData.getMaxLevel());
builder.getBuild().getPotentials().add(id, level);
}
case SubNoteSkill -> {
builder.getBuild().getSubNoteSkills().add(id, count);
}
default -> {
// Ignored
}
}
}
@Getter
private static class StarTowerBuildData {
private Player player;
private StarTowerBuild build;
private List<GameCharacter> characters;
private List<GameDisc> discs;
public StarTowerBuildData(Player player) {
this.player = player;
this.build = new StarTowerBuild(player);
this.characters = new ArrayList<>();
this.discs = new ArrayList<>();
}
public void addCharacter(GameCharacter character) {
if (this.characters.contains(character)) {
return;
}
this.characters.add(character);
}
public void addDisc(GameDisc disc) {
if (this.discs.contains(disc)) {
return;
}
this.discs.add(disc);
}
public StarTowerBuild toBuild() {
// Set characters and discs
build.setChars(this.getCharacters());
build.setDiscs(this.getDiscs());
// Clear character potential cache
build.getCharPots().clear();
for (int charId : build.getCharIds()) {
build.getCharPots().put(charId, 0);
}
// Add potentials to character potential cache
var it = build.getPotentials().iterator();
while (it.hasNext()) {
var potential = it.next();
var data = GameData.getPotentialDataTable().get(potential.getIntKey());
if (data == null || !build.getCharPots().containsKey(data.getCharId())) {
it.remove();
continue;
}
build.getCharPots().add(data.getCharId(), 1);
}
// Calculate score
build.calculateScore();
// Return build
return build;
}
}
}

View File

@@ -20,7 +20,7 @@ import java.util.HashSet;
aliases = {"cl", "clear"},
permission = "player.inventory",
requireTarget = true,
desc = "!clean [all | {id} ...] [items|resources]. Removes items/resources from the targeted player."
desc = "!clean [all | {id} ...] [items|resources] = Removes items/resources from the targeted player."
)
public class CleanCommand implements CommandHandler {

View File

@@ -84,7 +84,11 @@ public class GiveAllCommand implements CommandHandler {
var disc = target.getCharacters().addDisc(data.getId());
// Set properties
args.setProperties(disc);
boolean shouldSave = args.setProperties(disc);
if (shouldSave) {
disc.save();
}
// Add to change info
change.add(disc.toProto());
@@ -110,7 +114,11 @@ public class GiveAllCommand implements CommandHandler {
var character = target.getCharacters().addCharacter(data.getId());
// Set properties
args.setProperties(character);
boolean shouldSave = args.setProperties(character);
if (shouldSave) {
character.save();
}
// Add to change info
change.add(character.toProto());

View File

@@ -12,7 +12,7 @@ import emu.nebula.command.CommandHandler;
aliases = {"g", "item"},
permission = "player.give",
requireTarget = true,
desc = "/give [item id] x(amount). Gives the targeted player an item."
desc = "!give [item id] x(amount). Gives the targeted player an item."
)
public class GiveCommand implements CommandHandler {

View File

@@ -5,7 +5,7 @@ import emu.nebula.command.Command;
import emu.nebula.command.CommandArgs;
import emu.nebula.command.CommandHandler;
@Command(label = "help", permission = "player.help", desc = "/help. Displays a list of available commands.")
@Command(label = "help", permission = "player.help", desc = "!help = Displays a list of available commands. (Very spammy in-game)")
public class HelpCommand implements CommandHandler {
@Override

View File

@@ -8,8 +8,8 @@ import emu.nebula.net.NetMsgId;
import emu.nebula.proto.NotifyGm.GmWorldClass;
import emu.nebula.util.Utils;
@Command(label = "setlevel", aliases = {"level", "l"}, permission = "player.level", requireTarget = true, desc = "/level [level]. Set player level")
public class SetLevelCommand implements CommandHandler {
@Command(label = "setlevel", aliases = {"level", "l"}, permission = "player.level", requireTarget = true, desc = "!level [level] = Set's the targeted player's level")
public class LevelCommand implements CommandHandler {
@Override
public String execute(CommandArgs args) {

View File

@@ -14,7 +14,7 @@ import emu.nebula.util.Utils;
aliases = {"m"},
permission = "player.mail",
requireTarget = true,
desc = "/mail \"subject\" \"body\" [itemId xQty | itemId:qty ...]. Sends the targeted player a system mail."
desc = "!mail \"subject\" \"body\" [itemId xQty | itemId:qty ...] = Sends the targeted player a system mail."
)
public class MailCommand implements CommandHandler {
private static final String USAGE_TEXT = "Usage: /mail \"subject\" \"body\" [itemId xQty | itemId:qty ...]";

View File

@@ -5,7 +5,7 @@ import emu.nebula.command.Command;
import emu.nebula.command.CommandArgs;
import emu.nebula.command.CommandHandler;
@Command(label = "reload", permission = "admin.reload", desc = "/reload. Reloads the server config.")
@Command(label = "reload", permission = "admin.reload", desc = "!reload = Reloads the server config.")
public class ReloadCommand implements CommandHandler {
@Override

View File

@@ -7,7 +7,7 @@ import emu.nebula.command.CommandHandler;
import java.util.Random;
@Command(label = "remote", permission = "player.remote", requireTarget = true, desc = "/remote. Send remote to web remote")
@Command(label = "remote", permission = "player.remote", requireTarget = true, desc = "!remote = Creates a player token for remote api usage")
public class RemoteKeyCommand implements CommandHandler {
private final String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

View File

@@ -0,0 +1,101 @@
package emu.nebula.command.commands;
import emu.nebula.GameConstants;
import emu.nebula.Nebula;
import emu.nebula.command.Command;
import emu.nebula.command.CommandArgs;
import emu.nebula.command.CommandHandler;
import java.io.File;
import java.lang.management.ManagementFactory;
@Command(label = "status", permission = "admin.status", desc = "!status = Displays server runtime status.")
public class StatusCommand implements CommandHandler {
@Override
public String execute(CommandArgs args) {
var sb = new StringBuilder();
if (args.getRaw().contains("@")) {
if (args.getTarget() == null || !args.getTarget().hasSession()) {
return "Error - Targeted player not found or offline";
}
}
var runtime = Runtime.getRuntime();
var uptimeMs = ManagementFactory.getRuntimeMXBean().getUptime();
double usedMem = (runtime.totalMemory() - runtime.freeMemory()) / 1048576.0;
double maxMem = runtime.maxMemory() / 1048576.0;
int players = 0;
if (Nebula.getGameContext() != null) {
players = Nebula.getGameContext().getPlayerModule().getCachedPlayers().size();
}
var http = Nebula.getHttpServer();
String addr = "-";
String scheme = "http";
int port = 0;
if (http != null) {
addr = http.getServerConfig().getPublicAddress();
port = http.getServerConfig().getPublicPort();
scheme = http.getServerConfig().isUseSSL() ? "https" : "http";
}
sb.append("Server Status\n");
sb.append("Git: ").append(Nebula.getGitHash()).append('\n');
sb.append("Game: ").append(GameConstants.getGameVersion()).append('\n');
sb.append("HTTP: ").append(scheme).append("://").append(addr).append(":").append(port).append('\n');
sb.append("Uptime: ").append(formatUptime(uptimeMs)).append('\n');
sb.append("CPU: ").append(getCpu()).append('\n');
sb.append("Load: ").append(getLoad()).append('\n');
sb.append("Disk: ").append(getDisk()).append('\n');
sb.append("Memory: ").append(String.format("%.1fMB (max %.1fMB)", usedMem, maxMem)).append('\n');
sb.append("Players: ").append(players);
return sb.toString();
}
private static String formatUptime(long ms) {
long s = ms / 1000;
long h = s / 3600;
long m = (s % 3600) / 60;
long sec = s % 60;
return String.format("%02dh:%02dm:%02ds", h, m, sec);
}
private static String getCpu() {
var os = ManagementFactory.getOperatingSystemMXBean();
double v = -1;
if (os instanceof com.sun.management.OperatingSystemMXBean) {
v = ((com.sun.management.OperatingSystemMXBean) os).getProcessCpuLoad();
}
return v >= 0 ? String.format("%.1f%%", v * 100.0) : "-";
}
private static String getLoad() {
var os = ManagementFactory.getOperatingSystemMXBean();
double v = -1;
if (os instanceof com.sun.management.OperatingSystemMXBean csm) {
v = csm.getCpuLoad();
}
if (v < 0) {
double avg = os.getSystemLoadAverage();
if (avg > 0) {
int cores = Runtime.getRuntime().availableProcessors();
v = Math.min(1.0, avg / cores);
}
}
return v >= 0 ? String.format("%.1f%%", v * 100.0) : "-";
}
private static String getDisk() {
var root = new File(".");
long total = root.getTotalSpace();
long usable = root.getUsableSpace();
if (total <= 0) return "-";
double usedPercent = (double) (total - usable) / (double) total * 100.0;
return String.format("%.1f%%", usedPercent);
}
}

View File

@@ -0,0 +1,81 @@
package emu.nebula.command.commands;
import emu.nebula.Nebula;
import emu.nebula.command.Command;
import emu.nebula.command.CommandArgs;
import emu.nebula.command.CommandHandler;
import emu.nebula.game.ban.BanInfo;
import emu.nebula.game.player.Player;
import java.util.Locale;
@Command(label = "unban", permission = "admin.ban", desc = """
!unban {all | ip | uid} [player uid | ip]\
- all mode unbans both the player object and their IP address by UID, so the next parameter should be UID instead of IP
- ip mode can only unban IP addresses, so the next parameter should be an IP
- uid mode can only unban UIDs, so the next parameter should be a UID\
""")
public class UnbanCommand implements CommandHandler {
@Override
public String execute(CommandArgs args) {
if (args.size() < 2) {
return "Invalid amount of args";
}
int unbannedUid = 0;
String unbannedIp = null;
String mode = args.get(0).toLowerCase(Locale.ROOT);
if (!mode.equals("all") && !mode.equals("uid") && !mode.equals("ip"))
return "Unable to parse mode.";
if (mode.equals("ip")) {
unbannedIp = args.get(1);
} else {
try {
unbannedUid = Integer.parseInt(args.get(1));
} catch (NumberFormatException ignored) {
return "Unable to parse uid.";
}
}
var banModule = Nebula.getGameContext().getBanModule();
Player player = null;
if (!mode.equals("ip")) {
player = Nebula.getGameContext().getPlayerModule().getPlayer(unbannedUid);
if (player == null) {
return "Failed, player not found.";
}
}
switch (mode) {
case "all" -> {
BanInfo banInfo = banModule.getPlayerBanInfo(player.getUid());
banModule.unbanPlayer(player.getUid());
if (banInfo != null) {
unbannedIp = banInfo.getIpAddress();
if (unbannedIp != null)
banModule.unbanIp(unbannedIp);
}
return "Unban a player all mode " + unbannedUid;
} case "uid" -> {
banModule.unbanPlayer(player.getUid());
return "Unban a player " + unbannedUid;
} case "ip" -> {
banModule.unbanIp(unbannedIp);
return "Unban a ip " + unbannedIp;
}
default -> {
// Fallback
return "Unban sub command not found";
}
}
}
}

View File

@@ -11,14 +11,14 @@ import java.util.stream.Collectors;
import it.unimi.dsi.fastutil.ints.*;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import emu.nebula.data.custom.CharGemAttrGroupDef;
import emu.nebula.data.resources.*;
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,38 +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<>();
// Items
// Discs: Melody items
@Getter private static DataTable<SecondarySkillDef> SecondarySkillDataTable = new DataTable<>();
// ===== 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<>();
@@ -68,20 +73,39 @@ 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<>();
@Getter private static DataTable<StoryEvidenceDef> StoryEvidenceDataTable = 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<>();
@@ -89,46 +113,60 @@ 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<>();
@Getter private static DataTable<StarTowerFloorExpDef> StarTowerFloorExpDataTable = new DataTable<>();
@Getter private static DataTable<StarTowerTeamExpDef> StarTowerTeamExpDataTable = new DataTable<>();
@Getter private static DataTable<PotentialDef> PotentialDataTable = new DataTable<>();
@Getter private static DataTable<StarTowerEventDef> StarTowerEventDataTable = new DataTable<>();
@Getter private static DataTable<SubNoteSkillPromoteGroupDef> SubNoteSkillPromoteGroupDataTable = new DataTable<>();
@Getter private static DataTable<PotentialDef> PotentialDataTable = new DataTable<>();
@Getter private static DataTable<CharPotentialDef> CharPotentialDataTable = new DataTable<>();
@Getter private static DataTable<StarTowerBookFateCardBundleDef> StarTowerBookFateCardBundleDataTable = new DataTable<>();
@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<>();
@Getter private static DataTable<InfinityTowerDifficultyDef> InfinityTowerDifficultyDataTable = 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<>();
// ===== 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<>();
// 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<>();
// Activity: Levels
@Getter private static DataTable<ActivityLevelsLevelDef> ActivityLevelsLevelDataTable = new DataTable<>();
// Activity: Task
@Getter private static DataTable<ActivityTaskDef> ActivityTaskDataTable = new DataTable<>();
@Getter private static DataTable<ActivityTaskGroupDef> ActivityTaskGroupDataTable = new DataTable<>();
// Activity: Shop
@Getter private static DataTable<ActivityShopDef> ActivityShopDataTable = new DataTable<>();
@Getter private static DataTable<ActivityShopControlDef> ActivityShopControlDataTable = new DataTable<>();
@Getter private static DataTable<ActivityGoodsDef> ActivityGoodsDataTable = new DataTable<>();
}

View File

@@ -10,6 +10,7 @@ import org.reflections.Reflections;
import emu.nebula.util.JsonUtils;
import emu.nebula.util.Utils;
import emu.nebula.Nebula;
import emu.nebula.game.achievement.AchievementHelper;
public class ResourceLoader {
private static boolean loaded = false;
@@ -22,6 +23,9 @@ public class ResourceLoader {
// Load
loadResources();
// Add hardcoded achievements params
AchievementHelper.init();
// Done
loaded = true;
Nebula.getLogger().info("Resource loading complete");

View File

@@ -1,4 +1,4 @@
package emu.nebula.data.resources;
package emu.nebula.data.custom;
import java.util.List;
import java.util.Map;
@@ -6,6 +6,7 @@ import java.util.Map;
import emu.nebula.data.BaseDef;
import emu.nebula.data.GameData;
import emu.nebula.data.ResourceType;
import emu.nebula.data.resources.CharGemAttrValueDef;
import emu.nebula.util.WeightedList;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
@@ -97,7 +98,7 @@ public class CharGemAttrGroupDef extends BaseDef {
this.values = new WeightedList<>();
}
protected void addValue(CharGemAttrValueDef value) {
public void addValue(CharGemAttrValueDef value) {
this.values.add(value.getRarity(), value);
}

View File

@@ -2,7 +2,8 @@ package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import emu.nebula.game.achievement.AchievementHelper;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import lombok.Getter;
@Getter
@@ -18,9 +19,50 @@ public class AchievementDef extends BaseDef {
private int Tid1;
private int Qty1;
// Custom params
private transient int param1; // -1 == any, 0 = no param, 1+ = param required
private transient int param2; // -1 == any, 0 = no param, 1+ = param required
@Override
public int getId() {
return Id;
}
public void setParams(int param1, int param2) {
this.param1 = param1;
this.param2 = param2;
}
/**
* Checks if this achievement requires params to match
*/
public boolean hasParam1(int param) {
if (this.param1 < 0) {
return false;
} else if (this.param1 == 0) {
return param != 0;
} else {
return true;
}
}
/**
* Checks if this achievement requires params to match
*/
public boolean hasParam2(int param) {
if (this.param2 < 0) {
return false;
} else if (this.param2 == 0) {
return param != 0;
} else {
return true;
}
}
@Override
public void onLoad() {
// Add to cached achievement list
var list = AchievementHelper.getCache().computeIfAbsent(this.CompleteCond, i -> new ObjectArrayList<>());
list.add(this);
}
}

View File

@@ -0,0 +1,25 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "Activity.json")
public class ActivityDef extends BaseDef {
private int Id;
private int ActivityType;
private transient emu.nebula.game.activity.ActivityType type;
@Override
public int getId() {
return Id;
}
@Override
public void onLoad() {
this.type = emu.nebula.game.activity.ActivityType.getByValue(this.ActivityType);
}
}

View File

@@ -0,0 +1,38 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.GameData;
import emu.nebula.data.ResourceType;
import emu.nebula.data.ResourceType.LoadPriority;
import emu.nebula.game.inventory.ItemParamMap;
import lombok.Getter;
@Getter
@ResourceType(name = "ActivityGoods.json", loadPriority = LoadPriority.LOW)
public class ActivityGoodsDef extends BaseDef {
private int Id;
private int ShopId;
private int ItemId;
private int ItemQuantity;
private int MaximumLimit;
private int Price;
private transient ItemParamMap items;
@Override
public int getId() {
return Id;
}
@Override
public void onLoad() {
var shop = GameData.getActivityShopDataTable().get(this.getShopId());
if (shop != null) {
shop.getGoods().put(this.getId(), this);
}
this.items = new ItemParamMap();
this.items.add(this.ItemId, this.ItemQuantity);
}
}

View File

@@ -0,0 +1,65 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import emu.nebula.game.instance.InstanceData;
import emu.nebula.game.inventory.ItemRewardList;
import emu.nebula.game.inventory.ItemRewardParam;
import emu.nebula.util.JsonUtils;
import lombok.Getter;
@Getter
@ResourceType(name = "ActivityLevelsLevel.json")
public class ActivityLevelsLevelDef extends BaseDef implements InstanceData {
private int Id;
private int ActivityId;
private int EnergyConsume;
private String CompleteRewardPreview;
private transient ItemRewardList firstRewards;
private transient ItemRewardList rewards;
@Override
public int getId() {
return Id;
}
@Override
public int getNeedWorldClass() {
return 0;
}
@Override
public void onLoad() {
// Init reward lists
this.firstRewards = new ItemRewardList();
this.rewards = new ItemRewardList();
// Parse rewards
var awards = JsonUtils.decodeList(this.CompleteRewardPreview, int[].class);
if (awards == null) {
return;
}
for (int[] award : awards) {
int itemId = award[0];
int min = award[1];
int max = award.length >= 4 ? award[2] : min;
boolean isFirst = award[award.length - 1] == 1;
if (min == -1) {
min = 0;
max = 1;
}
var reward = new ItemRewardParam(itemId, min, max);
if (isFirst) {
this.firstRewards.add(reward);
} else {
this.rewards.add(reward);
}
}
}
}

View File

@@ -0,0 +1,18 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "ActivityShopControl.json")
public class ActivityShopControlDef extends BaseDef {
private int Id;
private int[] ShopIds;
@Override
public int getId() {
return Id;
}
}

View File

@@ -0,0 +1,28 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
@Getter
@ResourceType(name = "ActivityShop.json")
public class ActivityShopDef extends BaseDef {
private int Id;
private int CurrencyItemId;
private int ExchangeItemId;
private double Rate;
private Int2ObjectMap<ActivityGoodsDef> goods;
@Override
public int getId() {
return Id;
}
@Override
public void onLoad() {
this.goods = new Int2ObjectOpenHashMap<>();
}
}

View File

@@ -0,0 +1,40 @@
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 = "ActivityTask.json")
public class ActivityTaskDef extends BaseDef {
private int Id;
private int ActivityTaskGroupId;
private int CompleteCond;
private int AimNumShow;
private int Tid1;
private int Qty1;
private int Tid2;
private int Qty2;
private transient ItemParamMap rewards;
@Override
public int getId() {
return Id;
}
@Override
public void onLoad() {
this.rewards = new ItemParamMap();
if (this.Tid1 > 0) {
this.rewards.add(this.Tid1, this.Qty1);
}
if (this.Tid2 > 0) {
this.rewards.add(this.Tid2, this.Qty2);
}
}
}

View File

@@ -0,0 +1,57 @@
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 = "ActivityTaskGroup.json")
public class ActivityTaskGroupDef extends BaseDef {
private int Id;
private int ActivityId;
private int Reward1;
private int RewardQty1;
private int Reward2;
private int RewardQty2;
private int Reward3;
private int RewardQty3;
private int Reward4;
private int RewardQty4;
private int Reward5;
private int RewardQty5;
private int Reward6;
private int RewardQty6;
private transient ItemParamMap rewards;
@Override
public int getId() {
return Id;
}
@Override
public void onLoad() {
this.rewards = new ItemParamMap();
if (this.Reward1 > 0) {
this.rewards.add(this.Reward1, this.RewardQty1);
}
if (this.Reward2 > 0) {
this.rewards.add(this.Reward2, this.RewardQty2);
}
if (this.Reward3 > 0) {
this.rewards.add(this.Reward3, this.RewardQty3);
}
if (this.Reward4 > 0) {
this.rewards.add(this.Reward4, this.RewardQty4);
}
if (this.Reward5 > 0) {
this.rewards.add(this.Reward5, this.RewardQty5);
}
if (this.Reward6 > 0) {
this.rewards.add(this.Reward6, this.RewardQty6);
}
}
}

View File

@@ -3,6 +3,7 @@ package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.GameData;
import emu.nebula.data.ResourceType;
import emu.nebula.data.custom.CharGemAttrGroupDef;
import emu.nebula.util.CustomIntArray;
import emu.nebula.util.Utils;
import emu.nebula.util.WeightedList;

View File

@@ -0,0 +1,22 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "CharPotential.json")
public class CharPotentialDef extends BaseDef {
private int Id;
private int[] MasterSpecificPotentialIds;
private int[] AssistSpecificPotentialIds;
private int[] CommonPotentialIds;
private int[] MasterNormalPotentialIds;
private int[] AssistNormalPotentialIds;
@Override
public int getId() {
return Id;
}
}

View File

@@ -1,5 +1,6 @@
package emu.nebula.data.resources;
import java.util.Arrays;
import java.util.List;
import emu.nebula.data.BaseDef;
@@ -61,5 +62,8 @@ public class CharacterDef extends BaseDef {
public void onLoad() {
this.elementType = ElementType.getByValue(this.EET);
this.chats = new ObjectArrayList<>();
// Sort gem slots
this.GemSlots = Arrays.stream(this.GemSlots).sorted().toArray();
}
}

View File

@@ -18,6 +18,9 @@ public class DiscDef extends BaseDef {
private int TransformItemId;
private int[] MaxStarTransformItem;
private int[] ReadReward;
private int SecondarySkillGroupId1;
private int SecondarySkillGroupId2;
private int SubNoteSkillGroupId;
private transient ElementType elementType;

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,34 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.GameData;
import emu.nebula.data.ResourceType;
import emu.nebula.data.ResourceType.LoadPriority;
import lombok.Getter;
/**
* We don't need a DataTable for this, since we are only using this class to verify event options for the client
*/
@Getter
@ResourceType(name = "EventOptions.json", loadPriority = LoadPriority.LOW)
public class EventOptionsDef extends BaseDef {
private int Id;
@Override
public int getId() {
return Id;
}
@Override
public void onLoad() {
// Get event
var event = GameData.getStarTowerEventDataTable().get(this.Id / 100);
if (event == null) {
return;
}
// Add to avaliable options
event.getOptionIds().add(this.getId());
}
}

View File

@@ -0,0 +1,19 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "InfinityTowerDifficulty.json")
public class InfinityTowerDifficultyDef extends BaseDef {
private int Id;
private int TowerId;
@Override
public int getId() {
return Id;
}
}

View File

@@ -4,6 +4,7 @@ import java.util.ArrayList;
import java.util.List;
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.inventory.ItemRewardParam;
@@ -25,8 +26,13 @@ public class InfinityTowerLevelDef extends BaseDef {
return Id;
}
public int getEnergyConsume() {
return 0;
public int getTowerId() {
var diff = GameData.getInfinityTowerDifficultyDataTable().get(this.DifficultyId);
if (diff == null) {
return 0;
}
return diff.getTowerId();
}
public ItemParamMap generateRewards() {

View File

@@ -2,6 +2,7 @@ package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import emu.nebula.game.tower.StarTowerGame;
import lombok.Getter;
@Getter
@@ -10,11 +11,48 @@ public class PotentialDef extends BaseDef {
private int Id;
private int CharId;
private int Build;
private int BranchType;
private int MaxLevel;
private int[] BuildScore;
private String BriefDesc;
@Override
public int getId() {
return Id;
}
public boolean isRare() {
return this.BranchType != 3;
}
public int getMaxLevel() {
// Check if regular potential
if (this.BranchType == 3) {
return this.BuildScore.length;
}
// Special potential should always have a max level of 1
return this.MaxLevel;
}
public int getMaxLevel(StarTowerGame game) {
// Check if regular potential
if (this.BranchType == 3) {
return this.MaxLevel + game.getModifiers().getExtraMaxPotentialLevel();
}
// Special potential should always have a max level of 1
return this.MaxLevel;
}
public int getBuildScore(int level) {
int index = level - 1;
if (index >= this.BuildScore.length) {
index = this.BuildScore.length - 1;
}
return this.BuildScore[index];
}
}

View File

@@ -0,0 +1,19 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "ScoreBossReward.json")
public class ScoreBossRewardDef extends BaseDef {
private int StarNeed;
private int RewardItemId1;
private int RewardNum1;
@Override
public int getId() {
return StarNeed;
}
}

View File

@@ -0,0 +1,118 @@
package emu.nebula.data.resources;
import java.util.ArrayList;
import java.util.List;
import emu.nebula.data.BaseDef;
import emu.nebula.data.GameData;
import emu.nebula.data.ResourceType;
import emu.nebula.game.inventory.ItemParamMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import lombok.Getter;
@Getter
@ResourceType(name = "SecondarySkill.json")
public class SecondarySkillDef extends BaseDef {
private int Id;
private int GroupId;
private int Score;
private String NeedSubNoteSkills;
private transient ItemParamMap reqSubNotes;
@Getter
private static transient Int2ObjectMap<List<SecondarySkillDef>> groups = new Int2ObjectOpenHashMap<>();
@Override
public int getId() {
return Id;
}
public boolean match(ItemParamMap subNotes) {
for (var item : this.reqSubNotes) {
int reqId = item.getIntKey();
int reqCount = item.getIntValue();
int curCount = subNotes.get(reqId);
if (curCount < reqCount) {
return false;
}
}
return true;
}
@Override
public void onLoad() {
// Setup required subnotes
this.reqSubNotes = ItemParamMap.fromJsonString(this.NeedSubNoteSkills);
// Add to group cache
var group = groups.computeIfAbsent(this.GroupId, id -> new ArrayList<>());
group.add(this);
// Clear to save memory
this.NeedSubNoteSkills = null;
}
// Static sub note skill group group
public static List<SecondarySkillDef> getGroup(int id) {
return groups.get(id);
}
public static IntSet calculateSecondarySkills(int[] discIds, ItemParamMap subNotes) {
var secondarySkills = new IntOpenHashSet();
// Get first 3 discs
for (int i = 0; i < 3; i++) {
// Disc id
int discId = discIds[i];
// Get disc data
var data = GameData.getDiscDataTable().get(discId);
if (data == null) continue;
// Add secondary skills
int s1= getSecondarySkill(subNotes, data.getSecondarySkillGroupId1());
if (s1 > 0) {
secondarySkills.add(s1);
}
int s2 = getSecondarySkill(subNotes, data.getSecondarySkillGroupId2());
if (s2 > 0) {
secondarySkills.add(s2);
}
}
return secondarySkills;
}
private static int getSecondarySkill(ItemParamMap subNotes, int groupId) {
// Check group id
if (groupId <= 0) {
return 0;
}
// Get group
var group = SecondarySkillDef.getGroup(groupId);
if (group == null) {
return 0;
}
// Reverse iterator to try and match highest secondary skill first
for (int i = group.size() - 1; i >= 0; i--) {
var data = group.get(i);
if (data.match(subNotes)) {
return data.getId();
}
}
// Failure
return 0;
}
}

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

@@ -10,6 +10,8 @@ import lombok.Getter;
@ResourceType(name = "StarTower.json")
public class StarTowerDef extends BaseDef {
private int Id;
private int GroupId;
private int Difficulty;
private int[] FloorNum;
private transient int maxFloors;
@@ -19,8 +21,8 @@ public class StarTowerDef extends BaseDef {
return Id;
}
public int getMaxFloor(int stage) {
int index = stage - 1;
public int getMaxFloor(int stageNum) {
int index = stageNum - 1;
if (index < 0 || index >= this.FloorNum.length) {
return 0;

View File

@@ -0,0 +1,35 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import lombok.Getter;
@Getter
@ResourceType(name = "StarTowerEvent.json")
public class StarTowerEventDef extends BaseDef {
private int Id;
private int[] RelatedNPCs;
private transient IntList optionIds;
@Override
public int getId() {
return Id;
}
/**
* Returns a deep copy of our option ids
*/
public IntList getClonedOptionIds() {
var list = new IntArrayList();
list.addAll(this.getOptionIds());
return list;
}
@Override
public void onLoad() {
this.optionIds = new IntArrayList();
}
}

View File

@@ -1,7 +1,10 @@
package emu.nebula.data.resources;
import com.google.gson.annotations.SerializedName;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import emu.nebula.game.tower.room.RoomType;
import lombok.Getter;
@Getter
@@ -10,11 +13,19 @@ public class StarTowerStageDef extends BaseDef {
private int Id;
private int Stage;
private int Floor;
private int RoomType;
private int InteriorCurrencyQuantity;
@SerializedName("RoomType")
private int RoomTypeValue;
private transient RoomType roomType;
@Override
public int getId() {
return Id;
}
@Override
public void onLoad() {
this.roomType = RoomType.getByValue(this.RoomTypeValue);
}
}

View File

@@ -0,0 +1,17 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "StoryEvidence.json")
public class StoryEvidenceDef extends BaseDef {
private int Id;
@Override
public int getId() {
return Id;
}
}

View File

@@ -0,0 +1,33 @@
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 = "TowerDefenseLevel.json")
public class TowerDefenseLevelDef extends BaseDef {
private int Id;
private int Condition2;
private int Condition3;
private int Item1;
private int Qty1;
private int Item2;
private int Qty2;
private transient ItemParamMap rewards;
@Override
public int getId() {
return Id;
}
@Override
public void onLoad() {
// Parse rewards
this.rewards = new ItemParamMap();
this.rewards.add(this.Item1, this.Qty1);
this.rewards.add(this.Item2, this.Qty2);
}
}

View File

@@ -0,0 +1,18 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import lombok.Getter;
@Getter
@ResourceType(name = "TrialControl.json")
public class TrialControlDef extends BaseDef {
private int Id;
private IntOpenHashSet GroupIds;
@Override
public int getId() {
return Id;
}
}

View File

@@ -0,0 +1,41 @@
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 = "TrialGroup.json")
public class TrialGroupDef extends BaseDef {
private int Id;
private int RewardId1;
private int Qty1;
private int RewardId2;
private int Qty2;
private int RewardId3;
private int Qty3;
private transient ItemParamMap rewards;
@Override
public int getId() {
return Id;
}
@Override
public void onLoad() {
this.rewards = new ItemParamMap();
if (this.RewardId1 > 0) {
this.rewards.add(this.RewardId1, this.Qty1);
}
if (this.RewardId2 > 0) {
this.rewards.add(this.RewardId2, this.Qty2);
}
if (this.RewardId3 > 0) {
this.rewards.add(this.RewardId3, this.Qty3);
}
}
}

View File

@@ -1,7 +1,9 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.GameData;
import emu.nebula.data.ResourceType;
import emu.nebula.data.ResourceType.LoadPriority;
import emu.nebula.game.instance.InstanceData;
import emu.nebula.game.inventory.ItemRewardList;
import emu.nebula.game.inventory.ItemRewardParam;
@@ -10,9 +12,10 @@ import emu.nebula.util.JsonUtils;
import lombok.Getter;
@Getter
@ResourceType(name = "WeekBossLevel.json")
@ResourceType(name = "WeekBossLevel.json", loadPriority = LoadPriority.LOW)
public class WeekBossLevelDef extends BaseDef implements InstanceData {
private int Id;
private int Difficulty;
private int PreLevelId;
private int NeedWorldClass;
private String BaseAwardPreview;
@@ -44,14 +47,33 @@ public class WeekBossLevelDef extends BaseDef implements InstanceData {
for (int[] award : awards) {
int itemId = award[0];
int min = award[1];
int max = award.length >= 4 ? award[2] : min;
int max = award[1];
boolean isFirst = award[award.length - 1] == 1;
// Set reward count based on difficulty
if (min == -1) {
min = 0;
max = 1;
min = this.Difficulty;
max = this.Difficulty;
var item = GameData.getItemDataTable().get(itemId);
if (item != null) {
switch (item.getRarity()) {
case 2:
max = this.Difficulty * 3;
break;
case 3:
min = this.Difficulty * 2;
max = this.Difficulty * 6;
break;
case 4:
min = this.Difficulty * 3;
max = this.Difficulty * 9;
break;
}
}
}
// Create reward param
var reward = new ItemRewardParam(itemId, min, max);
if (isFirst) {

View File

@@ -26,7 +26,6 @@ import de.bwaldvogel.mongo.backend.h2.H2Backend;
import de.bwaldvogel.mongo.backend.memory.MemoryBackend;
import dev.morphia.*;
import dev.morphia.annotations.Entity;
import dev.morphia.mapping.Mapper;
import dev.morphia.mapping.MapperOptions;
import dev.morphia.query.FindOptions;
import dev.morphia.query.Sort;
@@ -84,7 +83,7 @@ public final class DatabaseManager {
.stream()
.filter(cls -> {
Entity e = cls.getAnnotation(Entity.class);
return e != null && !e.value().equals(Mapper.IGNORED_FIELDNAME);
return e != null;
})
.toList();
@@ -164,12 +163,16 @@ public final class DatabaseManager {
return getDatastore().find(cls).stream();
}
public <T> List<T> getSortedObjects(Class<T> cls, String filter, int limit) {
public <T> List<T> getSortedObjects(Class<T> cls, String filter, int value, String sortBy, int limit) {
var options = new FindOptions()
.sort(Sort.descending(filter))
.sort(Sort.descending(sortBy))
.limit(limit);
return getDatastore().find(cls).iterator(options).toList();
return getDatastore()
.find(cls)
.filter(Filters.eq(filter, value))
.iterator(options)
.toList();
}
public <T> void save(T obj) {

View File

@@ -8,6 +8,8 @@ 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.player.PlayerModule;
import emu.nebula.game.scoreboss.ScoreBossModule;
@@ -27,7 +29,9 @@ public class GameContext implements Runnable {
private final PlayerModule playerModule;
private final GachaModule gachaModule;
private final TutorialModule tutorialModule;
private final ActivityModule activityModule;
private final ScoreBossModule scoreBossModule;
private final BanModule banModule;
// Game loop
private final ScheduledExecutorService scheduler;
@@ -35,6 +39,7 @@ public class GameContext implements Runnable {
// Daily
private long epochDays;
private int epochWeeks;
private int epochMonths;
public GameContext() {
this.sessions = new Object2ObjectOpenHashMap<>();
@@ -43,7 +48,9 @@ public class GameContext implements Runnable {
this.playerModule = new PlayerModule(this);
this.gachaModule = new GachaModule(this);
this.tutorialModule = new TutorialModule(this);
this.activityModule = new ActivityModule(this);
this.scoreBossModule = new ScoreBossModule(this);
this.banModule = new BanModule(this);
// Run game loop
this.scheduler = Executors.newScheduledThreadPool(1);
@@ -102,12 +109,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

@@ -0,0 +1,159 @@
package emu.nebula.game.achievement;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
public enum AchievementCondition {
AchievementSpecific (1),
AchievementTotal (2),
BattleTotal (3),
CharacterAcquire (5),
CharacterAcquireQuantityRarityAndAdvancement (6),
CharacterAdvanceTotal (7),
CharacterSkillUpTotal (8),
CharacterSkillWithSpecificUpTotal (9),
CharacterSpecific (10),
CharacterUpLevel (11),
CharacterUpTotal (12),
CharacterWithSpecificAdvance (13),
CharacterWithSpecificAffinity (14),
CharacterWithSpecificUpLevel (15),
CharactersWithSpecificLevelAndQuantity (16),
CharactersWithSpecificNumberLevelAndAttributes (17),
CharactersWithSpecificPlot (18),
CharactersWithSpecificQuantityAdvancementCountAndAttribute (19),
CharactersWithSpecificQuantityAndRarity (20),
CharactersWithSpecificQuantityRarityAndAdvancement (21),
CharactersWithSpecificQuantityRarityAndLevel (22),
ChatTotal (23),
DailyInstanceClearSpecificDifficultyAndTotal (24),
DailyInstanceClearSpecificTypeAndTotal (25),
DailyInstanceClearTotal (26),
DateSpecific (27),
DiscAcquire (28),
DiscAcquireSpecificQuantityAndRarity (29),
DiscAcquireQuantityLevelAndRarity (30),
DiscAcquireQuantityPhaseAndRarity (31),
DiscAcquireQuantityStarAndRarity (32),
DiscLimitBreakTotal (33),
DiscPromoteTotal (34),
DiscStrengthenTotal (35),
DiscWithSpecificQuantityLevelAndRarity (36),
DiscWithSpecificQuantityPhaseAndRarity (37),
DiscWithSpecificQuantityStarAndRarity (38),
GachaCharacterNotSSRTotal (40),
GachaCharacterTenModeSSRTotal (41),
GachaCharacterTotal (42),
GachaTenModeAcquireQuantityAndRarityItems (43),
GachaTotal (44),
GiftGiveTotal (45),
InfinityTowerClearSpecificFloor (46),
InfinityTowerClearTotal (47),
ItemsAdd (48),
ItemsDeplete (49),
ItemsProductTotal (50),
LoginTotal (51),
QuestTravelerDuelChallengeTotal (52),
QuestTourGuideSpecific (53),
QuestTravelerDuelSpecific (54),
QuestWithSpecificType (55),
RegionBossClearSpecificFullStarWithBossIdAndDifficulty (56),
RegionBossClearSpecificLevelWithDifficultyAndTotal (57),
RegionBossClearSpecificTotal (58),
RegionBossClearTotal (59),
SkillsWithSpecificQuantityAndLevel (60),
SkinAcquire (61),
StageClearSpecificStars (62),
StoryClear (63),
TravelerDuelChallengeSpecificBoosLevelWithDifficultyAndTotal (64),
TravelerDuelClearBossTotal (65),
TravelerDuelClearSpecificBossIdAndDifficulty (66),
TravelerDuelChallengeClearSpecificBossLevelAndAffix (67),
TravelerDuelClearSpecificBossLevelWithDifficultyAndTotal (68),
TravelerDuelClearSpecificBossTotal (69),
TravelerDuelChallengeRankUploadTotal (70),
WorldClassSpecific (71),
RegionBossClearSpecificTypeWithTotal (72),
CharactersWithSpecificDatingCount (73),
CharactersDatingTotal (74),
VampireSurvivorScoreTotal (75),
VampireSurvivorSpecificLevelWithSpecificScore (76),
VampireSurvivorPassedSpecificLevel (77),
CharacterParticipateTowerNumber (78),
CharacterAllSkillReachSpecificLevel (79),
TravelerDuelPlayTotal (80),
VampireClearTotal (81),
VampireWithSpecificClearTotal (82),
AgentFinishTotal (83),
AgentWithSpecificFinishTotal (84),
ActivityMiningEnterLayer (86),
ActivityMiningDestroyGrid (87),
BossRushTotalStars (88),
InfinityTowerClearSpecificDifficultyAndTotal (89),
SkillInstanceClearTotal (90),
VampireSurvivorSpecificPassedLevel (91),
WeekBoosClearSpecificDifficultyAndTotal (92),
NpcAffinityWithSpecificLevel (93),
CharacterPassedWithSpecificTowerAndCount (94),
JointDrillScoreTotal (95),
CharGemInstanceClearTotal (104),
DailyShopReceiveShopTotal (105),
AgentApplyTotal (106),
DiscSpecific (114),
ClientReport (200),
TowerBattleTimes (501),
TowerBossChallengeSpecificHighRewardWithTotal (502),
TowerBuildSpecificCharacter (503),
TowerBuildSpecificScoreWithTotal (504),
TowerClearSpecificCharacterTypeWithTotal (505),
TowerClearSpecificGroupIdAndDifficulty (506),
TowerClearSpecificLevelWithDifficultyAndTotal (507),
TowerClearTotal (508),
TowerEnterRoom (509),
TowerEventTimes (511),
TowerFateTimes (512),
TowerItemsGet (513),
TowerSpecificDifficultyShopBuyTimes (514),
TowerGrowthSpecificNote (515),
TowerClearSpecificLevelWithDifficultyAndTotalHistory (516),
TowerBookWithSpecificEvent (517),
TowerBookWithSpecificFateCard (518),
TowerBookWithSpecificPotential (520),
TowerBuildSpecificDifficultyAndScoreWithTotal (521),
TowerSpecificDifficultyStrengthenMachineTotal (522),
TowerSpecificDifficultyKillBossTotal (524),
TowerBookSpecificCharWithPotentialTotal (525),
TowerBuildSpecificCharSpecificScoreWithTotal (526),
TowerGrowthWithSpecificNote (527),
TowerSpecificFateCardReRollTotal (528),
TowerSpecificPotentialReRollTotal (529),
TowerSpecificShopReRollTotal (530),
TowerSpecificNoteActivateTotal (531),
TowerSpecificNoteLevelTotal (532),
TowerSpecificPotentialBonusTotal (533),
TowerSpecificPotentialLuckyTotal (534),
TowerSpecificShopBuyDiscountTotal (535),
TowerSpecificSecondarySkillActivateTotal (536),
TowerSpecificGetExtraNoteLvTotal (537),
TowerSweepTimes (539),
TowerSweepTotal (540);
@Getter
private final int value;
private final static Int2ObjectMap<AchievementCondition> map = new Int2ObjectOpenHashMap<>();
static {
for (AchievementCondition type : AchievementCondition.values()) {
map.put(type.getValue(), type);
}
}
private AchievementCondition(int value) {
this.value = value;
}
public static AchievementCondition getByValue(int value) {
return map.get(value);
}
}

View File

@@ -0,0 +1,166 @@
package emu.nebula.game.achievement;
import java.util.List;
import emu.nebula.GameConstants;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.AchievementDef;
import emu.nebula.game.tower.room.RoomType;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import lombok.Getter;
// Because achievements in the data files do not have params, we will hardcode them here
public class AchievementHelper {
// Cache
private static IntSet incrementalAchievementSet = new IntOpenHashSet();
@Getter
private static Int2ObjectMap<List<AchievementDef>> cache = new Int2ObjectOpenHashMap<>();
public static List<AchievementDef> getAchievementsByCondition(int condition) {
return cache.get(condition);
}
//
public static boolean isIncrementalAchievement(int condition) {
return incrementalAchievementSet.contains(condition);
}
// Fix params
public static void init() {
// Cache total achievements
for (var condition : AchievementCondition.values()) {
if (condition.name().endsWith("Total") || condition.name().endsWith("Times")) {
incrementalAchievementSet.add(condition.getValue());
}
}
incrementalAchievementSet.remove(AchievementCondition.AchievementTotal.getValue());
incrementalAchievementSet.add(AchievementCondition.ItemsAdd.getValue());
incrementalAchievementSet.add(AchievementCondition.ItemsDeplete.getValue());
incrementalAchievementSet.add(AchievementCondition.TowerItemsGet.getValue());
incrementalAchievementSet.add(AchievementCondition.TowerEnterRoom.getValue());
// Fix params
fixParams();
}
private static void fixParams() {
// 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);
addParam(27, GameConstants.GOLD_ITEM_ID, 0);
addParam(28, GameConstants.GOLD_ITEM_ID, 0);
addParam(29, GameConstants.GOLD_ITEM_ID, 0);
// Ininfite tower
for (int diff = 10, id = 270; diff <= 60; diff += 10) {
addParam(id++, 11000 + diff, 0); // Infinite Arena
addParam(id++, 51000 + diff, 0); // Shake the Floor
addParam(id++, 41000 + diff, 0); // Elegance and Flow
addParam(id++, 71000 + diff, 0); // Upbeat Party
addParam(id++, 31000 + diff, 0); // Thrilling Beat
addParam(id++, 21000 + diff, 0); // Flames and Beats
addParam(id++, 61000 + diff, 0); // Sinister Ritual
}
// Character count
addParams(393, 398, 1, 0);
// Disc count
addParams(382, 387, 1, 0);
// Star Tower team clear
addParams(95, 98, 1, 0); // Aqua team clear
addParams(99, 102, 2, 0); // Fire team clear
addParams(103, 106, 3, 0); // Earth team clear
addParams(107, 110, 4, 0); // Wind team clear
addParams(111, 114, 5, 0); // Light team clear
addParams(115, 118, 6, 0); // Dark team clear
// Star tower items
addParams(139, 144, GameConstants.TOWER_COIN_ITEM_ID, 0);
addParams(145, 149, 90011, 0);
addParams(150, 154, 90012, 0);
addParams(155, 159, 90013, 0);
addParams(160, 164, 90014, 0);
addParams(165, 169, 90015, 0);
addParams(170, 174, 90016, 0);
addParams(175, 179, 90017, 0);
addParams(180, 184, 90018, 0);
addParams(185, 189, 90019, 0);
addParams(190, 194, 90020, 0);
addParams(195, 199, 90021, 0);
addParams(200, 204, 90022, 0);
addParams(205, 209, 90023, 0);
// Star tower rooms
addParams(210, 216, RoomType.BattleRoom.getValue() + 1, 0);
addParams(217, 223, RoomType.EliteBattleRoom.getValue() + 1, 0);
addParams(224, 230, RoomType.BossRoom.getValue() + 1, 0);
addParams(231, 237, RoomType.FinalBossRoom.getValue() + 1, 0);
addParams(238, 244, RoomType.ShopRoom.getValue() + 1, 0);
addParams(245, 251, RoomType.EventRoom.getValue() + 1, 0);
}
private static void addParam(int achievementId, int param1, int param2) {
var data = GameData.getAchievementDataTable().get(achievementId);
if (data == null) return;
data.setParams(param1, param2);
}
private static void addParams(int start, int end, int param1, int param2) {
for (int id = start; id <= end; id++) {
addParam(id, param1, param2);
}
}
}

View File

@@ -0,0 +1,282 @@
package emu.nebula.game.achievement;
import java.util.HashMap;
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;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
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 us.hebi.quickbuf.RepeatedInt;
@Getter
@Entity(value = "achievements", useDiscriminator = false)
public class AchievementManager extends PlayerManager implements GameDatabaseObject {
@Id
private int uid;
// Achievement data
private Map<Integer, GameAchievement> achievements;
// Flags
private transient boolean queueSave;
private transient boolean hasCompleted;
@Deprecated // Morphia only
public AchievementManager() {
}
public AchievementManager(Player player) {
super(player);
this.uid = player.getUid();
this.achievements = new HashMap<>();
this.save();
}
public synchronized int getCompletedAchievementsCount() {
return (int) this.getAchievements().values().stream()
.filter(GameAchievement::isComplete)
.count();
}
/**
* Returns true if there are any unclaimed achievements
*/
public synchronized boolean hasNewAchievements() {
for (var achievement : this.getAchievements().values()) {
if (achievement.isClaimable()) {
return true;
}
}
return false;
}
public synchronized GameAchievement getAchievement(AchievementDef data) {
// Try and get achievement normally
var achievement = this.getAchievements().get(data.getId());
// Create achievement if it doesnt exist
if (achievement == null) {
achievement = new GameAchievement(data);
this.getAchievements().put(achievement.getId(), achievement);
}
return achievement;
}
public synchronized void handleClientEvents(Events events) {
// Parse events
for (var event : events.getList()) {
// Check id
if (event.getId() != AchievementCondition.ClientReport.getValue()) {
continue;
}
// Sanity check
if (event.getData().length() < 2) {
continue;
}
// Get achievement id and progress
int id = event.getData().get(1);
int progress = event.getData().get(0);
if (progress <= 0) {
continue;
}
// Get achievement data
var data = GameData.getAchievementDataTable().get(id);
if (data == null) continue;
// Make sure achivement can be completed by the client
if (data.getCompleteCond() != AchievementCondition.ClientReport.getValue()) {
continue;
}
// Get achievement
var achievement = this.getAchievement(data);
// Update achievement
boolean changed = achievement.trigger(true, progress, 0, 0);
// 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());
}
}
public synchronized void trigger(AchievementCondition condition, int progress) {
this.trigger(condition.getValue(), progress, 0, 0);
}
public synchronized void trigger(AchievementCondition condition, int progress, int param1, int param2) {
this.trigger(condition.getValue(), progress, param1, param2);
}
public synchronized void trigger(int condition, int progress, int param1, int param2) {
// Sanity check
if (progress <= 0) {
return;
}
// Blacklist
if (condition == AchievementCondition.ClientReport.getValue()) {
return;
}
// Get achievements to trigger
var triggerList = AchievementHelper.getAchievementsByCondition(condition);
if (triggerList == null) {
return;
}
// Check what type of achievement condition this is
boolean isTotal = AchievementHelper.isIncrementalAchievement(condition);
// Parse achievements
for (var data : triggerList) {
// Get achievement
var achievement = this.getAchievement(data);
// 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());
}
}
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());
}
}
/**
* Update this achievement on the player client
*/
private void syncAchievement(GameAchievement achievement) {
if (!getPlayer().hasSession()) {
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()
);
}
public synchronized PlayerChangeInfo recvRewards(RepeatedInt ids) {
// Sanity check
if (ids.length() <= 0) {
return null;
}
// Init variables
var rewards = new ItemParamMap();
// Claim achievements
for (int id : ids) {
// Get achievement
var achievement = this.getAchievements().get(id);
if (achievement == null) continue;
// Check if we can claim this achievement
if (achievement.isClaimable()) {
// Claim
achievement.setClaimed(true);
// Add rewards
rewards.add(achievement.getData().getTid1(), achievement.getData().getQty1());
// Save
this.queueSave = true;
}
}
// Success
return this.getPlayer().getInventory().addItems(rewards);
}
// Proto
public synchronized Achievements toProto() {
var proto = Achievements.newInstance();
for (var achievement : this.getAchievements().values()) {
proto.addList(achievement.toProto());
}
return proto;
}
// Database
@Override
public void save() {
Nebula.getGameDatabase().save(this);
this.queueSave = false;
}
}

View File

@@ -0,0 +1,122 @@
package emu.nebula.game.achievement;
import dev.morphia.annotations.Entity;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.AchievementDef;
import emu.nebula.proto.Public.Achievement;
import emu.nebula.proto.Public.QuestProgress;
import lombok.Getter;
@Getter
@Entity(useDiscriminator = false)
public class GameAchievement {
private int id;
private int curProgress;
private long completed;
private boolean claimed;
private transient AchievementDef data;
@Deprecated // Morphia only
public GameAchievement() {
}
public GameAchievement(AchievementDef data) {
this.id = data.getId();
this.data = data;
}
public AchievementDef getData() {
if (this.data == null) {
this.data = GameData.getAchievementDataTable().get(this.getId());
}
return this.data;
}
public int getMaxProgress() {
return this.getData().getAimNumShow();
}
public boolean isComplete() {
return this.completed > 0;
}
public boolean isClaimable() {
return !this.isClaimed() && this.isComplete();
}
public void setClaimed(boolean claimed) {
this.claimed = claimed;
}
public int getStatus() {
if (this.isClaimed()) {
return 2;
} else if (this.isComplete()) {
return 1;
}
return 0;
}
/**
* Returns true if achievement was updated
*/
public boolean trigger(boolean isTotal, int progress, int param1, int param2) {
// Sanity check
if (this.isComplete()) {
return false;
}
// Check param conditions
var data = this.getData();
if (data == null) return false;
if (data.hasParam1(param1) && data.getParam1() != param1) {
return false;
}
if (data.hasParam2(param2) && data.getParam2() != param2) {
return false;
}
// Set previous progress
int prevProgress = this.curProgress;
// Update progress
if (isTotal) {
this.curProgress += progress;
} else {
this.curProgress = progress;
}
// Check if completed
if (this.getCurProgress() >= this.getMaxProgress()) {
this.curProgress = this.getMaxProgress();
this.completed = Nebula.getCurrentTime();
return true;
}
// Check if progress was changed
return prevProgress != this.curProgress;
}
// Proto
public Achievement toProto() {
var progress = QuestProgress.newInstance()
.setCur(this.getCurProgress())
.setMax(this.getMaxProgress());
var proto = Achievement.newInstance()
.setId(this.getId())
.setCompleted(this.getCompleted())
.setStatus(this.getStatus())
.addProgress(progress);
return proto;
}
}

View File

@@ -0,0 +1,132 @@
package emu.nebula.game.activity;
import java.util.HashMap;
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.database.GameDatabaseObject;
import emu.nebula.game.activity.type.*;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerManager;
import lombok.Getter;
import lombok.Setter;
@Getter
@Entity(value = "activity", useDiscriminator = false)
public class ActivityManager extends PlayerManager implements GameDatabaseObject {
@Id
private int uid;
// Achievement data
private Map<Integer, GameActivity> activities;
@Setter
private transient boolean queueSave;
@Deprecated // Morphia only
public ActivityManager() {
}
public ActivityManager(Player player) {
super(player);
this.uid = player.getUid();
this.activities = new HashMap<>();
}
public <T extends GameActivity> T getActivity(Class<T> activityClass, int id) {
// Get activity first
var activity = this.getActivities().get(id);
if (activity == null) return null;
// Check activity type
if (activityClass.isInstance(activity)) {
return activityClass.cast(activity);
}
// Failure
return null;
}
/**
* This needs to be called after being loaded from the database
*/
public synchronized void init() {
// Check if any activities
var it = this.getActivities().entrySet().iterator();
while (it.hasNext()) {
var entry = it.next();
var activity = entry.getValue();
// Validate the activity to make sure it exists
var data = GameData.getActivityDataTable().get(activity.getId());
if (data == null) {
it.remove();
this.queueSave = true;
}
// Set data
activity.setData(data);
activity.setManager(this);
}
// Load activities
for (var id : Nebula.getGameContext().getActivityModule().getActivities()) {
// Check if we already have this activity
if (this.getActivities().containsKey(id)) {
continue;
}
// Create activity
var activity = this.createActivity(id);
if (activity == null) {
continue;
}
// Add activity
this.getActivities().put(id, activity);
// Set save flag
this.queueSave = true;
}
// Save if any activities were changed
if (this.queueSave) {
this.save();
}
}
private GameActivity createActivity(int id) {
// Get activity data first
var data = GameData.getActivityDataTable().get(id);
if (data == null) {
return null;
}
// TODO improve activity creation
GameActivity activity = switch (data.getType()) {
case TowerDefense -> new TowerDefenseActivity(this, data);
case Trial -> new TrialActivity(this, data);
case Levels -> new LevelsActivity(this, data);
case Task -> new TaskActivity(this, data);
case Shop -> new ShopActivity(this, data);
default -> null;
};
return activity;
}
// Database
@Override
public void save() {
Nebula.getGameDatabase().save(this);
this.queueSave = false;
}
}

View File

@@ -0,0 +1,37 @@
package emu.nebula.game.activity;
import emu.nebula.game.GameContext;
import emu.nebula.game.GameContextModule;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import lombok.Getter;
@Getter
public class ActivityModule extends GameContextModule {
private IntList activities;
public ActivityModule(GameContext context) {
super(context);
this.activities = new IntArrayList();
// Hardcode these activities for now
// TODO make an activity json file to read activity ids from
// Trial activities
this.activities.add(700104);
this.activities.add(700107);
this.activities.add(700108);
// Tower defense activity
this.activities.add(102001);
//
this.activities.add(1010201);
this.activities.add(1010203);
this.activities.add(1010204);
//this.activities.add(101002);
//this.activities.add(101003);
}
}

View File

@@ -0,0 +1,93 @@
package emu.nebula.game.activity;
import dev.morphia.annotations.Entity;
import emu.nebula.data.resources.ActivityTaskDef;
import emu.nebula.proto.Public.Quest;
import emu.nebula.proto.Public.QuestProgress;
import lombok.Getter;
import lombok.Setter;
@Getter
@Entity(useDiscriminator = false)
public class ActivityTaskQuest {
private int id;
private int cond;
private int curProgress;
private int maxProgress;
@Setter
private boolean claimed;
@Deprecated
public ActivityTaskQuest() {
}
public ActivityTaskQuest(ActivityTaskDef data) {
this.id = data.getId();
this.cond = data.getCompleteCond();
this.maxProgress = data.getAimNumShow();
}
public void resetProgress() {
this.curProgress = 0;
this.claimed = false;
}
public boolean isComplete() {
return this.curProgress >= this.maxProgress;
}
private int getStatus() {
if (this.isClaimed()) {
return 2;
} else if (this.isComplete()) {
return 1;
}
return 0;
}
public boolean trigger(int condition, int progress, int param1, int param2) {
// Sanity check
if (this.isComplete()) {
return false;
}
// Skip if not the correct condition
if (this.cond != condition) {
return false;
}
// Check quest param TODO
// Get new progress
int newProgress = Math.min(this.curProgress + progress, this.maxProgress);
// Set
if (this.curProgress != newProgress) {
this.curProgress = newProgress;
return true;
}
return false;
}
// Proto
public Quest toProto() {
var progress = QuestProgress.newInstance()
.setCur(this.getCurProgress())
.setMax(this.getMaxProgress());
var proto = Quest.newInstance()
.setId(this.getId())
.setStatus(this.getStatus())
.addProgress(progress);
return proto;
}
}

View File

@@ -0,0 +1,40 @@
package emu.nebula.game.activity;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
public enum ActivityType {
None (0),
PeriodicQuest (1),
LoginReward (2),
Mining (3),
Cookie (4),
TowerDefense (5),
Trial (6),
JointDrill (7),
CG (8),
Levels (9),
Avg (10),
Task (11),
Shop (12),
Advertise (13);
@Getter
private final int value;
private final static Int2ObjectMap<ActivityType> map = new Int2ObjectOpenHashMap<>();
static {
for (ActivityType type : ActivityType.values()) {
map.put(type.getValue(), type);
}
}
private ActivityType(int value) {
this.value = value;
}
public static ActivityType getByValue(int value) {
return map.get(value);
}
}

View File

@@ -0,0 +1,65 @@
package emu.nebula.game.activity;
import dev.morphia.annotations.Entity;
import emu.nebula.Nebula;
import emu.nebula.data.resources.ActivityDef;
import emu.nebula.game.player.Player;
import emu.nebula.proto.ActivityDetail.ActivityMsg;
import emu.nebula.proto.Public.Activity;
import lombok.Getter;
import lombok.Setter;
@Getter
@Entity(useDiscriminator = true)
public abstract class GameActivity {
private int id;
@Setter private transient ActivityManager manager;
@Setter private transient ActivityDef data;
@Deprecated // Morhpia only
public GameActivity() {
}
public GameActivity(ActivityManager manager, ActivityDef data) {
this.id = data.getId();
this.manager = manager;
this.data = data;
}
public Player getPlayer() {
return this.getManager().getPlayer();
}
public void save() {
Nebula.getGameDatabase().update(
this.getManager(),
this.getManager().getPlayerUid(),
"activities." + this.getId(),
this
);
}
// Proto
public Activity toProto() {
var proto = Activity.newInstance()
.setId(this.getId())
.setStartTime(1)
.setEndTime(Integer.MAX_VALUE);
return proto;
}
public ActivityMsg toMsgProto() {
var proto = ActivityMsg.newInstance()
.setId(this.getId());
this.encodeActivityMsg(proto);
return proto;
}
public abstract void encodeActivityMsg(ActivityMsg msg);
}

View File

@@ -0,0 +1,209 @@
package emu.nebula.game.activity.type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import dev.morphia.annotations.Entity;
import emu.nebula.GameConstants;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.ActivityDef;
import emu.nebula.data.resources.ActivityLevelsLevelDef;
import emu.nebula.game.activity.ActivityManager;
import emu.nebula.game.activity.GameActivity;
import emu.nebula.game.instance.InstanceSettleData;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.game.quest.QuestCondition;
import emu.nebula.proto.ActivityDetail.ActivityMsg;
import emu.nebula.proto.Public.ActivityLevel;
import lombok.Getter;
import lombok.Setter;
@Getter
@Entity
public class LevelsActivity extends GameActivity {
private Map<Integer, ActivityLevelInfo> levels;
// Apply level data
private transient ActivityLevelsLevelDef level;
private transient long buildId;
@Deprecated // Morphia only
public LevelsActivity() {
}
public LevelsActivity(ActivityManager manager, ActivityDef data) {
super(manager, data);
this.levels = new HashMap<>();
}
public boolean apply(int levelId, long buildId) {
// Verify level
var level = GameData.getActivityLevelsLevelDataTable().get(levelId);
if (level == null || level.getActivityId() != this.getId()) {
return false;
}
// Verify build
var build = this.getPlayer().getStarTowerManager().getBuildById(buildId);
if (build == null) {
return false;
}
// Set
this.level = level;
this.buildId = buildId;
// Success
return true;
}
public PlayerChangeInfo settle(int star) {
// Check energy
if (!this.getLevel().hasEnergy(this.getPlayer(), 1)) {
return null;
}
// Calculate settle data
var settleData = new InstanceSettleData();
settleData.setWin(star > 0);
settleData.setFirst(settleData.isWin() && !this.getLevels().containsKey(this.getLevel().getId()));
// Init player change info
var change = new PlayerChangeInfo();
// Handle win
if (settleData.isWin()) {
// Calculate energy and exp
settleData.setExp(this.getLevel().getEnergyConsume());
getPlayer().consumeEnergy(settleData.getExp(), change);
// Calculate rewards
settleData.generateRewards(this.getLevel());
// Add to inventory
getPlayer().getInventory().addItem(GameConstants.EXP_ITEM_ID, settleData.getExp(), change);
getPlayer().getInventory().addItems(settleData.getRewards(), change);
getPlayer().getInventory().addItems(settleData.getFirstRewards(), change);
// Log
var level = getLevels().computeIfAbsent(this.getLevel().getId(), i -> new ActivityLevelInfo());
level.setStar(star);
level.setBuildId(this.getBuildId());
// Save to database
this.save();
// Quest triggers
this.getPlayer().trigger(QuestCondition.BattleTotal, 1);
}
// Set extra data
change.setExtraData(settleData);
// Success
return change;
}
public PlayerChangeInfo sweep(ActivityLevelsLevelDef data, int count) {
// Sanity check count
if (count <= 0) {
return null;
}
// Check if we have 3 starred this instance
var level = this.getLevels().get(data.getId());
if (level == null || level.getStar() != 3) {
return null;
}
// Check energy cost
int energyCost = data.getEnergyConsume() * count;
if (this.getPlayer().getEnergy() < energyCost) {
return null;
}
// Init variables
var change = new PlayerChangeInfo();
var list = new ArrayList<ItemParamMap>();
// Consume exp
getPlayer().consumeEnergy(energyCost, change);
getPlayer().getInventory().addItem(GameConstants.EXP_ITEM_ID, energyCost, change);
// Calculate total rewards
var totalRewards = new ItemParamMap();
for (int i = 0; i < count; i++) {
// Generate rewards for each settle count
var rewards = data.getRewards().generate();
// Add to reward list
list.add(rewards);
// Add to total rewards
totalRewards.add(rewards);
}
// Add total rewards to inventory
getPlayer().getInventory().addItems(totalRewards, change);
// Set reward list in change info so we can serialize it in the response proto later
change.setExtraData(list);
// Quest triggers
this.getPlayer().trigger(QuestCondition.BattleTotal, count);
// Success
return change.setSuccess(true);
}
// Proto
@Override
public void encodeActivityMsg(ActivityMsg msg) {
var proto = msg.getMutableLevels();
for (var entry : this.getLevels().entrySet()) {
int id = entry.getKey();
var level = entry.getValue();
var info = level.toProto()
.setId(id);
proto.addLevels(info);
}
}
@Setter
@Getter
@Entity(useDiscriminator = false)
public static class ActivityLevelInfo {
private int star;
private long buildId;
public ActivityLevelInfo() {
}
// Proto
public ActivityLevel toProto() {
var proto = ActivityLevel.newInstance()
.setStar(this.getStar())
.setBuildId(this.getBuildId());
return proto;
}
}
}

View File

@@ -0,0 +1,152 @@
package emu.nebula.game.activity.type;
import java.util.HashMap;
import java.util.Map;
import dev.morphia.annotations.Entity;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.ActivityDef;
import emu.nebula.data.resources.ActivityShopControlDef;
import emu.nebula.data.resources.ActivityShopDef;
import emu.nebula.game.activity.ActivityManager;
import emu.nebula.game.activity.GameActivity;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.proto.ActivityDetail.ActivityMsg;
import emu.nebula.proto.Public.BoughtGoods;
import emu.nebula.proto.Public.ResidentShop;
import lombok.Getter;
@Getter
@Entity
public class ShopActivity extends GameActivity {
private Map<Integer, ActivityShopInfo> shops;
@Deprecated // Morphia only
public ShopActivity() {
}
public ShopActivity(ActivityManager manager, ActivityDef data) {
super(manager, data);
this.shops = new HashMap<>();
// Load shops
var control = GameData.getActivityShopControlDataTable().get(data.getId());
if (control != null) {
this.initShops(control);
}
}
private void initShops(ActivityShopControlDef control) {
for (int id : control.getShopIds()) {
var data = GameData.getActivityShopDataTable().get(id);
if (data == null) {
continue;
}
// Create resident shop
var shop = new ActivityShopInfo(data);
// Add
this.getShops().put(data.getId(), shop);
}
}
public PlayerChangeInfo buy(int shopId, int goodsId, int count) {
// Get shop
var shop = this.getShops().get(shopId);
if (shop == null) return null;
// Get shop data
var data = GameData.getActivityShopDataTable().get(shopId);
if (data == null) return null;
// Get goods
var goods = data.getGoods().get(goodsId);
if (goods == null) return null;
// Check limit
if (goods.getMaximumLimit() > 0) {
var limit = goods.getMaximumLimit() - shop.getBoughtCount(goodsId);
if (count > limit) {
return null;
}
}
// Purchase
var change = getPlayer().getInventory().buyItem(data.getCurrencyItemId(), goods.getPrice(), goods.getItems(), count);
// Purchase failed
if (change == null) {
return null;
}
// Set log
shop.getBoughtGoods().add(goodsId, count);
// Save
this.save();
// Set extra data
change.setExtraData(shop);
// Success
return change;
}
// Proto
@Override
public void encodeActivityMsg(ActivityMsg msg) {
var proto = msg.getMutableShop();
for (var entry : this.getShops().entrySet()) {
var id = entry.getKey();
var shop = entry.getValue();
var info = shop.toProto()
.setId(id);
proto.addShops(info);
}
}
@Getter
@Entity(useDiscriminator = false)
public static class ActivityShopInfo {
private ItemParamMap boughtGoods;
@Deprecated // Morphia only
public ActivityShopInfo() {
}
public ActivityShopInfo(ActivityShopDef data) {
this.boughtGoods = new ItemParamMap();
}
public int getBoughtCount(int goodsId) {
return this.getBoughtGoods().get(goodsId);
}
// Proto
public ResidentShop toProto() {
var proto = ResidentShop.newInstance();
for (var item : this.getBoughtGoods().int2IntEntrySet()) {
var info = BoughtGoods.newInstance()
.setId(item.getIntKey())
.setNumber(item.getIntValue());
proto.addInfos(info);
}
return proto;
}
}
}

View File

@@ -0,0 +1,72 @@
package emu.nebula.game.activity.type;
import java.util.HashMap;
import java.util.Map;
import dev.morphia.annotations.Entity;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.ActivityDef;
import emu.nebula.game.activity.ActivityManager;
import emu.nebula.game.activity.ActivityTaskQuest;
import emu.nebula.game.activity.GameActivity;
import emu.nebula.proto.ActivityDetail.ActivityMsg;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import lombok.Getter;
@Getter
@Entity
public class TaskActivity extends GameActivity {
private Map<Integer, ActivityTaskQuest> quests;
private IntSet completedGroups;
@Deprecated // Morphia only
public TaskActivity() {
}
public TaskActivity(ActivityManager manager, ActivityDef data) {
super(manager, data);
this.quests = new HashMap<>();
this.completedGroups = new IntOpenHashSet();
var groupSet = new IntOpenHashSet();
for (var group : GameData.getActivityTaskGroupDataTable()) {
if (group.getActivityId() != this.getId()) {
continue;
}
groupSet.add(group.getId());
}
for (var task : GameData.getActivityTaskDataTable()) {
if (!groupSet.contains(task.getActivityTaskGroupId())) {
continue;
}
var quest = new ActivityTaskQuest(task);
this.getQuests().put(quest.getId(), quest);
}
}
// Proto
@Override
public void encodeActivityMsg(ActivityMsg msg) {
var proto = msg.getMutableTask();
for (int id : this.getCompletedGroups()) {
proto.addGroupIds(id);
}
var tasks = proto.getMutableActivityTasks();
for (var quest : this.getQuests().values()) {
tasks.addList(quest.toProto());
}
}
}

View File

@@ -0,0 +1,104 @@
package emu.nebula.game.activity.type;
import dev.morphia.annotations.Entity;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.ActivityDef;
import emu.nebula.game.activity.ActivityManager;
import emu.nebula.game.activity.GameActivity;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.proto.ActivityDetail.ActivityMsg;
import emu.nebula.proto.Public.ActivityQuest;
import emu.nebula.proto.Public.ActivityTowerDefenseLevel;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import lombok.Getter;
@Getter
@Entity
public class TowerDefenseActivity extends GameActivity {
private Int2IntMap completedStages;
private Int2IntMap completedQuests;
@Deprecated // Morphia only
public TowerDefenseActivity() {
}
public TowerDefenseActivity(ActivityManager manager, ActivityDef data) {
super(manager, data);
this.completedStages = new Int2IntOpenHashMap();
this.completedQuests = new Int2IntOpenHashMap();
}
public PlayerChangeInfo claimReward(int level) {
// Get rewards
var rewards = GameData.getTowerDefenseLevelDataTable().get(level).getRewards();
// Add rewards
return getPlayer().getInventory().addItems(rewards);
}
// public PlayerChangeInfo claimReward(int groupId) {
// // Create change info
// var change = new PlayerChangeInfo();
// // Make sure we haven't completed this group yet
// if (this.getCompleted().contains(groupId)) {
// return change;
// }
// // Get trial control
// var control = GameData.getTrialControlDataTable().get(this.getId());
// if (control == null) return change;
// // Get group
// var group = GameData.getTrialGroupDataTable().get(groupId);
// if (group == null) return change;
// // Set as completed
// this.getCompleted().add(groupId);
// // Save to database
// this.save();
// // Add rewards
// return getPlayer().getInventory().addItems(group.getRewards(), change);
// }
// Proto
@Override
public void encodeActivityMsg(ActivityMsg msg) {
var proto = msg.getMutableTowerDefense();
// Add completed stages
for (int id : this.completedStages.keySet()) {
// Create proto
var level = ActivityTowerDefenseLevel.newInstance();
// Set proto params
level.setId(id);
level.setStar(this.completedStages.get(id));
// Add to final msg proto
proto.addLevels(level);
}
// Add completed quests
for (int id : this.completedStages.keySet()) {
// Create proto
var quest = ActivityQuest.newInstance();
// Set proto params
quest.setActivityId(this.getId());
quest.setId(id);
quest.setStatus(2); // TODO: properly handle event quests
// Add to final msg proto
proto.addQuests(quest);
}
}
}

View File

@@ -0,0 +1,67 @@
package emu.nebula.game.activity.type;
import dev.morphia.annotations.Entity;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.ActivityDef;
import emu.nebula.game.activity.ActivityManager;
import emu.nebula.game.activity.GameActivity;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.proto.ActivityDetail.ActivityMsg;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import lombok.Getter;
@Getter
@Entity
public class TrialActivity extends GameActivity {
private IntList completed;
@Deprecated // Morphia only
public TrialActivity() {
}
public TrialActivity(ActivityManager manager, ActivityDef data) {
super(manager, data);
this.completed = new IntArrayList();
}
public PlayerChangeInfo claimReward(int groupId) {
// Create change info
var change = new PlayerChangeInfo();
// Make sure we haven't completed this group yet
if (this.getCompleted().contains(groupId)) {
return change;
}
// Get trial control
var control = GameData.getTrialControlDataTable().get(this.getId());
if (control == null) return change;
// Get group
var group = GameData.getTrialGroupDataTable().get(groupId);
if (group == null) return change;
// Set as completed
this.getCompleted().add(groupId);
// Save to database
this.save();
// Add rewards
return getPlayer().getInventory().addItems(group.getRewards(), change);
}
// Proto
@Override
public void encodeActivityMsg(ActivityMsg msg) {
var proto = msg.getMutableTrial();
for (int id : this.getCompleted()) {
proto.addCompletedGroupIds(id);
}
}
}

View File

@@ -8,11 +8,12 @@ import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import emu.nebula.data.GameData;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.character.GameCharacter;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.game.player.PlayerManager;
import emu.nebula.game.quest.QuestCondType;
import emu.nebula.game.quest.QuestCondition;
import lombok.Getter;
import us.hebi.quickbuf.RepeatedInt;
@@ -84,7 +85,7 @@ public class AgentManager extends PlayerManager implements GameDatabaseObject {
this.getAgents().put(agent.getId(), agent);
// Quest
this.getPlayer().triggerQuest(QuestCondType.AgentApplyTotal, 1);
this.getPlayer().trigger(QuestCondition.AgentApplyTotal, 1);
// Success
return agent;
@@ -182,8 +183,9 @@ public class AgentManager extends PlayerManager implements GameDatabaseObject {
// Save to database
this.save();
// Quest
this.getPlayer().triggerQuest(QuestCondType.AgentFinishTotal, list.size());
// Quest + Achievements
getPlayer().trigger(QuestCondition.AgentFinishTotal, list.size());
getPlayer().trigger(AchievementCondition.AgentWithSpecificFinishTotal, list.size());
// Success
return change.setSuccess(true);

View File

@@ -0,0 +1,63 @@
package emu.nebula.game.ban;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.player.PlayerErrorCode;
import emu.nebula.proto.Public.Error;
import emu.nebula.util.Utils;
import lombok.Getter;
@Getter
@Entity(value = "bans", useDiscriminator = false)
public class BanInfo implements GameDatabaseObject {
@Id
private String id;
private int playerUid;
private long startTime;
private long endTime;
private String reason;
private String bannedBy;
private String ipAddress;
@Deprecated // Morphia only
public BanInfo() {
}
public BanInfo(int playerUid, long endTime, String reason, String bannedBy, String ipAddress) {
this.playerUid = playerUid;
this.startTime = System.currentTimeMillis();
this.endTime = endTime;
this.reason = reason;
this.bannedBy = bannedBy;
this.ipAddress = ipAddress;
// Generate ID based on either player UID or IP address
this.id = (ipAddress != null && !ipAddress.isEmpty() && playerUid == 0) ? "ip_" + ipAddress :
"player_" + playerUid;
}
public boolean isExpired() {
return endTime != 0 && endTime < System.currentTimeMillis();
}
public String getExpirationDateString() {
if (endTime == 0) {
return "Never";
}
return Utils.formatTimestamp(this.endTime);
}
@Override
public void save() {
GameDatabaseObject.super.save();
}
public Error toProto() {
return Error.newInstance()
.setCode(PlayerErrorCode.ErrBan.getValue())
.addArguments(
getExpirationDateString() + "\n" +
(this.reason != null ? "\n (" + this.reason + ")" : "\n" + this.id));
}
}

View File

@@ -0,0 +1,242 @@
package emu.nebula.game.ban;
import emu.nebula.Nebula;
import emu.nebula.game.player.Player;
import emu.nebula.game.GameContext;
import emu.nebula.game.GameContextModule;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class BanModule extends GameContextModule {
private final Map<String, BanInfo> cachedIpBans = new ConcurrentHashMap<>();
private final Map<Integer, BanInfo> cachedPlayerBans = new ConcurrentHashMap<>();
public BanModule(GameContext context) {
super(context);
}
/**
* Ban a player by UID
* @param uid Player UID
* @param endTime Ban expiration time (0 = permanent)
* @param isBanIp Ban Ip
* @param reason Ban reason
* @param bannedBy Who banned the player
*/
public void banPlayer(int uid, long endTime, String reason, boolean isBanIp, String bannedBy) {
Player player = getGameContext().getPlayerModule().getPlayer(uid);
// It is only used as a supplement to find the banned IP when unblocking a player
String playerBindIp = null;
if (isBanIp) {
if (player != null && player.getSession() != null) {
String ipAddress = player.getSession().getIpAddress();
if (ipAddress != null && !ipAddress.isEmpty()) {
playerBindIp = ipAddress;
banIp(ipAddress, endTime, reason, bannedBy);
}
}
}
BanInfo banInfo = new BanInfo(uid, endTime, reason, bannedBy, playerBindIp);
cachedPlayerBans.put(uid, banInfo);
banInfo.save();
// Kick player
if (player != null && player.isLoaded()) {
player.setSession(null);
}
}
/**
* Ban an IP address
* <p>
* Please be cautious about disabling IPs
* in some regions where IPs are scarce, many people may share a public IP
* and restarting the optical cat device will reassign a new IP
*
* @param ipAddress IP address to ban
* @param endTime Ban expiration time (0 = permanent)
* @param reason Ban reason
* @param bannedBy Who banned the IP
*/
public void banIp(String ipAddress, long endTime, String reason, String bannedBy) {
BanInfo banInfo = new BanInfo(0, endTime, reason, bannedBy, ipAddress);
cachedIpBans.put(ipAddress, banInfo);
banInfo.save();
List<Player> playerList = Nebula.getGameContext().getPlayerModule().getCachedPlayers()
.values().stream().toList();
String playerIpAddress;
for (Player player : playerList) {
playerIpAddress = player.getSession().getIpAddress();
if (playerIpAddress == null) {
return;
}
// Kick player
if (playerIpAddress.equals(ipAddress)) {
player.setSession(null);
}
}
}
/**
* Unban a player
* @param uid Player UID
*/
public void unbanPlayer(int uid) {
BanInfo banInfo = cachedPlayerBans.remove(uid);
if (banInfo == null) {
banInfo = getPlayerBanInfo(uid);
}
if (banInfo == null) {
return;
}
deleteBan(banInfo);
}
/**
* Unban an IP address
* @param ipAddress IP address to unban
*/
public void unbanIp(String ipAddress) {
BanInfo banInfo = cachedIpBans.remove(ipAddress);
if (banInfo == null) {
banInfo = getIpBanInfo(ipAddress);
}
if (banInfo == null) {
return;
}
deleteBan(banInfo);
}
/**
* Check if a player is banned
* @param uid Player UID
* @return True if banned, false otherwise
*/
public boolean isPlayerBanned(int uid) {
BanInfo banInfo = cachedPlayerBans.get(uid);
if (banInfo == null) {
banInfo = loadPlayerBanFromDatabase(uid);
if (banInfo == null) {
return false;
}
cachedPlayerBans.put(uid, banInfo);
}
// Check if ban has expired
if (banInfo.isExpired()) {
unbanPlayer(uid);
return false;
}
return true;
}
/**
* Check if an IP address is banned
* @param ipAddress IP address
* @return True if banned, false otherwise
*/
public boolean isIpBanned(String ipAddress) {
BanInfo banInfo = cachedIpBans.get(ipAddress);
if (banInfo == null) {
banInfo = loadIpBanFromDatabase(ipAddress);
if (banInfo == null) {
return false;
}
cachedIpBans.put(ipAddress, banInfo);
}
// Check if ban has expired
if (banInfo.isExpired()) {
unbanIp(ipAddress);
return false;
}
return true;
}
/**
* Get ban info for a player
* @param uid Player UID
* @return BanInfo or null if not banned
*/
public BanInfo getPlayerBanInfo(int uid) {
BanInfo banInfo = cachedPlayerBans.get(uid);
if (banInfo == null) {
banInfo = loadPlayerBanFromDatabase(uid);
if (banInfo != null) {
cachedPlayerBans.put(uid, banInfo);
}
}
return banInfo;
}
/**
* Get ban info for an IP
* @param ipAddress IP address
* @return BanInfo or null if not banned
*/
public BanInfo getIpBanInfo(String ipAddress) {
BanInfo banInfo = cachedIpBans.get(ipAddress);
if (banInfo == null) {
banInfo = loadIpBanFromDatabase(ipAddress);
if (banInfo != null) {
cachedIpBans.put(ipAddress, banInfo);
}
}
return banInfo;
}
/**
* Delete ban from database
*/
private void deleteBan(BanInfo banInfo) {
try {
Nebula.getGameDatabase().delete(banInfo);
} catch (Exception e) {
Nebula.getLogger().error("Failed to delete ban info from database", e);
}
}
/**
* Load player ban from database
*/
private BanInfo loadPlayerBanFromDatabase(int uid) {
if (uid == 0)
return null;
try {
return Nebula.getGameDatabase().getObjectByField(BanInfo.class, "playerUid", uid);
} catch (Exception e) {
Nebula.getLogger().error("Failed to load player ban from database", e);
}
return null;
}
/**
* Load IP ban from database
*/
private BanInfo loadIpBanFromDatabase(String ipAddress) {
if (ipAddress == null || ipAddress.isEmpty())
return null;
try {
return Nebula.getGameDatabase().getObjectByField(BanInfo.class, "ipAddress", ipAddress);
} catch (Exception e) {
Nebula.getLogger().error("Failed to load IP ban from database", e);
}
return null;
}
}

View File

@@ -16,7 +16,6 @@ import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.game.quest.GameQuest;
import emu.nebula.game.quest.QuestCondType;
import emu.nebula.game.quest.QuestType;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.BattlePassInfoOuterClass.BattlePassInfo;
@@ -31,7 +30,7 @@ public class BattlePass implements GameDatabaseObject {
private int uid;
private transient BattlePassManager manager;
private int battlePassId;
private int battlePassId; // Season id
private int mode;
private int level;
private int exp;
@@ -72,6 +71,13 @@ public class BattlePass implements GameDatabaseObject {
return manager.getPlayer();
}
/**
* Sets the mode directly
*/
public synchronized void setMode(int mode) {
this.mode = mode;
}
public boolean isPremium() {
return this.mode > 0;
}
@@ -80,6 +86,14 @@ public class BattlePass implements GameDatabaseObject {
return GameData.getBattlePassRewardDataTable().get((this.getBattlePassId() << 16) + level);
}
/**
* Sets the level directly, use getMaxExp() instead if adding exp.
*/
public synchronized void setLevel(int level) {
this.level = level;
this.exp = 0;
}
public int getMaxExp() {
var data = GameData.getBattlePassLevelDataTable().get(this.getLevel() + 1);
return data != null ? data.getExp() : 0;
@@ -101,6 +115,32 @@ public class BattlePass implements GameDatabaseObject {
}
}
/**
* Returns true if any rewards or quests are claimable
*/
public synchronized boolean hasNew() {
// Check if any quests are complete but unclaimed
for (var quest : getQuests().values()) {
if (quest.isComplete() && !quest.isClaimed()) {
return true;
}
}
// Check if we have any pending rewards
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;
}
public synchronized void resetDailyQuests(boolean resetWeekly) {
// Reset daily quests
for (var data : GameData.getBattlePassQuestDataTable()) {
@@ -119,14 +159,19 @@ public class BattlePass implements GameDatabaseObject {
this.syncQuest(quest);
}
// Reset weekly limit for exp
if (resetWeekly) {
this.expWeek = 0;
}
// Persist to database
this.save();
}
public synchronized void trigger(QuestCondType condition, int progress, int param) {
public synchronized void trigger(int condition, int progress, int param1, int param2) {
for (var quest : getQuests().values()) {
// Try to trigger quest
boolean result = quest.trigger(condition, progress, param);
boolean result = quest.trigger(condition, progress, param1, param2);
// Skip if quest progress wasn't changed
if (!result) {

View File

@@ -12,6 +12,10 @@ public class BattlePassManager extends PlayerManager {
public BattlePassManager(Player player) {
super(player);
}
public boolean hasNew() {
return this.getBattlePass().hasNew();
}
// Database

View File

@@ -6,6 +6,7 @@ import java.util.Map;
import dev.morphia.annotations.Entity;
import emu.nebula.Nebula;
import emu.nebula.data.resources.ChatDef;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.Public.Contacts;
@@ -125,6 +126,9 @@ public class CharacterContact {
);
}
// Trigger quest/achievement
this.getCharacter().getPlayer().trigger(AchievementCondition.ChatTotal, 1);
// Success
return change.setSuccess(true);
}

View File

@@ -8,8 +8,10 @@ import emu.nebula.data.GameData;
import emu.nebula.data.resources.CharacterDef;
import emu.nebula.data.resources.DiscDef;
import emu.nebula.game.player.PlayerManager;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.Public.HandbookInfo;
import emu.nebula.util.Bitset;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
@@ -22,6 +24,10 @@ public class CharacterStorage extends PlayerManager {
private final Int2ObjectMap<GameCharacter> characters;
private final Int2ObjectMap<GameDisc> discs;
// Flags
@Setter private boolean hasAddedChar;
@Setter private boolean hasAddedDisc;
@Setter private boolean updateCharHandbook;
@Setter private boolean updateDiscHandbook;
@@ -52,26 +58,39 @@ 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;
}
// Prevent players from getting unavliable characters
if (!data.isAvailable()) {
return null;
}
// Create character
var character = new GameCharacter(this.getPlayer(), data);
// Save to database
character.save();
// Set flag for player to update character skins in their handbook
this.setUpdateCharHandbook(true);
// Add to characters
this.characters.put(character.getCharId(), character);
// Set flags for player to update character skins in their handbook
this.setUpdateCharHandbook(true);
this.setHasAddedChar(true);
// Return character
return character;
}
@@ -79,18 +98,6 @@ public class CharacterStorage extends PlayerManager {
return this.getCharacters().values();
}
public int getNewPhoneMessageCount() {
int count = 0;
for (var character : this.getCharacterCollection()) {
if (character.getContact().hasNew()) {
count++;
}
}
return count;
}
public HandbookInfo getCharacterHandbook() {
var bitset = new Bitset();
@@ -130,26 +137,39 @@ 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) {
// Sanity check to make sure we dont have this character already
public GameDisc addDisc(DiscDef data) {
// Sanity check to make sure we dont have this disc already
if (this.hasDisc(data.getId())) {
return null;
}
// Prevent players from getting unavliable discs
if (!data.isAvailable()) {
return null;
}
// Create disc
var disc = new GameDisc(this.getPlayer(), data);
// Save to database
disc.save();
// Set flag for player to update discs in their handbook
this.setUpdateDiscHandbook(true);
// Add to discs
this.discs.put(disc.getDiscId(), disc);
// Set flags for player to update discs in their handbook
this.setUpdateDiscHandbook(true);
this.setHasAddedDisc(true);
// Return disc
return disc;
}
@@ -215,6 +235,73 @@ public class CharacterStorage extends PlayerManager {
return change.setExtraData(modifiedDiscs);
}
// Contacts
public int getNewPhoneMessageCount() {
int count = 0;
for (var character : this.getCharacterCollection()) {
if (character.getContact().hasNew()) {
count++;
}
}
return count;
}
//
/**
* Checks if we should add next packages for player
*/
public void checkPlayerState() {
// Check if we need to trigger character achievements
if (this.hasAddedChar) {
this.hasAddedChar = false;
this.getPlayer().trigger(
AchievementCondition.CharactersWithSpecificQuantityAndRarity,
getCharacters().size()
);
this.getPlayer().trigger(
AchievementCondition.CharactersWithSpecificQuantityAndRarity,
(int) getCharacters().values().stream().filter(GameCharacter::isMaster).count(),
1,
0
);
}
// Check if we need to send handbook update to player
if (this.updateCharHandbook) {
this.updateCharHandbook = false;
this.getPlayer().addNextPackage(
NetMsgId.handbook_change_notify,
this.getPlayer().getCharacters().getCharacterHandbook());
}
// Check if we need to trigger disc achievements
if (this.hasAddedChar) {
this.hasAddedChar = false;
this.getPlayer().trigger(
AchievementCondition.DiscAcquireSpecificQuantityAndRarity,
this.getDiscs().size()
);
this.getPlayer().trigger(
AchievementCondition.DiscAcquireSpecificQuantityAndRarity,
(int) getDiscs().values().stream().filter(GameDisc::isMaster).count(),
1,
0
);
}
// Check if we need to send handbook update to player
if (this.updateDiscHandbook) {
this.updateDiscHandbook = false;
this.getPlayer().addNextPackage(
NetMsgId.handbook_change_notify,
this.getPlayer().getCharacters().getDiscHandbook());
}
}
// Database
public void loadFromDatabase() {

View File

@@ -7,10 +7,10 @@ import lombok.Getter;
@Getter
public enum ElementType {
INHERIT (0),
WIND (1, 90020),
AQUA (1, 90018),
FIRE (2, 90019),
EARTH (3, 90021),
AQUA (4, 90018),
WIND (4, 90020),
LIGHT (5, 90022),
DARK (6, 90023),
NONE (7);

View File

@@ -18,10 +18,11 @@ import emu.nebula.data.GameData;
import emu.nebula.data.resources.CharacterDef;
import emu.nebula.data.resources.TalentGroupDef;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.game.quest.QuestCondType;
import emu.nebula.game.quest.QuestCondition;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.Notify.Skin;
import emu.nebula.proto.Notify.SkinChange;
@@ -32,6 +33,7 @@ import emu.nebula.proto.Public.CharGemSlot;
import emu.nebula.proto.Public.UI32;
import emu.nebula.proto.PublicStarTower.StarTowerChar;
import emu.nebula.proto.PublicStarTower.StarTowerCharGem;
import emu.nebula.server.error.ServerException;
import emu.nebula.util.Bitset;
import emu.nebula.util.CustomIntArray;
import it.unimi.dsi.fastutil.ints.IntArrayList;
@@ -117,6 +119,10 @@ public class GameCharacter implements GameDatabaseObject {
}
}
public boolean isMaster() {
return this.getData().getGrade() == 1;
}
public void setLevel(int level) {
this.level = level;
}
@@ -186,7 +192,7 @@ public class GameCharacter implements GameDatabaseObject {
// Check if we leveled up
if (this.level > oldLevel) {
// Trigger quest
this.getPlayer().triggerQuest(QuestCondType.CharacterUpTotal, this.level - oldLevel);
this.getPlayer().trigger(QuestCondition.CharacterUpTotal, this.level - oldLevel);
}
// Save to database
@@ -272,6 +278,9 @@ public class GameCharacter implements GameDatabaseObject {
// Save to database
this.save();
// Trigger quest/achievement
this.getPlayer().trigger(AchievementCondition.CharacterAdvanceTotal, 1);
// Success
return changes.setSuccess(true);
}
@@ -459,8 +468,8 @@ public class GameCharacter implements GameDatabaseObject {
// Add affinity exp
this.addAffinityExp(exp);
// Trigger quest
this.getPlayer().triggerQuest(QuestCondType.GiftGiveTotal, count);
// Trigger quest/achievement
this.getPlayer().trigger(QuestCondition.GiftGiveTotal, count);
// Remove items
var change = this.getPlayer().getInventory().removeItems(items);
@@ -652,16 +661,16 @@ public class GameCharacter implements GameDatabaseObject {
return true;
}
public synchronized PlayerChangeInfo generateGem(int slotId) {
public synchronized PlayerChangeInfo generateGem(int slotId) throws ServerException {
// Get gem slot
var slot = this.getGemSlot(slotId);
if (slot == null) {
return null;
throw new ServerException(110105, "Emblem slot doesn't exist");
}
// Skip if slot is full
if (slot.isFull()) {
return null;
throw new ServerException(110105, "Emblem slots are full");
}
// Get gem data
@@ -670,12 +679,12 @@ public class GameCharacter implements GameDatabaseObject {
// Check character level
if (this.getLevel() < gemControl.getUnlockLevel()) {
return null;
throw new ServerException(110105, "Trekker needs to be at least level " + gemControl.getUnlockLevel());
}
// Make sure the player has the materials to craft the emblem
if (!getPlayer().getInventory().hasItem(gemData.getGenerateCostTid(), gemControl.getGeneratenCostQty())) {
return null;
throw new ServerException(119903);
}
// Generate attributes and create gem
@@ -699,10 +708,12 @@ public class GameCharacter implements GameDatabaseObject {
}
@SuppressWarnings("deprecation")
public synchronized PlayerChangeInfo refreshGem(int slotId, int gemIndex, RepeatedInt lockedAttributes) {
public synchronized PlayerChangeInfo refreshGem(int slotId, int gemIndex, RepeatedInt lockedAttributes) throws ServerException {
// Get gem from slot
var gem = this.getGemFromSlot(slotId, gemIndex);
if (gem == null) return null;
if (gem == null) {
throw new ServerException(111609);
}
// Get gem data
var gemData = this.getData().getCharGemData(slotId);
@@ -710,12 +721,12 @@ public class GameCharacter implements GameDatabaseObject {
// Check character level
if (this.getLevel() < gemControl.getUnlockLevel()) {
return null;
throw new ServerException(110105, "Trekker needs to be at least level " + gemControl.getUnlockLevel());
}
// Get locked attributes
if (lockedAttributes.length() > gemControl.getLockableNum()) {
return null;
throw new ServerException(110105, "You can only lock up to " + gemControl.getLockableNum() + " attributes");
}
// Calculate the materials we need

View File

@@ -11,10 +11,11 @@ import emu.nebula.data.GameData;
import emu.nebula.data.resources.DiscDef;
import emu.nebula.data.resources.SubNoteSkillPromoteGroupDef;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.game.quest.QuestCondType;
import emu.nebula.game.quest.QuestCondition;
import emu.nebula.proto.Public.Disc;
import emu.nebula.proto.PublicStarTower.StarTowerDisc;
@@ -70,6 +71,10 @@ public class GameDisc implements GameDatabaseObject {
}
}
public boolean isMaster() {
return GameData.getItemDataTable().get(this.getDiscId()).getRarity() == 1;
}
public void setLevel(int level) {
this.level = level;
}
@@ -145,7 +150,7 @@ public class GameDisc implements GameDatabaseObject {
// Check if we leveled up
if (this.level > oldLevel) {
// Trigger quest
this.getPlayer().triggerQuest(QuestCondType.DiscStrengthenTotal, this.level - oldLevel);
this.getPlayer().trigger(QuestCondition.DiscStrengthenTotal, this.level - oldLevel);
}
// Save to database
@@ -215,6 +220,9 @@ public class GameDisc implements GameDatabaseObject {
// Save to database
this.save();
// Trigger quest/achievement
this.getPlayer().trigger(AchievementCondition.DiscPromoteTotal, 1);
// Success
return change.setSuccess(true);
}
@@ -237,9 +245,11 @@ public class GameDisc implements GameDatabaseObject {
// Remove items
var change = this.getPlayer().getInventory().removeItems(materials, null);
// Add star
// Cache old star value
int old = this.star;
this.star = Math.max(this.star + count, 5);
// Add star
this.star = Math.min(this.star + count, 5);
// Save to database if star count changed
if (this.star != old) {

View File

@@ -4,7 +4,7 @@ import emu.nebula.data.GameData;
import emu.nebula.game.character.GameCharacter;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerManager;
import emu.nebula.game.quest.QuestCondType;
import emu.nebula.game.quest.QuestCondition;
import lombok.Getter;
@Getter
@@ -26,8 +26,8 @@ public class DatingManager extends PlayerManager {
// Set landmark + character
this.game = new DatingGame(character, data);
// Trigger quest
this.getPlayer().triggerQuest(QuestCondType.CharactersDatingTotal, 1);
// Trigger quest/achievement
this.getPlayer().trigger(QuestCondition.CharactersDatingTotal, 1);
// Success
return this.game;

View File

@@ -3,6 +3,7 @@ package emu.nebula.game.gacha;
import emu.nebula.data.GameData;
import emu.nebula.game.GameContext;
import emu.nebula.game.GameContextModule;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.inventory.ItemAcquireMap;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.inventory.ItemType;
@@ -164,6 +165,9 @@ public class GachaModule extends GameContextModule {
var log = new GachaHistoryLog(data.getGachaType(), results);
player.getGachaManager().addGachaHistory(log);
// Trigger achievements
player.trigger(AchievementCondition.GachaTotal, amount);
// Complete
return new GachaResult(info, change, results);
}

View File

@@ -2,22 +2,26 @@ package emu.nebula.game.infinitytower;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.InfinityTowerLevelDef;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.game.player.PlayerManager;
import emu.nebula.game.player.PlayerProgress;
import lombok.Getter;
@Getter
public class InfinityTowerManager extends PlayerManager {
private InfinityTowerLevelDef levelData;
private int levelId;
private long buildId;
public InfinityTowerManager(Player player) {
super(player);
}
private PlayerProgress getProgress() {
return this.getPlayer().getProgress();
}
public int getBountyLevel() {
return 0;
@@ -59,7 +63,8 @@ public class InfinityTowerManager extends PlayerManager {
}
// Check logs if the player has completed the level already
if (this.getPlayer().getProgress().getInfinityArenaLog().containsKey(this.getLevelId())) {
int highestLevel = this.getProgress().getInfinityTowerLog().get(this.getLevelData().getTowerId());
if (highestLevel >= this.getLevelId()) {
return change;
}
@@ -73,7 +78,14 @@ public class InfinityTowerManager extends PlayerManager {
change.setExtraData(rewards);
// Log in player progress
this.getPlayer().getProgress().addInfinityArenaLog(this.getLevelId());
this.getPlayer().getProgress().addInfinityTowerLog(this.getLevelData());
// Trigger achievement
this.getPlayer().trigger(AchievementCondition.InfinityTowerClearSpecificFloor, 10, this.getLevelId(), 0);
// Clear instance
this.levelId = 0;
this.buildId = 0;
// Success
return change.setSuccess(true);

View File

@@ -8,7 +8,7 @@ import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.game.player.PlayerManager;
import emu.nebula.game.player.PlayerProgress;
import emu.nebula.game.quest.QuestCondType;
import emu.nebula.game.quest.QuestCondition;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import lombok.Getter;
@@ -35,7 +35,7 @@ public class InstanceManager extends PlayerManager {
return this.getPlayer().getProgress();
}
public PlayerChangeInfo settleInstance(InstanceData data, QuestCondType questCondition, Int2IntMap log, String logName, int star) {
public PlayerChangeInfo settleInstance(InstanceData data, QuestCondition questCondition, Int2IntMap log, String logName, int star) {
// Calculate settle data
var settleData = new InstanceSettleData();
@@ -63,18 +63,22 @@ public class InstanceManager extends PlayerManager {
this.getProgress().saveInstanceLog(log, logName, data.getId(), star);
// Quest triggers
this.getPlayer().triggerQuest(questCondition, 1);
this.getPlayer().triggerQuest(QuestCondType.BattleTotal, 1);
this.getPlayer().trigger(questCondition, 1);
this.getPlayer().trigger(QuestCondition.BattleTotal, 1);
}
// Set extra data
change.setExtraData(settleData);
// Clear instance
this.curInstanceId = 0;
this.rewardType = 0;
// Success
return change.setSuccess(true);
}
public PlayerChangeInfo sweepInstance(InstanceData data, QuestCondType questCondition, Int2IntMap log, int rewardType, int count) {
public PlayerChangeInfo sweepInstance(InstanceData data, QuestCondition questCondition, Int2IntMap log, int rewardType, int count) {
// Sanity check count
if (count <= 0) {
return null;
@@ -131,8 +135,58 @@ public class InstanceManager extends PlayerManager {
change.setExtraData(list);
// Quest triggers
this.getPlayer().triggerQuest(questCondition, count);
this.getPlayer().triggerQuest(QuestCondType.BattleTotal, count);
this.getPlayer().trigger(questCondition, count);
this.getPlayer().trigger(QuestCondition.BattleTotal, count);
// Success
return change.setSuccess(true);
}
public PlayerChangeInfo settleWeekly(InstanceData data, boolean win) {
// Calculate settle data
var settleData = new InstanceSettleData();
settleData.setWin(win);
settleData.setFirst(settleData.isWin() && !getProgress().getWeekBossLog().containsKey(data.getId()));
// Init player change info
var change = new PlayerChangeInfo();
// Handle win
if (settleData.isWin()) {
// Calculate rewards
int entries = this.getPlayer().getInventory().getResourceCount(GameConstants.WEEKLY_ENTRY_ITEM_ID);
if (entries > 0) {
// Generate regular rewards
settleData.setRewards(data.getRewards().generate());
// Add regular rewards
getPlayer().getInventory().addItems(settleData.getRewards(), change);
// Remove weekly entry
getPlayer().getInventory().removeItem(GameConstants.WEEKLY_ENTRY_ITEM_ID, 1, change);
}
// Add first clear rewards even if we dont have the entry ticket
if (settleData.isFirst()) {
// Generate first clear rewards
settleData.setRewards(data.getFirstRewards().generate());
// Add to inventory
getPlayer().getInventory().addItems(settleData.getFirstRewards(), change);
}
// Log
this.getProgress().saveInstanceLog(getProgress().getWeekBossLog(), "weekBossLog", data.getId(), 1);
// Quest triggers
this.getPlayer().trigger(QuestCondition.WeekBoosClearSpecificDifficultyAndTotal, 1);
this.getPlayer().trigger(QuestCondition.BattleTotal, 1);
}
// Set extra data
change.setExtraData(settleData);
// Success
return change.setSuccess(true);

View File

@@ -17,6 +17,10 @@ public class InstanceSettleData {
}
public void generateRewards(InstanceData data) {
this.generateRewards(data, 0);
}
public void generateRewards(InstanceData data, int rewardType) {
if (this.isFirst) {
this.firstRewards = data.getFirstRewards(rewardType).generate();

View File

@@ -7,11 +7,12 @@ 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;
import emu.nebula.game.player.PlayerManager;
import emu.nebula.game.quest.QuestCondType;
import emu.nebula.game.quest.QuestCondition;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.Notify.Skin;
import emu.nebula.proto.Public.Honor;
@@ -20,6 +21,7 @@ import emu.nebula.proto.Public.Res;
import emu.nebula.proto.Public.Title;
import emu.nebula.proto.Public.UI32;
import emu.nebula.util.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.Int2ObjectMap;
@@ -283,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;
}
@@ -338,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);
@@ -376,25 +399,51 @@ 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 {
amount = 0;
}
}
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 {
amount = 0;
}
}
case Energy -> {
@@ -462,11 +511,11 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
}
}
// Trigger quest
// Trigger quest + achievement
if (amount > 0) {
this.getPlayer().triggerQuest(QuestCondType.ItemsAdd, amount, id);
} else {
this.getPlayer().triggerQuest(QuestCondType.ItemsDeplete, Math.abs(amount), id);
this.getPlayer().trigger(QuestCondition.ItemsAdd, amount, id);
} else if (amount < 0) {
this.getPlayer().trigger(QuestCondition.ItemsDeplete, Math.abs(amount), id);
}
//
@@ -634,6 +683,9 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
// Add produced items
this.addItem(data.getProductionId(), data.getProductionPerBatch() * num, change);
// Trigger achievement
this.getPlayer().trigger(AchievementCondition.ItemsProductTotal, num);
// Success
return change.setSuccess(true);
}
@@ -821,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

@@ -12,6 +12,9 @@ import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.account.Account;
import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.achievement.AchievementManager;
import emu.nebula.game.activity.ActivityManager;
import emu.nebula.game.agent.AgentManager;
import emu.nebula.game.battlepass.BattlePassManager;
import emu.nebula.game.character.CharacterStorage;
@@ -23,7 +26,7 @@ import emu.nebula.game.infinitytower.InfinityTowerManager;
import emu.nebula.game.instance.InstanceManager;
import emu.nebula.game.inventory.Inventory;
import emu.nebula.game.mail.Mailbox;
import emu.nebula.game.quest.QuestCondType;
import emu.nebula.game.quest.QuestCondition;
import emu.nebula.game.quest.QuestManager;
import emu.nebula.game.scoreboss.ScoreBossManager;
import emu.nebula.game.story.StoryManager;
@@ -32,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;
@@ -41,7 +45,6 @@ import emu.nebula.proto.Public.Friend;
import emu.nebula.proto.Public.HonorInfo;
import emu.nebula.proto.Public.NewbieInfo;
import emu.nebula.proto.Public.QuestType;
import emu.nebula.proto.Public.Story;
import emu.nebula.proto.Public.WorldClass;
import emu.nebula.proto.Public.WorldClassRewardState;
import emu.nebula.util.Utils;
@@ -81,6 +84,8 @@ public class Player implements GameDatabaseObject {
private int energy;
private long energyLastUpdate;
private int signInIndex;
private long lastEpochDay;
private long lastLogin;
private long createTime;
@@ -104,7 +109,9 @@ public class Player implements GameDatabaseObject {
private transient PlayerProgress progress;
private transient StoryManager storyManager;
private transient QuestManager questManager;
private transient AchievementManager achievementManager;
private transient AgentManager agentManager;
private transient ActivityManager activityManager;
// Extra
private transient Stack<NetMsgPacket> nextPackages;
@@ -180,23 +187,26 @@ public class Player implements GameDatabaseObject {
}
public void setSession(GameSession session) {
if (this.session != null) {
// Sanity check
if (this.session == session) {
return;
}
// Clear player from session
this.session.clearPlayer();
// Don't set session if it's the same session
if (this.session == session) {
return;
}
// 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() {
@@ -204,8 +214,14 @@ public class Player implements GameDatabaseObject {
}
public void setLevel(int level) {
// Set player world class (level)
this.level = level;
// Save to database
Nebula.getGameDatabase().update(this, this.getUid(), "level", this.level);
// Trigger achievement
this.trigger(AchievementCondition.WorldClassSpecific, this.getLevel());
}
public void setExp(int exp) {
@@ -215,14 +231,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
@@ -481,16 +495,17 @@ public class Player implements GameDatabaseObject {
// Save to database
Nebula.getGameDatabase().update(
this,
this.getUid(),
"level",
this.getLevel(),
"exp",
this.getExp()
this,
this.getUid(),
"level",
this.getLevel(),
"exp",
this.getExp()
);
// Save level rewards if we changed it
if (oldLevel != this.getLevel()) {
// Update level rewards
this.getQuestManager().saveLevelRewards();
this.addNextPackage(
@@ -498,6 +513,9 @@ public class Player implements GameDatabaseObject {
WorldClassRewardState.newInstance()
.setFlag(getQuestManager().getLevelRewards().toBigEndianByteArray())
);
// Trigger achievement
this.trigger(AchievementCondition.WorldClassSpecific, this.getLevel());
}
// Calculate changes
@@ -550,7 +568,7 @@ public class Player implements GameDatabaseObject {
change = modifyEnergy(-amount, change);
// Trigger quest
this.triggerQuest(QuestCondType.EnergyDeplete, amount);
this.trigger(QuestCondition.EnergyDeplete, amount);
// Complete
return change;
@@ -590,6 +608,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;
}
@@ -598,29 +623,102 @@ 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);
// 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);
// Check to reset weeklies
if (resetWeekly) {
// Add weekly boss entry item
int entries = this.getInventory().getResourceCount(GameConstants.WEEKLY_ENTRY_ITEM_ID);
if (entries < 3) {
this.getInventory().addItem(GameConstants.WEEKLY_ENTRY_ITEM_ID, 3 - entries);
}
// Reset weekly tower tickets
this.getProgress().clearWeeklyTowerTicketLog();
}
// Check if we need to reset monthly
if (resetMonthly) {
// Reset monthly shop purchases
this.getInventory().resetShopPurchases();
}
}
// Trigger quests
// Trigger quests + achievements
public void triggerQuest(QuestCondType condition, int progress) {
this.triggerQuest(condition, progress, 0);
public void trigger(int condition, int progress, int param1, int param2) {
this.getQuestManager().trigger(condition, progress, param1, param2);
this.getBattlePassManager().getBattlePass().trigger(condition, progress, param1, param2);
this.getAchievementManager().trigger(condition, progress, param1, param2);
}
public void triggerQuest(QuestCondType condition, int progress, int param) {
this.getQuestManager().trigger(condition, progress, param);
this.getBattlePassManager().getBattlePass().trigger(condition, progress, param);
public void trigger(QuestCondition condition, int progress) {
this.trigger(condition.getValue(), progress, 0, 0);
}
public void trigger(QuestCondition condition, int progress, int param1) {
this.trigger(condition.getValue(), progress, param1, 0);
}
public void trigger(AchievementCondition condition, int progress) {
this.trigger(condition.getValue(), progress, 0, 0);
}
public void trigger(AchievementCondition condition, int progress, int param1, int param2) {
this.trigger(condition.getValue(), progress, param1, param2);
}
// Login
@@ -664,7 +762,9 @@ public class Player implements GameDatabaseObject {
this.gachaManager = this.loadManagerFromDatabase(GachaManager.class);
this.storyManager = this.loadManagerFromDatabase(StoryManager.class);
this.questManager = this.loadManagerFromDatabase(QuestManager.class);
this.achievementManager = this.loadManagerFromDatabase(AchievementManager.class);
this.agentManager = this.loadManagerFromDatabase(AgentManager.class);
this.activityManager = this.loadManagerFromDatabase(ActivityManager.class);
// Database fixes
if (this.showChars == null) {
@@ -672,17 +772,22 @@ public class Player implements GameDatabaseObject {
this.save();
}
// Init activities
this.getActivityManager().init();
// Load complete
this.loaded = true;
}
public void onCreate() {
// Send welcome mail
this.getMailbox().sendWelcomeMail();
}
public void onLogin() {
// See if we need to reset dailies
this.checkResetDailies();
// Trigger quest login
this.triggerQuest(QuestCondType.LoginTotal, 1);
// Update last login time
this.lastLogin = System.currentTimeMillis();
Nebula.getGameDatabase().update(this, this.getUid(), "lastLogin", this.getLastLogin());
@@ -698,11 +803,25 @@ public class Player implements GameDatabaseObject {
this.getNextPackages().add(new NetMsgPacket(msgId, proto));
}
// Misc
/**
* Called AFTER a response is sent to the client
*/
public void afterResponse() {
// Check if we need save achievements
if (this.getAchievementManager().isQueueSave()) {
this.getAchievementManager().save();
}
}
// Proto
public PlayerInfo toProto() {
PlayerInfo proto = PlayerInfo.newInstance()
.setServerTs(Nebula.getCurrentTime())
.setSigninIndex(this.getSignInIndex())
.setTowerTicket(this.getProgress().getTowerTickets())
.setDailyShopRewardStatus(this.getQuestManager().hasDailyReward())
.setAchievements(new byte[64]);
@@ -776,18 +895,20 @@ public class Player implements GameDatabaseObject {
// Set player states
var state = proto.getMutableState()
.setStorySet(true)
.setStorySet(this.getStoryManager().hasNew())
.setFriend(this.getFriendList().hasPendingRequests());
state.getMutableMail()
.setNew(this.getMailbox().hasNewMail());
state.getMutableBattlePass()
.setState(1);
.setState(this.getBattlePassManager().hasNew() ? 1 : 0);
state.getMutableAchievement()
.setNew(this.getAchievementManager().hasNewAchievements());
state.getMutableFriendEnergy();
state.getMutableMallPackage();
state.getMutableAchievement();
state.getMutableTravelerDuelQuest()
.setType(QuestType.TravelerDuel);
state.getMutableTravelerDuelChallengeQuest()
@@ -809,14 +930,7 @@ public class Player implements GameDatabaseObject {
acc.addNewbies(NewbieInfo.newInstance().setGroupId(GameConstants.INTRO_GUIDE_ID).setStepId(-1));
// Story
var story = proto.getMutableStory();
for (int storyId : this.getStoryManager().getCompletedStories()) {
var storyProto = Story.newInstance()
.setIdx(storyId);
story.addStories(storyProto);
}
this.getStoryManager().encodePlayerInfo(proto);
// Add titles
for (int titleId : this.getInventory().getTitles()) {
@@ -832,7 +946,7 @@ public class Player implements GameDatabaseObject {
}
// Quests
this.getQuestManager().encodeProto(proto);
this.getQuestManager().encodePlayerInfo(proto);
// Add dictionary tabs
for (var dictionaryData : GameData.getDictionaryTabDataTable()) {
@@ -851,7 +965,7 @@ public class Player implements GameDatabaseObject {
}
// Add progress
this.getProgress().encodeProto(proto);
this.getProgress().encodePlayerInfo(proto);
// Handbook
proto.addHandbook(this.getCharacters().getCharacterHandbook());
@@ -868,6 +982,11 @@ public class Player implements GameDatabaseObject {
agentProto.addInfos(agent.toProto());
}
// Activities
for (var activity : getActivityManager().getActivities().values()) {
proto.addActivities(activity.toProto());
}
// Complete
return proto;
}

View File

@@ -0,0 +1,192 @@
package emu.nebula.game.player;
import lombok.Getter;
// resources\bin\ErrorCode.json
// resources\language\en_US\ErrorCode.json
@Getter
public enum PlayerErrorCode {
ErrInvalidToken(100101),
ErrInvalidTimestamp(100102),
ErrInvalidMsgId(100103),
ErrInvalidMsgBody(100104),
ErrAgentBusy(100105),
ErrServiceUnavailable(100106),
ErrRequestTimeout(100107),
ErrLoginFailed(100108),
ErrInvalidVersion(100109),
ErrLogin(100110),
ErrMask(110101),
ErrServerBusy(110102),
ErrData(110103),
ErrArgs(110104),
ErrInternal(110105),
ErrTokenExpire(110106),
ErrRelogin(110107),
ErrInvalidReq(110108),
ErrApiDisabled(110109),
ErrResUpdated(110110),
ErrOperateTooFast(110111),
ErrNickLenViolation(110201),
ErrNickIllegal(110202),
ErrNickForbidden(110203),
ErrBan(110205),
ErrSignatureForbidden(110206),
ErrSignatureLenViolation(110207),
ErrSignatureIllegal(110208),
ErrTitleNotFound(110209),
ErrSurveyNotFound(110210),
ErrSurveyNotOpenYet(110211),
ErrSurveyHasCompleted(110212),
ErrMailMiss(110401),
ErrMailRevoked(110402),
ErrMailUpdate(110403),
ErrMailPinned(110404),
ErrQuestNotComplete(110501),
ErrQuestClosed(110502),
ErrProductNotReady(110601),
ErrProductRestockLimit(110602),
ErrGoodsNotFound(110603),
ErrGoodsLockPurchase(110604),
ErrGoodsDelisted(110605),
ErrShopNotFound(110606),
ErrShopAlreadyClosed(110607),
ErrRecvReward(110701),
ErrMainlineCondPremission(110702),
ErrEnergyHoldLimit(110801),
ErrEnergyBuyLimit(110802),
ErrEnergyLack(110803),
ErrEnergyRestore(110804),
ErrGachaSpinLimit(110901),
ErrItemUseRefused(111001),
ErrRglInProgress(111101),
ErrRglCond(111102),
ErrRglBuildNotFound(111103),
ErrRglBuildLock(111104),
ErrFormationIdNeed(111105),
ErrFormationFullNeed(111106),
ErrBuildNameMinLenLimit(111107),
ErrBuildNameMaxLenLimit(111108),
ErrBuildNameViolation(111109),
ErrOutfitStrengthenMax(111201),
ErrOutfitNoData(111202),
ErrOutfitBeEquipped(111203),
ErrOutfitBeLocked(111204),
ErrOutfitPhaseMax(111205),
ErrOutfitStarMax(111206),
ErrOutfitLevelNotReached(111207),
ErrGiftPackageInvalid(111301),
ErrGiftPackageExpired(111302),
ErrGiftPackError(111303),
ErrRedeemCodeReachedLimit(111304),
ErrUserExchangeCountReachedLimit(111305),
ErrGiftPackageAlreadyExists(111306),
ErrGiftPackageInfoInvalid(111307),
ErrRedeemCodeItemListEmpty(111308),
ErrRedeemCodeCodeItemInvalid(111309),
ErrGiftPackageStartTimeError(111310),
ErrGiftPackageEndTimeError(111311),
ErrGiftPackageRedeemCodeQuantityExceedsLimit(111312),
ErrFixedRedeemCodeAlreadyExists(111313),
ErrFixedRedeemCodeCannotBeEmpty(111314),
ErrRedeemCodeQuantityCannotBeZero(111315),
ErrFixedRedeemCodeUserUsageQuantityRequired(111316),
ErrRedeemCodeInvalid(111317),
ErrRedeemCodeFailed(111318),
ErrRedeemCodeAlreadyUsed(111319),
ErrRedeemFailed(111320),
ErrInviteNotFound(111401),
ErrUserInviteCountLimit(111402),
ErrUserFriendCountLimit(111403),
ErrAlreadyExistFriend(111404),
ErrFriendEnergyReceiveMax(110681),
ErrMallPackageNotListed(111501),
ErrMallPackageDelisted(111502),
ErrMallPackageStockLimit(111503),
ErrMallPackageOrderLimit(111504),
ErrOrderCantRemove(111505),
ErrHeartStoneMax(111601),
ErrWeaponMaxAdvance(111602),
ErrWeaponCondNoMet(111603),
ErrCharMaxLevel(111604),
ErrCharCondMaxAdvance(111605),
ErrCharMaxAdvance(111606),
ErrCharSkillMaxLevel(111607),
ErrCharSkillCondNotMet(111608),
ErrPresentsNoData(111609),
ErrPresentsUpgradeMax(111610),
ErrPresentsBeEquipped(111611),
ErrPresentsBeLocked(111612),
ErrNotAllowedRecruit(111613),
ErrAffinityMaxLevel(111614),
ErrAffinityGiftSendCountLimit(111615),
ErrDatingCountLimit(111616),
ErrDatingSendCountLimit(111617),
ErrCharWorldClassCondNotMet(111618),
ErrDatingCharLimit(111619),
ErrCharArchiveRewardReceived(111620),
ErrReceiveEnergyFriendNotData(111621),
ErrBattlePassVersionMismatch(111701),
ErrTravelerDuelSeasonEnd(111801),
ErrProductionFormulaUnactivated(111901),
ErrTalentAlreayActiveated(112001),
ErrTalentGroupNotYetOpen(112002),
ErrTalentKeyNodesInsufficientQuantity(112003),
ErrTalentOrdinaryNodesInsufficientQuantity(112004),
ErrTalentListEmpty(112005),
ErrTalentResetFrequently(112006),
ErrTalentActivationLimit(112007),
ErrTalentNotYetActivated(112008),
ErrEquipmentNoData(112101),
ErrEquipmentUpgradeMax(112102),
ErrEquipmentBeEquipped(112103),
ErrEquipmentBeLocked(112104),
ErrDiscNoData(112201),
ErrDiscLevelNotReached(112202),
ErrDiscStrengthenMax(112203),
ErrDiscPhaseMax(112204),
ErrDiscStarMax(112205),
ErrStarTowerRankSeasonNoData(112301),
ErrStarTowerRankSeasonNotYetOpen(112302),
ErrStarTowerRankSeasonEnd(112303),
ErrAgentNoData(112401),
ErrAgentInProcess(112402),
ErrAgentCond(112403),
ErrAgentCharOccupied(112404),
ErrAgentBuildOccupied(112405),
ErrAgentBuildScoreLow(112406),
ErrAgentCountLimit(112407),
ErrAgentDailyLimit(112408),
ErrAgentWeeklyLimit(112409),
ErrAgentNotCompleted(112410),
ErrVampireSurvivorBuildNum(112501),
ErrVampireSurvivorBuildCharRepetition(112502),
ErrTowerGrowthGroupActivated(112601),
ErrJointDrillNotExist(112701),
ErrJointDrillInProgress(112702),
ErrJointDrillNotYetOpen(112703),
ErrJointDrillAlreadyClosed(112704),
ErrScoreBossNotOpen(112801),
ErrNickNameResetLimit(112901),
ErrCapOverflow(119901),
ErrConfig(119902),
ErrCondCheck(119903),
ErrNotYetOpen(119904),
ErrAlreadyClosed(119905),
ErrCondNotMet(119906),
ErrInsufficientWorldClass(119907),
ErrNoRewardsToReceive(119908),
ErrAlreayExists(119909),
ErrExchangeNotSupported(119910),
ErrRequestTooFrequent(119911),
ErrAlreaydReceive(119912),
ErrLimit(119913),
ErrDataUpdated(119914);
final int value;
PlayerErrorCode(int value) {
this.value = value;
}
}

View File

@@ -10,6 +10,8 @@ import emu.nebula.Nebula;
import emu.nebula.game.GameContext;
import emu.nebula.game.GameContextModule;
import emu.nebula.game.account.Account;
import emu.nebula.game.achievement.AchievementManager;
import emu.nebula.game.activity.ActivityManager;
import emu.nebula.game.agent.AgentManager;
import emu.nebula.game.battlepass.BattlePass;
import emu.nebula.game.character.GameCharacter;
@@ -130,12 +132,13 @@ public class PlayerModule extends GameContextModule {
player.onLoad();
player.save();
// Send welcome mail
player.getMailbox().sendWelcomeMail();
// Handle any player creation events
player.onCreate();
// Put in player cache
this.addToCache(player);
// Complete
return player;
}
@@ -179,6 +182,8 @@ public class PlayerModule extends GameContextModule {
datastore.getCollection(StoryManager.class).deleteOne(idFilter);
datastore.getCollection(QuestManager.class).deleteOne(idFilter);
datastore.getCollection(AgentManager.class).deleteOne(idFilter);
datastore.getCollection(AchievementManager.class).deleteOne(idFilter);
datastore.getCollection(ActivityManager.class).deleteOne(idFilter);
datastore.getCollection(BattlePass.class).deleteOne(idFilter);
datastore.getCollection(ScoreBossRankEntry.class).deleteOne(idFilter);

View File

@@ -8,6 +8,7 @@ import dev.morphia.annotations.Id;
import dev.morphia.annotations.PostLoad;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.InfinityTowerLevelDef;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.tutorial.TutorialLevelLog;
import emu.nebula.game.vampire.VampireSurvivorLog;
@@ -34,6 +35,7 @@ public class PlayerProgress extends PlayerManager implements GameDatabaseObject
// Star Tower
private IntSet starTowerLog;
private int[] starTowerGrowth;
private int towerTickets;
// Instances
private Int2IntMap dailyInstanceLog;
@@ -43,7 +45,8 @@ public class PlayerProgress extends PlayerManager implements GameDatabaseObject
private Int2IntMap weekBossLog;
// Infinite Arena
private Int2IntMap infinityArenaLog;
private Int2IntMap infinityTowerLog;
@Deprecated private Int2IntMap infinityArenaLog;
// Vampire Survivors
private Map<Integer, VampireSurvivorLog> vampireLog;
@@ -75,8 +78,8 @@ public class PlayerProgress extends PlayerManager implements GameDatabaseObject
this.charGemLog = new Int2IntOpenHashMap();
this.weekBossLog = new Int2IntOpenHashMap();
// Infinity Arena
this.infinityArenaLog = new Int2IntOpenHashMap();
// Infinity Tower
this.infinityTowerLog = new Int2IntOpenHashMap();
// Vampire Survivor
this.vampireLog = new HashMap<>();
@@ -122,17 +125,55 @@ public class PlayerProgress extends PlayerManager implements GameDatabaseObject
return true;
}
public void addInfinityArenaLog(int levelId) {
// Calculate arena id
int id = (int) Math.floor(levelId / 10000D);
/**
* Returns the maximum amount of weekly tickets that a player can receive without hitting the limit
*/
public int getMaxEarnableWeeklyTowerTickets() {
return Math.max(this.getWeeklyTowerTicketLimit() - this.getTowerTickets(), 0);
}
public int getWeeklyTowerTicketLimit() {
int limit = 2000;
if (this.getPlayer().getStarTowerManager().hasGrowthNode(10502)) {
limit += 1000;
} else if (this.getPlayer().getStarTowerManager().hasGrowthNode(10201)) {
limit += 500;
}
return limit;
}
public void addWeeklyTowerTicketLog(int count) {
this.towerTickets += count;
Nebula.getGameDatabase().update(this, this.getUid(), "towerTickets", this.towerTickets);
}
public void clearWeeklyTowerTicketLog() {
if (this.towerTickets == 0) {
return;
}
this.towerTickets = 0;
Nebula.getGameDatabase().update(this, this.getUid(), "towerTickets", this.towerTickets);
}
public void addInfinityTowerLog(InfinityTowerLevelDef level) {
// Calculate tower id
int towerId = level.getTowerId();
int levelId = level.getId();
if (towerId <= 0) {
return;
}
// Check highest clear
int highestClearId = this.getInfinityArenaLog().get(id);
int highestClearId = this.getInfinityTowerLog().get(towerId);
// Add & Save to database
if (levelId > highestClearId) {
this.getInfinityArenaLog().put(id, levelId);
Nebula.getGameDatabase().update(this, this.getUid(), "infinityArenaLog." + id, levelId);
this.getInfinityTowerLog().put(towerId, levelId);
Nebula.getGameDatabase().update(this, this.getUid(), "infinityArenaLog." + towerId, levelId);
}
}
@@ -152,7 +193,7 @@ public class PlayerProgress extends PlayerManager implements GameDatabaseObject
// Proto
public void encodeProto(PlayerInfo proto) {
public void encodePlayerInfo(PlayerInfo proto) {
// Check if we want to unlock all instances
boolean unlockAll = Nebula.getConfig().getServerOptions().unlockInstances;
@@ -261,8 +302,43 @@ public class PlayerProgress extends PlayerManager implements GameDatabaseObject
@PostLoad
public void postLoad() {
boolean shouldSave = false;
// Fix missing star tower growth
if (this.starTowerGrowth == null) {
this.starTowerGrowth = new int[1];
shouldSave = true;
}
// Fix missing infinity tower log
if (this.infinityTowerLog == null) {
this.infinityTowerLog = new Int2IntOpenHashMap();
shouldSave = true;
}
// Carry over infinity tower progress
if (this.infinityArenaLog != null) {
for (int levelId : this.infinityArenaLog.values()) {
var data = GameData.getInfinityTowerLevelDataTable().get(levelId);
if (data == null) {
continue;
}
int towerId = data.getTowerId();
if (towerId > 0) {
this.infinityTowerLog.put(data.getTowerId(), levelId);
}
}
// Clear old infinity tower logs when done
this.infinityArenaLog = null;
shouldSave = true;
}
// Update in database if anything changed
if (shouldSave) {
this.save();
}
}
}

View File

@@ -55,19 +55,19 @@ public class GameQuest {
return 0;
}
public boolean trigger(QuestCondType condition, int progress, int param) {
public boolean trigger(int condition, int progress, int param1, int param2) {
// Sanity check
if (this.isComplete()) {
return false;
}
// Skip if not the correct condition
if (this.cond != condition.getValue()) {
if (this.cond != condition) {
return false;
}
// Check quest param
if (this.param != 0 && param != this.param) {
if (this.param != 0 && param1 != this.param) {
return false;
}

View File

@@ -0,0 +1,144 @@
package emu.nebula.game.quest;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
public enum QuestCondition {
BattleTotal (3),
BattlesTotalWithPartner (4),
CharacterAcquireQuantityRarityAndAdvancement (6),
CharacterAdvanceTotal (7),
CharacterSkillUpTotal (8),
CharacterSkillWithSpecificUpTotal (9),
CharacterUpTotal (12),
CharacterWithSpecificAdvance (13),
CharacterWithSpecificUpLevel (15),
CharactersWithSpecificNumberLevelAndAttributes (17),
CharactersWithSpecificQuantityAdvancementCountAndAttribute (19),
CharactersWithSpecificQuantityRarityAndLevel (22),
ChatTotal (23),
DailyInstanceClearSpecificDifficultyAndTotal (24),
DailyInstanceClearSpecificTypeAndTotal (25),
DailyInstanceClearTotal (26),
DiscAcquireQuantityLevelAndRarity (30),
DiscAcquireQuantityPhaseAndRarity (31),
DiscAcquireQuantityStarAndRarity (32),
DiscLimitBreakTotal (33),
DiscPromoteTotal (34),
DiscStrengthenTotal (35),
DiscWithSpecificQuantityLevelAndRarity (36),
DiscWithSpecificQuantityPhaseAndRarity (37),
DiscWithSpecificQuantityStarAndRarity (38),
EnergyDeplete (39),
GachaTotal (44),
GiftGiveTotal (45),
InfinityTowerClearSpecificFloor (46),
InfinityTowerClearTotal (47),
ItemsAdd (48),
ItemsDeplete (49),
ItemsProductTotal (50),
LoginTotal (51),
QuestTravelerDuelChallengeTotal (52),
QuestTourGuideSpecific (53),
QuestTravelerDuelSpecific (54),
QuestWithSpecificType (55),
RegionBossClearSpecificFullStarWithBossIdAndDifficulty (56),
RegionBossClearSpecificLevelWithDifficultyAndTotal (57),
RegionBossClearSpecificTotal (58),
RegionBossClearTotal (59),
SkillsWithSpecificQuantityAndLevel (60),
StageClearSpecificStars (62),
StoryClear (63),
TravelerDuelChallengeSpecificBoosLevelWithDifficultyAndTotal (64),
TravelerDuelClearBossTotal (65),
TravelerDuelClearSpecificBossIdAndDifficulty (66),
TravelerDuelChallengeClearSpecificBossLevelAndAffix (67),
TravelerDuelClearSpecificBossLevelWithDifficultyAndTotal (68),
TravelerDuelClearSpecificBossTotal (69),
TravelerDuelChallengeRankUploadTotal (70),
WorldClassSpecific (71),
RegionBossClearSpecificTypeWithTotal (72),
CharactersWithSpecificDatingCount (73),
CharactersDatingTotal (74),
VampireSurvivorPassedSpecificLevel (77),
CharacterParticipateTowerNumber (78),
CharacterAllSkillReachSpecificLevel (79),
TravelerDuelPlayTotal (80),
VampireClearTotal (81),
VampireWithSpecificClearTotal (82),
AgentFinishTotal (83),
AgentWithSpecificFinishTotal (84),
ActivityMiningEnterLayer (86),
ActivityMiningDestroyGrid (87),
BossRushTotalStars (88),
InfinityTowerClearSpecificDifficultyAndTotal (89),
SkillInstanceClearTotal (90),
VampireSurvivorSpecificPassedLevel (91),
WeekBoosClearSpecificDifficultyAndTotal (92),
NpcAffinityWithSpecificLevel (93),
CharacterPassedWithSpecificTowerAndCount (94),
ActivityCookieLevelAccPackage (96),
ActivityCookieLevelScore (97),
ActivityCookieTypeAccPackage (98),
ActivityCookieTypeAccPackCookie (99),
ActivityCookieTypeAccRhythm (100),
ActivityCookieTypeChallenge (101),
CharGemInstanceClearTotal (104),
DailyShopReceiveShopTotal (105),
AgentApplyTotal (106),
ActivityScore (107),
ActivityTypeAvgReadWithSpecificIdAndLevelId (108),
ActivityTypeLevelPassedWithSpecificIdAndLevelId (109),
ActivityTypeLevel3StarPassedWithSpecificIdAndLevelId (110),
ActivityTypeLevelStarWithSpecificIdAndLevelTypeTotal (111),
ActivityTypeLevelPassedWithSpecificIdAndLevelIdAndSpecificPositionAndCharElem (112),
ActivityTypeLevelPassedSpecificIdTotal (113),
ClientReport (200),
TowerBuildSpecificScoreWithTotal (504),
TowerClearSpecificLevelWithDifficultyAndTotal (507),
TowerEnterTotal (510),
TowerSpecificDifficultyShopBuyTimes (514),
TowerGrowthSpecificNote (515),
TowerClearSpecificLevelWithDifficultyAndTotalHistory (516),
TowerBookWithSpecificEvent (517),
TowerBookWithSpecificFateCard (518),
TowerBookWithSpecificPotential (520),
TowerBuildSpecificDifficultyAndScoreWithTotal (521),
TowerSpecificDifficultyStrengthenMachineTotal (522),
TowerSpecificDifficultyKillBossTotal (524),
TowerBookSpecificCharWithPotentialTotal (525),
TowerBuildSpecificCharSpecificScoreWithTotal (526),
TowerGrowthWithSpecificNote (527),
TowerSpecificFateCardReRollTotal (528),
TowerSpecificPotentialReRollTotal (529),
TowerSpecificShopReRollTotal (530),
TowerSpecificNoteActivateTotal (531),
TowerSpecificNoteLevelTotal (532),
TowerSpecificPotentialBonusTotal (533),
TowerSpecificPotentialLuckyTotal (534),
TowerSpecificShopBuyDiscountTotal (535),
TowerSpecificSecondarySkillActivateTotal (536),
TowerSpecificGetExtraNoteLvTotal (537),
TowerEnterFloor (538),
TowerSweepTimes (539),
TowerSweepTotal (540);
@Getter
private final int value;
private final static Int2ObjectMap<QuestCondition> map = new Int2ObjectOpenHashMap<>();
static {
for (QuestCondition type : QuestCondition.values()) {
map.put(type.getValue(), type);
}
}
private QuestCondition(int value) {
this.value = value;
}
public static QuestCondition getByValue(int value) {
return map.get(value);
}
}

View File

@@ -11,19 +11,20 @@ public class QuestHelper {
@Getter
private static final Int2ObjectMap<QuestParams> battlePassQuestParams = new Int2ObjectOpenHashMap<>();
// Put params here
static {
battlePassQuestParams.put(1001, new QuestParams(QuestCondType.LoginTotal, 1));
battlePassQuestParams.put(1002, new QuestParams(QuestCondType.EnergyDeplete, 160));
battlePassQuestParams.put(1003, new QuestParams(QuestCondType.BattleTotal, 6));
battlePassQuestParams.put(1004, new QuestParams(QuestCondType.QuestWithSpecificType, 5, QuestType.Daily));
battlePassQuestParams.put(2001, new QuestParams(QuestCondType.TowerEnterFloor, 1));
battlePassQuestParams.put(2002, new QuestParams(QuestCondType.WeekBoosClearSpecificDifficultyAndTotal, 3));
battlePassQuestParams.put(2003, new QuestParams(QuestCondType.BattleTotal, 20));
battlePassQuestParams.put(2004, new QuestParams(QuestCondType.LoginTotal, 5));
battlePassQuestParams.put(2005, new QuestParams(QuestCondType.AgentFinishTotal, 3));
battlePassQuestParams.put(2006, new QuestParams(QuestCondType.ItemsDeplete, 100000, 1));
battlePassQuestParams.put(2007, new QuestParams(QuestCondType.GiftGiveTotal, 5));
battlePassQuestParams.put(2008, new QuestParams(QuestCondType.EnergyDeplete, 1200));
battlePassQuestParams.put(1001, new QuestParams(QuestCondition.LoginTotal, 1));
battlePassQuestParams.put(1002, new QuestParams(QuestCondition.EnergyDeplete, 160));
battlePassQuestParams.put(1003, new QuestParams(QuestCondition.BattleTotal, 6));
battlePassQuestParams.put(1004, new QuestParams(QuestCondition.QuestWithSpecificType, 5, QuestType.Daily));
battlePassQuestParams.put(2001, new QuestParams(QuestCondition.TowerEnterFloor, 1));
battlePassQuestParams.put(2002, new QuestParams(QuestCondition.WeekBoosClearSpecificDifficultyAndTotal, 3));
battlePassQuestParams.put(2003, new QuestParams(QuestCondition.BattleTotal, 20));
battlePassQuestParams.put(2004, new QuestParams(QuestCondition.LoginTotal, 5));
battlePassQuestParams.put(2005, new QuestParams(QuestCondition.AgentFinishTotal, 3));
battlePassQuestParams.put(2006, new QuestParams(QuestCondition.ItemsDeplete, 100000, 1));
battlePassQuestParams.put(2007, new QuestParams(QuestCondition.GiftGiveTotal, 5));
battlePassQuestParams.put(2008, new QuestParams(QuestCondition.EnergyDeplete, 1200));
}
@Getter
@@ -40,7 +41,7 @@ public class QuestHelper {
this(cond, new int[] {param});
}
public QuestParams(QuestCondType cond, int... params) {
public QuestParams(QuestCondition cond, int... params) {
this(cond.getValue(), params);
}
}

View File

@@ -114,10 +114,10 @@ public class QuestManager extends PlayerManager implements GameDatabaseObject {
this.save();
}
public synchronized void trigger(QuestCondType condition, int progress, int param) {
public synchronized void trigger(int condition, int progress, int param1, int param2) {
for (var quest : getQuests().values()) {
// Try to trigger quest
boolean result = quest.trigger(condition, progress, param);
boolean result = quest.trigger(condition, progress, param1, param2);
// Skip if quest progress wasn't changed
if (!result) {
@@ -132,7 +132,6 @@ public class QuestManager extends PlayerManager implements GameDatabaseObject {
}
}
/**
* Update this quest on the player client
*/
@@ -194,7 +193,7 @@ public class QuestManager extends PlayerManager implements GameDatabaseObject {
}
// Trigger quest
this.getPlayer().triggerQuest(QuestCondType.QuestWithSpecificType, claimList.size(), QuestType.Daily);
this.getPlayer().trigger(QuestCondition.QuestWithSpecificType, claimList.size(), QuestType.Daily);
// Success
return change.setSuccess(true);
@@ -305,7 +304,7 @@ public class QuestManager extends PlayerManager implements GameDatabaseObject {
Nebula.getGameDatabase().update(this, this.getUid(), "hasDailyReward", this.hasDailyReward);
// Trigger quest
this.getPlayer().triggerQuest(QuestCondType.DailyShopReceiveShopTotal, 1);
this.getPlayer().trigger(QuestCondition.DailyShopReceiveShopTotal, 1);
// Success
return change.setSuccess(true);
@@ -313,7 +312,7 @@ public class QuestManager extends PlayerManager implements GameDatabaseObject {
// Serialization
public void encodeProto(PlayerInfo proto) {
public void encodePlayerInfo(PlayerInfo proto) {
var quests = proto.getMutableQuests();
for (var quest : this.getQuests().values()) {

View File

@@ -1,10 +1,17 @@
package emu.nebula.game.scoreboss;
import java.util.Collection;
import java.util.List;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.ScoreBossControlDef;
import emu.nebula.data.resources.ScoreBossRewardDef;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.game.player.PlayerManager;
import lombok.Getter;
@Getter
@@ -20,14 +27,14 @@ public class ScoreBossManager extends PlayerManager {
}
public int getControlId() {
return 1;
return Nebula.getGameContext().getScoreBossModule().getControlId();
}
public ScoreBossControlDef getControlData() {
return GameData.getScoreBossControlDataTable().get(this.getControlId());
}
public ScoreBossRankEntry getRanking() {
public ScoreBossRankEntry getRankEntry() {
if (this.ranking == null && !this.checkedDatabase) {
this.ranking = Nebula.getGameDatabase().getObjectByUid(ScoreBossRankEntry.class, this.getPlayerUid());
this.checkedDatabase = true;
@@ -57,7 +64,7 @@ public class ScoreBossManager extends PlayerManager {
return true;
}
public boolean settle(int stars, int score) {
public boolean settle(int stars, int score, int skillScore) {
// Get level
var control = getControlData();
if (control == null || !control.getLevelGroup().contains(this.getLevelId())) {
@@ -71,7 +78,7 @@ public class ScoreBossManager extends PlayerManager {
}
// Get ranking from database
this.getRanking();
this.getRankEntry();
// Create ranking if its not in the database
if (this.ranking == null) {
@@ -79,7 +86,7 @@ public class ScoreBossManager extends PlayerManager {
}
// Settle
this.ranking.settle(this.getPlayer(), build, this.getLevelId(), stars, score);
this.ranking.settle(this.getPlayer(), build, getControlId(), getLevelId(), stars, score, skillScore);
// Save ranking
this.ranking.save();
@@ -91,4 +98,54 @@ public class ScoreBossManager extends PlayerManager {
// Success
return true;
}
public PlayerChangeInfo claimRewards(int star) {
// Get rank entry
this.getRankEntry();
if (this.ranking == null) {
return new PlayerChangeInfo();
}
// Init variables
Collection<ScoreBossRewardDef> claims = null;
// Add to claim list
if (star > 0) {
var data = GameData.getScoreBossRewardDataTable().get(star);
if (data != null) {
claims = List.of(data);
}
} else {
claims = GameData.getScoreBossRewardDataTable().values();
}
// Init rewards
var rewards = new ItemParamMap();
int starCount = ranking.getStars();
boolean shouldSave = false;
// Try and claim
for (var data : claims) {
// Check if we have earned enough stars
if (starCount >= data.getStarNeed() && !ranking.getClaimedRewards().contains(data.getId())) {
// Add rewards
rewards.add(data.getRewardItemId1(), data.getRewardNum1());
// Set in claimed rewards
this.ranking.getClaimedRewards().add(data.getId());
// Set save flag so we update ranking to the database
shouldSave = true;
}
}
// Save to database
if (shouldSave) {
this.ranking.save();
}
// Add rewards
return this.getPlayer().getInventory().addItems(rewards);
}
}

View File

@@ -21,6 +21,11 @@ public class ScoreBossModule extends GameContextModule {
this.ranking = new ArrayList<>();
}
// TODO calculate from bin data
public int getControlId() {
return 2;
}
private long getRefreshTime() {
return Nebula.getConfig().getServerOptions().leaderboardRefreshTime * 1000;
}
@@ -39,7 +44,7 @@ public class ScoreBossModule extends GameContextModule {
this.ranking.clear();
// Get from database
var list = Nebula.getGameDatabase().getSortedObjects(ScoreBossRankEntry.class, "score", 50);
var list = Nebula.getGameDatabase().getSortedObjects(ScoreBossRankEntry.class, "controlId", this.getControlId(), "score", 50);
for (int i = 0; i < list.size(); i++) {
// Get rank entry and set proto

View File

@@ -16,7 +16,8 @@ import emu.nebula.proto.Public.HonorInfo;
import emu.nebula.proto.ScoreBossRank.ScoreBossRankChar;
import emu.nebula.proto.ScoreBossRank.ScoreBossRankData;
import emu.nebula.proto.ScoreBossRank.ScoreBossRankTeam;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import lombok.Getter;
import lombok.Setter;
@@ -33,6 +34,9 @@ public class ScoreBossRankEntry implements GameDatabaseObject {
private int titleSuffix;
private int[] honor;
private int score;
@SuppressWarnings("unused")
private int stars;
private IntSet claimedRewards;
private int controlId;
private Map<Integer, ScoreBossTeamEntry> teams;
@@ -51,6 +55,25 @@ public class ScoreBossRankEntry implements GameDatabaseObject {
this.teams = new HashMap<>();
}
// TODO replace later
public int getStars() {
int stars = 0;
for (var team : this.getTeams().values()) {
stars += team.getStars();
}
return stars;
}
public IntSet getClaimedRewards() {
if (this.claimedRewards == null) {
this.claimedRewards = new IntOpenHashSet();
}
return this.claimedRewards;
}
public void update(Player player) {
this.name = player.getName();
this.level = player.getLevel();
@@ -60,22 +83,39 @@ public class ScoreBossRankEntry implements GameDatabaseObject {
this.honor = player.getHonor();
}
public void settle(Player player, StarTowerBuild build, int level, int stars, int score) {
public void settle(Player player, StarTowerBuild build, int controlId, int level, int stars, int score, int skillScore) {
// Update player data
this.update(player);
// Reset score entry if control id doesn't match
if (this.controlId != controlId) {
this.controlId = controlId;
this.reset();
}
// Set team entry
var team = new ScoreBossTeamEntry(player, build, stars, score);
var team = new ScoreBossTeamEntry(player, build, stars, score, skillScore);
// Put in team map
this.getTeams().put(level, team);
// Calculate score
// Calculate score/stars
this.score = 0;
this.stars = 0;
for (var t : this.getTeams().values()) {
this.score += t.getLevelScore();
this.stars += t.getStars();
}
}
private void reset() {
this.score = 0;
this.stars = 0;
this.getClaimedRewards().clear();
this.getTeams().clear();
}
// Proto
public ScoreBossRankData toProto() {
@@ -109,6 +149,7 @@ public class ScoreBossRankEntry implements GameDatabaseObject {
private int buildScore;
private int stars;
private int levelScore;
private int skillScore;
private List<ScoreBossCharEntry> characters;
@Deprecated // Morphia only
@@ -116,11 +157,12 @@ public class ScoreBossRankEntry implements GameDatabaseObject {
}
public ScoreBossTeamEntry(Player player, StarTowerBuild build, int stars, int score) {
public ScoreBossTeamEntry(Player player, StarTowerBuild build, int stars, int score, int skillScore) {
this.buildId = build.getUid();
this.buildScore = build.getScore();
this.stars = stars;
this.levelScore = score;
this.skillScore = skillScore;
this.characters = new ArrayList<>();
for (var charId : build.getCharIds()) {

View File

@@ -0,0 +1,32 @@
package emu.nebula.game.story;
import dev.morphia.annotations.Entity;
import emu.nebula.proto.Public.StoryChoice;
import lombok.Getter;
@Getter
@Entity(useDiscriminator = false)
public class StoryChoiceInfo {
private int group;
private int value;
@Deprecated
public StoryChoiceInfo() {
// Morphia only
}
public StoryChoiceInfo(int group, int value) {
this.group = group;
this.value = value;
}
// Proto
public StoryChoice toProto() {
var proto = StoryChoice.newInstance()
.setGroup(this.getGroup())
.setValue(this.getValue());
return proto;
}
}

View File

@@ -1,20 +1,28 @@
package emu.nebula.game.story;
import java.util.HashMap;
import java.util.Map;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import dev.morphia.annotations.PostLoad;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.game.player.PlayerManager;
import emu.nebula.proto.PlayerData.PlayerInfo;
import emu.nebula.proto.Public.Story;
import emu.nebula.proto.StorySett.StorySettle;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntList;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import lombok.Getter;
import us.hebi.quickbuf.RepeatedInt;
import us.hebi.quickbuf.RepeatedMessage;
@Getter
@Entity(value = "story", useDiscriminator = false)
@@ -24,6 +32,13 @@ public class StoryManager extends PlayerManager implements GameDatabaseObject {
private IntSet completedStories;
private Int2IntMap completedSets;
private IntSet evidences;
// Note: Story options are seperate from regular story ids to save database space, since most stories do not have options
private Map<Integer, StoryOptionLog> options;
// Current story
private transient int storyId;
@Deprecated // Morphia only
public StoryManager() {
@@ -35,19 +50,44 @@ public class StoryManager extends PlayerManager implements GameDatabaseObject {
this.uid = player.getUid();
this.completedStories = new IntOpenHashSet();
this.completedSets = new Int2IntOpenHashMap();
this.evidences = new IntOpenHashSet();
this.options = new HashMap<>();
this.save();
}
public PlayerChangeInfo settle(IntList list) {
// Player change info
var changes = new PlayerChangeInfo();
public void apply(int idx) {
this.storyId = idx;
}
public boolean hasNew() {
if (this.getCompletedStories().size() < GameData.getStoryDataTable().size()) {
return true;
}
for (int id : list) {
if (this.getCompletedSets().size() < GameData.getStorySetSectionDataTable().size()) {
return true;
}
return false;
}
public PlayerChangeInfo settle(RepeatedMessage<StorySettle> list, RepeatedInt evidences) {
// Player change info
var change = new PlayerChangeInfo();
// Handle regular story
for (var settle : list) {
// Get id
int id = settle.getIdx();
// Get story data
var data = GameData.getStoryDataTable().get(id);
if (data == null) continue;
// Settle options (Must be before the completion check as we need to do the same story multiple times to get all the endings)
this.settleOptions(settle);
// Check if we already completed the story
if (this.getCompletedStories().contains(id)) {
continue;
@@ -57,13 +97,64 @@ public class StoryManager extends PlayerManager implements GameDatabaseObject {
this.getCompletedStories().add(id);
// Add rewards
this.getPlayer().getInventory().addItems(data.getRewards(), changes);
this.getPlayer().getInventory().addItems(data.getRewards(), change);
// Save to db
Nebula.getGameDatabase().addToSet(this, this.getPlayerUid(), "completedStories", id);
}
return changes;
// Handle evidences
for (int id : evidences) {
// Verify that evidence id exists
var data = GameData.getStoryEvidenceDataTable().get(id);
if (data == null) continue;
// Sanity check
if (this.getEvidences().contains(id)) {
continue;
}
// Save to db
Nebula.getGameDatabase().addToSet(this, this.getPlayerUid(), "evidences", id);
}
// Clear current story
this.storyId = 0;
// Complete
return change;
}
private void settleOptions(StorySettle settle) {
// Init variables
boolean changed = false;
StoryOptionLog log = null;
// Update
if (settle.hasMajor()) {
if (log == null) {
log = getOptions().computeIfAbsent(settle.getIdx(), idx -> new StoryOptionLog());
}
if (log.settleMajor(settle.getMajor())) {
changed = true;
}
}
if (settle.hasPersonality()) {
if (log == null) {
log = getOptions().computeIfAbsent(settle.getIdx(), idx -> new StoryOptionLog());
}
if (log.settlePersonality(settle.getPersonality())) {
changed = true;
}
}
// Save to database if we changed anything
if (changed) {
Nebula.getGameDatabase().update(this, this.getPlayerUid(), "options." + settle.getIdx(), log);
}
}
public PlayerChangeInfo settleSet(int chapterId, int sectionId) {
@@ -90,6 +181,50 @@ public class StoryManager extends PlayerManager implements GameDatabaseObject {
// Save to db
Nebula.getGameDatabase().update(this, this.getPlayerUid(), "completedSets." + chapterId, sectionIndex);
// Complete
return changes;
}
// Proto
public void encodePlayerInfo(PlayerInfo proto) {
var story = proto.getMutableStory();
for (int storyId : this.getCompletedStories()) {
var storyProto = Story.newInstance()
.setIdx(storyId);
var storyOptions = this.getOptions().get(storyId);
if (storyOptions != null) {
storyOptions.encodeStoryProto(storyProto);
}
story.addStories(storyProto);
}
for (int id : this.getEvidences()) {
story.addEvidences(id);
}
}
// Database fixes
@PostLoad
public void onLoad() {
boolean save = false;
if (this.evidences == null) {
this.evidences = new IntOpenHashSet();
save = true;
}
if (this.options == null) {
this.options = new HashMap<>();
save = true;
}
if (save) {
this.save();
}
}
}

View File

@@ -0,0 +1,143 @@
package emu.nebula.game.story;
import java.util.ArrayList;
import java.util.List;
import dev.morphia.annotations.Entity;
import emu.nebula.proto.Public.Story;
import emu.nebula.proto.StorySett.StoryOptions;
import lombok.Getter;
import us.hebi.quickbuf.RepeatedMessage;
@Getter
@Entity(useDiscriminator = false)
public class StoryOptionLog {
private List<StoryChoiceInfo> major;
private List<StoryChoiceInfo> personality;
public StoryOptionLog() {
}
public int getMajorOptionSize() {
if (this.major == null) {
return 0;
}
return this.major.size();
}
public boolean hasMajorOption(int group, int choice) {
if (this.major == null) {
return false;
}
return this.major.stream()
.filter(c -> c.getGroup() == group && c.getValue() == choice)
.findFirst()
.isPresent();
}
public boolean addMajorOption(int group, int choice) {
if (this.major == null) {
this.major = new ArrayList<>();
}
return this.major.add(new StoryChoiceInfo(group, choice));
}
public boolean settleMajor(RepeatedMessage<StoryOptions> options) {
boolean success = false;
for (var option : options) {
// Sanity check
if (this.getMajorOptionSize() >= 5) {
break;
}
// Skip if we already have this choice
if (this.hasMajorOption(option.getGroup(), option.getChoice())) {
continue;
}
// Add
this.addMajorOption(option.getGroup(), option.getChoice());
// Set success flag
success = true;
}
return success;
}
public int getPersonalityOptionSize() {
if (this.personality == null) {
return 0;
}
return this.personality.size();
}
public boolean hasPersonalityOption(int group, int choice) {
if (this.personality == null) {
return false;
}
return this.personality.stream()
.filter(c -> c.getGroup() == group && c.getValue() == choice)
.findFirst()
.isPresent();
}
public boolean addPersonalityOption(int group, int choice) {
if (this.personality == null) {
this.personality = new ArrayList<>();
}
return this.personality.add(new StoryChoiceInfo(group, choice));
}
public boolean settlePersonality(RepeatedMessage<StoryOptions> options) {
boolean success = false;
for (var option : options) {
// Sanity check
if (this.getPersonalityOptionSize() >= 5) {
break;
}
// Skip if we already have this choice
if (this.hasPersonalityOption(option.getGroup(), option.getChoice())) {
continue;
}
// Add
this.addPersonalityOption(option.getGroup(), option.getChoice());
// Set success flag
success = true;
}
return success;
}
// Proto
public void encodeStoryProto(Story proto) {
if (this.major != null) {
for (var choice : this.major) {
proto.addMajor(choice.toProto());
}
}
if (this.personality != null) {
for (var choice : this.personality) {
proto.addMajor(choice.toProto());
}
}
}
}

View File

@@ -1,12 +1,18 @@
package emu.nebula.game.tower;
import java.util.List;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import dev.morphia.annotations.Indexed;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.SecondarySkillDef;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.character.GameCharacter;
import emu.nebula.game.character.GameDisc;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.Player;
import emu.nebula.proto.Public.ItemTpl;
import emu.nebula.proto.PublicStarTower.BuildPotential;
import emu.nebula.proto.PublicStarTower.StarTowerBuildBrief;
@@ -15,6 +21,7 @@ import emu.nebula.proto.PublicStarTower.StarTowerBuildInfo;
import emu.nebula.proto.PublicStarTower.TowerBuildChar;
import emu.nebula.util.Snowflake;
import it.unimi.dsi.fastutil.ints.IntSet;
import lombok.Getter;
@Getter
@@ -37,55 +44,62 @@ public class StarTowerBuild implements GameDatabaseObject {
private ItemParamMap potentials;
private ItemParamMap subNoteSkills;
private IntSet secondarySkills;
@Deprecated
public StarTowerBuild() {
}
public StarTowerBuild(StarTowerGame game) {
public StarTowerBuild(Player player) {
this.uid = Snowflake.newUid();
this.playerUid = game.getPlayer().getUid();
this.playerUid = player.getUid();
this.name = "";
this.charPots = new ItemParamMap();
this.potentials = new ItemParamMap();
this.subNoteSkills = new ItemParamMap();
}
public StarTowerBuild(StarTowerGame game) {
// Initialize basic variables
this(game.getPlayer());
// Characters
this.charIds = game.getChars().stream()
.filter(d -> d.getId() > 0)
.mapToInt(d -> d.getId())
.toArray();
// Discs
this.discIds = game.getDiscs().stream()
.filter(d -> d.getId() > 0)
.mapToInt(d -> d.getId())
.toArray();
// Set char/disc ids
this.charIds = game.getCharIds();
this.discIds = game.getDiscIds();
// Add potentials
for (var entry : game.getPotentials()) {
//
// Get potential data
int id = entry.getIntKey();
int level = entry.getIntValue();
// Add to potential map
this.getPotentials().put(id, level);
// Add to character
var potentialData = GameData.getPotentialDataTable().get(id);
if (potentialData != null) {
int charId = potentialData.getCharId();
this.getCharPots().put(charId, this.getCharPots().get(charId) + 1);
}
}
// Add sub note skills
for (var entry : game.getItems()) {
this.getSubNoteSkills().put(entry.getIntKey(), entry.getIntValue());
this.getSubNoteSkills().add(entry.getIntKey(), entry.getIntValue());
}
// Set secondary skills
this.secondarySkills = game.getSecondarySkills();
// Caclulate record score and cache it
this.score = this.calculateScore();
this.calculateScore();
}
public void setChars(List<GameCharacter> characters) {
this.charIds = characters.stream()
.mapToInt(c -> c.getCharId())
.toArray();
}
public void setDiscs(List<GameDisc> discs) {
this.discIds = discs.stream()
.mapToInt(d -> d.getDiscId())
.toArray();
}
public void setName(String newName) {
@@ -109,26 +123,43 @@ public class StarTowerBuild implements GameDatabaseObject {
// Score
private int calculateScore() {
// Init score
int score = 0;
public int calculateScore() {
// Clear
this.score = 0;
this.getCharPots().clear();
// Potentials
for (var potential : this.getPotentials().int2IntEntrySet()) {
// Add score from potentials
for (var potential : this.getPotentials()) {
var data = GameData.getPotentialDataTable().get(potential.getIntKey());
if (data == null) continue;
int index = potential.getIntValue() - 1;
score += data.getBuildScore()[index];
// Add score
this.score += data.getBuildScore(potential.getIntValue());
// Add for character potential count
this.getCharPots().add(data.getCharId(), potential.getIntValue());
}
// Sub note skills
// Add score from sub note skills
for (var item : this.getSubNoteSkills()) {
score += item.getIntValue() * 15;
this.score += item.getIntValue() * 15;
}
// Check secondary skills
if (this.getSecondarySkills() == null) {
this.secondarySkills = SecondarySkillDef.calculateSecondarySkills(this.getDiscIds(), this.getSubNoteSkills());
}
// Add score from secondary skills
for (int id : this.getSecondarySkills()) {
var data = GameData.getSecondarySkillDataTable().get(id);
if (data == null) continue;
this.score += data.getScore();
}
// Complete
return score;
return this.score;
}
// Proto
@@ -183,6 +214,11 @@ public class StarTowerBuild implements GameDatabaseObject {
proto.addSubNoteSkills(skill);
}
// Secondary skills
for (int id : this.getSecondarySkills()) {
proto.addActiveSecondaryIds(id);
}
return proto;
}

View File

@@ -1,128 +0,0 @@
package emu.nebula.game.tower;
import java.util.HashMap;
import java.util.Map;
import emu.nebula.proto.PublicStarTower.HawkerGoods;
import emu.nebula.proto.PublicStarTower.StarTowerRoomCase;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class StarTowerCase {
private int id;
@Setter(AccessLevel.NONE)
private CaseType type;
// Extra data
private int teamLevel;
private int subNoteSkillNum;
private int floorId;
private int roomType;
private int eventId;
private int npcId;
// Selector
private IntList ids;
// Hawker
private Map<Integer, StarTowerShopGoods> goodsList;
public StarTowerCase(CaseType type) {
this.type = type;
}
public void addId(int id) {
if (this.ids == null) {
this.ids = new IntArrayList();
}
this.ids.add(id);
}
public int selectId(int index) {
if (this.getIds() == null) {
return 0;
}
if (index < 0 || index >= this.getIds().size()) {
return 0;
}
return this.getIds().getInt(index);
}
public void addGoods(StarTowerShopGoods goods) {
if (this.goodsList == null) {
this.goodsList = new HashMap<>();
}
this.getGoodsList().put(getGoodsList().size() + 1, goods);
}
// Proto
public StarTowerRoomCase toProto() {
var proto = StarTowerRoomCase.newInstance()
.setId(this.getId());
switch (this.type) {
case Battle -> {
proto.getMutableBattleCase()
.setSubNoteSkillNum(this.getSubNoteSkillNum());
}
case OpenDoor -> {
proto.getMutableDoorCase()
.setFloor(this.getFloorId())
.setType(this.getRoomType());
}
case SyncHP, RecoveryHP -> {
proto.getMutableSyncHPCase();
}
case SelectSpecialPotential -> {
proto.getMutableSelectSpecialPotentialCase()
.setTeamLevel(this.getTeamLevel())
.addAllIds(this.getIds().toIntArray());
}
case PotentialSelect -> {
proto.getMutableSelectPotentialCase();
}
case NpcEvent -> {
proto.getMutableSelectOptionsEventCase()
.setEvtId(this.getEventId())
.setNPCId(this.getNpcId())
.addAllOptions(this.getIds().toIntArray());
}
case Hawker -> {
var hawker = proto.getMutableHawkerCase();
for (var entry : getGoodsList().entrySet()) {
var sid = entry.getKey();
var goods = entry.getValue();
var info = HawkerGoods.newInstance()
.setIdx(goods.getGoodsId())
.setSid(sid)
.setType(goods.getType())
.setGoodsId(102) // ?
.setPrice(goods.getPrice())
.setTag(1);
hawker.addList(info);
}
}
default -> {
}
}
return proto;
}
}

Some files were not shown because too many files have changed in this diff Show More