Compare commits
16 Commits
1-add-a-co
...
6-add-leve
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e459ec9fa | |||
| 99f6195542 | |||
| 60c3d41e50 | |||
| 8d99c2e67f | |||
| d68f7543ea | |||
| 917dac9c8e | |||
| 30c31c185b | |||
| fd5bf785d3 | |||
| 04a0c1d959 | |||
| f68d043d02 | |||
| 9be629d82e | |||
| d012d9e645 | |||
| 87b5efed8c | |||
| de3651f22c | |||
| da46d3e4b9 | |||
| b6b30621f0 |
@@ -21,14 +21,24 @@ Use this checklist for behavior that depends on a real Paper world, player inter
|
|||||||
### Join and plot allocation
|
### Join and plot allocation
|
||||||
|
|
||||||
- Join as a new player. Confirm one home is allocated and the welcome message reports its actual bounds. Check the corresponding `h_1_<UUID>` region has the player as owner and its bounds match the configured plot size.
|
- Join as a new player. Confirm one home is allocated and the welcome message reports its actual bounds. Check the corresponding `h_1_<UUID>` region has the player as owner and its bounds match the configured plot size.
|
||||||
|
- Set `plot.initial: 0` on a disposable server, restart, and join as a fresh player. Confirm no home is created until the player runs `/metropolis-home-acquire`; then verify the command creates the initial home and makes it active.
|
||||||
- Join with a second new player. Confirm the new home is distinct, does not overlap the first home or spawn, and the City region grows to include it.
|
- Join with a second new player. Confirm the new home is distinct, does not overlap the first home or spawn, and the City region grows to include it.
|
||||||
|
- Start from a disposable world configured with `plot.sizeY` and `plot.maxLevels` from the multi-level example. Confirm the level count is honored, the city does not exceed the world's build height, and upper plots are only allocated above footprints fully supported at every lower level. With a 1×1 spawn and a 2×2 home size, confirm a new home is allocated at base level rather than above the partially supported spawn.
|
||||||
|
- Generate homes at base and elevated levels, then verify each full home cuboid is inside the WorldGuard `City` region. Add a home/reservation extending the city bounds and confirm City expands without shrinking its existing bounds or flags.
|
||||||
|
- Set positive and negative `plot.offsetX`, `plot.offsetY`, and `plot.offsetZ` values on a disposable world. Verify plot regions, road bands, stair flights, and Spawn share the shifted grid origin; allocate multiple plots across negative coordinates and confirm logical-cell spacing stays consistent. Confirm changing offsets does not move existing saved regions; Metropolis-managed homes and reservations should retain their bounds and remain recognized. Treat automatic conversion of other saved regions as unverified.
|
||||||
|
- Set `spawn.sizeX`, `spawn.sizeY`, and `spawn.sizeZ` to distinct positive values on a fresh disposable world. Confirm the Spawn region spans the corresponding plot-plus-road logical cells horizontally and the configured plot-height multiple vertically. Check blocks outside those bounds remain unchanged; configure a height exceeding the world's build limit and confirm startup rejects it clearly.
|
||||||
- Join again as an existing player. Confirm the same home is selected and no duplicate region or plot is created.
|
- Join again as an existing player. Confirm the same home is selected and no duplicate region or plot is created.
|
||||||
- Reserve an area before allocating another home. Confirm future allocations skip it. Repeat with reservations near the city edge and near existing roads to look for overlaps or gaps.
|
- Reserve an area before allocating another home. Confirm future allocations skip it. Repeat with reservations near the city edge and near existing roads to look for overlaps or gaps.
|
||||||
|
- Create an unrelated saved WorldGuard region in the next candidate area, then allocate a home and confirm it is skipped. Repeat with a non-cuboid region and verify the full bounding cuboid is avoided; confirm `City` itself does not prevent allocation.
|
||||||
|
- Set a small WorldBorder in a disposable world, occupy or reserve the remaining valid grid cells, and request another home. Confirm search grows beyond 256 rings but never places a plot/road footprint outside the border; when no supported cell remains, check the error log and confirm no region is created.
|
||||||
- Test a user override that grants a different plot multiplier or home limit. Confirm the generated bounds and allowed number of homes match the override, and that the configured global defaults still apply to other players.
|
- Test a user override that grants a different plot multiplier or home limit. Confirm the generated bounds and allowed number of homes match the override, and that the configured global defaults still apply to other players.
|
||||||
|
|
||||||
### Generated blocks and geometry
|
### Generated blocks and geometry
|
||||||
|
|
||||||
- Inspect a newly allocated home and adjoining roads. Check road width, level, material, clearance, plot floor, support blocks, and optional sign against the active config.
|
- Inspect a newly allocated home and adjoining roads. Check road width, level, material, clearance, plot floor, support blocks, and optional sign against the active config.
|
||||||
|
- Generate one plot in a disposable world and confirm each adjoining road is fully paved to the configured width. Generate a plot directly beside it: confirm already-correct shared road blocks are untouched, any missing half-width legacy section is filled, and stair treads/openings remain intact.
|
||||||
|
- Configure streets and avenues with different widths, materials, clearances, and support settings. Verify streets along X use only the street options and avenues along Z use only avenue options.
|
||||||
|
- Enable stairs for both sections with different widths/cadences, each narrower than its road. Generate a plot on a level below `plot.maxLevels`; verify street stairs ascend along positive X at north/south borders and avenue stairs ascend along positive Z at east/west borders. Confirm lower roads remain unchanged, the first treads start one block above and forward from the road-level landing, connected treads use inverted backing, and top treads reach the upper road Y. Check each upper road leaves exactly three blocks open before its top tread only across that stair width. For a 2-plot-wide/long home with cadence 1, confirm two centered runs along each eligible edge. Repeat with higher levels to confirm existing flights remain open.
|
||||||
- Test generation over varied terrain, including water, caves, uneven ground, and existing structures. Confirm generated floors and roads do not leave unsafe gaps or erase blocks outside their intended area.
|
- Test generation over varied terrain, including water, caves, uneven ground, and existing structures. Confirm generated floors and roads do not leave unsafe gaps or erase blocks outside their intended area.
|
||||||
- Enable and disable optional floor, support, sign, spawn, world-spawn, and perimeter-wall settings one at a time on a disposable world. Confirm each setting changes only its intended behavior.
|
- Enable and disable optional floor, support, sign, spawn, world-spawn, and perimeter-wall settings one at a time on a disposable world. Confirm each setting changes only its intended behavior.
|
||||||
- Allocate plots in several directions and near the world origin. Check road joins, plot dimensions, City region expansion, and boundary coordinates for symmetry and off-by-one errors.
|
- Allocate plots in several directions and near the world origin. Check road joins, plot dimensions, City region expansion, and boundary coordinates for symmetry and off-by-one errors.
|
||||||
@@ -41,6 +51,11 @@ Use this checklist for behavior that depends on a real Paper world, player inter
|
|||||||
- As the home owner, build and interact inside the home. As another non-op player, try the same actions. Verify ownership and protection behave as intended in homes, reservations, City, and Spawn.
|
- As the home owner, build and interact inside the home. As another non-op player, try the same actions. Verify ownership and protection behave as intended in homes, reservations, City, and Spawn.
|
||||||
- Verify representative protection flags in-game, including PvP, mob damage/spawning, explosions, and fire/lava behavior where applicable. Run `/metropolis-flag-reset` and confirm the expected flags are restored without changing region bounds or ownership.
|
- Verify representative protection flags in-game, including PvP, mob damage/spawning, explosions, and fire/lava behavior where applicable. Run `/metropolis-flag-reset` and confirm the expected flags are restored without changing region bounds or ownership.
|
||||||
- Restart the server and confirm reservations and their protection are still present in WorldGuard and remain excluded from home allocation.
|
- Restart the server and confirm reservations and their protection are still present in WorldGuard and remain excluded from home allocation.
|
||||||
|
- Add a non-op test player as a WorldGuard member of a Metropolis reservation. Run `/metropolis-plot-go` and confirm they arrive inside that reservation at a safe location; repeat with an owner.
|
||||||
|
- Confirm a player who is neither owner nor member cannot use a reservation name to teleport there. Check that a region they own/member outside homes, City, and Spawn is treated as expected by the Metropolis reservation lookup.
|
||||||
|
- Add a player to two reservations. Confirm the no-argument form reports both names without teleporting, then specify one reservation and verify the chosen destination.
|
||||||
|
- As an authorized admin, target another online member with `/metropolis-plot-go <playerName>`. Confirm the target receives a notification. Repeat without `metropolis.plot.go` and verify targeting another player is denied.
|
||||||
|
- Test a reservation with no safe destination. Confirm the command reports failure and does not claim teleportation succeeded. Also test console use with an online player name and an unknown/offline name.
|
||||||
|
|
||||||
### Commands, permissions, and player state
|
### Commands, permissions, and player state
|
||||||
|
|
||||||
@@ -65,8 +80,18 @@ Use this checklist for behavior that depends on a real Paper world, player inter
|
|||||||
|
|
||||||
## Existing Automated Coverage
|
## Existing Automated Coverage
|
||||||
|
|
||||||
Run `./gradlew test` for the server-free JUnit suite. It has 20 tests covering:
|
Run `./gradlew test` for the server-free JUnit suite. It has 62 tests covering:
|
||||||
|
|
||||||
|
- `AvenueStairwayLayoutTest`: centered per-segment street and avenue flights, both border directions, independent cadence, top-tread alignment, inverted backing plan, exact-width openings, and invalid-fit rejection.
|
||||||
|
- `PlotLevelSupportTest`: rejection of upper plots with missing logical support cells and complete coverage requirements across all lower levels.
|
||||||
|
- `CityRegionBoundsTest`: City bounds expand to include elevated plots and Spawn without shrinking existing protection bounds.
|
||||||
|
- `PlotGridLayoutTest`: zero-offset compatibility, offsets on X/Y/Z, axis-specific spacing, and negative logical-index recovery.
|
||||||
|
- `WorldGuardRegionOccupancyTest`: arbitrary saved-region bounds are excluded while City/global regions are treated as non-occupying envelopes.
|
||||||
|
- `PlotWorldBorderBoundsTest`: search bounds follow WorldBorder size/center and require the full candidate footprint inside it.
|
||||||
|
- `SpawnLayoutTest`: default one-cell bounds, independent multipliers, and build-height validation.
|
||||||
|
- `AvenueStairwayBuilderTest`: configured stair material and facing/half data for avenue and street directions, upside-down backing stairs, exact upper-road AIR placements, and no block writes at lower-road Y.
|
||||||
|
- `RoadLayoutTest`: independently sized street/avenue bands, correct road-type tagging, non-overlapping corners, and disabled roads at zero width.
|
||||||
|
- `RoadBlockWriterTest`: no-op writes for existing roads, filling missing road blocks, and preserving stair treads.
|
||||||
- `CuboidTest`: WorldEdit bound conversion, vector ordering/null handling, touching intersections, and point containment.
|
- `CuboidTest`: WorldEdit bound conversion, vector ordering/null handling, touching intersections, and point containment.
|
||||||
- `CurrentHomesStoreTest`: UUID-based persistence and resolving a legacy player-name key.
|
- `CurrentHomesStoreTest`: UUID-based persistence and resolving a legacy player-name key.
|
||||||
- `PlayerJoinListenerTest`: the welcome bounds message when a home is supplied by a mock.
|
- `PlayerJoinListenerTest`: the welcome bounds message when a home is supplied by a mock.
|
||||||
@@ -75,8 +100,11 @@ Run `./gradlew test` for the server-free JUnit suite. It has 20 tests covering:
|
|||||||
- `HomePermissionOverrideTest`: dotted permission-node parsing, priority and tie resolution, invalid entries, and username/permission/default precedence.
|
- `HomePermissionOverrideTest`: dotted permission-node parsing, priority and tie resolution, invalid entries, and username/permission/default precedence.
|
||||||
- `HomeNumberAllocatorTest`: choosing the first unused positive home number.
|
- `HomeNumberAllocatorTest`: choosing the first unused positive home number.
|
||||||
- `MetropolisHomeMoveCommandTest`: selecting a sender's home, selecting an offline target's home, and rejecting invalid requests.
|
- `MetropolisHomeMoveCommandTest`: selecting a sender's home, selecting an offline target's home, and rejecting invalid requests.
|
||||||
|
- `PlotReservationLookupTest`: owner/member matching, exclusion of homes/City/Spawn, legacy reservation markers, and region-name aliases.
|
||||||
|
- `MetropolisPlotGoCommandTest`: self and target teleportation, ambiguous memberships, absent membership, and unsafe-destination feedback.
|
||||||
- `MetropolisPlotReserveCommandTest`: parsing six coordinates and rejecting malformed coordinates.
|
- `MetropolisPlotReserveCommandTest`: parsing six coordinates and rejecting malformed coordinates.
|
||||||
- `MetropolisHomeAcquireTest`: acquisition below the limit and refusal at the limit.
|
- `MetropolisHomeAcquireTest`: acquisition below the limit and refusal at the limit.
|
||||||
|
- `MetropolisDebugGenerateTestHomesCommandTest`: asynchronous tick-queued generation, one home per interval, no overlapping batches, and continuing test-player names across repeated runs.
|
||||||
|
|
||||||
The two opt-in tests in `src/integrationTest` require a running Paper server and RCON. They check that Metropolis appears in `/plugins` and that a coordinate reservation is persisted by WorldGuard. They do not currently verify restart persistence, join allocation, WorldEdit-selection reservations, permissions, teleports, generated blocks, protection behavior, or the other command workflows above. Run them with the environment variables documented in `README.md`; use a disposable server because the reservation test changes its WorldGuard data.
|
The two opt-in tests in `src/integrationTest` require a running Paper server and RCON. They check that Metropolis appears in `/plugins` and that a coordinate reservation is persisted by WorldGuard. They do not currently verify restart persistence, join allocation, WorldEdit-selection reservations, permissions, teleports, generated blocks, protection behavior, or the other command workflows above. Run them with the environment variables documented in `README.md`; use a disposable server because the reservation test changes its WorldGuard data.
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ Metropolis targets Paper only; Bukkit and Spigot servers are not supported.
|
|||||||
- Expands the protected city region as plots are occupied.
|
- Expands the protected city region as plots are occupied.
|
||||||
- Configures plot dimensions, roads, floors, supports, signs, spawn behavior, and perimeter walls.
|
- Configures plot dimensions, roads, floors, supports, signs, spawn behavior, and perimeter walls.
|
||||||
- Reserves named plots by coordinates or a WorldEdit selection.
|
- Reserves named plots by coordinates or a WorldEdit selection.
|
||||||
|
- Lets WorldGuard owners and members teleport to reservations they belong to, with a named choice when they have multiple.
|
||||||
- Tracks home ownership and selected homes by player UUID.
|
- Tracks home ownership and selected homes by player UUID.
|
||||||
- Supports home and plot teleportation, administration, and WorldGuard protection flags.
|
- Supports home and plot teleportation, administration, and WorldGuard protection flags.
|
||||||
|
|
||||||
@@ -38,7 +39,7 @@ Metropolis creates `plugins/Metropolis/config.yml` on first startup. Configure p
|
|||||||
- `/metropolis-home-move <homeNumber> [playerName]` - Selects an existing home as active. The player name defaults to the sender; authorized administrators and console can target online or cached offline players.
|
- `/metropolis-home-move <homeNumber> [playerName]` - Selects an existing home as active. The player name defaults to the sender; authorized administrators and console can target online or cached offline players.
|
||||||
- `/metropolis-home-evict <playerName>` - Removes a player's ownership from their selected home region.
|
- `/metropolis-home-evict <playerName>` - Removes a player's ownership from their selected home region.
|
||||||
- `/metropolis-plot-reserve <name> <minX> <minY> <minZ> <maxX> <maxY> <maxZ>` - Reserves a region by coordinates; players can use `/metropolis-plot-reserve <name>` with a WorldEdit selection.
|
- `/metropolis-plot-reserve <name> <minX> <minY> <minZ> <maxX> <maxY> <maxZ>` - Reserves a region by coordinates; players can use `/metropolis-plot-reserve <name>` with a WorldEdit selection.
|
||||||
- `/metropolis-plot-go <plotName> [playerName]` - Teleports to a named plot.
|
- `/metropolis-plot-go [playerName]` - Teleports to the sender's or an online player's reservation. Specify a reservation name to choose among multiple memberships; `metropolis.plot.go` allows targeting others or bypassing membership.
|
||||||
- `/metropolis-flag-reset` - Reapplies Metropolis protection flags.
|
- `/metropolis-flag-reset` - Reapplies Metropolis protection flags.
|
||||||
- `/metropolis-debug-generatetesthomes <count>` - Generates test homes; intended for development servers.
|
- `/metropolis-debug-generatetesthomes <count>` - Generates test homes; intended for development servers.
|
||||||
|
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
Metropolis turns player onboarding into city planning. As players arrive, the Paper plugin finds the next available plot, builds the surrounding roads and infrastructure, and extends a WorldGuard-protected city that feels designed rather than randomly generated.
|
Metropolis turns player onboarding into city planning. As players arrive, the Paper plugin finds the next available plot, builds the surrounding roads and infrastructure, and extends a WorldGuard-protected city that feels designed rather than randomly generated.
|
||||||
|
|
||||||
The project combines procedural spatial logic, persistent UUID-based ownership, configurable construction rules, WorldEdit-assisted reservations, and a command suite for acquiring and selecting multiple homes. Home limits and plot sizes can be assigned per user or through permission-manager groups using configurable permission tiers. Active-home selections persist across reconnects and server restarts. It is also a modernization case study: a legacy plugin brought forward to Paper 26.2 and 26.3 with Gradle, focused unit tests, and opt-in live-server integration tests. Bukkit and Spigot servers are outside the support target.
|
The project combines procedural spatial logic, persistent UUID-based ownership, configurable construction rules, WorldEdit-assisted reservations, and a command suite for acquiring and selecting multiple homes. Home limits and plot sizes can be assigned per user or through permission-manager groups using configurable permission tiers. Reservation owners and members can teleport to their reserved plots, with explicit selection when they belong to multiple reservations. Active-home selections persist across reconnects and server restarts. It is also a modernization case study: a legacy plugin brought forward to Paper 26.2 and 26.3 with Gradle, focused unit tests, and opt-in live-server integration tests. Bukkit and Spigot servers are outside the support target.
|
||||||
|
|||||||
28
README.md
28
README.md
@@ -4,11 +4,13 @@ Metropolis expands a protected Minecraft city as players join. Each player can r
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Assigns a protected home plot when a player joins and supports acquiring additional homes up to the configured player limit.
|
- Assigns a protected home plot when a player joins by default and supports acquiring additional homes up to the configured player limit; set `plot.initial` to `0` to require `/metropolis-home-acquire` for the first home.
|
||||||
|
- Adds stacked plot levels with `plot.maxLevels`, while `plot.sizeY` continues to define the full height of each individual plot.
|
||||||
- Supports per-player plot sizes and home limits through `userOverrides` or configurable permission tiers; acquired homes can be selected as the active home.
|
- Supports per-player plot sizes and home limits through `userOverrides` or configurable permission tiers; acquired homes can be selected as the active home.
|
||||||
- Expands the City region as plots are occupied, with optional spawn generation, world-spawn placement, floors, support blocks, signs, and perimeter walls.
|
- Expands the City region as plots are occupied, with optional spawn generation, world-spawn placement, floors, support blocks, signs, and perimeter walls.
|
||||||
- Generates roads around plots with configurable width, level, material, clearance, and supports.
|
- Generates roads around plots with configurable width, level, material, clearance, and supports.
|
||||||
- Creates named protected reservations from coordinates or a WorldEdit selection so reserved plots are not assigned as homes.
|
- Creates named protected reservations from coordinates or a WorldEdit selection so reserved plots are not assigned as homes.
|
||||||
|
- Lets WorldGuard reservation owners and members teleport to their reservations, with explicit selection when they belong to several.
|
||||||
- Persists selected homes by player UUID and recognizes legacy name-based home data during migration.
|
- Persists selected homes by player UUID and recognizes legacy name-based home data during migration.
|
||||||
- Supports online/offline player administration for home generation, movement, and eviction.
|
- Supports online/offline player administration for home generation, movement, and eviction.
|
||||||
|
|
||||||
@@ -56,7 +58,25 @@ Copy `.env.example` to `.env` and configure the Paper version/build and server p
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
`plugins/Metropolis/config.yml` controls plot dimensions and limits, floor and support generation, signs, road width/level/material/clearance, spawn behavior, wall generation, world name, and per-player overrides. `plot.multiplier` sets the default plot-size multiplier; `plot.maxPerPlayer` sets the default home limit.
|
`plugins/Metropolis/config.yml` controls plot dimensions and limits, floor and support generation, signs, road width/level/material/clearance, spawn behavior, wall generation, world name, and per-player overrides. The per-level stack is configured with `plot.sizeY` and `plot.maxLevels`: `sizeY` is the full height of one plot level, and `maxLevels` is the number of stacked levels that can be allocated without exceeding the world build height. `plot.multiplier` sets the default plot-size multiplier; `plot.maxPerPlayer` sets the default home limit; `plot.initial` sets how many homes are auto-generated on join, and `0` requires `/metropolis-home-acquire` for the player's first home.
|
||||||
|
|
||||||
|
`plot.offsetX`, `plot.offsetY`, and `plot.offsetZ` shift the logical plot grid in blocks. Positive or negative offsets apply consistently to plot/road placement, vertical levels, spawn alignment, and logical-cell indexing. Changing offsets does not move existing saved WorldGuard regions or automatically convert an existing world.
|
||||||
|
|
||||||
|
Before allocating, Metropolis treats every saved WorldGuard region except the enclosing `City` and special `__global__` region as occupied. It uses each region's bounding cuboid; for non-cuboid regions this can conservatively leave extra gaps to ensure new plot regions do not overlap them.
|
||||||
|
|
||||||
|
Plot allocation continues outward up to the configured WorldBorder. The complete logical plot-and-road footprint must fit inside the border; if no supported, unoccupied location remains, Metropolis logs an error and creates no region.
|
||||||
|
|
||||||
|
Set `spawn.sizeX`, `spawn.sizeY`, and `spawn.sizeZ` to positive logical-grid multipliers. X and Z include the matching plot dimension plus road spacing; Y sets the spawn volume's height upward from `road.level` in multiples of `plot.sizeY`. All default to `1`. A newly created Spawn region must fit within the world's build height, and generation clears only within that region's configured vertical bounds. Existing saved Spawn regions retain their saved bounds.
|
||||||
|
|
||||||
|
The `road.streets.*` and `road.avenues.*` sections independently configure width, level, material, clearance, supports, and stairs. Streets run along X; avenues run along Z. Each section's stair settings (`generate`, `material`, `width`, and `everyNBlocks`) use that section's road width and cadence. Street stair runs ascend along positive X at the north/south borders; avenue runs ascend along positive Z at the east/west borders. Runs are centered within each eligible logical plot-sized segment. The lower road remains the normal-material landing; stairs begin one block above and forward from it. Connected following treads use upside-down stairs facing back down the run as backing, and the final tread reaches the upper road Y. The three blocks before each top tread remain open only across the stair width.
|
||||||
|
|
||||||
|
Upper-level plots require complete generated support below: every logical plot cell in their footprint must be occupied by a generated home or spawn at every lower level. If a footprint has gaps below, allocation skips that upper candidate and keeps searching for a supported lower-level location.
|
||||||
|
|
||||||
|
The WorldGuard `City` region expands in all three dimensions to contain its previous bounds, the requested city footprint, Spawn, and generated homes/reservations. New upper-level plots therefore remain inside City protection; adding a home or reservation also triggers a resize.
|
||||||
|
|
||||||
|
The breaking multi-level schema is documented in the example config at `src/main/resources/examples/multi-level.yml`. It uses split road sections (`road.streets.*` and `road.avenues.*`), a level-aware `plot.maxLevels` field, and the valid stair material `COBBLESTONE_STAIRS`.
|
||||||
|
|
||||||
|
Roads are generated at their full configured width when a plot is created. Generating an adjacent plot does not rewrite road blocks that are already correct; it only fills missing blocks, including gaps left by older half-width generation. Existing stair treads and upper-road openings are preserved.
|
||||||
|
|
||||||
Use `permissionOverrides` to assign plot size and home limits through any permission manager. Each permission node maps to a `priority`, `plotMultiplier`, and `maxPlots`; the matching node with the highest priority wins, and the last matching entry in YAML order wins ties. An explicit username entry in `userOverrides` takes precedence over permission rules. If no override matches, the global `plot.*` defaults apply. Permission rules are checked for online players; offline home generation uses username overrides or global defaults. For example:
|
Use `permissionOverrides` to assign plot size and home limits through any permission manager. Each permission node maps to a `priority`, `plotMultiplier`, and `maxPlots`; the matching node with the highest priority wins, and the last matching entry in YAML order wins ties. An explicit username entry in `userOverrides` takes precedence over permission rules. If no override matches, the global `plot.*` defaults apply. Permission rules are checked for online players; offline home generation uses username overrides or global defaults. For example:
|
||||||
|
|
||||||
@@ -86,9 +106,9 @@ Assign these permission nodes to groups or individual users in your permission p
|
|||||||
| `/metropolis-home-move <homeNumber> [playerName]` | `metropolis.home.move` | Selects one of a player's existing homes as active. The player argument defaults to the sender; console and authorized admins can target online or cached offline players. The selection persists across restarts. |
|
| `/metropolis-home-move <homeNumber> [playerName]` | `metropolis.home.move` | Selects one of a player's existing homes as active. The player argument defaults to the sender; console and authorized admins can target online or cached offline players. The selection persists across restarts. |
|
||||||
| `/metropolis-home-evict <playerName>` | `metropolis.home.evict` | Removes the player's ownership from their selected home region. |
|
| `/metropolis-home-evict <playerName>` | `metropolis.home.evict` | Removes the player's ownership from their selected home region. |
|
||||||
| `/metropolis-plot-reserve <name> <minX> <minY> <minZ> <maxX> <maxY> <maxZ>` | `metropolis.plot.reserve` | Creates a named reservation from explicit bounds. A player can instead run `/metropolis-plot-reserve <name>` with a WorldEdit selection. |
|
| `/metropolis-plot-reserve <name> <minX> <minY> <minZ> <maxX> <maxY> <maxZ>` | `metropolis.plot.reserve` | Creates a named reservation from explicit bounds. A player can instead run `/metropolis-plot-reserve <name>` with a WorldEdit selection. |
|
||||||
| `/metropolis-plot-go <plotName> [playerName]` | `metropolis.plot.go` | Teleports the sender or target player to a named occupied or reserved plot. |
|
| `/metropolis-plot-go [playerName]` | Reservation owner/member; `metropolis.plot.go` to target others or bypass membership | Teleports the sender or an online target to a reservation they own or belong to. If they have several, specify one with `/metropolis-plot-go <reservationName> [playerName]`. |
|
||||||
| `/metropolis-flag-reset` | `metropolis.flag.reset` | Reapplies Metropolis protection flags to the City and home regions. |
|
| `/metropolis-flag-reset` | `metropolis.flag.reset` | Reapplies Metropolis protection flags to the City and home regions. |
|
||||||
| `/metropolis-debug-generatetesthomes <count>` | `metropolis.debug` | Generates test homes; intended for development servers. |
|
| `/metropolis-debug-generatetesthomes <count>` | `metropolis.debug` | Queues test-home generation on the main thread, creating one home every two ticks; intended for development servers. |
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
31
TODO.md
31
TODO.md
@@ -31,11 +31,11 @@
|
|||||||
| GIT-009 | Planning | i have three reserve plots and one just got overwritten when a new player joined. |
|
| GIT-009 | Planning | i have three reserve plots and one just got overwritten when a new player joined. |
|
||||||
| GIT-008 | Ready | Implement multiple plot sizes by granting multiple plots around each other.
|
| GIT-008 | Ready | Implement multiple plot sizes by granting multiple plots around each other.
|
||||||
| GIT-007 | Planning | Allow buying and selling of homes via an ecnoomy plugin. |
|
| GIT-007 | Planning | Allow buying and selling of homes via an ecnoomy plugin. |
|
||||||
| GIT-006 | Ready | Add levels support. |
|
| GIT-006 | Done | Add levels support. |
|
||||||
| GIT-005 | Planning | Add the possibility to create multiple plot homes in shapes. |
|
| GIT-005 | Planning | Add the possibility to create multiple plot homes in shapes. |
|
||||||
| GIT-004 | Planning | Add a command to request home allocation. |
|
| GIT-004 | Done | Add a command to request home allocation. |
|
||||||
| GIT-003 | Ready | Add support for multiple homes. |
|
| GIT-003 | Ready | Add support for multiple homes. |
|
||||||
| GIT-002 | Planning | Add a command to teleport to a reservation you are part of. |
|
| GIT-002 | Done | Add a command to teleport to a reservation you are part of. |
|
||||||
| GIT-001 | Done | Add a command to move to a different home. |
|
| GIT-001 | Done | Add a command to move to a different home. |
|
||||||
|
|
||||||
# GIT-001 - Add a command to move to a different home.
|
# GIT-001 - Add a command to move to a different home.
|
||||||
@@ -46,13 +46,11 @@
|
|||||||
`/metropolis-home-move <homeNumber> [playerName]` selects one of a player's existing homes as active. Without a player name, the command acts on the player who ran it; console and authorized administrators can specify an online or cached offline player. Reject non-positive or malformed numbers, unknown players, nonexistent homes, and extra arguments with clear feedback. Persist the selection by player UUID and confirm the change to the sender and, when online, the target player.
|
`/metropolis-home-move <homeNumber> [playerName]` selects one of a player's existing homes as active. Without a player name, the command acts on the player who ran it; console and authorized administrators can specify an online or cached offline player. Reject non-positive or malformed numbers, unknown players, nonexistent homes, and extra arguments with clear feedback. Persist the selection by player UUID and confirm the change to the sender and, when online, the target player.
|
||||||
|
|
||||||
# GIT-002 - Add a command to teleport to a reservation you are part of.
|
# GIT-002 - Add a command to teleport to a reservation you are part of.
|
||||||
**Status:** Ready
|
**Status:** Done
|
||||||
**Depends On:** [GIT-003](#git-003---add-support-for-multiple-homes)
|
**Depends On:** [GIT-003](#git-003---add-support-for-multiple-homes)
|
||||||
**Description:**
|
**Description:**
|
||||||
|
|
||||||
/metropolis-plot-go [playerName]
|
`/metropolis-plot-go [playerName]` teleports the sender, or the specified online player, to a Metropolis WorldGuard cuboid reservation where that player is an owner or member. If the player belongs to more than one reservation, report the available region names and require an explicit choice with `/metropolis-plot-go <reservationName> [playerName]`. The explicit-name form only permits destinations where the target is a member/owner, unless the sender has `metropolis.plot.go`. Remove the command-level permission gate so reservation members can use self-service teleportation; enforce admin-only target and membership bypass checks in the command. Report unknown players, missing membership, ambiguous reservations, and reservations without a safe teleport location. Do not report success unless teleportation succeeds.
|
||||||
The target player should be [playerName] if specified and otherwise should be the callee.
|
|
||||||
The reservation to teleport to is
|
|
||||||
|
|
||||||
### Comments
|
### Comments
|
||||||
|
|
||||||
@@ -70,24 +68,25 @@ On join, automatically allocate one home only when the player has no existing ho
|
|||||||
The alternative login allocation policies are tracked separately in MET-023 and MET-024; they are not part of the default policy in this task.
|
The alternative login allocation policies are tracked separately in MET-023 and MET-024; they are not part of the default policy in this task.
|
||||||
|
|
||||||
# GIT-004 - Add a command to request home allocation.
|
# GIT-004 - Add a command to request home allocation.
|
||||||
**Status:** Ready
|
**Status:** Done
|
||||||
**Depends On:** [GIT-003](#git-003---add-support-for-multiple-homes)
|
**Depends On:** [GIT-003](#git-003---add-support-for-multiple-homes)
|
||||||
**Description:**
|
**Description:**
|
||||||
|
|
||||||
Add a command that users can type to get their first or subsequent homes.
|
`/metropolis-home-acquire` gives players their next available home, including the first home when `plot.initial` is `0`. The command allocates the lowest unused positive home number, makes it active, and refuses requests once the player has reached their effective limit. The default policy remains automatic first-home generation, but servers can disable it by setting `plot.initial: 0` and requiring the command for new players.
|
||||||
Add a config option to not generate any homes automatically, but to require this command to generate a user's first home.
|
|
||||||
|
|
||||||
This could be used so that homes won't be generated until a user has read the server rules which would include telling them to type the command towards the end.
|
|
||||||
|
|
||||||
# GIT-005 - Add the possibility to create multiple plot homes in shapes.
|
# GIT-005 - Add the possibility to create multiple plot homes in shapes.
|
||||||
**Status:** Planning
|
**Status:** Planning
|
||||||
|
**Depends On:** [GIT-006](#git-006---add-levels-support), [MET-025](#met-025---add-a-configuration-option-to-choose-the-plot-allocation-traversal-algorithm-concentric-rectanglescuboids-or-concentric-rings-that-prefer-a-free-location-near-the-center)
|
||||||
**Description:**
|
**Description:**
|
||||||
|
|
||||||
Add the possibility to create multiple plot homes in shapes
|
Add the possibility to create multiple plot homes in contiguous shapes.
|
||||||
|
|
||||||
|
* Line all plots in a line x, y, or z.
|
||||||
|
* Rectangle with width (x), height(y), and length (z).
|
||||||
|
* Sphere closest to a point.
|
||||||
|
|
||||||
# GIT-006 - Add levels support.
|
# GIT-006 - Add levels support.
|
||||||
**Status:** Ready
|
**Status:** Done
|
||||||
**Description:**
|
**Description:**
|
||||||
|
|
||||||
Add multiple vertical levels to the city. Use the existing `road.level` as the base Y: the bottom of a ground-level plot is at that Y, and `plot.sizeY` defines the full plot height. For example, a five-block-high plot includes its floor at road level, three usable blocks, and a roof block; all five blocks belong to the plot region. Use the same plot-dimension-plus-road-spacing grid pitch vertically as horizontally; when road width is zero, adjacent plot volumes may touch. Do not allocate a plot if its full extent would exceed the world's buildable area. If the search cannot find any valid plot, log an allocation error and do not create the plot or region. The current config example is not intended for a multilevel world; request an updated `config.yml` before implementation. Existing `plot.offsetX`, `plot.offsetY`, and `plot.offsetZ` nudge the grid; `plot.multiplier`, `userOverrides[username].plotMultiplier`, and future `permissionOverrides[permission_name].plotMultiplier` affect plot size.
|
Add multiple vertical levels to the city. Use the existing `road.level` as the base Y: the bottom of a ground-level plot is at that Y, and `plot.sizeY` defines the full plot height. For example, a five-block-high plot includes its floor at road level, three usable blocks, and a roof block; all five blocks belong to the plot region. Use the same plot-dimension-plus-road-spacing grid pitch vertically as horizontally; when road width is zero, adjacent plot volumes may touch. Do not allocate a plot if its full extent would exceed the world's buildable area. If the search cannot find any valid plot, log an allocation error and do not create the plot or region. The current config example is not intended for a multilevel world; request an updated `config.yml` before implementation. Existing `plot.offsetX`, `plot.offsetY`, and `plot.offsetZ` nudge the grid; `plot.multiplier`, `userOverrides[username].plotMultiplier`, and future `permissionOverrides[permission_name].plotMultiplier` affect plot size.
|
||||||
@@ -130,3 +129,7 @@ x a b b x
|
|||||||
x x s x x
|
x x s x x
|
||||||
x x x x x
|
x x x x x
|
||||||
x x x x x
|
x x x x x
|
||||||
|
|
||||||
|
# MET-025 - Add a configuration option to choose the plot-allocation traversal algorithm: concentric rectangles/cuboids or concentric rings that prefer a free location near the center.
|
||||||
|
**Status:** Planning
|
||||||
|
**Depends On:** [MET-023], [MET-024]
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Metropolis.
|
||||||
|
|
||||||
|
Metropolis is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Metropolis is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.majinnaibu.bukkitplugins.metropolis;
|
||||||
|
|
||||||
|
import com.majinnaibu.bukkitplugins.metropolis.AvenueStairwayLayout.Flight;
|
||||||
|
import com.majinnaibu.bukkitplugins.metropolis.AvenueStairwayLayout.Step;
|
||||||
|
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.block.BlockFace;
|
||||||
|
import org.bukkit.block.data.Bisected;
|
||||||
|
import org.bukkit.block.data.BlockData;
|
||||||
|
import org.bukkit.block.data.type.Stairs;
|
||||||
|
|
||||||
|
public final class AvenueStairwayBuilder {
|
||||||
|
private AvenueStairwayBuilder() {}
|
||||||
|
|
||||||
|
public static void build(World world, Flight flight, Material stairMaterial) {
|
||||||
|
BlockFace ascendingFacing = flight.side().runsAlongX() ? BlockFace.EAST : BlockFace.SOUTH;
|
||||||
|
BlockFace descendingFacing = ascendingFacing.getOppositeFace();
|
||||||
|
for (Step step : flight.stairBlocks()) {
|
||||||
|
Block stairBlock = world.getBlockAt(step.x(), step.y(), step.z());
|
||||||
|
stairBlock.setType(stairMaterial, false);
|
||||||
|
BlockData blockData = stairBlock.getBlockData();
|
||||||
|
if (blockData instanceof Stairs stairs) {
|
||||||
|
stairs.setFacing(ascendingFacing);
|
||||||
|
stairs.setHalf(Bisected.Half.BOTTOM);
|
||||||
|
stairs.setShape(Stairs.Shape.STRAIGHT);
|
||||||
|
stairBlock.setBlockData(stairs, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Step backing : flight.invertedBackingBlocks()) {
|
||||||
|
Block supportBlock = world.getBlockAt(backing.x(), backing.y(), backing.z());
|
||||||
|
supportBlock.setType(stairMaterial, false);
|
||||||
|
BlockData supportData = supportBlock.getBlockData();
|
||||||
|
if (supportData instanceof Stairs stairs) {
|
||||||
|
stairs.setFacing(descendingFacing);
|
||||||
|
stairs.setHalf(Bisected.Half.TOP);
|
||||||
|
stairs.setShape(Stairs.Shape.STRAIGHT);
|
||||||
|
supportBlock.setBlockData(stairs, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Step opening : flight.roadOpenings()) {
|
||||||
|
world.getBlockAt(opening.x(), opening.y(), opening.z()).setType(Material.AIR, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Metropolis.
|
||||||
|
|
||||||
|
Metropolis is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Metropolis is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.majinnaibu.bukkitplugins.metropolis;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class AvenueStairwayLayout {
|
||||||
|
private AvenueStairwayLayout() {}
|
||||||
|
|
||||||
|
public static List<Step> centeredEastAvenue(
|
||||||
|
Cuboid plot, int roadWidth, int stairWidth, int lowerY, int rise) {
|
||||||
|
return stairBlocks(plot, roadWidth, stairWidth, lowerY, rise, Side.EAST);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Flight> forUpperRoadSegments(
|
||||||
|
Cuboid plot,
|
||||||
|
int plotSizeZ,
|
||||||
|
int gridSizeZ,
|
||||||
|
int firstBlockIndex,
|
||||||
|
int everyNBlocks,
|
||||||
|
int avenueWidth,
|
||||||
|
int stairWidth,
|
||||||
|
int lowerY,
|
||||||
|
int rise,
|
||||||
|
int openingLength) {
|
||||||
|
if (plotSizeZ < 1 || gridSizeZ < plotSizeZ || everyNBlocks < 1) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Flight> flights = new ArrayList<>();
|
||||||
|
int segmentIndex = 0;
|
||||||
|
for (int segmentStart = plot.minZ;
|
||||||
|
segmentStart <= plot.maxZ;
|
||||||
|
segmentStart += gridSizeZ, segmentIndex++) {
|
||||||
|
int avenueBlockIndex = firstBlockIndex + segmentIndex;
|
||||||
|
if (Math.floorMod(avenueBlockIndex + 1, everyNBlocks) != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cuboid segment =
|
||||||
|
new Cuboid(
|
||||||
|
plot.minX,
|
||||||
|
plot.minY,
|
||||||
|
segmentStart,
|
||||||
|
plot.maxX,
|
||||||
|
plot.maxY,
|
||||||
|
Math.min(segmentStart + plotSizeZ - 1, plot.maxZ));
|
||||||
|
for (Side side : List.of(Side.EAST, Side.WEST)) {
|
||||||
|
List<Step> stairs = stairBlocks(segment, avenueWidth, stairWidth, lowerY, rise, side);
|
||||||
|
if (stairs.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<Step> backings = invertedBackings(stairs, lowerY);
|
||||||
|
List<Step> openings =
|
||||||
|
upperRoadOpening(stairs, avenueWidth, stairWidth, lowerY, rise, openingLength, side);
|
||||||
|
flights.add(new Flight(side, stairs, backings, openings));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(flights);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Flight> forUpperStreetSegments(
|
||||||
|
Cuboid plot,
|
||||||
|
int plotSizeX,
|
||||||
|
int gridSizeX,
|
||||||
|
int firstBlockIndex,
|
||||||
|
int everyNBlocks,
|
||||||
|
int streetWidth,
|
||||||
|
int stairWidth,
|
||||||
|
int lowerY,
|
||||||
|
int rise,
|
||||||
|
int openingLength) {
|
||||||
|
if (plotSizeX < 1 || gridSizeX < plotSizeX || everyNBlocks < 1) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Flight> flights = new ArrayList<>();
|
||||||
|
int segmentIndex = 0;
|
||||||
|
for (int segmentStart = plot.minX;
|
||||||
|
segmentStart <= plot.maxX;
|
||||||
|
segmentStart += gridSizeX, segmentIndex++) {
|
||||||
|
if (Math.floorMod(firstBlockIndex + segmentIndex + 1, everyNBlocks) != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cuboid segment =
|
||||||
|
new Cuboid(
|
||||||
|
segmentStart,
|
||||||
|
plot.minY,
|
||||||
|
plot.minZ,
|
||||||
|
Math.min(segmentStart + plotSizeX - 1, plot.maxX),
|
||||||
|
plot.maxY,
|
||||||
|
plot.maxZ);
|
||||||
|
for (Side side : List.of(Side.NORTH, Side.SOUTH)) {
|
||||||
|
List<Step> stairs = stairBlocks(segment, streetWidth, stairWidth, lowerY, rise, side);
|
||||||
|
if (stairs.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
flights.add(
|
||||||
|
new Flight(
|
||||||
|
side,
|
||||||
|
stairs,
|
||||||
|
invertedBackings(stairs, lowerY),
|
||||||
|
upperRoadOpening(
|
||||||
|
stairs, streetWidth, stairWidth, lowerY, rise, openingLength, side)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(flights);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Step> upperRoadOpening(
|
||||||
|
Cuboid plot, int roadWidth, int stairWidth, int lowerY, int rise, int openingLength) {
|
||||||
|
List<Step> stairs = centeredEastAvenue(plot, roadWidth, stairWidth, lowerY, rise);
|
||||||
|
return upperRoadOpening(stairs, roadWidth, stairWidth, lowerY, rise, openingLength, Side.EAST);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Step> upperRoadOpening(
|
||||||
|
List<Step> stairs,
|
||||||
|
int roadWidth,
|
||||||
|
int stairWidth,
|
||||||
|
int lowerY,
|
||||||
|
int rise,
|
||||||
|
int openingLength,
|
||||||
|
Side side) {
|
||||||
|
if (stairs.isEmpty() || openingLength < 1) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
Step topStair = stairs.get(stairs.size() - 1);
|
||||||
|
Step firstStair = stairs.getFirst();
|
||||||
|
int upperRoadY = lowerY + rise;
|
||||||
|
List<Step> opening = new ArrayList<>(stairWidth * openingLength);
|
||||||
|
for (int widthIndex = 0; widthIndex < stairWidth; widthIndex++) {
|
||||||
|
for (int lengthIndex = openingLength; lengthIndex > 0; lengthIndex--) {
|
||||||
|
opening.add(
|
||||||
|
side.runsAlongX()
|
||||||
|
? new Step(topStair.x() - lengthIndex, upperRoadY, firstStair.z() + widthIndex)
|
||||||
|
: new Step(firstStair.x() + widthIndex, upperRoadY, topStair.z() - lengthIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(opening);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Flight forUpperRoad(
|
||||||
|
Cuboid plot, int roadWidth, int stairWidth, int lowerY, int rise, int openingLength) {
|
||||||
|
List<Step> stairs = centeredEastAvenue(plot, roadWidth, stairWidth, lowerY, rise);
|
||||||
|
if (stairs.isEmpty()) {
|
||||||
|
return new Flight(Side.EAST, List.of(), List.of(), List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Flight(
|
||||||
|
Side.EAST,
|
||||||
|
stairs,
|
||||||
|
invertedBackings(stairs, lowerY),
|
||||||
|
upperRoadOpening(stairs, roadWidth, stairWidth, lowerY, rise, openingLength, Side.EAST));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Step> invertedBackings(List<Step> stairs, int lowerY) {
|
||||||
|
List<Step> invertedBackings = new ArrayList<>();
|
||||||
|
for (Step stair : stairs) {
|
||||||
|
if (stair.y() > lowerY + 1) {
|
||||||
|
invertedBackings.add(new Step(stair.x(), stair.y() - 1, stair.z()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(invertedBackings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Step> stairBlocks(
|
||||||
|
Cuboid plot, int roadWidth, int stairWidth, int lowerY, int rise, Side side) {
|
||||||
|
int plotLength = side.runsAlongX() ? plot.maxX - plot.minX + 1 : plot.maxZ - plot.minZ + 1;
|
||||||
|
if (roadWidth < 1
|
||||||
|
|| stairWidth < 1
|
||||||
|
|| stairWidth >= roadWidth
|
||||||
|
|| rise < 1
|
||||||
|
|| rise + 2 > plotLength) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
int firstRun = (side.runsAlongX() ? plot.minX : plot.minZ) + (plotLength - rise) / 2 + 1;
|
||||||
|
int firstCross =
|
||||||
|
switch (side) {
|
||||||
|
case EAST -> plot.maxX + 1 + (roadWidth - stairWidth) / 2;
|
||||||
|
case WEST -> plot.minX - roadWidth + (roadWidth - stairWidth) / 2;
|
||||||
|
case NORTH -> plot.minZ - roadWidth + (roadWidth - stairWidth) / 2;
|
||||||
|
case SOUTH -> plot.maxZ + 1 + (roadWidth - stairWidth) / 2;
|
||||||
|
};
|
||||||
|
List<Step> steps = new ArrayList<>(stairWidth * rise);
|
||||||
|
for (int riseIndex = 0; riseIndex < rise; riseIndex++) {
|
||||||
|
for (int widthIndex = 0; widthIndex < stairWidth; widthIndex++) {
|
||||||
|
steps.add(
|
||||||
|
side.runsAlongX()
|
||||||
|
? new Step(firstRun + riseIndex, lowerY + riseIndex + 1, firstCross + widthIndex)
|
||||||
|
: new Step(firstCross + widthIndex, lowerY + riseIndex + 1, firstRun + riseIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return List.copyOf(steps);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Side {
|
||||||
|
EAST,
|
||||||
|
WEST,
|
||||||
|
NORTH,
|
||||||
|
SOUTH;
|
||||||
|
|
||||||
|
public boolean runsAlongX() {
|
||||||
|
return this == NORTH || this == SOUTH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Step(int x, int y, int z) {}
|
||||||
|
|
||||||
|
public record Flight(
|
||||||
|
Side side,
|
||||||
|
List<Step> stairBlocks,
|
||||||
|
List<Step> invertedBackingBlocks,
|
||||||
|
List<Step> roadOpenings) {
|
||||||
|
public Flight {
|
||||||
|
stairBlocks = List.copyOf(stairBlocks);
|
||||||
|
invertedBackingBlocks = List.copyOf(invertedBackingBlocks);
|
||||||
|
roadOpenings = List.copyOf(roadOpenings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Metropolis.
|
||||||
|
|
||||||
|
Metropolis is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Metropolis is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.majinnaibu.bukkitplugins.metropolis;
|
||||||
|
|
||||||
|
public final class CityRegionBounds {
|
||||||
|
private CityRegionBounds() {}
|
||||||
|
|
||||||
|
public static Cuboid expandToContain(
|
||||||
|
Cuboid existing, Cuboid requested, Iterable<Cuboid> occupiedRegions) {
|
||||||
|
int minX = Math.min(existing.minX, requested.minX);
|
||||||
|
int minY = Math.min(existing.minY, requested.minY);
|
||||||
|
int minZ = Math.min(existing.minZ, requested.minZ);
|
||||||
|
int maxX = Math.max(existing.maxX, requested.maxX);
|
||||||
|
int maxY = Math.max(existing.maxY, requested.maxY);
|
||||||
|
int maxZ = Math.max(existing.maxZ, requested.maxZ);
|
||||||
|
|
||||||
|
for (Cuboid occupied : occupiedRegions) {
|
||||||
|
minX = Math.min(minX, occupied.minX);
|
||||||
|
minY = Math.min(minY, occupied.minY);
|
||||||
|
minZ = Math.min(minZ, occupied.minZ);
|
||||||
|
maxX = Math.max(maxX, occupied.maxX);
|
||||||
|
maxY = Math.max(maxY, occupied.maxY);
|
||||||
|
maxZ = Math.max(maxZ, occupied.maxZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Cuboid(minX, minY, minZ, maxX, maxY, maxZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Metropolis.
|
||||||
|
|
||||||
|
Metropolis is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Metropolis is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.majinnaibu.bukkitplugins.metropolis;
|
||||||
|
|
||||||
|
import com.sk89q.worldedit.math.BlockVector3;
|
||||||
|
|
||||||
|
public final class PlotGridLayout {
|
||||||
|
private final int plotSizeX;
|
||||||
|
private final int plotSizeY;
|
||||||
|
private final int plotSizeZ;
|
||||||
|
private final int streetWidth;
|
||||||
|
private final int avenueWidth;
|
||||||
|
private final int offsetX;
|
||||||
|
private final int offsetY;
|
||||||
|
private final int offsetZ;
|
||||||
|
private final int roadLevel;
|
||||||
|
private final int gridSizeX;
|
||||||
|
private final int gridSizeY;
|
||||||
|
private final int gridSizeZ;
|
||||||
|
|
||||||
|
public PlotGridLayout(
|
||||||
|
int plotSizeX,
|
||||||
|
int plotSizeY,
|
||||||
|
int plotSizeZ,
|
||||||
|
int streetWidth,
|
||||||
|
int avenueWidth,
|
||||||
|
int offsetX,
|
||||||
|
int offsetY,
|
||||||
|
int offsetZ,
|
||||||
|
int roadLevel) {
|
||||||
|
if (plotSizeX < 1 || plotSizeY < 1 || plotSizeZ < 1 || streetWidth < 0 || avenueWidth < 0) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Plot dimensions must be positive and road widths nonnegative");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.plotSizeX = plotSizeX;
|
||||||
|
this.plotSizeY = plotSizeY;
|
||||||
|
this.plotSizeZ = plotSizeZ;
|
||||||
|
this.streetWidth = streetWidth;
|
||||||
|
this.avenueWidth = avenueWidth;
|
||||||
|
this.offsetX = offsetX;
|
||||||
|
this.offsetY = offsetY;
|
||||||
|
this.offsetZ = offsetZ;
|
||||||
|
this.roadLevel = roadLevel;
|
||||||
|
gridSizeX = Math.addExact(plotSizeX, avenueWidth);
|
||||||
|
gridSizeY = Math.addExact(plotSizeY, Math.max(streetWidth, avenueWidth));
|
||||||
|
gridSizeZ = Math.addExact(plotSizeZ, streetWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlockVector3 gridMin(int row, int col, int level) {
|
||||||
|
return BlockVector3.at(
|
||||||
|
offsetX + col * gridSizeX, levelStartY(level), offsetZ + row * gridSizeZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlockVector3 gridMax(int row, int col, int multiplierX, int multiplierZ, int level) {
|
||||||
|
return BlockVector3.at(
|
||||||
|
offsetX + (col + multiplierX) * gridSizeX - 1,
|
||||||
|
levelStartY(level) + plotSizeY - 1,
|
||||||
|
offsetZ + (row + multiplierZ) * gridSizeZ - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlockVector3 plotMin(int row, int col, int level) {
|
||||||
|
BlockVector3 gridMin = gridMin(row, col, level);
|
||||||
|
return BlockVector3.at(
|
||||||
|
gridMin.x() + avenueWidth / 2, gridMin.y(), gridMin.z() + streetWidth / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlockVector3 plotMax(int row, int col, int multiplierX, int multiplierZ, int level) {
|
||||||
|
BlockVector3 gridMax = gridMax(row, col, multiplierX, multiplierZ, level);
|
||||||
|
return BlockVector3.at(
|
||||||
|
gridMax.x() - (avenueWidth - avenueWidth / 2),
|
||||||
|
gridMax.y(),
|
||||||
|
gridMax.z() - (streetWidth - streetWidth / 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int plotXIndexFromMin(Cuboid plot) {
|
||||||
|
return Math.floorDiv(plot.minX - offsetX - avenueWidth / 2, gridSizeX);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int plotZIndexFromMin(Cuboid plot) {
|
||||||
|
return Math.floorDiv(plot.minZ - offsetZ - streetWidth / 2, gridSizeZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int levelStartY(int level) {
|
||||||
|
return roadLevel + offsetY + level * gridSizeY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int gridSizeX() {
|
||||||
|
return gridSizeX;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int gridSizeY() {
|
||||||
|
return gridSizeY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int gridSizeZ() {
|
||||||
|
return gridSizeZ;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Metropolis.
|
||||||
|
|
||||||
|
Metropolis is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Metropolis is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.majinnaibu.bukkitplugins.metropolis;
|
||||||
|
|
||||||
|
public final class PlotLevelSupport {
|
||||||
|
private PlotLevelSupport() {}
|
||||||
|
|
||||||
|
public static boolean hasCompleteSupport(
|
||||||
|
int row, int col, int multiplier, int targetLevel, GeneratedCellLookup generatedCellLookup) {
|
||||||
|
if (multiplier < 1 || targetLevel < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int supportLevel = 0; supportLevel < targetLevel; supportLevel++) {
|
||||||
|
for (int rowOffset = 0; rowOffset < multiplier; rowOffset++) {
|
||||||
|
for (int colOffset = 0; colOffset < multiplier; colOffset++) {
|
||||||
|
if (!generatedCellLookup.isGenerated(row + rowOffset, col + colOffset, supportLevel)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface GeneratedCellLookup {
|
||||||
|
boolean isGenerated(int row, int col, int level);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Metropolis.
|
||||||
|
|
||||||
|
Metropolis is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Metropolis is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.majinnaibu.bukkitplugins.metropolis;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import com.sk89q.worldguard.protection.flags.Flags;
|
||||||
|
import com.sk89q.worldguard.protection.flags.StateFlag;
|
||||||
|
import com.sk89q.worldguard.protection.regions.ProtectedCuboidRegion;
|
||||||
|
import com.sk89q.worldguard.protection.regions.ProtectedRegion;
|
||||||
|
|
||||||
|
public final class PlotReservationLookup {
|
||||||
|
private PlotReservationLookup() {}
|
||||||
|
|
||||||
|
public static List<Plot> forPlayer(Iterable<ProtectedRegion> regions, UUID playerId) {
|
||||||
|
List<Plot> reservations = new ArrayList<>();
|
||||||
|
for (ProtectedRegion region : regions) {
|
||||||
|
if (!(region instanceof ProtectedCuboidRegion) || !isReservation(region)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (region.getOwners().getUniqueIds().contains(playerId)
|
||||||
|
|| region.getMembers().getUniqueIds().contains(playerId)) {
|
||||||
|
reservations.add(Plot.get(region));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reservations.sort(Comparator.comparing(Plot::getRegionName));
|
||||||
|
return List.copyOf(reservations);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Plot findByName(List<Plot> reservations, String requestedName) {
|
||||||
|
String prefixedName = requestedName.startsWith("r_") ? requestedName : "r_" + requestedName;
|
||||||
|
for (Plot reservation : reservations) {
|
||||||
|
if (reservation.getRegionName().equals(requestedName)
|
||||||
|
|| reservation.getRegionName().equals(prefixedName)) {
|
||||||
|
return reservation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isReservation(ProtectedRegion region) {
|
||||||
|
String regionId = region.getId();
|
||||||
|
if (regionId.startsWith("r_")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (regionId.startsWith("h_")
|
||||||
|
|| regionId.equalsIgnoreCase("City")
|
||||||
|
|| regionId.equalsIgnoreCase("Spawn")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return region.getFlag(Flags.LAVA_FLOW) == StateFlag.State.DENY
|
||||||
|
&& region.getFlag(Flags.SNOW_FALL) == StateFlag.State.DENY;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Metropolis.
|
||||||
|
|
||||||
|
Metropolis is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Metropolis is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.majinnaibu.bukkitplugins.metropolis;
|
||||||
|
|
||||||
|
public final class PlotWorldBorderBounds {
|
||||||
|
private PlotWorldBorderBounds() {}
|
||||||
|
|
||||||
|
public static int maximumSearchRing(
|
||||||
|
double borderSize,
|
||||||
|
double borderCenterX,
|
||||||
|
double borderCenterZ,
|
||||||
|
int offsetX,
|
||||||
|
int offsetZ,
|
||||||
|
int gridSizeX,
|
||||||
|
int gridSizeZ,
|
||||||
|
int plotMultiplier) {
|
||||||
|
if (borderSize <= 0 || gridSizeX <= 0 || gridSizeZ <= 0 || plotMultiplier < 1) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
double halfBorder = borderSize / 2;
|
||||||
|
double farthestX = halfBorder + Math.abs(borderCenterX - offsetX);
|
||||||
|
double farthestZ = halfBorder + Math.abs(borderCenterZ - offsetZ);
|
||||||
|
double cellRadius = Math.max(farthestX / gridSizeX, farthestZ / gridSizeZ);
|
||||||
|
long bound = (long) Math.ceil(cellRadius) + plotMultiplier + 1L;
|
||||||
|
return (int) Math.min(bound, Integer.MAX_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean contains(Cuboid bounds, double borderSize, double centerX, double centerZ) {
|
||||||
|
if (borderSize <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
double halfBorder = borderSize / 2;
|
||||||
|
double borderMinX = centerX - halfBorder;
|
||||||
|
double borderMaxX = centerX + halfBorder;
|
||||||
|
double borderMinZ = centerZ - halfBorder;
|
||||||
|
double borderMaxZ = centerZ + halfBorder;
|
||||||
|
return bounds.minX + 0.5 >= borderMinX
|
||||||
|
&& bounds.maxX + 0.5 <= borderMaxX
|
||||||
|
&& bounds.minZ + 0.5 >= borderMinZ
|
||||||
|
&& bounds.maxZ + 0.5 <= borderMaxZ;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Metropolis.
|
||||||
|
|
||||||
|
Metropolis is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Metropolis is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.majinnaibu.bukkitplugins.metropolis;
|
||||||
|
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.block.data.type.Stairs;
|
||||||
|
|
||||||
|
public final class RoadBlockWriter {
|
||||||
|
private RoadBlockWriter() {}
|
||||||
|
|
||||||
|
public static void build(
|
||||||
|
World world,
|
||||||
|
int x,
|
||||||
|
int y,
|
||||||
|
int z,
|
||||||
|
Material roadMaterial,
|
||||||
|
Material supportMaterial,
|
||||||
|
boolean generateSupports,
|
||||||
|
int clearSpaceAbove) {
|
||||||
|
Block roadBlock = world.getBlockAt(x, y, z);
|
||||||
|
if (roadBlock.getType() == roadMaterial || roadBlock.getBlockData() instanceof Stairs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
roadBlock.setType(roadMaterial);
|
||||||
|
if (generateSupports && (roadMaterial == Material.GRAVEL || roadMaterial == Material.SAND)) {
|
||||||
|
Block blockUnder = world.getBlockAt(x, y - 1, z);
|
||||||
|
if (!isSolid(blockUnder.getType())) {
|
||||||
|
blockUnder.setType(supportMaterial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int clearance = 1; clearance <= clearSpaceAbove; clearance++) {
|
||||||
|
Block blockAbove = world.getBlockAt(x, y + clearance, z);
|
||||||
|
if (blockAbove.getType() != Material.AIR) {
|
||||||
|
blockAbove.setType(Material.AIR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isSolid(Material material) {
|
||||||
|
return material.isBlock()
|
||||||
|
&& material != Material.AIR
|
||||||
|
&& material != Material.WATER
|
||||||
|
&& material != Material.LAVA
|
||||||
|
&& material != Material.TORCH
|
||||||
|
&& material != Material.REDSTONE_TORCH;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Metropolis.
|
||||||
|
|
||||||
|
Metropolis is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Metropolis is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.majinnaibu.bukkitplugins.metropolis;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class RoadLayout {
|
||||||
|
private RoadLayout() {}
|
||||||
|
|
||||||
|
public static List<BlockPosition> surroundingRoad(Cuboid plot, int streetWidth, int avenueWidth) {
|
||||||
|
if (streetWidth < 0 || avenueWidth < 0 || streetWidth + avenueWidth == 0) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<BlockPosition> blocks = new ArrayList<>();
|
||||||
|
addRectangle(
|
||||||
|
blocks, plot.minX, plot.maxX, plot.minZ - streetWidth, plot.minZ - 1, RoadType.STREET);
|
||||||
|
addRectangle(
|
||||||
|
blocks, plot.minX, plot.maxX, plot.maxZ + 1, plot.maxZ + streetWidth, RoadType.STREET);
|
||||||
|
addRectangle(
|
||||||
|
blocks, plot.minX - avenueWidth, plot.minX - 1, plot.minZ, plot.maxZ, RoadType.AVENUE);
|
||||||
|
addRectangle(
|
||||||
|
blocks, plot.maxX + 1, plot.maxX + avenueWidth, plot.minZ, plot.maxZ, RoadType.AVENUE);
|
||||||
|
if (streetWidth > 0 && avenueWidth > 0) {
|
||||||
|
addRectangle(
|
||||||
|
blocks,
|
||||||
|
plot.minX - avenueWidth,
|
||||||
|
plot.minX - 1,
|
||||||
|
plot.minZ - streetWidth,
|
||||||
|
plot.minZ - 1,
|
||||||
|
RoadType.INTERSECTION);
|
||||||
|
addRectangle(
|
||||||
|
blocks,
|
||||||
|
plot.maxX + 1,
|
||||||
|
plot.maxX + avenueWidth,
|
||||||
|
plot.minZ - streetWidth,
|
||||||
|
plot.minZ - 1,
|
||||||
|
RoadType.INTERSECTION);
|
||||||
|
addRectangle(
|
||||||
|
blocks,
|
||||||
|
plot.minX - avenueWidth,
|
||||||
|
plot.minX - 1,
|
||||||
|
plot.maxZ + 1,
|
||||||
|
plot.maxZ + streetWidth,
|
||||||
|
RoadType.INTERSECTION);
|
||||||
|
addRectangle(
|
||||||
|
blocks,
|
||||||
|
plot.maxX + 1,
|
||||||
|
plot.maxX + avenueWidth,
|
||||||
|
plot.maxZ + 1,
|
||||||
|
plot.maxZ + streetWidth,
|
||||||
|
RoadType.INTERSECTION);
|
||||||
|
}
|
||||||
|
return List.copyOf(blocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addRectangle(
|
||||||
|
List<BlockPosition> blocks, int minX, int maxX, int minZ, int maxZ, RoadType roadType) {
|
||||||
|
for (int x = minX; x <= maxX; x++) {
|
||||||
|
for (int z = minZ; z <= maxZ; z++) {
|
||||||
|
blocks.add(new BlockPosition(x, z, roadType));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum RoadType {
|
||||||
|
STREET,
|
||||||
|
AVENUE,
|
||||||
|
INTERSECTION
|
||||||
|
}
|
||||||
|
|
||||||
|
public record BlockPosition(int x, int z, RoadType roadType) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Metropolis.
|
||||||
|
|
||||||
|
Metropolis is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Metropolis is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.majinnaibu.bukkitplugins.metropolis;
|
||||||
|
|
||||||
|
public final class SpawnLayout {
|
||||||
|
private SpawnLayout() {}
|
||||||
|
|
||||||
|
public static Cuboid bounds(
|
||||||
|
int sizeX,
|
||||||
|
int sizeY,
|
||||||
|
int sizeZ,
|
||||||
|
int plotSizeX,
|
||||||
|
int plotSizeY,
|
||||||
|
int plotSizeZ,
|
||||||
|
int avenueWidth,
|
||||||
|
int streetWidth,
|
||||||
|
int roadLevel,
|
||||||
|
int offsetX,
|
||||||
|
int offsetY,
|
||||||
|
int offsetZ) {
|
||||||
|
if (sizeX < 1 || sizeY < 1 || sizeZ < 1 || avenueWidth < 0 || streetWidth < 0) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Spawn multipliers must be positive and road widths nonnegative");
|
||||||
|
}
|
||||||
|
|
||||||
|
int gridSizeX = Math.addExact(plotSizeX, avenueWidth);
|
||||||
|
int gridSizeZ = Math.addExact(plotSizeZ, streetWidth);
|
||||||
|
int minX = avenueWidth / 2;
|
||||||
|
int minZ = streetWidth / 2;
|
||||||
|
int maxInsetX = avenueWidth - minX;
|
||||||
|
int maxInsetZ = streetWidth - minZ;
|
||||||
|
int maxX = Math.subtractExact(Math.multiplyExact(sizeX, gridSizeX) - 1, maxInsetX);
|
||||||
|
int baseY = Math.addExact(roadLevel, offsetY);
|
||||||
|
int maxY = Math.addExact(baseY, Math.multiplyExact(sizeY, plotSizeY) - 1);
|
||||||
|
int maxZ = Math.subtractExact(Math.multiplyExact(sizeZ, gridSizeZ) - 1, maxInsetZ);
|
||||||
|
|
||||||
|
return new Cuboid(offsetX + minX, baseY, offsetZ + minZ, offsetX + maxX, maxY, offsetZ + maxZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean withinBuildHeight(Cuboid cuboid, int minHeight, int maxHeight) {
|
||||||
|
return cuboid.minY >= minHeight && cuboid.maxY < maxHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Metropolis.
|
||||||
|
|
||||||
|
Metropolis is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Metropolis is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.majinnaibu.bukkitplugins.metropolis;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.sk89q.worldguard.protection.regions.ProtectedRegion;
|
||||||
|
|
||||||
|
public final class WorldGuardRegionOccupancy {
|
||||||
|
private WorldGuardRegionOccupancy() {}
|
||||||
|
|
||||||
|
public static List<Cuboid> boundsToAvoid(Collection<ProtectedRegion> regions) {
|
||||||
|
List<Cuboid> bounds = new ArrayList<>();
|
||||||
|
for (ProtectedRegion region : regions) {
|
||||||
|
String id = region.getId();
|
||||||
|
if (id.equalsIgnoreCase("city") || id.equalsIgnoreCase("__global__")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
bounds.add(new Cuboid(region.getMinimumPoint(), region.getMaximumPoint()));
|
||||||
|
}
|
||||||
|
return List.copyOf(bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean overlapsAny(Cuboid candidate, Collection<Cuboid> occupiedBounds) {
|
||||||
|
return occupiedBounds.stream().anyMatch(candidate::intersects);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,15 +17,22 @@ along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
|||||||
|
|
||||||
package com.majinnaibu.bukkitplugins.metropolis.commands;
|
package com.majinnaibu.bukkitplugins.metropolis.commands;
|
||||||
|
|
||||||
import com.majinnaibu.bukkitplugins.metropolis.MetropolisPlugin;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
import com.majinnaibu.bukkitplugins.metropolis.MetropolisPlugin;
|
||||||
|
import com.majinnaibu.bukkitplugins.metropolis.PlayerHome;
|
||||||
|
|
||||||
|
import org.bukkit.OfflinePlayer;
|
||||||
import org.bukkit.command.Command;
|
import org.bukkit.command.Command;
|
||||||
import org.bukkit.command.CommandExecutor;
|
import org.bukkit.command.CommandExecutor;
|
||||||
import org.bukkit.command.CommandSender;
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.scheduler.BukkitTask;
|
||||||
|
|
||||||
public class MetropolisDebugGenerateTestHomesCommand implements CommandExecutor {
|
public class MetropolisDebugGenerateTestHomesCommand implements CommandExecutor {
|
||||||
|
private static final long TICKS_BETWEEN_HOMES = 2L;
|
||||||
|
|
||||||
private MetropolisPlugin _plugin;
|
private final MetropolisPlugin _plugin;
|
||||||
|
private BukkitTask _activeTask;
|
||||||
|
|
||||||
public MetropolisDebugGenerateTestHomesCommand(MetropolisPlugin plugin) {
|
public MetropolisDebugGenerateTestHomesCommand(MetropolisPlugin plugin) {
|
||||||
_plugin = plugin;
|
_plugin = plugin;
|
||||||
@@ -33,15 +40,102 @@ public class MetropolisDebugGenerateTestHomesCommand implements CommandExecutor
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
|
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
|
||||||
|
if (args.length != 1) {
|
||||||
|
sender.sendMessage("Usage: /metropolis-debug-generatetesthomes <count>");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int numHomes;
|
||||||
try {
|
try {
|
||||||
int numHomes = Integer.parseInt(args[0]);
|
numHomes = Integer.parseInt(args[0]);
|
||||||
for (int i = 1; i <= numHomes; i++) {
|
} catch (NumberFormatException ex) {
|
||||||
_plugin.generateHome(String.format("test%d", i));
|
sender.sendMessage("Usage: /metropolis-debug-generatetesthomes <count>");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numHomes < 1) {
|
||||||
|
sender.sendMessage("Usage: /metropolis-debug-generatetesthomes <count>");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_activeTask != null && !_activeTask.isCancelled()) {
|
||||||
|
sender.sendMessage("[Metropolis] Test-home generation is already in progress.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestHomeBatch batch = new TestHomeBatch(sender, numHomes);
|
||||||
|
_activeTask =
|
||||||
|
_plugin.getServer().getScheduler().runTaskTimer(_plugin, batch, 1L, TICKS_BETWEEN_HOMES);
|
||||||
|
sender.sendMessage(
|
||||||
|
"[Metropolis] Queued "
|
||||||
|
+ numHomes
|
||||||
|
+ " test homes; generating one every "
|
||||||
|
+ TICKS_BETWEEN_HOMES
|
||||||
|
+ " ticks.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class TestHomeBatch implements Runnable {
|
||||||
|
private final CommandSender sender;
|
||||||
|
private final int requestedHomes;
|
||||||
|
private int generatedHomes;
|
||||||
|
private int nextTestNumber = 1;
|
||||||
|
private boolean finished;
|
||||||
|
|
||||||
|
private TestHomeBatch(CommandSender sender, int requestedHomes) {
|
||||||
|
this.sender = sender;
|
||||||
|
this.requestedHomes = requestedHomes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (finished) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
int testNumber = nextTestNumber++;
|
||||||
} catch (NumberFormatException ex) {
|
String testName = "Test" + testNumber;
|
||||||
return false;
|
try {
|
||||||
|
OfflinePlayer testPlayer = _plugin.getServer().getOfflinePlayer(testName);
|
||||||
|
if (_plugin.getNumPlots(testPlayer.getUniqueId()) > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerHome home = _plugin.generateHome(testPlayer, 1);
|
||||||
|
if (home == null) {
|
||||||
|
sender.sendMessage(
|
||||||
|
"[Metropolis] Could not generate "
|
||||||
|
+ testName
|
||||||
|
+ "; completed "
|
||||||
|
+ generatedHomes
|
||||||
|
+ " of "
|
||||||
|
+ requestedHomes
|
||||||
|
+ " test homes.");
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
generatedHomes++;
|
||||||
|
if (generatedHomes == requestedHomes) {
|
||||||
|
sender.sendMessage("[Metropolis] Generated " + generatedHomes + " test homes.");
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
_plugin
|
||||||
|
.getLogger()
|
||||||
|
.log(Level.SEVERE, "Failed to generate test home " + testName, exception);
|
||||||
|
sender.sendMessage(
|
||||||
|
"[Metropolis] Test-home generation failed after " + generatedHomes + " homes.");
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finish() {
|
||||||
|
finished = true;
|
||||||
|
if (_activeTask != null) {
|
||||||
|
_activeTask.cancel();
|
||||||
|
_activeTask = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ package com.majinnaibu.bukkitplugins.metropolis.commands;
|
|||||||
|
|
||||||
import com.majinnaibu.bukkitplugins.metropolis.MetropolisPlugin;
|
import com.majinnaibu.bukkitplugins.metropolis.MetropolisPlugin;
|
||||||
import com.majinnaibu.bukkitplugins.metropolis.Plot;
|
import com.majinnaibu.bukkitplugins.metropolis.Plot;
|
||||||
|
import com.majinnaibu.bukkitplugins.metropolis.PlotReservationLookup;
|
||||||
|
|
||||||
import org.bukkit.command.Command;
|
import org.bukkit.command.Command;
|
||||||
import org.bukkit.command.CommandExecutor;
|
import org.bukkit.command.CommandExecutor;
|
||||||
@@ -34,23 +35,85 @@ public class MetropolisPlotGoCommand implements CommandExecutor {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
|
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
|
||||||
Player player = null;
|
if (args.length > 2) {
|
||||||
Plot plot = null;
|
sender.sendMessage(
|
||||||
|
"Usage: /metropolis-plot-go [playerName] or /metropolis-plot-go <reservationName> [playerName]");
|
||||||
if (sender instanceof Player) {
|
|
||||||
player = (Player) sender;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.length == 1 && player != null) {
|
|
||||||
plot = _plugin.getPlot(args[0]);
|
|
||||||
} else if (args.length >= 2) {
|
|
||||||
player = _plugin.getPlayer(args[1]);
|
|
||||||
plot = _plugin.getPlot(args[0]);
|
|
||||||
} else {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (plot == null || player == null) {
|
Player player;
|
||||||
|
String requestedReservation = null;
|
||||||
|
if (args.length == 0) {
|
||||||
|
if (!(sender instanceof Player senderPlayer)) {
|
||||||
|
sender.sendMessage("Console must specify an online player name.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
player = senderPlayer;
|
||||||
|
} else if (args.length == 1) {
|
||||||
|
Player namedPlayer = _plugin.getPlayer(args[0]);
|
||||||
|
if (namedPlayer != null) {
|
||||||
|
player = namedPlayer;
|
||||||
|
} else if (sender instanceof Player senderPlayer) {
|
||||||
|
player = senderPlayer;
|
||||||
|
requestedReservation = args[0];
|
||||||
|
} else {
|
||||||
|
sender.sendMessage("No online player named " + args[0] + ".");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
requestedReservation = args[0];
|
||||||
|
player = _plugin.getPlayer(args[1]);
|
||||||
|
if (player == null) {
|
||||||
|
sender.sendMessage("No online player named " + args[1] + ".");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean canManagePlots = sender.hasPermission("metropolis.plot.go");
|
||||||
|
if (player != sender && !canManagePlots) {
|
||||||
|
sender.sendMessage("You do not have permission to teleport another player.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var reservations = _plugin.getReservationsForPlayer(player.getUniqueId());
|
||||||
|
Plot plot;
|
||||||
|
if (requestedReservation == null) {
|
||||||
|
if (reservations.isEmpty()) {
|
||||||
|
sender.sendMessage(
|
||||||
|
player.getName() + " is not a member or owner of a Metropolis reservation.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (reservations.size() > 1) {
|
||||||
|
String names =
|
||||||
|
reservations.stream()
|
||||||
|
.map(Plot::getRegionName)
|
||||||
|
.reduce((first, second) -> first + ", " + second)
|
||||||
|
.orElse("");
|
||||||
|
sender.sendMessage(
|
||||||
|
player.getName()
|
||||||
|
+ " belongs to multiple reservations: "
|
||||||
|
+ names
|
||||||
|
+ ". Specify one with /metropolis-plot-go <reservationName> [playerName].");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
plot = reservations.get(0);
|
||||||
|
} else {
|
||||||
|
plot = PlotReservationLookup.findByName(reservations, requestedReservation);
|
||||||
|
if (plot == null && canManagePlots) {
|
||||||
|
plot = _plugin.getPlot(requestedReservation);
|
||||||
|
}
|
||||||
|
if (plot == null) {
|
||||||
|
sender.sendMessage(
|
||||||
|
player.getName()
|
||||||
|
+ " is not a member or owner of reservation "
|
||||||
|
+ requestedReservation
|
||||||
|
+ ".");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (player != sender && !canManagePlots) {
|
||||||
|
sender.sendMessage("You do not have permission to teleport another player.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +123,11 @@ public class MetropolisPlotGoCommand implements CommandExecutor {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String destination = "reservation " + plot.getRegionName();
|
||||||
|
sender.sendMessage("Teleported " + player.getName() + " to " + destination + ".");
|
||||||
|
if (player != sender) {
|
||||||
|
player.sendMessage("You were teleported to " + destination + ".");
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,16 +18,38 @@ plot:
|
|||||||
sign:
|
sign:
|
||||||
generate: true
|
generate: true
|
||||||
road:
|
road:
|
||||||
width: 4
|
avenues:
|
||||||
clearSpaceAbove: 66
|
width: 4
|
||||||
level: 62
|
clearSpaceAbove: 66
|
||||||
material: COBBLESTONE
|
level: 62
|
||||||
supports:
|
material: COBBLESTONE
|
||||||
generate: true
|
supports:
|
||||||
material: STONE
|
generate: true
|
||||||
|
material: STONE
|
||||||
|
stairs:
|
||||||
|
generate: false
|
||||||
|
material: COBBLESTONE_STAIRS
|
||||||
|
everyNBlocks: 4
|
||||||
|
width: 2
|
||||||
|
streets:
|
||||||
|
width: 4
|
||||||
|
clearSpaceAbove: 66
|
||||||
|
level: 62
|
||||||
|
material: COBBLESTONE
|
||||||
|
supports:
|
||||||
|
generate: true
|
||||||
|
material: STONE
|
||||||
|
stairs:
|
||||||
|
generate: false
|
||||||
|
material: COBBLESTONE_STAIRS
|
||||||
|
everyNBlocks: 4
|
||||||
|
width: 2
|
||||||
spawn:
|
spawn:
|
||||||
generate: true
|
generate: true
|
||||||
setAsWorldSpawn: true
|
setAsWorldSpawn: true
|
||||||
|
sizeX: 1
|
||||||
|
sizeY: 1
|
||||||
|
sizeZ: 1
|
||||||
material: GRASS_BLOCK
|
material: GRASS_BLOCK
|
||||||
wall:
|
wall:
|
||||||
generate: true
|
generate: true
|
||||||
|
|||||||
66
src/main/resources/examples/multi-level.yml
Normal file
66
src/main/resources/examples/multi-level.yml
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
plot:
|
||||||
|
sizeX: 32
|
||||||
|
sizeY: 10
|
||||||
|
sizeZ: 32
|
||||||
|
offsetX: 0
|
||||||
|
offsetY: 0
|
||||||
|
offsetZ: 0
|
||||||
|
multiplier: 1
|
||||||
|
maxLevels: 10
|
||||||
|
maxPerPlayer: 1
|
||||||
|
initial: 1
|
||||||
|
floor:
|
||||||
|
generate: true
|
||||||
|
clearSpaceAbove: 0
|
||||||
|
material: GRASS_BLOCK
|
||||||
|
supports:
|
||||||
|
generate: true
|
||||||
|
material: STONE
|
||||||
|
sign:
|
||||||
|
generate: true
|
||||||
|
road:
|
||||||
|
avenues:
|
||||||
|
width: 4
|
||||||
|
clearSpaceAbove: 9
|
||||||
|
level: 62
|
||||||
|
material: COBBLESTONE
|
||||||
|
supports:
|
||||||
|
generate: true
|
||||||
|
material: STONE
|
||||||
|
stairs:
|
||||||
|
generate: true
|
||||||
|
material: COBBLESTONE_STAIRS
|
||||||
|
everyNBlocks: 4
|
||||||
|
width: 2
|
||||||
|
streets:
|
||||||
|
width: 4
|
||||||
|
clearSpaceAbove: 9
|
||||||
|
level: 62
|
||||||
|
material: COBBLESTONE
|
||||||
|
supports:
|
||||||
|
generate: true
|
||||||
|
material: STONE
|
||||||
|
stairs:
|
||||||
|
generate: false
|
||||||
|
material: COBBLESTONE_STAIRS
|
||||||
|
everyNBlocks: 4
|
||||||
|
width: 2
|
||||||
|
spawn:
|
||||||
|
generate: true
|
||||||
|
setAsWorldSpawn: true
|
||||||
|
sizeX: 1
|
||||||
|
sizeY: 1
|
||||||
|
sizeZ: 1
|
||||||
|
material: GRASS_BLOCK
|
||||||
|
wall:
|
||||||
|
generate: true
|
||||||
|
material: BEDROCK
|
||||||
|
height: 10
|
||||||
|
worldname: world
|
||||||
|
userOverrides:
|
||||||
|
- username: majinnaibu
|
||||||
|
plotMultiplier: 2
|
||||||
|
maxPlots: 2
|
||||||
|
- username: hoggrim
|
||||||
|
plotMultiplier: 2
|
||||||
|
maxPlots: 2
|
||||||
@@ -40,9 +40,8 @@ commands:
|
|||||||
permission: metropolis.plot.reserve
|
permission: metropolis.plot.reserve
|
||||||
usage: /metropolis-plot-reserve <name> <minX> <minY> <minZ> <maxX> <maxY> <maxZ>
|
usage: /metropolis-plot-reserve <name> <minX> <minY> <minZ> <maxX> <maxY> <maxZ>
|
||||||
metropolis-plot-go:
|
metropolis-plot-go:
|
||||||
description: This command teleports the user or another player to a plot.
|
description: Teleports the sender or an authorized target to a reservation.
|
||||||
permission: metropolis.plot.go
|
usage: /metropolis-plot-go [playerName] or /metropolis-plot-go <reservationName> [playerName]
|
||||||
usage: /metropolis-plot-go <plotName> [playerName]
|
|
||||||
metropolis-debug-generatetesthomes:
|
metropolis-debug-generatetesthomes:
|
||||||
description: This command is ignored on non-debug builds.
|
description: This command is ignored on non-debug builds.
|
||||||
permission: metropolis.debug
|
permission: metropolis.debug
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Metropolis.
|
||||||
|
|
||||||
|
Metropolis is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Metropolis is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.majinnaibu.bukkitplugins.metropolis;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.majinnaibu.bukkitplugins.metropolis.AvenueStairwayLayout.Flight;
|
||||||
|
import com.majinnaibu.bukkitplugins.metropolis.AvenueStairwayLayout.Step;
|
||||||
|
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.block.BlockFace;
|
||||||
|
import org.bukkit.block.data.Bisected;
|
||||||
|
import org.bukkit.block.data.type.Stairs;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class AvenueStairwayBuilderTest {
|
||||||
|
@Test
|
||||||
|
void buildsStreetStairsFacingEastWithWestFacingInvertedBacking() {
|
||||||
|
Cuboid plot = new Cuboid(2, 62, 2, 33, 71, 33);
|
||||||
|
Flight flight =
|
||||||
|
AvenueStairwayLayout.forUpperStreetSegments(plot, 32, 36, 0, 1, 4, 2, 62, 14, 3).getFirst();
|
||||||
|
World world = mock(World.class);
|
||||||
|
Map<Step, Block> blocks = new HashMap<>();
|
||||||
|
Map<Step, Stairs> blockData = new HashMap<>();
|
||||||
|
when(world.getBlockAt(anyInt(), anyInt(), anyInt()))
|
||||||
|
.thenAnswer(
|
||||||
|
invocation -> {
|
||||||
|
Step position =
|
||||||
|
new Step(
|
||||||
|
invocation.getArgument(0),
|
||||||
|
invocation.getArgument(1),
|
||||||
|
invocation.getArgument(2));
|
||||||
|
return blocks.computeIfAbsent(
|
||||||
|
position,
|
||||||
|
ignored -> {
|
||||||
|
Block block = mock(Block.class);
|
||||||
|
Stairs stairs = mock(Stairs.class);
|
||||||
|
when(block.getBlockData()).thenReturn(stairs);
|
||||||
|
blockData.put(position, stairs);
|
||||||
|
return block;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
AvenueStairwayBuilder.build(world, flight, Material.COBBLESTONE_STAIRS);
|
||||||
|
|
||||||
|
verify(blockData.get(flight.stairBlocks().getFirst())).setFacing(BlockFace.EAST);
|
||||||
|
verify(blockData.get(flight.invertedBackingBlocks().getFirst())).setFacing(BlockFace.WEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void buildsAscendingAndInvertedStairsAndClearsOnlyThePlannedOpening() {
|
||||||
|
Cuboid plot = new Cuboid(2, 62, 2, 33, 71, 33);
|
||||||
|
Flight flight = AvenueStairwayLayout.forUpperRoad(plot, 4, 2, 62, 14, 3);
|
||||||
|
World world = mock(World.class);
|
||||||
|
Map<Step, Block> blocks = new HashMap<>();
|
||||||
|
Map<Step, Stairs> blockData = new HashMap<>();
|
||||||
|
when(world.getBlockAt(anyInt(), anyInt(), anyInt()))
|
||||||
|
.thenAnswer(
|
||||||
|
invocation -> {
|
||||||
|
Step position =
|
||||||
|
new Step(
|
||||||
|
invocation.getArgument(0),
|
||||||
|
invocation.getArgument(1),
|
||||||
|
invocation.getArgument(2));
|
||||||
|
return blocks.computeIfAbsent(
|
||||||
|
position,
|
||||||
|
ignored -> {
|
||||||
|
Block block = mock(Block.class);
|
||||||
|
Stairs stairs = mock(Stairs.class);
|
||||||
|
when(block.getBlockData()).thenReturn(stairs);
|
||||||
|
blockData.put(position, stairs);
|
||||||
|
return block;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
AvenueStairwayBuilder.build(world, flight, Material.COBBLESTONE_STAIRS);
|
||||||
|
|
||||||
|
for (Step stair : flight.stairBlocks()) {
|
||||||
|
verify(blocks.get(stair)).setType(Material.COBBLESTONE_STAIRS, false);
|
||||||
|
}
|
||||||
|
for (Step backing : flight.invertedBackingBlocks()) {
|
||||||
|
verify(blocks.get(backing)).setType(Material.COBBLESTONE_STAIRS, false);
|
||||||
|
}
|
||||||
|
for (Step opening : flight.roadOpenings()) {
|
||||||
|
verify(blocks.get(opening)).setType(Material.AIR, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Step firstStair = flight.stairBlocks().getFirst();
|
||||||
|
Stairs firstStairData = blockData.get(firstStair);
|
||||||
|
assertNotNull(firstStairData);
|
||||||
|
verify(firstStairData).setFacing(BlockFace.SOUTH);
|
||||||
|
verify(firstStairData).setHalf(Bisected.Half.BOTTOM);
|
||||||
|
|
||||||
|
Step firstInvertedBacking = flight.invertedBackingBlocks().getFirst();
|
||||||
|
Stairs invertedBackingData = blockData.get(firstInvertedBacking);
|
||||||
|
assertNotNull(invertedBackingData);
|
||||||
|
verify(invertedBackingData).setFacing(BlockFace.NORTH);
|
||||||
|
verify(invertedBackingData).setHalf(Bisected.Half.TOP);
|
||||||
|
|
||||||
|
verify(world, never()).getBlockAt(anyInt(), eq(62), anyInt());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Metropolis.
|
||||||
|
|
||||||
|
Metropolis is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Metropolis is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.majinnaibu.bukkitplugins.metropolis;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class AvenueStairwayLayoutTest {
|
||||||
|
@Test
|
||||||
|
void centersStairsAcrossTheAvenueAndAlongThePlotLength() {
|
||||||
|
Cuboid plot = new Cuboid(2, 62, 2, 33, 71, 33);
|
||||||
|
|
||||||
|
List<AvenueStairwayLayout.Step> steps =
|
||||||
|
AvenueStairwayLayout.centeredEastAvenue(plot, 4, 2, 62, 14);
|
||||||
|
|
||||||
|
assertEquals(28, steps.size());
|
||||||
|
assertEquals(new AvenueStairwayLayout.Step(35, 63, 12), steps.get(0));
|
||||||
|
assertEquals(new AvenueStairwayLayout.Step(36, 63, 12), steps.get(1));
|
||||||
|
assertEquals(new AvenueStairwayLayout.Step(35, 64, 13), steps.get(2));
|
||||||
|
assertEquals(new AvenueStairwayLayout.Step(36, 76, 25), steps.get(27));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void opensOnlyTheThreeUpperRoadBlocksBeforeTheTopStair() {
|
||||||
|
Cuboid plot = new Cuboid(2, 62, 2, 33, 71, 33);
|
||||||
|
|
||||||
|
List<AvenueStairwayLayout.Step> openings =
|
||||||
|
AvenueStairwayLayout.upperRoadOpening(plot, 4, 2, 62, 14, 3);
|
||||||
|
|
||||||
|
assertEquals(6, openings.size());
|
||||||
|
assertEquals(new AvenueStairwayLayout.Step(35, 76, 22), openings.get(0));
|
||||||
|
assertEquals(new AvenueStairwayLayout.Step(35, 76, 23), openings.get(1));
|
||||||
|
assertEquals(new AvenueStairwayLayout.Step(35, 76, 24), openings.get(2));
|
||||||
|
assertEquals(new AvenueStairwayLayout.Step(36, 76, 24), openings.get(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void upperRoadFlightPreservesTheLowerRoadAndPlansInvertedBacking() {
|
||||||
|
Cuboid plot = new Cuboid(2, 62, 2, 33, 71, 33);
|
||||||
|
|
||||||
|
AvenueStairwayLayout.Flight flight = AvenueStairwayLayout.forUpperRoad(plot, 4, 2, 62, 14, 3);
|
||||||
|
|
||||||
|
assertEquals(28, flight.stairBlocks().size());
|
||||||
|
assertEquals(new AvenueStairwayLayout.Step(35, 63, 12), flight.stairBlocks().getFirst());
|
||||||
|
assertEquals(new AvenueStairwayLayout.Step(36, 76, 25), flight.stairBlocks().getLast());
|
||||||
|
assertEquals(26, flight.invertedBackingBlocks().size());
|
||||||
|
assertEquals(
|
||||||
|
new AvenueStairwayLayout.Step(35, 63, 13), flight.invertedBackingBlocks().getFirst());
|
||||||
|
assertTrue(flight.stairBlocks().stream().allMatch(step -> step.y() > 62));
|
||||||
|
assertTrue(flight.invertedBackingBlocks().stream().allMatch(step -> step.y() > 62));
|
||||||
|
assertEquals(6, flight.roadOpenings().size());
|
||||||
|
assertTrue(flight.roadOpenings().stream().allMatch(step -> step.y() == 76));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createsOneCenteredStairFlightPerLogicalPlotOnBothAvenueBorders() {
|
||||||
|
Cuboid twoByTwoPlot = new Cuboid(2, 62, 2, 69, 71, 69);
|
||||||
|
|
||||||
|
List<AvenueStairwayLayout.Flight> flights =
|
||||||
|
AvenueStairwayLayout.forUpperRoadSegments(twoByTwoPlot, 32, 36, 0, 1, 4, 2, 62, 14, 3);
|
||||||
|
|
||||||
|
assertEquals(4, flights.size());
|
||||||
|
assertEquals(
|
||||||
|
new AvenueStairwayLayout.Step(71, 63, 12), flights.get(0).stairBlocks().getFirst());
|
||||||
|
assertEquals(
|
||||||
|
new AvenueStairwayLayout.Step(-1, 63, 12), flights.get(1).stairBlocks().getFirst());
|
||||||
|
assertEquals(
|
||||||
|
new AvenueStairwayLayout.Step(71, 63, 48), flights.get(2).stairBlocks().getFirst());
|
||||||
|
assertEquals(
|
||||||
|
new AvenueStairwayLayout.Step(-1, 63, 48), flights.get(3).stairBlocks().getFirst());
|
||||||
|
assertTrue(flights.stream().allMatch(flight -> flight.stairBlocks().getLast().y() == 76));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void appliesStairCadenceToEachLogicalPlotSegment() {
|
||||||
|
Cuboid twoByTwoPlot = new Cuboid(2, 62, 2, 69, 71, 69);
|
||||||
|
|
||||||
|
List<AvenueStairwayLayout.Flight> flights =
|
||||||
|
AvenueStairwayLayout.forUpperRoadSegments(twoByTwoPlot, 32, 36, 0, 2, 4, 2, 62, 14, 3);
|
||||||
|
|
||||||
|
assertEquals(2, flights.size());
|
||||||
|
assertEquals(
|
||||||
|
new AvenueStairwayLayout.Step(71, 63, 48), flights.getFirst().stairBlocks().getFirst());
|
||||||
|
assertEquals(
|
||||||
|
new AvenueStairwayLayout.Step(-1, 63, 48), flights.getLast().stairBlocks().getFirst());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createsStreetFlightsAlongXOnBothNorthAndSouthBorders() {
|
||||||
|
Cuboid twoByTwoPlot = new Cuboid(2, 62, 2, 69, 71, 69);
|
||||||
|
|
||||||
|
List<AvenueStairwayLayout.Flight> flights =
|
||||||
|
AvenueStairwayLayout.forUpperStreetSegments(twoByTwoPlot, 32, 36, 0, 1, 4, 2, 62, 14, 3);
|
||||||
|
|
||||||
|
assertEquals(4, flights.size());
|
||||||
|
assertEquals(AvenueStairwayLayout.Side.NORTH, flights.getFirst().side());
|
||||||
|
assertEquals(
|
||||||
|
new AvenueStairwayLayout.Step(12, 63, -1), flights.getFirst().stairBlocks().getFirst());
|
||||||
|
assertEquals(AvenueStairwayLayout.Side.SOUTH, flights.get(1).side());
|
||||||
|
assertEquals(
|
||||||
|
new AvenueStairwayLayout.Step(12, 63, 71), flights.get(1).stairBlocks().getFirst());
|
||||||
|
assertEquals(
|
||||||
|
new AvenueStairwayLayout.Step(48, 63, -1), flights.get(2).stairBlocks().getFirst());
|
||||||
|
assertTrue(flights.stream().allMatch(flight -> flight.stairBlocks().getLast().y() == 76));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsAStairRunThatCannotFitWithinThePlotLength() {
|
||||||
|
Cuboid plot = new Cuboid(2, 62, 2, 33, 71, 13);
|
||||||
|
|
||||||
|
assertTrue(AvenueStairwayLayout.centeredEastAvenue(plot, 4, 2, 62, 14).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsStairsThatAreNotNarrowerThanTheAvenue() {
|
||||||
|
Cuboid plot = new Cuboid(2, 62, 2, 33, 71, 33);
|
||||||
|
|
||||||
|
assertTrue(AvenueStairwayLayout.centeredEastAvenue(plot, 4, 4, 62, 14).isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Metropolis.
|
||||||
|
|
||||||
|
Metropolis is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Metropolis is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.majinnaibu.bukkitplugins.metropolis;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class CityRegionBoundsTest {
|
||||||
|
@Test
|
||||||
|
void expandsExistingCityToIncludeElevatedHomesAndSpawnWithoutShrinking() {
|
||||||
|
Cuboid existingCity = new Cuboid(-20, 62, -20, 20, 71, 20);
|
||||||
|
Cuboid requestedCity = new Cuboid(-40, 62, -40, 40, 71, 40);
|
||||||
|
Cuboid elevatedHome = new Cuboid(10, 104, 10, 41, 113, 41);
|
||||||
|
Cuboid spawn = new Cuboid(2, 62, 2, 69, 81, 69);
|
||||||
|
|
||||||
|
Cuboid expanded =
|
||||||
|
CityRegionBounds.expandToContain(existingCity, requestedCity, List.of(elevatedHome, spawn));
|
||||||
|
|
||||||
|
assertEquals(-40, expanded.minX);
|
||||||
|
assertEquals(62, expanded.minY);
|
||||||
|
assertEquals(-40, expanded.minZ);
|
||||||
|
assertEquals(69, expanded.maxX);
|
||||||
|
assertEquals(113, expanded.maxY);
|
||||||
|
assertEquals(69, expanded.maxZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Metropolis.
|
||||||
|
|
||||||
|
Metropolis is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Metropolis is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.majinnaibu.bukkitplugins.metropolis;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import com.sk89q.worldedit.math.BlockVector3;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class PlotGridLayoutTest {
|
||||||
|
@Test
|
||||||
|
void zeroOffsetsPreserveTheExistingPlotGrid() {
|
||||||
|
PlotGridLayout grid = new PlotGridLayout(32, 10, 32, 4, 4, 0, 0, 0, 62);
|
||||||
|
|
||||||
|
assertEquals(BlockVector3.at(0, 62, 0), grid.gridMin(0, 0, 0));
|
||||||
|
assertEquals(BlockVector3.at(35, 71, 35), grid.gridMax(0, 0, 1, 1, 0));
|
||||||
|
assertEquals(BlockVector3.at(2, 62, 2), grid.plotMin(0, 0, 0));
|
||||||
|
assertEquals(BlockVector3.at(33, 71, 33), grid.plotMax(0, 0, 1, 1, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void offsetsShiftGridAndPlotsOnAllAxesIncludingHigherLevels() {
|
||||||
|
PlotGridLayout grid = new PlotGridLayout(32, 10, 32, 2, 6, -7, 3, 11, 62);
|
||||||
|
|
||||||
|
assertEquals(BlockVector3.at(-45, 97, -23), grid.gridMin(-1, -1, 2));
|
||||||
|
assertEquals(BlockVector3.at(30, 106, 44), grid.gridMax(-1, -1, 2, 2, 2));
|
||||||
|
assertEquals(BlockVector3.at(-42, 97, -22), grid.plotMin(-1, -1, 2));
|
||||||
|
assertEquals(BlockVector3.at(27, 106, 43), grid.plotMax(-1, -1, 2, 2, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mapsNegativePlotBoundsBackToTheirLogicalIndices() {
|
||||||
|
PlotGridLayout grid = new PlotGridLayout(32, 10, 32, 2, 6, -7, 3, 11, 62);
|
||||||
|
Cuboid plot = new Cuboid(grid.plotMin(-1, -1, 0), grid.plotMax(-1, -1, 2, 2, 0));
|
||||||
|
|
||||||
|
assertEquals(-1, grid.plotXIndexFromMin(plot));
|
||||||
|
assertEquals(-1, grid.plotZIndexFromMin(plot));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Metropolis.
|
||||||
|
|
||||||
|
Metropolis is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Metropolis is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.majinnaibu.bukkitplugins.metropolis;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class PlotLevelSupportTest {
|
||||||
|
@Test
|
||||||
|
void rejectsUpperPlotWhenAnyLogicalCellBelowIsMissing() {
|
||||||
|
Set<String> generatedCells = Set.of("0:0:0", "0:1:0", "1:0:0");
|
||||||
|
|
||||||
|
assertFalse(
|
||||||
|
PlotLevelSupport.hasCompleteSupport(
|
||||||
|
0, 0, 2, 1, (row, col, level) -> generatedCells.contains(key(row, col, level))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void requiresEveryLogicalCellAtEveryLowerLevel() {
|
||||||
|
Set<String> generatedCells =
|
||||||
|
Set.of("0:0:0", "0:1:0", "1:0:0", "1:1:0", "0:0:1", "0:1:1", "1:0:1", "1:1:1");
|
||||||
|
|
||||||
|
assertTrue(
|
||||||
|
PlotLevelSupport.hasCompleteSupport(
|
||||||
|
0, 0, 2, 2, (row, col, level) -> generatedCells.contains(key(row, col, level))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void baseLevelDoesNotRequireLowerSupport() {
|
||||||
|
assertTrue(PlotLevelSupport.hasCompleteSupport(3, 4, 2, 0, (row, col, level) -> false));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String key(int row, int col, int level) {
|
||||||
|
return row + ":" + col + ":" + level;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Metropolis.
|
||||||
|
|
||||||
|
Metropolis is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Metropolis is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.majinnaibu.bukkitplugins.metropolis;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import com.sk89q.worldedit.math.BlockVector3;
|
||||||
|
import com.sk89q.worldguard.protection.flags.Flags;
|
||||||
|
import com.sk89q.worldguard.protection.flags.StateFlag;
|
||||||
|
import com.sk89q.worldguard.protection.regions.ProtectedCuboidRegion;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class PlotReservationLookupTest {
|
||||||
|
private static final UUID PLAYER_ID = UUID.fromString("123e4567-e89b-12d3-a456-426614174000");
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findsReservationsWherePlayerIsOwnerOrMemberButExcludesHomeCityAndSpawn() {
|
||||||
|
ProtectedCuboidRegion memberReservation = region("r_member");
|
||||||
|
memberReservation.getMembers().addPlayer(PLAYER_ID);
|
||||||
|
ProtectedCuboidRegion ownerReservation = region("private-yard");
|
||||||
|
ownerReservation.getOwners().addPlayer(PLAYER_ID);
|
||||||
|
ProtectedCuboidRegion home = region("h_1_" + PLAYER_ID);
|
||||||
|
home.getMembers().addPlayer(PLAYER_ID);
|
||||||
|
ProtectedCuboidRegion city = region("City");
|
||||||
|
city.getMembers().addPlayer(PLAYER_ID);
|
||||||
|
ProtectedCuboidRegion spawn = region("Spawn");
|
||||||
|
spawn.getMembers().addPlayer(PLAYER_ID);
|
||||||
|
ProtectedCuboidRegion unrelated = region("other-region", false);
|
||||||
|
unrelated.getMembers().addPlayer(PLAYER_ID);
|
||||||
|
|
||||||
|
List<Plot> reservations =
|
||||||
|
PlotReservationLookup.forPlayer(
|
||||||
|
List.of(memberReservation, ownerReservation, home, city, spawn, unrelated), PLAYER_ID);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
List.of("private-yard", "r_member"),
|
||||||
|
reservations.stream().map(Plot::getRegionName).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findsNamedReservationWithOrWithoutPrefix() {
|
||||||
|
ProtectedCuboidRegion reservation = region("r_camp");
|
||||||
|
reservation.getMembers().addPlayer(PLAYER_ID);
|
||||||
|
List<Plot> reservations = PlotReservationLookup.forPlayer(List.of(reservation), PLAYER_ID);
|
||||||
|
|
||||||
|
assertEquals("r_camp", PlotReservationLookup.findByName(reservations, "camp").getRegionName());
|
||||||
|
assertEquals(
|
||||||
|
"r_camp", PlotReservationLookup.findByName(reservations, "r_camp").getRegionName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProtectedCuboidRegion region(String id) {
|
||||||
|
return region(id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProtectedCuboidRegion region(String id, boolean reservedFlags) {
|
||||||
|
ProtectedCuboidRegion region =
|
||||||
|
new ProtectedCuboidRegion(id, BlockVector3.ZERO, BlockVector3.at(5, 5, 5));
|
||||||
|
if (reservedFlags) {
|
||||||
|
region.setFlag(Flags.LAVA_FLOW, StateFlag.State.DENY);
|
||||||
|
region.setFlag(Flags.SNOW_FALL, StateFlag.State.DENY);
|
||||||
|
}
|
||||||
|
return region;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Metropolis.
|
||||||
|
|
||||||
|
Metropolis is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Metropolis is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.majinnaibu.bukkitplugins.metropolis;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class PlotWorldBorderBoundsTest {
|
||||||
|
@Test
|
||||||
|
void searchBoundFollowsTheWorldBorderInsteadOfAnArbitraryRingCount() {
|
||||||
|
int maximumRing = PlotWorldBorderBounds.maximumSearchRing(60_000_000, 0, 0, 0, 0, 36, 36, 1);
|
||||||
|
|
||||||
|
assertTrue(maximumRing > 256);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void accountsForAnOffsetBorderCenterAndMultiplier() {
|
||||||
|
int maximumRing = PlotWorldBorderBounds.maximumSearchRing(512, 100, -100, 64, -32, 36, 34, 2);
|
||||||
|
|
||||||
|
assertTrue(maximumRing > 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void requiresTheCompleteGridFootprintInsideTheBorder() {
|
||||||
|
assertTrue(PlotWorldBorderBounds.contains(new Cuboid(-9, 0, -9, 9, 10, 9), 20, 0, 0));
|
||||||
|
assertFalse(PlotWorldBorderBounds.contains(new Cuboid(-11, 0, -9, 9, 10, 9), 20, 0, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Metropolis.
|
||||||
|
|
||||||
|
Metropolis is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Metropolis is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.majinnaibu.bukkitplugins.metropolis;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.World;
|
||||||
|
import org.bukkit.block.Block;
|
||||||
|
import org.bukkit.block.data.type.Stairs;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class RoadBlockWriterTest {
|
||||||
|
@Test
|
||||||
|
void doesNotRewriteAnExistingRoadBlockOrItsClearance() {
|
||||||
|
World world = mock(World.class);
|
||||||
|
Block roadBlock = mock(Block.class);
|
||||||
|
when(world.getBlockAt(4, 62, 8)).thenReturn(roadBlock);
|
||||||
|
when(roadBlock.getType()).thenReturn(Material.COBBLESTONE);
|
||||||
|
|
||||||
|
RoadBlockWriter.build(world, 4, 62, 8, Material.COBBLESTONE, Material.STONE, true, 4);
|
||||||
|
|
||||||
|
verify(roadBlock, never()).setType(any(Material.class));
|
||||||
|
verify(world, never()).getBlockAt(4, 63, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fillsMissingRoadBlocksAndPreservesExistingStairTreads() {
|
||||||
|
World world = mock(World.class);
|
||||||
|
Block missingRoadBlock = mock(Block.class);
|
||||||
|
Block stairBlock = mock(Block.class);
|
||||||
|
when(world.getBlockAt(4, 62, 8)).thenReturn(missingRoadBlock);
|
||||||
|
when(world.getBlockAt(5, 76, 12)).thenReturn(stairBlock);
|
||||||
|
when(missingRoadBlock.getType()).thenReturn(Material.AIR);
|
||||||
|
when(stairBlock.getType()).thenReturn(Material.COBBLESTONE_STAIRS);
|
||||||
|
when(stairBlock.getBlockData()).thenReturn(mock(Stairs.class));
|
||||||
|
|
||||||
|
RoadBlockWriter.build(world, 4, 62, 8, Material.COBBLESTONE, Material.STONE, false, 0);
|
||||||
|
RoadBlockWriter.build(world, 5, 76, 12, Material.COBBLESTONE, Material.STONE, false, 4);
|
||||||
|
|
||||||
|
verify(missingRoadBlock).setType(Material.COBBLESTONE);
|
||||||
|
verify(stairBlock, never()).setType(any(Material.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Metropolis.
|
||||||
|
|
||||||
|
Metropolis is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Metropolis is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.majinnaibu.bukkitplugins.metropolis;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class RoadLayoutTest {
|
||||||
|
private static final Cuboid PLOT = new Cuboid(2, 62, 2, 33, 71, 33);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void buildsFullRoadWidthAndNonOverlappingCornersAtOnce() {
|
||||||
|
List<RoadLayout.BlockPosition> road = RoadLayout.surroundingRoad(PLOT, 4, 4);
|
||||||
|
HashSet<RoadLayout.BlockPosition> uniqueBlocks = new HashSet<>(road);
|
||||||
|
|
||||||
|
assertEquals(576, road.size());
|
||||||
|
assertEquals(road.size(), uniqueBlocks.size());
|
||||||
|
assertTrue(road.contains(new RoadLayout.BlockPosition(34, 2, RoadLayout.RoadType.AVENUE)));
|
||||||
|
assertTrue(road.contains(new RoadLayout.BlockPosition(2, 34, RoadLayout.RoadType.STREET)));
|
||||||
|
assertTrue(
|
||||||
|
road.contains(new RoadLayout.BlockPosition(34, 34, RoadLayout.RoadType.INTERSECTION)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsNoRoadForZeroWidth() {
|
||||||
|
assertTrue(RoadLayout.surroundingRoad(PLOT, 0, 0).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void usesIndependentWidthsForStreetAndAvenueBands() {
|
||||||
|
List<RoadLayout.BlockPosition> road = RoadLayout.surroundingRoad(PLOT, 2, 4);
|
||||||
|
|
||||||
|
assertEquals(416, road.size());
|
||||||
|
assertTrue(road.contains(new RoadLayout.BlockPosition(2, 0, RoadLayout.RoadType.STREET)));
|
||||||
|
assertTrue(road.contains(new RoadLayout.BlockPosition(-2, 2, RoadLayout.RoadType.AVENUE)));
|
||||||
|
assertTrue(
|
||||||
|
road.contains(new RoadLayout.BlockPosition(-2, 0, RoadLayout.RoadType.INTERSECTION)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Metropolis.
|
||||||
|
|
||||||
|
Metropolis is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Metropolis is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.majinnaibu.bukkitplugins.metropolis;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class SpawnLayoutTest {
|
||||||
|
@Test
|
||||||
|
void defaultMultipliersPreserveTheOnePlotSpawnBounds() {
|
||||||
|
Cuboid spawn = SpawnLayout.bounds(1, 1, 1, 32, 10, 32, 4, 4, 62, 0, 0, 0);
|
||||||
|
|
||||||
|
assertEqualsBounds(spawn, 2, 62, 2, 33, 71, 33);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void appliesEachMultiplierToItsOwnAlignedGridDimension() {
|
||||||
|
Cuboid spawn = SpawnLayout.bounds(2, 3, 4, 32, 10, 32, 4, 4, 62, 0, 0, 0);
|
||||||
|
|
||||||
|
assertEqualsBounds(spawn, 2, 62, 2, 69, 91, 141);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validatesTheEntireSpawnHeightAgainstWorldBounds() {
|
||||||
|
Cuboid spawn = SpawnLayout.bounds(1, 3, 1, 32, 10, 32, 4, 4, 62, 0, 0, 0);
|
||||||
|
|
||||||
|
assertTrue(SpawnLayout.withinBuildHeight(spawn, -64, 320));
|
||||||
|
assertFalse(SpawnLayout.withinBuildHeight(spawn, -64, 91));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void alignsSpawnAxesToTheirIndependentRoadWidths() {
|
||||||
|
Cuboid spawn = SpawnLayout.bounds(2, 1, 3, 32, 10, 32, 6, 2, 62, 0, 0, 0);
|
||||||
|
|
||||||
|
assertEqualsBounds(spawn, 3, 62, 1, 72, 71, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void alignsSpawnBoundsWithAllGridOffsets() {
|
||||||
|
Cuboid spawn = SpawnLayout.bounds(2, 2, 3, 32, 10, 32, 6, 2, 62, -7, 3, 11);
|
||||||
|
|
||||||
|
assertEqualsBounds(spawn, -4, 65, 12, 65, 84, 111);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsNonPositiveSpawnMultipliers() {
|
||||||
|
assertThrows(
|
||||||
|
IllegalArgumentException.class,
|
||||||
|
() -> SpawnLayout.bounds(0, 1, 1, 32, 10, 32, 4, 4, 62, 0, 0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertEqualsBounds(
|
||||||
|
Cuboid cuboid, int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
|
||||||
|
org.junit.jupiter.api.Assertions.assertEquals(minX, cuboid.minX);
|
||||||
|
org.junit.jupiter.api.Assertions.assertEquals(minY, cuboid.minY);
|
||||||
|
org.junit.jupiter.api.Assertions.assertEquals(minZ, cuboid.minZ);
|
||||||
|
org.junit.jupiter.api.Assertions.assertEquals(maxX, cuboid.maxX);
|
||||||
|
org.junit.jupiter.api.Assertions.assertEquals(maxY, cuboid.maxY);
|
||||||
|
org.junit.jupiter.api.Assertions.assertEquals(maxZ, cuboid.maxZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Metropolis.
|
||||||
|
|
||||||
|
Metropolis is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Metropolis is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.majinnaibu.bukkitplugins.metropolis;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.sk89q.worldedit.math.BlockVector3;
|
||||||
|
import com.sk89q.worldguard.protection.regions.ProtectedRegion;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class WorldGuardRegionOccupancyTest {
|
||||||
|
@Test
|
||||||
|
void treatsArbitrarySavedRegionBoundsAsOccupiedButIgnoresCityAndGlobalRegions() {
|
||||||
|
ProtectedRegion market =
|
||||||
|
region("market", BlockVector3.at(40, 62, 40), BlockVector3.at(70, 90, 70));
|
||||||
|
ProtectedRegion city =
|
||||||
|
region("City", BlockVector3.at(-100, 0, -100), BlockVector3.at(100, 320, 100));
|
||||||
|
ProtectedRegion global =
|
||||||
|
region(
|
||||||
|
"__global__",
|
||||||
|
BlockVector3.at(-30_000_000, -64, -30_000_000),
|
||||||
|
BlockVector3.at(30_000_000, 320, 30_000_000));
|
||||||
|
|
||||||
|
List<Cuboid> occupied = WorldGuardRegionOccupancy.boundsToAvoid(List.of(market, city, global));
|
||||||
|
|
||||||
|
assertEquals(1, occupied.size());
|
||||||
|
assertTrue(WorldGuardRegionOccupancy.overlapsAny(new Cuboid(60, 70, 60, 80, 75, 80), occupied));
|
||||||
|
assertFalse(WorldGuardRegionOccupancy.overlapsAny(new Cuboid(0, 62, 0, 31, 71, 31), occupied));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProtectedRegion region(String id, BlockVector3 minimum, BlockVector3 maximum) {
|
||||||
|
ProtectedRegion region = mock(ProtectedRegion.class);
|
||||||
|
when(region.getId()).thenReturn(id);
|
||||||
|
when(region.getMinimumPoint()).thenReturn(minimum);
|
||||||
|
when(region.getMaximumPoint()).thenReturn(maximum);
|
||||||
|
return region;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Metropolis.
|
||||||
|
|
||||||
|
Metropolis is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Metropolis is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.majinnaibu.bukkitplugins.metropolis.commands;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import com.majinnaibu.bukkitplugins.metropolis.MetropolisPlugin;
|
||||||
|
import com.majinnaibu.bukkitplugins.metropolis.PlayerHome;
|
||||||
|
|
||||||
|
import org.bukkit.OfflinePlayer;
|
||||||
|
import org.bukkit.Server;
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.scheduler.BukkitScheduler;
|
||||||
|
import org.bukkit.scheduler.BukkitTask;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class MetropolisDebugGenerateTestHomesCommandTest {
|
||||||
|
@Test
|
||||||
|
void repeatedRunsContinueWithTheNextUnusedTestName() {
|
||||||
|
MetropolisPlugin plugin = mock(MetropolisPlugin.class);
|
||||||
|
Server server = mock(Server.class);
|
||||||
|
BukkitScheduler scheduler = mock(BukkitScheduler.class);
|
||||||
|
CommandSender sender = mock(CommandSender.class);
|
||||||
|
Set<UUID> existingHomeOwners = new HashSet<>();
|
||||||
|
List<String> generatedNames = new ArrayList<>();
|
||||||
|
List<Runnable> scheduledBatches = new ArrayList<>();
|
||||||
|
|
||||||
|
when(plugin.getServer()).thenReturn(server);
|
||||||
|
when(server.getScheduler()).thenReturn(scheduler);
|
||||||
|
when(scheduler.runTaskTimer(eq(plugin), any(Runnable.class), eq(1L), eq(2L)))
|
||||||
|
.thenAnswer(
|
||||||
|
invocation -> {
|
||||||
|
scheduledBatches.add(invocation.getArgument(1));
|
||||||
|
return mock(BukkitTask.class);
|
||||||
|
});
|
||||||
|
when(server.getOfflinePlayer(anyString()))
|
||||||
|
.thenAnswer(
|
||||||
|
invocation -> {
|
||||||
|
String name = invocation.getArgument(0);
|
||||||
|
OfflinePlayer player = mock(OfflinePlayer.class);
|
||||||
|
UUID playerId = UUID.nameUUIDFromBytes(name.getBytes(StandardCharsets.UTF_8));
|
||||||
|
when(player.getName()).thenReturn(name);
|
||||||
|
when(player.getUniqueId()).thenReturn(playerId);
|
||||||
|
return player;
|
||||||
|
});
|
||||||
|
when(plugin.getNumPlots(any(UUID.class)))
|
||||||
|
.thenAnswer(invocation -> existingHomeOwners.contains(invocation.getArgument(0)) ? 1 : 0);
|
||||||
|
when(plugin.generateHome(any(OfflinePlayer.class), eq(1)))
|
||||||
|
.thenAnswer(
|
||||||
|
invocation -> {
|
||||||
|
OfflinePlayer player = invocation.getArgument(0);
|
||||||
|
generatedNames.add(player.getName());
|
||||||
|
existingHomeOwners.add(player.getUniqueId());
|
||||||
|
return mock(PlayerHome.class);
|
||||||
|
});
|
||||||
|
|
||||||
|
MetropolisDebugGenerateTestHomesCommand executor =
|
||||||
|
new MetropolisDebugGenerateTestHomesCommand(plugin);
|
||||||
|
|
||||||
|
assertTrue(executor.onCommand(sender, mock(Command.class), "testhomes", new String[] {"10"}));
|
||||||
|
assertTrue(generatedNames.isEmpty());
|
||||||
|
assertEquals(1, scheduledBatches.size());
|
||||||
|
assertFalse(executor.onCommand(sender, mock(Command.class), "testhomes", new String[] {"10"}));
|
||||||
|
|
||||||
|
scheduledBatches.getFirst().run();
|
||||||
|
assertEquals(List.of("Test1"), generatedNames);
|
||||||
|
for (int tick = 1; tick < 10; tick++) {
|
||||||
|
scheduledBatches.getFirst().run();
|
||||||
|
}
|
||||||
|
assertEquals(10, generatedNames.size());
|
||||||
|
|
||||||
|
assertTrue(executor.onCommand(sender, mock(Command.class), "testhomes", new String[] {"10"}));
|
||||||
|
assertEquals(2, scheduledBatches.size());
|
||||||
|
for (int tick = 0; tick < 20; tick++) {
|
||||||
|
scheduledBatches.get(1).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> expectedNames = new ArrayList<>();
|
||||||
|
for (int testNumber = 1; testNumber <= 20; testNumber++) {
|
||||||
|
expectedNames.add("Test" + testNumber);
|
||||||
|
}
|
||||||
|
assertEquals(expectedNames, generatedNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsNonPositiveCountsWithoutGeneratingHomes() {
|
||||||
|
MetropolisPlugin plugin = mock(MetropolisPlugin.class);
|
||||||
|
MetropolisDebugGenerateTestHomesCommand executor =
|
||||||
|
new MetropolisDebugGenerateTestHomesCommand(plugin);
|
||||||
|
|
||||||
|
assertFalse(
|
||||||
|
executor.onCommand(
|
||||||
|
mock(CommandSender.class), mock(Command.class), "testhomes", new String[] {"0"}));
|
||||||
|
|
||||||
|
verify(plugin, never()).generateHome(any(OfflinePlayer.class), eq(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
|||||||
|
|
||||||
package com.majinnaibu.bukkitplugins.metropolis.commands;
|
package com.majinnaibu.bukkitplugins.metropolis.commands;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
@@ -64,4 +65,23 @@ class MetropolisHomeAcquireTest {
|
|||||||
|
|
||||||
verify(plugin, never()).acquireHome(player);
|
verify(plugin, never()).acquireHome(player);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void supportsDisablingAutomaticInitialHomeAllocation() {
|
||||||
|
assertEquals(1, MetropolisPlugin.normalizeInitialHomeCount(1));
|
||||||
|
assertTrue(MetropolisPlugin.shouldAutoGenerateInitialHome(1));
|
||||||
|
|
||||||
|
int zero = MetropolisPlugin.normalizeInitialHomeCount(0);
|
||||||
|
assertEquals(0, zero);
|
||||||
|
assertFalse(MetropolisPlugin.shouldAutoGenerateInitialHome(zero));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void keepsLevelCountsWithinTheConfiguredAndWorldBounds() {
|
||||||
|
assertEquals(1, MetropolisPlugin.normalizeMaxLevels(0));
|
||||||
|
assertEquals(4, MetropolisPlugin.normalizeMaxLevels(4));
|
||||||
|
assertEquals(3, MetropolisPlugin.clampMaxLevels(10, 3));
|
||||||
|
assertEquals(1, MetropolisPlugin.clampMaxLevels(2, 1));
|
||||||
|
assertEquals(0, MetropolisPlugin.clampMaxLevels(0, 0));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
This file is part of Metropolis.
|
||||||
|
|
||||||
|
Metropolis is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
Metropolis is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Metropolis. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.majinnaibu.bukkitplugins.metropolis.commands;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.contains;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import com.majinnaibu.bukkitplugins.metropolis.MetropolisPlugin;
|
||||||
|
import com.majinnaibu.bukkitplugins.metropolis.Plot;
|
||||||
|
import com.sk89q.worldedit.math.BlockVector3;
|
||||||
|
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class MetropolisPlotGoCommandTest {
|
||||||
|
private static final UUID PLAYER_ID = UUID.fromString("123e4567-e89b-12d3-a456-426614174000");
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void teleportsPlayerToTheirOnlyReservation() {
|
||||||
|
MetropolisPlugin plugin = mock(MetropolisPlugin.class);
|
||||||
|
Player player = player("Member", PLAYER_ID);
|
||||||
|
Plot reservation = plot("r_camp");
|
||||||
|
when(plugin.getReservationsForPlayer(PLAYER_ID)).thenReturn(List.of(reservation));
|
||||||
|
MetropolisPlotGoCommand command = new MetropolisPlotGoCommand(plugin);
|
||||||
|
|
||||||
|
assertTrue(command.onCommand(player, mock(Command.class), "plot-go", new String[0]));
|
||||||
|
|
||||||
|
verify(plugin).teleportPlayerToPlot(player, reservation);
|
||||||
|
verify(player).sendMessage(contains("reservation r_camp"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void asksForAReservationNameWhenPlayerBelongsToSeveral() {
|
||||||
|
MetropolisPlugin plugin = mock(MetropolisPlugin.class);
|
||||||
|
Player player = player("Member", PLAYER_ID);
|
||||||
|
when(plugin.getReservationsForPlayer(PLAYER_ID))
|
||||||
|
.thenReturn(List.of(plot("r_camp"), plot("r_garden")));
|
||||||
|
MetropolisPlotGoCommand command = new MetropolisPlotGoCommand(plugin);
|
||||||
|
|
||||||
|
assertFalse(command.onCommand(player, mock(Command.class), "plot-go", new String[0]));
|
||||||
|
|
||||||
|
verify(player).sendMessage(contains("r_camp, r_garden"));
|
||||||
|
verify(plugin, never()).teleportPlayerToPlot(player, plot("r_camp"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void authorizedSenderCanTargetAnOnlineMember() {
|
||||||
|
MetropolisPlugin plugin = mock(MetropolisPlugin.class);
|
||||||
|
CommandSender sender = mock(CommandSender.class);
|
||||||
|
Player target = player("Member", PLAYER_ID);
|
||||||
|
Plot reservation = plot("r_camp");
|
||||||
|
when(sender.hasPermission("metropolis.plot.go")).thenReturn(true);
|
||||||
|
when(plugin.getPlayer("Member")).thenReturn(target);
|
||||||
|
when(plugin.getReservationsForPlayer(PLAYER_ID)).thenReturn(List.of(reservation));
|
||||||
|
MetropolisPlotGoCommand command = new MetropolisPlotGoCommand(plugin);
|
||||||
|
|
||||||
|
assertTrue(command.onCommand(sender, mock(Command.class), "plot-go", new String[] {"Member"}));
|
||||||
|
|
||||||
|
verify(plugin).teleportPlayerToPlot(target, reservation);
|
||||||
|
verify(target).sendMessage(contains("You were teleported"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void reportsNoMembershipAndUnsafeDestinations() {
|
||||||
|
MetropolisPlugin plugin = mock(MetropolisPlugin.class);
|
||||||
|
Player player = player("Member", PLAYER_ID);
|
||||||
|
when(plugin.getReservationsForPlayer(PLAYER_ID)).thenReturn(List.of());
|
||||||
|
MetropolisPlotGoCommand command = new MetropolisPlotGoCommand(plugin);
|
||||||
|
|
||||||
|
assertFalse(command.onCommand(player, mock(Command.class), "plot-go", new String[0]));
|
||||||
|
verify(player).sendMessage(contains("not a member or owner"));
|
||||||
|
|
||||||
|
Plot reservation = plot("r_camp");
|
||||||
|
when(plugin.getReservationsForPlayer(PLAYER_ID)).thenReturn(List.of(reservation));
|
||||||
|
when(plugin.teleportPlayerToPlot(player, reservation)).thenReturn("No safe teleport location");
|
||||||
|
assertFalse(command.onCommand(player, mock(Command.class), "plot-go", new String[0]));
|
||||||
|
verify(player).sendMessage(contains("No safe teleport location"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Player player(String name, UUID id) {
|
||||||
|
Player player = mock(Player.class);
|
||||||
|
when(player.getName()).thenReturn(name);
|
||||||
|
when(player.getUniqueId()).thenReturn(id);
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Plot plot(String name) {
|
||||||
|
return new Plot(name, BlockVector3.ZERO, BlockVector3.at(5, 5, 5));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user