Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b3eaad253 | ||
|
|
29413d36b3 | ||
|
|
b6bbf9183a | ||
|
|
b2ba4d5821 |
@@ -17,7 +17,7 @@ jobs:
|
|||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
java-version: 17
|
java-version: 25
|
||||||
cache: gradle
|
cache: gradle
|
||||||
|
|
||||||
- name: Validate conventional commits
|
- name: Validate conventional commits
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
java-version: 17
|
java-version: 25
|
||||||
cache: gradle
|
cache: gradle
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Spigot Base
|
# Spigot Base
|
||||||
|
|
||||||
A Spigot 26.2 plugin providing progression-gated player bases and quality-of-life unlocks.
|
A Purpur 26.2 plugin providing progression-gated player bases and quality-of-life unlocks.
|
||||||
|
|
||||||
Players begin by breaking Survival-mode grass blocks or dirt. The progression paths provide a persistent base, navigation particles, expanding base bounds, controlled flight, personal teleportation, and visitor teleportation.
|
Players begin by breaking Survival-mode grass blocks or dirt. The progression paths provide a persistent base, navigation particles, expanding base bounds, controlled flight, personal teleportation, and visitor teleportation.
|
||||||
|
|
||||||
@@ -8,8 +8,8 @@ The approved behavior is specified in the [OKF design bundle](design/index.md).
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Spigot 26.2
|
- Purpur 26.2 build 2618
|
||||||
- Java 17 or newer
|
- Java 25 or newer
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
@@ -26,10 +26,11 @@ The plugin JAR is written to `build/libs/`.
|
|||||||
/base (alias: /home)
|
/base (alias: /home)
|
||||||
/gotobase <player> (alias: /visit <player>)
|
/gotobase <player> (alias: /visit <player>)
|
||||||
/basesettings (alias: /homesettings)
|
/basesettings (alias: /homesettings)
|
||||||
|
/basesettings ui
|
||||||
/basesettings status
|
/basesettings status
|
||||||
/basesettings upgrade
|
/basesettings upgrade
|
||||||
/basesettings pocket upgrade
|
/basesettings pocket upgrade
|
||||||
/basesettings pocket mobs <enable|disable>
|
/basesettings pocket mobs <hostile|passive> <enable|disable>
|
||||||
/basesettings pocket type <void|nether|overworld> <subtype>
|
/basesettings pocket type <void|nether|overworld> <subtype>
|
||||||
/basesettings visitors <allowed|blocked>
|
/basesettings visitors <allowed|blocked>
|
||||||
/basesettings navigation <enable|disable>
|
/basesettings navigation <enable|disable>
|
||||||
@@ -39,13 +40,15 @@ The plugin JAR is written to `build/libs/`.
|
|||||||
/basesettings bossbar <enable|disable>
|
/basesettings bossbar <enable|disable>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`/basesettings` and `/basesettings ui` open Minecraft's native dialog dashboard. The dashboard summarizes Base and Pocket Base progression, provides unlocked setting controls and purchase confirmations, and marks unavailable controls as locked. `/basesettings status` retains the detailed chat report.
|
||||||
|
|
||||||
Navigation particles appear only in the base's world and when the player is more than 25 blocks beyond the current base border.
|
Navigation particles appear only in the base's world and when the player is more than 25 blocks beyond the current base border.
|
||||||
|
|
||||||
After 250 Survival-mode block placements anywhere by default, the spawnable overlay can mark nearby dark hostile-mob spawning surfaces inside the player's base with owner-only red particles. The threshold is configurable.
|
After 250 Survival-mode block placements anywhere by default, the spawnable overlay can mark nearby dark hostile-mob spawning surfaces inside the player's base with owner-only red particles. The threshold is configurable.
|
||||||
|
|
||||||
`/base` has a stationary warm-up. Looking around is allowed, while movement between blocks, damage, teleportation, world changes, death, logout, and conflicting teleport commands cancel it without consuming the cooldown.
|
`/base` has a stationary warm-up. Looking around is allowed, while movement between blocks, damage, teleportation, world changes, death, logout, and conflicting teleport commands cancel it without consuming the cooldown.
|
||||||
|
|
||||||
Base IV owners can purchase and expand a persistent Pocket Base with `/basesettings pocket upgrade`. Activating a complete diamond-block portal frame inside the normal base with flint and steel opens one public entrance to the owner's grass platform in a private void world. The owner can move the return portal by building and igniting another complete frame inside the unlocked Pocket Base boundary; only the newly activated return portal remains functional. Natural hostile and passive mob spawning is disabled by default and can be controlled by the owner with `/basesettings pocket mobs <enable|disable>`.
|
Base IV owners can purchase and expand a persistent Pocket Base with `/basesettings pocket upgrade`. Activating a complete diamond-block portal frame inside the normal base with flint and steel opens one public entrance to the owner's grass platform in a private void world. Players and non-player living mobs can travel through the entrance and active return portal; items, projectiles, and vehicles are not transported. The owner can move the return portal by building and igniting another complete frame inside the unlocked Pocket Base boundary; only the newly activated return portal remains functional. Natural hostile and passive mob spawning is disabled by default. Owners can control each category independently with `/basesettings pocket mobs <hostile|passive> <enable|disable>`.
|
||||||
|
|
||||||
Owners can change biome metadata without altering Pocket Base blocks, entities, inventories, or portals by using `/basesettings pocket type <void|nether|overworld> <subtype>`. Void uses `the_void`; Nether and Overworld accept their compatible vanilla biome names, such as `crimson_forest` or `plains`. Each change defaults to 16 netherite blocks, is configurable, and affects applicable natural mob selection when spawning is enabled. The Pocket Base remains a void-generated normal-environment world; dimension-specific mechanics do not change.
|
Owners can change biome metadata without altering Pocket Base blocks, entities, inventories, or portals by using `/basesettings pocket type <void|nether|overworld> <subtype>`. Void uses `the_void`; Nether and Overworld accept their compatible vanilla biome names, such as `crimson_forest` or `plains`. Each change defaults to 16 netherite blocks, is configurable, and affects applicable natural mob selection when spawning is enabled. The Pocket Base remains a void-generated normal-environment world; dimension-specific mechanics do not change.
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -9,23 +9,23 @@ version = providers.gradleProperty("releaseVersion")
|
|||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/")
|
maven("https://repo.purpurmc.org/snapshots/")
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
toolchain {
|
toolchain {
|
||||||
languageVersion = JavaLanguageVersion.of(17)
|
languageVersion = JavaLanguageVersion.of(25)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<JavaCompile>().configureEach {
|
tasks.withType<JavaCompile>().configureEach {
|
||||||
options.compilerArgs.addAll(listOf("-Xlint:all", "-Werror"))
|
options.compilerArgs.addAll(listOf("-Xlint:all,-deprecation,-removal", "-Werror"))
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly("org.spigotmc:spigot-api:26.2-R0.1-SNAPSHOT")
|
compileOnly("org.purpurmc.purpur:purpur-api:26.2.build.2618-stable")
|
||||||
|
|
||||||
testImplementation("org.spigotmc:spigot-api:26.2-R0.1-SNAPSHOT")
|
testImplementation("org.purpurmc.purpur:purpur-api:26.2.build.2618-stable")
|
||||||
testImplementation(platform("org.junit:junit-bom:5.13.4"))
|
testImplementation(platform("org.junit:junit-bom:5.13.4"))
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter")
|
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||||
testImplementation("org.mockito:mockito-core:5.18.0")
|
testImplementation("org.mockito:mockito-core:5.18.0")
|
||||||
|
|||||||
+25
-2
@@ -95,8 +95,9 @@ description: Chronological record of material decisions affecting the Spigot Bas
|
|||||||
## 2026-08-24 — Pocket Base mob-spawning control
|
## 2026-08-24 — Pocket Base mob-spawning control
|
||||||
|
|
||||||
- Disabled natural hostile and passive mob spawning by default in new and existing Pocket Bases without a saved preference.
|
- Disabled natural hostile and passive mob spawning by default in new and existing Pocket Bases without a saved preference.
|
||||||
- Added the persisted owner setting `/basesettings pocket mobs enable|disable`, contextual autocomplete, and status reporting.
|
- Added independent persisted owner settings through `/basesettings pocket mobs <hostile|passive> <enable|disable>`, contextual autocomplete, and status reporting.
|
||||||
- Reapplied saved spawn flags when Pocket Base worlds load and restored the prior world setting when persistence fails.
|
- Migrated each legacy combined mob-spawning preference to both categories to preserve existing behavior.
|
||||||
|
- Reapplied both saved spawn flags when Pocket Base worlds load and restored both prior world settings when persistence fails.
|
||||||
- Verified the feature with `./gradlew clean check jar`.
|
- Verified the feature with `./gradlew clean check jar`.
|
||||||
|
|
||||||
## 2026-08-24 — Relocatable Pocket Base return portals
|
## 2026-08-24 — Relocatable Pocket Base return portals
|
||||||
@@ -113,3 +114,25 @@ description: Chronological record of material decisions affecting the Spigot Bas
|
|||||||
- Applied biome metadata across unlocked columns and new expansion rings without changing blocks, entities, inventories, portals, progression, or mob-spawning preferences.
|
- Applied biome metadata across unlocked columns and new expansion rings without changing blocks, entities, inventories, portals, progression, or mob-spawning preferences.
|
||||||
- Added an atomic, configurable biome-change payment that defaults to 16 netherite blocks and restores the prior biome and payment on failure.
|
- Added an atomic, configurable biome-change payment that defaults to 16 netherite blocks and restores the prior biome and payment on failure.
|
||||||
- Verified the feature with `./gradlew clean check jar`.
|
- Verified the feature with `./gradlew clean check jar`.
|
||||||
|
|
||||||
|
## 2026-08-24 — Native base settings dialog
|
||||||
|
|
||||||
|
- Changed `/basesettings` and `/homesettings` with no arguments to open a native Minecraft dialog dashboard, with explicit `ui` subcommands providing the same behavior.
|
||||||
|
- Added progression summaries, current and locked setting states, toggles, Pocket Base controls, biome selection, and confirmations for Base IV, Pocket Base, and biome purchases.
|
||||||
|
- Retained the detailed chat report under `/basesettings status` and kept dialog actions routed through the existing validated command behavior.
|
||||||
|
- Added dialog routing, autocomplete, alias, non-player, generated-control, locked-state, and purchase-confirmation coverage.
|
||||||
|
- Verified the feature with `./gradlew clean check jar`.
|
||||||
|
|
||||||
|
## 2026-08-24 — Purpur dialog runtime compatibility
|
||||||
|
|
||||||
|
- Replaced the Spigot-only Bungee dialog types that were absent from production with Purpur's supported Paper dialog API.
|
||||||
|
- Compiled and tested against the exact Purpur 26.2 build 2618 API with Java 25, and updated both CI workflows to use Java 25.
|
||||||
|
- Preserved unit-test coverage through server-independent dialog specifications and adapted Bukkit test fixtures to Purpur's registry-aware API behavior.
|
||||||
|
- Verified 128 tests and the Java 25 plugin artifact with `./gradlew clean check jar`.
|
||||||
|
|
||||||
|
## 2026-08-24 — Pocket Base mob portal travel
|
||||||
|
|
||||||
|
- Added five-tick collision scanning that transports non-player living mobs through active Pocket Base entrance and return portals.
|
||||||
|
- Reused safe player destinations and server-spawn fallback behavior while excluding items, projectiles, vehicles, and other non-living entities.
|
||||||
|
- Applied the existing two-second portal cooldown to mobs to prevent immediate repeated transport.
|
||||||
|
- Verified the feature and Java 25 plugin artifact with `./gradlew clean check jar`.
|
||||||
|
|||||||
@@ -25,3 +25,4 @@ description: Catalog of user stories for the Spigot Base plugin.
|
|||||||
17. [US-017: Expand a Pocket Base](us-017-expand-a-pocket-base.md)
|
17. [US-017: Expand a Pocket Base](us-017-expand-a-pocket-base.md)
|
||||||
18. [US-018: Control Pocket Base mob spawning](us-018-control-pocket-base-mob-spawning.md)
|
18. [US-018: Control Pocket Base mob spawning](us-018-control-pocket-base-mob-spawning.md)
|
||||||
19. [US-019: Change a Pocket Base biome](us-019-change-a-pocket-base-biome.md)
|
19. [US-019: Change a Pocket Base biome](us-019-change-a-pocket-base-biome.md)
|
||||||
|
20. [US-020: Control a base through a dialog UI](us-020-control-base-through-dialog-ui.md)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ As a **player**, I want to inspect my progression and receive timely milestone f
|
|||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
- [x] `/basesettings` and `/basesettings status` show the player's Base, Base Size, Base Flight, Teleport Warm-up, and Teleport Cooldown paths plus current visitor, navigation, flight, and boss-bar settings.
|
- [x] `/basesettings status` shows the player's Base, Base Size, Base Flight, Teleport Warm-up, and Teleport Cooldown paths plus current visitor, navigation, flight, and boss-bar settings.
|
||||||
- [x] Each path identifies earned levels, unmet prerequisites, current progress, the next threshold, and the next reward.
|
- [x] Each path identifies earned levels, unmet prerequisites, current progress, the next threshold, and the next reward.
|
||||||
- [x] Locked secondary paths clearly identify Base I or another sequential level as their prerequisite.
|
- [x] Locked secondary paths clearly identify Base I or another sequential level as their prerequisite.
|
||||||
- [x] Relevant qualifying activity briefly displays a configurable progress boss bar for the active milestone.
|
- [x] Relevant qualifying activity briefly displays a configurable progress boss bar for the active milestone.
|
||||||
@@ -19,7 +19,7 @@ As a **player**, I want to inspect my progression and receive timely milestone f
|
|||||||
- [x] The automatic boss bar disappears after a configurable number of seconds.
|
- [x] The automatic boss bar disappears after a configurable number of seconds.
|
||||||
- [x] `/basesettings bossbar enable` enables automatic progress boss bars idempotently, and `/basesettings bossbar disable` disables them idempotently.
|
- [x] `/basesettings bossbar enable` enables automatic progress boss bars idempotently, and `/basesettings bossbar disable` disables them idempotently.
|
||||||
- [x] Invalid boss-bar arguments show command usage, and `enable` and `disable` are offered through autocomplete.
|
- [x] Invalid boss-bar arguments show command usage, and `enable` and `disable` are offered through autocomplete.
|
||||||
- [x] Disabling automatic boss bars does not prevent `/basesettings` from displaying progress.
|
- [x] Disabling automatic boss bars does not prevent `/basesettings status` from displaying progress.
|
||||||
- [x] The boss-bar preference persists across reconnects and restarts.
|
- [x] The boss-bar preference persists across reconnects and restarts.
|
||||||
- [x] Each newly unlocked level displays prominent full-screen title and subtitle text describing the reward.
|
- [x] Each newly unlocked level displays prominent full-screen title and subtitle text describing the reward.
|
||||||
- [x] Unlock notifications occur once per earned level and do not repeat after reconnecting or restarting.
|
- [x] Unlock notifications occur once per earned level and do not repeat after reconnecting or restarting.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
type: User Story
|
type: User Story
|
||||||
title: "US-012: Build and release the plugin"
|
title: "US-012: Build and release the plugin"
|
||||||
description: Give maintainers repeatable Spigot builds, automated verification, and versioned Gitea releases.
|
description: Give maintainers repeatable Purpur builds, automated verification, and versioned Gitea releases.
|
||||||
status: done
|
status: done
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ As a **plugin maintainer**, I want automated builds and releases modeled on Trig
|
|||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
- [x] The Gradle project compiles against Spigot API `26.2-R0.1-SNAPSHOT` using a Java 17 toolchain.
|
- [x] The Gradle project compiles against Purpur API `26.2.build.2618-stable` using a Java 25 toolchain.
|
||||||
- [x] Compiler lint warnings fail the build.
|
- [x] Compiler lint warnings fail the build.
|
||||||
- [x] Automated JUnit 5 tests run as part of the Gradle check lifecycle.
|
- [x] Automated JUnit 5 tests run as part of the Gradle check lifecycle.
|
||||||
- [x] Pushes and pull requests build and test the plugin in Gitea Actions.
|
- [x] Pushes and pull requests build and test the plugin in Gitea Actions.
|
||||||
|
|||||||
@@ -19,8 +19,13 @@ As a **Pocket Base owner**, I want to connect my normal base to my Pocket Base w
|
|||||||
- [x] Activating another valid entrance deactivates the previous entrance without removing its frame blocks.
|
- [x] Activating another valid entrance deactivates the previous entrance without removing its frame blocks.
|
||||||
- [x] Breaking any required frame block immediately deactivates the entrance.
|
- [x] Breaking any required frame block immediately deactivates the entrance.
|
||||||
- [x] Any player can use an active entrance without a separate visitor setting, warm-up, or cooldown.
|
- [x] Any player can use an active entrance without a separate visitor setting, warm-up, or cooldown.
|
||||||
|
- [x] Non-player living mobs entering an active entrance portal are teleported into that Pocket Base.
|
||||||
|
- [x] Portal cooldown handling prevents transported mobs from immediately bouncing back.
|
||||||
|
- [x] Items, projectiles, vehicles, and other non-living entities are not transported.
|
||||||
- [x] Visitors can build, break blocks, open containers, and otherwise interact normally inside the Pocket Base.
|
- [x] Visitors can build, break blocks, open containers, and otherwise interact normally inside the Pocket Base.
|
||||||
- [x] The generated return portal sends players safely to the owner's active entrance.
|
- [x] The generated return portal sends players safely to the owner's active entrance.
|
||||||
|
- [x] Non-player living mobs entering the active, intact return portal are teleported safely outside the normal-world entrance.
|
||||||
|
- [x] Mob return travel falls back safely to the server spawn when the owner has no valid normal-world entrance.
|
||||||
- [x] Only the owner can activate a complete portal frame inside their own Pocket Base with flint and steel.
|
- [x] Only the owner can activate a complete portal frame inside their own Pocket Base with flint and steel.
|
||||||
- [x] Every required return-frame block must be within the owner's unlocked Pocket Base boundary.
|
- [x] Every required return-frame block must be within the owner's unlocked Pocket Base boundary.
|
||||||
- [x] Each Pocket Base has at most one active return portal, and activating another valid frame moves the active return location without removing the old frame.
|
- [x] Each Pocket Base has at most one active return portal, and activating another valid frame moves the active return location without removing the old frame.
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
---
|
---
|
||||||
type: User Story
|
type: User Story
|
||||||
title: "US-018: Control Pocket Base mob spawning"
|
title: "US-018: Control Pocket Base mob spawning"
|
||||||
description: Let Pocket Base owners control natural mob spawning in their private world, with spawning disabled by default.
|
description: Let Pocket Base owners control hostile and passive natural mob spawning independently in their private world, with both disabled by default.
|
||||||
status: done
|
status: done
|
||||||
---
|
---
|
||||||
|
|
||||||
# US-018: Control Pocket Base mob spawning
|
# US-018: Control Pocket Base mob spawning
|
||||||
|
|
||||||
As a **Pocket Base owner**, I want to enable or disable natural mob spawning in my Pocket Base so that I can choose whether the private world supports normal mob activity.
|
As a **Pocket Base owner**, I want to enable or disable hostile and passive natural mob spawning independently in my Pocket Base so that I can choose which kinds of normal mob activity its private world supports.
|
||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
- [x] Natural mob spawning is disabled by default in every new Pocket Base and in existing Pocket Bases that have no saved preference.
|
- [x] Hostile and passive natural mob spawning are both disabled by default in every new Pocket Base and in existing Pocket Bases that have no saved preference.
|
||||||
- [x] A Pocket Base owner can use `/basesettings pocket mobs enable|disable` to control natural mob spawning in their own Pocket Base.
|
- [x] A Pocket Base owner can use `/basesettings pocket mobs hostile enable|disable` to control hostile natural mob spawning in their own Pocket Base.
|
||||||
- [x] The setting controls both hostile and passive natural spawning only in the owner's Pocket Base.
|
- [x] A Pocket Base owner can use `/basesettings pocket mobs passive enable|disable` to control passive natural mob spawning in their own Pocket Base.
|
||||||
- [x] The command is unavailable until Pocket Base I is unlocked.
|
- [x] Changing one mob category does not change the other category.
|
||||||
- [x] The command and its modes are offered through contextual autocomplete.
|
- [x] The commands are unavailable until Pocket Base I is unlocked.
|
||||||
- [x] `/basesettings status` displays the current Pocket Base mob-spawning setting.
|
- [x] The commands, categories, and modes are offered through contextual autocomplete.
|
||||||
- [x] The preference persists across restarts and is reapplied when the Pocket Base world loads.
|
- [x] `/basesettings status` displays the current hostile and passive Pocket Base mob-spawning settings.
|
||||||
- [x] Disabling natural spawning does not remove existing mobs or prevent explicitly spawned or summoned mobs.
|
- [x] Both preferences persist across restarts and are reapplied when the Pocket Base world loads.
|
||||||
- [x] Persistence or world-application failures produce a clear failure message without reporting a successful change.
|
- [x] Existing saved `mob-spawning-enabled` values migrate to both category preferences, preserving prior behavior.
|
||||||
|
- [x] Disabling either natural-spawning category does not remove existing mobs or prevent explicitly spawned or summoned mobs.
|
||||||
|
- [x] Persistence or world-application failures produce a clear failure message, restore both prior world flags, and do not report a successful change.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
type: User Story
|
||||||
|
title: "US-020: Control a base through a dialog UI"
|
||||||
|
description: Let players inspect and control their base through Minecraft's native dialog interface.
|
||||||
|
status: in-progress
|
||||||
|
---
|
||||||
|
|
||||||
|
# US-020: Control a base through a dialog UI
|
||||||
|
|
||||||
|
As a **player**, I want a dialog-based base dashboard so that I can inspect progression and manage my base without remembering command arguments.
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- [x] `/basesettings` and `/basesettings ui` open the base-control dialog.
|
||||||
|
- [x] `/homesettings` and `/homesettings ui` behave identically.
|
||||||
|
- [x] `/basesettings status` retains the chat-based progression report.
|
||||||
|
- [x] The dialog summarizes current Base and Pocket Base progression.
|
||||||
|
- [x] Dialog controls cover visitors, navigation, flight, border, spawnable overlay, boss bar, Pocket Base mob spawning, upgrades, and biome selection when applicable.
|
||||||
|
- [x] Controls indicate their current or locked state.
|
||||||
|
- [x] Purchases require confirmation and continue using existing validation, payment, and persistence logic.
|
||||||
|
- [x] `ui` is offered through command autocomplete.
|
||||||
|
- [x] Non-player execution remains safely rejected.
|
||||||
|
- [x] Automated tests verify dialog routing and generated controls.
|
||||||
|
- [ ] The dialog uses Purpur's supported Paper dialog API and opens on the production Purpur 26.2 runtime without missing-class or command exceptions.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [US-008: Unlock Base IV visitor access](us-008-unlock-visitor-access.md)
|
||||||
|
- [US-009: View progression and unlock notifications](us-009-view-progression-and-notifications.md)
|
||||||
|
- [US-015: Unlock a Pocket Base](us-015-unlock-a-pocket-base.md)
|
||||||
|
- [US-018: Control Pocket Base mob spawning](us-018-control-pocket-base-mob-spawning.md)
|
||||||
|
- [US-019: Change a Pocket Base biome](us-019-change-a-pocket-base-biome.md)
|
||||||
@@ -1 +1,5 @@
|
|||||||
|
plugins {
|
||||||
|
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
rootProject.name = "spigot-base"
|
rootProject.name = "spigot-base"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package games.dmg.spigotbase;
|
package games.dmg.spigotbase;
|
||||||
|
|
||||||
|
import io.papermc.paper.dialog.Dialog;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@@ -15,7 +16,7 @@ import org.bukkit.inventory.PlayerInventory;
|
|||||||
|
|
||||||
final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
||||||
private static final List<String> SETTINGS = List.of(
|
private static final List<String> SETTINGS = List.of(
|
||||||
"status", "upgrade", "pocket", "visitors", "navigation", "flight", "border",
|
"ui", "status", "upgrade", "pocket", "visitors", "navigation", "flight", "border",
|
||||||
"spawnable", "bossbar"
|
"spawnable", "bossbar"
|
||||||
);
|
);
|
||||||
private static final List<String> VISITOR_MODES = List.of("allowed", "blocked");
|
private static final List<String> VISITOR_MODES = List.of("allowed", "blocked");
|
||||||
@@ -28,6 +29,7 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
|||||||
private final TeleportPolicy teleportPolicy;
|
private final TeleportPolicy teleportPolicy;
|
||||||
private final BaseFlightController flightController;
|
private final BaseFlightController flightController;
|
||||||
private final PocketBaseManager pocketBases;
|
private final PocketBaseManager pocketBases;
|
||||||
|
private final DialogProvider dialogProvider;
|
||||||
|
|
||||||
BaseSettingsCommand(
|
BaseSettingsCommand(
|
||||||
BaseStateManager stateManager,
|
BaseStateManager stateManager,
|
||||||
@@ -42,12 +44,30 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
|||||||
PluginSettingsProvider settings,
|
PluginSettingsProvider settings,
|
||||||
BaseFlightController flightController,
|
BaseFlightController flightController,
|
||||||
PocketBaseManager pocketBases
|
PocketBaseManager pocketBases
|
||||||
|
) {
|
||||||
|
this(
|
||||||
|
stateManager,
|
||||||
|
settings,
|
||||||
|
flightController,
|
||||||
|
pocketBases,
|
||||||
|
(owner, pocket) -> new BaseSettingsDialogFactory(settings.current())
|
||||||
|
.create(owner, pocket)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
BaseSettingsCommand(
|
||||||
|
BaseStateManager stateManager,
|
||||||
|
PluginSettingsProvider settings,
|
||||||
|
BaseFlightController flightController,
|
||||||
|
PocketBaseManager pocketBases,
|
||||||
|
DialogProvider dialogProvider
|
||||||
) {
|
) {
|
||||||
this.stateManager = stateManager;
|
this.stateManager = stateManager;
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
this.teleportPolicy = new TeleportPolicy(settings);
|
this.teleportPolicy = new TeleportPolicy(settings);
|
||||||
this.flightController = flightController;
|
this.flightController = flightController;
|
||||||
this.pocketBases = pocketBases;
|
this.pocketBases = pocketBases;
|
||||||
|
this.dialogProvider = dialogProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -58,7 +78,11 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
|||||||
}
|
}
|
||||||
PlayerState state = stateManager.player(player.getUniqueId(), player.getName());
|
PlayerState state = stateManager.player(player.getUniqueId(), player.getName());
|
||||||
if (arguments.length == 0
|
if (arguments.length == 0
|
||||||
|| arguments.length == 1 && arguments[0].equalsIgnoreCase("status")) {
|
|| arguments.length == 1 && arguments[0].equalsIgnoreCase("ui")) {
|
||||||
|
showDialog(player, state);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (arguments.length == 1 && arguments[0].equalsIgnoreCase("status")) {
|
||||||
showStatus(player, state);
|
showStatus(player, state);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -69,9 +93,9 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
|||||||
&& arguments[1].equalsIgnoreCase("upgrade")) {
|
&& arguments[1].equalsIgnoreCase("upgrade")) {
|
||||||
return purchasePocketUpgrade(player, state);
|
return purchasePocketUpgrade(player, state);
|
||||||
}
|
}
|
||||||
if (arguments.length == 3 && arguments[0].equalsIgnoreCase("pocket")
|
if (arguments.length == 4 && arguments[0].equalsIgnoreCase("pocket")
|
||||||
&& arguments[1].equalsIgnoreCase("mobs")) {
|
&& arguments[1].equalsIgnoreCase("mobs")) {
|
||||||
return updatePocketMobSpawning(player, arguments[2]);
|
return updatePocketMobSpawning(player, arguments[2], arguments[3]);
|
||||||
}
|
}
|
||||||
if (arguments.length == 4 && arguments[0].equalsIgnoreCase("pocket")
|
if (arguments.length == 4 && arguments[0].equalsIgnoreCase("pocket")
|
||||||
&& arguments[1].equalsIgnoreCase("type")) {
|
&& arguments[1].equalsIgnoreCase("type")) {
|
||||||
@@ -240,7 +264,11 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean updatePocketMobSpawning(Player player, String mode) {
|
private boolean updatePocketMobSpawning(
|
||||||
|
Player player,
|
||||||
|
String category,
|
||||||
|
String mode
|
||||||
|
) {
|
||||||
if (pocketBases == null) {
|
if (pocketBases == null) {
|
||||||
player.sendMessage(ChatColor.RED + "Pocket Bases are currently unavailable.");
|
player.sendMessage(ChatColor.RED + "Pocket Bases are currently unavailable.");
|
||||||
return true;
|
return true;
|
||||||
@@ -256,17 +284,30 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
final PocketBaseState updated;
|
final PocketBaseState updated;
|
||||||
|
final boolean settingEnabled;
|
||||||
try {
|
try {
|
||||||
updated = pocketBases.setMobSpawning(player.getUniqueId(), enabled);
|
if (category.equalsIgnoreCase("hostile")) {
|
||||||
|
updated = pocketBases.setHostileMobSpawning(
|
||||||
|
player.getUniqueId(), enabled
|
||||||
|
);
|
||||||
|
settingEnabled = updated.hostileMobSpawningEnabled();
|
||||||
|
} else if (category.equalsIgnoreCase("passive")) {
|
||||||
|
updated = pocketBases.setPassiveMobSpawning(
|
||||||
|
player.getUniqueId(), enabled
|
||||||
|
);
|
||||||
|
settingEnabled = updated.passiveMobSpawningEnabled();
|
||||||
|
} else {
|
||||||
|
sendUsage(player);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
} catch (IOException | RuntimeException exception) {
|
} catch (IOException | RuntimeException exception) {
|
||||||
player.sendMessage(ChatColor.RED
|
player.sendMessage(ChatColor.RED
|
||||||
+ "The Pocket Base mob-spawning setting could not be changed.");
|
+ "The Pocket Base mob-spawning setting could not be changed.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
player.sendMessage(ChatColor.YELLOW + "Pocket Base mob spawning is now "
|
player.sendMessage(ChatColor.YELLOW + "Pocket Base "
|
||||||
+ (updated.mobSpawningEnabled()
|
+ category.toLowerCase(Locale.ROOT) + " mob spawning is now "
|
||||||
? ChatColor.GREEN + "enabled"
|
+ (settingEnabled ? ChatColor.GREEN + "enabled" : ChatColor.RED + "disabled")
|
||||||
: ChatColor.RED + "disabled")
|
|
||||||
+ ChatColor.YELLOW + ".");
|
+ ChatColor.YELLOW + ".");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -424,6 +465,13 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void showDialog(Player player, PlayerState state) {
|
||||||
|
PocketBaseState pocket = pocketBases == null
|
||||||
|
? PocketBaseState.locked(state.playerId())
|
||||||
|
: pocketBases.state(state.playerId());
|
||||||
|
player.showDialog(dialogProvider.create(state, pocket));
|
||||||
|
}
|
||||||
|
|
||||||
private void showStatus(Player player, PlayerState state) {
|
private void showStatus(Player player, PlayerState state) {
|
||||||
player.sendMessage(ChatColor.GOLD + "=== Base Progress ===");
|
player.sendMessage(ChatColor.GOLD + "=== Base Progress ===");
|
||||||
showBasePath(player, state);
|
showBasePath(player, state);
|
||||||
@@ -514,8 +562,10 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
|||||||
+ settings.current().pocketBaseCurrencyMaterial().toLowerCase(Locale.ROOT)
|
+ settings.current().pocketBaseCurrencyMaterial().toLowerCase(Locale.ROOT)
|
||||||
+ " → /basesettings pocket upgrade; type="
|
+ " → /basesettings pocket upgrade; type="
|
||||||
+ pocket.biome().worldType().commandName() + "/" + pocket.biome().commandName()
|
+ pocket.biome().worldType().commandName() + "/" + pocket.biome().commandName()
|
||||||
+ "; mob spawning="
|
+ "; hostile spawning="
|
||||||
+ (pocket.mobSpawningEnabled() ? "enabled" : "disabled"));
|
+ (pocket.hostileMobSpawningEnabled() ? "enabled" : "disabled")
|
||||||
|
+ "; passive spawning="
|
||||||
|
+ (pocket.passiveMobSpawningEnabled() ? "enabled" : "disabled"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showCooldownPath(Player player, PlayerState state) {
|
private void showCooldownPath(Player player, PlayerState state) {
|
||||||
@@ -557,7 +607,9 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
|||||||
if (arguments.length == 3 && arguments[0].equalsIgnoreCase("pocket")) {
|
if (arguments.length == 3 && arguments[0].equalsIgnoreCase("pocket")) {
|
||||||
String prefix = arguments[2].toLowerCase(Locale.ROOT);
|
String prefix = arguments[2].toLowerCase(Locale.ROOT);
|
||||||
if (arguments[1].equalsIgnoreCase("mobs")) {
|
if (arguments[1].equalsIgnoreCase("mobs")) {
|
||||||
return ENABLE_MODES.stream().filter(mode -> mode.startsWith(prefix)).toList();
|
return List.of("hostile", "passive").stream()
|
||||||
|
.filter(category -> category.startsWith(prefix))
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
if (arguments[1].equalsIgnoreCase("type")) {
|
if (arguments[1].equalsIgnoreCase("type")) {
|
||||||
return POCKET_WORLD_TYPES.stream()
|
return POCKET_WORLD_TYPES.stream()
|
||||||
@@ -565,15 +617,23 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (arguments.length == 4 && arguments[0].equalsIgnoreCase("pocket")
|
if (arguments.length == 4 && arguments[0].equalsIgnoreCase("pocket")) {
|
||||||
&& arguments[1].equalsIgnoreCase("type")) {
|
|
||||||
String prefix = arguments[3].toLowerCase(Locale.ROOT);
|
String prefix = arguments[3].toLowerCase(Locale.ROOT);
|
||||||
return PocketBaseWorldType.fromCommand(arguments[2])
|
if (arguments[1].equalsIgnoreCase("mobs")
|
||||||
.map(PocketBaseBiome::commandNames)
|
&& (arguments[2].equalsIgnoreCase("hostile")
|
||||||
.orElse(List.of())
|
|| arguments[2].equalsIgnoreCase("passive"))) {
|
||||||
.stream()
|
return ENABLE_MODES.stream()
|
||||||
.filter(biome -> biome.startsWith(prefix))
|
.filter(mode -> mode.startsWith(prefix))
|
||||||
.toList();
|
.toList();
|
||||||
|
}
|
||||||
|
if (arguments[1].equalsIgnoreCase("type")) {
|
||||||
|
return PocketBaseWorldType.fromCommand(arguments[2])
|
||||||
|
.map(PocketBaseBiome::commandNames)
|
||||||
|
.orElse(List.of())
|
||||||
|
.stream()
|
||||||
|
.filter(biome -> biome.startsWith(prefix))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
@@ -632,9 +692,15 @@ final class BaseSettingsCommand implements CommandExecutor, TabCompleter {
|
|||||||
return met ? ChatColor.YELLOW : ChatColor.RED;
|
return met ? ChatColor.YELLOW : ChatColor.RED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
interface DialogProvider {
|
||||||
|
Dialog create(PlayerState owner, PocketBaseState pocket);
|
||||||
|
}
|
||||||
|
|
||||||
private static void sendUsage(Player player) {
|
private static void sendUsage(Player player) {
|
||||||
player.sendMessage(ChatColor.RED + "Usage: /basesettings "
|
player.sendMessage(ChatColor.RED + "Usage: /basesettings "
|
||||||
+ "[status|upgrade|pocket upgrade|pocket mobs <enable|disable>"
|
+ "[ui|status|upgrade|pocket upgrade|pocket mobs "
|
||||||
|
+ "<hostile|passive> <enable|disable>"
|
||||||
+ "|pocket type <void|nether|overworld> <subtype>"
|
+ "|pocket type <void|nether|overworld> <subtype>"
|
||||||
+ "|visitors <allowed|blocked>|navigation <enable|disable>"
|
+ "|visitors <allowed|blocked>|navigation <enable|disable>"
|
||||||
+ "|flight <enable|disable>|border <enable|disable>"
|
+ "|flight <enable|disable>|border <enable|disable>"
|
||||||
|
|||||||
@@ -0,0 +1,432 @@
|
|||||||
|
package games.dmg.spigotbase;
|
||||||
|
|
||||||
|
import io.papermc.paper.dialog.Dialog;
|
||||||
|
import io.papermc.paper.registry.RegistryKey;
|
||||||
|
import io.papermc.paper.registry.data.dialog.ActionButton;
|
||||||
|
import io.papermc.paper.registry.data.dialog.DialogBase;
|
||||||
|
import io.papermc.paper.registry.data.dialog.action.DialogAction;
|
||||||
|
import io.papermc.paper.registry.data.dialog.body.DialogBody;
|
||||||
|
import io.papermc.paper.registry.data.dialog.type.DialogType;
|
||||||
|
import io.papermc.paper.registry.set.RegistrySet;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
|
||||||
|
final class BaseSettingsDialogFactory {
|
||||||
|
private static final int DIALOG_WIDTH = 420;
|
||||||
|
private final PluginSettings settings;
|
||||||
|
|
||||||
|
BaseSettingsDialogFactory(PluginSettings settings) {
|
||||||
|
this.settings = settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog create(PlayerState owner, PocketBaseState pocket) {
|
||||||
|
return render(specification(owner, pocket));
|
||||||
|
}
|
||||||
|
|
||||||
|
DialogSpec specification(PlayerState owner, PocketBaseState pocket) {
|
||||||
|
List<DialogSpec> dialogs = List.of(
|
||||||
|
generalSettings(owner),
|
||||||
|
progressReport(),
|
||||||
|
baseUpgrade(owner),
|
||||||
|
pocketSettings(owner, pocket)
|
||||||
|
);
|
||||||
|
return new ListSpec(
|
||||||
|
content(
|
||||||
|
"Base Settings",
|
||||||
|
"Base Settings",
|
||||||
|
"Base " + owner.baseLevel() + "/4 • Size " + owner.sizeLevel() + "/3"
|
||||||
|
+ " • Flight " + owner.flightLevel() + "/3\n"
|
||||||
|
+ "Pocket Base " + pocket.level() + " • "
|
||||||
|
+ humanize(pocket.biome().commandName())
|
||||||
|
),
|
||||||
|
dialogs,
|
||||||
|
2,
|
||||||
|
190
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DialogSpec generalSettings(PlayerState owner) {
|
||||||
|
List<ButtonSpec> actions = List.of(
|
||||||
|
toggle(
|
||||||
|
"Visitors",
|
||||||
|
owner.baseLevel() >= 4,
|
||||||
|
owner.visitorsEnabled(),
|
||||||
|
"Allowed",
|
||||||
|
"Blocked",
|
||||||
|
"basesettings visitors "
|
||||||
|
+ (owner.visitorsEnabled() ? "blocked" : "allowed")
|
||||||
|
),
|
||||||
|
toggle(
|
||||||
|
"Navigation",
|
||||||
|
owner.baseLevel() >= 2,
|
||||||
|
owner.navigationEnabled(),
|
||||||
|
"Enabled",
|
||||||
|
"Disabled",
|
||||||
|
"basesettings navigation " + mode(!owner.navigationEnabled())
|
||||||
|
),
|
||||||
|
toggle(
|
||||||
|
"Flight",
|
||||||
|
owner.flightLevel() >= 1,
|
||||||
|
owner.flightEnabled(),
|
||||||
|
"Enabled",
|
||||||
|
"Disabled",
|
||||||
|
"basesettings flight " + mode(!owner.flightEnabled())
|
||||||
|
),
|
||||||
|
toggle(
|
||||||
|
"Border",
|
||||||
|
owner.baseLevel() >= 1 && owner.base().isPresent(),
|
||||||
|
owner.borderEnabled(),
|
||||||
|
"Enabled",
|
||||||
|
"Disabled",
|
||||||
|
"basesettings border " + mode(!owner.borderEnabled())
|
||||||
|
),
|
||||||
|
toggle(
|
||||||
|
"Spawnable Overlay",
|
||||||
|
owner.baseLevel() >= 1
|
||||||
|
&& owner.base().isPresent()
|
||||||
|
&& owner.totalBlocksPlaced()
|
||||||
|
>= settings.spawnableOverlayUnlockPlacements(),
|
||||||
|
owner.spawnableOverlayEnabled(),
|
||||||
|
"Enabled",
|
||||||
|
"Disabled",
|
||||||
|
"basesettings spawnable " + mode(!owner.spawnableOverlayEnabled())
|
||||||
|
),
|
||||||
|
toggle(
|
||||||
|
"Progress Boss Bar",
|
||||||
|
true,
|
||||||
|
owner.bossBarEnabled(),
|
||||||
|
"Enabled",
|
||||||
|
"Disabled",
|
||||||
|
"basesettings bossbar " + mode(!owner.bossBarEnabled())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return new MultiSpec(
|
||||||
|
content(
|
||||||
|
"Base Controls",
|
||||||
|
"Base Controls",
|
||||||
|
"Select a setting to change it. Locked controls remain visible and explain "
|
||||||
|
+ "their requirement in chat when selected."
|
||||||
|
),
|
||||||
|
actions,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DialogSpec progressReport() {
|
||||||
|
return new NoticeSpec(
|
||||||
|
content(
|
||||||
|
"Progress Report",
|
||||||
|
"Progress Report",
|
||||||
|
"Display the detailed progression report in chat."
|
||||||
|
),
|
||||||
|
commandButton("Show in Chat", "basesettings status")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DialogSpec baseUpgrade(PlayerState owner) {
|
||||||
|
if (owner.baseLevel() >= 4) {
|
||||||
|
return notice("Base IV", "Base IV and visitor access are already unlocked.");
|
||||||
|
}
|
||||||
|
if (owner.baseLevel() < 3 || owner.base().isEmpty()) {
|
||||||
|
return notice(
|
||||||
|
"Base IV — Locked",
|
||||||
|
"Unlock and establish Base III before purchasing Base IV visitor access."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return confirmation(
|
||||||
|
"Unlock Base IV",
|
||||||
|
"Purchase visitor access for " + settings.visitorUnlockDiamondCost()
|
||||||
|
+ " diamonds?",
|
||||||
|
"basesettings upgrade"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DialogSpec pocketSettings(PlayerState owner, PocketBaseState pocket) {
|
||||||
|
List<DialogSpec> dialogs = List.of(
|
||||||
|
pocketUpgrade(owner, pocket),
|
||||||
|
pocketMobSettings(pocket),
|
||||||
|
pocketBiomeSettings(pocket)
|
||||||
|
);
|
||||||
|
return new ListSpec(
|
||||||
|
content(
|
||||||
|
"Pocket Base",
|
||||||
|
"Pocket Base " + pocket.level(),
|
||||||
|
pocket.level() == 0
|
||||||
|
? "Pocket Base is locked."
|
||||||
|
: "Biome: " + pocket.biome().worldType().commandName() + "/"
|
||||||
|
+ pocket.biome().commandName() + "\nHostile mobs: "
|
||||||
|
+ enabled(pocket.hostileMobSpawningEnabled()) + " • Passive mobs: "
|
||||||
|
+ enabled(pocket.passiveMobSpawningEnabled())
|
||||||
|
),
|
||||||
|
dialogs,
|
||||||
|
1,
|
||||||
|
260
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DialogSpec pocketUpgrade(PlayerState owner, PocketBaseState pocket) {
|
||||||
|
if (owner.baseLevel() < 4 || owner.base().isEmpty()) {
|
||||||
|
return notice(
|
||||||
|
"Pocket Upgrade — Locked",
|
||||||
|
"Establish Base IV before purchasing a Pocket Base."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
int price = pocket.level() == 0
|
||||||
|
? settings.pocketBaseUnlockCost()
|
||||||
|
: settings.pocketBaseUpgradeCost();
|
||||||
|
String currency = humanize(settings.pocketBaseCurrencyMaterial());
|
||||||
|
String title = pocket.level() == 0
|
||||||
|
? "Unlock Pocket Base I"
|
||||||
|
: "Upgrade to Pocket Base " + (pocket.level() + 1);
|
||||||
|
return confirmation(
|
||||||
|
title,
|
||||||
|
"Purchase this upgrade for " + price + " " + currency + "?",
|
||||||
|
"basesettings pocket upgrade"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DialogSpec pocketMobSettings(PocketBaseState pocket) {
|
||||||
|
if (pocket.level() < 1) {
|
||||||
|
return notice(
|
||||||
|
"Mob Spawning — Locked",
|
||||||
|
"Unlock Pocket Base I to control natural mob spawning."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
List<ButtonSpec> actions = List.of(
|
||||||
|
toggle(
|
||||||
|
"Hostile Mobs",
|
||||||
|
true,
|
||||||
|
pocket.hostileMobSpawningEnabled(),
|
||||||
|
"Enabled",
|
||||||
|
"Disabled",
|
||||||
|
"basesettings pocket mobs hostile "
|
||||||
|
+ mode(!pocket.hostileMobSpawningEnabled())
|
||||||
|
),
|
||||||
|
toggle(
|
||||||
|
"Passive Mobs",
|
||||||
|
true,
|
||||||
|
pocket.passiveMobSpawningEnabled(),
|
||||||
|
"Enabled",
|
||||||
|
"Disabled",
|
||||||
|
"basesettings pocket mobs passive "
|
||||||
|
+ mode(!pocket.passiveMobSpawningEnabled())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return new MultiSpec(
|
||||||
|
content(
|
||||||
|
"Mob Spawning",
|
||||||
|
"Mob Spawning",
|
||||||
|
"Control natural spawning independently for each mob category."
|
||||||
|
),
|
||||||
|
actions,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DialogSpec pocketBiomeSettings(PocketBaseState pocket) {
|
||||||
|
if (pocket.level() < 1) {
|
||||||
|
return notice(
|
||||||
|
"Change Biome — Locked",
|
||||||
|
"Unlock Pocket Base I to purchase biome changes."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
List<DialogSpec> worldTypes = Arrays.stream(PocketBaseWorldType.values())
|
||||||
|
.map(type -> pocketBiomeType(pocket, type))
|
||||||
|
.toList();
|
||||||
|
return new ListSpec(
|
||||||
|
content(
|
||||||
|
"Change Biome",
|
||||||
|
"Change Biome",
|
||||||
|
"Current biome: " + pocket.biome().worldType().commandName() + "/"
|
||||||
|
+ pocket.biome().commandName() + "\nEach change costs "
|
||||||
|
+ settings.pocketBaseBiomeChangeCost() + " "
|
||||||
|
+ humanize(settings.pocketBaseBiomeCurrencyMaterial()) + "."
|
||||||
|
),
|
||||||
|
worldTypes,
|
||||||
|
1,
|
||||||
|
220
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DialogSpec pocketBiomeType(
|
||||||
|
PocketBaseState pocket,
|
||||||
|
PocketBaseWorldType worldType
|
||||||
|
) {
|
||||||
|
List<DialogSpec> biomes = Arrays.stream(PocketBaseBiome.values())
|
||||||
|
.filter(biome -> biome.worldType() == worldType)
|
||||||
|
.map(biome -> biomeConfirmation(pocket, biome))
|
||||||
|
.toList();
|
||||||
|
return new ListSpec(
|
||||||
|
content(
|
||||||
|
humanize(worldType.commandName()),
|
||||||
|
humanize(worldType.commandName()) + " Biomes",
|
||||||
|
"Choose a biome subtype. The active biome is marked as selected."
|
||||||
|
),
|
||||||
|
biomes,
|
||||||
|
2,
|
||||||
|
190
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DialogSpec biomeConfirmation(PocketBaseState pocket, PocketBaseBiome biome) {
|
||||||
|
String title = humanize(biome.commandName())
|
||||||
|
+ (pocket.biome() == biome ? " — Selected" : "");
|
||||||
|
if (pocket.biome() == biome) {
|
||||||
|
return notice(title, "This is your Pocket Base's current biome.");
|
||||||
|
}
|
||||||
|
return confirmation(
|
||||||
|
title,
|
||||||
|
"Change the Pocket Base biome for " + settings.pocketBaseBiomeChangeCost()
|
||||||
|
+ " " + humanize(settings.pocketBaseBiomeCurrencyMaterial()) + "?",
|
||||||
|
"basesettings pocket type " + biome.worldType().commandName() + " "
|
||||||
|
+ biome.commandName()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DialogSpec confirmation(String title, String message, String command) {
|
||||||
|
return new ConfirmationSpec(
|
||||||
|
content(title, title, message),
|
||||||
|
commandButton("Confirm", command)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DialogSpec notice(String title, String message) {
|
||||||
|
return new NoticeSpec(content(title, title, message), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ButtonSpec toggle(
|
||||||
|
String name,
|
||||||
|
boolean unlocked,
|
||||||
|
boolean active,
|
||||||
|
String activeLabel,
|
||||||
|
String inactiveLabel,
|
||||||
|
String command
|
||||||
|
) {
|
||||||
|
String state = unlocked ? (active ? activeLabel : inactiveLabel) : "Locked";
|
||||||
|
String tooltip = unlocked
|
||||||
|
? "Click to change this setting."
|
||||||
|
: "This setting has not been unlocked yet.";
|
||||||
|
return new ButtonSpec(name + ": " + state, tooltip, 190, "/" + command);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ButtonSpec commandButton(String label, String command) {
|
||||||
|
return new ButtonSpec(label, "", 150, "/" + command);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DialogContent content(
|
||||||
|
String title,
|
||||||
|
String externalTitle,
|
||||||
|
String message
|
||||||
|
) {
|
||||||
|
return new DialogContent(title, externalTitle, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dialog render(DialogSpec specification) {
|
||||||
|
DialogType type;
|
||||||
|
if (specification instanceof ListSpec list) {
|
||||||
|
List<Dialog> dialogs = list.dialogs().stream()
|
||||||
|
.map(BaseSettingsDialogFactory::render)
|
||||||
|
.toList();
|
||||||
|
type = DialogType.dialogList(
|
||||||
|
RegistrySet.valueSet(RegistryKey.DIALOG, dialogs),
|
||||||
|
null,
|
||||||
|
list.columns(),
|
||||||
|
list.buttonWidth()
|
||||||
|
);
|
||||||
|
} else if (specification instanceof MultiSpec multi) {
|
||||||
|
List<ActionButton> actions = multi.actions().stream()
|
||||||
|
.map(BaseSettingsDialogFactory::render)
|
||||||
|
.toList();
|
||||||
|
type = DialogType.multiAction(actions, null, multi.columns());
|
||||||
|
} else if (specification instanceof ConfirmationSpec confirmation) {
|
||||||
|
ActionButton cancel = ActionButton.builder(Component.text("Cancel")).build();
|
||||||
|
type = DialogType.confirmation(render(confirmation.confirm()), cancel);
|
||||||
|
} else if (specification instanceof NoticeSpec notice) {
|
||||||
|
type = notice.action() == null
|
||||||
|
? DialogType.notice()
|
||||||
|
: DialogType.notice(render(notice.action()));
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Unsupported dialog specification");
|
||||||
|
}
|
||||||
|
DialogContent content = specification.content();
|
||||||
|
DialogBase base = DialogBase.builder(Component.text(content.title()))
|
||||||
|
.externalTitle(Component.text(content.externalTitle()))
|
||||||
|
.body(List.of(DialogBody.plainMessage(
|
||||||
|
Component.text(content.message()), DIALOG_WIDTH
|
||||||
|
)))
|
||||||
|
.canCloseWithEscape(true)
|
||||||
|
.pause(false)
|
||||||
|
.afterAction(DialogBase.DialogAfterAction.CLOSE)
|
||||||
|
.build();
|
||||||
|
return Dialog.create(factory -> factory.empty().base(base).type(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ActionButton render(ButtonSpec button) {
|
||||||
|
ActionButton.Builder builder = ActionButton.builder(Component.text(button.label()))
|
||||||
|
.width(button.width())
|
||||||
|
.action(DialogAction.commandTemplate(button.command()));
|
||||||
|
if (!button.tooltip().isEmpty()) {
|
||||||
|
builder.tooltip(Component.text(button.tooltip()));
|
||||||
|
}
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface DialogSpec permits ListSpec, MultiSpec, ConfirmationSpec, NoticeSpec {
|
||||||
|
DialogContent content();
|
||||||
|
}
|
||||||
|
|
||||||
|
record DialogContent(String title, String externalTitle, String message) {
|
||||||
|
}
|
||||||
|
|
||||||
|
record ListSpec(
|
||||||
|
DialogContent content,
|
||||||
|
List<DialogSpec> dialogs,
|
||||||
|
int columns,
|
||||||
|
int buttonWidth
|
||||||
|
) implements DialogSpec {
|
||||||
|
}
|
||||||
|
|
||||||
|
record MultiSpec(
|
||||||
|
DialogContent content,
|
||||||
|
List<ButtonSpec> actions,
|
||||||
|
int columns
|
||||||
|
) implements DialogSpec {
|
||||||
|
}
|
||||||
|
|
||||||
|
record ConfirmationSpec(
|
||||||
|
DialogContent content,
|
||||||
|
ButtonSpec confirm
|
||||||
|
) implements DialogSpec {
|
||||||
|
}
|
||||||
|
|
||||||
|
record NoticeSpec(
|
||||||
|
DialogContent content,
|
||||||
|
ButtonSpec action
|
||||||
|
) implements DialogSpec {
|
||||||
|
}
|
||||||
|
|
||||||
|
record ButtonSpec(String label, String tooltip, int width, String command) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String mode(boolean active) {
|
||||||
|
return active ? "enable" : "disable";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String enabled(boolean active) {
|
||||||
|
return active ? "enabled" : "disabled";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String humanize(String value) {
|
||||||
|
String normalized = value.toLowerCase(Locale.ROOT).replace('_', ' ');
|
||||||
|
StringBuilder result = new StringBuilder(normalized.length());
|
||||||
|
boolean capitalize = true;
|
||||||
|
for (int index = 0; index < normalized.length(); index++) {
|
||||||
|
char character = normalized.charAt(index);
|
||||||
|
result.append(capitalize ? Character.toUpperCase(character) : character);
|
||||||
|
capitalize = character == ' ';
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import java.util.HashMap;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
import org.bukkit.ChatColor;
|
import org.bukkit.ChatColor;
|
||||||
@@ -15,6 +16,7 @@ import org.bukkit.Server;
|
|||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.Block;
|
||||||
import org.bukkit.entity.Entity;
|
import org.bukkit.entity.Entity;
|
||||||
|
import org.bukkit.entity.Mob;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.event.EventHandler;
|
import org.bukkit.event.EventHandler;
|
||||||
import org.bukkit.event.EventPriority;
|
import org.bukkit.event.EventPriority;
|
||||||
@@ -26,6 +28,7 @@ import org.bukkit.event.entity.EntityDamageEvent;
|
|||||||
import org.bukkit.event.player.PlayerInteractEvent;
|
import org.bukkit.event.player.PlayerInteractEvent;
|
||||||
import org.bukkit.event.player.PlayerMoveEvent;
|
import org.bukkit.event.player.PlayerMoveEvent;
|
||||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||||
|
import org.bukkit.util.BoundingBox;
|
||||||
|
|
||||||
final class PocketBaseController implements Listener, Runnable {
|
final class PocketBaseController implements Listener, Runnable {
|
||||||
private static final int VOID_RETURN_Y = -64;
|
private static final int VOID_RETURN_Y = -64;
|
||||||
@@ -63,6 +66,7 @@ final class PocketBaseController implements Listener, Runnable {
|
|||||||
showPortal(pocketBases.returnPortal(state.ownerId()));
|
showPortal(pocketBases.returnPortal(state.ownerId()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
scanMobPortals();
|
||||||
long now = System.nanoTime();
|
long now = System.nanoTime();
|
||||||
cooldownUntil.entrySet().removeIf(entry -> entry.getValue() <= now);
|
cooldownUntil.entrySet().removeIf(entry -> entry.getValue() <= now);
|
||||||
}
|
}
|
||||||
@@ -235,23 +239,61 @@ final class PocketBaseController implements Listener, Runnable {
|
|||||||
deactivate(ownerId);
|
deactivate(ownerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void leavePocket(Player player, UUID ownerId) {
|
void scanMobPortals() {
|
||||||
|
for (PocketBaseState state : pocketBases.knownStates().values()) {
|
||||||
|
if (state.entrance().isPresent()) {
|
||||||
|
PocketPortalLocation entrance = state.entrance().orElseThrow();
|
||||||
|
if (isIntact(entrance)) {
|
||||||
|
transportMobs(
|
||||||
|
entrance,
|
||||||
|
mob -> teleport(mob, pocketBases.pocketArrival(state.ownerId()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (state.level() > 0 && pocketBases.returnPortalIsIntact(state.ownerId())) {
|
||||||
|
transportMobs(
|
||||||
|
pocketBases.returnPortal(state.ownerId()),
|
||||||
|
mob -> leavePocket(mob, state.ownerId())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void transportMobs(
|
||||||
|
PocketPortalLocation portal,
|
||||||
|
Consumer<Mob> transport
|
||||||
|
) {
|
||||||
|
World world = server.getWorld(portal.worldId());
|
||||||
|
if (world == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (Entity entity : world.getNearbyEntities(
|
||||||
|
portalBounds(portal),
|
||||||
|
candidate -> candidate instanceof Mob
|
||||||
|
)) {
|
||||||
|
if (entity instanceof Mob mob && !onCooldown(mob)) {
|
||||||
|
transport.accept(mob);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void leavePocket(Entity entity, UUID ownerId) {
|
||||||
PocketBaseState state = pocketBases.state(ownerId);
|
PocketBaseState state = pocketBases.state(ownerId);
|
||||||
if (state.entrance().isPresent() && isIntact(state.entrance().orElseThrow())) {
|
if (state.entrance().isPresent() && isIntact(state.entrance().orElseThrow())) {
|
||||||
teleport(player, outsidePortal(state.entrance().orElseThrow()));
|
teleport(entity, outsidePortal(state.entrance().orElseThrow()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
World fallbackWorld = server.getWorlds().get(0);
|
World fallbackWorld = server.getWorlds().get(0);
|
||||||
teleport(player, fallbackWorld.getSpawnLocation());
|
teleport(entity, fallbackWorld.getSpawnLocation());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void teleport(Player player, Location destination) {
|
private void teleport(Entity entity, Location destination) {
|
||||||
cooldownUntil.put(player.getUniqueId(), System.nanoTime() + PORTAL_COOLDOWN_NANOS);
|
cooldownUntil.put(entity.getUniqueId(), System.nanoTime() + PORTAL_COOLDOWN_NANOS);
|
||||||
player.teleport(destination, PlayerTeleportEvent.TeleportCause.PLUGIN);
|
entity.teleport(destination, PlayerTeleportEvent.TeleportCause.PLUGIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean onCooldown(Player player) {
|
private boolean onCooldown(Entity entity) {
|
||||||
return cooldownUntil.getOrDefault(player.getUniqueId(), 0L) > System.nanoTime();
|
return cooldownUntil.getOrDefault(entity.getUniqueId(), 0L) > System.nanoTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateEntrances() {
|
private void validateEntrances() {
|
||||||
@@ -320,6 +362,26 @@ final class PocketBaseController implements Listener, Runnable {
|
|||||||
: new Location(world, portal.x() + 1.5, portal.y() + 1.0, portal.z() + 2.0);
|
: new Location(world, portal.x() + 1.5, portal.y() + 1.0, portal.z() + 2.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static BoundingBox portalBounds(PocketPortalLocation portal) {
|
||||||
|
return portal.axis() == PocketPortalAxis.X
|
||||||
|
? new BoundingBox(
|
||||||
|
portal.x() + 1.0,
|
||||||
|
portal.y() + 1.0,
|
||||||
|
portal.z(),
|
||||||
|
portal.x() + 3.0,
|
||||||
|
portal.y() + 4.0,
|
||||||
|
portal.z() + 1.0
|
||||||
|
)
|
||||||
|
: new BoundingBox(
|
||||||
|
portal.x(),
|
||||||
|
portal.y() + 1.0,
|
||||||
|
portal.z() + 1.0,
|
||||||
|
portal.x() + 1.0,
|
||||||
|
portal.y() + 4.0,
|
||||||
|
portal.z() + 3.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean isAir(Material material) {
|
private static boolean isAir(Material material) {
|
||||||
return material == Material.AIR || material == Material.CAVE_AIR
|
return material == Material.AIR || material == Material.CAVE_AIR
|
||||||
|| material == Material.VOID_AIR;
|
|| material == Material.VOID_AIR;
|
||||||
|
|||||||
@@ -92,20 +92,48 @@ final class PocketBaseManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PocketBaseState setMobSpawning(UUID ownerId, boolean enabled) throws IOException {
|
PocketBaseState setHostileMobSpawning(UUID ownerId, boolean enabled)
|
||||||
|
throws IOException {
|
||||||
PocketBaseState current = states.state(ownerId);
|
PocketBaseState current = states.state(ownerId);
|
||||||
|
return setMobSpawning(
|
||||||
|
current,
|
||||||
|
enabled,
|
||||||
|
current.passiveMobSpawningEnabled(),
|
||||||
|
state -> state.withHostileMobSpawningEnabled(enabled)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PocketBaseState setPassiveMobSpawning(UUID ownerId, boolean enabled)
|
||||||
|
throws IOException {
|
||||||
|
PocketBaseState current = states.state(ownerId);
|
||||||
|
return setMobSpawning(
|
||||||
|
current,
|
||||||
|
current.hostileMobSpawningEnabled(),
|
||||||
|
enabled,
|
||||||
|
state -> state.withPassiveMobSpawningEnabled(enabled)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PocketBaseState setMobSpawning(
|
||||||
|
PocketBaseState current,
|
||||||
|
boolean hostileEnabled,
|
||||||
|
boolean passiveEnabled,
|
||||||
|
java.util.function.UnaryOperator<PocketBaseState> update
|
||||||
|
) throws IOException {
|
||||||
if (current.level() < 1) {
|
if (current.level() < 1) {
|
||||||
throw new IllegalStateException("Pocket Base I is still locked");
|
throw new IllegalStateException("Pocket Base I is still locked");
|
||||||
}
|
}
|
||||||
worlds.setMobSpawning(ownerId, enabled);
|
UUID ownerId = current.ownerId();
|
||||||
|
worlds.setMobSpawning(ownerId, hostileEnabled, passiveEnabled);
|
||||||
try {
|
try {
|
||||||
return states.updateAndSave(
|
return states.updateAndSave(ownerId, update);
|
||||||
ownerId,
|
|
||||||
state -> state.withMobSpawningEnabled(enabled)
|
|
||||||
);
|
|
||||||
} catch (IOException | RuntimeException exception) {
|
} catch (IOException | RuntimeException exception) {
|
||||||
try {
|
try {
|
||||||
worlds.setMobSpawning(ownerId, current.mobSpawningEnabled());
|
worlds.setMobSpawning(
|
||||||
|
ownerId,
|
||||||
|
current.hostileMobSpawningEnabled(),
|
||||||
|
current.passiveMobSpawningEnabled()
|
||||||
|
);
|
||||||
} catch (RuntimeException rollbackFailure) {
|
} catch (RuntimeException rollbackFailure) {
|
||||||
exception.addSuppressed(rollbackFailure);
|
exception.addSuppressed(rollbackFailure);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ public record PocketBaseState(
|
|||||||
int level,
|
int level,
|
||||||
Optional<PocketPortalLocation> entrance,
|
Optional<PocketPortalLocation> entrance,
|
||||||
Optional<PocketPortalLocation> returnPortal,
|
Optional<PocketPortalLocation> returnPortal,
|
||||||
boolean mobSpawningEnabled,
|
boolean hostileMobSpawningEnabled,
|
||||||
|
boolean passiveMobSpawningEnabled,
|
||||||
PocketBaseBiome biome
|
PocketBaseBiome biome
|
||||||
) {
|
) {
|
||||||
public PocketBaseState {
|
public PocketBaseState {
|
||||||
@@ -32,73 +33,64 @@ public record PocketBaseState(
|
|||||||
Optional<PocketPortalLocation> entrance
|
Optional<PocketPortalLocation> entrance
|
||||||
) {
|
) {
|
||||||
this(
|
this(
|
||||||
ownerId, level, entrance, Optional.empty(), false, PocketBaseBiome.THE_VOID
|
ownerId, level, entrance, Optional.empty(), false, false,
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public PocketBaseState(
|
|
||||||
UUID ownerId,
|
|
||||||
int level,
|
|
||||||
Optional<PocketPortalLocation> entrance,
|
|
||||||
boolean mobSpawningEnabled
|
|
||||||
) {
|
|
||||||
this(
|
|
||||||
ownerId, level, entrance, Optional.empty(), mobSpawningEnabled,
|
|
||||||
PocketBaseBiome.THE_VOID
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public PocketBaseState(
|
|
||||||
UUID ownerId,
|
|
||||||
int level,
|
|
||||||
Optional<PocketPortalLocation> entrance,
|
|
||||||
Optional<PocketPortalLocation> returnPortal,
|
|
||||||
boolean mobSpawningEnabled
|
|
||||||
) {
|
|
||||||
this(
|
|
||||||
ownerId, level, entrance, returnPortal, mobSpawningEnabled,
|
|
||||||
PocketBaseBiome.THE_VOID
|
PocketBaseBiome.THE_VOID
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PocketBaseState locked(UUID ownerId) {
|
public static PocketBaseState locked(UUID ownerId) {
|
||||||
return new PocketBaseState(
|
return new PocketBaseState(
|
||||||
ownerId, 0, Optional.empty(), Optional.empty(), false,
|
ownerId, 0, Optional.empty(), Optional.empty(), false, false,
|
||||||
PocketBaseBiome.THE_VOID
|
PocketBaseBiome.THE_VOID
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PocketBaseState withLevel(int newLevel) {
|
public PocketBaseState withLevel(int newLevel) {
|
||||||
return new PocketBaseState(
|
return new PocketBaseState(
|
||||||
ownerId, newLevel, entrance, returnPortal, mobSpawningEnabled, biome
|
ownerId, newLevel, entrance, returnPortal, hostileMobSpawningEnabled,
|
||||||
|
passiveMobSpawningEnabled, biome
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PocketBaseState withEntrance(PocketPortalLocation portal) {
|
public PocketBaseState withEntrance(PocketPortalLocation portal) {
|
||||||
return new PocketBaseState(
|
return new PocketBaseState(
|
||||||
ownerId, level, Optional.of(portal), returnPortal, mobSpawningEnabled, biome
|
ownerId, level, Optional.of(portal), returnPortal,
|
||||||
|
hostileMobSpawningEnabled, passiveMobSpawningEnabled, biome
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PocketBaseState withoutEntrance() {
|
public PocketBaseState withoutEntrance() {
|
||||||
return new PocketBaseState(
|
return new PocketBaseState(
|
||||||
ownerId, level, Optional.empty(), returnPortal, mobSpawningEnabled, biome
|
ownerId, level, Optional.empty(), returnPortal, hostileMobSpawningEnabled,
|
||||||
|
passiveMobSpawningEnabled, biome
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PocketBaseState withReturnPortal(PocketPortalLocation portal) {
|
public PocketBaseState withReturnPortal(PocketPortalLocation portal) {
|
||||||
return new PocketBaseState(
|
return new PocketBaseState(
|
||||||
ownerId, level, entrance, Optional.of(portal), mobSpawningEnabled, biome
|
ownerId, level, entrance, Optional.of(portal), hostileMobSpawningEnabled,
|
||||||
|
passiveMobSpawningEnabled, biome
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PocketBaseState withMobSpawningEnabled(boolean enabled) {
|
public PocketBaseState withHostileMobSpawningEnabled(boolean enabled) {
|
||||||
return new PocketBaseState(ownerId, level, entrance, returnPortal, enabled, biome);
|
return new PocketBaseState(
|
||||||
|
ownerId, level, entrance, returnPortal, enabled,
|
||||||
|
passiveMobSpawningEnabled, biome
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PocketBaseState withPassiveMobSpawningEnabled(boolean enabled) {
|
||||||
|
return new PocketBaseState(
|
||||||
|
ownerId, level, entrance, returnPortal, hostileMobSpawningEnabled,
|
||||||
|
enabled, biome
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PocketBaseState withBiome(PocketBaseBiome selectedBiome) {
|
public PocketBaseState withBiome(PocketBaseBiome selectedBiome) {
|
||||||
return new PocketBaseState(
|
return new PocketBaseState(
|
||||||
ownerId, level, entrance, returnPortal, mobSpawningEnabled, selectedBiome
|
ownerId, level, entrance, returnPortal, hostileMobSpawningEnabled,
|
||||||
|
passiveMobSpawningEnabled, selectedBiome
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,11 @@ final class PocketBaseWorldService {
|
|||||||
for (PocketBaseState state : states.values()) {
|
for (PocketBaseState state : states.values()) {
|
||||||
if (state.level() > 0) {
|
if (state.level() > 0) {
|
||||||
setBiome(state.ownerId(), state.level(), state.biome());
|
setBiome(state.ownerId(), state.level(), state.biome());
|
||||||
setMobSpawning(state.ownerId(), state.mobSpawningEnabled());
|
setMobSpawning(
|
||||||
|
state.ownerId(),
|
||||||
|
state.hostileMobSpawningEnabled(),
|
||||||
|
state.passiveMobSpawningEnabled()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,8 +95,12 @@ final class PocketBaseWorldService {
|
|||||||
return ensureWorld(ownerId);
|
return ensureWorld(ownerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setMobSpawning(UUID ownerId, boolean enabled) {
|
void setMobSpawning(
|
||||||
ensureWorld(ownerId).setSpawnFlags(enabled, enabled);
|
UUID ownerId,
|
||||||
|
boolean hostileEnabled,
|
||||||
|
boolean passiveEnabled
|
||||||
|
) {
|
||||||
|
ensureWorld(ownerId).setSpawnFlags(hostileEnabled, passiveEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setBiome(UUID ownerId, int level, PocketBaseBiome biome) {
|
void setBiome(UUID ownerId, int level, PocketBaseBiome biome) {
|
||||||
@@ -129,7 +137,7 @@ final class PocketBaseWorldService {
|
|||||||
World world = server.getWorld(name);
|
World world = server.getWorld(name);
|
||||||
boolean created = world == null;
|
boolean created = world == null;
|
||||||
if (created) {
|
if (created) {
|
||||||
WorldCreator creator = new WorldCreator(name)
|
WorldCreator creator = new WorldCreator(NamespacedKey.minecraft(name))
|
||||||
.environment(World.Environment.NORMAL)
|
.environment(World.Environment.NORMAL)
|
||||||
.generator(new VoidPocketChunkGenerator())
|
.generator(new VoidPocketChunkGenerator())
|
||||||
.generateStructures(false);
|
.generateStructures(false);
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ public final class YamlPocketBaseRepository {
|
|||||||
level,
|
level,
|
||||||
loadPortal(yaml, path + ".entrance"),
|
loadPortal(yaml, path + ".entrance"),
|
||||||
loadPortal(yaml, path + ".return-portal"),
|
loadPortal(yaml, path + ".return-portal"),
|
||||||
yaml.getBoolean(path + ".mob-spawning-enabled", false),
|
loadMobPreference(yaml, path, "hostile-mob-spawning-enabled"),
|
||||||
|
loadMobPreference(yaml, path, "passive-mob-spawning-enabled"),
|
||||||
loadBiome(yaml, path)
|
loadBiome(yaml, path)
|
||||||
);
|
);
|
||||||
states.put(ownerId, state);
|
states.put(ownerId, state);
|
||||||
@@ -64,7 +65,14 @@ public final class YamlPocketBaseRepository {
|
|||||||
for (PocketBaseState state : states.values()) {
|
for (PocketBaseState state : states.values()) {
|
||||||
String path = "owners." + state.ownerId();
|
String path = "owners." + state.ownerId();
|
||||||
yaml.set(path + ".level", state.level());
|
yaml.set(path + ".level", state.level());
|
||||||
yaml.set(path + ".mob-spawning-enabled", state.mobSpawningEnabled());
|
yaml.set(
|
||||||
|
path + ".hostile-mob-spawning-enabled",
|
||||||
|
state.hostileMobSpawningEnabled()
|
||||||
|
);
|
||||||
|
yaml.set(
|
||||||
|
path + ".passive-mob-spawning-enabled",
|
||||||
|
state.passiveMobSpawningEnabled()
|
||||||
|
);
|
||||||
yaml.set(path + ".world-type", state.biome().worldType().commandName());
|
yaml.set(path + ".world-type", state.biome().worldType().commandName());
|
||||||
yaml.set(path + ".biome", state.biome().commandName());
|
yaml.set(path + ".biome", state.biome().commandName());
|
||||||
state.entrance().ifPresent(portal -> savePortal(yaml, path + ".entrance", portal));
|
state.entrance().ifPresent(portal -> savePortal(yaml, path + ".entrance", portal));
|
||||||
@@ -90,6 +98,17 @@ public final class YamlPocketBaseRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean loadMobPreference(
|
||||||
|
YamlConfiguration yaml,
|
||||||
|
String path,
|
||||||
|
String categoryKey
|
||||||
|
) {
|
||||||
|
return yaml.getBoolean(
|
||||||
|
path + "." + categoryKey,
|
||||||
|
yaml.getBoolean(path + ".mob-spawning-enabled", false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private static PocketBaseBiome loadBiome(YamlConfiguration yaml, String path) {
|
private static PocketBaseBiome loadBiome(YamlConfiguration yaml, String path) {
|
||||||
String worldTypeName = yaml.getString(path + ".world-type");
|
String worldTypeName = yaml.getString(path + ".world-type");
|
||||||
String biomeName = yaml.getString(path + ".biome");
|
String biomeName = yaml.getString(path + ".biome");
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ commands:
|
|||||||
aliases: [visit]
|
aliases: [visit]
|
||||||
basesettings:
|
basesettings:
|
||||||
description: View progression, upgrade, and manage base settings.
|
description: View progression, upgrade, and manage base settings.
|
||||||
usage: /basesettings [status|upgrade|pocket|visitors|navigation|flight|border|spawnable|bossbar]
|
usage: /basesettings [ui|status|upgrade|pocket|visitors|navigation|flight|border|spawnable|bossbar]
|
||||||
aliases: [homesettings]
|
aliases: [homesettings]
|
||||||
baseadmin:
|
baseadmin:
|
||||||
description: Administer Spigot Base.
|
description: Administer Spigot Base.
|
||||||
|
|||||||
@@ -20,13 +20,29 @@ import java.util.concurrent.atomic.AtomicReference;
|
|||||||
import java.util.function.UnaryOperator;
|
import java.util.function.UnaryOperator;
|
||||||
import org.bukkit.ChatColor;
|
import org.bukkit.ChatColor;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
import org.bukkit.inventory.PlayerInventory;
|
import org.bukkit.inventory.PlayerInventory;
|
||||||
|
import net.kyori.adventure.dialog.DialogLike;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
|
|
||||||
final class BaseSettingsCommandTest {
|
final class BaseSettingsCommandTest {
|
||||||
|
@Test
|
||||||
|
void rejectsNonPlayerExecution() {
|
||||||
|
CommandSender sender = mock(CommandSender.class);
|
||||||
|
BaseSettingsCommand command = new BaseSettingsCommand(
|
||||||
|
mock(BaseStateManager.class),
|
||||||
|
new PluginSettingsProvider(PluginSettings.from(Map.of())),
|
||||||
|
mock(BaseFlightController.class)
|
||||||
|
);
|
||||||
|
|
||||||
|
command.onCommand(sender, null, "basesettings", new String[0]);
|
||||||
|
|
||||||
|
verify(sender).sendMessage("Only players have base progression.");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void spawnableEnableIsIdempotentAfterUnlock() {
|
void spawnableEnableIsIdempotentAfterUnlock() {
|
||||||
UUID playerId = UUID.randomUUID();
|
UUID playerId = UUID.randomUUID();
|
||||||
@@ -119,10 +135,23 @@ final class BaseSettingsCommandTest {
|
|||||||
new String[] {"pocket", "type", "nether", "c"}
|
new String[] {"pocket", "type", "nether", "c"}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
assertEquals(
|
||||||
|
List.of("hostile"),
|
||||||
|
command.onTabComplete(
|
||||||
|
null, null, "basesettings", new String[] {"pocket", "mobs", "h"}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
List.of("passive"),
|
||||||
|
command.onTabComplete(
|
||||||
|
null, null, "basesettings", new String[] {"pocket", "mobs", "p"}
|
||||||
|
)
|
||||||
|
);
|
||||||
assertEquals(
|
assertEquals(
|
||||||
List.of("enable"),
|
List.of("enable"),
|
||||||
command.onTabComplete(
|
command.onTabComplete(
|
||||||
null, null, "basesettings", new String[] {"pocket", "mobs", "e"}
|
null, null, "basesettings",
|
||||||
|
new String[] {"pocket", "mobs", "hostile", "e"}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
for (String setting : List.of("navigation", "flight", "border", "spawnable", "bossbar")) {
|
for (String setting : List.of("navigation", "flight", "border", "spawnable", "bossbar")) {
|
||||||
@@ -143,8 +172,8 @@ final class BaseSettingsCommandTest {
|
|||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
List.of(
|
List.of(
|
||||||
"status", "upgrade", "pocket", "visitors", "navigation", "flight", "border",
|
"ui", "status", "upgrade", "pocket", "visitors", "navigation", "flight",
|
||||||
"spawnable", "bossbar"
|
"border", "spawnable", "bossbar"
|
||||||
),
|
),
|
||||||
command.onTabComplete(null, null, "basesettings", new String[] {""})
|
command.onTabComplete(null, null, "basesettings", new String[] {""})
|
||||||
);
|
);
|
||||||
@@ -157,10 +186,12 @@ final class BaseSettingsCommandTest {
|
|||||||
when(player.getUniqueId()).thenReturn(playerId);
|
when(player.getUniqueId()).thenReturn(playerId);
|
||||||
when(player.getName()).thenReturn("Host");
|
when(player.getName()).thenReturn("Host");
|
||||||
PlayerInventory inventory = mock(PlayerInventory.class);
|
PlayerInventory inventory = mock(PlayerInventory.class);
|
||||||
|
ItemStack firstStack = item(Material.DIAMOND, 64);
|
||||||
|
ItemStack secondStack = item(Material.DIAMOND, 64);
|
||||||
when(player.getInventory()).thenReturn(inventory);
|
when(player.getInventory()).thenReturn(inventory);
|
||||||
when(inventory.getStorageContents()).thenReturn(new ItemStack[] {
|
when(inventory.getStorageContents()).thenReturn(new ItemStack[] {
|
||||||
new ItemStack(Material.DIAMOND, 64),
|
firstStack,
|
||||||
new ItemStack(Material.DIAMOND, 64)
|
secondStack
|
||||||
});
|
});
|
||||||
|
|
||||||
PlayerState current = PlayerState.newPlayer(playerId, "Host")
|
PlayerState current = PlayerState.newPlayer(playerId, "Host")
|
||||||
@@ -198,10 +229,9 @@ final class BaseSettingsCommandTest {
|
|||||||
when(player.getUniqueId()).thenReturn(playerId);
|
when(player.getUniqueId()).thenReturn(playerId);
|
||||||
when(player.getName()).thenReturn("Builder");
|
when(player.getName()).thenReturn("Builder");
|
||||||
PlayerInventory inventory = mock(PlayerInventory.class);
|
PlayerInventory inventory = mock(PlayerInventory.class);
|
||||||
|
ItemStack currency = item(Material.DIAMOND_BLOCK, 64);
|
||||||
when(player.getInventory()).thenReturn(inventory);
|
when(player.getInventory()).thenReturn(inventory);
|
||||||
when(inventory.getStorageContents()).thenReturn(new ItemStack[] {
|
when(inventory.getStorageContents()).thenReturn(new ItemStack[] {currency});
|
||||||
new ItemStack(Material.DIAMOND_BLOCK, 64)
|
|
||||||
});
|
|
||||||
PlayerState owner = PlayerState.newPlayer(playerId, "Builder")
|
PlayerState owner = PlayerState.newPlayer(playerId, "Builder")
|
||||||
.withAdministrativeLevels(4, 0, 0, 0, 0, false, false, true)
|
.withAdministrativeLevels(4, 0, 0, 0, 0, false, false, true)
|
||||||
.withBase(
|
.withBase(
|
||||||
@@ -235,10 +265,9 @@ final class BaseSettingsCommandTest {
|
|||||||
when(player.getUniqueId()).thenReturn(playerId);
|
when(player.getUniqueId()).thenReturn(playerId);
|
||||||
when(player.getName()).thenReturn("Builder");
|
when(player.getName()).thenReturn("Builder");
|
||||||
PlayerInventory inventory = mock(PlayerInventory.class);
|
PlayerInventory inventory = mock(PlayerInventory.class);
|
||||||
|
ItemStack currency = item(Material.NETHERITE_BLOCK, 16);
|
||||||
when(player.getInventory()).thenReturn(inventory);
|
when(player.getInventory()).thenReturn(inventory);
|
||||||
when(inventory.getStorageContents()).thenReturn(new ItemStack[] {
|
when(inventory.getStorageContents()).thenReturn(new ItemStack[] {currency});
|
||||||
new ItemStack(Material.NETHERITE_BLOCK, 16)
|
|
||||||
});
|
|
||||||
BaseStateManager stateManager = mock(BaseStateManager.class);
|
BaseStateManager stateManager = mock(BaseStateManager.class);
|
||||||
when(stateManager.player(playerId, "Builder"))
|
when(stateManager.player(playerId, "Builder"))
|
||||||
.thenReturn(PlayerState.newPlayer(playerId, "Builder"));
|
.thenReturn(PlayerState.newPlayer(playerId, "Builder"));
|
||||||
@@ -296,7 +325,7 @@ final class BaseSettingsCommandTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void pocketOwnerCanEnableNaturalMobSpawning() throws Exception {
|
void pocketOwnerCanEnableHostileMobSpawningWithoutChangingPassive() throws Exception {
|
||||||
UUID playerId = UUID.randomUUID();
|
UUID playerId = UUID.randomUUID();
|
||||||
Player player = mock(Player.class);
|
Player player = mock(Player.class);
|
||||||
when(player.getUniqueId()).thenReturn(playerId);
|
when(player.getUniqueId()).thenReturn(playerId);
|
||||||
@@ -305,11 +334,13 @@ final class BaseSettingsCommandTest {
|
|||||||
when(stateManager.player(playerId, "Builder"))
|
when(stateManager.player(playerId, "Builder"))
|
||||||
.thenReturn(PlayerState.newPlayer(playerId, "Builder"));
|
.thenReturn(PlayerState.newPlayer(playerId, "Builder"));
|
||||||
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
|
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
|
||||||
when(pocketBases.state(playerId)).thenReturn(
|
PocketBaseState current = new PocketBaseState(
|
||||||
new PocketBaseState(playerId, 1, java.util.Optional.empty(), false)
|
playerId, 1, java.util.Optional.empty(), java.util.Optional.empty(),
|
||||||
|
false, true, PocketBaseBiome.THE_VOID
|
||||||
);
|
);
|
||||||
when(pocketBases.setMobSpawning(playerId, true)).thenReturn(
|
when(pocketBases.state(playerId)).thenReturn(current);
|
||||||
new PocketBaseState(playerId, 1, java.util.Optional.empty(), true)
|
when(pocketBases.setHostileMobSpawning(playerId, true)).thenReturn(
|
||||||
|
current.withHostileMobSpawningEnabled(true)
|
||||||
);
|
);
|
||||||
BaseSettingsCommand command = new BaseSettingsCommand(
|
BaseSettingsCommand command = new BaseSettingsCommand(
|
||||||
stateManager,
|
stateManager,
|
||||||
@@ -318,13 +349,14 @@ final class BaseSettingsCommandTest {
|
|||||||
pocketBases
|
pocketBases
|
||||||
);
|
);
|
||||||
|
|
||||||
command.onCommand(
|
command.onCommand(player, null, "basesettings", new String[] {
|
||||||
player, null, "basesettings", new String[] {"pocket", "mobs", "enable"}
|
"pocket", "mobs", "hostile", "enable"
|
||||||
);
|
});
|
||||||
|
|
||||||
verify(pocketBases).setMobSpawning(playerId, true);
|
verify(pocketBases).setHostileMobSpawning(playerId, true);
|
||||||
|
verify(pocketBases, never()).setPassiveMobSpawning(playerId, true);
|
||||||
verify(player).sendMessage(org.mockito.ArgumentMatchers.<String>argThat(
|
verify(player).sendMessage(org.mockito.ArgumentMatchers.<String>argThat(
|
||||||
message -> message.contains("mob spawning") && message.contains("enabled")
|
message -> message.contains("hostile mob spawning") && message.contains("enabled")
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,9 +371,9 @@ final class BaseSettingsCommandTest {
|
|||||||
.thenReturn(PlayerState.newPlayer(playerId, "Builder"));
|
.thenReturn(PlayerState.newPlayer(playerId, "Builder"));
|
||||||
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
|
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
|
||||||
when(pocketBases.state(playerId)).thenReturn(
|
when(pocketBases.state(playerId)).thenReturn(
|
||||||
new PocketBaseState(playerId, 1, java.util.Optional.empty(), false)
|
new PocketBaseState(playerId, 1, java.util.Optional.empty())
|
||||||
);
|
);
|
||||||
when(pocketBases.setMobSpawning(playerId, true))
|
when(pocketBases.setPassiveMobSpawning(playerId, true))
|
||||||
.thenThrow(new IOException("save failed"));
|
.thenThrow(new IOException("save failed"));
|
||||||
BaseSettingsCommand command = new BaseSettingsCommand(
|
BaseSettingsCommand command = new BaseSettingsCommand(
|
||||||
stateManager,
|
stateManager,
|
||||||
@@ -350,9 +382,9 @@ final class BaseSettingsCommandTest {
|
|||||||
pocketBases
|
pocketBases
|
||||||
);
|
);
|
||||||
|
|
||||||
command.onCommand(
|
command.onCommand(player, null, "basesettings", new String[] {
|
||||||
player, null, "basesettings", new String[] {"pocket", "mobs", "enable"}
|
"pocket", "mobs", "passive", "enable"
|
||||||
);
|
});
|
||||||
|
|
||||||
verify(player).sendMessage(org.mockito.ArgumentMatchers.<String>argThat(
|
verify(player).sendMessage(org.mockito.ArgumentMatchers.<String>argThat(
|
||||||
message -> message.contains("could not be changed")
|
message -> message.contains("could not be changed")
|
||||||
@@ -380,11 +412,11 @@ final class BaseSettingsCommandTest {
|
|||||||
pocketBases
|
pocketBases
|
||||||
);
|
);
|
||||||
|
|
||||||
command.onCommand(
|
command.onCommand(player, null, "basesettings", new String[] {
|
||||||
player, null, "basesettings", new String[] {"pocket", "mobs", "enable"}
|
"pocket", "mobs", "hostile", "enable"
|
||||||
);
|
});
|
||||||
|
|
||||||
verify(pocketBases, never()).setMobSpawning(playerId, true);
|
verify(pocketBases, never()).setHostileMobSpawning(playerId, true);
|
||||||
verify(player).sendMessage(org.mockito.ArgumentMatchers.<String>argThat(
|
verify(player).sendMessage(org.mockito.ArgumentMatchers.<String>argThat(
|
||||||
message -> message.contains("Pocket Base I") && message.contains("locked")
|
message -> message.contains("Pocket Base I") && message.contains("locked")
|
||||||
));
|
));
|
||||||
@@ -397,10 +429,9 @@ final class BaseSettingsCommandTest {
|
|||||||
when(player.getUniqueId()).thenReturn(playerId);
|
when(player.getUniqueId()).thenReturn(playerId);
|
||||||
when(player.getName()).thenReturn("Builder");
|
when(player.getName()).thenReturn("Builder");
|
||||||
PlayerInventory inventory = mock(PlayerInventory.class);
|
PlayerInventory inventory = mock(PlayerInventory.class);
|
||||||
|
ItemStack currency = item(Material.DIAMOND_BLOCK, 64);
|
||||||
when(player.getInventory()).thenReturn(inventory);
|
when(player.getInventory()).thenReturn(inventory);
|
||||||
when(inventory.getStorageContents()).thenReturn(new ItemStack[] {
|
when(inventory.getStorageContents()).thenReturn(new ItemStack[] {currency});
|
||||||
new ItemStack(Material.DIAMOND_BLOCK, 64)
|
|
||||||
});
|
|
||||||
PlayerState owner = PlayerState.newPlayer(playerId, "Builder")
|
PlayerState owner = PlayerState.newPlayer(playerId, "Builder")
|
||||||
.withAdministrativeLevels(4, 0, 0, 0, 0, false, false, true)
|
.withAdministrativeLevels(4, 0, 0, 0, 0, false, false, true)
|
||||||
.withBase(
|
.withBase(
|
||||||
@@ -429,7 +460,7 @@ final class BaseSettingsCommandTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void statusDisplaysPocketMobSpawningPreference() {
|
void statusDisplaysPocketMobSpawningPreferences() {
|
||||||
UUID playerId = UUID.randomUUID();
|
UUID playerId = UUID.randomUUID();
|
||||||
Player player = mock(Player.class);
|
Player player = mock(Player.class);
|
||||||
when(player.getUniqueId()).thenReturn(playerId);
|
when(player.getUniqueId()).thenReturn(playerId);
|
||||||
@@ -439,7 +470,10 @@ final class BaseSettingsCommandTest {
|
|||||||
.thenReturn(PlayerState.newPlayer(playerId, "Builder"));
|
.thenReturn(PlayerState.newPlayer(playerId, "Builder"));
|
||||||
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
|
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
|
||||||
when(pocketBases.state(playerId)).thenReturn(
|
when(pocketBases.state(playerId)).thenReturn(
|
||||||
new PocketBaseState(playerId, 1, java.util.Optional.empty(), false)
|
new PocketBaseState(
|
||||||
|
playerId, 1, java.util.Optional.empty(), java.util.Optional.empty(),
|
||||||
|
true, false, PocketBaseBiome.THE_VOID
|
||||||
|
)
|
||||||
);
|
);
|
||||||
BaseSettingsCommand command = new BaseSettingsCommand(
|
BaseSettingsCommand command = new BaseSettingsCommand(
|
||||||
stateManager,
|
stateManager,
|
||||||
@@ -453,7 +487,8 @@ final class BaseSettingsCommandTest {
|
|||||||
verify(player).sendMessage(org.mockito.ArgumentMatchers.<String>argThat(
|
verify(player).sendMessage(org.mockito.ArgumentMatchers.<String>argThat(
|
||||||
message -> message.contains("Pocket Base 1")
|
message -> message.contains("Pocket Base 1")
|
||||||
&& message.contains("type=void/the_void")
|
&& message.contains("type=void/the_void")
|
||||||
&& message.contains("mob spawning=disabled")
|
&& message.contains("hostile spawning=enabled")
|
||||||
|
&& message.contains("passive spawning=disabled")
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -506,7 +541,7 @@ final class BaseSettingsCommandTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void defaultAndStatusDisplayTheFullProgressReport() {
|
void defaultAndUiOpenDialogWhileStatusDisplaysTheFullProgressReport() {
|
||||||
UUID playerId = UUID.randomUUID();
|
UUID playerId = UUID.randomUUID();
|
||||||
Player player = mock(Player.class);
|
Player player = mock(Player.class);
|
||||||
when(player.getUniqueId()).thenReturn(playerId);
|
when(player.getUniqueId()).thenReturn(playerId);
|
||||||
@@ -518,21 +553,25 @@ final class BaseSettingsCommandTest {
|
|||||||
BaseSettingsCommand command = new BaseSettingsCommand(
|
BaseSettingsCommand command = new BaseSettingsCommand(
|
||||||
stateManager,
|
stateManager,
|
||||||
new PluginSettingsProvider(PluginSettings.from(Map.of())),
|
new PluginSettingsProvider(PluginSettings.from(Map.of())),
|
||||||
mock(BaseFlightController.class)
|
mock(BaseFlightController.class),
|
||||||
|
null,
|
||||||
|
(owner, pocket) -> null
|
||||||
);
|
);
|
||||||
|
|
||||||
command.onCommand(player, null, "basesettings", new String[0]);
|
command.onCommand(player, null, "basesettings", new String[0]);
|
||||||
|
command.onCommand(player, null, "homesettings", new String[] {"ui"});
|
||||||
command.onCommand(player, null, "basesettings", new String[] {"status"});
|
command.onCommand(player, null, "basesettings", new String[] {"status"});
|
||||||
|
|
||||||
verify(player, times(2)).sendMessage(ChatColor.GOLD + "=== Base Progress ===");
|
verify(player, times(2)).showDialog((DialogLike) null);
|
||||||
verify(player, times(2)).sendMessage(
|
verify(player).sendMessage(ChatColor.GOLD + "=== Base Progress ===");
|
||||||
|
verify(player).sendMessage(
|
||||||
org.mockito.ArgumentMatchers.<String>argThat(message ->
|
org.mockito.ArgumentMatchers.<String>argThat(message ->
|
||||||
message.contains("Spawnable Overlay:")
|
message.contains("Spawnable Overlay:")
|
||||||
&& message.contains("0/250 placements")
|
&& message.contains("0/250 placements")
|
||||||
&& message.contains("locked")
|
&& message.contains("locked")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
verify(player, times(2)).sendMessage(
|
verify(player).sendMessage(
|
||||||
org.mockito.ArgumentMatchers.<String>argThat(message ->
|
org.mockito.ArgumentMatchers.<String>argThat(message ->
|
||||||
message.contains("Settings: visitors=")
|
message.contains("Settings: visitors=")
|
||||||
&& message.contains("navigation=")
|
&& message.contains("navigation=")
|
||||||
@@ -544,6 +583,18 @@ final class BaseSettingsCommandTest {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ItemStack item(Material material, int amount) {
|
||||||
|
ItemStack item = mock(ItemStack.class);
|
||||||
|
ItemStack copy = mock(ItemStack.class);
|
||||||
|
when(item.getType()).thenReturn(material);
|
||||||
|
when(item.getAmount()).thenReturn(amount);
|
||||||
|
when(item.clone()).thenReturn(copy);
|
||||||
|
when(copy.getType()).thenReturn(material);
|
||||||
|
when(copy.getAmount()).thenReturn(amount);
|
||||||
|
when(copy.clone()).thenReturn(copy);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
private static CommandResult execute(PlayerState current, String... arguments) {
|
private static CommandResult execute(PlayerState current, String... arguments) {
|
||||||
Player player = mock(Player.class);
|
Player player = mock(Player.class);
|
||||||
when(player.getUniqueId()).thenReturn(current.playerId());
|
when(player.getUniqueId()).thenReturn(current.playerId());
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package games.dmg.spigotbase;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
final class BaseSettingsDialogFactoryTest {
|
||||||
|
@Test
|
||||||
|
void createsControlsForEveryBaseSettingAndConfirmedPurchase() {
|
||||||
|
UUID playerId = UUID.randomUUID();
|
||||||
|
PlayerState owner = PlayerState.newPlayer(playerId, "Builder")
|
||||||
|
.withAdministrativeLevels(4, 2, 2, 1, 1, true, true, true)
|
||||||
|
.withBase(
|
||||||
|
new BaseLocation(UUID.randomUUID(), "world", 10, 64, 20, 10, 20),
|
||||||
|
Instant.EPOCH
|
||||||
|
)
|
||||||
|
.withBorderEnabled(true)
|
||||||
|
.withTotalBlocksPlaced(250)
|
||||||
|
.withSpawnableOverlayEnabled(true);
|
||||||
|
PocketBaseState pocket = new PocketBaseState(
|
||||||
|
playerId,
|
||||||
|
1,
|
||||||
|
Optional.empty(),
|
||||||
|
Optional.empty(),
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
PocketBaseBiome.PLAINS
|
||||||
|
);
|
||||||
|
|
||||||
|
BaseSettingsDialogFactory.DialogSpec specification =
|
||||||
|
new BaseSettingsDialogFactory(PluginSettings.from(Map.of()))
|
||||||
|
.specification(owner, pocket);
|
||||||
|
|
||||||
|
BaseSettingsDialogFactory.ListSpec root = assertInstanceOf(
|
||||||
|
BaseSettingsDialogFactory.ListSpec.class,
|
||||||
|
specification
|
||||||
|
);
|
||||||
|
assertTrue(root.content().message().contains("Base 4/4"));
|
||||||
|
List<String> commands = commands(root);
|
||||||
|
assertTrue(commands.contains("/basesettings status"));
|
||||||
|
assertTrue(commands.contains("/basesettings visitors blocked"));
|
||||||
|
assertTrue(commands.contains("/basesettings navigation disable"));
|
||||||
|
assertTrue(commands.contains("/basesettings flight disable"));
|
||||||
|
assertTrue(commands.contains("/basesettings border disable"));
|
||||||
|
assertTrue(commands.contains("/basesettings spawnable disable"));
|
||||||
|
assertTrue(commands.contains("/basesettings bossbar disable"));
|
||||||
|
assertTrue(commands.contains("/basesettings pocket upgrade"));
|
||||||
|
assertTrue(commands.contains("/basesettings pocket mobs hostile disable"));
|
||||||
|
assertTrue(commands.contains("/basesettings pocket mobs passive enable"));
|
||||||
|
assertTrue(commands.contains("/basesettings pocket type nether crimson_forest"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void requiresConfirmationForBaseIvPurchase() {
|
||||||
|
UUID playerId = UUID.randomUUID();
|
||||||
|
PlayerState owner = PlayerState.newPlayer(playerId, "Builder")
|
||||||
|
.withAdministrativeLevels(3, 0, 0, 0, 0, false, false, false)
|
||||||
|
.withBase(
|
||||||
|
new BaseLocation(UUID.randomUUID(), "world", 0, 64, 0, 0, 0),
|
||||||
|
Instant.EPOCH
|
||||||
|
);
|
||||||
|
|
||||||
|
BaseSettingsDialogFactory.ListSpec root = assertInstanceOf(
|
||||||
|
BaseSettingsDialogFactory.ListSpec.class,
|
||||||
|
new BaseSettingsDialogFactory(PluginSettings.from(Map.of())).specification(
|
||||||
|
owner,
|
||||||
|
PocketBaseState.locked(playerId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
BaseSettingsDialogFactory.ConfirmationSpec upgrade = assertInstanceOf(
|
||||||
|
BaseSettingsDialogFactory.ConfirmationSpec.class,
|
||||||
|
root.dialogs().get(2)
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals("/basesettings upgrade", upgrade.confirm().command());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void marksUnavailableControlsAsLocked() {
|
||||||
|
UUID playerId = UUID.randomUUID();
|
||||||
|
PlayerState owner = PlayerState.newPlayer(playerId, "Newcomer");
|
||||||
|
|
||||||
|
BaseSettingsDialogFactory.ListSpec root = assertInstanceOf(
|
||||||
|
BaseSettingsDialogFactory.ListSpec.class,
|
||||||
|
new BaseSettingsDialogFactory(PluginSettings.from(Map.of())).specification(
|
||||||
|
owner,
|
||||||
|
PocketBaseState.locked(playerId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
BaseSettingsDialogFactory.MultiSpec controls = assertInstanceOf(
|
||||||
|
BaseSettingsDialogFactory.MultiSpec.class,
|
||||||
|
root.dialogs().get(0)
|
||||||
|
);
|
||||||
|
List<String> labels = controls.actions().stream()
|
||||||
|
.map(BaseSettingsDialogFactory.ButtonSpec::label)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
assertTrue(labels.stream().anyMatch(label -> label.contains("Visitors: Locked")));
|
||||||
|
assertTrue(labels.stream().anyMatch(label -> label.contains("Navigation: Locked")));
|
||||||
|
assertTrue(labels.stream().anyMatch(label -> label.contains("Flight: Locked")));
|
||||||
|
assertTrue(labels.stream().anyMatch(label -> label.contains("Border: Locked")));
|
||||||
|
assertTrue(labels.stream().anyMatch(label -> label.contains("Spawnable Overlay: Locked")));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> commands(
|
||||||
|
BaseSettingsDialogFactory.DialogSpec specification
|
||||||
|
) {
|
||||||
|
List<String> commands = new ArrayList<>();
|
||||||
|
collect(specification, commands);
|
||||||
|
return commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void collect(
|
||||||
|
BaseSettingsDialogFactory.DialogSpec specification,
|
||||||
|
List<String> commands
|
||||||
|
) {
|
||||||
|
if (specification instanceof BaseSettingsDialogFactory.ListSpec list) {
|
||||||
|
list.dialogs().forEach(child -> collect(child, commands));
|
||||||
|
} else if (specification instanceof BaseSettingsDialogFactory.MultiSpec multi) {
|
||||||
|
multi.actions().forEach(button -> commands.add(button.command()));
|
||||||
|
} else if (specification
|
||||||
|
instanceof BaseSettingsDialogFactory.ConfirmationSpec confirmation) {
|
||||||
|
commands.add(confirmation.confirm().command());
|
||||||
|
} else if (specification instanceof BaseSettingsDialogFactory.NoticeSpec notice
|
||||||
|
&& notice.action() != null) {
|
||||||
|
commands.add(notice.action().command());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package games.dmg.spigotbase;
|
package games.dmg.spigotbase;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
@@ -8,20 +9,26 @@ import static org.mockito.Mockito.when;
|
|||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
import org.bukkit.Location;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.Server;
|
import org.bukkit.Server;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.entity.Entity;
|
||||||
|
import org.bukkit.entity.Mob;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.event.block.Action;
|
import org.bukkit.event.block.Action;
|
||||||
import org.bukkit.event.block.BlockPlaceEvent;
|
import org.bukkit.event.block.BlockPlaceEvent;
|
||||||
import org.bukkit.event.player.PlayerInteractEvent;
|
import org.bukkit.event.player.PlayerInteractEvent;
|
||||||
|
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
import org.bukkit.util.BoundingBox;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
final class PocketBaseControllerTest {
|
final class PocketBaseControllerTest {
|
||||||
@@ -37,6 +44,7 @@ final class PocketBaseControllerTest {
|
|||||||
World world = mock(World.class);
|
World world = mock(World.class);
|
||||||
Block clicked = block(world, -2, 66, 0, Material.DIAMOND_BLOCK);
|
Block clicked = block(world, -2, 66, 0, Material.DIAMOND_BLOCK);
|
||||||
PlayerInteractEvent event = mock(PlayerInteractEvent.class);
|
PlayerInteractEvent event = mock(PlayerInteractEvent.class);
|
||||||
|
ItemStack activator = item(Material.FLINT_AND_STEEL);
|
||||||
BaseStateManager baseStates = mock(BaseStateManager.class);
|
BaseStateManager baseStates = mock(BaseStateManager.class);
|
||||||
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
|
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
|
||||||
PluginSettings settings = PluginSettings.from(Map.of());
|
PluginSettings settings = PluginSettings.from(Map.of());
|
||||||
@@ -49,7 +57,7 @@ final class PocketBaseControllerTest {
|
|||||||
when(event.getPlayer()).thenReturn(owner);
|
when(event.getPlayer()).thenReturn(owner);
|
||||||
when(event.getAction()).thenReturn(Action.RIGHT_CLICK_BLOCK);
|
when(event.getAction()).thenReturn(Action.RIGHT_CLICK_BLOCK);
|
||||||
when(event.getClickedBlock()).thenReturn(clicked);
|
when(event.getClickedBlock()).thenReturn(clicked);
|
||||||
when(event.getItem()).thenReturn(new ItemStack(Material.FLINT_AND_STEEL));
|
when(event.getItem()).thenReturn(activator);
|
||||||
when(world.getUID()).thenReturn(worldId);
|
when(world.getUID()).thenReturn(worldId);
|
||||||
when(world.getName()).thenReturn("world");
|
when(world.getName()).thenReturn("world");
|
||||||
when(world.getBlockAt(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> {
|
when(world.getBlockAt(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> {
|
||||||
@@ -98,6 +106,7 @@ final class PocketBaseControllerTest {
|
|||||||
World world = mock(World.class);
|
World world = mock(World.class);
|
||||||
Block clicked = block(world, 8, 67, 8, Material.DIAMOND_BLOCK);
|
Block clicked = block(world, 8, 67, 8, Material.DIAMOND_BLOCK);
|
||||||
PlayerInteractEvent event = mock(PlayerInteractEvent.class);
|
PlayerInteractEvent event = mock(PlayerInteractEvent.class);
|
||||||
|
ItemStack activator = item(Material.FLINT_AND_STEEL);
|
||||||
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
|
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
|
||||||
PluginSettings settings = PluginSettings.from(Map.of());
|
PluginSettings settings = PluginSettings.from(Map.of());
|
||||||
|
|
||||||
@@ -105,7 +114,7 @@ final class PocketBaseControllerTest {
|
|||||||
when(event.getPlayer()).thenReturn(owner);
|
when(event.getPlayer()).thenReturn(owner);
|
||||||
when(event.getAction()).thenReturn(Action.RIGHT_CLICK_BLOCK);
|
when(event.getAction()).thenReturn(Action.RIGHT_CLICK_BLOCK);
|
||||||
when(event.getClickedBlock()).thenReturn(clicked);
|
when(event.getClickedBlock()).thenReturn(clicked);
|
||||||
when(event.getItem()).thenReturn(new ItemStack(Material.FLINT_AND_STEEL));
|
when(event.getItem()).thenReturn(activator);
|
||||||
when(world.getUID()).thenReturn(worldId);
|
when(world.getUID()).thenReturn(worldId);
|
||||||
when(world.getName()).thenReturn("pocket");
|
when(world.getName()).thenReturn("pocket");
|
||||||
when(world.getBlockAt(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> {
|
when(world.getBlockAt(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> {
|
||||||
@@ -152,6 +161,7 @@ final class PocketBaseControllerTest {
|
|||||||
World world = mock(World.class);
|
World world = mock(World.class);
|
||||||
Block clicked = block(world, 30, 67, 8, Material.DIAMOND_BLOCK);
|
Block clicked = block(world, 30, 67, 8, Material.DIAMOND_BLOCK);
|
||||||
PlayerInteractEvent event = mock(PlayerInteractEvent.class);
|
PlayerInteractEvent event = mock(PlayerInteractEvent.class);
|
||||||
|
ItemStack activator = item(Material.FLINT_AND_STEEL);
|
||||||
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
|
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
|
||||||
PluginSettings settings = PluginSettings.from(Map.of());
|
PluginSettings settings = PluginSettings.from(Map.of());
|
||||||
|
|
||||||
@@ -159,7 +169,7 @@ final class PocketBaseControllerTest {
|
|||||||
when(event.getPlayer()).thenReturn(owner);
|
when(event.getPlayer()).thenReturn(owner);
|
||||||
when(event.getAction()).thenReturn(Action.RIGHT_CLICK_BLOCK);
|
when(event.getAction()).thenReturn(Action.RIGHT_CLICK_BLOCK);
|
||||||
when(event.getClickedBlock()).thenReturn(clicked);
|
when(event.getClickedBlock()).thenReturn(clicked);
|
||||||
when(event.getItem()).thenReturn(new ItemStack(Material.FLINT_AND_STEEL));
|
when(event.getItem()).thenReturn(activator);
|
||||||
when(world.getUID()).thenReturn(worldId);
|
when(world.getUID()).thenReturn(worldId);
|
||||||
when(world.getName()).thenReturn("pocket");
|
when(world.getName()).thenReturn("pocket");
|
||||||
when(world.getBlockAt(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> {
|
when(world.getBlockAt(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> {
|
||||||
@@ -227,6 +237,157 @@ final class PocketBaseControllerTest {
|
|||||||
verify(event).setCancelled(true);
|
verify(event).setCancelled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void activeEntranceTransportsMobsOnceButNotOtherEntities() {
|
||||||
|
UUID ownerId = UUID.randomUUID();
|
||||||
|
UUID worldId = UUID.randomUUID();
|
||||||
|
PocketPortalLocation portal = new PocketPortalLocation(
|
||||||
|
worldId, "world", -2, 64, 0, PocketPortalAxis.X
|
||||||
|
);
|
||||||
|
PocketBaseState state = new PocketBaseState(ownerId, 1, Optional.of(portal));
|
||||||
|
Server server = mock(Server.class);
|
||||||
|
World world = intactPortalWorld(worldId, portal);
|
||||||
|
Mob mob = mock(Mob.class);
|
||||||
|
Entity item = mock(Entity.class);
|
||||||
|
Location arrival = new Location(mock(World.class), 0.5, 65.0, 5.5);
|
||||||
|
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
|
||||||
|
|
||||||
|
when(mob.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||||
|
when(item.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||||
|
when(world.getNearbyEntities(any(BoundingBox.class), any()))
|
||||||
|
.thenReturn(List.of(mob, item));
|
||||||
|
when(server.getWorld(worldId)).thenReturn(world);
|
||||||
|
when(pocketBases.knownStates()).thenReturn(Map.of(ownerId, state));
|
||||||
|
when(pocketBases.pocketArrival(ownerId)).thenReturn(arrival);
|
||||||
|
PocketBaseController controller = controller(server, pocketBases);
|
||||||
|
|
||||||
|
controller.scanMobPortals();
|
||||||
|
controller.scanMobPortals();
|
||||||
|
|
||||||
|
verify(mob).teleport(arrival, PlayerTeleportEvent.TeleportCause.PLUGIN);
|
||||||
|
verify(item, never()).teleport(
|
||||||
|
any(Location.class),
|
||||||
|
any(PlayerTeleportEvent.TeleportCause.class)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void activeReturnPortalTransportsMobOutsideIntactEntrance() {
|
||||||
|
UUID ownerId = UUID.randomUUID();
|
||||||
|
UUID normalWorldId = UUID.randomUUID();
|
||||||
|
UUID pocketWorldId = UUID.randomUUID();
|
||||||
|
PocketPortalLocation entrance = new PocketPortalLocation(
|
||||||
|
normalWorldId, "world", -2, 64, 0, PocketPortalAxis.X
|
||||||
|
);
|
||||||
|
PocketPortalLocation returnPortal = new PocketPortalLocation(
|
||||||
|
pocketWorldId, "pocket", 8, 65, 8, PocketPortalAxis.Z
|
||||||
|
);
|
||||||
|
PocketBaseState state = new PocketBaseState(ownerId, 1, Optional.of(entrance));
|
||||||
|
Server server = mock(Server.class);
|
||||||
|
World normalWorld = intactPortalWorld(normalWorldId, entrance);
|
||||||
|
World pocketWorld = intactPortalWorld(pocketWorldId, returnPortal);
|
||||||
|
Mob mob = mock(Mob.class);
|
||||||
|
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
|
||||||
|
|
||||||
|
when(mob.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||||
|
when(normalWorld.getNearbyEntities(any(BoundingBox.class), any()))
|
||||||
|
.thenReturn(List.of());
|
||||||
|
when(pocketWorld.getNearbyEntities(any(BoundingBox.class), any()))
|
||||||
|
.thenReturn(List.of(mob));
|
||||||
|
when(server.getWorld(normalWorldId)).thenReturn(normalWorld);
|
||||||
|
when(server.getWorld(pocketWorldId)).thenReturn(pocketWorld);
|
||||||
|
when(pocketBases.knownStates()).thenReturn(Map.of(ownerId, state));
|
||||||
|
when(pocketBases.state(ownerId)).thenReturn(state);
|
||||||
|
when(pocketBases.returnPortalIsIntact(ownerId)).thenReturn(true);
|
||||||
|
when(pocketBases.returnPortal(ownerId)).thenReturn(returnPortal);
|
||||||
|
PocketBaseController controller = controller(server, pocketBases);
|
||||||
|
|
||||||
|
controller.scanMobPortals();
|
||||||
|
|
||||||
|
verify(mob).teleport(
|
||||||
|
new Location(normalWorld, 0.0, 65.0, 1.5),
|
||||||
|
PlayerTeleportEvent.TeleportCause.PLUGIN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mobReturnFallsBackToServerSpawnWithoutValidEntrance() {
|
||||||
|
UUID ownerId = UUID.randomUUID();
|
||||||
|
UUID pocketWorldId = UUID.randomUUID();
|
||||||
|
PocketPortalLocation returnPortal = new PocketPortalLocation(
|
||||||
|
pocketWorldId, "pocket", 8, 65, 8, PocketPortalAxis.Z
|
||||||
|
);
|
||||||
|
PocketBaseState state = new PocketBaseState(ownerId, 1, Optional.empty());
|
||||||
|
Server server = mock(Server.class);
|
||||||
|
World pocketWorld = intactPortalWorld(pocketWorldId, returnPortal);
|
||||||
|
World fallbackWorld = mock(World.class);
|
||||||
|
Location spawn = new Location(fallbackWorld, 10.5, 70.0, 10.5);
|
||||||
|
Mob mob = mock(Mob.class);
|
||||||
|
PocketBaseManager pocketBases = mock(PocketBaseManager.class);
|
||||||
|
|
||||||
|
when(mob.getUniqueId()).thenReturn(UUID.randomUUID());
|
||||||
|
when(pocketWorld.getNearbyEntities(any(BoundingBox.class), any()))
|
||||||
|
.thenReturn(List.of(mob));
|
||||||
|
when(server.getWorld(pocketWorldId)).thenReturn(pocketWorld);
|
||||||
|
when(server.getWorlds()).thenReturn(List.of(fallbackWorld));
|
||||||
|
when(fallbackWorld.getSpawnLocation()).thenReturn(spawn);
|
||||||
|
when(pocketBases.knownStates()).thenReturn(Map.of(ownerId, state));
|
||||||
|
when(pocketBases.state(ownerId)).thenReturn(state);
|
||||||
|
when(pocketBases.returnPortalIsIntact(ownerId)).thenReturn(true);
|
||||||
|
when(pocketBases.returnPortal(ownerId)).thenReturn(returnPortal);
|
||||||
|
PocketBaseController controller = controller(server, pocketBases);
|
||||||
|
|
||||||
|
controller.scanMobPortals();
|
||||||
|
|
||||||
|
verify(mob).teleport(
|
||||||
|
spawn,
|
||||||
|
PlayerTeleportEvent.TeleportCause.PLUGIN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PocketBaseController controller(
|
||||||
|
Server server,
|
||||||
|
PocketBaseManager pocketBases
|
||||||
|
) {
|
||||||
|
PluginSettings settings = PluginSettings.from(Map.of());
|
||||||
|
return new PocketBaseController(
|
||||||
|
server,
|
||||||
|
mock(BaseStateManager.class),
|
||||||
|
new BaseBoundsService(settings),
|
||||||
|
pocketBases,
|
||||||
|
new PluginSettingsProvider(settings),
|
||||||
|
Logger.getAnonymousLogger()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static World intactPortalWorld(
|
||||||
|
UUID worldId,
|
||||||
|
PocketPortalLocation portal
|
||||||
|
) {
|
||||||
|
World world = mock(World.class);
|
||||||
|
Set<BlockPosition> frame = new HashSet<>(PocketPortalGeometry.frameBlocks(portal));
|
||||||
|
when(world.getUID()).thenReturn(worldId);
|
||||||
|
when(world.getBlockAt(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> {
|
||||||
|
BlockPosition position = new BlockPosition(
|
||||||
|
invocation.getArgument(0), invocation.getArgument(1), invocation.getArgument(2)
|
||||||
|
);
|
||||||
|
return block(
|
||||||
|
world,
|
||||||
|
position.x(),
|
||||||
|
position.y(),
|
||||||
|
position.z(),
|
||||||
|
frame.contains(position) ? Material.DIAMOND_BLOCK : Material.AIR
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return world;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ItemStack item(Material material) {
|
||||||
|
ItemStack item = mock(ItemStack.class);
|
||||||
|
when(item.getType()).thenReturn(material);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
private static Block block(
|
private static Block block(
|
||||||
World world,
|
World world,
|
||||||
int x,
|
int x,
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ final class PocketBaseManagerTest {
|
|||||||
PocketBaseStateManager states = mock(PocketBaseStateManager.class);
|
PocketBaseStateManager states = mock(PocketBaseStateManager.class);
|
||||||
PocketBaseWorldService worlds = mock(PocketBaseWorldService.class);
|
PocketBaseWorldService worlds = mock(PocketBaseWorldService.class);
|
||||||
when(states.state(ownerId)).thenReturn(new PocketBaseState(
|
when(states.state(ownerId)).thenReturn(new PocketBaseState(
|
||||||
ownerId, 1, Optional.empty(), Optional.of(relocated), false
|
ownerId, 1, Optional.empty(), Optional.of(relocated), false, false,
|
||||||
|
PocketBaseBiome.THE_VOID
|
||||||
));
|
));
|
||||||
PocketBaseManager manager = new PocketBaseManager(
|
PocketBaseManager manager = new PocketBaseManager(
|
||||||
states,
|
states,
|
||||||
@@ -58,9 +59,12 @@ final class PocketBaseManagerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void persistsAndAppliesMobSpawningPreference() throws Exception {
|
void persistsAndAppliesHostileMobSpawningWithoutChangingPassive() throws Exception {
|
||||||
UUID ownerId = UUID.randomUUID();
|
UUID ownerId = UUID.randomUUID();
|
||||||
PocketBaseState current = new PocketBaseState(ownerId, 1, Optional.empty(), false);
|
PocketBaseState current = new PocketBaseState(
|
||||||
|
ownerId, 1, Optional.empty(), Optional.empty(), false, true,
|
||||||
|
PocketBaseBiome.THE_VOID
|
||||||
|
);
|
||||||
PocketBaseStateManager states = mock(PocketBaseStateManager.class);
|
PocketBaseStateManager states = mock(PocketBaseStateManager.class);
|
||||||
PocketBaseWorldService worlds = mock(PocketBaseWorldService.class);
|
PocketBaseWorldService worlds = mock(PocketBaseWorldService.class);
|
||||||
when(states.state(ownerId)).thenReturn(current);
|
when(states.state(ownerId)).thenReturn(current);
|
||||||
@@ -76,10 +80,12 @@ final class PocketBaseManagerTest {
|
|||||||
new PluginSettingsProvider(PluginSettings.from(Map.of()))
|
new PluginSettingsProvider(PluginSettings.from(Map.of()))
|
||||||
);
|
);
|
||||||
|
|
||||||
manager.setMobSpawning(ownerId, true);
|
PocketBaseState updated = manager.setHostileMobSpawning(ownerId, true);
|
||||||
|
|
||||||
|
assertEquals(true, updated.hostileMobSpawningEnabled());
|
||||||
|
assertEquals(true, updated.passiveMobSpawningEnabled());
|
||||||
InOrder order = inOrder(worlds, states);
|
InOrder order = inOrder(worlds, states);
|
||||||
order.verify(worlds).setMobSpawning(ownerId, true);
|
order.verify(worlds).setMobSpawning(ownerId, true, true);
|
||||||
order.verify(states).updateAndSave(
|
order.verify(states).updateAndSave(
|
||||||
org.mockito.ArgumentMatchers.eq(ownerId), org.mockito.ArgumentMatchers.any()
|
org.mockito.ArgumentMatchers.eq(ownerId), org.mockito.ArgumentMatchers.any()
|
||||||
);
|
);
|
||||||
@@ -137,9 +143,12 @@ final class PocketBaseManagerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void restoresWorldSettingWhenPersistenceFails() throws Exception {
|
void restoresBothWorldSettingsWhenPersistenceFails() throws Exception {
|
||||||
UUID ownerId = UUID.randomUUID();
|
UUID ownerId = UUID.randomUUID();
|
||||||
PocketBaseState current = new PocketBaseState(ownerId, 1, Optional.empty(), false);
|
PocketBaseState current = new PocketBaseState(
|
||||||
|
ownerId, 1, Optional.empty(), Optional.empty(), true, false,
|
||||||
|
PocketBaseBiome.THE_VOID
|
||||||
|
);
|
||||||
PocketBaseStateManager states = mock(PocketBaseStateManager.class);
|
PocketBaseStateManager states = mock(PocketBaseStateManager.class);
|
||||||
PocketBaseWorldService worlds = mock(PocketBaseWorldService.class);
|
PocketBaseWorldService worlds = mock(PocketBaseWorldService.class);
|
||||||
when(states.state(ownerId)).thenReturn(current);
|
when(states.state(ownerId)).thenReturn(current);
|
||||||
@@ -151,9 +160,10 @@ final class PocketBaseManagerTest {
|
|||||||
new PluginSettingsProvider(PluginSettings.from(Map.of()))
|
new PluginSettingsProvider(PluginSettings.from(Map.of()))
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThrows(IOException.class, () -> manager.setMobSpawning(ownerId, true));
|
assertThrows(IOException.class,
|
||||||
|
() -> manager.setPassiveMobSpawning(ownerId, true));
|
||||||
|
|
||||||
verify(worlds).setMobSpawning(ownerId, true);
|
verify(worlds).setMobSpawning(ownerId, true, true);
|
||||||
verify(worlds).setMobSpawning(ownerId, false);
|
verify(worlds).setMobSpawning(ownerId, true, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,10 +93,13 @@ final class PocketBaseWorldServiceTest {
|
|||||||
|
|
||||||
service.loadExisting(Map.of(
|
service.loadExisting(Map.of(
|
||||||
ownerId,
|
ownerId,
|
||||||
new PocketBaseState(ownerId, 1, Optional.empty(), true)
|
new PocketBaseState(
|
||||||
|
ownerId, 1, Optional.empty(), Optional.empty(), true, false,
|
||||||
|
PocketBaseBiome.THE_VOID
|
||||||
|
)
|
||||||
));
|
));
|
||||||
|
|
||||||
verify(world).setSpawnFlags(true, true);
|
verify(world).setSpawnFlags(true, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ final class YamlPocketBaseRepositoryTest {
|
|||||||
worldId, "pocket", 20, 65, 12, PocketPortalAxis.Z
|
worldId, "pocket", 20, 65, 12, PocketPortalAxis.Z
|
||||||
)),
|
)),
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
PocketBaseBiome.CRIMSON_FOREST
|
PocketBaseBiome.CRIMSON_FOREST
|
||||||
);
|
);
|
||||||
YamlPocketBaseRepository repository = new YamlPocketBaseRepository(
|
YamlPocketBaseRepository repository = new YamlPocketBaseRepository(
|
||||||
@@ -50,8 +51,23 @@ final class YamlPocketBaseRepositoryTest {
|
|||||||
|
|
||||||
PocketBaseState loaded = repository.load().get(ownerId);
|
PocketBaseState loaded = repository.load().get(ownerId);
|
||||||
|
|
||||||
assertFalse(loaded.mobSpawningEnabled());
|
assertFalse(loaded.hostileMobSpawningEnabled());
|
||||||
|
assertFalse(loaded.passiveMobSpawningEnabled());
|
||||||
assertTrue(loaded.returnPortal().isEmpty());
|
assertTrue(loaded.returnPortal().isEmpty());
|
||||||
assertEquals(PocketBaseBiome.THE_VOID, loaded.biome());
|
assertEquals(PocketBaseBiome.THE_VOID, loaded.biome());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void migratesLegacyMobPreferenceToBothCategories() throws Exception {
|
||||||
|
UUID ownerId = UUID.randomUUID();
|
||||||
|
Path stateFile = temporaryDirectory.resolve("pocket-bases.yml");
|
||||||
|
Files.writeString(stateFile, "owners:\n " + ownerId
|
||||||
|
+ ":\n level: 1\n mob-spawning-enabled: true\n");
|
||||||
|
YamlPocketBaseRepository repository = new YamlPocketBaseRepository(stateFile);
|
||||||
|
|
||||||
|
PocketBaseState loaded = repository.load().get(ownerId);
|
||||||
|
|
||||||
|
assertTrue(loaded.hostileMobSpawningEnabled());
|
||||||
|
assertTrue(loaded.passiveMobSpawningEnabled());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user