Compare commits

..

144 Commits

Author SHA1 Message Date
Julius Riegel 1950f83df5
Update README.md 3 years ago
Trivernis c12e96e8a0
Merge pull request #114 from Trivernis/develop
Spanish Translation by Taichikuji
4 years ago
trivernis a5bb701c80
Increment Version
Signed-off-by: trivernis <trivernis@protonmail.com>
4 years ago
Trivernis df2f59469d
Merge pull request #113 from taichikuji/codespace-83d0
Add es_ES i18n Support
4 years ago
Iván Pérez 508cd7bf8f Add es_ES i18n Support
Changes to be committed:
    new file:   es.i18n.properties
4 years ago
Trivernis da1ff68542
Merge pull request #112 from Trivernis/develop
Update zh translation
4 years ago
trivernis a8a9be2bdb
Bump version
Signed-off-by: trivernis <trivernis@protonmail.com>
4 years ago
Trivernis 65cd93e188
Merge pull request #111 from NPBeta/develop
Update zh_CN Translations to 1.4.0
4 years ago
NPBeta e304f25103
Update zh.i18n.properties
Update to 1.4.0
4 years ago
NPBeta 6fedee2f1c
Merge pull request #1 from Trivernis/develop
Develop
4 years ago
Trivernis ebab533e08
Merge pull request #109 from Trivernis/develop
1.4.0 Fixes and additions
4 years ago
trivernis ec7af49267
Add support for escaped whitespace to arg parser
Signed-off-by: trivernis <trivernis@protonmail.com>
4 years ago
trivernis aad4143e8a
Add custom args parser to allow spaces in arguments
Fixes #96 where worlds with spaces could not be
used with chunkmaster.

Signed-off-by: trivernis <trivernis@protonmail.com>
4 years ago
trivernis f15dc646f6
Remove test dependencies from jar build
Signed-off-by: trivernis <trivernis@protonmail.com>
4 years ago
trivernis e545445348
Add completed command to list completed tasks
Signed-off-by: trivernis <trivernis@protonmail.com>
4 years ago
trivernis 90f2bbc2af
Fix gradle build
Signed-off-by: trivernis <trivernis@protonmail.com>
4 years ago
trivernis a3f940c7d0
Merge branch 'develop' of github.com:Trivernis/spigot-chunkmaster into develop 4 years ago
Trivernis 6680e82433
Merge pull request #108 from Trivernis/feature/unit-tests
Add unit tests for shapes
4 years ago
trivernis b46b3f78cd
Add unit test for language manager
Signed-off-by: trivernis <trivernis@protonmail.com>
4 years ago
trivernis dcfd9f11f1
Add unit tests for shapes
Signed-off-by: trivernis <trivernis@protonmail.com>
4 years ago
Trivernis 689fea28d1
Merge pull request #107 from Trivernis/develop
Patch 1.3.4
4 years ago
trivernis 49b1f8f1c7
Fix warnings in build.gradle
Signed-off-by: trivernis <trivernis@protonmail.com>
4 years ago
trivernis 930ae728d0
Add plugin version placeholder and bump version
Signed-off-by: trivernis <trivernis@protonmail.com>
4 years ago
Trivernis b485bf9999
Merge pull request #105 from Doregon/master
Update Kotlin and PaperLib
4 years ago
Trivernis fe3c59df5e
Merge pull request #106 from Trivernis/actions
Actions
4 years ago
trivernis 626003028a
Remove abstruse task
Signed-off-by: trivernis <trivernis@protonmail.com>
4 years ago
trivernis 7a27618100
Remove gradle folder from gitignore
Signed-off-by: trivernis <trivernis@protonmail.com>
4 years ago
trivernis 1119942082
Change gradle execution again
Signed-off-by: trivernis <trivernis@protonmail.com>
4 years ago
trivernis e458757609
Update gradle wrapper and change java version
Signed-off-by: trivernis <trivernis@protonmail.com>
4 years ago
trivernis cbc0fcdb64
Fix gradle wrapper call
Signed-off-by: trivernis <trivernis@protonmail.com>
4 years ago
trivernis aad3bbdb07
Change gradle command in build task
Signed-off-by: trivernis <trivernis@protonmail.com>
4 years ago
trivernis af5ed6413d
Remove gradle wrappers from gitignore
Signed-off-by: trivernis <trivernis@protonmail.com>
4 years ago
trivernis 8567a7ffd1
Add github action
Signed-off-by: trivernis <trivernis@protonmail.com>
4 years ago
Adam Tunnic 0e0166c7e3
Update Kotlin and PaperLib 4 years ago
Trivernis 6144576440
Merge pull request #100 from Trivernis/develop
Develop
4 years ago
trivernis fca6c9eea2
Fix typo in permissions
Signed-off-by: trivernis <trivernis@protonmail.com>
4 years ago
trivernis 3c07d317d2
Fix french translation
Signed-off-by: trivernis <trivernis@protonmail.com>
4 years ago
Trivernis a0f6f4832f
Merge pull request #95 from Trivernis/develop
Bug Fixes
4 years ago
trivernis 99a48c1465 Merge branch 'develop' of github.com:Trivernis/spigot-chunkmaster into develop 4 years ago
trivernis 4947731718 Remove inaccurate chunk count from worldborder chm list 4 years ago
Trivernis b72c473854
Merge branch 'master' into develop 4 years ago
trivernis 6df01dd137 Add progress report to tasks generating to the worldborder
- Closes #90
4 years ago
trivernis 3cfc49baa8 Fix player count togglable pausing
- Fixes #89
4 years ago
trivernis 9b618eccc2 Fix dynmap version being null when dynmap doesn't support the minecraft versio
- Fixes #69
4 years ago
trivernis 65e3e090bb Fix insertion of pending chunks using too many parameters
- Fixes #91
4 years ago
trivernis f0ea063026 Fix setCenter command throwing an error when only one argument is provided
- Fixes #94
4 years ago
Trivernis 82662bc4d3
Develop (#88)
Merge develop into master

* Update Translation to 1.3.0 (#86)

* Update Translation to 1.3.0

* Tweak SQL_ERROR word order

Co-authored-by: NPBeta <shanhang007@gmail.com>

* Update version to 1.3.1

* Remove empty body of PausedTaskEntry

Co-authored-by: NPBeta <shanhang007@gmail.com>
4 years ago
trivernis 733996e6a9 Remove empty body of PausedTaskEntry 4 years ago
trivernis a91e26652d Update version to 1.3.1 4 years ago
trivernis 3417ffe339 Merge branch 'master' into develop 4 years ago
Trivernis 441b13ec69
Fix/merge conflict (#87)
* Version 1.3.0 (#85)

* Feature/async chunkmaster (#81)

* Change generation to be asynchronous

At this stage it will most likely crash the server at a certain point.
Pausing and resuming isn't stable. Saving the progress isn't stable as well.
Chunks are being unloaded in the main thread by an unloader class.

* Switch to native threads

- Use thread instead of async tasks
- Store pending paper chunks in the database
- Interrupt the thread when it should be stopped

* Fix insertion of pending chunks

Fix an error that is thrown when the sql for inserting pending chunks doesn't have any chunks to insert.

* Add task states

Add states to differentiate between generating and validating tasks
as well as a field in the database to store this information.
A task will first generate a world until the required radius or the
worldborder is reached. Then it validates that each chunk has been generated.

* Add object representation of world_properties table

* Add DAO for pending_chunks table

* Add DAO for generation_tasks table

* Add state updating to periodic save

* Fix loading of world properties

* Add states to tasks and fix completion handling

* Fix progress report and spiral shape

* Modify the paper generation task so it works with spigot

This change is being made because normal chunk generation doesn't allow
chunks to be requested from a different thread. With PaperLib this issue
can be solved.

* Add workarounds for spigot problems

* Fix some blocking issues and update README

* Add locking to ChunkUnloader class

* Add total chunk count to list command (closes #79) (#82)

* Fix shape beign stuck (#83)

* Add autostart config parameter (closes #78) (#84)

* Add circleci badge to readme

* Add codefactor badge

* Update Translation to 1.3.0 (#86)

* Update Translation to 1.3.0

* Tweak SQL_ERROR word order

Co-authored-by: NPBeta <shanhang007@gmail.com>
4 years ago
trivernis aaa614f651 Cleanup 4 years ago
Trivernis 4d4107aaf3
Version 1.3.0 (#85)
* Feature/async chunkmaster (#81)

* Change generation to be asynchronous

At this stage it will most likely crash the server at a certain point.
Pausing and resuming isn't stable. Saving the progress isn't stable as well.
Chunks are being unloaded in the main thread by an unloader class.

* Switch to native threads

- Use thread instead of async tasks
- Store pending paper chunks in the database
- Interrupt the thread when it should be stopped

* Fix insertion of pending chunks

Fix an error that is thrown when the sql for inserting pending chunks doesn't have any chunks to insert.

* Add task states

Add states to differentiate between generating and validating tasks
as well as a field in the database to store this information.
A task will first generate a world until the required radius or the
worldborder is reached. Then it validates that each chunk has been generated.

* Add object representation of world_properties table

* Add DAO for pending_chunks table

* Add DAO for generation_tasks table

* Add state updating to periodic save

* Fix loading of world properties

* Add states to tasks and fix completion handling

* Fix progress report and spiral shape

* Modify the paper generation task so it works with spigot

This change is being made because normal chunk generation doesn't allow
chunks to be requested from a different thread. With PaperLib this issue
can be solved.

* Add workarounds for spigot problems

* Fix some blocking issues and update README

* Add locking to ChunkUnloader class

* Add total chunk count to list command (closes #79) (#82)

* Fix shape beign stuck (#83)

* Add autostart config parameter (closes #78) (#84)

* Add circleci badge to readme

* Add codefactor badge
4 years ago
trivernis 7ffada4100 Add codefactor badge 4 years ago
trivernis cf7ef7d887 Add circleci badge to readme 4 years ago
Trivernis ac6a80ed86
Add autostart config parameter (closes #78) (#84) 4 years ago
Trivernis af875aaca0
Fix shape beign stuck (#83) 4 years ago
Trivernis 1f167285b0
Add total chunk count to list command (closes #79) (#82) 4 years ago
Trivernis acf302e8c1
Feature/async chunkmaster (#81)
* Change generation to be asynchronous

At this stage it will most likely crash the server at a certain point.
Pausing and resuming isn't stable. Saving the progress isn't stable as well.
Chunks are being unloaded in the main thread by an unloader class.

* Switch to native threads

- Use thread instead of async tasks
- Store pending paper chunks in the database
- Interrupt the thread when it should be stopped

* Fix insertion of pending chunks

Fix an error that is thrown when the sql for inserting pending chunks doesn't have any chunks to insert.

* Add task states

Add states to differentiate between generating and validating tasks
as well as a field in the database to store this information.
A task will first generate a world until the required radius or the
worldborder is reached. Then it validates that each chunk has been generated.

* Add object representation of world_properties table

* Add DAO for pending_chunks table

* Add DAO for generation_tasks table

* Add state updating to periodic save

* Fix loading of world properties

* Add states to tasks and fix completion handling

* Fix progress report and spiral shape

* Modify the paper generation task so it works with spigot

This change is being made because normal chunk generation doesn't allow
chunks to be requested from a different thread. With PaperLib this issue
can be solved.

* Add workarounds for spigot problems

* Fix some blocking issues and update README

* Add locking to ChunkUnloader class
4 years ago
Trivernis 400295ac73
Merge pull request #80 from Trivernis/develop
French Translation
4 years ago
trivernis 1ebf3c96a8 Bump version and mention translators in README 4 years ago
Trivernis 8987fe94aa
Merge pull request #77 from Corenb/master
Create fr.i18n.properties
4 years ago
Corentin Boiteau af3739bd65
Create fr.i18n.properties 4 years ago
Trivernis 434deb33d3
Merge pull request #76 from Trivernis/develop
Mandarin Translation
4 years ago
trivernis efb28e5ff9 Bump Version for Release 4 years ago
Trivernis c69b8fd91d
Merge pull request #73 from NPBeta/master
Add zh_CN i18n Support
4 years ago
NPBeta a708c1cfdd
Added Missing Translation & Tweak Translation
Fix unconsistent translation:
Using 失败 for errors that has no further information.
Using 发生错误 for exceptions with stacktrace or other informations.
4 years ago
NPBeta 984c49c644
Add zh_CN i18n Support 4 years ago
Trivernis d078e1150f
Merge pull request #72 from Trivernis/develop
Fix #66, #70 Task progress being discarded on resume
4 years ago
trivernis 94f70c39b0 Fix #66, #70 Task progress being discarded on resume 4 years ago
Trivernis 602b7c92e7
Update bug_report.md
People just don't report the exact server version!
4 years ago
Trivernis 4b93fc8dd8
Merge pull request #71 from Trivernis/develop
Version 1.2.0
4 years ago
Trivernis 64795b341b
Update bug_report.md 4 years ago
trivernis 3dec787d0e Fix commands for worlds with numeral names 4 years ago
Trivernis 6a133410ac
Update README.md 4 years ago
trivernis 615280a2ed Add stats command to print chunkmaster stats
- Server information
- Worlds chunk information
4 years ago
Trivernis 8ad6bc44af
Merge pull request #63 from Trivernis/patch/1.0.1
Patch/1.0.1
4 years ago
trivernis 928dfbf264 Fix #61 incorrect progress percentage 4 years ago
trivernis c2fad45c26 Update README 4 years ago
Trivernis af03dc0325
Merge pull request #60 from Trivernis/develop
Add Stalebot Config
4 years ago
trivernis 7b7fdd1969 Merge branch 'develop' of https://github.com/Trivernis/spigot-chunkmaster into develop 4 years ago
trivernis a2673b4952 Add stalebot config 4 years ago
Trivernis f836052e20
Merge pull request #59 from Trivernis/develop
Version 1.0
4 years ago
Trivernis d5ce796f9e
Merge pull request #58 from Trivernis/release/1.0
Release/1.0
4 years ago
trivernis 3f28c89f8d Make chunk coordinates for setCenter optional
- use the player location if no coordinates have been provided (closes #41)
4 years ago
trivernis 0bc38384b4 Change pause on join to have a configurable player count
Closes #31
4 years ago
trivernis ecc9309b41 Update README 4 years ago
trivernis 29d0f8991e Add option to pass the world name for cancel
- Add the option to pass the world name to chm cancel to cancel
generation tasks
- Fix #46 by adding additional error handling
4 years ago
Trivernis 4a6f7d98cf
Merge pull request #56 from Trivernis/develop
Update README
4 years ago
trivernis fd6b1e9190 Update README 4 years ago
Trivernis 9c5f486414
Merge pull request #55 from Trivernis/develop
Release Beta 1.16
4 years ago
trivernis a6d5462433 Add abstruse badge to README 5 years ago
Trivernis 18ac52cab8
Merge pull request #33 from Trivernis/ci-config
Add caching to ci-config
5 years ago
trivernis a377392fd2 Remove creation of .gradle directory due to errors 5 years ago
trivernis ac3c068f5d Add dependency resolving to install task 5 years ago
trivernis 3367af9adc Create .gradle dir before installation
Create .gradle directory to avoid errors when unpacking cached files (I hope this helps).
5 years ago
trivernis 3cfa38e03d Add .gradle to cache to speed up building 5 years ago
Trivernis 95102b1e86
Merge pull request #32 from Trivernis/ci-config
Ci config
5 years ago
trivernis b587f491cf Remove gradle install instructions from buildfile 5 years ago
trivernis d52bc304f4 Rever to openjdk image
I've read it wrong lol
5 years ago
trivernis dc802f9e00 Change to default docker and add jdk to install 5 years ago
trivernis bd57087427 Remove openjdk version tag 5 years ago
trivernis 72618e3e50 Change version of image for ci 5 years ago
trivernis b06018ec0d Add abstruse ci config 5 years ago
trivernis 03eaf52dc3 Add more comments 5 years ago
trivernis c090977021 Revert optimizations on spigot
Revert the optimizations which check if the chunk is already generated because
the function doesn't report the right value.
https://hub.spigotmc.org/jira/browse/SPIGOT-5541?attachmentOrder=desc
5 years ago
trivernis be130adc6f Improve spigot chunk generation
Add skipping of already generated chunks.
An issue remaining is that some chunks are still
not generated and the shape doesn't always match.
5 years ago
trivernis 2b121b67fd Fix generation stopping one chunk too early 5 years ago
trivernis 0347730580 Improve performance of periodical chunk progress saving task 5 years ago
trivernis 572369192a Add circlular generation tasks 5 years ago
trivernis 2eeff350f8 Add circular shape class 5 years ago
trivernis 53aec18a06 Change group for Chunkmaster dynmap markers
- Change the group to "Chunkmaster" to be able to disable all of those markers.
- Add a marker that displays the last chunk that has been generated and refreshes
every 30 seconds.
5 years ago
trivernis d967dfd06d Change sqlite-jdbc to be a compileOnly dependency 5 years ago
Trivernis dca3f25358 Update issue templates 5 years ago
Trivernis 36394d1768
Merge pull request #29 from Trivernis/develop
Release Beta 0.15
5 years ago
trivernis 8953b63c2b Add integration for dynmap
By integrating with dynmap the plugin can show the area of the generation task for each world via an area marker and
triggers the rendering of tiles for generated chunks.
5 years ago
Trivernis 832b1c4b55 Add command to set and get the center
- Add getCenter command to get the center of the world used for generation
- Add setCenter to set the world center for generation tasks. If there is already a task running for the world the center for that task will not be  affected.
- Fix SQL problem with multiple queries at the same time creating new connections
- Add translations for new commands
- Add Examples to Readme
- Add new table to safe world centers
- Add query to Generation manager to get the center of the world for new generation tasks
5 years ago
Trivernis bdac496ce2 Fix unique world sql error
Fix error thrown by unique constrain for the world name in the sql table. The error occurs because the data from the database is loaded lazily after several seconds. If a task was started before that it wouldn't be checked against the existing tasks since they haven't been loaded yet. Fixed by loading the tasks if none exists when the generate command is invoked.
5 years ago
trivernis a09bebb6fa Update README 5 years ago
trivernis ecc3165f1f Add delay to starting of generation tasks
Add a delay to the starts of the tasks so that they don't all start on the same tick. To completely avoid two tasks being executed on the same tick, the "period" config option should be set to the expected number of tasks or higher.
5 years ago
trivernis 6cb39ce843 Remove logging of eta in seconds 5 years ago
Trivernis eb222ad582
Merge pull request #23 from Trivernis/utf-8-i18n-properties
Utf 8 i18n properties
5 years ago
trivernis 109699f639 Remove unused dependencies 5 years ago
trivernis e5f812e3bc Fix unicode loading of i18n files 5 years ago
trivernis 1c94ced875 [wip] Try to load i18n file with utf-8 encoding 5 years ago
trivernis a4d39bdb6b Merge branch 'master' into develop 5 years ago
Trivernis 5f14885737 Update Plugin version in gradle file and README 5 years ago
Trivernis 407edff2df
Merge pull request #20 from Trivernis/develop
Release Beta 0.14
5 years ago
Trivernis a4115ebdf7 Add ETA to periodic reports
Closes #17
5 years ago
Trivernis 3d8c47aee2 Update english i18n 5 years ago
Trivernis 965598de2b Add i18n to every output
Closes #16
5 years ago
trivernis bd731c85f0 Add i18n to generation manager 5 years ago
trivernis 900600714e Add i18n structure 5 years ago
trivernis f88100be5f Fix chunk center coordinate being calculated the wrong way
Fixes #19
5 years ago
Trivernis f1ad4473de
Merge pull request #15 from Trivernis/develop
Fixes and changes to the sql structure
5 years ago
trivernis f89be3a73c Update Readme 5 years ago
Trivernis 24b2707293
Merge branch 'master' into develop 5 years ago
trivernis f89262e538 Add ignore-worldborder config option
- [Convert to unix linebreaks]
- Add ignore-worldborder config option to generate past the vanilla world border
5 years ago
Trivernis ce826419f1 Added config parameter
- made database filename configurable
5 years ago
Trivernis 3a0012ef3f Added connection close safety
- moved connection close outside of try catch to finally to ensure that the connection is closed even on error (SqliteManager)
5 years ago
Trivernis 8b63643f7a Changes to sql structure
- changed SqliteUpdateManager to SqliteManager
- added methods to execute sql with values and callback to the SqliteManager
- added auto close of statements
- switched to establishing a connection only when sql should be executed
5 years ago
Trivernis b09b96d95c Fixes and version change
- increased version number
- fixed stacktrace print for sql update manager
- closed table info in the update manager
5 years ago
Trivernis d7052e1ac9 enabled error reports for sql update manager 5 years ago
Trivernis 160ee13ac8 Removed Chunk unloading on save event 5 years ago

@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG]"
labels: bug
assignees: Trivernis
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
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](PLEASE PROVIDE THE EXACT VERSION OF THE /version COMMAND, NOT JUST spigot 1.16.1!)
- Plugin Version [e.g. 0.15]
**Additional context**
Add any other context about the problem here.

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[FEATURE]"
labels: enhancement
assignees: Trivernis
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

@ -0,0 +1,16 @@
---
name: Question
about: Ask a question about this project
title: "[QUESTION]"
labels: question
assignees: Trivernis
---
**Question**
A clear and concise question that wasn't asked yet.
**Additional context**
Add any other context or screenshots that are needed to understand the question here.

19
.github/stale.yml vendored

@ -0,0 +1,19 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 14
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
- enhancement
- bug
# Label to use when marking an issue as stale
staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

@ -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

2
.gitignore vendored

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

@ -1,57 +1,99 @@
# chunkmaster [![CircleCI](https://circleci.com/gh/Trivernis/spigot-chunkmaster.svg?style=svg)](https://circleci.com/gh/Trivernis/spigot-chunkmaster) # 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. <div align="center">
<h1>This plugin isn't actively developed anymore. If you want to add features feel free to fork it and implement it yourself or use <a href="https://www.spigotmc.org/resources/chunky.81534/">Chunky</a> instead.</h1>
</div>
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) The generation automatically pauses when a player joins the server (assuming the server was empty before)
and resumes when the server is empty again. The generation also auto-resumes after a server and resumes when the server is empty again. The generation also auto-resumes after a server
restart. The plugin tracks the ticks per second and pauses the generation when the tps restart. The plugin tracks the ticks per second and pauses the generation when the tps
is lower than 2 (configurable). is lower than 2 (configurable).
## Commands ## Built with
- [Gradle](https://gradle.org/) - Dependency Management and Build Tool
- [Sqlite JDBC](https://bitbucket.org/xerial/sqlite-jdbc/) - Database Driver for JDBC
- [bStats](https://bstats.org/) - Statistical Insights
## Features
- Pregeneration of a specific area around the world center
- Configuration of world centers
- Integration into dynmap
- Teleportation to chunks
- Auto-Pause/Resume on player join/leave
- Highly configurable
## Installing
Just download the jar from the latest release and place it into the servers plugins folder.
## Usage and Configuration
### Commands
All features can be accessed with the command `/chunkmaster` or the aliases `/chm`, `chunkm`, `cmaster`. All features can be accessed with the command `/chunkmaster` or the aliases `/chm`, `chunkm`, `cmaster`.
- `/chunkmaster generate [world] [chunk count] [unit]` Starts the generation until the specified chunk count or the world border is reached. - `/chunkmaster generate [world] [radius] [<square|circle>]` Starts the generation until the specified chunk count or the world border is reached.
- `/chunkmaster list` Lists all running generation tasks - `/chunkmaster list` Lists all running generation tasks
- `/chunkmaster cancel <Task id>` Cancels the generation task with the specified id (if it is running). - `/chunkmaster cancel <Task id|world name>` Cancels the generation task with the specified id (if it is running).
- `/chunkmaster pause` Pauses all generation tasks until the resume command is executed. - `/chunkmaster pause` Pauses all generation tasks until the resume command is executed.
- `/chunkmaster resume` Resumes all paused generation tasks. - `/chunkmaster resume` Resumes all paused generation tasks.
- `/chunkmaster reload` Reloads the configuration file. - `/chunkmaster reload` Reloads the configuration file.
- `/chunkmaster tpchunk <X> <Z>` Teleports you to the specified chunk coordinates. - `/chunkmaster tpchunk <X> <Z>` Teleports you to the specified chunk coordinates.
- `/<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 the stats of the server or a specific world
#### Examples
**Generate a 100 chunks * 100 blocks square around the center:**
`/chm generate [world] 50`
## Config **Generate a circle with a radius of 100 blocks around the center:**
`/chm generate [world] 100 circle`
### Config
```yaml ```yaml
# The language settings.
# Supported out of the box are german (de) and english (en).
# Additional languages can be configured in the plugins folder under i18n.
# The file name must be in the format <language>.i18n.properties and the content
# must be in the java-property-file format.
# For non-defined translations the default (english) version is used.
# For built-in support please create a PullRequest with your translation.
language: en
# Actiates/deactivates the dynmap integration.
# With the setting set to 'true' the plugin tries to trigger the rendering
# of generated chunks right before unloading them. It also adds an area
# marker to the dynmap to show the area that will be pregenerated.
# The marker is removed automatically when the task is finished or canceled.
# The value should be a boolean <true/false>
dynmap: true
generation: generation:
# If set to true the plugin ignores the vanilla world border and doesn't stop
# the chunk generation when reaching it.
# The value should be a boolean <true/false>
ignore-worldborder: false
# The maximum amount of chunks that are loaded before unloading and saving them. # The maximum amount of chunks that are loaded before unloading and saving them.
# Higher values mean higher generation speed but greater memory usage. # Higher values mean higher generation speed but greater memory usage.
# The value should be a positive integer. # The value should be a positive integer.
max-loaded-chunks: 10 max-loaded-chunks: 1000
# Paper Only # Paper Only
# The maximum amount of requested chunks with the asynchronous paper chunk # The maximum amount of requested chunks with the asynchronous paper chunk
# loading method. Higher values mean faster generation but more memory usage # loading method. Higher values mean faster generation but more memory usage and
# (and probably bigger performance impact). # 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. # The value should be a positive integer.
period: 2 max-pending-chunks: 500
# 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
# The maximum milliseconds per tick the server is allowed to have # The maximum milliseconds per tick the server is allowed to have
# during the cunk generation process. # during the cunk generation process.
@ -59,17 +101,59 @@ generation:
# The value should be a positive integer greater than 50. # The value should be a positive integer greater than 50.
mspt-pause-threshold: 500 mspt-pause-threshold: 500
# If the chunk generation process should pause on player join. # 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 # Notice that playing on a server that constantly generates chunks can be
# very laggy and can cause it to crash. # very laggy and can cause it to crash.
# You could configure the values above so that the performance impact of the generation # The value should be a posivitve integer > 1.
# process is minimal. pause-on-player-count: 1
# The value should be a boolean <true/false>
pause-on-join: true # if the generation should automatically start on server startup
# the value should be a boolean
autostart: true
``` ```
## Spigot and Paper ### Spigot and Paper
The plugin works on spigot and paper servers but is significantly faster on paper servers The plugin works on spigot and paper servers but is significantly faster on paper servers
(because it profits from asynchronous chunk loading an the better implementation of the (because it profits from asynchronous chunk loading an the better implementation of the
isChunkGenerated method). 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.
## bStats
[![Plugin statistics](https://bstats.org/signatures/bukkit/chunkmaster.svg)](https://bstats.org/plugin/bukkit/Chunkmaster/5639)

@ -10,7 +10,7 @@ buildscript {
plugins { plugins {
id 'idea' 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' id 'com.github.johnrengelman.shadow' version '2.0.4'
} }
@ -22,8 +22,7 @@ idea {
} }
group "net.trivernis" group "net.trivernis"
version "0.12-beta" version PLUGIN_VERSION
sourceCompatibility = 1.8 sourceCompatibility = 1.8
repositories { repositories {
@ -42,16 +41,31 @@ repositories {
name 'CodeMc' name 'CodeMc'
url 'https://repo.codemc.org/repository/maven-public' url 'https://repo.codemc.org/repository/maven-public'
} }
maven {
name 'mikeprimm'
url 'https://repo.mikeprimm.com'
}
maven {
url 'https://jitpack.io'
}
} }
dependencies { dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8" 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'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
testCompile group: 'junit', name: 'junit', version: '4.12' implementation "io.papermc:paperlib:1.0.6"
compileOnly "org.spigotmc:spigot-api:1.14.4-R0.1-SNAPSHOT" implementation "org.bstats:bstats-bukkit:1.5"
compile group: 'org.xerial', name: 'sqlite-jdbc', version: '3.28.0'
compile "io.papermc:paperlib:1.0.2" testImplementation group: 'junit', name: 'junit', version: '4.12'
compile "org.bstats:bstats-bukkit:1.5" 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" apply plugin: "com.github.johnrengelman.shadow"
@ -60,10 +74,20 @@ apply plugin: 'java'
shadowJar { shadowJar {
relocate 'io.papermc.lib', 'net.trivernis.chunkmaster.paperlib' relocate 'io.papermc.lib', 'net.trivernis.chunkmaster.paperlib'
relocate 'org.bstats', 'net.trivernis.chunkmaster.bstats' 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 { processResources {
from configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } duplicatesStrategy = DuplicatesStrategy.INCLUDE
with copySpec {
from 'src/main/resources/'
include 'plugin.yml'
filter { String line ->
line.replace('$$PLUGIN_VERSION$$', PLUGIN_VERSION)
}
}
} }
compileKotlin { compileKotlin {

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

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

@ -1,22 +1,23 @@
package net.trivernis.chunkmaster package net.trivernis.chunkmaster
import io.papermc.lib.PaperLib import io.papermc.lib.PaperLib
import net.trivernis.chunkmaster.commands.* import net.trivernis.chunkmaster.commands.CommandChunkmaster
import net.trivernis.chunkmaster.lib.LanguageManager
import net.trivernis.chunkmaster.lib.database.SqliteManager
import net.trivernis.chunkmaster.lib.generation.GenerationManager import net.trivernis.chunkmaster.lib.generation.GenerationManager
import net.trivernis.chunkmaster.lib.SqlUpdateManager
import org.bstats.bukkit.Metrics import org.bstats.bukkit.Metrics
import org.bukkit.plugin.java.JavaPlugin import org.bukkit.plugin.java.JavaPlugin
import org.bukkit.scheduler.BukkitTask import org.bukkit.scheduler.BukkitTask
import java.lang.Exception import org.dynmap.DynmapAPI
import java.sql.Connection
import java.sql.DriverManager
class Chunkmaster: JavaPlugin() { open class Chunkmaster : JavaPlugin() {
lateinit var sqliteConnection: Connection lateinit var sqliteManager: SqliteManager
var dbname: String? = null
lateinit var generationManager: GenerationManager lateinit var generationManager: GenerationManager
lateinit var langManager: LanguageManager
private lateinit var tpsTask: BukkitTask private lateinit var tpsTask: BukkitTask
var mspt = 50L // keep track of the milliseconds per tick var dynmapApi: DynmapAPI? = null
private set
var mspt = 20 // keep track of the milliseconds per tick
private set private set
/** /**
@ -24,9 +25,17 @@ class Chunkmaster: JavaPlugin() {
*/ */
override fun onEnable() { override fun onEnable() {
PaperLib.suggestPaper(this) PaperLib.suggestPaper(this)
logger.fine("LogLevel: FINE")
logger.finer("LogLevel: FINER")
logger.finest("LogLevel: FINEST")
configure() configure()
val metrics = Metrics(this) Metrics(this)
langManager = LanguageManager(this)
langManager.loadProperties()
this.dynmapApi = getDynmap()
initDatabase() initDatabase()
generationManager = GenerationManager(this, server) generationManager = GenerationManager(this, server)
@ -37,21 +46,27 @@ class Chunkmaster: JavaPlugin() {
server.pluginManager.registerEvents(ChunkmasterEvents(this, server), this) server.pluginManager.registerEvents(ChunkmasterEvents(this, server), this)
if (PaperLib.isPaper() && PaperLib.getMinecraftPatchVersion() >= 225) {
tpsTask = server.scheduler.runTaskTimer(this, Runnable {
mspt = 1000 / server.currentTick // use papers exposed tick rather than calculating it
}, 1, 300)
} else {
tpsTask = server.scheduler.runTaskTimer(this, Runnable { tpsTask = server.scheduler.runTaskTimer(this, Runnable {
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
server.scheduler.runTaskLater(this, Runnable { server.scheduler.runTaskLater(this, Runnable {
mspt = System.currentTimeMillis() - start mspt = (System.currentTimeMillis() - start).toInt()
}, 1) }, 1)
}, 1, 300) }, 1, 300)
} }
}
/** /**
* Stop all tasks and close database connection on disable * Stop all tasks and close database connection on disable
*/ */
override fun onDisable() { override fun onDisable() {
logger.info("Stopping all generation tasks...") logger.info(langManager.getLocalized("STOPPING_ALL_TASKS"))
generationManager.stopAll() generationManager.stopAll()
sqliteConnection.close() server.scheduler.cancelTasks(this)
} }
/** /**
@ -59,13 +74,16 @@ class Chunkmaster: JavaPlugin() {
*/ */
private fun configure() { private fun configure() {
dataFolder.mkdir() 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.mspt-pause-threshold", 500L)
config.addDefault("generation.pause-on-join", true) config.addDefault("generation.pause-on-player-count", 1)
config.addDefault("generation.max-pending-chunks", 10) config.addDefault("generation.max-pending-chunks", 500)
config.addDefault("generation.max-loaded-chunks", 10) 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)
config.options().copyDefaults(true) config.options().copyDefaults(true)
saveConfig() saveConfig()
} }
@ -74,18 +92,27 @@ class Chunkmaster: JavaPlugin() {
* Initializes the database * Initializes the database
*/ */
private fun initDatabase() { private fun initDatabase() {
logger.info("Initializing Database...") logger.info(langManager.getLocalized("DB_INIT"))
try { try {
Class.forName("org.sqlite.JDBC") this.sqliteManager = SqliteManager(this)
sqliteConnection = DriverManager.getConnection("jdbc:sqlite:${dataFolder.absolutePath}/chunkmaster.db") sqliteManager.init()
logger.info("Database connection established.") logger.info(langManager.getLocalized("DB_INIT_FINISHED"))
val updateManager = SqlUpdateManager(sqliteConnection, this)
updateManager.checkUpdate()
updateManager.performUpdate()
logger.info("Database fully initialized.")
} catch (e: Exception) { } catch (e: Exception) {
logger.warning("Failed to init database: ${e.message}") logger.warning(langManager.getLocalized("DB_INIT_EROR", e.message!!))
}
}
private fun getDynmap(): DynmapAPI? {
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
} }
} }
} }

@ -1,16 +1,17 @@
package net.trivernis.chunkmaster package net.trivernis.chunkmaster
import net.trivernis.chunkmaster.lib.generation.GenerationTaskPaper
import org.bukkit.Server import org.bukkit.Server
import org.bukkit.event.EventHandler import org.bukkit.event.EventHandler
import org.bukkit.event.Listener import org.bukkit.event.Listener
import org.bukkit.event.player.PlayerJoinEvent import org.bukkit.event.player.PlayerJoinEvent
import org.bukkit.event.player.PlayerQuitEvent import org.bukkit.event.player.PlayerQuitEvent
import org.bukkit.event.world.WorldSaveEvent
class ChunkmasterEvents(private val chunkmaster: Chunkmaster, private val server: Server) : Listener { class ChunkmasterEvents(private val chunkmaster: Chunkmaster, private val server: Server) : Listener {
private val pauseOnJoin = chunkmaster.config.getBoolean("generation.pause-on-join") private val pauseOnPlayerCount: Int
get() {
return chunkmaster.config.getInt("generation.pause-on-player-count")
}
private var playerPaused = false private var playerPaused = false
/** /**
@ -18,21 +19,15 @@ class ChunkmasterEvents(private val chunkmaster: Chunkmaster, private val server
*/ */
@EventHandler @EventHandler
fun onPlayerQuit(event: PlayerQuitEvent) { fun onPlayerQuit(event: PlayerQuitEvent) {
if (pauseOnJoin) { if (server.onlinePlayers.size == pauseOnPlayerCount) {
if (server.onlinePlayers.size == 1 && server.onlinePlayers.contains(event.player) ||
server.onlinePlayers.isEmpty()
) {
if (!playerPaused) { if (!playerPaused) {
if (chunkmaster.generationManager.pausedTasks.isNotEmpty()) { if (chunkmaster.generationManager.pausedTasks.isNotEmpty()) {
chunkmaster.logger.info("Server is empty. Resuming chunk generation tasks.") chunkmaster.logger.info(chunkmaster.langManager.getLocalized("RESUME_PLAYER_LEAVE"))
} }
chunkmaster.generationManager.resumeAll() chunkmaster.generationManager.resumeAll()
} else if (chunkmaster.generationManager.paused) { } else if (chunkmaster.generationManager.paused) {
chunkmaster.logger.info("Generation was manually paused. Not resuming automatically.") chunkmaster.logger.info(chunkmaster.langManager.getLocalized("PAUSE_MANUALLY"))
playerPaused = chunkmaster.generationManager.paused playerPaused = chunkmaster.generationManager.paused
} else {
chunkmaster.logger.info("Generation tasks are already running.")
}
} }
} }
} }
@ -42,27 +37,14 @@ class ChunkmasterEvents(private val chunkmaster: Chunkmaster, private val server
*/ */
@EventHandler @EventHandler
fun onPlayerJoin(event: PlayerJoinEvent) { fun onPlayerJoin(event: PlayerJoinEvent) {
if (pauseOnJoin) { if (server.onlinePlayers.size >= pauseOnPlayerCount) {
if (server.onlinePlayers.size == 1 || server.onlinePlayers.isEmpty()) {
if (chunkmaster.generationManager.tasks.isNotEmpty()) { if (chunkmaster.generationManager.tasks.isNotEmpty()) {
chunkmaster.logger.info("Pausing generation tasks because of player join.") chunkmaster.logger.info(chunkmaster.langManager.getLocalized("PAUSE_PLAYER_JOIN"))
} }
if (!chunkmaster.generationManager.paused) {
playerPaused = chunkmaster.generationManager.paused playerPaused = chunkmaster.generationManager.paused
chunkmaster.generationManager.pauseAll() chunkmaster.generationManager.pauseAll()
} }
} }
} }
/**
* Unload all chunks before a save.
*/
@EventHandler
fun onWorldSave(event: WorldSaveEvent) {
val task = chunkmaster.generationManager.tasks.find { it.generationTask.world == event.world }
if (task != null) {
if (task.generationTask is GenerationTaskPaper) {
task.generationTask.unloadAllChunks()
}
}
}
} }

@ -1,10 +1,7 @@
package net.trivernis.chunkmaster.commands 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.Chunkmaster
import net.trivernis.chunkmaster.lib.Subcommand import net.trivernis.chunkmaster.lib.Subcommand
import net.trivernis.chunkmaster.lib.generation.TaskEntry
import org.bukkit.command.Command import org.bukkit.command.Command
import org.bukkit.command.CommandSender import org.bukkit.command.CommandSender
@ -30,18 +27,24 @@ class CmdCancel(private val chunkmaster: Chunkmaster): Subcommand {
* Cancels the generation task if it exists. * Cancels the generation task if it exists.
*/ */
override fun execute(sender: CommandSender, args: List<String>): Boolean { override fun execute(sender: CommandSender, args: List<String>): Boolean {
return if (args.isNotEmpty() && args[0].toIntOrNull() != null) { return if (args.isNotEmpty()) {
if (chunkmaster.generationManager.removeTask(args[0].toInt())) { val entry = chunkmaster.generationManager.tasks.find { it.generationTask.world.name == args[0] }
sender.sendMessage("Task ${args[0]} canceled.") val index = if (args[0].toIntOrNull() != null && entry == null) {
args[0].toInt()
} else {
entry?.id
}
if (index != null && chunkmaster.generationManager.removeTask(index)) {
sender.sendMessage(chunkmaster.langManager.getLocalized("TASK_CANCELLED", index))
true true
} else { } else {
sender.spigot().sendMessage(*ComponentBuilder("Task ${args[0]} not found!") sender.sendMessage(chunkmaster.langManager.getLocalized("TASK_NOT_FOUND", args[0]))
.color(ChatColor.RED).create())
false false
} }
} else { } else {
sender.spigot().sendMessage(*ComponentBuilder("You need to provide a task id to cancel.") sender.sendMessage(chunkmaster.langManager.getLocalized("TASK_ID_REQUIRED"))
.color(ChatColor.RED).create())
false 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()
}
}

@ -1,13 +1,10 @@
package net.trivernis.chunkmaster.commands 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.Chunkmaster
import net.trivernis.chunkmaster.lib.Subcommand import net.trivernis.chunkmaster.lib.Subcommand
import org.bukkit.command.Command import org.bukkit.command.Command
import org.bukkit.command.CommandSender import org.bukkit.command.CommandSender
import org.bukkit.entity.Player import org.bukkit.entity.Player
import kotlin.math.pow
class CmdGenerate(private val chunkmaster: Chunkmaster) : Subcommand { class CmdGenerate(private val chunkmaster: Chunkmaster) : Subcommand {
override val name = "generate" override val name = "generate"
@ -26,16 +23,17 @@ class CmdGenerate(private val chunkmaster: Chunkmaster): Subcommand {
.map { it.name }.toMutableList() .map { it.name }.toMutableList()
} else if (args.size == 2) { } else if (args.size == 2) {
if (args[0].toIntOrNull() != null) { if (args[0].toIntOrNull() != null) {
return units.filter {it.indexOf(args[1]) == 0}.toMutableList() return shapes.filter { it.indexOf(args[1]) == 0 }.toMutableList()
} }
} else if (args.size > 2) { } else if (args.size > 2) {
if (args[1].toIntOrNull() != null) { if (args[1].toIntOrNull() != null) {
return units.filter {it.indexOf(args[2]) == 0}.toMutableList() return shapes.filter { it.indexOf(args[2]) == 0 }.toMutableList()
} }
} }
return emptyList<String>().toMutableList() return emptyList<String>().toMutableList()
} }
val units = listOf("blockradius", "radius", "diameter")
val shapes = listOf("circle", "square")
/** /**
@ -43,90 +41,82 @@ class CmdGenerate(private val chunkmaster: Chunkmaster): Subcommand {
*/ */
override fun execute(sender: CommandSender, args: List<String>): Boolean { override fun execute(sender: CommandSender, args: List<String>): Boolean {
var worldName = "" var worldName = ""
var stopAfter = -1 var blockRadius = -1
var shape = "square"
if (sender is Player) { if (sender is Player) {
if (args.isNotEmpty()) {
if (args[0].toIntOrNull() != null) {
stopAfter = args[0].toInt()
worldName = sender.world.name worldName = sender.world.name
} else {
worldName = args[0]
} }
if (args.size > 1) { if (args.isEmpty()) {
if (args[1].toIntOrNull() != null) { if (sender is Player) {
stopAfter = args[1].toInt() return createTask(sender, worldName, blockRadius, shape)
} else if (args[1] in units && args[0].toIntOrNull() != null) {
stopAfter = getStopAfter(stopAfter, args[1])
} else { } else {
worldName = args[1] sender.sendMessage(chunkmaster.langManager.getLocalized("WORLD_NAME_REQUIRED"))
} return false
} }
if (args.size > 2 && args[2] in units && args[1].toIntOrNull() != null) {
stopAfter = getStopAfter(stopAfter, args[2])
} }
} else { if (args[0].toIntOrNull() != null && sender.server.worlds.find { it.name == args[0] } == null) {
worldName = sender.world.name if (sender !is Player) {
sender.sendMessage(chunkmaster.langManager.getLocalized("WORLD_NAME_REQUIRED"))
return false
} }
blockRadius = args[0].toInt()
} else { } else {
if (args.isNotEmpty()) {
worldName = args[0] worldName = args[0]
if (args.size > 1) {
if (args[1].toIntOrNull() != null) {
stopAfter = args[1].toInt()
}
}
if (args.size > 2 && args[2] in units) {
stopAfter = getStopAfter(stopAfter, args[2])
}
} else {
sender.spigot().sendMessage(
*ComponentBuilder("You need to provide a world name").color(ChatColor.RED).create())
return false
}
}
return createTask(sender, worldName, stopAfter)
} }
/** if (args.size == 1) {
* Returns stopAfter for a given unit return createTask(sender, worldName, blockRadius, shape)
*/
private fun getStopAfter(number: Int, unit: String): Int {
if (unit in units) {
return when (unit) {
"radius" -> {
((number * 2)+1).toDouble().pow(2.0).toInt()
} }
"diameter" -> {
number.toDouble().pow(2.0).toInt() when {
args[1].toIntOrNull() != null -> blockRadius = args[1].toInt()
args[1] in shapes -> shape = args[1]
else -> {
sender.sendMessage(chunkmaster.langManager.getLocalized("INVALID_ARGUMENT", 2, args[1]))
return false
} }
"blockradius" -> {
((number.toDouble()+1)/8).pow(2.0).toInt()
} }
else -> number if (args.size == 2) {
return createTask(sender, worldName, blockRadius, shape)
} }
if (args[2] in shapes) {
shape = args[2]
} else {
sender.sendMessage(chunkmaster.langManager.getLocalized("INVALID_ARGUMENT", 3, args[2]))
return false
} }
return number
return createTask(sender, worldName, blockRadius, shape)
} }
/** /**
* Creates the task with the given arguments. * Creates the task with the given arguments.
*/ */
private fun createTask(sender: CommandSender, worldName: String, stopAfter: Int): Boolean { private fun createTask(sender: CommandSender, worldName: String, blockRadius: Int, shape: String): Boolean {
val world = chunkmaster.server.getWorld(worldName) val world = chunkmaster.server.getWorld(worldName)
val allTasks = chunkmaster.generationManager.allTasks val allTasks = chunkmaster.generationManager.allTasks
return if (world != null && (allTasks.find { it.generationTask.world == world }) == null) { return if (world != null && (allTasks.find { it.generationTask.world == world }) == null) {
chunkmaster.generationManager.addTask(world, stopAfter) chunkmaster.generationManager.addTask(world, if (blockRadius > 0) blockRadius / 16 else -1, shape)
sender.spigot().sendMessage(*ComponentBuilder("Generation task for world ").color(ChatColor.BLUE) sender.sendMessage(
.append(worldName).color(ChatColor.GREEN).append(" until ").color(ChatColor.BLUE) chunkmaster.langManager
.append(if (stopAfter > 0) "$stopAfter chunks" else "WorldBorder").color(ChatColor.GREEN) .getLocalized(
.append(" successfully created").color(ChatColor.BLUE).create()) "TASK_CREATION_SUCCESS",
worldName,
if (blockRadius > 0) {
chunkmaster.langManager.getLocalized("TASK_UNIT_RADIUS", blockRadius)
} else {
chunkmaster.langManager.getLocalized("TASK_UNIT_WORLDBORDER")
},
shape
)
)
true true
} else if (world == null) { } else if (world == null) {
sender.spigot().sendMessage(*ComponentBuilder("World ").color(ChatColor.RED) sender.sendMessage(chunkmaster.langManager.getLocalized("WORLD_NOT_FOUND", worldName))
.append(worldName).color(ChatColor.GREEN).append(" not found!").color(ChatColor.RED).create())
false false
} else { } else {
sender.spigot().sendMessage(*ComponentBuilder("Task already exists!").color(ChatColor.RED).create()) sender.sendMessage(chunkmaster.langManager.getLocalized("TASK_ALREADY_EXISTS", worldName))
return false return false
} }
} }

@ -0,0 +1,68 @@
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
import org.bukkit.entity.Player
class CmdGetCenter(private val chunkmaster: Chunkmaster) : Subcommand {
override val name = "getCenter"
override fun onTabComplete(
sender: CommandSender,
command: Command,
alias: String,
args: List<String>
): MutableList<String> {
if (args.size == 1) {
return sender.server.worlds.filter { it.name.indexOf(args[0]) == 0 }
.map { it.name }.toMutableList()
}
return emptyList<String>().toMutableList()
}
override fun execute(sender: CommandSender, args: List<String>): Boolean {
val worldName: String = if (sender is Player) {
if (args.isNotEmpty()) {
args[0]
} else {
sender.world.name
}
} else {
if (args.isEmpty()) {
sender.sendMessage(chunkmaster.langManager.getLocalized("WORLD_NAME_REQUIRED"))
return false
} else {
args[0]
}
}
sendCenterInfo(sender, worldName)
return true
}
/**
* Sends the center information
*/
private fun sendCenterInfo(sender: CommandSender, worldName: String) {
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)
}
sender.sendMessage(
chunkmaster.langManager.getLocalized(
"CENTER_INFO",
worldName,
center.first,
center.second
)
)
}
}
}

@ -1,11 +1,11 @@
package net.trivernis.chunkmaster.commands 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.Chunkmaster
import net.trivernis.chunkmaster.lib.Subcommand import net.trivernis.chunkmaster.lib.Subcommand
import net.trivernis.chunkmaster.lib.generation.taskentry.TaskEntry
import org.bukkit.command.Command import org.bukkit.command.Command
import org.bukkit.command.CommandSender 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 val name = "list"
@ -27,37 +27,40 @@ class CmdList(private val chunkmaster: Chunkmaster): Subcommand {
val pausedTasks = chunkmaster.generationManager.pausedTasks val pausedTasks = chunkmaster.generationManager.pausedTasks
if (runningTasks.isEmpty() && pausedTasks.isEmpty()) { if (runningTasks.isEmpty() && pausedTasks.isEmpty()) {
sender.spigot().sendMessage(*ComponentBuilder("There are no generation tasks.") sender.sendMessage(chunkmaster.langManager.getLocalized("NO_GENERATION_TASKS"))
.color(ChatColor.BLUE).create())
} else if (runningTasks.isEmpty()) { } else if (runningTasks.isEmpty()) {
val response = ComponentBuilder("Currently paused generation tasks:").color(ChatColor.WHITE).bold(true) var response = chunkmaster.langManager.getLocalized("PAUSED_TASKS_HEADER")
for (taskEntry in pausedTasks) { for (taskEntry in pausedTasks) {
val genTask = taskEntry.generationTask response += getGenerationEntry(taskEntry)
response.append("\n - ").color(ChatColor.WHITE).bold(false)
response.append("#${taskEntry.id}").color(ChatColor.BLUE).append(" - ").color(ChatColor.WHITE)
response.append(genTask.world.name).color(ChatColor.GREEN).append(" - Progress: ").color(ChatColor.WHITE)
response.append("${genTask.count} chunks").color(ChatColor.BLUE)
if (genTask.stopAfter > 0) {
response.append(" (${(genTask.count.toDouble()/genTask.stopAfter.toDouble())*100}%).")
}
} }
sender.spigot().sendMessage(*response.create()) sender.sendMessage(response)
} else { } else {
val response = ComponentBuilder("Currently running generation tasks:").color(ChatColor.WHITE).bold(true) var response = chunkmaster.langManager.getLocalized("RUNNING_TASKS_HEADER")
for (task in runningTasks) { for (task in runningTasks) {
val genTask = task.generationTask response += getGenerationEntry(task)
response.append("\n - ").color(ChatColor.WHITE).bold(false)
.append("#${task.id}").color(ChatColor.BLUE).append(" - ").color(ChatColor.WHITE)
.append(genTask.world.name).color(ChatColor.GREEN).append(" - Progress: ").color(ChatColor.WHITE)
.append("${genTask.count} chunks").color(ChatColor.BLUE)
if (genTask.stopAfter > 0) {
response.append(" (${(genTask.count.toDouble()/genTask.stopAfter.toDouble())*100}%).")
} }
} sender.sendMessage(response)
sender.spigot().sendMessage(*response.create())
} }
return true return true
} }
/**
* Returns the report string for one generation task
*/
private fun getGenerationEntry(task: TaskEntry): String {
val genTask = task.generationTask
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 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.Chunkmaster
import net.trivernis.chunkmaster.lib.Subcommand import net.trivernis.chunkmaster.lib.Subcommand
import org.bukkit.command.Command import org.bukkit.command.Command
@ -22,15 +20,10 @@ class CmdPause(private val chunkmaster: Chunkmaster) : Subcommand {
override fun execute(sender: CommandSender, args: List<String>): Boolean { override fun execute(sender: CommandSender, args: List<String>): Boolean {
return if (!chunkmaster.generationManager.paused) { return if (!chunkmaster.generationManager.paused) {
chunkmaster.generationManager.pauseAll() chunkmaster.generationManager.pauseAll()
sender.spigot().sendMessage( sender.sendMessage(chunkmaster.langManager.getLocalized("PAUSE_SUCCESS"))
*ComponentBuilder("Paused all generation tasks.")
.color(ChatColor.BLUE).create()
)
true true
} else { } else {
sender.spigot().sendMessage( sender.sendMessage(chunkmaster.langManager.getLocalized("ALREADY_PAUSED"))
*ComponentBuilder("The generation process is already paused.").color(ChatColor.RED).create()
)
false false
} }
} }

@ -1,7 +1,5 @@
package net.trivernis.chunkmaster.commands 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.Chunkmaster
import net.trivernis.chunkmaster.lib.Subcommand import net.trivernis.chunkmaster.lib.Subcommand
import org.bukkit.command.Command import org.bukkit.command.Command
@ -23,12 +21,12 @@ class CmdReload(private val chunkmaster: Chunkmaster): Subcommand {
* Reload command to reload the config and restart the tasks. * Reload command to reload the config and restart the tasks.
*/ */
override fun execute(sender: CommandSender, args: List<String>): Boolean { override fun execute(sender: CommandSender, args: List<String>): Boolean {
sender.spigot().sendMessage(*ComponentBuilder("Reloading config and restarting tasks...") sender.sendMessage(chunkmaster.langManager.getLocalized("CONFIG_RELOADING"))
.color(ChatColor.YELLOW).create())
chunkmaster.generationManager.stopAll() chunkmaster.generationManager.stopAll()
chunkmaster.reloadConfig() chunkmaster.reloadConfig()
chunkmaster.generationManager.startAll() chunkmaster.generationManager.startAll()
sender.spigot().sendMessage(*ComponentBuilder("Config reload complete!").color(ChatColor.GREEN).create()) chunkmaster.langManager.loadProperties()
sender.sendMessage(chunkmaster.langManager.getLocalized("CONFIG_RELOADED"))
return true return true
} }
} }

@ -1,7 +1,5 @@
package net.trivernis.chunkmaster.commands 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.Chunkmaster
import net.trivernis.chunkmaster.lib.Subcommand import net.trivernis.chunkmaster.lib.Subcommand
import org.bukkit.command.Command import org.bukkit.command.Command
@ -22,12 +20,10 @@ class CmdResume(private val chunkmaster: Chunkmaster): Subcommand {
override fun execute(sender: CommandSender, args: List<String>): Boolean { override fun execute(sender: CommandSender, args: List<String>): Boolean {
return if (chunkmaster.generationManager.paused) { return if (chunkmaster.generationManager.paused) {
chunkmaster.generationManager.resumeAll() chunkmaster.generationManager.resumeAll()
sender.spigot().sendMessage( sender.sendMessage(chunkmaster.langManager.getLocalized("RESUME_SUCCESS"))
*ComponentBuilder("Resumed all generation tasks.").color(ChatColor.BLUE).create())
true true
} else { } else {
sender.spigot().sendMessage( sender.sendMessage(chunkmaster.langManager.getLocalized("NOT_PAUSED"))
*ComponentBuilder("There are no paused generation tasks.").color(ChatColor.RED).create())
false false
} }
} }

@ -0,0 +1,94 @@
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
import org.bukkit.entity.Player
class CmdSetCenter(private val chunkmaster: Chunkmaster) : Subcommand {
override val name = "setCenter"
override fun onTabComplete(
sender: CommandSender,
command: Command,
alias: String,
args: List<String>
): MutableList<String> {
if (args.size == 1) {
if (args[0].toIntOrNull() == null) {
return sender.server.worlds.filter { it.name.indexOf(args[0]) == 0 }
.map { it.name }.toMutableList()
}
}
return emptyList<String>().toMutableList()
}
override fun execute(sender: CommandSender, args: List<String>): Boolean {
val world: String
val centerX: Int
val centerZ: Int
if (sender is Player) {
when {
args.isEmpty() -> {
world = sender.world.name
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) {
sender.sendMessage(chunkmaster.langManager.getLocalized("COORD_INVALID", args[0], args[1]))
return false
}
centerX = args[0].toInt()
centerZ = args[1].toInt()
}
else -> {
if (!validateThreeArgs(sender, args)) {
return false
}
world = args[0]
centerX = args[1].toInt()
centerZ = args[2].toInt()
}
}
} else {
if (args.size < 3) {
sender.sendMessage(chunkmaster.langManager.getLocalized("TOO_FEW_ARGUMENTS"))
return false
} else {
if (!validateThreeArgs(sender, args)) {
return false
}
world = args[0]
centerX = args[1].toInt()
centerZ = args[2].toInt()
}
}
chunkmaster.generationManager.worldProperties.setWorldCenter(world, Pair(centerX, centerZ))
sender.sendMessage(chunkmaster.langManager.getLocalized("CENTER_UPDATED", world, centerX, centerZ))
return true
}
/**
* Validates the command values with three arguments
*/
private fun validateThreeArgs(sender: CommandSender, args: List<String>): Boolean {
return if (sender.server.worlds.none { it.name == args[0] }) {
sender.sendMessage(chunkmaster.langManager.getLocalized("WORLD_NOT_FOUND", args[0]))
false
} else if (args[1].toIntOrNull() == null || args[2].toIntOrNull() == null) {
sender.sendMessage(chunkmaster.langManager.getLocalized("COORD_INVALID", args[1], args[2]))
false
} else {
true
}
}
}

@ -0,0 +1,83 @@
package net.trivernis.chunkmaster.commands
import net.trivernis.chunkmaster.Chunkmaster
import net.trivernis.chunkmaster.lib.Subcommand
import org.bukkit.World
import org.bukkit.command.Command
import org.bukkit.command.CommandSender
class CmdStats(private val chunkmaster: Chunkmaster) : Subcommand {
override val name = "stats"
override fun onTabComplete(
sender: CommandSender,
command: Command,
alias: String,
args: List<String>
): MutableList<String> {
return sender.server.worlds.map { it.name }.toMutableList()
}
override fun execute(sender: CommandSender, args: List<String>): Boolean {
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])
)
return false
}
sender.sendMessage(getWorldStatsMessage(sender, world))
} else {
sender.sendMessage(getServerStatsMessage(sender))
}
return true
}
private fun getWorldStatsMessage(sender: CommandSender, world: World): String {
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)}
${
chunkmaster.langManager.getLocalized(
"STATS_GENERATING",
chunkmaster.generationManager.tasks.find { it.generationTask.world == world } != null)
}
""".trimIndent()
}
private fun getServerStatsMessage(sender: CommandSender): String {
val runtime = Runtime.getRuntime()
val memUsed = runtime.maxMemory() - runtime.freeMemory()
var message = "\n" + """
${chunkmaster.langManager.getLocalized("STATS_HEADER")}
${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_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)
}
return message
}
}

@ -1,15 +1,14 @@
package net.trivernis.chunkmaster.commands package net.trivernis.chunkmaster.commands
import io.papermc.lib.PaperLib import io.papermc.lib.PaperLib
import net.md_5.bungee.api.ChatColor import net.trivernis.chunkmaster.Chunkmaster
import net.md_5.bungee.api.chat.ComponentBuilder
import net.trivernis.chunkmaster.lib.Subcommand import net.trivernis.chunkmaster.lib.Subcommand
import org.bukkit.Material import org.bukkit.Material
import org.bukkit.command.Command import org.bukkit.command.Command
import org.bukkit.command.CommandSender import org.bukkit.command.CommandSender
import org.bukkit.entity.Player import org.bukkit.entity.Player
class CmdTpChunk: Subcommand { class CmdTpChunk(private val chunkmaster: Chunkmaster) : Subcommand {
override val name = "tpchunk" override val name = "tpchunk"
override fun onTabComplete( override fun onTabComplete(
@ -37,15 +36,13 @@ class CmdTpChunk: Subcommand {
} else { } else {
sender.teleport(location) sender.teleport(location)
} }
sender.spigot().sendMessage(*ComponentBuilder("You have been teleportet to chunk") sender.sendMessage(chunkmaster.langManager.getLocalized("TELEPORTED", args[0], args[1]))
.color(ChatColor.YELLOW).append("${args[0]}, ${args[1]}").color(ChatColor.BLUE).create())
return true return true
} else { } else {
return false return false
} }
} else { } else {
sender.spigot().sendMessage(*ComponentBuilder("This command can only be executed by a player!") sender.sendMessage(chunkmaster.langManager.getLocalized("TP_ONLY_PLAYER"))
.color(ChatColor.RED).create())
return false return false
} }
} }

@ -1,8 +1,7 @@
package net.trivernis.chunkmaster.commands 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.Chunkmaster
import net.trivernis.chunkmaster.lib.ArgParser
import net.trivernis.chunkmaster.lib.Subcommand import net.trivernis.chunkmaster.lib.Subcommand
import org.bukkit.Server import org.bukkit.Server
import org.bukkit.command.Command 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, class CommandChunkmaster(private val chunkmaster: Chunkmaster, private val server: Server) : CommandExecutor,
TabCompleter { TabCompleter {
private val commands = HashMap<String, Subcommand>() private val commands = HashMap<String, Subcommand>()
private val argParser = ArgParser()
init { init {
registerCommands() registerCommands()
@ -38,23 +38,24 @@ class CommandChunkmaster(private val chunkmaster: Chunkmaster, private val serve
/** /**
* /chunkmaster command to handle all commands * /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 (args.isNotEmpty()) {
if (sender.hasPermission("chunkmaster.${args[0]}")) { if (sender.hasPermission("chunkmaster.${args[0].toLowerCase()}")) {
return if (commands.containsKey(args[0])) { return if (commands.containsKey(args[0])) {
commands[args[0]]!!.execute(sender, args.slice(1 until args.size)) commands[args[0]]!!.execute(sender, args.slice(1 until args.size))
} else { } else {
sender.spigot().sendMessage( sender.sendMessage(chunkmaster.langManager.getLocalized("SUBCOMMAND_NOT_FOUND", args[0]))
*ComponentBuilder("Subcommand ").color(ChatColor.RED)
.append(args[0]).color(ChatColor.GREEN).append(" not found").color(ChatColor.RED).create()
)
false false
} }
} else { } else {
sender.spigot().sendMessage( sender.sendMessage(chunkmaster.langManager.getLocalized("NO_PERMISSION"))
*ComponentBuilder("You do not have permission!")
.color(ChatColor.RED).create()
)
} }
return true return true
} else { } else {
@ -84,7 +85,19 @@ class CommandChunkmaster(private val chunkmaster: Chunkmaster, private val serve
val cmdReload = CmdReload(chunkmaster) val cmdReload = CmdReload(chunkmaster)
commands[cmdReload.name] = cmdReload commands[cmdReload.name] = cmdReload
val cmdTpChunk = CmdTpChunk() val cmdTpChunk = CmdTpChunk(chunkmaster)
commands[cmdTpChunk.name] = cmdTpChunk commands[cmdTpChunk.name] = cmdTpChunk
val cmdSetCenter = CmdSetCenter(chunkmaster)
commands[cmdSetCenter.name] = cmdSetCenter
val cmdGetCenter = CmdGetCenter(chunkmaster)
commands[cmdGetCenter.name] = cmdGetCenter
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
}
}

@ -0,0 +1,74 @@
package net.trivernis.chunkmaster.lib
import net.trivernis.chunkmaster.Chunkmaster
import java.io.*
import java.util.*
class LanguageManager(private val plugin: Chunkmaster) {
private val langProps = Properties()
private val languageFolder = "${plugin.dataFolder.absolutePath}/i18n"
private var langFileLoaded = false
/**
* Loads the default properties file and then the language specific ones.
* If no lang-specific file is found in the plugins directory under i18n an attempt is made to
* load the file from inside the jar in i18n.
*/
fun loadProperties() {
val language = plugin.config.getString("language")
val langFile = "$languageFolder/$language.i18n.properties"
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)
if (inputStream != null) {
langProps.load(getReaderForProperties(inputStream))
langFileLoaded = true
inputStream.close()
}
} catch (e: Exception) {
plugin.logger.warning("Language file $langFile could not be loaded!")
plugin.logger.fine(e.toString())
}
} else {
val inputStream = this.javaClass.getResourceAsStream("/i18n/$language.i18n.properties")
if (inputStream != null) {
langProps.load(getReaderForProperties(inputStream))
langFileLoaded = true
inputStream.close()
} else {
plugin.logger.warning("Language File $langFile could not be found!")
}
}
}
/**
* Returns a localized message with replacements
*/
fun getLocalized(key: String, vararg replacements: Any): String {
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
}
}
/**
* Reads a properties file as utf-8 and returns a string reader for the contents
*/
private fun getReaderForProperties(stream: InputStream): Reader {
return BufferedReader(InputStreamReader(stream, "UTF-8"))
}
}

@ -1,64 +0,0 @@
package net.trivernis.chunkmaster.lib
import kotlin.math.abs
class Spiral(private val center: Pair<Int, Int>, start: Pair<Int, Int>) {
private var currentPos = start
private var direction = 0
var count = 0
/**
* Returns the next value in the spiral
*/
fun next(): Pair<Int, Int> {
if (count == 0 && currentPos != center) {
// simulate the spiral to get the correct direction
// TODO: Improve performance of this workaround (replace it with acutal stuff)
val simSpiral = Spiral(center, center)
while (simSpiral.next() != currentPos);
direction = simSpiral.direction
count = simSpiral.count
}
if (count == 1) { // because of the center behaviour
count ++
return currentPos
}
if (currentPos == center) { // the center has to be handled exclusively
currentPos = Pair(center.first, center.second + 1)
count ++
return center
} else {
val distances = getDistances(center, currentPos)
if (abs(distances.first) == abs(distances.second)) {
direction = (direction + 1)%5
}
}
when(direction) {
0 -> {
currentPos = Pair(currentPos.first + 1, currentPos.second)
}
1 -> {
currentPos = Pair(currentPos.first, currentPos.second - 1)
}
2 -> {
currentPos = Pair(currentPos.first - 1, currentPos.second)
}
3 -> {
currentPos = Pair(currentPos.first, currentPos.second + 1)
}
4 -> {
currentPos = Pair(currentPos.first, currentPos.second + 1)
direction = 0
}
}
count ++
return currentPos
}
/**
* Returns the distances between 2 coordinates
*/
private fun getDistances(pos1: Pair<Int, Int>, pos2: Pair<Int, Int>): Pair<Int, Int> {
return Pair(pos2.first - pos1.first, pos2.second - pos1.second)
}
}

@ -1,79 +0,0 @@
package net.trivernis.chunkmaster.lib
import net.trivernis.chunkmaster.Chunkmaster
import java.lang.Exception
import java.sql.Connection
class SqlUpdateManager(private val connnection: Connection, 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("stop_after", "integer DEFAULT -1")
)
)
)
private val needUpdate = HashSet<Pair<String, Pair<String, String>>>()
private val needCreation = HashSet<String>()
/**
* Checks which tables need an update or creation.
*/
fun checkUpdate() {
val meta = connnection.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))
}
}
} else {
needCreation.add(table.first)
}
}
}
/**
* Creates or updates tables that needed an update.
*/
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.info("Creating table $table with definition $tableDef")
val stmt = connnection.prepareStatement(tableDef)
stmt.execute()
stmt.close()
} catch (err: Exception) {
chunkmaster.logger.severe("Error creating table $table.")
}
}
for (table in needUpdate) {
val updateSql = "ALTER TABLE ${table.first} ADD COLUMN ${table.second.first} ${table.second.second}"
try {
val stmt = connnection.prepareStatement(updateSql)
stmt.execute()
stmt.close()
chunkmaster.logger.info("Updated table ${table.first} with sql $updateSql")
} catch (e: Exception) {
chunkmaster.logger.severe("Failed to update table ${table.first} with sql $updateSql")
}
}
}
}

@ -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
}
}

@ -0,0 +1,201 @@
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
}
}

@ -0,0 +1,30 @@
package net.trivernis.chunkmaster.lib.dynmap
import org.dynmap.DynmapAPI
class DynmapApiWrapper(private val dynmapAPI: DynmapAPI) {
/**
* Returns a marker set by name
*/
fun getMarkerSet(name: String): ExtendedMarkerSet? {
val set = dynmapAPI.markerAPI?.getMarkerSet(name)
return if (set != null) {
ExtendedMarkerSet(set)
} else {
null
}
}
fun getCreateMarkerSet(id: String, name: String): ExtendedMarkerSet? {
var set = dynmapAPI.markerAPI?.getMarkerSet(id)
if (set == null) {
set = dynmapAPI.markerAPI?.createMarkerSet(id, name, null, true)
}
return if (set != null) {
ExtendedMarkerSet(set)
} else {
null
}
}
}

@ -0,0 +1,108 @@
package net.trivernis.chunkmaster.lib.dynmap
import org.bukkit.Location
import org.dynmap.markers.AreaMarker
import org.dynmap.markers.MarkerSet
import org.dynmap.markers.PolyLineMarker
class ExtendedMarkerSet(private val markerSet: MarkerSet) {
/**
* Creates or updates an area marker depending on if it exists
* @param id - the unique id of the area marker
* @param label - the label that is displayed when clicking on the marker
* @param l1 - the top left corner
* @param l2 - the bottom right corner
*/
fun creUpdateAreMarker(id: String, label: String, l1: Location, l2: Location, style: MarkerStyle?): AreaMarker {
var marker = markerSet.findAreaMarker(id)
if (marker != null) {
marker.setCornerLocations(
doubleArrayOf(l1.x, l2.x),
doubleArrayOf(l1.z, l2.z)
)
} else {
marker = markerSet.createAreaMarker(
id,
label,
false,
l1.world.name,
doubleArrayOf(l1.x, l2.x),
doubleArrayOf(l1.z, l2.z),
true
)
}
if (style != null) {
marker.boostFlag = style.boostFlag
if (style.lineStyle != null) {
marker.setLineStyle(style.lineStyle.weight, style.lineStyle.opacity, style.lineStyle.color)
}
if (style.fillStyle != null) {
marker.setFillStyle(style.fillStyle.opacity, style.fillStyle.color)
}
}
return marker
}
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 }
val zList = edges.map { it.z }
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
)
}
if (style != null) {
if (style.lineStyle != null) {
marker.setLineStyle(style.lineStyle.weight, style.lineStyle.opacity, style.lineStyle.color)
}
}
return marker
}
/**
* Returns the area marker for an id
* @param id - the id of the marker
*/
fun findAreaMarker(id: String): AreaMarker? {
return markerSet.findAreaMarker(id)
}
/**
* Returns the polylinemarker for an id
*/
fun findPolyLineMarker(id: String): PolyLineMarker? {
return markerSet.findPolyLineMarker(id)
}
/**
* Deletes an area marker
* @param id - the id of the marker
*/
fun deleteAreaMarker(id: String) {
val marker = this.findAreaMarker(id)
marker?.deleteMarker()
}
fun deletePolyLineMarker(id: String) {
val marker = this.findPolyLineMarker(id)
marker?.deleteMarker()
}
}

@ -0,0 +1,3 @@
package net.trivernis.chunkmaster.lib.dynmap
data class FillStyle(val opacity: Double, val color: Int)

@ -0,0 +1,3 @@
package net.trivernis.chunkmaster.lib.dynmap
data class LineStyle(val weight: Int, val opacity: Double, val color: Int)

@ -0,0 +1,10 @@
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
)

@ -5,6 +5,10 @@ import org.bukkit.World
class ChunkCoordinates(val x: Int, val z: Int) { class ChunkCoordinates(val x: Int, val z: Int) {
fun getCenterLocation(world: World): Location { fun getCenterLocation(world: World): Location {
return Location(world, ((x*16) + 8).toDouble(), 1.0, ((x*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,19 +1,52 @@
package net.trivernis.chunkmaster.lib.generation package net.trivernis.chunkmaster.lib.generation
import io.papermc.lib.PaperLib
import net.trivernis.chunkmaster.Chunkmaster import net.trivernis.chunkmaster.Chunkmaster
import org.bukkit.Chunk 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.Square
import org.bukkit.Server import org.bukkit.Server
import org.bukkit.World import org.bukkit.World
import java.lang.Exception import java.util.concurrent.CompletableFuture
import java.lang.NullPointerException
class GenerationManager(private val chunkmaster: Chunkmaster, private val server: Server) { class GenerationManager(private val chunkmaster: Chunkmaster, private val server: Server) {
val tasks: HashSet<RunningTaskEntry> = HashSet() val tasks: HashSet<RunningTaskEntry> = HashSet()
val pausedTasks: HashSet<PausedTaskEntry> = HashSet() val pausedTasks: HashSet<PausedTaskEntry> = HashSet()
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> val allTasks: HashSet<TaskEntry>
get() { get() {
if (this.tasks.isEmpty() && this.pausedTasks.isEmpty()) {
this.startAll()
if (server.onlinePlayers.size >= pauseOnPlayerCount) {
this.pauseAll()
}
}
val all = HashSet<TaskEntry>() val all = HashSet<TaskEntry>()
all.addAll(pausedTasks) all.addAll(pausedTasks)
all.addAll(tasks) all.addAll(tasks)
@ -25,52 +58,39 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
/** /**
* Adds a generation task * Adds a generation task
*/ */
fun addTask(world: World, stopAfter: Int = -1): Int { fun addTask(world: World, radius: Int = -1, shape: String = "square"): Int {
val foundTask = allTasks.find { it.generationTask.world == world } val foundTask = allTasks.find { it.generationTask.world == world }
if (foundTask == null) {
val centerChunk = ChunkCoordinates(world.spawnLocation.chunk.x, world.spawnLocation.chunk.z)
val generationTask = createGenerationTask(world, centerChunk, centerChunk, stopAfter)
val insertStatement = chunkmaster.sqliteConnection.prepareStatement(
"""
INSERT INTO generation_tasks (center_x, center_z, last_x, last_z, world, stop_after)
values (?, ?, ?, ?, ?, ?)
"""
)
insertStatement.setInt(1, centerChunk.x)
insertStatement.setInt(2, centerChunk.z)
insertStatement.setInt(3, centerChunk.x)
insertStatement.setInt(4, centerChunk.z)
insertStatement.setString(5, world.name)
insertStatement.setInt(6, stopAfter)
insertStatement.execute()
val getIdStatement = chunkmaster.sqliteConnection.prepareStatement( if (foundTask == null) {
""" val center = worldProperties.getWorldCenter(world.name).join()
SELECT id FROM generation_tasks ORDER BY id DESC LIMIT 1
""".trimIndent()
)
getIdStatement.execute()
val result = getIdStatement.resultSet
result.next()
val id: Int = result.getInt("id")
insertStatement.close() val centerChunk = if (center == null) {
getIdStatement.close() ChunkCoordinates(world.spawnLocation.chunk.x, world.spawnLocation.chunk.z)
} else {
ChunkCoordinates(center.first, center.second)
}
val generationTask = createGenerationTask(world, centerChunk, centerChunk, radius, shape, null)
val id = generationTasks.addGenerationTask(world.name, centerChunk, radius, shape).join()
generationTask.onEndReached { generationTask.onEndReached {
chunkmaster.logger.info("Task #${id} finished after ${generationTask.count} chunks.") chunkmaster.logger.info(chunkmaster.langManager.getLocalized("TASK_FINISHED", id, it.count))
removeTask(id) removeTask(id)
} }
if (!paused) { if (!paused) {
val task = server.scheduler.runTaskTimer( val taskEntry = RunningTaskEntry(
chunkmaster, generationTask, 200, // 10 sec delay id,
chunkmaster.config.getLong("generation.period") generationTask
) )
tasks.add(RunningTaskEntry(id, task, generationTask)) taskEntry.start()
tasks.add(taskEntry)
} else { } else {
pausedTasks.add(PausedTaskEntry(id, generationTask)) pausedTasks.add(
PausedTaskEntry(
id,
generationTask
)
)
} }
return id return id
@ -87,18 +107,21 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
center: ChunkCoordinates, center: ChunkCoordinates,
last: ChunkCoordinates, last: ChunkCoordinates,
id: Int, id: Int,
stopAfter: Int = -1 radius: Int = -1,
shape: String = "square",
pendingChunks: List<ChunkCoordinates>?
) { ) {
if (!paused) { if (!paused) {
chunkmaster.logger.info("Resuming chunk generation task for world \"${world.name}\"") chunkmaster.logger.info(chunkmaster.langManager.getLocalized("RESUME_FOR_WORLD", world.name))
val generationTask = createGenerationTask(world, center, last, stopAfter) val generationTask = createGenerationTask(world, center, last, radius, shape, pendingChunks)
val task = server.scheduler.runTaskTimer( val taskEntry = RunningTaskEntry(
chunkmaster, generationTask, 200, // 10 sec delay id,
chunkmaster.config.getLong("generation.period") generationTask
) )
tasks.add(RunningTaskEntry(id, task, generationTask)) taskEntry.start()
tasks.add(taskEntry)
generationTask.onEndReached { generationTask.onEndReached {
chunkmaster.logger.info("Task #${id} finished after ${generationTask.count} chunks.") chunkmaster.logger.info(chunkmaster.langManager.getLocalized("TASK_FINISHED", id, generationTask.count))
removeTask(id) removeTask(id)
} }
} }
@ -113,26 +136,31 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
} else { } else {
this.tasks.find { it.id == id } this.tasks.find { it.id == id }
} }
try {
if (taskEntry != null) { if (taskEntry != null) {
taskEntry.cancel() if (taskEntry.generationTask.isRunning && taskEntry is RunningTaskEntry) {
val deleteTask = chunkmaster.sqliteConnection.prepareStatement( taskEntry.cancel(chunkmaster.config.getLong("mspt-pause-threshold"))
""" }
DELETE FROM generation_tasks WHERE id = ?; generationTasks.deleteGenerationTask(id)
""".trimIndent() completedGenerationTasks.addCompletedTask(
id,
taskEntry.generationTask.world.name,
taskEntry.generationTask.shape.currentRadius(),
taskEntry.generationTask.startChunk,
taskEntry.generationTask.shape.javaClass.simpleName
) )
deleteTask.setInt(1, taskEntry.id) pendingChunksTable.clearPendingChunks(id)
deleteTask.execute()
deleteTask.close()
if (taskEntry is RunningTaskEntry) { if (taskEntry is RunningTaskEntry) {
if (taskEntry.task.isCancelled) {
tasks.remove(taskEntry) tasks.remove(taskEntry)
}
} else if (taskEntry is PausedTaskEntry) { } else if (taskEntry is PausedTaskEntry) {
pausedTasks.remove(taskEntry) pausedTasks.remove(taskEntry)
} }
return true return true
} }
} catch (e: Exception) {
chunkmaster.logger.severe(e.toString())
}
return false return false
} }
@ -141,58 +169,64 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
* Loads tasks from the database and resumes them * Loads tasks from the database and resumes them
*/ */
fun init() { fun init() {
chunkmaster.logger.info("Creating task to load chunk generation Tasks later...") chunkmaster.logger.info(chunkmaster.langManager.getLocalized("CREATE_DELAYED_LOAD"))
server.scheduler.runTaskTimer(chunkmaster, Runnable { server.scheduler.runTaskTimer(chunkmaster, Runnable {
saveProgress() // save progress every 30 seconds saveProgress() // save progress every 30 seconds
}, 600, 600) }, 600, 600)
server.scheduler.runTaskLater(chunkmaster, Runnable { server.scheduler.runTaskLater(chunkmaster, Runnable {
if (server.onlinePlayers.isEmpty()) { this.startAll()
startAll() // run startAll after 10 seconds if empty if (server.onlinePlayers.count() >= pauseOnPlayerCount || !autostart) {
if (!autostart) {
chunkmaster.logger.info(chunkmaster.langManager.getLocalized("NO_AUTOSTART"))
}
this.pauseAll()
} }
}, 600) }, 20)
server.scheduler.runTaskTimer(chunkmaster, unloader, unloadingPeriod, unloadingPeriod)
} }
/** /**
* Stops all generation tasks * Stops all generation tasks
*/ */
fun stopAll() { fun stopAll() {
saveProgress()
val removalSet = HashSet<RunningTaskEntry>() val removalSet = HashSet<RunningTaskEntry>()
for (task in tasks) { for (task in tasks) {
task.generationTask.cancel() val id = task.id
task.task.cancel() chunkmaster.logger.info(chunkmaster.langManager.getLocalized("SAVING_TASK_PROGRESS", task.id))
if (task.task.isCancelled) { saveProgressToDatabase(task.generationTask, id).join()
removalSet.add(task) if (!task.cancel(chunkmaster.config.getLong("mspt-pause-threshold"))) {
chunkmaster.logger.warning(chunkmaster.langManager.getLocalized("CANCEL_FAIL", task.id))
} }
chunkmaster.logger.info("Canceled task #${task.id}") removalSet.add(task)
chunkmaster.logger.info(chunkmaster.langManager.getLocalized("TASK_CANCELLED", task.id))
} }
tasks.removeAll(removalSet) tasks.removeAll(removalSet)
if (unloader.pendingSize > 0) {
chunkmaster.logger.info(chunkmaster.langManager.getLocalized("SAVING_CHUNKS", unloader.pendingSize))
unloader.run()
}
} }
/** /**
* Starts all generation tasks. * Starts all generation tasks.
*/ */
fun startAll() { fun startAll() {
val savedTasksStatement = chunkmaster.sqliteConnection.prepareStatement("SELECT * FROM generation_tasks") generationTasks.getGenerationTasks().thenAccept { tasks ->
savedTasksStatement.execute() for (task in tasks) {
val res = savedTasksStatement.resultSet val world = server.getWorld(task.world)
while (res.next()) { if (world != null) {
try { pendingChunksTable.getPendingChunks(task.id).thenAccept {
val id = res.getInt("id") resumeTask(world, task.center, task.last, task.id, task.radius, task.shape, it)
val world = server.getWorld(res.getString("world")) }
val center = ChunkCoordinates(res.getInt("center_x"), res.getInt("center_z")) } else {
val last = ChunkCoordinates(res.getInt("last_x"), res.getInt("last_z")) chunkmaster.logger.severe(chunkmaster.langManager.getLocalized("TASK_LOAD_FAILED", task.id))
val stopAfter = res.getInt("stop_after")
if (this.tasks.find { it.id == id } == null) {
resumeTask(world!!, center, last, id, stopAfter)
} }
} catch (error: NullPointerException) {
chunkmaster.logger.severe("Failed to load Task ${res.getInt("id")}.")
} }
} }
savedTasksStatement.close()
if (tasks.isNotEmpty()) { if (tasks.isNotEmpty()) {
chunkmaster.logger.info("${tasks.size} saved tasks loaded.") chunkmaster.logger.info(chunkmaster.langManager.getLocalized("TASK_LOAD_SUCCESS", tasks.size))
} }
} }
@ -202,7 +236,12 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
fun pauseAll() { fun pauseAll() {
paused = true paused = true
for (task in tasks) { for (task in tasks) {
pausedTasks.add(PausedTaskEntry(task.id, task.generationTask)) pausedTasks.add(
PausedTaskEntry(
task.id,
task.generationTask
)
)
} }
stopAll() stopAll()
} }
@ -222,30 +261,95 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
private fun saveProgress() { private fun saveProgress() {
for (task in tasks) { for (task in tasks) {
try { 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()
}
}
}
/**
* Reports the progress for correcting tasks
*/
private fun reportCorrectionProgress(task: RunningTaskEntry) {
val genTask = task.generationTask val genTask = task.generationTask
val progress = if (genTask.missingChunks.size > 0) {
"(${(genTask.count / genTask.missingChunks.size) * 100}%)"
} else {
""
}
chunkmaster.logger.info( chunkmaster.logger.info(
"""Task #${task.id} running for "${genTask.world.name}". chunkmaster.langManager.getLocalized(
|Progress ${task.generationTask.count} chunks "TASK_PERIODIC_REPORT_CORRECTING",
|${if (task.generationTask.stopAfter > 0) "(${"%.2f".format((task.generationTask.count.toDouble() / task.id,
task.generationTask.stopAfter.toDouble()) * 100)}%)" else ""}. genTask.world.name,
| Speed: ${"%.1f".format(task.generationSpeed)} chunks/sec, genTask.count,
|Last Chunk: ${genTask.lastChunk.x}, ${genTask.lastChunk.z}""".trimMargin("|").replace('\n', ' ') progress
) )
val updateStatement = chunkmaster.sqliteConnection.prepareStatement(
"""
UPDATE generation_tasks SET last_x = ?, last_z = ?
WHERE id = ?
""".trimIndent()
) )
updateStatement.setInt(1, genTask.lastChunk.x)
updateStatement.setInt(2, genTask.lastChunk.z)
updateStatement.setInt(3, task.id)
updateStatement.execute()
updateStatement.close()
} catch (error: Exception) {
chunkmaster.logger.warning("Exception when saving task progress ${error.message}")
} }
/**
* Reports the progress of the chunk generation
*/
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(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
} }
/** /**
@ -256,12 +360,24 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
world: World, world: World,
center: ChunkCoordinates, center: ChunkCoordinates,
start: ChunkCoordinates, start: ChunkCoordinates,
stopAfter: Int radius: Int,
shapeName: String,
pendingChunks: List<ChunkCoordinates>?
): GenerationTask { ): GenerationTask {
return if (PaperLib.isPaper()) { val shape = when (shapeName) {
GenerationTaskPaper(chunkmaster, world, center, start, stopAfter) "circle" -> Circle(Pair(center.x, center.z), Pair(start.x, start.z), radius)
} else { "square" -> Square(Pair(center.x, center.z), Pair(start.x, start.z), radius)
GenerationTaskSpigot(chunkmaster, world, center, start, stopAfter) else -> Square(Pair(center.x, center.z), Pair(start.x, start.z), radius)
} }
return DefaultGenerationTask(
chunkmaster,
unloader,
world,
start,
radius,
shape, pendingChunks?.toHashSet() ?: HashSet(),
TaskState.GENERATING
)
} }
} }

@ -1,64 +1,144 @@
package net.trivernis.chunkmaster.lib.generation package net.trivernis.chunkmaster.lib.generation
import net.trivernis.chunkmaster.Chunkmaster import net.trivernis.chunkmaster.Chunkmaster
import net.trivernis.chunkmaster.lib.Spiral import net.trivernis.chunkmaster.lib.dynmap.*
import org.bukkit.Chunk import net.trivernis.chunkmaster.lib.shapes.Shape
import org.bukkit.World import org.bukkit.World
import kotlin.math.ceil
/** /**
* Interface for generation tasks. * Interface for generation tasks.
*/ */
abstract class GenerationTask(plugin: Chunkmaster, centerChunk: ChunkCoordinates, startChunk: ChunkCoordinates) : abstract class GenerationTask(
plugin: Chunkmaster,
val world: World,
protected val unloader: ChunkUnloader,
val startChunk: ChunkCoordinates,
val shape: Shape,
val missingChunks: HashSet<ChunkCoordinates>,
var state: TaskState
) :
Runnable { Runnable {
abstract val stopAfter: Int abstract val radius: Int
abstract val world: World abstract var count: Int
abstract val count: Int abstract var endReached: Boolean
abstract val endReached: Boolean var isRunning: Boolean = false
protected val spiral: Spiral = var lastChunkCoords = ChunkCoordinates(startChunk.x, startChunk.z)
Spiral(Pair(centerChunk.x, centerChunk.z), Pair(startChunk.x, startChunk.z)) protected set
protected val loadedChunks: HashSet<Chunk> = HashSet()
protected var lastChunkCoords = ChunkCoordinates(startChunk.x, startChunk.z)
protected val chunkSkips = plugin.config.getInt("generation.chunk-skips-per-step")
protected val msptThreshold = plugin.config.getLong("generation.mspt-pause-threshold") protected val msptThreshold = plugin.config.getLong("generation.mspt-pause-threshold")
protected val maxLoadedChunks = plugin.config.getInt("generation.max-loaded-chunks") protected var cancelRun: Boolean = false
protected val chunksPerStep = plugin.config.getInt("generation.chunks-per-step")
protected var endReachedCallback: (() -> Unit)? = null private var endReachedCallback: ((GenerationTask) -> Unit)? = null
private set
abstract override fun run() private val dynmapIntegration = plugin.config.getBoolean("dynmap")
private val dynmap = plugin.dynmapApi
private val markerSet: ExtendedMarkerSet? = if (dynmap != null) {
DynmapApiWrapper(dynmap).getCreateMarkerSet("chunkmaster", "Chunkmaster")
} else {
null
}
private val markerAreaStyle = MarkerStyle(null, LineStyle(2, 1.0, 0x0022FF), FillStyle(.0, 0))
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 fun generate()
abstract fun validate()
abstract fun generateMissing()
abstract fun cancel() 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 val nextChunkCoordinates: ChunkCoordinates
get() { get() {
val nextChunkCoords = spiral.next() val nextChunkCoords = shape.next()
return ChunkCoordinates(nextChunkCoords.first, nextChunkCoords.second) return ChunkCoordinates(nextChunkCoords.first, nextChunkCoords.second)
} }
val lastChunk: Chunk /**
get() { * Checks if the World border or the maximum chunk setting for the task is reached.
return world.getChunkAt(lastChunkCoords.x, lastChunkCoords.z) */
protected fun borderReached(): Boolean {
return (!world.worldBorder.isInside(lastChunkCoords.getCenterLocation(world)) && !ignoreWorldborder)
|| shape.endReached()
} }
val nextChunk: Chunk /**
get() { * Updates the dynmap marker for the generation radius
val next = nextChunkCoordinates */
return world.getChunkAt(next.x, next.z) protected fun updateGenerationAreaMarker(clear: Boolean = false) {
if (clear) {
markerSet?.deletePolyLineMarker(markerAreaId)
} else if (dynmapIntegration && radius > 0) {
markerSet?.creUpdatePolyLineMarker(
markerAreaId,
markerAreaName,
this.shape.getShapeEdgeLocations()
.map { ChunkCoordinates(it.first, it.second).getCenterLocation(this.world) },
markerAreaStyle
)
}
}
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
)
}
} }
/** /**
* Checks if the World border or the maximum chunk setting for the task is reached. * Handles the invocation of the end reached callback and additional logic
*/ */
protected fun borderReached(): Boolean { private fun setEndReached() {
return !world.worldBorder.isInside(lastChunkCoords.getCenterLocation(world)) || (stopAfter in 1..count) endReached = true
count = shape.count
updateGenerationAreaMarker(true)
endReachedCallback?.invoke(this)
} }
/** /**
* Registers end reached callback * Registers end reached callback
*/ */
fun onEndReached(cb: () -> Unit) { fun onEndReached(cb: (GenerationTask) -> Unit) {
endReachedCallback = cb endReachedCallback = cb
} }
} }

@ -1,117 +0,0 @@
package net.trivernis.chunkmaster.lib.generation
import net.trivernis.chunkmaster.Chunkmaster
import org.bukkit.Chunk
import org.bukkit.World
import java.util.concurrent.CompletableFuture
import io.papermc.lib.PaperLib
class GenerationTaskPaper(
private val plugin: Chunkmaster, override val world: World,
centerChunk: ChunkCoordinates, private val startChunk: ChunkCoordinates,
override val stopAfter: Int = -1
) : GenerationTask(plugin, centerChunk, startChunk) {
private val maxPendingChunks = plugin.config.getInt("generation.max-pending-chunks")
private val pendingChunks = HashSet<CompletableFuture<Chunk>>()
override var count = 0
private set
override var endReached: Boolean = false
private set
/**
* Runs the generation task. Every Iteration the next chunk will be generated if
* it hasn't been generated already.
* After 10 chunks have been generated, they will all be unloaded and saved.
*/
override fun run() {
if (plugin.mspt < msptThreshold) { // pause when tps < 2
if (loadedChunks.size > maxLoadedChunks) {
for (chunk in loadedChunks) {
if (chunk.isLoaded) {
chunk.unload(true)
}
}
loadedChunks.clear()
} else if (pendingChunks.size < maxPendingChunks) { // if more than 10 chunks are pending, wait.
if (borderReached()) {
endReached = true
endReachedCallback?.invoke()
return
}
var chunk = nextChunkCoordinates
for (i in 1 until chunkSkips) {
if (PaperLib.isChunkGenerated(world, chunk.x, chunk.z)) {
chunk = nextChunkCoordinates
} else {
break
}
}
if (!PaperLib.isChunkGenerated(world, chunk.x, chunk.z)) {
for (i in 0 until minOf(chunksPerStep, (stopAfter - count) - 1)) {
if (!PaperLib.isChunkGenerated(world, chunk.x, chunk.z)) {
pendingChunks.add(PaperLib.getChunkAtAsync(world, chunk.x, chunk.z, true))
}
chunk = nextChunkCoordinates
}
if (!PaperLib.isChunkGenerated(world, chunk.x, chunk.z)) {
pendingChunks.add(PaperLib.getChunkAtAsync(world, chunk.x, chunk.z, true))
}
}
lastChunkCoords = chunk
count = spiral.count // set the count to the more accurate spiral count
}
}
checkChunksLoaded()
}
/**
* Cancels the generation task.
* This unloads all chunks that were generated but not unloaded yet.
*/
override fun cancel() {
unloadAllChunks()
}
/**
* Cancels all pending chunks and unloads all loaded chunks.
*/
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) {
chunk.unload(true)
}
}
}
/**
* Checks if some chunks have been loaded and adds them to the loaded chunk set.
*/
private fun checkChunksLoaded() {
val completedEntrys = HashSet<CompletableFuture<Chunk>>()
for (pendingChunk in pendingChunks) {
if (pendingChunk.isDone) {
completedEntrys.add(pendingChunk)
loadedChunks.add(pendingChunk.get())
} else if (pendingChunk.isCompletedExceptionally || pendingChunk.isCancelled) {
completedEntrys.add(pendingChunk)
}
}
pendingChunks.removeAll(completedEntrys)
}
}

@ -1,70 +0,0 @@
package net.trivernis.chunkmaster.lib.generation
import net.trivernis.chunkmaster.Chunkmaster
import org.bukkit.Chunk
import org.bukkit.World
class GenerationTaskSpigot(
private val plugin: Chunkmaster, override val world: World,
centerChunk: ChunkCoordinates, private val startChunk: ChunkCoordinates,
override val stopAfter: Int = -1
) : GenerationTask(plugin, centerChunk, startChunk) {
override var count = 0
private set
override var endReached: Boolean = false
private set
/**
* Runs the generation task. Every Iteration the next chunk will be generated if
* it hasn't been generated already.
* After 10 chunks have been generated, they will all be unloaded and saved.
*/
override fun run() {
if (plugin.mspt < msptThreshold) { // pause when tps < 2
if (loadedChunks.size > maxLoadedChunks) {
for (chunk in loadedChunks) {
if (chunk.isLoaded) {
chunk.unload(true)
}
}
loadedChunks.clear()
} else {
if (borderReached()) {
endReached = true
endReachedCallback?.invoke()
return
}
var chunk = nextChunkCoordinates
if (!world.isChunkGenerated(chunk.x, chunk.z)) {
for (i in 0 until minOf(chunksPerStep, stopAfter - count)) {
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 = spiral.count // set the count to the more accurate spiral 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) {
chunk.unload(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,37 +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, Int>? = null
/**
* Returns the generation Speed
*/
val generationSpeed: Double?
get() {
var generationSpeed: Double? = null
if (lastProgress != null) {
val chunkDiff = generationTask.count - lastProgress!!.second
val timeDiff = (System.currentTimeMillis() - lastProgress!!.first).toDouble()/1000
generationSpeed = chunkDiff.toDouble()/timeDiff
}
lastProgress = Pair(System.currentTimeMillis(), generationTask.count)
return generationSpeed
}
init {
lastProgress = 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
}

@ -0,0 +1,148 @@
package net.trivernis.chunkmaster.lib.shapes
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashSet
import kotlin.math.PI
import kotlin.math.pow
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>>()
override fun endReached(): Boolean {
if ((radius + 1) in 1..r) return true
return radius > 0 && coords.isEmpty() && r >= radius
}
override fun total(): Double {
return (PI * radius.toFloat().pow(2))
}
override fun progress(maxRadius: Int?): Double {
// TODO: Radius inner progress
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 {
return r
}
/**
* Returns the edge locations of the shape to be used
* with dynmap markers
*/
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) }
}
/**
* Returns the next coordinate of the circle until the end is reached
*/
override fun next(): Pair<Int, Int> {
if (endReached()) {
return currentPos
}
if (count == 0 && currentPos != center) {
val tmpCircle = Circle(center, center, radius)
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))
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)
return currentPos
}
/**
* Returns the int coordinates for a circle
* Some coordinates might already be present in the list
* @param r - the radius
*/
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 = Vector<Pair<Int, Int>>()
for (pos in segCoords) {
val coord = when (step) {
1 -> Pair(pos.first, -pos.second)
2 -> Pair(pos.second, -pos.first)
3 -> Pair(-pos.second, -pos.first)
4 -> Pair(-pos.first, -pos.second)
5 -> Pair(-pos.first, pos.second)
6 -> Pair(-pos.second, pos.first)
7 -> Pair(pos.second, pos.first)
else -> pos
}
if (coord !in coords) {
tmpSeg.add(coord)
}
}
if (step % 2 == 0) {
coords.addAll(tmpSeg.reversed())
} else {
coords.addAll(tmpSeg)
}
}
return coords
}
/**
* Returns the int coordinates for a circles segment
* @param r - the radius
*/
private fun getSegment(r: Int): ArrayList<Pair<Int, Int>> {
var d = -r
var x = r
var y = 0
val coords = ArrayList<Pair<Int, Int>>()
while (y <= x) {
coords.add(Pair(x, y))
d += 2 * y + 1
y += 1
if (d > 0) {
x -= 1
d -= 2 * x
}
}
return coords
}
override fun reset() {
this.r = 0
this.currentPos = center
this.previousCoords.clear()
this.count = 0
}
}

@ -0,0 +1,43 @@
package net.trivernis.chunkmaster.lib.shapes
abstract class Shape(protected val center: Pair<Int, Int>, start: Pair<Int, Int>, radius: Int) {
protected var currentPos = start
protected var radius = radius
private set
var count = 0
/**
* Returns the next value
*/
abstract fun next(): Pair<Int, Int>
/**
* If the shape can provide a next value
*/
abstract fun endReached(): Boolean
/**
* Returns the progress of the shape
*/
abstract fun progress(maxRadius: Int?): Double
/**
* The total number of chunks to generate
*/
abstract fun total(): Double
/**
* Returns the current radius
*/
abstract fun currentRadius(): 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()
}

@ -0,0 +1,110 @@
package net.trivernis.chunkmaster.lib.shapes
import kotlin.math.abs
import kotlin.math.pow
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 && ((direction == 3
&& abs(distances.first) == abs(distances.second)
&& abs(distances.first) == radius)
|| (distances.first > radius || distances.second > radius))
}
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 {
val distances = getDistances(center, currentPos)
return distances.first.coerceAtLeast(distances.second)
}
/**
* 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 = 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++
return currentPos
}
if (currentPos == center) { // the center has to be handled exclusively
currentPos = Pair(center.first, center.second + 1)
count++
return center
} else {
val distances = getDistances(center, currentPos)
if (abs(distances.first) == abs(distances.second)) {
direction = (direction + 1) % 5
}
}
when (direction) {
0 -> {
currentPos = Pair(currentPos.first + 1, currentPos.second)
}
1 -> {
currentPos = Pair(currentPos.first, currentPos.second - 1)
}
2 -> {
currentPos = Pair(currentPos.first - 1, currentPos.second)
}
3 -> {
currentPos = Pair(currentPos.first, currentPos.second + 1)
}
4 -> {
currentPos = Pair(currentPos.first, currentPos.second + 1)
direction = 0
}
}
count++
return currentPos
}
/**
* Returns the edges to be used with dynmap markers
*/
override fun getShapeEdgeLocations(): List<Pair<Int, Int>> {
val a = Pair(this.radius + center.first, this.radius + center.second)
val b = Pair(this.radius + center.first, -this.radius + center.second)
val c = Pair(-this.radius + center.first, -this.radius + center.second)
val d = Pair(-this.radius + center.first, this.radius + center.second)
return listOf(a, b, c, d, a)
}
/**
* Returns the distances between 2 coordinates
*/
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
}
}

@ -0,0 +1,85 @@
RESUME_FOR_WORLD = Resuming chunk generation task for world '%s'...
TASK_FINISHED = Task #%d finished after %d chunks.
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'. 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!
WORLD_NOT_FOUND = §cWorld §2%s §cnot found!
TASK_ALREADY_EXISTS = §cA task for '%s' already exists!
TASK_CREATION_SUCCESS = §9Generation Task for world §2%s§9 §2%s§9 and shape %s successfully created!
TASK_UNIT_WORLDBORDER = until world border
TASK_UNIT_RADIUS = with a radius of %s
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%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!
RESUME_SUCCESS = §9Resumed all generation Tasks.
NOT_PAUSED = §cThe generation process is not paused!
CONFIG_RELOADING = Reloading the config file...
CONFIG_RELOADED = §2The config file has been reloaded!
TELEPORTED = §9You have been teleported to chunk §2%s, %s
TP_ONLY_PLAYER = §cThis command can only be executed by a player!
NO_PERMISSION = §cYou do not have the permission for this command!
SUBCOMMAND_NOT_FOUND = §cSubcommand §2%s §cnot found!
STOPPING_ALL_TASKS = Stopping all generation tasks...
SAVING_TASK_PROGRESS = Saving the progress of task #%s to the database...
DB_INIT = Initializing database...
DB_INIT_FINISHED = Database fully initialized.
DB_INIT_EROR = Failed to init database: %s.
DATABASE_CONNECTION_ERROR = §cCould not get the database connection!
SQL_ERROR = §cAn eror occured on sql %s!
NO_DATABASE_CONNECTION = §cCould not execute sql: No database connection.
CREATE_TABLE_DEFINITION = Created table %s with definition %s.
TABLE_CREATE_ERROR = §cError when creation table %s.
UPDATE_TABLE_DEFINITION = Updated table %s with sql %s.
UPDATE_TABLE_FAILED = Failed to update table %s with sql %s.
TOO_FEW_ARGUMENTS = §cYou did not provide enough arguments.
COORD_INVALID = §cThe provided coordinate ('%s', '%s') is invalid!
CENTER_UPDATED = §9The center for world §2%s §9has been set to §2(%s, %s)§9.
CENTER_INFO = §9The center for world §2%s §9is §2(%s, %s)§9.
PLUGIN_DETECTED = Detected %s version %s
RESUME_PLAYER_LEAVE = The player count is smaller than the configured pause value. Resuming generation...
PAUSE_PLAYER_JOIN = The player count has reached the pause value. Pausing generation...
PAUSE_MANUALLY = Generation was manually paused. Not resuming automatically.
STATS_HEADER = §nStats§r
STATS_SERVER = §lServer Stats§r
STATS_SERVER_VERSION = - Server Version: §2%s§r
STATS_PLUGIN_VERSION = - Plugin Version: §2%s§r
STATS_MEMORY = - Memory (u/a): §2%d mb / %d mb = (%.2f percent)§r
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_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...

@ -0,0 +1,86 @@
RESUME_FOR_WORLD = Setze das Chunk-Generieren für Welt '%s' fort...
TASK_FINISHED = Aufgabe #%d wurde nach %d chunks beendet.
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'. 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!
WORLD_NOT_FOUND = §c Die Welt §2%s §cwurde nicht gefunden!
TASK_ALREADY_EXISTS = §cEs existiert bereits eine Aufgabe für §2%s§c!
TASK_CREATION_SUCCESS = §9Generierungs-Aufgabe §2%s§9 §2%s§9 in der Form %s wurde erfolgreich erstellt!
TASK_UNIT_WORLDBORDER = bis zur World-Border
TASK_UNIT_RADIUS = mit einem Radius von %s
TASK_ID_REQUIRED = §cDu musst eine Aufgaben-Id angeben!
INVALID_ARGUMENT = §cFalscher Parameter an Stelle %s: %s!
PAUSED_TASKS_HEADER = §lPausierte Generierungsaufgaben§r
RUNNING_TASKS_HEADER = §lLaufende Generierungsaufgaben§r
NO_GENERATION_TASKS = Es gibt keine Aufgaben.
PAUSE_SUCCESS = §9Alle Aufgaben wurden pausiert.
ALREADY_PAUSED = §cDas Generieren ist bereits pausiert.
RESUME_SUCCESS = §9Alle Aufgaben wurden fortgesetzt.
NOT_PAUSED = §cEs gibt keine pausierten Aufgaben!
CONFIG_RELOADING = Die Konfigurationsdatei wird neu eingelesen...
CONFIG_RELOADED = §2Die Konfigurationsdatei wurde neu geladen!
TELEPORTED = §9Du wurdest zum Chunk §2%s, %s §9teleportiert
TP_ONLY_PLAYER = §cDieser Befehl kann nur von einem Spieler ausgeführt werden.
NO_PERMISSION = §cDu hast nicht die Rechte für diesen Befehl!
SUBCOMMAND_NOT_FOUND = §cUnteraktion §2%s §cwurde nicht gefunden!
STOPPING_ALL_TASKS = Stoppt alle Aufgaben...
SAVING_TASK_PROGRESS = Speichert den Fortschritt der Aufgabe #%s in der Datenbank...
DB_INIT = Initialisiere Datenbank...
DB_INIT_FINISHED = Die Datenbank wurde initialisiert.
DB_INIT_EROR = Fehler beim Initalisieren der Datenbank: %s.
DATABASE_CONNECTION_ERROR = §cDie Datenbankverbindung konnte nicht erzeugt werden.
SQL_ERROR = §cEin Fehler trat mit sql %s auf!
NO_DATABASE_CONNECTION = §cSql konnte nicht ausgeführt werden: Keine Datenbankverbindung.
CREATE_TABLE_DEFINITION = Tabelle %s mit Definition %s wurde erstellt.
TABLE_CREATE_ERROR = §cFehler beim erstellen der Tabelle %s.
UPDATE_TABLE_DEFINITION = Tabelle %s wurde mit sql %s geupdated.
UPDATE_TABLE_FAILED = Fehler beim Updaten der Tabelle %s mit sql %s.
TOO_FEW_ARGUMENTS = §cDu hast nicht genug Parameter angegeben.
COORD_INVALID = §cDie Koordinate ('%s', '%s') ist ungültig!
CENTER_UPDATED = §9Die Mitte der Welt §2%s §9wurde auf §2(%s, %s)§9 gesetzt.
CENTER_INFO = §9Die Mitte der Welt §2%s §9ist §2(%s, %s)§9.
PLUGIN_DETECTED = Plugin %s in der Version %s gefunden!
RESUME_PLAYER_LEAVE = Die Anzahl der Spieler hat den festgelegen Wert zum Pausieren unterschritten. Setze Generieren fort...
PAUSE_PLAYER_JOIN = Die Anzahl der Spieler hat den festgelegten Wert zum Pausieren erreicht. Pausiere...
PAUSE_MANUALLY = Das Generieren wurde manuell pausiert und wird deswegen nicht automatisch fortgesetzt.
STATS_HEADER = §nStatistiken§r
STATS_SERVER = §lServer Statistik§r
STATS_SERVER_VERSION = - Server Version: §2%s§r
STATS_PLUGIN_VERSION = - Plugin Version: §2%s§r
STATS_MEMORY = - Arbeitsspeicher (u/a): §2%d mb / %d mb = (%.2f percent)§r
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
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

@ -0,0 +1,84 @@
RESUME_FOR_WORLD = Resuming chunk generation task for world '%s'...
TASK_FINISHED = Task #%d finished after %d chunks.
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'. 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!
WORLD_NOT_FOUND = §cWorld §2%s §cnot found!
TASK_ALREADY_EXISTS = §cA task for '%s' already exists!
TASK_CREATION_SUCCESS = §9Generation Task for world §2%s§9 §2%s§9 and shape %s successfully created!
TASK_UNIT_WORLDBORDER = until world border
TASK_UNIT_RADIUS = with a radius of %s
TASK_ID_REQUIRED = §cYou need to provide a task id!
INVALID_ARGUMENT = §cInvalid argument at %s: %s!
PAUSED_TASKS_HEADER = Currently Paused Generation Tasks
RUNNING_TASKS_HEADER = Currently Running Generation Tasks
NO_GENERATION_TASKS = There are no generation tasks.
PAUSE_SUCCESS = §9Paused all generation tasks.
ALREADY_PAUSED = §cThe generation process is already paused!
RESUME_SUCCESS = §9Resumed all generation Tasks.
NOT_PAUSED = §cThe generation process is not paused!
CONFIG_RELOADING = Reloading the config file...
CONFIG_RELOADED = §2The config file has been reloaded!
TELEPORTED = §9You have been teleported to chunk §2%s, %s
TP_ONLY_PLAYER = §cThis command can only be executed by a player!
NO_PERMISSION = §cYou do not have the permission for this command!
SUBCOMMAND_NOT_FOUND = §cSubcommand §2%s §cnot found!
STOPPING_ALL_TASKS = Stopping all generation tasks...
SAVING_TASK_PROGRESS = Saving the progress of task #%s to the database...
DB_INIT = Initializing database...
DB_INIT_FINISHED = Database fully initialized.
DB_INIT_EROR = Failed to init database: %s.
DATABASE_CONNECTION_ERROR = §cCould not get the database connection!
SQL_ERROR = §cAn eror occured on sql %s!
NO_DATABASE_CONNECTION = §cCould not execute sql: No database connection.
CREATE_TABLE_DEFINITION = Created table %s with definition %s.
TABLE_CREATE_ERROR = §cError when creation table %s.
UPDATE_TABLE_DEFINITION = Updated table %s with sql %s.
UPDATE_TABLE_FAILED = Failed to update table %s with sql %s.
TOO_FEW_ARGUMENTS = §cYou did not provide enough arguments.
COORD_INVALID = §cThe provided coordinate ('%s', '%s') is invalid!
CENTER_UPDATED = §9The center for world §2%s §9has been set to §2(%s, %s)§9.
CENTER_INFO = §9The center for world §2%s §9is §2(%s, %s)§9.
PLUGIN_DETECTED = Detected %s version %s
RESUME_PLAYER_LEAVE = The player count is smaller than the configured pause value. Resuming generation...
PAUSE_PLAYER_JOIN = The player count has reached the pause value. Pausing generation...
PAUSE_MANUALLY = Generation was manually paused. Not resuming automatically.
STATS_HEADER = §nStats§r
STATS_SERVER = §lServer Stats§r
STATS_SERVER_VERSION = - Server Version: §2%s§r
STATS_PLUGIN_VERSION = - Plugin Version: §2%s§r
STATS_MEMORY = - Memory (u/a): §2%d mb / %d mb = (%.2f percent)§r
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
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,85 @@
RESUME_FOR_WORLD = Resumiendo tarea de generación de chunks para el mundo '%s'...
TASK_FINISHED = Tarea #%d terminada después de %d chunks.
TASK_CANCELLED = Tarea #%s cancelada.
TASK_LOAD_FAILED = §cFallo al cargar la tarea #%d.
TASK_LOAD_SUCCESS = %d tareas guardadas cargadas.
TASK_NOT_FOUND = §c¡Tarea %s no encontrada!
CREATE_DELAYED_LOAD = Creando tarea Bukkit para cargar la generación de chunks...
TASK_PERIODIC_REPORT = Tarea #%d en ejecución para '%s'. Estado: %s. Progreso: %d Chunks %s %s, Velocidad: %.1f ch/s, Último Chunk: %d, %d
TASK_PERIODIC_REPORT_CORRECTING = Tarea #%d gerenando chunks perdidos para '%s'. Progreso: %d Chunks %s
TASK_SAVE_FAILED = §cExcepción al guardar las tareas: %s
WORLD_NAME_REQUIRED = §c¡Tienes que proporcionar un nombre del mundo!
WORLD_NOT_FOUND = §c¡Mundo §2%s §cno encontrado!
TASK_ALREADY_EXISTS = §c¡Una tarea para '%s' ya existe!
TASK_CREATION_SUCCESS = §9¡Tarea de generación para el mundo §2%s§9 §2%s§9 y forma %s creada con éxito!
TASK_UNIT_WORLDBORDER = hasta el borde del mundo
TASK_UNIT_RADIUS = con un radius de %s
TASK_ID_REQUIRED = §c¡Necesitas proporcionar una id de tarea!
INVALID_ARGUMENT = §c¡Argumento invalido en %s: %s!
PAUSED_TASKS_HEADER = Tareas de generación actualmente en pausa
TASKS_ENTRY = - §9#%d§r - §2%s§r - §2%s§r - §2%s Chunks %s§r
RUNNING_TASKS_HEADER = Tareas de generación en curso
NO_GENERATION_TASKS = No hay tareas de generación.
COMPLETED_TASKS_HEADER = §nTareas de generación completadas§r
COMPLETED_WORLD_HEADER = §l%s§r
COMPLETED_TASK_ENTRY = - §9#%d§r: §2%d§r radio de chunks desde el centro §2(%d, %d)§r con forma §2%s§r
PAUSE_SUCCESS = §9Tareas de generación pausadas
ALREADY_PAUSED = §cEl progreso de generación ya está en pausa.
RESUME_SUCCESS = §9Tareas de generación renaudadas
NOT_PAUSED = §cEl progreso de generación no está en pausa.
CONFIG_RELOADING = Recargando el archivo de configuración...
CONFIG_RELOADED = §2El archivo de configuración ha sido recargado!
TELEPORTED = §9Has sido teletransportado al chunk §2%s, %s
TP_ONLY_PLAYER = §c¡Este comando solo puede ser ejecutado por un jugador!
NO_PERMISSION = §c¡No tienes permiso para este comando!
SUBCOMMAND_NOT_FOUND = §c¡Subcomando §2%s §cno encontrado!
STOPPING_ALL_TASKS = Deteniendo todas las tareas de generación...
SAVING_TASK_PROGRESS = Guardando el progreso de la tarea #%s en la base de datos...
DB_INIT = Iniciando la base de datos...
DB_INIT_FINISHED = Base de datos ha sido inicializada.
DB_INIT_EROR = Error al inicializar la base de datos: %s.
DATABASE_CONNECTION_ERROR = §c¡No se ha podido establecer la conexión con la base de datos!
SQL_ERROR = §cSe ha producido un error en sql %s!
NO_DATABASE_CONNECTION = §cNo se ha podido ejecutar sql: No hay conexión con la base de datos.
CREATE_TABLE_DEFINITION = Tabla %s creada con definición %s.
TABLE_CREATE_ERROR = §cSe ha producido un error al crear la tabla %s.
UPDATE_TABLE_DEFINITION = Tabla %s actualizada con sql %s.
UPDATE_TABLE_FAILED = Se ha producido un error al actualizar la tabla %s con sql %s.
TOO_FEW_ARGUMENTS = §cNo has aportado suficientes argumentos.
COORD_INVALID = §cLas coordenadas proporcionadas ('%s', '%s') son incorrectas!
CENTER_UPDATED = §9El centro del mundo §2%s §9se ha actualizado a §2(%s, %s)§9.
CENTER_INFO = §9El centro del mundo §2%s §9es §2(%s, %s)§9.
PLUGIN_DETECTED = ¡Plugin %s con versión %s detectada!
RESUME_PLAYER_LEAVE = El número de jugadores es menor que el configurado como valor de pausa. Reanudando generación...
PAUSE_PLAYER_JOIN = El número de jugadores es igual que el configurado como valor de pausa. Pausando generación...
PAUSE_MANUALLY = Generación se pausó manualmente. No se resumirá automáticamente.
STATS_HEADER = §nEstadísticas§r
STATS_SERVER = §lEstadísticas del Servidor§r
STATS_SERVER_VERSION = - Versión del Servidor: §2%s§r
STATS_PLUGIN_VERSION = - Versión del Plugin: §2%s§r
STATS_MEMORY = - Memoria (u/a): §2%d mb / %d mb = (%.2f porcentaje)§r
STATS_CORES = - Cores: §2%d§r
STATS_WORLD_NAME = §l%s§r
STATS_ENTITY_COUNT = - §2%d§r Entidades
STATS_LOADED_CHUNKS = - §2%d§r Chunks cargados
STATS_PLUGIN_LOADED_CHUNKS = - §2%d§r Chunks cargados por Chunkmaster
STATS_GENERATING = - Generando: §2%s§r
SAVING_CHUNKS = Guardando %d chunks guardados...
CANCEL_FAIL = Fallo en la cancelación de la tarea #%d en el tiempo de espera configurado!
NO_AUTOSTART = Autoarranque configurado a §2false§r. Pausando...

@ -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,85 @@
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 = 无区块生成任务.
COMPLETED_TASKS_HEADER = §n已完成任务列表§r
COMPLETED_WORLD_HEADER = §l%s§r
COMPLETED_TASK_ENTRY = - §9#%d§r: §2%d§r 区块半径, 中心点 §2(%d, %d)§r , 形状 §2%s§r
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 载入的区块
STATS_GENERATING = - 正在生成: §2%s§r
SAVING_CHUNKS = 正在保存 %d 已载入的区块...
CANCEL_FAIL = 取消任务 #%d 操作超时!
NO_AUTOSTART = 自动启动被设置为 §2关闭§r. 正在暂停...

@ -1,35 +1,42 @@
main: net.trivernis.chunkmaster.Chunkmaster main: net.trivernis.chunkmaster.Chunkmaster
name: Chunkmaster name: Chunkmaster
version: '0.12-beta' version: $$PLUGIN_VERSION$$
description: Chunk commands plugin. description: Automated world pregeneration.
author: Trivernis author: Trivernis
website: trivernis.net website: trivernis.net
api-version: '1.14' api-version: '1.14'
database: true
softdepend:
- dynmap
commands: commands:
chunkmaster: chunkmaster:
description: Main command description: Main command
permission: chunkmaster.chunkmaster permission: chunkmaster.chunkmaster
usage: | usage: |
/<command> generate [<world>, <chunk-count>] - generates chunks starting from the spawn until the chunk-count is reached /<command> generate [<world>] <radius> [<shape>] - generates chunks starting from the spawn until the chunk-count is reached
/<command> cancel <task-id> - cancels the generation task with the task-id /<command> cancel <task-id|world> - cancels the generation task with the task-id
/<command> list - lists all running and paused generation tasks /<command> list - lists all running and paused generation tasks
/<command> pause - pauses all generation tasks /<command> pause - pauses all generation tasks
/<command> resume - resumes all generation tasks /<command> resume - resumes all generation tasks
/<command> reload - reloads the configuration and restarts all tasks /<command> reload - reloads the configuration and restarts all tasks
/<command> tpchunk <chunkX> <chunkZ> - teleports you to the chunk with the given chunk coordinates /<command> tpchunk <chunkX> <chunkZ> - teleports you to the chunk with the given chunk coordinates
/<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: aliases:
- chm - chm
- chunkm - chunkm
- cmaster - cmaster
permissions: permissions:
cunkmaster.generate: chunkmaster.generate:
description: Allows the generate subcommand. description: Allows the generate subcommand.
default: op default: op
chunkmaster.list: chunkmaster.list:
description: Allows the list subcommand. description: Allows the list subcommand.
default: op default: op
chunkmaster.cancel: chunkmaster.cancel:
description: Allows the remove subcommand. description: Allows the cancel subcommand.
default: op default: op
chunkmaster.pause: chunkmaster.pause:
description: Allows the pause subcommand. description: Allows the pause subcommand.
@ -43,6 +50,18 @@ permissions:
chunkmaster.tpchunk: chunkmaster.tpchunk:
description: Allows the tpchunk subcommand. description: Allows the tpchunk subcommand.
default: op default: op
chunkmaster.setcenter:
description: Allows the setCenter subcommand.
default: op
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: chunkmaster.chunkmaster:
description: Allows Chunkmaster commands. description: Allows Chunkmaster commands.
default: op default: op
@ -51,8 +70,14 @@ permissions:
default: op default: op
children: children:
- chunkmaster.generate - chunkmaster.generate
- chunkmaster.listgentasks - chunkmaster.list
- chunkmaster.removegentask - chunkmaster.cancel
- chunkmaster.pausegentasks - chunkmaster.pause
- chunkmaster.resumegentasks - chunkmaster.resume
- chunkmaster.completed
- chunkmaster.tpchunk
- chunkmaster.reload
- chunkmaster.setcenter
- chunkmaster.getcenter
- chunkmaster.stats
- chunkmaster.chunkmaster - 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