From 04a0c1d95951499dbd32f396b9910f07de3f1c5d Mon Sep 17 00:00:00 2001 From: Tom Hicks Date: Sun, 27 Sep 2026 07:04:09 -0700 Subject: [PATCH] Add multi-level plot allocation support --- Human-Tests.md | 3 +- README.md | 5 +- .../metropolis/MetropolisPlugin.java | 295 ++++++++++++------ src/main/resources/config.yml | 23 +- src/main/resources/examples/multi-level.yml | 59 ++++ .../commands/MetropolisHomeAcquireTest.java | 9 + 6 files changed, 292 insertions(+), 102 deletions(-) create mode 100644 src/main/resources/examples/multi-level.yml diff --git a/Human-Tests.md b/Human-Tests.md index ddeadad..3398b48 100644 --- a/Human-Tests.md +++ b/Human-Tests.md @@ -23,6 +23,7 @@ Use this checklist for behavior that depends on a real Paper world, player inter - Join as a new player. Confirm one home is allocated and the welcome message reports its actual bounds. Check the corresponding `h_1_` 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. +- 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 stacked plots use the same X/Z allocation pattern at each level. - 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. - 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. @@ -71,7 +72,7 @@ Use this checklist for behavior that depends on a real Paper world, player inter ## Existing Automated Coverage -Run `./gradlew test` for the server-free JUnit suite. It has 26 tests covering: +Run `./gradlew test` for the server-free JUnit suite. It has 28 tests covering: - `CuboidTest`: WorldEdit bound conversion, vector ordering/null handling, touching intersections, and point containment. - `CurrentHomesStoreTest`: UUID-based persistence and resolving a legacy player-name key. diff --git a/README.md b/README.md index 5346207..ace68fe 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ Metropolis expands a protected Minecraft city as players join. Each player can r ## Features - 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. - 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. @@ -57,7 +58,9 @@ Copy `.env.example` to `.env` and configure the Paper version/build and server p ## 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; `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. + +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`. 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: diff --git a/src/main/java/com/majinnaibu/bukkitplugins/metropolis/MetropolisPlugin.java b/src/main/java/com/majinnaibu/bukkitplugins/metropolis/MetropolisPlugin.java index 9d491c7..ccd1e67 100644 --- a/src/main/java/com/majinnaibu/bukkitplugins/metropolis/MetropolisPlugin.java +++ b/src/main/java/com/majinnaibu/bukkitplugins/metropolis/MetropolisPlugin.java @@ -93,10 +93,11 @@ public class MetropolisPlugin extends JavaPlugin { int size = 1; private int plotSizeX = 24; - // private int plotSizeY = 256; + private int plotSizeY = 256; private int plotSizeZ = 24; private int gridSizeX = 28; private int gridSizeY = 256; + private int maxLevels = 1; private int gridSizeZ = 28; private int roadWidth = 4; private int roadLevel = 62; @@ -178,19 +179,40 @@ public class MetropolisPlugin extends JavaPlugin { getLogger().info("Reading configuration from file."); } plotSizeX = safeGetIntFromConfig(config, "plot.sizeX"); + plotSizeY = optionalIntFromConfig(config, "plot.sizeY", 256); plotSizeZ = safeGetIntFromConfig(config, "plot.sizeZ"); + maxLevels = optionalIntFromConfig(config, "plot.maxLevels", "maxLevels", 1); generateFloor = safeGetBooleanFromConfig(config, "plot.floor.generate"); floorMaterial = safeGetMaterialFromConfig(config, "plot.floor.material"); spaceAboveFloor = safeGetIntFromConfig(config, "plot.floor.clearSpaceAbove"); generateFloorSupports = safeGetBooleanFromConfig(config, "plot.floor.supports.generate"); floorSupportMaterial = safeGetMaterialFromConfig(config, "plot.floor.supports.material"); generateSign = safeGetBooleanFromConfig(config, "plot.sign.generate"); - roadWidth = safeGetIntFromConfig(config, "road.width"); - spaceAboveRoad = safeGetIntFromConfig(config, "road.clearSpaceAbove"); - roadLevel = safeGetIntFromConfig(config, "road.level"); - roadMaterial = safeGetMaterialFromConfig(config, "road.material"); - generateRoadSupports = safeGetBooleanFromConfig(config, "road.supports.generate"); - roadSupportMaterial = safeGetMaterialFromConfig(config, "road.supports.material"); + roadWidth = + safeGetIntFromConfig(config, "road.streets.width", "road.avenues.width", "road.width"); + spaceAboveRoad = + safeGetIntFromConfig( + config, + "road.streets.clearSpaceAbove", + "road.avenues.clearSpaceAbove", + "road.clearSpaceAbove"); + roadLevel = + safeGetIntFromConfig(config, "road.streets.level", "road.avenues.level", "road.level"); + roadMaterial = + safeGetMaterialFromConfig( + config, "road.streets.material", "road.avenues.material", "road.material"); + generateRoadSupports = + safeGetBooleanFromConfig( + config, + "road.streets.supports.generate", + "road.avenues.supports.generate", + "road.supports.generate"); + roadSupportMaterial = + safeGetMaterialFromConfig( + config, + "road.streets.supports.material", + "road.avenues.supports.material", + "road.supports.material"); generateSpawn = safeGetBooleanFromConfig(config, "spawn.generate"); setWorldSpawn = safeGetBooleanFromConfig(config, "spawn.setAsWorldSpawn"); spawnFloorMaterial = safeGetMaterialFromConfig(config, "spawn.material"); @@ -243,7 +265,7 @@ public class MetropolisPlugin extends JavaPlugin { } gridSizeX = plotSizeX + roadWidth; - gridSizeY = world.getMaxHeight(); + gridSizeY = Math.max(plotSizeY + roadWidth, 1); gridSizeZ = plotSizeZ + roadWidth; regionManager = @@ -415,43 +437,72 @@ public class MetropolisPlugin extends JavaPlugin { } } - private boolean safeGetBooleanFromConfig(Configuration config, String name) { - if (config.isBoolean(name)) { - return config.getBoolean(name); - } else { - throwInvalidConfigException(); - return false; + private boolean safeGetBooleanFromConfig(Configuration config, String... names) { + for (String name : names) { + if (config.isBoolean(name)) { + return config.getBoolean(name); + } } + throwInvalidConfigException(); + return false; } - private int safeGetIntFromConfig(Configuration config, String name) { - if (config.isInt(name)) { - return config.getInt(name); - } else { - throwInvalidConfigException(); - return 0; + private int safeGetIntFromConfig(Configuration config, String... names) { + for (String name : names) { + if (config.isInt(name)) { + return config.getInt(name); + } } + throwInvalidConfigException(); + return 0; } - private Material safeGetMaterialFromConfig(Configuration config, String name) { - Material material = null; - if (config.isInt(name)) { - material = - switch (config.getInt(name)) { - case 1 -> Material.STONE; - case 2 -> Material.GRASS_BLOCK; - case 4 -> Material.COBBLESTONE; - case 7 -> Material.BEDROCK; - default -> null; - }; - } else if (config.isString(name)) { - material = Material.matchMaterial(config.getString(name)); + private int optionalIntFromConfig(Configuration config, String[] names, int defaultValue) { + for (String name : names) { + if (config.isInt(name)) { + return config.getInt(name); + } } - if (material == null) { - getLogger().severe("Invalid material configured at " + name); - throwInvalidConfigException(); + return defaultValue; + } + + private int optionalIntFromConfig(Configuration config, String name, int defaultValue) { + return config.isInt(name) ? config.getInt(name) : defaultValue; + } + + private int optionalIntFromConfig( + Configuration config, String firstName, String secondName, int defaultValue) { + if (config.isInt(firstName)) { + return config.getInt(firstName); } - return material; + if (config.isInt(secondName)) { + return config.getInt(secondName); + } + return defaultValue; + } + + private Material safeGetMaterialFromConfig(Configuration config, String... names) { + for (String name : names) { + Material material = null; + if (config.isInt(name)) { + material = + switch (config.getInt(name)) { + case 1 -> Material.STONE; + case 2 -> Material.GRASS_BLOCK; + case 4 -> Material.COBBLESTONE; + case 7 -> Material.BEDROCK; + default -> null; + }; + } else if (config.isString(name)) { + material = Material.matchMaterial(config.getString(name)); + } + if (material != null) { + return material; + } + } + getLogger().severe("Invalid material configured at " + String.join(" or ", names)); + throwInvalidConfigException(); + return Material.STONE; } private void throwInvalidConfigException() { @@ -577,6 +628,16 @@ public class MetropolisPlugin extends JavaPlugin { return Math.max(0, count); } + public static int normalizeMaxLevels(int count) { + return Math.max(1, count); + } + + public static int clampMaxLevels(int requestedLevels, int availableLevels) { + int normalizedRequested = normalizeMaxLevels(requestedLevels); + int normalizedAvailable = Math.max(1, availableLevels); + return Math.min(normalizedRequested, normalizedAvailable); + } + public static boolean shouldAutoGenerateInitialHome(int count) { return count > 0; } @@ -595,7 +656,7 @@ public class MetropolisPlugin extends JavaPlugin { private void generateFloor(Cuboid plotCuboid) { int x = 0; - int y = roadLevel; + int y = plotCuboid.minY; int z = 0; for (x = plotCuboid.minX; x <= plotCuboid.maxX; x++) { @@ -636,7 +697,7 @@ public class MetropolisPlugin extends JavaPlugin { private void createRoads(Cuboid plotCuboid, int roadMask) { if (roadWidth > 0) { int x = 0; - int y = roadLevel; + int y = plotCuboid.minY; int z = 0; if (plotCuboid == null) { @@ -763,7 +824,14 @@ public class MetropolisPlugin extends JavaPlugin { } public boolean isBlockOccupied(int row, int col) { - Cuboid cuboid = new Cuboid(getGridMin(row, col, 1), getGridMax(row, col, 1)); + return isBlockOccupied(row, col, 0, 1); + } + + public boolean isBlockOccupied(int row, int col, int level, int plotMultiplier) { + Cuboid cuboid = + new Cuboid( + getGridMin(row, col, plotMultiplier, level), + getGridMax(row, col, plotMultiplier, level)); for (Plot plot : _occupiedPlots) { if (plot.getCuboid().intersects(cuboid)) { return true; @@ -778,9 +846,13 @@ public class MetropolisPlugin extends JavaPlugin { } private boolean areBlocksOccupied(int row, int col, int i) { + return areBlocksOccupied(row, col, i, 0); + } + + private boolean areBlocksOccupied(int row, int col, int i, int level) { for (int ix = col; ix < col + i; ix++) { for (int iy = row; iy < row + i; iy++) { - if (isBlockOccupied(iy, ix)) { + if (isBlockOccupied(iy, ix, level, 1)) { return true; } } @@ -796,56 +868,63 @@ public class MetropolisPlugin extends JavaPlugin { int min = -ring; int max = ring - (plotMultiplier - 1); boolean done = false; + int levelCount = getEffectiveMaxLevels(); while (!done) { row = min; col = min; - // Top - for (col = min; col <= max; col++) { - if (!areBlocksOccupied(row, col, plotMultiplier)) { - if (DEBUG) { - getLogger().info(String.format("row: %d, col: %d", row, col)); - } - return new Cuboid( - getPlotMin(row, col, plotMultiplier), getPlotMax(row, col, plotMultiplier)); - } - } - - // Right side - col = max; - for (row = min + 1; row < max; row++) { - if (!areBlocksOccupied(row, col, plotMultiplier)) { - if (DEBUG) { - getLogger().info(String.format("row: %d, col: %d", row, col)); - } - return new Cuboid( - getPlotMin(row, col, plotMultiplier), getPlotMax(row, col, plotMultiplier)); - } - } - - // Bottom - row = max; - for (col = max; col >= min; col--) { - if (!areBlocksOccupied(row, col, plotMultiplier)) { - if (DEBUG) { - getLogger().info(String.format("row: %d, col: %d", row, col)); - } - return new Cuboid( - getPlotMin(row, col, plotMultiplier), getPlotMax(row, col, plotMultiplier)); - } - } - - // Left - col = min; - for (row = max; row > min; row--) { - if (!areBlocksOccupied(row, col, plotMultiplier)) { - if (row != 0 || col != 0) { + for (int level = 0; level < levelCount; level++) { + // Top + for (col = min; col <= max; col++) { + if (!areBlocksOccupied(row, col, plotMultiplier, level)) { if (DEBUG) { - getLogger().info(String.format("row: %d, col: %d", row, col)); + getLogger().info(String.format("row: %d, col: %d, level: %d", row, col, level)); } return new Cuboid( - getPlotMin(row, col, plotMultiplier), getPlotMax(row, col, plotMultiplier)); + getPlotMin(row, col, plotMultiplier, level), + getPlotMax(row, col, plotMultiplier, level)); + } + } + + // Right side + col = max; + for (row = min + 1; row < max; row++) { + if (!areBlocksOccupied(row, col, plotMultiplier, level)) { + if (DEBUG) { + getLogger().info(String.format("row: %d, col: %d, level: %d", row, col, level)); + } + return new Cuboid( + getPlotMin(row, col, plotMultiplier, level), + getPlotMax(row, col, plotMultiplier, level)); + } + } + + // Bottom + row = max; + for (col = max; col >= min; col--) { + if (!areBlocksOccupied(row, col, plotMultiplier, level)) { + if (DEBUG) { + getLogger().info(String.format("row: %d, col: %d, level: %d", row, col, level)); + } + return new Cuboid( + getPlotMin(row, col, plotMultiplier, level), + getPlotMax(row, col, plotMultiplier, level)); + } + } + + // Left + col = min; + for (row = max; row > min; row--) { + if (!areBlocksOccupied(row, col, plotMultiplier, level)) { + if (row != 0 || col != 0) { + if (DEBUG) { + getLogger().info(String.format("row: %d, col: %d, level: %d", row, col, level)); + } + return new Cuboid( + getPlotMin(row, col, plotMultiplier, level), + getPlotMax(row, col, plotMultiplier, level)); + } } } } @@ -853,12 +932,16 @@ public class MetropolisPlugin extends JavaPlugin { ring++; min = -ring; max = ring - (plotMultiplier - 1); + if (ring > 256) { + done = true; + } } if (DEBUG) { getLogger().info(String.format("row: %d, col: %d", row, col)); } - return new Cuboid(getPlotMin(row, col, plotMultiplier), getPlotMax(row, col, plotMultiplier)); + return new Cuboid( + getPlotMin(row, col, plotMultiplier, 0), getPlotMax(row, col, plotMultiplier, 0)); } private void resizeCityRegion() { @@ -902,41 +985,54 @@ public class MetropolisPlugin extends JavaPlugin { } public BlockVector3 getPlotMin(int row, int col, int plotMultiplier) { - BlockVector3 gridMin = getGridMin(row, col, plotMultiplier); + return getPlotMin(row, col, plotMultiplier, 0); + } + + public BlockVector3 getPlotMin(int row, int col, int plotMultiplier, int level) { + BlockVector3 gridMin = getGridMin(row, col, plotMultiplier, level); BlockVector3 bv = - BlockVector3.at(gridMin.x() + roadWidth / 2, gridMin.y(), gridMin.z() + roadWidth / 2); + BlockVector3.at( + gridMin.x() + roadWidth / 2, getLevelStartY(level), gridMin.z() + roadWidth / 2); getLogger().info(String.format("getPlotMin (%d, %d, %d)", bv.x(), bv.y(), bv.z())); return bv; } public BlockVector3 getPlotMax(int row, int col, int plotMultiplier) { - BlockVector3 gridMax = getGridMax(row, col, plotMultiplier); + return getPlotMax(row, col, plotMultiplier, 0); + } + + public BlockVector3 getPlotMax(int row, int col, int plotMultiplier, int level) { + BlockVector3 gridMax = getGridMax(row, col, plotMultiplier, level); BlockVector3 bv = BlockVector3.at( gridMax.x() - (roadWidth - roadWidth / 2), - gridMax.y(), + getLevelStartY(level) + plotSizeY - 1, gridMax.z() - (roadWidth - roadWidth / 2)); getLogger().info(String.format("getPlotMax (%d, %d, %d)", bv.x(), bv.y(), bv.z())); return bv; } public BlockVector3 getGridMin(int row, int col, int plotMultiplier) { - int level = 0; + return getGridMin(row, col, plotMultiplier, 0); + } - BlockVector3 bv = BlockVector3.at(col * gridSizeX, level * gridSizeY, row * gridSizeZ); + public BlockVector3 getGridMin(int row, int col, int plotMultiplier, int level) { + BlockVector3 bv = BlockVector3.at(col * gridSizeX, getLevelStartY(level), row * gridSizeZ); getLogger().info(String.format("getGridMin (%d, %d, %d)", bv.x(), bv.y(), bv.z())); return bv; } public BlockVector3 getGridMax(int row, int col, int plotMultiplier) { - int level = 0; + return getGridMax(row, col, plotMultiplier, 0); + } + public BlockVector3 getGridMax(int row, int col, int plotMultiplier, int level) { BlockVector3 bv = BlockVector3.at( (col + plotMultiplier) * gridSizeX - 1, - (level + 1 /*plotMultiplier*/) * gridSizeY - 1, + getLevelStartY(level) + plotSizeY - 1, (row + plotMultiplier) * gridSizeZ - 1); getLogger().info(String.format("getGridMax (%d, %d, %d)", bv.x(), bv.y(), bv.z())); return bv; @@ -950,6 +1046,19 @@ public class MetropolisPlugin extends JavaPlugin { return (cuboid.minZ - roadWidth / 2) / gridSizeZ; } + private int getLevelPitchY() { + return plotSizeY + roadWidth; + } + + private int getEffectiveMaxLevels() { + int maxWorldLevels = Math.max(1, (world.getMaxHeight() - roadLevel) / getLevelPitchY()); + return clampMaxLevels(maxLevels, maxWorldLevels); + } + + private int getLevelStartY(int level) { + return roadLevel + level * getLevelPitchY(); + } + private void setHomeOccupied( UUID ownerId, String ownerName, @@ -1068,7 +1177,7 @@ public class MetropolisPlugin extends JavaPlugin { private void generateSign(Cuboid plotCuboid, String playerName) { Block signBlock = - world.getBlockAt(plotCuboid.getCenterX(), roadLevel + 1, plotCuboid.getCenterZ()); + world.getBlockAt(plotCuboid.getCenterX(), plotCuboid.minY + 1, plotCuboid.getCenterZ()); signBlock.setType(Material.OAK_SIGN); Sign sign = (Sign) signBlock.getState(); sign.setLine(0, "Home of"); diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index cfe6540..7affced 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -18,13 +18,22 @@ plot: sign: generate: true road: - width: 4 - clearSpaceAbove: 66 - level: 62 - material: COBBLESTONE - supports: - generate: true - material: STONE + avenues: + width: 4 + clearSpaceAbove: 66 + level: 62 + material: COBBLESTONE + supports: + generate: true + material: STONE + streets: + width: 4 + clearSpaceAbove: 66 + level: 62 + material: COBBLESTONE + supports: + generate: true + material: STONE spawn: generate: true setAsWorldSpawn: true diff --git a/src/main/resources/examples/multi-level.yml b/src/main/resources/examples/multi-level.yml new file mode 100644 index 0000000..46e1c3e --- /dev/null +++ b/src/main/resources/examples/multi-level.yml @@ -0,0 +1,59 @@ +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 + streets: + width: 4 + clearSpaceAbove: 9 + level: 62 + material: COBBLESTONE + supports: + generate: true + material: STONE + stairs: + generate: false +spawn: + generate: true + setAsWorldSpawn: true + 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 diff --git a/src/test/java/com/majinnaibu/bukkitplugins/metropolis/commands/MetropolisHomeAcquireTest.java b/src/test/java/com/majinnaibu/bukkitplugins/metropolis/commands/MetropolisHomeAcquireTest.java index 622d9cf..8adffc2 100644 --- a/src/test/java/com/majinnaibu/bukkitplugins/metropolis/commands/MetropolisHomeAcquireTest.java +++ b/src/test/java/com/majinnaibu/bukkitplugins/metropolis/commands/MetropolisHomeAcquireTest.java @@ -75,4 +75,13 @@ class MetropolisHomeAcquireTest { 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(1, MetropolisPlugin.clampMaxLevels(0, 0)); + } }