Merge pull request #1 from Trivernis/develop

Develop
pull/111/head
NPBeta 3 years ago committed by GitHub
commit 6fedee2f1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,13 +0,0 @@
image: openjdk
matrix:
- env: SCRIPT=shadowJar
install:
- gradle dependencies
script:
- if [[ "SCRIPT" ]]; then gradle $SCRIPT; fi
cache:
- .gradle

@ -23,11 +23,15 @@ A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Logfile**
Please add a logfile as either a file attachment or a link to something like pastebin.
For crashes please also add the crashlog file.
**Server (please complete the following information):**
- OS: [e.g. Ubuntu 20]
- Specs [e.g. Intel Xeon E5-2680, 8GB Ram]
- Java Version [e.g. Java 1.12]
- Minecraft Server Version [e.g. Paper 1.15.2 #176]
- Minecraft Server Version [e.g. Paper 1.15.2 #176](PLEASE PROVIDE THE EXACT VERSION OF THE /version COMMAND, NOT JUST spigot 1.16.1!)
- Plugin Version [e.g. 0.15]
**Additional context**

@ -0,0 +1,42 @@
name: Gradle Build
on:
workflow_dispatch:
push:
branches: [ main, develop, actions ]
pull_request:
branches: [ main, develop, actions ]
jobs:
shadowJar:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Cache build data
uses: actions/cache@v2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('build.gradle') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Build Jar
run: chmod +x gradlew && ./gradlew shadowJar
- name: Upload artifacts
uses: actions/upload-artifact@v2
with:
name: chunkmaster
path: build/libs/chunkmaster-*.jar
- name: Cleanup Gradle Cache
run: |
rm -f ~/.gradle/caches/modules-2/modules-2.lock
rm -f ~/.gradle/caches/modules-2/gc.properties

@ -0,0 +1,36 @@
name: Gradle Unit Tests
on:
workflow_dispatch:
push:
branches: [ main, develop, actions, feature/unit-tests ]
pull_request:
branches: [ main, develop, actions, feature/unit-tests ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Cache build data
uses: actions/cache@v2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('build.gradle') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Test
run: chmod +x gradlew && ./gradlew test
- name: Cleanup Gradle Cache
run: |
rm -f ~/.gradle/caches/modules-2/modules-2.lock
rm -f ~/.gradle/caches/modules-2/gc.properties

4
.gitignore vendored

@ -1,6 +1,4 @@
gradle
.gradle
.idea
build
out
gradlew*
out

@ -1,4 +1,4 @@
# chunkmaster ![](https://abstruse.trivernis.net/badge/1)
# chunkmaster [![](https://circleci.com/gh/Trivernis/spigot-chunkmaster.svg?style=shield)](https://app.circleci.com/pipelines/github/Trivernis/spigot-chunkmaster) [![CodeFactor](https://www.codefactor.io/repository/github/trivernis/spigot-chunkmaster/badge)](https://www.codefactor.io/repository/github/trivernis/spigot-chunkmaster) [![](https://img.shields.io/discord/729250668162056313)](https://discord.gg/KZcMAgN)
This plugin can be used to pre-generate the region of a world around the spawn chunk(s).
The generation automatically pauses when a player joins the server (assuming the server was empty before)
@ -82,45 +82,40 @@ generation:
# The maximum amount of chunks that are loaded before unloading and saving them.
# Higher values mean higher generation speed but greater memory usage.
# The value should be a positive integer.
max-loaded-chunks: 10
max-loaded-chunks: 1000
# Paper Only
# The maximum amount of requested chunks with the asynchronous paper chunk
# loading method. Higher values mean faster generation but more memory usage
# (and probably bigger performance impact).
# loading method. Higher values mean faster generation but more memory usage and
# bigger performance impact. Configuring it too hight might crash the server.
# The value should be a positive integer.
max-pending-chunks: 10
# The period (in ticks) in which a generation step is run.
# Higher values mean less performance impact but slower generation.
# The value should be a positive integer.
period: 2
# The max amount of chunks that should be generated per step.
# Higher values mean higher generation speed but higher performance impact.
# The value should be a positive integer.
chunks-per-step: 4
# Paper Only
# The number of already generated chunks that will be skipped for each step.
# Notice that these still have a performance impact because the server needs to check
# if the chunk is generated.
# Higher values mean faster generation but greater performance impact.
# The value should be a positive integer.
chunk-skips-per-step: 100
max-pending-chunks: 500
# The maximum milliseconds per tick the server is allowed to have
# during the cunk generation process.
# If the mspt is greather than this, the chunk generation task pauses.
# The value should be a positive integer greater than 50.
# The value should be a positive integer greater than 50.
mspt-pause-threshold: 500
# The period in ticks for how often loaded chunks get unloaded.
# Unloading happens in the main thread and can impact the server performance.
# You can tweak this setting with the max-loaded-chunks setting to have either
# a lot of chunks unloaded at once or fewer chunks unloaded more often.
# If the maximum number of loaded chunks is reached the generation pauses until the
# unloading task runs again so keep that in mind.
# The value should be a positive integer.
unloading-period: 50
# Pauses the generation if the number of players on the server is larger or equal
# to the configured value
# Notice that playing on a server that constantly generates chunks can be
# very laggy and can cause it to crash.
# The value should be a posivitve integer > 1.
pause-on-player-count: 1
# if the generation should automatically start on server startup
# the value should be a boolean
autostart: true
```
### Spigot and Paper
@ -129,10 +124,32 @@ The plugin works on spigot and paper servers but is significantly faster on pape
(because it profits from asynchronous chunk loading an the better implementation of the
isChunkGenerated method).
## Translation
The **Mandarin** translation is provided by [NPBeta](https://github.com/NPBeta) and
was validated by [ed3d3d](https://twitter.com/ed3d3d).
The **French** translation is provided by [Corenb](https://github.com/Corenb) and
was validated by [Fiwel00](https://github.com/Fiwel00) and [Youssef Habri](https://github.com/youssefhabri).
The **German** and **English** translation is provided by me.
You can translate the plugin yourself and start a PR to this repository to add it to the
provided translation.
1. create an i18n folder in the plugins folder (plugins/Chunkmaster)
2. copy the [default translations file](https://github.com/Trivernis/spigot-chunkmaster/blob/master/src/main/resources/i18n/DEFAULT.i18n.properties)
into the newly created folder and rename it to <language-abbrevation>.i18n.properties
3. modify the values in the file for your translation (you can use minecraft § formatting sequences)
4. set the language property in the config file to your language abbrevation
5. start the plugin
Now you should see your translation being used by the plugin for localized messages.
## License
This project is licensed under the GPLv3.0 License - see the [LICENSE](https://github.com/Trivernis/spigot-chunkmaster/blob/master/LICENSE) for details.
This project is licensed under the GPLv3.0 License - see the
[LICENSE](https://github.com/Trivernis/spigot-chunkmaster/blob/master/LICENSE) for details.
## bStats
[![Plugin statistics](https://bstats.org/signatures/bukkit/chunkmaster.svg)](https://bstats.org/plugin/bukkit/Chunkmaster/5639)
[![Plugin statistics](https://bstats.org/signatures/bukkit/chunkmaster.svg)](https://bstats.org/plugin/bukkit/Chunkmaster/5639)

@ -10,7 +10,7 @@ buildscript {
plugins {
id 'idea'
id 'org.jetbrains.kotlin.jvm' version '1.3.50'
id 'org.jetbrains.kotlin.jvm' version '1.4.10'
id 'com.github.johnrengelman.shadow' version '2.0.4'
}
@ -22,8 +22,7 @@ idea {
}
group "net.trivernis"
version "1.2.1"
version PLUGIN_VERSION
sourceCompatibility = 1.8
repositories {
@ -45,19 +44,28 @@ repositories {
maven {
name 'mikeprimm'
url 'http://repo.mikeprimm.com'
url 'https://repo.mikeprimm.com'
}
maven {
url 'https://jitpack.io'
}
}
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
testCompile group: 'junit', name: 'junit', version: '4.12'
compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
compileOnly "com.destroystokyo.paper:paper-api:1.14.4-R0.1-SNAPSHOT"
compileOnly "org.dynmap:dynmap-api:2.0"
compileOnly group: 'org.xerial', name: 'sqlite-jdbc', version: '3.28.0'
compile "io.papermc:paperlib:1.0.2"
compile "org.bstats:bstats-bukkit:1.5"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
implementation "io.papermc:paperlib:1.0.6"
implementation "org.bstats:bstats-bukkit:1.5"
testImplementation group: 'junit', name: 'junit', version: '4.12'
testImplementation 'io.kotest:kotest-runner-junit5:4.3.2'
testImplementation 'io.mockk:mockk:1.10.6'
testImplementation "org.dynmap:dynmap-api:2.0"
}
apply plugin: "com.github.johnrengelman.shadow"
@ -66,10 +74,20 @@ apply plugin: 'java'
shadowJar {
relocate 'io.papermc.lib', 'net.trivernis.chunkmaster.paperlib'
relocate 'org.bstats', 'net.trivernis.chunkmaster.bstats'
relocate 'kotlin', 'net.trivernis.chunkmaster.kotlin'
relocate 'org.intellij', 'net.trivernis.chunkmaster.intellij'
relocate 'org.jetbrains', 'net.trivernis.chunkmaster.jetbrains'
}
jar {
from configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
processResources {
duplicatesStrategy = DuplicatesStrategy.INCLUDE
with copySpec {
from 'src/main/resources/'
include 'plugin.yml'
filter { String line ->
line.replace('$$PLUGIN_VERSION$$', PLUGIN_VERSION)
}
}
}
compileKotlin {

@ -1 +1,2 @@
kotlin.code.style=official
kotlin.code.style=official
PLUGIN_VERSION=1.4.0

Binary file not shown.

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

185
gradlew vendored

@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# 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
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

89
gradlew.bat vendored

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

@ -3,15 +3,14 @@ package net.trivernis.chunkmaster
import io.papermc.lib.PaperLib
import net.trivernis.chunkmaster.commands.CommandChunkmaster
import net.trivernis.chunkmaster.lib.LanguageManager
import net.trivernis.chunkmaster.lib.SqliteManager
import net.trivernis.chunkmaster.lib.database.SqliteManager
import net.trivernis.chunkmaster.lib.generation.GenerationManager
import org.bstats.bukkit.Metrics
import org.bukkit.plugin.java.JavaPlugin
import org.bukkit.scheduler.BukkitTask
import org.dynmap.DynmapAPI
import java.util.logging.Level
class Chunkmaster: JavaPlugin() {
open class Chunkmaster : JavaPlugin() {
lateinit var sqliteManager: SqliteManager
lateinit var generationManager: GenerationManager
lateinit var langManager: LanguageManager
@ -31,7 +30,7 @@ class Chunkmaster: JavaPlugin() {
logger.finest("LogLevel: FINEST")
configure()
val metrics = Metrics(this)
Metrics(this)
langManager = LanguageManager(this)
langManager.loadProperties()
@ -49,7 +48,7 @@ class Chunkmaster: JavaPlugin() {
if (PaperLib.isPaper() && PaperLib.getMinecraftPatchVersion() >= 225) {
tpsTask = server.scheduler.runTaskTimer(this, Runnable {
mspt = 1000/server.currentTick // use papers exposed tick rather than calculating it
mspt = 1000 / server.currentTick // use papers exposed tick rather than calculating it
}, 1, 300)
} else {
tpsTask = server.scheduler.runTaskTimer(this, Runnable {
@ -67,6 +66,7 @@ class Chunkmaster: JavaPlugin() {
override fun onDisable() {
logger.info(langManager.getLocalized("STOPPING_ALL_TASKS"))
generationManager.stopAll()
server.scheduler.cancelTasks(this)
}
/**
@ -74,14 +74,13 @@ class Chunkmaster: JavaPlugin() {
*/
private fun configure() {
dataFolder.mkdir()
config.addDefault("generation.period", 2L)
config.addDefault("generation.chunks-per-step", 2)
config.addDefault("generation.chunk-skips-per-step", 100)
config.addDefault("generation.mspt-pause-threshold", 500L)
config.addDefault("generation.pause-on-player-count", 1)
config.addDefault("generation.max-pending-chunks", 10)
config.addDefault("generation.max-loaded-chunks", 10)
config.addDefault("generation.max-pending-chunks", 500)
config.addDefault("generation.max-loaded-chunks", 1000)
config.addDefault("generation.unloading-period", 50L)
config.addDefault("generation.ignore-worldborder", false)
config.addDefault("generation.autostart", true)
config.addDefault("database.filename", "chunkmaster.db")
config.addDefault("language", "en")
config.addDefault("dynmap", true)
@ -95,20 +94,24 @@ class Chunkmaster: JavaPlugin() {
private fun initDatabase() {
logger.info(langManager.getLocalized("DB_INIT"))
try {
this.sqliteManager = SqliteManager( this)
this.sqliteManager = SqliteManager(this)
sqliteManager.init()
logger.info(langManager.getLocalized("DB_INIT_FINISHED"))
} catch(e: Exception) {
} catch (e: Exception) {
logger.warning(langManager.getLocalized("DB_INIT_EROR", e.message!!))
}
}
private fun getDynmap(): DynmapAPI? {
val dynmap = server.pluginManager.getPlugin("dynmap")
return if (dynmap != null && dynmap is DynmapAPI) {
logger.info(langManager.getLocalized("PLUGIN_DETECTED", "dynmap", dynmap.dynmapVersion))
dynmap
} else {
return try {
val dynmap = server.pluginManager.getPlugin("dynmap")
if (dynmap != null && dynmap is DynmapAPI) {
logger.info(langManager.getLocalized("PLUGIN_DETECTED", "dynmap", dynmap.dynmapVersion))
dynmap
} else {
null
}
} catch (e: IllegalStateException) {
null
}
}

@ -9,7 +9,7 @@ import org.bukkit.event.player.PlayerQuitEvent
class ChunkmasterEvents(private val chunkmaster: Chunkmaster, private val server: Server) : Listener {
private val pauseOnPlayerCount: Int
get () {
get() {
return chunkmaster.config.getInt("generation.pause-on-player-count")
}
private var playerPaused = false
@ -25,7 +25,7 @@ class ChunkmasterEvents(private val chunkmaster: Chunkmaster, private val server
chunkmaster.logger.info(chunkmaster.langManager.getLocalized("RESUME_PLAYER_LEAVE"))
}
chunkmaster.generationManager.resumeAll()
} else if (chunkmaster.generationManager.paused){
} else if (chunkmaster.generationManager.paused) {
chunkmaster.logger.info(chunkmaster.langManager.getLocalized("PAUSE_MANUALLY"))
playerPaused = chunkmaster.generationManager.paused
}
@ -41,8 +41,10 @@ class ChunkmasterEvents(private val chunkmaster: Chunkmaster, private val server
if (chunkmaster.generationManager.tasks.isNotEmpty()) {
chunkmaster.logger.info(chunkmaster.langManager.getLocalized("PAUSE_PLAYER_JOIN"))
}
playerPaused = chunkmaster.generationManager.paused
chunkmaster.generationManager.pauseAll()
if (!chunkmaster.generationManager.paused) {
playerPaused = chunkmaster.generationManager.paused
chunkmaster.generationManager.pauseAll()
}
}
}
}

@ -1,14 +1,11 @@
package net.trivernis.chunkmaster.commands
import net.md_5.bungee.api.ChatColor
import net.md_5.bungee.api.chat.ComponentBuilder
import net.trivernis.chunkmaster.Chunkmaster
import net.trivernis.chunkmaster.lib.Subcommand
import net.trivernis.chunkmaster.lib.generation.TaskEntry
import org.bukkit.command.Command
import org.bukkit.command.CommandSender
class CmdCancel(private val chunkmaster: Chunkmaster): Subcommand {
class CmdCancel(private val chunkmaster: Chunkmaster) : Subcommand {
override val name = "cancel"
/**
@ -22,7 +19,7 @@ class CmdCancel(private val chunkmaster: Chunkmaster): Subcommand {
): MutableList<String> {
val genManager = chunkmaster.generationManager
val allTasks = genManager.allTasks
return allTasks.filter {it.id.toString().indexOf(args[0]) == 0}
return allTasks.filter { it.id.toString().indexOf(args[0]) == 0 }
.map { it.id.toString() }.toMutableList()
}
@ -39,7 +36,7 @@ class CmdCancel(private val chunkmaster: Chunkmaster): Subcommand {
}
if (index != null && chunkmaster.generationManager.removeTask(index)) {
sender.sendMessage(chunkmaster.langManager.getLocalized("TASK_CANCELED", index))
sender.sendMessage(chunkmaster.langManager.getLocalized("TASK_CANCELLED", index))
true
} else {
sender.sendMessage(chunkmaster.langManager.getLocalized("TASK_NOT_FOUND", args[0]))
@ -47,7 +44,7 @@ class CmdCancel(private val chunkmaster: Chunkmaster): Subcommand {
}
} else {
sender.sendMessage(chunkmaster.langManager.getLocalized("TASK_ID_REQUIRED"));
sender.sendMessage(chunkmaster.langManager.getLocalized("TASK_ID_REQUIRED"))
false
}
}

@ -0,0 +1,44 @@
package net.trivernis.chunkmaster.commands
import net.trivernis.chunkmaster.Chunkmaster
import net.trivernis.chunkmaster.lib.Subcommand
import org.bukkit.command.Command
import org.bukkit.command.CommandSender
class CmdCompleted(private val plugin: Chunkmaster) : Subcommand {
override val name = "completed"
override fun execute(sender: CommandSender, args: List<String>): Boolean {
plugin.sqliteManager.completedGenerationTasks.getCompletedTasks().thenAccept { tasks ->
val worlds = tasks.map { it.world }.toHashSet()
var response = "\n" + plugin.langManager.getLocalized("COMPLETED_TASKS_HEADER") + "\n\n"
for (world in worlds) {
response += plugin.langManager.getLocalized("COMPLETED_WORLD_HEADER", world) + "\n"
for (task in tasks.filter { it.world == world }) {
response += plugin.langManager.getLocalized(
"COMPLETED_TASK_ENTRY",
task.id,
task.radius,
task.center.x,
task.center.z,
task.shape
) + "\n"
}
response += "\n"
}
sender.sendMessage(response)
}
return true
}
override fun onTabComplete(
sender: CommandSender,
command: Command,
alias: String,
args: List<String>
): MutableList<String> {
return mutableListOf()
}
}

@ -6,7 +6,7 @@ import org.bukkit.command.Command
import org.bukkit.command.CommandSender
import org.bukkit.entity.Player
class CmdGenerate(private val chunkmaster: Chunkmaster): Subcommand {
class CmdGenerate(private val chunkmaster: Chunkmaster) : Subcommand {
override val name = "generate"
/**
@ -20,14 +20,14 @@ class CmdGenerate(private val chunkmaster: Chunkmaster): Subcommand {
): MutableList<String> {
if (args.size == 1) {
return sender.server.worlds.filter { it.name.indexOf(args[0]) == 0 }
.map {it.name}.toMutableList()
.map { it.name }.toMutableList()
} else if (args.size == 2) {
if (args[0].toIntOrNull() != null) {
return shapes.filter {it.indexOf(args[1]) == 0}.toMutableList()
return shapes.filter { it.indexOf(args[1]) == 0 }.toMutableList()
}
} else if (args.size > 2) {
if (args[1].toIntOrNull() != null) {
return shapes.filter {it.indexOf(args[2]) == 0}.toMutableList()
return shapes.filter { it.indexOf(args[2]) == 0 }.toMutableList()
}
}
return emptyList<String>().toMutableList()
@ -97,20 +97,23 @@ class CmdGenerate(private val chunkmaster: Chunkmaster): Subcommand {
val world = chunkmaster.server.getWorld(worldName)
val allTasks = chunkmaster.generationManager.allTasks
return if (world != null && (allTasks.find { it.generationTask.world == world }) == null) {
chunkmaster.generationManager.addTask(world, if (blockRadius > 0) blockRadius/16 else -1 , shape)
sender.sendMessage(chunkmaster.langManager
.getLocalized("TASK_CREATION_SUCCESS",
worldName,
if (blockRadius > 0) {
chunkmaster.langManager.getLocalized("TASK_UNIT_RADIUS", blockRadius)
} else{
chunkmaster.langManager.getLocalized("TASK_UNIT_WORLDBORDER")
},
shape
))
chunkmaster.generationManager.addTask(world, if (blockRadius > 0) blockRadius / 16 else -1, shape)
sender.sendMessage(
chunkmaster.langManager
.getLocalized(
"TASK_CREATION_SUCCESS",
worldName,
if (blockRadius > 0) {
chunkmaster.langManager.getLocalized("TASK_UNIT_RADIUS", blockRadius)
} else {
chunkmaster.langManager.getLocalized("TASK_UNIT_WORLDBORDER")
},
shape
)
)
true
} else if (world == null){
sender.sendMessage(chunkmaster.langManager.getLocalized("WORLD_NOT_FOUND", worldName));
} else if (world == null) {
sender.sendMessage(chunkmaster.langManager.getLocalized("WORLD_NOT_FOUND", worldName))
false
} else {
sender.sendMessage(chunkmaster.langManager.getLocalized("TASK_ALREADY_EXISTS", worldName))

@ -6,8 +6,8 @@ import org.bukkit.command.Command
import org.bukkit.command.CommandSender
import org.bukkit.entity.Player
class CmdGetCenter(private val chunkmaster: Chunkmaster): Subcommand {
override val name = "getCenter";
class CmdGetCenter(private val chunkmaster: Chunkmaster) : Subcommand {
override val name = "getCenter"
override fun onTabComplete(
sender: CommandSender,
@ -17,7 +17,7 @@ class CmdGetCenter(private val chunkmaster: Chunkmaster): Subcommand {
): MutableList<String> {
if (args.size == 1) {
return sender.server.worlds.filter { it.name.indexOf(args[0]) == 0 }
.map {it.name}.toMutableList()
.map { it.name }.toMutableList()
}
return emptyList<String>().toMutableList()
}
@ -27,7 +27,7 @@ class CmdGetCenter(private val chunkmaster: Chunkmaster): Subcommand {
if (args.isNotEmpty()) {
args[0]
} else {
sender.world.name;
sender.world.name
}
} else {
if (args.isEmpty()) {
@ -37,12 +37,6 @@ class CmdGetCenter(private val chunkmaster: Chunkmaster): Subcommand {
args[0]
}
}
if (chunkmaster.generationManager.worldCenters.isEmpty()) {
chunkmaster.generationManager.loadWorldCenters() {
sendCenterInfo(sender, worldName)
}
return true
}
sendCenterInfo(sender, worldName)
return true
}
@ -51,15 +45,24 @@ class CmdGetCenter(private val chunkmaster: Chunkmaster): Subcommand {
* Sends the center information
*/
private fun sendCenterInfo(sender: CommandSender, worldName: String) {
var center = chunkmaster.generationManager.worldCenters[worldName]
if (center == null) {
val world = sender.server.worlds.find { it.name == worldName }
if (world == null) {
sender.sendMessage(chunkmaster.langManager.getLocalized("WORLD_NOT_FOUND", worldName))
return
chunkmaster.generationManager.worldProperties.getWorldCenter(worldName).thenAccept { worldCenter ->
var center = worldCenter
if (center == null) {
val world = sender.server.worlds.find { it.name == worldName }
if (world == null) {
sender.sendMessage(chunkmaster.langManager.getLocalized("WORLD_NOT_FOUND", worldName))
return@thenAccept
}
center = Pair(world.spawnLocation.chunk.x, world.spawnLocation.chunk.z)
}
center = Pair(world.spawnLocation.chunk.x, world.spawnLocation.chunk.z)
sender.sendMessage(
chunkmaster.langManager.getLocalized(
"CENTER_INFO",
worldName,
center.first,
center.second
)
)
}
sender.sendMessage(chunkmaster.langManager.getLocalized("CENTER_INFO", worldName, center.first, center.second))
}
}

@ -2,11 +2,12 @@ package net.trivernis.chunkmaster.commands
import net.trivernis.chunkmaster.Chunkmaster
import net.trivernis.chunkmaster.lib.Subcommand
import net.trivernis.chunkmaster.lib.generation.TaskEntry
import net.trivernis.chunkmaster.lib.generation.taskentry.TaskEntry
import org.bukkit.command.Command
import org.bukkit.command.CommandSender
import kotlin.math.ceil
class CmdList(private val chunkmaster: Chunkmaster): Subcommand {
class CmdList(private val chunkmaster: Chunkmaster) : Subcommand {
override val name = "list"
override fun onTabComplete(
@ -48,11 +49,18 @@ class CmdList(private val chunkmaster: Chunkmaster): Subcommand {
*/
private fun getGenerationEntry(task: TaskEntry): String {
val genTask = task.generationTask
val percentage = if (genTask.radius > 0)
" (%.1f".format(genTask.shape.progress()*100) + "%)."
else
""
return "\n" + chunkmaster.langManager.getLocalized("TASKS_ENTRY",
task.id, genTask.world.name, genTask.count, percentage)
val progress =
genTask.shape.progress(if (genTask.radius < 0) (genTask.world.worldBorder.size / 32).toInt() else null)
val percentage = " (%.1f".format(progress * 100) + "%)."
val count = if (genTask.radius > 0) {
"${genTask.count} / ${ceil(genTask.shape.total()).toInt()}"
} else {
"${genTask.count} / worldborder"
}
return "\n" + chunkmaster.langManager.getLocalized(
"TASKS_ENTRY",
task.id, genTask.world.name, genTask.state.toString(), count, percentage
)
}
}

@ -1,7 +1,5 @@
package net.trivernis.chunkmaster.commands
import net.md_5.bungee.api.ChatColor
import net.md_5.bungee.api.chat.ComponentBuilder
import net.trivernis.chunkmaster.Chunkmaster
import net.trivernis.chunkmaster.lib.Subcommand
import org.bukkit.command.Command

@ -1,13 +1,11 @@
package net.trivernis.chunkmaster.commands
import net.md_5.bungee.api.ChatColor
import net.md_5.bungee.api.chat.ComponentBuilder
import net.trivernis.chunkmaster.Chunkmaster
import net.trivernis.chunkmaster.lib.Subcommand
import org.bukkit.command.Command
import org.bukkit.command.CommandSender
class CmdReload(private val chunkmaster: Chunkmaster): Subcommand {
class CmdReload(private val chunkmaster: Chunkmaster) : Subcommand {
override val name = "reload"
override fun onTabComplete(

@ -1,13 +1,11 @@
package net.trivernis.chunkmaster.commands
import net.md_5.bungee.api.ChatColor
import net.md_5.bungee.api.chat.ComponentBuilder
import net.trivernis.chunkmaster.Chunkmaster
import net.trivernis.chunkmaster.lib.Subcommand
import org.bukkit.command.Command
import org.bukkit.command.CommandSender
class CmdResume(private val chunkmaster: Chunkmaster): Subcommand {
class CmdResume(private val chunkmaster: Chunkmaster) : Subcommand {
override val name = "resume"
override fun onTabComplete(

@ -6,8 +6,8 @@ import org.bukkit.command.Command
import org.bukkit.command.CommandSender
import org.bukkit.entity.Player
class CmdSetCenter(private val chunkmaster: Chunkmaster): Subcommand {
override val name = "setCenter";
class CmdSetCenter(private val chunkmaster: Chunkmaster) : Subcommand {
override val name = "setCenter"
override fun onTabComplete(
sender: CommandSender,
@ -18,10 +18,10 @@ class CmdSetCenter(private val chunkmaster: Chunkmaster): Subcommand {
if (args.size == 1) {
if (args[0].toIntOrNull() == null) {
return sender.server.worlds.filter { it.name.indexOf(args[0]) == 0 }
.map {it.name}.toMutableList()
.map { it.name }.toMutableList()
}
}
return emptyList<String>().toMutableList();
return emptyList<String>().toMutableList()
}
override fun execute(sender: CommandSender, args: List<String>): Boolean {
@ -36,6 +36,11 @@ class CmdSetCenter(private val chunkmaster: Chunkmaster): Subcommand {
centerX = sender.location.chunk.x
centerZ = sender.location.chunk.z
}
args.size == 1 -> {
world = args[0]
centerX = sender.location.chunk.x
centerZ = sender.location.chunk.z
}
args.size == 2 -> {
world = sender.world.name
if (args[0].toIntOrNull() == null || args[1].toIntOrNull() == null) {
@ -67,7 +72,7 @@ class CmdSetCenter(private val chunkmaster: Chunkmaster): Subcommand {
centerZ = args[2].toInt()
}
}
chunkmaster.generationManager.updateWorldCenter(world, Pair(centerX, centerZ))
chunkmaster.generationManager.worldProperties.setWorldCenter(world, Pair(centerX, centerZ))
sender.sendMessage(chunkmaster.langManager.getLocalized("CENTER_UPDATED", world, centerX, centerZ))
return true
}

@ -5,9 +5,8 @@ import net.trivernis.chunkmaster.lib.Subcommand
import org.bukkit.World
import org.bukkit.command.Command
import org.bukkit.command.CommandSender
import org.bukkit.entity.Player
class CmdStats(private val chunkmaster: Chunkmaster): Subcommand {
class CmdStats(private val chunkmaster: Chunkmaster) : Subcommand {
override val name = "stats"
override fun onTabComplete(
@ -23,8 +22,10 @@ class CmdStats(private val chunkmaster: Chunkmaster): Subcommand {
if (args.isNotEmpty()) {
val world = sender.server.getWorld(args[0])
if (world == null) {
sender.sendMessage(chunkmaster.langManager.getLocalized("STATS_HEADER") + "\n" +
chunkmaster.langManager.getLocalized("WORLD_NOT_FOUND", args[0]))
sender.sendMessage(
chunkmaster.langManager.getLocalized("STATS_HEADER") + "\n" +
chunkmaster.langManager.getLocalized("WORLD_NOT_FOUND", args[0])
)
return false
}
sender.sendMessage(getWorldStatsMessage(sender, world))
@ -36,16 +37,16 @@ class CmdStats(private val chunkmaster: Chunkmaster): Subcommand {
}
private fun getWorldStatsMessage(sender: CommandSender, world: World): String {
var message = """
return """
${chunkmaster.langManager.getLocalized("STATS_WORLD_NAME", world.name)}
${chunkmaster.langManager.getLocalized("STATS_ENTITY_COUNT", world.entities.size)}
${chunkmaster.langManager.getLocalized("STATS_LOADED_CHUNKS", world.loadedChunks.size)}
""".trimIndent()
val task = chunkmaster.generationManager.tasks.find { it.generationTask.world == world }
if (task != null) {
message += "\n" + chunkmaster.langManager.getLocalized("STATS_PLUGIN_LOADED_CHUNKS", task.generationTask.loadedChunksCount)
${
chunkmaster.langManager.getLocalized(
"STATS_GENERATING",
chunkmaster.generationManager.tasks.find { it.generationTask.world == world } != null)
}
return message
""".trimIndent()
}
private fun getServerStatsMessage(sender: CommandSender): String {
@ -57,8 +58,22 @@ class CmdStats(private val chunkmaster: Chunkmaster): Subcommand {
${chunkmaster.langManager.getLocalized("STATS_SERVER")}
${chunkmaster.langManager.getLocalized("STATS_SERVER_VERSION", sender.server.version)}
${chunkmaster.langManager.getLocalized("STATS_PLUGIN_VERSION", chunkmaster.description.version)}
${chunkmaster.langManager.getLocalized("STATS_MEMORY", memUsed/1000000, runtime.maxMemory()/1000000, (memUsed.toFloat()/runtime.maxMemory().toFloat()) * 100)}
${
chunkmaster.langManager.getLocalized(
"STATS_MEMORY",
memUsed / 1000000,
runtime.maxMemory() / 1000000,
(memUsed.toFloat() / runtime.maxMemory().toFloat()) * 100
)
}
${chunkmaster.langManager.getLocalized("STATS_CORES", runtime.availableProcessors())}
${
chunkmaster.langManager.getLocalized(
"STATS_PLUGIN_LOADED_CHUNKS",
chunkmaster.generationManager.loadedChunkCount
)
}
""".trimIndent()
for (world in sender.server.worlds) {
message += "\n\n" + getWorldStatsMessage(sender, world)

@ -1,8 +1,6 @@
package net.trivernis.chunkmaster.commands
import io.papermc.lib.PaperLib
import net.md_5.bungee.api.ChatColor
import net.md_5.bungee.api.chat.ComponentBuilder
import net.trivernis.chunkmaster.Chunkmaster
import net.trivernis.chunkmaster.lib.Subcommand
import org.bukkit.Material
@ -10,7 +8,7 @@ import org.bukkit.command.Command
import org.bukkit.command.CommandSender
import org.bukkit.entity.Player
class CmdTpChunk(private val chunkmaster: Chunkmaster): Subcommand {
class CmdTpChunk(private val chunkmaster: Chunkmaster) : Subcommand {
override val name = "tpchunk"
override fun onTabComplete(

@ -1,8 +1,7 @@
package net.trivernis.chunkmaster.commands
import net.md_5.bungee.api.ChatColor
import net.md_5.bungee.api.chat.ComponentBuilder
import net.trivernis.chunkmaster.Chunkmaster
import net.trivernis.chunkmaster.lib.ArgParser
import net.trivernis.chunkmaster.lib.Subcommand
import org.bukkit.Server
import org.bukkit.command.Command
@ -13,6 +12,7 @@ import org.bukkit.command.TabCompleter
class CommandChunkmaster(private val chunkmaster: Chunkmaster, private val server: Server) : CommandExecutor,
TabCompleter {
private val commands = HashMap<String, Subcommand>()
private val argParser = ArgParser()
init {
registerCommands()
@ -38,7 +38,14 @@ class CommandChunkmaster(private val chunkmaster: Chunkmaster, private val serve
/**
* /chunkmaster command to handle all commands
*/
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>): Boolean {
override fun onCommand(
sender: CommandSender,
command: Command,
label: String,
bukkitArgs: Array<out String>
): Boolean {
val args = argParser.parseArguments(bukkitArgs.joinToString(" "))
if (args.isNotEmpty()) {
if (sender.hasPermission("chunkmaster.${args[0].toLowerCase()}")) {
return if (commands.containsKey(args[0])) {
@ -89,5 +96,8 @@ class CommandChunkmaster(private val chunkmaster: Chunkmaster, private val serve
val cmdStats = CmdStats(chunkmaster)
commands[cmdStats.name] = cmdStats
val cmdCompleted = CmdCompleted(chunkmaster)
commands[cmdCompleted.name] = cmdCompleted
}
}

@ -0,0 +1,92 @@
package net.trivernis.chunkmaster.lib
/**
* Better argument parser for command arguments
*/
class ArgParser {
private var input = ""
private var position = 0
private var currentChar = ' '
private var escaped = false
/**
* Parses arguments from a string and respects quotes
*/
fun parseArguments(arguments: String): List<String> {
if (arguments.isEmpty()) {
return emptyList()
}
input = arguments
position = 0
currentChar = input[position]
escaped = false
val args = ArrayList<String>()
var arg = ""
while (!endReached()) {
nextCharacter()
if (currentChar == '\\' && !escaped) {
escaped = true
continue
}
if (currentChar.isWhitespace() && !escaped) {
if (arg.isNotBlank()) {
args.add(arg)
}
arg = ""
} else if (currentChar == '"' && !escaped) {
if (arg.isNotBlank()) {
args.add(arg)
}
arg = parseString()
if (arg.isNotBlank()) {
args.add(arg)
}
arg = ""
} else {
arg += currentChar
}
escaped = false
}
if (arg.isNotBlank()) {
args.add(arg)
}
return args
}
/**
* Parses an enquoted string
*/
private fun parseString(): String {
var output = ""
while (!endReached()) {
nextCharacter()
if (currentChar == '\\') {
escaped = !escaped
continue
}
if (currentChar == '"' && !escaped) {
break
}
output += currentChar
escaped = false
}
return output
}
private fun nextCharacter() {
if (!endReached()) {
currentChar = input[position++]
}
}
private fun endReached(): Boolean {
return position >= input.length
}
}

@ -1,8 +1,8 @@
package net.trivernis.chunkmaster.lib
import net.trivernis.chunkmaster.Chunkmaster
import java.lang.Exception
import java.util.Properties
import java.io.*
import java.util.*
class LanguageManager(private val plugin: Chunkmaster) {
private val langProps = Properties()
@ -20,12 +20,14 @@ class LanguageManager(private val plugin: Chunkmaster) {
val file = File(langFile)
val loader = Thread.currentThread().contextClassLoader
val defaultStream = this.javaClass.getResourceAsStream("/i18n/DEFAULT.i18n.properties")
if (defaultStream != null) {
langProps.load(getReaderForProperties(defaultStream))
defaultStream.close()
} else {
plugin.logger.severe("Couldn't load default language properties.")
}
if (file.exists()) {
try {
val inputStream = loader.getResourceAsStream(langFile)
@ -34,7 +36,7 @@ class LanguageManager(private val plugin: Chunkmaster) {
langFileLoaded = true
inputStream.close()
}
} catch (e: Exception) {
} catch (e: Exception) {
plugin.logger.warning("Language file $langFile could not be loaded!")
plugin.logger.fine(e.toString())
}
@ -54,8 +56,13 @@ class LanguageManager(private val plugin: Chunkmaster) {
* Returns a localized message with replacements
*/
fun getLocalized(key: String, vararg replacements: Any): String {
val localizedString = langProps.getProperty(key)
return String.format(localizedString, *replacements)
try {
val localizedString = langProps.getProperty(key)
return String.format(localizedString, *replacements)
} catch (e: NullPointerException) {
plugin.logger.severe("Failed to get localized entry for $key")
throw e
}
}
/**

@ -0,0 +1,11 @@
package net.trivernis.chunkmaster.lib.database
import net.trivernis.chunkmaster.lib.generation.ChunkCoordinates
data class CompletedGenerationTask(
val id: Int,
val world: String,
val radius: Int,
val center: ChunkCoordinates,
val shape: String
)

@ -0,0 +1,80 @@
package net.trivernis.chunkmaster.lib.database
import net.trivernis.chunkmaster.lib.generation.ChunkCoordinates
import java.sql.ResultSet
import java.util.concurrent.CompletableFuture
class CompletedGenerationTasks(private val sqliteManager: SqliteManager) {
/**
* Returns the list of all completed tasks
*/
fun getCompletedTasks(): CompletableFuture<List<CompletedGenerationTask>> {
val completableFuture = CompletableFuture<List<CompletedGenerationTask>>()
sqliteManager.executeStatement("SELECT * FROM completed_generation_tasks", HashMap()) { res ->
val tasks = ArrayList<CompletedGenerationTask>()
while (res!!.next()) {
tasks.add(mapSqlResponseToWrapperObject(res))
}
completableFuture.complete(tasks)
}
return completableFuture
}
/**
* Returns a list of completed tasks for a world
*/
fun getCompletedTasksForWorld(world: String): CompletableFuture<List<CompletedGenerationTask>> {
val completableFuture = CompletableFuture<List<CompletedGenerationTask>>()
sqliteManager.executeStatement(
"SELECT * FROM completed_generation_tasks WHERE world = ?",
hashMapOf(1 to world)
) { res ->
val tasks = ArrayList<CompletedGenerationTask>()
while (res!!.next()) {
tasks.add(mapSqlResponseToWrapperObject(res))
}
completableFuture.complete(tasks)
}
return completableFuture
}
private fun mapSqlResponseToWrapperObject(res: ResultSet): CompletedGenerationTask {
val id = res.getInt("id")
val world = res.getString("world")
val center = ChunkCoordinates(res.getInt("center_x"), res.getInt("center_z"))
val radius = res.getInt("completed_radius")
val shape = res.getString("shape")
return CompletedGenerationTask(id, world, radius, center, shape)
}
/**
* Adds a completed task
*/
fun addCompletedTask(
id: Int,
world: String,
radius: Int,
center: ChunkCoordinates,
shape: String
): CompletableFuture<Void> {
val completableFuture = CompletableFuture<Void>()
sqliteManager.executeStatement(
"INSERT INTO completed_generation_tasks (id, world, completed_radius, center_x, center_z, shape) VALUES (?, ?, ?, ?, ?, ?)",
hashMapOf(
1 to id,
2 to world,
3 to radius,
4 to center.x,
5 to center.z,
6 to shape,
)
) {
completableFuture.complete(null)
}
return completableFuture
}
}

@ -0,0 +1,14 @@
package net.trivernis.chunkmaster.lib.database
import net.trivernis.chunkmaster.lib.generation.ChunkCoordinates
import net.trivernis.chunkmaster.lib.generation.TaskState
data class GenerationTaskData(
val id: Int,
val world: String,
val radius: Int,
val shape: String,
val state: TaskState,
val center: ChunkCoordinates,
val last: ChunkCoordinates
)

@ -0,0 +1,104 @@
package net.trivernis.chunkmaster.lib.database
import net.trivernis.chunkmaster.lib.generation.ChunkCoordinates
import net.trivernis.chunkmaster.lib.generation.TaskState
import java.util.concurrent.CompletableFuture
class GenerationTasks(private val sqliteManager: SqliteManager) {
/**
* Returns all stored generation tasks
*/
fun getGenerationTasks(): CompletableFuture<List<GenerationTaskData>> {
val completableFuture = CompletableFuture<List<GenerationTaskData>>()
sqliteManager.executeStatement("SELECT * FROM generation_tasks", HashMap()) { res ->
val tasks = ArrayList<GenerationTaskData>()
while (res!!.next()) {
val id = res.getInt("id")
val world = res.getString("world")
val center = ChunkCoordinates(res.getInt("center_x"), res.getInt("center_z"))
val last = ChunkCoordinates(res.getInt("last_x"), res.getInt("last_z"))
val radius = res.getInt("radius")
val shape = res.getString("shape")
val state = stringToState(res.getString("state"))
val taskData = GenerationTaskData(id, world, radius, shape, state, center, last)
if (tasks.find { it.id == id } == null) {
tasks.add(taskData)
}
}
completableFuture.complete(tasks)
}
return completableFuture
}
/**
* Adds a generation task to the database
*/
fun addGenerationTask(world: String, center: ChunkCoordinates, radius: Int, shape: String): CompletableFuture<Int> {
val completableFuture = CompletableFuture<Int>()
sqliteManager.executeStatement(
"""
INSERT INTO generation_tasks (center_x, center_z, last_x, last_z, world, radius, shape)
values (?, ?, ?, ?, ?, ?, ?)""",
hashMapOf(
1 to center.x,
2 to center.z,
3 to center.x,
4 to center.z,
5 to world,
6 to radius,
7 to shape
)
) {
sqliteManager.executeStatement(
"""
SELECT id FROM generation_tasks ORDER BY id DESC LIMIT 1
""".trimIndent(), HashMap()
) {
it!!.next()
completableFuture.complete(it.getInt("id"))
}
}
return completableFuture
}
/**
* Deletes a generationTask from the database
*/
fun deleteGenerationTask(id: Int): CompletableFuture<Void> {
val completableFuture = CompletableFuture<Void>()
sqliteManager.executeStatement("DELETE FROM generation_tasks WHERE id = ?;", hashMapOf(1 to id)) {
completableFuture.complete(null)
}
return completableFuture
}
fun updateGenerationTask(id: Int, last: ChunkCoordinates, state: TaskState): CompletableFuture<Void> {
val completableFuture = CompletableFuture<Void>()
sqliteManager.executeStatement(
"""
UPDATE generation_tasks SET last_x = ?, last_z = ?, state = ?
WHERE id = ?
""".trimIndent(),
hashMapOf(1 to last.x, 2 to last.z, 3 to state.toString(), 4 to id)
) {
completableFuture.complete(null)
}
return completableFuture
}
/**
* Converts a string into a task state
*/
private fun stringToState(stringState: String): TaskState {
TaskState.valueOf(stringState)
return when (stringState) {
"GENERATING" -> TaskState.GENERATING
"VALIDATING" -> TaskState.VALIDATING
"PAUSING" -> TaskState.PAUSING
"CORRECTING" -> TaskState.CORRECTING
else -> TaskState.GENERATING
}
}
}

@ -0,0 +1,84 @@
package net.trivernis.chunkmaster.lib.database
import net.trivernis.chunkmaster.lib.generation.ChunkCoordinates
import java.util.concurrent.CompletableFuture
import kotlin.math.ceil
class PendingChunks(private val sqliteManager: SqliteManager) {
private val insertionCount = 300
/**
* Returns a list of pending chunks for a taskId
*/
fun getPendingChunks(taskId: Int): CompletableFuture<List<ChunkCoordinates>> {
val completableFuture = CompletableFuture<List<ChunkCoordinates>>()
sqliteManager.executeStatement("SELECT * FROM pending_chunks WHERE task_id = ?", hashMapOf(1 to taskId)) {
val pendingChunks = ArrayList<ChunkCoordinates>()
while (it!!.next()) {
pendingChunks.add(ChunkCoordinates(it.getInt("chunk_x"), it.getInt("chunk_z")))
}
completableFuture.complete(pendingChunks)
}
return completableFuture
}
/**
* Clears all pending chunks of a task
*/
fun clearPendingChunks(taskId: Int): CompletableFuture<Void> {
val completableFuture = CompletableFuture<Void>()
sqliteManager.executeStatement("DELETE FROM pending_chunks WHERE task_id = ?", hashMapOf(1 to taskId)) {
completableFuture.complete(null)
}
return completableFuture
}
fun addPendingChunks(taskId: Int, pendingChunks: List<ChunkCoordinates>): CompletableFuture<Void> {
val futures = ArrayList<CompletableFuture<Void>>()
val statementCount = ceil(pendingChunks.size.toDouble() / insertionCount).toInt()
for (i in 0 until statementCount) {
futures.add(
insertPendingChunks(
taskId,
pendingChunks.subList(
i * insertionCount,
((i * insertionCount) + insertionCount).coerceAtMost(pendingChunks.size)
)
)
)
}
if (futures.size > 0) {
return CompletableFuture.allOf(*futures.toTypedArray())
} else {
return CompletableFuture.supplyAsync { null }
}
}
/**
* Adds pending chunks for a taskid
*/
private fun insertPendingChunks(taskId: Int, pendingChunks: List<ChunkCoordinates>): CompletableFuture<Void> {
val completableFuture = CompletableFuture<Void>()
if (pendingChunks.isEmpty()) {
completableFuture.complete(null)
} else {
var sql = "INSERT INTO pending_chunks (task_id, chunk_x, chunk_z) VALUES"
var index = 1
val valueMap = HashMap<Int, Any>()
for (coordinates in pendingChunks) {
sql += "(?, ?, ?),"
valueMap[index++] = taskId
valueMap[index++] = coordinates.x
valueMap[index++] = coordinates.z
}
sqliteManager.executeStatement(sql.removeSuffix(","), valueMap) {
completableFuture.complete(null)
}
}
return completableFuture
}
}

@ -1,157 +1,201 @@
package net.trivernis.chunkmaster.lib
import net.trivernis.chunkmaster.Chunkmaster
import org.apache.commons.lang.exception.ExceptionUtils
import org.sqlite.SQLiteConnection
import java.lang.Exception
import java.sql.Connection
import java.sql.DriverManager
import java.sql.PreparedStatement
import java.sql.ResultSet
class SqliteManager(private val chunkmaster: Chunkmaster) {
private val tables = listOf(
Pair(
"generation_tasks",
listOf(
Pair("id", "integer PRIMARY KEY AUTOINCREMENT"),
Pair("center_x", "integer NOT NULL DEFAULT 0"),
Pair("center_z", "integer NOT NULL DEFAULT 0"),
Pair("last_x", "integer NOT NULL DEFAULT 0"),
Pair("last_z", "integer NOT NULL DEFAULT 0"),
Pair("world", "text UNIQUE NOT NULL DEFAULT 'world'"),
Pair("radius", "integer DEFAULT -1"),
Pair("shape", "text NOT NULL DEFAULT 'square'")
)
),
Pair(
"world_properties",
listOf(
Pair("name", "text PRIMARY KEY"),
Pair("center_x", "integer NOT NULL DEFAULT 0"),
Pair("center_z", "integer NOT NULL DEFAULT 0")
)
)
)
private val needUpdate = HashSet<Pair<String, Pair<String, String>>>()
private val needCreation = HashSet<String>()
private var connection: Connection? = null
private var activeTasks = 0
/**
* Returns the connection to the database
*/
fun getConnection(): Connection? {
if (this.connection != null) {
return this.connection
}
try {
Class.forName("org.sqlite.JDBC")
this.connection = DriverManager.getConnection("jdbc:sqlite:${chunkmaster.dataFolder.absolutePath}/" +
"${chunkmaster.config.getString("database.filename")}")
return this.connection
} catch (e: Exception) {
chunkmaster.logger.severe(chunkmaster.langManager.getLocalized("DATABASE_CONNECTION_ERROR"))
chunkmaster.logger.severe(e.message)
}
return null
}
/**
* Checks for and performs an update
*/
fun init() {
this.checkUpdate()
this.performUpdate()
}
/**
* Checks which tables need an update or creation.
*/
private fun checkUpdate() {
val meta = getConnection()!!.metaData
for (table in tables) {
val resTables = meta.getTables(null, null, table.first, null)
if (resTables.next()) { // table exists
for (column in table.second) {
val resColumn = meta.getColumns(null, null, table.first, column.first)
if (!resColumn.next()) {
needUpdate.add(Pair(table.first, column))
}
resColumn.close()
}
} else {
needCreation.add(table.first)
}
resTables.close()
}
}
/**
* Executes a sql statement on the database.
*/
fun executeStatement(sql: String, values: HashMap<Int, Any>, callback: ((ResultSet) -> Unit)?) {
val connection = getConnection()
activeTasks++
if (connection != null) {
try {
val statement = connection.prepareStatement(sql)
for (parameterValue in values) {
statement.setObject(parameterValue.key, parameterValue.value)
}
statement.execute()
val res = statement.resultSet
if (callback != null) {
callback(res)
}
statement.close()
} catch (e: Exception) {
chunkmaster.logger.severe(chunkmaster.langManager.getLocalized("SQL_ERROR", e.toString()))
chunkmaster.logger.info(ExceptionUtils.getStackTrace(e))
} finally {
activeTasks--
if (activeTasks == 0) {
connection.close()
this.connection = null
}
}
} else {
chunkmaster.logger.severe(chunkmaster.langManager.getLocalized("NO_DATABASE_CONNECTION"))
}
}
/**
* Creates or updates tables that needed an update.
*/
private fun performUpdate() {
for (table in needCreation) {
try {
var tableDef = "CREATE TABLE IF NOT EXISTS $table ("
for (column in tables.find{it.first == table}!!.second) {
tableDef += "${column.first} ${column.second},"
}
tableDef = tableDef.substringBeforeLast(",") + ");"
chunkmaster.logger.finest(chunkmaster.langManager.getLocalized("CREATE_TABLE_DEFINITION", table, tableDef))
executeStatement(tableDef, HashMap(), null)
} catch (e: Exception) {
chunkmaster.logger.severe(chunkmaster.langManager.getLocalized("TABLE_CREATE_ERROR", table))
chunkmaster.logger.severe(e.message)
chunkmaster.logger.info(ExceptionUtils.getStackTrace(e))
}
}
for (table in needUpdate) {
val updateSql = "ALTER TABLE ${table.first} ADD COLUMN ${table.second.first} ${table.second.second}"
try {
executeStatement(updateSql, HashMap(), null)
chunkmaster.logger.finest(chunkmaster.langManager.getLocalized("UPDATE_TABLE_DEFINITION", table.first, updateSql))
} catch (e: Exception) {
chunkmaster.logger.severe(chunkmaster.langManager.getLocalized("UPDATE_TABLE_FAILED", table.first, updateSql))
chunkmaster.logger.severe(e.message)
chunkmaster.logger.info(ExceptionUtils.getStackTrace(e))
}
}
}
package net.trivernis.chunkmaster.lib.database
import net.trivernis.chunkmaster.Chunkmaster
import org.apache.commons.lang.exception.ExceptionUtils
import java.sql.Connection
import java.sql.DriverManager
import java.sql.ResultSet
class SqliteManager(private val chunkmaster: Chunkmaster) {
private val tables = listOf(
Pair(
"generation_tasks",
listOf(
Pair("id", "integer PRIMARY KEY AUTOINCREMENT"),
Pair("center_x", "integer NOT NULL DEFAULT 0"),
Pair("center_z", "integer NOT NULL DEFAULT 0"),
Pair("last_x", "integer NOT NULL DEFAULT 0"),
Pair("last_z", "integer NOT NULL DEFAULT 0"),
Pair("world", "text UNIQUE NOT NULL DEFAULT 'world'"),
Pair("radius", "integer DEFAULT -1"),
Pair("shape", "text NOT NULL DEFAULT 'square'"),
Pair("state", "text NOT NULL DEFAULT 'GENERATING'")
)
),
Pair(
"world_properties",
listOf(
Pair("name", "text PRIMARY KEY"),
Pair("center_x", "integer NOT NULL DEFAULT 0"),
Pair("center_z", "integer NOT NULL DEFAULT 0")
)
),
Pair(
"pending_chunks",
listOf(
Pair("id", "integer PRIMARY KEY AUTOINCREMENT"),
Pair("task_id", "integer NOT NULL"),
Pair("chunk_x", "integer NOT NULL"),
Pair("chunk_z", "integer NOT NULL")
)
),
Pair(
"completed_generation_tasks",
listOf(
Pair("id", "integer PRIMARY KEY"),
Pair("world", "text NOT NULL"),
Pair("completed_radius", "integer NOT NULL"),
Pair("center_x", "integer NOT NULL"),
Pair("center_z", "integer NOT NULL"),
Pair("shape", "text NOT NULL")
)
)
)
private val needUpdate = HashSet<Pair<String, Pair<String, String>>>()
private val needCreation = HashSet<String>()
private var connection: Connection? = null
private var activeTasks = 0
val worldProperties = WorldProperties(this)
val pendingChunks = PendingChunks(this)
val generationTasks = GenerationTasks(this)
val completedGenerationTasks = CompletedGenerationTasks(this)
/**
* Returns the connection to the database
*/
fun getConnection(): Connection? {
if (this.connection != null) {
return this.connection
}
try {
Class.forName("org.sqlite.JDBC")
this.connection = DriverManager.getConnection(
"jdbc:sqlite:${chunkmaster.dataFolder.absolutePath}/" +
"${chunkmaster.config.getString("database.filename")}"
)
return this.connection
} catch (e: Exception) {
chunkmaster.logger.severe(chunkmaster.langManager.getLocalized("DATABASE_CONNECTION_ERROR"))
chunkmaster.logger.severe(e.message)
}
return null
}
/**
* Checks for and performs an update
*/
fun init() {
this.checkUpdate()
this.performUpdate()
}
/**
* Checks which tables need an update or creation.
*/
private fun checkUpdate() {
val meta = getConnection()!!.metaData
for (table in tables) {
val resTables = meta.getTables(null, null, table.first, null)
if (resTables.next()) { // table exists
for (column in table.second) {
val resColumn = meta.getColumns(null, null, table.first, column.first)
if (!resColumn.next()) {
needUpdate.add(Pair(table.first, column))
}
resColumn.close()
}
} else {
needCreation.add(table.first)
}
resTables.close()
}
}
/**
* Executes a sql statement on the database.
*/
fun executeStatement(sql: String, values: HashMap<Int, Any>, callback: ((ResultSet?) -> Unit)?) {
val connection = getConnection()
activeTasks++
if (connection != null) {
try {
//println("'$sql' with values $values")
val statement = connection.prepareStatement(sql)
for (parameterValue in values) {
statement.setObject(parameterValue.key, parameterValue.value)
}
statement.execute()
val res: ResultSet? = statement.resultSet
if (callback != null) {
callback(res)
}
statement.close()
} catch (e: Exception) {
chunkmaster.logger.severe(chunkmaster.langManager.getLocalized("SQL_ERROR", e.toString()))
chunkmaster.logger.info(ExceptionUtils.getStackTrace(e))
} finally {
activeTasks--
if (activeTasks == 0) {
connection.close()
this.connection = null
}
}
} else {
chunkmaster.logger.severe(chunkmaster.langManager.getLocalized("NO_DATABASE_CONNECTION"))
}
}
/**
* Creates or updates tables that needed an update.
*/
private fun performUpdate() {
for (table in needCreation) {
try {
var tableDef = "CREATE TABLE IF NOT EXISTS $table ("
for (column in tables.find { it.first == table }!!.second) {
tableDef += "${column.first} ${column.second},"
}
tableDef = tableDef.substringBeforeLast(",") + ");"
chunkmaster.logger.finest(
chunkmaster.langManager.getLocalized(
"CREATE_TABLE_DEFINITION",
table,
tableDef
)
)
executeStatement(tableDef, HashMap(), null)
} catch (e: Exception) {
chunkmaster.logger.severe(chunkmaster.langManager.getLocalized("TABLE_CREATE_ERROR", table))
chunkmaster.logger.severe(e.message)
chunkmaster.logger.info(ExceptionUtils.getStackTrace(e))
}
}
for (table in needUpdate) {
val updateSql = "ALTER TABLE ${table.first} ADD COLUMN ${table.second.first} ${table.second.second}"
try {
executeStatement(updateSql, HashMap(), null)
chunkmaster.logger.finest(
chunkmaster.langManager.getLocalized(
"UPDATE_TABLE_DEFINITION",
table.first,
updateSql
)
)
} catch (e: Exception) {
chunkmaster.logger.severe(
chunkmaster.langManager.getLocalized(
"UPDATE_TABLE_FAILED",
table.first,
updateSql
)
)
chunkmaster.logger.severe(e.message)
chunkmaster.logger.info(ExceptionUtils.getStackTrace(e))
}
}
}
}

@ -0,0 +1,83 @@
package net.trivernis.chunkmaster.lib.database
import java.util.concurrent.CompletableFuture
class WorldProperties(private val sqliteManager: SqliteManager) {
private val properties = HashMap<String, Pair<Int, Int>>()
/**
* Returns the world center for one world
*/
fun getWorldCenter(worldName: String): CompletableFuture<Pair<Int, Int>?> {
val completableFuture = CompletableFuture<Pair<Int, Int>?>()
if (properties[worldName] != null) {
completableFuture.complete(properties[worldName])
} else {
sqliteManager.executeStatement("SELECT * FROM world_properties WHERE name = ?", hashMapOf(1 to worldName)) {
if (it != null && it.next()) {
completableFuture.complete(Pair(it.getInt("center_x"), it.getInt("center_z")))
} else {
completableFuture.complete(null)
}
}
}
return completableFuture
}
/**
* Updates the center of a world
*/
fun setWorldCenter(worldName: String, center: Pair<Int, Int>): CompletableFuture<Void> {
val completableFuture = CompletableFuture<Void>()
getWorldCenter(worldName).thenAccept {
if (it != null) {
updateWorldProperties(worldName, center).thenAccept { completableFuture.complete(null) }
} else {
insertWorldProperties(worldName, center).thenAccept { completableFuture.complete(null) }
}
}
return completableFuture
}
/**
* Updates an entry in the world properties
*/
private fun updateWorldProperties(worldName: String, center: Pair<Int, Int>): CompletableFuture<Void> {
val completableFuture = CompletableFuture<Void>()
sqliteManager.executeStatement(
"UPDATE world_properties SET center_x = ?, center_z = ? WHERE name = ?",
hashMapOf(
1 to center.first,
2 to center.second,
3 to worldName
)
) {
properties[worldName] = center
completableFuture.complete(null)
}
return completableFuture
}
/**
* Inserts into the world properties
*/
private fun insertWorldProperties(worldName: String, center: Pair<Int, Int>): CompletableFuture<Void> {
val completableFuture = CompletableFuture<Void>()
sqliteManager.executeStatement(
"INSERT INTO world_properties (name, center_x, center_z) VALUES (?, ?, ?)",
hashMapOf(
1 to worldName,
2 to center.first,
3 to center.second
)
) {
properties[worldName] = center
completableFuture.complete(null)
}
return completableFuture
}
}

@ -2,7 +2,6 @@ package net.trivernis.chunkmaster.lib.dynmap
import org.bukkit.Location
import org.dynmap.markers.AreaMarker
import org.dynmap.markers.Marker
import org.dynmap.markers.MarkerSet
import org.dynmap.markers.PolyLineMarker
@ -46,7 +45,12 @@ class ExtendedMarkerSet(private val markerSet: MarkerSet) {
}
fun creUpdatePolyLineMarker(id: String, label: String, edges: List<Location>, style: MarkerStyle?): PolyLineMarker? {
fun creUpdatePolyLineMarker(
id: String,
label: String,
edges: List<Location>,
style: MarkerStyle?
): PolyLineMarker? {
var marker = markerSet.findPolyLineMarker(id)
val xList = edges.map { it.x }
val yList = edges.map { it.y }
@ -54,7 +58,16 @@ class ExtendedMarkerSet(private val markerSet: MarkerSet) {
if (marker != null) {
marker.setCornerLocations(xList.toDoubleArray(), yList.toDoubleArray(), zList.toDoubleArray())
} else {
marker = markerSet.createPolyLineMarker(id, label, false, edges.first().world.name, xList.toDoubleArray(), yList.toDoubleArray(), zList.toDoubleArray(), true)
marker = markerSet.createPolyLineMarker(
id,
label,
false,
edges.first().world.name,
xList.toDoubleArray(),
yList.toDoubleArray(),
zList.toDoubleArray(),
true
)
}
if (style != null) {
if (style.lineStyle != null) {

@ -2,4 +2,9 @@ package net.trivernis.chunkmaster.lib.dynmap
import org.dynmap.markers.MarkerIcon
data class MarkerStyle(val icon: MarkerIcon?, val lineStyle: LineStyle?, val fillStyle: FillStyle?, val boostFlag: Boolean = false)
data class MarkerStyle(
val icon: MarkerIcon?,
val lineStyle: LineStyle?,
val fillStyle: FillStyle?,
val boostFlag: Boolean = false
)

@ -5,6 +5,10 @@ import org.bukkit.World
class ChunkCoordinates(val x: Int, val z: Int) {
fun getCenterLocation(world: World): Location {
return Location(world, ((x*16) + 8).toDouble(), 1.0, ((z*16) + 8).toDouble())
return Location(world, ((x * 16) + 8).toDouble(), 1.0, ((z * 16) + 8).toDouble())
}
override fun toString(): String {
return "($x, $z)"
}
}

@ -0,0 +1,57 @@
package net.trivernis.chunkmaster.lib.generation
import net.trivernis.chunkmaster.Chunkmaster
import org.bukkit.Chunk
import java.util.*
import java.util.concurrent.locks.ReentrantReadWriteLock
class ChunkUnloader(private val plugin: Chunkmaster) : Runnable {
private val maxLoadedChunks = plugin.config.getInt("generation.max-loaded-chunks")
private val lock = ReentrantReadWriteLock()
private var unloadingQueue = Vector<Chunk>(maxLoadedChunks)
val isFull: Boolean
get() {
return pendingSize == maxLoadedChunks
}
val pendingSize: Int
get() {
lock.readLock().lock()
val size = unloadingQueue.size
lock.readLock().unlock()
return size
}
/**
* Unloads all chunks in the unloading queue with each run
*/
override fun run() {
lock.writeLock().lock()
try {
val chunkToUnload = unloadingQueue.toHashSet()
for (chunk in chunkToUnload) {
try {
chunk.unload(true)
} catch (e: Exception) {
plugin.logger.severe(e.toString())
}
}
unloadingQueue.clear()
} finally {
lock.writeLock().unlock()
}
}
/**
* Adds a chunk to unload to the queue
*/
fun add(chunk: Chunk) {
lock.writeLock().lockInterruptibly()
try {
unloadingQueue.add(chunk)
} finally {
lock.writeLock().unlock()
}
}
}

@ -0,0 +1,145 @@
package net.trivernis.chunkmaster.lib.generation
import io.papermc.lib.PaperLib
import net.trivernis.chunkmaster.Chunkmaster
import net.trivernis.chunkmaster.lib.shapes.Shape
import org.bukkit.World
import java.util.concurrent.ArrayBlockingQueue
class DefaultGenerationTask(
private val plugin: Chunkmaster,
unloader: ChunkUnloader,
world: World,
startChunk: ChunkCoordinates,
override val radius: Int = -1,
shape: Shape,
missingChunks: HashSet<ChunkCoordinates>,
state: TaskState
) : GenerationTask(plugin, world, unloader, startChunk, shape, missingChunks, state) {
private val maxPendingChunks = plugin.config.getInt("generation.max-pending-chunks")
val pendingChunks = ArrayBlockingQueue<PendingChunkEntry>(maxPendingChunks)
override var count = 0
override var endReached: Boolean = false
init {
updateGenerationAreaMarker()
count = shape.count
}
/**
* Runs the generation task. Every Iteration the next chunks will be generated if
* they haven't been generated already
* After a configured number of chunks chunks have been generated, they will all be unloaded and saved.
*/
override fun generate() {
generateMissing()
seekGenerated()
generateUntilBorder()
}
/**
* Validates that all chunks have been generated or generates missing ones
*/
override fun validate() {
this.shape.reset()
val missedChunks = HashSet<ChunkCoordinates>()
while (!cancelRun && !borderReached()) {
val chunkCoordinates = nextChunkCoordinates
triggerDynmapRender(chunkCoordinates)
if (!PaperLib.isChunkGenerated(world, chunkCoordinates.x, chunkCoordinates.z)) {
missedChunks.add(chunkCoordinates)
}
}
this.missingChunks.addAll(missedChunks)
}
/**
* Generates chunks that are missing
*/
override fun generateMissing() {
val missing = this.missingChunks.toHashSet()
this.count = 0
while (missing.size > 0 && !cancelRun) {
if (plugin.mspt < msptThreshold && !unloader.isFull) {
val chunk = missing.first()
missing.remove(chunk)
this.requestGeneration(chunk)
this.count++
} else {
Thread.sleep(50L)
}
}
if (!cancelRun) {
this.joinPending()
}
}
/**
* Seeks until it encounters a chunk that hasn't been generated yet
*/
private fun seekGenerated() {
do {
lastChunkCoords = nextChunkCoordinates
count = shape.count
} while (PaperLib.isChunkGenerated(world, lastChunkCoords.x, lastChunkCoords.z) && !borderReached())
}
/**
* Generates the world until it encounters the worlds border
*/
private fun generateUntilBorder() {
var chunkCoordinates: ChunkCoordinates
while (!cancelRun && !borderReached()) {
if (plugin.mspt < msptThreshold && !unloader.isFull) {
chunkCoordinates = nextChunkCoordinates
requestGeneration(chunkCoordinates)
lastChunkCoords = chunkCoordinates
count = shape.count
} else {
Thread.sleep(50L)
}
}
if (!cancelRun) {
joinPending()
}
}
private fun joinPending() {
while (!this.pendingChunks.isEmpty()) {
Thread.sleep(msptThreshold)
}
}
/**
* Request the generation of a chunk
*/
private fun requestGeneration(chunkCoordinates: ChunkCoordinates) {
if (!PaperLib.isChunkGenerated(world, chunkCoordinates.x, chunkCoordinates.z) || PaperLib.isSpigot()) {
val pendingChunkEntry = PendingChunkEntry(
chunkCoordinates,
PaperLib.getChunkAtAsync(world, chunkCoordinates.x, chunkCoordinates.z, true)
)
this.pendingChunks.put(pendingChunkEntry)
pendingChunkEntry.chunk.thenAccept {
this.unloader.add(it)
this.pendingChunks.remove(pendingChunkEntry)
}
}
}
/**
* Cancels the generation task.
* This unloads all chunks that were generated but not unloaded yet.
*/
override fun cancel() {
this.cancelRun = true
this.pendingChunks.forEach { it.chunk.cancel(false) }
updateGenerationAreaMarker(true)
}
}

@ -1,25 +1,49 @@
package net.trivernis.chunkmaster.lib.generation
import io.papermc.lib.PaperLib
import net.trivernis.chunkmaster.Chunkmaster
import net.trivernis.chunkmaster.lib.generation.taskentry.PausedTaskEntry
import net.trivernis.chunkmaster.lib.generation.taskentry.RunningTaskEntry
import net.trivernis.chunkmaster.lib.generation.taskentry.TaskEntry
import net.trivernis.chunkmaster.lib.shapes.Circle
import net.trivernis.chunkmaster.lib.shapes.Spiral
import net.trivernis.chunkmaster.lib.shapes.Square
import org.bukkit.Server
import org.bukkit.World
import java.util.concurrent.CompletableFuture
class GenerationManager(private val chunkmaster: Chunkmaster, private val server: Server) {
val tasks: HashSet<RunningTaskEntry> = HashSet()
val pausedTasks: HashSet<PausedTaskEntry> = HashSet()
val worldCenters: HashMap<String, Pair<Int, Int>> = HashMap()
val worldProperties = chunkmaster.sqliteManager.worldProperties
private val pendingChunksTable = chunkmaster.sqliteManager.pendingChunks
private val generationTasks = chunkmaster.sqliteManager.generationTasks
private val completedGenerationTasks = chunkmaster.sqliteManager.completedGenerationTasks
private val unloadingPeriod: Long
get() {
return chunkmaster.config.getLong("generation.unloading-period")
}
private val pauseOnPlayerCount: Int
get() {
return chunkmaster.config.getInt("generation.pause-on-player-count")
}
private val autostart: Boolean
get() {
return chunkmaster.config.getBoolean("generation.autostart")
}
val loadedChunkCount: Int
get() {
return unloader.pendingSize
}
private val unloader = ChunkUnloader(chunkmaster)
val allTasks: HashSet<TaskEntry>
get() {
if (this.tasks.isEmpty() && this.pausedTasks.isEmpty()) {
if (this.worldCenters.isEmpty()) {
this.loadWorldCenters()
}
this.startAll()
if (server.onlinePlayers.size >= chunkmaster.config.getInt("generation.pause-on-player-count")) {
if (server.onlinePlayers.size >= pauseOnPlayerCount) {
this.pauseAll()
}
}
@ -36,41 +60,17 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
*/
fun addTask(world: World, radius: Int = -1, shape: String = "square"): Int {
val foundTask = allTasks.find { it.generationTask.world == world }
if (foundTask == null) {
val centerChunk = if (worldCenters[world.name] == null) {
val center = worldProperties.getWorldCenter(world.name).join()
val centerChunk = if (center == null) {
ChunkCoordinates(world.spawnLocation.chunk.x, world.spawnLocation.chunk.z)
} else {
val center = worldCenters[world.name]!!
ChunkCoordinates(center.first, center.second)
}
val generationTask = createGenerationTask(world, centerChunk, centerChunk, radius, shape)
chunkmaster.sqliteManager.executeStatement(
"""
INSERT INTO generation_tasks (center_x, center_z, last_x, last_z, world, radius, shape)
values (?, ?, ?, ?, ?, ?, ?)
""",
HashMap(
mapOf(
1 to centerChunk.x,
2 to centerChunk.z,
3 to centerChunk.x,
4 to centerChunk.z,
5 to world.name,
6 to radius,
7 to shape
)
),
null
)
var id = 0
chunkmaster.sqliteManager.executeStatement("""
SELECT id FROM generation_tasks ORDER BY id DESC LIMIT 1
""".trimIndent(), HashMap()) {
it.next()
id = it.getInt("id")
}
val generationTask = createGenerationTask(world, centerChunk, centerChunk, radius, shape, null)
val id = generationTasks.addGenerationTask(world.name, centerChunk, radius, shape).join()
generationTask.onEndReached {
chunkmaster.logger.info(chunkmaster.langManager.getLocalized("TASK_FINISHED", id, it.count))
@ -78,13 +78,19 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
}
if (!paused) {
val task = server.scheduler.runTaskTimer(
chunkmaster, generationTask, 200, // 10 sec delay
chunkmaster.config.getLong("generation.period")
val taskEntry = RunningTaskEntry(
id,
generationTask
)
tasks.add(RunningTaskEntry(id, task, generationTask))
taskEntry.start()
tasks.add(taskEntry)
} else {
pausedTasks.add(PausedTaskEntry(id, generationTask))
pausedTasks.add(
PausedTaskEntry(
id,
generationTask
)
)
}
return id
@ -102,17 +108,18 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
last: ChunkCoordinates,
id: Int,
radius: Int = -1,
delay: Long = 200L,
shape: String = "square"
shape: String = "square",
pendingChunks: List<ChunkCoordinates>?
) {
if (!paused) {
chunkmaster.logger.info(chunkmaster.langManager.getLocalized("RESUME_FOR_WORLD", world.name))
val generationTask = createGenerationTask(world, center, last, radius, shape)
val task = server.scheduler.runTaskTimer(
chunkmaster, generationTask, delay,
chunkmaster.config.getLong("generation.period")
val generationTask = createGenerationTask(world, center, last, radius, shape, pendingChunks)
val taskEntry = RunningTaskEntry(
id,
generationTask
)
tasks.add(RunningTaskEntry(id, task, generationTask))
taskEntry.start()
tasks.add(taskEntry)
generationTask.onEndReached {
chunkmaster.logger.info(chunkmaster.langManager.getLocalized("TASK_FINISHED", id, generationTask.count))
removeTask(id)
@ -129,22 +136,30 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
} else {
this.tasks.find { it.id == id }
}
if (taskEntry != null) {
taskEntry.cancel()
chunkmaster.sqliteManager.executeStatement("""
DELETE FROM generation_tasks WHERE id = ?;
""".trimIndent(), HashMap(mapOf(1 to taskEntry.id)),
null
)
try {
if (taskEntry != null) {
if (taskEntry.generationTask.isRunning && taskEntry is RunningTaskEntry) {
taskEntry.cancel(chunkmaster.config.getLong("mspt-pause-threshold"))
}
generationTasks.deleteGenerationTask(id)
completedGenerationTasks.addCompletedTask(
id,
taskEntry.generationTask.world.name,
taskEntry.generationTask.shape.currentRadius(),
taskEntry.generationTask.startChunk,
taskEntry.generationTask.shape.javaClass.simpleName
)
pendingChunksTable.clearPendingChunks(id)
if (taskEntry is RunningTaskEntry) {
if (taskEntry.task.isCancelled) {
if (taskEntry is RunningTaskEntry) {
tasks.remove(taskEntry)
} else if (taskEntry is PausedTaskEntry) {
pausedTasks.remove(taskEntry)
}
} else if (taskEntry is PausedTaskEntry) {
pausedTasks.remove(taskEntry)
return true
}
return true
} catch (e: Exception) {
chunkmaster.logger.severe(e.toString())
}
return false
}
@ -159,12 +174,15 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
saveProgress() // save progress every 30 seconds
}, 600, 600)
server.scheduler.runTaskLater(chunkmaster, Runnable {
this.loadWorldCenters()
this.startAll()
if (!server.onlinePlayers.isEmpty()) {
if (server.onlinePlayers.count() >= pauseOnPlayerCount || !autostart) {
if (!autostart) {
chunkmaster.logger.info(chunkmaster.langManager.getLocalized("NO_AUTOSTART"))
}
this.pauseAll()
}
}, 20)
server.scheduler.runTaskTimer(chunkmaster, unloader, unloadingPeriod, unloadingPeriod)
}
/**
@ -173,40 +191,36 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
fun stopAll() {
val removalSet = HashSet<RunningTaskEntry>()
for (task in tasks) {
val lastChunk = task.generationTask.lastChunkCoords
val id = task.id
chunkmaster.logger.info(chunkmaster.langManager.getLocalized("SAVING_TASK_PROGRESS", task.id))
saveProgressToDatabase(lastChunk, id)
task.task.cancel()
task.generationTask.cancel()
if (task.task.isCancelled) {
removalSet.add(task)
saveProgressToDatabase(task.generationTask, id).join()
if (!task.cancel(chunkmaster.config.getLong("mspt-pause-threshold"))) {
chunkmaster.logger.warning(chunkmaster.langManager.getLocalized("CANCEL_FAIL", task.id))
}
chunkmaster.logger.info(chunkmaster.langManager.getLocalized("TASK_CANCELED", task.id))
removalSet.add(task)
chunkmaster.logger.info(chunkmaster.langManager.getLocalized("TASK_CANCELLED", task.id))
}
tasks.removeAll(removalSet)
if (unloader.pendingSize > 0) {
chunkmaster.logger.info(chunkmaster.langManager.getLocalized("SAVING_CHUNKS", unloader.pendingSize))
unloader.run()
}
}
/**
* Starts all generation tasks.
*/
fun startAll() {
chunkmaster.sqliteManager.executeStatement("SELECT * FROM generation_tasks", HashMap()) { res ->
var count = 0
while (res.next()) {
count++
try {
val id = res.getInt("id")
val world = server.getWorld(res.getString("world"))
val center = ChunkCoordinates(res.getInt("center_x"), res.getInt("center_z"))
val last = ChunkCoordinates(res.getInt("last_x"), res.getInt("last_z"))
val radius = res.getInt("radius")
val shape = res.getString("shape")
if (this.tasks.find { it.id == id } == null) {
resumeTask(world!!, center, last, id, radius, 200L + count, shape)
generationTasks.getGenerationTasks().thenAccept { tasks ->
for (task in tasks) {
val world = server.getWorld(task.world)
if (world != null) {
pendingChunksTable.getPendingChunks(task.id).thenAccept {
resumeTask(world, task.center, task.last, task.id, task.radius, task.shape, it)
}
} catch (error: NullPointerException) {
chunkmaster.logger.severe(chunkmaster.langManager.getLocalized("TASK_LOAD_FAILED", res.getInt("id")))
} else {
chunkmaster.logger.severe(chunkmaster.langManager.getLocalized("TASK_LOAD_FAILED", task.id))
}
}
}
@ -222,7 +236,12 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
fun pauseAll() {
paused = true
for (task in tasks) {
pausedTasks.add(PausedTaskEntry(task.id, task.generationTask))
pausedTasks.add(
PausedTaskEntry(
task.id,
task.generationTask
)
)
}
stopAll()
}
@ -237,98 +256,100 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
}
/**
* Overload that doesn't need an argument
*/
private fun loadWorldCenters() {
loadWorldCenters(null)
}
/**
* Loads the world centers from the database
* Saves the task progress
*/
fun loadWorldCenters(cb: (() -> Unit)?) {
chunkmaster.sqliteManager.executeStatement("SELECT * FROM world_properties", HashMap()) {
while (it.next()) {
worldCenters[it.getString("name")] = Pair(it.getInt("center_x"), it.getInt("center_z"))
private fun saveProgress() {
for (task in tasks) {
try {
if (task.generationTask.state == TaskState.CORRECTING) {
reportCorrectionProgress(task)
} else {
reportGenerationProgress(task)
}
saveProgressToDatabase(task.generationTask, task.id)
} catch (error: Exception) {
chunkmaster.logger.warning(chunkmaster.langManager.getLocalized("TASK_SAVE_FAILED", error.toString()))
error.printStackTrace()
}
cb?.invoke()
}
}
/**
* Updates the center of a world
* Reports the progress for correcting tasks
*/
fun updateWorldCenter(worldName: String, center: Pair<Int, Int>) {
chunkmaster.sqliteManager.executeStatement("SELECT * FROM world_properties WHERE name = ?", HashMap(mapOf(1 to worldName))) {
if (it.next()) {
chunkmaster.sqliteManager.executeStatement("UPDATE world_properties SET center_x = ?, center_z = ? WHERE name = ?", HashMap(
mapOf(
1 to center.first,
2 to center.second,
3 to worldName
)
), null)
} else {
chunkmaster.sqliteManager.executeStatement("INSERT INTO world_properties (name, center_x, center_z) VALUES (?, ?, ?)", HashMap(
mapOf(
1 to worldName,
2 to center.first,
3 to center.second
)
), null)
}
private fun reportCorrectionProgress(task: RunningTaskEntry) {
val genTask = task.generationTask
val progress = if (genTask.missingChunks.size > 0) {
"(${(genTask.count / genTask.missingChunks.size) * 100}%)"
} else {
""
}
worldCenters[worldName] = center
chunkmaster.logger.info(
chunkmaster.langManager.getLocalized(
"TASK_PERIODIC_REPORT_CORRECTING",
task.id,
genTask.world.name,
genTask.count,
progress
)
)
}
/**
* Saves the task progress
* Reports the progress of the chunk generation
*/
private fun saveProgress() {
for (task in tasks) {
try {
val genTask = task.generationTask
val (speed, chunkSpeed) = task.generationSpeed
val percentage = if (genTask.radius > 0) "(${"%.2f".format(genTask.shape.progress() * 100)}%)" else ""
val eta = if (genTask.radius > 0 && speed!! > 0) {
val etaSeconds = (genTask.shape.progress())/speed
val hours: Int = (etaSeconds/3600).toInt()
val minutes: Int = ((etaSeconds % 3600) / 60).toInt()
val seconds: Int = (etaSeconds % 60).toInt()
", ETA: %d:%02d:%02d".format(hours, minutes, seconds)
} else {
""
}
chunkmaster.logger.info(chunkmaster.langManager.getLocalized(
"TASK_PERIODIC_REPORT",
task.id,
genTask.world.name,
genTask.count,
percentage,
eta,
chunkSpeed!!,
genTask.lastChunkCoords.x,
genTask.lastChunkCoords.z))
saveProgressToDatabase(genTask.lastChunkCoords, task.id)
genTask.updateLastChunkMarker()
} catch (error: Exception) {
chunkmaster.logger.warning(chunkmaster.langManager.getLocalized("TASK_SAVE_FAILED", error.toString()))
}
private fun reportGenerationProgress(task: RunningTaskEntry) {
val genTask = task.generationTask
val (speed, chunkSpeed) = task.generationSpeed
val progress =
genTask.shape.progress(if (genTask.radius < 0) (genTask.world.worldBorder.size / 32).toInt() else null)
val percentage =
"(${"%.2f".format(progress * 100)}%)"
val eta = if (speed!! > 0) {
val remaining = 1 - progress
val etaSeconds = remaining / speed
val hours: Int = (etaSeconds / 3600).toInt()
val minutes: Int = ((etaSeconds % 3600) / 60).toInt()
val seconds: Int = (etaSeconds % 60).toInt()
", ETA: %dh %dmin %ds".format(hours, minutes, seconds)
} else {
""
}
chunkmaster.logger.info(
chunkmaster.langManager.getLocalized(
"TASK_PERIODIC_REPORT",
task.id,
genTask.world.name,
genTask.state.toString(),
genTask.count,
percentage,
eta,
chunkSpeed!!,
genTask.lastChunkCoords.x,
genTask.lastChunkCoords.z
)
)
}
/**
* Saves the generation progress to the database
*/
private fun saveProgressToDatabase(lastChunk: ChunkCoordinates, id: Int) {
chunkmaster.sqliteManager.executeStatement(
"""
UPDATE generation_tasks SET last_x = ?, last_z = ?
WHERE id = ?
""".trimIndent(),
HashMap(mapOf(1 to lastChunk.x, 2 to lastChunk.z, 3 to id)),
null
)
private fun saveProgressToDatabase(generationTask: GenerationTask, id: Int): CompletableFuture<Void> {
val completableFuture = CompletableFuture<Void>()
generationTasks.updateGenerationTask(id, generationTask.lastChunkCoords, generationTask.state).thenAccept {
pendingChunksTable.clearPendingChunks(id).thenAccept {
if (generationTask is DefaultGenerationTask) {
if (generationTask.pendingChunks.size > 0) {
pendingChunksTable.addPendingChunks(id, generationTask.pendingChunks.map { it.coordinates })
}
}
pendingChunksTable.addPendingChunks(id, generationTask.missingChunks.toList()).thenAccept {
completableFuture.complete(null)
}
}
}
return completableFuture
}
/**
@ -340,18 +361,23 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
center: ChunkCoordinates,
start: ChunkCoordinates,
radius: Int,
shapeName: String
shapeName: String,
pendingChunks: List<ChunkCoordinates>?
): GenerationTask {
val shape = when (shapeName) {
"circle" -> Circle(Pair(center.x, center.z), Pair(start.x, start.z), radius)
"square" -> Spiral(Pair(center.x, center.z), Pair(start.x, start.z), radius)
else -> Spiral(Pair(center.x, center.z), Pair(start.x, start.z), radius)
"square" -> Square(Pair(center.x, center.z), Pair(start.x, start.z), radius)
else -> Square(Pair(center.x, center.z), Pair(start.x, start.z), radius)
}
return if (PaperLib.isPaper()) {
GenerationTaskPaper(chunkmaster, world, start, radius, shape)
} else {
GenerationTaskSpigot(chunkmaster, world, start, radius, shape)
}
return DefaultGenerationTask(
chunkmaster,
unloader,
world,
start,
radius,
shape, pendingChunks?.toHashSet() ?: HashSet(),
TaskState.GENERATING
)
}
}

@ -3,37 +3,32 @@ package net.trivernis.chunkmaster.lib.generation
import net.trivernis.chunkmaster.Chunkmaster
import net.trivernis.chunkmaster.lib.dynmap.*
import net.trivernis.chunkmaster.lib.shapes.Shape
import org.bukkit.Chunk
import org.bukkit.World
import java.lang.Exception
import kotlin.math.ceil
/**
* Interface for generation tasks.
*/
abstract class GenerationTask(
private val plugin: Chunkmaster,
startChunk: ChunkCoordinates,
val shape: Shape
plugin: Chunkmaster,
val world: World,
protected val unloader: ChunkUnloader,
val startChunk: ChunkCoordinates,
val shape: Shape,
val missingChunks: HashSet<ChunkCoordinates>,
var state: TaskState
) :
Runnable {
abstract val radius: Int
abstract val world: World
abstract var count: Int
abstract var endReached: Boolean
var isRunning: Boolean = false
val loadedChunksCount: Int
get() {
return loadedChunks.size
}
protected val loadedChunks: HashSet<Chunk> = HashSet()
var lastChunkCoords = ChunkCoordinates(startChunk.x, startChunk.z)
protected set
protected val chunkSkips = plugin.config.getInt("generation.chunk-skips-per-step")
protected val msptThreshold = plugin.config.getLong("generation.mspt-pause-threshold")
protected val maxLoadedChunks = plugin.config.getInt("generation.max-loaded-chunks")
protected val chunksPerStep = plugin.config.getInt("generation.chunks-per-step")
protected var cancelRun: Boolean = false
private var endReachedCallback: ((GenerationTask) -> Unit)? = null
@ -45,16 +40,51 @@ abstract class GenerationTask(
null
}
private val markerAreaStyle = MarkerStyle(null, LineStyle(2, 1.0, 0x0022FF), FillStyle(.0, 0))
private val markerAreaId = "chunkmaster_genarea"
private val markerAreaName = "Chunkmaster Generation Area"
private val markerLastStyle = MarkerStyle(null, LineStyle(2, 1.0, 0x0077FF), FillStyle(.5, 0x0077FF))
private val markerLastId = "chunkmaster_lastchunk"
private val markerLastName = "Chunkmaster Last Chunk"
private val markerAreaId = "chunkmaster_genarea_${world.name}"
private val markerAreaName = "Chunkmaster Generation Area (${ceil(shape.total()).toInt()} chunks)"
private val ignoreWorldborder = plugin.config.getBoolean("generation.ignore-worldborder")
abstract override fun run()
abstract fun generate()
abstract fun validate()
abstract fun generateMissing()
abstract fun cancel()
override fun run() {
isRunning = true
try {
when (state) {
TaskState.GENERATING -> {
this.generate()
if (!cancelRun) {
this.state = TaskState.VALIDATING
this.validate()
}
if (!cancelRun) {
this.state = TaskState.CORRECTING
this.generateMissing()
}
}
TaskState.VALIDATING -> {
this.validate()
if (!cancelRun) {
this.state = TaskState.CORRECTING
this.generateMissing()
}
}
TaskState.CORRECTING -> {
this.generateMissing()
}
else -> {
}
}
if (!cancelRun && this.borderReached()) {
this.setEndReached()
}
} catch (e: InterruptedException) {
}
isRunning = false
}
val nextChunkCoordinates: ChunkCoordinates
get() {
val nextChunkCoords = shape.next()
@ -69,26 +99,6 @@ abstract class GenerationTask(
|| shape.endReached()
}
/**
* Unloads all chunks that have been loaded
*/
protected fun unloadLoadedChunks() {
for (chunk in loadedChunks) {
if (chunk.isLoaded) {
try {
chunk.unload(true)
} catch (e: Exception) {
plugin.logger.severe(e.toString())
}
}
if (dynmapIntegration) {
dynmap?.triggerRenderOfVolume(chunk.getBlock(0, 0, 0).location, chunk.getBlock(15, 255, 15).location)
}
}
loadedChunks.clear()
}
/**
* Updates the dynmap marker for the generation radius
*/
@ -99,25 +109,18 @@ abstract class GenerationTask(
markerSet?.creUpdatePolyLineMarker(
markerAreaId,
markerAreaName,
this.shape.getShapeEdgeLocations().map { ChunkCoordinates(it.first, it.second).getCenterLocation(this.world) },
this.shape.getShapeEdgeLocations()
.map { ChunkCoordinates(it.first, it.second).getCenterLocation(this.world) },
markerAreaStyle
)
}
}
/**
* Updates the dynmap marker for the generation radius
*/
fun updateLastChunkMarker(clear: Boolean = false) {
if (clear) {
markerSet?.deleteAreaMarker(markerLastId)
} else if (dynmapIntegration) {
markerSet?.creUpdateAreMarker(
markerLastId,
markerLastName,
this.lastChunkCoords.getCenterLocation(world).chunk.getBlock(0, 0, 0).location,
this.lastChunkCoords.getCenterLocation(world).chunk.getBlock(15, 0, 15).location,
markerLastStyle
protected fun triggerDynmapRender(chunkCoordinates: ChunkCoordinates) {
if (dynmapIntegration) {
dynmap?.triggerRenderOfVolume(
world.getBlockAt(chunkCoordinates.x * 16, 0, chunkCoordinates.z * 16).location,
world.getBlockAt((chunkCoordinates.x * 16) + 16, 255, (chunkCoordinates.z * 16) + 16).location
)
}
}
@ -128,21 +131,8 @@ abstract class GenerationTask(
private fun setEndReached() {
endReached = true
count = shape.count
endReachedCallback?.invoke(this)
updateGenerationAreaMarker(true)
updateLastChunkMarker(true)
}
/**
* Performs a check if the border has been reached
*/
protected fun borderReachedCheck(): Boolean {
val done = borderReached()
if (done) {
unloadLoadedChunks()
setEndReached()
}
return done
endReachedCallback?.invoke(this)
}
/**

@ -1,121 +0,0 @@
package net.trivernis.chunkmaster.lib.generation
import net.trivernis.chunkmaster.Chunkmaster
import net.trivernis.chunkmaster.lib.shapes.Shape
import org.bukkit.Chunk
import org.bukkit.World
import java.lang.Exception
import java.util.concurrent.CompletableFuture
class GenerationTaskPaper(
private val plugin: Chunkmaster,
override val world: World,
startChunk: ChunkCoordinates,
override val radius: Int = -1,
shape: Shape
) : GenerationTask(plugin, startChunk, shape) {
private val maxPendingChunks = plugin.config.getInt("generation.max-pending-chunks")
private val pendingChunks = HashSet<CompletableFuture<Chunk>>()
override var count = 0
override var endReached: Boolean = false
init {
updateGenerationAreaMarker()
count = shape.count
}
/**
* Runs the generation task. Every Iteration the next chunks will be generated if
* they haven't been generated already
* After a configured number of chunks chunks have been generated, they will all be unloaded and saved.
*/
override fun run() {
if (plugin.mspt < msptThreshold) {
if (loadedChunks.size > maxLoadedChunks) {
unloadLoadedChunks()
} else if (pendingChunks.size < maxPendingChunks) {
if (borderReachedCheck()) return
var chunk = nextChunkCoordinates
for (i in 0 until chunkSkips) {
if (world.isChunkGenerated(chunk.x, chunk.z)) {
chunk = nextChunkCoordinates
} else {
break
}
}
if (!world.isChunkGenerated(chunk.x, chunk.z)) {
for (i in 0 until chunksPerStep) {
if (borderReached()) break
if (!world.isChunkGenerated(chunk.x, chunk.z)) {
pendingChunks.add(world.getChunkAtAsync(chunk.x, chunk.z, true))
}
chunk = nextChunkCoordinates
}
if (!world.isChunkGenerated(chunk.x, chunk.z)) {
pendingChunks.add(world.getChunkAtAsync(chunk.x, chunk.z, true))
}
}
lastChunkCoords = chunk
count = shape.count
}
}
checkChunksLoaded()
}
/**
* Cancels the generation task.
* This unloads all chunks that were generated but not unloaded yet.
*/
override fun cancel() {
updateGenerationAreaMarker(true)
updateLastChunkMarker(true)
unloadAllChunks()
}
/**
* Cancels all pending chunks and unloads all loaded chunks.
*/
private fun unloadAllChunks() {
for (pendingChunk in pendingChunks) {
if (pendingChunk.isDone) {
loadedChunks.add(pendingChunk.get())
} else {
pendingChunk.cancel(true)
}
}
pendingChunks.clear()
if (loadedChunks.isNotEmpty()) {
lastChunkCoords = ChunkCoordinates(loadedChunks.last().x, loadedChunks.last().z)
}
for (chunk in loadedChunks) {
if (chunk.isLoaded) {
try {
chunk.unload(true);
} catch (e: Exception){
plugin.logger.severe(e.toString())
}
}
}
}
/**
* Checks if some chunks have been loaded and adds them to the loaded chunk set.
*/
private fun checkChunksLoaded() {
val completedEntries = HashSet<CompletableFuture<Chunk>>()
for (pendingChunk in pendingChunks) {
if (pendingChunk.isDone) {
completedEntries.add(pendingChunk)
loadedChunks.add(pendingChunk.get())
} else if (pendingChunk.isCompletedExceptionally || pendingChunk.isCancelled) {
completedEntries.add(pendingChunk)
}
}
pendingChunks.removeAll(completedEntries)
}
}

@ -1,72 +0,0 @@
package net.trivernis.chunkmaster.lib.generation
import net.trivernis.chunkmaster.Chunkmaster
import net.trivernis.chunkmaster.lib.shapes.Shape
import org.bukkit.World
import java.lang.Exception
class GenerationTaskSpigot(
private val plugin: Chunkmaster,
override val world: World,
startChunk: ChunkCoordinates,
override val radius: Int = -1,
shape: Shape
) : GenerationTask(plugin, startChunk, shape) {
override var count = 0
override var endReached: Boolean = false
init {
updateGenerationAreaMarker()
count = shape.count
}
/**
* Runs the generation task. Every Iteration the next chunks will be generated if
* they haven't been generated already
* After a configured number of chunks chunks have been generated, they will all be unloaded and saved.
*/
override fun run() {
if (plugin.mspt < msptThreshold) {
if (loadedChunks.size > maxLoadedChunks) {
unloadLoadedChunks()
} else {
if (borderReachedCheck()) return
var chunk = nextChunkCoordinates
for (i in 0 until chunksPerStep) {
if (borderReached()) break
val chunkInstance = world.getChunkAt(chunk.x, chunk.z)
chunkInstance.load(true)
loadedChunks.add(chunkInstance)
chunk = nextChunkCoordinates
}
val chunkInstance = world.getChunkAt(chunk.x, chunk.z)
chunkInstance.load(true)
loadedChunks.add(chunkInstance)
lastChunkCoords = chunk
count = shape.count
}
}
}
/**
* Cancels the generation task.
* This unloads all chunks that were generated but not unloaded yet.
*/
override fun cancel() {
for (chunk in loadedChunks) {
if (chunk.isLoaded) {
try {
chunk.unload(true)
} catch (e: Exception) {
plugin.logger.severe(e.toString())
}
}
}
updateGenerationAreaMarker(true)
updateLastChunkMarker(true)
}
}

@ -1,10 +0,0 @@
package net.trivernis.chunkmaster.lib.generation
class PausedTaskEntry(
override val id: Int,
override val generationTask: GenerationTask
) : TaskEntry {
override fun cancel() {
generationTask.cancel()
}
}

@ -0,0 +1,9 @@
package net.trivernis.chunkmaster.lib.generation
import org.bukkit.Chunk
import java.util.concurrent.CompletableFuture
class PendingChunkEntry(val coordinates: ChunkCoordinates, val chunk: CompletableFuture<Chunk>) {
val isDone: Boolean
get() = chunk.isDone
}

@ -1,46 +0,0 @@
package net.trivernis.chunkmaster.lib.generation
import org.bukkit.scheduler.BukkitTask
class RunningTaskEntry(
override val id: Int,
val task: BukkitTask,
override val generationTask: GenerationTask
) : TaskEntry {
private var lastProgress: Pair<Long, Double>? = null
private var lastChunkCount: Pair<Long, Int>? = null
/**
* Returns the generation Speed
*/
val generationSpeed: Pair<Double?, Double?>
get() {
var generationSpeed: Double? = null
var chunkGenerationSpeed: Double? = null
if (lastProgress != null) {
val progressDiff = generationTask.shape.progress() - lastProgress!!.second
val timeDiff = (System.currentTimeMillis() - lastProgress!!.first).toDouble()/1000
generationSpeed = progressDiff/timeDiff
}
if (lastChunkCount != null) {
val chunkDiff = generationTask.count - lastChunkCount!!.second
val timeDiff = (System.currentTimeMillis() - lastChunkCount!!.first).toDouble()/1000
chunkGenerationSpeed = chunkDiff/timeDiff
}
lastProgress = Pair(System.currentTimeMillis(), generationTask.shape.progress())
lastChunkCount = Pair(System.currentTimeMillis(), generationTask.count)
return Pair(generationSpeed, chunkGenerationSpeed)
}
init {
lastProgress = Pair(System.currentTimeMillis(), generationTask.shape.progress())
lastChunkCount = Pair(System.currentTimeMillis(), generationTask.count)
}
override fun cancel() {
task.cancel()
generationTask.cancel()
}
}

@ -1,13 +0,0 @@
package net.trivernis.chunkmaster.lib.generation
import org.bukkit.scheduler.BukkitTask
/**
* Generic task entry
*/
interface TaskEntry {
val id: Int
val generationTask: GenerationTask
fun cancel()
}

@ -0,0 +1,24 @@
package net.trivernis.chunkmaster.lib.generation
enum class TaskState {
GENERATING {
override fun toString(): String {
return "GENERATING"
}
},
VALIDATING {
override fun toString(): String {
return "VALIDATING"
}
},
CORRECTING {
override fun toString(): String {
return "CORRECTING"
}
},
PAUSING {
override fun toString(): String {
return "PAUSING"
}
},
}

@ -0,0 +1,8 @@
package net.trivernis.chunkmaster.lib.generation.taskentry
import net.trivernis.chunkmaster.lib.generation.GenerationTask
class PausedTaskEntry(
override val id: Int,
override val generationTask: GenerationTask
) : TaskEntry

@ -0,0 +1,71 @@
package net.trivernis.chunkmaster.lib.generation.taskentry
import net.trivernis.chunkmaster.lib.generation.GenerationTask
class RunningTaskEntry(
override val id: Int,
override val generationTask: GenerationTask
) : TaskEntry {
private var lastProgress: Pair<Long, Double>? = null
private var lastChunkCount: Pair<Long, Int>? = null
private var thread = Thread(generationTask)
/**
* Returns the generation Speed
*/
val generationSpeed: Pair<Double?, Double?>
get() {
var generationSpeed: Double? = null
var chunkGenerationSpeed: Double? = null
val progress =
generationTask.shape.progress(if (generationTask.radius < 0) (generationTask.world.worldBorder.size / 32).toInt() else null)
if (lastProgress != null) {
val progressDiff = progress - lastProgress!!.second
val timeDiff = (System.currentTimeMillis() - lastProgress!!.first).toDouble() / 1000
generationSpeed = progressDiff / timeDiff
}
if (lastChunkCount != null) {
val chunkDiff = generationTask.count - lastChunkCount!!.second
val timeDiff = (System.currentTimeMillis() - lastChunkCount!!.first).toDouble() / 1000
chunkGenerationSpeed = chunkDiff / timeDiff
}
lastProgress = Pair(System.currentTimeMillis(), progress)
lastChunkCount = Pair(System.currentTimeMillis(), generationTask.count)
return Pair(generationSpeed, chunkGenerationSpeed)
}
init {
lastProgress = Pair(System.currentTimeMillis(), generationTask.shape.progress(null))
lastChunkCount = Pair(System.currentTimeMillis(), generationTask.count)
}
fun start() {
thread.start()
}
fun cancel(timeout: Long): Boolean {
if (generationTask.isRunning) {
generationTask.cancel()
thread.interrupt()
}
return try {
joinThread(timeout)
} catch (e: InterruptedException) {
true
}
}
private fun joinThread(timeout: Long): Boolean {
var threadStopped = false
for (i in 0..100) {
if (!thread.isAlive || !generationTask.isRunning) {
threadStopped = true
break
}
Thread.sleep(timeout / 100)
}
return threadStopped
}
}

@ -0,0 +1,11 @@
package net.trivernis.chunkmaster.lib.generation.taskentry
import net.trivernis.chunkmaster.lib.generation.GenerationTask
/**
* Generic task entry
*/
interface TaskEntry {
val id: Int
val generationTask: GenerationTask
}

@ -1,16 +1,12 @@
package net.trivernis.chunkmaster.lib.shapes
import net.trivernis.chunkmaster.lib.dynmap.ExtendedMarkerSet
import net.trivernis.chunkmaster.lib.dynmap.MarkerStyle
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashSet
import kotlin.math.PI
import kotlin.math.pow
import kotlin.math.sqrt
import kotlin.system.exitProcess
class Circle(center: Pair<Int, Int>, start: Pair<Int, Int>, radius: Int): Shape(center, start, radius) {
class Circle(center: Pair<Int, Int>, start: Pair<Int, Int>, radius: Int) : Shape(center, start, radius) {
private var r = 0
private var coords = Stack<Pair<Int, Int>>()
private var previousCoords = HashSet<Pair<Int, Int>>()
@ -20,9 +16,17 @@ class Circle(center: Pair<Int, Int>, start: Pair<Int, Int>, radius: Int): Shape(
return radius > 0 && coords.isEmpty() && r >= radius
}
override fun progress(): Double {
override fun total(): Double {
return (PI * radius.toFloat().pow(2))
}
override fun progress(maxRadius: Int?): Double {
// TODO: Radius inner progress
return (count/(PI* radius.toFloat().pow(2))).coerceAtMost(100.0)
return if (maxRadius != null) {
(count / (PI * maxRadius.toFloat().pow(2))).coerceAtMost(1.0)
} else {
(count / (PI * radius.toFloat().pow(2))).coerceAtMost(1.0)
}
}
override fun currentRadius(): Int {
@ -36,7 +40,7 @@ class Circle(center: Pair<Int, Int>, start: Pair<Int, Int>, radius: Int): Shape(
override fun getShapeEdgeLocations(): List<Pair<Int, Int>> {
val locations = this.getCircleCoordinates(this.radius)
locations.add(locations.first())
return locations.map{ Pair(it.first + center.first, it.second + center.second) }
return locations.map { Pair(it.first + center.first, it.second + center.second) }
}
/**
@ -46,26 +50,30 @@ class Circle(center: Pair<Int, Int>, start: Pair<Int, Int>, radius: Int): Shape(
if (endReached()) {
return currentPos
}
if (count == 0 && currentPos != center) {
val tmpCircle = Circle(center, center, radius)
while (tmpCircle.next() != currentPos);
while (tmpCircle.next() != currentPos && !tmpCircle.endReached());
this.count = tmpCircle.count
this.r = tmpCircle.r
}
if (count == 0) {
count++
return center
}
if (coords.isEmpty()) {
r++
val tmpCoords = HashSet<Pair<Int, Int>>()
tmpCoords.addAll(getCircleCoordinates((r*2)-1).map { Pair(it.first / 2, it.second / 2) })
tmpCoords.addAll(getCircleCoordinates((r * 2) - 1).map { Pair(it.first / 2, it.second / 2) })
tmpCoords.addAll(getCircleCoordinates(r))
tmpCoords.removeAll(previousCoords)
previousCoords.clear()
coords.addAll(tmpCoords)
previousCoords.addAll(tmpCoords)
}
count++
val coord = coords.pop()
currentPos = Pair(coord.first + center.first, coord.second + center.second)
@ -77,16 +85,18 @@ class Circle(center: Pair<Int, Int>, start: Pair<Int, Int>, radius: Int): Shape(
* Some coordinates might already be present in the list
* @param r - the radius
*/
private fun getCircleCoordinates(r: Int): ArrayList<Pair<Int, Int>> {
val coords = ArrayList<Pair<Int, Int>>()
private fun getCircleCoordinates(r: Int): Vector<Pair<Int, Int>> {
val coords = Vector<Pair<Int, Int>>()
val segCoords = getSegment(r)
coords.addAll(segCoords.reversed())
for (step in 1..7) {
val tmpSeg = ArrayList<Pair<Int, Int>>()
val tmpSeg = Vector<Pair<Int, Int>>()
for (pos in segCoords) {
val coord = when (step) {
1 -> Pair(pos.first, -pos.second)
2 ->Pair(pos.second, -pos.first)
2 -> Pair(pos.second, -pos.first)
3 -> Pair(-pos.second, -pos.first)
4 -> Pair(-pos.first, -pos.second)
5 -> Pair(-pos.first, pos.second)
@ -128,4 +138,11 @@ class Circle(center: Pair<Int, Int>, start: Pair<Int, Int>, radius: Int): Shape(
}
return coords
}
}
override fun reset() {
this.r = 0
this.currentPos = center
this.previousCoords.clear()
this.count = 0
}
}

@ -1,9 +1,5 @@
package net.trivernis.chunkmaster.lib.shapes
import net.trivernis.chunkmaster.lib.dynmap.ExtendedMarkerSet
import net.trivernis.chunkmaster.lib.dynmap.MarkerStyle
import javax.xml.stream.Location
abstract class Shape(protected val center: Pair<Int, Int>, start: Pair<Int, Int>, radius: Int) {
protected var currentPos = start
protected var radius = radius
@ -23,7 +19,12 @@ abstract class Shape(protected val center: Pair<Int, Int>, start: Pair<Int, Int>
/**
* Returns the progress of the shape
*/
abstract fun progress(): Double
abstract fun progress(maxRadius: Int?): Double
/**
* The total number of chunks to generate
*/
abstract fun total(): Double
/**
* Returns the current radius
@ -34,4 +35,9 @@ abstract class Shape(protected val center: Pair<Int, Int>, start: Pair<Int, Int>
* returns a poly marker for the shape
*/
abstract fun getShapeEdgeLocations(): List<Pair<Int, Int>>
/**
* Resets the shape to its center start position
*/
abstract fun reset()
}

@ -1,20 +1,29 @@
package net.trivernis.chunkmaster.lib.shapes
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.pow
import kotlin.math.sqrt
class Spiral(center: Pair<Int, Int>, start: Pair<Int, Int>, radius: Int): Shape(center, start, radius) {
class Square(center: Pair<Int, Int>, start: Pair<Int, Int>, radius: Int) : Shape(center, start, radius) {
private var direction = 0
override fun endReached(): Boolean {
val distances = getDistances(center, currentPos)
return radius > 0 && (distances.first > radius || distances.second > radius)
return radius > 0 && ((direction == 3
&& abs(distances.first) == abs(distances.second)
&& abs(distances.first) == radius)
|| (distances.first > radius || distances.second > radius))
}
override fun progress(): Double {
return (count / (radius * 2).toDouble().pow(2)).coerceAtMost(100.0)
override fun total(): Double {
return (radius * 2).toDouble().pow(2)
}
override fun progress(maxRadius: Int?): Double {
return if (maxRadius != null) {
(count / (maxRadius * 2).toDouble().pow(2)).coerceAtMost(1.0)
} else {
(count / (radius * 2).toDouble().pow(2)).coerceAtMost(1.0)
}
}
override fun currentRadius(): Int {
@ -26,28 +35,31 @@ class Spiral(center: Pair<Int, Int>, start: Pair<Int, Int>, radius: Int): Shape(
* Returns the next value in the spiral
*/
override fun next(): Pair<Int, Int> {
if (endReached()) {
return currentPos
}
if (count == 0 && currentPos != center) {
// simulate the spiral to get the correct direction and count
val simSpiral = Spiral(center, center, radius)
while (simSpiral.next() != currentPos);
val simSpiral = Square(center, center, radius)
while (simSpiral.next() != currentPos && !simSpiral.endReached());
direction = simSpiral.direction
count = simSpiral.count
}
if (count == 1) { // because of the center behaviour
count ++
count++
return currentPos
}
if (currentPos == center) { // the center has to be handled exclusively
currentPos = Pair(center.first, center.second + 1)
count ++
count++
return center
} else {
val distances = getDistances(center, currentPos)
if (abs(distances.first) == abs(distances.second)) {
direction = (direction + 1)%5
}
direction = (direction + 1) % 5
}
when(direction) {
}
when (direction) {
0 -> {
currentPos = Pair(currentPos.first + 1, currentPos.second)
}
@ -65,7 +77,7 @@ class Spiral(center: Pair<Int, Int>, start: Pair<Int, Int>, radius: Int): Shape(
direction = 0
}
}
count ++
count++
return currentPos
}
@ -86,4 +98,13 @@ class Spiral(center: Pair<Int, Int>, start: Pair<Int, Int>, radius: Int): Shape(
private fun getDistances(pos1: Pair<Int, Int>, pos2: Pair<Int, Int>): Pair<Int, Int> {
return Pair(pos2.first - pos1.first, pos2.second - pos1.second)
}
/**
* Resets the shape to its starting parameters
*/
override fun reset() {
this.currentPos = center
this.count = 0
this.direction = 0
}
}

@ -1,11 +1,12 @@
RESUME_FOR_WORLD = Resuming chunk generation task for world '%s'...
TASK_FINISHED = Task #%d finished after %d chunks.
TASK_CANCELED = Canceled task #%s.
TASK_CANCELLED = Cancelled task #%s.
TASK_LOAD_FAILED = §cFailed to load task #%d.
TASK_LOAD_SUCCESS = %d saved tasks loaded.
TASK_NOT_FOUND = §cTask %s not found!
CREATE_DELAYED_LOAD = Creating task to load chunk generation Tasks later...
TASK_PERIODIC_REPORT = Task #%d running for '%s'. Progress: %d chunks %s %s, Speed: %.1f ch/s, Last Chunk: %d, %d
TASK_PERIODIC_REPORT = Task #%d running for '%s'. State: %s. Progress: %d chunks %s %s, Speed: %.1f ch/s, Last Chunk: %d, %d
TASK_PERIODIC_REPORT_CORRECTING = Task #%d generating missing chunks for '%s'. Progress: %d chunks %s
TASK_SAVE_FAILED = §cException when saving tasks: %s
WORLD_NAME_REQUIRED = §cYou need to provide a world name!
@ -18,10 +19,14 @@ TASK_ID_REQUIRED = §cYou need to provide a task id!
INVALID_ARGUMENT = §cInvalid argument at %s: %s!
PAUSED_TASKS_HEADER = Currently Paused Generation Tasks
TASKS_ENTRY = - §9#%d§r - §2%s§r - §2%d chunks %s§r
TASKS_ENTRY = - §9#%d§r - §2%s§r - §2%s§r - §2%s chunks %s§r
RUNNING_TASKS_HEADER = Currently Running Generation Tasks
NO_GENERATION_TASKS = There are no generation tasks.
COMPLETED_TASKS_HEADER = §nCompleted Generation Tasks§r
COMPLETED_WORLD_HEADER = §l%s§r
COMPLETED_TASK_ENTRY = - §9#%d§r: §2%d§r chunks radius from center §2(%d, %d)§r with shape §2%s§r
PAUSE_SUCCESS = §9Paused all generation tasks.
ALREADY_PAUSED = §cThe generation process is already paused!
@ -72,4 +77,9 @@ STATS_CORES = - Cores: §2%d§r
STATS_WORLD_NAME = §l%s§r
STATS_ENTITY_COUNT = - §2%d§r Entities
STATS_LOADED_CHUNKS = - §2%d§r Loaded Chunks
STATS_PLUGIN_LOADED_CHUNKS = - §2%d§r Chunks Loaded by Chunkmaster
STATS_PLUGIN_LOADED_CHUNKS = - §2%d§r Chunks Loaded by Chunkmaster
STATS_GENERATING = - Generating: §2%s§r
SAVING_CHUNKS = Saving %d loaded chunks...
CANCEL_FAIL = Failed to cancel task #%d in the given timeout!
NO_AUTOSTART = Autostart set to §2false§r. Pausing...

@ -1,11 +1,12 @@
RESUME_FOR_WORLD = Setze das Chunk-Generieren für Welt '%s' fort...
TASK_FINISHED = Aufgabe #%d wurde nach %d chunks beendet.
TASK_CANCELED = Aufgabe #%s wurde abgebrochen.
TASK_CANCELLED = Aufgabe #%s wurde abgebrochen.
TASK_LOAD_FAILED = §cAufgabe #%d konnte nicht geladen werden.
TASK_LOAD_SUCCESS = %d gespeicherte Aufgaben wurden geladen.
TASK_NOT_FOUND = §cAufgabe %s konnte nicht gefunden werden!
CREATE_DELAYED_LOAD = Erstelle einen Bukkit-Task zum verzögerten Laden von Aufgaben...
TASK_PERIODIC_REPORT = Aufgabe #%d für Welt '%s'. Fortschritt: %d chunks %s %s, Geschwindigkeit: %.1f ch/s, Letzer Chunk: %d, %d
TASK_PERIODIC_REPORT = Aufgabe #%d für Welt '%s'. Status: %s. Fortschritt: %d chunks %s %s, Geschwindigkeit: %.1f ch/s, Letzer Chunk: %d, %d
TASK_PERIODIC_REPORT_CORRECTING = Aufgabe #%d generiert fehlende Chunks für Welt '%s'. Fortschritt: %d chunks %s
TASK_SAVE_FAILED = §cFehler beim Speichern der Aufgaben: %s
WORLD_NAME_REQUIRED = §cDu musst einen Weltennamen angeben!
@ -72,4 +73,14 @@ STATS_CORES = - Kerne: §2%d§r
STATS_WORLD_NAME = §l%s§r
STATS_ENTITY_COUNT = - §2%d§r Entities
STATS_LOADED_CHUNKS = - §2%d§r Geladene Chunks
STATS_PLUGIN_LOADED_CHUNKS = - §2%d§r von Chunkmaster geladene Chunks
STATS_PLUGIN_LOADED_CHUNKS = - §2%d§r von Chunkmaster geladene Chunks
SAVING_CHUNKS = Speichere %d geladene Chunks...
CANCEL_FAIL = Konnte Aufgabe #%d nicht im angegebenen Timeout stoppen!
NO_AUTOSTART = Autostart ist auf §2false§r gesetzt. Pausiere...
COMPLETED_TASKS_HEADER = §nAbgeschlossene Aufgaben§r
COMPLETED_WORLD_HEADER = §l%s§r
COMPLETED_TASK_ENTRY = - §9#%d§r: §2%d§r Chunks Radius von der Mitte §2(%d, %d)§r aus in der Form §2%s§r
STATS_GENERATING = - Generiert: §2%s§r

@ -1,11 +1,11 @@
RESUME_FOR_WORLD = Resuming chunk generation task for world '%s'...
TASK_FINISHED = Task #%d finished after %d chunks.
TASK_CANCELED = Canceled task #%s.
TASK_CANCELLED = Cancelled task #%s.
TASK_LOAD_FAILED = §cFailed to load task #%d.
TASK_LOAD_SUCCESS = %d saved tasks loaded.
TASK_NOT_FOUND = §cTask %s not found!
CREATE_DELAYED_LOAD = Creating task to load chunk generation Tasks later...
TASK_PERIODIC_REPORT = Task #%d running for '%s'. Progress: %d chunks %s %s, Speed: %.1f ch/s, Last Chunk: %d, %d
TASK_PERIODIC_REPORT = Task #%d running for '%s'. State: %s. Progress: %d chunks %s %s, Speed: %.1f ch/s, Last Chunk: %d, %d
TASK_SAVE_FAILED = §cException when saving tasks: %s
WORLD_NAME_REQUIRED = §cYou need to provide a world name!
@ -18,7 +18,6 @@ TASK_ID_REQUIRED = §cYou need to provide a task id!
INVALID_ARGUMENT = §cInvalid argument at %s: %s!
PAUSED_TASKS_HEADER = Currently Paused Generation Tasks
TASKS_ENTRY = - §9#%d§r - §2%s§r - §2%d chunks %s§r
RUNNING_TASKS_HEADER = Currently Running Generation Tasks
NO_GENERATION_TASKS = There are no generation tasks.
@ -72,4 +71,14 @@ STATS_CORES = - Cores: §2%d§r
STATS_WORLD_NAME = §l%s§r
STATS_ENTITY_COUNT = - §2%d§r Entities
STATS_LOADED_CHUNKS = - §2%d§r Loaded Chunks
STATS_PLUGIN_LOADED_CHUNKS = - §2%d§r Chunks Loaded by Chunkmaster
STATS_PLUGIN_LOADED_CHUNKS = - §2%d§r Chunks Loaded by Chunkmaster
SAVING_CHUNKS = Saving %d loaded chunks...
CANCEL_FAIL = Failed to cancel task #%d in the given timeout!
NO_AUTOSTART = Autostart set to §2false§r. Pausing...
COMPLETED_TASKS_HEADER = §nCompleted Generation Tasks§r
COMPLETED_WORLD_HEADER = §l%s§r
COMPLETED_TASK_ENTRY = - §9#%d§r: §2%d§r chunks radius from center §2(%d, %d)§r with shape §2%s§r
STATS_GENERATING = - Generating: §2%s§r

@ -0,0 +1,75 @@
RESUME_FOR_WORLD = Reprise de la tâche de génération pour le monde '%s'...
TASK_FINISHED = Tâche #%d terminée après %d chunks.
TASK_CANCELED = Tâche #%s annulée.
TASK_LOAD_FAILED = §cImpossible de charger la tâche #%d.
TASK_LOAD_SUCCESS = %d tâches chargées avec succès.
TASK_NOT_FOUND = §cTâche %s introuvable!
CREATE_DELAYED_LOAD = Création de la tâche pour la génération du terrain plus tard...
TASK_PERIODIC_REPORT = Tâche #%d en cours pour '%s'. État: %s. Progression: %d chunks %s %s, Vitesse: %.1f ch/s, Dernier Chunk: %d, %d
TASK_SAVE_FAILED = §cErreur lors de la sauvegarde de la tâche: %s
WORLD_NAME_REQUIRED = §cVous devez renseigner le nom d'un monde!
WORLD_NOT_FOUND = §cMonde §2%s §cintrouvable!
TASK_ALREADY_EXISTS = §cUne tâche pour '%s' existe déjà!
TASK_CREATION_SUCCESS = §9Tâche de génération pour le monde §2%s§9 §2%s§9 avec une forme %s créée avec succès!
TASK_UNIT_WORLDBORDER = jusqu'à la limite du monde
TASK_UNIT_RADIUS = avec un rayon de %s
TASK_ID_REQUIRED = §cVous devez renseigner l'id de la tache!
INVALID_ARGUMENT = §cArgument invalide à %s: %s!
PAUSED_TASKS_HEADER = Tâches de génération en pause
TASKS_ENTRY = - §9#%d§r - §2%s§r - §2%s§r - §2%s chunks %s§r
RUNNING_TASKS_HEADER = Tâches de génération en cours
NO_GENERATION_TASKS = Il n'y a aucune tâche de génération.
PAUSE_SUCCESS = §9Mise en pause de toutes les tâches de génération.
ALREADY_PAUSED = §cLa génération est déjà en pause!
RESUME_SUCCESS = §9Reprise de toutes les tâches de génération.
NOT_PAUSED = §cLa génération n'est pas en pause!
CONFIG_RELOADING = Rechargement du fichier de configuration...
CONFIG_RELOADED = §2La configuration a été rechargé!
TELEPORTED = §9Vous avez été téléporté au chunk §2%s, %s
TP_ONLY_PLAYER = §cCette commande ne peut être exécutée que par un joueur!
NO_PERMISSION = §cVous n'avez pas la permission d'exécuter cette commande!
SUBCOMMAND_NOT_FOUND = §cSSous-commande §2%s §cintrouvable!
STOPPING_ALL_TASKS = Arrêts de toutes les tâches de génération...
SAVING_TASK_PROGRESS = Sauvegarde de la progression de la tâche #%s dans la base de données...
DB_INIT = Initialisation de la base de données...
DB_INIT_FINISHED = Base de données initialisée.
DB_INIT_EROR = Erreur lors de l'initialisation de la base de données: %s.
DATABASE_CONNECTION_ERROR = §cConnexion impossible à la base de données!
SQL_ERROR = §cErreur lors de l'exécution SQL %s!
NO_DATABASE_CONNECTION = §cImpossible d'exécuter une requête SQL: Pas de connexion à la base de données.
CREATE_TABLE_DEFINITION = Table créée %s avec pour définition %s.
TABLE_CREATE_ERROR = §cErreur lors de la création de la table %s.
UPDATE_TABLE_DEFINITION = Mise à jour de la table %s par la requête SQL %s.
UPDATE_TABLE_FAILED = Impossible de mettre à jour la table %s par la requête SQL %s.
TOO_FEW_ARGUMENTS = §cIl manques des arguments pour exécuter la commande.
COORD_INVALID = §cLes coordonnées renseignées ('%s', '%s') sont invalides!
CENTER_UPDATED = §9Le centre du monde §2%s §9a été défini en §2(%s, %s)§9.
CENTER_INFO = §9Le centre du monde §2%s §9est §2(%s, %s)§9.
PLUGIN_DETECTED = Version de %s détectée %s
RESUME_PLAYER_LEAVE = Le nombre de joueurs est plus bas que la limite configurée. Reprise de la génération...
PAUSE_PLAYER_JOIN = Le nombre de joueurs a atteint la limite configurée. Arrêt de la génération...
PAUSE_MANUALLY = La génération a été manuellement arrêtée. Celle-ci ne reprendra pas automatiquement.
STATS_HEADER = §nStatistiques§r
STATS_SERVER = §lStatistiques serveur§r
STATS_SERVER_VERSION = - Version du serveur: §2%s§r
STATS_PLUGIN_VERSION = - Version du plugin: §2%s§r
STATS_MEMORY = - Mémoire vive (u/a): §2%d mb / %d mb = (%.2f pourcent)§r
STATS_CORES = - Coeurs processeur: §2%d§r
STATS_WORLD_NAME = §l%s§r
STATS_ENTITY_COUNT = - §2%d§r Entités
STATS_LOADED_CHUNKS = - §2%d§r Chunks chargés
STATS_PLUGIN_LOADED_CHUNKS = - §2%d§r Chunks chargés par Chunkmaster

@ -0,0 +1,80 @@
RESUME_FOR_WORLD = 正在恢复执行 '%s' 世界的区块生成任务...
TASK_FINISHED = 任务 #%d 在生成 %d 个区块后完成.
TASK_CANCELLED = 已取消任务 #%s.
TASK_LOAD_FAILED = §c加载任务 #%d 失败.
TASK_LOAD_SUCCESS = %d 个已保存的任务加载完成.
TASK_NOT_FOUND = §c任务 %s 未找到!
CREATE_DELAYED_LOAD = 正在创建延迟执行的区块生成任务...
TASK_PERIODIC_REPORT = 任务 #%d 正在 '%s' 世界执行. 状态: %s. 进度: %d 区块 %s %s, 速度: %.1f 区块 / 秒, 最新生成的区块: %d, %d
TASK_PERIODIC_REPORT_CORRECTING = 任务 #%d 正在为世界 '%s' 生成缺失的区块. 进度: %d 区块 %s
TASK_SAVE_FAILED = §c保存任务时发生错误: %s
WORLD_NAME_REQUIRED = §c你需要提供世界名称!
WORLD_NOT_FOUND = §c无法找到名为 §2%s §c的世界!
TASK_ALREADY_EXISTS = §c已存在 '%s' 的任务!
TASK_CREATION_SUCCESS = §9已创建 §2%s§9 世界的区块生成任务§2%s§9 的 %s 区域!
TASK_UNIT_WORLDBORDER = 直到世界边界
TASK_UNIT_RADIUS = 半径 %s
TASK_ID_REQUIRED = §c你需要提供任务 ID!
INVALID_ARGUMENT = §c在 %s: %s 存在无效的变量!
PAUSED_TASKS_HEADER = 当前暂停的区块生成任务
TASKS_ENTRY = - §9#%d§r - §2%s§r - §2%s§r - §2%s 区块 %s§r
RUNNING_TASKS_HEADER = 当前运行的区块生成任务
NO_GENERATION_TASKS = 无区块生成任务.
PAUSE_SUCCESS = §9已暂停所有区块生成任务.
ALREADY_PAUSED = §c区块生成进程已经暂停!
RESUME_SUCCESS = §9已恢复执行所有区块生成任务.
NOT_PAUSED = §c区块生成进程并未暂停!
CONFIG_RELOADING = 正在重载配置文件...
CONFIG_RELOADED = §2配置文件重载完成!
TELEPORTED = §9你已被传送到区块 §2%s, %s
TP_ONLY_PLAYER = §c此命令只能由玩家执行!
NO_PERMISSION = §c你无权执行此命令!
SUBCOMMAND_NOT_FOUND = §c子命令 §2%s §c未找到!
STOPPING_ALL_TASKS = 正在停止所有区块生成任务...
SAVING_TASK_PROGRESS = 正在保存任务进度 #%s 到数据库中...
DB_INIT = 正在初始化数据库...
DB_INIT_FINISHED = 数据库初始化完成.
DB_INIT_EROR = 初始化数据库时发生错误: %s.
DATABASE_CONNECTION_ERROR = §c连接数据库失败!
SQL_ERROR = §cSQL 发生错误: %s !
NO_DATABASE_CONNECTION = §c无法执行 SQL 语句: 无数据库连接.
CREATE_TABLE_DEFINITION = 已创建表 %s ,定义 %s.
TABLE_CREATE_ERROR = §c创建表 %s 失败.
UPDATE_TABLE_DEFINITION = 已更新表 %s SQL 语句为 %s.
UPDATE_TABLE_FAILED = 无法更新表 %s SQL 语句为 %s.
TOO_FEW_ARGUMENTS = §c你没有提供足够的参数.
COORD_INVALID = §c提供的坐标 ('%s', '%s') 无效!
CENTER_UPDATED = §9世界 §2%s §9的中心已设为 §2(%s, %s)§9.
CENTER_INFO = §9世界 §2%s §9的中心为 §2(%s, %s)§9.
PLUGIN_DETECTED = 检测到 %s 版本 %s
RESUME_PLAYER_LEAVE = 在线玩家数量小于配置文件设定的阈值。正在恢复生成...
PAUSE_PLAYER_JOIN = 在线玩家数量达到配置文件设定的阈值。正在暂停生成...
PAUSE_MANUALLY = 生成进程已被手动暂停。将不会自动重启任务.
STATS_HEADER = §n状态与统计§r
STATS_SERVER = §l服务器状态§r
STATS_SERVER_VERSION = - 服务器版本: §2%s§r
STATS_PLUGIN_VERSION = - 插件版本: §2%s§r
STATS_MEMORY = - 内存 (u/a): §2%d MB / %d MB = (%.2f percent)§r
STATS_CORES = - CPU 核心: §2%d§r
STATS_WORLD_NAME = §l%s§r
STATS_ENTITY_COUNT = - §2%d§r 实体
STATS_LOADED_CHUNKS = - §2%d§r 已载入区块
STATS_PLUGIN_LOADED_CHUNKS = - §2%d§r 被 Chunk Master 载入的区块
SAVING_CHUNKS = 正在保存 %d 已载入的区块...
CANCEL_FAIL = 取消任务 #%d 操作超时!
NO_AUTOSTART = 自动启动被设置为 §2关闭§r. 正在暂停...

@ -1,7 +1,7 @@
main: net.trivernis.chunkmaster.Chunkmaster
name: Chunkmaster
version: '1.2.1'
description: Chunk commands plugin.
version: $$PLUGIN_VERSION$$
description: Automated world pregeneration.
author: Trivernis
website: trivernis.net
api-version: '1.14'
@ -23,19 +23,20 @@ commands:
/<command> setCenter [[<world>] <chunkX> <chunkZ>]] - sets the center chunk of the world
/<command> getCenter [<world>] - returns the center chunk of the world
/<command> stats [<world>] - returns some chunk stats for the world or the whole server
/<command> completed - lists all completed tasks for all worlds
aliases:
- chm
- chunkm
- cmaster
permissions:
cunkmaster.generate:
chunkmaster.generate:
description: Allows the generate subcommand.
default: op
chunkmaster.list:
description: Allows the list subcommand.
default: op
chunkmaster.cancel:
description: Allows the remove subcommand.
description: Allows the cancel subcommand.
default: op
chunkmaster.pause:
description: Allows the pause subcommand.
@ -55,6 +56,12 @@ permissions:
chunkmaster.getcenter:
description: Allows the getCenter subcommand.
default: op
chunkmaster.stats:
description: Allows the stats subcommand.
deault: op
chunkmaster.completed:
description: Allows the completed subcommand.
default: op
chunkmaster.chunkmaster:
description: Allows Chunkmaster commands.
default: op
@ -63,8 +70,14 @@ permissions:
default: op
children:
- chunkmaster.generate
- chunkmaster.listgentasks
- chunkmaster.removegentask
- chunkmaster.pausegentasks
- chunkmaster.resumegentasks
- chunkmaster.list
- chunkmaster.cancel
- chunkmaster.pause
- chunkmaster.resume
- chunkmaster.completed
- chunkmaster.tpchunk
- chunkmaster.reload
- chunkmaster.setcenter
- chunkmaster.getcenter
- chunkmaster.stats
- chunkmaster.chunkmaster

@ -0,0 +1,60 @@
package net.trivernis.chunkmaster.lib
import io.kotest.matchers.shouldBe
import org.junit.Test
class ArgParserTest {
var argParser = ArgParser()
@Test
fun `it parses arguments`() {
argParser.parseArguments("first second third forth").shouldBe(listOf("first", "second", "third", "forth"))
}
@Test
fun `it handles escaped sequences`() {
argParser.parseArguments("first second\\ pt2 third").shouldBe(listOf("first", "second pt2", "third"))
argParser.parseArguments("first \"second\\\" part 2\" third")
.shouldBe(listOf("first", "second\" part 2", "third"))
argParser.parseArguments("first \\\\second third").shouldBe(listOf("first", "\\second", "third"))
}
@Test
fun `it parses quoted arguments as one argument`() {
argParser.parseArguments("first \"second with space\" third")
.shouldBe(listOf("first", "second with space", "third"))
argParser.parseArguments("\"first\" \"second\" \"third\"").shouldBe(listOf("first", "second", "third"))
}
@Test
fun `it parses single arguments`() {
argParser.parseArguments("one").shouldBe(listOf("one"))
argParser.parseArguments("\"one\"").shouldBe(listOf("one"))
}
@Test
fun `it parses no arguments`() {
argParser.parseArguments("").shouldBe(emptyList())
}
@Test
fun `it parses just whitespace as no arguments`() {
argParser.parseArguments(" ").shouldBe(emptyList())
argParser.parseArguments("\t\t").shouldBe(emptyList())
}
@Test
fun `it parses arguments with weird whitespace`() {
argParser.parseArguments(" first second \t third \n forth ")
.shouldBe(listOf("first", "second", "third", "forth"))
}
@Test
fun `it deals predictable with malformed input`() {
argParser.parseArguments("first \"second third fourth").shouldBe(listOf("first", "second third fourth"))
argParser.parseArguments("\"first second \"third\" fourth")
.shouldBe(listOf("first second ", "third", " fourth"))
argParser.parseArguments("first second third fourth\"").shouldBe(listOf("first", "second", "third", "fourth"))
argParser.parseArguments("\"").shouldBe(emptyList())
}
}

@ -0,0 +1,29 @@
package net.trivernis.chunkmaster.lib
import io.kotest.matchers.string.shouldNotBeEmpty
import io.mockk.every
import io.mockk.mockk
import net.trivernis.chunkmaster.Chunkmaster
import org.bukkit.configuration.file.FileConfiguration
import org.junit.Test
class LanguageManagerTest {
private var langManager: LanguageManager
init {
val plugin = mockk<Chunkmaster>()
val config = mockk<FileConfiguration>()
every { plugin.dataFolder } returns createTempDir()
every { plugin.config } returns config
every { config.getString("language") } returns "en"
langManager = LanguageManager(plugin)
langManager.loadProperties()
}
@Test
fun `it returns localized for a key`() {
langManager.getLocalized("NOT_PAUSED").shouldNotBeEmpty()
}
}

@ -0,0 +1,75 @@
package net.trivernis.chunkmaster.lib.shapes
import io.kotest.matchers.booleans.shouldBeTrue
import io.kotest.matchers.collections.shouldContainAll
import io.kotest.matchers.doubles.shouldBeBetween
import io.kotest.matchers.shouldBe
import org.junit.Test
import org.junit.jupiter.api.BeforeEach
class CircleTest {
private val circle = Circle(center = Pair(0, 0), radius = 2, start = Pair(0, 0))
@BeforeEach
fun init() {
circle.reset()
}
@Test
fun `it generates coordinates`() {
circle.next().shouldBe(Pair(0, 0))
circle.next().shouldBe(Pair(-1, -1))
circle.next().shouldBe(Pair(1, 0))
circle.next().shouldBe(Pair(-1, 0))
circle.next().shouldBe(Pair(1, -1))
circle.next().shouldBe(Pair(-1, 1))
circle.next().shouldBe(Pair(0, 1))
circle.next().shouldBe(Pair(0, -1))
circle.next().shouldBe(Pair(1, 1))
}
@Test
fun `it reports when reaching the end`() {
for (i in 1..25) {
circle.next()
}
circle.endReached().shouldBeTrue()
}
@Test
fun `it reports the radius`() {
for (i in 1..9) {
circle.next()
}
circle.currentRadius().shouldBe(1)
}
@Test
fun `it returns the right edges`() {
circle.getShapeEdgeLocations().shouldContainAll(
listOf(
Pair(2, -1),
Pair(2, 0),
Pair(2, 1),
Pair(1, 2),
Pair(0, 2),
Pair(-1, 2),
Pair(-2, 1),
Pair(-2, 0),
Pair(-2, -1),
Pair(-1, -2),
Pair(0, -2),
Pair(1, -2),
)
)
}
@Test
fun `it returns the progress`() {
circle.progress(2).shouldBe(0)
for (i in 1..7) {
circle.next()
}
circle.progress(2).shouldBeBetween(.5, .8, .0)
}
}

@ -0,0 +1,62 @@
package net.trivernis.chunkmaster.lib.shapes
import io.kotest.matchers.booleans.shouldBeTrue
import io.kotest.matchers.collections.shouldContainAll
import io.kotest.matchers.shouldBe
import org.junit.Test
import org.junit.jupiter.api.BeforeEach
class SquareTest {
private val square = Square(center = Pair(0, 0), radius = 2, start = Pair(0, 0))
@BeforeEach
fun init() {
square.reset()
}
@Test
fun `it generates coordinates`() {
square.next().shouldBe(Pair(0, 0))
square.next().shouldBe(Pair(0, 1))
square.next().shouldBe(Pair(1, 1))
square.next().shouldBe(Pair(1, 0))
square.next().shouldBe(Pair(1, -1))
square.next().shouldBe(Pair(0, -1))
square.next().shouldBe(Pair(-1, -1))
square.next().shouldBe(Pair(-1, 0))
square.next().shouldBe(Pair(-1, 1))
square.next().shouldBe(Pair(-1, 2))
square.next().shouldBe(Pair(0, 2))
}
@Test
fun `it reports when reaching the end`() {
for (i in 1..25) {
square.next()
}
square.endReached().shouldBeTrue()
}
@Test
fun `it reports the radius`() {
for (i in 1..9) {
square.next()
}
square.currentRadius().shouldBe(1)
}
@Test
fun `it returns the right edges`() {
square.getShapeEdgeLocations().shouldContainAll(listOf(Pair(2, 2), Pair(-2, 2), Pair(2, -2), Pair(-2, -2)))
}
@Test
fun `it returns the progress`() {
square.progress(2).shouldBe(0)
for (i in 1..8) {
square.next()
}
square.progress(2).shouldBe(0.5)
}
}
Loading…
Cancel
Save