Apply offsets to the logical plot grid

This commit is contained in:
2026-09-27 08:21:22 -07:00
parent 8d99c2e67f
commit 60c3d41e50
7 changed files with 234 additions and 40 deletions

View File

@@ -24,6 +24,7 @@ Use this checklist for behavior that depends on a real Paper world, player inter
- 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. - 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. - 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.
- 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. - 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.
@@ -76,10 +77,11 @@ 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 53 tests covering: Run `./gradlew test` for the server-free JUnit suite. It has 57 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. - `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. - `PlotLevelSupportTest`: rejection of upper plots with missing logical support cells and complete coverage requirements across all lower levels.
- `PlotGridLayoutTest`: zero-offset compatibility, offsets on X/Y/Z, axis-specific spacing, and negative logical-index recovery.
- `SpawnLayoutTest`: default one-cell bounds, independent multipliers, and build-height validation. - `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. - `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. - `RoadLayoutTest`: independently sized street/avenue bands, correct road-type tagging, non-overlapping corners, and disabled roads at zero width.

View File

@@ -60,6 +60,8 @@ Copy `.env.example` to `.env` and configure the Paper version/build and server p
`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. `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.
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. 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. 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.

View File

@@ -98,6 +98,9 @@ public class MetropolisPlugin extends JavaPlugin {
private int plotSizeX = 24; private int plotSizeX = 24;
private int plotSizeY = 256; private int plotSizeY = 256;
private int plotSizeZ = 24; private int plotSizeZ = 24;
private int plotOffsetX;
private int plotOffsetY;
private int plotOffsetZ;
private int gridSizeX = 28; private int gridSizeX = 28;
private int gridSizeY = 256; private int gridSizeY = 256;
private int maxLevels = 1; private int maxLevels = 1;
@@ -146,6 +149,7 @@ public class MetropolisPlugin extends JavaPlugin {
private Cuboid _cityCuboid = null; private Cuboid _cityCuboid = null;
private ProtectedRegion _spawnRegion = null; private ProtectedRegion _spawnRegion = null;
private ProtectedRegion _cityRegion = null; private ProtectedRegion _cityRegion = null;
private PlotGridLayout plotGridLayout;
@Override @Override
public void onDisable() { public void onDisable() {
@@ -201,6 +205,9 @@ public class MetropolisPlugin extends JavaPlugin {
plotSizeX = safeGetIntFromConfig(config, "plot.sizeX"); plotSizeX = safeGetIntFromConfig(config, "plot.sizeX");
plotSizeY = optionalIntFromConfig(config, "plot.sizeY", 256); plotSizeY = optionalIntFromConfig(config, "plot.sizeY", 256);
plotSizeZ = safeGetIntFromConfig(config, "plot.sizeZ"); plotSizeZ = safeGetIntFromConfig(config, "plot.sizeZ");
plotOffsetX = safeGetIntFromConfig(config, "plot.offsetX");
plotOffsetY = safeGetIntFromConfig(config, "plot.offsetY");
plotOffsetZ = safeGetIntFromConfig(config, "plot.offsetZ");
maxLevels = optionalIntFromConfig(config, "plot.maxLevels", "maxLevels", 1); maxLevels = optionalIntFromConfig(config, "plot.maxLevels", "maxLevels", 1);
generateFloor = safeGetBooleanFromConfig(config, "plot.floor.generate"); generateFloor = safeGetBooleanFromConfig(config, "plot.floor.generate");
floorMaterial = safeGetMaterialFromConfig(config, "plot.floor.material"); floorMaterial = safeGetMaterialFromConfig(config, "plot.floor.material");
@@ -329,9 +336,20 @@ public class MetropolisPlugin extends JavaPlugin {
if (streetWidth < 0 || avenueWidth < 0) { if (streetWidth < 0 || avenueWidth < 0) {
throw new IllegalArgumentException("Road widths must not be negative."); throw new IllegalArgumentException("Road widths must not be negative.");
} }
gridSizeX = plotSizeX + avenueWidth; plotGridLayout =
gridSizeY = Math.max(plotSizeY + Math.max(streetWidth, avenueWidth), 1); new PlotGridLayout(
gridSizeZ = plotSizeZ + streetWidth; plotSizeX,
plotSizeY,
plotSizeZ,
streetWidth,
avenueWidth,
plotOffsetX,
plotOffsetY,
plotOffsetZ,
roadLevel);
gridSizeX = plotGridLayout.gridSizeX();
gridSizeY = plotGridLayout.gridSizeY();
gridSizeZ = plotGridLayout.gridSizeZ();
regionManager = regionManager =
WorldGuard.getInstance().getPlatform().getRegionContainer().get(BukkitAdapter.adapt(world)); WorldGuard.getInstance().getPlatform().getRegionContainer().get(BukkitAdapter.adapt(world));
@@ -622,7 +640,10 @@ public class MetropolisPlugin extends JavaPlugin {
plotSizeZ, plotSizeZ,
avenueWidth, avenueWidth,
streetWidth, streetWidth,
roadLevel); roadLevel,
plotOffsetX,
plotOffsetY,
plotOffsetZ);
if (!SpawnLayout.withinBuildHeight(spawnCuboid, world.getMinHeight(), world.getMaxHeight())) { if (!SpawnLayout.withinBuildHeight(spawnCuboid, world.getMinHeight(), world.getMaxHeight())) {
getLogger() getLogger()
.severe( .severe(
@@ -1037,11 +1058,7 @@ public class MetropolisPlugin extends JavaPlugin {
} }
public BlockVector3 getPlotMin(int row, int col, int plotMultiplier, int level) { public BlockVector3 getPlotMin(int row, int col, int plotMultiplier, int level) {
BlockVector3 gridMin = getGridMin(row, col, plotMultiplier, level); BlockVector3 bv = plotGridLayout.plotMin(row, col, level);
BlockVector3 bv =
BlockVector3.at(
gridMin.x() + avenueWidth / 2, getLevelStartY(level), gridMin.z() + streetWidth / 2);
getLogger().info(String.format("getPlotMin (%d, %d, %d)", bv.x(), bv.y(), bv.z())); getLogger().info(String.format("getPlotMin (%d, %d, %d)", bv.x(), bv.y(), bv.z()));
return bv; return bv;
} }
@@ -1051,13 +1068,7 @@ public class MetropolisPlugin extends JavaPlugin {
} }
public BlockVector3 getPlotMax(int row, int col, int plotMultiplier, int level) { public BlockVector3 getPlotMax(int row, int col, int plotMultiplier, int level) {
BlockVector3 gridMax = getGridMax(row, col, plotMultiplier, level); BlockVector3 bv = plotGridLayout.plotMax(row, col, plotMultiplier, plotMultiplier, level);
BlockVector3 bv =
BlockVector3.at(
gridMax.x() - (avenueWidth - avenueWidth / 2),
getLevelStartY(level) + plotSizeY - 1,
gridMax.z() - (streetWidth - streetWidth / 2));
getLogger().info(String.format("getPlotMax (%d, %d, %d)", bv.x(), bv.y(), bv.z())); getLogger().info(String.format("getPlotMax (%d, %d, %d)", bv.x(), bv.y(), bv.z()));
return bv; return bv;
} }
@@ -1067,7 +1078,7 @@ public class MetropolisPlugin extends JavaPlugin {
} }
public BlockVector3 getGridMin(int row, int col, int plotMultiplier, int level) { public BlockVector3 getGridMin(int row, int col, int plotMultiplier, int level) {
BlockVector3 bv = BlockVector3.at(col * gridSizeX, getLevelStartY(level), row * gridSizeZ); BlockVector3 bv = plotGridLayout.gridMin(row, col, level);
getLogger().info(String.format("getGridMin (%d, %d, %d)", bv.x(), bv.y(), bv.z())); getLogger().info(String.format("getGridMin (%d, %d, %d)", bv.x(), bv.y(), bv.z()));
return bv; return bv;
} }
@@ -1077,31 +1088,29 @@ public class MetropolisPlugin extends JavaPlugin {
} }
public BlockVector3 getGridMax(int row, int col, int plotMultiplier, int level) { public BlockVector3 getGridMax(int row, int col, int plotMultiplier, int level) {
BlockVector3 bv = BlockVector3 bv = plotGridLayout.gridMax(row, col, plotMultiplier, plotMultiplier, level);
BlockVector3.at(
(col + plotMultiplier) * gridSizeX - 1,
getLevelStartY(level) + plotSizeY - 1,
(row + plotMultiplier) * gridSizeZ - 1);
getLogger().info(String.format("getGridMax (%d, %d, %d)", bv.x(), bv.y(), bv.z())); getLogger().info(String.format("getGridMax (%d, %d, %d)", bv.x(), bv.y(), bv.z()));
return bv; return bv;
} }
private int getPlotXFromMin(Cuboid cuboid) { private int getPlotXFromMin(Cuboid cuboid) {
return (cuboid.minX - avenueWidth / 2) / gridSizeX; return plotGridLayout.plotXIndexFromMin(cuboid);
} }
private int getPlotZFromMin(Cuboid cuboid) { private int getPlotZFromMin(Cuboid cuboid) {
return (cuboid.minZ - streetWidth / 2) / gridSizeZ; return plotGridLayout.plotZIndexFromMin(cuboid);
} }
private int getLevelPitchY() { private int getLevelPitchY() {
return plotSizeY + Math.max(streetWidth, avenueWidth); return plotGridLayout.gridSizeY();
} }
private int getEffectiveMaxLevels() { private int getEffectiveMaxLevels() {
int availableStreetLevels = getAvailableLevelsAt(roadLevel); int streetBaseY = getLevelStartY(0);
int avenueBaseY = streetBaseY + avenueLevel - roadLevel;
int availableStreetLevels = getAvailableLevelsAt(streetBaseY);
int availableAvenueLevels = int availableAvenueLevels =
avenueWidth > 0 ? getAvailableLevelsAt(avenueLevel) : availableStreetLevels; avenueWidth > 0 ? getAvailableLevelsAt(avenueBaseY) : availableStreetLevels;
int maxWorldLevels = Math.min(availableStreetLevels, availableAvenueLevels); int maxWorldLevels = Math.min(availableStreetLevels, availableAvenueLevels);
return clampMaxLevels(maxLevels, maxWorldLevels); return clampMaxLevels(maxLevels, maxWorldLevels);
} }
@@ -1119,7 +1128,7 @@ public class MetropolisPlugin extends JavaPlugin {
} }
private int getLevelStartY(int level) { private int getLevelStartY(int level) {
return roadLevel + level * getLevelPitchY(); return plotGridLayout.levelStartY(level);
} }
private void setHomeOccupied( private void setHomeOccupied(
@@ -1289,7 +1298,7 @@ public class MetropolisPlugin extends JavaPlugin {
private List<AvenueStairwayLayout.Step> getUpperRoadOpeningsForPlot( private List<AvenueStairwayLayout.Step> getUpperRoadOpeningsForPlot(
Cuboid plotCuboid, int roadY, RoadLayout.RoadType roadType) { Cuboid plotCuboid, int roadY, RoadLayout.RoadType roadType) {
int baseY = roadType == RoadLayout.RoadType.AVENUE ? avenueLevel : roadLevel; int baseY = (roadType == RoadLayout.RoadType.AVENUE ? avenueLevel : roadLevel) + plotOffsetY;
if (!areStairsEnabled(roadType) || roadY <= baseY) { if (!areStairsEnabled(roadType) || roadY <= baseY) {
return List.of(); return List.of();
} }
@@ -1325,7 +1334,7 @@ public class MetropolisPlugin extends JavaPlugin {
avenueStairsEveryNBlocks, avenueStairsEveryNBlocks,
avenueWidth, avenueWidth,
avenueStairWidth, avenueStairWidth,
avenueLevel + lowerLevel * getLevelPitchY(), avenueLevel + plotOffsetY + lowerLevel * getLevelPitchY(),
getLevelPitchY(), getLevelPitchY(),
3); 3);
} }
@@ -1338,7 +1347,7 @@ public class MetropolisPlugin extends JavaPlugin {
streetStairsEveryNBlocks, streetStairsEveryNBlocks,
streetWidth, streetWidth,
streetStairWidth, streetStairWidth,
roadLevel + lowerLevel * getLevelPitchY(), roadLevel + plotOffsetY + lowerLevel * getLevelPitchY(),
getLevelPitchY(), getLevelPitchY(),
3); 3);
} }

View File

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

View File

@@ -29,7 +29,10 @@ public final class SpawnLayout {
int plotSizeZ, int plotSizeZ,
int avenueWidth, int avenueWidth,
int streetWidth, int streetWidth,
int roadLevel) { int roadLevel,
int offsetX,
int offsetY,
int offsetZ) {
if (sizeX < 1 || sizeY < 1 || sizeZ < 1 || avenueWidth < 0 || streetWidth < 0) { if (sizeX < 1 || sizeY < 1 || sizeZ < 1 || avenueWidth < 0 || streetWidth < 0) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Spawn multipliers must be positive and road widths nonnegative"); "Spawn multipliers must be positive and road widths nonnegative");
@@ -42,10 +45,11 @@ public final class SpawnLayout {
int maxInsetX = avenueWidth - minX; int maxInsetX = avenueWidth - minX;
int maxInsetZ = streetWidth - minZ; int maxInsetZ = streetWidth - minZ;
int maxX = Math.subtractExact(Math.multiplyExact(sizeX, gridSizeX) - 1, maxInsetX); int maxX = Math.subtractExact(Math.multiplyExact(sizeX, gridSizeX) - 1, maxInsetX);
int maxY = Math.addExact(roadLevel, Math.multiplyExact(sizeY, plotSizeY) - 1); 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); int maxZ = Math.subtractExact(Math.multiplyExact(sizeZ, gridSizeZ) - 1, maxInsetZ);
return new Cuboid(minX, roadLevel, minZ, maxX, maxY, maxZ); return new Cuboid(offsetX + minX, baseY, offsetZ + minZ, offsetX + maxX, maxY, offsetZ + maxZ);
} }
public static boolean withinBuildHeight(Cuboid cuboid, int minHeight, int maxHeight) { public static boolean withinBuildHeight(Cuboid cuboid, int minHeight, int maxHeight) {

View File

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

View File

@@ -26,21 +26,21 @@ import org.junit.jupiter.api.Test;
class SpawnLayoutTest { class SpawnLayoutTest {
@Test @Test
void defaultMultipliersPreserveTheOnePlotSpawnBounds() { void defaultMultipliersPreserveTheOnePlotSpawnBounds() {
Cuboid spawn = SpawnLayout.bounds(1, 1, 1, 32, 10, 32, 4, 4, 62); Cuboid spawn = SpawnLayout.bounds(1, 1, 1, 32, 10, 32, 4, 4, 62, 0, 0, 0);
assertEqualsBounds(spawn, 2, 62, 2, 33, 71, 33); assertEqualsBounds(spawn, 2, 62, 2, 33, 71, 33);
} }
@Test @Test
void appliesEachMultiplierToItsOwnAlignedGridDimension() { void appliesEachMultiplierToItsOwnAlignedGridDimension() {
Cuboid spawn = SpawnLayout.bounds(2, 3, 4, 32, 10, 32, 4, 4, 62); Cuboid spawn = SpawnLayout.bounds(2, 3, 4, 32, 10, 32, 4, 4, 62, 0, 0, 0);
assertEqualsBounds(spawn, 2, 62, 2, 69, 91, 141); assertEqualsBounds(spawn, 2, 62, 2, 69, 91, 141);
} }
@Test @Test
void validatesTheEntireSpawnHeightAgainstWorldBounds() { void validatesTheEntireSpawnHeightAgainstWorldBounds() {
Cuboid spawn = SpawnLayout.bounds(1, 3, 1, 32, 10, 32, 4, 4, 62); Cuboid spawn = SpawnLayout.bounds(1, 3, 1, 32, 10, 32, 4, 4, 62, 0, 0, 0);
assertTrue(SpawnLayout.withinBuildHeight(spawn, -64, 320)); assertTrue(SpawnLayout.withinBuildHeight(spawn, -64, 320));
assertFalse(SpawnLayout.withinBuildHeight(spawn, -64, 91)); assertFalse(SpawnLayout.withinBuildHeight(spawn, -64, 91));
@@ -48,15 +48,23 @@ class SpawnLayoutTest {
@Test @Test
void alignsSpawnAxesToTheirIndependentRoadWidths() { void alignsSpawnAxesToTheirIndependentRoadWidths() {
Cuboid spawn = SpawnLayout.bounds(2, 1, 3, 32, 10, 32, 6, 2, 62); Cuboid spawn = SpawnLayout.bounds(2, 1, 3, 32, 10, 32, 6, 2, 62, 0, 0, 0);
assertEqualsBounds(spawn, 3, 62, 1, 72, 71, 100); 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 @Test
void rejectsNonPositiveSpawnMultipliers() { void rejectsNonPositiveSpawnMultipliers() {
assertThrows( assertThrows(
IllegalArgumentException.class, () -> SpawnLayout.bounds(0, 1, 1, 32, 10, 32, 4, 4, 62)); IllegalArgumentException.class,
() -> SpawnLayout.bounds(0, 1, 1, 32, 10, 32, 4, 4, 62, 0, 0, 0));
} }
private static void assertEqualsBounds( private static void assertEqualsBounds(