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

2240 lines
118 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 eu.mhsl.minecraft.pixelpics.render.entity.ObjectBaker;
import eu.mhsl.minecraft.pixelpics.survival.CameraItems;
import eu.mhsl.minecraft.pixelpics.utils.ImageMapRenderer;
import eu.mhsl.minecraft.pixelpics.utils.MapImageDither;
import eu.mhsl.minecraft.pixelpics.utils.MapManager;
import io.papermc.paper.registry.RegistryAccess;
import io.papermc.paper.registry.RegistryKey;
import io.papermc.paper.world.WeatheringCopperState;
import java.awt.Graphics2D;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import org.bukkit.attribute.Attribute;
import org.bukkit.block.Banner;
import org.bukkit.block.Campfire;
import org.bukkit.block.CreatureSpawner;
import org.bukkit.block.DecoratedPot;
import org.bukkit.block.Shelf;
import org.bukkit.block.Sign;
import org.bukkit.block.TrialSpawner;
import org.bukkit.block.banner.Pattern;
import org.bukkit.block.banner.PatternType;
import org.bukkit.block.sign.Side;
import org.bukkit.inventory.meta.BannerMeta;
import org.bukkit.inventory.meta.CrossbowMeta;
import org.bukkit.inventory.meta.LeatherArmorMeta;
import org.bukkit.inventory.meta.MapMeta;
import org.bukkit.inventory.meta.ShieldMeta;
import org.bukkit.map.MapView;
import org.bukkit.potion.PotionType;
import org.bukkit.util.EulerAngle;
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.Biome;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.BlockData;
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 {
/** {@code index}: the scene is built at world (5000 + index·64, 200, 5000); camera relative to that origin. */
record Scene(String name, int index, 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;
private void scene(String name, Consumer<Location> build, double camX, double camY, double camZ, double lookX,
double lookY, double lookZ) {
scenes.put(name, new Scene(name, scenes.size(), build, camX, camY, camZ, lookX, lookY, lookZ));
}
@Override
public void onEnable() {
// Each scene sits on its own stone platform; the camera is placed relative to the platform origin.
scene("held", this::held, 4.4, 2.6, -8.5, 4.4, 1.0, 1);
scene("equip", this::equip, 4, 3, -8.5, 4, 1.0, 1.5);
scene("riding", this::riding, 5, 3, -7.5, 5, 1.2, 1);
scene("leash", this::leash, 5, 4, -9, 5, 2.5, 2);
scene("displays", this::displays, 5, 3, -9, 5, 1.0, 1.5);
scene("pixelblock", this::pixelblock, 1.5, 1.9, -1.3, 0.5, 0.5, 0.5);
scene("misc", this::misc, 5, 3, -7, 5, 0.8, 1.5);
scene("dbg", this::dbg, 1, 1.6, -3, 1, 0.8, 0.5);
scene("shelf", this::shelf, 3.5, 3.2, -2.5, 0.5, 1.5, 0.5);
scene("shelftop", this::shelf, 0.52, 6, 0.5, 0.5, 0, 0.5);
scene("rotated", this::rotated, 3, 4.5, -4, 3, 0.5, 2);
scene("rotated2", this::rotated, -0.5, 3.5, 9.5, -0.5, 0.5, 5);
scene("redstone", this::redstone, 3, 3, -3, 3, 0, 2);
scene("piston", this::piston, 4.5, 3.2, -3.5, 5, 1, 1);
scene("pistonback", this::piston, 5.5, 3.2, 5.5, 5, 1, 1);
scene("pistondown", this::piston, -1.6, 2.6, -1.6, 0.5, 1.4, 0.5);
scene("pistonup", this::piston, 0.4, 2.6, -1.6, 2.5, 1.4, 0.5);
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.
scene("xrot", this::xrot, 3.5, 5.5, -4.5, 6, 0.5, 3.5);
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).
scene("chests_s", this::chests, 4.5, 5, 13, 4.5, 0, 3);
scene("chests_n", this::chests, 4.5, 5, -7, 4.5, 0, 3);
scene("chests_e", this::chests, 16, 5, 3, 4.5, 0, 3);
scene("chests_w", this::chests, -7, 5, 3, 4.5, 0, 3);
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.
scene("proj_fly_top", this::projFly, 6.5, 13, 2.2, 6.5, 0, 2);
scene("proj_fly_side", this::projFly, 6.5, 2.6, -8, 6.5, 2.4, 2);
scene("proj_fly_obl", this::projFly, -2, 6, -6, 6.5, 2, 2);
scene("proj_stuck_nw", this::projStuck, -3, 6, -6, 8, 1.5, 2);
scene("proj_stuck_se", this::projStuck, 19, 6, 10, 8, 1.5, 2);
scene("proj_stuck_ne", this::projStuck, 19, 6, -6, 8, 1.5, 2);
scene("proj_fly_gold", this::projFly, 2.6, 8.5, 2.5, 2.5, 0, 2.5);
scene("proj_fly_gold_side", this::projFly, 2.5, 2.6, -3.5, 2.5, 2.4, 2.5);
scene("proj_stuck_close_a", this::projStuck, -2.5, 2.4, -2.5, 2.5, 1.6, 1);
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).
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.
scene("layers", this::layers, 4.8, 2.4, -5.5, 4.8, 0.6, 1.5);
scene("layers_side", this::layers, 12.5, 2.2, 5.5, 5, 0.5, 2.5);
scene("layers_close", this::layers, 5.5, 1.3, 0.3, 5.5, 0.4, 2.5);
scene("layers_pets", this::layers, 4.5, 1.6, -3.2, 4.5, 0.3, 0);
scene("layers_dolphin", this::layers, 10, 1.0, 1.0, 7.5, 0.4, 2.0);
scene("layers_cats", this::layers, 1.7, 1.1, -1.6, 1.7, 0.3, 0);
scene("layers_cats_side", this::layers, 1.7, 1.6, 1.4, 1.7, 0.2, 0);
scene("layers_top", this::layers, 5.5, 6, 2.0, 5.5, 0, 2.5);
scene("layers_wither", this::withers, 3.5, 4, -9, 3.5, 2.2, 1);
scene("layers_wither_close", this::withers, 6, 3.4, -3.5, 6, 2.6, 1);
// Block entities (vanilla BlockEntityRenderer poses): signs, banners, heads in all 16 rotations + wall
// mounts, and bells/pots/shulker boxes/lecterns/conduits, each layout seen from two sides.
scene("be_signs_s", this::beSigns, 7, 3, 8, 7, 0.5, 1);
scene("be_signs_n", this::beSigns, 7, 1.3, -8, 7, 0.6, 1);
scene("be_signs_close", this::beSigns, 3, 1.8, 4.2, 3, 0.8, 2);
scene("be_signs_wall", this::beSigns, 4.6, 2.2, 10.2, 3.5, 0.9, 7);
scene("be_signs_wall_n", this::beSigns, 1.4, 2.2, 3.8, 3.5, 0.9, 7);
scene("be_signs_wall2", this::beSigns, 12.5, 2.4, 10.2, 10, 1.2, 7);
scene("be_signs_hang", this::beSigns, 4, 1.3, 0.2, 4, 1.4, -3);
scene("be_banners_s", this::beBanners, 7, 3, 10, 7, 1, 1.5);
scene("be_banners_n", this::beBanners, 7, 3, -6, 7, 1, 1.5);
scene("be_banners_wall", this::beBanners, 13, 3.5, 9, 16, 1.5, 5);
scene("be_banners_wall_n", this::beBanners, 13, 3.5, 1, 16, 1.5, 5);
scene("be_heads_s", this::beHeads, 5, 5, 11, 5, 0, 3);
scene("be_heads_n", this::beHeads, 5, 5, -6, 5, 0, 3);
scene("be_heads_close", this::beHeads, 4, 2.2, 9.5, 4, 0.3, 5);
scene("be_heads_wall_w", this::beHeads, 7, 3, 2.5, 12, 1, 2.5);
scene("be_heads_wall_e", this::beHeads, 17, 3, 2.5, 12, 1, 2.5);
scene("be_misc_s", this::beMisc, 6, 5, 11, 6, 0.5, 3);
scene("be_misc_n", this::beMisc, 6, 5, -6, 6, 0.5, 3);
scene("be_misc_close", this::beMisc, 3, 2.5, 3, 3, 0.5, 0);
scene("be_misc_lectern", this::beMisc, 3, 2.2, 9.5, 3, 0.6, 6);
scene("be_misc_shulker", this::beMisc, 11, 2.8, 9.5, 11, 0.3, 4);
scene("be_items", this::beItems, 4.5, 2.6, 11, 4.5, 0.8, 3);
scene("be_items_back", this::beItems, 4.5, 2.6, -5, 4.5, 0.8, 3);
scene("proj_stuck_sw", this::projStuck, -3, 6, 10, 8, 1.5, 2);
// Data-driven variants / pose flags / armor wearers (vanilla dump: asset_id, setupAnim flags, armor layers).
scene("rf_cats", this::rfCats, 6.5, 2.6, -4.5, 6.5, 0.2, 1.2);
scene("rf_wolves", this::rfWolves, 5.5, 3.2, -5.5, 5.5, 0.2, 1.8);
scene("rf_farm", this::rfFarm, 6, 3.4, -6, 6, 0.3, 1.8);
scene("rf_stands", this::rfStands, 5, 2.4, -4.5, 5, 0.9, 0);
scene("rf_stands_back", this::rfStands, 5, 2.4, 4.5, 5, 0.9, 0);
scene("rf_chests", this::rfChests, 7, 4, -7, 7, 0.8, 1);
scene("rf_chests_side", this::rfChests, 7, 2.5, 7, 7, 0.8, 1);
// Needs a datapack cat variant "pptest:ocelotcat" (asset_id entity/cat/ocelot) in the world's datapacks.
scene("rf_dpcat", this::rfDatapackCat, 1.2, 1.4, -2.2, 1.2, 0.2, 0.5);
scene("rf_armor", this::rfArmor, 7, 3.2, -6.5, 7, 1.0, 1.5);
// Item frames (vanilla ItemFrameRenderer): north wall with rotations 0-7, block/flat/tool/block-entity
// items, photo + synthetic + vanilla-data maps, invisible and glow frames; frames on all 6 block sides.
scene("frames", this::frames, 4.5, 2.8, -3.5, 4.5, 2.6, 4);
scene("frames_close", this::frames, 2.6, 2.4, 0.8, 2.6, 2.3, 4);
scene("frames_side", this::frames, -1.5, 3.2, -1.0, 4.5, 2.6, 3.6);
scene("frames_dirs_ne", this::framesDirs, 16, 7.5, -4, 8, 4, 3.5);
scene("frames_dirs_sw", this::framesDirs, 0, 1.2, 10, 8, 4, 3.5);
// Camera projection (pinhole, vertical FOV 70): frontal checkerboard wall / item-frame map (squares must be
// square, same size everywhere), ceiling straight up, floor straight down at yaw 30, oblique wall with
// stripes (edges must stay straight, the vertical stripe through the view centre vertical). *_sq = 384x384.
scene("cam_checker_sq", this::camChecker, 5.5, 5.5, 1, 5.5, 5.5, 6);
scene("cam_checker", this::camChecker, 5.5, 5.5, 1, 5.5, 5.5, 6);
scene("cam_frame_sq", this::camFrame, 5.5, 5.5, 3, 5.5, 5.5, 6);
scene("cam_up_sq", this::camChecker, 5.5, 6, 0.5, 5.5, 11, 0.5);
scene("cam_down_sq", this::camChecker, 5.5, 4, 0.5, 5.4995, 0, 0.500866);
scene("cam_oblique", this::camStripes, 2, 11, 1.5, 5.5, 5.5, 6);
// Performance (entity BVH): a 15x18 animal farm (270 mobs, randomizeData=false, fixed body yaw) and 300
// item displays from a seeded RNG -> deterministic, pixel-diffable; the render time is logged.
scene("perf_farm", this::perfFarm, -6, 9, -13, 4, 0, 0);
scene("perf_farm_close", this::perfFarm, 0.5, 3, -11.5, 5, 0.5, -4);
scene("perf_items", this::perfItems, 0.5, 5, -10, 5, 0, -2);
// Per-mob texture compositing (tinted-texture cache): villager type/profession/level, sheep colours, dyed
// leather armor, wolf collars and patterned banners, all from a seeded RNG (deterministic).
scene("perf_village", this::perfVillage, -6, 9, -13, 4, 0, 0);
scene("perf_village_close", this::perfVillage, 0.5, 3, -11.5, 5, 0.5, -4);
// Entity poses (vanilla setupAnim/renderer state from the dump): sitting/sleeping/crouching pets with mouth
// items, cats lying, parrots, pandas; camel/sniffer/bat/dolphin pitch/fish in water/sleeping villager/
// mannequin poses/baby rider; armor stands with custom part poses. All rfSpawn (deterministic).
scene("pose_pets", this::posePets, 4.3, 3.6, -6, 4.3, 0.2, 2);
scene("pose_pets_side", this::posePets, -3, 3, -3, 4, 0, 2);
scene("pose_fox_close", this::posePets, 2.2, 1.4, -2.2, 2.2, 0.2, 0);
scene("pose_misc", this::poseMisc, 6.5, 5, -10, 6.5, 0.5, 0);
scene("pose_misc_side", this::poseMisc, -4, 3, -7, 6, 0.5, -1);
scene("pose_humans", this::poseHumans, 6, 3.5, -7, 6, 0.6, 1);
scene("pose_humans_side", this::poseHumans, -3, 3, -4, 5, 0.5, 1);
scene("pose_humans_close", this::poseHumans, 4.8, 1.6, -5.2, 4.8, 0.5, -2.5);
scene("pose_beds", this::poseHumans, 1.3, 2.6, -3.2, 1.3, 0.4, 0.2);
scene("pose_stands", this::poseStands, 4, 2.4, -5, 4, 0.9, 0);
scene("pose_stands_side", this::poseStands, -3, 2.4, -3, 4, 0.9, 0);
// Entity texture / layer states (rfSpawn, deterministic): emissive eyes (spider, cave spider, enderman,
// phantom, breeze, copper golem, warden), sheep wool colour / undercoat / baby wool / jeb_, angry wolves,
// partial-alpha cutout textures (llama, tropical fish); small armor stands with armor, elytra at rest,
// piglin / illager arm poses, skeleton holding a sword, invulnerable wither, tipped / potion arrows.
scene("rf_vlayers", this::rfVisualLayers, 5.5, 3.4, -6, 5.5, 0.6, 2.5);
scene("rf_vlayers_close", this::rfVisualLayers, 3.2, 1.5, -2.4, 3.2, 0.5, 1);
scene("rf_vstates", this::rfVisualStates, 5.5, 3.4, -6, 5.5, 0.9, 2.5);
scene("rf_vstates_close", this::rfVisualStates, 3.2, 1.7, -2.6, 3.2, 0.9, 0.5);
// Block geometry (vanilla FaceBakery): rescaled element rotations (cross plants, amethyst, campfire, fire,
// raised rails, tripwire hooks, coral wall fans, dripleaf, stems, sea pickles), Euler x/y/z element
// rotations (hanging sign rot 3/7/11/15 chains), inverted elements (spawner/vault inner faces), 22.5° steps.
scene("geo_plants", this::geoPlants, 5.5, 2.6, -3.2, 5.5, 0.4, 2.2);
scene("geo_plants_close", this::geoPlants, 2.5, 1.1, -0.9, 2.5, 0.5, 1);
scene("geo_misc", this::geoMisc, 6, 3.4, -4.5, 6, 0.5, 3);
scene("geo_misc_close", this::geoMisc, 3, 1.6, -1.6, 3, 0.4, 1);
scene("geo_misc_signs", this::geoMisc, 4, 2.4, 3.5, 4, 2.2, 7);
// Water depth vs. translucency: a pool with columns 1-6 blocks deep (x pairs), each with a glass block,
// a gold block and white stained glass on the sand bottom; seen from the shore, from above and from inside.
scene("geo_water", this::geoWater, 6, 3.5, -5, 6, -3, 2.5);
scene("geo_water_top", this::geoWater, 6, 9, 2.4, 6, -4, 2.5);
scene("geo_water_under", this::geoWater, 11.5, -3.3, 0.3, 4, -5, 3);
// Block-entity contents: enchanting-table book (closed; open when the camera is within 3 blocks), campfire
// and shelf items, copper golem statues (4 poses x stages), spawner/trial-spawner mobs, vault, beacon beam
// through red+blue glass, 3x3 end portal and an end gateway. Rendered 100 ticks after building (beacon
// levels update every 80 ticks).
scene("be_contents", this::beContents, 7, 4.5, 13, 7, 0.8, 3);
scene("be_contents_n", this::beContents, 7, 4.5, -7, 7, 0.8, 3);
scene("be_contents_close", this::beContents, 1.2, 1.8, 2.2, 1.5, 0.6, 0);
scene("be_contents_beam", this::beContents, 4, 6, 26, 10, 9, 6);
scene("be_contents_portal", this::beContents, 11.5, 3.6, -1.8, 11.5, 0, 1.2);
// Items in detail (decompiled 26.3 special renderers): shields (plain, base colour, banner patterns), chests,
// spawner/vault held by armor stands (+ a baby zombie: baby hand offset), item displays (fixed), item frames
// (rotation 0/1) and dropped (ground: lift = 0.1625 - model min y). items_inverted: spawner, trial spawner and
// vault as block, block display, item display and dropped item (inverted inner faces). xmas_chests: chests,
// double/trapped/ender/copper chests + chest items; render it once normally and once with the server
// started with -Dpixelpics.testDate=2026-12-25 (christmas textures for chests and trapped chests).
// frames_markers: map decorations on framed maps (banner markers via toggleBanner, frame/target markers,
// an invisible-on-frames player marker; rotations 0/1/3), a PixelPics photo map and an undecorated map.
scene("items_detail", this::itemsDetail, 4.5, 3.2, 13, 4.5, 0.9, 2);
scene("items_detail_close", this::itemsDetail, 2.2, 1.7, 7.4, 2.2, 0.9, 2);
scene("items_detail_side", this::itemsDetail, -4.5, 2.4, 4.5, 5, 0.9, 3);
scene("items_inverted", this::itemsInverted, 3, 2.4, 7, 3, 0.6, 1.5);
scene("xmas_chests", this::xmasChests, 6, 3.6, 9.5, 6, 0.5, 1.5);
scene("frames_markers", this::framesMarkers, 2.5, 2.4, 4.2, 2.5, 2.4, 0);
// Fluids (vanilla FluidRenderer / FogRenderer): liq_flow = water flowing with physics (rendered 200 ticks after
// building): a source on a 3-high terrace spreading and falling over its edges, a lone source spreading on
// the floor, a sunk 3x3 source pool (surface 8/9 below the rim). liq_logged = waterlogged blocks in a 1-deep
// pool, standing in the air (water inside, surface at 8/9) and under water. liq_under* = camera in a
// 22x22x8 tank with concrete pillars at growing distances (water fog), seen from 3 and 7 deep, looking up at
// the surface (bridge + lily pads above), just under the surface, from above; _swamp/_warm = same tank in a
// swamp / warm ocean (water_fog_color, swamp end x0.85, water_color). liq_lava = lava pool, flowing levels
// and a falling column; liq_lava_in = camera inside the lava (lava fog).
scene("liq_flow", this::liqFlow, -3, 8, -9, 6, 0, 0);
scene("liq_flow_close", this::liqFlow, 7.5, 1.6, 9, 11, 0, 5);
scene("liq_flow_fall", this::liqFlow, -4, 3, 7, 2, 1.5, 0);
scene("liq_logged", this::liqLogged, 6, 3.6, -4.5, 6, -0.5, 3.5);
scene("liq_logged_close", this::liqLogged, 2.2, 0.7, -1.4, 3, -0.4, 1.5);
scene("liq_logged_air", this::liqLogged, 4, 1.6, 2.6, 4.5, 0.4, 5.5);
scene("liq_under", o -> liqUnder(o, Biome.PLAINS), -3.5, -3, -8.5, 12, -5, 6);
scene("liq_under_deep", o -> liqUnder(o, Biome.PLAINS), -3.5, -7, -8.5, 12, -6, 6);
scene("liq_under_up", o -> liqUnder(o, Biome.PLAINS), 6.2, -6, -4, 6.4, 0, 0.5);
scene("liq_under_shallow", o -> liqUnder(o, Biome.PLAINS), -3.5, -0.45, -8.5, 12, 0.6, 6);
scene("liq_under_top", o -> liqUnder(o, Biome.PLAINS), -3, 4, -10, 6, -6, 2);
scene("liq_under_swamp", o -> liqUnder(o, Biome.SWAMP), -3.5, -3, -8.5, 12, -5, 6);
scene("liq_under_warm", o -> liqUnder(o, Biome.WARM_OCEAN), -3.5, -3, -8.5, 12, -5, 6);
scene("liq_under_warm_top", o -> liqUnder(o, Biome.WARM_OCEAN), -3, 4, -10, 6, -6, 2);
scene("liq_lava", this::liqLava, 4, 4.5, -6, 9, 0, 1);
scene("liq_lava_in", this::liqLava, 3.5, -0.5, 1.5, 9, -0.5, 1.5);
}
private static ItemStack shield(DyeColor base, Object... patterns) {
ItemStack s = it(Material.SHIELD);
ShieldMeta m = (ShieldMeta) s.getItemMeta();
m.setBaseColor(base);
for (int i = 0; i + 1 < patterns.length; i += 2)
m.addPattern(new Pattern((DyeColor) patterns[i], (PatternType) patterns[i + 1]));
s.setItemMeta(m);
return s;
}
private void itemsDetail(Location o) {
for (int x = -1; x <= 11; x++) for (int y = 0; y <= 3; y++) put(o, x, y, -1, "stone");
ItemStack[] items = {it(Material.SHIELD), shield(DyeColor.RED),
shield(DyeColor.BLUE, DyeColor.YELLOW, PatternType.CREEPER,
DyeColor.WHITE, PatternType.BORDER),
it(Material.CHEST), it(Material.TRAPPED_CHEST), it(Material.ENDER_CHEST), it(Material.COPPER_CHEST),
it(Material.SPAWNER), it(Material.VAULT), it(Material.RED_BANNER)};
for (int i = 0; i < items.length; i++) {
frame(o, i, 1, 0, BlockFace.SOUTH, false, items[i], 0, true);
frame(o, i, 2, 0, BlockFace.SOUTH, false, items[i], 1, true);
ArmorStand as = spawn(o, i + 0.5, 0, 2.5, 0, ArmorStand.class);
as.setArms(true);
as.getEquipment().setItemInMainHand(items[i]);
if (i < 3) as.getEquipment().setItemInOffHand(items[i]);
ItemDisplay d = spawn(o, i + 0.5, 0.6, 4.5, 0, ItemDisplay.class);
d.setItemStack(items[i]);
d.setItemDisplayTransform(ItemDisplay.ItemDisplayTransform.FIXED);
d.setTransformation(new Transformation(new Vector3f(), new Quaternionf(), new Vector3f(0.6f), new Quaternionf()));
drop(o, i + 0.5, 0, 6.5, items[i], i * 20);
}
Zombie baby = spawn(o, 10.5, 0, 2.5, 0, Zombie.class);
baby.setBaby();
baby.getEquipment().setItemInMainHand(it(Material.CHEST));
baby.getEquipment().setItemInOffHand(items[2]);
Zombie z = spawn(o, 11.5, 0, 4, 0, Zombie.class);
z.setAdult();
z.getEquipment().setItemInMainHand(it(Material.TRAPPED_CHEST));
z.getEquipment().setItemInOffHand(items[2]);
}
private void itemsInverted(Location o) {
String[] blocks = {"spawner", "trial_spawner", "vault[facing=south]"};
Material[] mats = {Material.SPAWNER, Material.TRIAL_SPAWNER, Material.VAULT};
for (int i = 0; i < blocks.length; i++) {
put(o, i * 2, 0, 0, blocks[i]);
BlockDisplay b = spawn(o, i * 2, 0, 2, 0, BlockDisplay.class);
b.setBlock(Bukkit.createBlockData("minecraft:" + blocks[i]));
ItemDisplay d = spawn(o, i * 2 + 0.5, 0.5, 4.5, 0, ItemDisplay.class);
d.setItemStack(it(mats[i]));
drop(o, i * 2 + 0.5, 0, 6, it(mats[i]), 15);
}
}
private void xmasChests(Location o) {
put(o, 0, 0, 0, "chest[facing=south]");
put(o, 3, 0, 0, "chest[facing=south,type=left]");
put(o, 2, 0, 0, "chest[facing=south,type=right]");
put(o, 5, 0, 0, "trapped_chest[facing=south]");
put(o, 8, 0, 0, "trapped_chest[facing=south,type=left]");
put(o, 7, 0, 0, "trapped_chest[facing=south,type=right]");
put(o, 10, 0, 0, "ender_chest[facing=south]");
put(o, 12, 0, 0, "copper_chest[facing=south]");
Material[] mats = {Material.CHEST, Material.TRAPPED_CHEST};
for (int i = 0; i < 2; i++) {
ItemDisplay d = spawn(o, 0.5 + i * 1.5, 0.6, 3.5, 0, ItemDisplay.class);
d.setItemStack(it(mats[i]));
d.setItemDisplayTransform(ItemDisplay.ItemDisplayTransform.FIXED);
ArmorStand as = spawn(o, 4 + i * 1.5, 0, 3.5, 0, ArmorStand.class);
as.setArms(true);
as.getEquipment().setItemInMainHand(it(mats[i]));
drop(o, 7.5 + i, 0, 3.5, it(mats[i]), 30);
put(o, 10 + i, 0, 3, "stone");
frame(o, 10 + i, 1, 3, BlockFace.UP, false, it(mats[i]), 0, true);
}
}
private void framesMarkers(Location o) {
for (int x = -2; x <= 7; x++) for (int y = 0; y <= 4; y++) put(o, x, y, -1, "stone");
ItemStack marked = vanillaMap();
MapView v = ((MapMeta) marked.getItemMeta()).getMapView();
v.setScale(MapView.Scale.CLOSEST);
v.setCenterX(o.getBlockX());
v.setCenterZ(o.getBlockZ());
put(o, -5, 0, -10, "red_banner[rotation=0]");
put(o, 16, 0, -11, "lime_banner[rotation=4]");
mapBanner(v, o.clone().add(-5, 0, -10));
mapBanner(v, o.clone().add(16, 0, -11));
ItemFrame f0 = frame(o, 0, 2, 0, BlockFace.SOUTH, false, marked, 0, true);
frame(o, 1, 2, 0, BlockFace.SOUTH, false, marked, 1, true);
frame(o, 2, 2, 0, BlockFace.SOUTH, false, synthMap(), 0, true);
frame(o, 3, 2, 0, BlockFace.SOUTH, false, vanillaMap(), 0, true);
frame(o, 4, 2, 0, BlockFace.SOUTH, true, marked, 3, true);
// Frame markers like MapItemSavedData.tickCarriedBy (key frame-<id>, frame block pos, rot = 2D facing · 90):
// the real frame, plus fictitious ones turned east/north; a red X and a player marker (not on frames).
mapDecoration(v, "FRAME", "frame-" + f0.getEntityId(), o.getBlockX(), o.getBlockZ(), 0);
mapDecoration(v, "FRAME", "frame-pptest-1", o.getBlockX() + 30, o.getBlockZ() - 25, 270);
mapDecoration(v, "FRAME", "frame-pptest-2", o.getBlockX() - 28, o.getBlockZ() + 30, 180);
mapDecoration(v, "RED_X", "pptest-x", o.getBlockX() + 40, o.getBlockZ() + 40, 180);
mapDecoration(v, "PLAYER", "pptest-player", o.getBlockX() + 10, o.getBlockZ() + 10, 0);
}
/** MapItemSavedData.toggleBanner(level, pos) on the map's server data (reflective: no API). */
private void mapBanner(MapView v, Location banner) {
try {
Object data = mapData(v);
Object level = handle(w);
Class<?> bp = Class.forName("net.minecraft.core.BlockPos", true, data.getClass().getClassLoader());
Object pos = bp.getConstructor(int.class, int.class, int.class)
.newInstance(banner.getBlockX(), banner.getBlockY(), banner.getBlockZ());
for (Method m : data.getClass().getMethods())
if (m.getName().equals("toggleBanner") && m.getParameterCount() == 2)
getLogger().info("toggleBanner " + banner.toVector() + " -> " + m.invoke(data, level, pos));
} catch (ReflectiveOperationException e) {
getLogger().warning("mapBanner: " + e);
}
}
/** MapItemSavedData.addDecoration(MapDecorationTypes.<type>, level, key, x, z, yRot, null) (private, reflective). */
private void mapDecoration(MapView v, String type, String key, double x, double z, double rot) {
try {
Object data = mapData(v);
Object level = handle(w);
Object holder = Class.forName("net.minecraft.world.level.saveddata.maps.MapDecorationTypes", true,
data.getClass().getClassLoader()).getField(type).get(null);
for (Method m : data.getClass().getDeclaredMethods())
if (m.getName().equals("addDecoration") && m.getParameterCount() == 7) {
m.setAccessible(true);
m.invoke(data, holder, level, key, x, z, rot, null);
}
} catch (ReflectiveOperationException e) {
getLogger().warning("mapDecoration: " + e);
}
}
private static Object mapData(MapView v) throws ReflectiveOperationException {
Field wm = v.getClass().getDeclaredField("worldMap");
wm.setAccessible(true);
return wm.get(v);
}
private void beContents(Location o) {
put(o, 0, 0, 0, "enchanting_table");
put(o, 2, 0, 0, "campfire[facing=south,lit=true]");
put(o, 4, 0, 0, "soul_campfire[facing=east,lit=false]");
put(o, 6, 0, 0, "oak_shelf[facing=south]");
put(o, 8, 0, 0, "cherry_shelf[facing=south,powered=false]");
if (o.clone().add(2, 0, 0).getBlock().getState() instanceof Campfire c) {
Material[] food = {Material.BEEF, Material.COD, Material.PORKCHOP, Material.POTATO};
for (int i = 0; i < 4; i++) {
c.setItem(i, it(food[i]));
c.setCookTimeTotal(i, 1_000_000); // else the lit campfire pops them out at once (total 0)
}
c.stopCooking(); // keep the food raw and in place while the scene waits
c.update(true, false);
}
if (o.clone().add(4, 0, 0).getBlock().getState() instanceof Campfire c) {
c.setItem(0, it(Material.SALMON));
c.setItem(2, it(Material.CHICKEN));
c.update(true, false);
}
Material[][] shelves = {{Material.DIAMOND, Material.BOOK, Material.OAK_LOG}, {Material.GOLDEN_APPLE, null, Material.LANTERN}};
for (int k = 0; k < 2; k++) {
if (o.clone().add(6 + 2 * k, 0, 0).getBlock().getState() instanceof Shelf sh) {
for (int i = 0; i < 3; i++) if (shelves[k][i] != null) sh.getSnapshotInventory().setItem(i, it(shelves[k][i]));
sh.update(true, false);
}
}
String[] poses = {"standing", "running", "sitting", "star"};
String[] stages = {"", "exposed_", "weathered_", "waxed_oxidized_"};
String[] facing = {"south", "south", "east", "west"};
for (int i = 0; i < 4; i++)
put(o, i * 2, 0, 3, stages[i] + "copper_golem_statue[copper_golem_pose=" + poses[i] + ",facing=" + facing[i] + "]");
put(o, 0, 0, 6, "spawner");
put(o, 2, 0, 6, "spawner");
put(o, 4, 0, 6, "trial_spawner[trial_spawner_state=waiting_for_players]");
put(o, 6, 0, 6, "vault[facing=south]");
EntityType[] mobs = {EntityType.ZOMBIE, EntityType.PIG};
for (int i = 0; i < 2; i++) {
if (o.clone().add(i * 2, 0, 6).getBlock().getState() instanceof CreatureSpawner cs) {
cs.setSpawnedType(mobs[i]);
cs.update(true, false);
}
}
if (o.clone().add(4, 0, 6).getBlock().getState() instanceof TrialSpawner ts) {
ts.getNormalConfiguration().setSpawnedType(EntityType.SKELETON);
ts.update(true, false);
put(o, 4, 0, 6, "trial_spawner[trial_spawner_state=waiting_for_players]");
}
// Beacon on a 3x3 iron pyramid, red glass 2 above, blue glass 4 above (red+blue = averaged section).
for (int x = -1; x <= 1; x++) for (int z = -1; z <= 1; z++) put(o, 10 + x, -1, 6 + z, "iron_block");
put(o, 10, 0, 6, "beacon");
put(o, 10, 2, 6, "red_stained_glass");
put(o, 10, 4, 6, "blue_stained_glass_pane");
for (int x = 0; x < 3; x++) for (int z = 0; z < 3; z++) put(o, 10 + x, 0, z, "end_portal");
put(o, 14, 1, 1, "end_gateway");
put(o, 14, 0, 1, "obsidian");
}
private void rfVisualLayers(Location o) {
// Row 1: emissive eye layers.
rfSpawn(o, 0, 0, 0, 180, Spider.class);
rfSpawn(o, 1.6, 0, 0, 180, CaveSpider.class);
rfSpawn(o, 3.2, 0, 0, 180, Enderman.class);
rfSpawn(o, 4.8, 1.2, 0, 180, Phantom.class);
rfSpawn(o, 6.4, 0, 0, 180, Breeze.class);
rfSpawn(o, 8.0, 0, 0, 180, CopperGolem.class)
.setWeatheringState(WeatheringCopperState.OXIDIZED);
rfSpawn(o, 10.2, 0, 0.5, 180, Warden.class);
// Row 2: sheep (white, red, red sheared, baby red, jeb_, blue sheared), angry wolves (wild, tame).
rfSpawn(o, 0, 0, 2.6, 180, Sheep.class);
rfSpawn(o, 1.4, 0, 2.6, 180, Sheep.class).setColor(DyeColor.RED);
Sheep sr = rfSpawn(o, 2.8, 0, 2.6, 180, Sheep.class);
sr.setColor(DyeColor.RED);
sr.setSheared(true);
Sheep sb = rfSpawn(o, 4.2, 0, 2.6, 180, Sheep.class);
sb.setColor(DyeColor.RED);
sb.setBaby();
Sheep jeb = rfSpawn(o, 5.6, 0, 2.6, 180, Sheep.class);
jeb.customName(Component.text("jeb_"));
Sheep sbl = rfSpawn(o, 7.0, 0, 2.6, 180, Sheep.class);
sbl.setColor(DyeColor.BLUE);
sbl.setSheared(true);
rfSpawn(o, 8.6, 0, 2.6, 180, Wolf.class).setAngry(true);
Wolf wt = rfSpawn(o, 10.0, 0, 2.6, 180, Wolf.class);
tame(wt, DyeColor.RED);
wt.setAngry(true);
// Row 3: partial-alpha cutout textures.
rfSpawn(o, 0.5, 0, 5.2, 180, Llama.class).setColor(Llama.Color.GRAY);
rfSpawn(o, 2.0, 0, 5.2, 180, Llama.class).setColor(Llama.Color.WHITE);
TropicalFish tf = rfSpawn(o, 3.4, 0.3, 5.2, 180, TropicalFish.class);
tf.setPattern(TropicalFish.Pattern.BRINELY);
tf.setBodyColor(DyeColor.ORANGE);
tf.setPatternColor(DyeColor.LIGHT_BLUE);
for (Entity e : w.getNearbyEntities(o.clone().add(6, 1, 2), 9, 4, 6)) {
if (e instanceof LivingEntity le) le.setBodyYaw(180);
}
}
private void rfVisualStates(Location o) {
// Row 1: small armor stands with armor / elytra, elytra at rest, piglin arm poses.
ArmorStand small = rfSpawn(o, 0, 0, 0, 180, ArmorStand.class);
small.setSmall(true);
small.setArms(true);
diamondArmor(small);
ArmorStand smallE = rfSpawn(o, 1.2, 0, 0, 180, ArmorStand.class);
smallE.setSmall(true);
smallE.getEquipment().setChestplate(it(Material.ELYTRA));
rfSpawn(o, 2.4, 0, 0, 180, ArmorStand.class).getEquipment().setChestplate(it(Material.ELYTRA));
rfSpawn(o, 3.7, 0, 0, 180, Zombie.class).getEquipment().setChestplate(it(Material.ELYTRA));
Piglin p1 = rfSpawn(o, 5.0, 0, 0, 180, Piglin.class);
p1.getEquipment().setItemInMainHand(it(Material.GOLDEN_SWORD));
Piglin p2 = rfSpawn(o, 6.3, 0, 0, 180, Piglin.class);
p2.getEquipment().setItemInMainHand(it(Material.GOLDEN_SWORD));
p2.setAggressive(true);
Piglin p3 = rfSpawn(o, 7.6, 0, 0, 180, Piglin.class);
ItemStack cb = it(Material.CROSSBOW);
CrossbowMeta cm = (CrossbowMeta) cb.getItemMeta();
cm.addChargedProjectile(it(Material.ARROW));
cb.setItemMeta(cm);
p3.getEquipment().setItemInMainHand(cb);
Piglin p4 = rfSpawn(o, 8.9, 0, 0, 180, Piglin.class);
p4.getEquipment().setItemInOffHand(it(Material.GOLD_INGOT));
PiglinBrute pb = rfSpawn(o, 10.2, 0, 0, 180, PiglinBrute.class);
pb.getEquipment().setItemInMainHand(it(Material.GOLDEN_AXE));
pb.setAggressive(true);
for (Entity e : List.of(p1, p2, p3, p4, pb)) ((PiglinAbstract) e).setImmuneToZombification(true);
// Row 2: illagers, skeleton with a sword, invulnerable wither.
rfSpawn(o, 0, 0, 2.6, 180, Pillager.class);
rfSpawn(o, 1.3, 0, 2.6, 180, Pillager.class).getEquipment().setItemInMainHand(it(Material.CROSSBOW));
rfSpawn(o, 2.6, 0, 2.6, 180, Vindicator.class).getEquipment().setItemInMainHand(it(Material.IRON_AXE));
Vindicator va = rfSpawn(o, 3.9, 0, 2.6, 180, Vindicator.class);
va.getEquipment().setItemInMainHand(it(Material.IRON_AXE));
va.setAggressive(true);
rfSpawn(o, 5.2, 0, 2.6, 180, Skeleton.class).getEquipment().setItemInMainHand(it(Material.IRON_SWORD));
rfSpawn(o, 8.5, 0, 3.2, 180, Wither.class).setInvulnerableTicks(100);
// Row 3: hovering arrows: plain, tipped (custom colour without alpha bit), potion (opaque ARGB colour), spectral.
projectile(o.clone().add(0.5, 1, 5.5), new Vector(1, 0, 0), 0.0001, false, Arrow.class);
Arrow tipped = (Arrow) projectile(o.clone().add(0.5, 1.6, 5.5), new Vector(1, 0, 0), 0.0001, false, Arrow.class);
tipped.setColor(Color.fromARGB(0x00FF2020));
Arrow potion = (Arrow) projectile(o.clone().add(0.5, 2.2, 5.5), new Vector(1, 0, 0), 0.0001, false, Arrow.class);
potion.setBasePotionType(PotionType.POISON);
projectile(o.clone().add(0.5, 2.8, 5.5), new Vector(1, 0, 0), 0.0001, false, SpectralArrow.class);
for (Entity e : w.getNearbyEntities(o.clone().add(6, 1, 2), 9, 4, 6)) {
if (e instanceof LivingEntity le && !(e instanceof ArmorStand)) le.setBodyYaw(180);
}
}
private void perfFarm(Location o) {
Random r = new Random(42);
Class<?>[] types = {Cow.class, Sheep.class, Chicken.class, Pig.class};
for (int i = 0; i < 15; i++)
for (int j = 0; j < 18; j++) {
@SuppressWarnings("unchecked")
Class<? extends LivingEntity> type = (Class<? extends LivingEntity>) types[r.nextInt(types.length)];
LivingEntity e = rfSpawn(o, -5 + i * 1.2, 0, -10 + j * 1.2, 0, type);
if (r.nextInt(5) == 0 && e instanceof Ageable a) a.setBaby();
e.setBodyYaw(r.nextInt(8) * 45);
}
}
private void perfVillage(Location o) {
Random r = new Random(11);
List<Villager.Type> vTypes = registry(RegistryKey.VILLAGER_TYPE);
List<Villager.Profession> profs = registry(RegistryKey.VILLAGER_PROFESSION);
DyeColor[] dyes = DyeColor.values();
for (int i = 0; i < 14; i++)
for (int j = 0; j < 16; j++) {
double x = -5 + i * 1.2, z = -10 + j * 1.2;
DyeColor dye = dyes[r.nextInt(dyes.length)];
LivingEntity e;
switch (r.nextInt(4)) {
case 0 -> {
Villager v = rfSpawn(o, x, 0, z, 0, Villager.class);
v.setVillagerType(vTypes.get(r.nextInt(vTypes.size())));
if (r.nextInt(6) == 0) v.setBaby();
else {
v.setProfession(profs.get(r.nextInt(profs.size())));
v.setVillagerLevel(1 + r.nextInt(5));
}
e = v;
}
case 1 -> {
Sheep sh = rfSpawn(o, x, 0, z, 0, Sheep.class);
sh.setColor(dye);
if (r.nextInt(6) == 0) sh.setBaby();
e = sh;
}
case 2 -> {
Zombie zo = rfSpawn(o, x, 0, z, 0, Zombie.class);
Material[] pieces = {Material.LEATHER_HELMET, Material.LEATHER_CHESTPLATE,
Material.LEATHER_LEGGINGS, Material.LEATHER_BOOTS};
ItemStack[] armor = new ItemStack[4];
for (int k = 0; k < 4; k++) {
armor[k] = it(pieces[k]);
LeatherArmorMeta m =
(LeatherArmorMeta) armor[k].getItemMeta();
m.setColor(dye.getColor());
armor[k].setItemMeta(m);
}
zo.getEquipment().setHelmet(armor[0]);
zo.getEquipment().setChestplate(armor[1]);
zo.getEquipment().setLeggings(armor[2]);
zo.getEquipment().setBoots(armor[3]);
e = zo;
}
default -> {
Wolf wo = rfSpawn(o, x, 0, z, 0, Wolf.class);
tame(wo, dye);
if (r.nextInt(6) == 0) wo.setBaby();
e = wo;
}
}
e.setBodyYaw(r.nextInt(8) * 45);
}
for (int b = 0; b < 10; b++) {
int x = -4 + b * 2;
put(o, x, 0, 10, dyes[r.nextInt(dyes.length)].name().toLowerCase() + "_banner[rotation=" + r.nextInt(16) + "]");
if (o.clone().add(x, 0, 10).getBlock().getState() instanceof Banner bn) {
bn.addPattern(new Pattern(dyes[r.nextInt(dyes.length)], PatternType.STRIPE_TOP));
bn.addPattern(new Pattern(dyes[r.nextInt(dyes.length)], PatternType.CREEPER));
bn.update(true, false);
}
}
}
// --- poses ------------------------------------------------------------------------------------
private void posePets(Location o) {
// Row 1 (z=0): foxes — standing, sitting, sleeping, crouching, crouching+leaping, faceplanted.
Fox f0 = rfSpawn(o, 0, 0, 0, 180, Fox.class);
hands(f0, Material.SWEET_BERRIES, null);
Fox f1 = rfSpawn(o, 1.2, 0, 0, 180, Fox.class);
f1.setSitting(true);
hands(f1, Material.SWEET_BERRIES, null);
Fox f2 = rfSpawn(o, 2.4, 0, 0, 180, Fox.class);
f2.setSleeping(true);
hands(f2, Material.SWEET_BERRIES, null);
Fox f3 = rfSpawn(o, 3.6, 0, 0, 180, Fox.class);
f3.setCrouching(true);
hands(f3, Material.FEATHER, null);
Fox f4 = rfSpawn(o, 4.8, 0, 0, 180, Fox.class);
f4.setCrouching(true);
f4.setLeaping(true);
Fox f5 = rfSpawn(o, 6.0, 0, 0, 180, Fox.class);
f5.setFaceplanted(true);
f5.setFoxType(Fox.Type.SNOW);
Fox fb = rfSpawn(o, 7.2, 0, 0, 180, Fox.class);
fb.setBaby();
fb.setSleeping(true);
hands(fb, Material.SWEET_BERRIES, null);
Fox fb2 = rfSpawn(o, 8.2, 0, 0, 180, Fox.class);
fb2.setBaby();
fb2.setSitting(true);
// Row 2 (z=1.8): cats & wolves — sitting, lying, lying + head up, baby sitting; wolf/pup sitting.
Cat c0 = rfSpawn(o, 0, 0, 1.8, 180, Cat.class);
c0.setSitting(true);
Cat c1 = rfSpawn(o, 1.2, 0, 1.8, 180, Cat.class);
c1.setLyingDown(true);
c1.setCatType(Cat.Type.BLACK);
Cat c2 = rfSpawn(o, 2.6, 0, 1.8, 180, Cat.class);
c2.setLyingDown(true);
c2.setHeadUp(true);
c2.setCatType(Cat.Type.SIAMESE);
Cat c3 = rfSpawn(o, 4.0, 0, 1.8, 180, Cat.class);
c3.setBaby();
c3.setSitting(true);
tame(c3, DyeColor.RED);
Wolf w0 = rfSpawn(o, 5.2, 0, 1.8, 180, Wolf.class);
w0.setSitting(true);
Wolf w1 = rfSpawn(o, 6.6, 0, 1.8, 180, Wolf.class);
tame(w1, DyeColor.BLUE);
w1.setSitting(true);
Wolf w2 = rfSpawn(o, 8.0, 0, 1.8, 180, Wolf.class);
w2.setBaby();
w2.setSitting(true);
// Row 3 (z=3.6): parrots (flying default, sitting, standing on the ground), pandas.
Parrot p0 = rfSpawn(o, 0, 0, 3.6, 180, Parrot.class);
p0.setVariant(Parrot.Variant.BLUE);
Parrot p1 = rfSpawn(o, 0.8, 0, 3.6, 180, Parrot.class);
p1.setSitting(true);
p1.setVariant(Parrot.Variant.GREEN);
Parrot p2 = rfSpawn(o, 1.6, 0, 3.6, 180, Parrot.class);
p2.setVariant(Parrot.Variant.CYAN);
if (p2 instanceof Mob m) {
m.setAI(true);
m.setAware(false); // no goals, but physics: lands -> on ground -> standing pose
}
Panda pa0 = rfSpawn(o, 3.2, 0, 3.6, 180, Panda.class);
pa0.setSitting(true);
Panda pa1 = rfSpawn(o, 5.2, 0, 3.6, 180, Panda.class);
pa1.setOnBack(true);
Panda pa2 = rfSpawn(o, 7.2, 0, 3.6, 180, Panda.class);
pa2.setBaby();
pa2.setSitting(true);
Panda pa3 = rfSpawn(o, 8.6, 0, 3.6, 180, Panda.class);
pa3.setBaby();
pa3.setOnBack(true);
faceNorth(o);
}
private void poseMisc(Location o) {
Camel ca = rfSpawn(o, 0, 0, 0, 180, Camel.class);
ca.setSitting(true);
rfSpawn(o, 2.6, 0, 0, 180, Camel.class);
Sniffer sn = rfSpawn(o, 5.4, 0, 0, 180, Sniffer.class);
sn.setState(Sniffer.State.DIGGING);
rfSpawn(o, 8.6, 0, 0, 180, Sniffer.class);
// Bats: hanging under a block (resting) and flying.
put(o, 11, 2, 0, "stone");
Bat b0 = rfSpawn(o, 11.5, 1.1, 0.5, 180, Bat.class);
b0.setAwake(false);
Bat b1 = rfSpawn(o, 12.8, 1.0, 0.5, 180, Bat.class);
b1.setAwake(true);
// Dolphins pitched down/up/level, a strider looking up, fish in water vs. flopping on land.
Dolphin d0 = rfSpawn(o, 0, 0.6, -3.5, 180, Dolphin.class);
d0.setRotation(180, 35);
Dolphin d1 = rfSpawn(o, 2, 0.6, -3.5, 180, Dolphin.class);
d1.setRotation(180, -35);
hands(d1, Material.COD, null);
Dolphin d2 = rfSpawn(o, 4, 0.6, -3.5, 180, Dolphin.class);
hands(d2, Material.COD, null);
Strider st = rfSpawn(o, 6, 0, -3.5, 180, Strider.class);
st.setRotation(180, -30);
for (int i = 0; i < 3; i++) put(o, 8 + i, 0, -4, "water");
rfSpawn(o, 8.5, 0.3, -3.5, 180, Cod.class);
rfSpawn(o, 9.5, 0.3, -3.5, 180, Salmon.class);
TropicalFish tf = rfSpawn(o, 10.5, 0.3, -3.5, 180, TropicalFish.class);
tf.setPattern(TropicalFish.Pattern.BETTY);
rfSpawn(o, 8.5, 0, -2, 180, Cod.class);
rfSpawn(o, 9.5, 0, -2, 180, Salmon.class);
TropicalFish tf2 = rfSpawn(o, 10.5, 0, -2, 180, TropicalFish.class);
tf2.setPattern(TropicalFish.Pattern.BETTY);
faceNorth(o);
d0.setRotation(180, 35);
d1.setRotation(180, -35);
st.setRotation(180, -30);
}
private void poseHumans(Location o) {
// Villager asleep in a bed (head at z=0 facing south -> lies along z), a baby villager in a bed facing east.
put(o, 0, 0, 0, "red_bed[facing=south,part=head]");
put(o, 0, 0, -1, "red_bed[facing=south,part=foot]");
Villager v = rfSpawn(o, 0.5, 0, 1.5, 180, Villager.class);
v.setProfession(Villager.Profession.LIBRARIAN);
v.sleep(o.clone());
put(o, 2, 0, 0, "blue_bed[facing=east,part=head]");
put(o, 1, 0, 0, "blue_bed[facing=east,part=foot]");
Villager vb = rfSpawn(o, 2.5, 0, 1.5, 180, Villager.class);
vb.setBaby();
vb.sleep(o.clone().add(2, 0, 0));
// Mannequins: standing, crouching with a sword, crawling, swimming in water, gliding with elytra, sleeping.
Mannequin m0 = rfSpawn(o, 4, 0, 0, 180, Mannequin.class);
hands(m0, Material.IRON_SWORD, null);
Mannequin m1 = rfSpawn(o, 5.2, 0, 0, 180, Mannequin.class);
m1.setPose(Pose.SNEAKING, true);
hands(m1, Material.IRON_SWORD, null);
Mannequin m2 = rfSpawn(o, 6.6, 0, 0, 180, Mannequin.class);
m2.setPose(Pose.SWIMMING, true);
put(o, 8, 0, 0, "water");
put(o, 8, 0, 1, "water");
Mannequin m3 = rfSpawn(o, 8.5, 0.1, 0.5, 180, Mannequin.class);
m3.setPose(Pose.SWIMMING, true);
m3.setGravity(false); // else it floats up in the water while the scene waits (non-deterministic)
Mannequin m4 = rfSpawn(o, 10, 1.0, 0, 180, Mannequin.class);
m4.setGravity(false); // keeps gliding at y 1 instead of falling
m4.getEquipment().setChestplate(it(Material.ELYTRA));
m4.setPose(Pose.FALL_FLYING, true);
Mannequin m5 = rfSpawn(o, 12, 0, 0, 180, Mannequin.class);
m5.setPose(Pose.SLEEPING, true);
// Baby zombie riding a chicken (jockey: baby model pivots in the passenger pose), crouching zombie.
Chicken ch = rfSpawn(o, 4, 0, -2.5, 180, Chicken.class);
Zombie bz = rfSpawn(o, 4, 0, -2.5, 180, Zombie.class);
bz.setBaby();
ch.addPassenger(bz);
Zombie zc = rfSpawn(o, 5.5, 0, -2.5, 180, Zombie.class);
zc.setPose(Pose.SNEAKING, true);
faceNorth(o);
}
private void poseStands(Location o) {
// Custom armor-stand poses (head/body/arms/legs, radians), with arms, armor and held items.
ArmorStand a = rfSpawn(o, 0, 0, 0, 180, ArmorStand.class);
a.setArms(true);
a.setHeadPose(new EulerAngle(Math.toRadians(-20), Math.toRadians(30), 0));
a.setRightArmPose(new EulerAngle(Math.toRadians(-90), 0, 0));
a.setLeftArmPose(new EulerAngle(Math.toRadians(-30), 0, Math.toRadians(-40)));
a.getEquipment().setHelmet(it(Material.GOLDEN_HELMET));
hands(a, Material.BOW, Material.APPLE);
ArmorStand b = rfSpawn(o, 1.6, 0, 0, 180, ArmorStand.class);
b.setArms(true);
b.setBodyPose(new EulerAngle(Math.toRadians(15), Math.toRadians(20), 0));
b.setRightLegPose(new EulerAngle(Math.toRadians(-40), 0, 0));
b.setLeftLegPose(new EulerAngle(Math.toRadians(30), 0, Math.toRadians(-10)));
diamondArmor(b);
hands(b, Material.IRON_SWORD, Material.SHIELD);
ArmorStand c = rfSpawn(o, 3.2, 0, 0, 180, ArmorStand.class);
c.setArms(true);
c.setSmall(true);
c.setRightArmPose(new EulerAngle(Math.toRadians(-120), Math.toRadians(20), 0));
c.setHeadPose(new EulerAngle(0, 0, Math.toRadians(25)));
diamondArmor(c);
hands(c, Material.TORCH, null);
ArmorStand d = rfSpawn(o, 4.8, 0, 0, 180, ArmorStand.class);
d.setHeadPose(new EulerAngle(Math.toRadians(30), 0, 0));
d.getEquipment().setHelmet(it(Material.CARVED_PUMPKIN));
ArmorStand e = rfSpawn(o, 6.4, 0, 0, 180, ArmorStand.class);
e.setArms(true);
e.setInvisible(true);
e.setRightArmPose(new EulerAngle(Math.toRadians(-60), Math.toRadians(-30), 0));
e.setBodyPose(new EulerAngle(0, Math.toRadians(30), 0));
diamondArmor(e);
hands(e, Material.DIAMOND_SWORD, null);
ArmorStand f = rfSpawn(o, 8, 0, 0, 180, ArmorStand.class); // default pose (must match rf_stands)
f.setArms(true);
}
private void perfItems(Location o) {
Random r = new Random(7);
Material[] mats = {Material.WHEAT, Material.CARROT, Material.EGG, Material.BONE, Material.ROTTEN_FLESH,
Material.STRING, Material.GUNPOWDER, Material.IRON_INGOT, Material.COBBLESTONE, Material.OAK_LOG,
Material.PUMPKIN};
for (int i = 0; i < 300; i++) {
ItemDisplay d = spawn(o, -4 + r.nextDouble() * 16, 0.15 + r.nextDouble() * 0.2, -10 + r.nextDouble() * 16,
r.nextInt(360), ItemDisplay.class);
d.setItemStack(it(mats[r.nextInt(mats.length)]));
d.setTransformation(new Transformation(new Vector3f(), new AxisAngle4f((float) (Math.PI / 2), 1, 0, 0),
new Vector3f(0.4f), new AxisAngle4f()));
}
}
@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; }
Location origin = new Location(w, 5000 + sc.index * 64, 200, 5000);
Bukkit.getScheduler().runTaskLater(this, () -> run(sc, origin), 1 + i * 60L);
i++;
}
return true;
}
/**
* Loads the scene's chunks and waits until they are entity-ticking before building: entities spawned into a
* chunk that only becomes entity-ticking a few (variable) ticks later start moving late (projectile scenes were
* not reproducible).
*/
private void run(Scene sc, Location o) {
loadArea(o);
whenEntityTicking(sc, o, 0);
}
private void whenEntityTicking(Scene sc, Location o, int waited) {
boolean ready = true;
for (Chunk c : areaChunks(o)) ready &= c.getLoadLevel() == Chunk.LoadLevel.ENTITY_TICKING;
if (!ready && waited < 200) {
Bukkit.getScheduler().runTaskLater(this, () -> whenEntityTicking(sc, o, waited + 1), 1);
return;
}
if (!ready) getLogger().warning("scene " + sc.name + ": chunks not entity-ticking after 200 ticks");
build(sc, o);
}
private void build(Scene sc, Location o) {
try {
prepareArea(o);
sc.build.accept(o.clone());
} catch (Throwable t) {
getLogger().severe("build " + sc.name + " failed: " + t);
t.printStackTrace();
}
Bukkit.getScheduler().runTaskLater(this, () -> render(sc, o), settleTicks(sc.name));
}
/**
* Ticks between building and rendering: passengers/leashes settle (10); projectiles fly and stick (30), proj_shake
* catches freshly stuck arrows still wobbling (4); beacon levels update every 80 ticks (be_contents); flowing
* water spreads (liq_flow).
*/
private static int settleTicks(String name) {
if (name.equals("proj_shake")) return 4;
if (name.startsWith("proj_")) return 30;
if (name.startsWith("be_contents")) return 100;
if (name.startsWith("liq_flow")) return 200;
return 10;
}
private static final List<String> LARGE = List.of("proj_", "rf_", "pose_", "frames", "geo_", "liq_", "items_", "xmas_");
/** *_sq scenes square, newer scene families 960x640, the rest 480x320. */
private static Resolution resolution(String name) {
if (name.endsWith("_sq")) return new Resolution(384, 384);
return LARGE.stream().anyMatch(name::startsWith) ? new Resolution(960, 640) : new Resolution(480, 320);
}
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, resolution(sc.name), null);
if (sc.name.equals("dbg")) {
// Bake log of the display objects near the origin.
try {
Field f = DefaultScreenRenderer.class.getDeclaredField("objectBaker");
f.setAccessible(true);
var ob = (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 " + 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_")) logProjectiles(sc.name, job, o);
getLogger().info("scene " + sc.name + ": " + job.entities().size() + " entities, " + job.objects().size()
+ " objects, " + job.blockEntities().size() + " block entities");
Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
try {
long t0 = System.nanoTime();
BufferedImage img = r.execute(job);
getLogger().info(String.format("TIME %s %.1f ms", sc.name, (System.nanoTime() - t0) / 1e6));
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();
}
});
}
/** Projectile orientation vs. velocity (proj_shake: also the captured arrow pitch and vanilla shakeTime). */
private void logProjectiles(String name, RenderJob job, Location o) {
boolean shake = name.equals("proj_shake");
if (shake)
for (var st : job.entities())
if (st.typeKey().contains("arrow"))
getLogger().info(String.format("state %s pitch=%.2f", st.typeKey(), st.pitch()));
for (Entity e : w.getNearbyEntities(o.clone().add(8, 3, 2), 12, 8, 12)) {
if (!(e instanceof Projectile pr)) continue;
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 (shake && e instanceof AbstractArrow && !(e instanceof Trident)) {
try {
Object h = handle(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);
}
}
}
}
private static Object handle(Object craftObject) throws ReflectiveOperationException {
return craftObject.getClass().getMethod("getHandle").invoke(craftObject);
}
/** The chunks around a scene origin (its platform and camera). */
private List<Chunk> areaChunks(Location o) {
List<Chunk> out = new ArrayList<>();
for (int cx = -1; cx <= 2; cx++)
for (int cz = -2; cz <= 2; cz++) out.add(w.getChunkAt((o.getBlockX() >> 4) + cx, (o.getBlockZ() >> 4) + cz));
return out;
}
/** Loads the scene's chunks and keeps them loaded (plugin chunk tickets). */
private void loadArea(Location o) {
for (Chunk c : areaChunks(o)) {
c.load(true);
c.addPluginChunkTicket(this);
}
}
/** Clears the area (blocks + entities) and lays a 24x24 stone floor at the origin's y-1. */
private void prepareArea(Location o) {
// Blocks first: removing a campfire/shelf/container drops its items, which the entity sweep then removes
// (before, the previous run's cooked campfire food lay around at random).
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);
for (Entity e : w.getNearbyEntities(o.clone().add(6, 5, 2), 20, 20, 20)) if (!(e instanceof Player)) e.remove();
}
// --- helpers ---------------------------------------------------------------------------------
/** {@link #rfSpawn} with the body turned to {@code yaw} (spawned mobs otherwise keep a random body yaw). */
private <T extends Entity> T spawn(Location o, double x, double y, double z, float yaw, Class<T> type) {
T e = rfSpawn(o, x, y, z, yaw, type);
if (e instanceof LivingEntity le) le.setBodyYaw(yaw);
return e;
}
private Entity spawn(Location o, double x, double y, double z, float yaw, EntityType type) {
Entity e = rfSpawn(o, x, y, z, yaw, type);
e.setPersistent(false);
if (e instanceof LivingEntity le) {
le.setRemoveWhenFarAway(false);
le.setBodyYaw(yaw);
}
return e;
}
/**
* Respawns until the entity id is even: a full boat turns an animal passenger by 90° or 270° depending on the
* id's parity (AbstractBoat.positionRider), and ids depend on the server's history.
*/
private <T extends Entity> T evenId(Supplier<T> spawner) {
T e = spawner.get();
while (e.getEntityId() % 2 != 0) {
e.remove();
e = spawner.get();
}
return e;
}
/** A dropped item at rest: no velocity/gravity, fixed yaw (PixelPics freezes the item spin from the yaw). */
private Item drop(Location o, double x, double y, double z, ItemStack stack, float yaw) {
return w.dropItem(o.clone().add(x, y, z), stack, i -> {
i.setVelocity(new Vector());
i.setGravity(false);
i.setRotation(yaw, 0);
i.setPersistent(false);
});
}
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(evenId(() -> 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.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) {
drop(o, 0, 0, 0, it(Material.DIAMOND_SWORD), 30);
drop(o, 1, 0, 0, it(Material.OAK_LOG), 60);
drop(o, 2, 0, 0, new ItemStack(Material.COBBLESTONE, 32), 0);
drop(o, 3, 0, 0, it(Material.APPLE), 90);
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++) put(o, 0, y, 0, "oak_log");
put(o, 0, 0, -3, "diamond_block"); // north marker
put(o, 3, 0, 0, "gold_block"); // east marker
for (int i = 0; i < 4; i++)
for (int age = 0; age < 2; age++)
put(o, STEP[i][0], 1 + age, STEP[i][1], "shelf_mushroom[age=" + age + ",facing=" + DIRS[i] + "]");
}
/** Blockstate y-rotations: ladders, stairs, wall torches, furnaces, logs on all sides of stone pillars. */
private void rotated(Location o) {
String[] kinds = {"ladder[facing=%s]", "wall_torch[facing=%s]", "oak_stairs[facing=%s]", "furnace[facing=%s]"};
for (int k = 0; k < kinds.length; k++) {
int x = k * 3 - 1;
put(o, x, 0, 2, "stone");
put(o, x, 1, 2, "stone");
for (int i = 0; i < 4; i++)
put(o, x + STEP[i][0], k == 2 || k == 3 ? 0 : 1, 2 + STEP[i][1], String.format(kinds[k], DIRS[i]));
}
// Fence with a single connection to the east (stone post at +x), door facing east.
put(o, -3, 0, 5, "oak_fence[east=true]");
put(o, -2, 0, 5, "stone");
put(o, 1, 0, 5, "oak_door[facing=east,half=lower,hinge=left,open=false]");
put(o, 1, 1, 5, "oak_door[facing=east,half=upper,hinge=left,open=false]");
put(o, 9, 0, 2, "oak_log[axis=x]");
put(o, 9, 0, 4, "oak_log[axis=z]");
}
/**
* 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}};
for (int i = 0; i < 4; i++) {
put(o, i * 3, 0, 0, "chest[facing=" + f[i] + ",type=single]");
put(o, i * 3, 0, 3, "chest[facing=" + f[i] + ",type=left]");
put(o, i * 3 + cw[i][0], 0, 3 + cw[i][1], "chest[facing=" + f[i] + ",type=right]");
}
put(o, 0, 0, 6, "trapped_chest[facing=south,type=left]");
put(o, -1, 0, 6, "trapped_chest[facing=south,type=right]");
put(o, 3, 0, 6, "ender_chest[facing=south]");
put(o, 6, 0, 6, "copper_chest[facing=east,type=single]");
put(o, 9, 0, 6, "oxidized_copper_chest[facing=north,type=left]");
put(o, 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) {
put(o, 0, 0, 0, "redstone_wire[power=0]");
for (int x = 1; x <= 4; x++) put(o, x, 0, 0, "redstone_wire[east=side,west=side,power=" + (x * 3) + "]");
put(o, 5, 0, 0, "redstone_wire[west=side,north=side,power=15]");
put(o, 5, 0, -1, "redstone_wire[south=side,north=up,power=15]");
put(o, 5, 0, -2, "stone");
put(o, 5, 1, -2, "redstone_wire[south=side,power=15]");
put(o, 0, 0, 3, "glass");
put(o, 1, 0, 3, "red_stained_glass");
put(o, 2, 0, 3, "glass_pane[east=true,west=false]");
put(o, 3, 0, 3, "blue_stained_glass_pane[east=true,west=true]");
put(o, 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++) {
put(o, i * 2, 1, 0, "piston[facing=" + f[i] + ",extended=false]");
put(o, i * 2, 1, 2, "observer[facing=" + f[i] + "]");
put(o, i * 2, 0, 2, "dispenser[facing=" + f[i] + "]");
}
put(o, 0, 0, -2, "oak_log[axis=x]");
put(o, 2, 0, -2, "oak_log[axis=z]");
put(o, 4, 0, -2, "furnace[facing=north]");
}
/**
* 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) {
String[] pillars = {"oak_log", "basalt", "quartz_pillar", "hay_block"};
for (int i = 0; i < pillars.length; i++) {
put(o, i * 3, 0, 0, pillars[i] + "[axis=x]");
put(o, i * 3 + 1, 0, 0, pillars[i] + "[axis=z]");
}
String[] vertical = {"observer", "dispenser", "dropper"};
for (int i = 0; i < vertical.length; i++) {
put(o, i * 3, 0, 3, vertical[i] + "[facing=up]");
put(o, i * 3 + 1, 1, 3, vertical[i] + "[facing=down]");
put(o, i * 3 + 1, 0, 3, "glass");
}
String[] f = {"north", "east", "south", "west"};
for (int i = 0; i < f.length; i++) {
put(o, i * 2, 0, 6, "oak_stairs[facing=" + f[i] + ",half=top]");
put(o, i * 2 + 1, 0, 6, "stone_brick_stairs[facing=" + f[i] + ",half=top]");
}
put(o, 9, 1, 6, "stone");
put(o, 9, 0, 6, "oak_button[face=ceiling,facing=north]");
put(o, 10, 1, 6, "stone");
put(o, 10, 0, 6, "oak_button[face=ceiling,facing=east]");
put(o, 0, 0, -2, "stone");
put(o, 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) {
for (int x = -1; x <= 10; x++)
for (int y = 0; y <= 4; y++) put(o, 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(o, x, y, 1, "red_stained_glass");
put(o, 3, 0, 1, "blue_stained_glass");
put(o, 3, 0, 0, "yellow_stained_glass");
put(o, 4, 0, 1, "ice");
put(o, 4, 1, 1, "tinted_glass");
put(o, 5, 0, 1, "slime_block");
put(o, 6, 0, 1, "honey_block");
put(o, 7, 0, 1, "lime_stained_glass_pane[east=true]");
put(o, 8, 0, 1, "lime_stained_glass_pane[west=true,east=true]");
put(o, 9, 0, 1, "lime_stained_glass_pane[west=true]");
for (int x = 0; x <= 2; x++) put(o, x, -1, -2, "water");
put(o, 1, -2, -2, "magenta_stained_glass");
put(o, 0, -2, -2, "sand");
put(o, 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);
}
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(Attribute.MAX_HEALTH).getValue() / 2 - 1);
}
// --- data-driven variants / pose flags / armor wearers ------------------------------------------
private static <T extends Keyed> List<T> registry(RegistryKey<T> key) {
List<T> out = new ArrayList<>();
RegistryAccess.registryAccess().getRegistry(key).forEach(out::add);
out.sort(Comparator.comparing(k -> k.getKey().toString()));
return out;
}
/**
* Deterministic spawn (pixel-diffable between plugin builds): randomizeData=false (no finalizeSpawn: no random
* variants, sizes, babies/jockeys or equipment), no AI. Scenes set every variant/equipment they show explicitly.
*/
private <T extends Entity> T rfSpawn(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, false, en -> {
if (en instanceof Mob m) m.setAI(false);
en.setPersistent(false);
});
if (e instanceof LivingEntity le) le.setRemoveWhenFarAway(false);
return e;
}
private Entity rfSpawn(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, false);
if (e instanceof Mob m) m.setAI(false);
return e;
}
private void faceNorth(Location o) {
for (Entity e : w.getNearbyEntities(o.clone().add(6, 0, 0), 14, 4, 12))
if (e instanceof LivingEntity le && !(e instanceof Player)) le.setBodyYaw(180);
}
private void rfCats(Location o) {
List<Cat.Type> types = registry(RegistryKey.CAT_VARIANT);
for (int i = 0; i < types.size(); i++) {
rfSpawn(o, i * 1.2, 0, 0, 180, Cat.class).setCatType(types.get(i));
Cat kit = rfSpawn(o, i * 1.2, 0, 1.6, 180, Cat.class);
kit.setCatType(types.get(i));
kit.setBaby();
if (i % 2 == 0) tame(kit, DyeColor.values()[i % 16]);
}
faceNorth(o);
}
private void rfDatapackCat(Location o) {
Cat.Type custom = RegistryAccess.registryAccess()
.getRegistry(RegistryKey.CAT_VARIANT)
.get(new NamespacedKey("pptest", "ocelotcat"));
if (custom == null) throw new IllegalStateException("datapack variant pptest:ocelotcat missing");
rfSpawn(o, 0, 0, 0, 180, Cat.class).setCatType(custom);
Cat kit = rfSpawn(o, 1.2, 0, 0, 180, Cat.class);
kit.setCatType(custom);
kit.setBaby();
rfSpawn(o, 2.4, 0, 0, 180, Cat.class).setCatType(Cat.Type.TABBY);
faceNorth(o);
}
private void rfWolves(Location o) {
List<Wolf.Variant> vs = registry(RegistryKey.WOLF_VARIANT);
for (int i = 0; i < vs.size(); i++) {
rfSpawn(o, i * 1.3, 0, 0, 180, Wolf.class).setVariant(vs.get(i));
Wolf t = rfSpawn(o, i * 1.3, 0, 1.6, 180, Wolf.class);
t.setVariant(vs.get(i));
tame(t, DyeColor.values()[i % 16]);
Wolf pup = rfSpawn(o, i * 1.3, 0, 3.0, 180, Wolf.class);
pup.setVariant(vs.get(i));
pup.setBaby();
if (i % 2 == 1) tame(pup, DyeColor.BLUE);
}
faceNorth(o);
}
private void rfFarm(Location o) {
List<Cow.Variant> cows = registry(RegistryKey.COW_VARIANT);
List<Pig.Variant> pigs = registry(RegistryKey.PIG_VARIANT);
List<Chicken.Variant> chickens = registry(RegistryKey.CHICKEN_VARIANT);
List<Frog.Variant> frogs = registry(RegistryKey.FROG_VARIANT);
List<ZombieNautilus.Variant> nautili = registry(RegistryKey.ZOMBIE_NAUTILUS_VARIANT);
for (int i = 0; i < 3; i++) {
double x = i * 4;
setVar(rfSpawn(o, x, 0, 0, 180, Cow.class), cows.get(i));
Cow cb = rfSpawn(o, x + 1.6, 0, 0, 180, Cow.class);
setVar(cb, cows.get(i));
cb.setBaby();
setVar(rfSpawn(o, x, 0, 1.8, 180, Pig.class), pigs.get(i));
Pig pb = rfSpawn(o, x + 1.4, 0, 1.8, 180, Pig.class);
setVar(pb, pigs.get(i));
pb.setBaby();
setVar(rfSpawn(o, x + 2.6, 0, 1.8, 180, Chicken.class), chickens.get(i));
Chicken chb = rfSpawn(o, x + 3.3, 0, 1.8, 180, Chicken.class);
setVar(chb, chickens.get(i));
chb.setBaby();
setVar(rfSpawn(o, x + 0.5, 0, 3.4, 180, Frog.class), frogs.get(i));
}
for (int i = 0; i < nautili.size(); i++)
setVar(rfSpawn(o, 2.5 + i * 3, 0, 4.8, 180, ZombieNautilus.class), nautili.get(i));
faceNorth(o);
}
/** setVariant via reflection (the variant setters' declaring interface differs between API and server). */
private static void setVar(Entity e, Object variant) {
for (Method m : e.getClass().getMethods())
if (m.getName().equals("setVariant") && m.getParameterCount() == 1
&& m.getParameterTypes()[0].isInstance(variant)) {
try {
m.invoke(e, variant);
return;
} catch (ReflectiveOperationException ex) {
throw new RuntimeException(ex);
}
}
throw new IllegalStateException("no setVariant for " + variant);
}
private void rfStands(Location o) {
rfSpawn(o, 0, 0, 0, 180, ArmorStand.class);
rfSpawn(o, 1.5, 0, 0, 180, ArmorStand.class).setArms(true);
rfSpawn(o, 3, 0, 0, 180, ArmorStand.class).setSmall(true);
ArmorStand sa = rfSpawn(o, 4.2, 0, 0, 180, ArmorStand.class);
sa.setSmall(true);
sa.setArms(true);
ArmorStand full = rfSpawn(o, 5.7, 0, 0, 180, ArmorStand.class);
full.setArms(true);
diamondArmor(full);
hands(full, Material.IRON_SWORD, Material.SHIELD);
ArmorStand sfull = rfSpawn(o, 7.2, 0, 0, 180, ArmorStand.class);
sfull.setSmall(true);
sfull.setArms(true);
diamondArmor(sfull);
hands(sfull, Material.TORCH, null);
ArmorStand noArmsHeld = rfSpawn(o, 8.6, 0, 0, 180, ArmorStand.class);
hands(noArmsHeld, Material.DIAMOND, null);
ArmorStand inv = rfSpawn(o, 10, 0, 0, 180, ArmorStand.class);
inv.setArms(true);
inv.setInvisible(true);
diamondArmor(inv);
}
private static void diamondArmor(LivingEntity e) {
e.getEquipment().setHelmet(it(Material.DIAMOND_HELMET));
e.getEquipment().setChestplate(it(Material.DIAMOND_CHESTPLATE));
e.getEquipment().setLeggings(it(Material.DIAMOND_LEGGINGS));
e.getEquipment().setBoots(it(Material.DIAMOND_BOOTS));
}
private void rfChests(Location o) {
rfSpawn(o, 0, 0, 0, 180, Llama.class).setColor(Llama.Color.CREAMY);
Llama lc = rfSpawn(o, 1.6, 0, 0, 180, Llama.class);
lc.setColor(Llama.Color.BROWN);
lc.setCarryingChest(true);
Llama lcc = rfSpawn(o, 3.2, 0, 0, 180, Llama.class);
lcc.setColor(Llama.Color.GRAY);
lcc.setCarryingChest(true);
lcc.getInventory().setDecor(it(Material.RED_CARPET));
TraderLlama tl = rfSpawn(o, 4.8, 0, 0, 180, TraderLlama.class);
tl.setColor(Llama.Color.WHITE);
tl.setCarryingChest(true);
Llama bl = rfSpawn(o, 6.2, 0, 0, 180, Llama.class);
bl.setColor(Llama.Color.CREAMY);
bl.setBaby();
rfSpawn(o, 8, 0, 0, 180, Donkey.class);
Donkey dc = rfSpawn(o, 9.6, 0, 0, 180, Donkey.class);
dc.setCarryingChest(true);
Donkey dcs = rfSpawn(o, 11.2, 0, 0, 180, Donkey.class);
dcs.setCarryingChest(true);
dcs.getInventory().setSaddle(it(Material.SADDLE));
Mule mc = rfSpawn(o, 12.8, 0, 0, 180, Mule.class);
mc.setCarryingChest(true);
Mule mcs = rfSpawn(o, 14.4, 0, 0, 180, Mule.class);
mcs.setCarryingChest(true);
mcs.getInventory().setSaddle(it(Material.SADDLE));
faceNorth(o);
}
private void rfArmor(Location o) {
Class<?>[] wearers = {Zombie.class, Husk.class, Drowned.class, ZombieVillager.class, PigZombie.class,
Skeleton.class, Stray.class, WitherSkeleton.class, Bogged.class, Parched.class, Piglin.class, PiglinBrute.class};
for (int i = 0; i < wearers.length; i++) {
@SuppressWarnings("unchecked")
LivingEntity e = rfSpawn(o, i * 1.25, 0, 0, 180, (Class<LivingEntity>) wearers[i]);
diamondArmor(e);
}
LivingEntity m = (LivingEntity) rfSpawn(o, 0, 0, 2.2, 180, EntityType.MANNEQUIN);
diamondArmor(m);
Zombie bz = rfSpawn(o, 1.3, 0, 2.2, 180, Zombie.class);
bz.setBaby();
diamondArmor(bz);
Piglin bp = rfSpawn(o, 2.5, 0, 2.2, 180, Piglin.class);
bp.setBaby();
diamondArmor(bp);
// Non-wearers given armor: vanilla draws nothing.
Class<?>[] others = {Villager.class, Pillager.class, Vindicator.class, Witch.class, Evoker.class,
Creeper.class, Enderman.class, Cow.class, Fox.class};
for (int i = 0; i < others.length; i++) {
@SuppressWarnings("unchecked")
LivingEntity e = rfSpawn(o, 4 + i * 1.25, 0, 2.2, 180, (Class<LivingEntity>) others[i]);
diamondArmor(e);
}
faceNorth(o);
}
// --- 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++) {
put(o, 2, y, 2, "gold_block");
put(o, 11, y, 2, "diamond_block");
}
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++) {
put(o, 1 + x, y, 1 + z, "stone");
put(o, 12 + x, y, 1 + z, "stone");
}
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);
}
}
}
// --- block-entity scenes ------------------------------------------------------------------------
private void put(Location o, int x, int y, int z, String data) {
o.clone().add(x, y, z).getBlock().setBlockData(Bukkit.createBlockData("minecraft:" + data), false);
}
private void signText(Location o, int x, int y, int z, String front, String back) {
if (!(o.clone().add(x, y, z).getBlock().getState() instanceof Sign sg)) return;
sg.getSide(Side.FRONT).setLine(1, front);
sg.getSide(Side.BACK).setLine(1, back);
sg.getSide(Side.BACK).setColor(DyeColor.BLUE);
sg.update(true, false);
}
private static final String[] DIRS = {"north", "east", "south", "west"};
private static final int[][] STEP = {{0, -1}, {1, 0}, {0, 1}, {-1, 0}};
/** A stone pillar with one wall-mounted block on each of its four sides ({@code %s} = facing). */
private void pillar(Location o, int x, int z, int y, String fmt) {
put(o, x, y, z, "stone");
for (int i = 0; i < 4; i++) put(o, x + STEP[i][0], y, z + STEP[i][1], String.format(fmt, DIRS[i]));
}
/** Standing signs rot 0..15 (two rows), ceiling hanging signs rot 0..15, wall + wall-hanging signs. */
private void beSigns(Location o) {
for (int r = 0; r < 16; r++) {
int x = (r % 8) * 2, z = r < 8 ? 0 : 2;
put(o, x, 0, z, "oak_sign[rotation=" + r + "]");
signText(o, x, 0, z, "F" + r, "B" + r);
put(o, r, 2, -3, "stone");
put(o, r, 1, -3, "spruce_hanging_sign[rotation=" + r + ",attached=true]");
signText(o, r, 1, -3, "F" + r, "B" + r);
}
pillar(o, 3, 7, 0, "birch_wall_sign[facing=%s]");
pillar(o, 3, 7, 1, "cherry_wall_hanging_sign[facing=%s]");
for (int i = 0; i < 4; i++) {
signText(o, 3 + STEP[i][0], 0, 7 + STEP[i][1], DIRS[i], "back");
signText(o, 3 + STEP[i][0], 1, 7 + STEP[i][1], DIRS[i], "back");
}
pillar(o, 10, 7, 1, "dark_oak_wall_sign[facing=%s]");
for (int i = 0; i < 4; i++) signText(o, 10 + STEP[i][0], 1, 7 + STEP[i][1], DIRS[i].toUpperCase(), "");
}
/** Standing banners rot 0..15 with patterns, wall banners on a pillar. */
private void beBanners(Location o) {
DyeColor[] cols = DyeColor.values();
for (int r = 0; r < 16; r++) {
int x = (r % 8) * 2, z = r < 8 ? 0 : 3;
put(o, x, 0, z, cols[r].name().toLowerCase() + "_banner[rotation=" + r + "]");
if (o.clone().add(x, 0, z).getBlock().getState() instanceof Banner b) {
b.addPattern(new Pattern(DyeColor.BLACK, PatternType.STRIPE_TOP));
b.addPattern(new Pattern(DyeColor.WHITE, PatternType.CREEPER));
b.update(true, false);
}
}
pillar(o, 16, 5, 2, "red_wall_banner[facing=%s]");
put(o, 16, 1, 5, "stone");
put(o, 16, 0, 5, "stone");
}
/** Every head type in five rotations (0, 2, 4, 8, 12) per row, wall heads on pillars. */
private void beHeads(Location o) {
String[] t = {"skeleton_skull", "wither_skeleton_skull", "zombie_head", "creeper_head", "player_head", "piglin_head", "dragon_head"};
int[] rots = {0, 2, 4, 8, 12};
for (int i = 0; i < t.length; i++)
for (int j = 0; j < rots.length; j++) put(o, j * 2, 0, i, t[i] + "[rotation=" + rots[j] + "]");
String[] wall = {"skeleton_wall_skull", "dragon_wall_head", "piglin_wall_head", "player_wall_head"};
for (int i = 0; i < wall.length; i++) pillar(o, 12, i * 3 - 2, 1, wall[i] + "[facing=%s]");
for (int i = 0; i < wall.length; i++) put(o, 12, 0, i * 3 - 2, "stone");
}
/** Bells (all attachments/facings), decorated pots with sherds, shulker boxes (6 facings), lecterns, conduit. */
private void beMisc(Location o) {
for (int i = 0; i < 4; i++) {
put(o, i * 2, 0, 0, "bell[attachment=floor,facing=" + DIRS[i] + "]");
put(o, i * 2, 2, 2, "stone");
put(o, i * 2, 1, 2, "bell[attachment=ceiling,facing=" + DIRS[i] + "]");
put(o, i * 2, 0, 4, "decorated_pot[facing=" + DIRS[i] + "]");
if (o.clone().add(i * 2, 0, 4).getBlock().getState() instanceof DecoratedPot pot) {
pot.setSherd(DecoratedPot.Side.FRONT, Material.HEART_POTTERY_SHERD);
pot.setSherd(DecoratedPot.Side.LEFT, Material.SKULL_POTTERY_SHERD);
pot.setSherd(DecoratedPot.Side.RIGHT, Material.ANGLER_POTTERY_SHERD);
pot.update(true, false);
}
put(o, i * 2, 0, 6, "lectern[facing=" + DIRS[i] + ",has_book=true]");
}
put(o, 9, 1, 0, "stone");
put(o, 10, 1, 0, "bell[attachment=single_wall,facing=east]");
put(o, 12, 1, 0, "stone");
put(o, 11, 1, 0, "bell[attachment=double_wall,facing=east]");
String[] f6 = {"up", "down", "north", "south", "east", "west"};
String[] c6 = {"", "red_", "blue_", "lime_", "black_", "white_"};
for (int i = 0; i < 6; i++) put(o, 9 + (i % 3) * 2, 0, 3 + (i / 3) * 2, c6[i] + "shulker_box[facing=" + f6[i] + "]");
put(o, 9, 0, 7, "conduit");
put(o, 8, 0, 6, "lectern[facing=south,has_book=false]");
}
/** Block-entity items: held by armor stands, worn on heads, as item displays and as block displays. */
private void beItems(Location o) {
Material[] m = {Material.CHEST, Material.RED_BANNER, Material.SHULKER_BOX, Material.DECORATED_POT,
Material.CONDUIT, Material.DRAGON_HEAD, Material.PIGLIN_HEAD, Material.CREEPER_HEAD, Material.PLAYER_HEAD};
for (int i = 0; i < m.length; i++) {
ArmorStand as = spawn(o, i + 0.5, 0, 6.5, 0, ArmorStand.class);
as.setArms(true);
as.getEquipment().setItemInMainHand(it(m[i]));
if (m[i].name().endsWith("_HEAD")) as.getEquipment().setHelmet(it(m[i]));
ItemDisplay d = spawn(o, i + 0.5, 0.6, 3, 0, ItemDisplay.class);
d.setItemStack(it(m[i]));
d.setTransformation(new Transformation(new Vector3f(), new Quaternionf(), new Vector3f(0.6f), new Quaternionf()));
}
String[] blocks = {"chest[facing=south]", "red_banner[rotation=0]", "shulker_box[facing=up]", "decorated_pot[facing=south]",
"skeleton_skull[rotation=8]", "lectern[facing=south,has_book=true]"};
for (int i = 0; i < blocks.length; i++) {
BlockDisplay b = spawn(o, i * 1.5, 0, 0, 0, BlockDisplay.class);
b.setBlock(Bukkit.createBlockData("minecraft:" + blocks[i]));
}
}
// --- item frames -------------------------------------------------------------------------------
private void frames(Location o) {
for (int x = -2; x <= 11; x++)
for (int y = 0; y <= 5; y++) put(o, x, y, 4, "stone");
BlockFace f = BlockFace.NORTH;
for (int r = 0; r < 8; r++) frame(o, r, 4, 3, f, false, it(Material.DIAMOND_SWORD), r, true);
ItemStack banner = it(Material.RED_BANNER);
BannerMeta bm = (BannerMeta) banner.getItemMeta();
bm.addPattern(new Pattern(DyeColor.YELLOW, PatternType.STRIPE_TOP));
banner.setItemMeta(bm);
ItemStack[] row3 = {it(Material.STONE), it(Material.OAK_STAIRS), it(Material.GRASS_BLOCK), it(Material.APPLE),
it(Material.TORCH), it(Material.CHEST), banner, CameraItems.createCamera(3),
it(Material.CREEPER_HEAD), it(Material.SHIELD)};
for (int i = 0; i < row3.length; i++) frame(o, i - 1, 3, 3, f, false, row3[i], 0, true);
ItemStack photo = photoMap(), synth = synthMap(), vmap = vanillaMap();
int[] photoRot = {0, 1, 2, 3, 5};
for (int i = 0; i < photoRot.length; i++) frame(o, i - 1, 2, 3, f, false, photo, photoRot[i], true);
frame(o, 4, 2, 3, f, false, synth, 0, true);
frame(o, 5, 2, 3, f, false, synth, 1, true);
frame(o, 6, 2, 3, f, false, vmap, 0, true);
frame(o, 7, 2, 3, f, false, it(Material.MAP), 0, true);
frame(o, 8, 2, 3, f, false, it(Material.FILLED_MAP), 0, true);
ItemStack[] inv = {it(Material.DIAMOND_SWORD), it(Material.STONE), synth, it(Material.CHEST)};
for (int i = 0; i < inv.length; i++) frame(o, i - 1, 1, 3, f, false, inv[i], i == 0 ? 1 : 0, false);
ItemStack[] glow = {it(Material.APPLE), it(Material.STONE), synth, null};
for (int i = 0; i < glow.length; i++) frame(o, i + 3, 1, 3, f, true, glow[i], 0, true);
frame(o, 7, 1, 3, f, false, null, 0, true);
frame(o, 8, 1, 3, f, true, null, 0, true);
}
/** Three stone blocks with a frame on every side: block item, tool, synthetic map (all at rotation 1). */
private void framesDirs(Location o) {
ItemStack[] items = {it(Material.OAK_STAIRS), it(Material.DIAMOND_SWORD), synthMap()};
BlockFace[] faces = {BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST, BlockFace.UP, BlockFace.DOWN};
for (int i = 0; i < items.length; i++) {
int bx = 3 + i * 5, by = 4, bz = 3;
put(o, bx, by, bz, "stone");
for (BlockFace face : faces)
frame(o, bx + face.getModX(), by + face.getModY(), bz + face.getModZ(), face, i == 2, items[i], 1, true);
}
}
/** Black/white concrete checkerboard: wall z=6 (x -6..17, y 0..11), ceiling y=11 and floor y=-1. */
private void camChecker(Location o) {
for (int a = -6; a < 18; a++)
for (int b = 0; b < 12; b++) put(o, a, b, 6, checker(a + b));
for (int a = -6; a < 18; a++)
for (int c = -12; c < 6; c++) {
put(o, a, 11, c, checker(a + c));
put(o, a, -1, c, checker(a + c));
}
}
private static String checker(int parity) {
return (parity & 1) == 0 ? "white_concrete" : "black_concrete";
}
/** Black wall z=6 with one item frame (synthetic map, 1x1 block) facing north at cell (5,5,5). */
private void camFrame(Location o) {
for (int a = -6; a < 18; a++)
for (int b = 0; b < 12; b++) put(o, a, b, 6, "black_concrete");
frame(o, 5, 5, 5, BlockFace.NORTH, false, synthMap(), 0, true);
}
/** Rows of cross/rescaled plants on grass (z=0..3), potted plants and stems; see the geo_* scenes. */
private void geoPlants(Location o) {
for (int x = -1; x <= 12; x++) for (int z = -1; z <= 5; z++) put(o, x, -1, z, "grass_block");
String[] row0 = {"short_grass", "fern", "dandelion", "poppy", "azure_bluet", "oxeye_daisy", "cornflower",
"oak_sapling", "birch_sapling", "cherry_sapling", "dead_bush", "brown_mushroom"};
String[] row1 = {"sweet_berry_bush[age=3]", "sugar_cane", "cobweb", "red_tulip", "wither_rose", "bush",
"short_dry_grass", "firefly_bush", "mangrove_propagule[hanging=false,stage=1]", "lily_of_the_valley",
"pumpkin_stem[age=7]", "crimson_roots"};
String[] row3 = {"potted_fern", "potted_oak_sapling", "potted_red_tulip", "potted_dead_bush",
"potted_azalea_bush", "potted_mangrove_propagule", "big_dripleaf_stem[facing=north]",
"sea_pickle[pickles=4,waterlogged=false]", "sea_pickle[pickles=1,waterlogged=false]", "azalea",
"flowering_azalea", "pointed_dripstone[thickness=tip,vertical_direction=up]"};
for (int x = 0; x < 12; x++) {
put(o, x, 0, 0, row0[x]);
put(o, x, 0, 1, row1[x]);
put(o, x, 0, 3, row3[x]);
}
for (int x = 0; x < 12; x += 3) {
put(o, x, 0, 2, "tall_grass[half=lower]");
put(o, x, 1, 2, "tall_grass[half=upper]");
put(o, x + 1, 0, 2, "sunflower[half=lower]");
put(o, x + 1, 1, 2, "sunflower[half=upper]");
}
put(o, 2, 0, 2, "big_dripleaf[facing=north,tilt=none]");
put(o, 5, 0, 2, "melon_stem[age=4]");
put(o, 8, 0, 2, "attached_pumpkin_stem[facing=east]");
put(o, 9, 0, 2, "pumpkin");
put(o, 11, 0, 2, "large_fern[half=lower]");
put(o, 11, 1, 2, "large_fern[half=upper]");
}
/**
* Row z=0: amethyst cluster/buds (up/north/east), campfires, fire; z=2: raised rails, tripwire hooks, coral wall
* fans, spawner/vault/trial spawner (inverted inner faces), snow layer 2; z=6/7: hanging signs rot 0-15 under a
* stone ceiling (rot 3/7/11/15 = Euler chain rotation).
*/
private void geoMisc(Location o) {
put(o, 0, 0, 0, "amethyst_cluster[facing=up]");
put(o, 1, 0, 0, "large_amethyst_bud[facing=up]");
put(o, 2, 0, 0, "medium_amethyst_bud[facing=up]");
put(o, 3, 0, 0, "small_amethyst_bud[facing=up]");
put(o, 4, 0, 1, "amethyst_block");
put(o, 4, 0, 0, "amethyst_cluster[facing=north]");
put(o, 5, 0, 1, "amethyst_block");
put(o, 5, 1, 1, "large_amethyst_bud[facing=up]");
put(o, 6, 0, 0, "campfire[lit=true,facing=south]");
put(o, 7, 0, 0, "campfire[lit=false,facing=east]");
put(o, 8, 0, 0, "soul_campfire[lit=true,facing=south]");
put(o, 9, 0, 0, "netherrack");
put(o, 9, 1, 0, "fire");
put(o, 10, 0, 1, "stone");
put(o, 10, 0, 0, "fire[north=true]");
put(o, 11, 0, 0, "soul_fire");
put(o, 11, -1, 0, "soul_sand");
put(o, 0, 0, 2, "rail[shape=ascending_east]");
put(o, 1, 0, 2, "stone");
put(o, 2, 0, 2, "powered_rail[shape=ascending_north,powered=true]");
put(o, 2, 0, 1, "stone");
put(o, 3, 0, 2, "detector_rail[shape=ascending_west]");
put(o, 2, 0, 3, "stone");
put(o, 4, 0, 3, "stone");
put(o, 4, 0, 2, "tripwire_hook[attached=true,facing=north,powered=false]");
put(o, 5, 0, 3, "stone");
put(o, 5, 0, 2, "tripwire_hook[attached=true,facing=north,powered=true]");
put(o, 6, 0, 3, "stone");
put(o, 6, 0, 2, "tube_coral_wall_fan[facing=north,waterlogged=false]");
put(o, 7, 0, 3, "stone");
put(o, 7, 0, 2, "fire_coral_wall_fan[facing=north,waterlogged=false]");
put(o, 8, 0, 2, "spawner");
put(o, 9, 0, 2, "vault[facing=north]");
put(o, 10, 0, 2, "trial_spawner");
put(o, 11, 0, 2, "snow[layers=2]");
for (int x = -1; x <= 9; x++) for (int z = 5; z <= 8; z++) put(o, x, 4, z, "stone");
for (int r = 0; r < 16; r++) put(o, r % 8, 3, 6 + r / 8, "oak_hanging_sign[rotation=" + r + ",attached=false]");
}
/** Pool: water columns 1..6 deep (two blocks wide each) on sand, with glass/gold/white stained glass at the bottom. */
private void geoWater(Location o) {
for (int x = -1; x <= 12; x++)
for (int z = -1; z <= 5; z++)
for (int y = -8; y <= -1; y++) {
boolean wall = x == -1 || x == 12 || z == -1 || z == 5;
int depth = x < 0 || x > 11 ? 0 : x / 2 + 1;
String b = wall ? "smooth_stone" : y >= -depth ? "water" : y == -depth - 1 ? "sand" : "stone";
put(o, x, y, z, b);
}
for (int x = 0; x < 12; x += 2) {
int y = -(x / 2 + 1);
put(o, x, y, 1, "glass");
put(o, x + 1, y, 1, "glass");
put(o, x, y, 2, "gold_block");
put(o, x + 1, y, 3, "white_stained_glass");
}
for (int x = -1; x <= 12; x++) put(o, x, 0, 6, "stone");
}
/** Black wall z=6 with a white vertical stripe (x=5) and a white horizontal stripe (y=5). */
private void camStripes(Location o) {
for (int a = -6; a < 18; a++)
for (int b = 0; b < 12; b++) put(o, a, b, 6, a == 5 || b == 5 ? "white_concrete" : "black_concrete");
}
private ItemFrame frame(Location o, int x, int y, int z, BlockFace face, boolean glow, ItemStack item, int rot, boolean visible) {
Location l = o.clone().add(x + 0.5, y + 0.5, z + 0.5);
Class<? extends ItemFrame> c = glow ? GlowItemFrame.class : ItemFrame.class;
ItemFrame fr = w.spawn(l, c, e -> {
e.setFacingDirection(face, true);
e.setPersistent(false);
});
if (item != null) fr.setItem(item, false);
fr.setRotation(Rotation.values()[rot]);
fr.setVisible(visible);
fr.setFixed(true);
return fr;
}
private static ItemStack mapItem(MapView view) {
ItemStack s = it(Material.FILLED_MAP);
MapMeta mm = (MapMeta) s.getItemMeta();
mm.setMapView(view);
s.setItemMeta(mm);
return s;
}
/** A real stored PixelPics photo (lowest managed map id on disk), or the synthetic map if none exists. */
private ItemStack photoMap() {
File dir = new File(Main.getInstance().getDataFolder(), "images");
int best = Integer.MAX_VALUE;
File[] files = dir.listFiles();
if (files != null)
for (File f : files) {
Matcher m = MAP_FILE.matcher(f.getName());
if (m.matches()) best = Math.min(best, Integer.parseInt(m.group(1)));
}
int id = best == Integer.MAX_VALUE ? -1 : best;
@SuppressWarnings("deprecation") MapView v = id < 0 ? null : Bukkit.getMap(id);
if (v == null) return synthMap();
if (v.getRenderers().stream().noneMatch(r -> r instanceof ImageMapRenderer)) {
byte[] idx = MapManager.readIndices(id);
if (idx != null) MapManager.attachView(v, new ImageMapRenderer(idx));
}
getLogger().info("frames: photo map id " + id);
return mapItem(v);
}
private static final java.util.regex.Pattern MAP_FILE = java.util.regex.Pattern.compile("mapId_(\\d+)\\.mcmap");
private ItemStack synthMap;
/** Asymmetric test picture as a PixelPics map: red top-left, blue top-right, green bottom-left, black "F". */
private ItemStack synthMap() {
if (synthMap != null) return synthMap;
BufferedImage img = new BufferedImage(128, 128, BufferedImage.TYPE_INT_RGB);
Graphics2D g = img.createGraphics();
g.setColor(java.awt.Color.WHITE);
g.fillRect(0, 0, 128, 128);
g.setColor(java.awt.Color.RED);
g.fillRect(0, 0, 32, 32);
g.setColor(java.awt.Color.BLUE);
g.fillRect(96, 0, 32, 32);
g.setColor(new java.awt.Color(0, 160, 0));
g.fillRect(0, 96, 32, 32);
g.setColor(java.awt.Color.BLACK);
g.fillRect(48, 24, 14, 80);
g.fillRect(48, 24, 44, 14);
g.fillRect(48, 56, 32, 12);
g.dispose();
MapView v = Bukkit.createMap(w);
MapManager.attachView(v, new ImageMapRenderer(MapImageDither.dither(img)));
return synthMap = mapItem(v);
}
/** A plain vanilla map whose server-side colours are written directly (diagonal colour bands + transparent corner). */
private ItemStack vanillaMap() {
MapView v = Bukkit.createMap(w);
try {
Object data = mapData(v);
byte[] colors = (byte[]) data.getClass().getField("colors").get(data);
byte[] palette = {(byte) (18 * 4 + 2), (byte) (7 * 4 + 2), (byte) (12 * 4 + 1), (byte) (29 * 4 + 2)};
for (int y = 0; y < 128; y++)
for (int x = 0; x < 128; x++)
colors[y * 128 + x] = x < 24 && y < 24 ? 0 : palette[((x + y) / 32) % palette.length];
} catch (ReflectiveOperationException e) {
getLogger().warning("vanillaMap: " + e);
}
return mapItem(v);
}
// --- fluids ------------------------------------------------------------------------------------------------
/** Sets the biome of the scene area (deterministic water colour / water fog). */
private void biome(Location o, Biome biome) {
for (int x = -8; x < 20; x += 2)
for (int z = -14; z < 14; z += 2)
for (int y = -16; y < 16; y += 2)
w.setBiome(o.getBlockX() + x, o.getBlockY() + y, o.getBlockZ() + z, biome);
}
/** A block placed WITH physics (fluids schedule their flow ticks). */
private void putPhys(Location o, int x, int y, int z, String data) {
o.clone().add(x, y, z).getBlock().setBlockData(Bukkit.createBlockData("minecraft:" + data), true);
}
private void liqFlow(Location o) {
biome(o, Biome.PLAINS);
// Stone rim keeps the water on the platform.
for (int x = -5; x <= 16; x++)
for (int z = -11; z <= 10; z++)
if (x == -5 || x == 16 || z == -11 || z == 10) put(o, x, 0, z, "stone");
for (int x = 0; x <= 4; x++)
for (int z = -2; z <= 2; z++)
for (int y = 0; y <= 2; y++) put(o, x, y, z, "stone");
for (int x = 10; x <= 12; x++)
for (int z = -6; z <= -4; z++) {
put(o, x, -2, z, "sand");
put(o, x, -1, z, "water");
}
putPhys(o, 2, 3, 0, "water");
putPhys(o, 11, 0, 5, "water");
}
private static final String[] LOGGED = {
"oak_stairs[facing=north,waterlogged=true]", "stone_slab[type=bottom,waterlogged=true]",
"stone_slab[type=top,waterlogged=true]", "oak_fence[waterlogged=true]", "glass_pane[waterlogged=true]",
"seagrass", "kelp_plant", "tube_coral[waterlogged=true]", "oak_leaves[waterlogged=true,persistent=true]",
"lantern[waterlogged=true]", "sea_pickle[pickles=3,waterlogged=true]", "oak_trapdoor[half=bottom,open=false,waterlogged=true]"};
private void liqLogged(Location o) {
biome(o, Biome.PLAINS);
for (int x = -1; x <= LOGGED.length; x++)
for (int z = 0; z <= 2; z++) {
put(o, x, -2, z, "sand");
put(o, x, -1, z, "water");
}
for (int i = 0; i < LOGGED.length; i++) {
put(o, i, -1, 1, LOGGED[i]); // in the pool
put(o, i, 0, 5, LOGGED[i]); // in the air: water inside, surface at 8/9
}
// Under water: a waterlogged stair and slab with a water source above them.
put(o, 2, 0, 8, "oak_stairs[facing=south,waterlogged=true]");
put(o, 2, 1, 8, "water");
put(o, 4, 0, 8, "stone_slab[type=bottom,waterlogged=true]");
put(o, 4, 1, 8, "water");
// Water next to glass and leaves (overlay faces) on the floor.
put(o, 7, 0, 8, "water");
put(o, 8, 0, 8, "glass");
put(o, 9, 0, 8, "water");
put(o, 10, 0, 8, "oak_leaves[persistent=true]");
}
private void liqUnder(Location o, Biome biome) {
biome(o, biome);
for (int x = -5; x <= 17; x++)
for (int z = -11; z <= 11; z++)
for (int y = -9; y <= -1; y++) {
boolean wall = x == -5 || x == 17 || z == -11 || z == 11;
put(o, x, y, z, wall ? "stone_bricks" : y == -9 ? "sand" : "water");
}
String[] pillars = {"red_concrete", "yellow_concrete", "lime_concrete", "blue_concrete", "white_concrete", "orange_concrete"};
int[][] at = {{0, 0}, {4, -4}, {8, 4}, {12, -6}, {15, 8}, {16, -10}};
for (int i = 0; i < pillars.length; i++)
for (int y = -8; y <= -4; y++) put(o, at[i][0], y, at[i][1], pillars[i]);
put(o, 2, -8, 3, "gold_block");
put(o, 2, -7, 3, "glass");
put(o, 5, -8, 1, "oak_stairs[facing=west,waterlogged=true]");
for (int i = 0; i < 8; i++) put(o, -2 + i * 2, -8, -6 + (i % 3), "seagrass");
// Above the water: a bridge and lily pads, seen from below through the surface.
for (int x = 3; x <= 9; x++) put(o, x, 2, 1, "oak_planks");
put(o, 6, 0, -2, "lily_pad");
put(o, 7, 0, -1, "lily_pad");
}
private void liqLava(Location o) {
biome(o, Biome.PLAINS);
for (int x = -2; x <= 17; x++)
for (int z = -3; z <= 5; z++)
if (x == -2 || x == 17 || z == -3 || z == 5) put(o, x, 0, z, "stone");
for (int x = 2; x <= 4; x++)
for (int z = 0; z <= 2; z++) {
put(o, x, -2, z, "basalt");
put(o, x, -1, z, "lava");
}
// A source with flowing lava around it (overworld drop-off 2: amounts 8/6/4/2 = levels 0/2/4/6).
for (int dx = -3; dx <= 3; dx++)
for (int dz = -3; dz <= 3; dz++) {
int d = Math.abs(dx) + Math.abs(dz);
if (d <= 3) put(o, 9 + dx, 0, 1 + dz, d == 0 ? "lava" : "lava[level=" + (2 * d) + "]");
}
// Lava falling from a pillar.
for (int y = 0; y <= 2; y++) put(o, 14, y, 1, "stone");
put(o, 14, 3, 1, "lava");
put(o, 15, 3, 1, "lava[level=2]");
for (int y = 0; y <= 2; y++) put(o, 15, y, 1, "lava[level=10]");
}
}