Initial Stargate plugin: Paper gate plugin + Velocity/Bungee cross-server bridges
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
.gradle/
|
||||||
|
build/
|
||||||
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
out/
|
||||||
|
.vscode/
|
||||||
|
*.log
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# Stargate
|
||||||
|
|
||||||
|
A network-aware Stargate portal plugin for PaperMC, with sign-based dialing and
|
||||||
|
cross-server travel over Velocity or BungeeCord.
|
||||||
|
|
||||||
|
## Modules
|
||||||
|
|
||||||
|
| Module | What it is |
|
||||||
|
|---|---|
|
||||||
|
| `stargate-common` | Shared models + JDBC storage (SQLite / MySQL) used only by the Paper plugin |
|
||||||
|
| `stargate-paper` | The actual gate plugin — install this on every backend server |
|
||||||
|
| `stargate-velocity` | Proxy companion for Velocity — install on the proxy only if using cross-server gates |
|
||||||
|
| `stargate-bungee` | Same, for BungeeCord/Waterfall |
|
||||||
|
|
||||||
|
Build everything with `./gradlew build`. Jars land in each module's `build/libs/`.
|
||||||
|
|
||||||
|
## Building a gate
|
||||||
|
|
||||||
|
1. Build a ring out of any block in `gate.frame-materials` (default: obsidian, gold
|
||||||
|
block, birch/oak planks — matches an obsidian+gold arch design). Leave the middle
|
||||||
|
hollow.
|
||||||
|
2. Embed a few blocks of `gate.chevron-unlit-material` (default black stained glass)
|
||||||
|
into the ring — these light up to `gate.chevron-lit-material` (default glowstone)
|
||||||
|
as the gate dials.
|
||||||
|
3. Place a sign on the outside of the frame with:
|
||||||
|
- Line 1: `[Stargate]`
|
||||||
|
- Line 2: network name (blank = default network)
|
||||||
|
- Line 3: gate name (blank = auto-generated)
|
||||||
|
- Line 4: `hidden` to keep it out of the cycle list, otherwise blank
|
||||||
|
|
||||||
|
The plugin flood-fills the frame from the block behind the sign, confirms it's a
|
||||||
|
fully enclosed ring, and registers the gate. If nothing happens, the ring isn't
|
||||||
|
sealed or none of its blocks match `frame-materials`.
|
||||||
|
|
||||||
|
## Using a gate
|
||||||
|
|
||||||
|
- **Right-click** the sign: cycles the destination shown on line 3 among the other
|
||||||
|
gates on the same network.
|
||||||
|
- **Left-click** the sign: dials the shown destination — chevrons light in sequence,
|
||||||
|
then the interior fills with `gate.iris-material` (default water). Walk into it to
|
||||||
|
teleport. It auto-closes after `dialing.open-seconds`.
|
||||||
|
|
||||||
|
## Multi-world
|
||||||
|
|
||||||
|
Gates are addressed by network name, not world — a gate on any loaded world can dial
|
||||||
|
any other gate on the same network regardless of world, exactly like same-server
|
||||||
|
cross-world travel in the original Stargate mod.
|
||||||
|
|
||||||
|
## Cross-server (Velocity / Bungee)
|
||||||
|
|
||||||
|
1. Set `storage.type: mysql` in every backend server's `config.yml` and point them at
|
||||||
|
the **same** database — this is how servers see each other's gates.
|
||||||
|
2. Give each backend a unique `server-id` in `config.yml` that matches its name in
|
||||||
|
the proxy config (`velocity.toml` / `config.yml` servers list).
|
||||||
|
3. Set `cross-server.enabled: true` on every backend.
|
||||||
|
4. Drop `stargate-velocity` (or `stargate-bungee`) into the proxy's plugin folder.
|
||||||
|
|
||||||
|
When a player dials a gate hosted on another backend, the Paper plugin asks the
|
||||||
|
proxy (over the `stargate:teleport` plugin channel) to connect the player to that
|
||||||
|
server; once they land, the proxy forwards a delivery message so the destination
|
||||||
|
server's Stargate instance teleports them to the gate's exit point.
|
||||||
|
|
||||||
|
## Permissions
|
||||||
|
|
||||||
|
- `stargate.use` (default: true) — dial/cycle gates
|
||||||
|
- `stargate.create` (default: op) — build new gates
|
||||||
|
- `stargate.destroy` (default: op) — break your own gates
|
||||||
|
- `stargate.admin` (default: op) — reload, break/manage any gate
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
- `/sg list [network]`
|
||||||
|
- `/sg destroy` (look at a gate's sign)
|
||||||
|
- `/sg reload`
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
plugins {
|
||||||
|
id("java")
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
group = "dev.skywalker3200.stargate"
|
||||||
|
version = "1.0.0"
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
maven("https://repo.papermc.io/repository/maven-public/")
|
||||||
|
maven("https://oss.sonatype.org/content/repositories/snapshots/")
|
||||||
|
maven("https://repo.opencollab.dev/main/") // Velocity/Bungee mirrors sometimes needed
|
||||||
|
maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
apply(plugin = "java")
|
||||||
|
|
||||||
|
java {
|
||||||
|
toolchain {
|
||||||
|
languageVersion.set(JavaLanguageVersion.of(21))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<JavaCompile> {
|
||||||
|
options.encoding = "UTF-8"
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
BIN
Binary file not shown.
+7
@@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
#!/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.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# 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/HEAD/platforms/jvm/plugins-application/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
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||||
|
' "$PWD" ) || exit
|
||||||
|
|
||||||
|
# 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
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
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
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 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" "$@"
|
||||||
Vendored
+94
@@ -0,0 +1,94 @@
|
|||||||
|
@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
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@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=.
|
||||||
|
@rem This is normally unused
|
||||||
|
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=-Dfile.encoding=UTF-8 "-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% equ 0 goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
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% equ 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!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
rootProject.name = "stargate"
|
||||||
|
|
||||||
|
include(":stargate-common")
|
||||||
|
include(":stargate-paper")
|
||||||
|
include(":stargate-velocity")
|
||||||
|
include(":stargate-bungee")
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.gradleup.shadow") version "8.3.5"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":stargate-common"))
|
||||||
|
compileOnly("net.md-5:bungeecord-api:1.21-R0.4")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.shadowJar {
|
||||||
|
archiveClassifier.set("")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.build {
|
||||||
|
dependsOn(tasks.shadowJar)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.processResources {
|
||||||
|
filesMatching("bungee.yml") {
|
||||||
|
expand("version" to project.version)
|
||||||
|
}
|
||||||
|
}
|
||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
package dev.skywalker3200.stargate.bungee;
|
||||||
|
|
||||||
|
import dev.skywalker3200.stargate.common.network.StargateChannel;
|
||||||
|
import net.md_5.bungee.api.ProxyServer;
|
||||||
|
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||||
|
import net.md_5.bungee.api.connection.Server;
|
||||||
|
import net.md_5.bungee.api.config.ServerInfo;
|
||||||
|
import net.md_5.bungee.api.event.PluginMessageEvent;
|
||||||
|
import net.md_5.bungee.api.event.ServerConnectedEvent;
|
||||||
|
import net.md_5.bungee.api.plugin.Listener;
|
||||||
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
import net.md_5.bungee.event.EventHandler;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/** BungeeCord counterpart of the Velocity bridge: same wire protocol, same job. */
|
||||||
|
public class StargateBungeePlugin extends Plugin implements Listener {
|
||||||
|
|
||||||
|
private final Map<UUID, String> pendingGateByPlayer = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
getProxy().registerChannel(StargateChannel.CHANNEL);
|
||||||
|
getProxy().getPluginManager().registerListener(this, this);
|
||||||
|
getLogger().info("Stargate Bungee bridge ready on channel " + StargateChannel.CHANNEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onPluginMessage(PluginMessageEvent event) {
|
||||||
|
if (!event.getTag().equals(StargateChannel.CHANNEL)) return;
|
||||||
|
event.setCancelled(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
DataInputStream in = new DataInputStream(new ByteArrayInputStream(event.getData()));
|
||||||
|
byte op = in.readByte();
|
||||||
|
if (op != StargateChannel.OP_TELEPORT_REQUEST) return;
|
||||||
|
|
||||||
|
UUID playerId = UUID.fromString(in.readUTF());
|
||||||
|
String targetServer = in.readUTF();
|
||||||
|
String gateId = in.readUTF();
|
||||||
|
|
||||||
|
ProxiedPlayer player = ProxyServer.getInstance().getPlayer(playerId);
|
||||||
|
ServerInfo target = ProxyServer.getInstance().getServerInfo(targetServer);
|
||||||
|
if (player == null || target == null) {
|
||||||
|
getLogger().warning("Stargate teleport request for unknown player/server (" + playerId + "/" + targetServer + ")");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingGateByPlayer.put(playerId, gateId);
|
||||||
|
player.connect(target);
|
||||||
|
} catch (Exception e) {
|
||||||
|
getLogger().warning("Failed to process stargate plugin message: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onServerConnected(ServerConnectedEvent event) {
|
||||||
|
ProxiedPlayer player = event.getPlayer();
|
||||||
|
String gateId = pendingGateByPlayer.remove(player.getUniqueId());
|
||||||
|
if (gateId == null) return;
|
||||||
|
|
||||||
|
Server server = event.getServer();
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
|
DataOutputStream out = new DataOutputStream(bytes);
|
||||||
|
out.writeByte(StargateChannel.OP_TELEPORT_DELIVER);
|
||||||
|
out.writeUTF(player.getUniqueId().toString());
|
||||||
|
out.writeUTF(gateId);
|
||||||
|
server.sendData(StargateChannel.CHANNEL, bytes.toByteArray());
|
||||||
|
} catch (Exception e) {
|
||||||
|
getLogger().warning("Failed to deliver stargate teleport: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
name: StargateBungee
|
||||||
|
main: dev.skywalker3200.stargate.bungee.StargateBungeePlugin
|
||||||
|
version: '${version}'
|
||||||
|
author: skywalker3200
|
||||||
|
description: Relays Stargate cross-server teleport requests for BungeeCord.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
dependencies {
|
||||||
|
compileOnly("com.zaxxer:HikariCP:5.1.0")
|
||||||
|
implementation("com.zaxxer:HikariCP:5.1.0")
|
||||||
|
compileOnly("org.xerial:sqlite-jdbc:3.46.1.3")
|
||||||
|
implementation("org.xerial:sqlite-jdbc:3.46.1.3")
|
||||||
|
compileOnly("com.mysql:mysql-connector-j:8.4.0")
|
||||||
|
implementation("com.mysql:mysql-connector-j:8.4.0")
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package dev.skywalker3200.stargate.common.model;
|
||||||
|
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single stargate: its physical location, the sign that controls it, and the network
|
||||||
|
* it belongs to. Not tied to Bukkit types so it can be shared with the storage layer only.
|
||||||
|
*/
|
||||||
|
public class Gate {
|
||||||
|
|
||||||
|
public enum Flag {
|
||||||
|
PUBLIC, // visible to everyone when cycling destinations
|
||||||
|
HIDDEN, // only reachable by exact name, not shown while cycling
|
||||||
|
FIXED // destination cannot be changed by right-click; always dials the configured target
|
||||||
|
}
|
||||||
|
|
||||||
|
private final UUID id;
|
||||||
|
private String name;
|
||||||
|
private String network;
|
||||||
|
private String serverId;
|
||||||
|
private String world;
|
||||||
|
private int exitX;
|
||||||
|
private int exitY;
|
||||||
|
private int exitZ;
|
||||||
|
private float exitYaw;
|
||||||
|
private int signX;
|
||||||
|
private int signY;
|
||||||
|
private int signZ;
|
||||||
|
private String signWorld;
|
||||||
|
private String facing;
|
||||||
|
private UUID owner;
|
||||||
|
private final Set<Flag> flags;
|
||||||
|
private String fixedDestination;
|
||||||
|
|
||||||
|
public Gate(UUID id, String name, String network, String serverId, String world,
|
||||||
|
int exitX, int exitY, int exitZ, float exitYaw,
|
||||||
|
int signX, int signY, int signZ, String signWorld, String facing,
|
||||||
|
UUID owner, Set<Flag> flags, String fixedDestination) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
this.network = network;
|
||||||
|
this.serverId = serverId;
|
||||||
|
this.world = world;
|
||||||
|
this.exitX = exitX;
|
||||||
|
this.exitY = exitY;
|
||||||
|
this.exitZ = exitZ;
|
||||||
|
this.exitYaw = exitYaw;
|
||||||
|
this.signX = signX;
|
||||||
|
this.signY = signY;
|
||||||
|
this.signZ = signZ;
|
||||||
|
this.signWorld = signWorld;
|
||||||
|
this.facing = facing;
|
||||||
|
this.owner = owner;
|
||||||
|
this.flags = flags == null ? EnumSet.noneOf(Flag.class) : flags;
|
||||||
|
this.fixedDestination = fixedDestination;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getId() { return id; }
|
||||||
|
public String getName() { return name; }
|
||||||
|
public void setName(String name) { this.name = name; }
|
||||||
|
public String getNetwork() { return network; }
|
||||||
|
public void setNetwork(String network) { this.network = network; }
|
||||||
|
public String getServerId() { return serverId; }
|
||||||
|
public void setServerId(String serverId) { this.serverId = serverId; }
|
||||||
|
public String getWorld() { return world; }
|
||||||
|
public void setWorld(String world) { this.world = world; }
|
||||||
|
public int getExitX() { return exitX; }
|
||||||
|
public int getExitY() { return exitY; }
|
||||||
|
public int getExitZ() { return exitZ; }
|
||||||
|
public float getExitYaw() { return exitYaw; }
|
||||||
|
public void setExit(int x, int y, int z, float yaw) { this.exitX = x; this.exitY = y; this.exitZ = z; this.exitYaw = yaw; }
|
||||||
|
public int getSignX() { return signX; }
|
||||||
|
public int getSignY() { return signY; }
|
||||||
|
public int getSignZ() { return signZ; }
|
||||||
|
public String getSignWorld() { return signWorld; }
|
||||||
|
public String getFacing() { return facing; }
|
||||||
|
public UUID getOwner() { return owner; }
|
||||||
|
public void setOwner(UUID owner) { this.owner = owner; }
|
||||||
|
public Set<Flag> getFlags() { return flags; }
|
||||||
|
public boolean isPublic() { return flags.contains(Flag.PUBLIC); }
|
||||||
|
public boolean isHidden() { return flags.contains(Flag.HIDDEN); }
|
||||||
|
public boolean isFixed() { return flags.contains(Flag.FIXED); }
|
||||||
|
public String getFixedDestination() { return fixedDestination; }
|
||||||
|
public void setFixedDestination(String fixedDestination) { this.fixedDestination = fixedDestination; }
|
||||||
|
|
||||||
|
/** Fully-qualified identity used for cross-server lookups: server/network/name */
|
||||||
|
public String qualifiedName() {
|
||||||
|
return serverId + "/" + network + "/" + name;
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package dev.skywalker3200.stargate.common.network;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared plugin-messaging channel identifiers used between the Paper plugin and the
|
||||||
|
* Bungee/Velocity proxy companions to move a player to the backend server that hosts
|
||||||
|
* their destination gate.
|
||||||
|
*/
|
||||||
|
public final class StargateChannel {
|
||||||
|
|
||||||
|
private StargateChannel() {}
|
||||||
|
|
||||||
|
/** Modern namespaced channel (Paper/Velocity require this format). */
|
||||||
|
public static final String CHANNEL = "stargate:teleport";
|
||||||
|
|
||||||
|
/** Sub-channel byte sent first in the payload. */
|
||||||
|
public static final byte OP_TELEPORT_REQUEST = 1; // backend -> proxy: move this player to <server>, remember pending warp
|
||||||
|
public static final byte OP_TELEPORT_DELIVER = 2; // proxy -> new backend: this player just arrived, warp them to <gate id>
|
||||||
|
}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
package dev.skywalker3200.stargate.common.storage;
|
||||||
|
|
||||||
|
import dev.skywalker3200.stargate.common.model.Gate;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persistence for gates. Implementations back onto SQLite (single server) or MySQL
|
||||||
|
* (shared across a Bungee/Velocity network so every backend server sees the same gates).
|
||||||
|
*/
|
||||||
|
public interface GateStorage {
|
||||||
|
|
||||||
|
void init() throws Exception;
|
||||||
|
|
||||||
|
void close();
|
||||||
|
|
||||||
|
void saveGate(Gate gate);
|
||||||
|
|
||||||
|
void deleteGate(UUID id);
|
||||||
|
|
||||||
|
List<Gate> loadAll();
|
||||||
|
|
||||||
|
/** Reload gates belonging to other servers (call periodically when networked). */
|
||||||
|
List<Gate> loadAllForNetwork(String network);
|
||||||
|
}
|
||||||
+209
@@ -0,0 +1,209 @@
|
|||||||
|
package dev.skywalker3200.stargate.common.storage;
|
||||||
|
|
||||||
|
import com.zaxxer.hikari.HikariConfig;
|
||||||
|
import com.zaxxer.hikari.HikariDataSource;
|
||||||
|
import dev.skywalker3200.stargate.common.model.Gate;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JDBC-backed implementation of {@link GateStorage}. Works for both SQLite (file-based,
|
||||||
|
* one server) and MySQL (shared, used when networking multiple backend servers together
|
||||||
|
* behind Bungee/Velocity so they all see the same gate table).
|
||||||
|
*/
|
||||||
|
public class SqlGateStorage implements GateStorage {
|
||||||
|
|
||||||
|
public enum Driver { SQLITE, MYSQL }
|
||||||
|
|
||||||
|
private final Driver driver;
|
||||||
|
private final String jdbcUrl;
|
||||||
|
private final String user;
|
||||||
|
private final String pass;
|
||||||
|
private final String tablePrefix;
|
||||||
|
private final Logger logger;
|
||||||
|
private HikariDataSource dataSource;
|
||||||
|
|
||||||
|
public SqlGateStorage(Driver driver, String jdbcUrl, String user, String pass, String tablePrefix, Logger logger) {
|
||||||
|
this.driver = driver;
|
||||||
|
this.jdbcUrl = jdbcUrl;
|
||||||
|
this.user = user;
|
||||||
|
this.pass = pass;
|
||||||
|
this.tablePrefix = tablePrefix == null ? "stargate_" : tablePrefix;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init() throws Exception {
|
||||||
|
HikariConfig config = new HikariConfig();
|
||||||
|
config.setJdbcUrl(jdbcUrl);
|
||||||
|
if (driver == Driver.MYSQL) {
|
||||||
|
config.setUsername(user);
|
||||||
|
config.setPassword(pass);
|
||||||
|
config.setMaximumPoolSize(8);
|
||||||
|
config.addDataSourceProperty("cachePrepStmts", "true");
|
||||||
|
config.addDataSourceProperty("prepStmtCacheSize", "250");
|
||||||
|
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
|
||||||
|
} else {
|
||||||
|
config.setMaximumPoolSize(1); // SQLite is single-writer
|
||||||
|
}
|
||||||
|
this.dataSource = new HikariDataSource(config);
|
||||||
|
|
||||||
|
try (Connection c = dataSource.getConnection(); Statement s = c.createStatement()) {
|
||||||
|
s.executeUpdate("CREATE TABLE IF NOT EXISTS " + tablePrefix + "gates (" +
|
||||||
|
"id VARCHAR(36) PRIMARY KEY," +
|
||||||
|
"name VARCHAR(64) NOT NULL," +
|
||||||
|
"network VARCHAR(64) NOT NULL," +
|
||||||
|
"server_id VARCHAR(64) NOT NULL," +
|
||||||
|
"world VARCHAR(64) NOT NULL," +
|
||||||
|
"exit_x INTEGER NOT NULL," +
|
||||||
|
"exit_y INTEGER NOT NULL," +
|
||||||
|
"exit_z INTEGER NOT NULL," +
|
||||||
|
"exit_yaw REAL NOT NULL," +
|
||||||
|
"sign_x INTEGER NOT NULL," +
|
||||||
|
"sign_y INTEGER NOT NULL," +
|
||||||
|
"sign_z INTEGER NOT NULL," +
|
||||||
|
"sign_world VARCHAR(64) NOT NULL," +
|
||||||
|
"facing VARCHAR(16)," +
|
||||||
|
"owner VARCHAR(36)," +
|
||||||
|
"flags VARCHAR(128)," +
|
||||||
|
"fixed_destination VARCHAR(64)" +
|
||||||
|
")");
|
||||||
|
s.executeUpdate("CREATE INDEX IF NOT EXISTS idx_" + tablePrefix + "network ON " + tablePrefix + "gates(network)");
|
||||||
|
}
|
||||||
|
logger.info("[Stargate] Storage initialised (" + driver + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
if (dataSource != null) dataSource.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveGate(Gate gate) {
|
||||||
|
String sql = "REPLACE INTO " + tablePrefix + "gates " +
|
||||||
|
"(id,name,network,server_id,world,exit_x,exit_y,exit_z,exit_yaw,sign_x,sign_y,sign_z,sign_world,facing,owner,flags,fixed_destination) " +
|
||||||
|
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
|
||||||
|
if (driver == Driver.MYSQL) {
|
||||||
|
sql = "INSERT INTO " + tablePrefix + "gates " +
|
||||||
|
"(id,name,network,server_id,world,exit_x,exit_y,exit_z,exit_yaw,sign_x,sign_y,sign_z,sign_world,facing,owner,flags,fixed_destination) " +
|
||||||
|
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE " +
|
||||||
|
"name=VALUES(name),network=VALUES(network),server_id=VALUES(server_id),world=VALUES(world)," +
|
||||||
|
"exit_x=VALUES(exit_x),exit_y=VALUES(exit_y),exit_z=VALUES(exit_z),exit_yaw=VALUES(exit_yaw)," +
|
||||||
|
"sign_x=VALUES(sign_x),sign_y=VALUES(sign_y),sign_z=VALUES(sign_z),sign_world=VALUES(sign_world)," +
|
||||||
|
"facing=VALUES(facing),owner=VALUES(owner),flags=VALUES(flags),fixed_destination=VALUES(fixed_destination)";
|
||||||
|
}
|
||||||
|
try (Connection c = dataSource.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||||
|
ps.setString(1, gate.getId().toString());
|
||||||
|
ps.setString(2, gate.getName());
|
||||||
|
ps.setString(3, gate.getNetwork());
|
||||||
|
ps.setString(4, gate.getServerId());
|
||||||
|
ps.setString(5, gate.getWorld());
|
||||||
|
ps.setInt(6, gate.getExitX());
|
||||||
|
ps.setInt(7, gate.getExitY());
|
||||||
|
ps.setInt(8, gate.getExitZ());
|
||||||
|
ps.setFloat(9, gate.getExitYaw());
|
||||||
|
ps.setInt(10, gate.getSignX());
|
||||||
|
ps.setInt(11, gate.getSignY());
|
||||||
|
ps.setInt(12, gate.getSignZ());
|
||||||
|
ps.setString(13, gate.getSignWorld());
|
||||||
|
ps.setString(14, gate.getFacing());
|
||||||
|
ps.setString(15, gate.getOwner() == null ? null : gate.getOwner().toString());
|
||||||
|
ps.setString(16, serializeFlags(gate.getFlags()));
|
||||||
|
ps.setString(17, gate.getFixedDestination());
|
||||||
|
ps.executeUpdate();
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.severe("[Stargate] Failed to save gate " + gate.getName() + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteGate(UUID id) {
|
||||||
|
try (Connection c = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement("DELETE FROM " + tablePrefix + "gates WHERE id = ?")) {
|
||||||
|
ps.setString(1, id.toString());
|
||||||
|
ps.executeUpdate();
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.severe("[Stargate] Failed to delete gate " + id + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Gate> loadAll() {
|
||||||
|
List<Gate> gates = new ArrayList<>();
|
||||||
|
try (Connection c = dataSource.getConnection();
|
||||||
|
Statement s = c.createStatement();
|
||||||
|
ResultSet rs = s.executeQuery("SELECT * FROM " + tablePrefix + "gates")) {
|
||||||
|
while (rs.next()) {
|
||||||
|
gates.add(fromRow(rs));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.severe("[Stargate] Failed to load gates: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return gates;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Gate> loadAllForNetwork(String network) {
|
||||||
|
List<Gate> gates = new ArrayList<>();
|
||||||
|
try (Connection c = dataSource.getConnection();
|
||||||
|
PreparedStatement ps = c.prepareStatement("SELECT * FROM " + tablePrefix + "gates WHERE network = ?")) {
|
||||||
|
ps.setString(1, network);
|
||||||
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
gates.add(fromRow(rs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.severe("[Stargate] Failed to load network " + network + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
return gates;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Gate fromRow(ResultSet rs) throws Exception {
|
||||||
|
String ownerStr = rs.getString("owner");
|
||||||
|
String fixedDest = rs.getString("fixed_destination");
|
||||||
|
return new Gate(
|
||||||
|
UUID.fromString(rs.getString("id")),
|
||||||
|
rs.getString("name"),
|
||||||
|
rs.getString("network"),
|
||||||
|
rs.getString("server_id"),
|
||||||
|
rs.getString("world"),
|
||||||
|
rs.getInt("exit_x"), rs.getInt("exit_y"), rs.getInt("exit_z"), rs.getFloat("exit_yaw"),
|
||||||
|
rs.getInt("sign_x"), rs.getInt("sign_y"), rs.getInt("sign_z"), rs.getString("sign_world"),
|
||||||
|
rs.getString("facing"),
|
||||||
|
ownerStr == null ? null : UUID.fromString(ownerStr),
|
||||||
|
deserializeFlags(rs.getString("flags")),
|
||||||
|
fixedDest
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String serializeFlags(Set<Gate.Flag> flags) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (Gate.Flag f : flags) {
|
||||||
|
if (sb.length() > 0) sb.append(',');
|
||||||
|
sb.append(f.name());
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<Gate.Flag> deserializeFlags(String s) {
|
||||||
|
Set<Gate.Flag> flags = EnumSet.noneOf(Gate.Flag.class);
|
||||||
|
if (s == null || s.isEmpty()) return flags;
|
||||||
|
for (String part : s.split(",")) {
|
||||||
|
try {
|
||||||
|
flags.add(Gate.Flag.valueOf(part.trim()));
|
||||||
|
} catch (IllegalArgumentException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.gradleup.shadow") version "8.3.5"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":stargate-common"))
|
||||||
|
compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT")
|
||||||
|
implementation("com.zaxxer:HikariCP:5.1.0")
|
||||||
|
implementation("org.xerial:sqlite-jdbc:3.46.1.3")
|
||||||
|
implementation("com.mysql:mysql-connector-j:8.4.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.shadowJar {
|
||||||
|
archiveClassifier.set("")
|
||||||
|
relocate("com.zaxxer.hikari", "dev.skywalker3200.stargate.libs.hikari")
|
||||||
|
relocate("org.sqlite", "dev.skywalker3200.stargate.libs.sqlite")
|
||||||
|
relocate("com.mysql", "dev.skywalker3200.stargate.libs.mysql")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.build {
|
||||||
|
dependsOn(tasks.shadowJar)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.processResources {
|
||||||
|
filesMatching("plugin.yml") {
|
||||||
|
expand("version" to project.version)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package dev.skywalker3200.stargate.paper;
|
||||||
|
|
||||||
|
import dev.skywalker3200.stargate.common.storage.GateStorage;
|
||||||
|
import dev.skywalker3200.stargate.common.storage.SqlGateStorage;
|
||||||
|
import dev.skywalker3200.stargate.paper.command.StargateCommand;
|
||||||
|
import dev.skywalker3200.stargate.paper.gate.GateManager;
|
||||||
|
import dev.skywalker3200.stargate.paper.listener.SignInteractListener;
|
||||||
|
import dev.skywalker3200.stargate.paper.listener.SignCreateListener;
|
||||||
|
import dev.skywalker3200.stargate.paper.listener.StructureProtectListener;
|
||||||
|
import dev.skywalker3200.stargate.paper.network.CrossServerBridge;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
public class StargatePlugin extends JavaPlugin {
|
||||||
|
|
||||||
|
private GateStorage storage;
|
||||||
|
private GateManager gateManager;
|
||||||
|
private CrossServerBridge crossServerBridge;
|
||||||
|
private String serverId;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable() {
|
||||||
|
saveDefaultConfig();
|
||||||
|
this.serverId = getConfig().getString("server-id", "server1");
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.storage = buildStorage();
|
||||||
|
this.storage.init();
|
||||||
|
} catch (Exception e) {
|
||||||
|
getLogger().severe("Failed to initialise storage, disabling Stargate: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
getServer().getPluginManager().disablePlugin(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.gateManager = new GateManager(this, storage);
|
||||||
|
this.gateManager.loadAll();
|
||||||
|
|
||||||
|
boolean crossServer = getConfig().getBoolean("cross-server.enabled", false);
|
||||||
|
this.crossServerBridge = new CrossServerBridge(this, gateManager);
|
||||||
|
if (crossServer) {
|
||||||
|
this.crossServerBridge.register();
|
||||||
|
}
|
||||||
|
|
||||||
|
getServer().getPluginManager().registerEvents(new SignCreateListener(this, gateManager), this);
|
||||||
|
getServer().getPluginManager().registerEvents(new SignInteractListener(this, gateManager, crossServerBridge), this);
|
||||||
|
getServer().getPluginManager().registerEvents(new StructureProtectListener(gateManager), this);
|
||||||
|
getServer().getPluginManager().registerEvents(new dev.skywalker3200.stargate.paper.listener.GateTeleportListener(this, gateManager, crossServerBridge), this);
|
||||||
|
|
||||||
|
StargateCommand command = new StargateCommand(this, gateManager);
|
||||||
|
getCommand("stargate").setExecutor(command);
|
||||||
|
getCommand("stargate").setTabCompleter(command);
|
||||||
|
|
||||||
|
getLogger().info("Stargate enabled. server-id=" + serverId + " cross-server=" + crossServer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable() {
|
||||||
|
if (gateManager != null) {
|
||||||
|
gateManager.closeAllGates();
|
||||||
|
}
|
||||||
|
if (storage != null) {
|
||||||
|
storage.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private GateStorage buildStorage() {
|
||||||
|
String type = getConfig().getString("storage.type", "sqlite").toLowerCase();
|
||||||
|
if (type.equals("mysql")) {
|
||||||
|
String host = getConfig().getString("storage.mysql.host", "localhost");
|
||||||
|
int port = getConfig().getInt("storage.mysql.port", 3306);
|
||||||
|
String db = getConfig().getString("storage.mysql.database", "stargate");
|
||||||
|
String user = getConfig().getString("storage.mysql.username", "stargate");
|
||||||
|
String pass = getConfig().getString("storage.mysql.password", "");
|
||||||
|
String prefix = getConfig().getString("storage.mysql.table-prefix", "stargate_");
|
||||||
|
String url = "jdbc:mysql://" + host + ":" + port + "/" + db + "?useSSL=false&autoReconnect=true";
|
||||||
|
return new SqlGateStorage(SqlGateStorage.Driver.MYSQL, url, user, pass, prefix, getLogger());
|
||||||
|
}
|
||||||
|
File dataFile = new File(getDataFolder(), "gates.db");
|
||||||
|
String url = "jdbc:sqlite:" + dataFile.getAbsolutePath();
|
||||||
|
return new SqlGateStorage(SqlGateStorage.Driver.SQLITE, url, null, null, "stargate_", getLogger());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getServerId() {
|
||||||
|
return serverId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GateManager getGateManager() {
|
||||||
|
return gateManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CrossServerBridge getCrossServerBridge() {
|
||||||
|
return crossServerBridge;
|
||||||
|
}
|
||||||
|
}
|
||||||
+91
@@ -0,0 +1,91 @@
|
|||||||
|
package dev.skywalker3200.stargate.paper.command;
|
||||||
|
|
||||||
|
import dev.skywalker3200.stargate.paper.StargatePlugin;
|
||||||
|
import dev.skywalker3200.stargate.paper.gate.GateManager;
|
||||||
|
import dev.skywalker3200.stargate.paper.gate.RuntimeGate;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandExecutor;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.command.TabCompleter;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class StargateCommand implements CommandExecutor, TabCompleter {
|
||||||
|
|
||||||
|
private final StargatePlugin plugin;
|
||||||
|
private final GateManager gateManager;
|
||||||
|
|
||||||
|
public StargateCommand(StargatePlugin plugin, GateManager gateManager) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.gateManager = gateManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||||
|
if (args.length == 0) {
|
||||||
|
sender.sendMessage(Component.text("Usage: /sg <list|destroy|reload>", NamedTextColor.YELLOW));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (args[0].toLowerCase()) {
|
||||||
|
case "reload" -> {
|
||||||
|
if (!sender.hasPermission("stargate.admin")) {
|
||||||
|
sender.sendMessage(Component.text("No permission.", NamedTextColor.RED));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
plugin.reloadConfig();
|
||||||
|
gateManager.reloadConfig();
|
||||||
|
gateManager.loadAll();
|
||||||
|
sender.sendMessage(Component.text("Stargate reloaded.", NamedTextColor.GREEN));
|
||||||
|
}
|
||||||
|
case "list" -> {
|
||||||
|
String network = args.length > 1 ? args[1] : gateManager.getConfig().defaultNetwork;
|
||||||
|
List<RuntimeGate> gates = gateManager.getNetwork(network);
|
||||||
|
sender.sendMessage(Component.text("Network '" + network + "' (" + gates.size() + " gate(s)):", NamedTextColor.AQUA));
|
||||||
|
for (RuntimeGate rg : gates) {
|
||||||
|
sender.sendMessage(Component.text(" - " + rg.getGate().getName() + " @ " + rg.getGate().getServerId()
|
||||||
|
+ "/" + rg.getGate().getWorld(), NamedTextColor.GRAY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "destroy" -> {
|
||||||
|
if (!(sender instanceof Player player)) {
|
||||||
|
sender.sendMessage(Component.text("Players only.", NamedTextColor.RED));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Block target = player.getTargetBlockExact(6);
|
||||||
|
RuntimeGate rg = target == null ? null : gateManager.getBySign(target);
|
||||||
|
if (rg == null) {
|
||||||
|
player.sendMessage(Component.text("Look at a stargate sign to destroy it.", NamedTextColor.RED));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
boolean allowed = player.hasPermission("stargate.admin")
|
||||||
|
|| (rg.getGate().getOwner() != null && rg.getGate().getOwner().equals(player.getUniqueId()) && player.hasPermission("stargate.destroy"));
|
||||||
|
if (!allowed) {
|
||||||
|
player.sendMessage(Component.text("You can't destroy this stargate.", NamedTextColor.RED));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
gateManager.destroyGate(rg);
|
||||||
|
player.sendMessage(Component.text("Destroyed '" + rg.getGate().getName() + "'.", NamedTextColor.YELLOW));
|
||||||
|
}
|
||||||
|
default -> sender.sendMessage(Component.text("Unknown subcommand.", NamedTextColor.RED));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
|
||||||
|
if (args.length == 1) {
|
||||||
|
return List.of("list", "destroy", "reload").stream()
|
||||||
|
.filter(s -> s.startsWith(args[0].toLowerCase()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package dev.skywalker3200.stargate.paper.gate;
|
||||||
|
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/** Typed view over the `gate:` / `dialing:` / `network:` sections of config.yml. */
|
||||||
|
public class GateConfig {
|
||||||
|
|
||||||
|
public final Set<Material> frameMaterials;
|
||||||
|
public final Material chevronUnlit;
|
||||||
|
public final Material chevronLit;
|
||||||
|
public final Material irisMaterial;
|
||||||
|
public final int maxFrameBlocks;
|
||||||
|
public final int maxIrisBlocks;
|
||||||
|
public final int minFrameBlocks;
|
||||||
|
public final int openSeconds;
|
||||||
|
public final int chevronTickDelay;
|
||||||
|
public final boolean playSounds;
|
||||||
|
public final String defaultNetwork;
|
||||||
|
|
||||||
|
public GateConfig(FileConfiguration cfg, Logger logger) {
|
||||||
|
Set<Material> materials = new HashSet<>();
|
||||||
|
for (String s : cfg.getStringList("gate.frame-materials")) {
|
||||||
|
Material m = Material.matchMaterial(s);
|
||||||
|
if (m != null) materials.add(m);
|
||||||
|
else logger.warning("[Stargate] Unknown frame material in config: " + s);
|
||||||
|
}
|
||||||
|
if (materials.isEmpty()) materials.add(Material.OBSIDIAN);
|
||||||
|
this.frameMaterials = materials;
|
||||||
|
|
||||||
|
this.chevronUnlit = matOr(cfg.getString("gate.chevron-unlit-material"), Material.BLACK_STAINED_GLASS, logger);
|
||||||
|
this.chevronLit = matOr(cfg.getString("gate.chevron-lit-material"), Material.GLOWSTONE, logger);
|
||||||
|
this.irisMaterial = matOr(cfg.getString("gate.iris-material"), Material.WATER, logger);
|
||||||
|
this.maxFrameBlocks = cfg.getInt("gate.max-frame-blocks", 300);
|
||||||
|
this.maxIrisBlocks = cfg.getInt("gate.max-iris-blocks", 200);
|
||||||
|
this.minFrameBlocks = cfg.getInt("gate.min-frame-blocks", 8);
|
||||||
|
this.openSeconds = cfg.getInt("dialing.open-seconds", 10);
|
||||||
|
this.chevronTickDelay = cfg.getInt("dialing.chevron-tick-delay", 4);
|
||||||
|
this.playSounds = cfg.getBoolean("dialing.play-sounds", true);
|
||||||
|
this.defaultNetwork = cfg.getString("network.default-network", "main");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Material matOr(String s, Material fallback, Logger logger) {
|
||||||
|
if (s == null) return fallback;
|
||||||
|
Material m = Material.matchMaterial(s);
|
||||||
|
if (m == null) {
|
||||||
|
logger.warning("[Stargate] Unknown material in config: " + s + ", using " + fallback);
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,272 @@
|
|||||||
|
package dev.skywalker3200.stargate.paper.gate;
|
||||||
|
|
||||||
|
import dev.skywalker3200.stargate.common.model.Gate;
|
||||||
|
import dev.skywalker3200.stargate.common.storage.GateStorage;
|
||||||
|
import dev.skywalker3200.stargate.paper.StargatePlugin;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.Sound;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.block.data.Levelled;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/** Owns every gate known to this server: creation, lookup, dialing/animation, and teardown. */
|
||||||
|
public class GateManager {
|
||||||
|
|
||||||
|
private final StargatePlugin plugin;
|
||||||
|
private final GateStorage storage;
|
||||||
|
private GateConfig config;
|
||||||
|
private GateStructureScanner scanner;
|
||||||
|
|
||||||
|
private final Map<UUID, RuntimeGate> gatesById = new HashMap<>();
|
||||||
|
private final Map<String, RuntimeGate> gatesBySignBlock = new HashMap<>(); // "world,x,y,z" -> gate
|
||||||
|
|
||||||
|
public GateManager(StargatePlugin plugin, GateStorage storage) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.storage = storage;
|
||||||
|
reloadConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reloadConfig() {
|
||||||
|
this.config = new GateConfig(plugin.getConfig(), plugin.getLogger());
|
||||||
|
this.scanner = new GateStructureScanner(config.frameMaterials, config.chevronUnlit, config.chevronLit,
|
||||||
|
config.maxFrameBlocks, config.maxIrisBlocks, config.minFrameBlocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GateConfig getConfig() { return config; }
|
||||||
|
|
||||||
|
public void loadAll() {
|
||||||
|
gatesById.clear();
|
||||||
|
gatesBySignBlock.clear();
|
||||||
|
for (Gate gate : storage.loadAll()) {
|
||||||
|
GateStructure structure = null;
|
||||||
|
if (gate.getServerId().equals(plugin.getServerId())) {
|
||||||
|
World world = Bukkit.getWorld(gate.getSignWorld());
|
||||||
|
if (world != null) {
|
||||||
|
Block signBlock = world.getBlockAt(gate.getSignX(), gate.getSignY(), gate.getSignZ());
|
||||||
|
Block attached = attachedFrameBlock(signBlock);
|
||||||
|
if (attached != null) {
|
||||||
|
structure = scanner.scan(attached);
|
||||||
|
}
|
||||||
|
if (structure == null) {
|
||||||
|
plugin.getLogger().warning("[Stargate] Could not re-scan structure for gate '" + gate.getName() + "' - it may have been damaged.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RuntimeGate rg = new RuntimeGate(gate, structure);
|
||||||
|
gatesById.put(gate.getId(), rg);
|
||||||
|
if (gate.getServerId().equals(plugin.getServerId())) {
|
||||||
|
gatesBySignBlock.put(signKey(gate.getSignWorld(), gate.getSignX(), gate.getSignY(), gate.getSignZ()), rg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plugin.getLogger().info("[Stargate] Loaded " + gatesById.size() + " gate(s), " + gatesBySignBlock.size() + " local.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Given the sign block, finds the neighbouring block that belongs to the frame (the wall it's mounted on, or the block below for a sign post). */
|
||||||
|
public Block attachedFrameBlock(Block signBlock) {
|
||||||
|
org.bukkit.block.BlockState state = signBlock.getState();
|
||||||
|
if (state.getBlockData() instanceof org.bukkit.block.data.type.WallSign wallSign) {
|
||||||
|
return signBlock.getRelative(wallSign.getFacing().getOppositeFace());
|
||||||
|
}
|
||||||
|
// sign post or other: just probe all neighbours, scanner will validate
|
||||||
|
return signBlock.getRelative(org.bukkit.block.BlockFace.DOWN);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String signKey(String world, int x, int y, int z) {
|
||||||
|
return world + "," + x + "," + y + "," + z;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RuntimeGate getBySign(Block signBlock) {
|
||||||
|
return gatesBySignBlock.get(signKey(signBlock.getWorld().getName(), signBlock.getX(), signBlock.getY(), signBlock.getZ()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public RuntimeGate getById(UUID id) { return gatesById.get(id); }
|
||||||
|
|
||||||
|
public List<RuntimeGate> getNetwork(String network) {
|
||||||
|
return gatesById.values().stream()
|
||||||
|
.filter(g -> g.getGate().getNetwork().equalsIgnoreCase(network))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<RuntimeGate> all() { return new ArrayList<>(gatesById.values()); }
|
||||||
|
|
||||||
|
/** Other dialable gates in the same network, excluding this one and hidden ones (owner can still see their own hidden gates). */
|
||||||
|
public List<RuntimeGate> destinationsFor(RuntimeGate from) {
|
||||||
|
return getNetwork(from.getGate().getNetwork()).stream()
|
||||||
|
.filter(g -> !g.getGate().getId().equals(from.getGate().getId()))
|
||||||
|
.filter(g -> !g.getGate().isHidden())
|
||||||
|
.sorted((a, b) -> a.getGate().getName().compareToIgnoreCase(b.getGate().getName()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public RuntimeGate createGate(Block signBlock, String network, String name, UUID owner, EnumSet<Gate.Flag> flags) {
|
||||||
|
Block attached = attachedFrameBlock(signBlock);
|
||||||
|
if (attached == null) return null;
|
||||||
|
GateStructure structure = scanner.scan(attached);
|
||||||
|
if (structure == null) return null;
|
||||||
|
|
||||||
|
Location exit = computeExitLocation(structure, signBlock);
|
||||||
|
Gate gate = new Gate(UUID.randomUUID(), name, network, plugin.getServerId(),
|
||||||
|
exit.getWorld().getName(), exit.getBlockX(), exit.getBlockY(), exit.getBlockZ(), exit.getYaw(),
|
||||||
|
signBlock.getX(), signBlock.getY(), signBlock.getZ(), signBlock.getWorld().getName(),
|
||||||
|
signBlockFacing(signBlock), owner, flags, null);
|
||||||
|
|
||||||
|
RuntimeGate rg = new RuntimeGate(gate, structure);
|
||||||
|
gatesById.put(gate.getId(), rg);
|
||||||
|
gatesBySignBlock.put(signKey(gate.getSignWorld(), gate.getSignX(), gate.getSignY(), gate.getSignZ()), rg);
|
||||||
|
storage.saveGate(gate);
|
||||||
|
return rg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void destroyGate(RuntimeGate rg) {
|
||||||
|
closeGate(rg);
|
||||||
|
gatesById.remove(rg.getGate().getId());
|
||||||
|
gatesBySignBlock.remove(signKey(rg.getGate().getSignWorld(), rg.getGate().getSignX(), rg.getGate().getSignY(), rg.getGate().getSignZ()));
|
||||||
|
storage.deleteGate(rg.getGate().getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveGate(RuntimeGate rg) {
|
||||||
|
storage.saveGate(rg.getGate());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Location computeExitLocation(GateStructure structure, Block signBlock) {
|
||||||
|
List<Block> iris = structure.getIris();
|
||||||
|
World world = signBlock.getWorld();
|
||||||
|
if (iris.isEmpty()) {
|
||||||
|
return signBlock.getLocation().add(0, 0, 0);
|
||||||
|
}
|
||||||
|
long sumX = 0, sumZ = 0;
|
||||||
|
int minY = Integer.MAX_VALUE;
|
||||||
|
for (Block b : iris) {
|
||||||
|
sumX += b.getX();
|
||||||
|
sumZ += b.getZ();
|
||||||
|
minY = Math.min(minY, b.getY());
|
||||||
|
}
|
||||||
|
double avgX = (double) sumX / iris.size() + 0.5;
|
||||||
|
double avgZ = (double) sumZ / iris.size() + 0.5;
|
||||||
|
float yaw = 0f;
|
||||||
|
org.bukkit.block.BlockState state = signBlock.getState();
|
||||||
|
if (state.getBlockData() instanceof org.bukkit.block.data.type.WallSign wallSign) {
|
||||||
|
// face away from the wall the sign is mounted on, into the room the sign faces
|
||||||
|
yaw = faceToYaw(wallSign.getFacing());
|
||||||
|
}
|
||||||
|
return new Location(world, avgX, minY + 1, avgZ, yaw, 0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private float faceToYaw(org.bukkit.block.BlockFace face) {
|
||||||
|
return switch (face) {
|
||||||
|
case NORTH -> 180f;
|
||||||
|
case SOUTH -> 0f;
|
||||||
|
case EAST -> -90f;
|
||||||
|
case WEST -> 90f;
|
||||||
|
default -> 0f;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String signBlockFacing(Block signBlock) {
|
||||||
|
org.bukkit.block.BlockState state = signBlock.getState();
|
||||||
|
if (state.getBlockData() instanceof org.bukkit.block.data.type.WallSign wallSign) {
|
||||||
|
return wallSign.getFacing().name();
|
||||||
|
}
|
||||||
|
return "SELF";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Dialing ----
|
||||||
|
|
||||||
|
/** Starts the chevron-lighting animation, then opens the gate and connects it to the destination. */
|
||||||
|
public void dial(RuntimeGate from, RuntimeGate to, Player initiator) {
|
||||||
|
if (from.isOpen()) closeGate(from);
|
||||||
|
if (to.isOpen()) closeGate(to);
|
||||||
|
|
||||||
|
List<Block> chevrons = from.getStructure() != null ? from.getStructure().getChevrons() : List.of();
|
||||||
|
int delay = Math.max(1, config.chevronTickDelay);
|
||||||
|
|
||||||
|
Runnable finish = () -> {
|
||||||
|
openGate(from, to);
|
||||||
|
openGate(to, from);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (chevrons.isEmpty()) {
|
||||||
|
finish.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int[] i = {0};
|
||||||
|
final org.bukkit.scheduler.BukkitTask[] taskHolder = new org.bukkit.scheduler.BukkitTask[1];
|
||||||
|
taskHolder[0] = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
|
||||||
|
if (i[0] >= chevrons.size()) {
|
||||||
|
taskHolder[0].cancel();
|
||||||
|
finish.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Block chevron = chevrons.get(i[0]);
|
||||||
|
chevron.setType(config.chevronLit);
|
||||||
|
if (config.playSounds) {
|
||||||
|
chevron.getWorld().playSound(chevron.getLocation(), Sound.BLOCK_STONE_STEP, 1f, 1.4f);
|
||||||
|
}
|
||||||
|
i[0]++;
|
||||||
|
}, 0L, delay);
|
||||||
|
from.setDialTask(taskHolder[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void openGate(RuntimeGate gate, RuntimeGate connectedTo) {
|
||||||
|
gate.setOpen(true);
|
||||||
|
gate.setConnectedTo(connectedTo);
|
||||||
|
if (gate.getStructure() != null) {
|
||||||
|
for (Block b : gate.getStructure().getIris()) {
|
||||||
|
b.setType(config.irisMaterial);
|
||||||
|
if (b.getBlockData() instanceof Levelled lvl) {
|
||||||
|
lvl.setLevel(0);
|
||||||
|
b.setBlockData(lvl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Block c : gate.getStructure().getChevrons()) {
|
||||||
|
c.setType(config.chevronLit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (config.playSounds) {
|
||||||
|
World w = Bukkit.getWorld(gate.getGate().getWorld());
|
||||||
|
if (w != null) {
|
||||||
|
w.playSound(new Location(w, gate.getGate().getExitX(), gate.getGate().getExitY(), gate.getGate().getExitZ()),
|
||||||
|
Sound.ENTITY_GENERIC_EXPLODE, 0.5f, 1.8f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (gate.getCloseTask() != null) gate.getCloseTask().cancel();
|
||||||
|
var task = Bukkit.getScheduler().runTaskLater(plugin, () -> closeGate(gate), config.openSeconds * 20L);
|
||||||
|
gate.setCloseTask(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void closeGate(RuntimeGate gate) {
|
||||||
|
if (gate.getDialTask() != null) { gate.getDialTask().cancel(); gate.setDialTask(null); }
|
||||||
|
if (gate.getCloseTask() != null) { gate.getCloseTask().cancel(); gate.setCloseTask(null); }
|
||||||
|
gate.setOpen(false);
|
||||||
|
RuntimeGate other = gate.getConnectedTo();
|
||||||
|
gate.setConnectedTo(null);
|
||||||
|
if (gate.getStructure() != null) {
|
||||||
|
for (Block b : gate.getStructure().getIris()) {
|
||||||
|
b.setType(org.bukkit.Material.AIR);
|
||||||
|
}
|
||||||
|
for (Block c : gate.getStructure().getChevrons()) {
|
||||||
|
c.setType(config.chevronUnlit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (other != null && other.isOpen()) {
|
||||||
|
closeGate(other);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void closeAllGates() {
|
||||||
|
for (RuntimeGate rg : new ArrayList<>(gatesById.values())) {
|
||||||
|
if (rg.isOpen()) closeGate(rg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package dev.skywalker3200.stargate.paper.gate;
|
||||||
|
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** The physical blocks that make up a scanned gate: frame ring, chevrons, and interior iris. */
|
||||||
|
public class GateStructure {
|
||||||
|
private final List<Block> frame;
|
||||||
|
private final List<Block> chevrons;
|
||||||
|
private final List<Block> iris;
|
||||||
|
|
||||||
|
public GateStructure(List<Block> frame, List<Block> chevrons, List<Block> iris) {
|
||||||
|
this.frame = frame;
|
||||||
|
this.chevrons = chevrons;
|
||||||
|
this.iris = iris;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Block> getFrame() { return frame; }
|
||||||
|
public List<Block> getChevrons() { return chevrons; }
|
||||||
|
public List<Block> getIris() { return iris; }
|
||||||
|
}
|
||||||
+143
@@ -0,0 +1,143 @@
|
|||||||
|
package dev.skywalker3200.stargate.paper.gate;
|
||||||
|
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.block.BlockFace;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flood-fill scanner that discovers a gate's physical structure starting from the block
|
||||||
|
* a control sign is attached to: the frame ring, the chevron blocks embedded in it, and
|
||||||
|
* the enclosed interior ("iris") that gets filled with water while the gate is open.
|
||||||
|
*/
|
||||||
|
public class GateStructureScanner {
|
||||||
|
|
||||||
|
private static final BlockFace[] NEIGHBORS = {
|
||||||
|
BlockFace.UP, BlockFace.DOWN, BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST
|
||||||
|
};
|
||||||
|
|
||||||
|
private final Set<Material> frameMaterials;
|
||||||
|
private final Material chevronUnlit;
|
||||||
|
private final Material chevronLit;
|
||||||
|
private final int maxFrameBlocks;
|
||||||
|
private final int maxIrisBlocks;
|
||||||
|
private final int minFrameBlocks;
|
||||||
|
|
||||||
|
public GateStructureScanner(Set<Material> frameMaterials, Material chevronUnlit, Material chevronLit,
|
||||||
|
int maxFrameBlocks, int maxIrisBlocks, int minFrameBlocks) {
|
||||||
|
this.frameMaterials = frameMaterials;
|
||||||
|
this.chevronUnlit = chevronUnlit;
|
||||||
|
this.chevronLit = chevronLit;
|
||||||
|
this.maxFrameBlocks = maxFrameBlocks;
|
||||||
|
this.maxIrisBlocks = maxIrisBlocks;
|
||||||
|
this.minFrameBlocks = minFrameBlocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isFrameMaterial(Material m) {
|
||||||
|
return frameMaterials.contains(m) || m == chevronUnlit || m == chevronLit;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isChevronMaterial(Material m) {
|
||||||
|
return m == chevronUnlit || m == chevronLit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans outward from the given seed block (the block the sign is attached to).
|
||||||
|
* Returns null if no valid enclosed structure is found.
|
||||||
|
*/
|
||||||
|
public GateStructure scan(Block seed) {
|
||||||
|
if (!isFrameMaterial(seed.getType())) {
|
||||||
|
// seed itself may be the wall block behind a sign that's part of a bigger build;
|
||||||
|
// try its direct neighbors for the actual frame block.
|
||||||
|
for (BlockFace face : NEIGHBORS) {
|
||||||
|
Block b = seed.getRelative(face);
|
||||||
|
if (isFrameMaterial(b.getType())) {
|
||||||
|
seed = b;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isFrameMaterial(seed.getType())) return null;
|
||||||
|
|
||||||
|
Set<Long> frameKeys = new HashSet<>();
|
||||||
|
List<Block> frameBlocks = new ArrayList<>();
|
||||||
|
List<Block> chevronBlocks = new ArrayList<>();
|
||||||
|
Deque<Block> queue = new ArrayDeque<>();
|
||||||
|
queue.add(seed);
|
||||||
|
frameKeys.add(key(seed));
|
||||||
|
|
||||||
|
while (!queue.isEmpty()) {
|
||||||
|
Block cur = queue.poll();
|
||||||
|
frameBlocks.add(cur);
|
||||||
|
if (isChevronMaterial(cur.getType())) chevronBlocks.add(cur);
|
||||||
|
if (frameBlocks.size() > maxFrameBlocks) return null;
|
||||||
|
|
||||||
|
for (BlockFace face : NEIGHBORS) {
|
||||||
|
Block next = cur.getRelative(face);
|
||||||
|
long k = key(next);
|
||||||
|
if (frameKeys.contains(k)) continue;
|
||||||
|
if (isFrameMaterial(next.getType())) {
|
||||||
|
frameKeys.add(k);
|
||||||
|
queue.add(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frameBlocks.size() < minFrameBlocks) return null;
|
||||||
|
|
||||||
|
// Find an interior seed: a non-frame block adjacent to a frame block, that is not
|
||||||
|
// solid (roughly the middle of the ring). We try several candidates and flood-fill
|
||||||
|
// each; the first one that stays enclosed within maxIrisBlocks wins.
|
||||||
|
Set<Long> triedSeeds = new HashSet<>();
|
||||||
|
for (Block frameBlock : frameBlocks) {
|
||||||
|
for (BlockFace face : NEIGHBORS) {
|
||||||
|
Block candidate = frameBlock.getRelative(face);
|
||||||
|
long ck = key(candidate);
|
||||||
|
if (frameKeys.contains(ck) || triedSeeds.contains(ck)) continue;
|
||||||
|
triedSeeds.add(ck);
|
||||||
|
if (candidate.getType().isSolid()) continue;
|
||||||
|
|
||||||
|
List<Block> iris = floodInterior(candidate, frameKeys);
|
||||||
|
if (iris != null && !iris.isEmpty()) {
|
||||||
|
return new GateStructure(frameBlocks, chevronBlocks, iris);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Flood-fills non-frame blocks starting at seed; fails (returns null) if it escapes the frame boundary. */
|
||||||
|
private List<Block> floodInterior(Block seed, Set<Long> frameKeys) {
|
||||||
|
Set<Long> visited = new HashSet<>();
|
||||||
|
List<Block> interior = new ArrayList<>();
|
||||||
|
Deque<Block> queue = new ArrayDeque<>();
|
||||||
|
queue.add(seed);
|
||||||
|
visited.add(key(seed));
|
||||||
|
|
||||||
|
while (!queue.isEmpty()) {
|
||||||
|
Block cur = queue.poll();
|
||||||
|
interior.add(cur);
|
||||||
|
if (interior.size() > maxIrisBlocks) return null;
|
||||||
|
|
||||||
|
for (BlockFace face : NEIGHBORS) {
|
||||||
|
Block next = cur.getRelative(face);
|
||||||
|
long k = key(next);
|
||||||
|
if (visited.contains(k) || frameKeys.contains(k)) continue;
|
||||||
|
if (next.getType().isSolid()) return null; // hit solid, non-frame block: not a clean ring
|
||||||
|
visited.add(k);
|
||||||
|
queue.add(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return interior;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long key(Block b) {
|
||||||
|
return (((long) b.getX() & 0x3FFFFFF) << 38) | (((long) (b.getY() + 512) & 0xFFF) << 26) | ((long) b.getZ() & 0x3FFFFFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package dev.skywalker3200.stargate.paper.gate;
|
||||||
|
|
||||||
|
import dev.skywalker3200.stargate.common.model.Gate;
|
||||||
|
import org.bukkit.scheduler.BukkitTask;
|
||||||
|
|
||||||
|
/** Runtime state for a gate on this server: the persisted model, its scanned blocks, and dial state. */
|
||||||
|
public class RuntimeGate {
|
||||||
|
|
||||||
|
private final Gate gate;
|
||||||
|
private GateStructure structure; // null if structure could not be (re)scanned
|
||||||
|
private boolean open = false;
|
||||||
|
private RuntimeGate connectedTo = null;
|
||||||
|
private BukkitTask closeTask;
|
||||||
|
private BukkitTask dialTask;
|
||||||
|
private int cycleIndex = 0;
|
||||||
|
|
||||||
|
public RuntimeGate(Gate gate, GateStructure structure) {
|
||||||
|
this.gate = gate;
|
||||||
|
this.structure = structure;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Gate getGate() { return gate; }
|
||||||
|
public GateStructure getStructure() { return structure; }
|
||||||
|
public void setStructure(GateStructure structure) { this.structure = structure; }
|
||||||
|
public boolean isOpen() { return open; }
|
||||||
|
public void setOpen(boolean open) { this.open = open; }
|
||||||
|
public RuntimeGate getConnectedTo() { return connectedTo; }
|
||||||
|
public void setConnectedTo(RuntimeGate connectedTo) { this.connectedTo = connectedTo; }
|
||||||
|
public BukkitTask getCloseTask() { return closeTask; }
|
||||||
|
public void setCloseTask(BukkitTask closeTask) { this.closeTask = closeTask; }
|
||||||
|
public BukkitTask getDialTask() { return dialTask; }
|
||||||
|
public void setDialTask(BukkitTask dialTask) { this.dialTask = dialTask; }
|
||||||
|
public int getCycleIndex() { return cycleIndex; }
|
||||||
|
public void setCycleIndex(int cycleIndex) { this.cycleIndex = cycleIndex; }
|
||||||
|
}
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
package dev.skywalker3200.stargate.paper.listener;
|
||||||
|
|
||||||
|
import dev.skywalker3200.stargate.paper.StargatePlugin;
|
||||||
|
import dev.skywalker3200.stargate.paper.gate.GateManager;
|
||||||
|
import dev.skywalker3200.stargate.paper.gate.RuntimeGate;
|
||||||
|
import dev.skywalker3200.stargate.paper.network.CrossServerBridge;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.player.PlayerMoveEvent;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Walking into an open gate's water plane teleports the player to the connected gate. */
|
||||||
|
public class GateTeleportListener implements Listener {
|
||||||
|
|
||||||
|
private final StargatePlugin plugin;
|
||||||
|
private final GateManager gateManager;
|
||||||
|
private final CrossServerBridge crossServerBridge;
|
||||||
|
|
||||||
|
public GateTeleportListener(StargatePlugin plugin, GateManager gateManager, CrossServerBridge crossServerBridge) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.gateManager = gateManager;
|
||||||
|
this.crossServerBridge = crossServerBridge;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(ignoreCancelled = true)
|
||||||
|
public void onMove(PlayerMoveEvent event) {
|
||||||
|
Location to = event.getTo();
|
||||||
|
if (to == null) return;
|
||||||
|
if (event.getFrom().getBlockX() == to.getBlockX() && event.getFrom().getBlockY() == to.getBlockY()
|
||||||
|
&& event.getFrom().getBlockZ() == to.getBlockZ()) return;
|
||||||
|
|
||||||
|
Block standing = to.getBlock();
|
||||||
|
List<RuntimeGate> gates = gateManager.all();
|
||||||
|
for (RuntimeGate rg : gates) {
|
||||||
|
if (!rg.isOpen() || rg.getStructure() == null || rg.getConnectedTo() == null) continue;
|
||||||
|
for (Block iris : rg.getStructure().getIris()) {
|
||||||
|
if (iris.getX() == standing.getX() && iris.getY() == standing.getY() && iris.getZ() == standing.getZ()
|
||||||
|
&& iris.getWorld().equals(standing.getWorld())) {
|
||||||
|
teleport(event.getPlayer(), rg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void teleport(org.bukkit.entity.Player player, RuntimeGate entered) {
|
||||||
|
RuntimeGate dest = entered.getConnectedTo();
|
||||||
|
if (dest == null) return;
|
||||||
|
|
||||||
|
boolean remote = !dest.getGate().getServerId().equals(plugin.getServerId());
|
||||||
|
if (remote) {
|
||||||
|
crossServerBridge.sendPlayerThroughGate(player, dest.getGate());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var world = org.bukkit.Bukkit.getWorld(dest.getGate().getWorld());
|
||||||
|
if (world == null) return;
|
||||||
|
Location exit = new Location(world, dest.getGate().getExitX() + 0.5, dest.getGate().getExitY(),
|
||||||
|
dest.getGate().getExitZ() + 0.5, dest.getGate().getExitYaw(), 0f);
|
||||||
|
player.teleport(exit);
|
||||||
|
player.playSound(exit, org.bukkit.Sound.ENTITY_ENDERMAN_TELEPORT, 1f, 1f);
|
||||||
|
}
|
||||||
|
}
|
||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
package dev.skywalker3200.stargate.paper.listener;
|
||||||
|
|
||||||
|
import dev.skywalker3200.stargate.common.model.Gate;
|
||||||
|
import dev.skywalker3200.stargate.paper.StargatePlugin;
|
||||||
|
import dev.skywalker3200.stargate.paper.gate.GateManager;
|
||||||
|
import dev.skywalker3200.stargate.paper.gate.RuntimeGate;
|
||||||
|
import dev.skywalker3200.stargate.paper.util.SignRenderer;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.block.SignChangeEvent;
|
||||||
|
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/** Handles turning a freshly-placed sign reading "[Stargate]" into a registered gate. */
|
||||||
|
public class SignCreateListener implements Listener {
|
||||||
|
|
||||||
|
private static final String HEADER = "[stargate]";
|
||||||
|
|
||||||
|
private final StargatePlugin plugin;
|
||||||
|
private final GateManager gateManager;
|
||||||
|
|
||||||
|
public SignCreateListener(StargatePlugin plugin, GateManager gateManager) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.gateManager = gateManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(ignoreCancelled = true)
|
||||||
|
public void onSignChange(SignChangeEvent event) {
|
||||||
|
String line0 = stripped(event.line(0));
|
||||||
|
if (line0 == null || !line0.equalsIgnoreCase(HEADER)) return;
|
||||||
|
|
||||||
|
var player = event.getPlayer();
|
||||||
|
if (!player.hasPermission("stargate.create")) {
|
||||||
|
player.sendMessage(Component.text("You don't have permission to create stargates.", NamedTextColor.RED));
|
||||||
|
resetLine(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String network = stripped(event.line(1));
|
||||||
|
if (network == null || network.isBlank()) network = gateManager.getConfig().defaultNetwork;
|
||||||
|
|
||||||
|
String name = stripped(event.line(2));
|
||||||
|
if (name == null || name.isBlank()) name = "Gate-" + Integer.toHexString((int) (Math.random() * 0xFFFF));
|
||||||
|
|
||||||
|
EnumSet<Gate.Flag> flags = EnumSet.of(Gate.Flag.PUBLIC);
|
||||||
|
String flagLine = stripped(event.line(3));
|
||||||
|
if (flagLine != null) {
|
||||||
|
if (flagLine.equalsIgnoreCase("hidden")) { flags.clear(); flags.add(Gate.Flag.HIDDEN); }
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeGate rg = gateManager.createGate(event.getBlock(), network, name, player.getUniqueId(), flags);
|
||||||
|
if (rg == null) {
|
||||||
|
player.sendMessage(Component.text("No valid gate structure found. Build the frame first, then place the sign.", NamedTextColor.RED));
|
||||||
|
resetLine(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.sendMessage(Component.text("Stargate '" + name + "' created on network '" + network + "'.", NamedTextColor.AQUA));
|
||||||
|
SignRenderer.render(event, rg, gateManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String stripped(Component c) {
|
||||||
|
if (c == null) return null;
|
||||||
|
return net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer.plainText().serialize(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetLine(SignChangeEvent event) {
|
||||||
|
event.line(0, Component.text(""));
|
||||||
|
}
|
||||||
|
}
|
||||||
+100
@@ -0,0 +1,100 @@
|
|||||||
|
package dev.skywalker3200.stargate.paper.listener;
|
||||||
|
|
||||||
|
import dev.skywalker3200.stargate.paper.StargatePlugin;
|
||||||
|
import dev.skywalker3200.stargate.paper.gate.GateManager;
|
||||||
|
import dev.skywalker3200.stargate.paper.gate.RuntimeGate;
|
||||||
|
import dev.skywalker3200.stargate.paper.network.CrossServerBridge;
|
||||||
|
import dev.skywalker3200.stargate.paper.util.SignRenderer;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import org.bukkit.Sound;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.block.Sign;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.block.Action;
|
||||||
|
import org.bukkit.event.player.PlayerInteractEvent;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Right-click a gate's sign to cycle its destination, left-click to dial it. */
|
||||||
|
public class SignInteractListener implements Listener {
|
||||||
|
|
||||||
|
private final StargatePlugin plugin;
|
||||||
|
private final GateManager gateManager;
|
||||||
|
private final CrossServerBridge crossServerBridge;
|
||||||
|
|
||||||
|
public SignInteractListener(StargatePlugin plugin, GateManager gateManager, CrossServerBridge crossServerBridge) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.gateManager = gateManager;
|
||||||
|
this.crossServerBridge = crossServerBridge;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(ignoreCancelled = true)
|
||||||
|
public void onInteract(PlayerInteractEvent event) {
|
||||||
|
Block block = event.getClickedBlock();
|
||||||
|
if (block == null || !(block.getState() instanceof Sign)) return;
|
||||||
|
RuntimeGate rg = gateManager.getBySign(block);
|
||||||
|
if (rg == null) return;
|
||||||
|
|
||||||
|
var player = event.getPlayer();
|
||||||
|
if (!player.hasPermission("stargate.use")) return;
|
||||||
|
event.setCancelled(true);
|
||||||
|
|
||||||
|
if (event.getAction() == Action.RIGHT_CLICK_BLOCK) {
|
||||||
|
List<RuntimeGate> destinations = gateManager.destinationsFor(rg);
|
||||||
|
if (destinations.isEmpty()) {
|
||||||
|
player.sendMessage(Component.text("No other gates on network '" + rg.getGate().getNetwork() + "'.", NamedTextColor.RED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (rg.getGate().isFixed()) {
|
||||||
|
player.sendMessage(Component.text("This gate's destination is fixed.", NamedTextColor.RED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rg.setCycleIndex(rg.getCycleIndex() + 1);
|
||||||
|
SignRenderer.render(rg, gateManager);
|
||||||
|
player.playSound(player.getLocation(), Sound.UI_BUTTON_CLICK, 1f, 1f);
|
||||||
|
} else if (event.getAction() == Action.LEFT_CLICK_BLOCK) {
|
||||||
|
dial(rg, player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void dial(RuntimeGate from, org.bukkit.entity.Player player) {
|
||||||
|
if (from.isOpen()) {
|
||||||
|
player.sendMessage(Component.text("This gate is already active.", NamedTextColor.RED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<RuntimeGate> destinations = gateManager.destinationsFor(from);
|
||||||
|
RuntimeGate to;
|
||||||
|
if (from.getGate().isFixed()) {
|
||||||
|
to = destinations.stream()
|
||||||
|
.filter(g -> g.getGate().getName().equalsIgnoreCase(from.getGate().getFixedDestination()))
|
||||||
|
.findFirst().orElse(null);
|
||||||
|
} else {
|
||||||
|
if (destinations.isEmpty()) {
|
||||||
|
player.sendMessage(Component.text("No destinations available.", NamedTextColor.RED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int idx = ((from.getCycleIndex() % destinations.size()) + destinations.size()) % destinations.size();
|
||||||
|
to = destinations.get(idx);
|
||||||
|
}
|
||||||
|
if (to == null) {
|
||||||
|
player.sendMessage(Component.text("Destination gate not found.", NamedTextColor.RED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (to.isOpen()) {
|
||||||
|
player.sendMessage(Component.text("Destination gate is busy.", NamedTextColor.RED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean remote = !to.getGate().getServerId().equals(plugin.getServerId());
|
||||||
|
if (remote && !crossServerBridge.isEnabled()) {
|
||||||
|
player.sendMessage(Component.text("Cross-server dialing is disabled on this server.", NamedTextColor.RED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.sendMessage(Component.text("Dialing " + to.getGate().getName() + "...", NamedTextColor.AQUA));
|
||||||
|
gateManager.dial(from, to, player);
|
||||||
|
SignRenderer.render(from, gateManager);
|
||||||
|
}
|
||||||
|
}
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
package dev.skywalker3200.stargate.paper.listener;
|
||||||
|
|
||||||
|
import dev.skywalker3200.stargate.paper.gate.GateManager;
|
||||||
|
import dev.skywalker3200.stargate.paper.gate.RuntimeGate;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.block.BlockBreakEvent;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/** Breaking a gate's sign, frame, or chevron blocks destroys its registration (owner/admin only). */
|
||||||
|
public class StructureProtectListener implements Listener {
|
||||||
|
|
||||||
|
private final GateManager gateManager;
|
||||||
|
|
||||||
|
public StructureProtectListener(GateManager gateManager) {
|
||||||
|
this.gateManager = gateManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler(ignoreCancelled = true)
|
||||||
|
public void onBreak(BlockBreakEvent event) {
|
||||||
|
Block block = event.getBlock();
|
||||||
|
RuntimeGate rg = gateManager.getBySign(block);
|
||||||
|
boolean isSign = rg != null;
|
||||||
|
|
||||||
|
if (!isSign) {
|
||||||
|
rg = findByFrameBlock(block);
|
||||||
|
}
|
||||||
|
if (rg == null) return;
|
||||||
|
|
||||||
|
UUID owner = rg.getGate().getOwner();
|
||||||
|
var player = event.getPlayer();
|
||||||
|
boolean allowed = player.hasPermission("stargate.admin")
|
||||||
|
|| (owner != null && owner.equals(player.getUniqueId()) && player.hasPermission("stargate.destroy"));
|
||||||
|
if (!allowed) {
|
||||||
|
event.setCancelled(true);
|
||||||
|
player.sendMessage(Component.text("You can't break this stargate.", NamedTextColor.RED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gateManager.destroyGate(rg);
|
||||||
|
player.sendMessage(Component.text("Stargate '" + rg.getGate().getName() + "' destroyed.", NamedTextColor.YELLOW));
|
||||||
|
}
|
||||||
|
|
||||||
|
private RuntimeGate findByFrameBlock(Block block) {
|
||||||
|
for (RuntimeGate rg : gateManager.all()) {
|
||||||
|
if (rg.getStructure() == null) continue;
|
||||||
|
List<Block> frame = rg.getStructure().getFrame();
|
||||||
|
for (Block b : frame) {
|
||||||
|
if (b.getX() == block.getX() && b.getY() == block.getY() && b.getZ() == block.getZ()
|
||||||
|
&& b.getWorld().equals(block.getWorld())) {
|
||||||
|
return rg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
package dev.skywalker3200.stargate.paper.network;
|
||||||
|
|
||||||
|
import com.google.common.io.ByteArrayDataOutput;
|
||||||
|
import com.google.common.io.ByteStreams;
|
||||||
|
import dev.skywalker3200.stargate.common.model.Gate;
|
||||||
|
import dev.skywalker3200.stargate.common.network.StargateChannel;
|
||||||
|
import dev.skywalker3200.stargate.paper.StargatePlugin;
|
||||||
|
import dev.skywalker3200.stargate.paper.gate.GateManager;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.Location;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.plugin.messaging.PluginMessageListener;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Talks to the stargate-velocity / stargate-bungee companion plugin over plugin messaging so a
|
||||||
|
* player dialing a gate hosted on another backend server actually gets moved there and then
|
||||||
|
* warped to the right spot once they land.
|
||||||
|
*/
|
||||||
|
public class CrossServerBridge implements PluginMessageListener {
|
||||||
|
|
||||||
|
private final StargatePlugin plugin;
|
||||||
|
private final GateManager gateManager;
|
||||||
|
private boolean enabled = false;
|
||||||
|
|
||||||
|
public CrossServerBridge(StargatePlugin plugin, GateManager gateManager) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.gateManager = gateManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void register() {
|
||||||
|
this.enabled = true;
|
||||||
|
Bukkit.getMessenger().registerOutgoingPluginChannel(plugin, StargateChannel.CHANNEL);
|
||||||
|
Bukkit.getMessenger().registerIncomingPluginChannel(plugin, StargateChannel.CHANNEL, this);
|
||||||
|
plugin.getLogger().info("[Stargate] Cross-server bridge registered on channel " + StargateChannel.CHANNEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Asks the proxy to move this player to the backend server hosting {@code destGate}. */
|
||||||
|
public void sendPlayerThroughGate(Player player, Gate destGate) {
|
||||||
|
if (!enabled) return;
|
||||||
|
ByteArrayDataOutput out = ByteStreams.newDataOutput();
|
||||||
|
out.writeByte(StargateChannel.OP_TELEPORT_REQUEST);
|
||||||
|
out.writeUTF(player.getUniqueId().toString());
|
||||||
|
out.writeUTF(destGate.getServerId());
|
||||||
|
out.writeUTF(destGate.getId().toString());
|
||||||
|
player.sendPluginMessage(plugin, StargateChannel.CHANNEL, out.toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPluginMessageReceived(String channel, Player receivingPlayer, byte[] message) {
|
||||||
|
if (!channel.equals(StargateChannel.CHANNEL)) return;
|
||||||
|
try {
|
||||||
|
DataInputStream in = new DataInputStream(new ByteArrayInputStream(message));
|
||||||
|
byte op = in.readByte();
|
||||||
|
if (op != StargateChannel.OP_TELEPORT_DELIVER) return;
|
||||||
|
|
||||||
|
UUID playerId = UUID.fromString(in.readUTF());
|
||||||
|
UUID gateId = UUID.fromString(in.readUTF());
|
||||||
|
|
||||||
|
var rg = gateManager.getById(gateId);
|
||||||
|
if (rg == null) {
|
||||||
|
plugin.getLogger().warning("[Stargate] Received teleport-deliver for unknown gate " + gateId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Gate gate = rg.getGate();
|
||||||
|
World world = Bukkit.getWorld(gate.getWorld());
|
||||||
|
if (world == null) return;
|
||||||
|
|
||||||
|
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||||
|
Player p = Bukkit.getPlayer(playerId);
|
||||||
|
if (p == null) return;
|
||||||
|
Location exit = new Location(world, gate.getExitX() + 0.5, gate.getExitY(), gate.getExitZ() + 0.5, gate.getExitYaw(), 0f);
|
||||||
|
p.teleport(exit);
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("[Stargate] Failed to handle cross-server teleport message: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package dev.skywalker3200.stargate.paper.util;
|
||||||
|
|
||||||
|
import dev.skywalker3200.stargate.paper.gate.GateManager;
|
||||||
|
import dev.skywalker3200.stargate.paper.gate.RuntimeGate;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.block.Sign;
|
||||||
|
import org.bukkit.block.sign.Side;
|
||||||
|
import org.bukkit.event.block.SignChangeEvent;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Renders a gate's current state (name, network, selected destination) onto its control sign. */
|
||||||
|
public final class SignRenderer {
|
||||||
|
|
||||||
|
private SignRenderer() {}
|
||||||
|
|
||||||
|
public static void render(SignChangeEvent event, RuntimeGate rg, GateManager gateManager) {
|
||||||
|
event.line(0, Component.text(rg.getGate().getName(), NamedTextColor.DARK_AQUA));
|
||||||
|
event.line(1, Component.text(rg.getGate().getNetwork(), NamedTextColor.GRAY));
|
||||||
|
event.line(2, destinationLine(rg, gateManager));
|
||||||
|
event.line(3, rg.isOpen() ? Component.text("[connected]", NamedTextColor.GREEN) : Component.text(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void render(RuntimeGate rg, GateManager gateManager) {
|
||||||
|
Block b = org.bukkit.Bukkit.getWorld(rg.getGate().getSignWorld())
|
||||||
|
.getBlockAt(rg.getGate().getSignX(), rg.getGate().getSignY(), rg.getGate().getSignZ());
|
||||||
|
if (!(b.getState() instanceof Sign sign)) return;
|
||||||
|
sign.getSide(Side.FRONT).line(0, Component.text(rg.getGate().getName(), NamedTextColor.DARK_AQUA));
|
||||||
|
sign.getSide(Side.FRONT).line(1, Component.text(rg.getGate().getNetwork(), NamedTextColor.GRAY));
|
||||||
|
sign.getSide(Side.FRONT).line(2, destinationLine(rg, gateManager));
|
||||||
|
sign.getSide(Side.FRONT).line(3, rg.isOpen() ? Component.text("[connected]", NamedTextColor.GREEN) : Component.text(""));
|
||||||
|
sign.update(true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Component destinationLine(RuntimeGate rg, GateManager gateManager) {
|
||||||
|
List<RuntimeGate> destinations = gateManager.destinationsFor(rg);
|
||||||
|
if (destinations.isEmpty()) {
|
||||||
|
return Component.text("no destinations", NamedTextColor.DARK_GRAY);
|
||||||
|
}
|
||||||
|
int idx = ((rg.getCycleIndex() % destinations.size()) + destinations.size()) % destinations.size();
|
||||||
|
RuntimeGate dest = destinations.get(idx);
|
||||||
|
return Component.text("> " + dest.getGate().getName() + " <", NamedTextColor.GOLD);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Stargate configuration
|
||||||
|
|
||||||
|
# Unique id for THIS server. Used to tag gates created here and to route cross-server
|
||||||
|
# dials through the proxy. Must be unique across your network and match the server's
|
||||||
|
# name as configured in the Velocity/Bungee proxy config.
|
||||||
|
server-id: "server1"
|
||||||
|
|
||||||
|
storage:
|
||||||
|
# sqlite -> single server, file-based, zero setup
|
||||||
|
# mysql -> required if you want gates to be visible/dialable across multiple
|
||||||
|
# backend servers sharing this same database
|
||||||
|
type: sqlite
|
||||||
|
mysql:
|
||||||
|
host: localhost
|
||||||
|
port: 3306
|
||||||
|
database: stargate
|
||||||
|
username: stargate
|
||||||
|
password: ""
|
||||||
|
table-prefix: "stargate_"
|
||||||
|
|
||||||
|
# Enables sending players to another backend server when they dial a gate whose
|
||||||
|
# server-id differs from this one. Requires the stargate-velocity or stargate-bungee
|
||||||
|
# companion plugin installed on the proxy, and storage.type: mysql so all servers
|
||||||
|
# share gate data.
|
||||||
|
cross-server:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
gate:
|
||||||
|
# Any of these materials count as the decorative frame ring. Mix and match to match
|
||||||
|
# your build (the default matches an obsidian/gold-block/birch-planks arch).
|
||||||
|
frame-materials:
|
||||||
|
- OBSIDIAN
|
||||||
|
- GOLD_BLOCK
|
||||||
|
- BIRCH_PLANKS
|
||||||
|
- OAK_PLANKS
|
||||||
|
# Chevron blocks embedded in the frame. Build them as chevron-unlit-material; the
|
||||||
|
# plugin swaps them to chevron-lit-material as the gate dials/opens, and swaps them
|
||||||
|
# back when the gate closes/deactivates.
|
||||||
|
chevron-unlit-material: BLACK_STAINED_GLASS
|
||||||
|
chevron-lit-material: GLOWSTONE
|
||||||
|
# Material poured into the interior (the "event horizon") while the gate is open.
|
||||||
|
iris-material: WATER
|
||||||
|
max-frame-blocks: 300
|
||||||
|
max-iris-blocks: 200
|
||||||
|
min-frame-blocks: 8
|
||||||
|
|
||||||
|
dialing:
|
||||||
|
# Seconds the gate stays open (iris filled) before auto-closing if nobody walks through.
|
||||||
|
open-seconds: 10
|
||||||
|
# Delay in ticks between lighting each chevron during the dial animation.
|
||||||
|
chevron-tick-delay: 4
|
||||||
|
play-sounds: true
|
||||||
|
|
||||||
|
network:
|
||||||
|
default-network: "main"
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
name: Stargate
|
||||||
|
version: '${version}'
|
||||||
|
main: dev.skywalker3200.stargate.paper.StargatePlugin
|
||||||
|
api-version: '1.21'
|
||||||
|
author: skywalker3200
|
||||||
|
description: Network-aware Stargate portals with cross-server dialing.
|
||||||
|
folia-supported: false
|
||||||
|
|
||||||
|
commands:
|
||||||
|
stargate:
|
||||||
|
description: Manage Stargate networks and gates.
|
||||||
|
aliases: [sg]
|
||||||
|
usage: /sg <create|destroy|network|reload|list> ...
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
stargate.use:
|
||||||
|
description: Allows dialing and using stargates.
|
||||||
|
default: true
|
||||||
|
stargate.create:
|
||||||
|
description: Allows creating new stargates.
|
||||||
|
default: op
|
||||||
|
stargate.destroy:
|
||||||
|
description: Allows destroying stargates you do not own.
|
||||||
|
default: op
|
||||||
|
stargate.admin:
|
||||||
|
description: Allows reload, network admin, and bypassing ownership checks.
|
||||||
|
default: op
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.gradleup.shadow") version "8.3.5"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":stargate-common"))
|
||||||
|
compileOnly("com.velocitypowered:velocity-api:3.4.0-SNAPSHOT")
|
||||||
|
annotationProcessor("com.velocitypowered:velocity-api:3.4.0-SNAPSHOT")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.shadowJar {
|
||||||
|
archiveClassifier.set("")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.build {
|
||||||
|
dependsOn(tasks.shadowJar)
|
||||||
|
}
|
||||||
+110
@@ -0,0 +1,110 @@
|
|||||||
|
package dev.skywalker3200.stargate.velocity;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.velocitypowered.api.event.Subscribe;
|
||||||
|
import com.velocitypowered.api.event.connection.PluginMessageEvent;
|
||||||
|
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
|
||||||
|
import com.velocitypowered.api.plugin.Plugin;
|
||||||
|
import com.velocitypowered.api.proxy.Player;
|
||||||
|
import com.velocitypowered.api.proxy.ProxyServer;
|
||||||
|
import com.velocitypowered.api.proxy.ServerConnection;
|
||||||
|
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||||
|
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
|
||||||
|
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||||
|
import dev.skywalker3200.stargate.common.network.StargateChannel;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relays stargate cross-server teleport requests: when a backend server asks to move a
|
||||||
|
* player to another backend, this plugin connects them there, then forwards a "deliver"
|
||||||
|
* message to the new backend once they land so the Paper plugin can warp them to the gate.
|
||||||
|
*/
|
||||||
|
@Plugin(id = "stargate-velocity", name = "Stargate Velocity Bridge", version = "1.0.0", authors = {"skywalker3200"})
|
||||||
|
public class StargateVelocityPlugin {
|
||||||
|
|
||||||
|
private final ProxyServer server;
|
||||||
|
private final Logger logger;
|
||||||
|
private final ChannelIdentifier channel = MinecraftChannelIdentifier.from(StargateChannel.CHANNEL);
|
||||||
|
|
||||||
|
// playerId -> (targetServerName, gateId) pending until they finish connecting
|
||||||
|
private final Map<UUID, PendingWarp> pending = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private record PendingWarp(String gateId) {}
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public StargateVelocityPlugin(ProxyServer server, Logger logger) {
|
||||||
|
this.server = server;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onInit(ProxyInitializeEvent event) {
|
||||||
|
server.getChannelRegistrar().register(channel);
|
||||||
|
logger.info("Stargate Velocity bridge ready on channel {}", StargateChannel.CHANNEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onPluginMessage(PluginMessageEvent event) {
|
||||||
|
if (!event.getIdentifier().equals(channel)) return;
|
||||||
|
event.setResult(PluginMessageEvent.ForwardResult.handled());
|
||||||
|
|
||||||
|
byte[] data = event.getData();
|
||||||
|
try {
|
||||||
|
DataInputStream in = new DataInputStream(new ByteArrayInputStream(data));
|
||||||
|
byte op = in.readByte();
|
||||||
|
if (op != StargateChannel.OP_TELEPORT_REQUEST) return;
|
||||||
|
|
||||||
|
UUID playerId = UUID.fromString(in.readUTF());
|
||||||
|
String targetServer = in.readUTF();
|
||||||
|
String gateId = in.readUTF();
|
||||||
|
|
||||||
|
Optional<Player> playerOpt = server.getPlayer(playerId);
|
||||||
|
Optional<RegisteredServer> targetOpt = server.getServer(targetServer);
|
||||||
|
if (playerOpt.isEmpty() || targetOpt.isEmpty()) {
|
||||||
|
logger.warn("Stargate teleport request for unknown player/server ({}/{})", playerId, targetServer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pending.put(playerId, new PendingWarp(gateId));
|
||||||
|
playerOpt.get().createConnectionRequest(targetOpt.get()).connectWithIndication().thenAccept(success -> {
|
||||||
|
if (Boolean.TRUE.equals(success)) {
|
||||||
|
deliver(playerId, targetOpt.get());
|
||||||
|
} else {
|
||||||
|
pending.remove(playerId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Failed to process stargate plugin message", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deliver(UUID playerId, RegisteredServer server) {
|
||||||
|
PendingWarp warp = pending.remove(playerId);
|
||||||
|
if (warp == null) return;
|
||||||
|
Optional<ServerConnection> connection = server.getPlayersConnected().stream()
|
||||||
|
.filter(p -> p.getUniqueId().equals(playerId))
|
||||||
|
.findFirst()
|
||||||
|
.flatMap(Player::getCurrentServer);
|
||||||
|
if (connection.isEmpty()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
|
DataOutputStream out = new DataOutputStream(bytes);
|
||||||
|
out.writeByte(StargateChannel.OP_TELEPORT_DELIVER);
|
||||||
|
out.writeUTF(playerId.toString());
|
||||||
|
out.writeUTF(warp.gateId());
|
||||||
|
connection.get().sendPluginMessage(channel, bytes.toByteArray());
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Failed to deliver stargate teleport", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user