Merge pull request #79 from BentoBoxWorld/on_demand_structures

This commit is contained in:
tastybento 2024-07-02 07:10:02 -07:00 committed by GitHub
commit 25b06eca51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 146 additions and 99 deletions

1
.github/.gitignore vendored Normal file
View File

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

View File

@ -58,8 +58,8 @@
<!-- Non-minecraft related dependencies -->
<powermock.version>2.0.9</powermock.version>
<!-- More visible way how to change dependency versions -->
<spigot.version>1.20.4-R0.1-SNAPSHOT</spigot.version>
<bentobox.version>2.0.0-SNAPSHOT</bentobox.version>
<spigot.version>1.21-R0.1-SNAPSHOT</spigot.version>
<bentobox.version>2.4.0-SNAPSHOT</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. -->

View File

@ -11,6 +11,7 @@ import org.bukkit.Difficulty;
import org.bukkit.GameMode;
import org.bukkit.entity.EntityType;
import world.bentobox.bentobox.BentoBox;
import world.bentobox.bentobox.api.configuration.ConfigComment;
import world.bentobox.bentobox.api.configuration.ConfigEntry;
import world.bentobox.bentobox.api.configuration.StoreAt;
@ -106,6 +107,7 @@ public class Settings implements WorldSettings {
private int ticksPerMonsterSpawns = -1;
@ConfigComment("Radius of player area. (So distance between player starting spots is twice this)")
@ConfigComment("MUST BE A FACTOR OF 16. If not, it will be rounded to be one.")
@ConfigComment("It is the same for every dimension : Overworld, Nether and End.")
@ConfigEntry(path = "world.area-radius", needsReset = true)
private int islandDistance = 320;
@ -494,6 +496,11 @@ public class Settings implements WorldSettings {
*/
@Override
public int getIslandDistance() {
if (islandDistance % 16 != 0) {
islandDistance = islandDistance - (islandDistance % 16);
BentoBox.getInstance()
.logWarning("Boxed: Area radius is not a factor of 16. Rounding to " + islandDistance);
}
return islandDistance;
}

View File

@ -2,8 +2,10 @@ package world.bentobox.boxed.listeners;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
@ -12,6 +14,7 @@ import java.util.Queue;
import java.util.Random;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
@ -33,6 +36,7 @@ import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.world.ChunkLoadEvent;
import org.bukkit.loot.LootTables;
import org.bukkit.structure.Structure;
import org.bukkit.util.BoundingBox;
@ -93,19 +97,23 @@ public class NewAreaListener implements Listener {
private final File structureFile;
private final Queue<StructureRecord> itemsToBuild = new LinkedList<>();
private static final Random rand = new Random();
private boolean pasting;
private boolean pasting = true;
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 final Map<String, IslandStructures> islandStructureCache = new HashMap<>();
private Map<Pair<Integer, Integer>, List<StructureRecord>> readyToBuild = new HashMap<>();
private static String bukkitVersion = "v" + Bukkit.getBukkitVersion().replace('.', '_').replace('-', '_');
private static String pluginPackageName;
/**
* @param addon addon
*/
public NewAreaListener(Boxed addon) {
this.addon = addon;
pluginPackageName = addon.getClass().getPackage().getName();
// Save the default structures file from the jar
addon.saveResource("structures.yml", false);
// Load the config
@ -113,11 +121,11 @@ public class NewAreaListener implements Listener {
// Get database ready
handler = new Database<>(addon, IslandStructures.class);
// Try to build something every second
runStructurePrinter(addon);
runStructurePrinter();
}
private void runStructurePrinter(Boxed addon2) {
Bukkit.getScheduler().runTaskTimer(addon.getPlugin(), this::buildStructure, 20, 20);
private void runStructurePrinter() {
Bukkit.getScheduler().runTaskTimer(addon.getPlugin(), this::buildStructure, 100, 60);
for (String js : JAR_STRUCTURES) {
addon.saveResource("structures/" + js + ".nbt", false);
File structureFile = new File(addon.getDataFolder(), "structures/" + js + ".nbt");
@ -146,6 +154,29 @@ public class NewAreaListener implements Listener {
}
}
private void placeStructure(StructureRecord item) {
// Set the semaphore - only paste one at a time
pasting = true;
// Place the structure - this cannot be done async
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()));
// Remove any jigsaw artifacts
BoundingBox bb = removeJigsaw(item);
// Store it for future reference
addon.getIslands().getIslandAt(item.location()).map(Island::getUniqueId).ifPresent(id -> {
//.log("Saved " + item.name());
if (item.location().getWorld().getEnvironment().equals(Environment.NETHER)) {
getIslandStructData(id).addNetherStructure(bb, item.name());
} else {
getIslandStructData(id).addStructure(bb, item.name());
}
handler.saveObjectAsync(getIslandStructData(id));
});
// Clear the semaphore
pasting = false;
}
/**
* 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
@ -155,22 +186,49 @@ public class NewAreaListener implements Listener {
*/
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onBentoBoxReady(BentoBoxReadyEvent event) {
addon.saveResource("templates.yml", false);
File templateFile = new File(addon.getDataFolder(), "templates.yml");
if (templateFile.exists()) {
YamlConfiguration loader = YamlConfiguration.loadConfiguration(templateFile);
List<String> list = loader.getStringList("templates");
for (String struct : list) {
if (!struct.endsWith("/")) {
Bukkit.getStructureManager().loadStructure(NamespacedKey.fromString(struct));
Bukkit.getScheduler().runTaskAsynchronously(addon.getPlugin(), () -> {
addon.saveResource("templates.yml", false);
File templateFile = new File(addon.getDataFolder(), "templates.yml");
if (templateFile.exists()) {
YamlConfiguration loader = YamlConfiguration.loadConfiguration(templateFile);
List<String> list = loader.getStringList("templates");
for (String struct : list) {
if (!struct.endsWith("/")) {
Bukkit.getStructureManager().loadStructure(NamespacedKey.fromString(struct));
}
}
}
}
pasting = false; // Allow pasting
});
}
/**
* Track if a place has entered a structure.
* Add items to the queue when they are needed due to chunk loading
* @param e ChunkLoadEvent
*/
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onChunkLoad(ChunkLoadEvent e) {
Chunk chunk = e.getChunk();
// Check if this island is in this game
if (!(addon.inWorld(chunk.getWorld()))) {
return;
}
Pair<Integer, Integer> chunkCoords = new Pair<Integer, Integer>(chunk.getX(), chunk.getZ());
if (this.readyToBuild.containsKey(chunkCoords)) {
Iterator<StructureRecord> it = this.readyToBuild.get(chunkCoords).iterator();
while (it.hasNext()) {
StructureRecord item = it.next();
if (item.location().getWorld().equals(e.getWorld())) {
this.itemsToBuild.add(item);
it.remove();
}
}
}
}
/**
* Track if a player has entered a structure.
*
* @param e PlayerMoveEvent
*/
@ -312,36 +370,14 @@ public class NewAreaListener implements Listener {
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 StructureRecord(name, s, l, rot, mirror, noMobs));
readyToBuild.computeIfAbsent(new Pair<Integer, Integer>(x >> 4, z >> 4), k -> new ArrayList<>())
.add(new StructureRecord(name, s, l, rot, mirror, noMobs));
} else {
addon.logError("Structure file syntax error: " + vector + ": " + Arrays.toString(value));
}
}
}
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()));
// Remove any jigsaw artifacts
BoundingBox bb = removeJigsaw(item);
// 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)) {
getIslandStructData(id).addNetherStructure(bb, item.name());
} else {
getIslandStructData(id).addStructure(bb, item.name());
}
handler.saveObjectAsync(getIslandStructData(id));
});
// Clear the semaphore
pasting = false;
}
/**
* Removes Jigsaw blocks from a placed structure. Fills underwater ruins with
* water.
@ -403,6 +439,9 @@ public class NewAreaListener implements Listener {
private static void processStructureBlock(Block b) {
// I would like to read the data from the block and do something with it!
String data = nmsData(b);
if (data.isEmpty()) {
return;
}
BoxedStructureBlock bsb = gson.fromJson(data, BoxedStructureBlock.class);
b.setType(Material.STRUCTURE_VOID);
Enums.getIfPresent(EntityType.class, bsb.getMetadata().toUpperCase(Locale.ENGLISH)).toJavaUtil()
@ -425,6 +464,9 @@ public class NewAreaListener implements Listener {
private static void processJigsaw(Block b, StructureRotation structureRotation, boolean pasteMobs) {
String data = nmsData(b);
if (data.isEmpty()) {
return;
}
BoxedJigsawBlock bjb = gson.fromJson(data, BoxedJigsawBlock.class);
String finalState = correctDirection(bjb.getFinal_state(), structureRotation);
BlockData bd = Bukkit.createBlockData(finalState);
@ -542,10 +584,6 @@ public class NewAreaListener implements Listener {
}
private static String nmsData(Block block) {
// Bukkit method that was added in 2011
// Example value: 1.20.4-R0.1-SNAPSHOT
String bukkitVersion = "v" + Bukkit.getBukkitVersion().replace('.', '_').replace('-', '_');
String pluginPackageName = BentoBox.getInstance().getClass().getPackage().getName();
AbstractMetaData handler;
try {
Class<?> clazz = Class.forName(pluginPackageName + ".nms." + bukkitVersion + ".GetMetaData");
@ -555,6 +593,7 @@ public class NewAreaListener implements Listener {
throw new IllegalStateException("Class " + clazz.getName() + " does not implement AbstractGetMetaData");
}
} catch (Exception e) {
e.printStackTrace();
BentoBox.getInstance().logWarning("No metadata handler found for " + bukkitVersion + " in Boxed.");
handler = new world.bentobox.boxed.nms.fallback.GetMetaData();
}

View File

@ -1,7 +1,14 @@
package world.bentobox.boxed.nms;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import org.bukkit.block.Block;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.network.protocol.game.PacketPlayOutTileEntityData;
import net.minecraft.world.level.block.entity.TileEntity;
/**
*
*/
@ -9,4 +16,28 @@ public abstract class AbstractMetaData {
public abstract String nmsData(Block block);
protected String getData(TileEntity te, String method, String field) {
try {
// Check if the method 'j' exists
Method updatePacketMethod = te.getClass().getDeclaredMethod(method);
if (updatePacketMethod != null) {
// Invoke the method to get the PacketPlayOutTileEntityData object
updatePacketMethod.setAccessible(true);
PacketPlayOutTileEntityData packet = (PacketPlayOutTileEntityData) updatePacketMethod.invoke(te);
// Access the private field for the NBTTagCompound getter in PacketPlayOutTileEntityData
Field fieldC = packet.getClass().getDeclaredField(field);
fieldC.setAccessible(true);
NBTTagCompound nbtTag = (NBTTagCompound) fieldC.get(packet);
return nbtTag.toString(); // This will show what you want
}
} catch (NoSuchMethodException e) {
System.out.println("The method '" + method + "' does not exist in the TileEntity class.");
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
}

View File

@ -11,7 +11,7 @@ public class GetMetaData extends AbstractMetaData {
@Override
public String nmsData(Block block) {
return "Nothing"; // We cannot read it if we have no NMS
return ""; // We cannot read it if we have no NMS
}
}

View File

@ -1,14 +1,10 @@
package world.bentobox.boxed.nms.v1_20_4_R0_1_SNAPSHOT;
import java.lang.reflect.Field;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.craftbukkit.v1_20_R3.CraftWorld;
import net.minecraft.core.BlockPosition;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.network.protocol.game.PacketPlayOutTileEntityData;
import net.minecraft.world.level.block.entity.TileEntity;
import world.bentobox.boxed.nms.AbstractMetaData;
@ -18,20 +14,8 @@ public class GetMetaData extends AbstractMetaData {
public String nmsData(Block block) {
Location w = block.getLocation();
CraftWorld cw = (CraftWorld) w.getWorld(); // CraftWorld is NMS one
// for 1.13+ (we have use WorldServer)
TileEntity te = cw.getHandle().c_(new BlockPosition(w.getBlockX(), w.getBlockY(), w.getBlockZ()));
try {
PacketPlayOutTileEntityData packet = ((PacketPlayOutTileEntityData) te.j()); // get update packet from NMS
// object
// here we should use reflection because "c" field isn't accessible
Field f = packet.getClass().getDeclaredField("c"); // get field
f.setAccessible(true); // make it available
NBTTagCompound nbtTag = (NBTTagCompound) f.get(packet);
return nbtTag.toString(); // this will show what you want
} catch (Exception exc) {
exc.printStackTrace();
}
return "Nothing";
}
return getData(te, "j", "c");
}
}

View File

@ -1,14 +1,10 @@
package world.bentobox.boxed.nms.v1_20_6_R0_1_SNAPSHOT;
import java.lang.reflect.Field;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.craftbukkit.v1_20_R4.CraftWorld;
import net.minecraft.core.BlockPosition;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.network.protocol.game.PacketPlayOutTileEntityData;
import net.minecraft.world.level.block.entity.TileEntity;
import world.bentobox.boxed.nms.AbstractMetaData;
@ -18,20 +14,8 @@ public class GetMetaData extends AbstractMetaData {
public String nmsData(Block block) {
Location w = block.getLocation();
CraftWorld cw = (CraftWorld) w.getWorld(); // CraftWorld is NMS one
// for 1.13+ (we have use WorldServer)
TileEntity te = cw.getHandle().c_(new BlockPosition(w.getBlockX(), w.getBlockY(), w.getBlockZ()));
try {
PacketPlayOutTileEntityData packet = ((PacketPlayOutTileEntityData) te.j()); // get update packet from NMS
// object
// here we should use reflection because "c" field isn't accessible
Field f = packet.getClass().getDeclaredField("c"); // get field
f.setAccessible(true); // make it available
NBTTagCompound nbtTag = (NBTTagCompound) f.get(packet);
return nbtTag.toString(); // this will show what you want
} catch (Exception exc) {
exc.printStackTrace();
}
return "Nothing";
}
return getData(te, "j", "c");
}
}

View File

@ -1,14 +1,10 @@
package world.bentobox.boxed.nms.v1_21_R0_1_SNAPSHOT;
import java.lang.reflect.Field;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.craftbukkit.v1_21_R1.CraftWorld;
import net.minecraft.core.BlockPosition;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.network.protocol.game.PacketPlayOutTileEntityData;
import net.minecraft.world.level.block.entity.TileEntity;
import world.bentobox.boxed.nms.AbstractMetaData;
@ -20,18 +16,8 @@ public class GetMetaData extends AbstractMetaData {
CraftWorld cw = (CraftWorld) w.getWorld(); // CraftWorld is NMS one
// for 1.13+ (we have use WorldServer)
TileEntity te = cw.getHandle().c_(new BlockPosition(w.getBlockX(), w.getBlockY(), w.getBlockZ()));
try {
PacketPlayOutTileEntityData packet = ((PacketPlayOutTileEntityData) te.j()); // get update packet from NMS
// object
// here we should use reflection because "c" field isn't accessible
Field f = packet.getClass().getDeclaredField("c"); // get field
f.setAccessible(true); // make it available
NBTTagCompound nbtTag = (NBTTagCompound) f.get(packet);
return nbtTag.toString(); // this will show what you want
} catch (Exception exc) {
exc.printStackTrace();
}
return "Nothing";
return getData(te, "az_", "tag");
}
}

View File

@ -3,6 +3,7 @@ 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.Mockito.verify;
import java.util.Collections;
import java.util.List;
@ -13,17 +14,30 @@ import org.bukkit.Difficulty;
import org.bukkit.entity.EntityType;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.reflect.Whitebox;
import world.bentobox.bentobox.BentoBox;
/**
* @author tastybento
*
*/
@RunWith(PowerMockRunner.class)
@PrepareForTest({ BentoBox.class })
public class SettingsTest {
@Mock
private BentoBox plugin;
Settings s;
@Before
public void setUp() {
// Set up plugin
Whitebox.setInternalState(BentoBox.class, "instance", plugin);
s = new Settings();
}
/**
@ -59,7 +73,8 @@ public class SettingsTest {
@Test
public void testSetIslandDistance() {
s.setIslandDistance(123);
assertEquals(123, s.getIslandDistance());
assertEquals(112, s.getIslandDistance());
verify(plugin).logWarning("Boxed: Area radius is not a factor of 16. Rounding to 112");
}
/**