Initial Commit

This commit is contained in:
Melledy
2025-10-27 02:02:26 -07:00
commit f58951fe2a
378 changed files with 315914 additions and 0 deletions
+113
View File
@@ -0,0 +1,113 @@
package emu.nebula;
import java.util.Set;
import lombok.Getter;
@Getter
public class Config {
public DatabaseInfo accountDatabase = new DatabaseInfo();
public DatabaseInfo gameDatabase = new DatabaseInfo();
public InternalMongoInfo internalMongoServer = new InternalMongoInfo();
public boolean useSameDatabase = true;
public KeystoreInfo keystore = new KeystoreInfo();
public HttpServerConfig httpServer = new HttpServerConfig(80);
public GameServerConfig gameServer = new GameServerConfig(80);
public ServerOptions serverOptions = new ServerOptions();
public ServerRates serverRates = new ServerRates();
public LogOptions logOptions = new LogOptions();
public String resourceDir = "./resources";
public String dataDir = "./data";
@Getter
public static class DatabaseInfo {
public String uri = "mongodb://localhost:27017";
public String collection = "nebula";
public boolean useInternal = true;
}
@Getter
public static class InternalMongoInfo {
public String address = "localhost";
public int port = 27017;
public String filePath = "database.mv";
}
@Getter
public static class KeystoreInfo {
public String path = "./keystore.p12";
public String password = "";
}
@Getter
private static class ServerConfig {
public boolean useSSL = false;
public String bindAddress = "0.0.0.0";
public int bindPort;
public String publicAddress = "127.0.0.1"; // Will return bindAddress if publicAddress is null
public Integer publicPort; // Will return bindPort if publicPort is null
public ServerConfig(int port) {
this.bindPort = port;
}
public String getPublicAddress() {
if (this.publicAddress != null && !this.publicAddress.isEmpty()) {
return this.publicAddress;
}
return this.bindAddress;
}
public int getPublicPort() {
if (this.publicPort != null && this.publicPort != 0) {
return this.publicPort;
}
return this.bindPort;
}
public String getDisplayAddress() {
return (useSSL ? "https" : "http") + "://" + getPublicAddress() + ":" + getPublicPort();
}
}
@Getter
public static class HttpServerConfig extends ServerConfig {
public HttpServerConfig(int port) {
super(port);
}
}
@Getter
public static class GameServerConfig extends ServerConfig {
public GameServerConfig(int port) {
super(port);
}
}
@Getter
public static class ServerOptions {
public Set<String> defaultPermissions = Set.of("*");
public boolean autoCreateAccount = true;
public boolean skipIntro = false;
}
@Getter
public static class ServerRates {
public double exp = 1.0;
}
@Getter
public static class LogOptions {
public boolean commands = true;
public boolean packets = false;
}
}
@@ -0,0 +1,15 @@
package emu.nebula;
public class GameConstants {
public static final int DATA_VERSION = 22;
public static final String VERSION = "1.0.0." + DATA_VERSION;
public static final String PROTO_BASE_TYPE_URL = "type.googleapis.com/proto.";
public static final int INTRO_GUIDE_ID = 1;
public static final int GOLD_ITEM_ID = 1;
public static final int MAX_FORMATIONS = 5;
}
+247
View File
@@ -0,0 +1,247 @@
package emu.nebula;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import emu.nebula.command.CommandManager;
import emu.nebula.data.ResourceLoader;
import emu.nebula.database.DatabaseManager;
import emu.nebula.game.GameContext;
import emu.nebula.server.HttpServer;
import emu.nebula.util.Handbook;
import emu.nebula.util.JsonUtils;
import lombok.Getter;
public class Nebula {
private static final Logger log = LoggerFactory.getLogger(Nebula.class);
// Config
private static final File configFile = new File("./config.json");
@Getter private static Config config;
// Database
@Getter private static DatabaseManager accountDatabase;
@Getter private static DatabaseManager gameDatabase;
// Server
@Getter private static HttpServer httpServer;
@Getter private static HttpServer gameServer; // TODO
@Getter private static ServerType serverType = ServerType.BOTH;
@Getter private static GameContext gameContext;
@Getter private static CommandManager commandManager;
public static void main(String[] args) {
// Start Server
Nebula.getLogger().info("Starting Nebula " + getJarVersion());
Nebula.getLogger().info("Git hash: " + getGitHash());
Nebula.getLogger().info("Game version: " + GameConstants.VERSION);
boolean generateHandbook = true;
// Load config + commands
Nebula.loadConfig();
// Parse arguments
for (String arg : args) {
switch (arg) {
case "-login":
serverType = ServerType.LOGIN;
break;
case "-game":
serverType = ServerType.GAME;
break;
case "-nohandbook":
case "-skiphandbook":
generateHandbook = false;
break;
case "-database":
// Database only
DatabaseManager.startInternalMongoServer(Nebula.getConfig().getInternalMongoServer());
Nebula.getLogger().info("Running local Mongo server at " + DatabaseManager.getServer().getConnectionString());
return;
}
}
// Skip these if we are only running the http server in dispatch mode
if (serverType.runGame()) {
// Load resources
ResourceLoader.loadAll();
// Generate handbook
if (generateHandbook) {
Handbook.generate();
}
}
try {
// Start Database(s)
Nebula.initDatabases();
} catch (Exception exception) {
Nebula.getLogger().error("Unable to start the database(s).", exception);
}
// Start game context
Nebula.gameContext = new GameContext();
Nebula.commandManager = new CommandManager();
// Start servers
try {
// Always run http server as it is needed by for dispatch and gateserver
httpServer = new HttpServer(serverType);
httpServer.start();
} catch (Exception exception) {
Nebula.getLogger().error("Unable to start the HTTP server.", exception);
}
// Start console
Nebula.startConsole();
}
public static Logger getLogger() {
return log;
}
// Database
private static void initDatabases() {
if (Nebula.getConfig().useSameDatabase) {
// Setup account and game database
accountDatabase = new DatabaseManager(Nebula.getConfig().getAccountDatabase(), serverType);
// Optimization: Dont run a 2nd database manager if we are not running a gameserver
if (serverType.runGame()) {
gameDatabase = accountDatabase;
}
} else {
// Run separate databases
accountDatabase = new DatabaseManager(Nebula.getConfig().getAccountDatabase(), ServerType.LOGIN);
// Optimization: Dont run a 2nd database manager if we are not running a gameserver
if (serverType.runGame()) {
gameDatabase = new DatabaseManager(Nebula.getConfig().getGameDatabase(), ServerType.GAME);
}
}
}
// Config
public static void loadConfig() {
// Load from file
try (FileReader file = new FileReader(configFile)) {
Nebula.config = JsonUtils.loadToClass(file, Config.class);
} catch (Exception e) {
// Ignored
}
// Sanity check
if (Nebula.getConfig() == null) {
Nebula.config = new Config();
}
// Save config
Nebula.saveConfig();
}
public static void saveConfig() {
try (FileWriter file = new FileWriter(configFile)) {
Gson gson = new GsonBuilder()
.setDateFormat("dd-MM-yyyy hh:mm:ss")
.setPrettyPrinting()
.serializeNulls()
.create();
file.write(gson.toJson(config));
} catch (Exception e) {
getLogger().error("Config save error");
}
}
// Build Config
private static String getJarVersion() {
// Safely get the build config class without errors even if it hasnt been generated yet
try {
Class<?> buildConfig = Class.forName(Nebula.class.getPackageName() + ".BuildConfig");
return buildConfig.getField("VERSION").get(null).toString();
} catch (Exception e) {
// Ignored
}
return "";
}
public static String getGitHash() {
// Use a string builder in case one of the build config fields are missing
StringBuilder builder = new StringBuilder();
// Safely get the build config class without errors even if it hasnt been generated yet
try {
SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Class<?> buildConfig = Class.forName(Nebula.class.getPackageName() + ".BuildConfig");
String hash = buildConfig.getField("GIT_HASH").get(null).toString();
builder.append(hash);
String timestamp = buildConfig.getField("GIT_TIMESTAMP").get(null).toString();
long time = Long.parseLong(timestamp) * 1000;
builder.append(" (" + sf.format(new Date(time)) + ")");
} catch (Exception e) {
// Ignored
}
if (builder.isEmpty()) {
return "UNKNOWN";
} else {
return builder.toString();
}
}
// Utils
/**
* Returns the current server time in seconds
*/
public static long getCurrentTime() {
return System.currentTimeMillis() / 1000;
}
// Console
private static void startConsole() {
String input;
try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) {
while ((input = br.readLine()) != null) {
Nebula.getCommandManager().invoke(null, input);
}
} catch (Exception e) {
Nebula.getLogger().error("Console error:", e);
}
}
// Server enums
public enum ServerType {
LOGIN (0x1),
GAME (0x2),
BOTH (0x3);
private final int flags;
ServerType(int flags) {
this.flags = flags;
}
public boolean runLogin() {
return (this.flags & 0x1) == 0x1;
}
public boolean runGame() {
return (this.flags & 0x2) == 0x2;
}
}
}
@@ -0,0 +1,17 @@
package emu.nebula.command;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface Command {
public String label() default "";
public String[] aliases() default "";
public String desc() default "";
public String permission() default "";
public boolean requireTarget() default false;
}
@@ -0,0 +1,130 @@
package emu.nebula.command;
import java.util.List;
import emu.nebula.Nebula;
import emu.nebula.game.player.Player;
import emu.nebula.util.Utils;
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;
@Getter
public class CommandArgs {
private String raw;
private List<String> list;
private Player sender;
private Player target;
private int targetUid;
private int amount;
private int level = -1;
private int rank = -1;
private int promotion = -1;
private int stage = -1;
private Int2IntMap map;
private ObjectSet<String> flags;
public CommandArgs(Player sender, List<String> args) {
this.sender = sender;
this.raw = String.join(" ", args);
this.list = args;
// Parse args. Maybe regex is better.
var it = this.list.iterator();
while (it.hasNext()) {
// Lower case first
String arg = it.next().toLowerCase();
try {
if (arg.length() >= 2 && !Character.isDigit(arg.charAt(0)) && Character.isDigit(arg.charAt(arg.length() - 1))) {
if (arg.startsWith("@")) { // Target UID
this.targetUid = Utils.parseSafeInt(arg.substring(1));
it.remove();
} else if (arg.startsWith("x")) { // Amount
this.amount = Utils.parseSafeInt(arg.substring(1));
it.remove();
} else if (arg.startsWith("lv")) { // Level
this.level = Utils.parseSafeInt(arg.substring(2));
it.remove();
} else if (arg.startsWith("r")) { // Rank
this.rank = Utils.parseSafeInt(arg.substring(1));
it.remove();
} else if (arg.startsWith("e")) { // Eidolons
this.rank = Utils.parseSafeInt(arg.substring(1));
it.remove();
} else if (arg.startsWith("p")) { // Promotion
this.promotion = Utils.parseSafeInt(arg.substring(1));
it.remove();
} else if (arg.startsWith("s")) { // Stage or Superimposition
this.stage = Utils.parseSafeInt(arg.substring(1));
it.remove();
}
} else if (arg.startsWith("-")) { // Flag
if (this.flags == null) this.flags = new ObjectOpenHashSet<>();
this.flags.add(arg);
it.remove();
} else if (arg.contains(":") || arg.contains(",")) {
String[] split = arg.split("[:,]");
if (split.length >= 2) {
int key = Integer.parseInt(split[0]);
int value = Integer.parseInt(split[1]);
if (this.map == null) this.map = new Int2IntOpenHashMap();
this.map.put(key, value);
it.remove();
}
}
} catch (Exception e) {
}
}
// Get target player
if (targetUid != 0) {
if (Nebula.getGameContext() != null) {
target = Nebula.getGameContext().getPlayerModule().getCachedPlayerByUid(targetUid);
}
} else {
target = sender;
}
if (target != null) {
this.targetUid = target.getUid();
}
}
public int size() {
return this.list.size();
}
public String get(int index) {
if (index < 0 || index >= list.size()) {
return "";
}
return this.list.get(index);
}
/**
* Sends a message to the command sender
* @param message
*/
public void sendMessage(String message) {
if (sender != null) {
sender.sendMessage(message);
} else {
Nebula.getLogger().info(message);
}
}
public boolean hasFlag(String flag) {
if (this.flags == null) return false;
return this.flags.contains(flag);
}
}
@@ -0,0 +1,15 @@
package emu.nebula.command;
public interface CommandHandler {
public default Command getData() {
return this.getClass().getAnnotation(Command.class);
}
public default String getLabel() {
return getData().label();
}
public void execute(CommandArgs args);
}
@@ -0,0 +1,168 @@
package emu.nebula.command;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.reflections.Reflections;
import emu.nebula.Nebula;
import emu.nebula.game.player.Player;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import lombok.Getter;
@Getter
public class CommandManager {
private Object2ObjectMap<String, CommandHandler> labels;
private Object2ObjectMap<String, CommandHandler> commands;
public CommandManager() {
this.labels = new Object2ObjectOpenHashMap<>();
this.commands = new Object2ObjectOpenHashMap<>();
// Scan for commands
var commandClasses = new Reflections(CommandManager.class.getPackageName()).getTypesAnnotatedWith(Command.class);
for (var cls : commandClasses) {
if (!CommandHandler.class.isAssignableFrom(cls)) {
continue;
}
try {
CommandHandler handler = (CommandHandler) cls.getDeclaredConstructor().newInstance();
this.registerCommand(handler);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* Adds a command that players and server console users can use. Command handlers must have the proper command annotation attached to them.
*/
public CommandManager registerCommand(CommandHandler handler) {
Command command = handler.getClass().getAnnotation(Command.class);
if (command == null) return this;
this.getLabels().put(command.label(), handler);
this.getCommands().put(command.label(), handler);
for (String alias : command.aliases()) {
this.getCommands().put(alias, handler);
}
return this;
}
/**
* Removes a command from use.
* @param label The command name
* @return
*/
public CommandManager unregisterCommand(String label) {
CommandHandler handler = this.getLabels().get(label);
if (handler == null) return this;
Command command = handler.getClass().getAnnotation(Command.class);
if (command == null) {
return this;
}
this.getLabels().remove(command.label());
this.getCommands().remove(command.label());
for (String alias : command.aliases()) {
this.getCommands().remove(alias);
}
return this;
}
/**
* Checks if the sender has permission to use this command. Will always return true if the sender is the server console.
* @param sender The sender of the command.
* @param command
* @return true if the sender has permission to use this command
*/
public boolean checkPermission(Player sender, Command command) {
if (sender == null || command.permission().isEmpty()) {
return true;
}
return sender.getAccount().hasPermission(command.permission());
}
/**
* Checks if the sender has permission to use this command on other players. Will always return true if the sender is the server console.
* @param sender The sender of the command.
* @param command
* @return true if the sender has permission to use this command
*/
private boolean checkTargetPermission(Player sender, Command command) {
if (sender == null || command.permission().isEmpty()) {
return true;
}
return sender.getAccount().hasPermission("target." + command.permission());
}
public void invoke(Player sender, String message) {
// Parse message into arguments
List<String> args = Arrays.stream(message.split(" ")).collect(Collectors.toCollection(ArrayList::new));
// Get command label
String label = args.remove(0).toLowerCase();
// Filter out command prefixes
if (label.startsWith("/") || label.startsWith("!")) {
label = label.substring(1);
}
// Get command handler
CommandHandler handler = this.commands.get(label);
// Execute command
if (handler != null) {
// Get command annotation data
Command command = handler.getData();
// Check if sender has permission to run the command.
if (sender != null && !this.checkPermission(sender, command)) {
// We have a double null check here just in case
sender.sendMessage("You do not have permission to use this command.");
return;
}
// Build command arguments
CommandArgs cmdArgs = new CommandArgs(sender, args);
// Check targeted permission
if (sender != cmdArgs.getTarget() && !this.checkTargetPermission(sender, command)) {
cmdArgs.sendMessage("You do not have permission to use this command on another player.");
return;
}
// Make sure our command has a target
if (command.requireTarget() && cmdArgs.getTarget() == null) {
cmdArgs.sendMessage("Error: Targeted player not found or offline");
return;
}
// Log
if (sender != null && Nebula.getConfig().getLogOptions().commands) {
Nebula.getLogger().info("[UID: " + sender.getUid() + "] " + sender.getName() + " used command: " + message);
}
// Run command
handler.execute(cmdArgs);
} else {
if (sender != null) {
sender.sendMessage("Invalid Command!");
} else {
Nebula.getLogger().info("Invalid Command!");
}
}
}
}
@@ -0,0 +1,47 @@
package emu.nebula.command.commands;
import emu.nebula.command.Command;
import emu.nebula.command.CommandArgs;
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} [username] (reserved player uid). Creates or deletes an account.")
public class AccountCommand implements CommandHandler {
@Override
public void execute(CommandArgs args) {
if (args.size() < 2) {
args.sendMessage("Invalid amount of args");
return;
}
String command = args.get(0).toLowerCase();
String username = args.get(1);
switch (command) {
case "create" -> {
// Reserved player uid
int reservedUid = 0;
if (args.size() >= 3) {
reservedUid = Utils.parseSafeInt(args.get(2));
}
if (AccountHelper.createAccount(username, null, reservedUid) != null) {
args.sendMessage("Account created");
} else {
args.sendMessage("Account already exists");
}
}
case "delete" -> {
if (AccountHelper.deleteAccount(username)) {
args.sendMessage("Account deleted");
} else {
args.sendMessage("Account doesnt exist");
}
}
}
}
}
@@ -0,0 +1,45 @@
package emu.nebula.command.commands;
import emu.nebula.util.Utils;
import emu.nebula.data.GameData;
import emu.nebula.game.mail.GameMail;
import emu.nebula.command.Command;
import emu.nebula.command.CommandArgs;
import emu.nebula.command.CommandHandler;
@Command(
label = "give",
aliases = {"g", "item"},
permission = "player.give",
requireTarget = true,
desc = "/give [item id] x(amount). Gives the targeted player an item."
)
public class GiveCommand implements CommandHandler {
@Override
public void execute(CommandArgs args) {
// Setup mail
var mail = new GameMail("System", "Give Command Result", "");
// Get amount to give
int amount = Math.max(args.getAmount(), 1);
// Parse items
for (String arg : args.getList()) {
// Parse item id
int itemId = Utils.parseSafeInt(arg);
var itemData = GameData.getItemDataTable().get(itemId);
if (itemData == null) {
args.sendMessage("Item \"" + arg + "\" does not exist!");
continue;
}
// Add
mail.addAttachment(itemId, amount);
}
// Add mail
args.getTarget().getMailbox().sendMail(mail);
}
}
@@ -0,0 +1,20 @@
package emu.nebula.command.commands;
import emu.nebula.command.Command;
import emu.nebula.command.CommandArgs;
import emu.nebula.command.CommandHandler;
import emu.nebula.game.mail.GameMail;
@Command(label = "mail", aliases = {"m"}, permission = "player.mail", requireTarget = true, desc = "/mail [content]. Sends the targeted player a system mail.")
public class MailCommand implements CommandHandler {
@Override
public void execute(CommandArgs args) {
// Setup mail
var mail = new GameMail("System", "Test", "This is a test mail");
// Add mail
args.getTarget().getMailbox().sendMail(mail);
}
}
@@ -0,0 +1,17 @@
package emu.nebula.command.commands;
import emu.nebula.Nebula;
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.")
public class ReloadCommand implements CommandHandler {
@Override
public void execute(CommandArgs args) {
Nebula.loadConfig();
args.sendMessage("Reloaded the server config");
}
}
@@ -0,0 +1,15 @@
package emu.nebula.data;
public abstract class BaseDef implements Comparable<BaseDef> {
public abstract int getId();
public void onLoad() {
}
@Override
public int compareTo(BaseDef o) {
return this.getId() - o.getId();
}
}
@@ -0,0 +1,89 @@
package emu.nebula.data;
import java.util.Iterator;
import java.util.stream.Stream;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntCollection;
import it.unimi.dsi.fastutil.ints.IntSet;
import it.unimi.dsi.fastutil.objects.ObjectCollection;
import it.unimi.dsi.fastutil.objects.ObjectSet;
public class DataTable<T> implements Iterable<T> {
private Int2ObjectMap<T> map;
public DataTable() {
this.map = new Int2ObjectOpenHashMap<>();
}
@SuppressWarnings("unchecked")
public void add(Object res) {
if (res instanceof BaseDef r) {
this.map.put(r.getId(), (T) res);
}
}
public int size() {
return this.map.size();
}
// Wrapper functions
/**
* Wrapper for {@link it.unimi.dsi.fastutil.ints.Int2ObjectMap.get}
*/
public T get(int id) {
return this.map.get(id);
}
/**
* Wrapper for {@link it.unimi.dsi.fastutil.ints.Int2ObjectMap.containsKey}
*/
public boolean containsKey(int id) {
return this.map.containsKey(id);
}
/**
* Wrapper for {@link it.unimi.dsi.fastutil.ints.Int2ObjectMap.keySet}
*/
public IntSet keySet() {
return this.map.keySet();
}
/**
* Wrapper for {@link it.unimi.dsi.fastutil.ints.Int2ObjectMap.values}
*/
public ObjectCollection<T> values() {
return this.map.values();
}
/**
* Wrapper for {@link it.unimi.dsi.fastutil.ints.Int2ObjectMap.int2ObjectEntrySet}
*/
public ObjectSet<Int2ObjectMap.Entry<T>> int2ObjectEntrySet() {
return this.map.int2ObjectEntrySet();
}
// Iterable/Streamable
@Override
public Iterator<T> iterator() {
return this.values().iterator();
}
public Stream<T> stream() {
return this.values().stream();
}
// Custom
public IntCollection getIds() {
return this.map.keySet();
}
public IntCollection getAllIds() {
return this.getIds();
}
}
@@ -0,0 +1,45 @@
package emu.nebula.data;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.ArrayList;
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.resources.*;
import lombok.Getter;
@SuppressWarnings("unused")
public class GameData {
@Getter private static DataTable<CharacterDef> CharacterDataTable = new DataTable<>();
@Getter private static DataTable<CharacterAdvanceDef> CharacterAdvanceDataTable = new DataTable<>();
@Getter private static DataTable<CharacterSkillUpgradeDef> CharacterSkillUpgradeDataTable = new DataTable<>();
@Getter private static DataTable<CharacterUpgradeDef> CharacterUpgradeDataTable = new DataTable<>();
@Getter private static DataTable<CharItemExpDef> CharItemExpDataTable = new DataTable<>();
@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<>();
@Getter private static DataTable<ItemDef> ItemDataTable = new DataTable<>();
@Getter private static DataTable<MallMonthlyCardDef> MallMonthlyCardDataTable = new DataTable<>();
@Getter private static DataTable<MallPackageDef> MallPackageDataTable = new DataTable<>();
@Getter private static DataTable<MallShopDef> MallShopDataTable = new DataTable<>();
@Getter private static DataTable<MallGemDef> MallGemDataTable = new DataTable<>();
@Getter private static DataTable<WorldClassDef> WorldClassDataTable = new DataTable<>();
@Getter private static DataTable<GuideGroupDef> GuideGroupDataTable = new DataTable<>();
@Getter private static DataTable<StarTowerDef> StarTowerDataTable = new DataTable<>();
@Getter private static DataTable<PotentialDef> PotentialDataTable = new DataTable<>();
}
@@ -0,0 +1,121 @@
package emu.nebula.data;
import java.lang.reflect.Field;
import java.util.List;
import java.util.stream.Collectors;
import org.reflections.Reflections;
import emu.nebula.util.JsonUtils;
import emu.nebula.util.Utils;
import emu.nebula.Nebula;
public class ResourceLoader {
private static boolean loaded = false;
// Load all resources
public static void loadAll() {
// Make sure we don't load more than once
if (loaded) return;
// Load
loadResources();
// Done
loaded = true;
Nebula.getLogger().info("Resource loading complete");
}
public static void loadResources() {
// Get resource classes and sort
List<Class<?>> classes = new Reflections(ResourceLoader.class.getPackage().getName())
.getTypesAnnotatedWith(ResourceType.class)
.stream()
.collect(Collectors.toList());
classes.sort((a, b) -> b.getAnnotation(ResourceType.class).loadPriority().value() - a.getAnnotation(ResourceType.class).loadPriority().value());
// Load resource
for (Class<?> def : classes) {
loadFromResource(def);
}
}
public static void loadFromResource(Class<?> resourceClass) {
// Load to map
DataTable<?> table = getTableForResource(GameData.class, resourceClass);
ResourceType type = resourceClass.getAnnotation(ResourceType.class);
// Sanity check
if (type == null) {
return;
}
int count = 0;
try {
var json = JsonUtils.loadToMap(Nebula.getConfig().resourceDir + "/bin/" + type.name(), String.class, resourceClass);
for (Object o : json.values()) {
BaseDef res = (BaseDef) o;
if (res == null) {
continue;
}
res.onLoad();
count++;
if (table != null) {
table.add(o);
}
}
} catch (Exception e) {
e.printStackTrace();
Nebula.getLogger().error("Error loading resource file: " + type.name(), e);
}
Nebula.getLogger().info("Loaded " + count + " " + resourceClass.getSimpleName() + "s.");
}
// Utility
@SuppressWarnings("unchecked")
private static <T> DataTable<T> getTableForResource(Class<?> dataClass, Class<T> resourceClass) {
// Init
DataTable<T> table = null;
Field field = null;
// Parse out "Def" in the resource name
String simpleName = resourceClass.getSimpleName();
if (simpleName.endsWith("Def")) {
simpleName = simpleName.substring(0, simpleName.length() - 3) + "Data";
}
// Get table
try {
field = dataClass.getDeclaredField(simpleName + "Table");
} catch (Exception e) {
try {
field = dataClass.getDeclaredField(Utils.lowerCaseFirstChar(simpleName) + "Table");
} catch (Exception ex) {
}
}
if (field != null) {
try {
field.setAccessible(true);
table = (DataTable<T>) field.get(null);
} catch (Exception e) {
} finally {
field.setAccessible(false);
}
}
return table;
}
}
@@ -0,0 +1,33 @@
package emu.nebula.data;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface ResourceType {
/** Names of the file that this Resource loads from */
String name();
/**
* Load priority - dictates which order to load this resource, with "highest"
* being loaded first
*/
LoadPriority loadPriority() default LoadPriority.NORMAL;
Class<?> keyType() default int.class;
public enum LoadPriority {
HIGHEST(4), HIGH(3), NORMAL(2), LOW(1), LOWEST(0);
private final int value;
LoadPriority(int value) {
this.value = value;
}
public int value() {
return value;
}
}
}
@@ -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 = "CharItemExp.json")
public class CharItemExpDef extends BaseDef {
private int ItemId;
private int ExpValue;
@Override
public int getId() {
return ItemId;
}
}
@@ -0,0 +1,55 @@
package emu.nebula.data.resources;
import emu.nebula.GameConstants;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import emu.nebula.data.ResourceType.LoadPriority;
import emu.nebula.game.inventory.ItemParamMap;
import lombok.Getter;
@Getter
@ResourceType(name = "CharacterAdvance.json", loadPriority = LoadPriority.LOW)
public class CharacterAdvanceDef extends BaseDef {
private int Id;
private int Group;
private int AdvanceLvl;
private int Tid1;
private int Qty1;
private int Tid2;
private int Qty2;
private int Tid3;
private int Qty3;
private int Tid4;
private int Qty4;
private int GoldQty;
private transient ItemParamMap materials;
@Override
public int getId() {
return Id;
}
@Override
public void onLoad() {
this.materials = new ItemParamMap();
if (this.Tid1 > 0) {
this.materials.add(this.Tid1, this.Qty1);
}
if (this.Tid2 > 0) {
this.materials.add(this.Tid2, this.Qty2);
}
if (this.Tid3 > 0) {
this.materials.add(this.Tid3, this.Qty3);
}
if (this.Tid4 > 0) {
this.materials.add(this.Tid4, this.Qty4);
}
if (this.GoldQty > 0) {
this.materials.add(GameConstants.GOLD_ITEM_ID, this.GoldQty);
}
}
}
@@ -0,0 +1,32 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "Character.json")
public class CharacterDef extends BaseDef {
private int Id;
private String Name;
private boolean Available;
private int Grade;
private int DefaultSkinId;
private int AdvanceSkinId;
private int AdvanceGroup;
private int[] SkillsUpgradeGroup;
@Override
public int getId() {
return Id;
}
public int getSkillsUpgradeGroup(int index) {
if (index < 0 || index >= this.SkillsUpgradeGroup.length) {
return -1;
}
return this.SkillsUpgradeGroup[index];
}
}
@@ -0,0 +1,57 @@
package emu.nebula.data.resources;
import emu.nebula.GameConstants;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import emu.nebula.data.ResourceType.LoadPriority;
import emu.nebula.game.inventory.ItemParamMap;
import lombok.Getter;
@Getter
@ResourceType(name = "CharacterSkillUpgrade.json", loadPriority = LoadPriority.LOW)
public class CharacterSkillUpgradeDef extends BaseDef {
private int Group;
private int AdvanceNum;
private int Tid1;
private int Qty1;
private int Tid2;
private int Qty2;
private int Tid3;
private int Qty3;
private int Tid4;
private int Qty4;
private int GoldQty;
private transient int upgradeId;
private transient ItemParamMap materials;
@Override
public int getId() {
return upgradeId;
}
@Override
public void onLoad() {
this.materials = new ItemParamMap();
if (this.Tid1 > 0) {
this.materials.add(this.Tid1, this.Qty1);
}
if (this.Tid2 > 0) {
this.materials.add(this.Tid2, this.Qty2);
}
if (this.Tid3 > 0) {
this.materials.add(this.Tid3, this.Qty3);
}
if (this.Tid4 > 0) {
this.materials.add(this.Tid4, this.Qty4);
}
if (this.GoldQty > 0) {
this.materials.add(GameConstants.GOLD_ITEM_ID, this.GoldQty);
}
this.upgradeId = (this.Group * 100) + this.AdvanceNum;
}
}
@@ -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 = "CharacterUpgrade.json")
public class CharacterUpgradeDef extends BaseDef {
private int Level;
private int Exp;
@Override
public int getId() {
return Level;
}
}
@@ -0,0 +1,24 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "Disc.json")
public class DiscDef extends BaseDef {
private int Id;
private int StrengthenGroupId;
private int PromoteGroupId;
private int TransformItemId;
@Override
public int getId() {
return Id;
}
@Override
public void onLoad() {
}
}
@@ -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 = "DiscItemExp.json")
public class DiscItemExpDef extends BaseDef {
private int ItemId;
private int Exp;
@Override
public int getId() {
return ItemId;
}
}
@@ -0,0 +1,50 @@
package emu.nebula.data.resources;
import emu.nebula.GameConstants;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import emu.nebula.data.ResourceType.LoadPriority;
import emu.nebula.game.inventory.ItemParamMap;
import lombok.Getter;
@Getter
@ResourceType(name = "DiscPromote.json", loadPriority = LoadPriority.LOW)
public class DiscPromoteDef extends BaseDef {
private int Id;
private int Group;
private int AdvanceLvl;
private int ItemId1;
private int Num1;
private int ItemId2;
private int Num2;
private int ItemId3;
private int Num3;
private int ExpenseGold;
private transient ItemParamMap materials;
@Override
public int getId() {
return Id;
}
@Override
public void onLoad() {
this.materials = new ItemParamMap();
if (this.ItemId1 > 0 && this.Num1 > 0) {
this.materials.add(this.ItemId1, this.Num1);
}
if (this.ItemId2 > 0 && this.Num2 > 0) {
this.materials.add(this.ItemId2, this.Num2);
}
if (this.ItemId3 > 0 && this.Num3 > 0) {
this.materials.add(this.ItemId3, this.Num3);
}
if (this.ExpenseGold > 0) {
this.materials.add(GameConstants.GOLD_ITEM_ID, this.ExpenseGold);
}
}
}
@@ -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 = "DiscPromoteLimit.json")
public class DiscPromoteLimitDef extends BaseDef {
private int Id;
private int Rarity;
private int Phase;
private int MaxLevel;
private int WorldClassLimit;
@Override
public int getId() {
return Id;
}
}
@@ -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 = "DiscStrengthen.json")
public class DiscStrengthenDef extends BaseDef {
private int Id;
private int Exp;
@Override
public int getId() {
return Id;
}
}
@@ -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 = "GuideGroup.json")
public class GuideGroupDef extends BaseDef {
private int Id;
private boolean IsActive;
@Override
public int getId() {
return Id;
}
}
@@ -0,0 +1,32 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import emu.nebula.game.inventory.ItemSubType;
import emu.nebula.game.inventory.ItemType;
import lombok.Getter;
@Getter
@ResourceType(name = "Item.json")
public class ItemDef extends BaseDef {
private int Id;
private String Title;
private int Type;
private int Stype;
private int Rarity;
private boolean Stack;
private transient ItemType itemType;
private transient ItemSubType itemSubType;
@Override
public int getId() {
return Id;
}
@Override
public void onLoad() {
this.itemType = ItemType.getByValue(this.Type);
this.itemSubType = ItemSubType.getByValue(this.Stype);
}
}
@@ -0,0 +1,23 @@
package emu.nebula.data.resources;
import com.google.gson.annotations.SerializedName;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "MallGem.json")
public class MallGemDef extends BaseDef {
@SerializedName("Id")
private String IdString;
private int Stock;
private int ItemId;
private int CurrencyItemId;
private int ItemQty;
@Override
public int getId() {
return IdString.hashCode();
}
}
@@ -0,0 +1,23 @@
package emu.nebula.data.resources;
import com.google.gson.annotations.SerializedName;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "MallMonthlyCard.json")
public class MallMonthlyCardDef extends BaseDef {
@SerializedName("Id")
private String IdString;
private int MonthlyCardId;
private int Price;
private int BaseItemId;
private int BaseItemQty;
@Override
public int getId() {
return IdString.hashCode();
}
}
@@ -0,0 +1,23 @@
package emu.nebula.data.resources;
import com.google.gson.annotations.SerializedName;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "MallPackage.json")
public class MallPackageDef extends BaseDef {
@SerializedName("Id")
private String IdString;
private int Stock;
private int CurrencyType;
private int CurrencyItemId;
private int CurrencyItemQty;
@Override
public int getId() {
return IdString.hashCode();
}
}
@@ -0,0 +1,23 @@
package emu.nebula.data.resources;
import com.google.gson.annotations.SerializedName;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "MallShop.json")
public class MallShopDef extends BaseDef {
@SerializedName("Id")
private String IdString;
private int Stock;
private int ItemId;
private int CurrencyItemId;
private int ItemQty;
@Override
public int getId() {
return IdString.hashCode();
}
}
@@ -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 = "Potential.json")
public class PotentialDef extends BaseDef {
private int Id;
private int CharId;
private int Build;
private int MaxLevel;
@Override
public int getId() {
return Id;
}
}
@@ -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 = "StarTower.json")
public class StarTowerDef extends BaseDef {
private int Id;
private int[] FloorNum;
@Override
public int getId() {
return Id;
}
}
@@ -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 = "WorldClass.json")
public class WorldClassDef extends BaseDef {
private int Id;
private int Exp;
private String Reward;
@Override
public int getId() {
return Id;
}
}
@@ -0,0 +1,9 @@
package emu.nebula.database;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface AccountDatabaseOnly {
}
@@ -0,0 +1,23 @@
package emu.nebula.database;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
@Entity(value = "counters", useDiscriminator = false)
public class DatabaseCounter {
@Id
private String id;
private int count;
public DatabaseCounter() {}
public DatabaseCounter(String id) {
this.id = id;
this.count = 10000;
}
public int getNextId() {
int id = ++count;
return id;
}
}
@@ -0,0 +1,251 @@
package emu.nebula.database;
import java.util.stream.Stream;
import emu.nebula.Config.DatabaseInfo;
import emu.nebula.Config.InternalMongoInfo;
import emu.nebula.Nebula;
import emu.nebula.Nebula.ServerType;
import emu.nebula.database.codecs.*;
import emu.nebula.util.Utils;
import org.bson.codecs.configuration.CodecRegistries;
import org.reflections.Reflections;
import com.mongodb.MongoCommandException;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.MongoIterable;
import com.mongodb.client.result.DeleteResult;
import de.bwaldvogel.mongo.MongoBackend;
import de.bwaldvogel.mongo.MongoServer;
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.filters.Filters;
import dev.morphia.query.updates.UpdateOperators;
import lombok.Getter;
@Getter
public final class DatabaseManager {
@Getter
private static MongoServer server;
private Datastore datastore;
private static final InsertOneOptions INSERT_OPTIONS = new InsertOneOptions();
private static final DeleteOptions DELETE_OPTIONS = new DeleteOptions();
private static final DeleteOptions DELETE_MANY = new DeleteOptions().multi(true);
public DatabaseManager(DatabaseInfo info, ServerType type) {
// Variables
var internalConfig = Nebula.getConfig().getInternalMongoServer();
String connectionString = info.getUri();
// Start local mongo server
if (info.isUseInternal()) {
if (Utils.isPortOpen(internalConfig.getAddress(), internalConfig.getPort())) {
connectionString = startInternalMongoServer(internalConfig);
Nebula.getLogger().info("Started local MongoDB server at " + server.getConnectionString());
} else {
Nebula.getLogger().warn("Local MongoDB server could not be created because the port is in use.");
}
}
// Initialize
MongoClient mongoClient = MongoClients.create(connectionString);
// Add our custom fastutil codecs
var codecProvider = CodecRegistries.fromCodecs(
new IntSetCodec(), new IntListCodec(), new Int2IntMapCodec(), new ItemParamMapCodec()
);
// Set mapper options
MapperOptions mapperOptions = MapperOptions.builder()
.storeEmpties(true)
.storeNulls(false)
.codecProvider(codecProvider)
.build();
// Create data store.
datastore = Morphia.createDatastore(mongoClient, info.getCollection(), mapperOptions);
// Map classes
var entities = new Reflections(Nebula.class.getPackageName())
.getTypesAnnotatedWith(Entity.class)
.stream()
.filter(cls -> {
Entity e = cls.getAnnotation(Entity.class);
return e != null && !e.value().equals(Mapper.IGNORED_FIELDNAME);
})
.toList();
if (type.runLogin()) {
// Only map account related entities
var map = entities.stream().filter(cls -> {
return cls.getAnnotation(AccountDatabaseOnly.class) != null;
}).toArray(Class<?>[]::new);
datastore.getMapper().map(map);
}
if (type.runGame()) {
// Only map game related entities
var map = entities.stream().filter(cls -> {
return cls.getAnnotation(AccountDatabaseOnly.class) == null;
}).toArray(Class<?>[]::new);
datastore.getMapper().map(map);
}
// Ensure indexes
ensureIndexes();
// Done
Nebula.getLogger().info("Connected to the MongoDB database at " + connectionString);
}
public MongoDatabase getDatabase() {
return getDatastore().getDatabase();
}
private void ensureIndexes() {
try {
datastore.ensureIndexes();
} catch (MongoCommandException exception) {
Nebula.getLogger().warn("Mongo index error: ", exception);
// Duplicate index error
if (exception.getCode() == 85) {
// Drop all indexes and re add them
MongoIterable<String> collections = datastore.getDatabase().listCollectionNames();
for (String name : collections) {
datastore.getDatabase().getCollection(name).dropIndexes();
}
// Add back indexes
datastore.ensureIndexes();
}
}
}
// Database Functions
public boolean checkIfObjectExists(Class<?> cls, String filter, String value) {
return getDatastore().find(cls).filter(Filters.eq(filter, value)).count() > 0;
}
public <T> T getObjectByUid(Class<T> cls, long uid) {
return getDatastore().find(cls).filter(Filters.eq("_id", uid)).first();
}
public <T> T getObjectByField(Class<T> cls, String filter, Object value) {
return getDatastore().find(cls).filter(Filters.eq(filter, value)).first();
}
public <T> T getObjectByField(Class<T> cls, String filter, long value) {
return getDatastore().find(cls).filter(Filters.eq(filter, value)).first();
}
public <T> Stream<T> getObjects(Class<T> cls, String filter, Object value) {
return getDatastore().find(cls).filter(Filters.eq(filter, value)).stream();
}
public <T> Stream<T> getObjects(Class<T> cls, String filter, long value) {
return getDatastore().find(cls).filter(Filters.eq(filter, value)).stream();
}
public <T> Stream<T> getObjects(Class<T> cls) {
return getDatastore().find(cls).stream();
}
public <T> void save(T obj) {
getDatastore().save(obj, INSERT_OPTIONS);
}
public <T> boolean delete(T obj) {
DeleteResult result = getDatastore().delete(obj, DELETE_OPTIONS);
return result.getDeletedCount() > 0;
}
public boolean delete(Class<?> cls, String filter, long uid) {
DeleteResult result = getDatastore().find(cls).filter(Filters.eq(filter, uid)).delete(DELETE_MANY);
return result.getDeletedCount() > 0;
}
public void update(Object obj, int uid, String field, Object item) {
update(obj, uid, field, item, false);
}
public void update(Object obj, int uid, String field, Object value, boolean upsert) {
var opt = new UpdateOptions().upsert(upsert);
getDatastore().find(obj.getClass())
.filter(Filters.eq("_id", uid))
.update(opt, UpdateOperators.set(field, value));
}
@SuppressWarnings("removal")
public void update(Object obj, int uid, String field, Object value, String field2, Object value2) {
getDatastore().find(obj.getClass())
.filter(Filters.eq("_id", uid))
.update(UpdateOperators.set(field, value), UpdateOperators.set(field2, value2));
}
public void updateNested(Object obj, int uid, String filter, int filterId, String field, Object item) {
var opt = new UpdateOptions().upsert(false);
getDatastore().find(obj.getClass())
.filter(Filters.eq("_id", uid))
.filter(Filters.eq(filter, filterId))
.update(opt, UpdateOperators.set(field, item));
}
public void addToList(Object obj, int uid, String field, Object item) {
var opt = new UpdateOptions().upsert(false);
getDatastore().find(obj.getClass())
.filter(Filters.eq("_id", uid))
.update(opt, UpdateOperators.addToSet(field, item));
}
// Database counter
public synchronized int getNextObjectId(Class<?> c) {
DatabaseCounter counter = getDatastore().find(DatabaseCounter.class).filter(Filters.eq("_id", c.getSimpleName())).first();
if (counter == null) {
counter = new DatabaseCounter(c.getSimpleName());
}
try {
return counter.getNextId();
} finally {
getDatastore().save(counter);
}
}
// Internal MongoDB server
public static String startInternalMongoServer(InternalMongoInfo internalMongo) {
// Get backend
MongoBackend backend = null;
if (internalMongo.filePath != null && internalMongo.filePath.length() > 0) {
backend = new H2Backend(internalMongo.filePath);
} else {
backend = new MemoryBackend();
}
// Create the local mongo server and replace the connection string
server = new MongoServer(backend);
// Bind to address of it exists
if (internalMongo.getAddress() != null && internalMongo.getPort() != 0) {
server.bind(internalMongo.getAddress(), internalMongo.getPort());
} else {
server.bind(); // Binds to random port
}
return server.getConnectionString();
}
}
@@ -0,0 +1,11 @@
package emu.nebula.database;
import emu.nebula.Nebula;
public interface GameDatabaseObject {
public default void save() {
Nebula.getGameDatabase().save(this);
}
}
@@ -0,0 +1,43 @@
package emu.nebula.database.codecs;
import org.bson.BsonReader;
import org.bson.BsonType;
import org.bson.BsonWriter;
import org.bson.codecs.Codec;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.EncoderContext;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
/**
* Custom mongodb codec for encoding/decoding fastutil int2int maps.
*/
public class Int2IntMapCodec implements Codec<Int2IntMap> {
@Override
public Class<Int2IntMap> getEncoderClass() {
return Int2IntMap.class;
}
@Override
public void encode(BsonWriter writer, Int2IntMap collection, EncoderContext encoderContext) {
writer.writeStartDocument();
for (var entry : collection.int2IntEntrySet()) {
writer.writeName(Integer.toString(entry.getIntKey()));
writer.writeInt32(entry.getIntValue());
}
writer.writeEndDocument();
}
@Override
public Int2IntMap decode(BsonReader reader, DecoderContext decoderContext) {
Int2IntMap collection = new Int2IntOpenHashMap();
reader.readStartDocument();
while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
collection.put(Integer.parseInt(reader.readName()), reader.readInt32());
}
reader.readEndDocument();
return collection;
}
}
@@ -0,0 +1,42 @@
package emu.nebula.database.codecs;
import org.bson.BsonReader;
import org.bson.BsonType;
import org.bson.BsonWriter;
import org.bson.codecs.Codec;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.EncoderContext;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
/**
* Custom mongodb codec for encoding/decoding fastutil int sets.
*/
public class IntListCodec implements Codec<IntList> {
@Override
public Class<IntList> getEncoderClass() {
return IntList.class;
}
@Override
public void encode(BsonWriter writer, IntList collection, EncoderContext encoderContext) {
writer.writeStartArray();
for (int value : collection) {
writer.writeInt32(value);
}
writer.writeEndArray();
}
@Override
public IntList decode(BsonReader reader, DecoderContext decoderContext) {
IntList collection = new IntArrayList();
reader.readStartArray();
while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
collection.add(reader.readInt32());
}
reader.readEndArray();
return collection;
}
}
@@ -0,0 +1,42 @@
package emu.nebula.database.codecs;
import org.bson.BsonReader;
import org.bson.BsonType;
import org.bson.BsonWriter;
import org.bson.codecs.Codec;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.EncoderContext;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
/**
* Custom mongodb codec for encoding/decoding fastutil int sets.
*/
public class IntSetCodec implements Codec<IntSet> {
@Override
public Class<IntSet> getEncoderClass() {
return IntSet.class;
}
@Override
public void encode(BsonWriter writer, IntSet collection, EncoderContext encoderContext) {
writer.writeStartArray();
for (int value : collection) {
writer.writeInt32(value);
}
writer.writeEndArray();
}
@Override
public IntSet decode(BsonReader reader, DecoderContext decoderContext) {
IntSet collection = new IntOpenHashSet();
reader.readStartArray();
while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
collection.add(reader.readInt32());
}
reader.readEndArray();
return collection;
}
}
@@ -0,0 +1,42 @@
package emu.nebula.database.codecs;
import org.bson.BsonReader;
import org.bson.BsonType;
import org.bson.BsonWriter;
import org.bson.codecs.Codec;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.EncoderContext;
import emu.nebula.game.inventory.ItemParamMap;
/**
* Copy of Int2IntMapCodec.java
*/
public class ItemParamMapCodec implements Codec<ItemParamMap> {
@Override
public Class<ItemParamMap> getEncoderClass() {
return ItemParamMap.class;
}
@Override
public void encode(BsonWriter writer, ItemParamMap collection, EncoderContext encoderContext) {
writer.writeStartDocument();
for (var entry : collection.int2IntEntrySet()) {
writer.writeName(Integer.toString(entry.getIntKey()));
writer.writeInt32(entry.getIntValue());
}
writer.writeEndDocument();
}
@Override
public ItemParamMap decode(BsonReader reader, DecoderContext decoderContext) {
ItemParamMap collection = new ItemParamMap();
reader.readStartDocument();
while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
collection.put(Integer.parseInt(reader.readName()), reader.readInt32());
}
reader.readEndDocument();
return collection;
}
}
@@ -0,0 +1,87 @@
package emu.nebula.game;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import emu.nebula.game.player.PlayerModule;
import emu.nebula.net.GameSession;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import lombok.Getter;
@Getter
public class GameContext {
private final Object2ObjectMap<String, GameSession> sessions;
// Modules
private final PlayerModule playerModule;
// Cleanup thread
private final Timer cleanupTimer;
public GameContext() {
this.sessions = new Object2ObjectOpenHashMap<>();
this.playerModule = new PlayerModule(this);
this.cleanupTimer = new Timer();
this.cleanupTimer.scheduleAtFixedRate(new CleanupTask(this), 0, TimeUnit.SECONDS.toMillis(60));
}
public synchronized GameSession getSessionByToken(String token) {
return sessions.get(token);
}
public synchronized void addSession(GameSession session) {
this.sessions.put(session.getToken(), session);
}
public synchronized void generateSessionToken(GameSession session) {
// Remove token
if (session.getToken() != null) {
this.sessions.remove(session.getToken());
}
// Generate token
String token = null;
do {
token = session.generateToken();
} while (this.getSessions().containsKey(token));
// Register session
this.sessions.put(session.getToken(), session);
}
// TODO add timeout to config
public synchronized void cleanupInactiveSessions() {
var it = this.getSessions().entrySet().iterator();
long timeout = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(600); // 10 minutes
while (it.hasNext()) {
var session = it.next().getValue();
if (timeout > session.getLastActiveTime()) {
// Remove from session map
it.remove();
// Clear player
session.clearPlayer(this);
}
}
}
@Getter
public static class CleanupTask extends TimerTask {
private GameContext gameContext;
public CleanupTask(GameContext gameContext) {
this.gameContext = gameContext;
}
@Override
public void run() {
this.getGameContext().cleanupInactiveSessions();
}
}
}
@@ -0,0 +1,13 @@
package emu.nebula.game;
public abstract class GameContextModule {
private transient GameContext context;
public GameContextModule(GameContext player) {
this.context = player;
}
public GameContext getGameContext() {
return context;
}
}
@@ -0,0 +1,167 @@
package emu.nebula.game.account;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import dev.morphia.annotations.Indexed;
import emu.nebula.Nebula;
import emu.nebula.database.AccountDatabaseOnly;
import emu.nebula.util.Snowflake;
import lombok.Getter;
@Getter
@AccountDatabaseOnly
@Entity(value = "accounts", useDiscriminator = false)
public class Account {
@Id private String uid;
@Indexed
private String email;
private String code;
private String nickname;
private String picture;
@Indexed private String loginToken;
@Indexed private String gameToken;
private Set<String> permissions;
private int reservedPlayerUid;
private long createdAt;
@Deprecated
public Account() {
// Morphia only
}
public Account(String email, String password, int reservedUid) {
this.uid = Long.toString(Snowflake.newUid());
this.email = email;
this.nickname = "";
this.picture = "";
this.permissions = new HashSet<>();
this.reservedPlayerUid = reservedUid;
this.createdAt = System.currentTimeMillis() / 1000;
}
public boolean verifyCode(String code) {
// TODO
return true;
}
public void setNickname(String value) {
this.nickname = value;
}
// Tokens
public String generateLoginToken() {
this.loginToken = AccountHelper.createSessionKey(this.getUid());
this.save();
return this.loginToken;
}
public String generateGameToken() {
this.gameToken = AccountHelper.createSessionKey(this.getUid());
this.save();
return this.gameToken;
}
// Permissions
public Set<String> getPermissions() {
if (this.permissions == null) {
this.permissions = new HashSet<>();
this.save();
}
return this.permissions;
}
public boolean addPermission(String permission) {
if (this.getPermissions().contains(permission)) {
return false;
}
this.getPermissions().add(permission);
this.save();
return true;
}
public static boolean permissionMatchesWildcard(String wildcard, String[] permissionParts) {
String[] wildcardParts = wildcard.split("\\.");
if (permissionParts.length < wildcardParts.length) { // A longer wildcard can never match a shorter permission
return false;
}
for (int i = 0; i < wildcardParts.length; i++) {
switch (wildcardParts[i]) {
case "**": // Recursing match
return true;
case "*": // Match only one layer
if (i >= (permissionParts.length-1)) {
return true;
}
break;
default: // This layer isn't a wildcard, it needs to match exactly
if (!wildcardParts[i].equals(permissionParts[i])) {
return false;
}
}
}
// At this point the wildcard will have matched every layer, but if it is shorter then the permission then this is not a match at this point (no **).
return wildcardParts.length == permissionParts.length;
}
public boolean hasPermission(String permission) {
// Skip if permission isnt required
if (permission.isEmpty()) {
return true;
}
// Default permissions
var defaultPermissions = Nebula.getConfig().getServerOptions().getDefaultPermissions();
if (defaultPermissions.contains("*")) {
return true;
}
// Add default permissions if it doesn't exist
List<String> permissions = Stream.of(this.getPermissions(), defaultPermissions)
.flatMap(Collection::stream)
.distinct().toList();
if (permissions.contains(permission)) {
return true;
}
String[] permissionParts = permission.split("\\.");
for (String p : permissions) {
if (p.startsWith("-") && permissionMatchesWildcard(p.substring(1), permissionParts)) return false;
if (permissionMatchesWildcard(p, permissionParts)) return true;
}
return permissions.contains("*");
}
public boolean removePermission(String permission) {
boolean res = this.getPermissions().remove(permission);
if (res) this.save();
return res;
}
public void clearPermission() {
this.getPermissions().clear();
this.save();
}
// Database
public void save() {
Nebula.getAccountDatabase().save(this);
}
}
@@ -0,0 +1,71 @@
package emu.nebula.game.account;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64;
import emu.nebula.Nebula;
/**
* Helper class for handling account related stuff
*/
public class AccountHelper {
public static Account createAccount(String email, String password, int reservedUid) {
Account account = Nebula.getAccountDatabase().getObjectByField(Account.class, "email", email);
if (account != null) {
return null;
}
account = new Account(email, password, reservedUid);
account.save();
return account;
}
public static Account getAccountByEmail(String email) {
if (email == null || email.isEmpty()) {
return null;
}
return Nebula.getAccountDatabase().getObjectByField(Account.class, "email", email);
}
public static Account getAccountByLoginToken(String token) {
if (token == null || token.isEmpty()) {
return null;
}
return Nebula.getAccountDatabase().getObjectByField(Account.class, "loginToken", token);
}
public static boolean deleteAccount(String username) {
Account account = Nebula.getAccountDatabase().getObjectByField(Account.class, "username", username);
if (account == null) {
return false;
}
// Delete the account first
return Nebula.getAccountDatabase().delete(account);
}
// Simple way to create a unique session key
public static String createSessionKey(String accountUid) {
byte[] random = new byte[64];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(random);
String temp = accountUid + "." + System.currentTimeMillis() + "." + secureRandom.toString();
try {
MessageDigest md = MessageDigest.getInstance("SHA-512");
byte[] bytes = md.digest(temp.getBytes());
return Base64.getEncoder().encodeToString(bytes);
} catch (Exception e) {
return Base64.getEncoder().encodeToString(temp.getBytes());
}
}
}
@@ -0,0 +1,283 @@
package emu.nebula.game.character;
import org.bson.types.ObjectId;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import dev.morphia.annotations.Indexed;
import emu.nebula.GameConstants;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.CharacterDef;
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.proto.Public.Char;
import emu.nebula.proto.Public.CharGemPreset;
import emu.nebula.proto.Public.CharGemSlot;
import emu.nebula.proto.PublicStarTower.StarTowerChar;
import emu.nebula.proto.PublicStarTower.StarTowerCharGem;
import lombok.Getter;
@Getter
@Entity(value = "characters", useDiscriminator = false)
public class Character implements GameDatabaseObject {
@Id
private ObjectId uid;
@Indexed
private int playerUid;
private transient CharacterDef data;
private transient Player player;
private int charId;
private int advance;
private int level;
private int exp;
private int skin;
private int[] skills;
private byte[] talents;
private long createTime;
@Deprecated // Morphia only!
public Character() {
}
public Character(Player player, int charId) {
this(player, GameData.getCharacterDataTable().get(charId));
}
public Character(Player player, CharacterDef data) {
this.player = player;
this.playerUid = player.getUid();
this.charId = data.getId();
this.data = data;
this.level = 1;
this.skin = data.getDefaultSkinId();
this.skills = new int[] {1, 1, 1, 1, 1};
this.talents = new byte[8];
this.createTime = Nebula.getCurrentTime();
}
public void setPlayer(Player player) {
this.player = player;
}
public void setData(CharacterDef data) {
if (this.data == null && data.getId() == this.getCharId()) {
this.data = data;
}
}
public int getMaxGainableExp() {
if (this.getLevel() >= this.getMaxLevel()) {
return 0;
}
int maxLevel = this.getMaxLevel();
int max = 0;
for (int i = this.getLevel() + 1; i <= maxLevel; i++) {
var data = GameData.getCharacterUpgradeDataTable().get(i);
if (data != null) {
max += data.getExp();
}
}
return Math.max(max - this.getExp(), 0);
}
public int getMaxExp() {
if (this.getLevel() >= this.getMaxLevel()) {
return 0;
}
var data = GameData.getCharacterUpgradeDataTable().get(this.level + 1);
return data != null ? data.getExp() : 0;
}
public int getMaxLevel() {
return 10 + (this.getAdvance() * 10);
}
public void addExp(int amount) {
// Setup
int expRequired = this.getMaxExp();
// Add exp
this.exp += amount;
// Check for level ups
while (this.exp >= expRequired && expRequired > 0) {
this.level += 1;
this.exp -= expRequired;
expRequired = this.getMaxExp();
}
// Clamp exp
if (this.getLevel() >= this.getMaxLevel()) {
this.exp = 0;
}
// Save to database
this.save();
}
// Handlers
public PlayerChangeInfo upgrade(ItemParamMap params) {
// Calculate exp gained
int exp = 0;
// Check if item is an exp item
for (var entry : params.getEntrySet()) {
var data = GameData.getCharItemExpDataTable().get(entry.getIntKey());
if (data == null) return null;
exp += data.getExpValue() * entry.getIntValue();
}
// Clamp exp gain
exp = Math.min(this.getMaxGainableExp(), exp);
// Calculate gold required
params.add(GameConstants.GOLD_ITEM_ID, (int) Math.ceil(exp * 0.15D));
// Verify that the player has the items
if (!this.getPlayer().getInventory().verifyItems(params)) {
return null;
}
// Remove items
var changes = this.getPlayer().getInventory().removeItems(params, null);
// Add exp
this.addExp(exp);
// Success
return changes.setSuccess(true);
}
public PlayerChangeInfo advance() {
// TODO check player level to make sure they can advance this character
// Get advance data
int advanceId = (this.getData().getAdvanceGroup() * 100) + (this.advance + 1);
var data = GameData.getCharacterAdvanceDataTable().get(advanceId);
if (data == null) {
return null;
}
// Verify that the player has the items
if (!this.getPlayer().getInventory().verifyItems(data.getMaterials())) {
return null;
}
// Remove items
var changes = this.getPlayer().getInventory().removeItems(data.getMaterials(), null);
// Add advance level
this.advance++;
// Save to database
this.save();
// Success
return changes.setSuccess(true);
}
public PlayerChangeInfo upgradeSkill(int index) {
// TODO check player level to make sure they can advance this character
// Sanity check
if (index < 0 || index >= this.skills.length) {
return null;
}
// Get advance data
int upgradeId = (this.getData().getSkillsUpgradeGroup(index) * 100) + (this.skills[index] + 1);
var data = GameData.getCharacterSkillUpgradeDataTable().get(upgradeId);
if (data == null) {
return null;
}
// Verify that the player has the items
if (!this.getPlayer().getInventory().verifyItems(data.getMaterials())) {
return null;
}
// Remove items
var changes = this.getPlayer().getInventory().removeItems(data.getMaterials(), null);
// Add skill level
this.skills[index]++;
// Save to database
this.save();
// Success
return changes.setSuccess(true);
}
// Proto
public Char toProto() {
var proto = Char.newInstance()
.setTid(this.getCharId())
.setLevel(this.getLevel())
.setSkin(this.getSkin())
.setAdvance(this.getAdvance())
.setTalentNodes(this.getTalents())
.addAllSkillLvs(this.getSkills())
.setCreateTime(this.getCreateTime());
var gemPresets = proto.getMutableCharGemPresets()
.getMutableCharGemPresets();
for (int i = 0; i < 3; i++) {
var preset = CharGemPreset.newInstance()
.addAllSlotGem(-1, -1, -1);
gemPresets.add(preset);
}
for (int i = 1; i <= 3; i++) {
var slot = CharGemSlot.newInstance()
.setId(i);
proto.addCharGemSlots(slot);
}
proto.getMutableAffinityQuests();
return proto;
}
public StarTowerChar toStarTowerProto() {
var proto = StarTowerChar.newInstance()
.setId(this.getCharId())
.setAdvance(this.getAdvance())
.setLevel(this.getLevel())
.setTalentNodes(this.getTalents())
.addAllSkillLvs(this.getSkills());
for (int i = 1; i <= 3; i++) {
var slot = StarTowerCharGem.newInstance()
.setSlotId(i)
.addAllAttributes(new int[] {0, 0, 0, 0});
proto.addGems(slot);
}
return proto;
}
}
@@ -0,0 +1,151 @@
package emu.nebula.game.character;
import java.util.Collection;
import emu.nebula.Nebula;
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.game.player.Player;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
@Getter
public class CharacterStorage extends PlayerManager {
private final Int2ObjectMap<Character> characters;
private final Int2ObjectMap<GameDisc> discs;
public CharacterStorage(Player player) {
super(player);
this.characters = new Int2ObjectOpenHashMap<>();
this.discs = new Int2ObjectOpenHashMap<>();
}
// Characters
public Character getCharacterById(int id) {
if (id <= 0) {
return null;
}
return this.characters.get(id);
}
public boolean hasCharacter(int id) {
return this.characters.containsKey(id);
}
public Character addCharacter(int charId) {
// Sanity check to make sure we dont have this character already
if (this.hasCharacter(charId)) {
return null;
}
return this.addCharacter(GameData.getCharacterDataTable().get(charId));
}
private Character addCharacter(CharacterDef data) {
// Sanity check to make sure we dont have this character already
if (this.hasCharacter(data.getId())) {
return null;
}
// Create character
var character = new Character(this.getPlayer(), data);
// Save to database
character.save();
// Add to characters
this.characters.put(character.getCharId(), character);
return character;
}
public Collection<Character> getCharacterCollection() {
return this.getCharacters().values();
}
// Discs
public GameDisc getDiscById(int id) {
if (id <= 0) {
return null;
}
return this.discs.get(id);
}
public boolean hasDisc(int id) {
return this.discs.containsKey(id);
}
public GameDisc addDisc(int discId) {
// Sanity check to make sure we dont have this character already
if (this.hasDisc(discId)) {
return null;
}
return this.addDisc(GameData.getDiscDataTable().get(discId));
}
private GameDisc addDisc(DiscDef data) {
// Sanity check to make sure we dont have this character already
if (this.hasDisc(data.getId())) {
return null;
}
// Create disc
var disc = new GameDisc(this.getPlayer(), data);
// Save to database
disc.save();
// Add to discs
this.discs.put(disc.getDiscId(), disc);
return disc;
}
public Collection<GameDisc> getDiscCollection() {
return this.getDiscs().values();
}
// Database
public void loadFromDatabase() {
var db = Nebula.getGameDatabase();
db.getObjects(Character.class, "playerUid", getPlayerUid()).forEach(character -> {
// Get data
var data = GameData.getCharacterDataTable().get(character.getCharId());
// Validate
if (data == null) {
return;
}
character.setPlayer(this.getPlayer());
character.setData(data);
// Add to characters
this.characters.put(character.getCharId(), character);
});
db.getObjects(GameDisc.class, "playerUid", getPlayerUid()).forEach(disc -> {
// Get data
var data = GameData.getDiscDataTable().get(disc.getDiscId());
if (data == null) return;
disc.setPlayer(this.getPlayer());
disc.setData(data);
// Add
this.discs.put(disc.getDiscId(), disc);
});
}
}
@@ -0,0 +1,216 @@
package emu.nebula.game.character;
import org.bson.types.ObjectId;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import dev.morphia.annotations.Indexed;
import emu.nebula.GameConstants;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.DiscDef;
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.proto.Public.Disc;
import emu.nebula.proto.PublicStarTower.StarTowerDisc;
import lombok.Getter;
@Getter
@Entity(value = "discs", useDiscriminator = false)
public class GameDisc implements GameDatabaseObject {
@Id
private ObjectId uid;
@Indexed
private int playerUid;
private transient DiscDef data;
private transient Player player;
private int discId;
private int level;
private int exp;
private int phase;
private int star;
private long createTime;
@Deprecated // Morphia only!
public GameDisc() {
}
public GameDisc(Player player, int discId) {
this(player, GameData.getDiscDataTable().get(discId));
}
public GameDisc(Player player, DiscDef data) {
this.player = player;
this.playerUid = player.getUid();
this.data = data;
this.discId = data.getId();
this.level = 1;
this.createTime = Nebula.getCurrentTime();
}
public void setPlayer(Player player) {
this.player = player;
}
public void setData(DiscDef data) {
if (this.data == null && data.getId() == this.getDiscId()) {
this.data = data;
}
}
public int getMaxGainableExp() {
if (this.getLevel() >= this.getMaxLevel()) {
return 0;
}
int maxLevel = this.getMaxLevel();
int max = 0;
for (int i = this.getLevel() + 1; i <= maxLevel; i++) {
int dataId = (this.getData().getStrengthenGroupId() * 1000) + i;
var data = GameData.getDiscStrengthenDataTable().get(dataId);
if (data != null) {
max += data.getExp();
}
}
return Math.max(max - this.getExp(), 0);
}
public int getMaxExp() {
if (this.getLevel() >= this.getMaxLevel()) {
return 0;
}
int dataId = (this.getData().getStrengthenGroupId() * 1000) + (this.level + 1);
var data = GameData.getDiscStrengthenDataTable().get(dataId);
return data != null ? data.getExp() : 0;
}
public int getMaxLevel() {
return 10 + (this.getPhase() * 10);
}
public void addExp(int amount) {
// Setup
int expRequired = this.getMaxExp();
// Add exp
this.exp += amount;
// Check for level ups
while (this.exp >= expRequired && expRequired > 0) {
this.level += 1;
this.exp -= expRequired;
expRequired = this.getMaxExp();
}
// Clamp exp
if (this.getLevel() >= this.getMaxLevel()) {
this.exp = 0;
}
// Save to database
this.save();
}
// Handlers
public PlayerChangeInfo upgrade(ItemParamMap params) {
// Calculate exp gained
int exp = 0;
// Check if item is an exp item
for (var entry : params.getEntrySet()) {
var data = GameData.getDiscItemExpDataTable().get(entry.getIntKey());
if (data == null) return null;
exp += data.getExp() * entry.getIntValue();
}
// Clamp exp gain
exp = Math.min(this.getMaxGainableExp(), exp);
// Calculate gold required
params.add(GameConstants.GOLD_ITEM_ID, (int) Math.ceil(exp * 0.25D));
// Verify that the player has the items
if (!this.getPlayer().getInventory().verifyItems(params)) {
return null;
}
// Create change info
var changes = new PlayerChangeInfo();
// Remove items
this.getPlayer().getInventory().removeItems(params, changes);
// Add exp
this.addExp(exp);
// Success
return changes.setSuccess(true);
}
public PlayerChangeInfo promote() {
// TODO check player level to make sure they can advance this disc
// Get promote data
int phaseId = (this.getData().getPromoteGroupId() * 1000) + (this.phase + 1);
var data = GameData.getDiscPromoteDataTable().get(phaseId);
if (data == null) {
return null;
}
// Verify that the player has the items
if (!this.getPlayer().getInventory().verifyItems(data.getMaterials())) {
return null;
}
// Remove items
var changes = this.getPlayer().getInventory().removeItems(data.getMaterials(), null);
// Add phase level
this.phase++;
// Save to database
this.save();
// Success
return changes.setSuccess(true);
}
// Proto
public Disc toProto() {
var proto = Disc.newInstance()
.setId(this.getDiscId())
.setLevel(this.getLevel())
.setExp(this.getExp())
.setPhase(this.getPhase())
.setStar(this.getStar())
.setCreateTime(this.getCreateTime());
return proto;
}
public StarTowerDisc toStarTowerProto() {
var proto = StarTowerDisc.newInstance()
.setId(this.getDiscId())
.setLevel(this.getLevel())
.setPhase(this.getPhase())
.setStar(this.getStar());
return proto;
}
}
@@ -0,0 +1,57 @@
package emu.nebula.game.formation;
import dev.morphia.annotations.Entity;
import emu.nebula.proto.Public.FormationInfo;
import lombok.Getter;
@Getter
@Entity(useDiscriminator = false)
public class Formation {
private int num;
private int[] charIds;
private int[] discIds;
@Deprecated
public Formation() {
}
public Formation(int num) {
this.num = num;
this.charIds = new int[3];
this.discIds = new int[6];
}
public Formation(FormationInfo formation) {
this.num = formation.getNumber();
this.charIds = formation.getCharIds().toArray();
this.discIds = formation.getDiscIds().toArray();
}
public int getCharIdAt(int i) {
if (i < 0 || i >= this.charIds.length) {
return -1;
}
return this.charIds[i];
}
public int getDiscIdAt(int i) {
if (i < 0 || i >= this.discIds.length) {
return -1;
}
return this.discIds[i];
}
// Proto
public FormationInfo toProto() {
var proto = FormationInfo.newInstance()
.setNumber(this.getNum())
.addAllCharIds(this.getCharIds())
.addAllDiscIds(this.getDiscIds());
return proto;
}
}
@@ -0,0 +1,82 @@
package emu.nebula.game.formation;
import emu.nebula.game.player.PlayerManager;
import java.util.HashMap;
import java.util.Map;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import emu.nebula.GameConstants;
import emu.nebula.Nebula;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.player.Player;
import emu.nebula.proto.Public.FormationInfo;
import emu.nebula.proto.Public.TowerFormation;
import lombok.Getter;
@Getter
@Entity(value = "formations", useDiscriminator = false)
public class FormationManager extends PlayerManager implements GameDatabaseObject {
@Id
private int uid;
private Map<Integer, Formation> formations;
@Deprecated // Morphia only
public FormationManager() {
}
public FormationManager(Player player) {
super(player);
this.uid = player.getUid();
this.formations = new HashMap<>();
this.save();
}
public Formation getFormationById(int num) {
return this.formations.get(num);
}
public boolean updateFormation(FormationInfo info) {
// Sanity check
if (info.getNumber() < 1 || info.getNumber() > GameConstants.MAX_FORMATIONS) {
return false;
}
// More sanity
if (info.getCharIds().length() < 1 || info.getCharIds().length() > 3) {
return false;
}
if (info.getDiscIds().length() < 3 || info.getDiscIds().length() > 6) {
return false;
}
// Validate formation to make sure we have all the chars and discs
// TODO
// Create formation
var formation = new Formation(info);
// Add to formations map
this.formations.put(formation.getNum(), formation);
// Save to db
Nebula.getGameDatabase().update(this, this.getPlayerUid(), "formations." + formation.getNum(), formation, true);
// Success
return true;
}
// Proto
public TowerFormation toProto() {
var proto = TowerFormation.newInstance();
return proto;
}
}
@@ -0,0 +1,65 @@
package emu.nebula.game.inventory;
import org.bson.types.ObjectId;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import dev.morphia.annotations.Indexed;
import emu.nebula.Nebula;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.player.Player;
import emu.nebula.proto.Public.Item;
import emu.nebula.util.Utils;
import lombok.Getter;
@Getter
@Entity(value = "items", useDiscriminator = false)
public class GameItem implements GameDatabaseObject {
@Id
private ObjectId uid;
@Indexed
private int playerUid;
private int itemId;
private int count;
@Deprecated
public GameItem() {
}
public GameItem(Player player, int id, int count) {
this.playerUid = player.getUid();
this.itemId = id;
this.count = count;
}
public int add(int amount) {
int oldCount = this.count;
this.count = Utils.safeAdd(this.count, amount, Integer.MAX_VALUE, 0);
return this.count - oldCount;
}
// Database
@Override
public void save() {
if (this.getCount() <= 0) {
if (this.getUid() != null) {
Nebula.getGameDatabase().delete(this);
}
} else {
Nebula.getGameDatabase().save(this);
}
}
// Proto
public Item toProto() {
var proto = Item.newInstance()
.setTid(this.getItemId())
.setQty(this.getCount());
return proto;
}
}
@@ -0,0 +1,63 @@
package emu.nebula.game.inventory;
import org.bson.types.ObjectId;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import dev.morphia.annotations.Indexed;
import emu.nebula.Nebula;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.player.Player;
import emu.nebula.proto.Public.Res;
import emu.nebula.util.Utils;
import lombok.Getter;
@Getter
@Entity(value = "resources", useDiscriminator = false)
public class GameResource implements GameDatabaseObject {
@Id
private ObjectId uid;
@Indexed
private int playerUid;
public int resourceId;
public int count;
@Deprecated // Morphia only
public GameResource() {
}
public GameResource(Player player, int id, int count) {
this.playerUid = player.getUid();
this.resourceId = id;
this.count = count;
}
public int add(int amount) {
int oldCount = this.count;
this.count = Utils.safeAdd(this.count, amount, Integer.MAX_VALUE, 0);
return this.count - oldCount;
}
@Override
public void save() {
if (this.getCount() <= 0) {
if (this.getUid() != null) {
Nebula.getGameDatabase().delete(this);
}
} else {
Nebula.getGameDatabase().save(this);
}
}
// Proto
public Res toProto() {
var proto = Res.newInstance()
.setTid(this.getResourceId())
.setQty(this.getCount());
return proto;
}
}
@@ -0,0 +1,310 @@
package emu.nebula.game.inventory;
import java.util.List;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.game.player.PlayerManager;
import emu.nebula.proto.Public.Item;
import emu.nebula.proto.Public.Res;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
@Getter
public class Inventory extends PlayerManager {
private final Int2ObjectMap<GameResource> resources;
private final Int2ObjectMap<GameItem> items;
public Inventory(Player player) {
super(player);
this.resources = new Int2ObjectOpenHashMap<>();
this.items = new Int2ObjectOpenHashMap<>();
}
// Resources
public synchronized int getResourceCount(int id) {
var res = this.resources.get(id);
return res != null ? res.getCount() : 0;
}
// Items
public synchronized int getItemCount(int id) {
var item = this.getItems().get(id);
return item != null ? item.getCount() : 0;
}
//
public synchronized PlayerChangeInfo addItem(int id, int count, PlayerChangeInfo changes) {
// Changes
if (changes == null) {
changes = new PlayerChangeInfo();
}
// Sanity
if (count == 0) {
return changes;
}
// Get game data
var data = GameData.getItemDataTable().get(id);
if (data == null) {
return changes;
}
// Set amount
int amount = count;
// Add item
switch (data.getItemType()) {
case Res -> {
var res = this.resources.get(id);
int diff = 0;
if (amount > 0) {
// Add resource
if (res == null) {
res = new GameResource(this.getPlayer(), id, amount);
this.resources.put(res.getResourceId(), res);
diff = amount;
} else {
diff = res.add(amount);
}
res.save();
} else {
// Remove resource
if (res == null) {
break;
}
diff = res.add(amount);
res.save();
if (res.getCount() < 0) {
this.resources.remove(id);
}
}
if (diff != 0) {
var change = Res.newInstance()
.setTid(id)
.setQty(diff);
changes.add(change);
}
}
case Item -> {
var item = this.items.get(id);
int diff = 0;
if (amount > 0) {
// Add resource
if (item == null) {
item = new GameItem(this.getPlayer(), id, amount);
this.items.put(item.getItemId(), item);
diff = amount;
} else {
diff = item.add(amount);
}
item.save();
} else {
// Remove resource
if (item == null) {
break;
}
diff = item.add(amount);
item.save();
if (item.getCount() < 0) {
this.resources.remove(id);
}
}
if (diff != 0) {
var change = Item.newInstance()
.setTid(id)
.setQty(diff);
changes.add(change);
}
}
case Disc -> {
if (amount <= 0) {
break;
}
var disc = getPlayer().getCharacters().addDisc(id);
if (disc != null) {
changes.add(disc.toProto());
}
}
case Char -> {
if (amount <= 0) {
break;
}
var character = getPlayer().getCharacters().addCharacter(id);
if (character != null) {
changes.add(character.toProto());
}
}
case WorldRankExp -> {
this.getPlayer().addExp(amount, changes);
}
default -> {
// Not implemented
}
}
//
return changes;
}
@Deprecated
public synchronized PlayerChangeInfo addItems(List<ItemParam> params, PlayerChangeInfo changes) {
// Changes
if (changes == null) {
changes = new PlayerChangeInfo();
}
for (ItemParam param : params) {
this.addItem(param.getId(), param.getCount(), changes);
}
return changes;
}
public synchronized PlayerChangeInfo addItems(ItemParamMap params) {
return this.addItems(params, null);
}
public synchronized PlayerChangeInfo addItems(ItemParamMap params, PlayerChangeInfo changes) {
// Changes
if (changes == null) {
changes = new PlayerChangeInfo();
}
for (var param : params.getEntrySet()) {
this.addItem(param.getIntKey(), param.getIntValue(), changes);
}
return changes;
}
public synchronized PlayerChangeInfo removeItem(int id, int count, PlayerChangeInfo changes) {
if (count > 0) {
count = -count;
}
return this.addItem(id, count, changes);
}
public synchronized PlayerChangeInfo removeItems(ItemParamMap params) {
return this.removeItems(params, null);
}
public synchronized PlayerChangeInfo removeItems(ItemParamMap params, PlayerChangeInfo changes) {
// Changes
if (changes == null) {
changes = new PlayerChangeInfo();
}
for (var param : params.getEntrySet()) {
this.removeItem(param.getIntKey(), param.getIntValue(), changes);
}
return changes;
}
/**
* Checks if the player has enough quanity of this item
*/
public synchronized boolean verifyItem(int id, int count) {
// Sanity check
if (count == 0) {
return true;
} else if (count < 0) {
// Return false if we are trying to verify negative numbers
return false;
}
// Get game data
var data = GameData.getItemDataTable().get(id);
if (data == null) {
return false;
}
boolean result = switch (data.getItemType()) {
case Res -> {
yield this.getResourceCount(id) >= count;
}
case Item -> {
yield this.getItemCount(id) >= count;
}
case Disc -> {
yield getPlayer().getCharacters().hasDisc(id);
}
case Char -> {
yield getPlayer().getCharacters().hasCharacter(id);
}
default -> {
// Not implemented
yield false;
}
};
//
return result;
}
public synchronized boolean verifyItems(ItemParamMap params) {
boolean hasItems = true;
for (var param : params.getEntrySet()) {
hasItems = this.verifyItem(param.getIntKey(), param.getIntValue());
if (!hasItems) {
return hasItems;
}
}
return hasItems;
}
// Database
public void loadFromDatabase() {
var db = Nebula.getGameDatabase();
db.getObjects(GameItem.class, "playerUid", getPlayerUid()).forEach(item -> {
// Get data
var data = GameData.getItemDataTable().get(item.getItemId());
if (data == null) return;
// Add
this.items.put(item.getItemId(), item);
});
db.getObjects(GameResource.class, "playerUid", getPlayerUid()).forEach(res -> {
// Get data
var data = GameData.getItemDataTable().get(res.getResourceId());
if (data == null) return;
// Add
this.resources.put(res.getResourceId(), res);
});
}
}
@@ -0,0 +1,30 @@
package emu.nebula.game.inventory;
import dev.morphia.annotations.Entity;
import emu.nebula.proto.Public.ItemTpl;
import lombok.Getter;
@Getter
@Entity(useDiscriminator = false)
public class ItemParam {
public int id;
public int count;
@Deprecated // Morphia only
public ItemParam() {
}
public ItemParam(int id, int count) {
this.id = id;
this.count = count;
}
public ItemTpl toProto() {
var proto = ItemTpl.newInstance()
.setTid(this.getId())
.setQty(this.getCount());
return proto;
}
}
@@ -0,0 +1,100 @@
package emu.nebula.game.inventory;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import emu.nebula.proto.Public.ItemInfo;
import emu.nebula.proto.Public.ItemTpl;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import us.hebi.quickbuf.RepeatedMessage;
public class ItemParamMap extends Int2IntOpenHashMap {
private static final long serialVersionUID = -4186524272780523459L;
public FastEntrySet entries() {
return this.int2IntEntrySet();
}
@Override @Deprecated
public int addTo(int itemId, int count) {
return this.add(itemId, count);
}
public int add(int itemId, int count) {
if (count == 0) {
return 0;
}
return super.addTo(itemId, count);
}
/**
* Adds all item params from the other map to this one
* @param map The other item param map
*/
public void add(ItemParamMap map) {
for (var entry : map.entries()) {
this.add(entry.getIntKey(), entry.getIntValue());
}
}
/**
* Returns a new ItemParamMap with item amounts multiplied
* @param mult Value to multiply all item amounts in this map by
* @return
*/
public ItemParamMap mulitply(int multiplier) {
var params = new ItemParamMap();
for (var entry : this.int2IntEntrySet()) {
params.put(entry.getIntKey(), entry.getIntValue() * multiplier);
}
return params;
}
//
public FastEntrySet getEntrySet() {
return this.int2IntEntrySet();
}
public List<ItemParam> toList() {
List<ItemParam> list = new ArrayList<>();
for (var entry : this.int2IntEntrySet()) {
list.add(new ItemParam(entry.getIntKey(), entry.getIntValue()));
}
return list;
}
public Stream<ItemTpl> itemTemplateStream() {
return getEntrySet()
.stream()
.map(e -> ItemTpl.newInstance().setTid(e.getIntKey()).setQty(e.getIntValue()));
}
// Proto
public static ItemParamMap fromTemplates(RepeatedMessage<ItemTpl> items) {
var map = new ItemParamMap();
for (var template : items) {
map.add(template.getTid(), template.getQty());
}
return map;
}
public static ItemParamMap fromItemInfos(RepeatedMessage<ItemInfo> items) {
var map = new ItemParamMap();
for (var template : items) {
map.add(template.getTid(), template.getQty());
}
return map;
}
}
@@ -0,0 +1,56 @@
package emu.nebula.game.inventory;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
public enum ItemSubType {
Res (1),
Item (2),
Char (3),
Energy (4),
WorldRankExp (5),
CharShard (6),
Disc (8),
TalentStrengthen (9),
DiscStrengthen (12),
DiscPromote (13),
TreasureBox (17),
GearTreasureBox (18),
SubNoteSkill (19),
SkillStrengthen (24),
CharacterLimitBreak (25),
MonthlyCard (30),
EnergyItem (31),
ComCYO (32),
OutfitCYO (33),
RandomPackage (34),
Equipment (35),
FateCard (37),
EquipmentExp (38),
DiscLimitBreak (40),
Potential (41),
SpecificPotential (42),
Honor (43),
CharacterYO (44),
PlayHead (45),
CharacterSkin (46);
@Getter
private final int value;
private final static Int2ObjectMap<ItemSubType> map = new Int2ObjectOpenHashMap<>();
static {
for (ItemSubType type : ItemSubType.values()) {
map.put(type.getValue(), type);
}
}
private ItemSubType(int value) {
this.value = value;
}
public static ItemSubType getByValue(int value) {
return map.get(value);
}
}
@@ -0,0 +1,39 @@
package emu.nebula.game.inventory;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
public enum ItemType {
Res (1),
Item (2),
Char (3),
Energy (4),
WorldRankExp (5),
RogueItem (6),
Disc (7),
Equipment (8),
CharacterSkin (9),
MonthlyCard (10),
Title (11),
Honor (12),
HeadItem (13);
@Getter
private final int value;
private final static Int2ObjectMap<ItemType> map = new Int2ObjectOpenHashMap<>();
static {
for (ItemType type : ItemType.values()) {
map.put(type.getValue(), type);
}
}
private ItemType(int value) {
this.value = value;
}
public static ItemType getByValue(int value) {
return map.get(value);
}
}
@@ -0,0 +1,86 @@
package emu.nebula.game.mail;
import java.util.concurrent.TimeUnit;
import dev.morphia.annotations.Entity;
import emu.nebula.Nebula;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.proto.Public.Mail;
import lombok.Getter;
import lombok.Setter;
@Getter
@Entity(useDiscriminator = false)
public class GameMail {
private int id;
private String author;
private String subject;
private String desc;
private ItemParamMap attachments;
@Setter private boolean read;
@Setter private boolean recv;
@Setter private boolean pin;
private long flag;
private long time;
private long expiry;
@Deprecated // Morphia only
public GameMail() {
}
public GameMail(String author, String subject, String desc) {
this.author = author;
this.subject = subject;
this.desc = desc;
this.time = Nebula.getCurrentTime();
this.expiry = this.time + TimeUnit.DAYS.toSeconds(30);
}
protected void setId(int id) {
if (this.id == 0) {
this.id = id;
}
}
public boolean canRemove() {
return (this.isRead() || this.isRecv()) && !this.isPin() && (this.hasAttachments() && this.isRecv());
}
public boolean hasAttachments() {
return this.attachments != null;
}
public void addAttachment(int itemId, int count) {
if (this.attachments == null) {
this.attachments = new ItemParamMap();
}
this.attachments.add(itemId, count);
}
public Mail toProto() {
var proto = Mail.newInstance()
.setId(this.getId())
.setAuthor(this.getAuthor())
.setSubject(this.getSubject())
.setDesc(this.getDesc())
.setTime(this.getTime())
.setRead(this.isRead())
.setRecv(this.isRecv())
.setPin(this.isPin())
.setFlag(this.getFlag())
.setDeadline(this.getExpiry());
if (this.getAttachments() != null) {
this.getAttachments().itemTemplateStream()
.forEach(proto::addAttachments);
}
return proto;
}
}
@@ -0,0 +1,197 @@
package emu.nebula.game.mail;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import emu.nebula.Nebula;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import lombok.Getter;
@Getter
@Entity(value = "mailbox", useDiscriminator = false)
public class Mailbox implements GameDatabaseObject, Iterable<GameMail> {
@Id
private int uid;
private int lastMailId;
private List<GameMail> list;
@Deprecated // Morphia only
public Mailbox() {
}
public Mailbox(Player player) {
this.uid = player.getUid();
this.list = new ArrayList<>();
this.save();
}
// TODO optimize to an O(n) algorithm like a map
public GameMail getMailById(int id) {
return this.getList().stream()
.filter(m -> m.getId() == id)
.findFirst()
.orElse(null);
}
public void sendMail(GameMail mail) {
// Set mail id
mail.setId(++this.lastMailId);
// Add to mail list
this.list.add(mail);
// Save to database
Nebula.getGameDatabase().update(this, getUid(), "lastMailId", this.getLastMailId());
Nebula.getGameDatabase().addToList(this, getUid(), "list", mail);
}
public boolean readMail(int id, long flag) {
// Get mail
var mail = this.getMailById(id);
if (mail == null) {
return false;
}
// Set read
mail.setRead(true);
// Update in database
Nebula.getGameDatabase().updateNested(this, getUid(), "list.id", id, "list.$.read", true);
// Success
return true;
}
public GameMail pinMail(int id, long flag, boolean pin) {
// Get mail
var mail = this.getMailById(id);
if (mail == null) {
return null;
}
// Set pin
mail.setPin(pin);
// Update in database
Nebula.getGameDatabase().updateNested(this, getUid(), "list.id", id, "list.$.pin", true);
// Success
return mail;
}
public PlayerChangeInfo recvMail(Player player, int id) {
// Get mails that we want to claim
List<GameMail> mails = null;
if (id == 0) {
// Claim all
mails = this.getList()
.stream()
.filter(mail -> !mail.isRecv() && mail.hasAttachments())
.toList();
} else {
// Claim one
var mail = this.getMailById(id);
if (mail != null && !mail.isRecv() && mail.hasAttachments()) {
mails = List.of(mail);
}
}
// Create change info
var changes = new PlayerChangeInfo();
// Sanity
if (mails == null || mails.isEmpty()) {
return changes;
}
// Recieved mail id list
var recvMails = new IntArrayList();
// Recv mails
for (var mail : mails) {
// Add attachments to player
player.getInventory().addItems(mail.getAttachments(), changes);
// Set claimed flag
mail.setRecv(true);
// Add to recvied mail list
recvMails.add(mail.getId());
// Update in database
Nebula.getGameDatabase().updateNested(this, getUid(), "list.id", mail.getId(), "list.$.recv", true);
}
// Set extra change data
changes.setExtraData(recvMails);
// Success
return changes.setSuccess(true);
}
public IntList removeMail(Player player, int id) {
// Get mails that we want to claim
Set<GameMail> toRemove = null;
if (id == 0) {
// Claim all
toRemove = this.getList()
.stream()
.filter(mail -> mail.canRemove())
.collect(Collectors.toSet());
} else {
// Claim one
var mail = this.getMailById(id);
if (mail != null && mail.canRemove()) {
toRemove = Set.of(mail);
}
}
// Recieved mail id list
var removed = new IntArrayList();
// Sanity check
if (toRemove == null || toRemove.isEmpty()) {
return removed;
}
// Remove
var it = this.getList().iterator();
while (it.hasNext()) {
var mail = it.next();
if (toRemove.contains(mail)) {
removed.add(mail.getId());
it.remove();
}
}
// Save
this.save();
// Success
return removed;
}
@Override
public Iterator<GameMail> iterator() {
return this.getList().iterator();
}
}
@@ -0,0 +1,358 @@
package emu.nebula.game.player;
import java.util.HashSet;
import java.util.Set;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import dev.morphia.annotations.Indexed;
import emu.nebula.GameConstants;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.account.Account;
import emu.nebula.game.character.CharacterStorage;
import emu.nebula.game.formation.FormationManager;
import emu.nebula.game.inventory.Inventory;
import emu.nebula.game.mail.Mailbox;
import emu.nebula.game.tower.StarTowerManager;
import emu.nebula.net.GameSession;
import emu.nebula.proto.PlayerData.PlayerInfo;
import emu.nebula.proto.Public.NewbieInfo;
import emu.nebula.proto.Public.QuestType;
import emu.nebula.proto.Public.WorldClass;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import lombok.Getter;
@Getter
@Entity(value = "players", useDiscriminator = false)
public class Player implements GameDatabaseObject {
@Id private int uid;
@Indexed private String accountUid;
private transient Account account;
private transient Set<GameSession> sessions;
// Details
private String name;
private boolean gender;
private int headIcon;
private int skinId;
private int titlePrefix;
private int titleSuffix;
private int level;
private int exp;
private int energy;
private IntSet boards;
private IntSet titles;
private long createTime;
// Managers
private final transient CharacterStorage characters;
private final transient Inventory inventory;
// Referenced data
private transient FormationManager formations;
private transient Mailbox mailbox;
private transient StarTowerManager starTowerManager;
@Deprecated // Morphia only
public Player() {
this.sessions = new HashSet<>();
this.characters = new CharacterStorage(this);
this.inventory = new Inventory(this);
}
public Player(Account account, String name, boolean gender) {
this();
// Set uid first
if (account.getReservedPlayerUid() > 0) {
this.uid = account.getReservedPlayerUid();
} else {
this.uid = Nebula.getGameDatabase().getNextObjectId(Player.class);
}
// Set basic info
this.accountUid = account.getUid();
this.name = name;
this.gender = gender;
this.headIcon = 101;
this.skinId = 10301;
this.titlePrefix = 1;
this.titleSuffix = 2;
this.level = 1;
this.boards = new IntOpenHashSet();
this.titles = new IntOpenHashSet();
this.createTime = Nebula.getCurrentTime();
// Add starter characters
this.getCharacters().addCharacter(103);
this.getCharacters().addCharacter(112);
this.getCharacters().addCharacter(113);
// Add starter discs
this.getCharacters().addDisc(211001);
this.getCharacters().addDisc(211005);
this.getCharacters().addDisc(211007);
this.getCharacters().addDisc(211008);
// Add titles
this.getTitles().add(this.getTitlePrefix());
this.getTitles().add(this.getTitleSuffix());
// Add board ids
this.getBoards().add(410301);
}
public Account getAccount() {
if (this.account == null) {
this.account = Nebula.getAccountDatabase().getObjectByField(Account.class, "_id", this.getAccountUid());
}
return this.account;
}
public void addSession(GameSession session) {
synchronized (this.sessions) {
this.sessions.add(session);
}
}
public void removeSession(GameSession session) {
synchronized (this.sessions) {
this.sessions.remove(session);
}
}
public boolean hasSessions() {
synchronized (this.sessions) {
return !this.sessions.isEmpty();
}
}
public boolean getGender() {
return this.gender;
}
public boolean editName(String newName) {
// Sanity check
if (newName == null || newName.isEmpty() || newName.equals(this.getName())) {
return false;
}
// Limit name length
if (newName.length() > 20) {
newName = newName.substring(0, 19);
}
// Set name
this.name = newName;
// Update in database
Nebula.getGameDatabase().update(this, this.getUid(), "name", this.getName());
// Success
return true;
}
public void editGender() {
// Set name
this.gender = !this.gender;
// Update in database
Nebula.getGameDatabase().update(this, this.getUid(), "gender", this.getGender());
}
public void setNewbieInfo(int groupId, int stepId) {
// TODO
}
public int getMaxExp() {
var data = GameData.getWorldClassDataTable().get(this.level + 1);
return data != null ? data.getExp() : 0;
}
public PlayerChangeInfo addExp(int amount, PlayerChangeInfo changes) {
// Check if changes is null
if (changes == null) {
changes = new PlayerChangeInfo();
}
// Sanity
if (amount <= 0) {
return changes;
}
// Setup
int oldLevel = this.getLevel();
int oldExp = this.getExp();
int expRequired = this.getMaxExp();
// Add exp
this.exp += amount;
// Check for level ups
while (this.exp >= expRequired && expRequired > 0) {
this.level += 1;
this.exp -= expRequired;
expRequired = this.getMaxExp();
}
// Save to database
Nebula.getGameDatabase().update(
this,
this.getUid(),
"level",
this.getLevel(),
"exp",
this.getExp()
);
// Calculate changes
var proto = WorldClass.newInstance()
.setAddClass(this.getLevel() - oldLevel)
.setExpChange(this.getExp() - oldExp);
changes.add(proto);
return changes;
}
public void sendMessage(String string) {
// Empty
}
// Login
public void onLoad() {
// Load from database
this.getCharacters().loadFromDatabase();
this.getInventory().loadFromDatabase();
// Load referenced classes
this.formations = Nebula.getGameDatabase().getObjectByField(FormationManager.class, "_id", this.getUid());
if (this.formations == null) {
this.formations = new FormationManager(this);
} else {
this.formations.setPlayer(this);
}
this.mailbox = Nebula.getGameDatabase().getObjectByField(Mailbox.class, "_id", this.getUid());
if (this.mailbox == null) {
this.mailbox = new Mailbox(this);
}
this.starTowerManager = Nebula.getGameDatabase().getObjectByField(StarTowerManager.class, "_id", this.getUid());
if (this.starTowerManager == null) {
this.starTowerManager = new StarTowerManager(this);
} else {
this.starTowerManager.setPlayer(this);
}
}
// Proto
public PlayerInfo toProto() {
PlayerInfo proto = PlayerInfo.newInstance();
var acc = proto.getMutableAcc()
.setNickName(this.getName())
.setGender(this.getGender())
.setId(this.getUid())
.setHeadIcon(this.getHeadIcon())
.setSkinId(this.getSkinId())
.setTitlePrefix(this.getTitlePrefix())
.setTitleSuffix(this.getTitleSuffix())
.setCreateTime(this.getCreateTime());
proto.getMutableWorldClass()
.setStage(3)
.setCur(this.getLevel())
.setLastExp(this.getExp());
proto.getMutableEnergy()
.getMutableEnergy()
.setUpdateTime(Nebula.getCurrentTime())
.setNextDuration(60)
.setPrimary(240)
.setIsPrimary(true);
// Add characters/discs/res/items
for (var character : getCharacters().getCharacterCollection()) {
proto.addChars(character.toProto());
}
for (var disc : getCharacters().getDiscCollection()) {
proto.addDiscs(disc.toProto());
}
for (var item : getInventory().getItems().values()) {
proto.addItems(item.toProto());
}
for (var res : getInventory().getResources().values()) {
proto.addRes(res.toProto());
}
// Formations
for (var f : this.getFormations().getFormations().values()) {
proto.getMutableFormation().addInfo(f.toProto());
}
// Set state
var state = proto.getMutableState()
.setStorySet(true);
state.getMutableMail();
state.getMutableBattlePass();
state.getMutableWorldClassReward();
state.getMutableFriendEnergy();
state.getMutableMallPackage();
state.getMutableAchievement();
state.getMutableTravelerDuelQuest()
.setType(QuestType.TravelerDuel);
state.getMutableTravelerDuelChallengeQuest()
.setType(QuestType.TravelerDuelChallenge);
state.getMutableStarTower();
state.getMutableStarTowerBook();
state.getMutableScoreBoss();
state.getMutableCharAffinityRewards();
// Force complete tutorials
for (var guide : GameData.getGuideGroupDataTable()) {
var info = NewbieInfo.newInstance()
.setGroupId(guide.getId())
.setStepId(-1);
acc.addNewbies(info);
}
acc.addNewbies(NewbieInfo.newInstance().setGroupId(GameConstants.INTRO_GUIDE_ID).setStepId(-1));
//
proto.addBoard(410301);
proto.setServerTs(Nebula.getCurrentTime());
// Extra
proto.setAchievements(new byte[64]);
proto.getMutableVampireSurvivorRecord()
.getMutableSeason();
proto.getMutableQuests();
proto.getMutableAgent();
proto.getMutablePhone();
proto.getMutableStory();
return proto;
}
}
@@ -0,0 +1,49 @@
package emu.nebula.game.player;
import java.util.ArrayList;
import java.util.List;
import emu.nebula.GameConstants;
import emu.nebula.proto.AnyOuterClass.Any;
import emu.nebula.proto.Public.ChangeInfo;
import lombok.Getter;
import lombok.Setter;
import us.hebi.quickbuf.ProtoMessage;
@Getter
public class PlayerChangeInfo {
private boolean success;
private List<Any> list;
@Setter
private Object extraData;
public PlayerChangeInfo() {
this.list = new ArrayList<>();
}
public PlayerChangeInfo setSuccess(boolean success) {
this.success = success;
return this;
}
public void add(ProtoMessage<?> proto) {
var any = Any.newInstance()
.setTypeUrl(GameConstants.PROTO_BASE_TYPE_URL + proto.getClass().getSimpleName())
.setValue(proto.toByteArray());
this.list.add(any);
}
// Proto
public ChangeInfo toProto() {
var proto = ChangeInfo.newInstance();
for (var any : this.getList()) {
proto.addProps(any);
}
return proto;
}
}
@@ -0,0 +1,27 @@
package emu.nebula.game.player;
public abstract class PlayerManager {
private transient Player player;
public PlayerManager() {
}
public PlayerManager(Player player) {
this.player = player;
}
public Player getPlayer() {
return this.player;
}
public void setPlayer(Player player) {
if (this.player == null) {
this.player = player;
}
}
public int getPlayerUid() {
return this.getPlayer().getUid();
}
}
@@ -0,0 +1,116 @@
package emu.nebula.game.player;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import emu.nebula.Nebula;
import emu.nebula.game.GameContext;
import emu.nebula.game.GameContextModule;
import emu.nebula.game.account.Account;
import emu.nebula.net.GameSession;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
public class PlayerModule extends GameContextModule {
private final Int2ObjectMap<Player> cachedPlayers;
private final Object2ObjectMap<String, Player> cachedPlayersByAccount;
public PlayerModule(GameContext gameContext) {
super(gameContext);
this.cachedPlayers = new Int2ObjectOpenHashMap<>();
this.cachedPlayersByAccount = new Object2ObjectOpenHashMap<>();
}
public Int2ObjectMap<Player> getCachedPlayers() {
return cachedPlayers;
}
private void addToCache(Player player) {
this.cachedPlayers.put(player.getUid(), player);
this.cachedPlayersByAccount.put(player.getAccountUid(), player);
}
public void removeFromCache(Player player) {
this.cachedPlayers.remove(player.getUid());
this.cachedPlayersByAccount.remove(player.getAccountUid());
}
/**
* Returns a player object that has been previously cached. Returns null if the player isnt in the cache.
* @param uid User id of the player
* @return
*/
public synchronized Player getCachedPlayerByUid(int uid) {
return getCachedPlayers().get(uid);
}
/**
* Returns a player object with the given account. Returns null if the player doesnt exist.
* @param uid User id of the player
* @return
*/
public synchronized Player getPlayerByAccount(Account account) {
// Get player from cache
Player player = this.cachedPlayersByAccount.get(account.getUid());
if (player == null) {
// Retrieve player object from database if its not there
player = Nebula.getGameDatabase().getObjectByField(Player.class, "accountUid", account.getUid());
if (player != null) {
// Load player
player.onLoad();
// Put in cache
this.addToCache(player);
}
}
return player;
}
/**
* Creates a player with the specified user id.
* @param userId
* @return
*/
public synchronized Player createPlayer(GameSession session, String name, boolean gender) {
// Make sure player doesnt already exist
if (Nebula.getGameDatabase().checkIfObjectExists(Player.class, "accountUid", session.getAccount().getUid())) {
return null;
}
// Limit name length
if (name.length() > 20) {
name = name.substring(0, 19);
}
// Create player and save to db
var player = new Player(session.getAccount(), name, gender);
player.onLoad();
player.save();
// Put in cache
this.addToCache(player);
// Set player for session
session.setPlayer(player);
return player;
}
/**
* Returns a list of recent players that have logged on (for followers)
* @param player Player that requested this
*/
public synchronized List<Player> getRandomPlayerList(Player player) {
List<Player> list = getCachedPlayers().values().stream().filter(p -> p != player).collect(Collectors.toList());
Collections.shuffle(list);
return list.stream().limit(15).toList();
}
}
@@ -0,0 +1,12 @@
package emu.nebula.game.story;
import dev.morphia.annotations.Entity;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.player.PlayerManager;
import lombok.Getter;
@Getter
@Entity(value = "story", useDiscriminator = false)
public class StoryManager extends PlayerManager implements GameDatabaseObject {
}
@@ -0,0 +1,39 @@
package emu.nebula.game.tower;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
public enum CaseType {
Battle (1),
OpenDoor (2),
PotentialSelect (3),
FateCardSelect (4),
NoteSelect (5),
NpcEvent (6),
SelectSpecialPotential (7),
RecoveryHP (8),
NpcRecoveryHP (9),
Hawker (10),
StrengthenMachine (11),
DoorDanger (12),
SyncHP (13);
@Getter
private final int value;
private final static Int2ObjectMap<CaseType> map = new Int2ObjectOpenHashMap<>();
static {
for (CaseType type : CaseType.values()) {
map.put(type.getValue(), type);
}
}
private CaseType(int value) {
this.value = value;
}
public static CaseType getByValue(int value) {
return map.get(value);
}
}
@@ -0,0 +1,55 @@
package emu.nebula.game.tower;
import emu.nebula.proto.PublicStarTower.StarTowerRoomCase;
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 floorId;
// Select
private int[] ids;
public StarTowerCase(CaseType type) {
this.type = type;
}
public StarTowerRoomCase toProto() {
var proto = StarTowerRoomCase.newInstance()
.setId(this.getId());
switch (this.type) {
case Battle -> {
proto.getMutableBattleCase();
}
case OpenDoor -> {
proto.getMutableDoorCase();
}
case SyncHP -> {
proto.getMutableSyncHPCase();
}
case SelectSpecialPotential -> {
proto.getMutableSelectSpecialPotentialCase();
}
case PotentialSelect -> {
proto.getMutableSelectPotentialCase();
}
default -> {
}
}
return proto;
}
}
@@ -0,0 +1,256 @@
package emu.nebula.game.tower;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import dev.morphia.annotations.Entity;
import emu.nebula.data.resources.StarTowerDef;
import emu.nebula.game.formation.Formation;
import emu.nebula.game.player.Player;
import emu.nebula.proto.PublicStarTower.StarTowerChar;
import emu.nebula.proto.PublicStarTower.StarTowerDisc;
import emu.nebula.proto.PublicStarTower.StarTowerInfo;
import emu.nebula.proto.PublicStarTower.StarTowerRoomCase;
import emu.nebula.proto.StarTowerApply.StarTowerApplyReq;
import emu.nebula.proto.StarTowerInteract.StarTowerInteractReq;
import emu.nebula.proto.StarTowerInteract.StarTowerInteractResp;
import emu.nebula.util.Snowflake;
import emu.nebula.util.Utils;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import lombok.Getter;
import lombok.SneakyThrows;
@Getter
@Entity(useDiscriminator = false)
public class StarTowerInstance {
private transient StarTowerManager manager;
private transient StarTowerDef data;
// Tower id
private int id;
// Room
private int floor;
private int mapId;
private int mapTableId;
private String mapParam;
private int paramId;
// Team
private int formationId;
private int buildId;
private int teamLevel;
private int teamExp;
private int charHp;
private int battleTime;
private List<StarTowerChar> chars;
private List<StarTowerDisc> discs;
private int lastCaseId = 0;
private List<StarTowerCase> cases;
private Int2IntMap items;
@Deprecated // Morphia only
public StarTowerInstance() {
}
public StarTowerInstance(StarTowerManager manager, StarTowerDef data, Formation formation, StarTowerApplyReq req) {
this.manager = manager;
this.data = data;
this.id = req.getId();
this.mapId = req.getMapId();
this.mapTableId = req.getMapTableId();
this.mapParam = req.getMapParam();
this.paramId = req.getParamId();
this.formationId = req.getFormationId();
this.buildId = Snowflake.newUid();
this.teamLevel = 1;
this.floor = 1;
this.charHp = -1;
this.chars = new ArrayList<>();
this.discs = new ArrayList<>();
this.cases = new ArrayList<>();
this.items = new Int2IntOpenHashMap();
// Init formation
for (int i = 0; i < 3; i++) {
int id = formation.getCharIdAt(i);
var character = getPlayer().getCharacters().getCharacterById(id);
if (character != null) {
chars.add(character.toStarTowerProto());
} else {
chars.add(StarTowerChar.newInstance());
}
}
for (int i = 0; i < 6; i++) {
int id = formation.getDiscIdAt(i);
var disc = getPlayer().getCharacters().getDiscById(id);
if (disc != null) {
discs.add(disc.toStarTowerProto());
} else {
discs.add(StarTowerDisc.newInstance());
}
}
// Add cases
this.addCase(new StarTowerCase(CaseType.Battle));
this.addCase(new StarTowerCase(CaseType.SyncHP));
var doorCase = this.addCase(new StarTowerCase(CaseType.OpenDoor));
doorCase.setFloorId(this.getFloor() + 1);
}
public Player getPlayer() {
return this.manager.getPlayer();
}
public StarTowerCase addCase(StarTowerCase towerCase) {
return this.addCase(null, towerCase);
}
public StarTowerCase addCase(StarTowerInteractResp rsp, StarTowerCase towerCase) {
// Add to cases list
this.cases.add(towerCase);
// Increment id
towerCase.setId(++this.lastCaseId);
// Set proto
if (rsp != null) {
rsp.getMutableCases().add(towerCase.toProto());
}
return towerCase;
}
public StarTowerInteractResp handleInteract(StarTowerInteractReq req) {
var rsp = StarTowerInteractResp.newInstance()
.setId(req.getId());
if (req.hasBattleEndReq()) {
this.onBattleEnd(req, rsp);
} else if (req.hasRecoveryHPReq()) {
var proto = req.getRecoveryHPReq();
} else if (req.hasSelectReq()) {
} else if (req.hasEnterReq()) {
this.onEnterReq(req, rsp);
}
// Set data protos
rsp.getMutableData();
rsp.getMutableChange();
//rsp.getMutableNextPackage();
return rsp;
}
// Interact events
@SneakyThrows
public void onBattleEnd(StarTowerInteractReq req, StarTowerInteractResp rsp) {
var proto = req.getBattleEndReq();
if (proto.hasVictory()) {
// Add team level
this.teamLevel++;
// Add clear time
this.battleTime += proto.getVictory().getTime();
// Handle victory
rsp.getMutableBattleEndResp()
.getMutableVictory()
.setLv(this.getTeamLevel())
.setBattleTime(this.getBattleTime());
// Add potential selector TODO
} else {
// Handle defeat
}
}
public void onSelect(StarTowerInteractReq req, StarTowerInteractResp rsp) {
}
public void onEnterReq(StarTowerInteractReq req, StarTowerInteractResp rsp) {
var proto = req.getEnterReq();
// Set
this.floor = this.floor++;
this.mapId = proto.getMapId();
this.mapTableId = proto.getMapTableId();
// Clear cases TODO
this.lastCaseId = 0;
this.cases.clear();
// Add cases
var syncHpCase = this.addCase(new StarTowerCase(CaseType.SyncHP));
var doorCase = this.addCase(new StarTowerCase(CaseType.OpenDoor));
doorCase.setFloorId(this.getFloor() + 1);
// Proto
var room = rsp.getMutableEnterResp().getMutableRoom();
room.getMutableData()
.setMapId(this.getMapId())
.setMapTableId(this.getMapTableId())
.setFloor(this.getFloor());
room.addAllCases(syncHpCase.toProto(), doorCase.toProto());
}
public void onRecoveryHP(StarTowerInteractReq req, StarTowerInteractResp rsp) {
// Add case
this.addCase(rsp, new StarTowerCase(CaseType.RecoveryHP));
}
// Proto
public StarTowerInfo toProto() {
var proto = StarTowerInfo.newInstance();
proto.getMutableMeta()
.setId(this.getId())
.setCharHp(this.getCharHp())
.setTeamLevel(this.getTeamLevel())
.setNPCInteractions(1)
.setBuildId(this.getBuildId());
this.getChars().forEach(proto.getMutableMeta()::addChars);
this.getDiscs().forEach(proto.getMutableMeta()::addDiscs);
proto.getMutableRoom().getMutableData()
.setFloor(this.getFloor())
.setMapId(this.getMapId())
.setMapTableId(this.getMapTableId())
.setMapParam(this.getMapParam())
.setParamId(this.getParamId());
// Cases
for (var starTowerCase : this.getCases()) {
proto.getMutableRoom().addCases(starTowerCase.toProto());
}
// TODO
proto.getMutableBag();
return proto;
}
}
@@ -0,0 +1,51 @@
package emu.nebula.game.tower;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import emu.nebula.data.GameData;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerManager;
import emu.nebula.proto.StarTowerApply.StarTowerApplyReq;
import lombok.Getter;
@Getter
@Entity(value = "star_tower", useDiscriminator = false)
public class StarTowerManager extends PlayerManager implements GameDatabaseObject {
@Id
private int uid;
private transient StarTowerInstance instance;
@Deprecated // Morphia only
public StarTowerManager() {
}
public StarTowerManager(Player player) {
super(player);
this.uid = player.getUid();
this.save();
}
public StarTowerInstance apply(StarTowerApplyReq req) {
// Sanity checks
var data = GameData.getStarTowerDataTable().get(req.getId());
if (data == null) {
return null;
}
// Get formation
var formation = getPlayer().getFormations().getFormationById(req.getFormationId());
if (formation == null) {
return null;
}
// Create instance
this.instance = new StarTowerInstance(this, data, formation, req);
// Success
return this.instance;
}
}
@@ -0,0 +1,128 @@
package emu.nebula.net;
import java.security.MessageDigest;
import java.util.Base64;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import emu.nebula.Nebula;
import emu.nebula.game.GameContext;
import emu.nebula.game.account.Account;
import emu.nebula.game.account.AccountHelper;
import emu.nebula.game.player.Player;
import emu.nebula.util.AeadHelper;
import lombok.Getter;
import us.hebi.quickbuf.RepeatedByte;
@Getter
public class GameSession {
private String token;
private Account account;
private Player player;
// Crypto
private byte[] clientPublicKey;
private byte[] serverPublicKey;
private byte[] serverPrivateKey;
private byte[] key;
//
private long lastActiveTime;
public GameSession() {
this.updateLastActiveTime();
}
public void setPlayer(Player player) {
this.player = player;
this.player.addSession(this);
}
public void clearPlayer(GameContext context) {
// Sanity check
if (this.player == null) {
return;
}
// Clear player
var player = this.player;
this.player = null;
// Remove session from player
player.removeSession(this);
// Clean up from player module
if (!player.hasSessions()) {
context.getPlayerModule().removeFromCache(player);
}
}
public boolean hasPlayer() {
return this.player != null;
}
public void setClientKey(RepeatedByte key) {
this.clientPublicKey = key.toArray();
}
public void generateServerKey() {
var pair = AeadHelper.generateECDHKEyPair();
this.serverPrivateKey = ((ECPrivateKeyParameters) pair.getPrivate()).getD().toByteArray();
this.serverPublicKey = ((ECPublicKeyParameters) pair.getPublic()).getQ().getEncoded(false);
}
public void calculateKey() {
this.key = AeadHelper.generateKey(clientPublicKey, serverPublicKey, serverPrivateKey);
}
public String generateToken() {
String temp = System.currentTimeMillis() + ":" + AeadHelper.generateBytes(64).toString();
try {
MessageDigest md = MessageDigest.getInstance("SHA-512");
byte[] bytes = md.digest(temp.getBytes());
this.token = Base64.getEncoder().encodeToString(bytes);
} catch (Exception e) {
this.token = Base64.getEncoder().encodeToString(temp.getBytes());
}
return this.token;
}
public boolean login(String loginToken) {
// Sanity check
if (this.account != null) {
return false;
}
// Get account
this.account = AccountHelper.getAccountByLoginToken(loginToken);
if (account == null) {
return false;
}
// Note: We should cache players in case multiple sessions try to login to the same player at the time
// Get player by account
var player = Nebula.getGameContext().getPlayerModule().getPlayerByAccount(account);
// Skip intro
if (player == null && Nebula.getConfig().getServerOptions().skipIntro) {
player = Nebula.getGameContext().getPlayerModule().createPlayer(this, "Test", false);
}
// Set player
if (player != null) {
this.setPlayer(player);
}
return true;
}
public void updateLastActiveTime() {
this.lastActiveTime = System.currentTimeMillis();
}
}
@@ -0,0 +1,10 @@
package emu.nebula.net;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
@Retention(RUNTIME)
public @interface HandlerId {
public int value();
}
@@ -0,0 +1,67 @@
package emu.nebula.net;
import lombok.SneakyThrows;
import us.hebi.quickbuf.ProtoMessage;
import us.hebi.quickbuf.ProtoSink;
public abstract class NetHandler {
public boolean requireSession() {
return true;
}
public boolean requirePlayer() {
return true;
}
public byte[] encodeMsg(int msgId, byte[] packet) {
// Create data array
byte[] data = new byte[packet.length + 2];
// Encode msgId
short id = (short) msgId;
data[0] = (byte) (id >> 8);
data[1] = (byte) id;
// Copy packet to data array
System.arraycopy(packet, 0, data, 2, packet.length);
// Complete
return data;
}
@SneakyThrows
public byte[] encodeMsg(int msgId, ProtoMessage<?> proto) {
// Create data array
byte[] data = new byte[proto.getCachedSize() + 2];
// Encode msgId
short id = (short) msgId;
data[0] = (byte) (id >> 8);
data[1] = (byte) id;
// Create proto sink
var output = ProtoSink.newInstance(data, 2, proto.getCachedSize());
// Copy packet to data array
proto.writeTo(output);
// Complete
return data;
}
public byte[] encodeMsg(int msgId) {
// Create data array
byte[] data = new byte[2];
// Encode msgId
short id = (short) msgId;
data[0] = (byte) (id >> 8);
data[1] = (byte) id;
return data;
}
public abstract byte[] handle(GameSession session, byte[] message) throws Exception;
}
+780
View File
@@ -0,0 +1,780 @@
package emu.nebula.net;
public class NetMsgId {
public static final int none = 0;
public static final int clear_all_activity_levels_notify = -10046; // 一键通关当前所有活动关卡
public static final int clear_all_activity_avg_notify = -10045; // 一键通关当前所有活动剧情
public static final int vs_add_fate_card_notify = -10044; // 返回添加命运卡达到要求额外赠送卡片信息
public static final int char_gems_import_notify = -10043; // 导入角色宝石(纹章)槽位和预设数据
public static final int char_gems_export_notify = -10042; // 返回角色宝石(纹章)的槽位和预设数据
public static final int clear_all_char_gem_instance_notify = -10040; // 一键通关角色宝石(纹章)副本
public static final int change_npc_affinity_notify = -10039; // 修改NPC好感度
public static final int tower_change_sub_note_skill_notify = -10038; // 修改星塔属性音符数量
public static final int world_class_quest_complete_notify = -10037; // 当前世界等级阶段的任务
public static final int score_boss_level_reset_notify = -10036; // 积分boss关卡重置通知
public static final int score_boss_star_reward_reset_notify = -10035; // 积分boss星级奖励重置通知
public static final int char_up_change_notify = -10034; // 角色数据变更
public static final int tower_growth_node_change_notify = -10033; // 星塔养成节点变化通知
public static final int st_harmony_skill_notify = -10032; // 共鸣技能新增
public static final int clear_all_story_notify = -10031; // 一键通关故事并获得所有证据
public static final int story_complete_notify = -10030; // 通关指定的故事并获取证据
public static final int infinity_tower_skip_floor_notify = -10029; // 无尽之塔跳层
public static final int infinity_tower_all_infos_notify = -10028; // 全通所有无尽之塔
public static final int clear_all_equipment_instance_notify = -10027; // 全通所有装备副本
public static final int st_import_build_notify = -10026; // 返回星塔build导入的build信息
public static final int st_export_build_notify = -10025; // 返回星塔build的json序列化字符串
public static final int st_add_new_case_notify = -10024; // 返回操作添加的新case
public static final int st_add_team_exp_notify = -10023; // 队伍等级经验最终值及当前未处理的所有case
public static final int st_skip_floor_notify = -10022; // 星塔跳层
public static final int st_clear_all_star_tower_notify = -10021; // 全通所有星塔(返回通关星塔ID列表)
public static final int st_items_change_notify = -10020; // 星塔道具变化通知
public static final int char_affinity_final_notify = -10019; // 添加角色好感度
public static final int clear_all_skill_instance_notify = -10018; // 全通所有技能素材本
public static final int disc_reset_notify = -10017; // 星盘信息通知
public static final int clear_all_traveler_due_notify = -10016; // 全通所有旅人对决
public static final int clear_all_region_boss_level_notify = -10015; // 全通所有强敌讨伐
public static final int clear_all_week_boss_notify = -10014; // 一键通关所有周长Boss本
public static final int clear_all_daily_instance_notify = -10013; // 一键通关所有日常副本
public static final int refresh_agent_notify = -10012; // 一键刷新正在进行中的委托
public static final int star_tower_sub_note_skill_info_notify = -10011; // 星塔属性音符技能随机分布数据
public static final int vampire_survivor_talent_node_notify = -10010; // 修改后的天赋节点最终信息值
public static final int add_vampire_season_score_notify = -10009; // 增加吸血鬼模式副本赛季积分 返回当前总分
public static final int clear_all_vampire_survivor_notify = -10008; // 一键通关所有吸血鬼模式副本
public static final int region_boss_level_final_notify = -10007; // 地区boss关卡最终数据通知
public static final int chars_final_notify = -10006; // 角色列表最终数据通知
public static final int world_class_number_notify = -10005; // 世界等级最终值通知
public static final int char_change_notify = -10004; // 角色数据变化通知
public static final int char_reset_notify = -10003; // 角色信息通知
public static final int items_change_notify = -10001; // 道具变化通知
public static final int sudo_failed_ack = -3; // 请求失败
public static final int sudo_succeed_ack = -2; // 成功,响应会走notify附加数据流程
public static final int sudo_req = -1; // 客户端内置GM命令请求
public static final int ike_req = 1; // internet key exchange
public static final int ike_succeed_ack = 2; // 成功,返回服务器方的秘钥,之后每次请求都需要将Token写入header X-Token段,服务器以此识别用户
public static final int ike_failed_ack = 3; // 失败
public static final int player_login_req = 4; // 登录
public static final int player_login_succeed_ack = 5; // 成功,将返回的新token,替换之前ike阶段的token,放置于header X-Token段,服务器以此识别用户
public static final int player_login_failed_ack = 6; // 失败
public static final int player_data_req = 1001; // 获取用户全量数据
public static final int player_data_succeed_ack = 1002; // 成功,如果是新账号,会返回player_new_notify协议
public static final int player_data_failed_ack = 1003; // 请求失败
public static final int player_reg_req = 1004; // 注册角色
public static final int player_reg_failed_ack = 1005; // 注册失败 返回错误,反之,成功会调用player_data_succeed_ack
public static final int player_name_edit_req = 1006; // 请求修改昵称
public static final int player_name_edit_succeed_ack = 1007; // 请求修改昵称成功
public static final int player_name_edit_failed_ack = 1008; // 请求修改昵称失败
public static final int player_head_icon_set_req = 1009; // 请求设置头像
public static final int player_head_icon_set_succeed_ack = 1010; // 设置头像成功
public static final int player_head_icon_set_failed_ack = 1011; // 设置头像失败
public static final int player_ping_req = 1012; // 心跳
public static final int player_ping_succeed_ack = 1013; // 心跳回馈
public static final int player_ping_failed_ack = 1014; // 不会返回失败
public static final int player_learn_req = 1015; // 新手教学@提交教学信息
public static final int player_learn_succeed_ack = 1016; // 教学步骤记录成功
public static final int player_learn_failed_ack = 1017; // 教学记录失败
public static final int player_destroy_req = 1018; // 获取注销账号数据NotifyUrl
public static final int player_destroy_succeed_ack = 1019; // 生成注销回调地址以提交到sdk server
public static final int player_destroy_failed_ack = 1020; // 获取失败
public static final int player_board_set_req = 1021; // 请求设置看板
public static final int player_board_set_succeed_ack = 1022; // 设置看板成功
public static final int player_board_set_failed_ack = 1023; // 设置看板失败
public static final int player_world_class_reward_receive_req = 1024; // 请求领取世界等级奖励
public static final int player_world_class_reward_receive_succeed_ack = 1025; // 请求领取世界等级奖励成功
public static final int player_world_class_reward_receive_failed_ack = 1026; // 请求领取世界等级奖励失败
public static final int player_signature_edit_req = 1027; // 请求修改签名
public static final int player_signature_edit_succeed_ack = 1028; // 请求修改签名成功
public static final int player_signature_edit_failed_ack = 1029; // 请求修改签名失败
public static final int player_title_edit_req = 1030; // 请求修改头衔
public static final int player_title_edit_succeed_ack = 1031; // 请求修改头衔成功
public static final int player_title_edit_failed_ack = 1032; // 请求修改头衔失败
public static final int player_chars_show_req = 1033; // 请求展示角色
public static final int player_chars_show_succeed_ack = 1034; // 请求展示角色成功
public static final int player_chars_show_failed_ack = 1035; // 请求展示角色失败
public static final int player_skin_show_req = 1036; // 请求展示皮肤
public static final int player_skin_show_succeed_ack = 1037; // 请求展示皮肤成功
public static final int player_skin_show_failed_ack = 1038; // 请求展示皮肤失败
public static final int player_gender_edit_req = 1039; // 请求切换性别
public static final int player_gender_edit_succeed_ack = 1040; // 切换性别成功
public static final int player_gender_edit_failed_ack = 1041; // 切换性别失败
public static final int player_survey_req = 1042; // 申请发起调查问卷@填写问卷ID
public static final int player_survey_succeed_ack = 1043; // 返回第三方问卷ID和回调通知地址
public static final int player_survey_failed_ack = 1044; // 申请失败,比如过期,已经完成过
public static final int player_exit_req = 1045; // 退出游戏
public static final int player_exit_succeed_ack = 1046; // 退出成功
public static final int player_exit_failed_ack = 1047; // 退出失败
public static final int player_honor_edit_req = 1048; // 荣誉称号最新的列表
public static final int player_honor_edit_succeed_ack = 1049; // 修改成功
public static final int player_honor_edit_failed_ack = 1050; // 修改失败
public static final int player_world_class_advance_req = 1051; // 请求领取世界等级阶段奖励
public static final int player_world_class_advance_succeed_ack = 1052; // 请求领取世界等级奖励成功(等级和经验变化由changeInfo携带)
public static final int player_world_class_advance_failed_ack = 1053; // 请求领取世界
public static final int player_music_set_req = 1054; // 设置首页音乐(星盘ID)
public static final int player_music_set_succeed_ack = 1055; // 设置音乐成功
public static final int player_music_set_failed_ack = 1056; // 设置音乐失败
public static final int player_head_icon_info_req = 1057; // 获取所有头像列表
public static final int player_head_icon_info_succeed_ack = 1058; // 获取头像成功
public static final int player_head_icon_info_failed_ack = 1059; // 获取头像失败
public static final int item_use_req = 1101; // 道具使用
public static final int item_use_succeed_ack = 1102; // 道具使用成功,返回ChangeInfo
public static final int item_use_failed_ack = 1103; // 道具使用失败,返回错误信息
public static final int gem_convert_req = 1104; // 砖石转换@传入钻石数量,兑换心相碎片,默认规则(免费钻不够使用付费钻)
public static final int gem_convert_succeed_ack = 1105; // 兑换成功
public static final int gem_convert_failed_ack = 1106; // 兑换失败
public static final int item_product_req = 1107; // 材料合成
public static final int item_product_succeed_ack = 1108; // 材料合成成功
public static final int item_product_failed_ack = 1109; // 材料合成失败
public static final int fragments_convert_req = 1110; // 所有角色溢出碎片兑换
public static final int fragments_convert_succeed_ack = 1111; // 兑换成功
public static final int fragments_convert_failed_ack = 1112; // 兑换失败
public static final int daily_shop_reward_receive_req = 1113; // 领取商店每日免费赠礼
public static final int daily_shop_reward_receive_succeed_ack = 1114; // 领取成功
public static final int daily_shop_reward_receive_failed_ack = 1115; // 领取失败
public static final int item_quick_growth_req = 1116; // 道具快速养成
public static final int item_quick_growth_succeed_ack = 1117; // 养成成功
public static final int item_quick_growth_failed_ack = 1118; // 养成失败
public static final int friend_list_get_req = 1201; // 请求获取好友/好友申请列表
public static final int friend_list_get_succeed_ack = 1202; // 获取好友/好友申请列表成功
public static final int friend_list_get_failed_ack = 1203; // 获取好友/好友申请列表失败
public static final int friend_uid_search_req = 1204; // 请求通过UId搜索用户信息
public static final int friend_uid_search_succeed_ack = 1205; // 通过UId搜索用户信息成功
public static final int friend_uid_search_failed_ack = 1206; // 通过UId搜索用户信息失败
public static final int friend_name_search_req = 1207; // 搜索用户信息@传入用户昵称
public static final int friend_name_search_succeed_ack = 1208; // 通过用户昵称搜索用户信息成功
public static final int friend_name_search_failed_ack = 1209; // 通过用户昵称搜索用户信息失败
public static final int friend_add_req = 1210; // 请求申请添加好友
public static final int friend_add_succeed_ack = 1211; // 申请添加好友成功
public static final int friend_add_failed_ack = 1212; // 申请添加好友失败
public static final int friend_add_agree_req = 1213; // 同意添加好友请求
public static final int friend_add_agree_succeed_ack = 1214; // 同意添加好友成功
public static final int friend_add_agree_failed_ack = 1215; // 同意添加好友失败
public static final int friend_all_agree_req = 1216; // 请求一键添加好友
public static final int friend_all_agree_succeed_ack = 1217; // 一键添加好友成功
public static final int friend_all_agree_failed_ack = 1218; // 一键添加好友失败
public static final int friend_delete_req = 1219; // 请求删除好友
public static final int friend_delete_succeed_ack = 1220; // 删除好友成功
public static final int friend_delete_failed_ack = 1221; // 删除好友失败
public static final int friend_invites_delete_req = 1222; // 请求删除好友申请
public static final int friend_invites_delete_succeed_ack = 1223; // 删除好友申请成功
public static final int friend_invites_delete_failed_ack = 1224; // 删除好友申请失败
public static final int friend_star_set_req = 1225; // 请求设置星级好友
public static final int friend_star_set_succeed_ack = 1226; // 请求设置星级好友成功
public static final int friend_star_set_failed_ack = 1227; // 请求设置星级好友失败
public static final int friend_receive_energy_req = 1228; // 请求领取好友赠送体力
public static final int friend_receive_energy_succeed_ack = 1229; // 请求领取好友赠送体力成功
public static final int friend_receive_energy_failed_ack = 1230; // 请求领取好友赠送体力失败
public static final int friend_send_energy_req = 1231; // 请求赠送好友体力
public static final int friend_send_energy_succeed_ack = 1232; // 请求赠送好友体力成功
public static final int friend_send_energy_failed_ack = 1233; // 请求赠送好友体力失败
public static final int friend_recommendation_get_req = 1234; // 请求好友推荐列表
public static final int friend_recommendation_get_succeed_ack = 1235; // 请求好友推荐列表成功
public static final int friend_recommendation_get_failed_ack = 1236; // 请求好友推荐列表失败
public static final int tower_growth_detail_req = 1301; // 获取星塔养成详细信息
public static final int tower_growth_detail_succeed_ack = 1302; // 获取成功
public static final int tower_growth_detail_failed_ack = 1303; // 获取信息失败,返回错误
public static final int tower_growth_node_unlock_req = 1304; // 星塔天赋解锁@传入节点ID,解锁对应的养成节点
public static final int tower_growth_node_unlock_succeed_ack = 1305; // 解锁成功
public static final int tower_growth_node_unlock_failed_ack = 1306; // 解锁失败,返回错误信息
public static final int tower_growth_group_node_unlock_req = 1307; // 星塔天赋组解锁@传入节点组ID,根据材料解锁所有能解锁的节点
public static final int tower_growth_group_node_unlock_succeed_ack = 1308; // 解锁成功
public static final int tower_growth_group_node_unlock_failed_ack = 1309; // 解锁失败,返回错误信息
public static final int player_formation_req = 2001; // 设置编队
public static final int player_formation_succeed_ack = 2002; // 设置编队成功
public static final int player_formation_failed_ack = 2003; // 设置编队失败
public static final int char_upgrade_req = 2301; // 角色升级
public static final int char_upgrade_succeed_ack = 2302; // 角色升级成功
public static final int char_upgrade_failed_ack = 2303; // 角色升级失败
public static final int char_advance_req = 2304; // 角色进阶@传入角色ID
public static final int char_advance_succeed_ack = 2305; // 进阶成功
public static final int char_advance_failed_ack = 2306; // 进阶失败
public static final int char_skill_upgrade_req = 2307; // 角色技能升级
public static final int char_skill_upgrade_succeed_ack = 2308; // 升级成功
public static final int char_skill_upgrade_failed_ack = 2309; // 升级失败
public static final int char_advance_reward_receive_req = 2313; // 请求领取角色进阶奖励
public static final int char_advance_reward_receive_succeed_ack = 2314; // 请求领取角色进阶奖励成功
public static final int char_advance_reward_receive_failed_ack = 2315; // 请求领取角色进阶奖励失败
public static final int char_skin_set_req = 2316; // 设置角色皮肤
public static final int char_skin_set_succeed_ack = 2317; // 设置成功
public static final int char_skin_set_failed_ack = 2318; // 设置失败
public static final int char_affinity_quest_reward_receive_req = 2322; // 请求领取角色好感度任务奖励
public static final int char_affinity_quest_reward_receive_succeed_ack = 2323; // 请求领取角色好感度任务成功
public static final int char_affinity_quest_reward_receive_failed_ack = 2324; // 请求领取角色好感度任务失败
public static final int char_recruitment_req = 2325; // 招募角色@传入角色ID
public static final int char_recruitment_succeed_ack = 2326; // 招募成功
public static final int char_recruitment_failed_ack = 2327; // 招募失败
public static final int char_affinity_gift_send_req = 2328; // 请求赠送好感度礼物
public static final int char_affinity_gift_send_succeed_ack = 2329; // 请求赠送好感度礼物成功
public static final int char_affinity_gift_send_failed_ack = 2330; // 请求赠送好感度礼物失败
public static final int char_dating_landmark_select_req = 2401; // 选择地点邀约角色
public static final int char_dating_landmark_select_succeed_ack = 2402; // 选择地点邀约角色成功
public static final int char_dating_landmark_select_failed_ack = 2403; // 选择地点邀约角色失败
public static final int char_dating_gift_send_req = 2404; // 请求邀约赠礼
public static final int char_dating_gift_send_succeed_ack = 2405; // 请求邀约赠礼成功
public static final int char_dating_gift_send_failed_ack = 2406; // 请求邀约赠礼失败
public static final int char_dating_event_reward_receive_req = 2407; // 请求领取特殊事件奖励
public static final int char_dating_event_reward_receive_succeed_ack = 2408; // 请求领取特殊事件奖励成功
public static final int char_dating_event_reward_receive_failed_ack = 2409; // 请求领取特殊事件奖励失败
public static final int char_archive_reward_receive_req = 2410; // 请求领取角色档案奖励
public static final int char_archive_reward_receive_succeed_ack = 2411; // 请求领取角色档案奖励成功
public static final int char_archive_reward_receive_failed_ack = 2412; // 请求领取角色档案奖励失败
public static final int char_dating_branch_a_select_req = 2413; // 选择分支A选项
public static final int char_dating_branch_a_select_succeed_ack = 2414; // 选择分支A选项成功
public static final int char_dating_branch_a_select_failed_ack = 2415; // 选择分支A选项失败
public static final int char_dating_branch_b_select_req = 2416; // 选择分支B选项
public static final int char_dating_branch_b_select_succeed_ack = 2417; // 选择分支B选项成功
public static final int char_dating_branch_b_select_failed_ack = 2418; // 选择分支B选项失败
public static final int char_gem_generate_req = 2501; // 角色宝石生成
public static final int char_gem_generate_succeed_ack = 2502; // 生成成功
public static final int char_gem_generate_failed_ack = 2503; // 生成失败
public static final int char_gem_refresh_req = 2504; // 角色宝石刷新
public static final int char_gem_refresh_succeed_ack = 2505; // 刷新成功
public static final int char_gem_refresh_failed_ack = 2506; // 刷新失败
public static final int char_gem_replace_attribute_req = 2507; // 角色宝石属性替换
public static final int char_gem_replace_attribute_succeed_ack = 2508; // 替换成功
public static final int char_gem_replace_attribute_failed_ack = 2509; // 替换失败
public static final int char_gem_update_gem_lock_status_req = 2510; // 更新角色宝石锁定状态
public static final int char_gem_update_gem_lock_status_succeed_ack = 2511; // 更新成功
public static final int char_gem_update_gem_lock_status_failed_ack = 2512; // 更新失败
public static final int char_gem_use_preset_req = 2513; // 角色使用预设
public static final int char_gem_use_preset_succeed_ack = 2514; // 使用成功
public static final int char_gem_use_preset_failed_ack = 2515; // 使用失败
public static final int char_gem_rename_preset_req = 2516; // 角色预设重命名
public static final int char_gem_rename_preset_succeed_ack = 2517; // 重命名成功
public static final int char_gem_rename_preset_failed_ack = 2518; // 重命名失败
public static final int char_gem_equip_gem_req = 2519; // 角色宝石装备宝石
public static final int char_gem_equip_gem_succeed_ack = 2520; // 装备成功
public static final int char_gem_equip_gem_failed_ack = 2521; // 装备失败
public static final int disc_strengthen_req = 3119; // 星盘强化
public static final int disc_strengthen_succeed_ack = 3120; // 星盘强化成功
public static final int disc_strengthen_failed_ack = 3121; // 星盘强化失败
public static final int disc_promote_req = 3122; // 星盘升阶
public static final int disc_promote_succeed_ack = 3123; // 星盘升阶成功
public static final int disc_promote_failed_ack = 3124; // 星盘升阶失败
public static final int disc_limit_break_req = 3125; // 星盘突破
public static final int disc_limit_break_succeed_ack = 3126; // 星盘突破成功
public static final int disc_limit_break_failed_ack = 3127; // 星盘突破失败
public static final int disc_read_reward_receive_req = 3128; // 请求领取星盘阅读奖励
public static final int disc_read_reward_receive_succeed_ack = 3129; // 请求领取星盘阅读奖励成功
public static final int disc_read_reward_receive_failed_ack = 3130; // 请求领取星盘阅读奖励失败
public static final int agent_apply_req = 3301; // 请求派遣委托
public static final int agent_apply_succeed_ack = 3302; // 请求派遣委托成功
public static final int agent_apply_failed_ack = 3303; // 请求派遣委托失败
public static final int agent_give_up_req = 3304; // 请求放弃派遣委托
public static final int agent_give_up_succeed_ack = 3305; // 请求放弃派遣委托成功
public static final int agent_give_up_failed_ack = 3306; // 请求放弃派遣委托失败
public static final int agent_reward_receive_req = 3307; // 请求领取派遣委托奖励
public static final int agent_reward_receive_succeed_ack = 3308; // 请求领取派遣委托奖励成功
public static final int agent_reward_receive_failed_ack = 3309; // 请求领取派遣委托奖励失败
public static final int quest_tour_guide_reward_receive_req = 4201; // 领取手册任务奖励@value表示任务ID,0表示一键领取
public static final int quest_tour_guide_reward_receive_succeed_ack = 4202; // 获取成功
public static final int quest_tour_guide_reward_receive_failed_ack = 4203; // 获取失败
public static final int quest_daily_reward_receive_req = 4204; // 领取日常任务奖励@value表示任务ID,0表示一键领取
public static final int quest_daily_reward_receive_succeed_ack = 4205; // 获取成功
public static final int quest_daily_reward_receive_failed_ack = 4206; // 获取失败
public static final int dictionary_reward_receive_req = 4207; // 领取词条奖励
public static final int dictionary_reward_receive_succeed_ack = 4208; // 获取成功
public static final int dictionary_reward_receive_failed_ack = 4209; // 获取失败
public static final int quest_tower_reward_receive_req = 4210; // 领取星塔任务奖励@value表示任务ID,0表示一键领取
public static final int quest_tower_reward_receive_succeed_ack = 4211; // 获取成功
public static final int quest_tower_reward_receive_failed_ack = 4212; // 获取失败
public static final int quest_daily_active_reward_receive_req = 4213; // 领取日常任务活跃奖励
public static final int quest_daily_active_reward_receive_succeed_ack = 4214; // 领取日常任务活跃奖励成功
public static final int quest_daily_active_reward_receive_failed_ack = 4215; // 领取日常任务活跃奖励失败
public static final int quest_tour_guide_group_reward_receive_req = 4216; // 领取任务组奖励
public static final int quest_tour_guide_group_reward_receive_succeed_ack = 4217; // 获取成功
public static final int quest_tour_guide_group_reward_receive_failed_ack = 4218; // 获取失败
public static final int activity_task_reward_receive_req = 4301; // 请求领取活动任务完成奖励
public static final int activity_task_reward_receive_succeed_ack = 4302; // 请求领取活动任务完成奖励成功
public static final int activity_task_reward_receive_failed_ack = 4303; // 请求领取活动任务完成奖励失败
public static final int activity_task_group_reward_receive_req = 4304; // 请求领取活动任务组完成奖励
public static final int activity_task_group_reward_receive_succeed_ack = 4305; // 请求领取活动任务组完成奖励成功
public static final int activity_task_group_reward_receive_failed_ack = 4306; // 请求领取活动任务组完成奖励失败
public static final int achievement_reward_receive_req = 4401; // 领取成就奖励
public static final int achievement_reward_receive_succeed_ack = 4402; // 获取成功
public static final int achievement_reward_receive_failed_ack = 4403; // 获取失败
public static final int achievement_info_req = 4404; // 获取成就数据
public static final int achievement_info_succeed_ack = 4405; // 获取成功
public static final int achievement_info_failed_ack = 4406; // 获取失败
public static final int infinity_tower_info_req = 4501; // 申请无尽塔关卡数据
public static final int infinity_tower_info_succeed_ack = 4502; // 申请无尽塔关卡数据成功
public static final int infinity_tower_info_failed_ack = 4503; // 申请无尽塔关卡数据失败
public static final int infinity_tower_apply_req = 4504; // 申请进入无尽塔关卡
public static final int infinity_tower_apply_succeed_ack = 4505; // 申请进入无尽塔关卡成功
public static final int infinity_tower_apply_failed_ack = 4506; // 申请进入无尽塔关卡失败
public static final int infinity_tower_settle_req = 4507; // 申请结算无尽塔关卡
public static final int infinity_tower_settle_succeed_ack = 4508; // 申请结算无尽塔成功 非0表示可以继续挑战关卡ID(失败当前关卡ID,成功下一个关卡ID),无需再发申请
public static final int infinity_tower_settle_failed_ack = 4509; // 申请结算无尽塔失败
public static final int infinity_tower_daily_reward_receive_req = 4510; // 请求领取无尽塔每日奖励
public static final int infinity_tower_daily_reward_receive_succeed_ack = 4511; // 请求领取无尽塔每日奖励成功
public static final int infinity_tower_daily_reward_receive_failed_ack = 4512; // 请求领取无尽塔每日奖励失败
public static final int infinity_tower_plot_reward_receive_req = 4513; // 请求领取无尽塔剧情奖励
public static final int infinity_tower_plot_reward_receive_succeed_ack = 4514; // 请求领取无尽塔剧情奖励成功
public static final int infinity_tower_plot_reward_receive_failed_ack = 4515; // 请求领取无尽塔剧情奖励失败
public static final int star_tower_apply_req = 4601; // 申请探索星塔
public static final int star_tower_apply_succeed_ack = 4602; // 申请成功返回
public static final int star_tower_apply_failed_ack = 4603; // 申请失败
public static final int star_tower_interact_req = 4607; // 星塔交互请求
public static final int star_tower_interact_succeed_ack = 4608; // 交互请求成功
public static final int star_tower_interact_failed_ack = 4609; // 申请失败
public static final int star_tower_info_req = 4610; // 重连获取星塔信息
public static final int star_tower_info_succeed_ack = 4611; // 获取星塔信息成功
public static final int star_tower_info_failed_ack = 4612; // 获取星塔信息失败
public static final int star_tower_give_up_req = 4613; // 放弃星塔
public static final int star_tower_give_up_succeed_ack = 4614; // 放弃星塔成功
public static final int star_tower_give_up_failed_ack = 4615; // 放弃星塔失败
public static final int star_tower_build_whether_save_req = 4701; // 请求是否保存星塔build
public static final int star_tower_build_whether_save_succeed_ack = 4702; // 请求是否保存星塔build返回
public static final int star_tower_build_whether_save_failed_ack = 4703; // 请求是否保存星塔build失败
public static final int star_tower_build_brief_list_get_req = 4704; // 请求星塔build简要信息列表
public static final int star_tower_build_brief_list_get_succeed_ack = 4705; // 请求星塔build简要信息列表返回
public static final int star_tower_build_brief_list_get_failed_ack = 4706; // 请求星塔build简要信息列表失败
public static final int star_tower_build_detail_get_req = 4707; // 请求星塔build详细信息列表
public static final int star_tower_build_detail_get_succeed_ack = 4708; // 请求星塔build详细信息列表返回
public static final int star_tower_build_detail_get_failed_ack = 4709; // 请求遗迹build详细信息列表失败
public static final int star_tower_build_delete_req = 4710; // 请求解散星塔build
public static final int star_tower_build_delete_succeed_ack = 4711; // 请求解散星塔build返回
public static final int star_tower_build_delete_failed_ack = 4712; // 请求解散星塔build失败
public static final int star_tower_build_name_set_req = 4713; // 请求设置星塔build名
public static final int star_tower_build_name_set_succeed_ack = 4714; // 请求设置星塔build名返回
public static final int star_tower_build_name_set_failed_ack = 4715; // 请求设置星塔build名失败
public static final int star_tower_build_lock_unlock_req = 4716; // 请求星塔build加解锁
public static final int star_tower_build_lock_unlock_succeed_ack = 4717; // 请求星塔build加解锁返回
public static final int star_tower_build_lock_unlock_failed_ack = 4718; // 请求星塔build加解锁失败
public static final int star_tower_build_preference_set_req = 4719; // 请求设置星塔build偏好
public static final int star_tower_build_preference_set_succeed_ack = 4720; // 请求设置星塔build偏好返回
public static final int star_tower_build_preference_set_failed_ack = 4721; // 请求设置星塔build偏好失败
public static final int star_tower_book_potential_brief_list_get_req = 4901; // 请求星塔图鉴角色潜能简要信息
public static final int star_tower_book_potential_brief_list_get_succeed_ack = 4902; // 请求星塔图鉴角色潜能简要信息成功
public static final int star_tower_book_potential_brief_list_get_failed_ack = 4903; // 请求星塔图鉴角色潜能简要信息失败
public static final int star_tower_book_char_potential_get_req = 4904; // 请求星塔图鉴角色潜能信息
public static final int star_tower_book_char_potential_get_succeed_ack = 4905; // 请求星塔图鉴角色潜能信息成功
public static final int star_tower_book_char_potential_get_failed_ack = 4906; // 请求星塔图鉴角色潜能信息失败
public static final int star_tower_book_potential_reward_receive_req = 4907; // 请求领取星塔图鉴角色潜能奖励
public static final int star_tower_book_potential_reward_receive_succeed_ack = 4908; // 请求领取星塔图鉴角色潜能奖励成功
public static final int star_tower_book_potential_reward_receive_failed_ack = 4909; // 请求领取星塔图鉴角色潜能奖励失败
public static final int star_tower_book_event_reward_receive_req = 4913; // 请求领取星塔图鉴角色潜能奖励
public static final int star_tower_book_event_reward_receive_succeed_ack = 4914; // 请求领取星塔图鉴角色潜能奖励成功
public static final int star_tower_book_event_reward_receive_failed_ack = 4915; // 请求领取星塔图鉴角色潜能奖励失败
public static final int npc_affinity_book_get_req = 4916; // 请求NPC好感度图鉴信息
public static final int npc_affinity_book_get_succeed_ack = 4917; // 请求NPC好感度图鉴信息成功
public static final int npc_affinity_book_get_failed_ack = 4918; // 请求NPC好感度图鉴信息失败
public static final int npc_affinity_plot_reward_receive_req = 4919; // 请求领取好感度剧情奖励
public static final int npc_affinity_plot_reward_receive_succeed_ack = 4920; // 请求领取好感度剧情奖励成功
public static final int npc_affinity_plot_reward_receive_failed_ack = 4921; // 请求领取好感度剧情奖励失败
public static final int resident_shop_get_req = 5010; // 请求常驻商店信息
public static final int resident_shop_get_succeed_ack = 5011; // 请求常驻商店信息成功
public static final int resident_shop_get_failed_ack = 5012; // 请求常驻商店信息失败
public static final int resident_shop_purchase_req = 5013; // 请求常驻商店购买物品
public static final int resident_shop_purchase_succeed_ack = 5014; // 请求常驻商店购买物品成功
public static final int resident_shop_purchase_failed_ack = 5015; // 请求常驻商店购买物品失败
public static final int mall_gem_list_req = 5101; // 获取钻石商城产品列表
public static final int mall_gem_list_succeed_ack = 5102; // 获取成功的列表
public static final int mall_gem_list_failed_ack = 5103; // 获取失败
public static final int mall_gem_order_req = 5104; // 下单购买商品
public static final int mall_gem_order_succeed_ack = 5105; // 下单成功
public static final int mall_gem_order_failed_ack = 5106; // 下单失败
public static final int mall_order_cancel_req = 5107; // 取消某个尚未支付的订单
public static final int mall_order_cancel_succeed_ack = 5108; // 取消成功
public static final int mall_order_cancel_failed_ack = 5109; // 取消失败
public static final int mall_order_collect_req = 5110; // 领取某个支付成功的订单的奖励
public static final int mall_order_collect_succeed_ack = 5111; // 返回成功,请根据具体状态处理
public static final int mall_order_collect_failed_ack = 5112; // 领取失败
public static final int mall_monthlyCard_list_req = 5113; // 获取月卡商城产品列表
public static final int mall_monthlyCard_list_succeed_ack = 5114; // 获取成功的列表
public static final int mall_monthlyCard_list_failed_ack = 5115; // 获取失败
public static final int mall_monthlyCard_order_req = 5116; // 购买月卡商城商品
public static final int mall_monthlyCard_order_succeed_ack = 5117; // 下单成功
public static final int mall_monthlyCard_order_failed_ack = 5118; // 获取失败
public static final int mall_package_list_req = 5119; // 获取礼包商城商品列表
public static final int mall_package_list_succeed_ack = 5120; // 商品列表
public static final int mall_package_list_failed_ack = 5121; // 获取失败
public static final int mall_package_order_req = 5122; // 购买礼包商城产品
public static final int mall_package_order_succeed_ack = 5123; // 购买成功结果
public static final int mall_package_order_failed_ack = 5124; // 购买失败
public static final int mall_shop_list_req = 5125; // 获取星尘兑换商城商品列表
public static final int mall_shop_list_succeed_ack = 5126; // 商品列表
public static final int mall_shop_list_failed_ack = 5127; // 获取失败
public static final int mall_shop_order_req = 5128; // 购买星尘兑换商店产品
public static final int mall_shop_order_succeed_ack = 5129; // 购买成功结果
public static final int mall_shop_order_failed_ack = 5130; // 购买失败
public static final int gacha_spin_req = 6001; // 抽卡@传入卡池ID以及抽卡模式
public static final int gacha_spin_succeed_ack = 6002; // 成功,返回掉落道具以及ChangeInfo
public static final int gacha_spin_failed_ack = 6003; // 错误,返回错误信息
public static final int gacha_information_req = 6004; // 获取所有卡池数据
public static final int gacha_information_succeed_ack = 6005; // 成功,返回所有的卡池数据
public static final int gacha_information_failed_ack = 6006; // 错误,返回错误信息
public static final int gacha_histories_req = 6007; // 获取抽卡的历史记录@传入存盘ID
public static final int gacha_histories_succeed_ack = 6008; // 成功,返回抽卡的历史数据
public static final int gacha_histories_failed_ack = 6009; // 错误,返回错误
public static final int gacha_guarantee_reward_receive_req = 6010; // 领取天井奖励@传入卡池ID
public static final int gacha_guarantee_reward_receive_succeed_ack = 6011; // 成功,返回奖励
public static final int gacha_guarantee_reward_receive_failed_ack = 6012; // 失败,返回错误信息
public static final int gacha_newbie_spin_req = 6013; // 新手卡池抽卡@传入卡池ID
public static final int gacha_newbie_spin_succeed_ack = 6014; // 成功,返回抽卡的结果
public static final int gacha_newbie_spin_failed_ack = 6015; // 错误,返回错误
public static final int gacha_newbie_save_req = 6016; // 新手卡池结果保存
public static final int gacha_newbie_save_succeed_ack = 6017; // 成功,不返回任何内容
public static final int gacha_newbie_save_failed_ack = 6018; // 错误,返回错误
public static final int gacha_newbie_obtain_req = 6019; // 获取新手卡池结果
public static final int gacha_newbie_obtain_succeed_ack = 6020; // 成功,返回道具变化
public static final int gacha_newbie_obtain_failed_ack = 6021; // 错误,返回错误
public static final int gacha_newbie_info_req = 6022; // 获取新手卡池信息@返回所有新手卡池信息
public static final int gacha_newbie_info_succeed_ack = 6023; // 成功,返回道具变化
public static final int gacha_newbie_info_failed_ack = 6024; // 错误,返回错误
public static final int tower_book_fate_card_detail_req = 6101; // 获取已经拥有的命运卡图鉴
public static final int tower_book_fate_card_detail_succeed_ack = 6102; // 成功,返回已经拥有的命运卡和已经领取的任务
public static final int tower_book_fate_card_detail_failed_ack = 6103; // 错误,返回错误信息
public static final int tower_book_fate_card_reward_receive_req = 6104; // 领取命运卡任务奖励
public static final int tower_book_fate_card_reward_receive_succeed_ack = 6105; // 成功,返回任务奖励
public static final int tower_book_fate_card_reward_receive_failed_ack = 6106; // 错误,返回错误信息
public static final int joint_drill_apply_req = 6201; // 总力战申请
public static final int joint_drill_apply_succeed_ack = 6202; // 总力战申请成功
public static final int joint_drill_apply_failed_ack = 6203; // 总力战申请失败
public static final int joint_drill_continue_req = 6204; // 总力战申请继续战斗
public static final int joint_drill_continue_succeed_ack = 6205; // 总力战申请继续战斗成功
public static final int joint_drill_continue_failed_ack = 6206; // 总力战申请继续战斗失败
public static final int joint_drill_sync_req = 6207; // 总力战同步记录
public static final int joint_drill_sync_succeed_ack = 6208; // 总力战同步记录成功
public static final int joint_drill_sync_failed_ack = 6209; // 总力战同步记录失败
public static final int joint_drill_give_up_req = 6210; // 总力战战斗放弃/失败
public static final int joint_drill_give_up_succeed_ack = 6211; // 总力战申请小队战斗放弃/失败成功
public static final int joint_drill_give_up_failed_ack = 6212; // 总力战申请小队战斗放弃/失败失败
public static final int joint_drill_retreat_req = 6213; // 总力战申请小队战斗撤退
public static final int joint_drill_retreat_succeed_ack = 6214; // 总力战申请小队战斗撤退成功
public static final int joint_drill_retreat_failed_ack = 6215; // 总力战申请小队战斗撤退失败
public static final int joint_drill_settle_req = 6216; // 总力战小队结算(胜利)申请
public static final int joint_drill_settle_succeed_ack = 6217; // 总力战小队结算(胜利)申请成功
public static final int joint_drill_settle_failed_ack = 6218; // 总力战小队结算(胜利)申请失败
public static final int joint_drill_game_over_req = 6219; // 总力战结束挑战
public static final int joint_drill_game_over_succeed_ack = 6220; // 总力战结束挑战成功
public static final int joint_drill_game_over_failed_ack = 6221; // 总力战结束挑战失败
public static final int joint_drill_sweep_req = 6222; // 请求扫荡总力战关卡
public static final int joint_drill_sweep_succeed_ack = 6223; // 请求扫荡总力战关卡成功
public static final int joint_drill_sweep_failed_ack = 6224; // 请求扫荡总力战关卡失败
public static final int joint_drill_rank_req = 6225; // 请求总力战排行榜信息
public static final int joint_drill_rank_succeed_ack = 6226; // 请求总力战排行榜信息成功
public static final int joint_drill_rank_failed_ack = 6227; // 请求总力战排行榜信息失败
public static final int joint_drill_quest_reward_receive_req = 6228; // 请求领取总力战任务奖励
public static final int joint_drill_quest_reward_receive_succeed_ack = 6229; // 请求领取总力战任务奖励成功
public static final int joint_drill_quest_reward_receive_failed_ack = 6230; // 请求领取总力战任务奖励失败
public static final int plot_reward_receive_req = 7013; // 领取剧情奖励@传入剧情ID
public static final int plot_reward_receive_succeed_ack = 7014; // 领取成功
public static final int plot_reward_receive_failed_ack = 7015; // 领取失败
public static final int daily_instance_apply_req = 7016; // 日常副本申请
public static final int daily_instance_apply_succeed_ack = 7017; // 日常副本申请成功
public static final int daily_instance_apply_failed_ack = 7018; // 错误,返回错误信息
public static final int daily_instance_settle_req = 7019; // 日常副本结算请求
public static final int daily_instance_settle_succeed_ack = 7020; // 日常副本结算成功
public static final int daily_instance_settle_failed_ack = 7021; // 错误,返回错误信息
public static final int daily_instance_raid_req = 7022; // 日常副本扫荡请求
public static final int daily_instance_raid_succeed_ack = 7023; // 日常副本扫荡成功
public static final int daily_instance_raid_failed_ack = 7024; // 错误,返回错误信息
public static final int char_gem_instance_apply_req = 7028; // 角色宝石碎片副本申请
public static final int char_gem_instance_apply_succeed_ack = 7029; // 角色宝石碎片副本申请成功
public static final int char_gem_instance_apply_failed_ack = 7030; // 角色宝石碎片副本申请失败
public static final int char_gem_instance_settle_req = 7031; // 角色宝石碎片副本结算请求
public static final int char_gem_instance_settle_succeed_ack = 7032; // 角色宝石碎片副本结算成功
public static final int char_gem_instance_settle_failed_ack = 7033; // 错误,返回错误信息
public static final int char_gem_instance_sweep_req = 7034; // 角色宝石碎片副本扫荡请求
public static final int char_gem_instance_sweep_succeed_ack = 7035; // 角色宝石碎片副本扫荡成功
public static final int char_gem_instance_sweep_failed_ack = 7036; // 错误,返回错误信息
public static final int region_boss_level_apply_req = 7101; // 请求进入地区boss关卡
public static final int region_boss_level_apply_succeed_ack = 7102; // 请求进入地区boss关成功
public static final int region_boss_level_apply_failed_ack = 7103; // 请求进入地区boss关卡失败
public static final int region_boss_level_settle_req = 7104; // 请求结算地区boss关卡
public static final int region_boss_level_settle_succeed_ack = 7105; // 请求结算地区boss关成功
public static final int region_boss_level_settle_failed_ack = 7106; // 请求结算地区boss关卡失败
public static final int region_boss_level_sweep_req = 7107; // 请求扫荡地区boss关卡
public static final int region_boss_level_sweep_succeed_ack = 7108; // 请求扫荡地区boss关成功
public static final int region_boss_level_sweep_failed_ack = 7109; // 请求结算地区boss关卡失败
public static final int traveler_duel_level_apply_req = 7201; // 请求进入旅人对决关卡
public static final int traveler_duel_level_apply_succeed_ack = 7202; // 请求进入旅人对决关卡成功
public static final int traveler_duel_level_apply_failed_ack = 7203; // 请求进入旅人对决关卡失败
public static final int traveler_duel_level_settle_req = 7204; // 请求结算旅人对决关卡
public static final int traveler_duel_level_settle_succeed_ack = 7205; // 请求结算旅人对决关卡成功
public static final int traveler_duel_level_settle_failed_ack = 7206; // 请求结算旅人对决关卡失败
public static final int traveler_duel_info_req = 7207; // 请求旅人对决信息
public static final int traveler_duel_info_succeed_ack = 7208; // 请求旅人对决信息成功
public static final int traveler_duel_info_failed_ack = 7209; // 请求旅人对决信息失败
public static final int traveler_duel_quest_reward_receive_req = 7210; // 领取旅人对决任务奖励
public static final int traveler_duel_quest_reward_receive_succeed_ack = 7211; // 获取成功
public static final int traveler_duel_quest_reward_receive_failed_ack = 7212; // 获取失败
public static final int traveler_duel_rank_req = 7213; // 请求旅人对决排行榜信息
public static final int traveler_duel_rank_succeed_ack = 7214; // 请求旅人对决排行榜信息成功
public static final int traveler_duel_rank_failed_ack = 7215; // 请求旅人对决排行信息失败
public static final int traveler_duel_rank_upload_req = 7216; // 旅人对决信息上传@请求旅人对决上传分数、附带本次战斗统计数据
public static final int traveler_duel_rank_upload_succeed_ack = 7217; // 请求旅人对决上传分数成功
public static final int traveler_duel_rank_upload_failed_ack = 7218; // 请求旅人对决上传分数失败
public static final int story_apply_req = 7301; // 关卡申请
public static final int story_apply_succeed_ack = 7302; // 申请成功,返回 Nil
public static final int story_apply_failed_ack = 7303; // 错误,返回错误信息
public static final int story_settle_req = 7304; // 关卡结算
public static final int story_settle_succeed_ack = 7305; // 结算成功,发放通关奖励
public static final int story_settle_failed_ack = 7306; // 错误,返回错误信息
public static final int skill_instance_apply_req = 7401; // 请求进入技能素材关卡
public static final int skill_instance_apply_succeed_ack = 7402; // 请求进入技能素材关卡成功
public static final int skill_instance_apply_failed_ack = 7403; // 请求进入技能素材关卡失败
public static final int skill_instance_settle_req = 7404; // 请求结算技能素材本关卡
public static final int skill_instance_settle_succeed_ack = 7405; // 请求结算技能素材本关卡成功
public static final int skill_instance_settle_failed_ack = 7406; // 请求结算技能素材本关卡失败
public static final int skill_instance_sweep_req = 7407; // 请求扫荡技能素材关卡
public static final int skill_instance_sweep_succeed_ack = 7408; // 请求扫荡技能素材关卡成功
public static final int skill_instance_sweep_failed_ack = 7409; // 请求扫荡技能素材关卡失败
public static final int week_boss_apply_req = 7410; // 请求进入周长boss本
public static final int week_boss_apply_succeed_ack = 7411; // 请求进入周长boss本成功
public static final int week_boss_apply_failed_ack = 7412; // 请求进入周长boss本失败
public static final int week_boss_settle_req = 7413; // 请求结算周长boss本
public static final int week_boss_settle_succeed_ack = 7414; // 请求结算周长boss本成功
public static final int week_boss_settle_failed_ack = 7415; // 请求结算周长boss本失败
public static final int tutorial_level_settle_req = 7501; // 教学关卡结算
public static final int tutorial_level_settle_succeed_ack = 7502; // 教学关卡结算成功
public static final int tutorial_level_settle_failed_ack = 7503; // 教学关卡结算失败
public static final int tutorial_level_reward_receive_req = 7504; // 领取教学关卡奖励
public static final int tutorial_level_reward_receive_succeed_ack = 7505; // 领取教学关卡奖励成功
public static final int tutorial_level_reward_receive_failed_ack = 7506; // 领取教学关卡奖励失败
public static final int story_set_info_req = 7601; // 获取故事集数据
public static final int story_set_info_succeed_ack = 7602; // 获取故事集数据成功
public static final int story_set_info_failed_ack = 7603; // 错误,返回错误信息
public static final int story_set_reward_receive_req = 7604; // 领取故事集奖励
public static final int story_set_reward_receive_succeed_ack = 7605; // 领取故事集奖励成功
public static final int story_set_reward_receive_failed_ack = 7606; // 领取故事集奖励失败
public static final int energy_buy_req = 8001; // 购买体力请求
public static final int energy_buy_succeed_ack = 8002; // 购买成功,返回当日的购买次数,以及ChangeInfo
public static final int energy_buy_failed_ack = 8003; // 购买失败,返回错误信息
public static final int energy_extract_req = 8004; // 提取体力请求
public static final int energy_extract_succeed_ack = 8005; // 提取成功,返回 changeInfo
public static final int energy_extract_failed_ack = 8006; // 提取失败,返回错误信息
public static final int client_event_report_req = 8101; // 客户端事件上报
public static final int client_event_report_succeed_ack = 8102; // 客户端事件上报成功
public static final int client_event_report_failed_ack = 8103; // 客户端事件上报失败
public static final int vampire_survivor_apply_req = 8201; // 灾变防线副本申请
public static final int vampire_survivor_apply_succeed_ack = 8202; // 灾变防线副本申请成功
public static final int vampire_survivor_apply_failed_ack = 8203; // 灾变防线副本申请失败
public static final int vampire_survivor_area_change_req = 8204; // 灾变防线阶段转化
public static final int vampire_survivor_area_change_succeed_ack = 8205; // 灾变防线副本申请成功
public static final int vampire_survivor_area_change_failed_ack = 8206; // 灾变防线副本申请失败
public static final int vampire_survivor_settle_req = 8207; // 灾变防线副本成功/失败结算申请
public static final int vampire_survivor_settle_succeed_ack = 8208; // 灾变防线副本结算申请成功
public static final int vampire_survivor_settle_failed_ack = 8209; // 灾变防线副本结算申请失败
public static final int vampire_survivor_reward_select_req = 8210; // 灾变防线副本升级申请
public static final int vampire_survivor_reward_select_succeed_ack = 8211; // 灾变防线副本升级申请成功
public static final int vampire_survivor_reward_select_failed_ack = 8212; // 灾变防线副本升级申请失败
public static final int vampire_survivor_reward_chest_req = 8213; // 灾变防线开宝箱申请
public static final int vampire_survivor_reward_chest_succeed_ack = 8214; // 灾变防线开宝箱申请成功
public static final int vampire_survivor_reward_chest_failed_ack = 8215; // 灾变防线开宝箱申请失败
public static final int vampire_survivor_quest_reward_receive_req = 8216; // 灾变防线领取任务奖励申请
public static final int vampire_survivor_quest_reward_receive_succeed_ack = 8217; // 灾变防线领取任务奖励成功
public static final int vampire_survivor_quest_reward_receive_failed_ack = 8218; // 灾变防线领取任务奖励失败
public static final int vampire_survivor_extra_reward_select_req = 8222; // 灾变防线副本额外奖励领取
public static final int vampire_survivor_extra_reward_select_succeed_ack = 8223; // 灾变防线副本额外奖励领取成功
public static final int vampire_survivor_extra_reward_select_failed_ack = 8224; // 灾变防线副本额外奖励领取失败
public static final int vampire_survivor_restart_req = 8225; // 灾变防线第二阶段重开
public static final int vampire_survivor_restart_succeed_ack = 8226; // 灾变防线第二阶段重开成功
public static final int vampire_survivor_restart_failed_ack = 8227; // 灾变防线第二阶段重开失败
public static final int vampire_talent_detail_req = 8301; // 获取吸血鬼天赋信息
public static final int vampire_talent_detail_succeed_ack = 8302; // 获取节点信息成功
public static final int vampire_talent_detail_failed_ack = 8303; // 获取失败,返回错误信息
public static final int vampire_talent_reset_req = 8304; // 重置吸血鬼所有的天赋
public static final int vampire_talent_reset_succeed_ack = 8305; // 重置成功,不返回任何数据
public static final int vampire_talent_reset_failed_ack = 8306; // 重置失败,返回错误信息
public static final int vampire_talent_unlock_req = 8307; // 吸血鬼天赋解锁@传入节点ID,解锁对应的养成节点
public static final int vampire_talent_unlock_succeed_ack = 8308; // 解锁成功
public static final int vampire_talent_unlock_failed_ack = 8309; // 解锁失败,返回错误信息
public static final int vampire_talent_show_req = 8310; // 吸血鬼天赋点数展示
public static final int vampire_talent_show_succeed_ack = 8311; // 成功无任何返回
public static final int vampire_talent_show_failed_ack = 8312; // 失败返回错误
public static final int mail_list_req = 9001; // 获取邮件列表
public static final int mail_list_succeed_ack = 9002; // 邮件列表
public static final int mail_list_failed_ack = 9003; // 获取失败
public static final int mail_read_req = 9004; // 标记邮件已读
public static final int mail_read_succeed_ack = 9005; // 返回已设置为已读的邮件ID
public static final int mail_read_failed_ack = 9006; // 设置失败
public static final int mail_recv_req = 9007; // 领取邮件奖励@一键领取发送0,单独领取发送对应邮件ID上来
public static final int mail_recv_succeed_ack = 9008; // 领取成功
public static final int mail_recv_failed_ack = 9009; // 领取失败
public static final int mail_remove_req = 9010; // 删除邮件@一键删除所有已读已领发送0,单独删除发送对应邮件ID上来
public static final int mail_remove_succeed_ack = 9011; // 删除成功
public static final int mail_remove_failed_ack = 9012; // 删除失败
public static final int mail_pin_req = 9013; // 邮件星标操作
public static final int mail_pin_succeed_ack = 9014; // 标记成功
public static final int mail_pin_failed_ack = 9015; // 标记失败
public static final int activity_detail_req = 9101; // 获取所有的活动数据
public static final int activity_detail_succeed_ack = 9102; // 获取成功
public static final int activity_detail_failed_ack = 9103; // 获取失败
public static final int activity_periodic_reward_receive_req = 9104; // 领取周期活动奖励
public static final int activity_periodic_reward_receive_succeed_ack = 9105; // 领取成功
public static final int activity_periodic_reward_receive_failed_ack = 9106; // 领取失败
public static final int activity_periodic_final_reward_receive_req = 9107; // 领取周期活动最终奖励
public static final int activity_periodic_final_reward_receive_succeed_ack = 9108; // 领取成功
public static final int activity_periodic_final_reward_receive_failed_ack = 9109; // 领取失败
public static final int activity_login_reward_receive_req = 9110; // 领取登录活动奖励
public static final int activity_login_reward_receive_succeed_ack = 9111; // 领取成功
public static final int activity_login_reward_receive_failed_ack = 9112; // 领取失败
public static final int activity_tower_defense_story_reward_receive_req = 9113; // 领取塔防活动剧情奖励@传入剧情ID
public static final int activity_tower_defense_story_reward_receive_succeed_ack = 9114; // 领取成功
public static final int activity_tower_defense_story_reward_receive_failed_ack = 9115; // 领取失败
public static final int activity_tower_defense_quest_reward_receive_req = 9116; // 领取塔防活动任务奖励
public static final int activity_tower_defense_quest_reward_receive_succeed_ack = 9117; // 领取成功
public static final int activity_tower_defense_quest_reward_receive_failed_ack = 9118; // 领取失败
public static final int activity_tower_defense_level_apply_req = 9119; // 塔防活动关卡申请
public static final int activity_tower_defense_level_apply_succeed_ack = 9120; // 申请成功
public static final int activity_tower_defense_level_apply_failed_ack = 9121; // 申请失败
public static final int activity_tower_defense_level_settle_req = 9122; // 塔防活动关卡结算
public static final int activity_tower_defense_level_settle_succeed_ack = 9123; // 结算成功(首次通关会有奖励,之后没有奖励)
public static final int activity_tower_defense_level_settle_failed_ack = 9124; // 结算失败
public static final int phone_contacts_info_req = 9201; // 获取手机所有联系人的数据
public static final int phone_contacts_info_succeed_ack = 9202; // 获取联系人数据成功
public static final int phone_contacts_info_failed_ack = 9203; // 获取联系人数据
public static final int phone_contacts_report_req = 9204; // 联系人聊天上报
public static final int phone_contacts_report_succeed_ack = 9205; // 上报成功
public static final int phone_contacts_report_failed_ack = 9206; // 上报失败
public static final int phone_contacts_top_req = 9207; // 联系人置顶
public static final int phone_contacts_top_succeed_ack = 9208; // 置顶成功
public static final int phone_contacts_top_failed_ack = 9209; // 置顶失败
public static final int talent_unlock_req = 9301; // 天赋解锁@传入天赋ID
public static final int talent_unlock_succeed_ack = 9302; // 解锁成功
public static final int talent_unlock_failed_ack = 9303; // 解锁失败,返回错误信息
public static final int talent_reset_req = 9304; // 角色天赋/天赋组重置@传入角色ID
public static final int talent_reset_succeed_ack = 9305; // 重置成功
public static final int talent_reset_failed_ack = 9306; // 重置失败,返回错误信息
public static final int talent_node_reset_req = 9307; // 天赋重置
public static final int talent_node_reset_succeed_ack = 9308; // 重置成功
public static final int talent_node_reset_failed_ack = 9309; // 重置失败,返回错误信息
public static final int talent_background_set_req = 9310; // 设置天赋背景@传入节点组
public static final int talent_background_set_succeed_ack = 9311; // 设置成功
public static final int talent_background_set_failed_ack = 9312; // 设置失败,返回错误信息
public static final int talent_group_unlock_req = 9313; // 天赋组解锁
public static final int talent_group_unlock_succeed_ack = 9314; // 解锁成功
public static final int talent_group_unlock_failed_ack = 9315; // 解锁失败,返回错误信息
public static final int activity_trial_reward_receive_req = 9401; // 传入试玩角色组id领取奖励
public static final int activity_trial_reward_receive_succeed_ack = 9402; // 领取成功
public static final int activity_trial_reward_receive_failed_ack = 9403; // 领取失败
public static final int activity_cg_read_req = 9501; // 活动CG已读@传入活动ID
public static final int activity_cg_read_succeed_ack = 9502; // 操作成功
public static final int activity_cg_read_failed_ack = 9503; // 操作失败
public static final int activity_levels_apply_req = 9601; // 申请进入活动关卡
public static final int activity_levels_apply_succeed_ack = 9602; // 进入成功
public static final int activity_levels_apply_failed_ack = 9603; // 进入失败
public static final int activity_levels_settle_req = 9604; // 活动关卡结算
public static final int activity_levels_settle_succeed_ack = 9605; // 结算成功
public static final int activity_levels_settle_failed_ack = 9606; // 结算失败
public static final int activity_levels_sweep_req = 9607; // 活动关卡扫荡
public static final int activity_levels_sweep_succeed_ack = 9608; // 扫荡成功
public static final int activity_levels_sweep_failed_ack = 9609; // 领取失败
public static final int activity_avg_reward_receive_req = 9701; // 领取AVG活动关卡奖励
public static final int activity_avg_reward_receive_succeed_ack = 9702; // 领取成功
public static final int activity_avg_reward_receive_failed_ack = 9703; // 领取失败
public static final int activity_shop_purchase_req = 9751; // 请求活动商店购买物品
public static final int activity_shop_purchase_succeed_ack = 9752; // 请求活动商店购买物品成功
public static final int activity_shop_purchase_failed_ack = 9753; // 请求活动商店购买物品失败
public static final int battle_pass_info_req = 9801; // 获取当前战令信息
public static final int battle_pass_info_succeed_ack = 9802; // 战令信息
public static final int battle_pass_info_failed_ack = 9803; // 获取失败
public static final int battle_pass_reward_receive_req = 9804; // 领取战令奖励@传入战令等级和版本,全部领等级取传0
public static final int battle_pass_reward_receive_succeed_ack = 9805; // 领取战令奖励成功
public static final int battle_pass_reward_receive_failed_ack = 9806; // 领取战令奖励失败
public static final int battle_pass_level_buy_req = 9807; // 战令等级购买@传入需要购买的级数和版本
public static final int battle_pass_level_buy_succeed_ack = 9808; // 购买成功
public static final int battle_pass_level_buy_failed_ack = 9809; // 购买成功
public static final int battle_pass_order_req = 9810; // 战令进阶下单
public static final int battle_pass_order_succeed_ack = 9811; // 战令进阶下单成功
public static final int battle_pass_order_failed_ack = 9812; // 领取失败
public static final int battle_pass_order_collect_req = 9813; // 战令进阶订单收取
public static final int battle_pass_order_collect_succeed_ack = 9814; // 返回成功,请根据具体状态处理
public static final int battle_pass_order_collect_failed_ack = 9815; // 战令进阶失败
public static final int battle_pass_quest_reward_receive_req = 9816; // 战令任务一键领取@value表示任务ID,0表示一键领取
public static final int battle_pass_quest_reward_receive_succeed_ack = 9817; // 获取成功
public static final int battle_pass_quest_reward_receive_failed_ack = 9818; // 获取失败
public static final int redeem_code_req = 9901; // 兑换码兑换
public static final int redeem_code_succeed_ack = 9902; // 兑换成功,返回兑换后德道具以及ChangeInfo
public static final int redeem_code_failed_ack = 9903; // 兑换失败,返回错误信息
public static final int system_failed_ack = 10000; // 系统级失败,主要用于http模式下,强制失败返回
public static final int player_new_notify = 10001; // 新用户
public static final int mail_state_notify = 10002; // 邮件状态变更
public static final int player_relogin_notify = 10003; // 在其他地方登录
public static final int token_expire_notify = 10004; // token过期
public static final int player_ban_notify = 10005; // 用户被ban
public static final int quest_change_notify = 10006; // 任务进度变更
public static final int week_boss_refresh_ticket_notify = 10007; // 周长副本门票刷新通知
public static final int agent_new_notify = 10008; // 每周刷新新委托ID列表
public static final int world_class_change_notify = 10009; // 世界等级变化
public static final int friend_energy_state_notify = 10010; // 好友赠送体力状态变更
public static final int signin_reward_change_notify = 10011; // 登陆奖励更新
public static final int friend_state_notify = 10012; // 好友状态变更
public static final int order_paid_notify = 10013; // 订单已完成支付通知,可以发起领取
public static final int order_revoke_notify = 10014; // 订单道具被撤回,主要用于恶意退款
public static final int star_tower_book_potential_notify = 10015; // 星塔潜能图鉴状态变更
public static final int star_tower_book_event_notify = 10016; // 星塔潜能图鉴状态变更
public static final int battle_pass_state_notify = 10017; // 战令状态变更
public static final int world_class_reward_state_notify = 10018; // 世界等级奖励状态变更
public static final int char_advance_reward_state_notify = 10019; // 角色进阶奖励状态变更
public static final int achievement_change_notify = 10020; // 成就进度变更
public static final int achievement_state_notify = 10021; // 成就待领取红点提示
public static final int character_skin_gain_notify = 10022; // 角色获得新皮肤,如果是重复获取,将发送转换数据
public static final int character_skin_change_notify = 10023; // 角色装备的皮肤发生改变
public static final int handbook_change_notify = 10024; // 图鉴数据发生变化
public static final int monthly_card_rewards_notify = 10025; // 月卡奖励通知
public static final int quest_state_notify = 10026; // 任务红点奖励notify
public static final int mall_package_state_notify = 10027; // 礼包商城免费商品红点notify
public static final int dictionary_change_notify = 10028; // 字典数据变更
public static final int activity_change_notify = 10029; // 活动数据变化
public static final int activity_quest_change_notify = 10030; // 活动任务数据变化
public static final int char_affinity_reward_state_notify = 10031; // 角色好感度奖励最终值
public static final int mail_overflow_notify = 10032; // 道具超发进邮件
public static final int infinity_tower_rewards_state_notify = 10033; // 无尽塔是否有奖励可领最终值
public static final int phone_chat_change_notify = 10034; // 手机新聊天变化
public static final int character_fragments_overflow_change_notify = 10035; // 角色碎片溢出
public static final int activity_login_rewards_notify = 10036; // 七日登录活动通知
public static final int tower_book_fate_card_collect_notify = 10037; // 新获得的命运卡数据
public static final int tower_book_fate_card_reward_notify = 10038; // 命运卡图鉴奖励变化
public static final int region_boss_level_challenge_ticket_notify = 10039; // 区域boss挑战模式门票变更通知最终值
public static final int honor_change_notify = 10040; // 荣誉称号变更通知(最终值,多个Notify以最后一个为准)
public static final int activity_mining_daily_reward_notify = 10041; // 挖格子每日奖励发放通知
public static final int activity_mining_supplement_reward_notify = 10042; // 挖格子活动开启后登录,补发每日奖励通知
public static final int activity_mining_energy_convert_notify = 10043; // 挖格子活动消耗体力转换道具通知
public static final int notice_change_notify = 10044; // 跑马灯公告变更通知
public static final int activity_state_change_notify = 10045; // 活动状态变化通知
public static final int activity_joint_drill_refresh_ticket_notify = 10046; // 总力战门票刷新通知
public static final int force_update_notify = 10047; // 强制更新通知,当版本号小于此通知版本时,择期进行弹窗更新,注意,此消息可能重复发送
public static final int player_head_icon_change_notify = 10048; // 当玩家切换性别且设置的头像为主角头像时,改通知会下发
public static final int activity_mining_enter_layer_notify = 10049; // 挖格子活动跳层/刷新本层数据
public static final int story_set_state_notify = 10050; // 故事集活动红点
public static final int vampire_survivor_new_season_notify = 10051; // 吸血鬼新赛季开启通知
public static final int order_collected_notify = 10052; // 离线期间的订单已发放到账
public static final int activity_mining_apply_req = 11001; // 申请进入挖格子活动
public static final int activity_mining_apply_succeed_ack = 11002; // 申请进入挖格子活动成功
public static final int activity_mining_apply_failed_ack = 11003; // 申请进入挖格子活动失败
public static final int activity_mining_dig_req = 11004; // 挖格子
public static final int activity_mining_dig_succeed_ack = 11005; // 挖格子成功
public static final int activity_mining_dig_failed_ack = 11006; // 挖格子失败
public static final int activity_mining_move_to_next_layer_req = 11007; // 挖格子进入下一层
public static final int activity_mining_move_to_next_layer_succeed_ack = 11008; // 挖格子进入下一层成功
public static final int activity_mining_move_to_next_layer_failed_ack = 11009; // 挖格子进入下一层失败
public static final int activity_mining_story_reward_receive_req = 11010; // 请求领取挖格子活动剧情奖励
public static final int activity_mining_story_reward_receive_succeed_ack = 11011; // 求领取挖格子活动剧情奖励成功
public static final int activity_mining_story_reward_receive_failed_ack = 11012; // 求领取挖格子活动剧情奖励失败
public static final int activity_mining_quest_reward_receive_req = 11013; // 挖格子领取任务奖励
public static final int activity_mining_quest_reward_receive_succeed_ack = 11014; // 挖格子领取任务奖励成功
public static final int activity_mining_quest_reward_receive_failed_ack = 11015; // 求挖格子领取任务奖励失败
public static final int score_boss_apply_req = 11101; // 请求挑战积分boss
public static final int score_boss_apply_succeed_ack = 11102; // 请求挑战积分boss成功
public static final int score_boss_apply_failed_ack = 11103; // 请求挑战积分boss失败
public static final int score_boss_settle_req = 11104; // 挑战积分boss结算
public static final int score_boss_settle_succeed_ack = 11105; // 挑战积分boss结算成功
public static final int score_boss_settle_failed_ack = 11106; // 挑战积分boss结算失败
public static final int score_boss_rank_req = 11107; // 请求积分boss排行榜
public static final int score_boss_rank_succeed_ack = 11108; // 请求积分boss排行榜成功
public static final int score_boss_rank_failed_ack = 11109; // 请求积分boss失败
public static final int score_boss_star_reward_receive_req = 11110; // 挑战积分boss结算
public static final int score_boss_star_reward_receive_succeed_ack = 11111; // 挑战积分boss结算成功
public static final int score_boss_star_reward_receive_failed_ack = 11112; // 挑战积分boss结算失败
public static final int score_boss_info_req = 11113; // 请求积分挑战boss信息
public static final int score_boss_info_succeed_ack = 11114; // 请求积分挑战boss信息成功
public static final int score_boss_info_failed_ack = 11115; // 请求积分挑战boss信息失败
public static final int activity_cookie_settle_req = 11201; // 曲奇工坊结算请求
public static final int activity_cookie_settle_succeed_ack = 11202; // 曲奇工坊结算成功; 如果是首通返回首通奖励
public static final int activity_cookie_settle_failed_ack = 11203; // 曲奇工坊结算失败
public static final int activity_cookie_quest_reward_receive_req = 11204; // 曲奇工坊领取任务奖励
public static final int activity_cookie_quest_reward_receive_succeed_ack = 11205; // 曲奇工坊领取任务奖励成功
public static final int activity_cookie_quest_reward_receive_failed_ack = 11206; // 求曲奇工坊领取任务奖励失败
}
@@ -0,0 +1,55 @@
package emu.nebula.net;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
import emu.nebula.GameConstants;
import emu.nebula.Nebula;
import emu.nebula.util.JsonUtils;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
public class NetMsgIdUtils {
private static Int2ObjectMap<String> msgIdMap;
static {
msgIdMap = new Int2ObjectOpenHashMap<>();
Field[] fields = NetMsgId.class.getFields();
for (Field f : fields) {
if (f.getType().equals(int.class)) {
try {
msgIdMap.put(f.getInt(null), f.getName());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public static String getMsgIdName(int msgId) {
if (msgId <= 0) return "UNKNOWN";
return msgIdMap.getOrDefault(msgId, "UNKNOWN");
}
@SuppressWarnings("unused")
public static void dumpPacketIds() {
try (FileWriter writer = new FileWriter("./MsgIds_" + GameConstants.VERSION + ".json")) {
// Create sorted tree map
Map<Integer, String> packetIds = msgIdMap.int2ObjectEntrySet().stream()
.filter(e -> e.getIntKey() > 0)
.collect(Collectors.toMap(Int2ObjectMap.Entry::getIntKey, Int2ObjectMap.Entry::getValue, (k, v) -> v, TreeMap::new));
// Write to file
writer.write(JsonUtils.encode(packetIds));
Nebula.getLogger().info("Dumped packet ids.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
@@ -0,0 +1,122 @@
package emu.nebula.server;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import emu.nebula.Config.HttpServerConfig;
import emu.nebula.Nebula;
import emu.nebula.Nebula.ServerType;
import emu.nebula.server.routes.*;
import io.javalin.Javalin;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
import lombok.Getter;
@Getter
public class HttpServer {
private final Javalin app;
private ServerType type;
private boolean started;
public HttpServer(ServerType type) {
this.app = Javalin.create();
this.type = type;
this.addRoutes();
}
public HttpServerConfig getServerConfig() {
return Nebula.getConfig().getHttpServer();
}
private HttpConnectionFactory getHttpFactory() {
HttpConfiguration httpsConfig = new HttpConfiguration();
SecureRequestCustomizer src = new SecureRequestCustomizer();
src.setSniHostCheck(false);
httpsConfig.addCustomizer(src);
return new HttpConnectionFactory(httpsConfig);
}
private SslContextFactory.Server getSSLContextFactory() {
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
sslContextFactory.setKeyStorePath(Nebula.getConfig().getKeystore().getPath());
sslContextFactory.setKeyStorePassword(Nebula.getConfig().getKeystore().getPassword());
sslContextFactory.setSniRequired(false);
sslContextFactory.setRenegotiationAllowed(false);
return sslContextFactory;
}
// Start server
public void start() {
if (this.started) return;
this.started = true;
// Http server
if (getServerConfig().isUseSSL()) {
ServerConnector sslConnector = new ServerConnector(getApp().jettyServer().server(), getSSLContextFactory(), getHttpFactory());
sslConnector.setHost(getServerConfig().getBindAddress());
sslConnector.setPort(getServerConfig().getBindPort());
getApp().jettyServer().server().addConnector(sslConnector);
getApp().start();
} else {
getApp().start(getServerConfig().getBindAddress(), getServerConfig().getBindPort());
}
// Done
Nebula.getLogger().info("Http Server started on " + getServerConfig().getBindPort());
}
// Server endpoints
private void addRoutes() {
// Add routes
if (this.getType().runLogin()) {
this.addLoginServerRoutes();
}
if (this.getType().runGame()) {
this.addGameServerRoutes();
}
// Exception handler
getApp().exception(Exception.class, (e, _) -> {
e.printStackTrace();
});
// Fallback handler
getApp().error(404, this::notFoundHandler);
}
private void addLoginServerRoutes() {
// https://en-sdk-api.yostarplat.com/
getApp().post("/common/config", new CommonConfigHandler(this));
getApp().post("/common/version", new HttpJsonResponse("{\"Code\":200,\"Data\":{\"Agreement\":[{\"Version\":\"0.1\",\"Type\":\"user_agreement\",\"Title\":\"用户协议\",\"Content\":\"\",\"Lang\":\"en\"},{\"Version\":\"0.1\",\"Type\":\"privacy_agreement\",\"Title\":\"隐私政策\",\"Content\":\"\",\"Lang\":\"en\"}],\"ErrorCode\":\"4.4\"},\"Msg\":\"OK\"}"));
getApp().post("/user/detail", new UserLoginHandler());
getApp().post("/user/set", new UserSetDataHandler());
getApp().post("/user/login", new UserLoginHandler());
getApp().post("/user/quick-login", new UserLoginHandler());
getApp().post("/yostar/get-auth", new GetAuthHandler());
getApp().post("/yostar/send-code", new HttpJsonResponse("{\"Code\":200,\"Data\":{},\"Msg\":\"OK\"}")); // Dummy handler
// https://nova-static.stellasora.global/
getApp().get("/meta/serverlist.html", new MetaServerlistHandler(this));
getApp().get("/meta/win.html", new MetaWinHandler());
}
private void addGameServerRoutes() {
getApp().post("/agent-zone-1/", new AgentZoneHandler());
}
private void notFoundHandler(Context ctx) {
ctx.status(404);
ctx.contentType(ContentType.APPLICATION_JSON);
ctx.result("{}");
}
}
@@ -0,0 +1,17 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.none)
public class Handler extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Template
return null;
}
}
@@ -0,0 +1,19 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.Public.Achievements;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.achievement_info_req)
public class HandlerAchievementInfoReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
var rsp = Achievements.newInstance();
return this.encodeMsg(NetMsgId.achievement_info_succeed_ack, rsp);
}
}
@@ -0,0 +1,27 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.ActivityDetail.ActivityMsg;
import emu.nebula.proto.ActivityDetail.ActivityResp;
import emu.nebula.proto.Public.ActivityTrial;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.activity_detail_req)
public class HandlerActivityDetailReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
var rsp = ActivityResp.newInstance();
var activity = ActivityMsg.newInstance()
.setId(700101)
.setTrial(ActivityTrial.newInstance());
rsp.addList(activity);
return this.encodeMsg(NetMsgId.activity_detail_succeed_ack, rsp);
}
}
@@ -0,0 +1,16 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.agent_apply_req)
public class HandlerAgentApplyReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
return this.encodeMsg(NetMsgId.agent_apply_failed_ack);
}
}
@@ -0,0 +1,16 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.battle_pass_info_req)
public class HandlerBattlePassInfoReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
return this.encodeMsg(NetMsgId.battle_pass_info_failed_ack);
}
}
@@ -0,0 +1,33 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.Public.UI32;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.char_advance_req)
public class HandlerCharAdvanceReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
var req = UI32.parseFrom(message);
// Get character
var character = session.getPlayer().getCharacters().getCharacterById(req.getValue());
if (character == null) {
return this.encodeMsg(NetMsgId.char_advance_failed_ack);
}
// Advance character
var change = character.advance();
if (change == null) {
return this.encodeMsg(NetMsgId.char_advance_failed_ack);
}
return this.encodeMsg(NetMsgId.char_advance_succeed_ack, change.toProto());
}
}
@@ -0,0 +1,34 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.CharSkillUpgrade.CharSkillUpgradeReq;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.char_skill_upgrade_req)
public class HandlerCharSkillUpgradeReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
var req = CharSkillUpgradeReq.parseFrom(message);
// Get character
var character = session.getPlayer().getCharacters().getCharacterById(req.getCharId());
if (character == null) {
return this.encodeMsg(NetMsgId.char_skill_upgrade_failed_ack);
}
// Advance character
int index = req.getIndex() - 1; // Lua indexes start at 1
var change = character.upgradeSkill(index);
if (change == null) {
return this.encodeMsg(NetMsgId.char_skill_upgrade_failed_ack);
}
return this.encodeMsg(NetMsgId.char_skill_upgrade_succeed_ack, change.toProto());
}
}
@@ -0,0 +1,43 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.CharUpgrade.CharUpgradeReq;
import emu.nebula.proto.CharUpgrade.CharUpgradeResp;
import emu.nebula.net.HandlerId;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.char_upgrade_req)
public class HandlerCharUpgradeReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request
var req = CharUpgradeReq.parseFrom(message);
// Get character
var character = session.getPlayer().getCharacters().getCharacterById(req.getCharId());
if (character == null) {
return this.encodeMsg(NetMsgId.char_upgrade_failed_ack);
}
// Upgrade character
var params = ItemParamMap.fromTemplates(req.getItems());
var change = character.upgrade(params);
if (change == null) {
return this.encodeMsg(NetMsgId.char_upgrade_failed_ack);
}
// Create response
var rsp = CharUpgradeResp.newInstance()
.setChange(change.toProto())
.setLevel(character.getLevel())
.setExp(character.getExp());
return this.encodeMsg(NetMsgId.char_upgrade_succeed_ack, rsp);
}
}
@@ -0,0 +1,16 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.client_event_report_req)
public class HandlerClientEventReportReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
return this.encodeMsg(NetMsgId.client_event_report_succeed_ack);
}
}
@@ -0,0 +1,40 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.DiscPromote.DiscPromoteReq;
import emu.nebula.proto.DiscPromote.DiscPromoteResp;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.disc_promote_req)
public class HandlerDiscPromoteReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request
var req = DiscPromoteReq.parseFrom(message);
// Get character
var disc = session.getPlayer().getCharacters().getDiscById(req.getId());
if (disc == null) {
return this.encodeMsg(NetMsgId.disc_promote_failed_ack);
}
// Advance character
var change = disc.promote();
if (change == null) {
return this.encodeMsg(NetMsgId.disc_promote_failed_ack);
}
// Build request
var rsp = DiscPromoteResp.newInstance()
.setPhase(disc.getPhase())
.setChange(change.toProto());
return this.encodeMsg(NetMsgId.disc_promote_succeed_ack, rsp);
}
}
@@ -0,0 +1,16 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.disc_read_reward_receive_req)
public class HandlerDiscReadRewardReceiveReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
return this.encodeMsg(NetMsgId.disc_read_reward_receive_failed_ack);
}
}
@@ -0,0 +1,43 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.DiscStrengthen.DiscStrengthenReq;
import emu.nebula.proto.DiscStrengthen.DiscStrengthenResp;
import emu.nebula.net.HandlerId;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.disc_strengthen_req)
public class HandlerDiscStrengthenReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request
var req = DiscStrengthenReq.parseFrom(message);
// Get character
var disc = session.getPlayer().getCharacters().getDiscById(req.getId());
if (disc == null) {
return this.encodeMsg(NetMsgId.disc_strengthen_failed_ack);
}
// Upgrade character
var params = ItemParamMap.fromItemInfos(req.getItems());
var change = disc.upgrade(params);
if (change == null) {
return this.encodeMsg(NetMsgId.disc_strengthen_failed_ack);
}
// Create response
var rsp = DiscStrengthenResp.newInstance()
.setChange(change.toProto())
.setLevel(disc.getLevel())
.setExp(disc.getExp());
return this.encodeMsg(NetMsgId.disc_strengthen_succeed_ack, rsp);
}
}
@@ -0,0 +1,19 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.FriendListGet.FriendListGetResp;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.friend_list_get_req)
public class HandlerFriendListGetReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
var rsp = FriendListGetResp.newInstance();
return this.encodeMsg(NetMsgId.friend_list_get_succeed_ack, rsp);
}
}
@@ -0,0 +1,21 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.GachaInformation.GachaInformationResp;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.gacha_information_req)
public class HandlerGachaInformationReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
var rsp = GachaInformationResp.newInstance();
// TODO
return this.encodeMsg(NetMsgId.gacha_information_succeed_ack, rsp);
}
}
@@ -0,0 +1,19 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.GachaInformation.GachaInformationResp;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.gacha_newbie_info_req)
public class HandlerGachaNewbieInfoReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
var rsp = GachaInformationResp.newInstance();
return this.encodeMsg(NetMsgId.gacha_newbie_info_succeed_ack, rsp);
}
}
@@ -0,0 +1,21 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.GachaNewbieObtain.GachaNewbieObtainReq;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.gacha_newbie_obtain_req)
public class HandlerGachaNewbieObtainReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
@SuppressWarnings("unused")
var req = GachaNewbieObtainReq.parseFrom(message);
// TODO
return this.encodeMsg(NetMsgId.gacha_newbie_obtain_failed_ack);
}
}
@@ -0,0 +1,41 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.GachaNewbieSpin.GachaNewbieSpinResp;
import emu.nebula.proto.GachaSpin.GachaSpinReq;
import emu.nebula.util.Utils;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import emu.nebula.net.HandlerId;
import emu.nebula.data.GameData;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.gacha_newbie_spin_req)
public class HandlerGachaNewbieSpinReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
@SuppressWarnings("unused")
var req = GachaSpinReq.parseFrom(message);
// Temp
var list = new IntArrayList();
for (var d : GameData.getCharacterDataTable()) {
if (d.getGrade() == 1) {
list.add(d.getId());
}
}
//
var rsp = GachaNewbieSpinResp.newInstance();
for (int i = 0; i < 10; i++) {
int id = Utils.randomElement(list);
rsp.addCards(id);
}
return this.encodeMsg(NetMsgId.gacha_newbie_spin_succeed_ack, rsp);
}
}
@@ -0,0 +1,52 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.GachaSpin.GachaCard;
import emu.nebula.proto.GachaSpin.GachaSpinReq;
import emu.nebula.proto.GachaSpin.GachaSpinResp;
import emu.nebula.proto.Public.ItemTpl;
import emu.nebula.util.Utils;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import emu.nebula.net.HandlerId;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.gacha_spin_req)
public class HandlerGachaSpinReq extends NetHandler {
@SuppressWarnings("unused")
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
var req = GachaSpinReq.parseFrom(message);
// Temp
var list = new IntArrayList();
for (var def : GameData.getCharacterDataTable()) {
if (def.getGrade() == 1 && def.isAvailable()) {
list.add(def.getId());
}
}
// Build response
var rsp = GachaSpinResp.newInstance()
.setTime(Nebula.getCurrentTime());
rsp.getMutableChange();
rsp.getMutableNextPackage();
for (int i = 0; i < 10; i++) {
int id = Utils.randomElement(list);
var card = GachaCard.newInstance()
.setCard(ItemTpl.newInstance().setTid(id).setQty(1));
rsp.addCards(card);
}
return this.encodeMsg(NetMsgId.gacha_spin_succeed_ack, rsp);
}
}
@@ -0,0 +1,61 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.Ike.IKEReq;
import emu.nebula.proto.Ike.IKEResp;
import emu.nebula.util.Utils;
import emu.nebula.net.HandlerId;
import emu.nebula.Nebula;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.ike_req)
public class HandlerIkeReq extends NetHandler {
@Override
public boolean requireSession() {
return false;
}
@Override
public boolean requirePlayer() {
return false;
}
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Make sure we dont already have a session
if (session != null) {
return this.encodeMsg(NetMsgId.ike_failed_ack);
}
// Parse
var req = IKEReq.parseFrom(message);
// Create session
session = new GameSession();
session.setClientKey(req.getPubKey());
session.generateServerKey();
session.calculateKey();
// Register session to game context
Nebula.getGameContext().generateSessionToken(session);
// Create response
var rsp = IKEResp.newInstance()
.setToken(session.getToken())
.setCipher(1) // 0 = gcm, 1 = chacha20
.setServerTs(Nebula.getCurrentTime())
.setPubKey(session.getServerPublicKey());
// Debug
Nebula.getLogger().info("Client Public: " + Utils.base64Encode(session.getClientPublicKey()));
Nebula.getLogger().info("Server Public: " + Utils.base64Encode(session.getServerPublicKey()));
Nebula.getLogger().info("Server Private: " + Utils.base64Encode(session.getServerPrivateKey()));
Nebula.getLogger().info("Key: " + Utils.base64Encode(session.getKey()));
// Encode and send to client
return this.encodeMsg(NetMsgId.ike_succeed_ack, rsp);
}
}
@@ -0,0 +1,24 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.Public.Mails;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.mail_list_req)
public class HandlerMailListReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Build mail list proto
var rsp = Mails.newInstance();
for (var mail : session.getPlayer().getMailbox()) {
rsp.addList(mail.toProto());
}
return this.encodeMsg(NetMsgId.mail_list_succeed_ack, rsp);
}
}
@@ -0,0 +1,35 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.MailPin.MailPinRequest;
import emu.nebula.net.HandlerId;
import emu.nebula.game.mail.GameMail;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.mail_pin_req)
public class HandlerMailPinReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request
var req = MailPinRequest.parseFrom(message);
// Pin mail
GameMail mail = session.getPlayer().getMailbox().pinMail(req.getId(), req.getFlag(), req.hasPin());
// Sanity check
if (mail == null) {
return this.encodeMsg(NetMsgId.mail_pin_failed_ack);
}
// Build response
var rsp = MailPinRequest.newInstance()
.setId(mail.getId())
.setFlag(mail.getFlag())
.setPin(mail.isPin());
return this.encodeMsg(NetMsgId.mail_pin_succeed_ack, rsp);
}
}
@@ -0,0 +1,26 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.Public.MailRequest;
import emu.nebula.proto.Public.UI32;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.mail_read_req)
public class HandlerMailReadReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
var req = MailRequest.parseFrom(message);
boolean result = session.getPlayer().getMailbox().readMail(req.getId(), req.getFlag());
if (!result) {
return this.encodeMsg(NetMsgId.mail_read_failed_ack);
}
return this.encodeMsg(NetMsgId.mail_read_succeed_ack, UI32.newInstance().setValue(req.getId()));
}
}
@@ -0,0 +1,36 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.MailRecv.MailRecvResp;
import emu.nebula.proto.Public.MailRequest;
import it.unimi.dsi.fastutil.ints.IntList;
import emu.nebula.net.HandlerId;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.mail_recv_req)
public class HandlerMailRecvReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request
var req = MailRequest.parseFrom(message);
// Claim mail
PlayerChangeInfo changes = session.getPlayer().getMailbox().recvMail(session.getPlayer(), req.getId());
// Build response
var rsp = MailRecvResp.newInstance()
.setItems(changes.toProto());
var recvList = (IntList) changes.getExtraData();
for (int id : recvList) {
rsp.addIds(id);
}
return this.encodeMsg(NetMsgId.mail_recv_succeed_ack, rsp);
}
}
@@ -0,0 +1,32 @@
package emu.nebula.server.handlers;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.MailRemove.MailRemoveResp;
import emu.nebula.proto.Public.MailRequest;
import it.unimi.dsi.fastutil.ints.IntList;
import emu.nebula.net.HandlerId;
import emu.nebula.net.GameSession;
@HandlerId(NetMsgId.mail_remove_req)
public class HandlerMailRemoveReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request
var req = MailRequest.parseFrom(message);
// Claim mail
IntList removed = session.getPlayer().getMailbox().removeMail(session.getPlayer(), req.getId());
// Build response
var rsp = MailRemoveResp.newInstance();
for (int id : removed) {
rsp.addIds(id);
}
return this.encodeMsg(NetMsgId.mail_remove_succeed_ack, rsp);
}
}

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