25 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
54 changed files with 1802 additions and 723 deletions

View File

@@ -1,57 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" path="target/generated-sources/annotations">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="ignore_optional_problems" value="true"/>
<attribute name="m2e-apt" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="target/generated-test-sources/test-annotations">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="ignore_optional_problems" value="true"/>
<attribute name="m2e-apt" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -1,9 +1,24 @@
# Dev-server config for this project's tools/ scripts. # Configuration for tools/*.sh. Values are read as KEY=value data, not executed.
# Plain KEY=value only (no shell expansion). Environment variables take precedence. # Unquoted values may contain spaces; matching outer quotes are removed.
# Paper server
PAPER_VERSION=26.2 PAPER_VERSION=26.2
PAPER_BUILD=129 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 MINECRAFT_SERVER_JAR=paper-26.2-129.jar
# Relative paths are resolved from this directory; ~/ is expanded. # Relative paths are resolved from the project root; ~/ is expanded.
MINECRAFT_SERVER_PATH=./server MINECRAFT_SERVER_PATH=./server
MINECRAFT_BACKUP_PATH=./backups 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=

6
.gitignore vendored
View File

@@ -7,9 +7,3 @@ bin/
/server/ /server/
.env .env
# Old entries double check these later.
/server
/Plugin.jardesc
/Plugin-Release.jardesc
/lib
/target

View File

@@ -1,34 +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>
<filteredResources>
<filter>
<id>1790453742830</id>
<name></name>
<type>30</type>
<matcher>
<id>org.eclipse.core.resources.regexFilterMatcher</id>
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
</matcher>
</filter>
</filteredResources>
</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,2 +0,0 @@
eclipse.preferences.version=1
org.eclipse.jdt.apt.aptEnabled=false

View File

@@ -1,16 +0,0 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=1.8
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.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore
org.eclipse.jdt.core.compiler.processAnnotations=disabled
org.eclipse.jdt.core.compiler.release=disabled
org.eclipse.jdt.core.compiler.source=1.8

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.

View File

@@ -1,7 +1,7 @@
{ {
"id": "metropolis", "id": "metropolis",
"title": "Metropolis", "title": "Metropolis",
"description": "Legacy Bukkit plugin that automatically creates protected home plots and roads as players join a Minecraft server.", "description": "Paper plugin that automatically creates protected home plots and connecting roads as players join a Minecraft server.",
"docId": "Metropolis", "docId": "Metropolis",
"image": { "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.", "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.",
@@ -12,9 +12,7 @@
"backgroundClassName": "bg-surface" "backgroundClassName": "bg-surface"
}, },
"url": "/projects/metropolis", "url": "/projects/metropolis",
"tags": [ "tags": ["open-source"],
"open-source"
],
"source": { "source": {
"type": "github", "type": "github",
"url": "https://github.com/headhunter45/Metropolis" "url": "https://github.com/headhunter45/Metropolis"
@@ -22,4 +20,4 @@
"feedback": { "feedback": {
"url": "https://github.com/headhunter45/Metropolis/issues" "url": "https://github.com/headhunter45/Metropolis/issues"
} }
} }

View File

@@ -1,18 +1,12 @@
# Metropolis # Metropolis
Metropolis is a legacy Bukkit plugin that automatically expands a protected Minecraft city as players join the server. Each player receives a home plot surrounded by roads and positioned beside the existing city, with WorldGuard regions protecting the city and individual homes. 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.
## Details ## Platform
- Platform: Bukkit/Minecraft server plugin - Paper 26.2, Java 25+
- Language: Java - WorldGuard 7.0.19 and WorldEdit 7.4.5
- Bukkit API: 1.7.9-R0.2 - Built with Gradle 9.8 and Git-sensitive semantic versioning
- Dependencies: WorldGuard 5.9 and WorldEdit 5.6.3
- Features: automatic home plots, road and plot generation, WorldGuard region management, reserved plots, home teleportation, and configurable plot and road materials
- Configuration: `plugins/Metropolis/config.yml` - Configuration: `plugins/Metropolis/config.yml`
- Source: https://github.com/headhunter45/Metropolis - Unit tests run without a server; live integration tests use the separate Gradle `integrationTest` task.
- License: GNU General Public License v3 - License: GNU General Public License v3
## Image 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.

114
README.md
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
After creation these regions can be modified as normal via WorldGuard commands. - WorldEdit 7.4.5
INSTALL: WorldGuard and WorldEdit are required plugin dependencies. Install them alongside Metropolis in the server's `plugins` directory.
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: ```sh
v0.5 ./gradlew clean build
Added example docs ```
Added Commands to plugin.yml to move and evict homes
Added floor and road supports to config file and generation 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.
v0.4.6
Added optional generation of a sign identifying owner in plots on creation Run the opt-in live-server tests against a running Paper server with WorldGuard, WorldEdit, and RCON enabled:
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. ```sh
Added an occupied Plots list to MetropolisPlugin to keep track of both player homes and reserved plots. METROPOLIS_RCON_HOST=127.0.0.1 \
Added the Plot class as a parent of PlayerHome and moved relevant code to it. METROPOLIS_RCON_PORT=25575 \
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 METROPOLIS_RCON_PASSWORD=... \
v0.4.5 METROPOLIS_SERVER_DIR=/path/to/server \
v0.4.4 ./gradlew integrationTest
Added a welcome message telling players where their home is. ```
Switched from PlayerLoginEvent to PlayerJoinEvent
v0.4.3 `integrationTest` is separate from the normal `test` and `build` tasks. Keep RCON bound to a trusted interface and do not commit its password.
Removed call to saveconfig on plugin unload.
v0.4.2 ## Server Scripts
Made city region refresh on server restart.
v0.4.1 Copy `.env.example` to `.env` and configure the Paper version/build and server paths. The scripts read `.env` as key/value data.
v0.4
v0.3 - `tools/build-plugin.sh` builds and tests the plugin.
Added command to generate a home for a user that is not currently logged in. - `tools/deploy-plugin.sh` copies the Gradle-built jar to the configured server.
v0.2.1 - `tools/start-server.sh` downloads the configured Paper build if needed and starts it.
Made config file save on load and disable. - `tools/backup-server.sh` and `tools/restore-server.sh` back up or restore the configured server.
v0.2 - `tools/publish-local.sh` publishes the artifact to the local Maven repository.
Added configuration options. - `tools/publish-modrinth.sh` uploads a built jar after checking its version.
v0.1 ## Configuration
Initial Release
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.

49
TODO.md
View File

@@ -2,33 +2,28 @@
| ID | Status | Title | | ID | Status | Title |
|:-------:|:--------:|:------| |:-------:|:--------:|:------|
| MET-000 | Ready | Add Gradle plugins as needed (e.g., Shadow) | | MET-001 | Done | Initialize Gradle in the project root, add the Gradle wrapper, and create the project build file. |
| MET-000 | Ready | Add repositories and dependencies | | MET-002 | Done | Set Gradle project metadata and configure a Java toolchain compatible with the target modern Paper version. |
| MET-000 | Ready | Address any bugs or incompatibilities found during testing on a modern server. | 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-000 | Ready | Analyze all Java source files for deprecated or removed Bukkit API usage, including logger, event registration, listener classes, configuration API, and entity/player mapping. | | 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-000 | Ready | Build the plugin with Gradle (./gradlew build) and test on a modern Paper server using the provided bash scripts. | 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-000 | Ready | Configure Java version | | MET-006 | Done | Build the initial Gradle project and run its tests; resolve build setup issues before removing the existing Maven build. |
| MET-000 | Ready | Ensure all commands are properly defined in plugin.yml | | MET-007 | Done | Update .gitignore for Gradle and remove obsolete Maven and Eclipse project files after the Gradle build succeeds. |
| MET-000 | Ready | Ensure plugin.yml is present in src/main/resources and update for PaperMC compatibility (api-version, commands, required fields). | | 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-000 | Ready | Ensure resource handling for plugin.yml | | MET-009 | Done | Review custom scripts and resources for required updates to the Gradle build and modern Paper server workflow. |
| MET-000 | Ready | Identify and document any hardcoded Bukkit references or legacy patterns (e.g., Player as HashMap key, old event registration, CraftBukkit class names). | | MET-010 | Done | Refactor plugin logging to use JavaPlugin.getLogger(). |
| MET-000 | Ready | Initialize Gradle in the project root and create a build.gradle file with project metadata, PaperMC API dependency, JUnit, Java toolchain, repository, resource handling, and plugins as needed. | | MET-011 | Done | Refactor event listeners and registration to use Listener, @EventHandler, and the modern plugin manager API. |
| MET-000 | Ready | Refactor all event listeners to use the modern event system (Listener interface, @EventHandler, registerEvents). | MET-012 | Done | Replace deprecated configuration APIs with the modern Bukkit/Paper configuration API, including load, save, and reload behavior. |
| MET-000 | Ready | Refactor all logger usage to use getLogger() from JavaPlugin. | | MET-013 | Done | Replace Player-keyed storage with UUID-based storage wherever player identity is persisted or tracked. |
| MET-000 | Ready | Remove Maven-specific files (pom.xml, .mvn/ directory, Maven wrapper scripts) and Eclipse-specific files (.classpath, .project, .settings/) if present. | | MET-014 | Done | Review player lookup logic and use exact or otherwise explicit name-matching behavior where appropriate. |
| MET-000 | Ready | Remove or modernize any old/deprecated event registration (use @EventHandler and registerEvents) | | 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-000 | Ready | Remove or refactor any code using deprecated or removed Bukkit/Spigot/Paper APIs that are not available in the modern 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-000 | Ready | Replace all usage of org.bukkit.util.config.Configuration with the modern Bukkit configuration API (getConfig(), saveConfig(), reloadConfig(), etc.). | 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-000 | Ready | Review and list any custom scripts or resources (e.g., bash scripts in tools/bash/) that may need migration or updates.| | 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-000 | Ready | Review and update player lookup logic to use getPlayerExact or handle case sensitivity | | MET-019 | Done | Address bugs and incompatibilities found by unit tests, integration tests, or modern Paper server testing. |
| MET-000 | Ready | Set project metadata in build.gradle | | 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-000 | Ready | Switch all player score storage to use UUID instead of Player as the key | | 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-000 | Ready | Test the Gradle build | | MET-022 | Planning | (Optional) Add new features, quality-of-life improvements, or CI configuration after the modernization and documentation tasks are complete. |
| MET-000 | Ready | Update .gitignore to add Gradle-specific ignores and remove Maven/Eclipse-specific ignores. |
| MET-000 | Ready | Update README.md and CONTRIBUTING.md with new build, usage, and development instructions. _(Depends on: bugfixes)_
| MET-000 | Ready | Update documentation and scripts |
| MET-000 | Ready | Update logger usage to use getLogger() from JavaPlugin |
| MET-000 | Ready | (Optional) Add new features, quality-of-life improvements, automated tests, or CI configuration. _(Depends on: docs update)_
| GIT-009 | Planning | i have three reserve plots and one just got overwritten when a new player joined. | | 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-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-007 | Ready | Allow buying and selling of homes via an ecnoomy plugin. |

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

View File

@@ -1,58 +1,65 @@
package com.majinnaibu.bukkitplugins.metropolis; package com.majinnaibu.bukkitplugins.metropolis;
import javax.persistence.Entity; import java.util.Set;
import javax.persistence.Table; import java.util.UUID;
import com.avaje.ebean.validation.NotNull; import org.bukkit.Bukkit;
import com.sk89q.worldedit.BlockVector; import org.bukkit.OfflinePlayer;
import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldguard.protection.regions.ProtectedCuboidRegion; import com.sk89q.worldguard.protection.regions.ProtectedCuboidRegion;
import com.sk89q.worldguard.protection.regions.ProtectedRegion; import com.sk89q.worldguard.protection.regions.ProtectedRegion;
@Entity()
@Table(name="Metropolis_PlayerHome")
public class PlayerHome extends Plot{ public class PlayerHome extends Plot{
@NotNull private UUID playerId;
private String playerName; private String playerName;
public UUID getPlayerId(){return this.playerId;}
public String getPlayerName(){return this.playerName;} public String getPlayerName(){return this.playerName;}
public void setPlayerName(String playerName){this.playerName = playerName;} public void setPlayerName(String playerName){this.playerName = playerName;}
private int number; private int number;
public PlayerHome(String owner, BlockVector min, BlockVector max) { public PlayerHome(UUID ownerId, String ownerName, int homeNumber, BlockVector3 min, BlockVector3 max) {
super("h_" + owner, min, max); super(String.format("h_%d_%s", homeNumber, ownerId), min, max);
this.playerName = owner; this.playerId = ownerId;
this.playerName = ownerName;
this.number = homeNumber;
} }
public PlayerHome() { public PlayerHome() {
this.playerId = null;
this.playerName = ""; this.playerName = "";
} }
public PlayerHome(ProtectedRegion homeRegion){ public PlayerHome(ProtectedRegion homeRegion){
try{ String regionName = homeRegion.getId();
String rname = homeRegion.getId(); if (!regionName.startsWith("h_")) {
throw new IllegalArgumentException("Not a Metropolis home region: " + regionName);
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 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 @Override
@@ -63,7 +70,7 @@ public class PlayerHome extends Plot{
PlayerHome otherPlayerHome = (PlayerHome)other; PlayerHome otherPlayerHome = (PlayerHome)other;
if(!this.playerName.equals(otherPlayerHome.playerName)){ if(!java.util.Objects.equals(this.playerId, otherPlayerHome.playerId)){
return false; 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; package com.majinnaibu.bukkitplugins.metropolis;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.block.Block; import org.bukkit.block.Block;
import com.avaje.ebean.validation.NotNull; import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldedit.BlockVector;
import com.sk89q.worldguard.protection.regions.ProtectedCuboidRegion; import com.sk89q.worldguard.protection.regions.ProtectedCuboidRegion;
import com.sk89q.worldguard.protection.regions.ProtectedRegion; import com.sk89q.worldguard.protection.regions.ProtectedRegion;
@Entity()
@Table(name="Metropolis_Plot")
public class Plot implements Comparable<Plot>{ public class Plot implements Comparable<Plot>{
@Id
private int _id; private int _id;
public int getId(){return _id;} public int getId(){return _id;}
public void setId(int id){_id = id;} public void setId(int id){_id = id;}
@NotNull
private Cuboid _cuboid; private Cuboid _cuboid;
public Cuboid getCuboid(){return _cuboid;} public Cuboid getCuboid(){return _cuboid;}
public void setCuboid(Cuboid cuboid){_cuboid = cuboid;} public void setCuboid(Cuboid cuboid){_cuboid = cuboid;}
@NotNull
private String _regionName; private String _regionName;
public String getRegionName(){return _regionName;} public String getRegionName(){return _regionName;}
public void setRegionName(String regionName){_regionName = 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); _cuboid = new Cuboid(min, max);
_regionName = regionName; _regionName = regionName;
} }
@@ -48,17 +37,17 @@ public class Plot implements Comparable<Plot>{
_regionName = ""; _regionName = "";
} }
public BlockVector getPlotMin(int roadWidth) { public BlockVector3 getPlotMin(int roadWidth) {
return new BlockVector(_cuboid.minX - roadWidth/2, _cuboid.minY, _cuboid.minZ - roadWidth/2); return BlockVector3.at(_cuboid.minX - roadWidth/2, _cuboid.minY, _cuboid.minZ - roadWidth/2);
} }
public int getRow(int roadWidth, int plotSizeZ){ public int getRow(int roadWidth, int plotSizeZ){
BlockVector min = getPlotMin(roadWidth); BlockVector3 min = getPlotMin(roadWidth);
return min.getBlockZ() / plotSizeZ; return min.z() / plotSizeZ;
} }
public int getCol(int roadWidth, int plotSizeX){ public int getCol(int roadWidth, int plotSizeX){
return getPlotMin(roadWidth).getBlockX() / plotSizeX; return getPlotMin(roadWidth).x() / plotSizeX;
} }
@Override @Override

View File

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

View File

@@ -28,21 +28,21 @@ public class MetropolisHomeEvictCommand implements CommandExecutor {
playerName = args[0]; playerName = args[0];
} }
OfflinePlayer player = _plugin.getServer().getOfflinePlayer(playerName); OfflinePlayer player = _plugin.getOfflinePlayer(playerName);
if(player == null){ if(player == null){
sender.sendMessage(String.format("The requested player {%s}does not appear to exist.", playerName)); sender.sendMessage(String.format("The requested player {%s}does not appear to exist.", playerName));
return false; return false;
} }
ProtectedRegion region = _plugin.getRegion(String.format("h_%s", player.getName())); ProtectedRegion region = _plugin.getRegion(_plugin.getCurrentHomeRegionName(player.getUniqueId()));
if(region == null){ if(region == null){
sender.sendMessage(String.format("The player {%s} has no home to be evicted from.")); sender.sendMessage(String.format("The player {%s} has no home to be evicted from."));
return false; return false;
} }
//remove the player as owner and/or member of the region //remove the player as owner and/or member of the region
region.getMembers().removePlayer(playerName); region.getMembers().removePlayer(player.getUniqueId());
region.getOwners().removePlayer(playerName); region.getOwners().removePlayer(player.getUniqueId());
//if the region has no owners delete the region //if the region has no owners delete the region
if(region.getMembers().size() == 0 && region.getOwners().size() == 0){ 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.Command;
import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.OfflinePlayer;
import com.majinnaibu.bukkitplugins.metropolis.MetropolisPlugin; import com.majinnaibu.bukkitplugins.metropolis.MetropolisPlugin;
@@ -20,7 +21,12 @@ public class MetropolisHomeGenerateCommand implements CommandExecutor {
return false; 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]); sender.sendMessage("[Metropolis] Home generated for " + args[0]);

View File

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

View File

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

View File

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

View File

@@ -15,7 +15,6 @@ public class PlayerJoinListener implements Listener {
public PlayerJoinListener(MetropolisPlugin plugin){ public PlayerJoinListener(MetropolisPlugin plugin){
_plugin = plugin; _plugin = plugin;
plugin.getServer().getPluginManager().registerEvents(this, plugin);
} }
@EventHandler(priority=EventPriority.MONITOR) @EventHandler(priority=EventPriority.MONITOR)
@@ -26,12 +25,12 @@ public class PlayerJoinListener implements Listener {
} }
PlayerHome home = _plugin.getPlayerHome(player); PlayerHome home = _plugin.getPlayerHome(player);
if(home==null){MetropolisPlugin.log.info("home is null");} if(home==null){_plugin.getLogger().info("home is null");}
else if(home.getCuboid() == null){MetropolisPlugin.log.info("home.getCuboid() is null");} else if(home.getCuboid() == null){_plugin.getLogger().info("home.getCuboid() is null");}
else if(home.getCuboid().getVolume() == 0){MetropolisPlugin.log.info("home.getCuboid().getVolume() is 0");} else if(home.getCuboid().getVolume() == 0){_plugin.getLogger().info("home.getCuboid().getVolume() is 0");}
if(home == null || home.getCuboid() == null || home.getCuboid().getVolume() == 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{ }else{
Cuboid cuboid = home.getCuboid(); 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())); 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: plot:
sizeX: 32 sizeX: 32
sizeY: 256 sizeY: 256
sizeZ: 32
offsetX: 0
offsetY: 0
offsetZ: 0
multiplier: 1
maxPerPlayer: 1
initial: 1
floor: floor:
generate: true generate: true
clearSpaceAbove: 66 clearSpaceAbove: 66
offsetZ: 0 material: GRASS_BLOCK
supports: supports:
generate: true generate: true
initial: 1 material: STONE
sign: sign:
generate: true generate: true
road:
width: 4 width: 4
clearSpaceAbove: 66 clearSpaceAbove: 66
level: 62 level: 62
generate: true material: COBBLESTONE
supports: supports:
generate: true generate: true
generate: true material: STONE
spawn: spawn:
generate: true generate: true
setAsWorldSpawn: true setAsWorldSpawn: true
level: 62 material: GRASS_BLOCK
wall: wall:
generate: true generate: true
generate: true material: BEDROCK
height: 66 height: 66
worldname: world worldname: world
userOverrides: userOverrides:

View File

@@ -1,10 +1,50 @@
name: Metropolis name: Metropolis
main: com.majinnaibu.bukkitplugins.metropolis.MetropolisPlugin main: com.majinnaibu.bukkitplugins.metropolis.MetropolisPlugin
depend: [WorldGuard] api-version: '26.2'
softdepend: [WorldEdit,CommandBook] depend: [WorldGuard, WorldEdit]
version: 0.5 softdepend: [CommandBook]
version: '${version}'
database: false database: false
commands: commands:
metropolis: metropolis:
description: This command displays the current metropolis version. 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)"));
}
}

View File

@@ -8,8 +8,47 @@
set -euo pipefail set -euo pipefail
TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=setup-env.sh PROJECT_ROOT="$(cd "$TOOLS_DIR/.." && pwd)"
source "$TOOLS_DIR/setup-env.sh" 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 FORCE=false
LABEL="" LABEL=""

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -e set -euo pipefail
# Find project root (one directory above this script) # Find project root (one directory above this script)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -7,8 +7,4 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$PROJECT_ROOT" cd "$PROJECT_ROOT"
if [[ -x ./mvnw ]]; then exec ./gradlew build "$@"
./mvnw package
else
mvn package
fi

View File

@@ -4,19 +4,52 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
# shellcheck source=setup-env.sh if [[ ! -r "$PROJECT_ROOT/.env" ]]; then
source "$SCRIPT_DIR/setup-env.sh" 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 if [[ -z "$MINECRAFT_SERVER_PATH" ]]; then
echo "Error: MINECRAFT_SERVER_PATH environment variable is not set." echo "Error: MINECRAFT_SERVER_PATH environment variable is not set."
exit 1 exit 1
fi fi
# Skip Maven side artifacts (shade originals, sources, javadoc). PLUGIN_JAR=$(ls -t "$PROJECT_ROOT"/build/libs/*.jar 2>/dev/null | head -n1 || true)
PLUGIN_JAR=$(ls -t "$PROJECT_ROOT"/target/*.jar 2>/dev/null \
| grep -vE '/original-[^/]*\.jar$|-(sources|javadoc|tests)\.jar$' | head -n1 || true)
if [[ ! -f "$PLUGIN_JAR" ]]; then if [[ ! -f "$PLUGIN_JAR" ]]; then
echo "Error: No plugin jar found in $PROJECT_ROOT/target. Build the plugin first." echo "Error: No plugin jar found in $PROJECT_ROOT/build/libs. Build the plugin first."
exit 1 exit 1
fi fi

View File

@@ -42,8 +42,4 @@ ensure_java_25() {
ensure_java_25 ensure_java_25
cd "$PROJECT_ROOT" cd "$PROJECT_ROOT"
if [[ -x ./mvnw ]]; then exec ./gradlew publishToMavenLocal "$@"
./mvnw install -DskipTests "$@"
else
mvn install -DskipTests "$@"
fi

View File

@@ -4,30 +4,99 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
# shellcheck source=setup-env.sh if [[ ! -r "$PROJECT_ROOT/.env" ]]; then
source "$SCRIPT_DIR/setup-env.sh" 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_TOKEN="${MODRINTH_TOKEN:-}"
MODRINTH_PROJECT_ID="${MODRINTH_PROJECT_ID:-${MODRINTH_PROJECT_SLUG:-metropolis-builder}}" MODRINTH_PROJECT_ID="${MODRINTH_PROJECT_ID:-metropolis-builder}"
MODRINTH_VERSION="${MODRINTH_VERSION:-${VERSION:-$(grep -E '<version>|<project>' pom.xml | head -n2 | tail -n1 | sed -E 's#.*<version>([^<]+)</version>.*#\1#')}}" 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_CHANGELOG="${MODRINTH_CHANGELOG:-Auto release via publish-modrinth.sh}"
MODRINTH_GAME_VERSIONS="${MODRINTH_GAME_VERSIONS:-1.21,1.21.1,1.21.2,1.21.3,1.21.4,1.21.5,1.21.6,1.21.7}" MODRINTH_GAME_VERSIONS="${MODRINTH_GAME_VERSIONS:-26.2}"
MODRINTH_LOADERS="${MODRINTH_LOADERS:-paper}" MODRINTH_LOADERS="${MODRINTH_LOADERS:-paper}"
MODRINTH_VERSION_TYPE="${MODRINTH_VERSION_TYPE:-}"
MODRINTH_FILE="${MODRINTH_FILE:-}" MODRINTH_FILE="${MODRINTH_FILE:-}"
MODRINTH_DEPENDENCIES="${MODRINTH_DEPENDENCIES:-}" 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 if [[ -z "$MODRINTH_TOKEN" ]]; then
echo "Error: MODRINTH_TOKEN is not set in $PROJECT_ROOT/.env" >&2 echo "Error: MODRINTH_TOKEN is not set in $PROJECT_ROOT/.env" >&2
exit 1 exit 1
fi fi
if [[ -z "$MODRINTH_FILE" ]]; then if [[ -z "$MODRINTH_FILE" ]]; then
MODRINTH_FILE=$(find "$PROJECT_ROOT/target" -maxdepth 1 -type f -name '*.jar' ! -name '*-sources.jar' ! -name '*-javadoc.jar' | head -n1 || true) MODRINTH_FILE="$PROJECT_ROOT/build/libs/Metropolis-$MODRINTH_VERSION.jar"
fi fi
if [[ -z "$MODRINTH_FILE" || ! -f "$MODRINTH_FILE" ]]; then 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 echo "Error: no jar found for Modrinth upload. Build the project first or set MODRINTH_FILE." >&2
exit 1 exit 1
fi 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() { csv_to_json_array() {
jq -cn --arg csv "$1" '$csv | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))' jq -cn --arg csv "$1" '$csv | split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))'
@@ -36,19 +105,41 @@ MODRINTH_GAME_VERSIONS_JSON=$(csv_to_json_array "$MODRINTH_GAME_VERSIONS")
MODRINTH_LOADERS_JSON=$(csv_to_json_array "$MODRINTH_LOADERS") MODRINTH_LOADERS_JSON=$(csv_to_json_array "$MODRINTH_LOADERS")
if [[ -n "$MODRINTH_DEPENDENCIES" ]]; then if [[ -n "$MODRINTH_DEPENDENCIES" ]]; then
MODRINTH_DEPENDENCIES_ARG=(--form "dependencies=$MODRINTH_DEPENDENCIES") 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 else
MODRINTH_DEPENDENCIES_ARG=() MODRINTH_DEPENDENCIES_JSON='[]'
fi fi
curl -fsSL \ MODRINTH_DATA=$(jq -cn \
-X POST "https://api.modrinth.com/v2/version" \ --arg project_id "$MODRINTH_PROJECT_ID" \
-H "Authorization: $MODRINTH_TOKEN" \ --arg name "$MODRINTH_VERSION_NAME" \
-F "project_id=$MODRINTH_PROJECT_ID" \ --arg version_number "$MODRINTH_VERSION" \
-F "version_number=$MODRINTH_VERSION" \ --arg version_type "$MODRINTH_VERSION_TYPE" \
-F "changelog=$MODRINTH_CHANGELOG" \ --arg changelog "$MODRINTH_CHANGELOG" \
-F "game_versions=${MODRINTH_GAME_VERSIONS_JSON}" \ --argjson featured false \
-F "loaders=${MODRINTH_LOADERS_JSON}" \ --argjson dependencies "$MODRINTH_DEPENDENCIES_JSON" \
-F "file=@$MODRINTH_FILE" \ --argjson game_versions "$MODRINTH_GAME_VERSIONS_JSON" \
"${MODRINTH_DEPENDENCIES_ARG[@]}" \ --argjson loaders "$MODRINTH_LOADERS_JSON" \
| jq . '{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 .

View File

@@ -9,8 +9,47 @@
set -euo pipefail set -euo pipefail
TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=setup-env.sh PROJECT_ROOT="$(cd "$TOOLS_DIR/.." && pwd)"
source "$TOOLS_DIR/setup-env.sh" 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 ASSUME_YES=false
SAFETY_BACKUP=true SAFETY_BACKUP=true

View File

@@ -1,39 +0,0 @@
#!/usr/bin/env bash
# Source (bash or zsh) to export the shared server config from ../.env
# (relative to this script):
# PAPER_VERSION, PAPER_BUILD, MINECRAFT_SERVER_PATH, MINECRAFT_SERVER_JAR, MINECRAFT_BACKUP_PATH
_setup_env_self="${BASH_SOURCE[0]:-${(%):-%x}}"
_setup_env_root="$(cd "$(dirname "$_setup_env_self")/.." && pwd)"
_setup_env_file="${MINECRAFT_ENV_FILE:-$_setup_env_root/.env}"
if [[ -f "$_setup_env_file" ]]; then
while IFS= read -r _line || [[ -n "$_line" ]]; do
_line="${_line%$'\r'}"
[[ "$_line" =~ ^[[:space:]]*(#|$) ]] && continue
_key="${_line%%=*}"
_value="${_line#*=}"
_key="${_key//[[:space:]]/}"
[[ "$_key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
_value="${_value#\"}"; _value="${_value%\"}"
_value="${_value#\'}"; _value="${_value%\'}"
eval "_current=\"\${$_key:-}\""
[[ -z "$_current" ]] && export "$_key=$_value"
done < "$_setup_env_file"
fi
PAPER_VERSION="${PAPER_VERSION:-}"
PAPER_BUILD="${PAPER_BUILD:-}"
MINECRAFT_SERVER_PATH="${MINECRAFT_SERVER_PATH:-MCServer}"
MINECRAFT_SERVER_PATH="${MINECRAFT_SERVER_PATH/#\~/$HOME}"
[[ "$MINECRAFT_SERVER_PATH" == /* ]] || MINECRAFT_SERVER_PATH="$_setup_env_root/$MINECRAFT_SERVER_PATH"
MINECRAFT_BACKUP_PATH="${MINECRAFT_BACKUP_PATH:-backups}"
MINECRAFT_BACKUP_PATH="${MINECRAFT_BACKUP_PATH/#\~/$HOME}"
[[ "$MINECRAFT_BACKUP_PATH" == /* ]] || MINECRAFT_BACKUP_PATH="$_setup_env_root/$MINECRAFT_BACKUP_PATH"
if [[ -z "${MINECRAFT_SERVER_JAR:-}" && -n "$PAPER_VERSION" && -n "$PAPER_BUILD" ]]; then
MINECRAFT_SERVER_JAR="paper-${PAPER_VERSION}-${PAPER_BUILD}.jar"
fi
export PAPER_VERSION PAPER_BUILD MINECRAFT_SERVER_PATH MINECRAFT_SERVER_JAR MINECRAFT_BACKUP_PATH
unset _setup_env_self _setup_env_root _setup_env_file _line _key _value _current

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Downloads (if missing) and runs a Paper server via the Fill v3 API. # Downloads (if missing) and runs a Paper server via the Fill v3 API.
# Server settings come from ../.env relative to this script (see setup-env.sh). # Server settings come from the project .env file.
# #
# Env vars: # Env vars:
# JAVA_OPTS Extra JVM args (default: -Xms2G -Xmx2G) # JAVA_OPTS Extra JVM args (default: -Xms2G -Xmx2G)
@@ -8,8 +8,47 @@
set -euo pipefail set -euo pipefail
TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=setup-env.sh PROJECT_ROOT="$(cd "$TOOLS_DIR/.." && pwd)"
source "$TOOLS_DIR/setup-env.sh" 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; } [[ -n "$PAPER_VERSION" ]] || { echo "Error: PAPER_VERSION is not set (see .env)." >&2; exit 1; }