Merge pull request #56 from BentoBoxWorld/develop

Release 2.3.0
This commit is contained in:
tastybento 2023-07-11 07:35:12 -07:00 committed by GitHub
commit 470f893bbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 634 additions and 202 deletions

View File

@ -11,21 +11,22 @@ jobs:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Set up JDK 17
uses: actions/setup-java@v1
uses: actions/setup-java@v3
with:
distribution: 'adopt'
java-version: 17
- name: Cache SonarCloud packages
uses: actions/cache@v1
uses: actions/cache@v3
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: Cache Maven packages
uses: actions/cache@v1
uses: actions/cache@v3
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}

View File

@ -4,14 +4,16 @@ A game mode where you are boxed into a tiny space that only expands by completin
## BentoBox Requirements
* Requires BentoBox 1.16.0 or later (Snapshots can be downloaded here: [https://ci.bentobox.world](https://ci.bentobox.world))
* Requires BentoBox 1.23.0 or later (Snapshots can be downloaded here: [https://ci.bentobox.world](https://ci.bentobox.world))
* InvSwitcher - keeps advancements, inventory, etc. separate between worlds on a server.
* Border - shows the box
## Required Plugins
* Requires WorldGeneratorAPI plugin. [Download the correct one for your server here.](https://github.com/rutgerkok/WorldGeneratorApi/releases)
* Border requires WorldBorderAPI by default. [Download it here.](https://github.com/yannicklamprecht/WorldBorderAPI/releases)
### Warning!!
Boxed requires **a lot of RAM** and can take up to **10 minutes** to boot up for the first time as it pre-generates the worlds. After the initial start, it will start up much quicker. With 12GB of RAM running on a fast ARM-based system, it takes ~ 8 minutes for the first boot. If you do not have enough RAM then weird things will happen to you server including strange errors about chunks and things like that. To dedicate enough RAM to your JVM, use the correct flags during startup. Here is my `start.sh` for running on Paper 1.19.4:
```
#!/bin/sh
java -Xms12G -Xmx12G -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200 -XX:+UnlockExperimentalVMOptions -XX:+DisableExplicitGC -XX:+AlwaysPreTouch -XX:G1HeapWastePercent=5 -XX:G1MixedGCCountTarget=4 -XX:G1MixedGCLiveThresholdPercent=90 -XX:G1RSetUpdatingPauseTimePercent=5 -XX:SurvivorRatio=32 -XX:+PerfDisableSharedMem -XX:MaxTenuringThreshold=1 -XX:G1NewSizePercent=30 -XX:G1MaxNewSizePercent=40 -XX:G1HeapRegionSize=8M -XX:G1ReservePercent=20 -XX:InitiatingHeapOccupancyPercent=15 -Dusing.aikars.flags=https://mcflags.emc.gs -Daikars.new.flags=true -jar paper-1.19.4.jar nogui
```
## How to install

View File

@ -58,14 +58,14 @@
<!-- Non-minecraft related dependencies -->
<powermock.version>2.0.9</powermock.version>
<!-- More visible way how to change dependency versions -->
<spigot.version>1.19.4-R0.1-SNAPSHOT</spigot.version>
<bentobox.version>1.23.0</bentobox.version>
<spigot.version>1.20.1-R0.1-SNAPSHOT</spigot.version>
<bentobox.version>1.24.0</bentobox.version>
<!-- Revision variable removes warning about dynamic version -->
<revision>${build.version}-SNAPSHOT</revision>
<!-- Do not change unless you want different name for local builds. -->
<build.number>-LOCAL</build.number>
<!-- This allows to change between versions. -->
<build.version>2.1.1</build.version>
<build.version>2.3.0</build.version>
<sonar.projectKey>BentoBoxWorld_Boxed</sonar.projectKey>
<sonar.organization>bentobox-world</sonar.organization>
@ -254,6 +254,7 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<!--suppress MavenModelInspection -->
<configuration>
<argLine>
${argLine}
@ -338,7 +339,7 @@
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.7</version>
<version>0.8.10</version>
<configuration>
<append>true</append>
<excludes>

1
src/main/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/.DS_Store

View File

@ -20,7 +20,6 @@ import world.bentobox.bentobox.api.events.island.IslandEvent;
import world.bentobox.bentobox.database.Database;
import world.bentobox.bentobox.database.objects.Island;
import world.bentobox.bentobox.managers.RanksManager;
import world.bentobox.bentobox.util.Util;
import world.bentobox.boxed.objects.IslandAdvancements;
/**
@ -34,7 +33,7 @@ public class AdvancementsManager {
// Database handler for level data
private final Database<IslandAdvancements> handler;
// A cache of island levels.
private final Map<String, IslandAdvancements> cache;
private final Map<String, IslandAdvancements> cache = new HashMap<>();
private final YamlConfiguration advConfig;
private int unknownAdvChange;
private int unknownRecipeChange;
@ -48,8 +47,6 @@ public class AdvancementsManager {
// Set up the database handler to store and retrieve data
// Note that these are saved by the BentoBox database
handler = new Database<>(addon, IslandAdvancements.class);
// Initialize the cache
cache = new HashMap<>();
// Advancement score sheet
addon.saveResource("advancements.yml", false);
advConfig = new YamlConfiguration();
@ -185,7 +182,7 @@ public class AdvancementsManager {
* @return score for advancement. 0 if the advancement was not added.
*/
public int addAdvancement(Player p, Advancement advancement) {
if (!addon.getOverWorld().equals(Util.getWorld(p.getWorld()))) {
if (!addon.inWorld(p.getWorld())) {
// Wrong world
return 0;
}
@ -240,7 +237,7 @@ public class AdvancementsManager {
/**
* Get the score for this advancement
* @param a - advancement
* @return score of advancement, or 0 if cannot be worked out
* @return score of advancement, or 0 if it cannot be worked out
*/
public int getScore(Advancement a) {
String adv = "advancements." + a.getKey().getKey();
@ -255,8 +252,7 @@ public class AdvancementsManager {
if (!a.getKey().getKey().contains("recipes") && a.getDisplay() != null) {
float x = a.getDisplay().getX();
float y = a.getDisplay().getY();
int score = (int) Math.round(Math.sqrt(x * x + y * y));
return score;
return (int) Math.round(Math.sqrt(x * x + y * y));
} else {
return 0;
}

View File

@ -1,7 +1,11 @@
package world.bentobox.boxed;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import org.bukkit.Bukkit;
import org.bukkit.Difficulty;
import org.bukkit.Material;
import org.bukkit.World;
@ -9,8 +13,8 @@ import org.bukkit.World.Environment;
import org.bukkit.WorldCreator;
import org.bukkit.entity.SpawnCategory;
import org.bukkit.generator.BiomeProvider;
import org.bukkit.generator.BlockPopulator;
import org.bukkit.generator.ChunkGenerator;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import world.bentobox.bentobox.api.addons.GameModeAddon;
@ -21,13 +25,14 @@ import world.bentobox.bentobox.api.configuration.WorldSettings;
import world.bentobox.bentobox.api.flags.Flag;
import world.bentobox.bentobox.api.flags.Flag.Mode;
import world.bentobox.bentobox.api.flags.Flag.Type;
import world.bentobox.bentobox.api.hooks.Hook;
import world.bentobox.bentobox.hooks.WorldManagementHook;
import world.bentobox.bentobox.managers.RanksManager;
import world.bentobox.boxed.commands.AdminPlaceStructureCommand;
import world.bentobox.boxed.generators.biomes.BoxedBiomeGenerator;
import world.bentobox.boxed.generators.biomes.NetherSeedBiomeGenerator;
import world.bentobox.boxed.generators.biomes.SeedBiomeGenerator;
import world.bentobox.boxed.generators.chunks.AbstractBoxedChunkGenerator;
import world.bentobox.boxed.generators.chunks.BoxedBlockPopulator;
import world.bentobox.boxed.generators.chunks.BoxedChunkGenerator;
import world.bentobox.boxed.generators.chunks.BoxedSeedChunkGenerator;
import world.bentobox.boxed.listeners.AdvancementListener;
@ -61,13 +66,10 @@ public class Boxed extends GameModeAddon {
private final Config<Settings> configObject = new Config<>(this, Settings.class);
private AdvancementsManager advManager;
private AbstractBoxedChunkGenerator netherChunkGenerator;
private World baseWorld;
private World baseWorldNether;
private World seedWorld;
private World seedWorldNether;
private final Map<World, ChunkGenerator> generatorMap = new HashMap<>();
//private World seedWorldEnd;
private BiomeProvider boxedBiomeProvider;
private BlockPopulator boxedBlockPopulator;
@Override
public void onLoad() {
@ -180,29 +182,32 @@ public class Boxed extends GameModeAddon {
log("Creating Boxed Seed Nether world ...");
// This creates a vanilla base world with biomes
AbstractBoxedChunkGenerator seedBaseGen = new BoxedSeedChunkGenerator(this, Environment.NETHER);
baseWorldNether = WorldCreator
.name(worldName + "/" + SEED+NETHER+BASE)
World baseWorldNether = WorldCreator
.name(worldName + "/" + SEED + NETHER + BASE)
.generator(seedBaseGen)
.environment(Environment.NETHER)
.seed(getSettings().getSeed())
.createWorld();
baseWorldNether.setDifficulty(Difficulty.PEACEFUL);
baseWorldNether.setSpawnLocation(settings.getSeedX(), 64, settings.getSeedZ());
generatorMap.put(baseWorldNether, seedBaseGen);
getPlugin().getIWM().addWorld(baseWorldNether, this);
copyChunks(baseWorldNether, seedBaseGen);
// Create seed world
// This copies a base world with custom biomes
log("Creating Boxed Biomed Nether world ...");
seedWorldNether = WorldCreator
.name(worldName + "/" + SEED+NETHER)
.generator(new BoxedSeedChunkGenerator(this, Environment.NETHER, new NetherSeedBiomeGenerator(this, seedBaseGen)))
BoxedSeedChunkGenerator seedWorldNetherGenerator = new BoxedSeedChunkGenerator(this, Environment.NETHER, new NetherSeedBiomeGenerator(this, seedBaseGen));
World seedWorldNether = WorldCreator
.name(worldName + "/" + SEED + NETHER)
.generator(seedWorldNetherGenerator)
.environment(Environment.NETHER)
.seed(getSettings().getSeed())
.createWorld();
seedWorldNether.setDifficulty(Difficulty.EASY);
seedWorldNether.setSpawnLocation(settings.getNetherSeedX(), 64, settings.getNetherSeedZ());
generatorMap.put(seedWorldNether, seedWorldNetherGenerator);
getPlugin().getIWM().addWorld(seedWorldNether, this);
copyChunks(seedWorldNether, netherChunkGenerator);
if (getServer().getWorld(worldName + NETHER) == null) {
@ -216,22 +221,24 @@ public class Boxed extends GameModeAddon {
log("Creating Boxed Seed world ...");
// This creates a vanilla base world with biomes
AbstractBoxedChunkGenerator seedBaseGen = new BoxedSeedChunkGenerator(this, Environment.NORMAL);
baseWorld = WorldCreator
.name(worldName + "/" + SEED+BASE)
World baseWorld = WorldCreator
.name(worldName + "/" + SEED + BASE)
.generator(seedBaseGen)
.environment(Environment.NORMAL)
.seed(getSettings().getSeed())
.createWorld();
baseWorld.setDifficulty(Difficulty.PEACEFUL);
baseWorld.setSpawnLocation(settings.getSeedX(), 64, settings.getSeedZ());
generatorMap.put(baseWorld, seedBaseGen);
getPlugin().getIWM().addWorld(baseWorld, this);
copyChunks(baseWorld, seedBaseGen);
// Create seed world
// This copies a base world with custom biomes
log("Creating Boxed Biomed world ...");
BoxedSeedChunkGenerator seedWorldGenerator = new BoxedSeedChunkGenerator(this, Environment.NORMAL, new SeedBiomeGenerator(this, seedBaseGen));
seedWorld = WorldCreator
.name(worldName + "/" + SEED)
.generator(new BoxedSeedChunkGenerator(this, Environment.NORMAL, new SeedBiomeGenerator(this, seedBaseGen)))
.generator(seedWorldGenerator)
.environment(Environment.NORMAL)
.seed(getSettings().getSeed())
.createWorld();
@ -239,6 +246,8 @@ public class Boxed extends GameModeAddon {
seedWorld.setSpawnLocation(settings.getSeedX(), 64, settings.getSeedZ());
generatorMap.put(seedWorld, seedWorldGenerator);
getPlugin().getIWM().addWorld(seedWorld, this);
copyChunks(seedWorld, chunkGenerator);
@ -251,9 +260,28 @@ public class Boxed extends GameModeAddon {
islandWorld = getWorld(worldName, World.Environment.NORMAL);
}
/**
* Registers a world with world management plugins
*
* @param world the World to register
*/
private void registerToWorldManagementPlugins(@NonNull World world) {
if (getPlugin().getHooks() != null) {
for (Hook hook : getPlugin().getHooks().getHooks()) {
if (hook instanceof final WorldManagementHook worldManagementHook) {
if (Bukkit.isPrimaryThread()) {
worldManagementHook.registerWorld(world, true);
} else {
Bukkit.getScheduler().runTask(getPlugin(), () -> worldManagementHook.registerWorld(world, true));
}
}
}
}
}
/**
* Copies chunks from the seed world so they can be pasted in the game world
* Copies chunks from the seed world, so they can be pasted in the game world
* @param world - source world
* @param gen - generator to store the chunks
*/
@ -307,10 +335,9 @@ public class Boxed extends GameModeAddon {
worldName2 = env.equals(World.Environment.NETHER) ? worldName2 + NETHER : worldName2;
worldName2 = env.equals(World.Environment.THE_END) ? worldName2 + THE_END : worldName2;
boxedBiomeProvider = new BoxedBiomeGenerator(this);
boxedBlockPopulator = new BoxedBlockPopulator(this);
World w = WorldCreator
.name(worldName2)
.generator(env.equals(World.Environment.NETHER) ? netherChunkGenerator : chunkGenerator)
.generator(getChunkGenerator(env))
.environment(env)
.seed(seedWorld.getSeed()) // For development
.createWorld();
@ -318,6 +345,8 @@ public class Boxed extends GameModeAddon {
if (w != null) {
setSpawnRates(w);
}
// Store main generators
generatorMap.put(w, getChunkGenerator(env));
return w;
}
@ -357,7 +386,12 @@ public class Boxed extends GameModeAddon {
@Override
public @Nullable ChunkGenerator getDefaultWorldGenerator(String worldName, String id) {
return worldName.endsWith(NETHER) ? netherChunkGenerator : chunkGenerator;
for (Entry<World, ChunkGenerator> en : generatorMap.entrySet()) {
if (en.getKey().getName().equalsIgnoreCase(worldName)) {
return en.getValue();
}
}
return null;
}
@Override
@ -374,6 +408,9 @@ public class Boxed extends GameModeAddon {
public void allLoaded() {
// Save settings. This will occur after all addons have loaded
this.saveWorldSettings();
// Register generators for worlds with multiverse etc.
this.log("Registering Boxed worlds with other plugins (if applicable)...");
generatorMap.keySet().forEach(this::registerToWorldManagementPlugins);
}
/**
@ -383,13 +420,6 @@ public class Boxed extends GameModeAddon {
return advManager;
}
/**
* @return the boxedBlockPopulator
*/
public BlockPopulator getBoxedBlockPopulator() {
return boxedBlockPopulator;
}
@Override
public boolean isUsesNewChunkGeneration() {
return true;

View File

@ -26,9 +26,10 @@ import world.bentobox.bentobox.api.user.User;
import world.bentobox.bentobox.util.Util;
import world.bentobox.boxed.Boxed;
import world.bentobox.boxed.listeners.NewAreaListener;
import world.bentobox.boxed.listeners.NewAreaListener.Item;
import world.bentobox.boxed.listeners.NewAreaListener.StructureRecord;
/**
* Enables admins to place templates in a Box and have them recorded for future boxes.
* @author tastybento
*
*/
@ -74,11 +75,11 @@ public class AdminPlaceStructureCommand extends CompositeCommand {
// Check world
if (!((Boxed)getAddon()).inWorld(getWorld())) {
user.sendMessage("boxed.commands.boxadmin.place.wrong-world");
return false;
return false;
}
/*
* Acceptable syntax with number of args:
* 1. place <structure>
* 1. place <structure>
* 4. place <structure> ~ ~ ~
* 5. place <structure> ~ ~ ~ ROTATION
* 6. place <structure> ~ ~ ~ ROTATION MIRROR
@ -90,7 +91,7 @@ public class AdminPlaceStructureCommand extends CompositeCommand {
return false;
}
// First arg must always be the structure name
List<String> options = Bukkit.getStructureManager().getStructures().keySet().stream().map(k -> k.getKey()).toList();
List<String> options = Bukkit.getStructureManager().getStructures().keySet().stream().map(NamespacedKey::getKey).toList();
if (!options.contains(args.get(0).toLowerCase(Locale.ENGLISH))) {
user.sendMessage("boxed.commands.boxadmin.place.unknown-structure");
return false;
@ -101,10 +102,10 @@ public class AdminPlaceStructureCommand extends CompositeCommand {
}
// Next come the coordinates - there must be at least 3 of them
if ((!args.get(1).equals("~") && !Util.isInteger(args.get(1), true))
|| (!args.get(2).equals("~") && !Util.isInteger(args.get(2), true))
|| (!args.get(2).equals("~") && !Util.isInteger(args.get(2), true))
|| (!args.get(3).equals("~") && !Util.isInteger(args.get(3), true))) {
user.sendMessage("boxed.commands.boxadmin.place.use-integers");
return false;
return false;
}
// If that is all we have, we're done
if (args.size() == 4) {
@ -115,7 +116,7 @@ public class AdminPlaceStructureCommand extends CompositeCommand {
if (sr == null) {
user.sendMessage("boxed.commands.boxadmin.place.unknown-rotation");
Arrays.stream(StructureRotation.values()).map(StructureRotation::name).forEach(user::sendRawMessage);
return false;
return false;
}
if (args.size() == 5) {
return true;
@ -125,11 +126,11 @@ public class AdminPlaceStructureCommand extends CompositeCommand {
if (mirror == null) {
user.sendMessage("boxed.commands.boxadmin.place.unknown-mirror");
Arrays.stream(Mirror.values()).map(Mirror::name).forEach(user::sendRawMessage);
return false;
return false;
}
if (args.size() == 7) {
if (args.get(6).toUpperCase(Locale.ENGLISH).equals("NO_MOBS")) {
noMobs = true;
noMobs = true;
} else {
user.sendMessage("boxed.commands.boxadmin.place.unknown", TextVariables.LABEL, args.get(6).toUpperCase(Locale.ENGLISH));
return false;
@ -143,13 +144,13 @@ public class AdminPlaceStructureCommand extends CompositeCommand {
public boolean execute(User user, String label, List<String> args) {
NamespacedKey tag = NamespacedKey.fromString(args.get(0).toLowerCase(Locale.ENGLISH));
Structure s = Bukkit.getStructureManager().loadStructure(tag);
int x = args.size() == 1 || args.get(1).equals("~") ? user.getLocation().getBlockX() : Integer.valueOf(args.get(1).trim());
int y = args.size() == 1 || args.get(2).equals("~") ? user.getLocation().getBlockY() : Integer.valueOf(args.get(2).trim());
int z = args.size() == 1 || args.get(3).equals("~") ? user.getLocation().getBlockZ() : Integer.valueOf(args.get(3).trim());
int x = args.size() == 1 || args.get(1).equals("~") ? user.getLocation().getBlockX() : Integer.parseInt(args.get(1).trim());
int y = args.size() == 1 || args.get(2).equals("~") ? user.getLocation().getBlockY() : Integer.parseInt(args.get(2).trim());
int z = args.size() == 1 || args.get(3).equals("~") ? user.getLocation().getBlockZ() : Integer.parseInt(args.get(3).trim());
Location spot = new Location(user.getWorld(), x, y, z);
s.place(spot, true, sr, mirror, PALETTE, INTEGRITY, new Random());
NewAreaListener.removeJigsaw(new Item(tag.getKey(), s, spot, sr, mirror, noMobs));
boolean result = saveStructure(spot, tag, user, sr, mirror);
NewAreaListener.removeJigsaw(new StructureRecord(tag.getKey(), s, spot, sr, mirror, noMobs));
boolean result = saveStructure(spot, tag, user, sr, mirror);
if (result) {
user.sendMessage("boxed.commands.boxadmin.place.saved");
} else {
@ -167,14 +168,14 @@ public class AdminPlaceStructureCommand extends CompositeCommand {
try {
config.load(structures);
StringBuilder v = new StringBuilder();
v.append(tag.getKey() + "," + sr2.name() + "," + mirror2.name());
v.append(tag.getKey()).append(",").append(sr2.name()).append(",").append(mirror2.name());
if (noMobs) {
v.append(" NO_MOBS");
}
config.set(spot.getWorld().getEnvironment().name().toLowerCase(Locale.ENGLISH) + "." + xx + "," + spot.getBlockY() + "," + zz, v.toString());
config.save(structures);
} catch (IOException | InvalidConfigurationException e) {
// TODO Auto-generated catch block
// TODO Auto-generated catch block
e.printStackTrace();
return false;
}
@ -188,7 +189,7 @@ public class AdminPlaceStructureCommand extends CompositeCommand {
{
String lastArg = !args.isEmpty() ? args.get(args.size() - 1) : "";
if (args.size() == 2) {
return Optional.of(Util.tabLimit(Bukkit.getStructureManager().getStructures().keySet().stream().map(k -> k.getKey()).toList(), lastArg));
return Optional.of(Util.tabLimit(Bukkit.getStructureManager().getStructures().keySet().stream().map(NamespacedKey::getKey).toList(), lastArg));
} else if (args.size() == 3) {
return Optional.of(List.of(String.valueOf(user.getLocation().getBlockX()), "~"));
} else if (args.size() == 4) {

View File

@ -14,8 +14,8 @@ import com.google.common.base.Enums;
import world.bentobox.bentobox.BentoBox;
import world.bentobox.boxed.Boxed;
import world.bentobox.boxed.generators.chunks.AbstractBoxedChunkGenerator;
import world.bentobox.boxed.generators.chunks.AbstractBoxedChunkGenerator.ChunkStore;
import world.bentobox.boxed.generators.chunks.BoxedChunkGenerator;
/**
* Copies biomes from seed world
@ -39,15 +39,14 @@ public abstract class AbstractCopyBiomeProvider extends BiomeProvider {
public Biome getBiome(WorldInfo worldInfo, int x, int y, int z) {
int chunkX = x >> 4;
int chunkZ = z >> 4;
chunkX = BoxedChunkGenerator.repeatCalc(chunkX);
chunkZ = BoxedChunkGenerator.repeatCalc(chunkZ);
chunkX = AbstractBoxedChunkGenerator.repeatCalc(chunkX);
chunkZ = AbstractBoxedChunkGenerator.repeatCalc(chunkZ);
@Nullable ChunkStore c = addon.getChunkGenerator(worldInfo.getEnvironment()).getChunk(chunkX, chunkZ);
if (c != null) {
int xx = Math.floorMod(x, 16);
int zz = Math.floorMod(z, 16);
Biome biome = c.chunkBiomes().getOrDefault(new Vector(xx, y, zz), defaultBiome);
return biome;
return c.chunkBiomes().getOrDefault(new Vector(xx, y, zz), defaultBiome);
} else {
BentoBox.getInstance().logWarning("Snapshot at " + chunkX + " " + chunkZ + " is not stored");
return defaultBiome;

View File

@ -26,7 +26,6 @@ import com.google.common.base.Enums;
import world.bentobox.boxed.Boxed;
import world.bentobox.boxed.generators.chunks.AbstractBoxedChunkGenerator;
import world.bentobox.boxed.generators.chunks.AbstractBoxedChunkGenerator.ChunkStore;
import world.bentobox.boxed.generators.chunks.BoxedChunkGenerator;
/**
* Generates the biomes for the seed world. A seed world is the template for the chunks that
@ -115,8 +114,8 @@ public abstract class AbstractSeedBiomeProvider extends BiomeProvider {
@NonNull
private Biome getVanillaBiome(WorldInfo worldInfo, int x, int y, int z) {
// Get the chunk coordinates
int chunkX = BoxedChunkGenerator.repeatCalc(x >> 4);
int chunkZ = BoxedChunkGenerator.repeatCalc(z >> 4);
int chunkX = AbstractBoxedChunkGenerator.repeatCalc(x >> 4);
int chunkZ = AbstractBoxedChunkGenerator.repeatCalc(z >> 4);
// Get the stored snapshot
ChunkStore snapshot = this.seedGen.getChunk(chunkX, chunkZ);
if (snapshot == null) {
@ -200,7 +199,7 @@ public abstract class AbstractSeedBiomeProvider extends BiomeProvider {
* Loads the custom biomes from the config file
* @param config - Yaml configuration object
* @param sector - the direction section to load
* @return
* @return sorted map of the biomes and their probabilities as keys
*/
private SortedMap<Double, Biome> loadQuad(YamlConfiguration config, String sector) {
SortedMap<Double, Biome> result = new TreeMap<>();

View File

@ -6,6 +6,7 @@ import org.bukkit.block.Biome;
import world.bentobox.boxed.Boxed;
/**
* Generator for the over world
* @author tastybento
*
*/

View File

@ -6,6 +6,7 @@ import org.bukkit.block.Biome;
import world.bentobox.boxed.Boxed;
/**
* Generator for the nether world
* @author tastybento
*
*/

View File

@ -7,6 +7,7 @@ import world.bentobox.boxed.Boxed;
import world.bentobox.boxed.generators.chunks.AbstractBoxedChunkGenerator;
/**
* Generator for the Nether seed world
* @author tastybento
*
*/

View File

@ -7,6 +7,7 @@ import world.bentobox.boxed.Boxed;
import world.bentobox.boxed.generators.chunks.AbstractBoxedChunkGenerator;
/**
* Generator for the seed world
* @author tastybento
*
*/

View File

@ -1 +1,5 @@
/**
* Generators to make the worlds with the custom biomes
* @author tastybento
*/
package world.bentobox.boxed.generators.biomes;

View File

@ -26,10 +26,12 @@ public abstract class AbstractBoxedChunkGenerator extends ChunkGenerator {
protected final Boxed addon;
protected static int size;
protected Map<Pair<Integer, Integer>, ChunkStore> chunks = new HashMap<>();
public record ChunkStore(ChunkSnapshot snapshot, List<EntityData> bpEnts, List<ChestData> chests, Map<Vector, Biome> chunkBiomes) {};
public record EntityData(Vector relativeLoc, BlueprintEntity entity) {};
public record ChestData(Vector relativeLoc, BlueprintBlock chest) {};
protected final Map<Pair<Integer, Integer>, ChunkStore> chunks = new HashMap<>();
public record ChunkStore(ChunkSnapshot snapshot, List<EntityData> bpEnts, List<ChestData> chests, Map<Vector, Biome> chunkBiomes) {}
public record EntityData(Vector relativeLoc, BlueprintEntity entity) {}
public record ChestData(Vector relativeLoc, BlueprintBlock chest) {}
//private final WorldRef wordRefNether;

View File

@ -31,10 +31,10 @@ import world.bentobox.boxed.generators.chunks.AbstractBoxedChunkGenerator.ChunkS
*/
public class BoxedBlockPopulator extends BlockPopulator {
private Boxed addon;
private final Boxed addon;
/**
* @param addon
* @param addon Boxed
*/
public BoxedBlockPopulator(Boxed addon) {
this.addon = addon;
@ -82,7 +82,7 @@ public class BoxedBlockPopulator extends BlockPopulator {
/**
* Handles signs, chests and mob spawner blocks
*
* @param block - block
* @param bs - block state
* @param bpBlock - config
*/
public void setBlockState(BlockState bs, BlueprintBlock bpBlock) {

View File

@ -45,9 +45,12 @@ import world.bentobox.boxed.Boxed;
*
*/
public class BoxedChunkGenerator extends AbstractBoxedChunkGenerator {
private final BlockPopulator boxedBlockPopulator;
public BoxedChunkGenerator(Boxed addon) {
super(addon);
boxedBlockPopulator = new BoxedBlockPopulator(addon);
}
@Override
@ -57,7 +60,10 @@ public class BoxedChunkGenerator extends AbstractBoxedChunkGenerator {
@Override
public List<BlockPopulator> getDefaultPopulators(World world) {
world.getPopulators().add(addon.getBoxedBlockPopulator());
// Only add it once
if (!world.getPopulators().contains(boxedBlockPopulator)) {
world.getPopulators().add(boxedBlockPopulator);
}
return world.getPopulators();
}

View File

@ -1,4 +1,5 @@
/**
* Generators and populators to make the base world
* @author tastybento
*
*/

View File

@ -36,7 +36,6 @@ import world.bentobox.bentobox.api.events.team.TeamLeaveEvent;
import world.bentobox.bentobox.api.localization.TextVariables;
import world.bentobox.bentobox.api.user.User;
import world.bentobox.bentobox.database.objects.Island;
import world.bentobox.bentobox.managers.RanksManager;
import world.bentobox.bentobox.util.Util;
import world.bentobox.boxed.Boxed;
@ -66,22 +65,31 @@ public class AdvancementListener implements Listener {
}
public static Advancement getAdvancement(String string) {
/**
* Get Advancement given the namespaced key for it
* @param key namespaced key name for Advancement
* @return Advancement or null if none found
*/
public static Advancement getAdvancement(String key) {
return StreamSupport.stream(
Spliterators.spliteratorUnknownSize(Bukkit.advancementIterator(), Spliterator.ORDERED), false)
.filter(a -> a.getKey().toString().equals(string))
.filter(a -> a.getKey().toString().equals(key))
.findFirst().orElse(null);
}
/**
* Awards a bigger box when an advancement is done. Removes advancements if they are not valid.
* @param e PlayerAdvancementDoneEvent
*/
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onAdvancement(PlayerAdvancementDoneEvent e) {
// Ignore if player is not in survival
if (!e.getPlayer().getGameMode().equals(GameMode.SURVIVAL)) {
return;
}
if (Util.sameWorld(e.getPlayer().getWorld(), addon.getOverWorld())) {
// Check if player is in the Boxed worlds
if (addon.inWorld(e.getPlayer().getWorld())) {
// Only allow members or higher to get advancements in a box
if (addon.getSettings().isDenyVisitorAdvancements() && !addon.getIslands().getIslandAt(e.getPlayer().getLocation()).map(i -> i.getMemberSet().contains(e.getPlayer().getUniqueId())).orElse(false)) {
// Remove advancement from player
@ -93,13 +101,12 @@ public class AdvancementListener implements Listener {
}
return;
}
// Add the advancement to the island
int score = addon.getAdvManager().addAdvancement(e.getPlayer(), e.getAdvancement());
// Tell other team players one tick after it occurs if it is something that has a score
if (score != 0) {
User user = User.getInstance(e.getPlayer());
if (user != null) {
Bukkit.getScheduler().runTask(addon.getPlugin(), () -> tellTeam(user, e.getAdvancement().getKey(), score));
}
Bukkit.getScheduler().runTask(addon.getPlugin(), () -> tellTeam(user, e.getAdvancement().getKey(), score));
}
}
}
@ -107,11 +114,10 @@ public class AdvancementListener implements Listener {
private void tellTeam(User user, NamespacedKey key, int score) {
Island island = addon.getIslands().getIsland(addon.getOverWorld(), user);
if (island == null) {
// Something went wrong here
return;
}
island.getMemberSet(RanksManager.MEMBER_RANK).stream()
.map(User::getInstance)
.filter(User::isOnline)
island.getMemberSet().stream().map(User::getInstance).filter(User::isOnline)
.forEach(u -> {
informPlayer(u, key, score);
// Sync
@ -126,15 +132,15 @@ public class AdvancementListener implements Listener {
}
/**
* Synchronize the player's advancements to that of the island.
* Player's advancements should be cleared before calling this otherwise they will get add the island ones as well.
* Synchronize the player's advancements to that of the box.
* Player's advancements should be cleared before calling this otherwise they will get add the box ones as well.
* @param user - user
*/
public void syncAdvancements(User user) {
Island island = addon.getIslands().getIsland(addon.getOverWorld(), user);
if (island != null) {
grantAdv(user, addon.getAdvManager().getIsland(island).getAdvancements());
int diff = addon.getAdvManager().checkIslandSize(island);
Island box = addon.getIslands().getIsland(addon.getOverWorld(), user);
if (box != null) {
grantAdv(user, addon.getAdvManager().getIsland(box).getAdvancements());
int diff = addon.getAdvManager().checkIslandSize(box);
if (diff > 0) {
user.sendMessage("boxed.size-changed", TextVariables.NUMBER, String.valueOf(diff));
user.getPlayer().playSound(Objects.requireNonNull(user.getLocation()), Sound.ENTITY_PLAYER_LEVELUP, 1F, 2F);
@ -161,9 +167,14 @@ public class AdvancementListener implements Listener {
}
/**
* Special case Advancement awarding
* Awards the nether and end advancements when they use a portal for the first time.
* @param e PlayerPortalEvent
*/
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPortal(PlayerPortalEvent e) {
if (!Util.sameWorld(e.getPlayer().getWorld(), addon.getOverWorld()) || !e.getPlayer().getGameMode().equals(GameMode.SURVIVAL)) {
if (!addon.inWorld(e.getPlayer().getWorld()) || !e.getPlayer().getGameMode().equals(GameMode.SURVIVAL)) {
return;
}
if (e.getCause().equals(TeleportCause.NETHER_PORTAL)) {
@ -177,47 +188,50 @@ public class AdvancementListener implements Listener {
}
/**
* Special case Advancement awarding
* Looks for certain blocks, and if they are found then awards an advancement
* @param e - PlayerMoveEvent
*/
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onMove(PlayerMoveEvent e) {
if (!Util.sameWorld(e.getPlayer().getWorld(), addon.getNetherWorld())) {
if (!addon.getSettings().isNetherGenerate() || !Util.sameWorld(e.getPlayer().getWorld(), addon.getNetherWorld())) {
return;
}
// Nether fortress advancement
if (e.getTo().getBlock().getRelative(BlockFace.DOWN).getType().equals(Material.NETHER_BRICKS)) {
giveAdv(e.getPlayer(), netherFortressAdvancement);
}
}
/**
* Give player an advancement
* @param player - player
* @param adv - Advancement
*/
public static void giveAdv(Player player, Advancement adv) {
//BentoBox.getInstance().logDebug("Give Adv " + adv.getKey() + " done status " + player.getAdvancementProgress(adv).isDone());
if (adv != null && !player.getAdvancementProgress(adv).isDone()) {
adv.getCriteria().forEach(player.getAdvancementProgress(adv)::awardCriteria);
}
}
/**
* Sync advancements when player joins server if they are in the Boxed world
* @param e PlayerJoinEvent
*/
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onPlayerJoin(PlayerJoinEvent e) {
/*
StreamSupport.stream(
Spliterators.spliteratorUnknownSize(Bukkit.advancementIterator(), Spliterator.ORDERED), false).forEach(a-> {
AdvancementProgress progress = e.getPlayer().getAdvancementProgress(a);
BentoBox.getInstance().logDebug(a.getKey() + " " + progress.isDone());
BentoBox.getInstance().logDebug("Awarded");
progress.getAwardedCriteria().forEach(c -> BentoBox.getInstance().logDebug(c));
BentoBox.getInstance().logDebug("Remaining");
progress.getRemainingCriteria().forEach(c -> BentoBox.getInstance().logDebug(c));
});
*/
User user = User.getInstance(e.getPlayer());
if (Util.sameWorld(addon.getOverWorld(), e.getPlayer().getWorld())) {
if (addon.inWorld(e.getPlayer().getWorld())) {
// Set advancements to same as island
syncAdvancements(user);
}
}
/**
* Sync advancements when player enters the Boxed world
* @param e PlayerChangedWorldEvent
*/
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onPlayerEnterWorld(PlayerChangedWorldEvent e) {
User user = User.getInstance(e.getPlayer());
@ -227,6 +241,10 @@ public class AdvancementListener implements Listener {
}
}
/**
* Clear and sync advancements for a player when they join a team if the settings require it
* @param e TeamJoinedEvent
*/
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onTeamJoinTime(TeamJoinedEvent e) {
User user = User.getInstance(e.getPlayerUUID());
@ -239,6 +257,10 @@ public class AdvancementListener implements Listener {
}
}
/**
* Clear player's advancements when they leave a team if the setting requires it
* @param e TeamLeaveEvent
*/
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onTeamLeaveTime(TeamLeaveEvent e) {
User user = User.getInstance(e.getPlayerUUID());
@ -250,6 +272,10 @@ public class AdvancementListener implements Listener {
}
}
/**
* Clear player's advancements when they start an island for the first time.
* @param e IslandNewIslandEvent
*/
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onFirstTime(IslandNewIslandEvent e) {
User user = User.getInstance(e.getPlayerUUID());
@ -259,6 +285,12 @@ public class AdvancementListener implements Listener {
}
/**
* Clear and set advancements for user. Will not do anything if the user is offline
* @param user - user
* @param clear - whether to clear advacements for this user or not
* @param list - list of advacements (namespaced keys) to grant to user
*/
private void clearAndSetAdv(User user, boolean clear, List<String> list) {
if (!user.isOnline()) {
return;
@ -288,10 +320,8 @@ public class AdvancementListener implements Listener {
}
}
private void clearAdv(User user) {
// Clear stats
// Statistics
// Clear Statistics
Arrays.stream(Statistic.values()).forEach(s -> resetStats(user, s));
// Clear advancements
Iterator<Advancement> it = Bukkit.advancementIterator();
@ -303,36 +333,12 @@ public class AdvancementListener implements Listener {
}
@SuppressWarnings("deprecation")
private void resetStats(User user, Statistic s) {
switch(s.getType()) {
case BLOCK:
for (Material m: Material.values()) {
if (m.isBlock() && !m.isLegacy()) {
user.getPlayer().setStatistic(s, m, 0);
}
}
break;
case ITEM:
for (Material m: Material.values()) {
if (m.isItem() && !m.isLegacy()) {
user.getPlayer().setStatistic(s, m, 0);
}
}
break;
case ENTITY:
for (EntityType en: EntityType.values()) {
if (en.isAlive()) {
user.getPlayer().setStatistic(s, en, 0);
}
}
break;
case UNTYPED:
user.getPlayer().setStatistic(s, 0);
break;
default:
break;
case BLOCK -> Arrays.stream(Material.values()).filter(Material::isBlock).forEach(m -> user.getPlayer().setStatistic(s, m, 0));
case ITEM -> Arrays.stream(Material.values()).filter(Material::isItem).forEach(m -> user.getPlayer().setStatistic(s, m, 0));
case ENTITY -> Arrays.stream(EntityType.values()).filter(EntityType::isAlive).forEach(m -> user.getPlayer().setStatistic(s, m, 0));
case UNTYPED -> user.getPlayer().setStatistic(s, 0);
}
}

View File

@ -47,13 +47,12 @@ public class EnderPearlListener implements Listener {
}
User u = User.getInstance(e.getPlayer());
// If the to is outside the box, cancel it
// If the to-location is outside the box, cancel it
if (e.getTo() != null) {
Island i = addon.getIslands().getIsland(e.getFrom().getWorld(), u);
if (i == null || !i.onIsland(e.getTo())) {
u.sendMessage("boxed.general.errors.no-teleport-outside");
e.setCancelled(true);
return;
}
}
}
@ -92,19 +91,16 @@ public class EnderPearlListener implements Listener {
// Moving is allowed
moveBox(u, fromIsland, l);
Util.teleportAsync(player, l);
return;
}
} else {
// Different box. This is never allowed. Cancel the throw
e.setCancelled(true);
u.sendMessage("boxed.general.errors.no-teleport-outside");
return;
}
}, () -> {
// No box. This is never allowed. Cancel the throw
e.setCancelled(true);
u.sendMessage("boxed.general.errors.no-teleport-outside");
return;
});
});

View File

@ -3,13 +3,7 @@ package world.bentobox.boxed.listeners;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Queue;
import java.util.Random;
import java.util.*;
import org.bukkit.Bukkit;
import org.bukkit.Location;
@ -26,7 +20,7 @@ import org.bukkit.block.structure.Mirror;
import org.bukkit.block.structure.StructureRotation;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.craftbukkit.v1_19_R3.CraftWorld;
import org.bukkit.craftbukkit.v1_20_R1.CraftWorld;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
@ -65,6 +59,19 @@ import world.bentobox.boxed.objects.IslandStructures;
*/
public class NewAreaListener implements Listener {
/**
* Structure record contains the name of the structure, the structure itself, where it was placed and
* enums for rotation, mirror, and a flag to paste mobs or not.
* @param name - name of structure
* @param structure - Structure object
* @param location - location where it has been placed
* @param rot - rotation
* @param mirror - mirror setting
* @param noMobs - if false, mobs not pasted
*/
public record StructureRecord(String name, Structure structure, Location location, StructureRotation rot, Mirror mirror, Boolean noMobs) {}
private static final Map<Integer, EntityType> BUTCHER_ANIMALS = Map.of(0, EntityType.COW, 1, EntityType.SHEEP, 2, EntityType.PIG);
private static final List<BlockFace> CARDINALS = List.of(BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST);
private static final List<String> JAR_STRUCTURES = List.of("bee", "pillager", "polar_bear", "axolotl", "allay", "parrot", "frog");
private static final List<String> STRUCTURES = List.of("ancient_city", "bastion_remnant", "bastion",
@ -76,17 +83,16 @@ public class NewAreaListener implements Listener {
"shipwreck", "stronghold", "swamp_hut", "village_desert", "village_plains",
"village_savanna", "village_snowy", "village_taiga");
private final Boxed addon;
private File structureFile;
private Queue<Item> itemsToBuild = new LinkedList<>();
private static Random rand = new Random();
private final File structureFile;
private final Queue<StructureRecord> itemsToBuild = new LinkedList<>();
private static final Random rand = new Random();
private boolean pasting;
private static Gson gson = new Gson();
public record Item(String name, Structure structure, Location location, StructureRotation rot, Mirror mirror, Boolean noMobs) {};
Pair<Integer, Integer> min = new Pair<Integer, Integer>(0,0);
Pair<Integer, Integer> max = new Pair<Integer, Integer>(0,0);
private static final Gson gson = new Gson();
Pair<Integer, Integer> min = new Pair<>(0, 0);
Pair<Integer, Integer> max = new Pair<>(0, 0);
// Database handler for structure data
private final Database<IslandStructures> handler;
private Map<String, IslandStructures> islandStructureCache = new HashMap<>();
private final Map<String, IslandStructures> islandStructureCache = new HashMap<>();
@ -95,14 +101,18 @@ public class NewAreaListener implements Listener {
*/
public NewAreaListener(Boxed addon) {
this.addon = addon;
// Save the default structures file from the jar
addon.saveResource("structures.yml", false);
// Load the config
structureFile = new File(addon.getDataFolder(), "structures.yml");
// Get database ready
handler = new Database<>(addon, IslandStructures.class);
// Try to build something every second
Bukkit.getScheduler().runTaskTimer(addon.getPlugin(), () -> BuildItem(), 20, 20);
// Experiment: TODO: read all files in from the structure folder including the ones saved from the jar file
runStructurePrinter(addon);
}
private void runStructurePrinter(Boxed addon2) {
Bukkit.getScheduler().runTaskTimer(addon.getPlugin(), this::buildStructure, 20, 20);
for (String js : JAR_STRUCTURES) {
addon.saveResource("structures/" + js + ".nbt", false);
File structureFile = new File(addon.getDataFolder(), "structures/" + js + ".nbt");
@ -112,24 +122,29 @@ public class NewAreaListener implements Listener {
addon.log("Loaded " + js + ".nbt");
} catch (IOException e) {
addon.logError("Error trying to load " + structureFile.getAbsolutePath());
e.printStackTrace();
addon.getPlugin().logStacktrace(e);
}
}
}
private void BuildItem() {
/**
* Build something in the queue
*/
private void buildStructure() {
// Only kick off a build if there is something to build and something isn't already being built
if (!pasting && !itemsToBuild.isEmpty()) {
// Build item
Item item = itemsToBuild.poll();
LoadChunksAsync(item);
StructureRecord item = itemsToBuild.poll();
placeStructure(item);
}
}
/**
* Build a list of structures
* @param event event
* Load known structures from the templates file.
* This makes them available for admins to use in the boxed place file.
* If this is not done, then no structures (templates) will be available.
* @param event BentoBoxReadyEvent
*/
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onBentoBoxReady(BentoBoxReadyEvent event) {
@ -147,6 +162,10 @@ public class NewAreaListener implements Listener {
}
/**
* Track if a place has entered a structure.
* @param e PlayerMoveEvent
*/
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onPlayerMove(PlayerMoveEvent e) {
// Ignore head movements
@ -195,6 +214,11 @@ public class NewAreaListener implements Listener {
}
/**
* Get all the known island structures for this island
* @param islandId - island ID
* @return IslandStructures
*/
private IslandStructures getIslandStructData(String islandId) {
// Return from cache if it exists
if (islandStructureCache.containsKey(islandId)) {
@ -229,13 +253,13 @@ public class NewAreaListener implements Listener {
if (e == null) {
addon.logError("Error in structures.yml - unknown environment " + env);
} else {
place("structure",config.getConfigurationSection(env), center, e);
place(config.getConfigurationSection(env), center, e);
}
}
}
private void place(String string, ConfigurationSection section, Location center, Environment env) {
private void place(ConfigurationSection section, Location center, Environment env) {
World world = env.equals(Environment.NORMAL) ? addon.getOverWorld() : addon.getNetherWorld();
// Loop through the structures in the file - there could be more than one
for (String vector : section.getKeys(false)) {
@ -266,24 +290,26 @@ public class NewAreaListener implements Listener {
// Extract coords
String[] value = vector.split(",");
if (value.length > 2) {
int x = Integer.valueOf(value[0].strip()) + center.getBlockX();
int y = Integer.valueOf(value[1].strip());
int z = Integer.valueOf(value[2].strip()) + center.getBlockZ();
int x = Integer.parseInt(value[0].strip()) + center.getBlockX();
int y = Integer.parseInt(value[1].strip());
int z = Integer.parseInt(value[2].strip()) + center.getBlockZ();
Location l = new Location(world, x, y, z);
itemsToBuild.add(new Item(name, s, l, rot, mirror, noMobs));
itemsToBuild.add(new StructureRecord(name, s, l, rot, mirror, noMobs));
} else {
addon.logError("Structure file syntax error: " + vector + ": " + value);
addon.logError("Structure file syntax error: " + vector + ": " + Arrays.toString(value));
}
}
}
private void LoadChunksAsync(Item item) {
private void placeStructure(StructureRecord item) {
// Set the semaphore - only paste one at a time
pasting = true;
// Place the structure
item.structure().place(item.location(), true, item.rot(), item.mirror(), -1, 1, rand);
addon.log(item.name() + " placed at " + item.location().getWorld().getName() + " " + Util.xyz(item.location().toVector()));
// Find it
// Remove any jigsaw artifacts
BoundingBox bb = removeJigsaw(item);
// Store it
// Store it for future reference
addon.getIslands().getIslandAt(item.location()).map(Island::getUniqueId).ifPresent(id -> {
addon.log("Saved " + item.name());
if (item.location().getWorld().getEnvironment().equals(Environment.NETHER)) {
@ -293,7 +319,7 @@ public class NewAreaListener implements Listener {
}
handler.saveObjectAsync(getIslandStructData(id));
});
// Clear the semaphore
pasting = false;
}
@ -302,7 +328,7 @@ public class NewAreaListener implements Listener {
* @param item - record of what's required
* @return the resulting bounding box of the structure
*/
public static BoundingBox removeJigsaw(Item item) {
public static BoundingBox removeJigsaw(StructureRecord item) {
Location loc = item.location();
Structure structure = item.structure();
StructureRotation structureRotation = item.rot();
@ -342,8 +368,15 @@ public class NewAreaListener implements Listener {
}
/**
* Process a structure block. Sets it to a structure void at a minimum.
* If the structure block has metadata indicating it is a chest, then it will fill
* the chest with a buried treasure loot. If it is waterlogged, then it will change
* the void to water.
* @param b structure block
*/
private static void processStructureBlock(Block b) {
// I would like to read the data from the block an do something with it!
// I would like to read the data from the block and do something with it!
String data = nmsData(b);
BoxedStructureBlock bsb = gson.fromJson(data, BoxedStructureBlock.class);
b.setType(Material.STRUCTURE_VOID);
@ -365,7 +398,6 @@ public class NewAreaListener implements Listener {
}
}
private static final Map<Integer, EntityType> BUTCHER_ANIMALS = Map.of(0, EntityType.COW, 1, EntityType.SHEEP, 2, EntityType.PIG);
private static void processJigsaw(Block b, StructureRotation structureRotation, boolean pasteMobs) {
String data = nmsData(b);
BoxedJigsawBlock bjb = gson.fromJson(data, BoxedJigsawBlock.class);
@ -390,9 +422,8 @@ public class NewAreaListener implements Listener {
case "minecraft:village/common/pigs" -> EntityType.PIG;
case "minecraft:village/common/cows" -> EntityType.COW;
case "minecraft:village/common/iron_golem" -> EntityType.IRON_GOLEM;
case "minecraft:village/common/butcher_animals" -> BUTCHER_ANIMALS.get(rand.nextInt(3));
case "minecraft:village/common/animals" -> BUTCHER_ANIMALS.get(rand.nextInt(3));
default -> null;
case "minecraft:village/common/butcher_animals", "minecraft:village/common/animals" -> BUTCHER_ANIMALS.get(rand.nextInt(3));
default -> null;
};
// Boxed
if (type == null && bjb.getPool().startsWith("minecraft:boxed/")) {

View File

@ -2,8 +2,13 @@ package world.bentobox.boxed.objects;
import com.google.gson.annotations.Expose;
/**
* This replicates a jigsaw block.
* @author tastybento
*
*/
public class BoxedJigsawBlock {
// final_state:"minecraft:polished_blackstone_bricks",joint:"aligned",name:"minecraft:empty",pool:"minecraft:bastion/bridge/legs",target:"minecraft:leg_connector"
// final_state:"minecraft:polished_blackstone_bricks",joint:"aligned",name:"minecraft:empty",pool:"minecraft:bastion/bridge/legs",target:"minecraft:leg_connector"
@Expose
private String final_state;
@Expose

View File

@ -7,11 +7,12 @@ import org.bukkit.block.structure.StructureRotation;
import com.google.gson.annotations.Expose;
/**
* This represents a Structure Block
* @author tastybento
*
*/
public class BoxedStructureBlock {
//{author:"LadyAgnes",ignoreEntities:1b,integrity:1.0f,metadata:"drowned",mirror:"NONE",mode:"DATA",name:"",posX:0,posY:1,posZ:0,powered:0b,rotation:"NONE",seed:0L,showair:0b
//{author:"LadyAgnes",ignoreEntities:1b,integrity:1.0f,metadata:"drowned",mirror:"NONE",mode:"DATA",name:"",posX:0,posY:1,posZ:0,powered:0b,rotation:"NONE",seed:0L,showair:0b
//,showboundingbox:1b,sizeX:0,sizeY:0,sizeZ:0}
@Expose
private String author;
@ -168,7 +169,7 @@ public class BoxedStructureBlock {
+ seed + ", showair=" + showair + ", showboundingbox=" + showboundingbox + ", sizeX=" + sizeX
+ ", sizeY=" + sizeY + ", sizeZ=" + sizeZ + "]";
}
}

View File

@ -10,6 +10,13 @@ import com.google.gson.annotations.Expose;
import world.bentobox.bentobox.database.objects.DataObject;
import world.bentobox.bentobox.database.objects.Table;
/**
* Stores all the structures placed in the box when it is made.
* These are used later to identify when a player enters such a structure and
* trigger an Advancement
* @author tastybento
*
*/
@Table(name = "IslandStructures")
public class IslandStructures implements DataObject {

View File

@ -1,7 +1,7 @@
name: Boxed
main: world.bentobox.boxed.Boxed
version: ${version}${build.number}
api-version: 1.22
api-version: 1.24
metrics: true
icon: "COMPOSTER"
repository: "BentoBoxWorld/Boxed"

View File

@ -9,11 +9,11 @@ boxed:
admin: boxadmin
# The default action for new player command call.
# Sub-command of main player command that will be run on first player command call.
# By default it is sub-command 'create'.
# By default, it is sub-command 'create'.
new-player-action: create
# The default action for player command.
# Sub-command of main player command that will be run on each player command call.
# By default it is sub-command 'go'.
# By default, it is sub-command 'go'.
default-action: go
# Announce advancements. We recommend you set the game rule `/gamerule announceAdvancements false`
# but that blocks all new advancement announcements. This setting tells Boxed to broadcast new advancements.

View File

@ -0,0 +1,339 @@
package world.bentobox.boxed;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.beans.IntrospectionException;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import org.bukkit.Bukkit;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.advancement.Advancement;
import org.bukkit.advancement.AdvancementDisplay;
import org.bukkit.entity.Player;
import org.eclipse.jdt.annotation.NonNull;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.reflect.Whitebox;
import world.bentobox.bentobox.BentoBox;
import world.bentobox.bentobox.Settings;
import world.bentobox.bentobox.api.user.User;
import world.bentobox.bentobox.database.AbstractDatabaseHandler;
import world.bentobox.bentobox.database.DatabaseSetup;
import world.bentobox.bentobox.database.DatabaseSetup.DatabaseType;
import world.bentobox.bentobox.database.objects.Island;
import world.bentobox.bentobox.managers.IslandsManager;
import world.bentobox.bentobox.managers.RanksManager;
import world.bentobox.bentobox.util.Util;
import world.bentobox.boxed.objects.IslandAdvancements;
/**
* @author tastybento
*
*/
@RunWith(PowerMockRunner.class)
@PrepareForTest({Bukkit.class, BentoBox.class, DatabaseSetup.class, Util.class})
public class AdvancementsManagerTest {
private static AbstractDatabaseHandler<Object> h;
@Mock
private BentoBox plugin;
@Mock
private Settings pluginSettings;
@Mock
private Boxed addon;
private AdvancementsManager am;
private File dataFolder;
@Mock
private Island island;
@Mock
private Player player;
@Mock
private Advancement advancement;
@Mock
private World world;
@Mock
private IslandsManager im;
@Mock
private AdvancementDisplay display;
@SuppressWarnings("unchecked")
@BeforeClass
public static void beforeClass() throws IllegalAccessException, InvocationTargetException, IntrospectionException {
// This has to be done beforeClass otherwise the tests will interfere with each other
h = mock(AbstractDatabaseHandler.class);
// Database
PowerMockito.mockStatic(DatabaseSetup.class);
DatabaseSetup dbSetup = mock(DatabaseSetup.class);
when(DatabaseSetup.getDatabase()).thenReturn(dbSetup);
when(dbSetup.getHandler(any())).thenReturn(h);
when(h.saveObject(any())).thenReturn(CompletableFuture.completedFuture(true));
}
/**
* @throws java.lang.Exception
*/
@Before
public void setUp() throws Exception {
when(addon.getPlugin()).thenReturn(plugin);
// Set up plugin
Whitebox.setInternalState(BentoBox.class, "instance", plugin);
// The database type has to be created one line before the thenReturn() to work!
DatabaseType value = DatabaseType.JSON;
when(plugin.getSettings()).thenReturn(pluginSettings);
when(pluginSettings.getDatabaseType()).thenReturn(value);
// Addon
dataFolder = new File("dataFolder");
dataFolder.mkdirs();
when(addon.getDataFolder()).thenReturn(dataFolder);
Files.copy(Path.of("src/main/resources/advancements.yml"), Path.of("dataFolder/advancements.yml"));
when(addon.inWorld(world)).thenReturn(true);
when(addon.getOverWorld()).thenReturn(world);
// Island
when(island.getUniqueId()).thenReturn("uniqueId");
// Player
when(player.getWorld()).thenReturn(world);
UUID uuid = UUID.randomUUID();
when(player.getUniqueId()).thenReturn(uuid);
NamespacedKey key = NamespacedKey.fromString("adventure/honey_block_slide");
// Advancement
when(advancement.getKey()).thenReturn(key);
when(display.getX()).thenReturn(9F);
when(display.getY()).thenReturn(0F);
when(advancement.getDisplay()).thenReturn(display);
// Bukkit
PowerMockito.mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS);
when(Bukkit.getAdvancement(any(NamespacedKey.class))).thenReturn(advancement);
// Island
when(addon.getIslands()).thenReturn(im);
when(im.getIsland(world, uuid)).thenReturn(island);
when(island.getRank(uuid)).thenReturn(RanksManager.MEMBER_RANK);
when(island.getProtectionRange()).thenReturn(5);
am = new AdvancementsManager(addon);
}
/**
* @throws java.lang.Exception - exception
*/
@After
public void tearDown() throws Exception {
deleteAll(new File("database"));
deleteAll(dataFolder);
User.clearUsers();
Mockito.framework().clearInlineMocks();
}
private static void deleteAll(File file) throws IOException {
if (file.exists()) {
Files.walk(file.toPath())
.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.forEach(File::delete);
}
}
/**
* Test method for {@link world.bentobox.boxed.AdvancementsManager#AdvancementsManager(world.bentobox.boxed.Boxed)}.
* @throws Exception
*/
@Test
public void testAdvancementsManagerNoFile() throws Exception {
tearDown();
am = new AdvancementsManager(addon);
verify(addon).logError("advancements.yml cannot be found!");
}
/**
* Test method for {@link world.bentobox.boxed.AdvancementsManager#AdvancementsManager(world.bentobox.boxed.Boxed)}.
* @throws IOException
*/
@Test
public void testAdvancementsManager() throws IOException {
verify(addon).saveResource("advancements.yml", false);
verify(addon, never()).logError(anyString());
}
/**
* Test method for {@link world.bentobox.boxed.AdvancementsManager#getIsland(world.bentobox.bentobox.database.objects.Island)}.
*/
@Test
public void testGetIsland() {
@NonNull
IslandAdvancements adv = am.getIsland(island);
assertEquals("uniqueId", adv.getUniqueId());
assertTrue(adv.getAdvancements().isEmpty());
}
/**
* Test method for {@link world.bentobox.boxed.AdvancementsManager#saveIsland(world.bentobox.bentobox.database.objects.Island)}.
* @throws IntrospectionException
* @throws InvocationTargetException
* @throws IllegalAccessException
*/
@Test
public void testSaveIslandNotInCache() throws IllegalAccessException, InvocationTargetException, IntrospectionException {
am.removeFromCache(island);
am.saveIsland(island);
verify(island, times(2)).getUniqueId(); // 2x
}
/**
* Test method for {@link world.bentobox.boxed.AdvancementsManager#saveIsland(world.bentobox.bentobox.database.objects.Island)}.
* @throws IntrospectionException
* @throws InvocationTargetException
* @throws IllegalAccessException
*/
@Test
public void testSaveIslandInCache() throws IllegalAccessException, InvocationTargetException, IntrospectionException {
testGetIsland();
am.saveIsland(island);
verify(island, times(3)).getUniqueId(); // 3x
}
/**
* Test method for {@link world.bentobox.boxed.AdvancementsManager#save()}.
* @throws IntrospectionException
* @throws InvocationTargetException
* @throws IllegalAccessException
*/
@Test
public void testSaveNothingToSave() throws IllegalAccessException, InvocationTargetException, IntrospectionException {
am.removeFromCache(island);
am.save();
verify(island).getUniqueId();
}
/**
* Test method for {@link world.bentobox.boxed.AdvancementsManager#save()}.
* @throws IntrospectionException
* @throws InvocationTargetException
* @throws IllegalAccessException
*/
@Test
public void testSave() throws IllegalAccessException, InvocationTargetException, IntrospectionException {
testGetIsland();
am.save();
verify(island).getUniqueId();
}
/**
* Test method for {@link world.bentobox.boxed.AdvancementsManager#addAdvancement(world.bentobox.bentobox.database.objects.Island, java.lang.String)}.
*/
@Test
public void testAddAdvancementIslandString() {
assertTrue(am.addAdvancement(island, "advancement"));
assertFalse(am.addAdvancement(island, "advancement")); // Second time should fail
}
/**
* Test method for {@link world.bentobox.boxed.AdvancementsManager#removeAdvancement(world.bentobox.bentobox.database.objects.Island, java.lang.String)}.
*/
@Test
public void testRemoveAdvancement() {
assertTrue(am.addAdvancement(island, "advancement"));
am.removeAdvancement(island, "advancement");
assertTrue(am.addAdvancement(island, "advancement")); // Should work because it was removed
}
/**
* Test method for {@link world.bentobox.boxed.AdvancementsManager#hasAdvancement(world.bentobox.bentobox.database.objects.Island, java.lang.String)}.
*/
@Test
public void testHasAdvancement() {
assertFalse(am.hasAdvancement(island, "advancement"));
am.addAdvancement(island, "advancement");
assertTrue(am.hasAdvancement(island, "advancement"));
}
/**
* Test method for {@link world.bentobox.boxed.AdvancementsManager#checkIslandSize(world.bentobox.bentobox.database.objects.Island)}.
*/
@Test
public void testCheckIslandSize() {
// Island protection size is set to 5, but after checking, the size is reduced by 4
assertEquals(-4, am.checkIslandSize(island));
}
/**
* Test method for {@link world.bentobox.boxed.AdvancementsManager#addAdvancement(org.bukkit.entity.Player, org.bukkit.advancement.Advancement)}.
*/
@Test
public void testAddAdvancementPlayerAdvancementWrongWorld() {
when(addon.inWorld(world)).thenReturn(false);
assertEquals(0, am.addAdvancement(player, advancement));
}
/**
* Test method for {@link world.bentobox.boxed.AdvancementsManager#addAdvancement(org.bukkit.entity.Player, org.bukkit.advancement.Advancement)}.
*/
@Test
public void testAddAdvancementPlayerAdvancement() {
assertEquals(9, am.addAdvancement(player, advancement));
verify(island).setProtectionRange(14); // (9 + 5)
}
/**
* Test method for {@link world.bentobox.boxed.AdvancementsManager#addAdvancement(org.bukkit.entity.Player, org.bukkit.advancement.Advancement)}.
*/
@Test
public void testAddAdvancementPlayerAdvancementZeroScore() {
when(display.getX()).thenReturn(0F);
assertEquals(0, am.addAdvancement(player, advancement));
verify(island, never()).setProtectionRange(anyInt());
}
/**
* Test method for {@link world.bentobox.boxed.AdvancementsManager#getScore(java.lang.String)}.
*/
@Test
public void testGetScoreString() {
assertEquals(9, am.getScore("adventure/lightning_rod_with_villager_no_fire"));
}
/**
* Test method for {@link world.bentobox.boxed.AdvancementsManager#getScore(org.bukkit.advancement.Advancement)}.
*/
@Test
public void testGetScoreAdvancement() {
assertEquals(9, am.getScore(advancement));
}
}