Initial commit

This commit is contained in:
Melledy 2023-09-25 06:03:09 -07:00
commit 48b267cecd
214 changed files with 11265 additions and 0 deletions

75
.gitignore vendored Normal file
View File

@ -0,0 +1,75 @@
# Compiled class file
*.class
#idea
*.idea
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.war
*.nar
*.ear
*.zip
*.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
# Ignore Gradle project-specific cache directory
.gradle
# Ignore Gradle build output directory
build/
out/
# Ignore Gradle properties
gradle.properties
# Eclipse
.project
.classpath
.settings
.metadata
.properties
bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
.loadpath
.recommenders
# VSCode
.vscode
# lombok
/.apt_generated/
# macOS
.DS_Store
.directory
# Lunar Rail generated/resource/log folders
src/generated
/resources
/logs
# Lunar Rail compiled
/*.jar
/*.sh
# Lunar Rail extra
Star Rail Handbook.txt
config.json
*.mv
*.exe
Test.java

31
README.md Normal file
View File

@ -0,0 +1,31 @@
# Lunar Rail
A WIP server emulator for version 1.3.0 of a certain turn based anime game.
# Running the server and client
### Prerequisites
* Java 17 JDK
### Recommended
* Mongodb (4.0+)
### Starting up the server
1. Compile the server with `./gradlew jar`
2. Create a folder named `resources` in your server directory, you will need to downlaod `TextMap` and `ExcelBin` folders which you can get from a repo like [https://github.com/Dimbreath/StarRailData](https://github.com/Dimbreath/StarRailData) into your resources folder.
3. Run the server with `java -jar LunarRail.jar`. Lunar Rail comes with a built in internal mongo server for its database, so no Mongodb installation is required. However, it is highly recomended to install Mongodb anyways.
### Connecting with the client
1. Login with the client to an official server at least once to download game data.
2. If you are using the provided keystore, you will need to install and have [Fiddler](https://www.telerik.com/fiddler) running. Make sure fiddler is set to decrypt https traffic.
3. Set your hosts file to redirect at least `hkrpg-sdk-os-static.hoyoverse.com` and `globaldp-prod-os01.starrails.com` to your http (dispatch) server ip.
### Server console commands
`/account create [username] {playerid}` - Creates an account with the specified username and the in-game uid for that account. The playerid parameter is optional and will be auto generated if not set.
### In-Game commands
There is a dummy user named "Server" in every player's friends list that you can message to use commands. Commands also work in other chat rooms, such as private/team chats.
`!spawn [monster id] [stage id]`
`!give [item id] [amount]`

148
build.gradle Normal file
View File

@ -0,0 +1,148 @@
/*
* This file was generated by the Gradle 'init' task.
*
* This generated file contains a sample Java project to get you started.
* For more details take a look at the Java Quickstart chapter in the Gradle
* User Manual available at https://docs.gradle.org/5.6.3/userguide/tutorial_java_projects.html
*/
plugins {
// Apply the application plugin to add support for building a CLI application
id 'application'
// Apply the java plugin to add support for Java
id 'java'
// Protoc plugin
id 'com.google.protobuf' version '0.8.19'
id 'eclipse'
}
compileJava.options.encoding = "UTF-8"
compileTestJava.options.encoding = "UTF-8"
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
repositories {
mavenCentral()
jcenter()
}
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.24.3'
}
plugins {
quickbuf {
artifact = 'us.hebi.quickbuf:protoc-gen-quickbuf:1.3.1'
}
}
generateProtoTasks {
all().each { task ->
task.builtins {
remove java
}
task.plugins {
quickbuf {
option 'store_unknown_fields=true'
outputSubDir = ''
}
}
}
}
generatedFilesBaseDir = "$projectDir/src/generated/"
}
dependencies {
implementation fileTree(dir: 'lib', include: ['*.jar'])
implementation group: 'org.slf4j', name: 'slf4j-api', version: '2.0.9'
implementation group: 'ch.qos.logback', name: 'logback-core', version: '1.4.11'
implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.4.11'
implementation group: 'it.unimi.dsi', name: 'fastutil-core', version: '8.5.12'
implementation group: 'org.reflections', name: 'reflections', version: '0.10.2'
implementation group: 'com.google.code.gson', name: 'gson', version: '2.10.1'
implementation group: 'us.hebi.quickbuf', name: 'quickbuf-runtime', version: '1.3.1'
implementation group: 'io.javalin', name: 'javalin', version: '5.6.2'
implementation group: 'io.netty', name: 'netty-common', version: '4.1.97.Final'
implementation group: 'io.netty', name: 'netty-handler', version: '4.1.97.Final'
implementation group: 'io.netty', name: 'netty-transport-native-epoll', version: '4.1.97.Final'
implementation group: 'io.netty', name: 'netty-transport-native-kqueue', version: '4.1.97.Final'
implementation group: 'dev.morphia.morphia', name: 'morphia-core', version: '2.3.8'
implementation group: 'de.bwaldvogel', name: 'mongo-java-server', version: '1.44.0'
implementation group: 'de.bwaldvogel', name: 'mongo-java-server-h2-backend', version: '1.44.0'
protobuf files('proto/')
compileOnly 'org.projectlombok:lombok:1.18.30'
annotationProcessor 'org.projectlombok:lombok:1.18.30'
}
configurations.all {
exclude group: 'org.slf4j', module: 'slf4j'
}
clean {
delete protobuf.generatedFilesBaseDir
}
application {
// Define the main class for the application
mainClassName = 'emu.lunarcore.LunarRail'
}
jar {
exclude '*.proto'
manifest {
attributes 'Main-Class': 'emu.lunarcore.LunarRail'
}
jar {
archiveBaseName = 'LunarRail'
archiveVersion = ''
}
from {
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
}
duplicatesStrategy = DuplicatesStrategy.INCLUDE
from('src/main/java') {
include '*.xml'
}
getDestinationDirectory().set(file("."))
}
sourceSets {
main {
proto {
srcDir 'src/generated'
}
java {
srcDir 'src/main/java'
}
}
}
eclipse {
classpath {
file.whenMerged { cp ->
cp.entries.add( new org.gradle.plugins.ide.eclipse.model.SourceFolder('src/generated/main/', null) )
}
}
}
processResources {
dependsOn "generateProto"
}

27
data/Banners.json Normal file
View File

@ -0,0 +1,27 @@
[
{
"id": 1001,
"gachaType": "Normal",
"beginTime": 0,
"endTime": 0,
"rateUpItems5": [],
"rateUpItems4": []
},
{
"id": 2009,
"gachaType": "AvatarUp",
"beginTime": 0,
"endTime": 1924992000,
"rateUpItems5": [1213],
"rateUpItems4": [1207, 1001, 1009]
},
{
"id": 3009,
"gachaType": "WeaponUp",
"beginTime": 0,
"endTime": 1924992000,
"eventChance": 75,
"rateUpItems5": [1208],
"rateUpItems4": [1106, 1109, 1110]
}
]

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

234
gradlew vendored Normal file
View File

@ -0,0 +1,234 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View File

@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

BIN
keystore.p12 Normal file

Binary file not shown.

BIN
lib/kcp.jar Normal file

Binary file not shown.

10
settings.gradle Normal file
View File

@ -0,0 +1,10 @@
/*
* This file was generated by the Gradle 'init' task.
*
* The settings file is used to specify which projects to include in your build.
*
* Detailed information about configuring a multi-project build in Gradle can be found
* in the user manual at https://docs.gradle.org/5.6.3/userguide/multi_project_builds.html
*/
rootProject.name = 'Lunar Rail'

View File

@ -0,0 +1,79 @@
package emu.lunarcore;
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 ServerConfig httpServer = new ServerConfig("127.0.0.1", 443);
public GameServerConfig gameServer = new GameServerConfig("127.0.0.1", 23301);
public DownloadData downloadData = new DownloadData();
public String resourceDir = "./resources";
public String dataDir = "./data";
@Getter
public static class DatabaseInfo {
public String uri = "mongodb://localhost:27017";
public String collection = "lunarrail";
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 = "lunar";
}
@Getter
public static class ServerConfig {
public String bindAddress = "0.0.0.0";
public String publicAddress = "127.0.0.1";
public int port;
public boolean useSSL = true;
public ServerConfig(String address, int port) {
this.publicAddress = address;
this.port = port;
}
public String getDisplayAddress() {
return (useSSL ? "https" : "http") + "://" + publicAddress + ":" + port;
}
}
@Getter
public static class GameServerConfig extends ServerConfig {
public String id = "lunar_rail_test";
public String name = "Test";
public String description = "Test Server";
public GameServerConfig(String address, int port) {
super(address, port);
}
}
@Getter
public static class DownloadData {
public String assetBundleUrl = null;
public String exResourceUrl = null;
public String luaUrl = null;
public String ifixUrl = null;
}
}

View File

@ -0,0 +1,22 @@
package emu.lunarcore;
import java.time.Instant;
import java.time.ZoneOffset;
public class GameConstants {
public static String VERSION = "1.3.0";
public static String MDK_VERSION = "5377911";
public static final ZoneOffset CURRENT_OFFSET = ZoneOffset.systemDefault().getRules().getOffset(Instant.now());
// Game
public static final String DEFAULT_NAME = "Trailblazer";
public static final int MAX_TRAILBLAZER_LEVEL = 70;
public static final int MAX_STAMINA = 240;
public static final int MAX_AVATARS_IN_TEAM = 4;
public static final int DEFAULT_TEAMS = 6;
// Custom
public static final int SERVER_CONSOLE_UID = 99;
public static final int EQUIPMENT_SLOT_ID = 100;
}

View File

@ -0,0 +1,160 @@
package emu.lunarcore;
import java.io.*;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import ch.qos.logback.classic.Logger;
import emu.lunarcore.commands.ServerCommands;
import emu.lunarcore.data.ResourceLoader;
import emu.lunarcore.database.DatabaseManager;
import emu.lunarcore.server.game.GameServer;
import emu.lunarcore.server.http.HttpServer;
import emu.lunarcore.util.Handbook;
import emu.lunarcore.util.JsonUtils;
import lombok.Getter;
public class LunarRail {
private static Logger log = (Logger) LoggerFactory.getLogger(LunarRail.class);
private static File configFile = new File("./config.json");
private static Config config;
@Getter private static DatabaseManager accountDatabase;
@Getter private static DatabaseManager gameDatabase;
@Getter private static HttpServer httpServer;
@Getter private static GameServer gameServer;
private static ServerType serverType = ServerType.BOTH;
// Load config first before doing anything
static {
LunarRail.loadConfig();
}
public static void main(String[] args) {
// Start Server
LunarRail.getLogger().info("Starting Lunar Rail...");
// Parse arguments
for (String arg : args) {
switch (arg) {
case "-dispatch":
serverType = ServerType.DISPATCH;
break;
case "-game":
serverType = ServerType.GAME;
break;
case "-database":
// Database only
DatabaseManager databaseManager = new DatabaseManager();
databaseManager.startInternalMongoServer(LunarRail.getConfig().getInternalMongoServer());
LunarRail.getLogger().info("Running local mongo server at " + databaseManager.getServer().getConnectionString());
// Console
LunarRail.startConsole();
return;
}
}
// Load resources
ResourceLoader.loadAll();
// Build handbook TODO
Handbook.generate();
// Start Database(s)
LunarRail.initDatabases();
// Start Servers TODO
httpServer = new HttpServer(serverType);
httpServer.start();
if (serverType.runGame()) {
gameServer = new GameServer(getConfig().getGameServer());
gameServer.start();
}
// Start console
LunarRail.startConsole();
}
public static Config getConfig() {
return config;
}
public static Logger getLogger() {
return log;
}
// Database
private static void initDatabases() {
accountDatabase = new DatabaseManager(LunarRail.getConfig().getAccountDatabase());
if (LunarRail.getConfig().useSameDatabase) {
gameDatabase = accountDatabase;
} else {
gameDatabase = new DatabaseManager(LunarRail.getConfig().getGameDatabase());
}
}
// Config
public static void loadConfig() {
try (FileReader file = new FileReader(configFile)) {
config = JsonUtils.loadToClass(file, Config.class);
} catch (Exception e) {
LunarRail.config = new Config();
}
saveConfig();
}
public static void saveConfig() {
try (FileWriter file = new FileWriter(configFile)) {
Gson gson = new GsonBuilder().setPrettyPrinting().serializeNulls().create();
file.write(gson.toJson(config));
} catch (Exception e) {
getLogger().error("Config save error");
}
}
// Server console
private static void startConsole() {
String input;
try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) {
while ((input = br.readLine()) != null) {
ServerCommands.handle(input);
}
} catch (Exception e) {
LunarRail.getLogger().error("Console error:", e);
}
}
// Server enums
public enum ServerType {
BOTH (true, true),
DISPATCH (true, false),
GAME (false, true);
private final boolean runDispatch;
private final boolean runGame;
private ServerType(boolean runDispatch, boolean runGame) {
this.runDispatch = runDispatch;
this.runGame = runGame;
}
public boolean runDispatch() {
return runDispatch;
}
public boolean runGame() {
return runGame;
}
}
}

View File

@ -0,0 +1,13 @@
package emu.lunarcore.commands;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface Command {
public String[] aliases() default "";
public int gmLevel() default 1;
public String desc() default "";
}

View File

@ -0,0 +1,177 @@
package emu.lunarcore.commands;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import emu.lunarcore.data.GameData;
import emu.lunarcore.data.excel.ItemExcel;
import emu.lunarcore.data.excel.NpcMonsterExcel;
import emu.lunarcore.data.excel.StageExcel;
import emu.lunarcore.game.inventory.GameItem;
import emu.lunarcore.game.player.Player;
import emu.lunarcore.game.scene.EntityMonster;
import emu.lunarcore.util.Position;
@SuppressWarnings("unused")
public class PlayerCommands {
private static HashMap<String, PlayerCommand> list = new HashMap<>();
static {
try {
// Look for classes
for (Class<?> cls : PlayerCommands.class.getDeclaredClasses()) {
// Get non abstract classes
if (!Modifier.isAbstract(cls.getModifiers())) {
Command commandAnnotation = cls.getAnnotation(Command.class);
PlayerCommand command = (PlayerCommand) cls.getConstructor().newInstance();
if (commandAnnotation != null) {
command.setLevel(commandAnnotation.gmLevel());
for (String alias : commandAnnotation.aliases()) {
if (alias.length() == 0) {
continue;
}
String commandName = "!" + alias;
list.put(commandName, command);
commandName = "/" + alias;
list.put(commandName, command);
}
}
String commandName = "!" + cls.getSimpleName().toLowerCase();
list.put(commandName, command);
commandName = "/" + cls.getSimpleName().toLowerCase();
list.put(commandName, command);
}
}
} catch (Exception e) {
}
}
public static void handle(Player player, String msg) {
String[] split = msg.split(" ");
// End if invalid
if (split.length == 0) {
return;
}
//
String first = split[0].toLowerCase();
PlayerCommand c = PlayerCommands.list.get(first);
if (c != null) {
// Execute
int len = Math.min(first.length() + 1, msg.length());
c.execute(player, msg.substring(len));
} else {
player.dropMessage("Error: Invalid command!");
}
}
public static abstract class PlayerCommand {
// GM level required to use this command
private int level;
protected int getLevel() { return this.level; }
protected void setLevel(int minLevel) { this.level = minLevel; }
// Main
public abstract void execute(Player player, String raw);
}
// ================ Commands ================
@Command(aliases = {"g", "item"}, desc = "/give [item id] [count] - Gives {count} amount of {item id}")
public static class Give extends PlayerCommand {
@Override
public void execute(Player player, String raw) {
String[] split = raw.split(" ");
int itemId = 0, count = 1;
try {
itemId = Integer.parseInt(split[0]);
} catch (Exception e) {
itemId = 0;
}
try {
count = Math.max(Math.min(Integer.parseInt(split[1]), Integer.MAX_VALUE), 1);
} catch (Exception e) {
count = 1;
}
// Give
ItemExcel itemData = GameData.getItemExcelMap().get(itemId);
GameItem item;
if (itemData == null) {
player.dropMessage("Error: Item data not found");
return;
}
if (itemData.isEquippable()) {
List<GameItem> items = new LinkedList<>();
for (int i = 0; i < count; i++) {
item = new GameItem(itemData);
//items.add(item);
player.getInventory().addItem(item);
}
// TODO add item hint packet
} else {
item = new GameItem(itemData, count);
player.getInventory().addItem(item);
// TODO add item hint packet
}
player.dropMessage("Giving you " + count + " of " + itemId);
}
}
/* Temporarily disabled as spawned monsters need
@Command(desc = "/spawn [monster id] [count] - Creates {count} amount of {item id}")
public static class Spawn extends PlayerCommand {
@Override
public void execute(Player player, String raw) {
String[] split = raw.split(" ");
int monsterId = 0, stageId = 2;
try {
monsterId = Integer.parseInt(split[0]);
} catch (Exception e) {
monsterId = 0;
}
try {
stageId = Integer.parseInt(split[1]);
} catch (Exception e) {
stageId = 2;
}
// TODO
NpcMonsterExcel excel = GameData.getNpcMonsterExcelMap().get(monsterId);
if (excel == null) {
player.dropMessage("Npc monster id not found!");
return;
}
StageExcel stage = GameData.getStageExcelMap().get(stageId);
if (stage == null) {
player.dropMessage("Stage id not found!");
return;
}
Position pos = player.getPos().clone();
pos.setX(pos.getX() + 50);
// Add to scene
EntityMonster monster = new EntityMonster(excel, stage, pos);
player.getScene().addMonster(monster);
}
}
*/
}

View File

@ -0,0 +1,119 @@
package emu.lunarcore.commands;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import emu.lunarcore.LunarRail;
import emu.lunarcore.util.Utils;
@SuppressWarnings("unused")
public class ServerCommands {
private static HashMap<String, ServerCommand> list = new HashMap<>();
static {
try {
// Look for classes
for (Class<?> cls : ServerCommands.class.getDeclaredClasses()) {
// Get non abstract classes
if (!Modifier.isAbstract(cls.getModifiers())) {
String commandName = cls.getSimpleName().toLowerCase();
list.put(commandName, (ServerCommand) cls.newInstance());
}
}
} catch (Exception e) {
}
}
public static void handle(String msg) {
String[] split = msg.split(" ");
// End if invalid
if (split.length == 0) {
return;
}
//
String first = split[0].toLowerCase();
ServerCommand c = ServerCommands.list.get(first);
if (c != null) {
// Execute
int len = Math.min(first.length() + 1, msg.length());
c.execute(msg.substring(len));
} else {
LunarRail.getLogger().info("Invalid command!");
}
}
public static abstract class ServerCommand {
public abstract void execute(String raw);
}
// ================ Commands ================
private static class Account extends ServerCommand {
@Override
public void execute(String raw) {
String[] split = raw.split(" ");
if (split.length < 2) {
LunarRail.getLogger().error("Invalid amount of args");
return;
}
emu.lunarcore.game.account.Account account = null;
String command = split[0].toLowerCase();
String username = split[1];
switch (command) {
case "create":
if (split.length < 2) { // Should be 3 if passwords were enabled
LunarRail.getLogger().error("Invalid amount of args");
return;
}
// Get password
//String password = split[2];
// Reserved player uid
int reservedUid = Utils.parseSafeInt(split[2]);
// Get acocunt from database
account = LunarRail.getAccountDatabase().getObjectByField(emu.lunarcore.game.account.Account.class, "username", username);
if (account == null) {
// Create account
//String hash = BCrypt.withDefaults().hashToString(12, password.toCharArray());
account = new emu.lunarcore.game.account.Account(username);
account.setReservedPlayerUid(reservedUid);
account.save();
LunarRail.getLogger().info("Account created");
} else {
LunarRail.getLogger().error("Account already exists");
}
break;
case "delete":
account = LunarRail.getAccountDatabase().getObjectByField(emu.lunarcore.game.account.Account.class, "name", username);
if (account == null) {
LunarRail.getLogger().info("Account doesnt exist");
return;
}
boolean success = LunarRail.getAccountDatabase().delete(account);
if (success) {
LunarRail.getLogger().info("Account deleted");
}
break;
}
}
}
}

View File

@ -0,0 +1,103 @@
package emu.lunarcore.data;
import java.lang.reflect.Field;
import emu.lunarcore.data.config.FloorInfo;
import emu.lunarcore.data.excel.*;
import emu.lunarcore.util.Utils;
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;
import lombok.Getter;
@SuppressWarnings("unused")
public class GameData {
// Excels
@Getter private static Int2ObjectMap<AvatarExcel> avatarExcelMap = new Int2ObjectOpenHashMap<>();
@Getter private static Int2ObjectMap<ItemExcel> itemExcelMap = new Int2ObjectOpenHashMap<>();
@Getter private static Int2ObjectMap<EquipmentExcel> equipExcelMap = new Int2ObjectOpenHashMap<>();
@Getter private static Int2ObjectMap<RelicExcel> relicExcelMap = new Int2ObjectOpenHashMap<>();
@Getter private static Int2ObjectMap<MonsterExcel> monsterExcelMap = new Int2ObjectOpenHashMap<>();
@Getter private static Int2ObjectMap<NpcMonsterExcel> npcMonsterExcelMap = new Int2ObjectOpenHashMap<>();
@Getter private static Int2ObjectMap<StageExcel> stageExcelMap = new Int2ObjectOpenHashMap<>();
@Getter private static Int2ObjectMap<MapEntranceExcel> mapEntranceExcelMap = new Int2ObjectOpenHashMap<>();
private static Int2ObjectMap<AvatarPromotionExcel> avatarPromotionExcelMap = new Int2ObjectOpenHashMap<>();
private static Int2ObjectMap<AvatarSkillTreeExcel> avatarSkillTreeExcelMap = new Int2ObjectOpenHashMap<>();
private static Int2ObjectMap<AvatarRankExcel> avatarRankExcelMap = new Int2ObjectOpenHashMap<>();
private static Int2ObjectMap<EquipmentPromotionExcel> equipmentPromotionExcelMap = new Int2ObjectOpenHashMap<>();
private static Int2ObjectMap<PlayerLevelExcel> playerLevelExcelMap = new Int2ObjectOpenHashMap<>();
private static Int2ObjectMap<ExpTypeExcel> expTypeExcelMap = new Int2ObjectOpenHashMap<>();
private static Int2ObjectMap<EquipmentExpTypeExcel> equipmentExpTypeExcelMap = new Int2ObjectOpenHashMap<>();
private static Int2ObjectMap<RelicExpTypeExcel> relicExpTypeExcelMap = new Int2ObjectOpenHashMap<>();
@Getter
private static Int2ObjectMap<RelicMainAffixExcel> relicMainAffixExcelMap = new Int2ObjectOpenHashMap<>();
private static Int2ObjectMap<RelicSubAffixExcel> relicSubAffixExcelMap = new Int2ObjectOpenHashMap<>();
// Configs (Bin)
@Getter private static Object2ObjectMap<String, FloorInfo> floorInfos = new Object2ObjectOpenHashMap<>();
public static Int2ObjectMap<?> getMapForExcel(Class<?> resourceDefinition) {
Int2ObjectMap<?> map = null;
try {
Field field = GameData.class.getDeclaredField(Utils.lowerCaseFirstChar(resourceDefinition.getSimpleName()) + "Map");
field.setAccessible(true);
map = (Int2ObjectMap<?>) field.get(null);
field.setAccessible(false);
} catch (Exception e) {
}
return map;
}
public static AvatarPromotionExcel getAvatarPromotionExcel(int id, int promotion) {
return avatarPromotionExcelMap.get((id << 8) + promotion);
}
public static AvatarSkillTreeExcel getAvatarSkillTreeExcel(int skill, int level) {
return avatarSkillTreeExcelMap.get((skill << 4) + level);
}
public static AvatarRankExcel getAvatarRankExcel(int rankId) {
return avatarRankExcelMap.get(rankId);
}
public static EquipmentPromotionExcel getEquipmentPromotionExcel(int id, int promotion) {
return equipmentPromotionExcelMap.get((id << 8) + promotion);
}
public static int getPlayerExpRequired(int level) {
var excel = playerLevelExcelMap.get(level);
return excel != null ? excel.getPlayerExp() : 0;
}
public static int getAvatarExpRequired(int expGroup, int level) {
var excel = expTypeExcelMap.get((expGroup << 16) + level);
return excel != null ? excel.getExp() : 0;
}
public static int getEquipmentExpRequired(int expGroup, int level) {
var excel = equipmentExpTypeExcelMap.get((expGroup << 16) + level);
return excel != null ? excel.getExp() : 0;
}
public static int getRelicExpRequired(int expGroup, int level) {
var excel = relicExpTypeExcelMap.get((expGroup << 16) + level);
return excel != null ? excel.getExp() : 0;
}
public static RelicSubAffixExcel getRelicSubAffixExcel(int groupId, int affixId) {
return relicSubAffixExcelMap.get((groupId << 8) + affixId);
}
public static FloorInfo getFloorInfo(int planeId, int floorId) {
return floorInfos.get("P" + planeId + "_F" + floorId);
}
}

View File

@ -0,0 +1,37 @@
package emu.lunarcore.data;
import java.util.ArrayList;
import java.util.List;
import emu.lunarcore.data.excel.RelicMainAffixExcel;
import emu.lunarcore.data.excel.RelicSubAffixExcel;
import emu.lunarcore.util.Utils;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
// Game data that is parsed by the server goes here
public class GameDepot {
private static Int2ObjectMap<List<RelicMainAffixExcel>> relicMainAffixDepot = new Int2ObjectOpenHashMap<>();
private static Int2ObjectMap<List<RelicSubAffixExcel>> relicSubAffixDepot = new Int2ObjectOpenHashMap<>();
public static void addRelicMainAffix(RelicMainAffixExcel affix) {
List<RelicMainAffixExcel> list = relicMainAffixDepot.computeIfAbsent(affix.getGroupID(), k -> new ArrayList<>());
list.add(affix);
}
public static void addRelicSubAffix(RelicSubAffixExcel affix) {
List<RelicSubAffixExcel> list = relicSubAffixDepot.computeIfAbsent(affix.getGroupID(), k -> new ArrayList<>());
list.add(affix);
}
public static RelicMainAffixExcel getRandomRelicMainAffix(int groupId) {
var list = relicMainAffixDepot.get(groupId);
if (list == null) return null;
return list.get(Utils.randomRange(0, list.size() - 1));
}
public static List<RelicSubAffixExcel> getRelicSubAffixList(int groupId) {
return relicSubAffixDepot.get(groupId);
}
}

View File

@ -0,0 +1,15 @@
package emu.lunarcore.data;
public abstract class GameResource implements Comparable<GameResource> {
public abstract int getId();
public void onLoad() {
}
@Override
public int compareTo(GameResource o) {
return this.getId() - o.getId();
}
}

View File

@ -0,0 +1,37 @@
package emu.lunarcore.data;
import java.lang.reflect.Type;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
public class ResourceDeserializers {
protected static class LunarRailDoubleDeserializer implements JsonDeserializer<Double> {
@Override
public Double deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (json.isJsonPrimitive()) {
return json.getAsDouble();
} else {
// FixPoint
var obj = json.getAsJsonObject();
return obj.get("Value").getAsDouble();
}
}
}
protected static class LunarRailHashDeserializer implements JsonDeserializer<Long> {
@Override
public Long deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (json.isJsonPrimitive()) {
return json.getAsLong();
} else {
// TextID
var obj = json.getAsJsonObject();
return obj.get("Hash").getAsLong();
}
}
}
}

View File

@ -0,0 +1,211 @@
package emu.lunarcore.data;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
import org.reflections.Reflections;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;
import emu.lunarcore.LunarRail;
import emu.lunarcore.data.ResourceDeserializers.LunarRailDoubleDeserializer;
import emu.lunarcore.data.ResourceDeserializers.LunarRailHashDeserializer;
import emu.lunarcore.data.config.FloorInfo;
import emu.lunarcore.data.config.FloorInfo.FloorGroupSimpleInfo;
import emu.lunarcore.data.config.GroupInfo;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
public class ResourceLoader {
private static boolean loaded = false;
// Special gson factory we create for loading resources
private static final Gson gson = new GsonBuilder()
.registerTypeAdapter(double.class, new LunarRailDoubleDeserializer())
.registerTypeAdapter(long.class, new LunarRailHashDeserializer())
.create();
// Load all resources
public static void loadAll() {
// Make sure we don't load more than once
if (loaded) return;
// Start loading resources
loadResources();
// Load floor infos after resources
loadFloorInfos();
// Done
loaded = true;
}
private static List<Class<?>> getResourceDefClasses() {
Reflections reflections = new Reflections(ResourceLoader.class.getPackage().getName());
Set<?> classes = reflections.getSubTypesOf(GameResource.class);
List<Class<?>> classList = new ArrayList<>(classes.size());
classes.forEach(o -> {
Class<?> c = (Class<?>) o;
if (c.getAnnotation(ResourceType.class) != null) {
classList.add(c);
}
});
classList.sort((a, b) -> b.getAnnotation(ResourceType.class).loadPriority().value() - a.getAnnotation(ResourceType.class).loadPriority().value());
return classList;
}
private static void loadResources() {
for (Class<?> resourceDefinition : getResourceDefClasses()) {
ResourceType type = resourceDefinition.getAnnotation(ResourceType.class);
if (type == null) {
continue;
}
@SuppressWarnings("rawtypes")
Int2ObjectMap map = GameData.getMapForExcel(resourceDefinition);
try {
loadFromResource(resourceDefinition, type, map);
} catch (Exception e) {
LunarRail.getLogger().error("Error loading resource file: " + Arrays.toString(type.name()), e);
}
}
}
@SuppressWarnings("rawtypes")
private static void loadFromResource(Class<?> c, ResourceType type, Int2ObjectMap map) throws Exception {
int count = 0;
for (String name : type.name()) {
count += loadFromResource(c, type, name, map);
}
LunarRail.getLogger().info("Loaded " + count + " " + c.getSimpleName() + "s.");
}
@SuppressWarnings({"rawtypes", "unchecked"})
private static <T> int loadFromResource(Class<T> c, ResourceType type, String fileName, Int2ObjectMap map) throws Exception {
String file = LunarRail.getConfig().getResourceDir() + "/ExcelOutput/" + fileName;
// Load reader from file
try (InputStreamReader fileReader = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)) {
// Setup variables
Stream<T> stream = null;
// Determine format of json
JsonElement json = JsonParser.parseReader(fileReader);
if (json.isJsonArray()) {
// Parse list
List<T> excels = gson.fromJson(json, TypeToken.getParameterized(List.class, c).getType());
stream = excels.stream();
} else if (json.isJsonObject()) {
// Check if object is map or a nested map
boolean isMap = true;
var it = json.getAsJsonObject().asMap().entrySet().iterator();
if (it.hasNext()) {
var it2 = it.next().getValue().getAsJsonObject().asMap().entrySet().iterator();
String key = it2.next().getKey();
try {
Integer.parseInt(key);
isMap = false;
} catch (Exception ex) {
}
}
// Parse json
if (isMap) {
// Map
Map<Integer, T> excels = gson.fromJson(json, TypeToken.getParameterized(Map.class, Integer.class, c).getType());
stream = excels.values().stream();
} else {
// Nested Map
Map<Integer, Map<Integer, T>> excels = gson.fromJson(json, TypeToken.getParameterized(Map.class, Integer.class, TypeToken.getParameterized(Map.class, Integer.class, c).getType()).getType());
stream = excels.values().stream().flatMap(m -> m.values().stream());
}
} else {
throw new Exception("Invalid excel file: " + fileName);
}
// Sanity check
if (stream == null) return 0;
// Mutable integer
AtomicInteger count = new AtomicInteger();
stream.forEach(o -> {
GameResource res = (GameResource) o;
res.onLoad();
count.getAndIncrement();
if (map != null) {
map.put(res.getId(), res);
}
});
return count.get();
}
}
// Might be better to cache
private static void loadFloorInfos() {
// Load floor infos
File floorDir = new File(LunarRail.getConfig().getResourceDir() + "/Config/LevelOutput/Floor/");
if (!floorDir.exists()) {
LunarRail.getLogger().warn("Floor infos are missing, please check your resources.");
return;
}
// Dump
for (File file : floorDir.listFiles()) {
try (FileReader reader = new FileReader(file)) {
FloorInfo floor = gson.fromJson(reader, FloorInfo.class);
String name = file.getName().substring(0, file.getName().indexOf('.'));
GameData.getFloorInfos().put(name, floor);
} catch (Exception e) {
e.printStackTrace();
}
}
// Load group infos
for (FloorInfo floor : GameData.getFloorInfos().values()) {
for (FloorGroupSimpleInfo simpleGroup : floor.getSimpleGroupList()) {
File file = new File(LunarRail.getConfig().getResourceDir() + "/" + simpleGroup.getGroupPath());
if (!file.exists()) {
continue;
}
// TODO optimize
try (FileReader reader = new FileReader(file)) {
GroupInfo group = gson.fromJson(reader, GroupInfo.class);
group.setId(simpleGroup.getID());
floor.getGroups().put(simpleGroup.getID(), group);
} catch (Exception e) {
e.printStackTrace();
}
}
// Post load callback to cache floor info
floor.onLoad();
}
// Done
LunarRail.getLogger().info("Loaded " + GameData.getFloorInfos().size() + " FloorInfos.");
}
}

View File

@ -0,0 +1,32 @@
package emu.lunarcore.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;
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;
}
}
}

View File

@ -0,0 +1,46 @@
package emu.lunarcore.data.common;
import com.google.gson.annotations.SerializedName;
import emu.lunarcore.proto.ItemCostOuterClass.ItemCost;
import lombok.Getter;
@Getter
public class ItemParam {
@SerializedName(value = "id", alternate = {"ItemId", "ItemID"})
private int id;
@SerializedName(value = "count", alternate = {"ItemCount", "ItemNum"})
private int count;
private ItemParamType type = ItemParamType.PILE;
public ItemParam() {
// Gson
}
public ItemParam(ItemParamType type, int id, int count) {
this.type = type;
this.id = id;
this.count = count;
}
public ItemParam(ItemCost itemCost) {
if (itemCost.hasPileItem()) {
this.id = itemCost.getPileItem().getItemId();
this.count = itemCost.getPileItem().getItemNum();
} else if (itemCost.hasEquipmentUniqueId()) {
this.type = ItemParamType.UNIQUE;
this.id = itemCost.getEquipmentUniqueId();
this.count = 1;
} else if (itemCost.hasRelicUniqueId()) {
this.type = ItemParamType.UNIQUE;
this.id = itemCost.getRelicUniqueId();
this.count = 1;
}
}
public static enum ItemParamType {
UNKNOWN, PILE, UNIQUE;
}
}

View File

@ -0,0 +1,5 @@
package emu.lunarcore.data.config;
public class AnchorInfo extends ObjectInfo {
}

View File

@ -0,0 +1,59 @@
package emu.lunarcore.data.config;
import java.util.List;
import com.google.gson.annotations.SerializedName;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
@Getter
public class FloorInfo {
private int FloorID;
@SerializedName(value = "GroupList")
private List<FloorGroupSimpleInfo> SimpleGroupList;
// Cached data
private transient boolean loaded;
private transient Int2ObjectMap<GroupInfo> groups;
private transient Int2ObjectMap<PropInfo> cachedTeleports;
public FloorInfo() {
this.groups = new Int2ObjectOpenHashMap<>();
this.cachedTeleports = new Int2ObjectOpenHashMap<>();
}
public AnchorInfo getAnchorInfo(int groupId, int anchorId) {
GroupInfo group = this.getGroups().get(groupId);
if (group == null) return null;
return group.getAnchorList().stream().filter(a -> a.getID() == anchorId).findFirst().orElse(null);
}
public void onLoad() {
if (this.loaded) return;
// Cache anchors
for (GroupInfo group : groups.values()) {
if (group.getPropList() == null) {
continue;
}
for (PropInfo prop : group.getPropList()) {
if (prop.getAnchorID() > 0) {
this.cachedTeleports.put(prop.getMappingInfoID(), prop);
}
}
}
this.loaded = true;
}
@Getter
public static class FloorGroupSimpleInfo {
private String GroupPath;
private int ID;
}
}

View File

@ -0,0 +1,24 @@
package emu.lunarcore.data.config;
import java.util.List;
import lombok.Getter;
@Getter
public class GroupInfo {
private transient int id;
private GroupLoadSide LoadSide;
private boolean LoadOnInitial;
private List<AnchorInfo> AnchorList;
private List<MonsterInfo> MonsterList;
private List<PropInfo> PropList;
public void setId(int id) {
if (this.id == 0) this.id = id;
}
public static enum GroupLoadSide {
Client, Server;
}
}

View File

@ -0,0 +1,9 @@
package emu.lunarcore.data.config;
import lombok.Getter;
@Getter
public class MonsterInfo extends ObjectInfo {
private int NPCMonsterID;
private int EventID;
}

View File

@ -0,0 +1,21 @@
package emu.lunarcore.data.config;
import emu.lunarcore.util.Position;
import lombok.Getter;
@Getter
public class ObjectInfo {
public int ID;
public float PosX;
public float PosY;
public float PosZ;
public String Name;
public float RotY;
/*
* Returns a new Position object
*/
public Position clonePos() {
return new Position((int) (this.PosX * 1000f), (int) (this.PosY * 1000f), (int) (this.PosZ * 1000f));
}
}

View File

@ -0,0 +1,17 @@
package emu.lunarcore.data.config;
import emu.lunarcore.game.scene.PropState;
import lombok.Getter;
@Getter
public class PropInfo extends ObjectInfo {
public float RotX;
public float RotZ;
private int MappingInfoID;
private int AnchorGroupID;
private int AnchorID;
private int PropID;
private int EventID;
private PropState State;
private boolean IsDelete;
}

View File

@ -0,0 +1,64 @@
package emu.lunarcore.data.excel;
import java.util.ArrayList;
import java.util.List;
import emu.lunarcore.data.GameData;
import emu.lunarcore.data.GameResource;
import emu.lunarcore.data.ResourceType;
import emu.lunarcore.game.avatar.AvatarBaseType;
import emu.lunarcore.game.avatar.DamageType;
import lombok.AccessLevel;
import lombok.Getter;
@Getter
@ResourceType(name = {"AvatarConfig.json"})
public class AvatarExcel extends GameResource {
private int AvatarID;
private long AvatarName;
private DamageType DamageType;
private AvatarBaseType AvatarBaseType;
private double SPNeed;
private int ExpGroup;
private int MaxPromotion;
private int MaxRank;
private int[] RankIDList;
private int[] SkillList;
@Getter(AccessLevel.NONE)
private transient AvatarPromotionExcel[] promotionData;
private transient List<AvatarSkillTreeExcel> defaultSkillTrees;
private transient int maxSp;
public AvatarExcel() {
this.defaultSkillTrees = new ArrayList<>();
}
@Override
public int getId() {
return AvatarID;
}
public AvatarPromotionExcel getPromotionData(int i) {
return this.promotionData[i];
}
public int getRankId(int rank) {
return RankIDList[Math.min(rank, RankIDList.length - 1)];
}
@Override
public void onLoad() {
// Load promotion data
this.promotionData = new AvatarPromotionExcel[MaxPromotion + 1];
for (int i = 0; i <= MaxPromotion; i++) {
this.promotionData[i] = GameData.getAvatarPromotionExcel(getId(), i);
}
// Cache max sp
this.maxSp = (int) this.SPNeed * 100;
}
}

View File

@ -0,0 +1,27 @@
package emu.lunarcore.data.excel;
import emu.lunarcore.data.GameData;
import emu.lunarcore.data.GameResource;
import emu.lunarcore.data.ResourceType;
import emu.lunarcore.data.ResourceType.LoadPriority;
import lombok.Getter;
@Getter
@ResourceType(name = {"AvatarExpItemConfig.json"}, loadPriority = LoadPriority.LOW)
public class AvatarExpItemExcel extends GameResource {
private int ItemID;
private int Exp;
@Override
public int getId() {
return ItemID;
}
@Override
public void onLoad() {
ItemExcel excel = GameData.getItemExcelMap().get(ItemID);
if (excel == null) return;
excel.setAvatarExp(Exp);
}
}

View File

@ -0,0 +1,56 @@
package emu.lunarcore.data.excel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import emu.lunarcore.data.GameResource;
import emu.lunarcore.data.ResourceType;
import emu.lunarcore.data.ResourceType.LoadPriority;
import emu.lunarcore.data.common.ItemParam;
import lombok.Getter;
@Getter
@ResourceType(name = {"AvatarPromotionConfig.json"}, loadPriority = LoadPriority.HIGHEST)
public class AvatarPromotionExcel extends GameResource {
private int AvatarID;
private int Promotion;
private int MaxLevel;
private int PlayerLevelRequire;
private int WorldLevelRequire;
private List<ItemParam> PromotionCostList;
private transient int PromotionCostCoin;
private double AttackBase;
private double AttackAdd;
private double DefenceBase;
private double DefenceAdd;
private double HPBase;
private double HPAdd;
private double SpeedBase;
private double CriticalChance;
private double CriticalDamage;
private double BaseAggro;
@Override
public int getId() {
return (AvatarID << 8) + Promotion;
}
@Override
public void onLoad() {
if (this.PromotionCostList == null) {
this.PromotionCostList = new ArrayList<>();
} else {
Iterator<ItemParam> it = this.PromotionCostList.iterator();
while (it.hasNext()) {
ItemParam param = it.next();
if (param.getId() == 2) {
this.PromotionCostCoin = param.getCount();
it.remove();
}
}
}
}
}

View File

@ -0,0 +1,30 @@
package emu.lunarcore.data.excel;
import java.util.List;
import emu.lunarcore.data.GameResource;
import emu.lunarcore.data.ResourceType;
import emu.lunarcore.data.ResourceType.LoadPriority;
import emu.lunarcore.data.common.ItemParam;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import lombok.Getter;
@Getter
@ResourceType(name = {"AvatarRankConfig.json"}, loadPriority = LoadPriority.HIGHEST)
public class AvatarRankExcel extends GameResource {
private int RankID;
private int Rank;
private Int2IntOpenHashMap SkillAddLevelList;
private List<ItemParam> UnlockCost;
@Override
public int getId() {
return RankID;
}
@Override
public void onLoad() {
}
}

View File

@ -0,0 +1,62 @@
package emu.lunarcore.data.excel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import emu.lunarcore.data.GameData;
import emu.lunarcore.data.GameResource;
import emu.lunarcore.data.ResourceType;
import emu.lunarcore.data.ResourceType.LoadPriority;
import emu.lunarcore.data.common.ItemParam;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import lombok.Getter;
@Getter
@ResourceType(name = {"AvatarSkillTreeConfig.json"}, loadPriority = LoadPriority.LOW)
public class AvatarSkillTreeExcel extends GameResource {
private int PointID;
private int Level;
private int MaxLevel;
private boolean DefaultUnlock;
private int AvatarID;
private int AvatarPromotionLimit;
private int AvatarLevelLimit;
private List<ItemParam> MaterialList;
private IntArrayList PrePoint;
private IntArrayList LevelUpSkillID;
private transient int MaterialCostCoin;
@Override
public int getId() {
return (PointID << 4) + Level;
}
@Override
public void onLoad() {
// Parse material list
if (this.MaterialList == null) {
this.MaterialList = new ArrayList<>();
} else {
Iterator<ItemParam> it = this.MaterialList.iterator();
while (it.hasNext()) {
ItemParam param = it.next();
if (param.getId() == 2) {
this.MaterialCostCoin = param.getCount();
it.remove();
}
}
}
// Load to excel
AvatarExcel excel = GameData.getAvatarExcelMap().get(AvatarID);
if (excel == null) return;
if (this.isDefaultUnlock()) {
excel.getDefaultSkillTrees().add(this);
}
}
}

View File

@ -0,0 +1,41 @@
package emu.lunarcore.data.excel;
import emu.lunarcore.data.GameData;
import emu.lunarcore.data.GameResource;
import emu.lunarcore.data.ResourceType;
import emu.lunarcore.data.ResourceType.LoadPriority;
import emu.lunarcore.game.inventory.GameItem;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import lombok.Getter;
@Getter
@ResourceType(name = {"EquipmentConfig.json"}, loadPriority = LoadPriority.LOW)
public class EquipmentExcel extends GameResource {
private int EquipmentID;
private int MaxPromotion;
private int MaxRank;
private int ExpType;
private int ExpProvide;
private int CoinCost;
private IntOpenHashSet RankUpCostList;
@Override
public int getId() {
return EquipmentID;
}
public boolean isRankUpItem(GameItem item) {
return item.getItemId() == this.EquipmentID || RankUpCostList.contains(item.getItemId());
}
@Override
public void onLoad() {
ItemExcel excel = GameData.getItemExcelMap().get(this.getId());
if (excel != null) {
excel.setEquipmentExcel(this);
}
}
}

View File

@ -0,0 +1,29 @@
package emu.lunarcore.data.excel;
import emu.lunarcore.data.GameData;
import emu.lunarcore.data.GameResource;
import emu.lunarcore.data.ResourceType;
import emu.lunarcore.data.ResourceType.LoadPriority;
import lombok.Getter;
@Getter
@ResourceType(name = {"EquipmentExpItemConfig.json"}, loadPriority = LoadPriority.LOW)
public class EquipmentExpItemExcel extends GameResource {
private int ItemID;
private int ExpProvide;
private int CoinCost;
@Override
public int getId() {
return ItemID;
}
@Override
public void onLoad() {
ItemExcel excel = GameData.getItemExcelMap().get(ItemID);
if (excel == null) return;
excel.setEquipmentExp(ExpProvide);
excel.setExpCost(CoinCost);
}
}

View File

@ -0,0 +1,27 @@
package emu.lunarcore.data.excel;
import com.google.gson.annotations.SerializedName;
import emu.lunarcore.data.GameResource;
import emu.lunarcore.data.ResourceType;
import emu.lunarcore.data.ResourceType.LoadPriority;
import lombok.Getter;
@Getter
@ResourceType(name = {"EquipmentExpType.json"}, loadPriority = LoadPriority.NORMAL)
public class EquipmentExpTypeExcel extends GameResource {
@SerializedName(value = "id", alternate = {"ExpType"})
private int TypeID;
private int Level;
private int Exp;
@Override
public int getId() {
return (TypeID << 16) + Level;
}
@Override
public void onLoad() {
}
}

View File

@ -0,0 +1,52 @@
package emu.lunarcore.data.excel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import emu.lunarcore.data.GameResource;
import emu.lunarcore.data.ResourceType;
import emu.lunarcore.data.ResourceType.LoadPriority;
import emu.lunarcore.data.common.ItemParam;
import lombok.Getter;
@Getter
@ResourceType(name = {"EquipmentPromotionConfig.json"}, loadPriority = LoadPriority.HIGHEST)
public class EquipmentPromotionExcel extends GameResource {
private int EquipmentID;
private int Promotion;
private int MaxLevel;
private int PlayerLevelRequire;
private int WorldLevelRequire;
private List<ItemParam> PromotionCostList;
private transient int PromotionCostCoin;
private double AttackBase;
private double AttackAdd;
private double DefenceBase;
private double DefenceAdd;
private double HPBase;
private double HPAdd;
@Override
public int getId() {
return (EquipmentID << 8) + Promotion;
}
@Override
public void onLoad() {
if (this.PromotionCostList == null) {
this.PromotionCostList = new ArrayList<>();
} else {
Iterator<ItemParam> it = this.PromotionCostList.iterator();
while (it.hasNext()) {
ItemParam param = it.next();
if (param.getId() == 2) {
this.PromotionCostCoin = param.getCount();
it.remove();
}
}
}
}
}

View File

@ -0,0 +1,24 @@
package emu.lunarcore.data.excel;
import emu.lunarcore.data.GameResource;
import emu.lunarcore.data.ResourceType;
import emu.lunarcore.data.ResourceType.LoadPriority;
import lombok.Getter;
@Getter
@ResourceType(name = {"ExpType.json"}, loadPriority = LoadPriority.NORMAL)
public class ExpTypeExcel extends GameResource {
private int TypeID;
private int Level;
private int Exp;
@Override
public int getId() {
return (TypeID << 16) + Level;
}
@Override
public void onLoad() {
}
}

View File

@ -0,0 +1,90 @@
package emu.lunarcore.data.excel;
import java.util.List;
import emu.lunarcore.data.GameResource;
import emu.lunarcore.data.ResourceType;
import emu.lunarcore.data.common.ItemParam;
import emu.lunarcore.game.inventory.ItemMainType;
import emu.lunarcore.game.inventory.ItemRarity;
import emu.lunarcore.game.inventory.ItemSubType;
import lombok.Getter;
import lombok.Setter;
@Getter
@ResourceType(name = {"ItemConfig.json", "ItemConfigAvatar.json", "ItemConfigAvatarPlayerIcon.json", "ItemConfigAvatarRank.json",
"ItemConfigBook.json", "ItemConfigDisk.json", "ItemConfigEquipment.json", "ItemConfigRelic.json", "ItemPlayerCard.json"})
public class ItemExcel extends GameResource {
// General item data
private int ID;
private long ItemName;
private ItemMainType ItemMainType;
private ItemSubType ItemSubType;
private ItemRarity Rarity;
private int PileLimit;
private List<ItemParam> ReturnItemIDList;
// Transient cache
@Setter private transient EquipmentExcel equipmentExcel;
@Setter private transient RelicExcel relicExcel;
@Setter private transient int avatarExp;
@Setter private transient int relicExp;
@Setter private transient int equipmentExp;
@Setter private transient int expCost;
@Override
public int getId() {
return ID;
}
public boolean isEquipment() {
return ItemMainType == emu.lunarcore.game.inventory.ItemMainType.Equipment && this.getEquipmentExcel() != null;
}
public boolean isRelic() {
return ItemMainType == emu.lunarcore.game.inventory.ItemMainType.Relic && this.getRelicExcel() != null;
}
public boolean isEquippable() {
return ItemMainType == emu.lunarcore.game.inventory.ItemMainType.Relic || ItemMainType == emu.lunarcore.game.inventory.ItemMainType.Equipment;
}
public int getRelicExp() {
if (this.relicExcel != null) {
return this.relicExcel.getExpProvide();
}
return this.relicExp;
}
public int getRelicExpCost() {
if (this.relicExcel != null) {
return this.relicExcel.getCoinCost();
}
return this.expCost;
}
public int getEquipmentExp() {
if (this.equipmentExcel != null) {
return this.equipmentExcel.getExpProvide();
}
return this.equipmentExp;
}
public int getEquipmentExpCost() {
if (this.equipmentExcel != null) {
return this.equipmentExcel.getCoinCost();
}
return this.expCost;
}
public int getEquipSlot() {
if (this.getRelicExcel() != null) {
return this.getRelicExcel().getType().getVal();
} else if (this.getEquipmentExcel() != null) {
return 100;
}
return 0;
}
}

View File

@ -0,0 +1,20 @@
package emu.lunarcore.data.excel;
import emu.lunarcore.data.GameResource;
import emu.lunarcore.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = {"MapEntrance.json"})
public class MapEntranceExcel extends GameResource {
private int ID;
private int PlaneID;
private int FloorID;
private int StartGroupID;
private int StartAnchorID;
@Override
public int getId() {
return ID;
}
}

View File

@ -0,0 +1,18 @@
package emu.lunarcore.data.excel;
import emu.lunarcore.data.GameResource;
import emu.lunarcore.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = {"MonsterConfig.json"})
public class MonsterExcel extends GameResource {
private int MonsterID;
private int MonsterTemplateID;
private long MonsterName;
@Override
public int getId() {
return MonsterID;
}
}

View File

@ -0,0 +1,22 @@
package emu.lunarcore.data.excel;
import emu.lunarcore.data.GameResource;
import emu.lunarcore.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = {"NPCMonsterData.json"})
public class NpcMonsterExcel extends GameResource {
private int ID;
private long NPCName;
@Override
public int getId() {
return ID;
}
@Override
public void onLoad() {
}
}

View File

@ -0,0 +1,18 @@
package emu.lunarcore.data.excel;
import emu.lunarcore.data.GameResource;
import emu.lunarcore.data.ResourceType;
import emu.lunarcore.data.ResourceType.LoadPriority;
import lombok.Getter;
@Getter
@ResourceType(name = {"PlayerLevelConfig.json"}, loadPriority = LoadPriority.NORMAL)
public class PlayerLevelExcel extends GameResource {
private int Level;
private int PlayerExp;
@Override
public int getId() {
return Level;
}
}

View File

@ -0,0 +1,37 @@
package emu.lunarcore.data.excel;
import emu.lunarcore.data.GameData;
import emu.lunarcore.data.GameResource;
import emu.lunarcore.data.ResourceType;
import emu.lunarcore.data.ResourceType.LoadPriority;
import emu.lunarcore.game.inventory.RelicType;
import lombok.Getter;
@Getter
@ResourceType(name = {"RelicConfig.json"}, loadPriority = LoadPriority.LOW)
public class RelicExcel extends GameResource {
private int ID;
private int SetID;
private RelicType Type;
private int MainAffixGroup;
private int SubAffixGroup;
private int MaxLevel;
private int ExpType;
private int ExpProvide;
private int CoinCost;
@Override
public int getId() {
return ID;
}
@Override
public void onLoad() {
ItemExcel excel = GameData.getItemExcelMap().get(this.getId());
if (excel != null) {
excel.setRelicExcel(this);
}
}
}

View File

@ -0,0 +1,29 @@
package emu.lunarcore.data.excel;
import emu.lunarcore.data.GameData;
import emu.lunarcore.data.GameResource;
import emu.lunarcore.data.ResourceType;
import emu.lunarcore.data.ResourceType.LoadPriority;
import lombok.Getter;
@Getter
@ResourceType(name = {"RelicExpItem.json"}, loadPriority = LoadPriority.LOW)
public class RelicExpItemExcel extends GameResource {
private int ItemID;
private int ExpProvide;
private int CoinCost;
@Override
public int getId() {
return ItemID;
}
@Override
public void onLoad() {
ItemExcel excel = GameData.getItemExcelMap().get(ItemID);
if (excel == null) return;
excel.setRelicExp(ExpProvide);
excel.setExpCost(CoinCost);
}
}

View File

@ -0,0 +1,24 @@
package emu.lunarcore.data.excel;
import emu.lunarcore.data.GameResource;
import emu.lunarcore.data.ResourceType;
import emu.lunarcore.data.ResourceType.LoadPriority;
import lombok.Getter;
@Getter
@ResourceType(name = {"RelicExpType.json"}, loadPriority = LoadPriority.NORMAL)
public class RelicExpTypeExcel extends GameResource {
private int TypeID;
private int Level;
private int Exp;
@Override
public int getId() {
return (TypeID << 16) + Level;
}
@Override
public void onLoad() {
}
}

View File

@ -0,0 +1,31 @@
package emu.lunarcore.data.excel;
import emu.lunarcore.data.GameDepot;
import emu.lunarcore.data.GameResource;
import emu.lunarcore.data.ResourceType;
import emu.lunarcore.data.ResourceType.LoadPriority;
import emu.lunarcore.game.avatar.AvatarPropertyType;
import lombok.Getter;
@Getter
@ResourceType(name = {"RelicMainAffixConfig.json"}, loadPriority = LoadPriority.NORMAL)
public class RelicMainAffixExcel extends GameResource {
private int GroupID;
private int AffixID;
private AvatarPropertyType Property;
private double BaseValue;
private double LevelAdd;
private boolean IsAvailable;
@Override
public int getId() {
return (GroupID << 16) + AffixID;
}
@Override
public void onLoad() {
GameDepot.addRelicMainAffix(this);
}
}

View File

@ -0,0 +1,31 @@
package emu.lunarcore.data.excel;
import emu.lunarcore.data.GameDepot;
import emu.lunarcore.data.GameResource;
import emu.lunarcore.data.ResourceType;
import emu.lunarcore.data.ResourceType.LoadPriority;
import emu.lunarcore.game.avatar.AvatarPropertyType;
import lombok.Getter;
@Getter
@ResourceType(name = {"RelicSubAffixConfig.json"}, loadPriority = LoadPriority.NORMAL)
public class RelicSubAffixExcel extends GameResource {
private int GroupID;
private int AffixID;
private AvatarPropertyType Property;
private double BaseValue;
private double StepValue;
private int StepNum;
@Override
public int getId() {
return (GroupID << 16) + AffixID;
}
@Override
public void onLoad() {
GameDepot.addRelicSubAffix(this);
}
}

View File

@ -0,0 +1,23 @@
package emu.lunarcore.data.excel;
import emu.lunarcore.data.GameResource;
import emu.lunarcore.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = {"StageConfig.json"})
public class StageExcel extends GameResource {
private int StageID;
private long StageName;
private int Level;
@Override
public int getId() {
return StageID;
}
@Override
public void onLoad() {
}
}

View File

@ -0,0 +1,23 @@
package emu.lunarcore.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;
}
}

View File

@ -0,0 +1,182 @@
package emu.lunarcore.database;
import java.util.stream.Stream;
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.Datastore;
import dev.morphia.DeleteOptions;
import dev.morphia.Morphia;
import dev.morphia.annotations.Entity;
import dev.morphia.mapping.Mapper;
import dev.morphia.mapping.MapperOptions;
import dev.morphia.query.filters.Filters;
import emu.lunarcore.Config.DatabaseInfo;
import emu.lunarcore.Config.InternalMongoInfo;
import emu.lunarcore.LunarRail;
public final class DatabaseManager {
private MongoServer server;
private Datastore datastore;
private DeleteOptions DELETE_MANY = new DeleteOptions().multi(true);
public DatabaseManager() {
}
public DatabaseManager(DatabaseInfo info) {
// Variables
String connectionString = info.getUri();
// Local mongo server
if (info.isUseInternal()) {
connectionString = startInternalMongoServer(LunarRail.getConfig().getInternalMongoServer());
LunarRail.getLogger().info("Using local mongo server at " + server.getConnectionString());
}
// Initialize
MongoClient gameMongoClient = MongoClients.create(connectionString);
// Set mapper options.
MapperOptions mapperOptions = MapperOptions.builder()
.storeEmpties(true)
.storeNulls(false)
.build();
// Create data store.
datastore = Morphia.createDatastore(gameMongoClient, info.getCollection(), mapperOptions);
// Map classes
Class<?>[] entities = new Reflections(LunarRail.class.getPackageName())
.getTypesAnnotatedWith(Entity.class)
.stream()
.filter(cls -> {
Entity e = cls.getAnnotation(Entity.class);
return e != null && !e.value().equals(Mapper.IGNORED_FIELDNAME);
})
.toArray(Class<?>[]::new);
datastore.getMapper().map(entities);
// Ensure indexes
ensureIndexes();
}
public MongoServer getServer() {
return server;
}
public MongoDatabase getDatabase() {
return getDatastore().getDatabase();
}
public Datastore getDatastore() {
return datastore;
}
private void ensureIndexes() {
try {
datastore.ensureIndexes();
} catch (MongoCommandException exception) {
LunarRail.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();
}
}
}
//
public 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();
}
// Database Functions
public boolean checkIfObjectExists(Class<?> cls, long uid) {
return getDatastore().find(cls).filter(Filters.eq("_id", uid)).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, String value) {
return getDatastore().find(cls).filter(Filters.eq(filter, value)).first();
}
public <T> T getObjectByField(Class<T> cls, String filter, int value) {
return getDatastore().find(cls).filter(Filters.eq(filter, value)).first();
}
public <T> Stream<T> getObjects(Class<T> cls, String filter, long uid) {
return getDatastore().find(cls).filter(Filters.eq(filter, uid)).stream();
}
public <T> Stream<T> getObjects(Class<T> cls) {
return getDatastore().find(cls).stream();
}
public <T> void save(T obj) {
getDatastore().save(obj);
}
public <T> boolean delete(T obj) {
DeleteResult result = getDatastore().delete(obj);
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 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);
}
}
}

View File

@ -0,0 +1,60 @@
package emu.lunarcore.game.account;
import dev.morphia.annotations.*;
import emu.lunarcore.LunarRail;
import emu.lunarcore.util.Crypto;
import emu.lunarcore.util.Snowflake32;
import emu.lunarcore.util.Utils;
import lombok.Getter;
@Getter
@Entity(value = "accounts", useDiscriminator = false)
public class Account {
@Id private String uid;
@Indexed(options = @IndexOptions(unique = true))
@Collation(locale = "simple", caseLevel = true)
private String username;
private String password; // Unused for now
private int reservedPlayerUid;
private String comboToken; // Combo token
private String dispatchToken; // Session token for dispatch server
@Deprecated
public Account() {
}
public Account(String username) {
this.uid = Long.toString(Snowflake32.newUid());
this.username = username;
}
public String getEmail() {
return username;
}
public void setReservedPlayerUid(int uid) {
this.reservedPlayerUid = uid;
}
// TODO make unique
public String generateComboToken() {
this.comboToken = Utils.bytesToHex(Crypto.createSessionKey(32));
this.save();
return this.comboToken;
}
// TODO make unique
public String generateDispatchToken() {
this.dispatchToken = Utils.bytesToHex(Crypto.createSessionKey(32));
this.save();
return this.dispatchToken;
}
public void save() {
LunarRail.getAccountDatabase().save(this);
}
}

View File

@ -0,0 +1,21 @@
package emu.lunarcore.game.avatar;
import lombok.Getter;
@Getter
public enum AvatarBaseType {
Unknown (0),
Warrior (1),
Rogue (2),
Mage (3),
Shaman (4),
Warlock (5),
Knight (6),
Priest (7);
private final int val;
private AvatarBaseType(int value) {
this.val = value;
}
}

View File

@ -0,0 +1,74 @@
package emu.lunarcore.game.avatar;
import lombok.Getter;
public enum AvatarPropertyType {
Unknown (0),
MaxHP (1),
Attack (2),
Defence (3),
Speed (4),
CriticalChance (5),
CriticalDamage (6),
HealRatio (7),
StanceBreakAddedRatio (8),
SPRatio (9),
StatusProbability (10),
StatusResistance (11),
PhysicalAddedRatio (12),
PhysicalResistance (13),
FireAddedRatio (14),
FireResistance (15),
IceAddedRatio (16),
IceResistance (17),
ThunderAddedRatio (18),
ThunderResistance (19),
WindAddedRatio (20),
WindResistance (21),
QuantumAddedRatio (22),
QuantumResistance (23),
ImaginaryAddedRatio (24),
ImaginaryResistance (25),
BaseHP (26),
HPDelta (27),
BaseAttack (28),
AttackDelta (29),
BaseDefence (30),
DefenceDelta (31),
HPAddedRatio (32),
AttackAddedRatio (33),
DefenceAddedRatio (34),
BaseSpeed (35),
HealTakenRatio (36),
PhysicalResistanceDelta (37),
FireResistanceDelta (38),
IceResistanceDelta (39),
ThunderResistanceDelta (40),
WindResistanceDelta (41),
QuantumResistanceDelta (42),
ImaginaryResistanceDelta (43),
AllDamageReduce (44),
RelicValueExtraAdditionRatio (45),
EquipValueExtraAdditionRatio (46),
EquipExtraRank (47),
AvatarExtraRank (48),
AllDamageTypeAddedRatio (49),
SpeedAddedRatio (50),
SpeedDelta (51),
CriticalChanceBase (52),
CriticalDamageBase (53),
SPRatioBase (54),
HealRatioBase (55),
StatusProbabilityBase (56),
StatusResistanceBase (57),
BreakDamageAddedRatio (58),
BreakDamageAddedRatioBase (59),
MaxSP (60);
@Getter
private int val;
private AvatarPropertyType(int value) {
this.val = value;
}
}

View File

@ -0,0 +1,94 @@
package emu.lunarcore.game.avatar;
import java.util.Iterator;
import java.util.stream.Stream;
import emu.lunarcore.LunarRail;
import emu.lunarcore.data.GameData;
import emu.lunarcore.data.excel.AvatarExcel;
import emu.lunarcore.game.player.BasePlayerManager;
import emu.lunarcore.game.player.Player;
import emu.lunarcore.server.packet.send.PacketPlayerSyncScNotify;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
public class AvatarStorage extends BasePlayerManager implements Iterable<GameAvatar> {
private final Int2ObjectMap<GameAvatar> avatars;
public AvatarStorage(Player player) {
super(player);
this.avatars = new Int2ObjectOpenHashMap<>();
}
public Int2ObjectMap<GameAvatar> getAvatars() {
return avatars;
}
public int getAvatarCount() {
return this.avatars.size();
}
public GameAvatar getAvatarById(int id) {
return getAvatars().get(id);
}
public boolean hasAvatar(int id) {
return getAvatars().containsKey(id);
}
public boolean addAvatar(GameAvatar avatar) {
// Sanity
if (avatar.getExcel() == null || this.hasAvatar(avatar.getAvatarId())) {
return false;
}
// Set owner first
avatar.setOwner(getPlayer());
// Put into maps
this.avatars.put(avatar.getAvatarId(), avatar);
// Save to database
avatar.save();
// Send packet
getPlayer().sendPacket(new PacketPlayerSyncScNotify(avatar));
return true;
}
public void recalcAvatarStats() {
//this.getAvatars().values().stream().forEach(GameAvatar::recalcStats);
}
@Override
public Iterator<GameAvatar> iterator() {
return getAvatars().values().iterator();
}
// Database
public void loadFromDatabase() {
Stream<GameAvatar> stream = LunarRail.getGameDatabase().getObjects(GameAvatar.class, "ownerUid", this.getPlayer().getUid());
stream.forEach(avatar -> {
// Should never happen
if (avatar.getId() == null) {
return;
}
// Load avatar excel data
AvatarExcel excel = GameData.getAvatarExcelMap().get(avatar.getAvatarId());
if (excel == null) {
return;
}
// Set ownerships
avatar.setExcel(excel);
avatar.setOwner(getPlayer());
// Add to avatar storage
this.avatars.put(avatar.getAvatarId(), avatar);
});
}
}

View File

@ -0,0 +1,6 @@
package emu.lunarcore.game.avatar;
// These are in excels but i prefer them as enums
public enum DamageType {
Physical, Ice, Fire, Thunder, Wind, Quantum, Imaginary;
}

View File

@ -0,0 +1,276 @@
package emu.lunarcore.game.avatar;
import java.util.HashMap;
import java.util.Map;
import org.bson.types.ObjectId;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import dev.morphia.annotations.Indexed;
import emu.lunarcore.GameConstants;
import emu.lunarcore.LunarRail;
import emu.lunarcore.data.GameData;
import emu.lunarcore.data.excel.AvatarExcel;
import emu.lunarcore.game.inventory.GameItem;
import emu.lunarcore.game.inventory.ItemMainType;
import emu.lunarcore.game.player.Player;
import emu.lunarcore.game.scene.GameEntity;
import emu.lunarcore.proto.AvatarOuterClass.Avatar;
import emu.lunarcore.proto.AvatarSkillTreeOuterClass.AvatarSkillTree;
import emu.lunarcore.proto.AvatarTypeOuterClass.AvatarType;
import emu.lunarcore.proto.BattleAvatarOuterClass.BattleAvatar;
import emu.lunarcore.proto.BattleEquipmentOuterClass.BattleEquipment;
import emu.lunarcore.proto.BattleRelicOuterClass.BattleRelic;
import emu.lunarcore.proto.EquipRelicOuterClass.EquipRelic;
import emu.lunarcore.proto.LineupAvatarOuterClass.LineupAvatar;
import emu.lunarcore.proto.MotionInfoOuterClass.MotionInfo;
import emu.lunarcore.proto.SceneActorInfoOuterClass.SceneActorInfo;
import emu.lunarcore.proto.SceneEntityInfoOuterClass.SceneEntityInfo;
import emu.lunarcore.proto.SpBarInfoOuterClass.SpBarInfo;
import emu.lunarcore.proto.VectorOuterClass.Vector;
import emu.lunarcore.server.packet.send.PacketPlayerSyncScNotify;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
import lombok.Setter;
@Getter
@Entity(value = "avatars", useDiscriminator = false)
public class GameAvatar implements GameEntity {
@Id private ObjectId id;
@Indexed @Getter private int ownerUid; // Uid of player that this avatar belongs to
private transient Player owner;
private transient AvatarExcel excel;
private int avatarId; // Id of avatar
@Setter private int level;
@Setter private int exp;
@Setter private int promotion;
@Setter private int rank; // Eidolons
private int currentHp;
private int currentSp;
private Map<Integer, Integer> skills;
private transient int entityId;
private transient Int2ObjectMap<GameItem> equips;
@Deprecated // Morphia only
public GameAvatar() {
this.equips = new Int2ObjectOpenHashMap<>();
this.currentHp = 10000;
this.currentSp = 0;
}
// On creation
public GameAvatar(int avatarId) {
this(GameData.getAvatarExcelMap().get(avatarId));
}
public GameAvatar(AvatarExcel excel) {
this();
this.excel = excel;
this.avatarId = excel.getId();
this.level = 1;
// Set default skills
this.skills = new HashMap<>();
for (var skillTree : excel.getDefaultSkillTrees()) {
this.skills.put(skillTree.getPointID(), skillTree.getLevel());
}
// Set stats
this.currentHp = 10000;
}
public void setOwner(Player player) {
this.owner = player;
this.ownerUid = player.getUid();
}
public void setExcel(AvatarExcel excel) {
this.excel = excel;
}
@Override
public void setEntityId(int entityId) {
this.entityId = entityId;
}
public int getMaxSp() {
return this.getExcel().getMaxSp();
}
public void setCurrentHp(int amount) {
this.currentHp = Math.max(Math.min(amount, 10000), 0);
}
public void setCurrentSp(int amount) {
this.currentSp = Math.max(Math.min(amount, getMaxSp()), 0);
}
// Equips
public GameItem getEquipBySlot(int slot) {
return this.getEquips().get(slot);
}
public GameItem getEquipment() {
return this.getEquips().get(GameConstants.EQUIPMENT_SLOT_ID);
}
public boolean equipItem(GameItem item) {
// Sanity check
int slot = item.getEquipSlot();
if (slot == 0) return false;
// Check if other avatars have this item equipped
GameAvatar otherAvatar = getOwner().getAvatarById(item.getEquipAvatar());
if (otherAvatar != null) {
// Unequip this item from the other avatar
if (otherAvatar.unequipItem(slot) != null) {
getOwner().sendPacket(new PacketPlayerSyncScNotify(otherAvatar));
}
// Swap with other avatar
if (getEquips().containsKey(slot)) {
GameItem toSwap = this.getEquipBySlot(slot);
otherAvatar.equipItem(toSwap);
}
} else if (getEquips().containsKey(slot)) {
// Unequip item in current slot if it exists
GameItem unequipped = unequipItem(slot);
if (unequipped != null) {
getOwner().sendPacket(new PacketPlayerSyncScNotify(unequipped));
}
}
// Set equip
getEquips().put(slot, item);
// Save equip if equipped avatar was changed
if (item.setEquipAvatar(this.getAvatarId())) {
item.save();
}
// Send packet
getOwner().sendPacket(new PacketPlayerSyncScNotify(this, item));
return true;
}
public GameItem unequipItem(int slot) {
GameItem item = getEquips().remove(slot);
if (item != null) {
item.setEquipAvatar(0);
item.save();
return item;
}
return null;
}
// Proto
public Avatar toProto() {
var proto = Avatar.newInstance()
.setBaseAvatarId(this.getAvatarId())
.setLevel(this.getLevel())
.setExp(this.getExp())
.setPromotion(this.getPromotion())
.setRank(this.getRank());
for (var equip : this.getEquips().values()) {
if (equip.getItemMainType() == ItemMainType.Relic) {
proto.addEquipRelicList(EquipRelic.newInstance().setSlot(equip.getEquipSlot()).setRelicUniqueId(equip.getInternalUid()));
} else if (equip.getItemMainType() == ItemMainType.Equipment) {
proto.setEquipmentUniqueId(equip.getInternalUid());
}
}
for (var skill : getSkills().entrySet()) {
proto.addSkilltreeList(AvatarSkillTree.newInstance().setPointId(skill.getKey()).setLevel(skill.getValue()));
}
return proto;
}
public LineupAvatar toLineupAvatarProto(int slot) {
var proto = LineupAvatar.newInstance()
.setAvatarType(AvatarType.AVATAR_FORMAL_TYPE)
.setId(this.getAvatarId())
.setSpBar(SpBarInfo.newInstance().setCurSp(this.getCurrentSp()).setMaxSp(this.getMaxSp()))
.setHp(this.getCurrentHp())
.setSlot(slot);
return proto;
}
@Override
public SceneEntityInfo toSceneEntityProto() {
var proto = SceneEntityInfo.newInstance()
.setEntityId(this.getEntityId())
.setMotion(MotionInfo.newInstance().setPos(getOwner().getPos().toProto()).setRot(Vector.newInstance().setY(0)))
.setActor(SceneActorInfo.newInstance().setBaseAvatarId(this.getAvatarId()).setAvatarType(AvatarType.AVATAR_FORMAL_TYPE));
return proto;
}
public BattleAvatar toBattleProto(int index) {
var proto = BattleAvatar.newInstance()
.setAvatarType(AvatarType.AVATAR_FORMAL_TYPE)
.setId(this.getAvatarId())
.setLevel(this.getLevel())
.setPromotion(this.getPromotion())
.setRank(this.getRank())
.setIndex(index)
.setHp(this.getCurrentHp())
.setSpBar(SpBarInfo.newInstance().setCurSp(this.getCurrentSp()).setMaxSp(this.getMaxSp()))
.setWorldLevel(this.getOwner().getWorldLevel());
// Skill tree
for (var skill : getSkills().entrySet()) {
proto.addSkilltreeList(AvatarSkillTree.newInstance().setPointId(skill.getKey()).setLevel(skill.getValue()));
}
// Build equips
for (var equip : this.getEquips().values()) {
if (equip.getItemMainType() == ItemMainType.Relic) {
// Build battle relic proto
var relic = BattleRelic.newInstance()
.setId(equip.getItemId())
.setLevel(equip.getLevel())
.setUniqueId(equip.getInternalUid())
.setMainAffixId(equip.getMainAffix());
if (equip.getSubAffixes() != null) {
for (var subAffix : equip.getSubAffixes()) {
relic.addSubAffixList(subAffix.toProto());
}
}
proto.addRelicList(relic);
} else if (equip.getItemMainType() == ItemMainType.Equipment) {
// Build battle equipment proto
var equipment = BattleEquipment.newInstance()
.setId(equip.getItemId())
.setLevel(equip.getLevel())
.setPromotion(equip.getPromotion())
.setRank(equip.getRank());
proto.addEquipmentList(equipment);
}
}
return proto;
}
// Database
public void save() {
LunarRail.getGameDatabase().save(this);
}
}

View File

@ -0,0 +1,15 @@
package emu.lunarcore.game.battle;
import emu.lunarcore.game.player.Player;
public class Battle {
private final Player player;
public Battle(Player player) {
this.player = player;
}
public Player getPlayer() {
return player;
}
}

View File

@ -0,0 +1,72 @@
package emu.lunarcore.game.battle;
import java.util.Collection;
import java.util.List;
import emu.lunarcore.game.avatar.GameAvatar;
import emu.lunarcore.game.player.Player;
import emu.lunarcore.game.scene.EntityMonster;
import emu.lunarcore.game.scene.GameEntity;
import emu.lunarcore.proto.AvatarBattleInfoOuterClass.AvatarBattleInfo;
import emu.lunarcore.proto.AvatarPropertyOuterClass.AvatarProperty;
import emu.lunarcore.proto.BattleEndStatusOuterClass.BattleEndStatus;
import emu.lunarcore.server.game.BaseGameService;
import emu.lunarcore.server.game.GameServer;
import emu.lunarcore.server.packet.send.PacketSceneCastSkillScRsp;
import emu.lunarcore.server.packet.send.PacketSyncLineupNotify;
import us.hebi.quickbuf.RepeatedInt;
import us.hebi.quickbuf.RepeatedMessage;
public class BattleService extends BaseGameService {
public BattleService(GameServer server) {
super(server);
}
public void onBattleStart(Player player, int attackerId, RepeatedInt attackedList) {
// Setup variables
int entityId = attackedList.get(0);
GameEntity entity = null;
// Check if attacker is the player or not
if (player.getScene().getAvatarEntityIds().contains(attackerId)) {
entity = player.getScene().getEntities().get(entityId);
} else if (player.getScene().getAvatarEntityIds().contains(entityId)) {
entity = player.getScene().getEntities().get(attackerId);
}
if (entity != null) {
if (entity instanceof EntityMonster) {
player.sendPacket(new PacketSceneCastSkillScRsp(player, (EntityMonster) entity));
return;
}
}
player.sendPacket(new PacketSceneCastSkillScRsp(1));
}
public void onBattleResult(Player player, BattleEndStatus result, RepeatedMessage<AvatarBattleInfo> battleAvatars) {
// Lose
if (result == BattleEndStatus.BATTLE_END_LOSE) {
}
// Set health/energy
for (var battleAvatar : battleAvatars) {
GameAvatar avatar = player.getAvatarById(battleAvatar.getId());
if (avatar == null) continue;
AvatarProperty prop = battleAvatar.getAvatarStatus();
int currentHp = (int) Math.round((prop.getLeftHp() / prop.getMaxHp()) * 100);
int currentSp = (int) prop.getLeftSp() * 100;
//avatar.setCurrentHp(currentHp);
avatar.setCurrentSp(currentSp);
avatar.save();
}
// Sync with player
player.sendPacket(new PacketSyncLineupNotify(player.getLineupManager().getCurrentLineup()));
}
}

View File

@ -0,0 +1,46 @@
package emu.lunarcore.game.gacha;
import emu.lunarcore.proto.GachaCeilingOuterClass.GachaCeiling;
import emu.lunarcore.proto.GachaInfoOuterClass.GachaInfo;
import lombok.Getter;
@Getter
public class GachaBanner {
private int id; // Id should match one of the ids in GachaBasicInfo.json
private GachaType gachaType;
private int beginTime;
private int endTime;
private int[] rateUpItems5;
private int[] rateUpItems4;
private int eventChance = 50;
public GachaInfo toProto() {
var info = GachaInfo.newInstance()
.setGachaId(this.getId())
.setDetailUrl("")
.setHistoryUrl("");
if (this.gachaType == GachaType.Normal) {
// Gacha ceiling
info.setGachaCeiling(GachaCeiling.newInstance());
} else {
info.setBeginTime(this.getBeginTime());
info.setEndTime(this.getEndTime());
}
if (this.getRateUpItems4().length > 0) {
for (int id : getRateUpItems4()) {
info.addUpInfo(id);
}
}
if (this.getRateUpItems5().length > 0) {
for (int id : getRateUpItems5()) {
info.addUpInfo(id);
info.addFeatured(id);
}
}
return info;
}
}

View File

@ -0,0 +1,283 @@
package emu.lunarcore.game.gacha;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import emu.lunarcore.LunarRail;
import emu.lunarcore.data.GameData;
import emu.lunarcore.data.excel.ItemExcel;
import emu.lunarcore.game.avatar.GameAvatar;
import emu.lunarcore.game.inventory.GameItem;
import emu.lunarcore.game.inventory.ItemMainType;
import emu.lunarcore.game.inventory.ItemRarity;
import emu.lunarcore.game.player.Player;
import emu.lunarcore.proto.GachaItemOuterClass.GachaItem;
import emu.lunarcore.proto.GetGachaInfoScRspOuterClass.GetGachaInfoScRsp;
import emu.lunarcore.proto.ItemListOuterClass.ItemList;
import emu.lunarcore.server.game.BaseGameService;
import emu.lunarcore.server.game.GameServer;
import emu.lunarcore.server.packet.send.PacketDoGachaScRsp;
import emu.lunarcore.util.JsonUtils;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
public class GachaService extends BaseGameService {
private final Int2ObjectMap<GachaBanner> gachaBanners;
private GetGachaInfoScRsp cachedProto;
private int[] yellowAvatars = new int[] {1003, 1004, 1101, 1107, 1104, 1209, 1211};
private int[] yellowWeapons = new int[] {23000, 23002, 23003, 23004, 23005, 23012, 23013};
private int[] purpleAvatars = new int[] {1001, 1002, 1008, 1009, 1013, 1103, 1105, 1106, 1108, 1109, 1111, 1201, 1202, 1206, 1207};
private int[] purpleWeapons = new int[] {21000, 21001, 21002, 21003, 21004, 21005, 21006, 21007, 21008, 21009, 21010, 21011, 21012, 21013, 21014, 21015, 21016, 21017, 21018, 21019, 21020};
private int[] blueWeapons = new int[] {20000, 20001, 20002, 20003, 20004, 20005, 20006, 20007, 20008, 20009, 20010, 20011, 20012, 20013, 20014, 20015, 20016, 20017, 20018, 20019, 20020};
private static int starglitterId = 251;
private static int stardustId = 252;
public GachaService(GameServer server) {
super(server);
this.gachaBanners = new Int2ObjectOpenHashMap<>();
this.load();
}
public Int2ObjectMap<GachaBanner> getGachaBanners() {
return gachaBanners;
}
public int randomRange(int min, int max) {
return ThreadLocalRandom.current().nextInt(max - min + 1) + min;
}
public int getRandom(int[] array) {
return array[randomRange(0, array.length - 1)];
}
public synchronized void load() {
try (FileReader fileReader = new FileReader(LunarRail.getConfig().getDataDir() + "/Banners.json")) {
List<GachaBanner> banners = JsonUtils.loadToList(fileReader, GachaBanner.class);
for (GachaBanner banner : banners) {
getGachaBanners().put(banner.getId(), banner);
}
} catch (Exception e) {
// TODO Auto-generated catch block
LunarRail.getLogger().warn("No gacha banners loaded!");
}
}
public synchronized void doPulls(Player player, int gachaId, int times) {
// Sanity check
if (times != 10 && times != 1) {
return;
}
if (player.getInventory().getInventoryTab(ItemMainType.Equipment).getSize() + times > player.getInventory().getInventoryTab(ItemMainType.Equipment).getMaxCapacity()) {
player.sendPacket(new PacketDoGachaScRsp());
return;
}
// Get banner
GachaBanner banner = this.getGachaBanners().get(gachaId);
if (banner == null) {
player.sendPacket(new PacketDoGachaScRsp());
return;
}
// Spend currency
if (banner.getGachaType().getCostItem() > 0) {
GameItem costItem = player.getInventory().getInventoryTab(ItemMainType.Material).getItemById(banner.getGachaType().getCostItem());
if (costItem == null || costItem.getCount() < times) {
return;
}
player.getInventory().removeItem(costItem, times);
}
// Roll
PlayerGachaBannerInfo gachaInfo = player.getGachaInfo().getBannerInfo(banner.getGachaType());
IntList wonItems = new IntArrayList(times);
for (int i = 0; i < times; i++) {
int random = this.randomRange(1, 10000);
int itemId = 0;
int bonusYellowChance = gachaInfo.getPity5() >= 74 ? 100 * (gachaInfo.getPity5() - 73): 0;
int yellowChance = 60 + (int) Math.floor(100f * (gachaInfo.getPity5() / 73f)) + bonusYellowChance;
int purpleChance = 10000 - (510 + (int) Math.floor(790f * (gachaInfo.getPity4() / 8f)));
if (random <= yellowChance || gachaInfo.getPity5() >= 89) {
if (banner.getRateUpItems5().length > 0) {
int eventChance = this.randomRange(1, 100);
if (eventChance <= banner.getEventChance() || gachaInfo.getFailedFeaturedItemPulls() >= 1) {
itemId = getRandom(banner.getRateUpItems5());
gachaInfo.setFailedFeaturedItemPulls(0);
} else {
// Lost the 50/50... rip
gachaInfo.addFailedFeaturedItemPulls(1);
}
}
if (itemId == 0) {
int typeChance = this.randomRange(banner.getGachaType().getMinItemType(), banner.getGachaType().getMaxItemType());
if (typeChance == 1) {
itemId = getRandom(this.yellowAvatars);
} else {
itemId = getRandom(this.yellowWeapons);
}
}
// Pity
gachaInfo.addPity4(1);
gachaInfo.setPity5(0);
} else if (random >= purpleChance || gachaInfo.getPity4() >= 9) {
if (banner.getRateUpItems4().length > 0) {
int eventChance = this.randomRange(1, 100);
if (eventChance >= 50) {
itemId = getRandom(banner.getRateUpItems4());
}
}
if (itemId == 0) {
int typeChance = this.randomRange(banner.getGachaType().getMinItemType(), banner.getGachaType().getMaxItemType());
if (typeChance == 1) {
itemId = getRandom(this.purpleAvatars);
} else {
itemId = getRandom(this.purpleWeapons);
}
}
// Pity
gachaInfo.addPity5(1);
gachaInfo.setPity4(0);
} else {
itemId = getRandom(this.blueWeapons);
// Pity
gachaInfo.addPity4(1);
gachaInfo.addPity5(1);
}
// Add winning item
wonItems.add(itemId);
}
// Add to character
List<GachaItem> list = new ArrayList<>();
int stardust = 0, starglitter = 0;
for (int itemId : wonItems) {
ItemExcel itemData = GameData.getItemExcelMap().get(itemId);
if (itemData == null) {
continue;
}
// Create gacha item
GachaItem gachaItem = GachaItem.newInstance();
int addStardust = 0, addStarglitter = 0;
// Dupe check
if (itemData.getItemMainType() == ItemMainType.AvatarCard) {
int avatarId = itemData.getId();
GameAvatar avatar = player.getAvatars().getAvatarById(avatarId);
if (avatar != null) {
int constLevel = avatar.getRank();
int constItemId = avatarId + 10000; // Hacky. TODO optimize by using AvatarRankExcel
GameItem constItem = player.getInventory().getInventoryTab(ItemMainType.Material).getItemById(constItemId);
if (constItem != null) {
constLevel += constItem.getCount();
}
if (constLevel < 6) {
// Not max const
addStarglitter = 2;
// Add 1 const
//gachaItem.addTransferItems(GachaTransferItem.newBuilder().setItem(ItemParam.newBuilder().setItemId(constItemId).setCount(1)).setIsTransferItemNew(constItem == null));
//gachaItem.addTokenItemList(ItemParam.newBuilder().setItemId(constItemId).setCount(1));
player.getInventory().addItem(constItemId, 1);
} else {
// Is max const
addStarglitter = 5;
}
if (itemData.getRarity() == ItemRarity.SuperRare) {
addStarglitter *= 5;
}
} else {
// New
gachaItem.setIsNew(true);
}
} else {
// Is weapon
switch (itemData.getRarity()) {
case SuperRare:
addStarglitter = 10;
break;
case VeryRare:
addStarglitter = 2;
break;
case Rare:
addStardust = 15;
break;
default:
break;
}
}
// Create item
GameItem item = new GameItem(itemData);
gachaItem.setGachaItem(item.toProto());
gachaItem.setUnk1(ItemList.newInstance());
gachaItem.setUnk2(ItemList.newInstance());
player.getInventory().addItem(item);
stardust += addStardust;
starglitter += addStarglitter;
/*
if (addStardust > 0) {
gachaItem.addTokenItemList(ItemParam.newBuilder().setItemId(stardustId).setCount(addStardust));
} if (addStarglitter > 0) {
ItemParam starglitterParam = ItemParam.newBuilder().setItemId(starglitterId).setCount(addStarglitter).build();
if (isTransferItem) {
gachaItem.addTransferItems(GachaTransferItem.newBuilder().setItem(starglitterParam));
}
gachaItem.addTokenItemList(starglitterParam);
}
*/
list.add(gachaItem.newInstance());
}
// Add stardust/starglitter
if (stardust > 0) {
player.getInventory().addItem(stardustId, stardust);
} if (starglitter > 0) {
player.getInventory().addItem(starglitterId, starglitter);
}
// Packets
player.sendPacket(new PacketDoGachaScRsp(banner, times, list));
}
private synchronized GetGachaInfoScRsp createProto() {
var proto = GetGachaInfoScRsp.newInstance();
for (GachaBanner banner : getGachaBanners().values()) {
proto.addGachaInfoList(banner.toProto());
}
return proto;
}
public GetGachaInfoScRsp toProto() {
if (this.cachedProto == null) {
this.cachedProto = createProto();
}
return this.cachedProto;
}
}

View File

@ -0,0 +1,21 @@
package emu.lunarcore.game.gacha;
import lombok.Getter;
@Getter
public enum GachaType {
Newbie (101, 1, 2),
Normal (101, 1, 2),
AvatarUp (102, 1, 1),
WeaponUp (102, 2, 2);
private int costItem;
private int minItemType;
private int maxItemType;
private GachaType(int costItem, int min, int max) {
this.costItem = costItem;
this.minItemType = min;
this.maxItemType = max;
}
}

View File

@ -0,0 +1,46 @@
package emu.lunarcore.game.gacha;
import dev.morphia.annotations.Entity;
@Entity(useDiscriminator = false)
public class PlayerGachaBannerInfo {
private int pity5 = 0;
private int pity4 = 0;
private int failedFeaturedItemPulls = 0;
public int getPity5() {
return pity5;
}
public void setPity5(int pity5) {
this.pity5 = pity5;
}
public void addPity5(int amount) {
this.pity5 += amount;
}
public int getPity4() {
return pity4;
}
public void setPity4(int pity4) {
this.pity4 = pity4;
}
public void addPity4(int amount) {
this.pity4 += amount;
}
public int getFailedFeaturedItemPulls() {
return failedFeaturedItemPulls;
}
public void setFailedFeaturedItemPulls(int failedEventCharacterPulls) {
this.failedFeaturedItemPulls = failedEventCharacterPulls;
}
public void addFailedFeaturedItemPulls(int amount) {
failedFeaturedItemPulls += amount;
}
}

View File

@ -0,0 +1,38 @@
package emu.lunarcore.game.gacha;
import dev.morphia.annotations.Entity;
@Entity(useDiscriminator = false)
public class PlayerGachaInfo {
private PlayerGachaBannerInfo standardBanner;
private PlayerGachaBannerInfo eventCharacterBanner;
private PlayerGachaBannerInfo eventWeaponBanner;
public PlayerGachaInfo() {
this.standardBanner = new PlayerGachaBannerInfo();
this.eventCharacterBanner = new PlayerGachaBannerInfo();
this.eventWeaponBanner = new PlayerGachaBannerInfo();
}
public PlayerGachaBannerInfo getStandardBanner() {
return standardBanner;
}
public PlayerGachaBannerInfo getEventCharacterBanner() {
return eventCharacterBanner;
}
public PlayerGachaBannerInfo getEventWeaponBanner() {
return eventWeaponBanner;
}
public PlayerGachaBannerInfo getBannerInfo(GachaType type) {
if (type == GachaType.AvatarUp) {
return this.eventCharacterBanner;
} else if (type == GachaType.WeaponUp) {
return this.eventWeaponBanner;
}
return this.standardBanner;
}
}

View File

@ -0,0 +1,45 @@
package emu.lunarcore.game.inventory;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class EquipInventoryTab extends InventoryTab {
private final Set<GameItem> items;
private final int maxCapacity;
public EquipInventoryTab(int maxCapacity) {
this.items = new HashSet<>();
this.maxCapacity = maxCapacity;
}
@Override
public GameItem getItemById(int id) {
return null;
}
@Override
public void onAddItem(GameItem item) {
this.items.add(item);
}
@Override
public void onRemoveItem(GameItem item) {
this.items.remove(item);
}
@Override
public int getSize() {
return this.items.size();
}
@Override
public int getMaxCapacity() {
return this.maxCapacity;
}
@Override
public Iterator<GameItem> iterator() {
return items.iterator();
}
}

View File

@ -0,0 +1,266 @@
package emu.lunarcore.game.inventory;
import java.util.ArrayList;
import java.util.List;
import org.bson.types.ObjectId;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import dev.morphia.annotations.Indexed;
import emu.lunarcore.LunarRail;
import emu.lunarcore.data.GameData;
import emu.lunarcore.data.GameDepot;
import emu.lunarcore.data.excel.ItemExcel;
import emu.lunarcore.data.excel.RelicMainAffixExcel;
import emu.lunarcore.data.excel.RelicSubAffixExcel;
import emu.lunarcore.game.avatar.AvatarPropertyType;
import emu.lunarcore.game.player.Player;
import emu.lunarcore.proto.EquipmentOuterClass.Equipment;
import emu.lunarcore.proto.ItemOuterClass.Item;
import emu.lunarcore.proto.MaterialOuterClass.Material;
import emu.lunarcore.proto.PileItemOuterClass.PileItem;
import emu.lunarcore.proto.RelicOuterClass.Relic;
import emu.lunarcore.util.Utils;
import emu.lunarcore.util.WeightedList;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import lombok.Getter;
import lombok.Setter;
@Getter
@Entity(value = "items", useDiscriminator = false)
public class GameItem {
@Id private ObjectId id;
@Indexed private int ownerUid; // Uid of player that this avatar belongs to
private transient int internalUid; // Internal unique id of item
private transient ItemExcel excel;
private int itemId;
private int count;
@Setter private int level;
@Setter private int exp;
@Setter private int totalExp;
@Setter private int promotion;
@Setter private int rank; // Superimpose
@Setter private boolean locked;
private int mainAffix;
private List<ItemSubAffix> subAffixes;
private int equipAvatar;
@Deprecated
public GameItem() {
// Morphia only
}
public GameItem(int itemId) {
this(GameData.getItemExcelMap().get(itemId));
}
public GameItem(int itemId, int count) {
this(GameData.getItemExcelMap().get(itemId), count);
}
public GameItem(ItemExcel data) {
this(data, 1);
}
public GameItem(ItemExcel excel, int count) {
this.itemId = excel.getId();
this.excel = excel;
switch (excel.getItemMainType()) {
case Virtual:
this.count = count;
break;
case Equipment:
this.count = 1;
this.level = 1;
this.rank = 1;
break;
case Relic:
this.count = 1;
// Init affixes
if (getExcel().getRelicExcel() != null) {
// Main affix
var affix = GameDepot.getRandomRelicMainAffix(getExcel().getRelicExcel().getMainAffixGroup());
if (affix != null) {
this.mainAffix = affix.getAffixID();
}
// Sub affixes
int baseSubAffixes = Math.min(Math.max(getExcel().getRarity().getVal() - 2, 0), 3);
this.addSubAffixes(Utils.randomRange(baseSubAffixes, baseSubAffixes + 1));
}
break;
default:
this.count = Math.min(count, excel.getPileLimit());
}
}
public void setOwner(Player player) {
this.ownerUid = player.getUid();
this.internalUid = player.getInventory().getNextItemInternalUid();
}
public void setExcel(ItemExcel excel) {
this.excel = excel;
}
public ItemMainType getItemMainType() {
return excel.getItemMainType();
}
public int getEquipSlot() {
return excel.getEquipSlot();
}
public boolean isEquipped() {
return this.getEquipAvatar() > 0;
}
public boolean isDestroyable() {
return !this.isLocked() && !this.isEquipped();
}
public void setCount(int count) {
this.count = count;
}
public boolean setEquipAvatar(int newEquipAvatar) {
if (this.equipAvatar != newEquipAvatar) {
this.equipAvatar = newEquipAvatar;
return true;
}
return false;
}
// Sub affixes
public void addSubAffixes(int quantity) {
for (int i = 0; i < quantity; i++) {
this.addSubAffix();
}
}
public void addSubAffix() {
if (this.subAffixes == null) {
this.subAffixes = new ArrayList<>();
}
if (this.subAffixes.size() < 4) {
this.addNewSubAffix();
} else {
this.upgradeRandomSubAffix();
}
}
private void addNewSubAffix() {
// Get list of affixes to add
List<RelicSubAffixExcel> affixList = GameDepot.getRelicSubAffixList(getExcel().getRelicExcel().getSubAffixGroup());
if (affixList == null) return;
// Blacklist main affix and any sub affixes
AvatarPropertyType mainAffixProperty = AvatarPropertyType.Unknown;
RelicMainAffixExcel mainAffix = GameData.getRelicMainAffixExcelMap().get(this.mainAffix);
if (mainAffix != null) {
mainAffixProperty = mainAffix.getProperty();
}
IntSet blacklist = new IntOpenHashSet();
for (ItemSubAffix subAffix : this.getSubAffixes()) {
blacklist.add(subAffix.getId());
}
// Build random list
WeightedList<RelicSubAffixExcel> randomList = new WeightedList<>();
for (RelicSubAffixExcel affix : affixList) {
if (affix.getProperty() != mainAffixProperty && !blacklist.contains(affix.getAffixID())) {
randomList.add(10, affix);
}
}
// Sanity check
if (randomList.size() == 0) {
return;
}
// Add random stat
RelicSubAffixExcel subAffix = randomList.next();
this.subAffixes.add(new ItemSubAffix(subAffix));
}
private void upgradeRandomSubAffix() {
ItemSubAffix subAffix = Utils.randomElement(this.subAffixes);
subAffix.incrementCount();
}
// Database
public void save() {
if (this.count > 0 && this.ownerUid > 0) {
LunarRail.getGameDatabase().save(this);
} else if (this.getId() != null) {
LunarRail.getGameDatabase().delete(this);
}
}
// Proto
public Material toMaterialProto() {
var proto = Material.newInstance()
.setTid(this.getItemId())
.setNum(this.getCount());
return proto;
}
public Relic toRelicProto() {
var proto = Relic.newInstance()
.setTid(this.getItemId())
.setUniqueId(this.getInternalUid())
.setLevel(this.getLevel())
.setExp(this.getExp())
.setIsProtected(this.isLocked())
.setBaseAvatarId(this.getEquipAvatar())
.setMainAffixId(this.mainAffix);
if (this.subAffixes != null) {
for (var subAffix : this.subAffixes) {
proto.addSubAffixList(subAffix.toProto());
}
}
return proto;
}
public Equipment toEquipmentProto() {
var proto = Equipment.newInstance()
.setTid(this.getItemId())
.setUniqueId(this.getInternalUid())
.setLevel(this.getLevel())
.setExp(this.getExp())
.setIsProtected(this.isLocked())
.setPromotion(this.getPromotion())
.setRank(this.getRank())
.setBaseAvatarId(this.getEquipAvatar());
return proto;
}
public PileItem toPileProto() {
return PileItem.newInstance()
.setItemId(this.getItemId())
.setItemNum(this.getCount());
}
public Item toProto() {
return Item.newInstance()
.setItemId(this.getItemId())
.setNum(this.getCount());
}
}

View File

@ -0,0 +1,348 @@
package emu.lunarcore.game.inventory;
import java.util.Collection;
import java.util.stream.Stream;
import emu.lunarcore.LunarRail;
import emu.lunarcore.data.GameData;
import emu.lunarcore.data.common.ItemParam;
import emu.lunarcore.data.common.ItemParam.ItemParamType;
import emu.lunarcore.data.excel.AvatarExcel;
import emu.lunarcore.data.excel.ItemExcel;
import emu.lunarcore.game.avatar.AvatarStorage;
import emu.lunarcore.game.avatar.GameAvatar;
import emu.lunarcore.game.player.BasePlayerManager;
import emu.lunarcore.game.player.Player;
import emu.lunarcore.server.packet.send.PacketPlayerSyncScNotify;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
public class Inventory extends BasePlayerManager {
private final Long2ObjectMap<GameItem> store;
private final Int2ObjectMap<InventoryTab> inventoryTypes;
private int nextInternalUid;
public Inventory(Player player) {
super(player);
this.store = new Long2ObjectOpenHashMap<>();
this.inventoryTypes = new Int2ObjectOpenHashMap<>();
this.createInventoryTab(ItemMainType.Equipment, new EquipInventoryTab(1500));
this.createInventoryTab(ItemMainType.Relic, new EquipInventoryTab(1500));
this.createInventoryTab(ItemMainType.Material, new MaterialInventoryTab(2000));
}
public AvatarStorage getAvatarStorage() {
return this.getPlayer().getAvatars();
}
public Long2ObjectMap<GameItem> getItems() {
return store;
}
public Int2ObjectMap<InventoryTab> getInventoryTypes() {
return inventoryTypes;
}
public InventoryTab getInventoryTab(ItemMainType type) {
return getInventoryTypes().get(type.getVal());
}
public void createInventoryTab(ItemMainType type, InventoryTab tab) {
this.getInventoryTypes().put(type.getVal(), tab);
}
public int getNextItemInternalUid() {
return ++nextInternalUid;
}
/* Returns an item using its internal uid
* */
public GameItem getItemByUid(int uid) {
return this.getItems().get(uid);
}
public GameItem getMaterialByItemId(int id) {
return this.getInventoryTab(ItemMainType.Material).getItemById(id);
}
public GameItem getItemByParam(ItemParam param) {
if (param.getType() == ItemParamType.PILE) {
return this.getMaterialByItemId(param.getId());
} else if (param.getType() == ItemParamType.UNIQUE) {
return this.getItemByUid(param.getId());
}
return null;
}
public boolean addItem(int itemId) {
return addItem(itemId, 1);
}
public boolean addItem(int itemId, int count) {
ItemExcel excel = GameData.getItemExcelMap().get(itemId);
if (excel == null) {
return false;
}
GameItem item = new GameItem(excel, count);
return addItem(item);
}
public boolean addItem(GameItem item) {
GameItem result = putItem(item);
if (result != null) {
// TODO Send packet (update)
getPlayer().sendPacket(new PacketPlayerSyncScNotify(item));
return true;
}
return false;
}
private synchronized GameItem putItem(GameItem item) {
// Dont add items that dont have a valid item definition.
if (item.getExcel() == null) {
return null;
}
// Add item to inventory store
ItemMainType type = item.getExcel().getItemMainType();
InventoryTab tab = getInventoryTab(type);
// Add
switch (type) {
case Equipment:
case Relic:
if (tab.getSize() >= tab.getMaxCapacity()) {
return null;
}
// Duplicates cause problems
item.setCount(Math.max(item.getCount(), 1));
// Adds to inventory
this.putItem(item, tab);
// Set ownership and save to database
item.save();
return item;
case Virtual:
// Handle
this.addVirtualItem(item.getItemId(), item.getCount());
return item;
case AvatarCard:
// Add avatar
AvatarExcel avatarExcel = GameData.getAvatarExcelMap().get(item.getItemId());
if (avatarExcel != null && !getPlayer().getAvatars().hasAvatar(avatarExcel.getId())) {
getPlayer().addAvatar(new GameAvatar(avatarExcel));
}
return null;
case Material:
switch (item.getExcel().getItemSubType()) {
default:
if (tab == null) {
return null;
}
GameItem existingItem = tab.getItemById(item.getItemId());
if (existingItem == null) {
// Item type didnt exist before, we will add it to main inventory map if there is enough space
if (tab.getSize() >= tab.getMaxCapacity()) {
return null;
}
this.putItem(item, tab);
// Set ownership and save to db
item.save();
return item;
} else {
// Add count
existingItem.setCount(Math.min(existingItem.getCount() + item.getCount(), item.getExcel().getPileLimit()));
existingItem.save();
return existingItem;
}
}
default:
return null;
}
}
private synchronized void putItem(GameItem item, InventoryTab tab) {
// Set owner and internal uid first
item.setOwner(this.getPlayer());
// Add if tab exists
if (tab != null) {
// Put in item store
getItems().put(item.getInternalUid(), item);
// Add to tab
tab.onAddItem(item);
}
}
private void addVirtualItem(int itemId, int count) {
switch (itemId) {
case 1: // Stellar Jade
getPlayer().addHCoin(count);
break;
case 2: // Credit
getPlayer().addSCoin(count);
break;
case 3: // Oneiric Shard
getPlayer().addMCoin(count);
break;
case 11: // Trailblaze Power
getPlayer().addStamina(count);
break;
case 22: // Trailblaze EXP
getPlayer().addExp(count);
break;
}
}
public synchronized void removeItems(Collection<ItemParam> items) {
for (ItemParam param : items) {
GameItem item = this.getItemByParam(param);
if (item != null) {
this.removeItem(item, param.getCount());
}
}
}
public synchronized boolean removePileItem(int uid, int count) {
GameItem item = this.getMaterialByItemId(uid);
if (item == null) {
return false;
}
return removeItem(item, count);
}
public synchronized boolean removeUniqueItem(int uid, int count) {
GameItem item = this.getItemByUid(uid);
if (item == null) {
return false;
}
return removeItem(item, count);
}
public synchronized boolean removeItem(GameItem item, int count) {
// Sanity check
if (count <= 0 || item == null || item.getOwnerUid() != getPlayer().getUid()) {
return false;
}
if (item.getExcel() == null || item.getExcel().isEquippable()) {
item.setCount(0);
} else {
item.setCount(item.getCount() - count);
}
if (item.getCount() <= 0) {
// Remove from inventory tab too
InventoryTab tab = null;
if (item.getExcel() != null) {
tab = getInventoryTab(item.getExcel().getItemMainType());
}
// Remove from inventory if less than 0
deleteItem(item, tab);
// TODO Send packet (delete)
getPlayer().sendPacket(new PacketPlayerSyncScNotify(item));
} else {
// TODO Send packet (update)
getPlayer().sendPacket(new PacketPlayerSyncScNotify(item));
}
// Update in db
item.save();
// Returns true on success
return true;
}
private void deleteItem(GameItem item, InventoryTab tab) {
getItems().remove(item.getInternalUid());
if (tab != null) {
tab.onRemoveItem(item);
}
}
// Equips
public boolean equipItem(int avatarId, int equipId) {
GameAvatar avatar = getPlayer().getAvatarById(avatarId);
GameItem item = this.getItemByUid(equipId);
if (avatar != null && item != null) {
return avatar.equipItem(item);
}
return false;
}
public boolean unequipItem(int avatarId, int slot) {
GameAvatar avatar = getPlayer().getAvatars().getAvatarById(avatarId);
if (avatar != null) {
GameItem unequipped = avatar.unequipItem(slot);
if (unequipped != null) {
getPlayer().sendPacket(new PacketPlayerSyncScNotify(avatar, unequipped));
return true;
}
}
return false;
}
// Database
public void loadFromDatabase() {
Stream<GameItem> stream = LunarRail.getGameDatabase().getObjects(GameItem.class, "ownerUid", this.getPlayer().getUid());
stream.forEach(item -> {
// Should never happen
if (item.getId() == null) {
return;
}
// Load item excel data
ItemExcel excel = GameData.getItemExcelMap().get(item.getItemId());
if (excel == null) {
// Delete item if it has no excel data
item.setCount(0);
item.save();
return;
}
// Set ownerships
item.setExcel(excel);
// Put in inventory
InventoryTab tab = getInventoryTab(item.getExcel().getItemMainType());
putItem(item, tab);
// Equip to a character if possible
if (item.isEquipped()) {
GameAvatar avatar = getPlayer().getAvatarById(item.getEquipAvatar());
boolean hasEquipped = false;
if (avatar != null) {
hasEquipped = avatar.equipItem(item);
}
if (!hasEquipped) {
// Unset equipped flag on item since we couldnt find an avatar to equip it to
item.setEquipAvatar(0);
item.save();
}
}
});
}
}

View File

@ -0,0 +1,13 @@
package emu.lunarcore.game.inventory;
public abstract class InventoryTab implements Iterable<GameItem> {
public abstract GameItem getItemById(int id);
public abstract void onAddItem(GameItem item);
public abstract void onRemoveItem(GameItem item);
public abstract int getSize();
public abstract int getMaxCapacity();
}

View File

@ -0,0 +1,22 @@
package emu.lunarcore.game.inventory;
import lombok.Getter;
@Getter
public enum ItemMainType {
Unknown (0),
Virtual (1),
AvatarCard (2),
Equipment (3),
Relic (4),
Usable (5),
Material (6),
Mission (7),
Display (8);
private int val;
private ItemMainType(int value) {
this.val = value;
}
}

View File

@ -0,0 +1,19 @@
package emu.lunarcore.game.inventory;
import lombok.Getter;
@Getter
public enum ItemRarity {
Unknown (0),
Normal (1),
NotNormal (2),
Rare (3),
VeryRare (4),
SuperRare (5);
private int val;
private ItemRarity(int value) {
this.val = value;
}
}

View File

@ -0,0 +1,39 @@
package emu.lunarcore.game.inventory;
import dev.morphia.annotations.Entity;
import emu.lunarcore.data.excel.RelicSubAffixExcel;
import emu.lunarcore.proto.RelicAffixOuterClass.RelicAffix;
import emu.lunarcore.util.Utils;
import lombok.Getter;
@Getter
@Entity(useDiscriminator = false)
public class ItemSubAffix {
private int id; // Affix id
private int count;
private int step;
@Deprecated
public ItemSubAffix() {
// Morphia only!
}
public ItemSubAffix(RelicSubAffixExcel subAffix) {
this.id = subAffix.getAffixID();
this.count = 1;
this.step = Utils.randomRange(0, subAffix.getStepNum());
}
public void incrementCount() {
this.count += 1;
}
public RelicAffix toProto() {
var proto = RelicAffix.newInstance()
.setAffixId(this.id)
.setCnt(this.count)
.setStep(this.step);
return proto;
}
}

View File

@ -0,0 +1,35 @@
package emu.lunarcore.game.inventory;
import lombok.Getter;
@Getter
public enum ItemSubType {
Unknown (0),
Virtual (101),
GameplayCounter (102),
AvatarCard (201),
Equipment (301),
Relic (401),
Gift (501),
Food (502),
ForceOpitonalGift (503),
Book (504),
HeadIcon (505),
MusicAlbum (506),
Formula (507),
ChatBubble (508),
PhoneTheme (510),
Material (601),
Eidolon (602),
MuseumExhibit (603),
MuseumStuff (604),
Mission (701),
RelicSetShowOnly (801),
RelicRarityShowOnly (802);
private int val;
private ItemSubType(int value) {
this.val = value;
}
}

View File

@ -0,0 +1,46 @@
package emu.lunarcore.game.inventory;
import java.util.Iterator;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
public class MaterialInventoryTab extends InventoryTab {
private final Int2ObjectMap<GameItem> items;
private final int maxCapacity;
public MaterialInventoryTab(int maxCapacity) {
this.items = new Int2ObjectOpenHashMap<>();
this.maxCapacity = maxCapacity;
}
@Override
public GameItem getItemById(int id) {
return this.items.get(id);
}
@Override
public void onAddItem(GameItem item) {
this.items.put(item.getItemId(), item);
}
@Override
public void onRemoveItem(GameItem item) {
this.items.remove(item.getItemId());
}
@Override
public int getSize() {
return this.items.size();
}
@Override
public int getMaxCapacity() {
return this.maxCapacity;
}
@Override
public Iterator<GameItem> iterator() {
return items.values().iterator();
}
}

View File

@ -0,0 +1,20 @@
package emu.lunarcore.game.inventory;
import lombok.Getter;
@Getter
public enum RelicType {
Unknow (0),
HEAD (1),
HAND (2),
BODY (3),
FOOT (4),
NECK (5),
OBJECT (6);
private int val;
private RelicType(int value) {
this.val = value;
}
}

View File

@ -0,0 +1,13 @@
package emu.lunarcore.game.player;
public abstract class BasePlayerManager {
private transient Player player;
public BasePlayerManager(Player player) {
this.player = player;
}
public Player getPlayer() {
return player;
}
}

View File

@ -0,0 +1,266 @@
package emu.lunarcore.game.player;
import java.util.List;
import dev.morphia.annotations.Entity;
import emu.lunarcore.GameConstants;
import emu.lunarcore.game.avatar.GameAvatar;
import emu.lunarcore.server.packet.send.PacketSyncLineupNotify;
import lombok.Getter;
@Entity(useDiscriminator = false) @Getter
public class LineupManager {
private transient Player player;
private PlayerLineup[] lineups;
private int currentIndex;
private int currentLeader;
@Deprecated
public LineupManager() {
// Morphia only!
}
public LineupManager(Player player) {
this();
this.validate(player);
}
public PlayerLineup getLineup(int index) {
// Sanity
if (index < 0 || index >= this.getLineups().length) {
return null;
}
return this.lineups[index];
}
public PlayerLineup getCurrentLineup() {
return getLineup(this.currentIndex);
}
// Lineup functions
public boolean changeLeader(int slot) {
if (slot >= 0 && slot < this.getCurrentLineup().size()) {
this.currentLeader = slot;
return true;
}
return false;
}
public boolean joinLineup(int index, int slot, int avatarId) {
// Get lineup
PlayerLineup lineup = this.getLineup(index);
if (lineup == null) return false;
boolean isCurrentLineup = lineup == getCurrentLineup();
// Get avatar
GameAvatar avatar = getPlayer().getAvatarById(avatarId);
if (avatar == null) return false;
// Join lineup
if (slot >= 0 && slot < lineup.size()) {
// Replace avatar
lineup.getAvatars().set(slot, avatarId);
} else if (lineup.size() < GameConstants.MAX_AVATARS_IN_TEAM) {
// Add avatar
lineup.getAvatars().add(avatarId);
} else {
// No changes were made
return false;
}
// Save
this.getPlayer().save();
// Sync lineup with scene
if (isCurrentLineup) {
player.getScene().syncLineup();
}
// Sync lineup data
player.sendPacket(new PacketSyncLineupNotify(lineup));
return true;
}
public boolean quitLineup(int index, int avatarId) {
// Get lineup
PlayerLineup lineup = this.getLineup(index);
if (lineup == null) return false;
boolean isCurrentLineup = lineup == getCurrentLineup();
// Sanity check to make sure were not removing the last member of our lineup
if (isCurrentLineup && lineup.size() <= 1) {
return false;
}
//
int i = lineup.getAvatars().indexOf(avatarId);
if (i != -1) {
lineup.getAvatars().remove(i);
} else {
return false;
}
// Validate leader index
if (this.getCurrentLeader() >= lineup.size()) {
this.currentLeader = 0;
}
// Save
this.getPlayer().save();
// Sync lineup with scene
if (isCurrentLineup) {
player.getScene().syncLineup();
}
// Sync lineup data
player.sendPacket(new PacketSyncLineupNotify(lineup));
return true;
}
public boolean switchLineup(int index) {
// Sanity
if (index == this.getCurrentIndex()) {
return false;
}
// Get lineup
PlayerLineup lineup = this.getLineup(index);
// Make sure lineup exists and has size
if (lineup == null || lineup.size() == 0) {
return false;
}
// Set index
this.currentIndex = index;
this.currentLeader = 0;
// Save
this.getPlayer().save();
// Sync lineup data
player.getScene().syncLineup();
player.sendPacket(new PacketSyncLineupNotify(lineup));
return true;
}
public boolean replaceLineup(int index, List<Integer> lineupList) {
// Sanity - Make sure player cant remove all avatars from the current lineup
if (index == this.currentIndex && lineupList.size() == 0) {
return false;
}
// Get lineup
PlayerLineup lineup = this.getLineup(index);
if (lineup == null) return false;
// Clear
lineup.getAvatars().clear();
// Add
for (int avatarId : lineupList) {
GameAvatar avatar = getPlayer().getAvatarById(avatarId);
if (avatar != null) {
lineup.getAvatars().add(avatarId);
}
}
// Validate leader index
if (this.getCurrentLeader() >= lineup.size()) {
this.currentLeader = 0;
}
// Save
this.getPlayer().save();
// Sync lineup with scene
if (lineup == getCurrentLineup()) {
player.getScene().syncLineup();
}
// Sync lineup data
player.sendPacket(new PacketSyncLineupNotify(lineup));
return true;
}
public boolean swapLineup(int index, int src, int dest) {
// Sanity
if (src == dest) return false;
// Get lineup
PlayerLineup lineup = this.getLineup(index);
// Validate slots
if ((lineup == null) || (src < 0 && src >= lineup.size())) {
return false;
}
if (dest < 0 && dest >= lineup.size()) {
return false;
}
// Swap
int srcId = lineup.getAvatars().get(src);
int destId = lineup.getAvatars().get(dest);
lineup.getAvatars().set(src, destId);
lineup.getAvatars().set(dest, srcId);
// Save
this.getPlayer().save();
// Sync lineup data
player.sendPacket(new PacketSyncLineupNotify(lineup));
return true;
}
public boolean changeLineupName(int index, String name) {
// Get lineup
PlayerLineup lineup = this.getLineup(index);
if (lineup == null) return false;
// Change name
lineup.setName(name);
return true;
}
// Max sure all lineups exist in the array
public void validate(Player player) {
// Set player
this.player = player;
// Make sure lineups exist
if (this.getLineups() == null) {
this.lineups = new PlayerLineup[GameConstants.DEFAULT_TEAMS];
} else if (this.getLineups().length != GameConstants.DEFAULT_TEAMS) {
// TODO move lineups from old array to this new one
this.lineups = new PlayerLineup[GameConstants.DEFAULT_TEAMS];
}
// Create new lineups for any missing ones
for (int i = 0; i < this.lineups.length; i++) {
if (this.lineups[i] == null) {
this.lineups[i] = new PlayerLineup(i);
}
this.lineups[i].setOwnerAndIndex(getPlayer(), i);
}
// Set current index if out of bounds
if (this.currentIndex < 0) {
this.currentIndex = 0;
} else if (this.currentIndex >= this.lineups.length) {
this.currentIndex = this.lineups.length - 1;
}
}
}

View File

@ -0,0 +1,310 @@
package emu.lunarcore.game.player;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import dev.morphia.annotations.Indexed;
import emu.lunarcore.GameConstants;
import emu.lunarcore.LunarRail;
import emu.lunarcore.data.GameData;
import emu.lunarcore.data.config.AnchorInfo;
import emu.lunarcore.data.config.FloorInfo;
import emu.lunarcore.data.config.PropInfo;
import emu.lunarcore.data.excel.MapEntranceExcel;
import emu.lunarcore.game.account.Account;
import emu.lunarcore.game.avatar.AvatarStorage;
import emu.lunarcore.game.avatar.GameAvatar;
import emu.lunarcore.game.gacha.PlayerGachaInfo;
import emu.lunarcore.game.inventory.Inventory;
import emu.lunarcore.game.scene.Scene;
import emu.lunarcore.proto.PlayerBasicInfoOuterClass.PlayerBasicInfo;
import emu.lunarcore.server.game.GameServer;
import emu.lunarcore.server.game.GameSession;
import emu.lunarcore.server.packet.BasePacket;
import emu.lunarcore.server.packet.SessionState;
import emu.lunarcore.server.packet.send.PacketEnterSceneByServerScNotify;
import emu.lunarcore.server.packet.send.PacketPlayerSyncScNotify;
import emu.lunarcore.server.packet.send.PacketRevcMsgScNotify;
import emu.lunarcore.util.Position;
import lombok.Getter;
@Entity(value = "players", useDiscriminator = false)
@Getter
public class Player {
private transient GameSession session;
@Id private int uid;
@Indexed private String accountUid;
private String name;
private String signature;
private int birthday;
private int level;
private int exp;
private int worldLevel;
private int stamina;
private int scoin; // Credits
private int hcoin; // Jade
private int mcoin; // Crystals
private transient Scene scene;
private Position pos;
private int planeId;
private int floorId;
private int entryId;
// Player managers
private transient final AvatarStorage avatars;
private transient final Inventory inventory;
// Database persistent data
private LineupManager lineupManager;
private PlayerGachaInfo gachaInfo;
@Deprecated // Morphia only
public Player() {
this.avatars = new AvatarStorage(this);
this.inventory = new Inventory(this);
}
// Called when player is created
public Player(GameSession session) {
this();
this.session = session;
this.accountUid = getAccount().getUid();
this.name = GameConstants.DEFAULT_NAME;
this.level = 1;
this.stamina = GameConstants.MAX_STAMINA;
this.pos = new Position(-36589, -5400, 70019);
this.planeId = 10101;
this.floorId = 10101001;
this.entryId = 1010101;
this.lineupManager = new LineupManager(this);
this.gachaInfo = new PlayerGachaInfo();
// Setup uid
this.initUid();
// Give us a starter character.
// TODO script tutorial
GameAvatar avatar = new GameAvatar(8001);
this.getAvatars().addAvatar(avatar);
this.getLineupManager().getCurrentLineup().getAvatars().add(8001);
}
public GameServer getServer() {
return session.getServer();
}
public Account getAccount() {
return session.getAccount();
}
public void setSession(GameSession session) {
if (this.session == null) {
this.session = session;
}
}
public boolean setNickname(String nickname) {
if (nickname != this.name && nickname.length() <= 32) {
this.name = nickname;
this.sendPacket(new PacketPlayerSyncScNotify(this));
this.save();
return true;
}
return false;
}
public int setBirthday(int birthday) {
if (this.birthday == 0) {
int month = birthday / 100;
int day = birthday % 100;
if (month >= 1 && month <= 12 && day >= 1 && day <= 31) {
this.birthday = birthday;
this.save();
return this.birthday;
}
}
return 0;
}
public boolean hasLoggedIn() {
return this.getSession() != null && this.getSession().getState() != SessionState.WAITING_FOR_TOKEN;
}
public boolean addAvatar(GameAvatar avatar) {
return getAvatars().addAvatar(avatar);
}
public GameAvatar getAvatarById(int avatarId) {
return getAvatars().getAvatarById(avatarId);
}
private void initUid() {
if (this.uid > 0) return;
int nextUid = session.getAccount().getReservedPlayerUid();
if (nextUid == GameConstants.SERVER_CONSOLE_UID) {
nextUid = 0;
}
while (nextUid == 0 || LunarRail.getGameDatabase().checkIfObjectExists(Player.class, nextUid)) {
nextUid = LunarRail.getGameDatabase().getNextObjectId(Player.class);
}
this.uid = nextUid;
}
public void addSCoin(int amount) {
this.scoin += amount;
this.sendPacket(new PacketPlayerSyncScNotify(this));
}
public void addHCoin(int amount) {
this.hcoin += amount;
this.sendPacket(new PacketPlayerSyncScNotify(this));
}
public void addMCoin(int amount) {
this.mcoin += amount;
this.sendPacket(new PacketPlayerSyncScNotify(this));
}
public void addStamina(int amount) {
this.stamina = Math.min(this.stamina + amount, GameConstants.MAX_STAMINA);
this.sendPacket(new PacketPlayerSyncScNotify(this));
}
public void addExp(int amount) {
// Required exp
int reqExp = GameData.getPlayerExpRequired(level + 1);
// Add exp
this.exp += amount;
while (this.exp >= reqExp && reqExp > 0) {
this.level += 1;
reqExp = GameData.getPlayerExpRequired(this.level + 1);
}
// Save
this.save();
// Send packet
this.sendPacket(new PacketPlayerSyncScNotify(this));
}
public int getDisplayExp() {
return this.exp - GameData.getPlayerExpRequired(this.level);
}
public void enterScene(int entryId, int teleportId) {
// Get map entrance excel
MapEntranceExcel entry = GameData.getMapEntranceExcelMap().get(entryId);
if (entry == null) return;
// Get floor info
FloorInfo floor = GameData.getFloorInfo(entry.getPlaneID(), entry.getFloorID());
if (floor == null) return;
// Get teleport anchor
int startGroup = entry.getStartGroupID();
int anchorId = entry.getStartAnchorID();
if (teleportId != 0 || anchorId == 0) {
PropInfo teleport = floor.getCachedTeleports().get(teleportId);
if (teleport != null) {
startGroup = teleport.getAnchorGroupID();
anchorId = teleport.getAnchorID();
}
}
AnchorInfo anchor = floor.getAnchorInfo(startGroup, anchorId);
if (anchor == null) return;
// Set position
this.getPos().set(
(int) (anchor.getPosX() * 1000f),
(int) (anchor.getPosY() * 1000f),
(int) (anchor.getPosZ() * 1000f)
);
this.planeId = entry.getPlaneID();
this.floorId = entry.getFloorID();
this.entryId = entry.getId();
// Save player
this.save();
// Move to scene
loadScene(entry.getPlaneID(), entry.getFloorID(), entry.getId());
}
private void loadScene(int planeId, int floorId, int entryId) {
// Sanity check
if (this.scene != null && this.scene.getPlaneId() == planeId) {
// Don't create a new scene if were already in the one we want to teleport to
} else {
this.scene = new Scene(this, planeId, floorId, entryId);
}
// TODO send packet
if (this.getSession().getState() != SessionState.WAITING_FOR_TOKEN) {
this.sendPacket(new PacketEnterSceneByServerScNotify(this));
}
}
public void dropMessage(String message) {
this.sendPacket(new PacketRevcMsgScNotify(this, message));
}
public void sendPacket(BasePacket packet) {
if (this.hasLoggedIn()) {
this.getSession().send(packet);
}
}
public void save() {
if (this.uid <= 0) {
LunarRail.getLogger().error("Tried to save a player object without a uid!");
return;
}
LunarRail.getGameDatabase().save(this);
}
public void onLogin() {
// Load avatars and inventory first
this.getAvatars().loadFromDatabase();
this.getInventory().loadFromDatabase();
this.getAvatars().recalcAvatarStats(); // Recalc stats after items have loaded for the avatars
// Load Etc
this.getLineupManager().validate(this);
// Enter scene (should happen after everything else loads)
this.loadScene(planeId, floorId, entryId);
}
public PlayerBasicInfo toProto() {
var proto = PlayerBasicInfo.newInstance()
.setNickname(this.getName())
.setLevel(this.getLevel())
.setExp(this.getDisplayExp())
.setWorldLevel(this.getWorldLevel())
.setScoin(this.getScoin())
.setHcoin(this.getHcoin())
.setMcoin(this.getMcoin())
.setStamina(this.getStamina());
return proto;
}
}

View File

@ -0,0 +1,85 @@
package emu.lunarcore.game.player;
import java.util.ArrayList;
import java.util.List;
import dev.morphia.annotations.Entity;
import emu.lunarcore.GameConstants;
import emu.lunarcore.game.avatar.GameAvatar;
import emu.lunarcore.proto.ExtraLineupTypeOuterClass.ExtraLineupType;
import emu.lunarcore.proto.LineupInfoOuterClass.LineupInfo;
import lombok.Getter;
@Entity(useDiscriminator = false) @Getter
public class PlayerLineup {
private transient Player owner;
private transient int index;
private String name;
private List<Integer> avatars;
@Deprecated // Morphia only!
public PlayerLineup() {
}
public PlayerLineup(int index) {
this.name = "Squad " + (index + 1);
this.avatars = new ArrayList<>(GameConstants.MAX_AVATARS_IN_TEAM);
}
protected void setOwnerAndIndex(Player player, int index) {
this.owner = player;
this.index = index;
}
public void setName(String name) {
this.name = name;
}
public int size() {
return avatars.size();
}
public boolean contains(GameAvatar avatar) {
return getAvatars().contains(avatar.getAvatarId());
}
public boolean addAvatar(GameAvatar avatar) {
if (contains(avatar)) {
return false;
}
getAvatars().add(avatar.getAvatarId());
return true;
}
public boolean removeAvatar(int slot) {
if (size() <= 1) {
return false;
}
getAvatars().remove(slot);
return true;
}
public LineupInfo toProto() {
var proto = LineupInfo.newInstance()
.setIndex(index)
.setName(this.getName())
.setLeaderSlot(this.getOwner().getLineupManager().getCurrentLeader())
.setTechniquePoints(5)
.setExtraLineupType(ExtraLineupType.LINEUP_NONE);
for (int slot = 0; slot < this.getAvatars().size(); slot++) {
GameAvatar avatar = owner.getAvatars().getAvatarById(getAvatars().get(slot));
if (avatar == null) continue;
proto.addAvatarList(avatar.toLineupAvatarProto(slot));
}
return proto;
}
}

View File

@ -0,0 +1,50 @@
package emu.lunarcore.game.scene;
import emu.lunarcore.data.excel.NpcMonsterExcel;
import emu.lunarcore.data.excel.StageExcel;
import emu.lunarcore.proto.MotionInfoOuterClass.MotionInfo;
import emu.lunarcore.proto.SceneEntityInfoOuterClass.SceneEntityInfo;
import emu.lunarcore.proto.SceneNpcMonsterInfoOuterClass.SceneNpcMonsterInfo;
import emu.lunarcore.proto.VectorOuterClass.Vector;
import emu.lunarcore.util.Position;
import lombok.Getter;
import lombok.Setter;
@Getter
public class EntityMonster implements GameEntity {
@Setter private int entityId;
@Setter private int worldLevel;
@Setter private int groupId;
@Setter private int instId;
@Setter private int eventId;
private NpcMonsterExcel excel;
private StageExcel stage;
private Position pos;
private Position rot;
public EntityMonster(NpcMonsterExcel excel, StageExcel stage, Position pos) {
this.excel = excel;
this.stage = stage;
this.pos = pos;
this.rot = new Position();
}
@Override
public SceneEntityInfo toSceneEntityProto() {
var monster = SceneNpcMonsterInfo.newInstance()
.setWorldLevel(this.getWorldLevel())
.setMonsterId(excel.getId())
.setEventId(this.getEventId());
var proto = SceneEntityInfo.newInstance()
.setEntityId(this.getEntityId())
.setGroupId(this.getGroupId())
.setInstId(this.getInstId())
.setMotion(MotionInfo.newInstance().setPos(getPos().toProto()).setRot(getRot().toProto()))
.setNpcMonster(monster);
return proto;
}
}

View File

@ -0,0 +1,48 @@
package emu.lunarcore.game.scene;
import emu.lunarcore.data.excel.NpcMonsterExcel;
import emu.lunarcore.data.excel.StageExcel;
import emu.lunarcore.proto.MotionInfoOuterClass.MotionInfo;
import emu.lunarcore.proto.SceneEntityInfoOuterClass.SceneEntityInfo;
import emu.lunarcore.proto.SceneNpcMonsterInfoOuterClass.SceneNpcMonsterInfo;
import emu.lunarcore.proto.ScenePropInfoOuterClass.ScenePropInfo;
import emu.lunarcore.proto.VectorOuterClass.Vector;
import emu.lunarcore.util.Position;
import lombok.Getter;
import lombok.Setter;
@Getter
public class EntityProp implements GameEntity {
@Setter private int entityId;
@Setter private int groupId;
@Setter private int instId;
@Setter private int propId;
@Setter private PropState state;
private Position pos;
private Position rot;
public EntityProp(int propId, Position pos) {
this.propId = propId;
this.pos = pos;
this.rot = new Position();
this.state = PropState.Closed;
}
@Override
public SceneEntityInfo toSceneEntityProto() {
var prop = ScenePropInfo.newInstance()
.setPropId(this.getPropId())
.setPropState(this.getState().getVal());
var proto = SceneEntityInfo.newInstance()
.setEntityId(this.getEntityId())
.setGroupId(this.getGroupId())
.setInstId(this.getInstId())
.setMotion(MotionInfo.newInstance().setPos(getPos().toProto()).setRot(getRot().toProto()))
.setProp(prop);
return proto;
}
}

View File

@ -0,0 +1,15 @@
package emu.lunarcore.game.scene;
import emu.lunarcore.proto.SceneEntityInfoOuterClass.SceneEntityInfo;
public interface GameEntity {
public int getEntityId();
public void setEntityId(int id);
public default int getGroupId() {
return 0;
}
public SceneEntityInfo toSceneEntityProto();
}

View File

@ -0,0 +1,48 @@
package emu.lunarcore.game.scene;
import lombok.Getter;
public enum PropState {
Closed (0),
Open (1),
Locked (2),
BridgeState1 (3),
BridgeState2 (4),
BridgeState3 (5),
BridgeState4 (6),
CheckPointDisable (7),
CheckPointEnable (8),
TriggerDisable (9),
TriggerEnable (10),
ChestLocked (11),
ChestClosed (12),
ChestUsed (13),
Elevator1 (14),
Elevator2 (15),
Elevator3 (16),
WaitActive (17),
EventClose (18),
EventOpen (19),
Hidden (20),
TeleportGate0 (21),
TeleportGate1 (22),
TeleportGate2 (23),
TeleportGate3 (24),
Destructed (25),
CustomState01 (101),
CustomState02 (102),
CustomState03 (103),
CustomState04 (104),
CustomState05 (105),
CustomState06 (106),
CustomState07 (107),
CustomState08 (108),
CustomState09 (109);
@Getter
private final int val;
private PropState(int val) {
this.val = val;
}
}

View File

@ -0,0 +1,224 @@
package emu.lunarcore.game.scene;
import java.util.ArrayList;
import java.util.List;
import emu.lunarcore.data.GameData;
import emu.lunarcore.data.config.FloorInfo;
import emu.lunarcore.data.config.GroupInfo;
import emu.lunarcore.data.config.GroupInfo.GroupLoadSide;
import emu.lunarcore.data.config.MonsterInfo;
import emu.lunarcore.data.config.PropInfo;
import emu.lunarcore.data.excel.NpcMonsterExcel;
import emu.lunarcore.data.excel.StageExcel;
import emu.lunarcore.game.avatar.GameAvatar;
import emu.lunarcore.game.player.PlayerLineup;
import emu.lunarcore.game.player.Player;
import emu.lunarcore.proto.SceneEntityGroupInfoOuterClass.SceneEntityGroupInfo;
import emu.lunarcore.proto.SceneInfoOuterClass.SceneInfo;
import emu.lunarcore.server.packet.send.PacketSceneEntityUpdateScNotify;
import emu.lunarcore.server.packet.send.PacketSceneGroupRefreshScNotify;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import lombok.Getter;
@Getter
public class Scene {
private final Player player;
private final int planeId;
private final int floorId;
private final int entryId;
private int lastEntityId = 0;
// Avatar entites
private IntSet avatarEntityIds;
private Int2ObjectMap<GameAvatar> avatars;
// Other entities TODO
private Int2ObjectMap<GameEntity> entities;
public Scene(Player player, int planeId, int floorId, int entryId) {
this.player = player;
this.planeId = planeId;
this.floorId = floorId;
this.entryId = entryId;
// Setup avatars
this.avatarEntityIds = new IntOpenHashSet();
this.avatars = new Int2ObjectOpenHashMap<>();
this.entities = new Int2ObjectOpenHashMap<>();
PlayerLineup lineup = getPlayer().getLineupManager().getCurrentLineup();
for (int avatarId : lineup.getAvatars()) {
GameAvatar avatar = getPlayer().getAvatarById(avatarId);
if (avatar == null) continue;
this.avatars.put(avatarId, avatar);
// Add entity id
avatar.setEntityId(this.getNextEntityId());
this.avatarEntityIds.add(avatar.getEntityId());
}
// Spawn monsters
FloorInfo floorInfo = GameData.getFloorInfo(this.planeId, this.floorId);
if (floorInfo == null) return;
for (GroupInfo group : floorInfo.getGroups().values()) {
// Skip non-server groups
if (group.getLoadSide() != GroupLoadSide.Server) {
continue;
}
// Add monsters
if (group.getMonsterList() != null || group.getMonsterList().size() > 0) {
for (MonsterInfo monsterInfo : group.getMonsterList()) {
// Get excels from game data
NpcMonsterExcel excel = GameData.getNpcMonsterExcelMap().get(monsterInfo.getNPCMonsterID());
StageExcel stage = GameData.getStageExcelMap().get(1);
if (excel == null || stage == null) continue;
// Create monster with excels
EntityMonster monster = new EntityMonster(excel, stage, monsterInfo.clonePos());
monster.getRot().setY((int) (monsterInfo.getRotY() * 1000f));
monster.setInstId(monsterInfo.getID());
monster.setEventId(monsterInfo.getEventID());
monster.setGroupId(group.getId());
// Add to monsters
this.addEntity(monster);
}
}
// Add props
if (group.getPropList() != null || group.getPropList().size() > 0) {
for (PropInfo propInfo : group.getPropList()) {
// Dont add deleted props?
/*
if (propInfo.isIsDelete()) {
continue;
}
*/
// Create prop from prop info
EntityProp prop = new EntityProp(propInfo.getPropID(), propInfo.clonePos());
//prop.setState(propInfo.getState());
prop.getRot().set(
(int) (propInfo.getRotX() * 1000f),
(int) (propInfo.getRotY() * 1000f),
(int) (propInfo.getRotZ() * 1000f)
);
prop.setInstId(propInfo.getID());
prop.setGroupId(group.getId());
// Add to monsters
this.addEntity(prop);
}
}
}
}
private int getNextEntityId() {
return ++lastEntityId;
}
public void syncLineup() {
// Get current lineup
PlayerLineup lineup = getPlayer().getLineupManager().getCurrentLineup();
// Setup new avatars list
var newAvatars = new Int2ObjectOpenHashMap<GameAvatar>();
for (int avatarId : lineup.getAvatars()) {
GameAvatar avatar = getPlayer().getAvatarById(avatarId);
if (avatar == null) continue;
newAvatars.put(avatarId, avatar);
}
// Clear entity id cache
this.avatarEntityIds.clear();
// Add/Remove
List<GameAvatar> toAdd = new ArrayList<>();
List<GameAvatar> toRemove = new ArrayList<>();
for (var avatar : newAvatars.values()) {
if (!this.avatars.containsKey(avatar.getAvatarId())) {
toAdd.add(avatar);
avatar.setEntityId(getNextEntityId());
}
// Add to entity id cache
this.avatarEntityIds.add(avatar.getEntityId());
}
for (var avatar : this.avatars.values()) {
if (!newAvatars.containsKey(avatar.getAvatarId())) {
toRemove.add(avatar);
avatar.setEntityId(0);
}
}
// Sync packet
getPlayer().sendPacket(new PacketSceneGroupRefreshScNotify(toAdd, toRemove));
}
public synchronized void addEntity(GameEntity entity) {
// Dont add if monster id already exists
if (entity.getEntityId() != 0) return;
// Set entity id and add monster to entity map
entity.setEntityId(this.getNextEntityId());
this.entities.put(entity.getEntityId(), entity);
}
public synchronized void removeEntity(int entityId) {
GameEntity entity = this.entities.remove(entityId);
// TODO send packet
}
public SceneInfo toProto() {
// Proto
var proto = SceneInfo.newInstance()
.setWorldId(301)
.setLCMMECNPOBA(2)
.setPlaneId(this.getPlaneId())
.setFloorId(this.getFloorId())
.setEntryId(this.getEntryId());
// Get current lineup
PlayerLineup lineup = getPlayer().getLineupManager().getCurrentLineup();
int leaderAvatarId = lineup.getAvatars().get(getPlayer().getLineupManager().getCurrentLeader());
// Scene group
var playerGroup = SceneEntityGroupInfo.newInstance();
for (var avatar : avatars.values()) {
playerGroup.addEntityList(avatar.toSceneEntityProto());
if (leaderAvatarId == avatar.getAvatarId()) {
proto.setLeaderEntityId(avatar.getEntityId());
}
}
proto.addEntityGroupList(playerGroup);
// Sort entities into groups
var groups = new Int2ObjectOpenHashMap<SceneEntityGroupInfo>();
for (var monster : entities.values()) {
var group = groups.computeIfAbsent(monster.getGroupId(), i -> SceneEntityGroupInfo.newInstance().setGroupId(i));
group.addEntityList(monster.toSceneEntityProto());
}
for (var group : groups.values()) {
proto.addEntityGroupList(group);
}
// Done
return proto;
}
}

View File

@ -0,0 +1,46 @@
package emu.lunarcore.game.service;
import emu.lunarcore.commands.PlayerCommands;
import emu.lunarcore.game.player.Player;
import emu.lunarcore.server.game.BaseGameService;
import emu.lunarcore.server.game.GameServer;
public class ChatService extends BaseGameService {
public ChatService(GameServer server) {
super(server);
}
public void sendPrivChat(Player player, int targetUid, String message) {
// Sanity checks
if (message == null || message.length() == 0) {
return;
}
// Check if command
if (message.charAt(0) == '!') {
PlayerCommands.handle(player, message);
return;
}
// Get target
Player target = getServer().getPlayerByUid(targetUid);
if (target == null) {
return;
}
// Create chat packet TODO
}
public void sendPrivChat(Player player, int targetUid, int emote) {
// Get target
Player target = getServer().getPlayerByUid(targetUid);
if (target == null) {
return;
}
// Create chat packet TODO
}
}

View File

@ -0,0 +1,496 @@
package emu.lunarcore.game.service;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import emu.lunarcore.data.GameData;
import emu.lunarcore.data.common.ItemParam;
import emu.lunarcore.data.excel.AvatarPromotionExcel;
import emu.lunarcore.data.excel.AvatarRankExcel;
import emu.lunarcore.data.excel.AvatarSkillTreeExcel;
import emu.lunarcore.data.excel.EquipmentPromotionExcel;
import emu.lunarcore.game.avatar.GameAvatar;
import emu.lunarcore.game.inventory.GameItem;
import emu.lunarcore.game.player.Player;
import emu.lunarcore.server.game.BaseGameService;
import emu.lunarcore.server.game.GameServer;
import emu.lunarcore.server.packet.BasePacket;
import emu.lunarcore.server.packet.CmdId;
import emu.lunarcore.server.packet.send.*;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
public class InventoryService extends BaseGameService {
public InventoryService(GameServer server) {
super(server);
}
// === Avatars ===
public void levelUpAvatar(Player player, int avatarId, Collection<ItemParam> items) {
// Get avatar
GameAvatar avatar = player.getAvatarById(avatarId);
if (avatar == null) return;
AvatarPromotionExcel promoteData = GameData.getAvatarPromotionExcel(avatarId, avatar.getPromotion());
if (promoteData == null) return;
// Exp gain
int expGain = 0;
// Verify items
for (ItemParam param : items) {
GameItem item = player.getInventory().getItemByParam(param);
if (item == null || item.getExcel().getAvatarExp() == 0 || item.getCount() < param.getCount()) return;
expGain += item.getExcel().getAvatarExp() * param.getCount();
}
// Verify credits
int cost = expGain / 10;
if (player.getScoin() < cost) {
player.sendPacket(new PacketAvatarExpUpScRsp());
return;
}
// Pay items
player.getInventory().removeItems(items);
player.addSCoin(-cost);
// Level up
int maxLevel = promoteData.getMaxLevel();
int level = avatar.getLevel();
int exp = avatar.getExp();
int reqExp = GameData.getAvatarExpRequired(avatar.getExcel().getExpGroup(), level);
while (expGain > 0 && reqExp > 0 && level < maxLevel) {
// Do calculations
int toGain = Math.min(expGain, reqExp - exp);
exp += toGain;
expGain -= toGain;
// Level up
if (exp >= reqExp) {
// Exp
exp = 0;
level += 1;
// Set req exp
reqExp = GameData.getAvatarExpRequired(avatar.getExcel().getExpGroup(), level);
}
}
// Done
avatar.setLevel(level);
avatar.setExp(exp);
avatar.save();
player.save();
// TODO add back leftover exp
List<GameItem> returnItems = new ArrayList<>();
// Send packets
player.sendPacket(new PacketPlayerSyncScNotify(avatar));
player.sendPacket(new PacketAvatarExpUpScRsp(returnItems));
}
public void promoteAvatar(Player player, int avatarId) {
// Get avatar
GameAvatar avatar = player.getAvatarById(avatarId);
if (avatar == null || avatar.getPromotion() >= avatar.getExcel().getMaxPromotion()) return;
AvatarPromotionExcel promotion = GameData.getAvatarPromotionExcel(avatarId, avatar.getPromotion());
// Sanity check
if ((promotion == null) || avatar.getLevel() < promotion.getMaxLevel() || player.getLevel() < promotion.getPlayerLevelRequire() || player.getWorldLevel() < promotion.getWorldLevelRequire()) {
return;
}
// Verify items
for (ItemParam param : promotion.getPromotionCostList()) {
GameItem item = player.getInventory().getItemByParam(param);
if (item == null || item.getCount() < param.getCount()) return;
}
// Verify credits
if (player.getScoin() < promotion.getPromotionCostCoin()) {
player.sendPacket(new BasePacket(CmdId.PromoteAvatarScRsp));
return;
}
// Pay items
player.getInventory().removeItems(promotion.getPromotionCostList());
player.addSCoin(-promotion.getPromotionCostCoin());
// Promote
avatar.setPromotion(avatar.getPromotion() + 1);
avatar.save();
player.save();
// Send packets
player.sendPacket(new PacketPlayerSyncScNotify(avatar));
player.sendPacket(new BasePacket(CmdId.PromoteAvatarScRsp));
}
public void unlockSkillTreeAvatar(Player player, int pointId) {
// Hacky way to get avatar id
int avatarId = pointId / 1000;
// Get avatar + Skill Tree data
GameAvatar avatar = player.getAvatarById(avatarId);
if (avatar == null) return;
int nextLevel = avatar.getSkills().getOrDefault(pointId, 0) + 1;
AvatarSkillTreeExcel skillTree = GameData.getAvatarSkillTreeExcel(pointId, nextLevel);
if (skillTree == null || skillTree.getAvatarID() != avatarId) return;
// Verify items
for (ItemParam param : skillTree.getMaterialList()) {
GameItem item = player.getInventory().getItemByParam(param);
if (item == null || item.getCount() < param.getCount()) {
player.sendPacket(new PacketUnlockSkilltreeScRsp());
return;
}
}
// Verify credits
if (player.getScoin() < skillTree.getMaterialCostCoin()) {
player.sendPacket(new PacketUnlockSkilltreeScRsp());
return;
}
// Pay items
player.getInventory().removeItems(skillTree.getMaterialList());
player.addSCoin(-skillTree.getMaterialCostCoin());
// Add skill
avatar.getSkills().put(pointId, nextLevel);
avatar.save();
player.save();
// Send packets
player.sendPacket(new PacketPlayerSyncScNotify(avatar));
player.sendPacket(new PacketUnlockSkilltreeScRsp(avatarId, pointId, nextLevel));
}
public void rankUpAvatar(Player player, int avatarId) {
// Get avatar
GameAvatar avatar = player.getAvatarById(avatarId);
if (avatar == null || avatar.getRank() >= avatar.getExcel().getMaxRank()) return;
AvatarRankExcel rankData = GameData.getAvatarRankExcel(avatar.getExcel().getRankId(avatar.getRank()));
if (rankData == null) return;
// Verify items
for (ItemParam param : rankData.getUnlockCost()) {
GameItem item = player.getInventory().getItemByParam(param);
if (item == null || item.getCount() < param.getCount()) {
player.sendPacket(new BasePacket(CmdId.RankUpAvatarScRsp));
return;
}
}
// Pay items
player.getInventory().removeItems(rankData.getUnlockCost());
// Add rank
avatar.setRank(avatar.getRank() + 1);
avatar.save();
// Send packets
player.sendPacket(new PacketPlayerSyncScNotify(avatar));
player.sendPacket(new BasePacket(CmdId.RankUpAvatarScRsp));
}
// === Equipment ===
public void levelUpEquipment(Player player, int equipId, Collection<ItemParam> items) {
// Get equipment
GameItem equip = player.getInventory().getItemByUid(equipId);
if (equip == null || !equip.getExcel().isEquipment()) {
player.sendPacket(new PacketExpUpEquipmentScRsp());
return;
}
EquipmentPromotionExcel promoteData = GameData.getEquipmentPromotionExcel(equip.getItemId(), equip.getPromotion());
if (promoteData == null) return;
// Exp gain
int cost = 0;
int expGain = 0;
// Verify items
for (ItemParam param : items) {
GameItem item = player.getInventory().getItemByParam(param);
System.out.println(param.getId());
if (item == null || item.isLocked() || item.getCount() < param.getCount()) {
player.sendPacket(new PacketExpUpEquipmentScRsp());
return;
}
if (item.getExcel().getEquipmentExp() > 0) {
expGain += item.getExcel().getEquipmentExp() * param.getCount();
cost += item.getExcel().getEquipmentExpCost() * param.getCount();
}
}
// Verify credits
if (player.getScoin() < cost) {
player.sendPacket(new PacketExpUpEquipmentScRsp());
return;
}
// Pay items
player.getInventory().removeItems(items);
player.addSCoin(-cost);
// Level up
int maxLevel = promoteData.getMaxLevel();
int level = equip.getLevel();
int exp = equip.getExp();
int reqExp = GameData.getEquipmentExpRequired(equip.getExcel().getEquipmentExcel().getExpType(), level);
while (expGain > 0 && reqExp > 0 && level < maxLevel) {
// Do calculations
int toGain = Math.min(expGain, reqExp - exp);
exp += toGain;
expGain -= toGain;
// Level up
if (exp >= reqExp) {
// Exp
exp = 0;
level += 1;
// Set req exp
reqExp = GameData.getEquipmentExpRequired(equip.getExcel().getEquipmentExcel().getExpType(), level);
}
}
// Done
equip.setLevel(level);
equip.setExp(exp);
equip.save();
player.save();
// TODO add back leftover exp
List<GameItem> returnItems = new ArrayList<>();
// Send packets
player.sendPacket(new PacketPlayerSyncScNotify(equip));
player.sendPacket(new PacketExpUpEquipmentScRsp(returnItems));
}
public void promoteEquipment(Player player, int equipId) {
// Get equipment
GameItem equip = player.getInventory().getItemByUid(equipId);
if (equip == null || !equip.getExcel().isEquipment() || equip.getPromotion() >= equip.getExcel().getEquipmentExcel().getMaxPromotion()) {
player.sendPacket(new BasePacket(CmdId.PromoteEquipmentScRsp));
return;
}
EquipmentPromotionExcel promotion = GameData.getEquipmentPromotionExcel(equip.getItemId(), equip.getPromotion());
// Sanity check
if ((promotion == null) || equip.getLevel() < promotion.getMaxLevel() || player.getLevel() < promotion.getPlayerLevelRequire() || player.getWorldLevel() < promotion.getWorldLevelRequire()) {
player.sendPacket(new BasePacket(CmdId.PromoteEquipmentScRsp));
return;
}
// Verify items
for (ItemParam param : promotion.getPromotionCostList()) {
GameItem item = player.getInventory().getItemByParam(param);
if (item == null || item.getCount() < param.getCount()) {
player.sendPacket(new BasePacket(CmdId.PromoteEquipmentScRsp));
return;
}
}
// Verify credits
if (player.getScoin() < promotion.getPromotionCostCoin()) {
player.sendPacket(new BasePacket(CmdId.PromoteEquipmentScRsp));
return;
}
// Pay items
player.getInventory().removeItems(promotion.getPromotionCostList());
player.addSCoin(-promotion.getPromotionCostCoin());
// Promote
equip.setPromotion(equip.getPromotion() + 1);
equip.save();
player.save();
// Send packets
player.sendPacket(new PacketPlayerSyncScNotify(equip));
player.sendPacket(new BasePacket(CmdId.PromoteEquipmentScRsp));
}
public void rankUpEquipment(Player player, int equipId, List<ItemParam> items) {
// Get avatar
GameItem equip = player.getInventory().getItemByUid(equipId);
if (equip == null || !equip.getExcel().isEquipment() || equip.getRank() >= equip.getExcel().getEquipmentExcel().getMaxRank()) {
player.sendPacket(new BasePacket(CmdId.RankUpEquipmentScRsp));
return;
}
// Verify items
for (ItemParam param : items) {
GameItem item = player.getInventory().getItemByParam(param);
if (item == null || !equip.getExcel().getEquipmentExcel().isRankUpItem(item) || item.getCount() < param.getCount()) {
player.sendPacket(new BasePacket(CmdId.RankUpEquipmentScRsp));
return;
}
}
// Pay items
player.getInventory().removeItems(items);
// Add rank
equip.setRank(Math.min(equip.getRank() + items.size(), equip.getExcel().getEquipmentExcel().getMaxRank()));
equip.save();
// Send packets
player.sendPacket(new PacketPlayerSyncScNotify(equip));
player.sendPacket(new BasePacket(CmdId.RankUpEquipmentScRsp));
}
// === Relic ===
public void levelUpRelic(Player player, int equipId, Collection<ItemParam> items) {
// Get relic
GameItem equip = player.getInventory().getItemByUid(equipId);
if (equip == null || !equip.getExcel().isRelic()) {
player.sendPacket(new PacketExpUpRelicScRsp());
return;
}
// Exp gain
int cost = 0;
int expGain = 0;
// Verify items
for (ItemParam param : items) {
GameItem item = player.getInventory().getItemByParam(param);
if (item == null || item.isLocked() || item.getCount() < param.getCount()) {
player.sendPacket(new PacketExpUpRelicScRsp());
return;
}
if (item.getExcel().getRelicExp() > 0) {
expGain += item.getExcel().getRelicExp() * param.getCount();
cost += item.getExcel().getRelicExpCost() * param.getCount();
}
if (item.getTotalExp() > 0) {
expGain += (int) Math.floor(item.getTotalExp() * 0.80D);
}
}
// Verify credits
if (player.getScoin() < cost) {
player.sendPacket(new PacketExpUpRelicScRsp());
return;
}
// Pay items
player.getInventory().removeItems(items);
player.addSCoin(-cost);
// Level up
int maxLevel = equip.getExcel().getRelicExcel().getMaxLevel();
int level = equip.getLevel();
int exp = equip.getExp();
int upgrades = 0;
int reqExp = GameData.getRelicExpRequired(equip.getExcel().getRelicExcel().getExpType(), level);
while (expGain > 0 && reqExp > 0 && level < maxLevel) {
// Do calculations
int toGain = Math.min(expGain, reqExp - exp);
exp += toGain;
expGain -= toGain;
// Level up
if (exp >= reqExp) {
// Exp
exp = 0;
level += 1;
// Check upgrades
if (level % 3 == 0) {
upgrades++;
}
// Set req exp
reqExp = GameData.getRelicExpRequired(equip.getExcel().getRelicExcel().getExpType(), level);
}
}
// Add affixes
if (upgrades > 0) {
equip.addSubAffixes(upgrades);
}
// Done
equip.setLevel(level);
equip.setExp(exp);
equip.save();
player.save();
// TODO add back leftover exp
List<GameItem> returnItems = new ArrayList<>();
// Send packets
player.sendPacket(new PacketPlayerSyncScNotify(equip));
player.sendPacket(new PacketExpUpRelicScRsp(returnItems));
}
// === Etc ===
public void lockEquip(Player player, int equipId, boolean locked) {
GameItem equip = player.getInventory().getItemByUid(equipId);
if (equip == null || !equip.getExcel().isEquippable()) {
return;
}
equip.setLocked(locked);
equip.save();
// Send packet
player.sendPacket(new PacketPlayerSyncScNotify(equip));
}
public void sellItems(Player player, List<ItemParam> items) {
// Verify items
var returnItems = new Int2IntOpenHashMap();
for (ItemParam param : items) {
GameItem item = player.getInventory().getItemByParam(param);
if (item == null || item.isLocked() || item.getCount() < param.getCount()) {
player.sendPacket(new PacketSellItemScRsp(null));
return;
}
// Add return items
for (ItemParam ret : item.getExcel().getReturnItemIDList()) {
// Add to return items
returnItems.put(ret.getId(), returnItems.getOrDefault(ret.getId(), 0) + ret.getCount());
}
}
// Delete items
player.getInventory().removeItems(items);
// Add return items
for (var returnItem : returnItems.int2IntEntrySet()) {
player.getInventory().addItem(returnItem.getIntKey(), returnItem.getIntValue());
}
// Send packet
player.sendPacket(new PacketSellItemScRsp(returnItems));
}
}

View File

@ -0,0 +1,13 @@
package emu.lunarcore.server.game;
public abstract class BaseGameService {
private final GameServer server;
public BaseGameService(GameServer server) {
this.server = server;
}
public GameServer getServer() {
return server;
}
}

View File

@ -0,0 +1,98 @@
package emu.lunarcore.server.game;
import java.net.InetSocketAddress;
import emu.lunarcore.Config.GameServerConfig;
import emu.lunarcore.LunarRail;
import emu.lunarcore.game.battle.BattleService;
import emu.lunarcore.game.gacha.GachaService;
import emu.lunarcore.game.player.Player;
import emu.lunarcore.game.service.ChatService;
import emu.lunarcore.game.service.InventoryService;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import kcp.highway.ChannelConfig;
import kcp.highway.KcpServer;
import lombok.Getter;
public class GameServer extends KcpServer {
private final GameServerConfig serverConfig;
private final RegionInfo info;
private final InetSocketAddress address;
private final GameServerPacketHandler packetHandler;
private final Int2ObjectMap<Player> players;
// Managers
@Getter private final BattleService battleService;
@Getter private final InventoryService inventoryService;
@Getter private final GachaService gachaService;
@Getter private final ChatService chatService;
public GameServer(GameServerConfig serverConfig) {
// Game Server base
this.serverConfig = serverConfig;
this.info = new RegionInfo(this);
this.address = new InetSocketAddress(serverConfig.bindAddress, serverConfig.getPort());
this.packetHandler = new GameServerPacketHandler();
this.players = Int2ObjectMaps.synchronize(new Int2ObjectOpenHashMap<>());
// Setup managers
this.battleService = new BattleService(this);
this.inventoryService = new InventoryService(this);
this.gachaService = new GachaService(this);
this.chatService = new ChatService(this);
// Hook into shutdown event.
Runtime.getRuntime().addShutdownHook(new Thread(this::onShutdown));
}
public GameServerConfig getServerConfig() {
return this.serverConfig;
}
public GameServerPacketHandler getPacketHandler() {
return this.packetHandler;
}
public void registerPlayer(Player player) {
players.put(player.getUid(), player);
}
public void deregisterPlayer(int uid) {
players.remove(uid);
}
public Player getPlayerByUid(int uid) {
return players.get(uid);
}
public void start() {
// Setup config and init server
ChannelConfig channelConfig = new ChannelConfig();
channelConfig.nodelay(true, 50, 2, true);
channelConfig.setMtu(1400);
channelConfig.setSndwnd(256);
channelConfig.setRcvwnd(256);
channelConfig.setTimeoutMillis(30 * 1000);//30s
channelConfig.setUseConvChannel(true);
channelConfig.setAckNoDelay(true);
this.init(new GameServerKcpListener(this), channelConfig, address);
// Setup region info
this.info.setUp(true);
this.info.save();
// Done
LunarRail.getLogger().info("Game Server started on " + address.getPort());
}
private void onShutdown() {
// Set region info
this.info.setUp(false);
this.info.save();
}
}

View File

@ -0,0 +1,52 @@
package emu.lunarcore.server.game;
import io.netty.buffer.ByteBuf;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectMaps;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import kcp.highway.KcpListener;
import kcp.highway.Ukcp;
public class GameServerKcpListener implements KcpListener {
private final GameServer server;
private final Object2ObjectMap<Ukcp, GameSession> sessions;
public GameServerKcpListener(GameServer server) {
this.server = server;
this.sessions = Object2ObjectMaps.synchronize(new Object2ObjectOpenHashMap<>());
}
public GameServer getServer() {
return this.server;
}
@Override
public void onConnected(Ukcp ukcp) {
GameSession session = new GameSession(server, ukcp);
sessions.put(ukcp, session);
session.onConnect();
}
@Override
public void handleClose(Ukcp ukcp) {
GameSession session = sessions.remove(ukcp);
if (session != null) {
session.onDisconnect();
}
}
@Override
public void handleReceive(ByteBuf packet, Ukcp ukcp) {
GameSession session = sessions.get(ukcp);
if (session != null) {
session.onMessage(packet);
}
}
@Override
public void handleException(Throwable err, Ukcp ukcp) {
// TODO
}
}

View File

@ -0,0 +1,97 @@
package emu.lunarcore.server.game;
import java.util.Set;
import org.reflections.Reflections;
import emu.lunarcore.LunarRail;
import emu.lunarcore.server.packet.CmdId;
import emu.lunarcore.server.packet.Opcodes;
import emu.lunarcore.server.packet.PacketHandler;
import emu.lunarcore.server.packet.SessionState;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
@SuppressWarnings("unchecked")
public class GameServerPacketHandler {
private final Int2ObjectMap<PacketHandler> handlers;
public GameServerPacketHandler() {
this.handlers = new Int2ObjectOpenHashMap<>();
this.registerHandlers();
}
public void registerPacketHandler(Class<? extends PacketHandler> handlerClass) {
try {
Opcodes opcode = handlerClass.getAnnotation(Opcodes.class);
if (opcode == null || opcode.disabled() || opcode.value() <= 0) {
return;
}
PacketHandler packetHandler = handlerClass.getDeclaredConstructor().newInstance();
this.handlers.put(opcode.value(), packetHandler);
} catch (Exception e) {
e.printStackTrace();
}
}
public void registerHandlers() {
Reflections reflections = new Reflections(LunarRail.class.getPackageName());
Set<?> handlerClasses = reflections.getSubTypesOf(PacketHandler.class);
for (Object obj : handlerClasses) {
this.registerPacketHandler((Class<? extends PacketHandler>) obj);
}
// Debug
LunarRail.getLogger().info("Game Server registered " + this.handlers.size() + " packet handlers");
}
public void handle(GameSession session, int opcode, byte[] header, byte[] data) {
PacketHandler handler = this.handlers.get(opcode);
if (handler != null) {
try {
// Make sure session is ready for packets
SessionState state = session.getState();
if (opcode == CmdId.PlayerHeartBeatCsReq) {
// Always continue if packet is ping request
} else if (opcode == CmdId.PlayerGetTokenCsReq) {
if (state != SessionState.WAITING_FOR_TOKEN) {
return;
}
} else if (opcode == CmdId.PlayerLoginCsReq) {
if (state != SessionState.WAITING_FOR_LOGIN) {
return;
}
}
/*
else if (opcode == PacketOpcodes.SetPlayerBornDataReq) {
if (state != SessionState.PICKING_CHARACTER) {
return;
}
}
*/
else {
if (state != SessionState.ACTIVE) {
return;
}
}
// Handle packet
handler.handle(session, header, data);
} catch (Exception ex) {
// TODO Remove this when no more needed
ex.printStackTrace();
}
return; // Packet successfully handled
}
// Log unhandled packets
//LunarRail.getLogger().info("Unhandled packet (" + opcode + "): " + CmdIdUtils.getOpcodeName(opcode));
}
}

View File

@ -0,0 +1,170 @@
package emu.lunarcore.server.game;
import java.net.InetSocketAddress;
import emu.lunarcore.LunarRail;
import emu.lunarcore.game.account.Account;
import emu.lunarcore.game.player.Player;
import emu.lunarcore.server.packet.BasePacket;
import emu.lunarcore.server.packet.CmdIdUtils;
import emu.lunarcore.server.packet.SessionState;
import emu.lunarcore.util.Utils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import kcp.highway.Ukcp;
import lombok.Getter;
public class GameSession {
@Getter private final GameServer server;
@Getter private InetSocketAddress address;
@Getter private Account account;
@Getter private Player player;
// Network
private Ukcp ukcp;
// Flags
@Getter private SessionState state = SessionState.WAITING_FOR_TOKEN;
private boolean useSecretKey;
private GameSession(GameServer server) {
this.server = server;
}
public GameSession(GameServer server, Ukcp ukcp) {
this(server);
this.ukcp = ukcp;
this.address = this.ukcp.user().getRemoteAddress();
}
public int getUid() {
return this.player.getUid();
}
public boolean useSecretKey() {
return useSecretKey;
}
public void setAccount(Account account) {
this.account = account;
}
public void setPlayer(Player player) {
this.player = player;
this.player.setSession(this);
this.getServer().registerPlayer(player);
}
public void setUseSecretKey(boolean key) {
this.useSecretKey = key;
}
public void setState(SessionState state) {
this.state = state;
}
public void onConnect() {
LunarRail.getLogger().info("Client connected from " + address.getHostString());
}
public void onDisconnect() {
LunarRail.getLogger().info("Client disconnected from " + address.getHostString());
this.state = SessionState.INACTIVE;
if (player != null) {
// Save first
player.save();
// Deregister
this.getServer().deregisterPlayer(player.getUid());
}
}
public void onMessage(ByteBuf packet) {
try {
// Decrypt and turn back into a packet
// Crypto.xor(packet.array(), useSecretKey() ? Crypto.ENCRYPT_KEY : Crypto.DISPATCH_KEY);
// Decode
while (packet.readableBytes() > 0) {
// Length
if (packet.readableBytes() < 16) {
return;
}
// Packet header sanity check
int constHeader = packet.readInt();
if (constHeader != BasePacket.HEADER_CONST) {
return; // Bad packet
}
// Data
int opcode = packet.readShort();
int headerLength = packet.readShort();
int dataLength = packet.readInt();
byte[] header = new byte[headerLength];
byte[] data = new byte[dataLength];
packet.readBytes(header);
packet.readBytes(data);
// Packet tail sanity check
int constTail = packet.readInt();
if (constTail != BasePacket.TAIL_CONST) {
return; // Bad packet
}
// Log packet
logPacket("RECV", opcode, data);
// Handle
getServer().getPacketHandler().handle(this, opcode, header, data);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// packet.release();
}
}
public void send(BasePacket packet) {
// Test
if (packet.getOpcode() <= 0) {
LunarRail.getLogger().warn("Tried to send packet with missing cmd id!");
return;
}
// Send
this.send(packet.build());
// Log
logPacket("SEND", packet.getOpcode(), packet.getData());
}
/**
* Sends a empty packet with the specified cmd id.
* @param cmdId
*/
public void send(int cmdId) {
// TODO optimize to send bytes with cmdId instead of creating a new base packet object
this.send(new BasePacket(cmdId));
}
private void send(byte[] bytes) {
if (this.ukcp != null) {
ByteBuf buf = Unpooled.wrappedBuffer(bytes);
this.ukcp.write(buf);
buf.release();
}
}
public void logPacket(String sendOrRecv, int opcode, byte[] payload) {
LunarRail.getLogger().info(sendOrRecv + ": " + CmdIdUtils.getOpcodeName(opcode) + " (" + opcode + ")");
System.out.println(Utils.bytesToHex(payload));
}
public void close() {
this.ukcp.close();
}
}

View File

@ -0,0 +1,38 @@
package emu.lunarcore.server.game;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import emu.lunarcore.LunarRail;
import lombok.Getter;
import lombok.Setter;
@Getter
@Entity(value = "regions", useDiscriminator = false)
public class RegionInfo {
@Id private String id;
private String name;
private String desc;
private String gateAddress;
private String gameAddress;
@Setter private boolean up;
@Deprecated
public RegionInfo() {
// Morphia only
}
public RegionInfo(GameServer server) {
this.id = server.getServerConfig().getId();
this.name = server.getServerConfig().getName();
this.desc = server.getServerConfig().getDescription();
this.gateAddress = LunarRail.getHttpServer().getServerConfig().getDisplayAddress();
this.gameAddress = server.getServerConfig().getDisplayAddress();
this.up = true;
}
public void save() {
LunarRail.getAccountDatabase().save(this);
}
}

View File

@ -0,0 +1,153 @@
package emu.lunarcore.server.http;
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.lunarcore.Config.ServerConfig;
import emu.lunarcore.LunarRail;
import emu.lunarcore.LunarRail.ServerType;
import emu.lunarcore.server.http.handlers.*;
import io.javalin.Javalin;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
public class HttpServer {
private final Javalin app;
private final ServerType type;
private boolean started;
public HttpServer(ServerType type) {
this.type = type;
this.app = Javalin.create();
this.addRoutes();
}
public Javalin getApp() {
return this.app;
}
public ServerType getType() {
return type;
}
public ServerConfig getServerConfig() {
return LunarRail.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(LunarRail.getConfig().getKeystore().getPath());
sslContextFactory.setKeyStorePassword(LunarRail.getConfig().getKeystore().getPassword());
sslContextFactory.setSniRequired(false);
sslContextFactory.setRenegotiationAllowed(false);
return sslContextFactory;
}
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().getPort());
getApp().jettyServer().server().addConnector(sslConnector);
getApp().start();
} else {
getApp().start(getServerConfig().getBindAddress(), getServerConfig().getPort());
}
// Done
LunarRail.getLogger().info("Http Server started on " + getServerConfig().getPort());
}
private void addRoutes() {
// Add routes based on what type of server this is
if (this.getType().runDispatch()) {
this.addDispatchRoutes();
this.addLogServerRoutes();
}
if (this.getType().runGame()) {
this.addGateServerRoutes();
}
// Fallback handler
getApp().error(404, this::notFoundHandler);
}
private void addDispatchRoutes() {
// Get region info
getApp().get("/query_dispatch", new QueryDispatchHandler());
// Captcha -> api-account-os.hoyoverse.com
getApp().post("/account/risky/api/check", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"id\":\"none\",\"action\":\"ACTION_NONE\",\"geetest\":null}}"));
// === AUTHENTICATION === hkrpg-sdk-os-static.hoyoverse.com
// Username & Password login (from client). Returns a session key to the client.
getApp().post("/hkrpg_global/mdk/shield/api/login", new UsernameLoginHandler());
// Cached session key verify (from registry). Returns a session key to the client.
getApp().post("/hkrpg_global/mdk/shield/api/verify", new TokenLoginHandler());
// Exchange session key for login token (combo token)
getApp().post("/hkrpg_global/combo/granter/login/v2/login", new ComboTokenGranterHandler());
// Config
getApp().get("/hkrpg_global/combo/granter/api/getConfig", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"protocol\":true,\"qr_enabled\":false,\"log_level\":\"INFO\",\"announce_url\":\"\",\"push_alias_type\":0,\"disable_ysdk_guard\":true,\"enable_announce_pic_popup\":false,\"app_name\":\"<EFBFBD>??RPG\",\"qr_enabled_apps\":{\"bbs\":false,\"cloud\":false},\"qr_app_icons\":{\"app\":\"\",\"bbs\":\"\",\"cloud\":\"\"},\"qr_cloud_display_name\":\"\",\"enable_user_center\":true,\"functional_switch_configs\":{}}}"));
getApp().get("/hkrpg_global/mdk/shield/api/loadConfig", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"id\":24,\"game_key\":\"hkrpg_global\",\"client\":\"PC\",\"identity\":\"I_IDENTITY\",\"guest\":false,\"ignore_versions\":\"\",\"scene\":\"S_NORMAL\",\"name\":\"<EFBFBD>??RPG\",\"disable_regist\":false,\"enable_email_captcha\":false,\"thirdparty\":[\"fb\",\"tw\",\"gl\",\"ap\"],\"disable_mmt\":false,\"server_guest\":false,\"thirdparty_ignore\":{},\"enable_ps_bind_account\":false,\"thirdparty_login_configs\":{\"tw\":{\"token_type\":\"TK_GAME_TOKEN\",\"game_token_expires_in\":2592000},\"ap\":{\"token_type\":\"TK_GAME_TOKEN\",\"game_token_expires_in\":604800},\"fb\":{\"token_type\":\"TK_GAME_TOKEN\",\"game_token_expires_in\":2592000},\"gl\":{\"token_type\":\"TK_GAME_TOKEN\",\"game_token_expires_in\":604800}},\"initialize_firebase\":false,\"bbs_auth_login\":false,\"bbs_auth_login_ignore\":[],\"fetch_instance_id\":false,\"enable_flash_login\":false}}"));
// === EXTRA ===
// hkrpg-sdk-os.hoyoverse.com
getApp().post("/hkrpg_global/combo/granter/api/compareProtocolVersion", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"modified\":false,\"protocol\":null}}"));
getApp().get("/hkrpg_global/mdk/agreement/api/getAgreementInfos", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"marketing_agreements\":[]}}"));
// sdk-os-static.hoyoverse.com
getApp().get("/combo/box/api/config/sdk/combo", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"vals\":{\"kibana_pc_config\":\"{ \\\"enable\\\": 0, \\\"level\\\": \\\"Info\\\",\\\"modules\\\": [\\\"download\\\"] }\\n\",\"network_report_config\":\"{ \\\"enable\\\": 0, \\\"status_codes\\\": [206], \\\"url_paths\\\": [\\\"dataUpload\\\", \\\"red_dot\\\"] }\\n\",\"list_price_tierv2_enable\":\"false\\n\",\"pay_payco_centered_host\":\"bill.payco.com\",\"telemetry_config\":\"{\\n \\\"dataupload_enable\\\": 0,\\n}\",\"enable_web_dpi\":\"true\"}}}"));
getApp().get("/combo/box/api/config/sw/precache", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"vals\":{\"url\":\"\",\"enable\":\"false\"}}}"));
// sg-public-data-api.hoyoverse.com
getApp().get("/device-fp/api/getFp", new FingerprintHandler());
getApp().get("/device-fp/api/getExtList", new HttpJsonResponse("{\"retcode\":0,\"message\":\"OK\",\"data\":{\"code\":200,\"msg\":\"ok\",\"ext_list\":[],\"pkg_list\":[],\"pkg_str\":\"/vK5WTh5SS3SAj8Zm0qPWg==\"}}"));
// abtest-api-data-sg.hoyoverse.com
getApp().post("/data_abtest_api/config/experiment/list", new HttpJsonResponse("{\"retcode\":0,\"success\":true,\"message\":\"\",\"data\":[{\"code\":1000,\"type\":2,\"config_id\":\"14\",\"period_id\":\"6125_197\",\"version\":\"1\",\"configs\":{\"cardType\":\"direct\"}}]}"));
}
private void addLogServerRoutes() {
// hkrpg-log-upload-os.hoyoverse.com
getApp().post("/sdk/dataUpload", new HttpJsonResponse("{\"code\":0}"));
// log-upload-os.hoyoverse.com
getApp().post("/crashdump/dataUpload", new HttpJsonResponse("{\"code\":0}"));
getApp().post("/apm/dataUpload", new HttpJsonResponse("{\"code\":0}"));
// minor-api-os.hoyoverse.com
getApp().post("/common/h5log/log/batch", new HttpJsonResponse("{\"retcode\":0,\"message\":\"success\",\"data\":null}"));
}
private void addGateServerRoutes() {
// Gateway info
getApp().get("/query_gateway", new QueryGatewayHandler());
}
private void notFoundHandler(Context ctx) {
ctx.status(404);
ctx.contentType(ContentType.TEXT_PLAIN);
ctx.result("not found");
}
}

View File

@ -0,0 +1,63 @@
package emu.lunarcore.server.http.handlers;
import org.jetbrains.annotations.NotNull;
import emu.lunarcore.LunarRail;
import emu.lunarcore.game.account.Account;
import emu.lunarcore.server.http.objects.ComboTokenReqJson;
import emu.lunarcore.server.http.objects.ComboTokenReqJson.LoginTokenData;
import emu.lunarcore.server.http.objects.ComboTokenResJson;
import emu.lunarcore.server.http.objects.ComboTokenResJson.LoginData;
import emu.lunarcore.util.JsonUtils;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
import io.javalin.http.Handler;
public class ComboTokenGranterHandler implements Handler {
public ComboTokenGranterHandler() {
// TODO Auto-generated constructor stub
}
@Override
public void handle(@NotNull Context ctx) throws Exception {
// Setup response
ComboTokenResJson res = new ComboTokenResJson();
// Parse request
ComboTokenReqJson req = JsonUtils.decode(ctx.body(), ComboTokenReqJson.class);
// Validate
if (req == null || req.data == null) {
res.retcode = -202;
res.message = "Error logging in";
return;
}
// Get login data
LoginTokenData data = JsonUtils.decode(req.data, LoginTokenData.class);
// Validate 2
if (data == null) {
res.retcode = -202;
res.message = "Invalid login data";
return;
}
// Login
Account account = LunarRail.getAccountDatabase().getObjectByField(Account.class, "_id", data.uid);
if (account == null || !account.getDispatchToken().equals(data.token)) {
res.retcode = -201;
res.message = "Game account cache information error";
} else {
res.message = "OK";
res.data = new LoginData(account.getUid(), account.generateComboToken());
}
// Result
ctx.contentType(ContentType.APPLICATION_JSON);
ctx.result(JsonUtils.encode(res));
}
}

View File

@ -0,0 +1,34 @@
package emu.lunarcore.server.http.handlers;
import org.jetbrains.annotations.NotNull;
import emu.lunarcore.server.http.objects.FingerprintReqJson;
import emu.lunarcore.server.http.objects.FingerprintResJson;
import emu.lunarcore.server.http.objects.FingerprintResJson.FingerprintDataJson;
import emu.lunarcore.util.JsonUtils;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
import io.javalin.http.Handler;
public class FingerprintHandler implements Handler {
@Override
public void handle(@NotNull Context ctx) throws Exception {
FingerprintResJson res = new FingerprintResJson();
FingerprintReqJson req = JsonUtils.decode(ctx.body(), FingerprintReqJson.class);
if (req == null) {
res.retcode = -202;
res.message = "Error";
}
res.message = "OK";
res.data = new FingerprintDataJson(req.device_fp);
// Result
ctx.contentType(ContentType.APPLICATION_JSON);
ctx.result(JsonUtils.encode(res));
}
}

View File

@ -0,0 +1,22 @@
package emu.lunarcore.server.http.handlers;
import org.jetbrains.annotations.NotNull;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
import io.javalin.http.Handler;
public class HttpJsonResponse implements Handler {
private final String json;
public HttpJsonResponse(String jsonString) {
this.json = jsonString;
}
@Override
public void handle(@NotNull Context ctx) throws Exception {
ctx.status(200);
ctx.contentType(ContentType.APPLICATION_JSON);
ctx.result(json);
}
}

View File

@ -0,0 +1,38 @@
package emu.lunarcore.server.http.handlers;
import org.jetbrains.annotations.NotNull;
import emu.lunarcore.LunarRail;
import emu.lunarcore.proto.DispatchRegionDataOuterClass.DispatchRegionData;
import emu.lunarcore.proto.RegionEntryOuterClass.RegionEntry;
import emu.lunarcore.util.Utils;
import io.javalin.http.Context;
import io.javalin.http.Handler;
public class QueryDispatchHandler implements Handler {
public QueryDispatchHandler() {
}
@Override
public void handle(@NotNull Context ctx) throws Exception {
// Get regions TODO get regions from database
RegionEntry region = RegionEntry.newInstance()
.setName(LunarRail.getConfig().getGameServer().getId())
.setDispatchUrl(LunarRail.getConfig().getHttpServer().getDisplayAddress() + "/query_gateway")
.setEnvType("2")
.setDisplayName(LunarRail.getConfig().getGameServer().getName());
// Build region list
DispatchRegionData regions = DispatchRegionData.newInstance();
regions.addRegionList(region);
// Log
LunarRail.getLogger().info("Client request: query_dispatch");
// Encode to base64 and send to client
ctx.result(Utils.base64Encode(regions.toByteArray()));
}
}

View File

@ -0,0 +1,53 @@
package emu.lunarcore.server.http.handlers;
import org.jetbrains.annotations.NotNull;
import emu.lunarcore.GameConstants;
import emu.lunarcore.LunarRail;
import emu.lunarcore.proto.GateserverOuterClass.Gateserver;
import emu.lunarcore.util.Utils;
import io.javalin.http.Context;
import io.javalin.http.Handler;
public class QueryGatewayHandler implements Handler {
public QueryGatewayHandler() {
}
@Override
public void handle(@NotNull Context ctx) throws Exception {
// Build gateserver proto
Gateserver gateserver = Gateserver.newInstance()
.setRegionName(LunarRail.getConfig().getGameServer().getId())
.setIp(LunarRail.getConfig().getGameServer().getPublicAddress())
.setPort(LunarRail.getConfig().getGameServer().getPort())
.setUnk1(true)
.setUnk2(true)
.setUnk3(true)
.setMdkResVersion(GameConstants.MDK_VERSION);
// Set streaming data urls
var data = LunarRail.getConfig().getDownloadData();
if (data.assetBundleUrl != null) {
gateserver.setAssetBundleUrl(data.assetBundleUrl);
}
if (data.exResourceUrl != null) {
gateserver.setAssetBundleUrl(data.exResourceUrl);
}
if (data.luaUrl != null) {
gateserver.setAssetBundleUrl(data.luaUrl);
}
if (data.ifixUrl != null) {
gateserver.setAssetBundleUrl(data.ifixUrl);
}
// Log
LunarRail.getLogger().info("Client request: query_gateway");
// Encode to base64 and send to client
ctx.result(Utils.base64Encode(gateserver.toByteArray()));
}
}

View File

@ -0,0 +1,52 @@
package emu.lunarcore.server.http.handlers;
import org.jetbrains.annotations.NotNull;
import emu.lunarcore.LunarRail;
import emu.lunarcore.game.account.Account;
import emu.lunarcore.server.http.objects.LoginResJson;
import emu.lunarcore.server.http.objects.LoginResJson.VerifyData;
import emu.lunarcore.server.http.objects.LoginTokenReqJson;
import emu.lunarcore.util.JsonUtils;
import io.javalin.http.ContentType;
import io.javalin.http.Context;
import io.javalin.http.Handler;
public class TokenLoginHandler implements Handler {
public TokenLoginHandler() {
}
@Override
public void handle(@NotNull Context ctx) throws Exception {
// Setup response
LoginResJson res = new LoginResJson();
// Parse request
LoginTokenReqJson req = JsonUtils.decode(ctx.body(), LoginTokenReqJson.class);
// Validate
if (req == null) {
res.retcode = -202;
res.message = "Error logging in";
return;
}
// Login
Account account = LunarRail.getAccountDatabase().getObjectByField(Account.class, "_id", req.uid);
if (account == null || !account.getDispatchToken().equals(req.token)) {
res.retcode = -201;
res.message = "Game account cache information error";
} else {
res.message = "OK";
res.data = new VerifyData(account.getUid(), account.getEmail(), req.token);
}
// Result
ctx.contentType(ContentType.APPLICATION_JSON);
ctx.result(JsonUtils.encode(res));
}
}

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