Files
PixelPics/tools/edgecase-plugin/pptest/PPTest.java
T

745 lines
43 KiB
Java

package pptest;
import eu.mhsl.minecraft.pixelpics.Main;
import eu.mhsl.minecraft.pixelpics.render.render.DefaultScreenRenderer;
import eu.mhsl.minecraft.pixelpics.render.render.RenderJob;
import eu.mhsl.minecraft.pixelpics.render.render.Resolution;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.*;
import java.util.function.Consumer;
import javax.imageio.ImageIO;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.*;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.type.Bed;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.*;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.util.Transformation;
import org.bukkit.util.Vector;
import org.joml.AxisAngle4f;
import org.joml.Quaternionf;
import org.joml.Vector3f;
/** Test-only plugin: builds edge-case scenes and renders them through PixelPics into PNGs. */
public class PPTest extends JavaPlugin {
record Scene(String name, Consumer<Location> build, double camX, double camY, double camZ, double lookX, double lookY, double lookZ) {}
private final Map<String, Scene> scenes = new LinkedHashMap<>();
private World w;
@Override
public void onEnable() {
// Each scene sits on its own stone platform; the camera is placed relative to the platform origin.
scenes.put("held", new Scene("held", this::held, 4.4, 2.6, -8.5, 4.4, 1.0, 1));
scenes.put("equip", new Scene("equip", this::equip, 4, 3, -8.5, 4, 1.0, 1.5));
scenes.put("riding", new Scene("riding", this::riding, 5, 3, -7.5, 5, 1.2, 1));
scenes.put("leash", new Scene("leash", this::leash, 5, 4, -9, 5, 2.5, 2));
scenes.put("displays", new Scene("displays", this::displays, 5, 3, -9, 5, 1.0, 1.5));
scenes.put("pixelblock", new Scene("pixelblock", this::pixelblock, 1.5, 1.9, -1.3, 0.5, 0.5, 0.5));
scenes.put("misc", new Scene("misc", this::misc, 5, 3, -7, 5, 0.8, 1.5));
scenes.put("dbg", new Scene("dbg", this::dbg, 1, 1.6, -3, 1, 0.8, 0.5));
scenes.put("shelf", new Scene("shelf", this::shelf, 3.5, 3.2, -2.5, 0.5, 1.5, 0.5));
scenes.put("shelftop", new Scene("shelftop", this::shelf, 0.52, 6, 0.5, 0.5, 0, 0.5));
scenes.put("rotated", new Scene("rotated", this::rotated, 3, 4.5, -4, 3, 0.5, 2));
scenes.put("rotated2", new Scene("rotated2", this::rotated, -0.5, 3.5, 9.5, -0.5, 0.5, 5));
scenes.put("redstone", new Scene("redstone", this::redstone, 3, 3, -3, 3, 0, 2));
scenes.put("piston", new Scene("piston", this::piston, 4.5, 3.2, -3.5, 5, 1, 1));
scenes.put("pistonback", new Scene("pistonback", this::piston, 5.5, 3.2, 5.5, 5, 1, 1));
scenes.put("pistondown", new Scene("pistondown", this::piston, -1.6, 2.6, -1.6, 0.5, 1.4, 0.5));
scenes.put("pistonup", new Scene("pistonup", this::piston, 0.4, 2.6, -1.6, 2.5, 1.4, 0.5));
scenes.put("vehicles", new Scene("vehicles", this::vehicles, 5, 3, -7, 5, 0.6, 1.5));
// Blockstate x rotations (+ x/y combined, uvlock): lying pillars, vertical observers/dispensers, top stairs.
scenes.put("xrot", new Scene("xrot", this::xrot, 3.5, 5.5, -4.5, 6, 0.5, 3.5));
scenes.put("xrot2", new Scene("xrot2", this::xrot, 11, 5, 10.5, 6, 0.5, 3.5));
// Chests: same layout seen from all four sides (single/double per facing, trapped, ender, copper).
scenes.put("chests_s", new Scene("chests_s", this::chests, 4.5, 5, 13, 4.5, 0, 3));
scenes.put("chests_n", new Scene("chests_n", this::chests, 4.5, 5, -7, 4.5, 0, 3));
scenes.put("chests_e", new Scene("chests_e", this::chests, 16, 5, 3, 4.5, 0, 3));
scenes.put("chests_w", new Scene("chests_w", this::chests, -7, 5, 3, 4.5, 0, 3));
scenes.put("translucent", new Scene("translucent", this::translucent, 4.5, 2.8, -5.5, 4.5, 0.8, 2));
// Projectile orientation (vanilla ArrowRenderer/ThrownTridentRenderer): flying stars around gold pillars
// (tips point radially outward, rings level / +35 deg / -35 deg) and arrows/tridents stuck in stone pillars.
scenes.put("proj_fly_top", new Scene("proj_fly_top", this::projFly, 6.5, 13, 2.2, 6.5, 0, 2));
scenes.put("proj_fly_side", new Scene("proj_fly_side", this::projFly, 6.5, 2.6, -8, 6.5, 2.4, 2));
scenes.put("proj_fly_obl", new Scene("proj_fly_obl", this::projFly, -2, 6, -6, 6.5, 2, 2));
scenes.put("proj_stuck_nw", new Scene("proj_stuck_nw", this::projStuck, -3, 6, -6, 8, 1.5, 2));
scenes.put("proj_stuck_se", new Scene("proj_stuck_se", this::projStuck, 19, 6, 10, 8, 1.5, 2));
scenes.put("proj_stuck_ne", new Scene("proj_stuck_ne", this::projStuck, 19, 6, -6, 8, 1.5, 2));
scenes.put("proj_fly_gold", new Scene("proj_fly_gold", this::projFly, 2.6, 8.5, 2.5, 2.5, 0, 2.5));
scenes.put("proj_fly_gold_side", new Scene("proj_fly_gold_side", this::projFly, 2.5, 2.6, -3.5, 2.5, 2.4, 2.5));
scenes.put("proj_stuck_close_a", new Scene("proj_stuck_close_a", this::projStuck, -2.5, 2.4, -2.5, 2.5, 1.6, 1));
scenes.put("proj_stuck_close_t", new Scene("proj_stuck_close_t", this::projStuck, 18.5, 2.6, -3, 13.5, 1.6, 1));
// Rendered 4 ticks after the shots: freshly stuck arrows still wobble (vanilla shakeTime -> pitch offset).
scenes.put("proj_shake", new Scene("proj_shake", this::projStuck, -2.5, 2.4, -2.5, 2.5, 1.6, 1));
// Conditional vanilla layers: collars (tamed only, dye-tinted), charged creeper / wither armor (additive
// energy swirl), fox/dolphin mouth items.
scenes.put("layers", new Scene("layers", this::layers, 4.8, 2.4, -5.5, 4.8, 0.6, 1.5));
scenes.put("layers_side", new Scene("layers_side", this::layers, 12.5, 2.2, 5.5, 5, 0.5, 2.5));
scenes.put("layers_close", new Scene("layers_close", this::layers, 5.5, 1.3, 0.3, 5.5, 0.4, 2.5));
scenes.put("layers_pets", new Scene("layers_pets", this::layers, 4.5, 1.6, -3.2, 4.5, 0.3, 0));
scenes.put("layers_dolphin", new Scene("layers_dolphin", this::layers, 10, 1.0, 1.0, 7.5, 0.4, 2.0));
scenes.put("layers_cats", new Scene("layers_cats", this::layers, 1.7, 1.1, -1.6, 1.7, 0.3, 0));
scenes.put("layers_cats_side", new Scene("layers_cats_side", this::layers, 1.7, 1.6, 1.4, 1.7, 0.2, 0));
scenes.put("layers_top", new Scene("layers_top", this::layers, 5.5, 6, 2.0, 5.5, 0, 2.5));
scenes.put("layers_wither", new Scene("layers_wither", this::withers, 3.5, 4, -9, 3.5, 2.2, 1));
scenes.put("layers_wither_close", new Scene("layers_wither_close", this::withers, 6, 3.4, -3.5, 6, 2.6, 1));
scenes.put("proj_stuck_sw", new Scene("proj_stuck_sw", this::projStuck, -3, 6, 10, 8, 1.5, 2));
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
w = Bukkit.getWorlds().getFirst();
w.setTime(6000);
w.setStorm(false);
List<String> todo = args.length == 0 || args[0].equals("all") ? new ArrayList<>(scenes.keySet()) : List.of(args);
int i = 0;
for (String n : todo) {
Scene sc = scenes.get(n);
if (sc == null) { getLogger().warning("unknown scene " + n); continue; }
int idx = new ArrayList<>(scenes.keySet()).indexOf(n);
Location origin = new Location(w, 5000 + idx * 64, 200, 5000);
Bukkit.getScheduler().runTaskLater(this, () -> run(sc, origin), 1 + i * 60L);
i++;
}
return true;
}
private void run(Scene sc, Location o) {
try {
prepareArea(o);
sc.build.accept(o.clone());
} catch (Throwable t) {
getLogger().severe("build " + sc.name + " failed: " + t);
t.printStackTrace();
}
// Let one tick pass (passengers/leashes settle), then render.
Bukkit.getScheduler().runTaskLater(this, () -> render(sc, o), sc.name.equals("proj_shake") ? 4 : sc.name.startsWith("proj_") ? 30 : 10);
}
private void render(Scene sc, Location o) {
DefaultScreenRenderer r = Main.getInstance().getScreenRenderer();
Location eye = o.clone().add(sc.camX, sc.camY, sc.camZ);
Vector look = o.clone().add(sc.lookX, sc.lookY, sc.lookZ).toVector().subtract(eye.toVector());
eye.setDirection(look);
RenderJob job = r.prepare(eye, sc.name.startsWith("proj_") ? new Resolution(960, 640) : new Resolution(480, 320), null);
if (sc.name.equals("dbg")) {
try {
java.lang.reflect.Field f = DefaultScreenRenderer.class.getDeclaredField("objectBaker");
f.setAccessible(true);
var ob = (eu.mhsl.minecraft.pixelpics.render.entity.ObjectBaker) f.get(r);
for (var st : job.objects()) {
double[] pos = st.toWorld().t;
if (Math.abs(pos[0] - o.getX()) > 5 || Math.abs(pos[2] - o.getZ()) > 5) continue;
try {
var re = ob.bake(st);
getLogger().info("dbg obj " + st.kind() + " item=" + st.item() + " ctx=" + st.context() + " -> "
+ (re == null ? "null" : re.cubes.size() + " cubes " + java.util.Arrays.toString(re.aabbMin)));
} catch (Throwable t) {
getLogger().warning("dbg bake failed: " + t);
t.printStackTrace();
}
}
} catch (Throwable t) {
getLogger().warning("dbg: " + t);
}
}
if (sc.name.startsWith("proj_"))
for (Entity e : w.getNearbyEntities(o.clone().add(8, 3, 2), 12, 8, 12))
if (e instanceof Projectile pr) {
Vector v = pr.getVelocity();
getLogger().info(String.format("proj %s yaw=%.1f pitch=%.1f velYaw=%.1f velPitch=%.1f ground=%s",
e.getType(), e.getLocation().getYaw(), e.getLocation().getPitch(),
Math.toDegrees(Math.atan2(v.getX(), v.getZ())),
Math.toDegrees(Math.atan2(v.getY(), Math.hypot(v.getX(), v.getZ()))),
pr instanceof AbstractArrow a && a.isInBlock()));
}
if (sc.name.equals("proj_shake"))
for (var st : job.entities())
if (st.typeKey().contains("arrow"))
getLogger().info(String.format("state %s pitch=%.2f", st.typeKey(), st.pitch()));
if (sc.name.equals("proj_shake"))
for (Entity e : w.getNearbyEntities(o.clone().add(8, 3, 2), 12, 8, 12))
if (e instanceof AbstractArrow a && !(e instanceof Trident))
try {
Object h = e.getClass().getMethod("getHandle").invoke(e);
getLogger().info(String.format("shake %s pitch=%.2f shakeTime=%d", e.getType(), e.getLocation().getPitch(),
h.getClass().getField("shakeTime").getInt(h)));
} catch (ReflectiveOperationException ex) {
getLogger().warning("shake: " + ex);
}
getLogger().info("scene " + sc.name + ": " + job.entities().size() + " entities, " + job.objects().size()
+ " objects, " + job.blockEntities().size() + " block entities");
Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
try {
BufferedImage img = r.execute(job);
File f = new File(getDataFolder(), sc.name + ".png");
f.getParentFile().mkdirs();
ImageIO.write(img, "png", f);
getLogger().info("WROTE " + f);
} catch (Throwable t) {
getLogger().severe("render " + sc.name + " failed: " + t);
t.printStackTrace();
}
});
}
/** Clears the area (entities + blocks) and lays a 14x14 stone floor at the origin's y-1. */
private void prepareArea(Location o) {
for (int cx = -1; cx <= 2; cx++)
for (int cz = -2; cz <= 2; cz++) {
Chunk c = w.getChunkAt((o.getBlockX() >> 4) + cx, (o.getBlockZ() >> 4) + cz);
c.load(true);
c.addPluginChunkTicket(this);
}
for (Entity e : w.getNearbyEntities(o.clone().add(6, 5, 2), 20, 20, 20)) if (!(e instanceof Player)) e.remove();
for (int x = -6; x < 18; x++)
for (int z = -12; z < 12; z++)
for (int y = -1; y < 12; y++)
w.getBlockAt(o.getBlockX() + x, o.getBlockY() + y, o.getBlockZ() + z)
.setType(y == -1 ? Material.SMOOTH_STONE : Material.AIR, false);
}
// --- helpers ---------------------------------------------------------------------------------
private <T extends Entity> T spawn(Location o, double x, double y, double z, float yaw, Class<T> type) {
Location l = o.clone().add(x, y, z);
l.setYaw(yaw);
T e = w.spawn(l, type, en -> {
if (en instanceof Mob m) m.setAI(false);
en.setPersistent(false);
});
if (e instanceof LivingEntity le) le.setRemoveWhenFarAway(false);
return e;
}
private Entity spawn(Location o, double x, double y, double z, float yaw, EntityType type) {
Location l = o.clone().add(x, y, z);
l.setYaw(yaw);
Entity e = w.spawnEntity(l, type);
if (e instanceof Mob m) m.setAI(false);
return e;
}
private static ItemStack it(Material m) {
return new ItemStack(m);
}
private static void hands(Entity e, Material main, Material off) {
if (!(e instanceof LivingEntity le) || le.getEquipment() == null) return;
if (main != null) le.getEquipment().setItemInMainHand(it(main));
if (off != null) le.getEquipment().setItemInOffHand(it(off));
}
// --- scenes ----------------------------------------------------------------------------------
private void held(Location o) {
hands(spawn(o, 0, 0, 0, 180, Zombie.class), Material.IRON_SWORD, Material.SHIELD);
hands(spawn(o, 1.6, 0, 0, 180, Skeleton.class), Material.BOW, null);
hands(spawn(o, 3.2, 0, 0, 180, Piglin.class), Material.GOLDEN_SWORD, null);
hands(spawn(o, 4.8, 0, 0, 180, EntityType.MANNEQUIN), Material.DIAMOND_PICKAXE, Material.TORCH);
ArmorStand as = spawn(o, 6.4, 0, 0, 180, ArmorStand.class);
as.setArms(true);
hands(as, Material.TRIDENT, Material.GRASS_BLOCK);
hands(spawn(o, 8.0, 0, 0, 180, Drowned.class), Material.TRIDENT, null);
hands(spawn(o, 0.8, 0, 2, 180, Pillager.class), Material.CROSSBOW, null);
hands(spawn(o, 2.4, 0, 2, 180, Vindicator.class), Material.IRON_AXE, null);
hands(spawn(o, 4.0, 0, 2, 180, Fox.class), Material.SWEET_BERRIES, null);
hands(spawn(o, 5.6, 0.5, 2, 180, Allay.class), Material.DIAMOND, null);
hands(spawn(o, 7.2, 0, 2, 180, Witch.class), Material.POTION, null);
hands(spawn(o, 8.8, 0, 2, 180, Villager.class), Material.EMERALD, null);
}
private void equip(Location o) {
LivingEntity m = (LivingEntity) spawn(o, 0, 0, 0, 180, EntityType.MANNEQUIN);
m.getEquipment().setChestplate(it(Material.ELYTRA));
m.getEquipment().setHelmet(it(Material.IRON_HELMET));
LivingEntity m2 = (LivingEntity) spawn(o, 1.6, 0, 0, 0, EntityType.MANNEQUIN); // back view: elytra
m2.getEquipment().setChestplate(it(Material.ELYTRA));
ArmorStand pumpkin = spawn(o, 3.2, 0, 0, 180, ArmorStand.class);
pumpkin.getEquipment().setHelmet(it(Material.CARVED_PUMPKIN));
pumpkin.getEquipment().setChestplate(it(Material.CHAINMAIL_CHESTPLATE));
ArmorStand head = spawn(o, 4.8, 0, 0, 180, ArmorStand.class);
head.getEquipment().setHelmet(it(Material.CREEPER_HEAD));
head.setInvisible(true);
Zombie z = spawn(o, 6.4, 0, 0, 180, Zombie.class);
z.getEquipment().setHelmet(it(Material.GOLDEN_HELMET));
z.getEquipment().setChestplate(it(Material.COPPER_CHESTPLATE));
z.getEquipment().setLeggings(it(Material.NETHERITE_LEGGINGS));
z.getEquipment().setBoots(it(Material.LEATHER_BOOTS));
spawn(o, 8.0, 0, 0, 180, Snowman.class);
Wolf wolf = spawn(o, 1, 0, 2.5, 180, Wolf.class);
wolf.getEquipment().setItem(EquipmentSlot.BODY, it(Material.WOLF_ARMOR));
Horse h = spawn(o, 4, 0, 3, 90, Horse.class);
h.getInventory().setSaddle(it(Material.SADDLE));
h.getInventory().setArmor(it(Material.GOLDEN_HORSE_ARMOR));
Zombie baby = spawn(o, 7.5, 0, 2.5, 180, Zombie.class);
baby.setBaby();
baby.getEquipment().setHelmet(it(Material.DIAMOND_HELMET));
hands(baby, Material.STONE_SWORD, null);
}
private void riding(Location o) {
Spider sp = spawn(o, 0, 0, 0, 180, Spider.class);
sp.addPassenger(spawn(o, 0, 1, 0, 180, Skeleton.class));
Chicken ch = spawn(o, 2, 0, 0, 180, Chicken.class);
Zombie bz = spawn(o, 2, 1, 0, 180, Zombie.class);
bz.setBaby();
ch.addPassenger(bz);
Horse h = spawn(o, 4.5, 0, 0.5, 180, Horse.class);
h.getInventory().setSaddle(it(Material.SADDLE));
h.addPassenger(spawn(o, 4.5, 2, 0.5, 180, EntityType.MANNEQUIN));
Minecart mc = spawn(o, 7, 0, 0, 0, Minecart.class);
mc.addPassenger(spawn(o, 7, 1, 0, 180, Villager.class));
Boat b = (Boat) spawn(o, 9.5, 0, 0, 180, EntityType.OAK_BOAT);
b.addPassenger(spawn(o, 9.5, 1, 0, 180, Zombie.class));
b.addPassenger(spawn(o, 9.5, 1, 0, 180, Pig.class));
Camel c = spawn(o, 2, 0, 3.5, 90, Camel.class);
c.getInventory().setSaddle(it(Material.SADDLE));
c.addPassenger(spawn(o, 2, 2, 3.5, 90, EntityType.MANNEQUIN));
c.addPassenger(spawn(o, 2, 2, 3.5, 90, Villager.class));
Strider st = spawn(o, 6, 0, 3.5, 180, Strider.class);
st.setSaddle(true);
st.addPassenger(spawn(o, 6, 2, 3.5, 180, Piglin.class));
Boat cb = (Boat) spawn(o, 9, 0, 3.5, 180, EntityType.BIRCH_CHEST_BOAT);
cb.addPassenger(spawn(o, 9, 1, 3.5, 180, EntityType.MANNEQUIN));
}
private void leash(Location o) {
Block fence = o.clone().add(0, 0, 0).getBlock();
fence.setType(Material.OAK_FENCE);
LeashHitch knot = w.spawn(fence.getLocation(), LeashHitch.class);
Cow cow = spawn(o, 2.5, 0, 0.5, 180, Cow.class);
cow.setLeashHolder(knot);
LivingEntity holder = (LivingEntity) spawn(o, 5, 0, 0, 180, EntityType.MANNEQUIN);
Wolf wolf = spawn(o, 7, 0, 0.5, 180, Wolf.class);
wolf.setLeashHolder(holder);
HappyGhast g = spawn(o, 4, 4, 5, 180, HappyGhast.class);
g.getEquipment().setItem(EquipmentSlot.BODY, it(Material.BLUE_HARNESS));
Boat boat = (Boat) spawn(o, 4, 0, 4, 180, EntityType.OAK_BOAT);
try {
boat.getClass().getMethod("setLeashHolder", Entity.class).invoke(boat, g);
} catch (Throwable t) {
getLogger().warning("boat not leashable: " + t);
}
Llama l1 = spawn(o, 9, 0, 2, 180, Llama.class);
Llama l2 = spawn(o, 10.5, 0, 3.5, 180, Llama.class);
l2.setLeashHolder(l1);
l1.getInventory().setDecor(it(Material.RED_CARPET));
l1.setCarryingChest(true);
}
private void displays(Location o) {
// Text displays: default background, coloured + shadowed + billboard center, multi-line wrapped.
TextDisplay t1 = spawn(o, 0.5, 1.5, 0, 0, TextDisplay.class);
t1.text(Component.text("Default Text"));
TextDisplay t2 = spawn(o, 2.5, 1.5, 0, 0, TextDisplay.class);
t2.text(Component.text("Billboard CENTER", NamedTextColor.GOLD));
t2.setBillboard(Display.Billboard.CENTER);
t2.setShadowed(true);
t2.setBackgroundColor(Color.fromARGB(255, 30, 30, 90));
TextDisplay t3 = spawn(o, 5, 1.5, 0, 180, TextDisplay.class);
t3.text(Component.text("Dies ist ein langer Text, der umgebrochen werden sollte weil er breit ist"));
t3.setLineWidth(80);
// Item displays.
ItemDisplay i1 = spawn(o, 7, 1, 0, 0, ItemDisplay.class);
i1.setItemStack(it(Material.DIAMOND_SWORD));
i1.setTransformation(new Transformation(new Vector3f(), new AxisAngle4f((float) Math.toRadians(45), 0, 0, 1),
new Vector3f(1, 1, 1), new AxisAngle4f()));
ItemDisplay i2 = spawn(o, 8.5, 0.5, 0, 30, ItemDisplay.class);
i2.setItemStack(it(Material.CHEST));
ItemDisplay i3 = spawn(o, 0.5, 0.5, 2.5, 0, ItemDisplay.class);
i3.setItemStack(it(Material.PLAYER_HEAD));
ItemDisplay i4 = spawn(o, 2, 0.5, 2.5, 0, ItemDisplay.class);
i4.setItemStack(it(Material.SHIELD));
// Block displays: rotated stairs, scaled chest (block entity!), banner, bed, glass.
BlockDisplay b1 = spawn(o, 3.5, 0, 2.5, 0, BlockDisplay.class);
b1.setBlock(Bukkit.createBlockData("minecraft:oak_stairs[facing=east]"));
b1.setTransformation(new Transformation(new Vector3f(), new Quaternionf().rotateY(0.6f), new Vector3f(0.8f), new Quaternionf()));
BlockDisplay b2 = spawn(o, 5, 0, 2.5, 0, BlockDisplay.class);
b2.setBlock(Bukkit.createBlockData("minecraft:chest[facing=north]"));
BlockDisplay b3 = spawn(o, 6.5, 0, 2.5, 0, BlockDisplay.class);
b3.setBlock(Bukkit.createBlockData("minecraft:red_banner"));
BlockDisplay b4 = spawn(o, 8, 0, 2.5, 0, BlockDisplay.class);
b4.setBlock(Bukkit.createBlockData("minecraft:light_blue_stained_glass"));
BlockDisplay b5 = spawn(o, 9.5, 0, 2.5, 0, BlockDisplay.class);
b5.setBlock(Bukkit.createBlockData("minecraft:oak_sign"));
}
/** Mimics PixelBlocks: 1/16-scaled block displays forming a mini build + the placeholder displays. */
private void pixelblock(Location o) {
float s = 1 / 16f;
String[] palette = {"minecraft:oak_planks", "minecraft:stone_bricks", "minecraft:glass", "minecraft:red_wool",
"minecraft:chest[facing=north]", "minecraft:red_bed[part=head,facing=north]", "minecraft:oak_stairs[facing=south]",
"minecraft:torch", "minecraft:grass_block"};
Random rnd = new Random(1);
for (int x = 0; x < 16; x++)
for (int z = 0; z < 16; z++) {
int h = 1 + (int) (3 * Math.abs(Math.sin(x * 0.4) * Math.cos(z * 0.3)));
for (int y = 0; y < h; y++) {
String b = y == h - 1 ? palette[rnd.nextInt(palette.length)] : "minecraft:stone_bricks";
pixel(o, x, y, z, s, Bukkit.createBlockData(b));
}
}
// Placeholder look (small pixel blocks): end-crystal item cores + glass container, next to it.
Location p = o.clone().add(-1.5, 0.5, 0.5);
for (int i = 0; i <= 90; i += 90) {
ItemDisplay core = w.spawn(p, ItemDisplay.class);
core.setRotation(i, 0);
core.setItemStack(it(Material.END_CRYSTAL));
Transformation t = core.getTransformation();
t.getScale().set(0.5);
core.setTransformation(t);
}
ItemDisplay glass = w.spawn(p, ItemDisplay.class);
glass.setItemStack(it(Material.WHITE_STAINED_GLASS));
Interaction hit = w.spawn(o.clone().add(0.5, 0, 0.5), Interaction.class);
hit.setInteractionWidth(1);
hit.setInteractionHeight(1);
}
private void pixel(Location o, int x, int y, int z, float s, BlockData data) {
Location l = o.clone().add(x * s + s / 2, y * s + s / 2, z * s + s / 2);
BlockDisplay d = w.spawn(l, BlockDisplay.class);
d.setBlock(data);
Transformation t = d.getTransformation();
t.getScale().set(s);
t.getTranslation().set(-s / 2, -s / 2, -s / 2);
d.setTransformation(t);
}
private void misc(Location o) {
w.dropItem(o.clone().add(0, 0.2, 0), it(Material.DIAMOND_SWORD), i -> i.setVelocity(new Vector()));
w.dropItem(o.clone().add(1, 0.2, 0), it(Material.OAK_LOG), i -> i.setVelocity(new Vector()));
w.dropItem(o.clone().add(2, 0.2, 0), new ItemStack(Material.COBBLESTONE, 32), i -> i.setVelocity(new Vector()));
w.dropItem(o.clone().add(3, 0.2, 0), it(Material.APPLE), i -> i.setVelocity(new Vector()));
FallingBlock fb = w.spawn(o.clone().add(4.5, 0.5, 0.5), FallingBlock.class, f -> f.setGravity(false));
fb.setBlockData(Bukkit.createBlockData("minecraft:sand"));
TNTPrimed tnt = w.spawn(o.clone().add(6, 0, 0.5), TNTPrimed.class);
tnt.setFuseTicks(10000);
tnt.setGravity(false);
w.spawn(o.clone().add(8, 0, 0.5), EnderCrystal.class);
Sheep sh = spawn(o, 0.5, 0, 3, 180, Sheep.class);
sh.setSheared(true);
Sheep sh2 = spawn(o, 2.5, 0, 3, 180, Sheep.class);
sh2.setColor(DyeColor.LIME);
ArmorStand small = spawn(o, 4.5, 0, 3, 180, ArmorStand.class);
small.setSmall(true);
small.getEquipment().setHelmet(it(Material.LEATHER_HELMET));
spawn(o, 6, 0, 3, 180, EntityType.EXPERIENCE_ORB);
Arrow a = w.spawn(o.clone().add(7, 0.5, 3), Arrow.class, ar -> ar.setGravity(false));
a.setVelocity(new Vector(1, -0.4, 0));
Trident tr = w.spawn(o.clone().add(9, 0.5, 3), Trident.class, ar -> ar.setGravity(false));
tr.setVelocity(new Vector(0, -1, 0.2));
spawn(o, 10, 0, 0, 180, TropicalFish.class);
}
private void dbg(Location o) {
TextDisplay t1 = spawn(o, 0, 1.2, 0, 0, TextDisplay.class);
t1.text(Component.text("Default Text"));
ItemDisplay i4 = spawn(o, 2, 0.5, 0.5, 0, ItemDisplay.class);
i4.setItemStack(it(Material.SHIELD));
getLogger().info("dbg text=" + t1.isValid() + " opacity=" + t1.getTextOpacity() + " shield=" + i4.getItemStack());
}
/** World blocks: a log column with shelf mushrooms (26.x) on all four sides, both growth stages. */
private void shelf(Location o) {
for (int y = 0; y < 3; y++) o.clone().add(0, y, 0).getBlock().setType(Material.OAK_LOG, false);
o.clone().add(0, 0, -3).getBlock().setType(Material.DIAMOND_BLOCK, false); // north marker
o.clone().add(3, 0, 0).getBlock().setType(Material.GOLD_BLOCK, false); // east marker
String[] f = {"north", "south", "east", "west"};
int[][] off = {{0, -1}, {0, 1}, {1, 0}, {-1, 0}};
for (int i = 0; i < 4; i++) {
for (int age = 0; age < 2; age++) {
o.clone().add(off[i][0], 1 + age, off[i][1]).getBlock().setBlockData(
Bukkit.createBlockData("minecraft:shelf_mushroom[age=" + age + ",facing=" + f[i] + "]"), false);
}
}
}
/** Blockstate y-rotations: ladders, stairs, wall torches, furnaces, logs on all sides of stone pillars. */
private void rotated(Location o) {
String[] f = {"north", "south", "east", "west"};
int[][] off = {{0, -1}, {0, 1}, {1, 0}, {-1, 0}};
String[] kinds = {"ladder[facing=%s]", "wall_torch[facing=%s]", "oak_stairs[facing=%s]", "furnace[facing=%s]"};
for (int k = 0; k < kinds.length; k++) {
Location c = o.clone().add(k * 3 - 1, 0, 2);
c.getBlock().setType(Material.STONE, false);
c.clone().add(0, 1, 0).getBlock().setType(Material.STONE, false);
for (int i = 0; i < 4; i++) {
c.clone().add(off[i][0], k == 2 || k == 3 ? 0 : 1, off[i][1]).getBlock().setBlockData(
Bukkit.createBlockData("minecraft:" + String.format(kinds[k], f[i])), false);
}
}
// Fence with a single connection to the east (stone post at +x), door facing east, chest-free check.
o.clone().add(-3, 0, 5).getBlock().setBlockData(Bukkit.createBlockData("minecraft:oak_fence[east=true]"), false);
o.clone().add(-2, 0, 5).getBlock().setType(Material.STONE, false);
o.clone().add(1, 0, 5).getBlock().setBlockData(
Bukkit.createBlockData("minecraft:oak_door[facing=east,half=lower,hinge=left,open=false]"), false);
o.clone().add(1, 1, 5).getBlock().setBlockData(
Bukkit.createBlockData("minecraft:oak_door[facing=east,half=upper,hinge=left,open=false]"), false);
o.clone().add(9, 0, 2).getBlock().setBlockData(Bukkit.createBlockData("minecraft:oak_log[axis=x]"), false);
o.clone().add(9, 0, 4).getBlock().setBlockData(Bukkit.createBlockData("minecraft:oak_log[axis=z]"), false);
}
/**
* Row z=0: single chests facing S/W/N/E (x=0,3,6,9). Row z=3: double chests per facing — the LEFT half
* at x, its RIGHT partner on facing.getClockWise(). Row z=6: trapped double (S), ender, copper, oxidized
* copper double (N).
*/
private void chests(Location o) {
String[] f = {"south", "west", "north", "east"};
int[][] cw = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}};
java.util.function.BiConsumer<int[], String> put = (p, data) -> o.clone().add(p[0], p[1], p[2]).getBlock()
.setBlockData(Bukkit.createBlockData("minecraft:" + data), false);
for (int i = 0; i < 4; i++) {
put.accept(new int[] {i * 3, 0, 0}, "chest[facing=" + f[i] + ",type=single]");
put.accept(new int[] {i * 3, 0, 3}, "chest[facing=" + f[i] + ",type=left]");
put.accept(new int[] {i * 3 + cw[i][0], 0, 3 + cw[i][1]}, "chest[facing=" + f[i] + ",type=right]");
}
put.accept(new int[] {0, 0, 6}, "trapped_chest[facing=south,type=left]");
put.accept(new int[] {-1, 0, 6}, "trapped_chest[facing=south,type=right]");
put.accept(new int[] {3, 0, 6}, "ender_chest[facing=south]");
put.accept(new int[] {6, 0, 6}, "copper_chest[facing=east,type=single]");
put.accept(new int[] {9, 0, 6}, "oxidized_copper_chest[facing=north,type=left]");
put.accept(new int[] {10, 0, 6}, "oxidized_copper_chest[facing=north,type=right]");
}
/** Redstone wire (dot, line, corner, climbing a wall, powered/unpowered) next to glass and panes. */
private void redstone(Location o) {
java.util.function.BiConsumer<int[], String> put = (p, data) -> o.clone().add(p[0], p[1], p[2]).getBlock()
.setBlockData(Bukkit.createBlockData("minecraft:" + data), false);
put.accept(new int[] {0, 0, 0}, "redstone_wire[power=0]");
for (int x = 1; x <= 4; x++) put.accept(new int[] {x, 0, 0}, "redstone_wire[east=side,west=side,power=" + (x * 3) + "]");
put.accept(new int[] {5, 0, 0}, "redstone_wire[west=side,north=side,power=15]");
put.accept(new int[] {5, 0, -1}, "redstone_wire[south=side,north=up,power=15]");
put.accept(new int[] {5, 0, -2}, "stone");
put.accept(new int[] {5, 1, -2}, "redstone_wire[south=side,power=15]");
put.accept(new int[] {0, 0, 3}, "glass");
put.accept(new int[] {1, 0, 3}, "red_stained_glass");
put.accept(new int[] {2, 0, 3}, "glass_pane[east=true,west=false]");
put.accept(new int[] {3, 0, 3}, "blue_stained_glass_pane[east=true,west=true]");
put.accept(new int[] {4, 0, 3}, "blue_stained_glass_pane[west=true,north=true]");
}
/** x/y blockstate rotations without uvlock: pistons in all six directions, observers, dispensers, logs. */
private void piston(Location o) {
String[] f = {"down", "up", "north", "south", "east", "west"};
for (int i = 0; i < f.length; i++) {
o.clone().add(i * 2, 1, 0).getBlock().setBlockData(
Bukkit.createBlockData("minecraft:piston[facing=" + f[i] + ",extended=false]"), false);
o.clone().add(i * 2, 1, 2).getBlock().setBlockData(
Bukkit.createBlockData("minecraft:observer[facing=" + f[i] + "]"), false);
o.clone().add(i * 2, 0, 2).getBlock().setBlockData(
Bukkit.createBlockData("minecraft:dispenser[facing=" + f[i] + "]"), false);
}
o.clone().add(0, 0, -2).getBlock().setBlockData(Bukkit.createBlockData("minecraft:oak_log[axis=x]"), false);
o.clone().add(2, 0, -2).getBlock().setBlockData(Bukkit.createBlockData("minecraft:oak_log[axis=z]"), false);
o.clone().add(4, 0, -2).getBlock().setBlockData(Bukkit.createBlockData("minecraft:furnace[facing=north]"), false);
}
/**
* Row z=0: oak log, basalt, quartz pillar, hay bale lying along x (x=90,y=90) and z (x=90). Row z=3: observer,
* dispenser, dropper facing up and down. Row z=6: oak and stone-brick stairs half=top in all four facings
* (x=180 + uvlock), oak buttons on the ceiling. A stone column at z=-2 marks north.
*/
private void xrot(Location o) {
java.util.function.BiConsumer<int[], String> put = (p, data) -> o.clone().add(p[0], p[1], p[2]).getBlock()
.setBlockData(Bukkit.createBlockData("minecraft:" + data), false);
String[] pillars = {"oak_log", "basalt", "quartz_pillar", "hay_block"};
for (int i = 0; i < pillars.length; i++) {
put.accept(new int[] {i * 3, 0, 0}, pillars[i] + "[axis=x]");
put.accept(new int[] {i * 3 + 1, 0, 0}, pillars[i] + "[axis=z]");
}
String[] vertical = {"observer", "dispenser", "dropper"};
for (int i = 0; i < vertical.length; i++) {
put.accept(new int[] {i * 3, 0, 3}, vertical[i] + "[facing=up]");
put.accept(new int[] {i * 3 + 1, 1, 3}, vertical[i] + "[facing=down]");
put.accept(new int[] {i * 3 + 1, 0, 3}, "glass");
}
String[] f = {"north", "east", "south", "west"};
for (int i = 0; i < f.length; i++) {
put.accept(new int[] {i * 2, 0, 6}, "oak_stairs[facing=" + f[i] + ",half=top]");
put.accept(new int[] {i * 2 + 1, 0, 6}, "stone_brick_stairs[facing=" + f[i] + ",half=top]");
}
put.accept(new int[] {9, 1, 6}, "stone");
put.accept(new int[] {9, 0, 6}, "oak_button[face=ceiling,facing=north]");
put.accept(new int[] {10, 1, 6}, "stone");
put.accept(new int[] {10, 0, 6}, "oak_button[face=ceiling,facing=east]");
put.accept(new int[] {0, 0, -2}, "stone");
put.accept(new int[] {0, 1, -2}, "stone");
}
private void vehicles(Location o) {
spawn(o, 0, 0, 0, 0, EntityType.CHEST_MINECART);
spawn(o, 1.8, 0, 0, 90, EntityType.FURNACE_MINECART);
spawn(o, 3.6, 0, 0, 0, EntityType.HOPPER_MINECART);
spawn(o, 5.4, 0, 0, 0, EntityType.TNT_MINECART);
spawn(o, 7.2, 0, 0, 0, EntityType.SPAWNER_MINECART);
spawn(o, 9, 0, 0, 0, EntityType.COMMAND_BLOCK_MINECART);
spawn(o, 0.5, 0, 3, 30, EntityType.PALE_OAK_BOAT);
spawn(o, 3, 0, 3, 60, EntityType.BAMBOO_CHEST_RAFT);
spawn(o, 6, 0, 3, 90, EntityType.CHERRY_CHEST_BOAT);
spawn(o, 9, 0, 3, 120, EntityType.MANGROVE_BOAT);
}
/**
* Alpha blending: a stained-glass wall (inner faces culled) and stacked glass colours in front of a striped
* wool wall, ice/tinted glass/slime/honey blocks, connected panes, glass under water, a slime and a sulfur
* cube (translucent outer skins, one behind glass) and a text display with the default background.
*/
private void translucent(Location o) {
java.util.function.BiConsumer<int[], String> put = (p, data) -> o.clone().add(p[0], p[1], p[2]).getBlock()
.setBlockData(Bukkit.createBlockData("minecraft:" + data), false);
for (int x = -1; x <= 10; x++)
for (int y = 0; y <= 4; y++) put.accept(new int[] {x, y, 5}, (x + y) % 2 == 0 ? "white_wool" : "black_wool");
for (int x = 0; x <= 2; x++) for (int y = 0; y <= 1; y++) put.accept(new int[] {x, y, 1}, "red_stained_glass");
put.accept(new int[] {3, 0, 1}, "blue_stained_glass");
put.accept(new int[] {3, 0, 0}, "yellow_stained_glass");
put.accept(new int[] {4, 0, 1}, "ice");
put.accept(new int[] {4, 1, 1}, "tinted_glass");
put.accept(new int[] {5, 0, 1}, "slime_block");
put.accept(new int[] {6, 0, 1}, "honey_block");
put.accept(new int[] {7, 0, 1}, "lime_stained_glass_pane[east=true]");
put.accept(new int[] {8, 0, 1}, "lime_stained_glass_pane[west=true,east=true]");
put.accept(new int[] {9, 0, 1}, "lime_stained_glass_pane[west=true]");
for (int x = 0; x <= 2; x++) put.accept(new int[] {x, -1, -2}, "water");
put.accept(new int[] {1, -2, -2}, "magenta_stained_glass");
put.accept(new int[] {0, -2, -2}, "sand");
put.accept(new int[] {2, -2, -2}, "sand");
Slime slime = spawn(o, 1.5, 0, 3, 200, Slime.class);
slime.setSize(2);
Slime front = spawn(o, 7.5, 0, -1.5, 160, Slime.class);
front.setSize(1);
EntityType sulfur = Registry.ENTITY_TYPE.get(NamespacedKey.minecraft("sulfur_cube"));
if (sulfur != null) spawn(o, 5.5, 0, -1.5, 200, sulfur);
TextDisplay t = spawn(o, 4.5, 2.6, 0, 0, TextDisplay.class);
t.text(Component.text("Default background"));
t.setBillboard(Display.Billboard.CENTER);
}
// --- conditional layers ----------------------------------------------------------------------
private void layers(Location o) {
tame(spawn(o, 0, 0, 0, 180, Cat.class), DyeColor.RED);
tame(spawn(o, 1.2, 0, 0, 180, Cat.class), DyeColor.BLUE);
spawn(o, 2.4, 0, 0, 180, Cat.class); // untamed: no collar
Cat kitten = spawn(o, 3.4, 0, 0, 180, Cat.class);
kitten.setBaby();
tame(kitten, DyeColor.YELLOW);
tame(spawn(o, 5, 0, 0, 180, Wolf.class), DyeColor.RED);
tame(spawn(o, 6.4, 0, 0, 180, Wolf.class), DyeColor.LIME);
spawn(o, 7.8, 0, 0, 180, Wolf.class); // untamed: wild texture, no collar
Wolf pup = spawn(o, 9.0, 0, 0, 180, Wolf.class);
pup.setBaby();
tame(pup, DyeColor.MAGENTA);
spawn(o, 0.5, 0, 2.5, 180, Creeper.class);
spawn(o, 2.2, 0, 2.5, 180, Creeper.class).setPowered(true);
hands(spawn(o, 4.2, 0, 2.5, 180, Fox.class), Material.EMERALD, null);
Fox kit = spawn(o, 5.5, 0, 2.5, 180, Fox.class);
kit.setBaby();
hands(kit, Material.SWEET_BERRIES, null);
hands(spawn(o, 7.5, 0, 2.5, 180, Dolphin.class), Material.COD, null);
// Spawned mobs get a random body yaw: face them all to the camera (north).
for (Entity e : w.getNearbyEntities(o.clone().add(5, 0, 1), 7, 3, 4))
if (e instanceof LivingEntity le && !(e instanceof Player)) le.setBodyYaw(180);
}
private void tame(Tameable t, DyeColor collar) {
t.setTamed(true);
if (t instanceof Cat c) c.setCollarColor(collar);
if (t instanceof Wolf wo) wo.setCollarColor(collar);
}
private void withers(Location o) {
spawn(o, 1, 0, 1, 180, Wither.class); // full health: no armor
Wither half = spawn(o, 6, 0, 1, 180, Wither.class);
half.setHealth(half.getAttribute(org.bukkit.attribute.Attribute.MAX_HEALTH).getValue() / 2 - 1);
for (Entity e : w.getNearbyEntities(o, 12, 8, 8)) if (e instanceof Wither wi) wi.setBodyYaw(180);
}
// --- projectiles -----------------------------------------------------------------------------
/** Spawns a projectile at p moving along dir, rotated like vanilla Projectile#shoot (yaw atan2(x,z), pitch up). */
private Projectile projectile(Location p, Vector dir, double speed, boolean gravity, Class<? extends Projectile> type) {
Vector d = dir.clone().normalize();
Location l = p.clone();
l.setYaw((float) Math.toDegrees(Math.atan2(d.getX(), d.getZ())));
l.setPitch((float) Math.toDegrees(Math.atan2(d.getY(), Math.hypot(d.getX(), d.getZ()))));
Projectile pr = w.spawn(l, type, e -> {
e.setPersistent(false);
e.setGravity(gravity);
e.setVelocity(d.clone().multiply(speed));
if (e instanceof AbstractArrow a) a.setPickupStatus(AbstractArrow.PickupStatus.DISALLOWED);
});
pr.setRotation(l.getYaw(), l.getPitch()); // spawning resets it to 0 (it would only lerp over ~20 ticks)
return pr;
}
private static Vector dir(double yawDeg, double pitchDeg) {
double y = Math.toRadians(yawDeg), p = Math.toRadians(pitchDeg);
return new Vector(Math.sin(y) * Math.cos(p), Math.sin(p), Math.cos(y) * Math.cos(p));
}
/** Hovering (almost still, no gravity) arrows, spectral arrows and tridents pointing radially away from a pillar. */
private void projFly(Location o) {
for (int y = 0; y <= 4; y++) {
o.clone().add(2, y, 2).getBlock().setType(Material.GOLD_BLOCK, false);
o.clone().add(11, y, 2).getBlock().setType(Material.DIAMOND_BLOCK, false);
}
double[][] rings = {{2.5, 0}, {4.3, 35}, {0.9, -35}}; // height, pitch
for (double[] ring : rings)
for (int i = 0; i < 8; i++) {
double yaw = i * 45;
Vector d = dir(yaw, ring[1]);
Vector h = dir(yaw, 0);
Location a = o.clone().add(2.5 + h.getX() * 1.3, ring[0], 2.5 + h.getZ() * 1.3);
projectile(a, d, 1e-4, false, (Class<? extends AbstractArrow>) (i % 2 == 0 ? Arrow.class : SpectralArrow.class));
Location t = o.clone().add(11.5 + h.getX() * 2.1, ring[0], 2.5 + h.getZ() * 2.1);
projectile(t, d, 1e-4, false, Trident.class);
}
}
/** Arrows and tridents shot into stone pillars from all four sides (rising, level+sideways, falling) and the floor. */
private void projStuck(Location o) {
for (int x = 0; x < 3; x++)
for (int z = 0; z < 3; z++)
for (int y = 0; y < 4; y++) {
o.clone().add(1 + x, y, 1 + z).getBlock().setType(Material.STONE, false);
o.clone().add(12 + x, y, 1 + z).getBlock().setType(Material.STONE, false);
}
double[][] pillars = {{2.5, 2.5}, {13.5, 2.5}}; // centres; arrows left, tridents right
for (int pi = 0; pi < 2; pi++) {
boolean trident = pi == 1;
double cx = pillars[pi][0], cz = pillars[pi][1];
for (int side = 0; side < 4; side++) {
double out = side * 90; // yaw (projectile convention) pointing away from the pillar
Vector n = dir(out, 0);
Vector t = new Vector(n.getZ(), 0, -n.getX());
double dist = 1.5 + 3.5;
// rising from low, level but 20 deg sideways, falling from high
double[][] shots = {{-1, 0.6, 25, 0}, {0, 1.6, 0, 20}, {1, 3.4, -25, 0}};
for (double[] sh : shots) {
Location p = o.clone().add(cx + n.getX() * dist + t.getX() * sh[0], sh[1], cz + n.getZ() * dist + t.getZ() * sh[0]);
Vector d = dir(out + 180 + sh[3], sh[2]);
if (trident) projectile(p, d, 2.0, true, Trident.class);
else projectile(p, d, 1.5, true, (Class<? extends AbstractArrow>) (side % 2 == 0 ? Arrow.class : SpectralArrow.class));
}
// steep shot into the floor, diagonally away from the pillar
Vector diag = dir(out + 45, 0);
Location p = o.clone().add(cx + diag.getX() * 2.6, 3, cz + diag.getZ() * 2.6);
Vector d = dir(out + 45, trident ? -70 : -60);
if (trident) projectile(p, d, 2.0, true, Trident.class);
else projectile(p, d, 1.5, true, Arrow.class);
}
}
}
}