commit 0bdc4d0cd81fafc3e7a2486f67c35625f89a1fae Author: MiniDay <372403923@qq.com> Date: Mon Oct 24 02:43:46 2022 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e0d53d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.gradle +.idea +build diff --git a/README.md b/README.md new file mode 100644 index 0000000..8dbef39 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# HamsterBall + +仓鼠球:一个基于 Netty 的 Minecraft 服务端通用消息中间件(原HamsterService) diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..ff98f87 --- /dev/null +++ b/build.gradle @@ -0,0 +1,70 @@ +plugins { + id 'java' +} + +group 'cn.hamster3.mc.plugin' + +subprojects { + apply plugin: 'java-library' + apply plugin: 'maven-publish' + + group = rootProject.group + version = rootProject.version + + repositories { + maven { + url = "https://maven.airgame.net/maven-public/" + } + } + + configurations { + shade + api.extendsFrom apiShade + implementation.extendsFrom implementationShade + } + + dependencies { + // https://mvnrepository.com/artifact/org.projectlombok/lombok + compileOnly 'org.projectlombok:lombok:1.18.24' + annotationProcessor 'org.projectlombok:lombok:1.18.24' + // https://mvnrepository.com/artifact/org.jetbrains/annotations + compileOnly 'org.jetbrains:annotations:23.0.0' + } + + tasks.withType(JavaCompile) { + options.setEncoding("UTF-8") + } + + tasks.withType(Jar) { + from([rootProject.file("LICENSE")]) + duplicatesStrategy(DuplicatesStrategy.EXCLUDE) + } + + java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +// withJavadocJar() + withSourcesJar() + } + + publishing { + publications { + mavenJava(MavenPublication) { + from getProject().getComponents().java + } + } + repositories { + maven { + def releasesRepoUrl = 'https://maven.airgame.net/maven-releases/' + def snapshotsRepoUrl = 'https://maven.airgame.net/maven-snapshots/' + + url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl + + credentials { + username rootProject.getProperties().getOrDefault("maven_username", "") + password rootProject.getProperties().getOrDefault("maven_password", "") + } + } + } + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..41d9927 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..41dfb87 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -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" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -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 diff --git a/hamster-ball-bukkit/build.gradle b/hamster-ball-bukkit/build.gradle new file mode 100644 index 0000000..2f726ba --- /dev/null +++ b/hamster-ball-bukkit/build.gradle @@ -0,0 +1,41 @@ +version = '1.0.0' +setArchivesBaseName("HamsterBall-Bukkit") + +evaluationDependsOn(':hamster-ball-common') + +dependencies { + apiShade(project(":hamster-ball-common")) { + exclude group: "*" + } + + compileOnly 'org.spigotmc:spigot-api:1.12.2-R0.1-SNAPSHOT' +} + +processResources { + inputs.property "version", project.version + filesMatching("plugin.yml") { + expand "version": project.version + } + duplicatesStrategy(DuplicatesStrategy.EXCLUDE) +} + +tasks.compileJava.dependsOn(":hamster-ball-common:build") +tasks.create("shadowJar", Jar) { + dependsOn("jar") + from([ + tasks.jar.outputs.files.collect { + it.isDirectory() ? it : zipTree(it) + }, + configurations.shade.collect { + it.isDirectory() ? it : zipTree(it) + }, + configurations.apiShade.collect { + it.isDirectory() ? it : zipTree(it) + }, + configurations.implementationShade.collect { + it.isDirectory() ? it : zipTree(it) + } + ]) + destinationDir(rootProject.buildDir) +} +tasks.build.dependsOn(shadowJar) diff --git a/hamster-ball-bukkit/src/main/java/cn/hamster3/mc/plugin/ball/bukkit/HamsterBallPlugin.java b/hamster-ball-bukkit/src/main/java/cn/hamster3/mc/plugin/ball/bukkit/HamsterBallPlugin.java new file mode 100644 index 0000000..2e97ca1 --- /dev/null +++ b/hamster-ball-bukkit/src/main/java/cn/hamster3/mc/plugin/ball/bukkit/HamsterBallPlugin.java @@ -0,0 +1,42 @@ +package cn.hamster3.mc.plugin.ball.bukkit; + +import cn.hamster3.mc.plugin.ball.bukkit.api.BallBukkitAPI; +import org.bukkit.Bukkit; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.logging.Logger; + +public class HamsterBallPlugin extends JavaPlugin { + private static HamsterBallPlugin instance; + + public static HamsterBallPlugin getInstance() { + return instance; + } + + public static void sync(Runnable runnable) { + Bukkit.getScheduler().runTask(instance, runnable); + } + + @Override + public void onLoad() { + instance = this; + Logger logger = getLogger(); + BallBukkitAPI.init(); + logger.info("BallBukkitAPI 已初始化."); + try { + BallBukkitAPI.getInstance().enable(); + } catch (Exception e) { + e.printStackTrace(); + sync(Bukkit::shutdown); + } + } + + @Override + public void onDisable() { + try { + BallBukkitAPI.getInstance().disable(); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/hamster-ball-bukkit/src/main/java/cn/hamster3/mc/plugin/ball/bukkit/api/BallBukkitAPI.java b/hamster-ball-bukkit/src/main/java/cn/hamster3/mc/plugin/ball/bukkit/api/BallBukkitAPI.java new file mode 100644 index 0000000..1cfadca --- /dev/null +++ b/hamster-ball-bukkit/src/main/java/cn/hamster3/mc/plugin/ball/bukkit/api/BallBukkitAPI.java @@ -0,0 +1,59 @@ +package cn.hamster3.mc.plugin.ball.bukkit.api; + +import cn.hamster3.mc.plugin.ball.bukkit.HamsterBallPlugin; +import cn.hamster3.mc.plugin.ball.bukkit.listener.BallBukkitListener; +import cn.hamster3.mc.plugin.ball.common.api.BallAPI; +import cn.hamster3.mc.plugin.ball.common.config.BallConfig; +import cn.hamster3.mc.plugin.ball.common.entity.ServerInfo; +import cn.hamster3.mc.plugin.ball.common.entity.ServerType; +import org.bukkit.Bukkit; +import org.bukkit.configuration.file.FileConfiguration; +import org.jetbrains.annotations.NotNull; + +import java.sql.SQLException; + +public class BallBukkitAPI extends BallAPI { + public BallBukkitAPI(@NotNull BallConfig config) { + super(config); + } + + public static BallBukkitAPI getInstance() { + return (BallBukkitAPI) instance; + } + + public static void init() { + if (instance != null) { + return; + } + HamsterBallPlugin plugin = HamsterBallPlugin.getInstance(); + plugin.saveDefaultConfig(); + FileConfiguration pluginConfig = plugin.getConfig(); + + String host = pluginConfig.getString("server-info.name.host", Bukkit.getIp()); + BallConfig config = new BallConfig( + new ServerInfo( + pluginConfig.getString("server-info.id"), + pluginConfig.getString("server-info.name"), + ServerType.GAME, + host.isEmpty() ? "127.0.0.1" : host, + pluginConfig.getInt("server-info.name.port", Bukkit.getPort()) + ), + pluginConfig.getString("ball-server.host"), + pluginConfig.getInt("ball-server.port"), + pluginConfig.getInt("ball-server.nio-thread") + ); + instance = new BallBukkitAPI(config); + + instance.addListener(BallBukkitListener.INSTANCE); + } + + @Override + public void enable() throws SQLException, InterruptedException { + super.enable(); + } + + @Override + public void disable() throws SQLException, InterruptedException { + super.disable(); + } +} diff --git a/hamster-ball-bukkit/src/main/java/cn/hamster3/mc/plugin/ball/bukkit/data/BukkitBlockPos.java b/hamster-ball-bukkit/src/main/java/cn/hamster3/mc/plugin/ball/bukkit/data/BukkitBlockPos.java new file mode 100644 index 0000000..74c0405 --- /dev/null +++ b/hamster-ball-bukkit/src/main/java/cn/hamster3/mc/plugin/ball/bukkit/data/BukkitBlockPos.java @@ -0,0 +1,54 @@ +package cn.hamster3.mc.plugin.ball.bukkit.data; + +import cn.hamster3.mc.plugin.ball.common.api.BallAPI; +import cn.hamster3.mc.plugin.ball.common.data.ServiceBlockPos; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.entity.Entity; +import org.jetbrains.annotations.NotNull; + +@SuppressWarnings("unused") +public class BukkitBlockPos extends ServiceBlockPos { + public BukkitBlockPos(@NotNull String serverID, @NotNull String worldName, int x, int y, int z) { + super(serverID, worldName, x, y, z); + } + + public BukkitBlockPos(@NotNull Entity player) { + this(player.getLocation()); + } + + public BukkitBlockPos(@NotNull Block block) { + this(block.getLocation()); + } + + public BukkitBlockPos(@NotNull Location location) { + super( + BallAPI.getInstance().getLocalServerId(), + location.getWorld().getName(), + location.getBlockX(), + location.getBlockY(), + location.getBlockZ() + ); + } + + public BukkitBlockPos(@NotNull ServiceBlockPos location) { + super( + BallAPI.getInstance().getLocalServerId(), + location.getWorldName(), + location.getX(), + location.getY(), + location.getZ() + ); + } + + @NotNull + public Location toBukkitLocation() { + return new Location(Bukkit.getWorld(getWorldName()), getX(), getY(), getZ(), 0, 0); + } + + @NotNull + public BukkitLocation toServiceLocation() { + return new BukkitLocation(getServerID(), getWorldName(), getX(), getY(), getZ()); + } +} diff --git a/hamster-ball-bukkit/src/main/java/cn/hamster3/mc/plugin/ball/bukkit/data/BukkitLocation.java b/hamster-ball-bukkit/src/main/java/cn/hamster3/mc/plugin/ball/bukkit/data/BukkitLocation.java new file mode 100644 index 0000000..c49b5cf --- /dev/null +++ b/hamster-ball-bukkit/src/main/java/cn/hamster3/mc/plugin/ball/bukkit/data/BukkitLocation.java @@ -0,0 +1,62 @@ +package cn.hamster3.mc.plugin.ball.bukkit.data; + +import cn.hamster3.mc.plugin.ball.common.api.BallAPI; +import cn.hamster3.mc.plugin.ball.common.data.ServiceLocation; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.block.Block; +import org.bukkit.entity.Entity; +import org.jetbrains.annotations.NotNull; + +@SuppressWarnings("unused") +public class BukkitLocation extends ServiceLocation { + public BukkitLocation(@NotNull String serverID, @NotNull String worldName, double x, double y, double z) { + super(serverID, worldName, x, y, z, 0, 0); + } + + public BukkitLocation(@NotNull String serverID, @NotNull String worldName, double x, double y, double z, float yaw, float pitch) { + super(serverID, worldName, x, y, z, yaw, pitch); + } + + public BukkitLocation(@NotNull Entity player) { + this(player.getLocation()); + } + + public BukkitLocation(@NotNull Block block) { + this(block.getLocation()); + } + + public BukkitLocation(@NotNull Location location) { + super( + BallAPI.getInstance().getLocalServerId(), + location.getWorld().getName(), + location.getX(), + location.getY(), + location.getZ(), + location.getYaw(), + location.getPitch() + ); + } + + public BukkitLocation(@NotNull ServiceLocation location) { + super( + location.getServerID(), + location.getWorldName(), + location.getX(), + location.getY(), + location.getZ(), + location.getYaw(), + location.getPitch() + ); + } + + @NotNull + public Location toBukkitLocation() { + return new Location(Bukkit.getWorld(getWorldName()), getX(), getY(), getZ(), getYaw(), getPitch()); + } + + @NotNull + public BukkitBlockPos toBukkitBlockPos() { + return new BukkitBlockPos(getServerID(), getWorldName(), getBlockX(), getBlockY(), getBlockZ()); + } +} diff --git a/hamster-ball-bukkit/src/main/java/cn/hamster3/mc/plugin/ball/bukkit/listener/BallBukkitListener.java b/hamster-ball-bukkit/src/main/java/cn/hamster3/mc/plugin/ball/bukkit/listener/BallBukkitListener.java new file mode 100644 index 0000000..eb29b26 --- /dev/null +++ b/hamster-ball-bukkit/src/main/java/cn/hamster3/mc/plugin/ball/bukkit/listener/BallBukkitListener.java @@ -0,0 +1,87 @@ +package cn.hamster3.mc.plugin.ball.bukkit.listener; + +import cn.hamster3.mc.plugin.ball.bukkit.data.BukkitLocation; +import cn.hamster3.mc.plugin.ball.common.api.BallAPI; +import cn.hamster3.mc.plugin.ball.common.data.ServiceLocation; +import cn.hamster3.mc.plugin.ball.common.entity.ServerType; +import cn.hamster3.mc.plugin.ball.common.event.operate.DispatchConsoleCommandEvent; +import cn.hamster3.mc.plugin.ball.common.event.operate.DispatchPlayerCommandEvent; +import cn.hamster3.mc.plugin.ball.common.event.operate.SendPlayerToLocationEvent; +import cn.hamster3.mc.plugin.ball.common.event.operate.SendPlayerToPlayerEvent; +import cn.hamster3.mc.plugin.ball.common.listener.BallListener; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerLoginEvent; +import org.bukkit.event.player.PlayerTeleportEvent; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.UUID; + +public class BallBukkitListener extends BallListener implements Listener { + public static final BallBukkitListener INSTANCE = new BallBukkitListener(); + + private final HashMap playerToLocation = new HashMap<>(); + + @Override + public void onDispatchConsoleCommand(@NotNull DispatchConsoleCommandEvent event) { + if (event.getType() != null && event.getType() != ServerType.GAME) { + return; + } + if (event.getServerID() != null && !BallAPI.getInstance().isLocalServer(event.getServerID())) { + return; + } + Bukkit.dispatchCommand(Bukkit.getConsoleSender(), event.getCommand()); + } + + @Override + public void onDispatchGamePlayerCommand(@NotNull DispatchPlayerCommandEvent event) { + if (event.getType() != null && event.getType() != ServerType.GAME) { + return; + } + if (event.getUuid() != null && Bukkit.getPlayer(event.getUuid()) == null) { + return; + } + Bukkit.dispatchCommand(Bukkit.getPlayer(event.getUuid()), event.getCommand()); + } + + @Override + public void onSendPlayerToLocation(@NotNull SendPlayerToLocationEvent event) { + ServiceLocation location = event.getLocation(); + if (!BallAPI.getInstance().isLocalServer(location.getServerID())) { + return; + } + for (UUID uuid : event.getSendPlayerUUID()) { + playerToLocation.put(uuid, new BukkitLocation(location).toBukkitLocation()); + } + } + + @Override + public void onSendPlayerToPlayer(@NotNull SendPlayerToPlayerEvent event) { + Player player = Bukkit.getPlayer(event.getToPlayerUUID()); + if (player == null) { + return; + } + Location location = player.getLocation(); + for (UUID uuid : event.getSendPlayerUUID()) { + playerToLocation.put(uuid, location); + } + } + + @Override + public void onReconnectFailed() { + Bukkit.shutdown(); + } + + @EventHandler(ignoreCancelled = true) + public void onPlayerLogin(PlayerLoginEvent event) { + Player player = event.getPlayer(); + Location location = playerToLocation.remove(player.getUniqueId()); + if (location != null) { + player.teleport(location, PlayerTeleportEvent.TeleportCause.PLUGIN); + } + } +} diff --git a/hamster-ball-bukkit/src/main/resources/config.yml b/hamster-ball-bukkit/src/main/resources/config.yml new file mode 100644 index 0000000..69ae34a --- /dev/null +++ b/hamster-ball-bukkit/src/main/resources/config.yml @@ -0,0 +1,24 @@ +ball-server: + host: "ball.hamster3.cn" + port: 58888 + nio-thread: 10 + +server-info: + # 服务器唯一识别码,最长 32 字符 + # 推荐格式:全小写英文+横杠+数字尾号 + # 例如: + # survival-1, survival-2(生存1区,生存2区) + # plot-1, plot-2(地皮1区,地皮2区) + # resource-1, resource-2(资源1区,资源2区) + id: "test-1" + # 服务端名称,用于展示给玩家看 + name: "测试1区" + # 当前子服的内网地址 + # 不填则自动获取 server.properties 文件中的设置 + # 若都为空,则自动设定为 127.0.0.1 + # 连接上 HamsterBall 之后,BC 端的插件会自动将这个服务器的地址和端口加入到列表 + # 因此只需在这里填上无需每次都手动更改 BC 的配置文件 + # host: "127.0.0.1" + # 当前子服的内网端口 + # 不填则自动获取 server.properties 文件中的设置 + # port: 25577 diff --git a/hamster-ball-bukkit/src/main/resources/plugin.yml b/hamster-ball-bukkit/src/main/resources/plugin.yml new file mode 100644 index 0000000..56cd22d --- /dev/null +++ b/hamster-ball-bukkit/src/main/resources/plugin.yml @@ -0,0 +1,10 @@ +name: HamsterBall +main: cn.hamster3.mc.plugin.ball.bukkit.HamsterBallPlugin +version: ${version} + +author: MiniDay +website: https://github.com/MiniDay/hamster-ball +description: 仓鼠球:一个基于 Netty 的 Minecraft 服务端通用消息中间件(原HamsterService) + +depend: + - HamsterCore diff --git a/hamster-ball-bungeecord/build.gradle b/hamster-ball-bungeecord/build.gradle new file mode 100644 index 0000000..8b51e6a --- /dev/null +++ b/hamster-ball-bungeecord/build.gradle @@ -0,0 +1,42 @@ +version = '1.0.0' +setArchivesBaseName("HamsterBall-BungeeCord") + +evaluationDependsOn(':hamster-ball-common') + +dependencies { + apiShade(project(":hamster-ball-common")) { + exclude group: "*" + } + + compileOnly 'net.md-5:bungeecord-api:1.17-R0.1-SNAPSHOT' + compileOnly "cn.hamster3.mc.plugin:hamster-core-bungeecord:1.0.0" +} + +processResources { + inputs.property "version", project.version + filesMatching("plugin.yml") { + expand "version": project.version + } + duplicatesStrategy(DuplicatesStrategy.EXCLUDE) +} + +tasks.compileJava.dependsOn(":hamster-ball-common:build") +tasks.create("shadowJar", Jar) { + dependsOn("jar") + from([ + tasks.jar.outputs.files.collect { + it.isDirectory() ? it : zipTree(it) + }, + configurations.shade.collect { + it.isDirectory() ? it : zipTree(it) + }, + configurations.apiShade.collect { + it.isDirectory() ? it : zipTree(it) + }, + configurations.implementationShade.collect { + it.isDirectory() ? it : zipTree(it) + } + ]) + destinationDir(rootProject.buildDir) +} +tasks.build.dependsOn(shadowJar) diff --git a/hamster-ball-bungeecord/src/main/java/cn/hamster3/mc/plugin/core/bungee/HamsterBallPlugin.java b/hamster-ball-bungeecord/src/main/java/cn/hamster3/mc/plugin/core/bungee/HamsterBallPlugin.java new file mode 100644 index 0000000..d8fe2a5 --- /dev/null +++ b/hamster-ball-bungeecord/src/main/java/cn/hamster3/mc/plugin/core/bungee/HamsterBallPlugin.java @@ -0,0 +1,18 @@ +package cn.hamster3.mc.plugin.core.bungee; + +import net.md_5.bungee.api.plugin.Plugin; + +public class HamsterBallPlugin extends Plugin { + @Override + public void onLoad() { + } + + @Override + public void onEnable() { + } + + @Override + public void onDisable() { + super.onDisable(); + } +} diff --git a/hamster-ball-bungeecord/src/main/resources/bungee.yml b/hamster-ball-bungeecord/src/main/resources/bungee.yml new file mode 100644 index 0000000..14f50cf --- /dev/null +++ b/hamster-ball-bungeecord/src/main/resources/bungee.yml @@ -0,0 +1,9 @@ +name: HamsterBall +main: cn.hamster3.mc.plugin.core.bungee.HamsterBallPlugin +version: ${version} + +author: MiniDay +description: 仓鼠球:一个基于 Netty 的 Minecraft 服务端通用消息中间件(原HamsterService) + +depend: + - HamsterCore diff --git a/hamster-ball-bungeecord/src/main/resources/config.yml b/hamster-ball-bungeecord/src/main/resources/config.yml new file mode 100644 index 0000000..715974e --- /dev/null +++ b/hamster-ball-bungeecord/src/main/resources/config.yml @@ -0,0 +1,10 @@ +ball-server: + host: "ball.hamster3.cn" + port: 58888 + nio-thread: 10 + +server-info: + # 服务器唯一识别码,最长 32 字符 + id: "BungeeCord" + # 服务端名称,用于展示给玩家看 + name: "代理端" diff --git a/hamster-ball-common/build.gradle b/hamster-ball-common/build.gradle new file mode 100644 index 0000000..adb18c6 --- /dev/null +++ b/hamster-ball-common/build.gradle @@ -0,0 +1,19 @@ +version = '1.0.0' +setArchivesBaseName("HamsterBall-Common") + +dependencies { + // https://mvnrepository.com/artifact/com.google.code.gson/gson + //noinspection GradlePackageUpdate + compileOnly 'com.google.code.gson:gson:2.8.0' + // https://mvnrepository.com/artifact/io.netty/netty-all + compileOnly 'io.netty:netty-all:4.1.84.Final' + + compileOnly "cn.hamster3.mc.plugin:hamster-core-common:1.0.0" + + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' +} + +test { + useJUnitPlatform() +} diff --git a/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/api/BallAPI.java b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/api/BallAPI.java new file mode 100644 index 0000000..8063918 --- /dev/null +++ b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/api/BallAPI.java @@ -0,0 +1,792 @@ +package cn.hamster3.mc.plugin.ball.common.api; + +import cn.hamster3.mc.plugin.ball.common.config.BallConfig; +import cn.hamster3.mc.plugin.ball.common.connector.BallChannelInitializer; +import cn.hamster3.mc.plugin.ball.common.constant.BallCommonConstants; +import cn.hamster3.mc.plugin.ball.common.data.ServiceLocation; +import cn.hamster3.mc.plugin.ball.common.data.ServiceMessageInfo; +import cn.hamster3.mc.plugin.ball.common.entity.PlayerInfo; +import cn.hamster3.mc.plugin.ball.common.entity.ServerInfo; +import cn.hamster3.mc.plugin.ball.common.entity.ServerType; +import cn.hamster3.mc.plugin.ball.common.event.operate.*; +import cn.hamster3.mc.plugin.ball.common.event.player.*; +import cn.hamster3.mc.plugin.ball.common.event.server.ServerOfflineEvent; +import cn.hamster3.mc.plugin.ball.common.event.server.ServerOnlineEvent; +import cn.hamster3.mc.plugin.ball.common.listener.BallListener; +import cn.hamster3.mc.plugin.core.common.api.CoreAPI; +import cn.hamster3.mc.plugin.core.common.constant.CoreConstantObjects; +import cn.hamster3.mc.plugin.core.common.data.Message; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelOption; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.sql.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public abstract class BallAPI { + /** + * API 使用的通信频道 + */ + public static final String BALL_CHANNEL = "HamsterBall"; + /** + * API 实例 + */ + protected static BallAPI instance; + @NotNull + protected final ConcurrentHashMap serverInfo; + @NotNull + protected final ConcurrentHashMap playerInfo; + + @NotNull + private final BallConfig config; + @NotNull + private final List listeners; + + private final Bootstrap bootstrap; + private final NioEventLoopGroup executors; + + protected boolean enable; + protected Channel channel; + + protected BallAPI(@NotNull BallConfig config) { + this.config = config; + executors = new NioEventLoopGroup(config.getNioThread()); + + serverInfo = new ConcurrentHashMap<>(); + playerInfo = new ConcurrentHashMap<>(); + listeners = new ArrayList<>(); + + bootstrap = new Bootstrap(); + bootstrap.group(executors) + .channel(NioSocketChannel.class) + .option(ChannelOption.TCP_NODELAY, true) + .option(ChannelOption.SO_KEEPALIVE, true) + .handler(BallChannelInitializer.INSTANCE); + + addListener(new BallListener() { + @Override + public int getPriority() { + return 0; + } + + @Override + public void onPlayerConnectServer(@NotNull PlayerConnectServerEvent event) { + PlayerInfo info = event.getPlayerInfo(); + playerInfo.put(info.getUuid(), info); + } + + @Override + public void onPlayerDisconnect(@NotNull PlayerDisconnectEvent event) { + PlayerInfo info = playerInfo.get(event.getPlayerUUID()); + info.setOnline(false); + } + + @Override + public void onPlayerLogin(@NotNull PlayerLoginEvent event) { + PlayerInfo info = event.getPlayerInfo(); + playerInfo.put(info.getUuid(), info); + } + + @Override + public void onPlayerPostConnectServer(@NotNull PlayerPostConnectServerEvent event) { + PlayerInfo info = event.getPlayerInfo(); + playerInfo.put(info.getUuid(), info); + } + + @Override + public void onPlayerPostLogin(@NotNull PlayerPostLoginEvent event) { + PlayerInfo info = event.getPlayerInfo(); + playerInfo.put(info.getUuid(), info); + } + + @Override + public void onPlayerPreConnectServer(@NotNull PlayerPreConnectServerEvent event) { + PlayerInfo info = event.getPlayerInfo(); + playerInfo.put(info.getUuid(), info); + } + + @Override + public void onServerOffline(@NotNull ServerOfflineEvent event) { + String serverID = event.getServerID(); + serverInfo.remove(serverID); + } + + @Override + public void onServerOnline(@NotNull ServerOnlineEvent event) { + ServerInfo info = event.getServerInfo(); + serverInfo.put(info.getId(), info); + } + + @Override + public void onConnectInactive() { + reconnect(5); + } + }); + } + + /** + * 获取 API 实例 + * + * @return API 实例 + */ + public static BallAPI getInstance() { + return instance; + } + + protected void enable() throws SQLException, InterruptedException { + if (enable) { + return; + } + enable = true; + ServerInfo localInfo = getLocalServerInfo(); + + connect(); + + try (Connection connection = CoreAPI.getInstance().getConnection()) { + + { + Statement statement = connection.createStatement(); + statement.execute("CREATE TABLE IF NOT EXISTS " + BallCommonConstants.SQL.PLAYER_INFO_TABLE + "(" + + "`uuid` CHAR(36) PRIMARY KEY," + + "`name` VARCHAR(16) NOT NULL," + + "`profile` TEXT NOT NULL," + + "`game_server` VARCHAR(32) NOT NULL," + + "`proxy_server` VARCHAR(32) NOT NULL," + + "`online` BOOLEAN NOT NULL" + + ") CHARSET utf8mb4;"); + statement.execute("CREATE TABLE IF NOT EXISTS " + BallCommonConstants.SQL.SERVER_INFO_TABLE + "(" + + "`id` VARCHAR(32) PRIMARY KEY NOT NULL," + + "`name` VARCHAR(32) NOT NULL," + + "`type` VARCHAR(16) NOT NULL," + + "`host` VARCHAR(32) NOT NULL," + + "`port` INT NOT NULL" + + ") CHARSET utf8mb4;"); + statement.execute("CREATE TABLE IF NOT EXISTS " + BallCommonConstants.SQL.CACHED_MESSAGE_TABLE + "(" + + "`uuid` CHAR(36) NOT NULL," + + "`message` MEDIUMTEXT NOT NULL" + + ") CHARSET utf8mb4;"); + statement.close(); + } + + { + PreparedStatement statement = connection.prepareStatement("REPLACE INTO " + BallCommonConstants.SQL.SERVER_INFO_TABLE + " VALUES(?, ?, ?, ?, ?);"); + statement.setString(1, localInfo.getId()); + statement.setString(2, localInfo.getName()); + statement.setString(3, localInfo.getType().name()); + statement.setString(4, localInfo.getHost()); + statement.setInt(5, localInfo.getPort()); + statement.executeUpdate(); + statement.close(); + } + + { + PreparedStatement statement = connection.prepareStatement("SELECT * FROM " + BallCommonConstants.SQL.SERVER_INFO_TABLE + ";"); + ResultSet set = statement.executeQuery(); + while (set.next()) { + String serverID = set.getString("id"); + serverInfo.put(serverID, new ServerInfo( + serverID, + set.getString("name"), + ServerType.valueOf(set.getString("type")), + set.getString("host"), + set.getInt("port") + )); + } + set.close(); + statement.close(); + } + + { + PreparedStatement statement = connection.prepareStatement("SELECT * FROM " + BallCommonConstants.SQL.PLAYER_INFO_TABLE + ";"); + ResultSet set = statement.executeQuery(); + while (set.next()) { + UUID uuid = UUID.fromString(set.getString("uuid")); + playerInfo.put(uuid, new PlayerInfo(uuid, + set.getString("name"), + CoreConstantObjects.JSON_PARSER.parse(set.getString("profile")).getAsJsonObject(), + set.getString("game_server"), + set.getString("proxy_server"), + set.getBoolean("online") + )); + } + set.close(); + statement.close(); + } + + } + + sendMessagingMessage( + BALL_CHANNEL, + ServerOnlineEvent.ACTION, + new ServerOnlineEvent(localInfo) + ); + } + + protected void connect() throws InterruptedException { + if (!enable) { + return; + } + ChannelFuture future = bootstrap.connect(config.getHost(), config.getPort()).await(); + if (future.isSuccess()) { + channel = future.channel(); + } + } + + protected void reconnect(int ttl) { + if (!enable) { + return; + } + if (channel != null && channel.isOpen() && channel.isRegistered() && channel.isActive() && channel.isWritable()) { + return; + } + channel = null; + if (ttl <= 0) { + for (BallListener listener : getListeners()) { + try { + listener.onReconnectFailed(); + } catch (Exception e) { + e.printStackTrace(); + } + } + return; + } + try { + connect(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + if (channel != null) { + return; + } + reconnect(ttl - 1); + } + + protected void disable() throws SQLException, InterruptedException { + if (!enable) { + return; + } + enable = false; + + sendMessagingMessage( + BALL_CHANNEL, + ServerOfflineEvent.ACTION, + new ServerOfflineEvent(getLocalServerId()) + ); + + try (Connection connection = CoreAPI.getInstance().getConnection()) { + PreparedStatement statement = connection.prepareStatement("DELETE FROM " + BallCommonConstants.SQL.SERVER_INFO_TABLE + " WHERE `id`=?;"); + statement.setString(1, getLocalServerId()); + statement.executeUpdate(); + statement.close(); + } + + channel = null; + executors.shutdownGracefully().await(); + } + + /** + * 判断该服务器信息是否为本服 + * + * @param info 服务器信息 + * @return true 代表该服务器信息是本服服务器 + */ + public boolean isLocalServer(@NotNull ServerInfo info) { + return getLocalServerInfo().equals(info); + } + + /** + * 判断该服务器信息是否为本服 + * + * @param serverID 服务器ID + * @return true 代表该服务器信息是本服服务器 + */ + public boolean isLocalServer(@NotNull String serverID) { + return getLocalServerId().equalsIgnoreCase(serverID); + } + + /** + * 给服务器的在线玩家广播一条消息 + * + * @param message 消息 + */ + public void broadcastPlayerMessage(@NotNull String message) { + broadcastPlayerMessage(new Message().message(message)); + } + + /** + * 给服务器的在线玩家广播一条消息 + * + * @param message 消息 + */ + public void broadcastPlayerMessage(@NotNull Message message) { + sendMessagingMessage( + BALL_CHANNEL, + ServerType.PROXY, + BroadcastPlayerMessageEvent.ACTION, + new BroadcastPlayerMessageEvent(message) + ); + } + + /** + * 强制控制台执行命令 + * + * @param type 执行对象的服务端类型 + * @param serverID 执行对象的 ID + * @param command 命令内容 + */ + public void dispatchConsoleCommand(@Nullable ServerType type, @Nullable String serverID, @NotNull String command) { + sendMessagingMessage( + BALL_CHANNEL, + ServerType.GAME, + DispatchConsoleCommandEvent.ACTION, + new DispatchConsoleCommandEvent(type, serverID, command) + + ); + } + + /** + * 强制玩家执行命令 + * + * @param type 执行对象的服务端类型 + * @param uuid 执行对象的 UUID + * @param command 命令内容 + */ + public void dispatchPlayerCommand(@Nullable ServerType type, @Nullable UUID uuid, @NotNull String command) { + sendMessagingMessage( + BALL_CHANNEL, + ServerType.GAME, + DispatchPlayerCommandEvent.ACTION, + new DispatchPlayerCommandEvent(type, uuid, command) + + ); + } + + /** + * 踢出玩家 + * + * @param uuid 玩家 + * @param reason 原因 + */ + public void kickPlayer(@NotNull UUID uuid, @NotNull String reason) { + kickPlayer(uuid, Component.text(reason)); + } + + /** + * 踢出玩家 + * + * @param uuid 玩家 + * @param reason 原因 + */ + public void kickPlayer(@NotNull UUID uuid, @NotNull Component reason) { + sendMessagingMessage( + BALL_CHANNEL, + ServerType.PROXY, + KickPlayerEvent.ACTION, + new KickPlayerEvent(uuid, reason) + + ); + } + + /** + * 给玩家发送一条消息 + * + * @param uuid 玩家 + * @param message 消息 + */ + public void sendMessageToPlayer(@NotNull UUID uuid, @NotNull String message) { + sendMessageToPlayer(uuid, new Message().message(message), false); + } + + /** + * 给玩家发送一条消息 + * + * @param uuid 玩家 + * @param message 消息 + */ + public void sendMessageToPlayer(@NotNull UUID uuid, @NotNull Component message) { + sendMessageToPlayer(uuid, new Message().message(message), false); + } + + /** + * 给玩家发送一条消息 + * + * @param uuid 玩家 + * @param message 消息 + * @param cache 当玩家不在线时,是否缓存消息等待玩家上线再发送 + */ + public void sendMessageToPlayer(@NotNull UUID uuid, @NotNull Message message, boolean cache) { + PlayerInfo info = getPlayerInfo(uuid); + if (info == null || !info.isOnline()) { + if (!cache) { + return; + } + try (Connection connection = CoreAPI.getInstance().getConnection()) { + PreparedStatement statement = connection.prepareStatement("INSERT INTO " + BallCommonConstants.SQL.CACHED_MESSAGE_TABLE + " VALUES(?, ?);"); + statement.setString(1, uuid.toString()); + statement.setString(2, message.saveToJson().toString()); + statement.executeUpdate(); + statement.close(); + } catch (Exception e) { + e.printStackTrace(); + } + return; + } + sendMessagingMessage( + BALL_CHANNEL, + ServerType.PROXY, + SendMessageToPlayerEvent.ACTION, + new SendMessageToPlayerEvent(uuid, message) + + ); + } + + /** + * 把玩家传送到一个位置 + *

+ * 如果目标位置不在当前服务器 + *

+ * 则会先尝试将玩家连接至目标服务器再进行传送 + * + * @param sendPlayerUUID 玩家的uuid + * @param location 坐标 + */ + public void sendPlayerToLocation(@NotNull UUID sendPlayerUUID, @NotNull ServiceLocation location) { + sendMessagingMessage( + BALL_CHANNEL, + SendPlayerToLocationEvent.ACTION, + new SendPlayerToLocationEvent(Collections.singleton(sendPlayerUUID), location, null) + ); + } + + /** + * 把玩家传送到一个位置 + *

+ * 如果目标位置不在当前服务器 + *

+ * 则会先尝试将玩家连接至目标服务器再进行传送 + * + * @param sendPlayerUUID 玩家的uuid + * @param location 坐标 + * @param doneMessage 传送完成后显示的消息 + */ + public void sendPlayerToLocation(@NotNull UUID sendPlayerUUID, @NotNull ServiceLocation location, @Nullable Message doneMessage) { + sendMessagingMessage( + BALL_CHANNEL, + SendPlayerToLocationEvent.ACTION, + new SendPlayerToLocationEvent(Collections.singleton(sendPlayerUUID), location, doneMessage) + ); + } + + /** + * 把玩家传送到一个位置 + *

+ * 如果目标位置不在当前服务器 + *

+ * 则会先尝试将玩家连接至目标服务器再进行传送 + * + * @param uuidSet 玩家的uuid + * @param location 坐标 + */ + public void sendPlayerToLocation(@NotNull HashSet uuidSet, @NotNull ServiceLocation location) { + sendMessagingMessage( + BALL_CHANNEL, + SendPlayerToLocationEvent.ACTION, + new SendPlayerToLocationEvent(uuidSet, location, null) + ); + } + + /** + * 把玩家传送到一个位置 + *

+ * 如果目标位置不在当前服务器 + *

+ * 则会先尝试将玩家连接至目标服务器再进行传送 + * + * @param uuidSet 玩家的uuid + * @param location 坐标 + * @param doneMessage 传送完成后显示的消息 + */ + public void sendPlayerToLocation(@NotNull HashSet uuidSet, @NotNull ServiceLocation location, @Nullable Message doneMessage) { + sendMessagingMessage( + BALL_CHANNEL, + SendPlayerToLocationEvent.ACTION, + new SendPlayerToLocationEvent(uuidSet, location, doneMessage) + ); + } + + /** + * 把玩家传送到另一个玩家身边 + *

+ * 支持跨服传送 + * + * @param sendPlayerUUID 被传送的玩家 + * @param toPlayerUUID 传送的目标玩家 + */ + public void sendPlayerToPlayer(@NotNull UUID sendPlayerUUID, @NotNull UUID toPlayerUUID) { + sendMessagingMessage( + BALL_CHANNEL, + SendPlayerToPlayerEvent.ACTION, + new SendPlayerToPlayerEvent(Collections.singleton(sendPlayerUUID), toPlayerUUID, null, null) + ); + } + + /** + * 把玩家传送到另一个玩家身边 + *

+ * 支持跨服传送 + * + * @param sendPlayerUUID 被传送的玩家 + * @param toPlayerUUID 传送的目标玩家 + * @param doneMessage 传送完成后显示的消息 + */ + public void sendPlayerToPlayer(@NotNull UUID sendPlayerUUID, @NotNull UUID toPlayerUUID, @Nullable Message doneMessage) { + sendMessagingMessage( + BALL_CHANNEL, + SendPlayerToPlayerEvent.ACTION, + new SendPlayerToPlayerEvent(Collections.singleton(sendPlayerUUID), toPlayerUUID, doneMessage, null) + ); + } + + /** + * 把玩家传送到另一个玩家身边 + *

+ * 支持跨服传送 + * + * @param sendPlayers 被传送的玩家 + * @param toPlayerUUID 传送的目标玩家 + */ + public void sendPlayerToPlayer(@NotNull HashSet sendPlayers, @NotNull UUID toPlayerUUID) { + sendMessagingMessage( + BALL_CHANNEL, + SendPlayerToPlayerEvent.ACTION, + new SendPlayerToPlayerEvent(sendPlayers, toPlayerUUID, null, null) + ); + } + + /** + * 把玩家传送到另一个玩家身边 + *

+ * 支持跨服传送 + * + * @param sendPlayers 被传送的玩家 + * @param toPlayerUUID 传送的目标玩家 + * @param doneMessage 传送完成后显示的消息 + */ + public void sendPlayerToPlayer(@NotNull HashSet sendPlayers, @NotNull UUID toPlayerUUID, @Nullable Message doneMessage, @Nullable Message doneTargetMessage) { + sendMessagingMessage( + BALL_CHANNEL, + SendPlayerToPlayerEvent.ACTION, + new SendPlayerToPlayerEvent(sendPlayers, toPlayerUUID, doneMessage, doneTargetMessage) + ); + } + + /** + * 发送一条服务消息 + * + * @param channel 消息标签 + * @param action 执行动作 + */ + public void sendMessagingMessage(@NotNull String channel, @NotNull String action) { + sendMessagingMessage(new ServiceMessageInfo(channel, getLocalServerId(), null, null, action, null)); + } + + /** + * 发送一条有附加参数的服务消息 + * + * @param channel 消息标签 + * @param action 执行动作 + * @param content 附加参数 + */ + public void sendMessagingMessage(@NotNull String channel, @NotNull String action, @NotNull String content) { + sendMessagingMessage(new ServiceMessageInfo(channel, getLocalServerId(), null, null, action, new JsonPrimitive(content))); + } + + /** + * 发送一条有附加参数的消息 + * + * @param channel 消息频道 + * @param action 执行动作 + * @param content 附加参数 + */ + public void sendMessagingMessage(@NotNull String channel, @NotNull String action, @NotNull JsonElement content) { + sendMessagingMessage(new ServiceMessageInfo(channel, getLocalServerId(), null, null, action, content)); + } + + /** + * 发送一条有附加参数的服务消息 + * + * @param channel 消息标签 + * @param action 执行动作 + * @param content 附加参数 + */ + public void sendMessagingMessage(@NotNull String channel, @NotNull String action, @NotNull Object content) { + sendMessagingMessage(new ServiceMessageInfo(channel, getLocalServerId(), null, null, action, CoreConstantObjects.GSON.toJsonTree(content))); + } + + /** + * 发送一条有附加参数的消息 + * + * @param channel 消息频道 + * @param action 执行动作 + * @param content 附加参数 + */ + public void sendMessagingMessage(@NotNull String channel, @Nullable ServerType receiverType, @NotNull String action, @NotNull JsonElement content) { + sendMessagingMessage(new ServiceMessageInfo(channel, getLocalServerId(), null, receiverType, action, content)); + } + + /** + * 发送一条有附加参数的服务消息 + * + * @param channel 消息标签 + * @param action 执行动作 + * @param content 附加参数 + */ + public void sendMessagingMessage(@NotNull String channel, @Nullable ServerType receiverType, @NotNull String action, @NotNull Object content) { + sendMessagingMessage(new ServiceMessageInfo(channel, getLocalServerId(), null, receiverType, action, CoreConstantObjects.GSON.toJsonTree(content))); + } + + /** + * 发送自定义消息 + * + * @param messageInfo 消息内容 + */ + public void sendMessagingMessage(@NotNull ServiceMessageInfo messageInfo) { + sendMessagingMessage(messageInfo, false); + } + + /** + * 自定义服务消息信息并发送 + * + * @param messageInfo 消息内容 + * @param block 是否阻塞(设置为 true 则必须等待消息写入网络的操作完成后,该方法才会退出) + */ + public void sendMessagingMessage(@NotNull ServiceMessageInfo messageInfo, boolean block) { + if (channel == null || !channel.isWritable()) { + return; + } + ChannelFuture future = channel.write(CoreConstantObjects.GSON.toJsonTree(messageInfo)); + if (block) { + try { + future.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + for (BallListener listener : BallAPI.getInstance().getListeners()) { + try { + listener.onMessageSend(messageInfo); + } catch (Exception | Error e) { + e.printStackTrace(); + } + } + } + + @NotNull + public List getCachedPlayerMessage(@NotNull UUID uuid) throws SQLException { + ArrayList list = new ArrayList<>(); + try (Connection connection = CoreAPI.getInstance().getConnection()) { + PreparedStatement statement = connection.prepareStatement("SELECT message FROM " + BallCommonConstants.SQL.CACHED_MESSAGE_TABLE + " WHERE `uuid`=?;"); + statement.setString(1, uuid.toString()); + ResultSet set = statement.executeQuery(); + while (set.next()) { + JsonObject object = CoreConstantObjects.JSON_PARSER.parse(set.getString("msg")).getAsJsonObject(); + list.add(new Message().json(object)); + } + statement.close(); + } + return list; + } + + public void removeCachedPlayerMessage(@NotNull UUID uuid) throws SQLException { + try (Connection connection = CoreAPI.getInstance().getConnection()) { + PreparedStatement statement = connection.prepareStatement("DELETE FROM " + BallCommonConstants.SQL.CACHED_MESSAGE_TABLE + " WHERE `uuid`=?;"); + statement.setString(1, uuid.toString()); + statement.executeUpdate(); + statement.close(); + } + } + + public void addListener(@NotNull BallListener listener) { + listeners.add(listener); + listeners.sort(Comparator.comparingInt(BallListener::getPriority)); + } + + public void removeListener(@NotNull BallListener listener) { + listeners.remove(listener); + } + + /** + * 获取本地服务器ID + * + * @return 服务器ID + */ + @NotNull + public ServerInfo getLocalServerInfo() { + return config.getLocalInfo(); + } + + @NotNull + public String getLocalServerId() { + return config.getLocalInfo().getId(); + } + + /** + * 获取服务器信息 + * + * @param serverID 服务器ID + * @return 可能为 null + */ + public ServerInfo getServerInfo(@NotNull String serverID) { + return serverInfo.get(serverID); + } + + /** + * 获取玩家信息 + * + * @param uuid 玩家的 UUID + * @return 玩家信息 + */ + public PlayerInfo getPlayerInfo(@NotNull UUID uuid) { + return playerInfo.get(uuid); + } + + /** + * 获取玩家信息 + * + * @param playerName 玩家名称 + * @return 玩家信息 + */ + public PlayerInfo getPlayerInfo(@NotNull String playerName) { + return playerInfo.searchValues(Long.MAX_VALUE, info -> { + if (info.getName().equalsIgnoreCase(playerName)) { + return info; + } + return null; + }); + } + + @NotNull + public ConcurrentHashMap getAllServerInfo() { + return serverInfo; + } + + @NotNull + public ConcurrentHashMap getAllPlayerInfo() { + return playerInfo; + } + + @NotNull + public List getListeners() { + return listeners; + } +} diff --git a/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/config/BallConfig.java b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/config/BallConfig.java new file mode 100644 index 0000000..fc5fe28 --- /dev/null +++ b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/config/BallConfig.java @@ -0,0 +1,19 @@ +package cn.hamster3.mc.plugin.ball.common.config; + +import cn.hamster3.mc.plugin.ball.common.entity.ServerInfo; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.jetbrains.annotations.NotNull; + +@Data +@AllArgsConstructor +public class BallConfig { + @NotNull + private ServerInfo localInfo; + + @NotNull + private String host; + private int port; + + private int nioThread; +} diff --git a/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/connector/BallChannelInboundHandler.java b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/connector/BallChannelInboundHandler.java new file mode 100644 index 0000000..b5a1244 --- /dev/null +++ b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/connector/BallChannelInboundHandler.java @@ -0,0 +1,224 @@ +package cn.hamster3.mc.plugin.ball.common.connector; + +import cn.hamster3.mc.plugin.ball.common.api.BallAPI; +import cn.hamster3.mc.plugin.ball.common.data.ServiceMessageInfo; +import cn.hamster3.mc.plugin.ball.common.event.operate.*; +import cn.hamster3.mc.plugin.ball.common.event.player.*; +import cn.hamster3.mc.plugin.ball.common.event.server.ServerOfflineEvent; +import cn.hamster3.mc.plugin.ball.common.event.server.ServerOnlineEvent; +import cn.hamster3.mc.plugin.ball.common.listener.BallListener; +import cn.hamster3.mc.plugin.core.common.constant.CoreConstantObjects; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; + +public class BallChannelInboundHandler extends SimpleChannelInboundHandler { + public static final BallChannelInboundHandler INSTANCE = new BallChannelInboundHandler(); + + private BallChannelInboundHandler() { + super(true); + } + + @Override + protected void channelRead0(ChannelHandlerContext context, String message) { + ServiceMessageInfo info = CoreConstantObjects.GSON.fromJson(message, ServiceMessageInfo.class); + for (BallListener listener : BallAPI.getInstance().getListeners()) { + try { + listener.onMessageReceived(info); + } catch (Exception | Error e) { + e.printStackTrace(); + } + } + if (!BallAPI.BALL_CHANNEL.equals(info.getChannel())) { + return; + } + switch (info.getAction()) { + case BroadcastPlayerMessageEvent.ACTION: { + BroadcastPlayerMessageEvent event = CoreConstantObjects.GSON.fromJson(info.getContent(), BroadcastPlayerMessageEvent.class); + for (BallListener listener : BallAPI.getInstance().getListeners()) { + try { + listener.onBroadcastPlayerMessage(event); + } catch (Exception | Error e) { + e.printStackTrace(); + } + } + break; + } + case DispatchConsoleCommandEvent.ACTION: { + DispatchConsoleCommandEvent event = CoreConstantObjects.GSON.fromJson(info.getContent(), DispatchConsoleCommandEvent.class); + for (BallListener listener : BallAPI.getInstance().getListeners()) { + try { + listener.onDispatchConsoleCommand(event); + } catch (Exception | Error e) { + e.printStackTrace(); + } + } + break; + } + case DispatchPlayerCommandEvent.ACTION: { + DispatchPlayerCommandEvent event = CoreConstantObjects.GSON.fromJson(info.getContent(), DispatchPlayerCommandEvent.class); + for (BallListener listener : BallAPI.getInstance().getListeners()) { + try { + listener.onDispatchGamePlayerCommand(event); + } catch (Exception | Error e) { + e.printStackTrace(); + } + } + break; + } + case KickPlayerEvent.ACTION: { + KickPlayerEvent event = CoreConstantObjects.GSON.fromJson(info.getContent(), KickPlayerEvent.class); + for (BallListener listener : BallAPI.getInstance().getListeners()) { + try { + listener.onKickPlayer(event); + } catch (Exception | Error e) { + e.printStackTrace(); + } + } + break; + } + case SendMessageToPlayerEvent.ACTION: { + SendMessageToPlayerEvent event = CoreConstantObjects.GSON.fromJson(info.getContent(), SendMessageToPlayerEvent.class); + for (BallListener listener : BallAPI.getInstance().getListeners()) { + try { + listener.onSendMessageToPlayer(event); + } catch (Exception | Error e) { + e.printStackTrace(); + } + } + break; + } + case SendPlayerToLocationEvent.ACTION: { + SendPlayerToLocationEvent event = CoreConstantObjects.GSON.fromJson(info.getContent(), SendPlayerToLocationEvent.class); + for (BallListener listener : BallAPI.getInstance().getListeners()) { + try { + listener.onSendPlayerToLocation(event); + } catch (Exception | Error e) { + e.printStackTrace(); + } + } + break; + } + case SendPlayerToPlayerEvent.ACTION: { + SendPlayerToPlayerEvent event = CoreConstantObjects.GSON.fromJson(info.getContent(), SendPlayerToPlayerEvent.class); + for (BallListener listener : BallAPI.getInstance().getListeners()) { + try { + listener.onSendPlayerToPlayer(event); + } catch (Exception | Error e) { + e.printStackTrace(); + } + } + break; + } + case PlayerChatEvent.ACTION: { + PlayerChatEvent event = CoreConstantObjects.GSON.fromJson(info.getContent(), PlayerChatEvent.class); + for (BallListener listener : BallAPI.getInstance().getListeners()) { + try { + listener.onPlayerChat(event); + } catch (Exception | Error e) { + e.printStackTrace(); + } + } + break; + } + case PlayerConnectServerEvent.ACTION: { + PlayerConnectServerEvent event = CoreConstantObjects.GSON.fromJson(info.getContent(), PlayerConnectServerEvent.class); + for (BallListener listener : BallAPI.getInstance().getListeners()) { + try { + listener.onPlayerConnectServer(event); + } catch (Exception | Error e) { + e.printStackTrace(); + } + } + break; + } + case PlayerDisconnectEvent.ACTION: { + PlayerDisconnectEvent event = CoreConstantObjects.GSON.fromJson(info.getContent(), PlayerDisconnectEvent.class); + for (BallListener listener : BallAPI.getInstance().getListeners()) { + try { + listener.onPlayerDisconnect(event); + } catch (Exception | Error e) { + e.printStackTrace(); + } + } + break; + } + case PlayerLoginEvent.ACTION: { + PlayerLoginEvent event = CoreConstantObjects.GSON.fromJson(info.getContent(), PlayerLoginEvent.class); + for (BallListener listener : BallAPI.getInstance().getListeners()) { + try { + listener.onPlayerLogin(event); + } catch (Exception | Error e) { + e.printStackTrace(); + } + } + break; + } + case PlayerPostConnectServerEvent.ACTION: { + PlayerPostConnectServerEvent event = CoreConstantObjects.GSON.fromJson(info.getContent(), PlayerPostConnectServerEvent.class); + for (BallListener listener : BallAPI.getInstance().getListeners()) { + try { + listener.onPlayerPostConnectServer(event); + } catch (Exception | Error e) { + e.printStackTrace(); + } + } + break; + } + case PlayerPostLoginEvent.ACTION: { + PlayerPostLoginEvent event = CoreConstantObjects.GSON.fromJson(info.getContent(), PlayerPostLoginEvent.class); + for (BallListener listener : BallAPI.getInstance().getListeners()) { + try { + listener.onPlayerPostLogin(event); + } catch (Exception | Error e) { + e.printStackTrace(); + } + } + break; + } + case PlayerPreConnectServerEvent.ACTION: { + PlayerPreConnectServerEvent event = CoreConstantObjects.GSON.fromJson(info.getContent(), PlayerPreConnectServerEvent.class); + for (BallListener listener : BallAPI.getInstance().getListeners()) { + try { + listener.onPlayerPreConnectServer(event); + } catch (Exception | Error e) { + e.printStackTrace(); + } + } + break; + } + case PlayerPreLoginEvent.ACTION: { + PlayerPreLoginEvent event = CoreConstantObjects.GSON.fromJson(info.getContent(), PlayerPreLoginEvent.class); + for (BallListener listener : BallAPI.getInstance().getListeners()) { + try { + listener.onPlayerPreLogin(event); + } catch (Exception | Error e) { + e.printStackTrace(); + } + } + break; + } + case ServerOfflineEvent.ACTION: { + ServerOfflineEvent event = CoreConstantObjects.GSON.fromJson(info.getContent(), ServerOfflineEvent.class); + for (BallListener listener : BallAPI.getInstance().getListeners()) { + try { + listener.onServerOffline(event); + } catch (Exception | Error e) { + e.printStackTrace(); + } + } + break; + } + case ServerOnlineEvent.ACTION: { + ServerOnlineEvent event = CoreConstantObjects.GSON.fromJson(info.getContent(), ServerOnlineEvent.class); + for (BallListener listener : BallAPI.getInstance().getListeners()) { + try { + listener.onServerOnline(event); + } catch (Exception | Error e) { + e.printStackTrace(); + } + } + break; + } + } + } +} diff --git a/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/connector/BallChannelInitializer.java b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/connector/BallChannelInitializer.java new file mode 100644 index 0000000..2201cc4 --- /dev/null +++ b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/connector/BallChannelInitializer.java @@ -0,0 +1,55 @@ +package cn.hamster3.mc.plugin.ball.common.connector; + +import cn.hamster3.mc.plugin.ball.common.api.BallAPI; +import cn.hamster3.mc.plugin.ball.common.listener.BallListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.LengthFieldBasedFrameDecoder; +import io.netty.handler.codec.LengthFieldPrepender; +import io.netty.handler.codec.string.StringDecoder; +import io.netty.handler.codec.string.StringEncoder; +import org.jetbrains.annotations.NotNull; + +import java.nio.charset.StandardCharsets; + +public class BallChannelInitializer extends ChannelInitializer { + public static final BallChannelInitializer INSTANCE = new BallChannelInitializer(); + + private BallChannelInitializer() { + } + + @Override + protected void initChannel(@NotNull NioSocketChannel channel) { + channel.pipeline() + .addLast(new LengthFieldPrepender(8)) + .addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 8, 0, 8)) + .addLast(new StringDecoder(StandardCharsets.UTF_8)) + .addLast(new StringEncoder(StandardCharsets.UTF_8)) + .addLast(BallChannelInboundHandler.INSTANCE) + ; + } + + @Override + public void channelInactive(@NotNull ChannelHandlerContext context) { + for (BallListener listener : BallAPI.getInstance().getListeners()) { + try { + listener.onConnectInactive(); + } catch (Exception | Error e) { + e.printStackTrace(); + } + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext context, Throwable cause) { + for (BallListener listener : BallAPI.getInstance().getListeners()) { + try { + listener.onConnectException(cause); + } catch (Exception | Error e) { + e.printStackTrace(); + } + } + } +} + diff --git a/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/constant/BallCommonConstants.java b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/constant/BallCommonConstants.java new file mode 100644 index 0000000..d240bff --- /dev/null +++ b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/constant/BallCommonConstants.java @@ -0,0 +1,9 @@ +package cn.hamster3.mc.plugin.ball.common.constant; + +public interface BallCommonConstants { + interface SQL { + String PLAYER_INFO_TABLE = "hamster_ball_player_info"; + String SERVER_INFO_TABLE = "hamster_ball_server_info"; + String CACHED_MESSAGE_TABLE = "hamster_ball_cached_message"; + } +} diff --git a/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/data/ServiceBlockPos.java b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/data/ServiceBlockPos.java new file mode 100644 index 0000000..9e14bde --- /dev/null +++ b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/data/ServiceBlockPos.java @@ -0,0 +1,23 @@ +package cn.hamster3.mc.plugin.ball.common.data; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.jetbrains.annotations.NotNull; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@SuppressWarnings("unused") +public class ServiceBlockPos { + private String serverID; + private String worldName; + private int x; + private int y; + private int z; + + @NotNull + public ServiceLocation toServiceLocation() { + return new ServiceLocation(getServerID(), getWorldName(), getX(), getY(), getZ(), 0, 0); + } +} diff --git a/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/data/ServiceLocation.java b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/data/ServiceLocation.java new file mode 100644 index 0000000..5f7d258 --- /dev/null +++ b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/data/ServiceLocation.java @@ -0,0 +1,38 @@ +package cn.hamster3.mc.plugin.ball.common.data; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.jetbrains.annotations.NotNull; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@SuppressWarnings("unused") +public class ServiceLocation { + private String serverID; + private String worldName; + private double x; + private double y; + private double z; + + private float yaw; + private float pitch; + + public int getBlockX() { + return (int) x; + } + + public int getBlockY() { + return (int) y; + } + + public int getBlockZ() { + return (int) z; + } + + @NotNull + public ServiceBlockPos toServiceBlockPos() { + return new ServiceBlockPos(getServerID(), getWorldName(), getBlockX(), getBlockY(), getBlockZ()); + } +} diff --git a/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/data/ServiceMessageInfo.java b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/data/ServiceMessageInfo.java new file mode 100644 index 0000000..bc5743a --- /dev/null +++ b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/data/ServiceMessageInfo.java @@ -0,0 +1,154 @@ +package cn.hamster3.mc.plugin.ball.common.data; + +import cn.hamster3.mc.plugin.ball.common.entity.ServerType; +import cn.hamster3.mc.plugin.core.common.constant.CoreConstantObjects; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.UUID; + +/** + * 服务消息 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@SuppressWarnings("unused") +public class ServiceMessageInfo { + /** + * 消息的频道 + */ + @NotNull + private String channel; + /** + * 消息发送者 + */ + @NotNull + private String senderID; + /** + * 接受该消息的目标服务器ID + *

+ * 若设定该值,则仅服务器名称匹配的子端才能接收到这条消息 + *

+ * 若不设定(即为 null),则该消息会广播给所有子端 + */ + @Nullable + private String receiverID; + /** + * 接受该消息的目标服务器类型 + *

+ * 若设定该值,则仅服务器类型匹配的子端才能接收到这条消息 + *

+ * 若不设定(值为null),则该消息会广播给所有子端 + */ + @Nullable + private ServerType receiverType; + /** + * 消息动作 + *

+ * 一般用这个来判断插件应该如何处理这条消息 + */ + private String action; + /** + * 消息内容 + *

+ * 这里是消息的附加参数 + */ + private JsonElement content; + + /** + * 序列化至 Json + * + * @return json对象 + */ + @NotNull + public JsonObject saveToJson() { + JsonObject object = new JsonObject(); + object.addProperty("channel", channel); + object.addProperty("senderID", senderID); + if (receiverID != null) { + object.addProperty("toServer", receiverID); + } + if (receiverType != null) { + object.addProperty("toServer", receiverType.name()); + } + object.addProperty("action", action); + object.add("content", content); + return object; + } + + /** + * 以 Java 对象获取消息内容 + * + * @param clazz 对象所属的类 + * @param 对象类型 + * @return Java 对象 + */ + public T getContentAs(Class clazz) { + return CoreConstantObjects.GSON.fromJson(content, clazz); + } + + /** + * 以字符串形式获取消息内容 + * + * @return 消息内容 + */ + public String getContentAsString() { + return content.getAsString(); + } + + public boolean getContentAsBoolean() { + return content.getAsBoolean(); + } + + public int getContentAsInt() { + return content.getAsInt(); + } + + public long getContentAsLong() { + return content.getAsLong(); + } + + public BigInteger getContentAsBigInteger() { + return content.getAsBigInteger(); + } + + public BigDecimal getContentAsBigDecimal() { + return content.getAsBigDecimal(); + } + + public UUID getContentAsUUID() { + return UUID.fromString(content.getAsString()); + } + + /** + * 以 JsonObject 对象获取消息内容 + * + * @return 消息内容 + */ + public JsonObject getContentAsJsonObject() { + return content.getAsJsonObject(); + } + + /** + * 以 JsonArray 对象获取消息内容 + * + * @return 消息内容 + */ + public JsonArray getContentAsJsonArray() { + return content.getAsJsonArray(); + } + + @Override + public String toString() { + return saveToJson().toString(); + } +} diff --git a/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/entity/PlayerInfo.java b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/entity/PlayerInfo.java new file mode 100644 index 0000000..584b43c --- /dev/null +++ b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/entity/PlayerInfo.java @@ -0,0 +1,65 @@ +package cn.hamster3.mc.plugin.ball.common.entity; + +import com.google.gson.JsonElement; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * 玩家信息 + */ +@Data +@NotNull +@AllArgsConstructor +public class PlayerInfo { + /** + * 玩家的uuid + */ + @NotNull + private UUID uuid; + /** + * 玩家的名称 + */ + @NotNull + private String name; + /** + * 玩家的档案(包含皮肤等信息) + */ + @NotNull + private JsonElement profile; + /** + * 玩家所在的游戏服务器 ID + *

+ * 不应超过 32 个字符 + */ + @NotNull + private String gameServer; + /** + * 玩家所在的代理服务器 ID + *

+ * 不应超过 32 个字符 + */ + @NotNull + private String proxyServer; + /** + * 玩家是否在线 + */ + private boolean online; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PlayerInfo that = (PlayerInfo) o; + + return uuid.equals(that.uuid); + } + + @Override + public int hashCode() { + return uuid.hashCode(); + } +} diff --git a/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/entity/ServerInfo.java b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/entity/ServerInfo.java new file mode 100644 index 0000000..3b2e3bd --- /dev/null +++ b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/entity/ServerInfo.java @@ -0,0 +1,55 @@ +package cn.hamster3.mc.plugin.ball.common.entity; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Objects; + +/** + * 消息发送者信息 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ServerInfo { + /** + * 服务器 ID + *

+ * 不应超过 32 个字符 + */ + private String id; + /** + * 服务器名称 + *

+ * 不应超过 32 个字符 + */ + private String name; + /** + * 服务器类型 + */ + private ServerType type; + /** + * 服务器主机名 + *

+ * 不应超过 32 个字符 + */ + private String host; + /** + * 服务器端口号 + */ + private int port; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ServerInfo that = (ServerInfo) o; + return id.equals(that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/entity/ServerType.java b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/entity/ServerType.java new file mode 100644 index 0000000..263488f --- /dev/null +++ b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/entity/ServerType.java @@ -0,0 +1,24 @@ +package cn.hamster3.mc.plugin.ball.common.entity; + +/** + * Service 接入者的类型 + */ +@SuppressWarnings("unused") +public enum ServerType { + /** + * 游戏服务器 + */ + GAME, + /** + * 代理服务器 + */ + PROXY, + /** + * 测试服务器 + */ + TEST, + /** + * 其他类型的接入者 + */ + OTHER +} diff --git a/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/operate/BroadcastPlayerMessageEvent.java b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/operate/BroadcastPlayerMessageEvent.java new file mode 100644 index 0000000..3d09c01 --- /dev/null +++ b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/operate/BroadcastPlayerMessageEvent.java @@ -0,0 +1,16 @@ +package cn.hamster3.mc.plugin.ball.common.event.operate; + +import cn.hamster3.mc.plugin.core.common.data.Message; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.jetbrains.annotations.NotNull; + +@Data +@AllArgsConstructor +public class BroadcastPlayerMessageEvent { + public static final String ACTION = "BroadcastPlayerMessage"; + + @NotNull + private final Message message; + +} diff --git a/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/operate/DispatchConsoleCommandEvent.java b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/operate/DispatchConsoleCommandEvent.java new file mode 100644 index 0000000..a1c0d00 --- /dev/null +++ b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/operate/DispatchConsoleCommandEvent.java @@ -0,0 +1,20 @@ +package cn.hamster3.mc.plugin.ball.common.event.operate; + +import cn.hamster3.mc.plugin.ball.common.entity.ServerType; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@Data +@AllArgsConstructor +public class DispatchConsoleCommandEvent { + public static final String ACTION = "DispatchConsoleCommand"; + + @Nullable + private final ServerType type; + @Nullable + private final String serverID; + @NotNull + private final String command; +} diff --git a/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/operate/DispatchPlayerCommandEvent.java b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/operate/DispatchPlayerCommandEvent.java new file mode 100644 index 0000000..2ff635b --- /dev/null +++ b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/operate/DispatchPlayerCommandEvent.java @@ -0,0 +1,22 @@ +package cn.hamster3.mc.plugin.ball.common.event.operate; + +import cn.hamster3.mc.plugin.ball.common.entity.ServerType; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +@Data +@AllArgsConstructor +public class DispatchPlayerCommandEvent { + public static final String ACTION = "DispatchPlayerCommand"; + + @Nullable + private final ServerType type; + @Nullable + private final UUID uuid; + @NotNull + private final String command; +} diff --git a/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/operate/KickPlayerEvent.java b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/operate/KickPlayerEvent.java new file mode 100644 index 0000000..0e83ad2 --- /dev/null +++ b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/operate/KickPlayerEvent.java @@ -0,0 +1,20 @@ +package cn.hamster3.mc.plugin.ball.common.event.operate; + +import lombok.AllArgsConstructor; +import lombok.Data; +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +@Data +@AllArgsConstructor +public class KickPlayerEvent { + public static final String ACTION = "KickPlayer"; + + @NotNull + private final UUID uuid; + @NotNull + private final Component reason; + +} diff --git a/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/operate/SendMessageToPlayerEvent.java b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/operate/SendMessageToPlayerEvent.java new file mode 100644 index 0000000..0419174 --- /dev/null +++ b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/operate/SendMessageToPlayerEvent.java @@ -0,0 +1,19 @@ +package cn.hamster3.mc.plugin.ball.common.event.operate; + +import cn.hamster3.mc.plugin.core.common.data.Message; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +@Data +@AllArgsConstructor +public class SendMessageToPlayerEvent { + public static final String ACTION = "SendMessageToPlayer"; + + @NotNull + private final UUID uuid; + @NotNull + private final Message message; +} diff --git a/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/operate/SendPlayerToLocationEvent.java b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/operate/SendPlayerToLocationEvent.java new file mode 100644 index 0000000..23f9887 --- /dev/null +++ b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/operate/SendPlayerToLocationEvent.java @@ -0,0 +1,26 @@ +package cn.hamster3.mc.plugin.ball.common.event.operate; + +import cn.hamster3.mc.plugin.ball.common.data.ServiceLocation; +import cn.hamster3.mc.plugin.core.common.data.Message; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +@Data +@AllArgsConstructor +public class SendPlayerToLocationEvent { + public static final String ACTION = "SendPlayerToLocation"; + + @NotNull + private final Set sendPlayerUUID; + @NotNull + private final ServiceLocation location; + @Nullable + private final Message doneMessage; + +} diff --git a/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/operate/SendPlayerToPlayerEvent.java b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/operate/SendPlayerToPlayerEvent.java new file mode 100644 index 0000000..48c11b8 --- /dev/null +++ b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/operate/SendPlayerToPlayerEvent.java @@ -0,0 +1,27 @@ +package cn.hamster3.mc.plugin.ball.common.event.operate; + +import cn.hamster3.mc.plugin.core.common.data.Message; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +@Data +@AllArgsConstructor +public class SendPlayerToPlayerEvent { + public static final String ACTION = "SendPlayerToPlayer"; + + @NotNull + private final Set sendPlayerUUID; + @NotNull + private final UUID toPlayerUUID; + @Nullable + private final Message doneMessage; + @Nullable + private final Message doneTargetMessage; + +} diff --git a/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/player/PlayerChatEvent.java b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/player/PlayerChatEvent.java new file mode 100644 index 0000000..0c60eed --- /dev/null +++ b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/player/PlayerChatEvent.java @@ -0,0 +1,25 @@ +package cn.hamster3.mc.plugin.ball.common.event.player; + +import lombok.AllArgsConstructor; +import lombok.Data; +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * 玩家的聊天信息 + */ +@Data +@AllArgsConstructor +public class PlayerChatEvent { + public static final String ACTION = "PlayerChat"; + + @NotNull + private final UUID playerUUID; + @NotNull + private final Component displayName; + @NotNull + private final Component message; + +} diff --git a/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/player/PlayerConnectServerEvent.java b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/player/PlayerConnectServerEvent.java new file mode 100644 index 0000000..7ab7586 --- /dev/null +++ b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/player/PlayerConnectServerEvent.java @@ -0,0 +1,23 @@ +package cn.hamster3.mc.plugin.ball.common.event.player; + +import cn.hamster3.mc.plugin.ball.common.entity.PlayerInfo; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * 玩家进入子服 + */ +@Data +@AllArgsConstructor +public class PlayerConnectServerEvent { + public static final String ACTION = "PlayerConnectServer"; + + @NotNull + private final PlayerInfo playerInfo; + @Nullable + private final String from; + @NotNull + private final String to; +} diff --git a/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/player/PlayerDisconnectEvent.java b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/player/PlayerDisconnectEvent.java new file mode 100644 index 0000000..3baf7cb --- /dev/null +++ b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/player/PlayerDisconnectEvent.java @@ -0,0 +1,20 @@ +package cn.hamster3.mc.plugin.ball.common.event.player; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * 玩家与服务器断开连接 + */ +@Data +@AllArgsConstructor +public class PlayerDisconnectEvent { + public static final String ACTION = "PlayerDisconnect"; + + @NotNull + private final UUID playerUUID; + private final String serverID; +} diff --git a/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/player/PlayerLoginEvent.java b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/player/PlayerLoginEvent.java new file mode 100644 index 0000000..6bf495a --- /dev/null +++ b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/player/PlayerLoginEvent.java @@ -0,0 +1,19 @@ +package cn.hamster3.mc.plugin.ball.common.event.player; + +import cn.hamster3.mc.plugin.ball.common.entity.PlayerInfo; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.jetbrains.annotations.NotNull; + +/** + * 玩家连接到服务器 + */ +@Data +@AllArgsConstructor +public class PlayerLoginEvent { + public static final String ACTION = "PlayerLogin"; + + @NotNull + private final PlayerInfo playerInfo; + +} diff --git a/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/player/PlayerPostConnectServerEvent.java b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/player/PlayerPostConnectServerEvent.java new file mode 100644 index 0000000..f07d76f --- /dev/null +++ b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/player/PlayerPostConnectServerEvent.java @@ -0,0 +1,23 @@ +package cn.hamster3.mc.plugin.ball.common.event.player; + +import cn.hamster3.mc.plugin.ball.common.entity.PlayerInfo; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * 玩家已进入子服 + */ +@Data +@AllArgsConstructor +public class PlayerPostConnectServerEvent { + public static final String ACTION = "PlayerPostConnectServer"; + + @NotNull + private final PlayerInfo playerInfo; + @Nullable + private final String from; + @NotNull + private final String to; +} diff --git a/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/player/PlayerPostLoginEvent.java b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/player/PlayerPostLoginEvent.java new file mode 100644 index 0000000..b3dc6f2 --- /dev/null +++ b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/player/PlayerPostLoginEvent.java @@ -0,0 +1,18 @@ +package cn.hamster3.mc.plugin.ball.common.event.player; + +import cn.hamster3.mc.plugin.ball.common.entity.PlayerInfo; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.jetbrains.annotations.NotNull; + +/** + * 玩家已连接到服务器 + */ +@Data +@AllArgsConstructor +public class PlayerPostLoginEvent { + public static final String ACTION = "PlayerPostLogin"; + + @NotNull + private final PlayerInfo playerInfo; +} diff --git a/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/player/PlayerPreConnectServerEvent.java b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/player/PlayerPreConnectServerEvent.java new file mode 100644 index 0000000..fa755b1 --- /dev/null +++ b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/player/PlayerPreConnectServerEvent.java @@ -0,0 +1,24 @@ +package cn.hamster3.mc.plugin.ball.common.event.player; + +import cn.hamster3.mc.plugin.ball.common.entity.PlayerInfo; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * 玩家准备进入子服 + */ +@Data +@AllArgsConstructor +public class PlayerPreConnectServerEvent { + public static final String ACTION = "PlayerPreConnectServer"; + + @NotNull + private final PlayerInfo playerInfo; + @Nullable + private final String from; + @NotNull + private final String to; + +} diff --git a/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/player/PlayerPreLoginEvent.java b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/player/PlayerPreLoginEvent.java new file mode 100644 index 0000000..9784b18 --- /dev/null +++ b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/player/PlayerPreLoginEvent.java @@ -0,0 +1,17 @@ +package cn.hamster3.mc.plugin.ball.common.event.player; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.jetbrains.annotations.NotNull; + +/** + * 玩家准备连接到服务器 + */ +@Data +@AllArgsConstructor +public class PlayerPreLoginEvent { + public static final String ACTION = "PlayerPreLogin"; + + @NotNull + private final String playerName; +} diff --git a/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/server/ServerOfflineEvent.java b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/server/ServerOfflineEvent.java new file mode 100644 index 0000000..2109925 --- /dev/null +++ b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/server/ServerOfflineEvent.java @@ -0,0 +1,17 @@ +package cn.hamster3.mc.plugin.ball.common.event.server; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.jetbrains.annotations.NotNull; + +/** + * 服务器离线 + */ +@Data +@AllArgsConstructor +public class ServerOfflineEvent { + public static final String ACTION = "ServerOffline"; + + @NotNull + private final String serverID; +} diff --git a/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/server/ServerOnlineEvent.java b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/server/ServerOnlineEvent.java new file mode 100644 index 0000000..3c0d3be --- /dev/null +++ b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/event/server/ServerOnlineEvent.java @@ -0,0 +1,19 @@ +package cn.hamster3.mc.plugin.ball.common.event.server; + +import cn.hamster3.mc.plugin.ball.common.entity.ServerInfo; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.jetbrains.annotations.NotNull; + +/** + * 服务器上线 + */ +@Data +@AllArgsConstructor +public class ServerOnlineEvent { + public static final String ACTION = "ServerOnline"; + + @NotNull + private final ServerInfo serverInfo; + +} diff --git a/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/listener/BallListener.java b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/listener/BallListener.java new file mode 100644 index 0000000..44b8acb --- /dev/null +++ b/hamster-ball-common/src/main/java/cn/hamster3/mc/plugin/ball/common/listener/BallListener.java @@ -0,0 +1,88 @@ +package cn.hamster3.mc.plugin.ball.common.listener; + +import cn.hamster3.mc.plugin.ball.common.data.ServiceMessageInfo; +import cn.hamster3.mc.plugin.ball.common.event.operate.*; +import cn.hamster3.mc.plugin.ball.common.event.player.*; +import cn.hamster3.mc.plugin.ball.common.event.server.ServerOfflineEvent; +import cn.hamster3.mc.plugin.ball.common.event.server.ServerOnlineEvent; +import org.jetbrains.annotations.NotNull; + +public abstract class BallListener { + /** + * 该监听器的执行优先级 + *

+ * 数字越低越先执行 + * + * @return 优先级 + */ + public int getPriority() { + return 100; + } + + public void onBroadcastPlayerMessage(@NotNull BroadcastPlayerMessageEvent event) { + } + + public void onDispatchConsoleCommand(@NotNull DispatchConsoleCommandEvent event) { + } + + public void onDispatchGamePlayerCommand(@NotNull DispatchPlayerCommandEvent event) { + } + + public void onKickPlayer(@NotNull KickPlayerEvent event) { + } + + public void onSendMessageToPlayer(@NotNull SendMessageToPlayerEvent event) { + } + + public void onSendPlayerToLocation(@NotNull SendPlayerToLocationEvent event) { + } + + public void onSendPlayerToPlayer(@NotNull SendPlayerToPlayerEvent event) { + } + + public void onPlayerChat(@NotNull PlayerChatEvent event) { + } + + public void onPlayerConnectServer(@NotNull PlayerConnectServerEvent event) { + } + + public void onPlayerDisconnect(@NotNull PlayerDisconnectEvent event) { + } + + public void onPlayerLogin(@NotNull PlayerLoginEvent event) { + } + + public void onPlayerPostConnectServer(@NotNull PlayerPostConnectServerEvent event) { + } + + public void onPlayerPostLogin(@NotNull PlayerPostLoginEvent event) { + } + + public void onPlayerPreConnectServer(@NotNull PlayerPreConnectServerEvent event) { + } + + public void onPlayerPreLogin(@NotNull PlayerPreLoginEvent event) { + } + + public void onServerOffline(@NotNull ServerOfflineEvent event) { + } + + public void onServerOnline(@NotNull ServerOnlineEvent event) { + } + + public void onMessageReceived(@NotNull ServiceMessageInfo event) { + } + + public void onMessageSend(@NotNull ServiceMessageInfo event) { + } + + public void onConnectInactive() { + } + + public void onConnectException(Throwable throwable) { + } + + public void onReconnectFailed() { + } + +} diff --git a/hamster-ball-server/build.gradle b/hamster-ball-server/build.gradle new file mode 100644 index 0000000..2b8ab4c --- /dev/null +++ b/hamster-ball-server/build.gradle @@ -0,0 +1,56 @@ +version = '1.0.0' +setArchivesBaseName("HamsterBall-Server") + +evaluationDependsOn(':hamster-ball-common') + +dependencies { + apiShade(project(":hamster-ball-common")) { + exclude group: "*" + } + + // https://mvnrepository.com/artifact/org.slf4j/slf4j-api + implementation 'org.slf4j:slf4j-api:2.0.3' + + // https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core + shade 'org.apache.logging.log4j:log4j-core:2.19.0' + // https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j-impl + shade 'org.apache.logging.log4j:log4j-slf4j-impl:2.19.0' + + // https://mvnrepository.com/artifact/io.netty/netty-all + implementationShade 'io.netty:netty-all:4.1.84.Final' + // https://mvnrepository.com/artifact/org.yaml/snakeyaml + implementationShade 'org.yaml:snakeyaml:1.33' + // https://mvnrepository.com/artifact/com.google.code.gson/gson + implementation 'com.google.code.gson:gson:2.9.1' + + + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' +} + +test { + useJUnitPlatform() +} + +tasks.compileJava.dependsOn(":hamster-ball-common:build") +tasks.create("shadowJar", Jar) { + dependsOn("jar") + manifest.attributes('Main-Class': 'cn.hamster3.mc.plugin.ball.server.Bootstrap') + manifest.attributes('ball-version': project.version) + from([ + tasks.jar.outputs.files.collect { + it.isDirectory() ? it : zipTree(it) + }, + configurations.shade.collect { + it.isDirectory() ? it : zipTree(it) + }, + configurations.apiShade.collect { + it.isDirectory() ? it : zipTree(it) + }, + configurations.implementationShade.collect { + it.isDirectory() ? it : zipTree(it) + } + ]) + destinationDir(rootProject.buildDir) +} +tasks.build.dependsOn(shadowJar) diff --git a/hamster-ball-server/src/main/java/cn/hamster3/mc/plugin/ball/server/Bootstrap.java b/hamster-ball-server/src/main/java/cn/hamster3/mc/plugin/ball/server/Bootstrap.java new file mode 100644 index 0000000..7549269 --- /dev/null +++ b/hamster-ball-server/src/main/java/cn/hamster3/mc/plugin/ball/server/Bootstrap.java @@ -0,0 +1,76 @@ +package cn.hamster3.mc.plugin.ball.server; + +import cn.hamster3.mc.plugin.ball.server.command.CommandHandler; +import cn.hamster3.mc.plugin.ball.server.config.ServerConfig; +import cn.hamster3.mc.plugin.ball.server.connector.BallChannelInitializer; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelOption; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; + +public class Bootstrap { + private static final Logger LOGGER = LoggerFactory.getLogger("Bootstrap"); + + public static void main(String[] args) throws IOException { + if (initDefaultFile()) { + System.out.println("请重新启动该程序."); + return; + } + ServerConfig.init(); + LOGGER.info("配置文件加载完成."); + + NioEventLoopGroup loopGroup = new NioEventLoopGroup(ServerConfig.getNioThread()); + ServerBootstrap bootstrap = new ServerBootstrap() + .group(loopGroup) + .channel(NioServerSocketChannel.class) + .childOption(ChannelOption.TCP_NODELAY, true) + .childOption(ChannelOption.SO_KEEPALIVE, true) + .childHandler(BallChannelInitializer.INSTANCE); + ChannelFuture channelFuture = bootstrap.bind(ServerConfig.getHost(), ServerConfig.getPort()); + channelFuture.addListener(future -> { + if (future.isSuccess()) { + LOGGER.info("服务器已启动. 输入 stop 来关闭该程序."); + } else { + LOGGER.error("仓鼠球服务器启动失败!", future.cause()); + loopGroup.shutdownGracefully(); + } + }); + + CommandHandler.INSTANCE.start(loopGroup); + } + + private static boolean initDefaultFile() throws IOException { + boolean saved = false; + File log4jFile = new File("log4j2.xml"); + if (!log4jFile.exists()) { + InputStream stream = ServerConfig.class.getResourceAsStream("/log4j2.xml"); + if (stream == null) { + throw new IOException("log4j2.xml 文件损坏!"); + } + Files.copy(stream, log4jFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + System.out.println("已生成默认 log4j2.xml 文件!"); + saved = true; + } + + File configFile = new File("config.yml"); + if (!configFile.exists()) { + InputStream stream = ServerConfig.class.getResourceAsStream("/config.yml"); + if (stream == null) { + throw new IOException("config.yml 文件损坏!"); + } + Files.copy(stream, configFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + System.out.println("已生成默认 config.yml 文件!"); + saved = true; + } + return saved; + } +} diff --git a/hamster-ball-server/src/main/java/cn/hamster3/mc/plugin/ball/server/command/CommandHandler.java b/hamster-ball-server/src/main/java/cn/hamster3/mc/plugin/ball/server/command/CommandHandler.java new file mode 100644 index 0000000..1e9aa6a --- /dev/null +++ b/hamster-ball-server/src/main/java/cn/hamster3/mc/plugin/ball/server/command/CommandHandler.java @@ -0,0 +1,67 @@ +package cn.hamster3.mc.plugin.ball.server.command; + +import io.netty.channel.nio.NioEventLoopGroup; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Scanner; + +public class CommandHandler { + public static final CommandHandler INSTANCE = new CommandHandler(); + + private static final Logger LOGGER = LoggerFactory.getLogger("command"); + private NioEventLoopGroup loopGroup; + + private boolean started; + + public void start(NioEventLoopGroup loopGroup) { + this.loopGroup = loopGroup; + + started = true; + Scanner scanner = new Scanner(System.in); + LOGGER.info("命令执行器准备就绪. 输入 help 查看命令帮助."); + while (started) { + String command = scanner.nextLine(); + try { + executeCommand(command); + } catch (Exception e) { + LOGGER.error("执行命令 " + command + " 时遇到了一个异常: ", e); + } + } + } + + public void executeCommand(String command) throws Exception { + String[] args = command.split(" "); + switch (args[0].toLowerCase()) { + case "?": + case "help": { + help(); + break; + } + case "end": + case "stop": { + stop(); + break; + } + default: { + LOGGER.info("未知指令. 请输入 help 查看帮助."); + break; + } + } + } + + public void help() { + LOGGER.info("==============================================================="); + LOGGER.info("help - 查看帮助."); + LOGGER.info("stop - 关闭该程序."); + LOGGER.info("==============================================================="); + } + + public void stop() throws Exception { + started = false; + LOGGER.info("准备关闭服务器..."); + loopGroup.shutdownGracefully().await(); + LOGGER.info("服务器已关闭!"); + } + +} diff --git a/hamster-ball-server/src/main/java/cn/hamster3/mc/plugin/ball/server/config/ServerConfig.java b/hamster-ball-server/src/main/java/cn/hamster3/mc/plugin/ball/server/config/ServerConfig.java new file mode 100644 index 0000000..521e069 --- /dev/null +++ b/hamster-ball-server/src/main/java/cn/hamster3/mc/plugin/ball/server/config/ServerConfig.java @@ -0,0 +1,65 @@ +package cn.hamster3.mc.plugin.ball.server.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.yaml.snakeyaml.Yaml; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.List; +import java.util.Map; + +public final class ServerConfig { + private static final Logger LOGGER = LoggerFactory.getLogger("ServerConfig"); + + private static String host; + private static int port; + private static int nioThread; + private static boolean enableAcceptList; + private static List acceptList; + + private ServerConfig() { + } + + @SuppressWarnings("unchecked") + public static void init() throws IOException { + File configFile = new File("config.yml"); + InputStream stream = Files.newInputStream(configFile.toPath()); + Map map = new Yaml().load(stream); + stream.close(); + + host = (String) map.get("host"); + port = (int) map.get("port"); + nioThread = (int) map.get("nio-thread"); + enableAcceptList = (boolean) map.get("enable-accept-list"); + acceptList = (List) map.get("accept-list"); + + LOGGER.info("host: {}", host); + LOGGER.info("port: {}", port); + LOGGER.info("nioThread: {}", nioThread); + LOGGER.info("enableAcceptList: {}", enableAcceptList); + LOGGER.info("acceptList: {}", acceptList); + } + + public static String getHost() { + return host; + } + + public static int getPort() { + return port; + } + + public static int getNioThread() { + return nioThread; + } + + public static boolean isEnableAcceptList() { + return enableAcceptList; + } + + public static List getAcceptList() { + return acceptList; + } +} diff --git a/hamster-ball-server/src/main/java/cn/hamster3/mc/plugin/ball/server/connector/BallChannelHandler.java b/hamster-ball-server/src/main/java/cn/hamster3/mc/plugin/ball/server/connector/BallChannelHandler.java new file mode 100644 index 0000000..6737190 --- /dev/null +++ b/hamster-ball-server/src/main/java/cn/hamster3/mc/plugin/ball/server/connector/BallChannelHandler.java @@ -0,0 +1,40 @@ +package cn.hamster3.mc.plugin.ball.server.connector; + +import cn.hamster3.mc.plugin.ball.common.data.ServiceMessageInfo; +import cn.hamster3.mc.plugin.ball.server.constant.ConstantObjects; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BallChannelHandler extends SimpleChannelInboundHandler { + private static final Logger LOGGER = LoggerFactory.getLogger("ChannelHandler"); + + public BallChannelHandler() { + super(true); + } + + @Override + protected void channelRead0(ChannelHandlerContext context, String message) { + try { + ServiceMessageInfo messageInfo = ConstantObjects.GSON.fromJson(message, ServiceMessageInfo.class); + LOGGER.info("从服务器 {} 上收到一条消息: \n {}", messageInfo.getSenderID(), messageInfo); + BallChannelInitializer.broadcastMessage(messageInfo); + } catch (Exception e) { + LOGGER.error(String.format("处理消息 %s 时出现错误: ", message), e); + } + } + + @Override + public void channelInactive(ChannelHandlerContext context) { + context.close(); + BallChannelInitializer.CHANNELS.remove(context.channel()); + LOGGER.warn("与服务器 {} 的连接已断开.", context.channel().remoteAddress()); + } + + @Override + public void exceptionCaught(ChannelHandlerContext context, Throwable cause) { + LOGGER.warn("与服务器 {} 通信时出现了一个错误: ", context.channel().remoteAddress(), cause); + } +} diff --git a/hamster-ball-server/src/main/java/cn/hamster3/mc/plugin/ball/server/connector/BallChannelInitializer.java b/hamster-ball-server/src/main/java/cn/hamster3/mc/plugin/ball/server/connector/BallChannelInitializer.java new file mode 100644 index 0000000..0e71a10 --- /dev/null +++ b/hamster-ball-server/src/main/java/cn/hamster3/mc/plugin/ball/server/connector/BallChannelInitializer.java @@ -0,0 +1,57 @@ +package cn.hamster3.mc.plugin.ball.server.connector; + +import cn.hamster3.mc.plugin.ball.common.data.ServiceMessageInfo; +import cn.hamster3.mc.plugin.ball.server.config.ServerConfig; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.LengthFieldBasedFrameDecoder; +import io.netty.handler.codec.LengthFieldPrepender; +import io.netty.handler.codec.string.StringDecoder; +import io.netty.handler.codec.string.StringEncoder; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +public class BallChannelInitializer extends ChannelInitializer { + public static final BallChannelInitializer INSTANCE = new BallChannelInitializer(); + public static final List CHANNELS = new ArrayList<>(); + + private static final Logger LOGGER = LoggerFactory.getLogger("BallServerCentre"); + + private BallChannelInitializer() { + } + + public static void broadcastMessage(ServiceMessageInfo messageInfo) { + String string = messageInfo.toString(); + for (Channel channel : CHANNELS) { + channel.writeAndFlush(string); + } + } + + @Override + protected void initChannel(@NotNull NioSocketChannel channel) { + LOGGER.info("远程地址 {} 请求建立连接...", channel.remoteAddress().toString()); + + String hostAddress = channel.remoteAddress().getAddress().getHostAddress(); + if (ServerConfig.isEnableAcceptList() && !ServerConfig.getAcceptList().contains(hostAddress)) { + channel.disconnect(); + LOGGER.warn("{} 不在白名单列表中, 已断开连接!", hostAddress); + return; + } + + channel.pipeline() + .addLast(new LengthFieldPrepender(8)) + .addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 8, 0, 8)) + .addLast(new StringDecoder(StandardCharsets.UTF_8)) + .addLast(new StringEncoder(StandardCharsets.UTF_8)) + .addLast(new BallChannelHandler()); + + CHANNELS.add(channel); + } + +} diff --git a/hamster-ball-server/src/main/java/cn/hamster3/mc/plugin/ball/server/constant/ConstantObjects.java b/hamster-ball-server/src/main/java/cn/hamster3/mc/plugin/ball/server/constant/ConstantObjects.java new file mode 100644 index 0000000..0996cbb --- /dev/null +++ b/hamster-ball-server/src/main/java/cn/hamster3/mc/plugin/ball/server/constant/ConstantObjects.java @@ -0,0 +1,12 @@ +package cn.hamster3.mc.plugin.ball.server.constant; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +public interface ConstantObjects { + /** + * GSON 工具 + */ + Gson GSON = new GsonBuilder().create(); + +} diff --git a/hamster-ball-server/src/main/resources/config.yml b/hamster-ball-server/src/main/resources/config.yml new file mode 100644 index 0000000..c8bda4d --- /dev/null +++ b/hamster-ball-server/src/main/resources/config.yml @@ -0,0 +1,16 @@ +# 绑定网卡地址 +host: "0.0.0.0" +# 绑定端口 +port: 58888 + +# 线程池数量 +# 建议设置为全服最大玩家数 / 20 +# 不建议低于 5 +nio-thread: 10 + +# 是否启用IP 白名单 +enable-accept-list: true + +# 允许连接至服务的 ip 名单 +accept-list: + - "127.0.0.1" diff --git a/hamster-ball-server/src/main/resources/log4j2.xml b/hamster-ball-server/src/main/resources/log4j2.xml new file mode 100644 index 0000000..d521006 --- /dev/null +++ b/hamster-ball-server/src/main/resources/log4j2.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..457151b --- /dev/null +++ b/settings.gradle @@ -0,0 +1,6 @@ +rootProject.name = 'hamster-ball' +include 'hamster-ball-common' +include 'hamster-ball-bukkit' +include 'hamster-ball-bungeecord' +include 'hamster-ball-server' +