29 Commits

Author SHA1 Message Date
2a34e5deae Ads support scripts. 2026-09-27 00:06:01 -07:00
8b3421e8d6 Normalize documentation line endings 2026-09-26 23:58:14 -07:00
a1f8fb3950 Document Gradle and Paper workflow 2026-09-26 23:56:21 -07:00
726b178a53 Adopt Git-sensitive semantic versioning 2026-09-26 23:53:38 -07:00
f1de135dfe Verify Metropolis on Paper 26.2 2026-09-26 23:52:37 -07:00
bc39305e90 Replace City region bounds safely 2026-09-26 23:52:15 -07:00
1eab80ab2b Add opt-in Paper integration tests 2026-09-26 23:48:35 -07:00
1721349a71 Persist plot reservation regions 2026-09-26 23:48:24 -07:00
5656083871 Deploy Gradle-built plugin jar 2026-09-26 22:56:43 -07:00
c760f5ce46 Add server-free plugin behavior tests 2026-09-26 22:53:58 -07:00
ba96b8a73a Remove Maven and Eclipse metadata 2026-09-26 22:50:07 -07:00
e97f63af1f Verify initial Gradle build 2026-09-26 22:49:36 -07:00
5f6acb86c3 Modernize Paper and region API usage 2026-09-26 22:49:15 -07:00
c127e45f8d Resolve player names exactly in commands 2026-09-26 22:46:45 -07:00
81b34862b4 Track Metropolis home ownership by UUID 2026-09-26 22:46:12 -07:00
509df1b452 Use Bukkit YAML for home configuration 2026-09-26 22:43:41 -07:00
6df83fbee9 Register join listener through Paper manager 2026-09-26 22:42:45 -07:00
832eb59624 Use plugin logger for diagnostics 2026-09-26 22:42:27 -07:00
316accff2d Migrate build scripts to Gradle 2026-09-26 22:25:23 -07:00
7a5764ebe0 Audit deprecated plugin API usage 2026-09-26 22:24:48 -07:00
f6ee7ba85c Add JUnit and Mockito test setup 2026-09-26 22:23:40 -07:00
712e7f8ba0 Update Paper plugin descriptor 2026-09-26 22:23:02 -07:00
3ace91bcff Add modern Paper and region APIs 2026-09-26 22:22:21 -07:00
648871e784 Configure Java 25 toolchain 2026-09-26 22:21:46 -07:00
911f9161f3 Initialize Gradle build for Metropolis 2026-09-26 22:21:29 -07:00
74c36a8aa7 Adds todo doc with tasks to migrate to gradle and add some features. 2026-09-26 18:37:11 -07:00
33ed09da09 Script cleanup. 2026-09-26 13:47:19 -07:00
2437d38e70 Adds project description files. 2026-09-24 19:47:32 -07:00
990ce6811c Adds .worktrees to gitignore. 2026-09-23 19:57:03 -07:00
53 changed files with 2301 additions and 528 deletions

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java"/>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6"/>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER"/>
<classpathentry kind="output" path="target/classes"/>
</classpath>

24
.env.example Normal file
View File

@@ -0,0 +1,24 @@
# Configuration for tools/*.sh. Values are read as KEY=value data, not executed.
# Unquoted values may contain spaces; matching outer quotes are removed.
# Paper server
PAPER_VERSION=26.2
PAPER_BUILD=129
PAPER_USER_AGENT="majinnaibu-plugin-dev/1.0 (https://github.com/majinnaibu)"
JAVA_OPTS="-Xms2G -Xmx2G"
STOP_TIMEOUT=60
MINECRAFT_SERVER_JAR=paper-26.2-129.jar
# Relative paths are resolved from the project root; ~/ is expanded.
MINECRAFT_SERVER_PATH=./server
MINECRAFT_BACKUP_PATH=./backups
# Modrinth publishing
# Release version is read from Gradle's Git-sensitive semantic version and checked against the jar's plugin.yml.
MODRINTH_TOKEN=
MODRINTH_PROJECT_ID=metropolis-builder
MODRINTH_CHANGELOG=
MODRINTH_GAME_VERSIONS=26.2
MODRINTH_LOADERS=paper
MODRINTH_VERSION_TYPE=
MODRINTH_DEPENDENCIES=
MODRINTH_FILE=

14
.gitignore vendored
View File

@@ -1,5 +1,9 @@
/server
/Plugin.jardesc
/Plugin-Release.jardesc
/lib
/target
.gradle/
build/
target/
.worktrees/
bin/
/backups/
/server/
.env

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>Metropolis</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>

View File

@@ -1,5 +0,0 @@
#Sat Mar 03 20:54:28 PST 2012
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding//src/main/resources=UTF-8
encoding/<project>=UTF-8

View File

@@ -1,13 +0,0 @@
#Sat Mar 03 21:02:20 PST 2012
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=1.6
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
org.eclipse.jdt.core.compiler.source=1.6

View File

@@ -1,3 +0,0 @@
#Sat Feb 18 20:11:20 PST 2012
eclipse.preferences.version=1
org.eclipse.ltk.core.refactoring.enable.project.refactoring.history=false

View File

@@ -1,5 +0,0 @@
#Sat Feb 18 18:33:58 PST 2012
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

33
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,33 @@
# Contributing
## Development Setup
Use Java 25 or newer. The Gradle wrapper downloads Gradle 9.8; dependencies are declared in `build.gradle` and resolved from Maven Central, PaperMC, and EngineHub.
```sh
./gradlew build
```
The standard build runs JUnit 5 tests without starting Minecraft. Tests use Mockito for Bukkit and external-plugin boundaries. Keep domain and configuration behavior testable without a running server.
## Live Server Tests
Live tests are opt-in and run separately from `build`:
```sh
METROPOLIS_RCON_HOST=127.0.0.1 \
METROPOLIS_RCON_PORT=25575 \
METROPOLIS_RCON_PASSWORD=... \
METROPOLIS_SERVER_DIR=/path/to/server \
./gradlew integrationTest
```
Use a disposable or backed-up Paper server with compatible WorldGuard and WorldEdit plugin jars installed. Keep RCON local, use a temporary password, and restore the server after tests that modify regions or worlds.
## Pull Requests
- Keep changes focused and include tests for behavior changes.
- Run `./gradlew build` before submitting.
- Run `integrationTest` only when changing behavior that requires Paper or the external plugins; do not add it to the default build.
- Update `README.md` and project descriptions when user-facing setup, commands, or dependencies change.
- Keep generated build output, server data, credentials, and local `.env` files out of commits.

23
Project-Description.json Normal file
View File

@@ -0,0 +1,23 @@
{
"id": "metropolis",
"title": "Metropolis",
"description": "Paper plugin that automatically creates protected home plots and connecting roads as players join a Minecraft server.",
"docId": "Metropolis",
"image": {
"url": "Pixel-art aerial view of a growing Minecraft city planned in a square grid, small player home plots connected by pale roads, a protected central spawn, blocky green landscape, subtle WorldGuard-inspired boundary lines, warm daylight, clean game-plugin thumbnail composition, no words or logos.",
"alt": "Pixel-art aerial view of a Minecraft city with player home plots and connecting roads",
"prompt": "Pixel-art aerial view of a growing Minecraft city planned in a square grid, small player home plots connected by pale roads, a protected central spawn, blocky green landscape, subtle WorldGuard-inspired boundary lines, warm daylight, clean game-plugin thumbnail composition, no words or logos.",
"width": 0,
"height": 0,
"backgroundClassName": "bg-surface"
},
"url": "/projects/metropolis",
"tags": ["open-source"],
"source": {
"type": "github",
"url": "https://github.com/headhunter45/Metropolis"
},
"feedback": {
"url": "https://github.com/headhunter45/Metropolis/issues"
}
}

12
Project-Description.md Normal file
View File

@@ -0,0 +1,12 @@
# Metropolis
Metropolis is a Paper plugin that expands a protected Minecraft city as players join. It generates home plots and connecting roads, uses WorldGuard to protect the city and homes, and supports reserved plots and home teleportation.
## Platform
- Paper 26.2, Java 25+
- WorldGuard 7.0.19 and WorldEdit 7.4.5
- Built with Gradle 9.8 and Git-sensitive semantic versioning
- Configuration: `plugins/Metropolis/config.yml`
- Unit tests run without a server; live integration tests use the separate Gradle `integrationTest` task.
- License: GNU General Public License v3

View File

@@ -1,47 +1,67 @@
Metropolis generates a home for each user connecting to your world with roads between them. As new users connect the city expands.
# Metropolis
WorldGuard regions are used for each home and for the city as a whole.
Metropolis expands a protected Minecraft city as players join. Each player can receive a home plot connected by generated roads; WorldGuard protects the city, homes, and reserved plots.
One region called "City" is created to protect the spawn and roads. As the assigned blocks expand this region is automatically updated.
## Requirements
Each user gets assigned a home region named "h_username" where username is replaced with their username. This region is surrounded by a road and located next to an existing region.
- Paper 26.2, with Java 25 or newer
- WorldGuard 7.0.19
- WorldEdit 7.4.5
After creation these regions can be modified as normal via WorldGuard commands.
WorldGuard and WorldEdit are required plugin dependencies. Install them alongside Metropolis in the server's `plugins` directory.
INSTALL:
To install just drop the jar into your plugins folder. All necessarry data will be created on first launch.
## Build and Test
CONFIG:
Edit the config.yml file. You can set the height to clear above roads, the road width, the plot size (incluing half the road width on either side), the road material, and the level at which the road is generated.
Build the plugin and run the server-free unit tests:
Changelog:
v0.5
Added example docs
Added Commands to plugin.yml to move and evict homes
Added floor and road supports to config file and generation
v0.4.6
Added optional generation of a sign identifying owner in plots on creation
Added a Cuboid constructor that takes a worldedit selection
Added a debug mode that enables extra logging. MetropolisPlugin.DEBUG should be true for snapshots and false for release builds.
Added an occupied Plots list to MetropolisPlugin to keep track of both player homes and reserved plots.
Added the Plot class as a parent of PlayerHome and moved relevant code to it.
Added a command to reserve plots that aren't tied to a player. This can be used to setup a larger protected area around spawn
v0.4.5
v0.4.4
Added a welcome message telling players where their home is.
Switched from PlayerLoginEvent to PlayerJoinEvent
v0.4.3
Removed call to saveconfig on plugin unload.
v0.4.2
Made city region refresh on server restart.
v0.4.1
v0.4
v0.3
Added command to generate a home for a user that is not currently logged in.
v0.2.1
Made config file save on load and disable.
v0.2
Added configuration options.
```sh
./gradlew clean build
```
v0.1
Initial Release
The plugin jar is written to `build/libs/`. The Gradle wrapper uses Gradle 9.8, and Git-sensitive semantic versioning derives the artifact and `plugin.yml` versions from repository tags and commits.
Run the opt-in live-server tests against a running Paper server with WorldGuard, WorldEdit, and RCON enabled:
```sh
METROPOLIS_RCON_HOST=127.0.0.1 \
METROPOLIS_RCON_PORT=25575 \
METROPOLIS_RCON_PASSWORD=... \
METROPOLIS_SERVER_DIR=/path/to/server \
./gradlew integrationTest
```
`integrationTest` is separate from the normal `test` and `build` tasks. Keep RCON bound to a trusted interface and do not commit its password.
## Server Scripts
Copy `.env.example` to `.env` and configure the Paper version/build and server paths. The scripts read `.env` as key/value data.
- `tools/build-plugin.sh` builds and tests the plugin.
- `tools/deploy-plugin.sh` copies the Gradle-built jar to the configured server.
- `tools/start-server.sh` downloads the configured Paper build if needed and starts it.
- `tools/backup-server.sh` and `tools/restore-server.sh` back up or restore the configured server.
- `tools/publish-local.sh` publishes the artifact to the local Maven repository.
- `tools/publish-modrinth.sh` uploads a built jar after checking its version.
## Configuration
The default configuration is in `src/main/resources/config.yml` and is copied to `plugins/Metropolis/config.yml` on first startup. Materials use Bukkit names; the previous numeric IDs for stone, grass, cobblestone, and bedrock are still accepted when loading a configuration.
Player home ownership and selected-home data use UUIDs. Existing name-keyed `currentHomes.yml` entries and name-based WorldGuard home regions are recognized during migration.
## Commands
- `/metropolis` displays the plugin version.
- `/metropolis-home-acquire` acquires an available home plot.
- `/metropolis-home-generate <player>` generates a home for a known online or cached offline player.
- `/metropolis-home-list` lists Metropolis plots.
- `/metropolis-home-go [player]` teleports to a home.
- `/metropolis-home-move <home-number> [player]` changes a player's selected home.
- `/metropolis-home-evict <player>` removes a player's home ownership.
- `/metropolis-plot-reserve <name> <minX> <minY> <minZ> <maxX> <maxY> <maxZ>` reserves coordinates; a player may instead provide a WorldEdit selection.
- `/metropolis-plot-go <plot-name> [player]` teleports to a plot.
- `/metropolis-flag-reset` reapplies Metropolis protection flags.
## License
GNU General Public License v3.

119
TODO.md Normal file
View File

@@ -0,0 +1,119 @@
# Modernize Metropolis Plugin: Implementation Tasks
| ID | Status | Title |
|:-------:|:--------:|:------|
| MET-001 | Done | Initialize Gradle in the project root, add the Gradle wrapper, and create the project build file. |
| MET-002 | Done | Set Gradle project metadata and configure a Java toolchain compatible with the target modern Paper version. |
| MET-003 | Done | Configure repositories and compatible PaperMC, WorldEdit, WorldGuard, and other required or optional dependencies; add build plugins such as Shadow only if needed. |
| MET-004 | Done | Move or verify plugin.yml under src/main/resources, configure Gradle resource processing, and update its Paper api-version, dependencies, commands, permissions, and required fields. |
| MET-005 | Done | Add JUnit 5 test infrastructure and keep the default Gradle `test` task server-free. Use an in-memory Paper harness such as MockBukkit for Bukkit/Paper behavior and mocks or test doubles for WorldEdit, WorldGuard, and narrow API boundaries where appropriate. |
| MET-006 | Done | Build the initial Gradle project and run its tests; resolve build setup issues before removing the existing Maven build. |
| MET-007 | Done | Update .gitignore for Gradle and remove obsolete Maven and Eclipse project files after the Gradle build succeeds. |
| MET-008 | Done | Analyze Java sources for deprecated or removed Bukkit/Paper APIs and legacy patterns, including logging, event registration, configuration, player lookup, and player-keyed storage. |
| MET-009 | Done | Review custom scripts and resources for required updates to the Gradle build and modern Paper server workflow. |
| MET-010 | Done | Refactor plugin logging to use JavaPlugin.getLogger(). |
| MET-011 | Done | Refactor event listeners and registration to use Listener, @EventHandler, and the modern plugin manager API. |
| MET-012 | Done | Replace deprecated configuration APIs with the modern Bukkit/Paper configuration API, including load, save, and reload behavior. |
| MET-013 | Done | Replace Player-keyed storage with UUID-based storage wherever player identity is persisted or tracked. |
| MET-014 | Done | Review player lookup logic and use exact or otherwise explicit name-matching behavior where appropriate. |
| MET-015 | Done | Refactor remaining code that uses deprecated or removed Bukkit, Spigot, or Paper APIs so it is compatible with the target Paper API. |
| MET-016 | Done | Add fast unit tests for plot and home allocation, reservation validation, player lookup and UUID-based ownership, configuration persistence, command validation, and event behavior. Keep domain rules independent of a running Minecraft server and mock external plugin boundaries as needed. |
| MET-017 | Done | Add a separate Gradle `integrationTest` source set/task for tests requiring an actual Paper server and compatible WorldGuard/WorldEdit plugins. Verify plugin loading, command registration and dispatch, region-backed reservations and home allocation, and persistence across a server restart. Run it only when explicitly requested; keep it out of the normal `test` and `build` tasks. |
| MET-018 | Done | Build the plugin with Gradle and use the provided server scripts to smoke-test it on modern Paper with its required plugins installed. |
| MET-019 | Done | Address bugs and incompatibilities found by unit tests, integration tests, or modern Paper server testing. |
| MET-020 | Done | Adopt the git-sensitive semantic versioning Gradle plugin used by MobScores and ScoreKeeper, using the existing 0.5-SNAPSHOT version as the migration baseline; verify version/tag behavior and enable the Gradle configuration cache when compatible. |
| MET-021 | Done | Update README.md, CONTRIBUTING.md, other documentation, and project scripts with the Gradle build, test, versioning, and modern Paper server workflow. |
| MET-022 | Planning | (Optional) Add new features, quality-of-life improvements, or CI configuration after the modernization and documentation tasks are complete. |
| GIT-009 | Planning | i have three reserve plots and one just got overwritten when a new player joined. |
| GIT-008 | Ready | Implement multiple plot sizes by granting multiple plots around each other.
| GIT-007 | Ready | Allow buying and selling of homes via an ecnoomy plugin. |
| GIT-006 | Ready | Add levels support. |
| GIT-005 | Planning | Add the possibility to create multiple plot homes in shapes. |
| GIT-004 | Ready | Add a command to request home allocation. |
| GIT-003 | Ready | Add support for multiple homes. |
| GIT-002 | Ready | Add a command to teleport to a reservation you are part of. |
| GIT-001 | Ready | Add a command to move to a different home. |
# GIT-001 - Add a command to move to a different home.
**Status:** Ready
**Descriptioon:**
/metropolis-home-move [playerName]
The target player will be the callee or if specified [playerName].
This command should set the target player's home to the plot defined by .
# GIT-002 - Add a command to teleport to a reservation you are part of.
**Status:** Ready
**Description:**
/metropolis-plot-go [playerName]
The target player should be [playerName] if specified and otherwise should be the callee.
The reservation to teleport to is
### Comments
Or better yet just use uhome or create your own, integrated that records the home location at the height set in the configuration file, but on the road next to the plot. Using existing plugins to insert into SQL say uhome, would allow privatization and minimal work on your end.
# GIT-003 - Add support for multiple homes
**Status:** Ready
**Description:**
The max number of homes per player should be defined by a permission such as
metropolis.maxhomes: 2
A config option should be added to determine when the homes are added with at least the following options
Allocate all homes that aren't allocated on login
Allocate at most 1 home per login
Only allocate the first home on login, and require a command to allocate additional homes.
# GIT-004 - Add a command to request home allocation.
**Status:** Ready
**Description:**
Add a command that users can type to get their first or subsequent homes.
Add a config option to not generate any homes automatically, but to require this command to generate a user's first home.
This could be used so that homes won't be generated until a user has read the server rules which would include telling them to type the command towards the end.
# GIT-005 - Add the possibility to create multiple plot homes in shapes.
**Status:** Planning
**Description:**
Add the possibility to create multiple plot homes in shapes
# GIT-006 - Add levels support.
**Status:** Ready
**Description:**
Allow generating multilevel cities with stairs or interchanges to go between levels (streets are level but avenues are ramps.
# GIT-007 - Allow buying and selling of homes via an ecnoomy plugin.
**Status:** Ready
**Description:**
Allow buying and selling of homes via an ecnoomy plugin. Include decent real estate support including wtb and wts listings as well as an auction facility.
This should also support not giving free plots to users with none. This way one would have to go out and mine to earn money which could be spent to purchase a plot.
# GIT-008 - Implement multiple plot sizes by granting multiple plots around each other.
**Status:** Ready
**Description:**
a= user a
x= empty plot
s= spawn
user a get's a small plot which is defined as 1 plot
a x x
x s x
x x x
user b gets a large plot which is defined as 4 plots
x x b b x
x a b b x
x x s x x
x x x x x
x x x x x

90
build.gradle Normal file
View File

@@ -0,0 +1,90 @@
plugins {
id 'java-library'
id 'maven-publish'
id 'org.danilopianini.git-sensitive-semantic-versioning' version '7.0.24'
}
group = 'com.majinnaibu.bukkitplugins'
description = 'Metropolis'
gitSemVer {
minimumVersion.set('0.5.0')
versionPrefix.set('')
}
repositories {
mavenCentral()
maven { url = 'https://repo.papermc.io/repository/maven-public/' }
maven { url = 'https://maven.enginehub.org/repo/' }
mavenLocal()
}
dependencies {
compileOnly 'io.papermc.paper:paper-api:26.2.build.129-stable'
compileOnly 'com.sk89q.worldedit:worldedit-bukkit:7.4.5'
compileOnly 'com.sk89q.worldguard:worldguard-bukkit:7.0.19'
testImplementation 'org.junit.jupiter:junit-jupiter:5.13.4'
testImplementation 'org.mockito:mockito-junit-jupiter:5.24.0'
testImplementation 'io.papermc.paper:paper-api:26.2.build.129-stable'
testImplementation 'com.sk89q.worldedit:worldedit-bukkit:7.4.5'
testImplementation 'com.sk89q.worldguard:worldguard-bukkit:7.0.19'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.13.4'
}
sourceSets {
integrationTest {
compileClasspath += sourceSets.main.output
runtimeClasspath += sourceSets.main.output
}
}
configurations {
integrationTestImplementation.extendsFrom testImplementation
integrationTestRuntimeOnly.extendsFrom testRuntimeOnly
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(25)
}
}
tasks.named('updateDaemonJvm') {
languageVersion = JavaLanguageVersion.of(25)
}
tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8'
}
def pluginVersion = project.version.toString()
tasks.named('processResources') {
inputs.property('version', pluginVersion)
filesMatching('plugin.yml') {
expand(version: pluginVersion)
}
}
tasks.named('test') {
useJUnitPlatform()
}
tasks.register('integrationTest', Test) {
description = 'Runs opt-in tests against a running Paper server with RCON enabled.'
group = 'verification'
testClassesDirs = sourceSets.integrationTest.output.classesDirs
classpath = sourceSets.integrationTest.runtimeClasspath
useJUnitPlatform()
shouldRunAfter tasks.named('test')
}
tasks.named('wrapper') {
gradleVersion = '9.8.0'
distributionType = Wrapper.DistributionType.BIN
distributionBase = Wrapper.PathBase.PROJECT
distributionPath = '.gradle/wrapper/dists'
archiveBase = Wrapper.PathBase.PROJECT
archivePath = '.gradle/wrapper/dists'
}

5
gradle.properties Normal file
View File

@@ -0,0 +1,5 @@
org.gradle.configuration-cache=true
org.gradle.daemon=true
org.gradle.java.installations.auto-download=true
org.gradle.java.installations.auto-detect=false
org.gradle.java.installations.fromEnv=JAVA_HOME

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

Binary file not shown.

View File

@@ -0,0 +1,9 @@
distributionBase=PROJECT
distributionPath=.gradle/wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.0-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=PROJECT
zipStorePath=.gradle/wrapper/dists

248
gradlew vendored Executable file
View File

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

112
gradlew.bat vendored Normal file
View File

@@ -0,0 +1,112 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem gradlew startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables, and ensure extensions are enabled
setlocal EnableExtensions
@rem Catch executions from older scripts and ensure they exit cleanly.
@rem This can be removed once we can be reasonably confident that few people
@rem will be migrating directly to this new wrapper.
goto afterSafetyNet
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
goto exitWithErrorLevel
:afterSafetyNet
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
1>&2 echo.
1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
1>&2 echo.
1>&2 echo Please set the JAVA_HOME variable in your environment to match the
1>&2 echo location of your Java installation.
"%COMSPEC%" /c exit 1
goto exitWithErrorLevel
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
1>&2 echo.
1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
1>&2 echo.
1>&2 echo Please set the JAVA_HOME variable in your environment to match the
1>&2 echo location of your Java installation.
"%COMSPEC%" /c exit 1
goto exitWithErrorLevel
:execute
@rem Setup the command line
@rem Execute gradlew
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel
@rem This label must not be changed. We rely on old scripts being able to jump to this point.
:exitWithErrorLevel
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL%

94
pom.xml
View File

@@ -1,94 +0,0 @@
<!--
Maven build file for Metropolis
Copyright (c) 2012 headhunter45 <http://majinnaibu.com>
Metropolis is available under the GNU General Public License v3
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.majinnaibu.bukkitplugins</groupId>
<artifactId>Metropolis</artifactId>
<version>0.5-SNAPSHOT</version>
<name>Metropolis</name>
<description>Metropolis generates homes and streets for players as they connect to your server.</description>
<url>http://www.github.com/headhunter45/Metropolis</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<scm>
<connection>scm:git:git://github.com/headhunter45/Metropolis.git</connection>
<url>https://github.com/headhunter45/Metropolis</url>
<developerConnection>scm:git:git@github.com:headhunter45/Metropolis.git</developerConnection>
</scm>
<repositories>
<!--
<repository>
<id>wg</id>
<url>http://files.zachsthings.com/repo</url>
</repository>
-->
<repository>
<id>sk89q</id>
<url>http://mvn2.sk89q.com/repo</url>
</repository>
<repository>
<id>bukkit-repo</id>
<url>http://repo.bukkit.org/content/groups/public</url>
</repository>
<repository>
<id>sonatype-nexus-releases</id>
<url>https://oss.sonatype.org/content/repositories/releases</url>
</repository>
<repository>
<id>sonatype-nexus-snapshots</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<dependencies>
<!-- WorldEdit -->
<dependency>
<groupId>com.sk89q</groupId>
<artifactId>worldedit</artifactId>
<version>5.6.3</version>
</dependency>
<!-- Bukkit -->
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>bukkit</artifactId>
<version>1.7.9-R0.2</version>
</dependency>
<!-- WorldGuard -->
<dependency>
<groupId>com.sk89q</groupId>
<artifactId>worldguard</artifactId>
<version>5.9</version>
</dependency>
</dependencies>
<distributionManagement>
<repository>
<id>majinnaibu-repository</id>
<url>ftp://repo.majinnaibu.com</url>
</repository>
</distributionManagement>
<build>
<extensions>
<!-- Enabling the use of FTP -->
<extension>
<groupId>org.apache.maven.wagon</groupId>
<artifactId>wagon-ftp</artifactId>
<version>1.0-beta-6</version>
</extension>
</extensions>
</build>
</project>

5
settings.gradle Normal file
View File

@@ -0,0 +1,5 @@
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
}
rootProject.name = 'Metropolis'

View File

@@ -0,0 +1,124 @@
package com.majinnaibu.bukkitplugins.metropolis;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.UUID;
import org.bukkit.configuration.file.YamlConfiguration;
import org.junit.jupiter.api.Test;
class MetropolisServerIntegrationTest {
@Test
void runningPaperLoadsMetropolis() throws IOException {
String installedPlugins;
try (RconClient rcon = connectToConfiguredServer()) {
installedPlugins = rcon.execute("plugins");
}
assertTrue(installedPlugins.contains("Metropolis"), installedPlugins);
}
@Test
void plotReservationIsPersistedByTheLoadedWorldGuardPlugin() throws IOException {
String regionId = "metropolis-it-" + UUID.randomUUID().toString().substring(0, 8);
try (RconClient rcon = connectToConfiguredServer()) {
String response = rcon.execute("metropolis-plot-reserve " + regionId + " 100 60 100 105 70 105");
assertTrue(response.contains("Reserved plot region " + regionId), response);
}
Path serverDirectory = Path.of(requiredEnvironment("METROPOLIS_SERVER_DIR"));
String worldName = System.getenv().getOrDefault("METROPOLIS_WORLD", "world");
Path regionsFile = serverDirectory.resolve("plugins/WorldGuard/worlds")
.resolve(worldName)
.resolve("regions.yml");
YamlConfiguration regions = YamlConfiguration.loadConfiguration(regionsFile.toFile());
assertTrue(regions.contains("regions." + regionId), "WorldGuard did not persist region " + regionId);
}
private static RconClient connectToConfiguredServer() throws IOException {
String host = requiredEnvironment("METROPOLIS_RCON_HOST");
int port = Integer.parseInt(requiredEnvironment("METROPOLIS_RCON_PORT"));
String password = requiredEnvironment("METROPOLIS_RCON_PASSWORD");
return new RconClient(host, port, password);
}
private static String requiredEnvironment(String name) {
String value = System.getenv(name);
if (value == null || value.isBlank()) {
throw new IllegalStateException("Set " + name + " to run integrationTest.");
}
return value;
}
private static final class RconClient implements AutoCloseable {
private final Socket socket = new Socket();
private final DataInputStream input;
private final DataOutputStream output;
private int requestId = 1;
private RconClient(String host, int port, String password) throws IOException {
socket.connect(new InetSocketAddress(host, port), 5000);
socket.setSoTimeout(5000);
input = new DataInputStream(socket.getInputStream());
output = new DataOutputStream(socket.getOutputStream());
int authId = requestId++;
Packet response = send(authId, 3, password);
if (response.id != authId) {
throw new IOException("RCON authentication failed.");
}
}
private String execute(String command) throws IOException {
return send(requestId++, 2, command).payload;
}
private Packet send(int id, int type, String payload) throws IOException {
byte[] data = payload.getBytes(StandardCharsets.UTF_8);
writeLittleEndianInt(data.length + 10);
writeLittleEndianInt(id);
writeLittleEndianInt(type);
output.write(data);
output.writeByte(0);
output.writeByte(0);
output.flush();
int length = readLittleEndianInt();
byte[] packet = input.readNBytes(length);
if (packet.length != length || length < 10) {
throw new IOException("Received an invalid RCON packet.");
}
int responseId = littleEndianInt(packet, 0);
String response = new String(packet, 8, length - 10, StandardCharsets.UTF_8);
return new Packet(responseId, response);
}
private void writeLittleEndianInt(int value) throws IOException {
output.writeInt(Integer.reverseBytes(value));
}
private int readLittleEndianInt() throws IOException {
return Integer.reverseBytes(input.readInt());
}
private static int littleEndianInt(byte[] bytes, int offset) {
return (bytes[offset] & 0xff)
| ((bytes[offset + 1] & 0xff) << 8)
| ((bytes[offset + 2] & 0xff) << 16)
| (bytes[offset + 3] << 24);
}
@Override
public void close() throws IOException {
socket.close();
}
}
private record Packet(int id, String payload) {}
}

View File

@@ -1,16 +1,11 @@
package com.majinnaibu.bukkitplugins.metropolis;
import java.util.logging.Logger;
import javax.persistence.Embeddable;
import org.bukkit.Location;
import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldedit.regions.Region;
import com.sk89q.worldedit.BlockVector;
import com.sk89q.worldedit.bukkit.selections.Selection;
@Embeddable
public class Cuboid implements Comparable<Cuboid> {
private int id;
@@ -39,13 +34,13 @@ public class Cuboid implements Comparable<Cuboid> {
this.maxZ = maxZ;
}
public Cuboid(BlockVector min, BlockVector max) {
this.minX = min.getBlockX();
this.minY = min.getBlockY();
this.minZ = min.getBlockZ();
this.maxX = max.getBlockX();
this.maxY = max.getBlockY();
this.maxZ = max.getBlockZ();
public Cuboid(BlockVector3 min, BlockVector3 max) {
this.minX = min.x();
this.minY = min.y();
this.minZ = min.z();
this.maxX = max.x();
this.maxY = max.y();
this.maxZ = max.z();
}
public Cuboid() {
@@ -57,79 +52,73 @@ public class Cuboid implements Comparable<Cuboid> {
this.maxZ = 0;
}
public Cuboid(Selection selection) {
this.minX = selection.getMinimumPoint().getBlockX();
this.minY = selection.getMinimumPoint().getBlockY();
this.minZ = selection.getMinimumPoint().getBlockZ();
this.maxX = selection.getMaximumPoint().getBlockX();
this.maxY = selection.getMaximumPoint().getBlockY();
this.maxZ = selection.getMaximumPoint().getBlockZ();
public Cuboid(Region selection) {
this.minX = selection.getMinimumPoint().x();
this.minY = selection.getMinimumPoint().y();
this.minZ = selection.getMinimumPoint().z();
this.maxX = selection.getMaximumPoint().x();
this.maxY = selection.getMaximumPoint().y();
this.maxZ = selection.getMaximumPoint().z();
}
public BlockVector getMin(){
return new BlockVector(minX, minY, minZ);
public BlockVector3 getMin(){
return BlockVector3.at(minX, minY, minZ);
}
public BlockVector getMax(){
return new BlockVector(maxX, maxY, maxZ);
public BlockVector3 getMax(){
return BlockVector3.at(maxX, maxY, maxZ);
}
@Override
public int compareTo(Cuboid o) {
BlockVector min = getMin();
BlockVector otherMin = o.getMin();
BlockVector3 min = getMin();
BlockVector3 otherMin = o.getMin();
if(min.getBlockX() < otherMin.getBlockX()){
if(min.x() < otherMin.x()){
return -1;
}else if(min.getBlockX() > otherMin.getBlockX()){
}else if(min.x() > otherMin.x()){
return 1;
}else if(min.getBlockZ() < otherMin.getBlockZ()){
}else if(min.z() < otherMin.z()){
return -1;
}else if(min.getBlockZ() > otherMin.getBlockZ()){
}else if(min.z() > otherMin.z()){
return 1;
}else if(min.getBlockY() < otherMin.getBlockY()){
}else if(min.y() < otherMin.y()){
return -1;
}else if(min.getBlockY() > otherMin.getBlockY()){
}else if(min.y() > otherMin.y()){
return 1;
}else{
return 0;
}
}
public static int compareBlockVectors(BlockVector v1, BlockVector v2){
Logger log = Logger.getLogger("Minecraft");
public static int compareBlockVectors(BlockVector3 v1, BlockVector3 v2){
if(v1 == null){
if(v2 == null){
log.info("in Cuboid.compareBlockVectors v1 and v2 are null");
return 0;
}else{
log.info("in Cubiod.compareBlockVectors v1 is null");
return -1;
}
}else if(v2 == null){
log.info("in Cubiod.compareBlockVectors v2 is null");
return 1;
}
log.info(String.format("v1.x: %d, v1.y: %d, v1.z: %d, v2.x: %d, v2.y: %d, v2.z: %d", v1.getBlockX(), v1.getBlockY(), v1.getBlockZ(), v2.getBlockX(), v2.getBlockY(), v2.getBlockZ()));
if(v1.getBlockX() < v2.getBlockX()){
if(v1.x() < v2.x()){
return -1;
}else if(v1.getBlockX() > v2.getBlockX()){
}else if(v1.x() > v2.x()){
return 1;
}else if(v1.getBlockZ() < v2.getBlockZ()){
}else if(v1.z() < v2.z()){
return -1;
}else if(v1.getBlockZ() > v2.getBlockZ()){
}else if(v1.z() > v2.z()){
return 1;
}else if(v1.getBlockY() < v2.getBlockY()){
}else if(v1.y() < v2.y()){
return -1;
}else if(v1.getBlockY() > v2.getBlockY()){
}else if(v1.y() > v2.y()){
return 1;
}else{
return 0;
}
}
public static boolean isBlockLessThan(BlockVector v1, BlockVector v2) {
public static boolean isBlockLessThan(BlockVector3 v1, BlockVector3 v2) {
return compareBlockVectors(v1, v2) < 0;
}

View File

@@ -0,0 +1,50 @@
package com.majinnaibu.bukkitplugins.metropolis;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.function.Function;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration;
final class CurrentHomesStore {
private CurrentHomesStore() {}
static Map<UUID, Integer> load(File file, Function<String, UUID> legacyOwnerResolver)
throws IOException, InvalidConfigurationException {
Map<UUID, Integer> homesByOwner = new HashMap<>();
if (!file.exists()) {
return homesByOwner;
}
YamlConfiguration yaml = new YamlConfiguration();
yaml.load(file);
for (String ownerKey : yaml.getKeys(false)) {
UUID ownerId;
try {
ownerId = UUID.fromString(ownerKey);
} catch (IllegalArgumentException ex) {
ownerId = legacyOwnerResolver.apply(ownerKey);
}
int homeNumber = yaml.getInt(ownerKey, 0);
if (ownerId != null && homeNumber > 0) {
homesByOwner.put(ownerId, homeNumber);
}
}
return homesByOwner;
}
static void save(File file, Map<UUID, Integer> homesByOwner) throws IOException {
File parent = file.getParentFile();
if (parent != null) {
parent.mkdirs();
}
YamlConfiguration yaml = new YamlConfiguration();
homesByOwner.forEach((ownerId, homeNumber) -> yaml.set(ownerId.toString(), homeNumber));
yaml.save(file);
}
}

View File

@@ -7,7 +7,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;
import java.util.UUID;
import org.bukkit.Location;
import org.bukkit.Material;
@@ -31,6 +31,7 @@ import com.majinnaibu.bukkitplugins.metropolis.commands.MetropolisCommand;
import com.majinnaibu.bukkitplugins.metropolis.commands.MetropolisDebugGenerateTestHomesCommand;
import com.majinnaibu.bukkitplugins.metropolis.commands.MetropolisFlagResetCommand;
import com.majinnaibu.bukkitplugins.metropolis.commands.MetropolisHomeEvictCommand;
import com.majinnaibu.bukkitplugins.metropolis.commands.MetropolisHomeAcquire;
import com.majinnaibu.bukkitplugins.metropolis.commands.MetropolisHomeGenerateCommand;
import com.majinnaibu.bukkitplugins.metropolis.commands.MetropolisHomeGoCommand;
import com.majinnaibu.bukkitplugins.metropolis.commands.MetropolisHomeListCommand;
@@ -38,12 +39,13 @@ import com.majinnaibu.bukkitplugins.metropolis.commands.MetropolisHomeMoveComman
import com.majinnaibu.bukkitplugins.metropolis.commands.MetropolisPlotGoCommand;
import com.majinnaibu.bukkitplugins.metropolis.commands.MetropolisPlotReserveCommand;
import com.majinnaibu.bukkitplugins.metropolis.eventlisteners.PlayerJoinListener;
import com.sk89q.util.yaml.YAMLProcessor;
import com.sk89q.worldedit.BlockVector;
import com.sk89q.worldedit.bukkit.BukkitAdapter;
import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldedit.bukkit.WorldEditPlugin;
import com.sk89q.worldguard.WorldGuard;
import com.sk89q.worldguard.bukkit.WorldGuardPlugin;
import com.sk89q.worldguard.domains.DefaultDomain;
import com.sk89q.worldguard.protection.flags.DefaultFlag;
import com.sk89q.worldguard.protection.flags.Flags;
import com.sk89q.worldguard.protection.flags.StateFlag;
import com.sk89q.worldguard.protection.managers.RegionManager;
import com.sk89q.worldguard.protection.regions.ProtectedCuboidRegion;
@@ -51,7 +53,6 @@ import com.sk89q.worldguard.protection.regions.ProtectedRegion;
public class MetropolisPlugin extends JavaPlugin {
public static final boolean DEBUG = false;
public static final Logger log=Logger.getLogger("Minecraft");
private static final int version = 1;
public static final int ROAD_NORTH=1;
@@ -66,9 +67,9 @@ public class MetropolisPlugin extends JavaPlugin {
public RegionManager regionManager = null;
private List<Plot> _occupiedPlots;
private HashMap<String, List<Plot>> _ownedPlots;
private HashMap<String, UserOverride> _userOverrides;
private HashMap<String, Integer> _currentHomes;
private HashMap<UUID, List<Plot>> _ownedPlots;
private HashMap<UUID, UserOverride> _userOverrides;
private HashMap<UUID, Integer> _currentHomes;
private PlayerJoinListener _playerJoinListener = null;
@@ -88,7 +89,7 @@ public class MetropolisPlugin extends JavaPlugin {
private Material roadSupportMaterial = Material.STONE;
private String worldName = "world";
private boolean generateFloor = false;
private Material floorMaterial = Material.GRASS;
private Material floorMaterial = Material.GRASS_BLOCK;
private int spaceAboveFloor = 2;
private boolean generateSign = false;
private boolean generateSpawn = true;
@@ -110,33 +111,33 @@ public class MetropolisPlugin extends JavaPlugin {
@Override
public void onDisable() {
log.info(String.format("%s disabled", pdf.getFullName()));
getLogger().info(String.format("%s disabled", pdf.getFullName()));
}
@Override
public void onEnable() {
pdf = getDescription();
_ownedPlots = new HashMap<String, List<Plot>>();
_userOverrides = new HashMap<String, UserOverride>();
_currentHomes = new HashMap<String, Integer>();
_ownedPlots = new HashMap<UUID, List<Plot>>();
_userOverrides = new HashMap<UUID, UserOverride>();
_currentHomes = new HashMap<UUID, Integer>();
loadCurrentHomes();
if(DEBUG){log.info("Checking config");}
if(DEBUG){getLogger().info("Checking config");}
Configuration config = getConfig();
if(!config.contains("version")){
//new
if(DEBUG){log.info("No config exists. Assuming new installation.");}
if(DEBUG){getLogger().info("No config exists. Assuming new installation.");}
}else{
int configVersion = safeGetIntFromConfig(config, "version");
if(configVersion < version){
if(DEBUG){log.info(String.format("Updating config from version v%s to v%s.", configVersion, version));}
if(DEBUG){getLogger().info(String.format("Updating config from version v%s to v%s.", configVersion, version));}
if(configVersion != version){
//upgrade config
config.set("version", version);
}
saveConfig();
if(DEBUG){log.info("Config updated");}
if(DEBUG){getLogger().info("Config updated");}
}
}
@@ -145,7 +146,7 @@ public class MetropolisPlugin extends JavaPlugin {
config.options().copyDefaults(true);
if(DEBUG){log.info("Reading configuration from file.");}
if(DEBUG){getLogger().info("Reading configuration from file.");}
plotSizeX = safeGetIntFromConfig(config, "plot.sizeX");
plotSizeZ = safeGetIntFromConfig(config, "plot.sizeZ");
generateFloor = safeGetBooleanFromConfig(config, "plot.floor.generate");
@@ -165,7 +166,7 @@ public class MetropolisPlugin extends JavaPlugin {
spawnFloorMaterial = safeGetMaterialFromConfig(config, "spawn.material");
generateWall = safeGetBooleanFromConfig(config, "wall.generate");
wallMaterial = safeGetMaterialFromConfig(config, "wall.material");
wallHeight = safeGetIntFromConfig(config, "wall.material");
wallHeight = safeGetIntFromConfig(config, "wall.height");
worldName = safeGetStringFromConfig(config, "worldname");
_maxPlots = safeGetIntFromConfig(config, "plot.multiplier");
_plotMultiplier = safeGetIntFromConfig(config, "plot.maxPerPlayer");
@@ -173,9 +174,9 @@ public class MetropolisPlugin extends JavaPlugin {
buildUserOverrides();
saveConfig();
if(DEBUG){log.info("Done reading config.");}
if(DEBUG){getLogger().info("Done reading config.");}
log.info(String.format("Metropolis: world name is %s", worldName));
getLogger().info(String.format("Metropolis: world name is %s", worldName));
Server server = getServer();
if(server == null){
@@ -208,7 +209,7 @@ public class MetropolisPlugin extends JavaPlugin {
gridSizeY = world.getMaxHeight();
gridSizeZ = plotSizeZ + roadWidth;
regionManager = worldGuard.getRegionManager(world);
regionManager = WorldGuard.getInstance().getPlatform().getRegionContainer().get(BukkitAdapter.adapt(world));
if(regionManager == null){
throw new RuntimeException("WorldGuard regions don't seem to be enabled.");
}
@@ -217,15 +218,15 @@ public class MetropolisPlugin extends JavaPlugin {
if(_cityRegion == null){
_cityRegion = new ProtectedCuboidRegion("City", getPlotMin(0, 0, 1), this.getPlotMax(0, 0, 1));
_cityRegion.setPriority(0);
_cityRegion.setFlag(DefaultFlag.PVP, StateFlag.State.DENY);
_cityRegion.setFlag(DefaultFlag.MOB_DAMAGE, StateFlag.State.DENY);
_cityRegion.setFlag(DefaultFlag.MOB_SPAWNING, StateFlag.State.DENY);
_cityRegion.setFlag(DefaultFlag.CREEPER_EXPLOSION, StateFlag.State.DENY);
_cityRegion.setFlag(DefaultFlag.ENDER_BUILD, StateFlag.State.DENY);
_cityRegion.setFlag(DefaultFlag.GHAST_FIREBALL, StateFlag.State.DENY);
_cityRegion.setFlag(DefaultFlag.TNT, StateFlag.State.DENY);
_cityRegion.setFlag(DefaultFlag.LAVA_FLOW, StateFlag.State.DENY);
_cityRegion.setFlag(DefaultFlag.SNOW_FALL, StateFlag.State.DENY);
_cityRegion.setFlag(Flags.PVP, StateFlag.State.DENY);
_cityRegion.setFlag(Flags.MOB_DAMAGE, StateFlag.State.DENY);
_cityRegion.setFlag(Flags.MOB_SPAWNING, StateFlag.State.DENY);
_cityRegion.setFlag(Flags.CREEPER_EXPLOSION, StateFlag.State.DENY);
_cityRegion.setFlag(Flags.ENDER_BUILD, StateFlag.State.DENY);
_cityRegion.setFlag(Flags.GHAST_FIREBALL, StateFlag.State.DENY);
_cityRegion.setFlag(Flags.TNT, StateFlag.State.DENY);
_cityRegion.setFlag(Flags.LAVA_FLOW, StateFlag.State.DENY);
_cityRegion.setFlag(Flags.SNOW_FALL, StateFlag.State.DENY);
regionManager.addRegion(_cityRegion);
}
@@ -235,15 +236,15 @@ public class MetropolisPlugin extends JavaPlugin {
if(_spawnRegion == null){
_spawnRegion = new ProtectedCuboidRegion("Spawn", getPlotMin(0, 0, 1), getPlotMax(0, 0, 1));
_spawnRegion.setPriority(1);
_spawnRegion.setFlag(DefaultFlag.PVP, StateFlag.State.DENY);
_spawnRegion.setFlag(DefaultFlag.MOB_DAMAGE, StateFlag.State.DENY);
_spawnRegion.setFlag(DefaultFlag.MOB_SPAWNING, StateFlag.State.DENY);
_spawnRegion.setFlag(DefaultFlag.CREEPER_EXPLOSION, StateFlag.State.DENY);
_spawnRegion.setFlag(DefaultFlag.ENDER_BUILD, StateFlag.State.DENY);
_spawnRegion.setFlag(DefaultFlag.GHAST_FIREBALL, StateFlag.State.DENY);
_spawnRegion.setFlag(DefaultFlag.TNT, StateFlag.State.DENY);
_spawnRegion.setFlag(DefaultFlag.LAVA_FLOW, StateFlag.State.DENY);
_spawnRegion.setFlag(DefaultFlag.SNOW_FALL, StateFlag.State.DENY);
_spawnRegion.setFlag(Flags.PVP, StateFlag.State.DENY);
_spawnRegion.setFlag(Flags.MOB_DAMAGE, StateFlag.State.DENY);
_spawnRegion.setFlag(Flags.MOB_SPAWNING, StateFlag.State.DENY);
_spawnRegion.setFlag(Flags.CREEPER_EXPLOSION, StateFlag.State.DENY);
_spawnRegion.setFlag(Flags.ENDER_BUILD, StateFlag.State.DENY);
_spawnRegion.setFlag(Flags.GHAST_FIREBALL, StateFlag.State.DENY);
_spawnRegion.setFlag(Flags.TNT, StateFlag.State.DENY);
_spawnRegion.setFlag(Flags.LAVA_FLOW, StateFlag.State.DENY);
_spawnRegion.setFlag(Flags.SNOW_FALL, StateFlag.State.DENY);
regionManager.addRegion(_spawnRegion);
_spawnCuboid = new Cuboid(_spawnRegion.getMinimumPoint(), _spawnRegion.getMaximumPoint());
@@ -257,29 +258,28 @@ public class MetropolisPlugin extends JavaPlugin {
if(DEBUG){
/*
log.info("Metropolis: first 25 plots");
getLogger().info("Metropolis: first 25 plots");
int n = 5;
for (int ix=-n; ix<=n; ix++){
for (int iz=-n; iz<=n; iz++){
log.info(getCuboid(iz, ix).toString());
getLogger().info(getCuboid(iz, ix).toString());
}
}
*/
log.info(String.format("roadWidth = %d", roadWidth));
getLogger().info(String.format("roadWidth = %d", roadWidth));
}
_occupiedPlots = new ArrayList<Plot>();
fillOccupiedPlots();
resizeCityRegion();
if(_playerJoinListener == null){
_playerJoinListener = new PlayerJoinListener(this);
}
_playerJoinListener = new PlayerJoinListener(this);
getServer().getPluginManager().registerEvents(_playerJoinListener, this);
log.info(String.format("%s enabled", pdf.getFullName()));
getLogger().info(String.format("%s enabled", pdf.getFullName()));
RegisterCommandHandler("metropolis", new MetropolisCommand(this));
@@ -288,6 +288,7 @@ public class MetropolisPlugin extends JavaPlugin {
RegisterCommandHandler("metropolis-flag-reset", new MetropolisFlagResetCommand(this));
RegisterCommandHandler("metropolis-home-evict", new MetropolisHomeEvictCommand(this));
RegisterCommandHandler("metropolis-home-acquire", new MetropolisHomeAcquire(this));
RegisterCommandHandler("metropolis-home-generate", new MetropolisHomeGenerateCommand(this));
RegisterCommandHandler("metropolis-home-go", new MetropolisHomeGoCommand(this));
RegisterCommandHandler("metropolis-home-list", new MetropolisHomeListCommand(this));
@@ -298,19 +299,14 @@ public class MetropolisPlugin extends JavaPlugin {
}
private void loadCurrentHomes() {
YAMLProcessor processor = new YAMLProcessor(new File(getDataFolder(), "currentHomes.yml"), true);
File homesFile = new File(getDataFolder(), "currentHomes.yml");
try {
processor.load();
} catch (IOException e) {
log.info(e.toString());
return;
}
Set<String> keys = processor.getMap().keySet();
_currentHomes.clear();
for(String username : keys){
_currentHomes.put(username, processor.getInt(username, 0));
_currentHomes.putAll(CurrentHomesStore.load(
homesFile,
ownerName -> getServer().getOfflinePlayer(ownerName).getUniqueId()));
saveCurrentHomes();
} catch (IOException | org.bukkit.configuration.InvalidConfigurationException e) {
getLogger().log(java.util.logging.Level.SEVERE, "Unable to load currentHomes.yml", e);
}
}
@@ -345,8 +341,9 @@ public class MetropolisPlugin extends JavaPlugin {
}
}
UUID playerId = getServer().getOfflinePlayer(username).getUniqueId();
UserOverride override = new UserOverride(username, plotMultiplier, maxPlots);
_userOverrides.put(username, override);
_userOverrides.put(playerId, override);
}
}
}
@@ -354,8 +351,8 @@ public class MetropolisPlugin extends JavaPlugin {
private Cuboid getCuboid(int row, int col) {
//This is only used for debug info
BlockVector min = getPlotMin(row, col, 1);
BlockVector max = getPlotMax(row, col, 1);
BlockVector3 min = getPlotMin(row, col, 1);
BlockVector3 max = getPlotMax(row, col, 1);
return new Cuboid(min, max);
}
@@ -398,24 +395,30 @@ public class MetropolisPlugin extends JavaPlugin {
private Material safeGetMaterialFromConfig(Configuration config, String name){
Material material = null;
if(config.isInt(name)){
material = Material.getMaterial(config.getInt(name));
material = switch (config.getInt(name)) {
case 1 -> Material.STONE;
case 2 -> Material.GRASS_BLOCK;
case 4 -> Material.COBBLESTONE;
case 7 -> Material.BEDROCK;
default -> null;
};
}else if(config.isString(name)){
material = Material.getMaterial(config.getString(name));
if(material== null){
material = Material.matchMaterial(config.getString(name));
}
material = Material.matchMaterial(config.getString(name));
}
if (material == null) {
getLogger().severe("Invalid material configured at " + name);
throwInvalidConfigException();
}
return material;
}
private void throwInvalidConfigException() {
log.info("Metropolis: ERROR config file is invalid. Please correct Metropolis/config.yml and restart the server.");
getLogger().info("Metropolis: ERROR config file is invalid. Please correct Metropolis/config.yml and restart the server.");
throw new RuntimeException("Config file is invalid.");
}
private void setupSpawn() {
log.info("Metropolis: Spawn Cuboid is " + _spawnCuboid.toString());
getLogger().info("Metropolis: Spawn Cuboid is " + _spawnCuboid.toString());
if(generateSpawn){
int x= 0;
@@ -454,12 +457,12 @@ public class MetropolisPlugin extends JavaPlugin {
ProtectedCuboidRegion cuboidRegion = (ProtectedCuboidRegion) region;
if(cuboidRegion.getId().startsWith("h_")){
PlayerHome home = PlayerHome.get(region);
if(!_currentHomes.containsKey(home.getPlayerName()))
if(!_currentHomes.containsKey(home.getPlayerId()))
{
_currentHomes.put(home.getPlayerName(), home.getNumber());
_currentHomes.put(home.getPlayerId(), home.getNumber());
}
_occupiedPlots.add(home);
addOwnedPlot(home.getPlayerName(), home);
addOwnedPlot(home.getPlayerId(), home);
}else if(cuboidRegion.getId().startsWith("r_")){
_occupiedPlots.add(Plot.get(cuboidRegion));
}
@@ -469,14 +472,14 @@ public class MetropolisPlugin extends JavaPlugin {
size=calculateCitySize();
}
private void addOwnedPlot(String substring, Plot plot) {
if(_ownedPlots.containsKey(substring)){
List<Plot> plots = _ownedPlots.get(substring);
private void addOwnedPlot(UUID playerId, Plot plot) {
if(_ownedPlots.containsKey(playerId)){
List<Plot> plots = _ownedPlots.get(playerId);
plots.add(plot);
}else{
List<Plot> plots = new ArrayList<Plot>();
plots.add(plot);
_ownedPlots.put(substring, plots);
_ownedPlots.put(playerId, plots);
}
}
@@ -488,16 +491,23 @@ public class MetropolisPlugin extends JavaPlugin {
public PlayerHome getPlayerHome(Player player) {
PlayerHome home = null;
String regionName = "h_" + player.getName();
int homeNumber = _currentHomes.getOrDefault(player.getUniqueId(), 1);
String regionName = String.format("h_%d_%s", homeNumber, player.getUniqueId());
ProtectedRegion homeRegion = regionManager.getRegion(regionName);
if(homeRegion == null){
if(DEBUG){
log.info(String.format("Creating home for player %s", player.getName()));
PlayerHome existingHome = getOwnedHome(player.getUniqueId(), homeNumber);
if (existingHome != null) {
existingHome.setPlayerName(player.getName());
return existingHome;
}
home = generateHome(player.getName());
if(DEBUG){
getLogger().info(String.format("Creating home for player %s", player.getName()));
}
home = generateHome(player);
}else{
home = new PlayerHome(homeRegion);
home.setPlayerName(player.getName());
}
return home;
@@ -527,7 +537,7 @@ public class MetropolisPlugin extends JavaPlugin {
}
private void setFloor(int x, int y, int z) {
//if(DEBUG){log.info(String.format("setting road at (%d, %d, %d)", x, y, z));}
//if(DEBUG){getLogger().info(String.format("setting road at (%d, %d, %d)", x, y, z));}
Block block = world.getBlockAt(x, y, z);
@@ -551,7 +561,7 @@ public class MetropolisPlugin extends JavaPlugin {
if(plotCuboid == null){
if(DEBUG){
log.warning("plotCuboid is null");
getLogger().warning("plotCuboid is null");
}
return;
}
@@ -638,7 +648,7 @@ public class MetropolisPlugin extends JavaPlugin {
}
private void setRoad(int x, int y, int z) {
//if(DEBUG){log.info(String.format("setting road at (%d, %d, %d)", x, y, z));}
//if(DEBUG){getLogger().info(String.format("setting road at (%d, %d, %d)", x, y, z));}
Block block = world.getBlockAt(x, y, z);
//Set the road block
@@ -665,8 +675,7 @@ public class MetropolisPlugin extends JavaPlugin {
material != Material.WATER &&
material != Material.LAVA &&
material != Material.TORCH &&
material != Material.REDSTONE_TORCH_OFF &&
material != Material.REDSTONE_TORCH_ON;
material != Material.REDSTONE_TORCH;
}
private boolean isPhysicsMaterial(Material material) {
@@ -716,7 +725,7 @@ public class MetropolisPlugin extends JavaPlugin {
//Top
for(col = min; col <= max; col++){
if(!areBlocksOccupied(row, col, plotMultiplier)){
if(DEBUG){log.info(String.format("row: %d, col: %d", row, col));}
if(DEBUG){getLogger().info(String.format("row: %d, col: %d", row, col));}
return new Cuboid(getPlotMin(row, col, plotMultiplier), getPlotMax(row, col, plotMultiplier));
}
}
@@ -725,7 +734,7 @@ public class MetropolisPlugin extends JavaPlugin {
col = max;
for(row=min + 1; row < max; row++){
if(!areBlocksOccupied(row, col, plotMultiplier)){
if(DEBUG){log.info(String.format("row: %d, col: %d", row, col));}
if(DEBUG){getLogger().info(String.format("row: %d, col: %d", row, col));}
return new Cuboid(getPlotMin(row, col, plotMultiplier), getPlotMax(row, col, plotMultiplier));
}
}
@@ -734,7 +743,7 @@ public class MetropolisPlugin extends JavaPlugin {
row = max;
for(col = max; col >= min; col--){
if(!areBlocksOccupied(row, col, plotMultiplier)){
if(DEBUG){log.info(String.format("row: %d, col: %d", row, col));}
if(DEBUG){getLogger().info(String.format("row: %d, col: %d", row, col));}
return new Cuboid(getPlotMin(row, col, plotMultiplier), getPlotMax(row, col, plotMultiplier));
}
}
@@ -744,7 +753,7 @@ public class MetropolisPlugin extends JavaPlugin {
for(row = max; row > min; row--){
if(!areBlocksOccupied(row, col, plotMultiplier)){
if(row != 0 || col != 0){
if(DEBUG){log.info(String.format("row: %d, col: %d", row, col));}
if(DEBUG){getLogger().info(String.format("row: %d, col: %d", row, col));}
return new Cuboid(getPlotMin(row, col, plotMultiplier), getPlotMax(row, col, plotMultiplier));
}
}
@@ -755,7 +764,7 @@ public class MetropolisPlugin extends JavaPlugin {
max = ring - (plotMultiplier-1);
}
if(DEBUG){log.info(String.format("row: %d, col: %d", row, col));}
if(DEBUG){getLogger().info(String.format("row: %d, col: %d", row, col));}
return new Cuboid(getPlotMin(row, col, plotMultiplier), getPlotMax(row, col, plotMultiplier));
}
@@ -765,14 +774,19 @@ public class MetropolisPlugin extends JavaPlugin {
if(cityRegion instanceof ProtectedCuboidRegion){
ProtectedCuboidRegion region = (ProtectedCuboidRegion)cityRegion;
BlockVector min;
BlockVector max;
BlockVector3 min;
BlockVector3 max;
min = getPlotMin(-size/2, -size/2, 1);
max = getPlotMax(size/2, size/2, 1);
region.setMinimumPoint(min);
region.setMaximumPoint(max);
ProtectedCuboidRegion resizedRegion = new ProtectedCuboidRegion(region.getId(), min, max);
resizedRegion.copyFrom(region);
regionManager.removeRegion(region.getId());
regionManager.addRegion(resizedRegion);
_cityRegion = resizedRegion;
_cityCuboid = new Cuboid(min, max);
saveRegions();
}
}
@@ -782,43 +796,43 @@ public class MetropolisPlugin extends JavaPlugin {
for(Plot home: _occupiedPlots){
int plotCol=Math.abs(getPlotXFromMin(home.getCuboid()));
int plotRow=Math.abs(getPlotZFromMin(home.getCuboid()));
if(DEBUG){log.info(String.format("col: %d, row: %d, iSize: %d", plotCol, plotRow, iSize));}
if(DEBUG){getLogger().info(String.format("col: %d, row: %d, iSize: %d", plotCol, plotRow, iSize));}
iSize = Math.max(Math.max(plotRow*2+1, plotCol*2+1), iSize);
}
if(DEBUG){log.info(String.format("City size is %d", iSize));}
if(DEBUG){getLogger().info(String.format("City size is %d", iSize));}
return iSize;
}
public BlockVector getPlotMin(int row, int col, int plotMultiplier){
BlockVector gridMin = getGridMin(row, col, plotMultiplier);
public BlockVector3 getPlotMin(int row, int col, int plotMultiplier){
BlockVector3 gridMin = getGridMin(row, col, plotMultiplier);
BlockVector bv = new BlockVector(gridMin.getBlockX() + roadWidth/2, gridMin.getBlockY(), gridMin.getBlockZ() + roadWidth/2);
log.info(String.format("getPlotMin (%d, %d, %d)", bv.getBlockX(), bv.getBlockY(), bv.getBlockZ()));
BlockVector3 bv = BlockVector3.at(gridMin.x() + roadWidth/2, gridMin.y(), gridMin.z() + roadWidth/2);
getLogger().info(String.format("getPlotMin (%d, %d, %d)", bv.x(), bv.y(), bv.z()));
return bv;
}
public BlockVector getPlotMax(int row, int col, int plotMultiplier){
BlockVector gridMax = getGridMax(row, col, plotMultiplier);
public BlockVector3 getPlotMax(int row, int col, int plotMultiplier){
BlockVector3 gridMax = getGridMax(row, col, plotMultiplier);
BlockVector bv = new BlockVector(gridMax.getBlockX() - (roadWidth - roadWidth/2), gridMax.getBlockY(), gridMax.getBlockZ() - (roadWidth-roadWidth/2));
log.info(String.format("getPlotMax (%d, %d, %d)", bv.getBlockX(), bv.getBlockY(), bv.getBlockZ()));
BlockVector3 bv = BlockVector3.at(gridMax.x() - (roadWidth - roadWidth/2), gridMax.y(), gridMax.z() - (roadWidth-roadWidth/2));
getLogger().info(String.format("getPlotMax (%d, %d, %d)", bv.x(), bv.y(), bv.z()));
return bv;
}
public BlockVector getGridMin(int row, int col, int plotMultiplier){
public BlockVector3 getGridMin(int row, int col, int plotMultiplier){
int level = 0;
BlockVector bv = new BlockVector(col * gridSizeX, level * gridSizeY, row * gridSizeZ);
log.info(String.format("getGridMin (%d, %d, %d)", bv.getBlockX(), bv.getBlockY(), bv.getBlockZ()));
BlockVector3 bv = BlockVector3.at(col * gridSizeX, level * gridSizeY, row * gridSizeZ);
getLogger().info(String.format("getGridMin (%d, %d, %d)", bv.x(), bv.y(), bv.z()));
return bv;
}
public BlockVector getGridMax(int row, int col, int plotMultiplier){
public BlockVector3 getGridMax(int row, int col, int plotMultiplier){
int level = 0;
BlockVector bv = new BlockVector((col+plotMultiplier) * gridSizeX*plotMultiplier-1, (level+1/*plotMultiplier*/) * gridSizeY-1, (row+plotMultiplier) * gridSizeZ-1);
log.info(String.format("getGridMax (%d, %d, %d)", bv.getBlockX(), bv.getBlockY(), bv.getBlockZ()));
BlockVector3 bv = BlockVector3.at((col+plotMultiplier) * gridSizeX*plotMultiplier-1, (level+1/*plotMultiplier*/) * gridSizeY-1, (row+plotMultiplier) * gridSizeZ-1);
getLogger().info(String.format("getGridMax (%d, %d, %d)", bv.x(), bv.y(), bv.z()));
return bv;
}
@@ -830,68 +844,81 @@ public class MetropolisPlugin extends JavaPlugin {
return (cuboid.minZ - roadWidth/2)/gridSizeZ;
}
private void setHomeOccupied(String owner, BlockVector minimumPoint, BlockVector maximumPoint) {
PlayerHome home = new PlayerHome(owner, minimumPoint, maximumPoint);
private void setHomeOccupied(UUID ownerId, String ownerName, int homeNumber, BlockVector3 minimumPoint, BlockVector3 maximumPoint) {
PlayerHome home = new PlayerHome(ownerId, ownerName, homeNumber, minimumPoint, maximumPoint);
if(!_occupiedPlots.contains(home)){
_occupiedPlots.add(home);
addOwnedPlot(ownerId, home);
}
}
public PlayerHome generateHome(String playerName) {
int multiplier = getPlotMultiplier(playerName);
return generateHome(getServer().getOfflinePlayer(playerName));
}
if(DEBUG){log.info(String.format("Generating home for %s", playerName));}
public PlayerHome generateHome(OfflinePlayer player) {
UUID playerId = player.getUniqueId();
String playerName = player.getName() == null ? playerId.toString() : player.getName();
int homeNumber = _currentHomes.getOrDefault(playerId, 1);
int multiplier = getPlotMultiplier(playerId);
if(DEBUG){getLogger().info(String.format("Generating home for %s", playerName));}
Cuboid homeCuboid = null;
ProtectedRegion phomeRegion = null;
String regionName = "h_1_" + playerName;
String regionName = getHomeRegionName(playerId, homeNumber);
phomeRegion = regionManager.getRegion(regionName);
if(phomeRegion != null){
return PlayerHome.get(phomeRegion);
}
PlayerHome existingHome = getOwnedHome(playerId, homeNumber);
if (existingHome != null) {
return existingHome;
}
homeCuboid = findNextUnownedHomeRegion(multiplier);
log.info("Metropolis Generating home in " + homeCuboid.toString());
getLogger().info("Metropolis Generating home in " + homeCuboid.toString());
ProtectedCuboidRegion newHomeRegion = new ProtectedCuboidRegion(regionName, homeCuboid.getMin(), homeCuboid.getMax());
newHomeRegion.setFlag(DefaultFlag.PVP, StateFlag.State.DENY);
newHomeRegion.setFlag(DefaultFlag.MOB_DAMAGE, StateFlag.State.DENY);
newHomeRegion.setFlag(DefaultFlag.MOB_SPAWNING, StateFlag.State.DENY);
newHomeRegion.setFlag(DefaultFlag.CREEPER_EXPLOSION, StateFlag.State.DENY);
newHomeRegion.setFlag(DefaultFlag.ENDER_BUILD, StateFlag.State.DENY);
newHomeRegion.setFlag(DefaultFlag.GHAST_FIREBALL, StateFlag.State.DENY);
newHomeRegion.setFlag(DefaultFlag.TNT, StateFlag.State.DENY);
newHomeRegion.setFlag(Flags.PVP, StateFlag.State.DENY);
newHomeRegion.setFlag(Flags.MOB_DAMAGE, StateFlag.State.DENY);
newHomeRegion.setFlag(Flags.MOB_SPAWNING, StateFlag.State.DENY);
newHomeRegion.setFlag(Flags.CREEPER_EXPLOSION, StateFlag.State.DENY);
newHomeRegion.setFlag(Flags.ENDER_BUILD, StateFlag.State.DENY);
newHomeRegion.setFlag(Flags.GHAST_FIREBALL, StateFlag.State.DENY);
newHomeRegion.setFlag(Flags.TNT, StateFlag.State.DENY);
DefaultDomain d = newHomeRegion.getOwners();
d.addPlayer(playerName);
d.addPlayer(playerId);
newHomeRegion.setPriority(1);
regionManager.addRegion(newHomeRegion);
try {
regionManager.save();
} catch (Exception e) {
log.info("Metropolis: ERROR Problem saving region");
getLogger().info("Metropolis: ERROR Problem saving region");
e.printStackTrace();
}
try {
regionManager.save();
} catch (Exception e) {
log.info("Metropolis: ERROR Problem saving region");
getLogger().info("Metropolis: ERROR Problem saving region");
e.printStackTrace();
}
log.info(String.format(
getLogger().info(String.format(
"New home region (%d, %d, %d) (%d, %d, %d)",
newHomeRegion.getMinimumPoint().getBlockX(),
newHomeRegion.getMinimumPoint().getBlockY(),
newHomeRegion.getMinimumPoint().getBlockZ(),
newHomeRegion.getMaximumPoint().getBlockX(),
newHomeRegion.getMaximumPoint().getBlockY(),
newHomeRegion.getMaximumPoint().getBlockZ()
newHomeRegion.getMinimumPoint().x(),
newHomeRegion.getMinimumPoint().y(),
newHomeRegion.getMinimumPoint().z(),
newHomeRegion.getMaximumPoint().x(),
newHomeRegion.getMaximumPoint().y(),
newHomeRegion.getMaximumPoint().z()
));
setHomeOccupied(playerName, newHomeRegion.getMinimumPoint(), newHomeRegion.getMaximumPoint());
setHomeOccupied(playerId, playerName, homeNumber, newHomeRegion.getMinimumPoint(), newHomeRegion.getMaximumPoint());
_currentHomes.putIfAbsent(playerId, homeNumber);
saveCurrentHomes();
createRoads(homeCuboid);
@@ -899,19 +926,21 @@ public class MetropolisPlugin extends JavaPlugin {
generateFloor(homeCuboid);
}
if(DEBUG){log.info(String.format("generateSign: %s", String.valueOf(generateSign)));}
if(DEBUG){getLogger().info(String.format("generateSign: %s", String.valueOf(generateSign)));}
if(generateSign){
generateSign(homeCuboid, playerName);
}
if(DEBUG){log.info(String.format("Done generating home for %s", playerName));}
if(DEBUG){getLogger().info(String.format("Done generating home for %s", playerName));}
return new PlayerHome(newHomeRegion);
PlayerHome home = new PlayerHome(newHomeRegion);
home.setPlayerName(playerName);
return home;
}
private void generateSign(Cuboid plotCuboid, String playerName) {
Block signBlock = world.getBlockAt(plotCuboid.getCenterX(), roadLevel+1, plotCuboid.getCenterZ());
signBlock.setType(Material.SIGN_POST);
signBlock.setType(Material.OAK_SIGN);
Sign sign = (Sign)signBlock.getState();
sign.setLine(0, "Home of");
@@ -936,18 +965,19 @@ public class MetropolisPlugin extends JavaPlugin {
public void reserveCuboid(String regionName, Cuboid cuboid) {
ProtectedCuboidRegion reservedRegion = new ProtectedCuboidRegion(regionName, cuboid.getMin(), cuboid.getMax());
reservedRegion.setFlag(DefaultFlag.PVP, StateFlag.State.DENY);
reservedRegion.setFlag(DefaultFlag.MOB_DAMAGE, StateFlag.State.DENY);
reservedRegion.setFlag(DefaultFlag.MOB_SPAWNING, StateFlag.State.DENY);
reservedRegion.setFlag(DefaultFlag.CREEPER_EXPLOSION, StateFlag.State.DENY);
reservedRegion.setFlag(DefaultFlag.ENDER_BUILD, StateFlag.State.DENY);
reservedRegion.setFlag(DefaultFlag.GHAST_FIREBALL, StateFlag.State.DENY);
reservedRegion.setFlag(DefaultFlag.TNT, StateFlag.State.DENY);
reservedRegion.setFlag(DefaultFlag.LAVA_FLOW, StateFlag.State.DENY);
reservedRegion.setFlag(DefaultFlag.SNOW_FALL, StateFlag.State.DENY);
reservedRegion.setFlag(Flags.PVP, StateFlag.State.DENY);
reservedRegion.setFlag(Flags.MOB_DAMAGE, StateFlag.State.DENY);
reservedRegion.setFlag(Flags.MOB_SPAWNING, StateFlag.State.DENY);
reservedRegion.setFlag(Flags.CREEPER_EXPLOSION, StateFlag.State.DENY);
reservedRegion.setFlag(Flags.ENDER_BUILD, StateFlag.State.DENY);
reservedRegion.setFlag(Flags.GHAST_FIREBALL, StateFlag.State.DENY);
reservedRegion.setFlag(Flags.TNT, StateFlag.State.DENY);
reservedRegion.setFlag(Flags.LAVA_FLOW, StateFlag.State.DENY);
reservedRegion.setFlag(Flags.SNOW_FALL, StateFlag.State.DENY);
regionManager.addRegion(reservedRegion);
_occupiedPlots.add(Plot.get(reservedRegion));
saveRegions();
}
public Cuboid getCityCuboid(){
@@ -981,7 +1011,7 @@ public class MetropolisPlugin extends JavaPlugin {
try{
regionManager.removeRegion(regionId);
}catch(Exception ex){
log.info(String.format("[ERROR] Metropolis: Unable to remove region {%s}.", regionId));
getLogger().info(String.format("[ERROR] Metropolis: Unable to remove region {%s}.", regionId));
return;
}
}
@@ -990,14 +1020,14 @@ public class MetropolisPlugin extends JavaPlugin {
try {
regionManager.save();
} catch (Exception ex) {
log.info(String.format("[SEVERE] Metropolis: Unable to save WorldGuard regions."));
getLogger().info(String.format("[SEVERE] Metropolis: Unable to save WorldGuard regions."));
return;
}
}
public int getNumPlots(String name) {
if(_ownedPlots.containsKey(name)){
List<Plot> plots = _ownedPlots.get(name);
public int getNumPlots(UUID playerId) {
if(_ownedPlots.containsKey(playerId)){
List<Plot> plots = _ownedPlots.get(playerId);
if(plots == null){
return 0;
}else{
@@ -1008,22 +1038,21 @@ public class MetropolisPlugin extends JavaPlugin {
}
}
public int getMaxPlots(String name) {
if(_userOverrides.containsKey(name)){
return _userOverrides.get(name).getMaxPlots();
public int getMaxPlots(UUID playerId) {
if(_userOverrides.containsKey(playerId)){
return _userOverrides.get(playerId).getMaxPlots();
}else{
return _maxPlots;
}
}
public void assignPlot(OfflinePlayer player) {
//PlayerHome home = generateHome(player.getName());
generateHome(player.getName());
generateHome(player);
}
private int getPlotMultiplier(String name) {
if(_userOverrides.containsKey(name)){
return _userOverrides.get(name).getPlotMultiplier();
private int getPlotMultiplier(UUID playerId) {
if(_userOverrides.containsKey(playerId)){
return _userOverrides.get(playerId).getPlotMultiplier();
}else{
return _plotMultiplier;
}
@@ -1045,11 +1074,24 @@ public class MetropolisPlugin extends JavaPlugin {
}
public Player getPlayer(String name) {
return getServer().getPlayer(name);
Player player = getServer().getPlayerExact(name);
if (player != null) {
return player;
}
return PlayerLookup.findOnline(name, getServer().getOnlinePlayers());
}
public OfflinePlayer getOfflinePlayer(String name){
return getServer().getOfflinePlayer(name);
Player onlinePlayer = getPlayer(name);
if (onlinePlayer != null) {
return onlinePlayer;
}
for (OfflinePlayer offlinePlayer : getServer().getOfflinePlayers()) {
if (name.equalsIgnoreCase(offlinePlayer.getName())) {
return offlinePlayer;
}
}
return null;
}
public String teleportPlayerToPlot(Player player, Plot plot) {
@@ -1062,9 +1104,9 @@ public class MetropolisPlugin extends JavaPlugin {
return null;
}
public boolean homeExists(String playerName, int homeNumber) {
public boolean homeExists(UUID playerId, int homeNumber) {
for(Plot plot: _occupiedPlots){
if(plot.getRegionName().equalsIgnoreCase(String.format("h_%d_%s", homeNumber, playerName))){
if(plot instanceof PlayerHome home && home.getPlayerId().equals(playerId) && home.getNumber() == homeNumber){
return true;
}
}
@@ -1072,17 +1114,41 @@ public class MetropolisPlugin extends JavaPlugin {
return false;
}
public void setHome(String name, int newHomeNumber) {
_currentHomes.put(name, newHomeNumber);
public void setHome(UUID playerId, int newHomeNumber) {
_currentHomes.put(playerId, newHomeNumber);
saveCurrentHomes();
}
public String getHomeRegionName(UUID playerId, int homeNumber) {
PlayerHome existingHome = getOwnedHome(playerId, homeNumber);
return existingHome == null
? String.format("h_%d_%s", homeNumber, playerId)
: existingHome.getRegionName();
}
public String getCurrentHomeRegionName(UUID playerId) {
return getHomeRegionName(playerId, _currentHomes.getOrDefault(playerId, 1));
}
private PlayerHome getOwnedHome(UUID playerId, int homeNumber) {
List<Plot> plots = _ownedPlots.get(playerId);
if (plots == null) {
return null;
}
for (Plot plot : plots) {
if (plot instanceof PlayerHome home && home.getNumber() == homeNumber) {
return home;
}
}
return null;
}
private void saveCurrentHomes() {
File outFile = new File(getDataFolder(), "currentHomes.yml");
// TODO Auto-generated method stub
//YAMLProcessor processor = new YAMLProcessor(new File(getDataFolder(), "currentHomes.yml"), true);
//processor.save();
File homesFile = new File(getDataFolder(), "currentHomes.yml");
try {
CurrentHomesStore.save(homesFile, _currentHomes);
} catch (IOException e) {
getLogger().log(java.util.logging.Level.SEVERE, "Unable to save currentHomes.yml", e);
}
}
}

View File

@@ -1,58 +1,65 @@
package com.majinnaibu.bukkitplugins.metropolis;
import javax.persistence.Entity;
import javax.persistence.Table;
import java.util.Set;
import java.util.UUID;
import com.avaje.ebean.validation.NotNull;
import com.sk89q.worldedit.BlockVector;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldguard.protection.regions.ProtectedCuboidRegion;
import com.sk89q.worldguard.protection.regions.ProtectedRegion;
@Entity()
@Table(name="Metropolis_PlayerHome")
public class PlayerHome extends Plot{
@NotNull
private UUID playerId;
private String playerName;
public UUID getPlayerId(){return this.playerId;}
public String getPlayerName(){return this.playerName;}
public void setPlayerName(String playerName){this.playerName = playerName;}
private int number;
public PlayerHome(String owner, BlockVector min, BlockVector max) {
super("h_" + owner, min, max);
this.playerName = owner;
public PlayerHome(UUID ownerId, String ownerName, int homeNumber, BlockVector3 min, BlockVector3 max) {
super(String.format("h_%d_%s", homeNumber, ownerId), min, max);
this.playerId = ownerId;
this.playerName = ownerName;
this.number = homeNumber;
}
public PlayerHome() {
this.playerId = null;
this.playerName = "";
}
public PlayerHome(ProtectedRegion homeRegion){
try{
String rname = homeRegion.getId();
if(rname.startsWith("h_")){
int secondUnderscore = rname.indexOf('_', 2);
if(secondUnderscore > 2){
try{
this.number = Integer.parseInt(rname.substring(2, secondUnderscore));
this.playerName = rname.substring(secondUnderscore+1);
}catch(Exception ex){
this.number = 0;
}
}else{
this.number = 0;
this.playerName = rname.substring(2);
}
setCuboid(new Cuboid(homeRegion.getMinimumPoint(), homeRegion.getMaximumPoint()));
}
else{
throw new RuntimeException("Method not implemented.");
}
}catch(Exception ex){
throw new RuntimeException("Method not implemented.", ex);
String regionName = homeRegion.getId();
if (!regionName.startsWith("h_")) {
throw new IllegalArgumentException("Not a Metropolis home region: " + regionName);
}
String regionOwner = regionName.substring(2);
int separator = regionOwner.indexOf('_');
if (separator > 0) {
try {
this.number = Integer.parseInt(regionOwner.substring(0, separator));
regionOwner = regionOwner.substring(separator + 1);
} catch (NumberFormatException ex) {
this.number = 1;
}
} else {
this.number = 1;
}
try {
this.playerId = UUID.fromString(regionOwner);
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(playerId);
this.playerName = offlinePlayer.getName() == null ? regionOwner : offlinePlayer.getName();
} catch (IllegalArgumentException ex) {
Set<UUID> ownerIds = homeRegion.getOwners().getUniqueIds();
this.playerId = ownerIds.isEmpty()
? Bukkit.getOfflinePlayer(regionOwner).getUniqueId()
: ownerIds.iterator().next();
this.playerName = regionOwner;
}
setCuboid(new Cuboid(homeRegion.getMinimumPoint(), homeRegion.getMaximumPoint()));
}
@Override
@@ -63,7 +70,7 @@ public class PlayerHome extends Plot{
PlayerHome otherPlayerHome = (PlayerHome)other;
if(!this.playerName.equals(otherPlayerHome.playerName)){
if(!java.util.Objects.equals(this.playerId, otherPlayerHome.playerId)){
return false;
}

View File

@@ -0,0 +1,19 @@
package com.majinnaibu.bukkitplugins.metropolis;
import org.bukkit.entity.Player;
final class PlayerLookup {
private PlayerLookup() {}
static Player findOnline(String requestedName, Iterable<? extends Player> onlinePlayers) {
if (requestedName == null) {
return null;
}
for (Player player : onlinePlayers) {
if (requestedName.equalsIgnoreCase(player.getName())) {
return player;
}
}
return null;
}
}

View File

@@ -1,39 +1,28 @@
package com.majinnaibu.bukkitplugins.metropolis;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import com.avaje.ebean.validation.NotNull;
import com.sk89q.worldedit.BlockVector;
import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldguard.protection.regions.ProtectedCuboidRegion;
import com.sk89q.worldguard.protection.regions.ProtectedRegion;
@Entity()
@Table(name="Metropolis_Plot")
public class Plot implements Comparable<Plot>{
@Id
private int _id;
public int getId(){return _id;}
public void setId(int id){_id = id;}
@NotNull
private Cuboid _cuboid;
public Cuboid getCuboid(){return _cuboid;}
public void setCuboid(Cuboid cuboid){_cuboid = cuboid;}
@NotNull
private String _regionName;
public String getRegionName(){return _regionName;}
public void setRegionName(String regionName){_regionName = regionName;}
public Plot(String regionName, BlockVector min, BlockVector max){
public Plot(String regionName, BlockVector3 min, BlockVector3 max){
_cuboid = new Cuboid(min, max);
_regionName = regionName;
}
@@ -48,17 +37,17 @@ public class Plot implements Comparable<Plot>{
_regionName = "";
}
public BlockVector getPlotMin(int roadWidth) {
return new BlockVector(_cuboid.minX - roadWidth/2, _cuboid.minY, _cuboid.minZ - roadWidth/2);
public BlockVector3 getPlotMin(int roadWidth) {
return BlockVector3.at(_cuboid.minX - roadWidth/2, _cuboid.minY, _cuboid.minZ - roadWidth/2);
}
public int getRow(int roadWidth, int plotSizeZ){
BlockVector min = getPlotMin(roadWidth);
return min.getBlockZ() / plotSizeZ;
BlockVector3 min = getPlotMin(roadWidth);
return min.z() / plotSizeZ;
}
public int getCol(int roadWidth, int plotSizeX){
return getPlotMin(roadWidth).getBlockX() / plotSizeX;
return getPlotMin(roadWidth).x() / plotSizeX;
}
@Override

View File

@@ -5,7 +5,7 @@ import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import com.majinnaibu.bukkitplugins.metropolis.MetropolisPlugin;
import com.sk89q.worldguard.protection.flags.DefaultFlag;
import com.sk89q.worldguard.protection.flags.Flags;
import com.sk89q.worldguard.protection.flags.StateFlag;
import com.sk89q.worldguard.protection.regions.ProtectedRegion;
@@ -19,25 +19,25 @@ public class MetropolisFlagResetCommand implements CommandExecutor {
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
ProtectedRegion cityRegion = _plugin.regionManager.getRegion("City");
cityRegion.setFlag(DefaultFlag.PVP, StateFlag.State.DENY);
cityRegion.setFlag(DefaultFlag.MOB_DAMAGE, StateFlag.State.DENY);
cityRegion.setFlag(DefaultFlag.MOB_SPAWNING, StateFlag.State.DENY);
cityRegion.setFlag(DefaultFlag.CREEPER_EXPLOSION, StateFlag.State.DENY);
cityRegion.setFlag(DefaultFlag.ENDER_BUILD, StateFlag.State.DENY);
cityRegion.setFlag(DefaultFlag.GHAST_FIREBALL, StateFlag.State.DENY);
cityRegion.setFlag(DefaultFlag.TNT, StateFlag.State.DENY);
cityRegion.setFlag(DefaultFlag.LAVA_FLOW, StateFlag.State.DENY);
cityRegion.setFlag(DefaultFlag.SNOW_FALL, StateFlag.State.DENY);
cityRegion.setFlag(Flags.PVP, StateFlag.State.DENY);
cityRegion.setFlag(Flags.MOB_DAMAGE, StateFlag.State.DENY);
cityRegion.setFlag(Flags.MOB_SPAWNING, StateFlag.State.DENY);
cityRegion.setFlag(Flags.CREEPER_EXPLOSION, StateFlag.State.DENY);
cityRegion.setFlag(Flags.ENDER_BUILD, StateFlag.State.DENY);
cityRegion.setFlag(Flags.GHAST_FIREBALL, StateFlag.State.DENY);
cityRegion.setFlag(Flags.TNT, StateFlag.State.DENY);
cityRegion.setFlag(Flags.LAVA_FLOW, StateFlag.State.DENY);
cityRegion.setFlag(Flags.SNOW_FALL, StateFlag.State.DENY);
for(ProtectedRegion homeRegion: _plugin.regionManager.getRegions().values()){
if(homeRegion.getId().startsWith("h_")){
homeRegion.setFlag(DefaultFlag.PVP, StateFlag.State.DENY);
homeRegion.setFlag(DefaultFlag.MOB_DAMAGE, StateFlag.State.DENY);
homeRegion.setFlag(DefaultFlag.MOB_SPAWNING, StateFlag.State.DENY);
homeRegion.setFlag(DefaultFlag.CREEPER_EXPLOSION, StateFlag.State.DENY);
homeRegion.setFlag(DefaultFlag.ENDER_BUILD, StateFlag.State.DENY);
homeRegion.setFlag(DefaultFlag.GHAST_FIREBALL, StateFlag.State.DENY);
homeRegion.setFlag(DefaultFlag.TNT, StateFlag.State.DENY);
homeRegion.setFlag(Flags.PVP, StateFlag.State.DENY);
homeRegion.setFlag(Flags.MOB_DAMAGE, StateFlag.State.DENY);
homeRegion.setFlag(Flags.MOB_SPAWNING, StateFlag.State.DENY);
homeRegion.setFlag(Flags.CREEPER_EXPLOSION, StateFlag.State.DENY);
homeRegion.setFlag(Flags.ENDER_BUILD, StateFlag.State.DENY);
homeRegion.setFlag(Flags.GHAST_FIREBALL, StateFlag.State.DENY);
homeRegion.setFlag(Flags.TNT, StateFlag.State.DENY);
}
}

View File

@@ -23,7 +23,7 @@ public class MetropolisHomeAcquire implements CommandExecutor {
Player player = (Player) sender;
if(_plugin.getNumPlots(player.getName()) >= _plugin.getMaxPlots(player.getName())){
if(_plugin.getNumPlots(player.getUniqueId()) >= _plugin.getMaxPlots(player.getUniqueId())){
sender.sendMessage("You cannot have any more plots");
return false;
}

View File

@@ -28,21 +28,21 @@ public class MetropolisHomeEvictCommand implements CommandExecutor {
playerName = args[0];
}
OfflinePlayer player = _plugin.getServer().getOfflinePlayer(playerName);
OfflinePlayer player = _plugin.getOfflinePlayer(playerName);
if(player == null){
sender.sendMessage(String.format("The requested player {%s}does not appear to exist.", playerName));
return false;
}
ProtectedRegion region = _plugin.getRegion(String.format("h_%s", player.getName()));
ProtectedRegion region = _plugin.getRegion(_plugin.getCurrentHomeRegionName(player.getUniqueId()));
if(region == null){
sender.sendMessage(String.format("The player {%s} has no home to be evicted from."));
return false;
}
//remove the player as owner and/or member of the region
region.getMembers().removePlayer(playerName);
region.getOwners().removePlayer(playerName);
region.getMembers().removePlayer(player.getUniqueId());
region.getOwners().removePlayer(player.getUniqueId());
//if the region has no owners delete the region
if(region.getMembers().size() == 0 && region.getOwners().size() == 0){

View File

@@ -3,6 +3,7 @@ package com.majinnaibu.bukkitplugins.metropolis.commands;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.OfflinePlayer;
import com.majinnaibu.bukkitplugins.metropolis.MetropolisPlugin;
@@ -20,7 +21,12 @@ public class MetropolisHomeGenerateCommand implements CommandExecutor {
return false;
}
_plugin.generateHome(args[0]);
OfflinePlayer player = _plugin.getOfflinePlayer(args[0]);
if (player == null) {
sender.sendMessage("Unable to find player " + args[0]);
return false;
}
_plugin.generateHome(player);
sender.sendMessage("[Metropolis] Home generated for " + args[0]);

View File

@@ -24,7 +24,7 @@ public class MetropolisHomeGoCommand implements CommandExecutor {
if(args.length >= 1){
if(player.hasPermission("")){
player = _plugin.getServer().getPlayer(args[0]);
player = _plugin.getPlayer(args[0]);
if(player == null){
sender.sendMessage(String.format("Unable to find player %s", args[0]));
@@ -37,7 +37,7 @@ public class MetropolisHomeGoCommand implements CommandExecutor {
}
}else{
if(args.length >= 1){
player = _plugin.getServer().getPlayer(args[0]);
player = _plugin.getPlayer(args[0]);
if(player == null){
sender.sendMessage(String.format("Unable to find player %s", args[0]));

View File

@@ -41,11 +41,11 @@ public class MetropolisHomeMoveCommand implements CommandExecutor {
return false;
}
if(player == null || !_plugin.homeExists(player.getName(), newHomeNumber)){
if(player == null || !_plugin.homeExists(player.getUniqueId(), newHomeNumber)){
return false;
}
_plugin.setHome(player.getName(), newHomeNumber);
_plugin.setHome(player.getUniqueId(), newHomeNumber);
return true;
}

View File

@@ -7,7 +7,9 @@ import org.bukkit.entity.Player;
import com.majinnaibu.bukkitplugins.metropolis.Cuboid;
import com.majinnaibu.bukkitplugins.metropolis.MetropolisPlugin;
import com.sk89q.worldedit.bukkit.selections.Selection;
import com.sk89q.worldedit.IncompleteRegionException;
import com.sk89q.worldedit.bukkit.BukkitAdapter;
import com.sk89q.worldedit.regions.Region;
public class MetropolisPlotReserveCommand implements CommandExecutor {
MetropolisPlugin _plugin;
@@ -27,9 +29,15 @@ public class MetropolisPlotReserveCommand implements CommandExecutor {
Cuboid cuboid = new Cuboid();
if(sender instanceof Player && args.length == 1){
Selection selection = _plugin.worldEdit.getSelection((Player) sender);
cuboid = new Cuboid(selection);
}else if(args.length == 6){
try {
Region selection = _plugin.worldEdit.getSession((Player) sender)
.getSelection(BukkitAdapter.adapt(_plugin.getWorld()));
cuboid = new Cuboid(selection);
} catch (IncompleteRegionException ex) {
sender.sendMessage("Select a WorldEdit region first.");
return false;
}
}else if(args.length == 7){
try{
minX = Integer.parseInt(args[1]);
minY = Integer.parseInt(args[2]);
@@ -41,7 +49,7 @@ public class MetropolisPlotReserveCommand implements CommandExecutor {
return false;
}
cuboid = new Cuboid(minX, minY, minZ, maxX, maxY, maxZ);
}else if(args.length == 4){
}else if(args.length == 5){
try{
minX = Integer.parseInt(args[1]);
minY = 0;
@@ -60,6 +68,7 @@ public class MetropolisPlotReserveCommand implements CommandExecutor {
String regionName = args[0];
_plugin.reserveCuboid(regionName, cuboid);
sender.sendMessage("Reserved plot region " + regionName + ".");
return true;
}

View File

@@ -15,7 +15,6 @@ public class PlayerJoinListener implements Listener {
public PlayerJoinListener(MetropolisPlugin plugin){
_plugin = plugin;
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@EventHandler(priority=EventPriority.MONITOR)
@@ -26,12 +25,12 @@ public class PlayerJoinListener implements Listener {
}
PlayerHome home = _plugin.getPlayerHome(player);
if(home==null){MetropolisPlugin.log.info("home is null");}
else if(home.getCuboid() == null){MetropolisPlugin.log.info("home.getCuboid() is null");}
else if(home.getCuboid().getVolume() == 0){MetropolisPlugin.log.info("home.getCuboid().getVolume() is 0");}
if(home==null){_plugin.getLogger().info("home is null");}
else if(home.getCuboid() == null){_plugin.getLogger().info("home.getCuboid() is null");}
else if(home.getCuboid().getVolume() == 0){_plugin.getLogger().info("home.getCuboid().getVolume() is 0");}
if(home == null || home.getCuboid() == null || home.getCuboid().getVolume() == 0){
MetropolisPlugin.log.info(String.format("Metropolis: Unable to get or create home for player %s", player.getName()));
_plugin.getLogger().info(String.format("Metropolis: Unable to get or create home for player %s", player.getName()));
}else{
Cuboid cuboid = home.getCuboid();
player.sendMessage(String.format("Metropolis: Welcome %s your home is between (%d, %d, %d) and (%d, %d, %d)", player.getName(), cuboid.getMinX(), cuboid.getMinY(), cuboid.getMinZ(), cuboid.getMaxX(), cuboid.getMaxY(), cuboid.getMaxZ()));

View File

@@ -1,29 +1,37 @@
plot:
sizeX: 32
sizeY: 256
sizeX: 32
sizeY: 256
sizeZ: 32
offsetX: 0
offsetY: 0
offsetZ: 0
multiplier: 1
maxPerPlayer: 1
initial: 1
floor:
generate: true
clearSpaceAbove: 66
offsetZ: 0
material: GRASS_BLOCK
supports:
generate: true
initial: 1
material: STONE
sign:
generate: true
generate: true
road:
width: 4
clearSpaceAbove: 66
level: 62
generate: true
material: COBBLESTONE
supports:
generate: true
generate: true
material: STONE
spawn:
generate: true
setAsWorldSpawn: true
level: 62
material: GRASS_BLOCK
wall:
generate: true
generate: true
material: BEDROCK
height: 66
worldname: world
userOverrides:

View File

@@ -1,10 +1,50 @@
name: Metropolis
main: com.majinnaibu.bukkitplugins.metropolis.MetropolisPlugin
depend: [WorldGuard]
softdepend: [WorldEdit,CommandBook]
version: 0.5
api-version: '26.2'
depend: [WorldGuard, WorldEdit]
softdepend: [CommandBook]
version: '${version}'
database: false
commands:
metropolis:
description: This command displays the current metropolis version.
usage: /metropolis
usage: /metropolis
metropolis-home-generate:
description: This command generates a home for a user as if they'd just logged in.
permission: metropolis.home.generate
usage: /metropolis-home-generate <playername>
metropolis-home-list:
description: This lists the regions managed by Metropolis
permission: metropolis.home.list
usage: /metropolis-home-list
metropolis-home-evict:
description: This unassigns a player's current home.
permission: metropolis.home.evict
usage: /metropolis-home-evict <playerName>
metropolis-home-move:
description: This command swaps a player's current home with a reserved plot.
permission: metropolis.home.move
usage: /metropolis-home-move <newHomeNumber> [player]
metropolis-home-go:
description: Teleports the user or another player to his home.
permission: metropolis.home.go
usage: /metropolis-home-go
metropolis-home-acquire:
description: Acquires an available home plot.
usage: /metropolis-home-acquire
metropolis-flag-reset:
description: This command resets the WorldGuard flags for all managed regions (city and h_*).
permission: metropolis.flag.reset
usage: /metropolis-flag-reset
metropolis-plot-reserve:
description: This command reserves a plot so it won't be assigned as a home.
permission: metropolis.plot.reserve
usage: /metropolis-plot-reserve <name> <minX> <minY> <minZ> <maxX> <maxY> <maxZ>
metropolis-plot-go:
description: This command teleports the user or another player to a plot.
permission: metropolis.plot.go
usage: /metropolis-plot-go <plotName> [playerName]
metropolis-debug-generatetesthomes:
description: This command is ignored on non-debug builds.
permission: metropolis.debug
usage: /metropolis-debug-gentesthomes

View File

@@ -0,0 +1,53 @@
package com.majinnaibu.bukkitplugins.metropolis;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.bukkit.Location;
import org.junit.jupiter.api.Test;
import com.sk89q.worldedit.math.BlockVector3;
class CuboidTest {
@Test
void convertsWorldEditBoundsToCuboidCoordinates() {
Cuboid cuboid = new Cuboid(BlockVector3.at(1, 2, 3), BlockVector3.at(4, 5, 6));
assertEquals(1, cuboid.getMinX());
assertEquals(2, cuboid.getMinY());
assertEquals(3, cuboid.getMinZ());
assertEquals(4, cuboid.getMaxX());
assertEquals(5, cuboid.getMaxY());
assertEquals(6, cuboid.getMaxZ());
}
@Test
void comparesVectorsByXThenZThenYAndHandlesNulls() {
assertEquals(-1, Cuboid.compareBlockVectors(null, BlockVector3.ZERO));
assertEquals(1, Cuboid.compareBlockVectors(BlockVector3.ZERO, null));
assertEquals(0, Cuboid.compareBlockVectors(null, null));
assertTrue(Cuboid.compareBlockVectors(BlockVector3.at(1, 0, 0), BlockVector3.at(2, 0, 0)) < 0);
assertTrue(Cuboid.compareBlockVectors(BlockVector3.at(1, 0, 1), BlockVector3.at(1, 0, 2)) < 0);
assertTrue(Cuboid.compareBlockVectors(BlockVector3.at(1, 1, 2), BlockVector3.at(1, 2, 2)) < 0);
}
@Test
void treatsTouchingBoundsAsIntersecting() {
Cuboid first = new Cuboid(0, 0, 0, 10, 10, 10);
Cuboid touching = new Cuboid(10, 10, 10, 20, 20, 20);
Cuboid separate = new Cuboid(11, 0, 0, 20, 10, 10);
assertTrue(first.intersects(touching));
assertFalse(first.intersects(separate));
}
@Test
void containsPointsOnBoundsButRejectsPointsOutside() {
Cuboid cuboid = new Cuboid(1, 2, 3, 4, 5, 6);
assertTrue(cuboid.contains(new Location(null, 1, 2, 3)));
assertTrue(cuboid.contains(new Location(null, 4, 5, 6)));
assertFalse(cuboid.contains(new Location(null, 5, 5, 6)));
}
}

View File

@@ -0,0 +1,41 @@
package com.majinnaibu.bukkitplugins.metropolis;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.File;
import java.util.Map;
import java.util.UUID;
import org.bukkit.configuration.file.YamlConfiguration;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
class CurrentHomesStoreTest {
@TempDir
File temporaryDirectory;
@Test
void roundTripsHomesByUuid() throws Exception {
File file = new File(temporaryDirectory, "currentHomes.yml");
UUID playerId = UUID.fromString("123e4567-e89b-12d3-a456-426614174000");
Map<UUID, Integer> expected = Map.of(playerId, 3);
CurrentHomesStore.save(file, expected);
assertEquals(expected, CurrentHomesStore.load(file, ignored -> null));
}
@Test
void resolvesLegacyPlayerNameKeys() throws Exception {
File file = new File(temporaryDirectory, "currentHomes.yml");
UUID playerId = UUID.fromString("123e4567-e89b-12d3-a456-426614174000");
YamlConfiguration yaml = new YamlConfiguration();
yaml.set("OldPlayerName", 2);
yaml.save(file);
assertEquals(Map.of(playerId, 2), CurrentHomesStore.load(file, name -> {
assertEquals("OldPlayerName", name);
return playerId;
}));
}
}

View File

@@ -0,0 +1,27 @@
package com.majinnaibu.bukkitplugins.metropolis;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import com.sk89q.worldedit.math.BlockVector3;
class PlayerHomeTest {
@Test
void buildsUuidBasedRegionIdentityAndPreservesDisplayName() {
UUID playerId = UUID.fromString("123e4567-e89b-12d3-a456-426614174000");
PlayerHome home = new PlayerHome(
playerId,
"PlayerName",
2,
BlockVector3.at(1, 2, 3),
BlockVector3.at(4, 5, 6));
assertEquals(playerId, home.getPlayerId());
assertEquals("PlayerName", home.getPlayerName());
assertEquals(2, home.getNumber());
assertEquals("h_2_" + playerId, home.getRegionName());
}
}

View File

@@ -0,0 +1,22 @@
package com.majinnaibu.bukkitplugins.metropolis;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.List;
import org.bukkit.entity.Player;
import org.junit.jupiter.api.Test;
class PlayerLookupTest {
@Test
void matchesOnlinePlayerNamesCaseInsensitivelyButNotByPrefix() {
Player player = mock(Player.class);
when(player.getName()).thenReturn("PlayerName");
assertSame(player, PlayerLookup.findOnline("playername", List.of(player)));
assertNull(PlayerLookup.findOnline("Player", List.of(player)));
}
}

View File

@@ -0,0 +1,48 @@
package com.majinnaibu.bukkitplugins.metropolis.commands;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.UUID;
import org.bukkit.command.Command;
import org.bukkit.entity.Player;
import org.junit.jupiter.api.Test;
import com.majinnaibu.bukkitplugins.metropolis.MetropolisPlugin;
class MetropolisHomeAcquireTest {
@Test
void assignsAPlotWhenThePlayerIsBelowTheirLimit() {
MetropolisPlugin plugin = mock(MetropolisPlugin.class);
Player player = mock(Player.class);
UUID playerId = UUID.fromString("123e4567-e89b-12d3-a456-426614174000");
when(player.getUniqueId()).thenReturn(playerId);
when(plugin.getNumPlots(playerId)).thenReturn(0);
when(plugin.getMaxPlots(playerId)).thenReturn(1);
MetropolisHomeAcquire executor = new MetropolisHomeAcquire(plugin);
assertTrue(executor.onCommand(player, mock(Command.class), "acquire", new String[0]));
verify(plugin).assignPlot(player);
}
@Test
void doesNotAssignAPlotAfterThePlayerReachesTheirLimit() {
MetropolisPlugin plugin = mock(MetropolisPlugin.class);
Player player = mock(Player.class);
UUID playerId = UUID.fromString("123e4567-e89b-12d3-a456-426614174000");
when(player.getUniqueId()).thenReturn(playerId);
when(plugin.getNumPlots(playerId)).thenReturn(1);
when(plugin.getMaxPlots(playerId)).thenReturn(1);
MetropolisHomeAcquire executor = new MetropolisHomeAcquire(plugin);
assertFalse(executor.onCommand(player, mock(Command.class), "acquire", new String[0]));
verify(plugin, never()).assignPlot(player);
}
}

View File

@@ -0,0 +1,49 @@
package com.majinnaibu.bukkitplugins.metropolis.commands;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import com.majinnaibu.bukkitplugins.metropolis.Cuboid;
import com.majinnaibu.bukkitplugins.metropolis.MetropolisPlugin;
class MetropolisPlotReserveCommandTest {
@Test
void reservesCoordinatesFromAllSixArguments() {
MetropolisPlugin plugin = mock(MetropolisPlugin.class);
CommandSender sender = mock(CommandSender.class);
Command command = mock(Command.class);
MetropolisPlotReserveCommand executor = new MetropolisPlotReserveCommand(plugin);
assertTrue(executor.onCommand(sender, command, "reserve", new String[] {"north", "1", "2", "3", "4", "5", "6"}));
ArgumentCaptor<Cuboid> cuboid = ArgumentCaptor.forClass(Cuboid.class);
verify(plugin).reserveCuboid(eq("north"), cuboid.capture());
assertEquals(1, cuboid.getValue().getMinX());
assertEquals(2, cuboid.getValue().getMinY());
assertEquals(3, cuboid.getValue().getMinZ());
assertEquals(4, cuboid.getValue().getMaxX());
assertEquals(5, cuboid.getValue().getMaxY());
assertEquals(6, cuboid.getValue().getMaxZ());
}
@Test
void rejectsMalformedCoordinateArgumentsWithoutCallingPlugin() {
MetropolisPlugin plugin = mock(MetropolisPlugin.class);
CommandSender sender = mock(CommandSender.class);
Command command = mock(Command.class);
MetropolisPlotReserveCommand executor = new MetropolisPlotReserveCommand(plugin);
assertFalse(executor.onCommand(sender, command, "reserve", new String[] {"north", "1", "two", "3", "4", "5", "6"}));
verifyNoInteractions(plugin);
}
}

View File

@@ -0,0 +1,37 @@
package com.majinnaibu.bukkitplugins.metropolis.eventlisteners;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.UUID;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerJoinEvent;
import org.junit.jupiter.api.Test;
import com.majinnaibu.bukkitplugins.metropolis.MetropolisPlugin;
import com.majinnaibu.bukkitplugins.metropolis.PlayerHome;
import com.sk89q.worldedit.math.BlockVector3;
class PlayerJoinListenerTest {
@Test
void tellsJoiningPlayerTheirHomeBounds() {
MetropolisPlugin plugin = mock(MetropolisPlugin.class);
Player player = mock(Player.class);
PlayerJoinEvent event = mock(PlayerJoinEvent.class);
when(event.getPlayer()).thenReturn(player);
when(player.getName()).thenReturn("PlayerName");
when(plugin.getPlayerHome(player)).thenReturn(new PlayerHome(
UUID.fromString("123e4567-e89b-12d3-a456-426614174000"),
"PlayerName",
1,
BlockVector3.at(1, 2, 3),
BlockVector3.at(4, 5, 6)));
new PlayerJoinListener(plugin).onPlayerJoin(event);
verify(player).sendMessage(contains("home is between (1, 2, 3) and (4, 5, 6)"));
}
}

103
tools/backup-server.sh Executable file
View File

@@ -0,0 +1,103 @@
#!/usr/bin/env bash
# Backs up plugins, config, worlds, and settings from MINECRAFT_SERVER_PATH
# to MINECRAFT_BACKUP_PATH/backup-<timestamp>[-label].tar.bz2.
#
# Usage: backup-server.sh [-f|--force] [label]
# -f Back up even if the server is running (worlds may be inconsistent).
set -euo pipefail
TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$TOOLS_DIR/.." && pwd)"
if [[ ! -r "$PROJECT_ROOT/.env" ]]; then
echo "Error: $PROJECT_ROOT/.env is missing or unreadable. Copy .env.example to .env and set its values." >&2
exit 1
fi
load_project_env() {
local env_line env_key env_value first_char last_char
while IFS= read -r env_line || [[ -n "$env_line" ]]; do
env_line="${env_line%$'\r'}"
[[ "$env_line" =~ ^[[:space:]]*(#|$) ]] && continue
[[ "$env_line" == *=* ]] || continue
env_key="${env_line%%=*}"
env_value="${env_line#*=}"
env_key="${env_key#"${env_key%%[![:space:]]*}"}"
env_key="${env_key%"${env_key##*[![:space:]]}"}"
[[ "$env_key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
env_value="${env_value#"${env_value%%[![:space:]]*}"}"
env_value="${env_value%"${env_value##*[![:space:]]}"}"
if (( ${#env_value} >= 2 )); then
first_char="${env_value:0:1}"
last_char="${env_value:$((${#env_value} - 1)):1}"
if [[ "$first_char" == "$last_char" && ( "$first_char" == '"' || "$first_char" == "'" ) ]]; then
env_value="${env_value:1:$((${#env_value} - 2))}"
fi
fi
if [[ -z "${!env_key:-}" ]]; then
export "$env_key=$env_value"
fi
done < "$PROJECT_ROOT/.env"
}
load_project_env
MINECRAFT_SERVER_PATH="${MINECRAFT_SERVER_PATH:-MCServer}"
MINECRAFT_SERVER_PATH="${MINECRAFT_SERVER_PATH/#\~/$HOME}"
[[ "$MINECRAFT_SERVER_PATH" == /* ]] || MINECRAFT_SERVER_PATH="$PROJECT_ROOT/$MINECRAFT_SERVER_PATH"
MINECRAFT_BACKUP_PATH="${MINECRAFT_BACKUP_PATH:-backups}"
MINECRAFT_BACKUP_PATH="${MINECRAFT_BACKUP_PATH/#\~/$HOME}"
[[ "$MINECRAFT_BACKUP_PATH" == /* ]] || MINECRAFT_BACKUP_PATH="$PROJECT_ROOT/$MINECRAFT_BACKUP_PATH"
FORCE=false
LABEL=""
for arg in "$@"; do
case "$arg" in
-f|--force) FORCE=true ;;
-h|--help) sed -n '2,7p' "$0"; exit 0 ;;
-*) echo "Unknown option: $arg" >&2; exit 1 ;;
*) LABEL="$arg" ;;
esac
done
[[ -d "$MINECRAFT_SERVER_PATH" ]] || { echo "Error: $MINECRAFT_SERVER_PATH does not exist." >&2; exit 1; }
if pgrep -f "java.*paper-.*\.jar" >/dev/null && ! $FORCE; then
echo "Error: the server is running. Stop it first (tools/stop-server.sh) or pass --force." >&2
exit 1
fi
cd "$MINECRAFT_SERVER_PATH"
shopt -s nullglob
ITEMS=()
[[ -d plugins ]] && ITEMS+=(plugins)
[[ -d config ]] && ITEMS+=(config)
for dir in */; do
dir="${dir%/}"
[[ -f "$dir/level.dat" ]] && ITEMS+=("$dir")
done
for f in *.yml *.yaml *.json *.properties eula.txt; do
ITEMS+=("$f")
done
if (( ${#ITEMS[@]} == 0 )); then
echo "Error: nothing to back up in $MINECRAFT_SERVER_PATH." >&2
exit 1
fi
# Keep labels filename-safe.
LABEL="${LABEL//[^A-Za-z0-9._-]/_}"
NAME="backup-$(date +%Y%m%d-%H%M%S)${LABEL:+-$LABEL}.tar.bz2"
mkdir -p "$MINECRAFT_BACKUP_PATH"
ARCHIVE="$MINECRAFT_BACKUP_PATH/$NAME"
echo "Backing up: ${ITEMS[*]}"
trap 'rm -f "$ARCHIVE.part"' EXIT
tar --exclude 'plugins/.paper-remapped' --exclude '*/session.lock' \
-cjf "$ARCHIVE.part" "${ITEMS[@]}"
mv "$ARCHIVE.part" "$ARCHIVE"
trap - EXIT
echo "Created $ARCHIVE ($(du -h "$ARCHIVE" | awk '{print $1}'))"

10
tools/build-plugin.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
# Find project root (one directory above this script)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$PROJECT_ROOT"
exec ./gradlew build "$@"

62
tools/deploy-plugin.sh Executable file
View File

@@ -0,0 +1,62 @@
#!/usr/bin/env bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
if [[ ! -r "$PROJECT_ROOT/.env" ]]; then
echo "Error: $PROJECT_ROOT/.env is missing or unreadable. Copy .env.example to .env and set its values." >&2
exit 1
fi
load_project_env() {
local env_line env_key env_value first_char last_char
while IFS= read -r env_line || [[ -n "$env_line" ]]; do
env_line="${env_line%$'\r'}"
[[ "$env_line" =~ ^[[:space:]]*(#|$) ]] && continue
[[ "$env_line" == *=* ]] || continue
env_key="${env_line%%=*}"
env_value="${env_line#*=}"
env_key="${env_key#"${env_key%%[![:space:]]*}"}"
env_key="${env_key%"${env_key##*[![:space:]]}"}"
[[ "$env_key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
env_value="${env_value#"${env_value%%[![:space:]]*}"}"
env_value="${env_value%"${env_value##*[![:space:]]}"}"
if (( ${#env_value} >= 2 )); then
first_char="${env_value:0:1}"
last_char="${env_value:$((${#env_value} - 1)):1}"
if [[ "$first_char" == "$last_char" && ( "$first_char" == '"' || "$first_char" == "'" ) ]]; then
env_value="${env_value:1:$((${#env_value} - 2))}"
fi
fi
if [[ -z "${!env_key:-}" ]]; then
export "$env_key=$env_value"
fi
done < "$PROJECT_ROOT/.env"
}
load_project_env
MINECRAFT_SERVER_PATH="${MINECRAFT_SERVER_PATH:-MCServer}"
MINECRAFT_SERVER_PATH="${MINECRAFT_SERVER_PATH/#\~/$HOME}"
[[ "$MINECRAFT_SERVER_PATH" == /* ]] || MINECRAFT_SERVER_PATH="$PROJECT_ROOT/$MINECRAFT_SERVER_PATH"
if [[ -z "$MINECRAFT_SERVER_PATH" ]]; then
echo "Error: MINECRAFT_SERVER_PATH environment variable is not set."
exit 1
fi
PLUGIN_JAR=$(ls -t "$PROJECT_ROOT"/build/libs/*.jar 2>/dev/null | head -n1 || true)
if [[ ! -f "$PLUGIN_JAR" ]]; then
echo "Error: No plugin jar found in $PROJECT_ROOT/build/libs. Build the plugin first."
exit 1
fi
if find "$PROJECT_ROOT/src/main/java" "$PROJECT_ROOT/src/main/resources" -type f -newer "$PLUGIN_JAR" | grep -q .; then
echo "Warning: The built plugin jar is older than some source files. Consider rebuilding."
fi
mkdir -p "$MINECRAFT_SERVER_PATH/plugins"
cp "$PLUGIN_JAR" "$MINECRAFT_SERVER_PATH/plugins/"
echo "Deployed $PLUGIN_JAR to $MINECRAFT_SERVER_PATH/plugins/"

45
tools/publish-local.sh Executable file
View File

@@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
# Use the same local JDK bootstrap pattern as the Gradle projects.
export GRADLE_USER_HOME="${GRADLE_USER_HOME:-$PROJECT_ROOT/.gradle/user-home}"
mkdir -p "$GRADLE_USER_HOME/jdks"
ensure_java_25() {
local version="25"
local os arch archive java_bin actual_home
case "$(uname -s)" in
Darwin) os="mac" ;;
Linux) os="linux" ;;
*) echo "Unsupported OS: $(uname -s)" >&2; exit 1 ;;
esac
case "$(uname -m)" in
arm64|aarch64) arch="aarch64" ;;
x86_64|amd64) arch="x64" ;;
*) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;;
esac
java_bin=$(find "$GRADLE_USER_HOME/jdks" -path '*/bin/java' -type f 2>/dev/null | head -n 1 || true)
if [[ -z "$java_bin" ]]; then
echo "Downloading Temurin JDK $version into $GRADLE_USER_HOME/jdks..."
archive="$GRADLE_USER_HOME/jdks/temurin-$version.tar.gz"
curl -fsSL "https://api.adoptium.net/v3/binary/latest/$version/ga/${os}/${arch}/jdk/hotspot/normal/eclipse" -o "$archive"
tar -xzf "$archive" -C "$GRADLE_USER_HOME/jdks"
rm -f "$archive"
java_bin=$(find "$GRADLE_USER_HOME/jdks" -path '*/bin/java' -type f 2>/dev/null | head -n 1 || true)
fi
[[ -n "$java_bin" ]] || { echo "Error: could not locate installed JDK 25." >&2; exit 1; }
actual_home="$(dirname "$(dirname "$java_bin")")"
export JAVA_HOME="$actual_home"
export PATH="$JAVA_HOME/bin:$PATH"
}
ensure_java_25
cd "$PROJECT_ROOT"
exec ./gradlew publishToMavenLocal "$@"

145
tools/publish-modrinth.sh Executable file
View File

@@ -0,0 +1,145 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
if [[ ! -r "$PROJECT_ROOT/.env" ]]; then
echo "Error: $PROJECT_ROOT/.env is missing or unreadable. Copy .env.example to .env and set its values." >&2
exit 1
fi
load_project_env() {
local env_line env_key env_value first_char last_char
while IFS= read -r env_line || [[ -n "$env_line" ]]; do
env_line="${env_line%$'\r'}"
[[ "$env_line" =~ ^[[:space:]]*(#|$) ]] && continue
[[ "$env_line" == *=* ]] || continue
env_key="${env_line%%=*}"
env_value="${env_line#*=}"
env_key="${env_key#"${env_key%%[![:space:]]*}"}"
env_key="${env_key%"${env_key##*[![:space:]]}"}"
[[ "$env_key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
env_value="${env_value#"${env_value%%[![:space:]]*}"}"
env_value="${env_value%"${env_value##*[![:space:]]}"}"
if (( ${#env_value} >= 2 )); then
first_char="${env_value:0:1}"
last_char="${env_value:$((${#env_value} - 1)):1}"
if [[ "$first_char" == "$last_char" && ( "$first_char" == '"' || "$first_char" == "'" ) ]]; then
env_value="${env_value:1:$((${#env_value} - 2))}"
fi
fi
if [[ -z "${!env_key:-}" ]]; then
export "$env_key=$env_value"
fi
done < "$PROJECT_ROOT/.env"
}
load_project_env
MODRINTH_TOKEN="${MODRINTH_TOKEN:-}"
MODRINTH_PROJECT_ID="${MODRINTH_PROJECT_ID:-metropolis-builder}"
MODRINTH_VERSION="${MODRINTH_VERSION:-${VERSION:-}}"
if [[ -z "$MODRINTH_VERSION" ]]; then
MODRINTH_VERSION=$(cd "$PROJECT_ROOT" && ./gradlew -q properties --property version | sed -n 's/^version: //p' | head -n1)
fi
MODRINTH_CHANGELOG="${MODRINTH_CHANGELOG:-Auto release via publish-modrinth.sh}"
MODRINTH_GAME_VERSIONS="${MODRINTH_GAME_VERSIONS:-26.2}"
MODRINTH_LOADERS="${MODRINTH_LOADERS:-paper}"
MODRINTH_VERSION_TYPE="${MODRINTH_VERSION_TYPE:-}"
MODRINTH_FILE="${MODRINTH_FILE:-}"
MODRINTH_DEPENDENCIES="${MODRINTH_DEPENDENCIES:-}"
if [[ -z "$MODRINTH_VERSION" ]]; then
echo "Error: could not determine the project version from Gradle." >&2
exit 1
fi
if [[ -z "$MODRINTH_VERSION_TYPE" ]]; then
if [[ "$MODRINTH_VERSION" == *-* ]]; then
MODRINTH_VERSION_TYPE="beta"
else
MODRINTH_VERSION_TYPE="release"
fi
fi
case "$MODRINTH_VERSION_TYPE" in
release|beta|alpha) ;;
*) echo "Error: MODRINTH_VERSION_TYPE must be release, beta, or alpha." >&2; exit 1 ;;
esac
MODRINTH_VERSION_NAME="Metropolis $MODRINTH_VERSION"
if [[ -n "$MODRINTH_FILE" && "$MODRINTH_FILE" != /* ]]; then
MODRINTH_FILE="$PROJECT_ROOT/$MODRINTH_FILE"
fi
if [[ -z "$MODRINTH_TOKEN" ]]; then
echo "Error: MODRINTH_TOKEN is not set in $PROJECT_ROOT/.env" >&2
exit 1
fi
if [[ -z "$MODRINTH_FILE" ]]; then
MODRINTH_FILE="$PROJECT_ROOT/build/libs/Metropolis-$MODRINTH_VERSION.jar"
fi
if [[ -z "$MODRINTH_FILE" || ! -f "$MODRINTH_FILE" ]]; then
echo "Error: no jar found for Modrinth upload. Build the project first or set MODRINTH_FILE." >&2
exit 1
fi
if ! command -v unzip >/dev/null; then
echo "Error: 'unzip' is required to verify the plugin version in the jar." >&2
exit 1
fi
JAR_VERSION=$(unzip -p "$MODRINTH_FILE" plugin.yml 2>/dev/null | sed -n 's/^version: //p' | tr -d "'\"" | head -n1 || true)
if [[ -z "$JAR_VERSION" ]]; then
echo "Error: could not read the plugin version from $MODRINTH_FILE." >&2
exit 1
fi
if [[ "$JAR_VERSION" != "$MODRINTH_VERSION" ]]; then
echo "Error: jar version $JAR_VERSION does not match Gradle version $MODRINTH_VERSION. Rebuild the plugin and retry." >&2
exit 1
fi
csv_to_json_array() {
jq -cn --arg csv "$1" '$csv | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))'
}
MODRINTH_GAME_VERSIONS_JSON=$(csv_to_json_array "$MODRINTH_GAME_VERSIONS")
MODRINTH_LOADERS_JSON=$(csv_to_json_array "$MODRINTH_LOADERS")
if [[ -n "$MODRINTH_DEPENDENCIES" ]]; then
if ! MODRINTH_DEPENDENCIES_JSON=$(jq -ce 'if type == "array" then . else error("dependencies must be a JSON array") end' <<<"$MODRINTH_DEPENDENCIES"); then
echo "Error: MODRINTH_DEPENDENCIES must be a JSON array." >&2
exit 1
fi
else
MODRINTH_DEPENDENCIES_JSON='[]'
fi
MODRINTH_DATA=$(jq -cn \
--arg project_id "$MODRINTH_PROJECT_ID" \
--arg name "$MODRINTH_VERSION_NAME" \
--arg version_number "$MODRINTH_VERSION" \
--arg version_type "$MODRINTH_VERSION_TYPE" \
--arg changelog "$MODRINTH_CHANGELOG" \
--argjson featured false \
--argjson dependencies "$MODRINTH_DEPENDENCIES_JSON" \
--argjson game_versions "$MODRINTH_GAME_VERSIONS_JSON" \
--argjson loaders "$MODRINTH_LOADERS_JSON" \
'{project_id: $project_id, name: $name, version_number: $version_number, version_type: $version_type, featured: $featured, changelog: $changelog, dependencies: $dependencies, game_versions: $game_versions, loaders: $loaders, file_parts: ["file"], primary_file: "file"}')
CURL_ARGS=(
-X POST "https://api.modrinth.com/v2/version"
-H "Authorization: $MODRINTH_TOKEN"
-F "data=$MODRINTH_DATA;type=application/json"
-F "file=@$MODRINTH_FILE"
)
HTTP_RESPONSE=$(curl -sS -w $'\n%{http_code}' "${CURL_ARGS[@]}")
HTTP_STATUS="${HTTP_RESPONSE##*$'\n'}"
HTTP_BODY="${HTTP_RESPONSE%$'\n'*}"
if [[ "$HTTP_STATUS" != 2?? ]]; then
echo "Error: Modrinth API returned HTTP $HTTP_STATUS:" >&2
if ! printf '%s\n' "$HTTP_BODY" | jq -r '.description // .error // .' >&2; then
printf '%s\n' "$HTTP_BODY" >&2
fi
exit 1
fi
printf '%s\n' "$HTTP_BODY" | jq .

107
tools/restore-server.sh Executable file
View File

@@ -0,0 +1,107 @@
#!/usr/bin/env bash
# Restores a backup made by backup-server.sh into MINECRAFT_SERVER_PATH.
# Each top-level item in the archive (plugins, config, worlds, settings) is
# replaced; a "pre-restore" backup of the current state is taken first.
#
# Usage: restore-server.sh [-y|--yes] [--no-backup] [backup-file|name]
# Defaults to the newest backup in MINECRAFT_BACKUP_PATH.
set -euo pipefail
TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$TOOLS_DIR/.." && pwd)"
if [[ ! -r "$PROJECT_ROOT/.env" ]]; then
echo "Error: $PROJECT_ROOT/.env is missing or unreadable. Copy .env.example to .env and set its values." >&2
exit 1
fi
load_project_env() {
local env_line env_key env_value first_char last_char
while IFS= read -r env_line || [[ -n "$env_line" ]]; do
env_line="${env_line%$'\r'}"
[[ "$env_line" =~ ^[[:space:]]*(#|$) ]] && continue
[[ "$env_line" == *=* ]] || continue
env_key="${env_line%%=*}"
env_value="${env_line#*=}"
env_key="${env_key#"${env_key%%[![:space:]]*}"}"
env_key="${env_key%"${env_key##*[![:space:]]}"}"
[[ "$env_key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
env_value="${env_value#"${env_value%%[![:space:]]*}"}"
env_value="${env_value%"${env_value##*[![:space:]]}"}"
if (( ${#env_value} >= 2 )); then
first_char="${env_value:0:1}"
last_char="${env_value:$((${#env_value} - 1)):1}"
if [[ "$first_char" == "$last_char" && ( "$first_char" == '"' || "$first_char" == "'" ) ]]; then
env_value="${env_value:1:$((${#env_value} - 2))}"
fi
fi
if [[ -z "${!env_key:-}" ]]; then
export "$env_key=$env_value"
fi
done < "$PROJECT_ROOT/.env"
}
load_project_env
MINECRAFT_SERVER_PATH="${MINECRAFT_SERVER_PATH:-MCServer}"
MINECRAFT_SERVER_PATH="${MINECRAFT_SERVER_PATH/#\~/$HOME}"
[[ "$MINECRAFT_SERVER_PATH" == /* ]] || MINECRAFT_SERVER_PATH="$PROJECT_ROOT/$MINECRAFT_SERVER_PATH"
MINECRAFT_BACKUP_PATH="${MINECRAFT_BACKUP_PATH:-backups}"
MINECRAFT_BACKUP_PATH="${MINECRAFT_BACKUP_PATH/#\~/$HOME}"
[[ "$MINECRAFT_BACKUP_PATH" == /* ]] || MINECRAFT_BACKUP_PATH="$PROJECT_ROOT/$MINECRAFT_BACKUP_PATH"
ASSUME_YES=false
SAFETY_BACKUP=true
ARCHIVE=""
for arg in "$@"; do
case "$arg" in
-y|--yes) ASSUME_YES=true ;;
--no-backup) SAFETY_BACKUP=false ;;
-h|--help) sed -n '2,8p' "$0"; exit 0 ;;
-*) echo "Unknown option: $arg" >&2; exit 1 ;;
*) ARCHIVE="$arg" ;;
esac
done
if [[ -z "$ARCHIVE" ]]; then
ARCHIVE=$(ls -t "$MINECRAFT_BACKUP_PATH"/backup-*.tar.bz2 2>/dev/null | head -n1 || true)
[[ -n "$ARCHIVE" ]] || { echo "Error: no backups found in $MINECRAFT_BACKUP_PATH." >&2; exit 1; }
elif [[ ! -f "$ARCHIVE" && -f "$MINECRAFT_BACKUP_PATH/$ARCHIVE" ]]; then
ARCHIVE="$MINECRAFT_BACKUP_PATH/$ARCHIVE"
fi
[[ -f "$ARCHIVE" ]] || { echo "Error: backup not found: $ARCHIVE" >&2; exit 1; }
ARCHIVE="$(cd "$(dirname "$ARCHIVE")" && pwd)/$(basename "$ARCHIVE")"
if pgrep -f "java.*paper-.*\.jar" >/dev/null; then
echo "Error: the server is running. Stop it first (tools/stop-server.sh)." >&2
exit 1
fi
ENTRIES=$(tar -tjf "$ARCHIVE")
if grep -qE '^/|(^|/)\.\.(/|$)' <<<"$ENTRIES"; then
echo "Error: archive contains absolute or '..' paths; refusing to restore." >&2
exit 1
fi
TOP_LEVEL=$(sed -E 's#^\./##; s#/.*##' <<<"$ENTRIES" | { grep -vE '^\.?$' || true; } | sort -u)
[[ -n "$TOP_LEVEL" ]] || { echo "Error: backup is empty." >&2; exit 1; }
echo "Restore $(basename "$ARCHIVE") into $MINECRAFT_SERVER_PATH"
echo "These items will be replaced:"
sed 's/^/ /' <<<"$TOP_LEVEL"
if ! $ASSUME_YES; then
read -r -p "Continue? [y/N] " reply
[[ "$reply" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 1; }
fi
mkdir -p "$MINECRAFT_SERVER_PATH"
if $SAFETY_BACKUP && [[ -n "$(ls -A "$MINECRAFT_SERVER_PATH")" ]]; then
"$TOOLS_DIR/backup-server.sh" pre-restore
fi
while IFS= read -r item; do
rm -rf "${MINECRAFT_SERVER_PATH:?}/$item"
done <<<"$TOP_LEVEL"
tar -xjf "$ARCHIVE" -C "$MINECRAFT_SERVER_PATH"
echo "Restored $(basename "$ARCHIVE")."

131
tools/start-server.sh Executable file
View File

@@ -0,0 +1,131 @@
#!/usr/bin/env bash
# Downloads (if missing) and runs a Paper server via the Fill v3 API.
# Server settings come from the project .env file.
#
# Env vars:
# JAVA_OPTS Extra JVM args (default: -Xms2G -Xmx2G)
set -euo pipefail
TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$TOOLS_DIR/.." && pwd)"
if [[ ! -r "$PROJECT_ROOT/.env" ]]; then
echo "Error: $PROJECT_ROOT/.env is missing or unreadable. Copy .env.example to .env and set its values." >&2
exit 1
fi
load_project_env() {
local env_line env_key env_value first_char last_char
while IFS= read -r env_line || [[ -n "$env_line" ]]; do
env_line="${env_line%$'\r'}"
[[ "$env_line" =~ ^[[:space:]]*(#|$) ]] && continue
[[ "$env_line" == *=* ]] || continue
env_key="${env_line%%=*}"
env_value="${env_line#*=}"
env_key="${env_key#"${env_key%%[![:space:]]*}"}"
env_key="${env_key%"${env_key##*[![:space:]]}"}"
[[ "$env_key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
env_value="${env_value#"${env_value%%[![:space:]]*}"}"
env_value="${env_value%"${env_value##*[![:space:]]}"}"
if (( ${#env_value} >= 2 )); then
first_char="${env_value:0:1}"
last_char="${env_value:$((${#env_value} - 1)):1}"
if [[ "$first_char" == "$last_char" && ( "$first_char" == '"' || "$first_char" == "'" ) ]]; then
env_value="${env_value:1:$((${#env_value} - 2))}"
fi
fi
if [[ -z "${!env_key:-}" ]]; then
export "$env_key=$env_value"
fi
done < "$PROJECT_ROOT/.env"
}
load_project_env
PAPER_VERSION="${PAPER_VERSION:-}"
PAPER_BUILD="${PAPER_BUILD:-}"
MINECRAFT_SERVER_JAR="${MINECRAFT_SERVER_JAR:-}"
MINECRAFT_SERVER_PATH="${MINECRAFT_SERVER_PATH:-MCServer}"
MINECRAFT_SERVER_PATH="${MINECRAFT_SERVER_PATH/#\~/$HOME}"
[[ "$MINECRAFT_SERVER_PATH" == /* ]] || MINECRAFT_SERVER_PATH="$PROJECT_ROOT/$MINECRAFT_SERVER_PATH"
[[ -n "$PAPER_VERSION" ]] || { echo "Error: PAPER_VERSION is not set (see .env)." >&2; exit 1; }
PAPER_PROJECT="paper"
JAVA_OPTS="${JAVA_OPTS:--Xms2G -Xmx2G}"
FILL_API="https://fill.papermc.io/v3/projects/${PAPER_PROJECT}/versions/${PAPER_VERSION}"
# Fill requires a non-generic User-Agent with contact info.
USER_AGENT="${PAPER_USER_AGENT:-majinnaibu-plugin-dev/1.0 (https://github.com/majinnaibu)}"
for cmd in curl jq java shasum; do
command -v "$cmd" >/dev/null || { echo "Error: '$cmd' is required." >&2; exit 1; }
done
api_get() {
curl -fsSL -H "User-Agent: $USER_AGENT" "$1"
}
mkdir -p "$MINECRAFT_SERVER_PATH"
cd "$MINECRAFT_SERVER_PATH"
JAR_BUILD="${MINECRAFT_SERVER_JAR:-}"
JAR_BUILD="${JAR_BUILD#paper-"$PAPER_VERSION"-}"; JAR_BUILD="${JAR_BUILD%.jar}"
if [[ -z "$PAPER_BUILD" && "$JAR_BUILD" =~ ^[0-9]+$ ]]; then
PAPER_BUILD="$JAR_BUILD"
fi
if [[ -z "$PAPER_BUILD" ]]; then
echo "Resolving latest stable build for Paper $PAPER_VERSION..."
PAPER_BUILD=$(api_get "$FILL_API/builds" \
| jq -r 'map(select(.channel == "STABLE")) | max_by(.id) | .id // empty') || true
if [[ -z "$PAPER_BUILD" ]]; then
# Offline or no stable build: fall back to the newest local jar for this version.
for f in paper-"$PAPER_VERSION"-*.jar; do
b="${f#paper-"$PAPER_VERSION"-}"; b="${b%.jar}"
[[ "$b" =~ ^[0-9]+$ ]] && (( b > ${PAPER_BUILD:-0} )) && PAPER_BUILD="$b"
done
[[ -n "$PAPER_BUILD" ]] || { echo "Error: no stable build found for $PAPER_VERSION." >&2; exit 1; }
echo "Could not resolve from API; using local build $PAPER_BUILD."
fi
fi
EXPECTED_JAR="paper-${PAPER_VERSION}-${PAPER_BUILD}.jar"
if [[ -n "${MINECRAFT_SERVER_JAR:-}" && "$MINECRAFT_SERVER_JAR" != "$EXPECTED_JAR" ]]; then
echo "Error: MINECRAFT_SERVER_JAR=$MINECRAFT_SERVER_JAR does not match PAPER_VERSION/PAPER_BUILD ($EXPECTED_JAR)." >&2
exit 1
fi
MINECRAFT_SERVER_JAR="$EXPECTED_JAR"
if [[ ! -f "$MINECRAFT_SERVER_JAR" ]]; then
echo "Downloading $MINECRAFT_SERVER_JAR..."
BUILD_JSON=$(api_get "$FILL_API/builds/$PAPER_BUILD")
DOWNLOAD_URL=$(jq -r '.downloads."server:default".url // empty' <<<"$BUILD_JSON")
EXPECTED_SHA=$(jq -r '.downloads."server:default".checksums.sha256 // empty' <<<"$BUILD_JSON")
[[ -n "$DOWNLOAD_URL" ]] || { echo "Error: no download for $PAPER_VERSION build $PAPER_BUILD." >&2; exit 1; }
TMP_JAR="$MINECRAFT_SERVER_JAR.part"
trap 'rm -f "$TMP_JAR"' EXIT
curl -fL --progress-bar -H "User-Agent: $USER_AGENT" -o "$TMP_JAR" "$DOWNLOAD_URL"
ACTUAL_SHA=$(shasum -a 256 "$TMP_JAR" | awk '{print $1}')
if [[ -n "$EXPECTED_SHA" && "$ACTUAL_SHA" != "$EXPECTED_SHA" ]]; then
echo "Error: checksum mismatch (expected $EXPECTED_SHA, got $ACTUAL_SHA)." >&2
exit 1
fi
mv "$TMP_JAR" "$MINECRAFT_SERVER_JAR"
trap - EXIT
fi
export MINECRAFT_SERVER_PATH MINECRAFT_SERVER_JAR
"$TOOLS_DIR/stop-server.sh"
if ! grep -qs '^eula=true' eula.txt; then
echo "Note: accept the EULA by setting eula=true in $MINECRAFT_SERVER_PATH/eula.txt (server will exit until then)."
fi
echo "Starting $MINECRAFT_SERVER_JAR in $MINECRAFT_SERVER_PATH..."
# shellcheck disable=SC2086
exec java $JAVA_OPTS -jar "$MINECRAFT_SERVER_JAR" --nogui

34
tools/stop-server.sh Executable file
View File

@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# Stops any running Paper server (java process running a paper-*.jar).
set -euo pipefail
STOP_TIMEOUT="${STOP_TIMEOUT:-60}"
SERVER_PIDS=$(pgrep -f "java.*paper-.*\.jar" || true)
if [[ -z "$SERVER_PIDS" ]]; then
echo "Minecraft server is not running."
exit 0
fi
for pid in $SERVER_PIDS; do
echo "Stopping Minecraft server (PID: $pid)..."
# SIGTERM lets Paper run its shutdown hook and save worlds.
kill "$pid"
done
for ((i = 0; i < STOP_TIMEOUT; i++)); do
remaining=""
for pid in $SERVER_PIDS; do
kill -0 "$pid" 2>/dev/null && remaining="$remaining $pid"
done
if [[ -z "$remaining" ]]; then
echo "Minecraft server stopped."
exit 0
fi
sleep 1
done
echo "Server did not stop within ${STOP_TIMEOUT}s; forcing:$remaining"
kill -9 $remaining