9
0
mirror of https://github.com/Xiao-MoMi/craft-engine.git synced 2025-12-29 03:49:15 +00:00

Initial commit

This commit is contained in:
XiaoMoMi
2025-03-13 15:52:31 +08:00
commit 29276e92cc
3956 changed files with 108711 additions and 0 deletions

View File

@@ -0,0 +1,245 @@
package net.momirealms.craftengine.bukkit.api;
import net.momirealms.craftengine.bukkit.block.BukkitBlockManager;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.bukkit.plugin.user.BukkitServerPlayer;
import net.momirealms.craftengine.bukkit.util.BlockStateUtils;
import net.momirealms.craftengine.bukkit.util.Reflections;
import net.momirealms.craftengine.bukkit.world.BukkitWorld;
import net.momirealms.craftengine.core.block.CustomBlock;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.block.UpdateOption;
import net.momirealms.craftengine.core.entity.player.InteractionHand;
import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.loot.parameter.LootParameters;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.context.ContextHolder;
import net.momirealms.craftengine.core.world.Vec3d;
import net.momirealms.craftengine.core.world.World;
import net.momirealms.sparrow.nbt.CompoundTag;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.SoundCategory;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public final class CraftEngineBlocks {
/**
* Gets a custom block by ID
*
* @param id id
* @return the custom block
*/
@Nullable
public static CustomBlock byId(@NotNull Key id) {
return BukkitBlockManager.instance().getBlock(id).orElse(null);
}
/**
* Places a custom block state at a certain location
*
* @param location location
* @param block block state to place
* @param playSound whether to play place sounds
* @return success or not
*/
public static boolean place(@NotNull Location location,
@NotNull ImmutableBlockState block,
boolean playSound) {
return place(location, block, UpdateOption.UPDATE_ALL, playSound);
}
/**
* Place a custom block with given properties
*
* @param location location
* @param blockId block owner id
* @param properties properties
* @param playSound whether to play place sounds
* @return success or not
*/
public static boolean place(@NotNull Location location,
@NotNull Key blockId,
@NotNull CompoundTag properties,
boolean playSound) {
CustomBlock block = byId(blockId);
if (block == null) return false;
return place(location, block.getBlockState(properties), UpdateOption.UPDATE_ALL, playSound);
}
/**
* Place a custom block with given properties
*
* @param location location
* @param blockId block owner id
* @param properties properties
* @param option update options
* @param playSound whether to play place sounds
* @return success or not
*/
public static boolean place(@NotNull Location location,
@NotNull Key blockId,
@NotNull CompoundTag properties,
@NotNull UpdateOption option,
boolean playSound) {
CustomBlock block = byId(blockId);
if (block == null) return false;
return place(location, block.getBlockState(properties), option, playSound);
}
/**
* Places a custom block state at a certain location
*
* @param location location
* @param block block state to place
* @param option update options
* @param playSound whether to play place sounds
* @return success or not
*/
public static boolean place(@NotNull Location location,
@NotNull ImmutableBlockState block,
@NotNull UpdateOption option,
boolean playSound) {
boolean success;
try {
Object worldServer = Reflections.field$CraftWorld$ServerLevel.get(location.getWorld());
Object blockPos = Reflections.constructor$BlockPos.newInstance(location.getBlockX(), location.getBlockY(), location.getBlockZ());
Object blockState = block.customBlockState().handle();
Object oldBlockState = Reflections.method$BlockGetter$getBlockState.invoke(worldServer, blockPos);
success = (boolean) Reflections.method$LevelWriter$setBlock.invoke(worldServer, blockPos, blockState, option.flags());
if (success) {
Reflections.method$BlockStateBase$onPlace.invoke(blockState, worldServer, blockPos, oldBlockState, true);
if (playSound) {
location.getWorld().playSound(location, block.sounds().placeSound().toString(), SoundCategory.BLOCKS, 1, 0.8f);
}
}
} catch (ReflectiveOperationException e) {
CraftEngine.instance().logger().warn("Failed to set nms block", e);
return false;
}
return success;
}
/**
* Removes a block from the world if it's a custom one
*
* @param block block to remove
* @return success or not
*/
public static boolean remove(@NotNull Block block) {
if (!isCustomBlock(block)) return false;
block.setType(Material.AIR, true);
return true;
}
/**
* Removes a block from the world if it's a custom one
*
* @param block block to remove
* @param applyPhysics whether to apply physics
* @return success or not
*/
public static boolean remove(@NotNull Block block,
boolean applyPhysics) {
if (!isCustomBlock(block)) return false;
block.setType(Material.AIR, applyPhysics);
return true;
}
/**
* Removes a block from the world if it's a custom one
*
* @param block block to remove
* @param player player who breaks the block
* @param applyPhysics whether to apply physics
* @param dropLoot whether to drop block loots
* @param playSound whether to play break sounds
* @param sendParticles whether to send break particles
* @return success or not
*/
public static boolean remove(@NotNull Block block,
@Nullable Player player,
boolean applyPhysics,
boolean dropLoot,
boolean playSound,
boolean sendParticles) {
ImmutableBlockState state = getCustomBlockState(block);
if (state == null || state.isEmpty()) return false;
World world = new BukkitWorld(block.getWorld());
Location location = block.getLocation();
Vec3d vec3d = new Vec3d(location.getBlockX() + 0.5, location.getBlockY() + 0.5, location.getBlockZ() + 0.5);
if (dropLoot) {
ContextHolder.Builder builder = new ContextHolder.Builder().withParameter(LootParameters.WORLD, world).withParameter(LootParameters.LOCATION, vec3d);
BukkitServerPlayer serverPlayer = BukkitCraftEngine.instance().adapt(player);
if (player != null) {
builder.withParameter(LootParameters.PLAYER, serverPlayer);
builder.withParameter(LootParameters.TOOL, serverPlayer.getItemInHand(InteractionHand.MAIN_HAND));
}
for (Item<?> item : state.getDrops(builder, world)) {
world.dropItemNaturally(vec3d, item);
}
}
if (playSound) {
world.playBlockSound(vec3d, state.sounds().breakSound(), 1, 0.8f);
}
if (sendParticles) {
// TODO Particles
//ParticleUtils.addBlockBreakParticles(block.getWorld(), LocationUtils.toBlockPos(location), state.customBlockState().handle());
}
block.setType(Material.AIR, applyPhysics);
return true;
}
/**
* Checks if a block is a custom one
*
* @param block block
* @return is custom block or not
*/
public static boolean isCustomBlock(@NotNull Block block) {
BlockData blockData = block.getBlockData();
int stateId = BlockStateUtils.blockDataToId(blockData);
return !BlockStateUtils.isVanillaBlock(stateId);
}
/**
* Gets custom block state from a bukkit block
*
* @param block block
* @return custom block state
*/
@Nullable
public static ImmutableBlockState getCustomBlockState(@NotNull Block block) {
BlockData blockData = block.getBlockData();
int stateId = BlockStateUtils.blockDataToId(blockData);
return BukkitBlockManager.instance().getImmutableBlockState(stateId);
}
/**
* Gets custom block state from bukkit block data
*
* @param blockData block data
* @return custom block state
*/
@Nullable
public static ImmutableBlockState getCustomBlockState(@NotNull BlockData blockData) {
int stateId = BlockStateUtils.blockDataToId(blockData);
return BukkitBlockManager.instance().getImmutableBlockState(stateId);
}
/**
* Creates bukkit block data from a custom block state
*
* @param blockState custom block state
* @return bukkit block data
*/
@NotNull
public static BlockData createBukkitBlockData(@NotNull ImmutableBlockState blockState) {
return BlockStateUtils.createBlockData(blockState.customBlockState().handle());
}
}

View File

@@ -0,0 +1,260 @@
package net.momirealms.craftengine.bukkit.api;
import net.momirealms.craftengine.bukkit.entity.furniture.BukkitFurnitureManager;
import net.momirealms.craftengine.bukkit.entity.furniture.LoadedFurniture;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.bukkit.util.LocationUtils;
import net.momirealms.craftengine.bukkit.world.BukkitWorld;
import net.momirealms.craftengine.core.entity.furniture.AnchorType;
import net.momirealms.craftengine.core.entity.furniture.CustomFurniture;
import net.momirealms.craftengine.core.entity.player.InteractionHand;
import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.loot.LootTable;
import net.momirealms.craftengine.core.loot.parameter.LootParameters;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.context.ContextHolder;
import net.momirealms.craftengine.core.world.Vec3d;
import net.momirealms.craftengine.core.world.World;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
public class CraftEngineFurniture {
/**
* Gets custom furniture by ID
*
* @param id id
* @return the custom furniture
*/
public static CustomFurniture byId(@NotNull Key id) {
return BukkitFurnitureManager.instance().getFurniture(id).orElse(null);
}
/**
* Places furniture at the certain location
*
* @param location location
* @param furnitureId furniture to place
* @param anchorType anchor type
* @return the loaded furniture
*/
@Nullable
public static LoadedFurniture place(Location location, Key furnitureId, AnchorType anchorType) {
CustomFurniture furniture = byId(furnitureId);
if (furniture == null) return null;
return BukkitFurnitureManager.instance().place(furniture, location, anchorType, true);
}
/**
* Places furniture at the certain location
*
* @param location location
* @param furniture furniture to place
* @param anchorType anchor type
* @return the loaded furniture
*/
@NotNull
public static LoadedFurniture place(Location location, CustomFurniture furniture, AnchorType anchorType) {
return BukkitFurnitureManager.instance().place(furniture, location, anchorType, true);
}
/**
* Places furniture at the certain location
*
* @param location location
* @param furnitureId furniture to place
* @param anchorType anchor type
* @param playSound whether to play place sounds
* @return the loaded furniture
*/
@Nullable
public static LoadedFurniture place(Location location, Key furnitureId, AnchorType anchorType, boolean playSound) {
CustomFurniture furniture = byId(furnitureId);
if (furniture == null) return null;
return BukkitFurnitureManager.instance().place(furniture, location, anchorType, playSound);
}
/**
* Places furniture at the certain location
*
* @param location location
* @param furniture furniture to place
* @param anchorType anchor type
* @param playSound whether to play place sounds
* @return the loaded furniture
*/
@NotNull
public static LoadedFurniture place(Location location, CustomFurniture furniture, AnchorType anchorType, boolean playSound) {
return BukkitFurnitureManager.instance().place(furniture, location, anchorType, playSound);
}
/**
* Check if an entity is a piece of furniture
*
* @param entity entity to check
* @return is furniture or not
*/
public static boolean isFurniture(@NotNull Entity entity) {
String furnitureId = entity.getPersistentDataContainer().get(BukkitFurnitureManager.FURNITURE_KEY, PersistentDataType.STRING);
return furnitureId != null;
}
/**
* Check if an entity is a seat
*
* @param entity entity to check
* @return is seat or not
*/
public static boolean isSeat(@NotNull Entity entity) {
Integer baseEntityId = entity.getPersistentDataContainer().get(BukkitFurnitureManager.FURNITURE_SEAT_BASE_ENTITY_KEY, PersistentDataType.INTEGER);
return baseEntityId != null;
}
/**
* Gets the base furniture by the base entity
*
* @param baseEntity base entity
* @return the loaded furniture
*/
@Nullable
public static LoadedFurniture getLoadedFurnitureByBaseEntity(@NotNull Entity baseEntity) {
return BukkitFurnitureManager.instance().getLoadedFurnitureByBaseEntityId(baseEntity.getEntityId());
}
/**
* Gets the base furniture by the seat entity
*
* @param seat seat entity
* @return the loaded furniture
*/
@Nullable
public static LoadedFurniture getLoadedFurnitureBySeat(@NotNull Entity seat) {
Integer baseEntityId = seat.getPersistentDataContainer().get(BukkitFurnitureManager.FURNITURE_SEAT_BASE_ENTITY_KEY, PersistentDataType.INTEGER);
if (baseEntityId == null) return null;
return BukkitFurnitureManager.instance().getLoadedFurnitureByBaseEntityId(baseEntityId);
}
/**
* Removes furniture
*
* @param furniture furniture base entity
* @return success or not
*/
public static boolean remove(@NotNull Entity furniture) {
if (!isFurniture(furniture)) return false;
LoadedFurniture loadedFurniture = BukkitFurnitureManager.instance().getLoadedFurnitureByBaseEntityId(furniture.getEntityId());
if (loadedFurniture == null) return false;
loadedFurniture.destroy();
return true;
}
/**
* Removes furniture, with more options
*
* @param furniture furniture base entity
* @param dropLoot whether to drop loots
* @param playSound whether to play break sound
* @return success or not
*/
public static boolean remove(@NotNull Entity furniture,
boolean dropLoot,
boolean playSound) {
if (!isFurniture(furniture)) return false;
LoadedFurniture loadedFurniture = BukkitFurnitureManager.instance().getLoadedFurnitureByBaseEntityId(furniture.getEntityId());
if (loadedFurniture == null) return false;
remove(loadedFurniture, (net.momirealms.craftengine.core.entity.player.Player) null, dropLoot, playSound);
return true;
}
/**
* Removes furniture, with more options
*
* @param furniture furniture base entity
* @param player the player who removes the furniture
* @param dropLoot whether to drop loots
* @param playSound whether to play break sound
* @return success or not
*/
public static boolean remove(@NotNull Entity furniture,
@Nullable Player player,
boolean dropLoot,
boolean playSound) {
if (!isFurniture(furniture)) return false;
LoadedFurniture loadedFurniture = BukkitFurnitureManager.instance().getLoadedFurnitureByBaseEntityId(furniture.getEntityId());
if (loadedFurniture == null) return false;
remove(loadedFurniture, player, dropLoot, playSound);
return true;
}
/**
* Removes furniture by providing a plugin furniture instance
*
* @param loadedFurniture loaded furniture
* @param dropLoot whether to drop loots
* @param playSound whether to play break sound
*/
public static void remove(@NotNull LoadedFurniture loadedFurniture,
boolean dropLoot,
boolean playSound) {
remove(loadedFurniture, (net.momirealms.craftengine.core.entity.player.Player) null, dropLoot, playSound);
}
/**
* Removes furniture by providing a plugin furniture instance
*
* @param loadedFurniture loaded furniture
* @param player the player who removes the furniture
* @param dropLoot whether to drop loots
* @param playSound whether to play break sound
*/
public static void remove(@NotNull LoadedFurniture loadedFurniture,
@Nullable Player player,
boolean dropLoot,
boolean playSound) {
remove(loadedFurniture, player == null ? null : BukkitCraftEngine.instance().adapt(player), dropLoot, playSound);
}
/**
* Removes furniture by providing a plugin furniture instance
*
* @param loadedFurniture loaded furniture
* @param player the player who removes the furniture
* @param dropLoot whether to drop loots
* @param playSound whether to play break sound
*/
@SuppressWarnings("unchecked")
public static void remove(@NotNull LoadedFurniture loadedFurniture,
@Nullable net.momirealms.craftengine.core.entity.player.Player player,
boolean dropLoot,
boolean playSound) {
Location location = loadedFurniture.location();
loadedFurniture.destroy();
LootTable<ItemStack> lootTable = (LootTable<ItemStack>) loadedFurniture.furniture().lootTable();
Vec3d vec3d = LocationUtils.toVec3d(location);
World world = new BukkitWorld(location.getWorld());
if (dropLoot && lootTable != null) {
ContextHolder.Builder builder = ContextHolder.builder();
builder.withParameter(LootParameters.LOCATION, vec3d);
builder.withParameter(LootParameters.WORLD, world);
if (player != null) {
builder.withParameter(LootParameters.PLAYER, player);
builder.withParameter(LootParameters.TOOL, player.getItemInHand(InteractionHand.MAIN_HAND));
}
List<Item<ItemStack>> items = lootTable.getRandomItems(builder.build(), world);
for (Item<ItemStack> item : items) {
world.dropItemNaturally(vec3d, item);
}
}
if (playSound) {
world.playBlockSound(vec3d, loadedFurniture.furniture().settings().sounds().breakSound(), 1f, 0.8f);
}
}
}

View File

@@ -0,0 +1,58 @@
package net.momirealms.craftengine.bukkit.api;
import net.momirealms.craftengine.bukkit.item.BukkitItemManager;
import net.momirealms.craftengine.bukkit.util.ItemUtils;
import net.momirealms.craftengine.core.item.CustomItem;
import net.momirealms.craftengine.core.util.Key;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class CraftEngineItems {
/**
* Gets a custom item by ID
*
* @param id id
* @return the custom item
*/
@Nullable
public static CustomItem<ItemStack> byId(@NotNull Key id) {
return BukkitItemManager.instance().getCustomItem(id).orElse(null);
}
/**
* Gets a custom item by existing item stack
*
* @param itemStack item stack
* @return the custom item
*/
@Nullable
public static CustomItem<ItemStack> byItemStack(@NotNull ItemStack itemStack) {
if (ItemUtils.isEmpty(itemStack)) return null;
return BukkitItemManager.instance().wrap(itemStack).getCustomItem().orElse(null);
}
/**
* Checks if an item is a custom one
*
* @param itemStack item stack
* @return true if it's a custom item
*/
public static boolean isCustomItem(@NotNull ItemStack itemStack) {
if (ItemUtils.isEmpty(itemStack)) return false;
return BukkitItemManager.instance().wrap(itemStack).isCustomItem();
}
/**
* Gets custom item id from item stack
*
* @param itemStack item stack
* @return the custom id, null if it's not a custom one
*/
@Nullable
public static Key getCustomItemId(@NotNull ItemStack itemStack) {
if (ItemUtils.isEmpty(itemStack)) return null;
return BukkitItemManager.instance().wrap(itemStack).customId().orElse(null);
}
}

View File

@@ -0,0 +1,40 @@
package net.momirealms.craftengine.bukkit.api.event;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
import java.nio.file.Path;
public class AsyncResourcePackGenerateEvent extends Event {
private static final HandlerList HANDLER_LIST = new HandlerList();
private final Path generatedPackPath;
private final Path zipFilePath;
public AsyncResourcePackGenerateEvent(@NotNull Path generatedPackPath,
@NotNull Path zipFilePath) {
super(true);
this.generatedPackPath = generatedPackPath;
this.zipFilePath = zipFilePath;
}
@NotNull
public Path resourcePackFolder() {
return generatedPackPath;
}
@NotNull
public Path zipFilePath() {
return zipFilePath;
}
@NotNull
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
@NotNull
public HandlerList getHandlers() {
return getHandlerList();
}
}

View File

@@ -0,0 +1,29 @@
package net.momirealms.craftengine.bukkit.api.event;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
public class CraftEngineReloadEvent extends Event {
private static final HandlerList HANDLER_LIST = new HandlerList();
private final BukkitCraftEngine plugin;
public CraftEngineReloadEvent(BukkitCraftEngine plugin) {
this.plugin = plugin;
}
public BukkitCraftEngine plugin() {
return plugin;
}
@NotNull
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
@Override
public @NotNull HandlerList getHandlers() {
return HANDLER_LIST;
}
}

View File

@@ -0,0 +1,94 @@
package net.momirealms.craftengine.bukkit.api.event;
import net.momirealms.craftengine.core.block.CustomBlock;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.entity.player.InteractionHand;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;
import org.jetbrains.annotations.NotNull;
public class CustomBlockAttemptPlaceEvent extends PlayerEvent implements Cancellable {
private static final HandlerList HANDLER_LIST = new HandlerList();
private boolean cancelled;
private final CustomBlock customBlock;
private final ImmutableBlockState state;
private final Location location;
private final BlockFace clickedFace;
private final Block clickedBlock;
private final InteractionHand hand;
public CustomBlockAttemptPlaceEvent(@NotNull Player player,
@NotNull Location location,
@NotNull ImmutableBlockState state,
@NotNull BlockFace clickedFace,
@NotNull Block clickedBlock,
@NotNull InteractionHand hand) {
super(player);
this.customBlock = state.owner().value();
this.state = state;
this.location = location;
this.clickedFace = clickedFace;
this.clickedBlock = clickedBlock;
this.hand = hand;
}
@NotNull
public Player player() {
return getPlayer();
}
@NotNull
public InteractionHand hand() {
return hand;
}
@NotNull
public Block clickedBlock() {
return clickedBlock;
}
@NotNull
public BlockFace clickedFace() {
return clickedFace;
}
@NotNull
public CustomBlock customBlock() {
return this.customBlock;
}
@NotNull
public Location location() {
return this.location;
}
@NotNull
public ImmutableBlockState blockState() {
return this.state;
}
@NotNull
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
@NotNull
public HandlerList getHandlers() {
return getHandlerList();
}
@Override
public boolean isCancelled() {
return this.cancelled;
}
@Override
public void setCancelled(boolean cancel) {
this.cancelled = cancel;
}
}

View File

@@ -0,0 +1,86 @@
package net.momirealms.craftengine.bukkit.api.event;
import net.momirealms.craftengine.core.block.CustomBlock;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;
import org.jetbrains.annotations.NotNull;
public class CustomBlockBreakEvent extends PlayerEvent implements Cancellable {
private static final HandlerList HANDLER_LIST = new HandlerList();
private boolean cancelled;
private final CustomBlock customBlock;
private final ImmutableBlockState state;
private final Location location;
private final Block bukkitBlock;
private boolean dropItems;
public CustomBlockBreakEvent(@NotNull Player player,
@NotNull Location location,
@NotNull Block bukkitBlock,
@NotNull ImmutableBlockState state) {
super(player);
this.customBlock = state.owner().value();
this.state = state;
this.bukkitBlock = bukkitBlock;
this.location = location;
this.dropItems = true;
}
public boolean dropItems() {
return dropItems;
}
public void setDropItems(boolean dropItems) {
this.dropItems = dropItems;
}
@NotNull
public Block bukkitBlock() {
return bukkitBlock;
}
@NotNull
public Player player() {
return getPlayer();
}
@NotNull
public CustomBlock customBlock() {
return this.customBlock;
}
@NotNull
public Location location() {
return this.location;
}
@NotNull
public ImmutableBlockState blockState() {
return this.state;
}
@NotNull
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
@NotNull
public HandlerList getHandlers() {
return getHandlerList();
}
@Override
public boolean isCancelled() {
return this.cancelled;
}
@Override
public void setCancelled(boolean cancel) {
this.cancelled = cancel;
}
}

View File

@@ -0,0 +1,116 @@
package net.momirealms.craftengine.bukkit.api.event;
import net.momirealms.craftengine.core.block.CustomBlock;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.entity.player.InteractionHand;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class CustomBlockInteractEvent extends PlayerEvent implements Cancellable {
private static final HandlerList HANDLER_LIST = new HandlerList();
private boolean cancelled;
private final CustomBlock customBlock;
private final Block bukkitBlock;
private final ImmutableBlockState state;
private final Location location;
private final Location interactionPoint;
private final InteractionHand hand;
private final Action action;
private final BlockFace clickedFace;
public CustomBlockInteractEvent(@NotNull Player player,
@NotNull Location location,
@Nullable Location interactionPoint,
@NotNull ImmutableBlockState state,
@NotNull Block bukkitBlock,
@NotNull BlockFace clickedFace,
@NotNull InteractionHand hand,
@NotNull Action action) {
super(player);
this.customBlock = state.owner().value();
this.bukkitBlock = bukkitBlock;
this.state = state;
this.location = location;
this.interactionPoint = interactionPoint;
this.hand = hand;
this.action = action;
this.clickedFace = clickedFace;
}
@NotNull
public BlockFace clickedFace() {
return clickedFace;
}
@Nullable
public Location interactionPoint() {
return interactionPoint;
}
@NotNull
public Action action() {
return action;
}
@NotNull
public InteractionHand hand() {
return hand;
}
@NotNull
public Block bukkitBlock() {
return bukkitBlock;
}
@NotNull
public CustomBlock customBlock() {
return this.customBlock;
}
@NotNull
public Player player() {
return getPlayer();
}
@NotNull
public Location location() {
return this.location;
}
@NotNull
public ImmutableBlockState blockState() {
return this.state;
}
@NotNull
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
@NotNull
public HandlerList getHandlers() {
return getHandlerList();
}
@Override
public boolean isCancelled() {
return this.cancelled;
}
@Override
public void setCancelled(boolean cancel) {
this.cancelled = cancel;
}
public enum Action {
LEFT_CLICK,
RIGHT_CLICK
}
}

View File

@@ -0,0 +1,85 @@
package net.momirealms.craftengine.bukkit.api.event;
import net.momirealms.craftengine.core.block.CustomBlock;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.entity.player.InteractionHand;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;
import org.jetbrains.annotations.NotNull;
public class CustomBlockPlaceEvent extends PlayerEvent implements Cancellable {
private static final HandlerList HANDLER_LIST = new HandlerList();
private final CustomBlock customBlock;
private final ImmutableBlockState state;
private final Location location;
private final InteractionHand hand;
private final Block bukkitBlock;
private boolean cancelled;
public CustomBlockPlaceEvent(@NotNull Player player,
@NotNull Location location,
@NotNull ImmutableBlockState state,
@NotNull Block bukkitBlock,
@NotNull InteractionHand hand) {
super(player);
this.customBlock = state.owner().value();
this.state = state;
this.location = location;
this.hand = hand;
this.bukkitBlock = bukkitBlock;
}
@NotNull
public Block bukkitBlock() {
return bukkitBlock;
}
@NotNull
public InteractionHand hand() {
return hand;
}
@NotNull
public CustomBlock customBlock() {
return this.customBlock;
}
@NotNull
public Location location() {
return this.location;
}
@NotNull
public ImmutableBlockState blockState() {
return this.state;
}
@NotNull
public Player player() {
return getPlayer();
}
@NotNull
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
@NotNull
public HandlerList getHandlers() {
return getHandlerList();
}
@Override
public boolean isCancelled() {
return this.cancelled;
}
@Override
public void setCancelled(boolean cancel) {
this.cancelled = cancel;
}
}

View File

@@ -0,0 +1,95 @@
package net.momirealms.craftengine.bukkit.api.event;
import net.momirealms.craftengine.core.entity.furniture.AnchorType;
import net.momirealms.craftengine.core.entity.furniture.CustomFurniture;
import net.momirealms.craftengine.core.entity.player.InteractionHand;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;
import org.jetbrains.annotations.NotNull;
public class FurnitureAttemptPlaceEvent extends PlayerEvent implements Cancellable {
private static final HandlerList HANDLER_LIST = new HandlerList();
private boolean cancelled;
private final CustomFurniture furniture;
private final Location location;
private final AnchorType anchorType;
private final BlockFace clickedFace;
private final Block clickedBlock;
private final InteractionHand hand;
public FurnitureAttemptPlaceEvent(@NotNull Player player,
@NotNull CustomFurniture furniture,
@NotNull AnchorType anchorType,
@NotNull Location location,
@NotNull BlockFace clickedFace,
@NotNull InteractionHand hand,
@NotNull Block clickedBlock) {
super(player);
this.furniture = furniture;
this.location = location;
this.anchorType = anchorType;
this.clickedFace = clickedFace;
this.clickedBlock = clickedBlock;
this.hand = hand;
}
@NotNull
public Block clickedBlock() {
return clickedBlock;
}
@NotNull
public InteractionHand hand() {
return hand;
}
@NotNull
public BlockFace clickedFace() {
return clickedFace;
}
@NotNull
public Player player() {
return getPlayer();
}
@NotNull
public AnchorType anchorType() {
return anchorType;
}
@NotNull
public Location location() {
return location;
}
@NotNull
public CustomFurniture furniture() {
return furniture;
}
@NotNull
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
@NotNull
public HandlerList getHandlers() {
return getHandlerList();
}
@Override
public boolean isCancelled() {
return this.cancelled;
}
@Override
public void setCancelled(boolean cancel) {
this.cancelled = cancel;
}
}

View File

@@ -0,0 +1,50 @@
package net.momirealms.craftengine.bukkit.api.event;
import net.momirealms.craftengine.bukkit.entity.furniture.LoadedFurniture;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;
import org.jetbrains.annotations.NotNull;
public class FurnitureBreakEvent extends PlayerEvent implements Cancellable {
private static final HandlerList HANDLER_LIST = new HandlerList();
private boolean cancelled;
private final LoadedFurniture furniture;
public FurnitureBreakEvent(@NotNull Player player,
@NotNull LoadedFurniture furniture) {
super(player);
this.furniture = furniture;
}
@NotNull
public Player player() {
return getPlayer();
}
@NotNull
public LoadedFurniture furniture() {
return this.furniture;
}
@NotNull
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
@NotNull
public HandlerList getHandlers() {
return getHandlerList();
}
@Override
public boolean isCancelled() {
return this.cancelled;
}
@Override
public void setCancelled(boolean cancel) {
this.cancelled = cancel;
}
}

View File

@@ -0,0 +1,68 @@
package net.momirealms.craftengine.bukkit.api.event;
import net.momirealms.craftengine.bukkit.entity.furniture.LoadedFurniture;
import net.momirealms.craftengine.core.entity.player.InteractionHand;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;
import org.jetbrains.annotations.NotNull;
public class FurnitureInteractEvent extends PlayerEvent implements Cancellable {
private static final HandlerList HANDLER_LIST = new HandlerList();
private boolean cancelled;
private final LoadedFurniture furniture;
private final InteractionHand hand;
private final Location interactionPoint;
public FurnitureInteractEvent(@NotNull Player player,
@NotNull LoadedFurniture furniture,
@NotNull InteractionHand hand,
@NotNull Location interactionPoint) {
super(player);
this.furniture = furniture;
this.hand = hand;
this.interactionPoint = interactionPoint;
}
@NotNull
public Location interactionPoint() {
return interactionPoint;
}
@NotNull
public Player player() {
return getPlayer();
}
@NotNull
public InteractionHand hand() {
return hand;
}
@NotNull
public LoadedFurniture furniture() {
return this.furniture;
}
@NotNull
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
@NotNull
public HandlerList getHandlers() {
return getHandlerList();
}
@Override
public boolean isCancelled() {
return this.cancelled;
}
@Override
public void setCancelled(boolean cancel) {
this.cancelled = cancel;
}
}

View File

@@ -0,0 +1,68 @@
package net.momirealms.craftengine.bukkit.api.event;
import net.momirealms.craftengine.bukkit.entity.furniture.LoadedFurniture;
import net.momirealms.craftengine.core.entity.player.InteractionHand;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;
import org.jetbrains.annotations.NotNull;
public class FurniturePlaceEvent extends PlayerEvent implements Cancellable {
private static final HandlerList HANDLER_LIST = new HandlerList();
private final Location location;
private final LoadedFurniture furniture;
private final InteractionHand hand;
private boolean cancelled;
public FurniturePlaceEvent(@NotNull Player player,
@NotNull LoadedFurniture furniture,
@NotNull Location location,
@NotNull InteractionHand hand) {
super(player);
this.location = location;
this.furniture = furniture;
this.hand = hand;
}
@NotNull
public InteractionHand hand() {
return hand;
}
@NotNull
public LoadedFurniture furniture() {
return furniture;
}
@NotNull
public Location location() {
return location;
}
@NotNull
public Player player() {
return getPlayer();
}
@NotNull
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
@NotNull
public HandlerList getHandlers() {
return getHandlerList();
}
@Override
public boolean isCancelled() {
return this.cancelled;
}
@Override
public void setCancelled(boolean cancel) {
this.cancelled = cancel;
}
}

View File

@@ -0,0 +1,314 @@
package net.momirealms.craftengine.bukkit.block;
import net.momirealms.craftengine.bukkit.api.event.CustomBlockBreakEvent;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.bukkit.plugin.user.BukkitServerPlayer;
import net.momirealms.craftengine.bukkit.util.*;
import net.momirealms.craftengine.bukkit.world.BukkitWorld;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.block.PushReaction;
import net.momirealms.craftengine.core.block.properties.Property;
import net.momirealms.craftengine.core.entity.player.InteractionHand;
import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.item.ItemKeys;
import net.momirealms.craftengine.core.loot.parameter.LootParameters;
import net.momirealms.craftengine.core.plugin.config.ConfigManager;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.context.ContextHolder;
import net.momirealms.craftengine.core.world.BlockPos;
import net.momirealms.craftengine.core.world.Vec3d;
import org.bukkit.*;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.type.NoteBlock;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.*;
import org.bukkit.event.entity.EntityExplodeEvent;
import org.bukkit.event.world.GenericGameEvent;
import org.bukkit.inventory.ItemStack;
import java.util.List;
import java.util.Optional;
public class BlockEventListener implements Listener {
private final BukkitCraftEngine plugin;
private final boolean enableNoteBlockCheck;
private final BukkitBlockManager manager;
// private static final Set<Material> WATER_BUCKETS = Arrays.stream(ItemKeys.WATER_BUCKETS).map(it -> Registry.MATERIAL.get(new NamespacedKey(it.namespace(), it.value()))).collect(Collectors.toSet());
public BlockEventListener(BukkitCraftEngine plugin, BukkitBlockManager manager, boolean enableNoteBlockCheck) {
this.plugin = plugin;
this.manager = manager;
this.enableNoteBlockCheck = enableNoteBlockCheck;
}
@EventHandler(ignoreCancelled = true)
public void onPlaceBlock(BlockPlaceEvent event) {
Player player = event.getPlayer();
BukkitServerPlayer serverPlayer = plugin.adapt(player);
// send swing if player is clicking a replaceable block
if (serverPlayer.shouldResendSwing()) {
player.swingHand(event.getHand());
}
// send sound if the placed block's sounds are removed
if (ConfigManager.enableSoundSystem()) {
Block block = event.getBlock();
Object blockState = BlockStateUtils.blockDataToBlockState(block.getBlockData());
Object ownerBlock = BlockStateUtils.getBlockOwner(blockState);
if (this.manager.isBlockSoundRemoved(ownerBlock)) {
if (player.getInventory().getItemInMainHand().getType() != Material.DEBUG_STICK) {
try {
Object soundType = Reflections.field$BlockBehaviour$soundType.get(ownerBlock);
Object placeSound = Reflections.field$SoundType$placeSound.get(soundType);
player.playSound(block.getLocation(), Reflections.field$SoundEvent$location.get(placeSound).toString(), SoundCategory.BLOCKS, 1f, 0.8f);
} catch (ReflectiveOperationException e) {
this.plugin.logger().warn("Failed to get sound type", e);
}
}
return;
}
}
// resend sound if the clicked block is interactable on client side
if (serverPlayer.shouldResendSound()) {
try {
Block block = event.getBlock();
Object blockState = BlockStateUtils.blockDataToBlockState(block.getBlockData());
Object ownerBlock = BlockStateUtils.getBlockOwner(blockState);
Object soundType = Reflections.field$BlockBehaviour$soundType.get(ownerBlock);
Object placeSound = Reflections.field$SoundType$placeSound.get(soundType);
player.playSound(block.getLocation(), Reflections.field$SoundEvent$location.get(placeSound).toString(), SoundCategory.BLOCKS, 1f, 0.8f);
} catch (ReflectiveOperationException e) {
this.plugin.logger().warn("Failed to get sound type", e);
}
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOW)
public void onPlayerBreak(BlockBreakEvent event) {
org.bukkit.block.Block block = event.getBlock();
Object blockState = BlockStateUtils.blockDataToBlockState(block.getBlockData());
int stateId = BlockStateUtils.blockStateToId(blockState);
if (!BlockStateUtils.isVanillaBlock(stateId)) {
ImmutableBlockState state = manager.getImmutableBlockStateUnsafe(stateId);
if (!state.isEmpty()) {
Location location = block.getLocation();
// trigger event
CustomBlockBreakEvent customBreakEvent = new CustomBlockBreakEvent(event.getPlayer(), location, block, state);
boolean isCancelled = EventUtils.fireAndCheckCancel(customBreakEvent);
if (isCancelled) {
event.setCancelled(true);
return;
}
net.momirealms.craftengine.core.world.World world = new BukkitWorld(location.getWorld());
// handle waterlogged blocks
@SuppressWarnings("unchecked")
Property<Boolean> waterloggedProperty = (Property<Boolean>) state.owner().value().getProperty("waterlogged");
if (waterloggedProperty != null) {
boolean waterlogged = state.get(waterloggedProperty);
if (waterlogged) {
location.getWorld().setBlockData(location, Material.WATER.createBlockData());
}
}
// play sound
Vec3d vec3d = new Vec3d(location.getBlockX() + 0.5, location.getBlockY() + 0.5, location.getBlockZ() + 0.5);
world.playBlockSound(vec3d, state.sounds().breakSound(), 1f, 0.8f);
Player player = event.getPlayer();
if (player.getGameMode() == GameMode.CREATIVE) {
return;
}
BukkitServerPlayer serverPlayer = plugin.adapt(player);
Item<ItemStack> itemInHand = serverPlayer.getItemInHand(InteractionHand.MAIN_HAND);
Key itemId = Optional.ofNullable(itemInHand).map(Item::id).orElse(ItemKeys.AIR);
// do not drop if it's not the correct tool
if (!state.settings().isCorrectTool(itemId) || !customBreakEvent.dropItems()) {
return;
}
// drop items
ContextHolder.Builder builder = ContextHolder.builder();
builder.withParameter(LootParameters.WORLD, world);
builder.withParameter(LootParameters.LOCATION, vec3d);
builder.withParameter(LootParameters.PLAYER, plugin.adapt(player));
builder.withParameter(LootParameters.TOOL, itemInHand);
for (Item<Object> item : state.getDrops(builder, world)) {
world.dropItemNaturally(vec3d, item);
}
}
} else if (ConfigManager.enableSoundSystem()) {
Object ownerBlock = BlockStateUtils.getBlockOwner(blockState);
if (manager.isBlockSoundRemoved(ownerBlock)) {
try {
Object soundType = Reflections.field$BlockBehaviour$soundType.get(ownerBlock);
Object breakSound = Reflections.field$SoundType$breakSound.get(soundType);
block.getWorld().playSound(block.getLocation(), Reflections.field$SoundEvent$location.get(breakSound).toString(), SoundCategory.BLOCKS, 1f, 0.8f);
} catch (ReflectiveOperationException e) {
plugin.logger().warn("Failed to get sound type", e);
}
}
}
}
@EventHandler(ignoreCancelled = true)
public void onStep(GenericGameEvent event) {
if (event.getEvent() != GameEvent.STEP) return;
Entity entity = event.getEntity();
if (!(entity instanceof Player player)) return;
BlockPos pos = EntityUtils.getOnPos(player);
Location playerLocation = player.getLocation();
BlockData blockData = player.getWorld().getBlockData(pos.x(), pos.y(), pos.z());
Object blockState = BlockStateUtils.blockDataToBlockState(blockData);
int stateId = BlockStateUtils.blockStateToId(blockState);
if (!BlockStateUtils.isVanillaBlock(stateId)) {
ImmutableBlockState state = manager.getImmutableBlockStateUnsafe(stateId);
player.playSound(playerLocation, state.sounds().stepSound().toString(), SoundCategory.BLOCKS, 0.15f, 1f);
} else if (ConfigManager.enableSoundSystem()) {
Object ownerBlock = BlockStateUtils.getBlockOwner(blockState);
if (manager.isBlockSoundRemoved(ownerBlock)) {
try {
Object soundType = Reflections.field$BlockBehaviour$soundType.get(ownerBlock);
Object stepSound = Reflections.field$SoundType$stepSound.get(soundType);
player.playSound(playerLocation, Reflections.field$SoundEvent$location.get(stepSound).toString(), SoundCategory.BLOCKS, 0.15f, 1f);
} catch (ReflectiveOperationException e) {
plugin.logger().warn("Failed to get sound type", e);
}
}
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onPistonRetract(BlockPistonRetractEvent event) {
handlePistonEvent(event.getDirection(), event.getBlocks(), event.getBlock());
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onPistonExtend(BlockPistonExtendEvent event) {
handlePistonEvent(event.getDirection(), event.getBlocks(), event.getBlock());
}
private void handlePistonEvent(BlockFace face, List<Block> blocksList, Block piston) {
int blocks = blocksList.size();
net.momirealms.craftengine.core.world.World world = new BukkitWorld(piston.getWorld());
for (int i = blocks - 1; i >= 0; --i) {
Location oldLocation = blocksList.get(i).getLocation();
BlockPos oldPos = new BlockPos(oldLocation.getBlockX(), oldLocation.getBlockY(), oldLocation.getBlockZ());
Block block = blocksList.get(i);
ImmutableBlockState blockState = manager.getImmutableBlockState(BlockStateUtils.blockDataToId(block.getBlockData()));
if (blockState != null && blockState.pushReaction() == PushReaction.DESTROY) {
// break actions
ContextHolder.Builder builder = ContextHolder.builder();
Vec3d vec3d = Vec3d.atCenterOf(oldPos);
builder.withParameter(LootParameters.LOCATION, vec3d);
builder.withParameter(LootParameters.WORLD, world);
for (Item<Object> item : blockState.getDrops(builder, world)) {
world.dropItemNaturally(vec3d, item);
}
world.playBlockSound(vec3d, blockState.sounds().breakSound(),1f, 0.8f);
}
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onEntityExplode(EntityExplodeEvent event) {
handleExplodeEvent(event.blockList(), new BukkitWorld(event.getEntity().getWorld()), event.getYield());
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onBlockExplode(BlockExplodeEvent event) {
handleExplodeEvent(event.blockList(), new BukkitWorld(event.getBlock().getWorld()), event.getYield());
}
private void handleExplodeEvent(List<org.bukkit.block.Block> blocks, net.momirealms.craftengine.core.world.World world, float yield) {
for (int i = blocks.size() - 1; i >= 0; i--) {
Block block = blocks.get(i);
Location location = block.getLocation();
BlockPos blockPos = new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ());
ImmutableBlockState state = manager.getImmutableBlockState(BlockStateUtils.blockDataToId(block.getBlockData()));
if (state != null && !state.isEmpty()) {
ContextHolder.Builder builder = ContextHolder.builder();
Vec3d vec3d = Vec3d.atCenterOf(blockPos);
builder.withParameter(LootParameters.LOCATION, vec3d);
builder.withParameter(LootParameters.WORLD, world);
if (yield < 1f) {
builder.withParameter(LootParameters.EXPLOSION_RADIUS, 1.0f / yield);
}
for (Item<Object> item : state.getDrops(builder, world)) {
world.dropItemNaturally(vec3d, item);
}
world.playBlockSound(vec3d, state.sounds().breakSound(), 1f, 0.8f);
}
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onBlockPhysics(BlockPhysicsEvent event) {
if (!this.enableNoteBlockCheck) return;
Block block = event.getBlock();
// for vanilla blocks
if (block.getBlockData() instanceof NoteBlock) {
try {
World world = block.getWorld();
Location location = block.getLocation();
Block sourceBlock = event.getSourceBlock();
BlockFace direction = sourceBlock.getFace(block);
if (direction == BlockFace.UP || direction == BlockFace.DOWN) {
Object serverLevel = Reflections.field$CraftWorld$ServerLevel.get(world);
Object chunkSource = Reflections.field$ServerLevel$chunkSource.get(serverLevel);
Object blockPos = Reflections.constructor$BlockPos.newInstance(location.getBlockX(), location.getBlockY(), location.getBlockZ());
Reflections.method$ServerChunkCache$blockChanged.invoke(chunkSource, blockPos);
if (direction == BlockFace.UP) {
NoteBlockChainUpdateUtils.noteBlockChainUpdate(serverLevel, chunkSource, Reflections.instance$Direction$UP, blockPos, 0);
} else {
NoteBlockChainUpdateUtils.noteBlockChainUpdate(serverLevel, chunkSource, Reflections.instance$Direction$DOWN, blockPos, 0);
}
}
} catch (ReflectiveOperationException e) {
plugin.logger().warn("Failed to sync note block states", e);
}
}
}
// TODO Is there a way to deceive the server?
// @SuppressWarnings("unchecked")
// @EventHandler(ignoreCancelled = true)
// public void onDispenserWork(BlockDispenseEvent event) {
// ItemStack itemStack = event.getItem();
// Material type = itemStack.getType();
// Block block = event.getBlock();
// if (type == Material.BUCKET) {
// if (block.getBlockData() instanceof Dispenser dispenser) {
// Block against = block.getRelative(dispenser.getFacing());
// ImmutableBlockState state = this.manager.getImmutableBlockState(BlockStateUtils.blockDataToId(against.getBlockData()));
// if (state != null && !state.isEmpty()) {
// Location location = against.getLocation();
// CustomBlock customBlock = state.owner().value();
// Property<Boolean> waterlogged = (Property<Boolean>) customBlock.getProperty("waterlogged");
// if (waterlogged == null) return;
// if (!state.get(waterlogged)) return;
// ImmutableBlockState nextState = state.with(waterlogged, false);
// CraftEngineBlocks.place(location, nextState, UpdateOption.UPDATE_ALL);
// }
// }
// } else if (WATER_BUCKETS.contains(type)) {
// if (block.getBlockData() instanceof Dispenser dispenser) {
// Block against = block.getRelative(dispenser.getFacing());
// ImmutableBlockState state = this.manager.getImmutableBlockState(BlockStateUtils.blockDataToId(against.getBlockData()));
// if (state != null && !state.isEmpty()) {
// Location location = against.getLocation();
// CustomBlock customBlock = state.owner().value();
// Property<Boolean> waterlogged = (Property<Boolean>) customBlock.getProperty("waterlogged");
// if (waterlogged == null) return;
// ImmutableBlockState nextState = state.with(waterlogged, true);
// CraftEngineBlocks.place(location, nextState, UpdateOption.UPDATE_ALL);
// }
// }
// }
// }
}

View File

@@ -0,0 +1,775 @@
package net.momirealms.craftengine.bukkit.block;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import dev.dejvokep.boostedyaml.YamlDocument;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.bukkit.plugin.injector.BukkitInjector;
import net.momirealms.craftengine.bukkit.plugin.network.PacketConsumers;
import net.momirealms.craftengine.bukkit.util.BlockStateUtils;
import net.momirealms.craftengine.bukkit.util.KeyUtils;
import net.momirealms.craftengine.bukkit.util.Reflections;
import net.momirealms.craftengine.bukkit.util.RegistryUtils;
import net.momirealms.craftengine.core.block.*;
import net.momirealms.craftengine.core.block.properties.Properties;
import net.momirealms.craftengine.core.block.properties.Property;
import net.momirealms.craftengine.core.loot.LootTable;
import net.momirealms.craftengine.core.pack.Pack;
import net.momirealms.craftengine.core.pack.model.generator.ModelGeneration;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.config.ConfigManager;
import net.momirealms.craftengine.core.registry.BuiltInRegistries;
import net.momirealms.craftengine.core.registry.Holder;
import net.momirealms.craftengine.core.registry.WritableRegistry;
import net.momirealms.craftengine.core.util.*;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.block.data.BlockData;
import org.bukkit.event.HandlerList;
import org.bukkit.inventory.ItemStack;
import org.incendo.cloud.suggestion.Suggestion;
import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable;
import java.io.File;
import java.lang.reflect.Field;
import java.nio.file.Path;
import java.util.*;
public class BukkitBlockManager extends AbstractBlockManager {
private static BukkitBlockManager instance;
private final BukkitCraftEngine plugin;
// Used to store override information of json files
private final Map<Key, Map<String, JsonElement>> blockStateOverrides = new HashMap<>();
// A temporary map used to detect whether the same block state corresponds to multiple models.
private final Map<Integer, Key> tempRegistryIdConflictMap = new HashMap<>();
// A temporary map that converts the custom block registered on the server to the vanilla block ID.
private final Map<Integer, Integer> tempBlockAppearanceConvertor = new HashMap<>();
// The total amount of blocks registered
private int customBlockCount;
// CraftEngine objects
private final Map<Key, CustomBlock> id2CraftEngineBlocks = new HashMap<>();
private final ImmutableBlockState[] stateId2ImmutableBlockStates;
// Minecraft objects
// Cached new blocks $ holders
private ImmutableMap<Key, Integer> internalId2StateId;
private ImmutableMap<Integer, Object> stateId2BlockHolder;
// This map is used to change the block states that are not necessarily needed into a certain block state
private ImmutableMap<Integer, Integer> blockAppearanceMapper;
// Used to automatically arrange block states for strings such as minecraft:note_block:0
private ImmutableMap<Key, List<Integer>> blockAppearanceArranger;
private ImmutableMap<Key, List<Integer>> realBlockArranger;
// Record the amount of real blocks by block type
private LinkedHashMap<Key, Integer> registeredRealBlockSlots;
// A set of blocks that sounds have been removed
private ImmutableSet<Object> affectedSoundBlocks;
private ImmutableMap<Key, Key> soundMapper;
// A list to record the order of registration
private List<Key> blockRegisterOrder = new ArrayList<>();
// a reverted mapper
private final Map<Integer, List<Integer>> appearanceToRealState = new HashMap<>();
// Cached command suggestions
private final List<Suggestion> cachedSuggestions = new ArrayList<>();
// Event listeners
private final BlockEventListener blockEventListener;
private final FallingBlockRemoveListener fallingBlockRemoveListener;
public BukkitBlockManager(BukkitCraftEngine plugin) {
super(plugin);
this.plugin = plugin;
this.initVanillaRegistry();
this.loadMappingsAndAdditionalBlocks();
if (plugin.hasMod() && plugin.requiresRestart()) {
blockEventListener = null;
fallingBlockRemoveListener = null;
stateId2ImmutableBlockStates = null;
return;
}
this.registerBlocks();
this.registerEmptyBlock();
this.initMirrorRegistry();
boolean enableNoteBlocks = this.blockAppearanceArranger.containsKey(BlockKeys.NOTE_BLOCK);
this.blockEventListener = new BlockEventListener(plugin, this, enableNoteBlocks);
if (enableNoteBlocks) {
this.recordVanillaNoteBlocks();
}
if (VersionHelper.isVersionNewerThan1_20_3()) {
this.fallingBlockRemoveListener = new FallingBlockRemoveListener();
} else this.fallingBlockRemoveListener = null;
this.stateId2ImmutableBlockStates = new ImmutableBlockState[customBlockCount];
Arrays.fill(this.stateId2ImmutableBlockStates, EmptyBlock.INSTANCE.getDefaultState());
instance = this;
}
public static BukkitBlockManager instance() {
return instance;
}
@Override
public void delayedInit() {
Bukkit.getPluginManager().registerEvents(this.blockEventListener, plugin.bootstrap());
if (this.fallingBlockRemoveListener != null) {
Bukkit.getPluginManager().registerEvents(this.fallingBlockRemoveListener, plugin.bootstrap());
}
}
@Override
public void unload() {
super.clearModelsToGenerate();
this.clearCache();
this.appearanceToRealState.clear();
this.id2CraftEngineBlocks.clear();
this.cachedSuggestions.clear();
this.blockStateOverrides.clear();
if (EmptyBlock.INSTANCE != null)
Arrays.fill(this.stateId2ImmutableBlockStates, EmptyBlock.INSTANCE.getDefaultState());
}
@Override
public void disable() {
this.unload();
HandlerList.unregisterAll(this.blockEventListener);
if (this.fallingBlockRemoveListener != null) HandlerList.unregisterAll(this.fallingBlockRemoveListener);
}
@Override
public Map<Key, Key> soundMapper() {
return this.soundMapper;
}
@Override
public void delayedLoad() {
initSuggestions();
resetPacketConsumers();
clearCache();
}
private void clearCache() {
this.tempRegistryIdConflictMap.clear();
this.tempBlockAppearanceConvertor.clear();
}
public void initFastAsyncWorldEditHook() {
// do nothing
}
public void initWorldEditHook() {
for (Key newBlockId : this.blockRegisterOrder) {
WorldEditHook.register(newBlockId);
}
}
@Nullable
public Object getMinecraftBlockHolder(int stateId) {
return stateId2BlockHolder.get(stateId);
}
@NotNull
public ImmutableBlockState getImmutableBlockStateUnsafe(int stateId) {
return stateId2ImmutableBlockStates[stateId - BlockStateUtils.vanillaStateSize()];
}
@Nullable
public ImmutableBlockState getImmutableBlockState(int stateId) {
if (!BlockStateUtils.isVanillaBlock(stateId)) {
return stateId2ImmutableBlockStates[stateId - BlockStateUtils.vanillaStateSize()];
}
return null;
}
@Override
public Map<Key, Map<String, JsonElement>> blockOverrides() {
return blockStateOverrides;
}
@Override
public Map<Key, CustomBlock> blocks() {
return this.id2CraftEngineBlocks;
}
@Override
public Optional<CustomBlock> getBlock(Key key) {
return Optional.ofNullable(this.id2CraftEngineBlocks.get(key));
}
@Override
public Collection<Suggestion> cachedSuggestions() {
return this.cachedSuggestions;
}
@Override
public void initSuggestions() {
this.cachedSuggestions.clear();
Set<String> states = new HashSet<>();
for (CustomBlock block : this.id2CraftEngineBlocks.values()) {
states.add(block.id().toString());
for (ImmutableBlockState state : block.variantProvider().states()) {
states.add(state.toString());
}
}
for (String state : states) {
this.cachedSuggestions.add(Suggestion.suggestion(state));
}
}
public ImmutableMap<Key, List<Integer>> blockAppearanceArranger() {
return blockAppearanceArranger;
}
public ImmutableMap<Key, List<Integer>> realBlockArranger() {
return realBlockArranger;
}
@Nullable
public List<Integer> appearanceToRealStates(int appearanceStateId) {
return this.appearanceToRealState.get(appearanceStateId);
}
private void initMirrorRegistry() {
int size = RegistryUtils.currentBlockRegistrySize();
PackedBlockState[] states = new PackedBlockState[size];
for (int i = 0; i < size; i++) {
states[i] = new PackedBlockState(BlockStateUtils.idToBlockState(i), i);
}
BlockRegistryMirror.init(states);
}
private void registerEmptyBlock() {
Holder.Reference<CustomBlock> holder = ((WritableRegistry<CustomBlock>) BuiltInRegistries.BLOCK).registerForHolder(new ResourceKey<>(BuiltInRegistries.BLOCK.key().location(), Key.withDefaultNamespace("empty")));
EmptyBlock emptyBlock = new EmptyBlock(Key.withDefaultNamespace("empty"), holder);
holder.bindValue(emptyBlock);
}
private void resetPacketConsumers() {
Map<Integer, Integer> finalMapping = new HashMap<>(this.blockAppearanceMapper);
int stoneId = BlockStateUtils.blockStateToId(Reflections.instance$Blocks$STONE$defaultState);
for (int custom : this.internalId2StateId.values()) {
finalMapping.put(custom, stoneId);
}
finalMapping.putAll(this.tempBlockAppearanceConvertor);
PacketConsumers.init(finalMapping, RegistryUtils.currentBlockRegistrySize());
}
private void initVanillaRegistry() {
int vanillaStateCount;
if (plugin.hasMod()) {
try {
Class<?> modClass = ReflectionUtils.getClazz(CraftEngine.MOD_CLASS);
Field amountField = ReflectionUtils.getDeclaredField(modClass, "vanillaRegistrySize");
vanillaStateCount = (int) amountField.get(null);
} catch (Exception e) {
vanillaStateCount = RegistryUtils.currentBlockRegistrySize();
plugin.logger().severe("Fatal error", e);
}
} else {
vanillaStateCount = RegistryUtils.currentBlockRegistrySize();
}
plugin.logger().info("Vanilla block count: " + vanillaStateCount);
BlockStateUtils.init(vanillaStateCount);
}
@SuppressWarnings("unchecked")
private void registerBlocks() {
plugin.logger().info("Registering blocks. Please wait...");
try {
ImmutableMap.Builder<Key, Integer> builder1 = ImmutableMap.builder();
ImmutableMap.Builder<Integer, Object> builder2 = ImmutableMap.builder();
ImmutableMap.Builder<Key, List<Integer>> builder3 = ImmutableMap.builder();
Set<Object> affectedSounds = new HashSet<>();
Set<Object> affectedBlocks = new HashSet<>();
List<Key> order = new ArrayList<>();
unfreezeRegistry();
int counter = 0;
for (Map.Entry<Key, Integer> baseBlockAndItsCount : this.registeredRealBlockSlots.entrySet()) {
counter = registerBlockVariants(baseBlockAndItsCount, counter, builder1, builder2, builder3, affectedSounds, order);
}
freezeRegistry();
this.plugin.logger().info("Registered block count: " + counter);
this.customBlockCount = counter;
this.internalId2StateId = builder1.build();
this.stateId2BlockHolder = builder2.build();
this.realBlockArranger = builder3.build();
this.blockRegisterOrder = ImmutableList.copyOf(order);
for (Object block : (Iterable<Object>) Reflections.instance$BuiltInRegistries$BLOCK) {
Object soundType = Reflections.field$BlockBehaviour$soundType.get(block);
if (affectedSounds.contains(soundType)) {
affectedBlocks.add(block);
}
}
this.affectedSoundBlocks = ImmutableSet.copyOf(affectedBlocks);
ImmutableMap.Builder<Key, Key> soundMapperBuilder = ImmutableMap.builder();
for (Object soundType : affectedSounds) {
for (Field field : List.of(Reflections.field$SoundType$placeSound, Reflections.field$SoundType$fallSound, Reflections.field$SoundType$hitSound, Reflections.field$SoundType$stepSound, Reflections.field$SoundType$breakSound)) {
Object soundEvent = field.get(soundType);
Key previousId = Key.of(Reflections.field$SoundEvent$location.get(soundEvent).toString());
soundMapperBuilder.put(previousId, Key.of(previousId.namespace(), "replaced." + previousId.value()));
}
}
this.soundMapper = soundMapperBuilder.build();
} catch (Throwable e) {
plugin.logger().warn("Failed to inject blocks.", e);
}
}
@Override
public void parseSection(Pack pack, Path path, Key id, Map<String, Object> section) {
// read block settings
BlockSettings settings = BlockSettings.fromMap(MiscUtils.castToMap(section.getOrDefault("settings", Map.of()), false));
// read loot table
LootTable<ItemStack> lootTable = LootTable.fromMap(MiscUtils.castToMap(section.getOrDefault("loot", Map.of()), false));
// read states
Map<String, Property<?>> properties;
Map<String, Integer> appearances;
Map<String, VariantState> variants;
Map<String, Object> stateSection = MiscUtils.castToMap(section.get("state"), true);
if (stateSection != null) {
properties = Map.of();
int internalId = MiscUtils.getAsInt(stateSection.getOrDefault("id", -1));
if (PreConditions.runIfTrue(internalId < 0, () -> plugin.logger().warn(path, "No state id configured for block " + id))) return;
Pair<Key, Integer> pair = parseAppearanceSection(path, stateSection, id);
if (pair == null) return;
appearances = Map.of("default", pair.right());
Key internalBlockId = Key.of(CraftEngine.NAMESPACE, pair.left().value() + "_" + internalId);
int internalBlockRegistryId = MiscUtils.getAsInt(this.internalId2StateId.getOrDefault(internalBlockId, -1));
if (internalBlockRegistryId == -1) {
plugin.logger().warn(path, "Failed to register " + id + " because id " + internalId + " is not a value between 0~" + (MiscUtils.getAsInt(this.registeredRealBlockSlots.get(pair.left()))-1) +
". Consider editing additional-real-blocks.yml if the number of real block IDs is insufficient while there are still available appearances");
return;
}
variants = Map.of("", new VariantState("default", settings, internalBlockRegistryId));
} else {
Map<String, Object> statesSection = MiscUtils.castToMap(section.get("states"), true);
if (statesSection == null) {
plugin.logger().warn(path, "No states configured for block " + id);
return;
}
Map<String, Object> propertySection = MiscUtils.castToMap(statesSection.get("properties"), true);
if (PreConditions.isNull(propertySection, () -> plugin.logger().warn(path, "No properties configured for block " + id))) return;
properties = parseProperties(path, propertySection);
Map<String, Object> appearancesSection = MiscUtils.castToMap(statesSection.get("appearances"), true);
if (PreConditions.isNull(appearancesSection, () -> plugin.logger().warn(path, "No appearances configured for block " + id))) return;
appearances = new HashMap<>();
Map<String, Key> tempTypeMap = new HashMap<>();
for (Map.Entry<String, Object> appearanceEntry : appearancesSection.entrySet()) {
if (appearanceEntry.getValue() instanceof Map<?, ?> appearanceSection) {
Pair<Key, Integer> pair = parseAppearanceSection(path, MiscUtils.castToMap(appearanceSection, false), id);
if (pair == null) return;
appearances.put(appearanceEntry.getKey(), pair.right());
tempTypeMap.put(appearanceEntry.getKey(), pair.left());
}
}
Map<String, Object> variantsSection = MiscUtils.castToMap(statesSection.get("variants"), true);
if (PreConditions.isNull(variantsSection, () -> plugin.logger().warn(path, "No variants configured for block " + id))) return;
variants = new HashMap<>();
for (Map.Entry<String, Object> variantEntry : variantsSection.entrySet()) {
if (variantEntry.getValue() instanceof Map<?, ?> variantSection0) {
Map<String, Object> variantSection = MiscUtils.castToMap(variantSection0, false);
String variantName = variantEntry.getKey();
String appearance = (String) variantSection.get("appearance");
if (appearance == null) {
plugin.logger().warn(path, "No appearance configured for variant " + variantName);
return;
}
if (!appearances.containsKey(appearance)) {
plugin.logger().warn(path, appearance + " is not a valid appearance for block " + id);
return;
}
int internalId = MiscUtils.getAsInt(variantSection.getOrDefault("id", -1));
Key baseBlock = tempTypeMap.get(appearance);
Key internalBlockId = Key.of(CraftEngine.NAMESPACE, baseBlock.value() + "_" + internalId);
int internalBlockRegistryId = MiscUtils.getAsInt(this.internalId2StateId.getOrDefault(internalBlockId, -1));
if (internalBlockRegistryId == -1) {
plugin.logger().warn(path, "Failed to register " + id + " because id " + internalId + " is not a value between 0~" + (MiscUtils.getAsInt(this.registeredRealBlockSlots.getOrDefault(baseBlock, 1))-1) +
". Consider editing additional-real-blocks.yml if the number of real block IDs is insufficient while there are still available appearances");
return;
}
Map<String, Object> anotherSetting = MiscUtils.castToMap(variantSection.get("settings"), true);
variants.put(variantName, new VariantState(appearance, anotherSetting == null ? settings : BlockSettings.ofFullCopy(settings, anotherSetting), internalBlockRegistryId));
}
}
}
// create or get block holder
Holder.Reference<CustomBlock> holder = BuiltInRegistries.BLOCK.get(id).orElseGet(() ->
((WritableRegistry<CustomBlock>) BuiltInRegistries.BLOCK).registerForHolder(new ResourceKey<>(BuiltInRegistries.BLOCK.key().location(), id)));
// create block
Map<String, Object> behaviorSection = MiscUtils.castToMap(section.getOrDefault("behavior", Map.of()), false);
CustomBlock block = new BukkitCustomBlock(id, holder, properties, appearances, variants, settings, behaviorSection, lootTable);
// bind appearance
bindAppearance(block);
this.id2CraftEngineBlocks.put(id, block);
}
private void bindAppearance(CustomBlock block) {
for (ImmutableBlockState state : block.variantProvider().states()) {
ImmutableBlockState previous = this.stateId2ImmutableBlockStates[state.customBlockState().registryId() - BlockStateUtils.vanillaStateSize()];
if (previous != null && !previous.isEmpty()) {
this.plugin.logger().severe("[Fatal] Failed to bind real block state for " + state + ": the state is already occupied by " + previous);
continue;
}
this.stateId2ImmutableBlockStates[state.customBlockState().registryId() - BlockStateUtils.vanillaStateSize()] = state;
this.tempBlockAppearanceConvertor.put(state.customBlockState().registryId(), state.vanillaBlockState().registryId());
this.appearanceToRealState.computeIfAbsent(state.vanillaBlockState().registryId(), k -> new ArrayList<>()).add(state.customBlockState().registryId());
}
}
private Map<String, Property<?>> parseProperties(Path path, Map<String, Object> propertiesSection) {
Map<String, Property<?>> properties = new HashMap<>();
for (Map.Entry<String, Object> entry : propertiesSection.entrySet()) {
if (entry.getValue() instanceof Map<?, ?> params) {
Property<?> property = Properties.fromMap(entry.getKey(), MiscUtils.castToMap(params, false));
properties.put(entry.getKey(), property);
} else {
this.plugin.logger().warn(path, "Invalid property format: " + entry.getKey());
}
}
return properties;
}
@Nullable
private Pair<Key, Integer> parseAppearanceSection(Path path, Map<String, Object> section, Key id) {
String vanillaStateString = (String) section.get("state");
if (PreConditions.isNull(vanillaStateString,
() -> this.plugin.logger().warn(path, "No block state found for: " + id))) return null;
int vanillaStateRegistryId;
try {
vanillaStateRegistryId = parseVanillaStateRegistryId(vanillaStateString);
} catch (BlockStateArrangeException e) {
this.plugin.logger().warn(path, "Failed to load " + id + " - " + e.getMessage(), e);
return null;
}
Key ifAny = this.tempRegistryIdConflictMap.get(vanillaStateRegistryId);
if (ifAny != null && !ifAny.equals(id)) {
this.plugin.logger().warn(path, "Can't use " + BlockStateUtils.idToBlockState(vanillaStateRegistryId) + " as the base block for " + id + " because the state has already been used by " + ifAny);
return null;
}
Object models = section.getOrDefault("models", section.get("model"));
List<JsonObject> variants = new ArrayList<>();
if (models instanceof Map<?, ?> singleModelSection) {
loadVariantModel(variants, MiscUtils.castToMap(singleModelSection, false));
} else if (models instanceof List<?> modelList) {
for (Object model : modelList) {
if (model instanceof Map<?,?> singleModelMap) {
loadVariantModel(variants, MiscUtils.castToMap(singleModelMap, false));
}
}
} else {
this.plugin.logger().warn(path, "No model set for " + id);
return null;
}
if (variants.isEmpty()) return null;
this.tempRegistryIdConflictMap.put(vanillaStateRegistryId, id);
String blockState = BlockStateUtils.idToBlockState(vanillaStateRegistryId).toString();
Key block = Key.of(blockState.substring(blockState.indexOf('{') + 1, blockState.lastIndexOf('}')));
String propertyData = blockState.substring(blockState.indexOf('[') + 1, blockState.lastIndexOf(']'));
Map<String, JsonElement> paths = this.blockStateOverrides.computeIfAbsent(block, k -> new HashMap<>());
if (variants.size() == 1) {
paths.put(propertyData, variants.get(0));
} else {
JsonArray array = new JsonArray();
for (JsonObject object : variants) {
array.add(object);
}
paths.put(propertyData, array);
}
return Pair.of(block, vanillaStateRegistryId);
}
private void loadVariantModel(List<JsonObject> variants, Map<String, Object> singleModelMap) {
JsonObject json = new JsonObject();
String modelPath = (String) singleModelMap.get("path");
json.addProperty("model", modelPath);
if (singleModelMap.containsKey("x")) json.addProperty("x", MiscUtils.getAsInt(singleModelMap.get("x")));
if (singleModelMap.containsKey("y")) json.addProperty("y", MiscUtils.getAsInt(singleModelMap.get("y")));
if (singleModelMap.containsKey("uvlock")) json.addProperty("uvlock", (boolean) singleModelMap.get("uvlock"));
if (singleModelMap.containsKey("weight")) json.addProperty("weight", MiscUtils.getAsInt(singleModelMap.get("weight")));
Map<String, Object> generationMap = MiscUtils.castToMap(singleModelMap.get("generation"), true);
if (generationMap != null) {
prepareModelGeneration(new ModelGeneration(Key.of(modelPath), generationMap));
}
variants.add(json);
}
private int parseVanillaStateRegistryId(String blockState) throws BlockStateArrangeException {
String[] split = blockState.split(":", 3);
PreConditions.runIfTrue(split.length >= 4, () -> {
throw new BlockStateArrangeException("Invalid vanilla block state: " + blockState);
});
int registryId;
// minecraft:xxx:0
// xxx:0
String stateOrId = split[split.length - 1];
boolean isId = !stateOrId.contains("[") && !stateOrId.contains("]");
if (isId) {
if (split.length == 1) {
throw new BlockStateArrangeException("Invalid vanilla block state: " + blockState);
}
Key block = split.length == 2 ? Key.of(split[0]) : Key.of(split[0], split[1]);
try {
int id = split.length == 2 ? Integer.parseInt(split[1]) : Integer.parseInt(split[2]);
PreConditions.runIfTrue(id < 0, () -> {
throw new BlockStateArrangeException("Invalid block state: " + blockState);
});
List<Integer> arranger = this.blockAppearanceArranger.get(block);
if (arranger == null) {
throw new BlockStateArrangeException("No freed block state is available for block " + block);
}
if (id >= arranger.size()) {
throw new BlockStateArrangeException(blockState + " is not a valid block state because " + id + " is outside of the range (0~" + (arranger.size() - 1) + ")");
}
registryId = arranger.get(id);
} catch (NumberFormatException e) {
throw new BlockStateArrangeException("Invalid block state: " + blockState);
}
} else {
try {
BlockData blockData = Bukkit.createBlockData(blockState);
registryId = BlockStateUtils.blockDataToId(blockData);
} catch (IllegalArgumentException e) {
throw new BlockStateArrangeException("Invalid block state: " + blockState);
}
}
return registryId;
}
private void loadMappingsAndAdditionalBlocks() {
this.plugin.logger().info("Loading mappings.yml.");
File mappingFile = new File(plugin.dataFolderFile(), "mappings.yml");
YamlDocument mappings = ConfigManager.instance().loadOrCreateYamlData("mappings.yml");
Map<String, String> blockStateMappings = loadBlockStateMappings(mappings);
this.validateBlockStateMappings(mappingFile, blockStateMappings);
Map<Integer, String> stateMap = new HashMap<>();
Map<Key, Integer> blockTypeCounter = new LinkedHashMap<>();
Map<Integer, Integer> appearanceMapper = new HashMap<>();
Map<Key, List<Integer>> appearanceArranger = new HashMap<>();
for (Map.Entry<String, String> entry : blockStateMappings.entrySet()) {
this.processBlockStateMapping(mappingFile, entry, stateMap, blockTypeCounter, appearanceMapper, appearanceArranger);
}
this.blockAppearanceMapper = ImmutableMap.copyOf(appearanceMapper);
this.blockAppearanceArranger = ImmutableMap.copyOf(appearanceArranger);
this.plugin.logger().info("Freed " + this.blockAppearanceMapper.size() + " block state appearances.");
YamlDocument additionalYaml = ConfigManager.instance().loadOrCreateYamlData("additional-real-blocks.yml");
this.registeredRealBlockSlots = this.buildRegisteredRealBlockSlots(blockTypeCounter, additionalYaml);
}
private void recordVanillaNoteBlocks() {
try {
Object resourceLocation = Reflections.method$ResourceLocation$fromNamespaceAndPath.invoke(null, BlockKeys.NOTE_BLOCK.namespace(), BlockKeys.NOTE_BLOCK.value());
Object block = Reflections.method$Registry$get.invoke(Reflections.instance$BuiltInRegistries$BLOCK, resourceLocation);
Object stateDefinition = Reflections.field$Block$StateDefinition.get(block);
@SuppressWarnings("unchecked")
ImmutableList<Object> states = (ImmutableList<Object>) Reflections.field$StateDefinition$states.get(stateDefinition);
for (Object state : states) {
BlockStateUtils.CLIENT_SIDE_NOTE_BLOCKS.put(state, new Object());
}
} catch (ReflectiveOperationException e) {
plugin.logger().warn("Failed to init vanilla note block", e);
}
}
@Nullable
public Key replaceSoundIfExist(Key id) {
return this.soundMapper.get(id);
}
public boolean isBlockSoundRemoved(Object block) {
return this.affectedSoundBlocks.contains(block);
}
private Map<String, String> loadBlockStateMappings(YamlDocument mappings) {
Map<String, String> blockStateMappings = new LinkedHashMap<>();
for (Map.Entry<String, Object> entry : mappings.getStringRouteMappedValues(false).entrySet()) {
if (entry.getValue() instanceof String afterValue) {
blockStateMappings.put(entry.getKey(), afterValue);
}
}
return blockStateMappings;
}
private void validateBlockStateMappings(File mappingFile, Map<String, String> blockStateMappings) {
Map<String, String> temp = new HashMap<>(blockStateMappings);
for (Map.Entry<String, String> entry : temp.entrySet()) {
String state = entry.getValue();
if (blockStateMappings.containsKey(state)) {
String after = blockStateMappings.remove(state);
plugin.logger().warn(mappingFile, "'" + state + ": " + after + "' is invalid because '" + state + "' has already been used as a base block.");
}
}
}
private void processBlockStateMapping(File mappingFile,
Map.Entry<String, String> entry,
Map<Integer, String> stateMap,
Map<Key, Integer> counter,
Map<Integer, Integer> mapper,
Map<Key, List<Integer>> arranger) {
BlockData before = createBlockData(mappingFile, entry.getKey());
BlockData after = createBlockData(mappingFile, entry.getValue());
if (before == null || after == null) return;
int beforeId = BlockStateUtils.blockDataToId(before);
int afterId = BlockStateUtils.blockDataToId(after);
Integer previous = mapper.put(beforeId, afterId);
if (previous == null) {
Key key = KeyUtils.namespacedKey2Key(before.getMaterial().getKey());
counter.compute(key, (k, count) -> count == null ? 1 : count + 1);
stateMap.put(beforeId, entry.getKey());
stateMap.put(afterId, entry.getValue());
arranger.computeIfAbsent(key, (k) -> new ArrayList<>()).add(beforeId);
} else {
String previousState = stateMap.get(previous);
plugin.logger().warn(mappingFile, "Duplicate entry: '" + previousState + "' equals '" + entry.getKey() + "'");
}
}
private BlockData createBlockData(File mappingFile, String blockState) {
try {
return Bukkit.createBlockData(blockState);
} catch (IllegalArgumentException e) {
plugin.logger().warn(mappingFile, "'" + blockState + "' is not a valid block state.");
return null;
}
}
private LinkedHashMap<Key, Integer> buildRegisteredRealBlockSlots(Map<Key, Integer> counter, YamlDocument additionalYaml) {
LinkedHashMap<Key, Integer> map = new LinkedHashMap<>();
for (Map.Entry<Key, Integer> entry : counter.entrySet()) {
String id = entry.getKey().toString();
int additionalStates = additionalYaml.getInt(id, 0);
int internalIds = entry.getValue() + additionalStates;
plugin.logger().info("Loaded " + id + " with " + entry.getValue() + " appearances and " + internalIds + " real block states");
map.put(entry.getKey(), internalIds);
}
return map;
}
private void unfreezeRegistry() throws IllegalAccessException {
Reflections.field$MappedRegistry$frozen.set(Reflections.instance$BuiltInRegistries$BLOCK, false);
Reflections.field$MappedRegistry$unregisteredIntrusiveHolders.set(Reflections.instance$BuiltInRegistries$BLOCK, new IdentityHashMap<>());
}
private void freezeRegistry() throws IllegalAccessException {
Reflections.field$MappedRegistry$frozen.set(Reflections.instance$BuiltInRegistries$BLOCK, true);
}
private int registerBlockVariants(Map.Entry<Key, Integer> blockWithCount,
int counter,
ImmutableMap.Builder<Key, Integer> builder1,
ImmutableMap.Builder<Integer, Object> builder2,
ImmutableMap.Builder<Key, List<Integer>> builder3,
Set<Object> affectSoundTypes,
List<Key> order) throws Exception {
Key clientSideBlockType = blockWithCount.getKey();
boolean isNoteBlock = clientSideBlockType.equals(BlockKeys.NOTE_BLOCK);
Object clientSideBlock = getBlockFromRegistry(createResourceLocation(clientSideBlockType));
int amount = blockWithCount.getValue();
List<Integer> stateIds = new ArrayList<>();
for (int i = 0; i < amount; i++) {
Key realBlockKey = createRealBlockKey(clientSideBlockType, i);
Object blockProperties = createBlockProperties(realBlockKey);
Object newRealBlock;
Object newBlockState;
Object blockHolder;
Object resourceLocation = createResourceLocation(realBlockKey);
if (plugin.hasMod()) {
newRealBlock = Reflections.method$Registry$get.invoke(Reflections.instance$BuiltInRegistries$BLOCK, resourceLocation);
newBlockState = getOnlyBlockState(newRealBlock);
@SuppressWarnings("unchecked")
Optional<Object> optionalHolder = (Optional<Object>) Reflections.method$Registry$getHolder0.invoke(Reflections.instance$BuiltInRegistries$BLOCK, resourceLocation);
blockHolder = optionalHolder.get();
} else {
try {
newRealBlock = BukkitInjector.generateBlock(clientSideBlockType, clientSideBlock, blockProperties);
} catch (Throwable throwable) {
plugin.logger().warn("Failed to generate dynamic block class", throwable);
continue;
}
blockHolder = Reflections.method$Registry$registerForHolder.invoke(null, Reflections.instance$BuiltInRegistries$BLOCK, resourceLocation, newRealBlock);
Reflections.method$Holder$Reference$bindValue.invoke(blockHolder, newRealBlock);
Reflections.field$Holder$Reference$tags.set(blockHolder, Set.of());
newBlockState = getOnlyBlockState(newRealBlock);
Reflections.method$IdMapper$add.invoke(Reflections.instance$BLOCK_STATE_REGISTRY, newBlockState);
}
if (isNoteBlock) {
BlockStateUtils.CLIENT_SIDE_NOTE_BLOCKS.put(newBlockState, new Object());
}
int stateId = BlockStateUtils.vanillaStateSize() + counter;
builder1.put(realBlockKey, stateId);
builder2.put(stateId, blockHolder);
stateIds.add(stateId);
deceiveBukkit(newRealBlock, clientSideBlockType);
order.add(realBlockKey);
counter++;
}
builder3.put(clientSideBlockType, stateIds);
Object soundType = Reflections.field$BlockBehaviour$soundType.get(clientSideBlock);
affectSoundTypes.add(soundType);
return counter;
}
private Object createResourceLocation(Key key) throws Exception {
return Reflections.method$ResourceLocation$fromNamespaceAndPath.invoke(null, key.namespace(), key.value());
}
private Object getBlockFromRegistry(Object resourceLocation) throws Exception {
return Reflections.method$Registry$get.invoke(Reflections.instance$BuiltInRegistries$BLOCK, resourceLocation);
}
private Key createRealBlockKey(Key replacedBlock, int index) {
return Key.of(CraftEngine.NAMESPACE, replacedBlock.value() + "_" + index);
}
private Object createBlockProperties(Key realBlockKey) throws Exception {
Object blockProperties = Reflections.method$BlockBehaviour$Properties$of.invoke(null);
Object realBlockResourceLocation = createResourceLocation(realBlockKey);
Object realBlockResourceKey = Reflections.method$ResourceKey$create.invoke(null, Reflections.instance$Registries$BLOCK, realBlockResourceLocation);
if (Reflections.field$BlockBehaviour$Properties$id != null) {
Reflections.field$BlockBehaviour$Properties$id.set(blockProperties, realBlockResourceKey);
}
return blockProperties;
}
private Object getOnlyBlockState(Object newBlock) throws IllegalAccessException {
Object stateDefinition = Reflections.field$Block$StateDefinition.get(newBlock);
@SuppressWarnings("unchecked")
ImmutableList<Object> states = (ImmutableList<Object>) Reflections.field$StateDefinition$states.get(stateDefinition);
return states.get(0);
}
private void deceiveBukkit(Object newBlock, Key replacedBlock) throws IllegalAccessException {
@SuppressWarnings("unchecked")
Map<Object, Material> magicMap = (Map<Object, Material>) Reflections.field$CraftMagicNumbers$BLOCK_MATERIAL.get(null);
magicMap.put(newBlock, org.bukkit.Registry.MATERIAL.get(Objects.requireNonNull(NamespacedKey.fromString(replacedBlock.toString()))));
}
}

View File

@@ -0,0 +1,17 @@
package net.momirealms.craftengine.bukkit.block;
import net.momirealms.craftengine.bukkit.util.Reflections;
import net.momirealms.craftengine.shared.block.BlockShape;
public class BukkitBlockShape implements BlockShape {
private final Object rawBlockState;
public BukkitBlockShape(Object rawBlockState) {
this.rawBlockState = rawBlockState;
}
@Override
public Object getShape(Object thisObj, Object[] args) throws Exception {
return Reflections.method$BlockBehaviour$getShape.invoke(Reflections.field$StateHolder$owner.get(this.rawBlockState), this.rawBlockState, args[1], args[2], args[3]);
}
}

View File

@@ -0,0 +1,124 @@
package net.momirealms.craftengine.bukkit.block;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.bukkit.util.BlockStateUtils;
import net.momirealms.craftengine.bukkit.util.Reflections;
import net.momirealms.craftengine.bukkit.util.SoundUtils;
import net.momirealms.craftengine.core.block.*;
import net.momirealms.craftengine.core.block.properties.Property;
import net.momirealms.craftengine.core.loot.LootTable;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.registry.Holder;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.Tristate;
import net.momirealms.craftengine.shared.ObjectHolder;
import net.momirealms.craftengine.shared.block.BlockBehavior;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Field;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class BukkitCustomBlock extends CustomBlock {
public BukkitCustomBlock(
Key id,
Holder.Reference<CustomBlock> holder,
Map<String, Property<?>> properties,
Map<String, Integer> appearances,
Map<String, VariantState> variantMapper,
BlockSettings settings,
Map<String, Object> behaviorSettings,
@Nullable LootTable<?> lootTable
) {
super(id, holder, properties, appearances, variantMapper, settings, behaviorSettings, lootTable);
}
@SuppressWarnings("unchecked")
@Nullable
@Override
public LootTable<ItemStack> lootTable() {
return (LootTable<ItemStack>) super.lootTable();
}
@Override
protected void applyPlatformSettings() {
try {
for (ImmutableBlockState state : variantProvider().states()) {
if (state.vanillaBlockState() == null) {
CraftEngine.instance().logger().warn("Could not find vanilla block state for " + state + ". This might cause errors!");
continue;
} else if (state.customBlockState() == null) {
CraftEngine.instance().logger().warn("Could not find custom block state for " + state + ". This might cause errors!");
continue;
}
Object mcBlockState = state.customBlockState().handle();
BlockSettings settings = state.settings();
// set block state properties
BlockStateUtils.setInstrument(mcBlockState, settings.instrument());
BlockStateUtils.setMapColor(mcBlockState, settings.mapColor());
BlockStateUtils.setLightEmission(mcBlockState, settings.luminance());
BlockStateUtils.setBurnable(mcBlockState, settings.burnable());
BlockStateUtils.setHardness(mcBlockState, settings.hardness());
BlockStateUtils.setPushReaction(mcBlockState, settings.pushReaction());
BlockStateUtils.setReplaceable(mcBlockState, settings.replaceable());
BlockStateUtils.setCanOcclude(mcBlockState, settings.canOcclude());
if (settings.isRedstoneConductor() == Tristate.TRUE) {
BlockStateUtils.setIsRedstoneConductor(mcBlockState, StatePredicate.alwaysTrue());
} else if (settings.isRedstoneConductor() == Tristate.FALSE) {
BlockStateUtils.setIsRedstoneConductor(mcBlockState, StatePredicate.alwaysFalse());
}
if (settings.isSuffocating() == Tristate.TRUE) {
BlockStateUtils.setIsSuffocating(mcBlockState, StatePredicate.alwaysTrue());
} else if (settings.isSuffocating() == Tristate.FALSE) {
BlockStateUtils.setIsSuffocating(mcBlockState, StatePredicate.alwaysFalse());
}
if (settings.isViewBlocking() == Tristate.TRUE) {
BlockStateUtils.setIsViewBlocking(mcBlockState, StatePredicate.alwaysTrue());
} else if (settings.isViewBlocking() == Tristate.FALSE) {
BlockStateUtils.setIsViewBlocking(mcBlockState, StatePredicate.alwaysFalse());
} else {
if (settings.isSuffocating() == Tristate.TRUE) {
BlockStateUtils.setIsViewBlocking(mcBlockState, StatePredicate.alwaysTrue());
} else if (settings.isSuffocating() == Tristate.FALSE) {
BlockStateUtils.setIsViewBlocking(mcBlockState, StatePredicate.alwaysFalse());
}
}
// set parent block properties
Object mcBlock = BlockStateUtils.getBlockOwner(mcBlockState);
// bind shape
Field shapeField = mcBlock.getClass().getField("shapeHolder");
@SuppressWarnings("unchecked")
ObjectHolder<BukkitBlockShape> shapeHolder = (ObjectHolder<BukkitBlockShape>) shapeField.get(mcBlock);
shapeHolder.bindValue(new BukkitBlockShape(state.vanillaBlockState().handle()));
// bind behavior
Field behaviorField = mcBlock.getClass().getField("behaviorHolder");
@SuppressWarnings("unchecked")
ObjectHolder<BlockBehavior> behaviorHolder = (ObjectHolder<BlockBehavior>) behaviorField.get(mcBlock);
behaviorHolder.bindValue(super.behavior);
// set block side properties
Reflections.field$BlockBehaviour$explosionResistance.set(mcBlock, settings.resistance());
Reflections.field$BlockBehaviour$soundType.set(mcBlock, SoundUtils.toSoundType(settings.sounds()));
// init cache
Reflections.method$BlockStateBase$initCache.invoke(mcBlockState);
// set random tick later
BlockStateUtils.setIsRandomlyTicking(mcBlockState, settings.isRandomlyTicking());
// bind tags
Object holder = BukkitCraftEngine.instance().blockManager().getMinecraftBlockHolder(state.customBlockState().registryId());
Set<Object> tags = new HashSet<>();
for (Key tag : settings.tags()) {
tags.add(Reflections.method$TagKey$create.invoke(null, Reflections.instance$Registries$BLOCK, Reflections.method$ResourceLocation$fromNamespaceAndPath.invoke(null, tag.namespace(), tag.value())));
}
Reflections.field$Holder$Reference$tags.set(holder, tags);
// set burning properties
if (settings.burnable()) {
Reflections.method$FireBlock$setFlammable.invoke(Reflections.instance$Blocks$FIRE, mcBlock, settings.burnChance(), settings.fireSpreadChance());
}
}
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to init block settings", e);
}
}
}

View File

@@ -0,0 +1,51 @@
package net.momirealms.craftengine.bukkit.block;
import net.momirealms.craftengine.bukkit.util.BlockStateUtils;
import net.momirealms.craftengine.bukkit.util.Reflections;
import net.momirealms.craftengine.bukkit.world.BukkitWorld;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.loot.parameter.LootParameters;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.util.context.ContextHolder;
import net.momirealms.craftengine.core.world.Vec3d;
import org.bukkit.entity.FallingBlock;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
public class FallingBlockRemoveListener implements Listener {
/*
* This is not an event that would be removed
* Paper mistakenly marked it as deprecated in 1.20.4
*/
@SuppressWarnings("removal")
@EventHandler
public void onFallingBlockBreak(org.bukkit.event.entity.EntityRemoveEvent event) {
if (event.getCause() == org.bukkit.event.entity.EntityRemoveEvent.Cause.DROP && event.getEntity() instanceof FallingBlock fallingBlock) {
try {
Object fallingBlockEntity = Reflections.field$CraftEntity$entity.get(fallingBlock);
boolean cancelDrop = (boolean) Reflections.field$FallingBlockEntity$cancelDrop.get(fallingBlockEntity);
if (cancelDrop) return;
Object blockState = Reflections.field$FallingBlockEntity$blockState.get(fallingBlockEntity);
int stateId = BlockStateUtils.blockStateToId(blockState);
ImmutableBlockState immutableBlockState = BukkitBlockManager.instance().getImmutableBlockState(stateId);
if (immutableBlockState == null || immutableBlockState.isEmpty()) return;
ContextHolder.Builder builder = ContextHolder.builder();
builder.withParameter(LootParameters.FALLING_BLOCK, true);
double x = Reflections.field$Entity$xo.getDouble(fallingBlockEntity);
double y = Reflections.field$Entity$yo.getDouble(fallingBlockEntity);
double z = Reflections.field$Entity$zo.getDouble(fallingBlockEntity);
Vec3d vec3d = new Vec3d(x, y, z);
net.momirealms.craftengine.core.world.World world = new BukkitWorld(fallingBlock.getWorld());
builder.withParameter(LootParameters.LOCATION, vec3d);
builder.withParameter(LootParameters.WORLD, world);
for (Item<Object> item : immutableBlockState.getDrops(builder, world)) {
world.dropItemNaturally(vec3d, item);
}
} catch (ReflectiveOperationException e) {
CraftEngine.instance().logger().warn("Failed to handle EntityRemoveEvent", e);
}
}
}
}

View File

@@ -0,0 +1,11 @@
package net.momirealms.craftengine.bukkit.block;
import com.sk89q.worldedit.world.block.BlockType;
import net.momirealms.craftengine.core.util.Key;
public class WorldEditHook {
public static void register(Key id) {
BlockType.REGISTRY.register(id.toString(), new BlockType(id.toString(), blockState -> blockState));
}
}

View File

@@ -0,0 +1,23 @@
package net.momirealms.craftengine.bukkit.block.behavior;
import net.momirealms.craftengine.core.block.behavior.BlockBehaviors;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.shared.block.EmptyBlockBehavior;
public class BukkitBlockBehaviors extends BlockBehaviors {
public static final Key EMPTY = Key.from("craftengine:empty");
public static final Key BUSH_BLOCK = Key.from("craftengine:bush_block");
public static final Key FALLING_BLOCK = Key.from("craftengine:falling_block");
public static final Key LEAVES_BLOCK = Key.from("craftengine:leaves_block");
public static final Key STRIPPABLE_BLOCK = Key.from("craftengine:strippable_block");
public static final Key SAPLING_BLOCK = Key.from("craftengine:sapling_block");
public static void init() {
register(EMPTY, (block, args) -> EmptyBlockBehavior.INSTANCE);
register(FALLING_BLOCK, FallingBlockBehavior.FACTORY);
register(BUSH_BLOCK, BushBlockBehavior.FACTORY);
register(LEAVES_BLOCK, LeavesBlockBehavior.FACTORY);
register(STRIPPABLE_BLOCK, StrippableBlockBehavior.FACTORY);
register(SAPLING_BLOCK, SaplingBlockBehavior.FACTORY);
}
}

View File

@@ -0,0 +1,111 @@
package net.momirealms.craftengine.bukkit.block.behavior;
import net.momirealms.craftengine.bukkit.block.BukkitBlockManager;
import net.momirealms.craftengine.bukkit.util.BlockStateUtils;
import net.momirealms.craftengine.bukkit.util.BlockTags;
import net.momirealms.craftengine.bukkit.util.LocationUtils;
import net.momirealms.craftengine.bukkit.util.Reflections;
import net.momirealms.craftengine.bukkit.world.BukkitWorld;
import net.momirealms.craftengine.core.block.CustomBlock;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.block.behavior.BlockBehaviorFactory;
import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.loot.parameter.LootParameters;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.MiscUtils;
import net.momirealms.craftengine.core.util.VersionHelper;
import net.momirealms.craftengine.core.util.context.ContextHolder;
import net.momirealms.craftengine.core.world.BlockPos;
import net.momirealms.craftengine.core.world.Vec3d;
import net.momirealms.craftengine.shared.block.BlockBehavior;
import org.bukkit.World;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
public class BushBlockBehavior extends BlockBehavior {
public static final Factory FACTORY = new Factory();
protected static final Object DIRT_TAG = BlockTags.getOrCreate(Key.of("minecraft", "dirt"));
protected static final Object FARMLAND = BlockTags.getOrCreate(Key.of("minecraft", "farmland"));
public static final BushBlockBehavior INSTANCE = new BushBlockBehavior(List.of(DIRT_TAG, FARMLAND));
protected final List<Object> tagsCanSurviveOn;
public BushBlockBehavior(List<Object> tagsCanSurviveOn) {
this.tagsCanSurviveOn = tagsCanSurviveOn;
}
@Override
public void onPlace(Object thisBlock, Object[] args, Callable<Object> superMethod) throws Exception {
Object world = args[1];
Object blockPos = args[2];
Reflections.method$LevelAccessor$scheduleTick.invoke(world, blockPos, thisBlock, 2);
}
@Override
public Object updateShape(Object thisBlock, Object[] args, Callable<Object> superMethod) throws Exception {
Object level;
Object blockPos;
Object state = args[0];
if (VersionHelper.isVersionNewerThan1_21_2()) {
level = args[1];
blockPos = args[3];
} else {
level = args[3];
blockPos = args[4];
}
if (!canSurvive(thisBlock, state, level, blockPos)) {
ImmutableBlockState previousState = BukkitBlockManager.instance().getImmutableBlockState(BlockStateUtils.blockStateToId(state));
if (previousState != null && !previousState.isEmpty()) {
ContextHolder.Builder builder = ContextHolder.builder();
BlockPos pos = LocationUtils.fromBlockPos(blockPos);
Vec3d vec3d = Vec3d.atCenterOf(pos);
net.momirealms.craftengine.core.world.World world = new BukkitWorld((World) Reflections.method$Level$getCraftWorld.invoke(level));
builder.withParameter(LootParameters.LOCATION, vec3d);
builder.withParameter(LootParameters.WORLD, world);
for (Item<Object> item : previousState.getDrops(builder, world)) {
world.dropItemNaturally(vec3d, item);
}
}
return Reflections.method$Block$defaultBlockState.invoke(Reflections.instance$Blocks$AIR);
}
return super.updateShape(thisBlock, args, superMethod);
}
@Override
public boolean canSurvive(Object thisBlock, Object[] args, Callable<Object> superMethod) throws Exception {
Object state = args[0];
Object world = args[1];
Object pos = args[2];
return canSurvive(thisBlock, state, world, pos);
}
public static class Factory implements BlockBehaviorFactory {
@Override
public BlockBehavior create(CustomBlock block, Map<String, Object> arguments) {
if (arguments.containsKey("tags")) {
return new BushBlockBehavior(MiscUtils.getAsStringList(arguments.get("tags")).stream().map(it -> BlockTags.getOrCreate(Key.of(it))).toList());
} else {
return INSTANCE;
}
}
}
private boolean canSurvive(Object thisBlock, Object state, Object world, Object blockPos) throws ReflectiveOperationException {
int y = Reflections.field$Vec3i$y.getInt(blockPos);
int x = Reflections.field$Vec3i$x.getInt(blockPos);
int z = Reflections.field$Vec3i$z.getInt(blockPos);
Object belowPos = Reflections.constructor$BlockPos.newInstance(x, y - 1, z);
Object belowState = Reflections.method$BlockGetter$getBlockState.invoke(world, belowPos);
return mayPlaceOn(belowState, world, belowPos);
}
private boolean mayPlaceOn(Object belowState, Object world, Object blockPos) throws ReflectiveOperationException {
for (Object tag : this.tagsCanSurviveOn) {
if ((boolean) Reflections.method$BlockStateBase$hasTag.invoke(belowState, tag)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,113 @@
package net.momirealms.craftengine.bukkit.block.behavior;
import net.momirealms.craftengine.bukkit.block.BukkitBlockManager;
import net.momirealms.craftengine.bukkit.util.BlockStateUtils;
import net.momirealms.craftengine.bukkit.util.Reflections;
import net.momirealms.craftengine.bukkit.world.BukkitWorld;
import net.momirealms.craftengine.core.block.CustomBlock;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.block.behavior.BlockBehaviorFactory;
import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.loot.parameter.LootParameters;
import net.momirealms.craftengine.core.util.MiscUtils;
import net.momirealms.craftengine.core.util.VersionHelper;
import net.momirealms.craftengine.core.util.context.ContextHolder;
import net.momirealms.craftengine.core.world.Vec3d;
import net.momirealms.craftengine.shared.block.BlockBehavior;
import org.bukkit.World;
import java.util.Map;
import java.util.concurrent.Callable;
public class FallingBlockBehavior extends BlockBehavior {
public static final Factory FACTORY = new Factory();
private final float hurtAmount;
private final int maxHurt;
public FallingBlockBehavior(float hurtAmount, int maxHurt) {
this.hurtAmount = hurtAmount;
this.maxHurt = maxHurt;
}
@Override
public void onPlace(Object thisBlock, Object[] args, Callable<Object> superMethod) throws Exception {
Object world = args[1];
Object blockPos = args[2];
Reflections.method$LevelAccessor$scheduleTick.invoke(world, blockPos, thisBlock, 2);
}
@Override
public Object updateShape(Object thisBlock, Object[] args, Callable<Object> superMethod) throws Exception {
Object world;
Object blockPos;
if (VersionHelper.isVersionNewerThan1_21_2()) {
world = args[1];
blockPos = args[3];
} else {
world = args[3];
blockPos = args[4];
}
Reflections.method$LevelAccessor$scheduleTick.invoke(world, blockPos, thisBlock, 2);
return super.updateShape(thisBlock, args, superMethod);
}
@Override
public void tick(Object thisBlock, Object[] args, Callable<Object> superMethod) throws Exception {
Object blockPos = args[2];
int y = Reflections.field$Vec3i$y.getInt(blockPos);
Object world = args[1];
Object dimension = Reflections.method$$LevelReader$dimensionType.invoke(world);
int minY = Reflections.field$DimensionType$minY.getInt(dimension);
if (y < minY) {
return;
}
int x = Reflections.field$Vec3i$x.getInt(blockPos);
int z = Reflections.field$Vec3i$z.getInt(blockPos);
Object belowPos = Reflections.constructor$BlockPos.newInstance(x, y - 1, z);
Object belowState = Reflections.method$BlockGetter$getBlockState.invoke(world, belowPos);
boolean isFree = (boolean) Reflections.method$FallingBlock$isFree.invoke(null, belowState);
if (!isFree) {
return;
}
Object blockState = args[0];
Object fallingBlockEntity = Reflections.method$FallingBlockEntity$fall.invoke(null, world, blockPos, blockState);
if (this.hurtAmount > 0 && this.maxHurt > 0) {
Reflections.method$FallingBlockEntity$setHurtsEntities.invoke(fallingBlockEntity, this.hurtAmount, this.maxHurt);
}
}
@Override
public void onBrokenAfterFall(Object thisBlock, Object[] args, Callable<Object> superMethod) throws Exception {
// Use EntityRemoveEvent for 1.20.3+
if (VersionHelper.isVersionNewerThan1_20_3()) return;
Object level = args[0];
Object fallingBlockEntity = args[2];
boolean cancelDrop = (boolean) Reflections.field$FallingBlockEntity$cancelDrop.get(fallingBlockEntity);
if (cancelDrop) return;
Object blockState = Reflections.field$FallingBlockEntity$blockState.get(fallingBlockEntity);
int stateId = BlockStateUtils.blockStateToId(blockState);
ImmutableBlockState immutableBlockState = BukkitBlockManager.instance().getImmutableBlockState(stateId);
if (immutableBlockState == null || immutableBlockState.isEmpty()) return;
ContextHolder.Builder builder = ContextHolder.builder();
builder.withParameter(LootParameters.FALLING_BLOCK, true);
double x = Reflections.field$Entity$xo.getDouble(fallingBlockEntity);
double y = Reflections.field$Entity$yo.getDouble(fallingBlockEntity);
double z = Reflections.field$Entity$zo.getDouble(fallingBlockEntity);
Vec3d vec3d = new Vec3d(x, y, z);
net.momirealms.craftengine.core.world.World world = new BukkitWorld((World) Reflections.method$Level$getCraftWorld.invoke(level));
builder.withParameter(LootParameters.LOCATION, vec3d);
builder.withParameter(LootParameters.WORLD, world);
for (Item<Object> item : immutableBlockState.getDrops(builder, world)) {
world.dropItemNaturally(vec3d, item);
}
}
public static class Factory implements BlockBehaviorFactory {
@Override
public BlockBehavior create(CustomBlock block, Map<String, Object> arguments) {
float hurtAmount = MiscUtils.getAsFloat(arguments.getOrDefault("hurt-amount", -1f));
int hurtMax = MiscUtils.getAsInt(arguments.getOrDefault("max-hurt", -1));
return new FallingBlockBehavior(hurtAmount, hurtMax);
}
}
}

View File

@@ -0,0 +1,195 @@
package net.momirealms.craftengine.bukkit.block.behavior;
import net.momirealms.craftengine.bukkit.block.BukkitBlockManager;
import net.momirealms.craftengine.bukkit.util.BlockStateUtils;
import net.momirealms.craftengine.bukkit.util.BlockTags;
import net.momirealms.craftengine.bukkit.util.LocationUtils;
import net.momirealms.craftengine.bukkit.util.Reflections;
import net.momirealms.craftengine.bukkit.world.BukkitWorld;
import net.momirealms.craftengine.core.block.CustomBlock;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.block.UpdateOption;
import net.momirealms.craftengine.core.block.behavior.BlockBehaviorFactory;
import net.momirealms.craftengine.core.block.properties.Property;
import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.loot.parameter.LootParameters;
import net.momirealms.craftengine.core.util.Direction;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.VersionHelper;
import net.momirealms.craftengine.core.util.context.ContextHolder;
import net.momirealms.craftengine.core.world.BlockPos;
import net.momirealms.craftengine.core.world.Vec3d;
import net.momirealms.craftengine.shared.block.BlockBehavior;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.event.block.LeavesDecayEvent;
import org.jetbrains.annotations.Nullable;
import java.util.Map;
import java.util.concurrent.Callable;
public class LeavesBlockBehavior extends BlockBehavior {
public static final Factory FACTORY = new Factory();
private static final Object LOG_TAG = BlockTags.getOrCreate(Key.of("minecraft", "logs"));
private final int maxDistance;
private final Property<Integer> distanceProperty;
private final Property<Boolean> persistentProperty;
@Nullable
private final Property<Boolean> waterloggedProperty;
public LeavesBlockBehavior(int maxDistance, Property<Integer> distanceProperty, Property<Boolean> persistentProperty, @Nullable Property<Boolean> waterloggedProperty) {
this.maxDistance = maxDistance;
this.distanceProperty = distanceProperty;
this.persistentProperty = persistentProperty;
this.waterloggedProperty = waterloggedProperty;
}
public int getDistance(ImmutableBlockState state) {
return state.get(this.distanceProperty);
}
public boolean isPersistent(ImmutableBlockState state) {
return state.get(this.persistentProperty);
}
public boolean isWaterLogged(ImmutableBlockState state) {
if (this.waterloggedProperty == null) return false;
return state.get(this.waterloggedProperty);
}
@Override
public Object updateShape(Object thisBlock, Object[] args, Callable<Object> superMethod) throws Exception {
Object world;
Object blockPos;
Object neighborState;
Object blockState = args[0];
if (VersionHelper.isVersionNewerThan1_21_2()) {
world = args[1];
neighborState = args[6];
blockPos = args[3];
} else {
world = args[3];
blockPos = args[4];
neighborState = args[2];
}
ImmutableBlockState thisState = BukkitBlockManager.instance().getImmutableBlockState(BlockStateUtils.blockStateToId(blockState));
if (thisState != null && thisState.behavior() instanceof LeavesBlockBehavior behavior) {
int distance = behavior.getDistanceAt(neighborState) + 1;
if (distance != 1 || behavior.getDistance(thisState) != distance) {
Reflections.method$LevelAccessor$scheduleTick.invoke(world, blockPos, thisBlock, 1);
}
}
return blockState;
}
@Override
public void tick(Object thisBlock, Object[] args, Callable<Object> superMethod) throws Exception {
Object blockState = args[0];
Object level = args[1];
Object blockPos = args[2];
ImmutableBlockState currentState = BukkitBlockManager.instance().getImmutableBlockState(BlockStateUtils.blockStateToId(blockState));
if (currentState != null && !currentState.isEmpty() && currentState.behavior() instanceof LeavesBlockBehavior behavior) {
ImmutableBlockState newState = behavior.updateDistance(currentState, level, blockPos);
if (newState != currentState) {
if (blockState == newState.customBlockState().handle()) {
Reflections.method$BlockStateBase$updateNeighbourShapes.invoke(blockState, level, blockPos, UpdateOption.UPDATE_ALL.flags(), 512);
} else {
Reflections.method$Level$setBlock.invoke(level, blockPos, newState.customBlockState().handle(), UpdateOption.UPDATE_ALL.flags());
}
}
}
}
@Override
public void randomTick(Object thisBlock, Object[] args, Callable<Object> superMethod) throws Exception {
Object level = args[1];
Object blockPos = args[2];
ImmutableBlockState immutableBlockState = BukkitBlockManager.instance().getImmutableBlockState(BlockStateUtils.blockStateToId(args[0]));
if (immutableBlockState != null && immutableBlockState.behavior() instanceof LeavesBlockBehavior behavior && behavior.isDecaying(immutableBlockState)) {
World bukkitWorld = (World) Reflections.method$Level$getCraftWorld.invoke(level);
BlockPos pos = LocationUtils.fromBlockPos(blockPos);
// call bukkit event
LeavesDecayEvent event = new LeavesDecayEvent(bukkitWorld.getBlockAt(pos.x(), pos.y(), pos.z()));
Bukkit.getPluginManager().callEvent(event);
if (event.isCancelled()) {
return;
}
Reflections.method$Level$removeBlock.invoke(level, blockPos, false);
if (isWaterLogged(immutableBlockState)) {
bukkitWorld.setBlockData(pos.x(), pos.y(), pos.z(), Material.WATER.createBlockData());
}
Vec3d vec3d = Vec3d.atCenterOf(pos);
net.momirealms.craftengine.core.world.World world = new BukkitWorld(bukkitWorld);
ContextHolder.Builder builder = ContextHolder.builder()
.withParameter(LootParameters.LOCATION, vec3d)
.withParameter(LootParameters.WORLD, world);
for (Item<Object> item : immutableBlockState.getDrops(builder, world)) {
world.dropItemNaturally(vec3d, item);
}
}
}
private boolean isDecaying(ImmutableBlockState blockState) {
return !isPersistent(blockState) && getDistance(blockState) == this.maxDistance;
}
private ImmutableBlockState updateDistance(ImmutableBlockState state, Object world, Object blockPos) throws ReflectiveOperationException {
int i = this.maxDistance;
Object mutablePos = Reflections.constructor$MutableBlockPos.newInstance();
int j = Direction.values().length;
for (int k = 0; k < j; ++k) {
Object direction = Reflections.instance$Directions[k];
Reflections.method$MutableBlockPos$setWithOffset.invoke(mutablePos, blockPos, direction);
Object blockState = Reflections.method$BlockGetter$getBlockState.invoke(world, mutablePos);
i = Math.min(i, getDistanceAt(blockState) + 1);
if (i == 1) {
break;
}
}
return state.with(this.distanceProperty, i);
}
private int getDistanceAt(Object blockState) throws ReflectiveOperationException {
boolean isLog = (boolean) Reflections.method$BlockStateBase$hasTag.invoke(blockState, LOG_TAG);
if (isLog) return 0;
int id = BlockStateUtils.blockStateToId(blockState);
if (BlockStateUtils.isVanillaBlock(id)) {
Object distanceProperty = Reflections.field$LeavesBlock$DISTANCE.get(null);
boolean hasDistanceProperty = (boolean) Reflections.method$StateHolder$hasProperty.invoke(blockState, distanceProperty);
if (!hasDistanceProperty) return this.maxDistance;
return (int) Reflections.method$StateHolder$getValue.invoke(blockState, distanceProperty);
} else {
ImmutableBlockState anotherBlockState = BukkitBlockManager.instance().getImmutableBlockStateUnsafe(id);
if (!(anotherBlockState.behavior() instanceof LeavesBlockBehavior otherBehavior)) return this.maxDistance;
return otherBehavior.getDistance(anotherBlockState);
}
}
@SuppressWarnings("unchecked")
public static class Factory implements BlockBehaviorFactory {
@Override
public BlockBehavior create(CustomBlock block, Map<String, Object> arguments) {
Property<Boolean> persistent = (Property<Boolean>) block.getProperty("persistent");
if (persistent == null) {
throw new NullPointerException("persistent property not set for block " + block.id());
}
Property<Integer> distance = (Property<Integer>) block.getProperty("distance");
if (distance == null) {
throw new NullPointerException("distance not set for block " + block.id());
}
Property<Boolean> waterlogged = (Property<Boolean>) block.getProperty("waterlogged");
int actual = distance.possibleValues().get(distance.possibleValues().size() - 1);
return new LeavesBlockBehavior(actual, distance, persistent, waterlogged);
}
}
@Override
public Object getFluidState(Object thisBlock, Object[] args, Callable<Object> superMethod) throws Exception {
Object blockState = args[0];
ImmutableBlockState state = BukkitBlockManager.instance().getImmutableBlockState(BlockStateUtils.blockStateToId(blockState));
if (state == null || state.isEmpty() || waterloggedProperty == null) return super.getFluidState(thisBlock, args, superMethod);
boolean waterlogged = state.get(waterloggedProperty);
return waterlogged ? Reflections.method$FlowingFluid$getSource.invoke(Reflections.instance$Fluids$WATER, false) : super.getFluidState(thisBlock, args, superMethod);
}
}

View File

@@ -0,0 +1,131 @@
package net.momirealms.craftengine.bukkit.block.behavior;
import net.momirealms.craftengine.bukkit.api.CraftEngineBlocks;
import net.momirealms.craftengine.bukkit.block.BukkitBlockManager;
import net.momirealms.craftengine.bukkit.util.*;
import net.momirealms.craftengine.core.block.CustomBlock;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.block.UpdateOption;
import net.momirealms.craftengine.core.block.behavior.BlockBehaviorFactory;
import net.momirealms.craftengine.core.block.properties.Property;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.MiscUtils;
import net.momirealms.craftengine.core.util.RandomUtils;
import net.momirealms.craftengine.shared.block.BlockBehavior;
import org.bukkit.Location;
import org.bukkit.World;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Callable;
public class SaplingBlockBehavior extends BushBlockBehavior {
public static final Factory FACTORY = new Factory();
private final Key feature;
private final Property<Integer> stageProperty;
private final double boneMealSuccessChance;
public SaplingBlockBehavior(Key feature, Property<Integer> stageProperty, List<Object> tagsCanSurviveOn, double boneMealSuccessChance) {
super(tagsCanSurviveOn);
this.feature = feature;
this.stageProperty = stageProperty;
this.boneMealSuccessChance = boneMealSuccessChance;
}
public Key treeFeature() {
return feature;
}
@Override
public void randomTick(Object thisBlock, Object[] args, Callable<Object> superMethod) throws Exception {
Object world = args[1];
Object blockPos = args[2];
Object blockState = args[0];
int x = (int) Reflections.field$Vec3i$x.get(blockPos);
int y = (int) Reflections.field$Vec3i$y.get(blockPos);
int z = (int) Reflections.field$Vec3i$z.get(blockPos);
Object aboveBlockPos = LocationUtils.toBlockPos(x, y + 1, z);
if ((int) Reflections.method$LevelReader$getMaxLocalRawBrightness.invoke(world, aboveBlockPos) >= 9 && (float) Reflections.method$RandomSource$nextFloat.invoke(args[3]) < (1.0f / 7.0f)) {
increaseStage(world, blockPos, blockState, args[3]);
}
}
private void increaseStage(Object world, Object blockPos, Object blockState, Object randomSource) throws Exception {
ImmutableBlockState immutableBlockState = BukkitBlockManager.instance().getImmutableBlockState(BlockStateUtils.blockStateToId(blockState));
if (immutableBlockState == null || immutableBlockState.isEmpty()) return;
int currentStage = immutableBlockState.get(this.stageProperty);
if (currentStage != this.stageProperty.possibleValues().get(this.stageProperty.possibleValues().size() - 1)) {
ImmutableBlockState nextStage = immutableBlockState.cycle(this.stageProperty);
World bukkitWorld = (World) Reflections.method$Level$getCraftWorld.invoke(world);
int x = (int) Reflections.field$Vec3i$x.get(blockPos);
int y = (int) Reflections.field$Vec3i$y.get(blockPos);
int z = (int) Reflections.field$Vec3i$z.get(blockPos);
CraftEngineBlocks.place(new Location(bukkitWorld, x, y, z), nextStage, UpdateOption.UPDATE_NONE, false);
} else {
generateTree(world, blockPos, blockState, randomSource);
}
}
private void generateTree(Object world, Object blockPos, Object blockState, Object randomSource) throws Exception {
Object registry = Reflections.method$RegistryAccess$registryOrThrow.invoke(Reflections.instance$registryAccess, Reflections.instance$Registries$CONFIGURED_FEATURE);
if (registry == null) return;
@SuppressWarnings("unchecked")
Optional<Object> holder = (Optional<Object>) Reflections.method$Registry$getHolder1.invoke(registry, FeatureUtils.createFeatureKey(treeFeature()));
if (holder.isEmpty()) {
CraftEngine.instance().logger().warn("Configured feature not found: " + treeFeature());
return;
}
Object chunkGenerator = Reflections.method$ServerChunkCache$getGenerator.invoke(Reflections.field$ServerLevel$chunkSource.get(world));
Object configuredFeature = Reflections.method$Holder$value.invoke(holder.get());
Object fluidState = Reflections.method$Level$getFluidState.invoke(world, blockPos);
Object legacyState = Reflections.method$FluidState$createLegacyBlock.invoke(fluidState);
Reflections.method$Level$setBlock.invoke(world, blockPos, legacyState, UpdateOption.UPDATE_NONE.flags());
if ((boolean) Reflections.method$ConfiguredFeature$place.invoke(configuredFeature, world, chunkGenerator, randomSource, blockPos)) {
if (Reflections.method$BlockGetter$getBlockState.invoke(world, blockPos) == legacyState) {
Reflections.method$ServerLevel$sendBlockUpdated.invoke(world, blockPos, blockState, legacyState, 2);
}
} else {
// failed to place, rollback changes
Reflections.method$Level$setBlock.invoke(world, blockPos, blockState, UpdateOption.UPDATE_NONE.flags());
}
}
@Override
public boolean isBoneMealSuccess(Object thisBlock, Object[] args) {
return RandomUtils.generateRandomDouble(0d, 1d) < this.boneMealSuccessChance;
}
@Override
public boolean isValidBoneMealTarget(Object thisBlock, Object[] args) {
return true;
}
@Override
public void performBoneMeal(Object thisBlock, Object[] args) throws Exception {
this.increaseStage(args[0], args[2], args[3], args[1]);
}
public static class Factory implements BlockBehaviorFactory {
@SuppressWarnings("unchecked")
@Override
public BlockBehavior create(CustomBlock block, Map<String, Object> arguments) {
String feature = (String) arguments.get("feature");
if (feature == null) {
throw new IllegalArgumentException("feature is null");
}
Property<Integer> stageProperty = (Property<Integer>) block.getProperty("stage");
if (stageProperty == null) {
throw new IllegalArgumentException("stage property not set for sapling");
}
double boneMealSuccessChance = MiscUtils.getAsDouble(arguments.getOrDefault("bone-meal-success-chance", 0.45));
if (arguments.containsKey("tags")) {
return new SaplingBlockBehavior(Key.of(feature), stageProperty, MiscUtils.getAsStringList(arguments.get("tags")).stream().map(it -> BlockTags.getOrCreate(Key.of(it))).toList(), boneMealSuccessChance);
} else {
return new SaplingBlockBehavior(Key.of(feature), stageProperty, List.of(DIRT_TAG, FARMLAND), boneMealSuccessChance);
}
}
}
}

View File

@@ -0,0 +1,33 @@
package net.momirealms.craftengine.bukkit.block.behavior;
import net.momirealms.craftengine.core.block.CustomBlock;
import net.momirealms.craftengine.core.block.behavior.BlockBehaviorFactory;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.shared.block.BlockBehavior;
import java.util.Map;
public class StrippableBlockBehavior extends BlockBehavior {
public static final Factory FACTORY = new Factory();
private final Key stripped;
public StrippableBlockBehavior(Key stripped) {
this.stripped = stripped;
}
public Key stripped() {
return stripped;
}
public static class Factory implements BlockBehaviorFactory {
@Override
public BlockBehavior create(CustomBlock block, Map<String, Object> arguments) {
String stripped = (String) arguments.get("stripped");
if (stripped == null) {
throw new IllegalArgumentException("stripped is null");
}
return new StrippableBlockBehavior(Key.of(stripped));
}
}
}

View File

@@ -0,0 +1,112 @@
package net.momirealms.craftengine.bukkit.entity;
import net.momirealms.craftengine.bukkit.util.Reflections;
import net.momirealms.craftengine.core.util.VersionHelper;
import java.util.List;
public class DisplayEntityData<T> {
private final int id;
private final Object serializer;
private final T defaultValue;
// Entity
public static final DisplayEntityData<Byte> EntityMasks = of(0, EntityDataValue.Serializers$BYTE, (byte) 0);
// Display only
public static final DisplayEntityData<Integer> InterpolationDelay = of(8, EntityDataValue.Serializers$INT, 0);
// 1.19.4-1.20.1
public static final DisplayEntityData<Integer> InterpolationDuration = of(9, EntityDataValue.Serializers$INT, 0);
// 1.20.2+
public static final DisplayEntityData<Integer> TransformationInterpolationDuration = of(9, EntityDataValue.Serializers$INT, 0);
public static final DisplayEntityData<Integer> PositionRotationInterpolationDuration = of(10, EntityDataValue.Serializers$INT, 0);
public static final DisplayEntityData<Object> Translation = of(11, EntityDataValue.Serializers$VECTOR3, Reflections.instance$Vector3f$None);
public static final DisplayEntityData<Object> Scale = of(12, EntityDataValue.Serializers$VECTOR3, Reflections.instance$Vector3f$Normal);
public static final DisplayEntityData<Object> RotationLeft = of(13, EntityDataValue.Serializers$QUATERNION, Reflections.instance$Quaternionf$None);
public static final DisplayEntityData<Object> RotationRight = of(14, EntityDataValue.Serializers$QUATERNION, Reflections.instance$Quaternionf$None);
/**
* Billboard Constraints (0 = FIXED, 1 = VERTICAL, 2 = HORIZONTAL, 3 = CENTER)
*/
public static final DisplayEntityData<Byte> BillboardConstraints = of(15, EntityDataValue.Serializers$BYTE, (byte) 0);
/**
* Brightness override (blockLight << 4 | skyLight << 20)
*/
public static final DisplayEntityData<Integer> BrightnessOverride = of(16, EntityDataValue.Serializers$INT, -1);
public static final DisplayEntityData<Float> ViewRange = of(17, EntityDataValue.Serializers$FLOAT, 1f);
public static final DisplayEntityData<Float> ShadowRadius = of(18, EntityDataValue.Serializers$FLOAT, 0f);
public static final DisplayEntityData<Float> ShadowStrength = of(19, EntityDataValue.Serializers$FLOAT, 0f);
public static final DisplayEntityData<Float> Width = of(20, EntityDataValue.Serializers$FLOAT, 0f);
public static final DisplayEntityData<Float> Height = of(21, EntityDataValue.Serializers$FLOAT, 0f);
public static final DisplayEntityData<Integer> GlowColorOverride = of(22, EntityDataValue.Serializers$INT, -1);
// Text display only
public static final DisplayEntityData<Object> Text = of(23, EntityDataValue.Serializers$COMPONENT, Reflections.instance$Component$empty);
public static final DisplayEntityData<Integer> LineWidth = of(24, EntityDataValue.Serializers$INT, 200);
public static final DisplayEntityData<Integer> BackgroundColor = of(25, EntityDataValue.Serializers$INT, 0x40000000);
public static final DisplayEntityData<Byte> TextOpacity = of(26, EntityDataValue.Serializers$BYTE, (byte) -1);
public static final DisplayEntityData<Byte> TextDisplayMasks = of(27, EntityDataValue.Serializers$BYTE, (byte) 0);
// Item display only
public static final DisplayEntityData<Object> DisplayedItem = of(23, EntityDataValue.Serializers$ITEM_STACK, Reflections.instance$ItemStack$Air);
/**
* Display type:
* 0 = NONE
* 1 = THIRD_PERSON_LEFT_HAND
* 2 = THIRD_PERSON_RIGHT_HAND
* 3 = FIRST_PERSON_LEFT_HAND
* 4 = FIRST_PERSON_RIGHT_HAND
* 5 = HEAD
* 6 = GUI
* 7 = GROUND
* 8 = FIXED
*/
public static final DisplayEntityData<Byte> DisplayType = of(24, EntityDataValue.Serializers$BYTE, (byte) 0);
// Block display only
public static final DisplayEntityData<Object> DisplayedBlock = of(23, EntityDataValue.Serializers$BLOCK_STATE, Reflections.instance$Blocks$AIR$defaultState);
public static <T> DisplayEntityData<T> of(final int id, final Object serializer, T defaultValue) {
return new DisplayEntityData<>(id, serializer, defaultValue);
}
public DisplayEntityData(int id, Object serializer, T defaultValue) {
if (!VersionHelper.isVersionNewerThan1_20_2()) {
if (id >= 11) {
id--;
}
}
this.id = id;
this.serializer = serializer;
this.defaultValue = defaultValue;
}
public Object serializer() {
return serializer;
}
public int id() {
return id;
}
public T defaultValue() {
return defaultValue;
}
public Object createEntityDataIfNotDefaultValue(T value) {
if (defaultValue().equals(value)) return null;
return EntityDataValue.create(id, serializer, value);
}
public void addEntityDataIfNotDefaultValue(T value, List<Object> list) {
if (defaultValue().equals(value)) return;
list.add(EntityDataValue.create(id, serializer, value));
}
public void addEntityData(T value, List<Object> list) {
list.add(EntityDataValue.create(id, serializer, value));
}
}

View File

@@ -0,0 +1,104 @@
package net.momirealms.craftengine.bukkit.entity;
import net.momirealms.craftengine.bukkit.util.Reflections;
import net.momirealms.craftengine.core.util.ReflectionUtils;
import net.momirealms.craftengine.core.util.VersionHelper;
public class EntityDataValue {
private static int internalID = 0;
private static final String[] fieldsObf = {
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"
};
public static final Object Serializers$BYTE;
public static final Object Serializers$INT;
public static final Object Serializers$LONG;
public static final Object Serializers$FLOAT;
public static final Object Serializers$STRING;
public static final Object Serializers$COMPONENT;
public static final Object Serializers$OPTIONAL_COMPONENT;
public static final Object Serializers$ITEM_STACK;
public static final Object Serializers$BLOCK_STATE;
public static final Object Serializers$OPTIONAL_BLOCK_STATE;
public static final Object Serializers$BOOLEAN;
public static final Object Serializers$PARTICLE;
public static final Object Serializers$PARTICLES;
public static final Object Serializers$ROTATIONS;
public static final Object Serializers$BLOCK_POS;
public static final Object Serializers$OPTIONAL_BLOCK_POS;
public static final Object Serializers$DIRECTION;
public static final Object Serializers$OPTIONAL_UUID;
public static final Object Serializers$OPTIONAL_GLOBAL_POS;
public static final Object Serializers$COMPOUND_TAG;
public static final Object Serializers$VILLAGER_DATA;
public static final Object Serializers$OPTIONAL_UNSIGNED_INT;
public static final Object Serializers$POSE;
public static final Object Serializers$CAT_VARIANT;
public static final Object Serializers$WOLF_VARIANT;
public static final Object Serializers$FROG_VARIANT;
public static final Object Serializers$PAINTING_VARIANT;
public static final Object Serializers$ARMADILLO_STATE;
public static final Object Serializers$SNIFFER_STATE;
public static final Object Serializers$VECTOR3;
public static final Object Serializers$QUATERNION;
static {
try {
Serializers$BYTE = initSerializersByName("BYTE");
Serializers$INT = initSerializersByName("INT");
Serializers$LONG = initSerializersByName("LONG");
Serializers$FLOAT = initSerializersByName("FLOAT");
Serializers$STRING = initSerializersByName("STRING");
Serializers$COMPONENT = initSerializersByName("COMPONENT");
Serializers$OPTIONAL_COMPONENT = initSerializersByName("OPTIONAL_COMPONENT");
Serializers$ITEM_STACK = initSerializersByName("ITEM_STACK");
Serializers$BLOCK_STATE = initSerializersByName("BLOCK_STATE");
Serializers$OPTIONAL_BLOCK_STATE = initSerializersByName("OPTIONAL_BLOCK_STATE");
Serializers$BOOLEAN = initSerializersByName("BOOLEAN");
Serializers$PARTICLE = initSerializersByName("PARTICLE");
if (VersionHelper.isVersionNewerThan1_20_5()) Serializers$PARTICLES = initSerializersByName("PARTICLES");
else Serializers$PARTICLES = null;
Serializers$ROTATIONS = initSerializersByName("ROTATIONS");
Serializers$BLOCK_POS = initSerializersByName("BLOCK_POS");
Serializers$OPTIONAL_BLOCK_POS = initSerializersByName("OPTIONAL_BLOCK_POS");
Serializers$DIRECTION = initSerializersByName("DIRECTION");
Serializers$OPTIONAL_UUID = initSerializersByName("OPTIONAL_UUID");
Serializers$OPTIONAL_GLOBAL_POS = initSerializersByName("OPTIONAL_GLOBAL_POS");
Serializers$COMPOUND_TAG = initSerializersByName("COMPOUND_TAG");
Serializers$VILLAGER_DATA = initSerializersByName("VILLAGER_DATA");
Serializers$OPTIONAL_UNSIGNED_INT = initSerializersByName("OPTIONAL_UNSIGNED_INT");
Serializers$POSE = initSerializersByName("POSE");
Serializers$CAT_VARIANT = initSerializersByName("CAT_VARIANT");
if (VersionHelper.isVersionNewerThan1_20_5()) Serializers$WOLF_VARIANT = initSerializersByName("WOLF_VARIANT");
else Serializers$WOLF_VARIANT = null;
Serializers$FROG_VARIANT = initSerializersByName("FROG_VARIANT");
Serializers$PAINTING_VARIANT = initSerializersByName("PAINTING_VARIANT");
if (VersionHelper.isVersionNewerThan1_20_5()) Serializers$ARMADILLO_STATE = initSerializersByName("ARMADILLO_STATE");
else Serializers$ARMADILLO_STATE = null;
Serializers$SNIFFER_STATE = initSerializersByName("SNIFFER_STATE");
Serializers$VECTOR3 = initSerializersByName("VECTOR3");
Serializers$QUATERNION = initSerializersByName("QUATERNION");
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
private static Object initSerializersByName(String name) throws ReflectiveOperationException {
return ReflectionUtils.getDeclaredField(Reflections.clazz$EntityDataSerializers, new String[]{fieldsObf[internalID++], name}).get(null);
}
private EntityDataValue() {
throw new IllegalAccessError("Utility class");
}
public static Object create(int id, Object serializer, Object value) {
try {
Object entityDataAccessor = Reflections.constructor$EntityDataAccessor.newInstance(id, serializer);
return Reflections.method$SynchedEntityData$DataValue$create.invoke(null, entityDataAccessor, value);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,60 @@
package net.momirealms.craftengine.bukkit.entity;
import net.momirealms.craftengine.core.util.VersionHelper;
import java.util.List;
public class InteractionEntityData<T> {
private final int id;
private final Object serializer;
private final T defaultValue;
// Entity
public static final InteractionEntityData<Byte> EntityMasks = of(0, EntityDataValue.Serializers$BYTE, (byte) 0);
// Interaction only
public static final InteractionEntityData<Float> Width = of(8, EntityDataValue.Serializers$FLOAT, 1F);
public static final InteractionEntityData<Float> Height = of(9, EntityDataValue.Serializers$FLOAT, 1F);
public static final InteractionEntityData<Boolean> Responsive = of(10, EntityDataValue.Serializers$BOOLEAN, false);
public static <T> InteractionEntityData<T> of(final int id, final Object serializer, T defaultValue) {
return new InteractionEntityData<>(id, serializer, defaultValue);
}
public InteractionEntityData(int id, Object serializer, T defaultValue) {
if (!VersionHelper.isVersionNewerThan1_20_2()) {
if (id >= 11) {
id--;
}
}
this.id = id;
this.serializer = serializer;
this.defaultValue = defaultValue;
}
public Object serializer() {
return serializer;
}
public int id() {
return id;
}
public T defaultValue() {
return defaultValue;
}
public Object createEntityDataIfNotDefaultValue(T value) {
if (defaultValue().equals(value)) return null;
return EntityDataValue.create(id, serializer, value);
}
public void addEntityDataIfNotDefaultValue(T value, List<Object> list) {
if (defaultValue().equals(value)) return;
list.add(EntityDataValue.create(id, serializer, value));
}
public void addEntityData(T value, List<Object> list) {
list.add(EntityDataValue.create(id, serializer, value));
}
}

View File

@@ -0,0 +1,354 @@
package net.momirealms.craftengine.bukkit.entity.furniture;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.bukkit.util.EntityUtils;
import net.momirealms.craftengine.core.entity.furniture.*;
import net.momirealms.craftengine.core.loot.LootTable;
import net.momirealms.craftengine.core.pack.Pack;
import net.momirealms.craftengine.core.plugin.config.ConfigManager;
import net.momirealms.craftengine.core.plugin.scheduler.SchedulerTask;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.MiscUtils;
import net.momirealms.craftengine.core.util.VersionHelper;
import org.bukkit.*;
import org.bukkit.entity.*;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.persistence.PersistentDataType;
import org.jetbrains.annotations.NotNull;
import org.joml.Vector3f;
import javax.annotation.Nullable;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class BukkitFurnitureManager implements FurnitureManager {
public static final NamespacedKey FURNITURE_KEY = Objects.requireNonNull(NamespacedKey.fromString("craftengine:furniture_id"));
public static final NamespacedKey FURNITURE_ANCHOR_KEY = Objects.requireNonNull(NamespacedKey.fromString("craftengine:anchor_type"));
public static final NamespacedKey FURNITURE_SEAT_BASE_ENTITY_KEY = Objects.requireNonNull(NamespacedKey.fromString("craftengine:seat_to_base_entity"));
public static final NamespacedKey FURNITURE_SEAT_VECTOR_3F_KEY = Objects.requireNonNull(NamespacedKey.fromString("craftengine:seat_vector"));
private static BukkitFurnitureManager instance;
private final BukkitCraftEngine plugin;
private final Map<Key, CustomFurniture> byId = new HashMap<>();
private final Map<Integer, LoadedFurniture> furnitureByBaseEntityId = new ConcurrentHashMap<>(256, 0.5f);
private final Map<Integer, LoadedFurniture> furnitureByInteractionEntityId = new ConcurrentHashMap<>(512, 0.5f);
private final Map<Integer, int[]> baseEntity2SubEntities = new ConcurrentHashMap<>(256, 0.5f);
// Delay furniture cache remove for about 4-5 ticks
private static final int DELAYED_TICK = 5;
private final IntSet[] delayedRemove = new IntSet[DELAYED_TICK];
// Event listeners
private final Listener dismountListener;
private final FurnitureEventListener furnitureEventListener;
// tick task
private SchedulerTask tickTask;
public static BukkitFurnitureManager instance() {
return instance;
}
public BukkitFurnitureManager(BukkitCraftEngine plugin) {
this.plugin = plugin;
this.furnitureEventListener = new FurnitureEventListener(this);
this.dismountListener = VersionHelper.isVersionNewerThan1_20_3() ? new DismountListener1_20_3(this) : new DismountListener1_20(this::handleDismount);
for (int i = 0; i < DELAYED_TICK; i++) {
this.delayedRemove[i] = new IntOpenHashSet();
}
instance = this;
}
public LoadedFurniture place(CustomFurniture furniture, Location location, AnchorType anchorType, boolean playSound) {
Entity furnitureEntity = EntityUtils.spawnEntity(location.getWorld(), location, EntityType.ITEM_DISPLAY, entity -> {
ItemDisplay display = (ItemDisplay) entity;
display.getPersistentDataContainer().set(BukkitFurnitureManager.FURNITURE_KEY, PersistentDataType.STRING, furniture.id().toString());
display.getPersistentDataContainer().set(BukkitFurnitureManager.FURNITURE_ANCHOR_KEY, PersistentDataType.STRING, anchorType.name());
handleEntityLoadEarly(display);
});
if (playSound) {
location.getWorld().playSound(location, furniture.settings().sounds().placeSound().toString(), SoundCategory.BLOCKS,1f, 1f);
}
return getLoadedFurnitureByBaseEntityId(furnitureEntity.getEntityId());
}
@SuppressWarnings("unchecked")
@Override
public void parseSection(Pack pack, Path path, Key id, Map<String, Object> section) {
Map<String, Object> lootMap = MiscUtils.castToMap(section.get("loot"), true);
Map<String, Object> settingsMap = MiscUtils.castToMap(section.get("settings"), true);
Map<String, Object> placementMap = MiscUtils.castToMap(section.get("placement"), true);
EnumMap<AnchorType, CustomFurniture.Placement> placements = new EnumMap<>(AnchorType.class);
if (placementMap == null) {
throw new IllegalArgumentException("Missing required parameter 'placement' for furniture " + id);
}
for (Map.Entry<String, Object> entry : placementMap.entrySet()) {
AnchorType anchorType = AnchorType.valueOf(entry.getKey().toUpperCase(Locale.ENGLISH));
Map<String, Object> placementArguments = MiscUtils.castToMap(entry.getValue(), true);
List<FurnitureElement> elements = new ArrayList<>();
List<Map<String, Object>> elementConfigs = (List<Map<String, Object>>) placementArguments.getOrDefault("elements", List.of());
for (Map<String, Object> element : elementConfigs) {
String key = (String) element.get("item");
if (key == null) {
throw new IllegalArgumentException("Missing required parameter 'item' for furniture " + id);
}
ItemDisplayContext transform = ItemDisplayContext.valueOf(element.getOrDefault("transform", "NONE").toString().toUpperCase(Locale.ENGLISH));
Billboard billboard = Billboard.valueOf(element.getOrDefault("billboard", "FIXED").toString().toUpperCase(Locale.ENGLISH));
FurnitureElement furnitureElement = new FurnitureElement(Key.of(key), billboard, transform,
MiscUtils.getVector3f(element.getOrDefault("scale", "1")),
MiscUtils.getVector3f(element.getOrDefault("translation", "0")),
MiscUtils.getVector3f(element.getOrDefault("position", "0")),
MiscUtils.getQuaternionf(element.getOrDefault("rotation", "0"))
);
elements.add(furnitureElement);
}
List<Map<String, Object>> hitboxConfigs = (List<Map<String, Object>>) placementArguments.getOrDefault("hitboxes", List.of());
List<HitBox> hitboxes = new ArrayList<>();
for (Map<String, Object> config : hitboxConfigs) {
List<String> seats = (List<String>) config.getOrDefault("seats", List.of());
Seat[] seatArray = seats.stream()
.map(arg -> {
String[] split = arg.split(" ");
if (split.length == 1) return new Seat(MiscUtils.getVector3f(split[0]), 0, false);
return new Seat(MiscUtils.getVector3f(split[0]), Float.parseFloat(split[1]), true);
})
.toArray(Seat[]::new);
Vector3f position = MiscUtils.getVector3f(config.getOrDefault("position", "0"));
float width = MiscUtils.getAsFloat(config.getOrDefault("width", "1"));
float height = MiscUtils.getAsFloat(config.getOrDefault("height", "1"));
HitBox hitBox = new HitBox(
position,
new Vector3f(width, height, width),
seatArray,
(boolean) config.getOrDefault("interactive", true)
);
hitboxes.add(hitBox);
}
if (hitboxes.isEmpty()) {
hitboxes.add(new HitBox(
new Vector3f(),
new Vector3f(1,1,1),
new Seat[0],
true
));
}
Map<String, Object> ruleSection = MiscUtils.castToMap(placementArguments.get("rules"), true);
if (ruleSection != null) {
RotationRule rotationRule = Optional.ofNullable((String) ruleSection.get("rotation"))
.map(it -> RotationRule.valueOf(it.toUpperCase(Locale.ENGLISH)))
.orElse(RotationRule.ANY);
AlignmentRule alignmentRule = Optional.ofNullable((String) ruleSection.get("alignment"))
.map(it -> AlignmentRule.valueOf(it.toUpperCase(Locale.ENGLISH)))
.orElse(AlignmentRule.CENTER);
placements.put(anchorType, new CustomFurniture.Placement(
elements.toArray(new FurnitureElement[0]),
hitboxes.toArray(new HitBox[0]),
rotationRule,
alignmentRule
));
} else {
placements.put(anchorType, new CustomFurniture.Placement(
elements.toArray(new FurnitureElement[0]),
hitboxes.toArray(new HitBox[0]),
RotationRule.ANY,
AlignmentRule.CENTER
));
}
}
CustomFurniture furniture = new CustomFurniture(
id,
FurnitureSettings.fromMap(settingsMap),
placements,
lootMap == null ? null : LootTable.fromMap(lootMap)
);
this.byId.put(id, furniture);
}
public void tick() {
IntSet first = this.delayedRemove[0];
for (int i : first) {
// unloaded furniture might be loaded again
LoadedFurniture furniture = getLoadedFurnitureByBaseEntityId(i);
if (furniture == null)
this.baseEntity2SubEntities.remove(i);
}
first.clear();
for (int i = 1; i < DELAYED_TICK; i++) {
this.delayedRemove[i - 1] = this.delayedRemove[i];
}
this.delayedRemove[DELAYED_TICK-1] = first;
}
@Override
public void delayedInit() {
Bukkit.getPluginManager().registerEvents(this.dismountListener, this.plugin.bootstrap());
Bukkit.getPluginManager().registerEvents(this.furnitureEventListener, this.plugin.bootstrap());
this.tickTask = plugin.scheduler().sync().runRepeating(this::tick, 1, 1);
for (World world : Bukkit.getWorlds()) {
List<Entity> entities = world.getEntities();
for (Entity entity : entities) {
handleEntityLoadEarly(entity);
}
}
}
@Override
public void unload() {
this.byId.clear();
}
@Override
public void disable() {
HandlerList.unregisterAll(this.dismountListener);
HandlerList.unregisterAll(this.furnitureEventListener);
if (tickTask != null && !tickTask.cancelled()) {
tickTask.cancel();
}
unload();
for (Player player : Bukkit.getOnlinePlayers()) {
Entity vehicle = player.getVehicle();
if (vehicle != null) {
tryLeavingSeat(player, vehicle);
}
}
}
@Override
public Optional<CustomFurniture> getFurniture(Key id) {
return Optional.ofNullable(this.byId.get(id));
}
@Nullable
@Override
public int[] getSubEntityIdsByBaseEntityId(int entityId) {
return this.baseEntity2SubEntities.get(entityId);
}
@Override
public boolean isFurnitureBaseEntity(int entityId) {
return this.furnitureByBaseEntityId.containsKey(entityId);
}
@Nullable
public LoadedFurniture getLoadedFurnitureByBaseEntityId(int entityId) {
return this.furnitureByBaseEntityId.get(entityId);
}
@Nullable
public LoadedFurniture getLoadedFurnitureByInteractionEntityId(int entityId) {
return this.furnitureByInteractionEntityId.get(entityId);
}
protected void handleEntityUnload(Entity entity) {
int id = entity.getEntityId();
LoadedFurniture furniture = this.furnitureByBaseEntityId.remove(id);
if (furniture != null) {
furniture.destroySeats();
for (int sub : furniture.interactionEntityIds()) {
this.furnitureByInteractionEntityId.remove(sub);
}
this.delayedRemove[DELAYED_TICK-1].add(id);
}
}
@SuppressWarnings("deprecation") // just a misleading name `getTrackedPlayers`
protected void handleEntityLoadLate(Entity entity) {
if (entity instanceof ItemDisplay display) {
String id = entity.getPersistentDataContainer().get(FURNITURE_KEY, PersistentDataType.STRING);
if (id == null) return;
Key key = Key.of(id);
Optional<CustomFurniture> optionalFurniture = getFurniture(key);
if (optionalFurniture.isEmpty()) return;
CustomFurniture customFurniture = optionalFurniture.get();
LoadedFurniture previous = this.furnitureByBaseEntityId.get(display.getEntityId());
if (previous != null) return;
LoadedFurniture furniture = addNewFurniture(display, customFurniture, getAnchorType(entity, customFurniture));
for (Player player : display.getTrackedPlayers()) {
this.plugin.networkManager().sendPacket(player, furniture.spawnPacket());
}
}
}
public void handleEntityLoadEarly(Entity entity) {
if (entity instanceof ItemDisplay display) {
String id = entity.getPersistentDataContainer().get(FURNITURE_KEY, PersistentDataType.STRING);
if (id == null) return;
Key key = Key.of(id);
Optional<CustomFurniture> optionalFurniture = getFurniture(key);
if (optionalFurniture.isPresent()) {
CustomFurniture customFurniture = optionalFurniture.get();
LoadedFurniture previous = this.furnitureByBaseEntityId.get(display.getEntityId());
if (previous != null) return;
addNewFurniture(display, customFurniture, getAnchorType(entity, customFurniture));
return;
}
// Remove the entity if it's not a valid furniture
if (ConfigManager.removeInvalidFurniture()) {
if (ConfigManager.furnitureToRemove().isEmpty() || ConfigManager.furnitureToRemove().contains(id)) {
entity.remove();
}
}
}
}
private AnchorType getAnchorType(Entity baseEntity, CustomFurniture furniture) {
String anchorType = baseEntity.getPersistentDataContainer().get(FURNITURE_ANCHOR_KEY, PersistentDataType.STRING);
if (anchorType != null) {
try {
AnchorType unverified = AnchorType.valueOf(anchorType);
if (furniture.isAllowedPlacement(unverified)) {
return unverified;
}
} catch (IllegalArgumentException ignored) {
}
}
AnchorType anchorTypeEnum = furniture.getAnyPlacement();
baseEntity.getPersistentDataContainer().set(FURNITURE_ANCHOR_KEY, PersistentDataType.STRING, anchorTypeEnum.name());
return anchorTypeEnum;
}
private synchronized LoadedFurniture addNewFurniture(ItemDisplay display, CustomFurniture furniture, AnchorType anchorType) {
LoadedFurniture loadedFurniture = new LoadedFurniture(display, furniture, anchorType);
this.furnitureByBaseEntityId.put(loadedFurniture.baseEntityId(), loadedFurniture);
this.baseEntity2SubEntities.put(loadedFurniture.baseEntityId(), loadedFurniture.subEntityIds());
for (int entityId : loadedFurniture.interactionEntityIds()) {
this.furnitureByInteractionEntityId.put(entityId, loadedFurniture);
}
return loadedFurniture;
}
protected void handleDismount(Player player, Entity entity) {
if (!isSeatCarrierType(entity)) return;
Location location = entity.getLocation();
plugin.scheduler().sync().runDelayed(() -> tryLeavingSeat(player, entity), player.getWorld(), location.getBlockX() >> 4, location.getBlockZ() >> 4);
}
protected void tryLeavingSeat(@NotNull Player player, @NotNull Entity vehicle) {
Integer baseFurniture = vehicle.getPersistentDataContainer().get(FURNITURE_SEAT_BASE_ENTITY_KEY, PersistentDataType.INTEGER);
if (baseFurniture == null) return;
vehicle.remove();
LoadedFurniture furniture = getLoadedFurnitureByBaseEntityId(baseFurniture);
if (furniture == null) {
return;
}
String vector3f = vehicle.getPersistentDataContainer().get(BukkitFurnitureManager.FURNITURE_SEAT_VECTOR_3F_KEY, PersistentDataType.STRING);
if (vector3f == null) {
plugin.logger().warn("Failed to get vector3f for player " + player.getName() + "'s seat");
return;
}
Vector3f seatPos = MiscUtils.getVector3f(vector3f);
if (!furniture.releaseSeat(seatPos)) {
plugin.logger().warn("Failed to release seat " + seatPos + " for player " + player.getName());
}
}
protected boolean isSeatCarrierType(Entity entity) {
return (entity instanceof ArmorStand || entity instanceof ItemDisplay);
}
}

View File

@@ -0,0 +1,21 @@
package net.momirealms.craftengine.bukkit.entity.furniture;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDismountEvent;
public class DismountListener1_20_3 implements Listener {
private final BukkitFurnitureManager manager;
public DismountListener1_20_3(final BukkitFurnitureManager manager) {
this.manager = manager;
}
@EventHandler(ignoreCancelled = true)
public void onDismount(EntityDismountEvent event) {
if (event.getEntity() instanceof Player player) {
this.manager.handleDismount(player, event.getDismounted());
}
}
}

View File

@@ -0,0 +1,108 @@
package net.momirealms.craftengine.bukkit.entity.furniture;
import com.destroystokyo.paper.event.entity.EntityAddToWorldEvent;
import com.destroystokyo.paper.event.entity.EntityRemoveFromWorldEvent;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.PlayerInteractAtEntityEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.world.ChunkUnloadEvent;
import org.bukkit.event.world.EntitiesLoadEvent;
import org.bukkit.event.world.WorldLoadEvent;
import org.bukkit.event.world.WorldUnloadEvent;
import org.bukkit.persistence.PersistentDataType;
import java.util.List;
public class FurnitureEventListener implements Listener {
private final BukkitFurnitureManager manager;
public FurnitureEventListener(final BukkitFurnitureManager manager) {
this.manager = manager;
}
/*
* Load Entities
*/
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onEntitiesLoadEarly(EntitiesLoadEvent event) {
List<Entity> entities = event.getEntities();
for (Entity entity : entities) {
this.manager.handleEntityLoadEarly(entity);
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onWorldLoad(WorldLoadEvent event) {
List<Entity> entities = event.getWorld().getEntities();
for (Entity entity : entities) {
this.manager.handleEntityLoadEarly(entity);
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onEntityLoad(EntityAddToWorldEvent event) {
this.manager.handleEntityLoadLate(event.getEntity());
}
/*
* Unload Entities
*/
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
public void onChunkUnload(ChunkUnloadEvent event) {
Entity[] entities = event.getChunk().getEntities();
for (Entity entity : entities) {
this.manager.handleEntityUnload(entity);
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
public void onWorldUnload(WorldUnloadEvent event) {
List<Entity> entities = event.getWorld().getEntities();
for (Entity entity : entities) {
this.manager.handleEntityUnload(entity);
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)
public void onEntityUnload(EntityRemoveFromWorldEvent event) {
this.manager.handleEntityUnload(event.getEntity());
}
@EventHandler(ignoreCancelled = true)
public void onPlayerQuit(PlayerQuitEvent event) {
Player player = event.getPlayer();
Entity entity = player.getVehicle();
if (entity == null) return;
if (this.manager.isSeatCarrierType(entity)) {
this.manager.tryLeavingSeat(player, entity);
}
}
@EventHandler(ignoreCancelled = true)
public void onPlayerDeath(PlayerDeathEvent event) {
Player player = event.getPlayer();
Entity entity = player.getVehicle();
if (entity == null) return;
if (this.manager.isSeatCarrierType(entity)) {
this.manager.tryLeavingSeat(player, entity);
}
}
// do not allow players to put item on seats
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onInteractArmorStand(PlayerInteractAtEntityEvent event) {
Entity clicked = event.getRightClicked();
if (clicked instanceof ArmorStand armorStand) {
Integer baseFurniture = armorStand.getPersistentDataContainer().get(BukkitFurnitureManager.FURNITURE_SEAT_BASE_ENTITY_KEY, PersistentDataType.INTEGER);
if (baseFurniture == null) return;
event.setCancelled(true);
}
}
}

View File

@@ -0,0 +1,289 @@
package net.momirealms.craftengine.bukkit.entity.furniture;
import net.momirealms.craftengine.bukkit.entity.DisplayEntityData;
import net.momirealms.craftengine.bukkit.entity.InteractionEntityData;
import net.momirealms.craftengine.bukkit.item.BukkitItemManager;
import net.momirealms.craftengine.bukkit.util.EntityUtils;
import net.momirealms.craftengine.bukkit.util.LegacyAttributeUtils;
import net.momirealms.craftengine.bukkit.util.Reflections;
import net.momirealms.craftengine.core.entity.furniture.*;
import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.util.ArrayUtils;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.QuaternionUtils;
import net.momirealms.craftengine.core.util.VersionHelper;
import org.bukkit.Location;
import org.bukkit.attribute.Attribute;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.ItemDisplay;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataType;
import org.jetbrains.annotations.NotNull;
import org.joml.Vector3f;
import java.lang.ref.WeakReference;
import java.util.*;
public class LoadedFurniture {
private final Key id;
private final CustomFurniture furniture;
private final AnchorType anchorType;
private final Map<Integer, FurnitureElement> elements;
private final Map<Integer, HitBox> hitBoxes;
// location
private Location location;
// cached spawn packet
private Object cachedSpawnPacket;
// base entity
private final WeakReference<Entity> baseEntity;
private final int baseEntityId;
// includes elements + interactions
private final int[] subEntityIds;
// interactions
private final int[] interactionEntityIds;
// seats
private final Set<Vector3f> occupiedSeats = Collections.synchronizedSet(new HashSet<>());
private final Vector<Entity> seats = new Vector<>();
public LoadedFurniture(Entity baseEntity,
CustomFurniture furniture,
AnchorType anchorType) {
this.id = furniture.id();
this.baseEntityId = baseEntity.getEntityId();
this.anchorType = anchorType;
this.location = baseEntity.getLocation();
this.baseEntity = new WeakReference<>(baseEntity);
this.furniture = furniture;
this.hitBoxes = new HashMap<>();
this.elements = new HashMap<>();
List<Integer> entityIds = new ArrayList<>();
List<Integer> interactionEntityIds = new ArrayList<>();
CustomFurniture.Placement placement = furniture.getPlacement(anchorType);
for (FurnitureElement element : placement.elements()) {
int entityId = Reflections.instance$Entity$ENTITY_COUNTER.incrementAndGet();
entityIds.add(entityId);
this.elements.put(entityId, element);
}
for (HitBox hitBox : placement.hitbox()) {
int entityId = Reflections.instance$Entity$ENTITY_COUNTER.incrementAndGet();
entityIds.add(entityId);
interactionEntityIds.add(entityId);
this.hitBoxes.put(entityId, hitBox);
}
this.subEntityIds = new int[entityIds.size()];
for (int i = 0; i < entityIds.size(); ++i) {
this.subEntityIds[i] = entityIds.get(i);
}
this.interactionEntityIds = new int[interactionEntityIds.size()];
for (int i = 0; i < interactionEntityIds.size(); ++i) {
this.interactionEntityIds[i] = interactionEntityIds.get(i);
}
}
private void resetSpawnPackets() {
try {
List<Object> packets = new ArrayList<>();
for (Map.Entry<Integer, FurnitureElement> entry : elements.entrySet()) {
int entityId = entry.getKey();
FurnitureElement element = entry.getValue();
Item<ItemStack> item = BukkitItemManager.instance().createWrappedItem(element.item(), null);
if (item == null) {
CraftEngine.instance().logger().warn("Failed to create furniture element for " + id + " because item " + element.item() + " not found");
continue;
}
item.load();
Vector3f offset = QuaternionUtils.toQuaternionf(0, Math.toRadians(180 - this.location.getYaw()), 0).conjugate().transform(new Vector3f(element.offset()));
Object addEntityPacket = Reflections.constructor$ClientboundAddEntityPacket.newInstance(
entityId, UUID.randomUUID(), this.location.getX() + offset.x, this.location.getY() + offset.y, this.location.getZ() - offset.z, 0, this.location.getYaw(),
Reflections.instance$EntityType$ITEM_DISPLAY, 0, Reflections.instance$Vec3$Zero, 0
);
ArrayList<Object> values = new ArrayList<>();
DisplayEntityData.DisplayedItem.addEntityDataIfNotDefaultValue(item.getLiteralObject(), values);
DisplayEntityData.Scale.addEntityDataIfNotDefaultValue(element.scale(), values);
DisplayEntityData.RotationLeft.addEntityDataIfNotDefaultValue(element.rotation(), values);
DisplayEntityData.BillboardConstraints.addEntityDataIfNotDefaultValue(element.billboard().id(), values);
DisplayEntityData.Translation.addEntityDataIfNotDefaultValue(element.translation(), values);
DisplayEntityData.DisplayType.addEntityDataIfNotDefaultValue(element.transform().id(), values);
Object setDataPacket = Reflections.constructor$ClientboundSetEntityDataPacket.newInstance(entityId, values);
packets.add(addEntityPacket);
packets.add(setDataPacket);
}
for (Map.Entry<Integer, HitBox> entry : hitBoxes.entrySet()) {
int entityId = entry.getKey();
HitBox hitBox = entry.getValue();
Vector3f offset = QuaternionUtils.toQuaternionf(0, Math.toRadians(180 - this.location.getYaw()), 0).conjugate().transform(new Vector3f(hitBox.offset()));
Object addEntityPacket = Reflections.constructor$ClientboundAddEntityPacket.newInstance(
entityId, UUID.randomUUID(), this.location.getX() + offset.x, this.location.getY() + offset.y, this.location.getZ() - offset.z, 0, this.location.getYaw(),
Reflections.instance$EntityType$INTERACTION, 0, Reflections.instance$Vec3$Zero, 0
);
ArrayList<Object> values = new ArrayList<>();
InteractionEntityData.Height.addEntityDataIfNotDefaultValue(hitBox.size().y, values);
InteractionEntityData.Width.addEntityDataIfNotDefaultValue(hitBox.size().x, values);
InteractionEntityData.Responsive.addEntityDataIfNotDefaultValue(hitBox.responsive(), values);
Object setDataPacket = Reflections.constructor$ClientboundSetEntityDataPacket.newInstance(entityId, values);
packets.add(addEntityPacket);
packets.add(setDataPacket);
}
this.cachedSpawnPacket = Reflections.constructor$ClientboundBundlePacket.newInstance(packets);
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to init spawn packets for furniture " + id, e);
}
}
@NotNull
public Location location() {
return this.location;
}
public void teleport(@NotNull Location location) {
if (location.equals(this.location)) return;
this.location = location;
}
public Object spawnPacket() {
if (this.cachedSpawnPacket == null) {
this.resetSpawnPackets();
}
return this.cachedSpawnPacket;
}
@NotNull
public Entity baseEntity() {
Entity entity = baseEntity.get();
if (entity == null) {
throw new RuntimeException("Base entity not found");
}
return entity;
}
public boolean isValid() {
return baseEntity().isValid();
}
public void destroy() {
if (!isValid()) {
return;
}
this.baseEntity().remove();
for (Entity entity : this.seats) {
for (Entity passenger : entity.getPassengers()) {
entity.removePassenger(passenger);
}
entity.remove();
}
this.seats.clear();
}
public void destroySeats() {
for (Entity entity : this.seats) {
entity.remove();
}
this.seats.clear();
}
public void addSeatEntity(Entity entity) {
this.seats.add(entity);
}
public Optional<Seat> getAvailableSeat(int clickedEntityId) {
HitBox hitbox = this.hitBoxes.get(clickedEntityId);
if (hitbox == null)
return Optional.empty();
Seat[] seats = hitbox.seats();
if (ArrayUtils.isEmpty(seats)) {
return Optional.empty();
}
for (Seat seat : seats) {
if (!this.occupiedSeats.contains(seat.offset())) {
return Optional.of(seat);
}
}
return Optional.empty();
}
public Location getSeatLocation(Seat seat) {
Vector3f offset = QuaternionUtils.toQuaternionf(0, Math.toRadians(180 - this.location.getYaw()), 0).conjugate().transform(new Vector3f(seat.offset()));
double yaw = seat.yaw() + this.location.getYaw();
if (yaw < -180) yaw += 360;
Location newLocation = this.location.clone();
newLocation.setYaw((float) yaw);
newLocation.add(offset.x, offset.y + 0.6, -offset.z);
return newLocation;
}
public boolean releaseSeat(Vector3f seat) {
return this.occupiedSeats.remove(seat);
}
public boolean occupySeat(Seat seat) {
if (this.occupiedSeats.contains(seat.offset())) {
return false;
}
this.occupiedSeats.add(seat.offset());
return true;
}
public int baseEntityId() {
return baseEntityId;
}
public int[] interactionEntityIds() {
return interactionEntityIds;
}
public int[] subEntityIds() {
return this.subEntityIds;
}
public AnchorType anchorType() {
return anchorType;
}
public Key furnitureId() {
return id;
}
public CustomFurniture furniture() {
return furniture;
}
public void mountSeat(org.bukkit.entity.Player player, Seat seat) {
Location location = this.getSeatLocation(seat);
Entity seatEntity = seat.limitPlayerRotation() ?
EntityUtils.spawnEntity(player.getWorld(), VersionHelper.isVersionNewerThan1_20_2() ? location.subtract(0,0.9875,0) : location.subtract(0,0.990625,0), EntityType.ARMOR_STAND, entity -> {
ArmorStand armorStand = (ArmorStand) entity;
if (VersionHelper.isVersionNewerThan1_21_3()) {
Objects.requireNonNull(armorStand.getAttribute(Attribute.MAX_HEALTH)).setBaseValue(0.01);
} else {
LegacyAttributeUtils.setMaxHealth(armorStand);
}
armorStand.setSmall(true);
armorStand.setInvisible(true);
armorStand.setSilent(true);
armorStand.setInvulnerable(true);
armorStand.setArms(false);
armorStand.setCanTick(false);
armorStand.setAI(false);
armorStand.setGravity(false);
armorStand.setPersistent(false);
armorStand.getPersistentDataContainer().set(BukkitFurnitureManager.FURNITURE_SEAT_BASE_ENTITY_KEY, PersistentDataType.INTEGER, this.baseEntityId());
armorStand.getPersistentDataContainer().set(BukkitFurnitureManager.FURNITURE_SEAT_VECTOR_3F_KEY, PersistentDataType.STRING, seat.offset().x + ", " + seat.offset().y + ", " + seat.offset().z);
}) :
EntityUtils.spawnEntity(player.getWorld(), VersionHelper.isVersionNewerThan1_20_2() ? location : location.subtract(0,0.25,0), EntityType.ITEM_DISPLAY, entity -> {
ItemDisplay itemDisplay = (ItemDisplay) entity;
itemDisplay.setPersistent(false);
itemDisplay.getPersistentDataContainer().set(BukkitFurnitureManager.FURNITURE_SEAT_BASE_ENTITY_KEY, PersistentDataType.INTEGER, this.baseEntityId());
itemDisplay.getPersistentDataContainer().set(BukkitFurnitureManager.FURNITURE_SEAT_VECTOR_3F_KEY, PersistentDataType.STRING, seat.offset().x + ", " + seat.offset().y + ", " + seat.offset().z);
});
this.addSeatEntity(seatEntity);
seatEntity.addPassenger(player);
}
}

View File

@@ -0,0 +1,148 @@
package net.momirealms.craftengine.bukkit.item;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.bukkit.util.MaterialUtils;
import net.momirealms.craftengine.core.item.CustomItem;
import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.item.ItemBuildContext;
import net.momirealms.craftengine.core.item.ItemSettings;
import net.momirealms.craftengine.core.item.behavior.ItemBehavior;
import net.momirealms.craftengine.core.item.modifier.ItemModifier;
import net.momirealms.craftengine.core.util.Key;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class BukkitCustomItem implements CustomItem<ItemStack> {
private final Key id;
private final Key materialKey;
private final Material material;
private final List<ItemModifier<ItemStack>> modifiers;
private final List<ItemBehavior> behavior;
private final ItemSettings settings;
public BukkitCustomItem(Key id, Key materialKey, Material material, List<ItemModifier<ItemStack>> modifiers, List<ItemBehavior> behavior, ItemSettings settings) {
this.id = id;
this.material = material;
this.modifiers = modifiers;
this.behavior = behavior;
this.materialKey = materialKey;
this.settings = settings;
}
@Override
public Key id() {
return id;
}
@Override
public Key material() {
return materialKey;
}
@Override
public List<ItemModifier<ItemStack>> modifiers() {
return modifiers;
}
@Override
public ItemStack buildItemStack(ItemBuildContext context, int count) {
ItemStack item = new ItemStack(material);
if (this.modifiers.isEmpty()) {
return item;
}
Item<ItemStack> wrapped = BukkitCraftEngine.instance().itemManager().wrap(item);
wrapped.count(count);
for (ItemModifier<ItemStack> modifier : this.modifiers) {
modifier.apply(wrapped, context);
}
return wrapped.load();
}
@Override
public ItemSettings settings() {
return settings;
}
@Override
public Item<ItemStack> buildItem(ItemBuildContext context) {
ItemStack item = new ItemStack(material);
Item<ItemStack> wrapped = BukkitCraftEngine.instance().itemManager().wrap(item);
for (ItemModifier<ItemStack> modifier : modifiers()) {
modifier.apply(wrapped, context);
}
wrapped.load();
return wrapped;
}
@Override
public @NotNull List<ItemBehavior> behaviors() {
return this.behavior;
}
public static Builder<ItemStack> builder() {
return new BuilderImpl();
}
public static class BuilderImpl implements Builder<ItemStack> {
private Key id;
private Material material;
private Key materialKey;
private List<ItemBehavior> behavior = List.of();
private ItemSettings settings = ItemSettings.of();
private final List<ItemModifier<ItemStack>> modifiers = new ArrayList<>();
@Override
public Builder<ItemStack> id(Key id) {
this.id = id;
return this;
}
@Override
public Builder<ItemStack> material(Key material) {
this.materialKey = material;
this.material = MaterialUtils.getMaterial(material.value());
return this;
}
@Override
public Builder<ItemStack> modifier(ItemModifier<ItemStack> modifier) {
this.modifiers.add(modifier);
return this;
}
@Override
public Builder<ItemStack> modifiers(List<ItemModifier<ItemStack>> list) {
this.modifiers.addAll(list);
return this;
}
@Override
public Builder<ItemStack> behavior(ItemBehavior behavior) {
this.behavior= List.of(behavior);
return this;
}
@Override
public Builder<ItemStack> behavior(List<ItemBehavior> behaviors) {
this.behavior = behaviors;
return this;
}
@Override
public Builder<ItemStack> settings(ItemSettings settings) {
this.settings = settings;
return this;
}
@Override
public CustomItem<ItemStack> build() {
this.modifiers.addAll(this.settings.modifiers());
return new BukkitCustomItem(id, materialKey, material, Collections.unmodifiableList(modifiers), behavior, settings);
}
}
}

View File

@@ -0,0 +1,615 @@
package net.momirealms.craftengine.bukkit.item;
import net.momirealms.craftengine.bukkit.item.behavior.AxeItemBehavior;
import net.momirealms.craftengine.bukkit.item.behavior.BoneMealBehavior;
import net.momirealms.craftengine.bukkit.item.behavior.BucketItemBehavior;
import net.momirealms.craftengine.bukkit.item.behavior.WaterBucketItemBehavior;
import net.momirealms.craftengine.bukkit.item.factory.BukkitItemFactory;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.bukkit.util.ItemUtils;
import net.momirealms.craftengine.bukkit.util.MaterialUtils;
import net.momirealms.craftengine.bukkit.util.Reflections;
import net.momirealms.craftengine.core.entity.player.Player;
import net.momirealms.craftengine.core.item.*;
import net.momirealms.craftengine.core.item.behavior.ItemBehavior;
import net.momirealms.craftengine.core.item.behavior.ItemBehaviors;
import net.momirealms.craftengine.core.item.modifier.CustomModelDataModifier;
import net.momirealms.craftengine.core.item.modifier.IdModifier;
import net.momirealms.craftengine.core.item.modifier.ItemModelModifier;
import net.momirealms.craftengine.core.pack.LegacyOverridesModel;
import net.momirealms.craftengine.core.pack.Pack;
import net.momirealms.craftengine.core.pack.model.*;
import net.momirealms.craftengine.core.pack.model.generator.ModelGeneration;
import net.momirealms.craftengine.core.plugin.config.ConfigManager;
import net.momirealms.craftengine.core.registry.BuiltInRegistries;
import net.momirealms.craftengine.core.registry.Holder;
import net.momirealms.craftengine.core.registry.WritableRegistry;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.MiscUtils;
import net.momirealms.craftengine.core.util.ResourceKey;
import net.momirealms.craftengine.core.util.VersionHelper;
import net.momirealms.craftengine.core.util.context.ContextHolder;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.event.HandlerList;
import org.bukkit.inventory.ItemStack;
import org.incendo.cloud.suggestion.Suggestion;
import org.incendo.cloud.type.Either;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Stream;
public class BukkitItemManager extends AbstractItemManager<ItemStack> {
private static final Map<Key, List<ItemBehavior>> VANILLA_ITEM_EXTRA_BEHAVIORS = new HashMap<>();
private static final List<Key> VANILLA_ITEMS = new ArrayList<>();
static {
registerVanillaItemExtraBehavior(AxeItemBehavior.INSTANCE, ItemKeys.AXES);
registerVanillaItemExtraBehavior(WaterBucketItemBehavior.INSTANCE, ItemKeys.WATER_BUCKETS);
registerVanillaItemExtraBehavior(BucketItemBehavior.INSTANCE, ItemKeys.BUCKET);
registerVanillaItemExtraBehavior(BoneMealBehavior.INSTANCE, ItemKeys.BONE_MEAL);
}
private static void registerVanillaItemExtraBehavior(ItemBehavior behavior, Key... items) {
for (Key key : items) {
VANILLA_ITEM_EXTRA_BEHAVIORS.computeIfAbsent(key, k -> new ArrayList<>()).add(behavior);
}
}
private static BukkitItemManager instance;
private final BukkitItemFactory factory;
private final Map<Key, TreeSet<LegacyOverridesModel>> legacyOverrides;
private final Map<Key, TreeMap<Integer, ItemModel>> modernOverrides;
private final BukkitCraftEngine plugin;
private final ItemEventListener itemEventListener;
private final DebugStickListener debugStickListener;
private final Map<Key, List<Holder<Key>>> vanillaItemTags;
private final Map<Key, List<Holder<Key>>> customItemTags;
private final Map<Key, Map<Integer, Key>> cmdConflictChecker;
private final Map<Key, ItemModel> modernItemModels1_21_4;
private final Map<Key, List<LegacyOverridesModel>> modernItemModels1_21_2;
// Cached command suggestions
private final List<Suggestion> cachedSuggestions = new ArrayList<>();
public BukkitItemManager(BukkitCraftEngine plugin) {
super(plugin);
this.plugin = plugin;
this.factory = BukkitItemFactory.create(plugin);
this.legacyOverrides = new HashMap<>();
this.modernOverrides = new HashMap<>();
this.vanillaItemTags = new HashMap<>();
this.customItemTags = new HashMap<>();
this.cmdConflictChecker = new HashMap<>();
this.modernItemModels1_21_4 = new HashMap<>();
this.modernItemModels1_21_2 = new HashMap<>();
this.itemEventListener = new ItemEventListener(plugin);
this.debugStickListener = new DebugStickListener(plugin);
this.registerAllVanillaItems();
instance = this;
}
@Override
public void delayedInit() {
Bukkit.getPluginManager().registerEvents(this.itemEventListener, plugin.bootstrap());
Bukkit.getPluginManager().registerEvents(this.debugStickListener, plugin.bootstrap());
}
public static BukkitItemManager instance() {
return instance;
}
@Override
public Optional<BuildableItem<ItemStack>> getVanillaItem(Key key) {
Material material = Registry.MATERIAL.get(Objects.requireNonNull(NamespacedKey.fromString(key.toString())));
if (material == null) {
return Optional.empty();
}
return Optional.of(new CloneableConstantItem(key, new ItemStack(material)));
}
@Override
public List<Holder<Key>> tagToItems(Key tag) {
List<Holder<Key>> items = new ArrayList<>();
List<Holder<Key>> holders = vanillaItemTags.get(tag);
if (holders != null) {
items.addAll(holders);
}
List<Holder<Key>> customItems = customItemTags.get(tag);
if (customItems != null) {
items.addAll(customItems);
}
return items;
}
@Override
public List<Holder<Key>> tagToVanillaItems(Key tag) {
return this.vanillaItemTags.getOrDefault(tag, List.of());
}
@Override
public List<Holder<Key>> tagToCustomItems(Key tag) {
return this.customItemTags.getOrDefault(tag, List.of());
}
@Override
public int fuelTime(ItemStack itemStack) {
if (ItemUtils.isEmpty(itemStack)) return 0;
Optional<CustomItem<ItemStack>> customItem = wrap(itemStack).getCustomItem();
return customItem.map(it -> it.settings().fuelTime()).orElse(0);
}
@Override
public int fuelTime(Key id) {
return getCustomItem(id).map(it -> it.settings().fuelTime()).orElse(0);
}
@Override
public Collection<Suggestion> cachedSuggestions() {
return this.cachedSuggestions;
}
@Override
public void load() {
super.load();
}
@Override
public void unload() {
super.unload();
this.cachedSuggestions.clear();
this.legacyOverrides.clear();
this.modernOverrides.clear();
this.customItemTags.clear();
this.cmdConflictChecker.clear();
}
@Override
public void disable() {
unload();
HandlerList.unregisterAll(this.itemEventListener);
HandlerList.unregisterAll(this.debugStickListener);
}
@Override
public ItemStack buildCustomItemStack(Key id, Player player) {
return Optional.ofNullable(customItems.get(id)).map(it -> it.buildItemStack(new ItemBuildContext(player, ContextHolder.EMPTY), 1)).orElse(null);
}
@Override
public ItemStack buildItemStack(Key id, @Nullable Player player) {
return Optional.ofNullable(buildCustomItemStack(id, player)).orElseGet(() -> createVanillaItemStack(id));
}
@Override
public Item<ItemStack> createCustomWrappedItem(Key id, Player player) {
return Optional.ofNullable(customItems.get(id)).map(it -> it.buildItem(player)).orElse(null);
}
private ItemStack createVanillaItemStack(Key id) {
NamespacedKey key = NamespacedKey.fromString(id.toString());
if (key == null) {
this.plugin.logger().warn(id + " is not a valid namespaced key");
return new ItemStack(Material.AIR);
}
Material material = Registry.MATERIAL.get(key);
if (material == null) {
this.plugin.logger().warn(id + " is not a valid material");
return new ItemStack(Material.AIR);
}
return new ItemStack(material);
}
@Override
public Item<ItemStack> createWrappedItem(Key id, @Nullable Player player) {
return Optional.ofNullable(customItems.get(id)).map(it -> it.buildItem(player)).orElseGet(() -> {
ItemStack itemStack = createVanillaItemStack(id);
return wrap(itemStack);
});
}
@Override
public Item<ItemStack> wrap(ItemStack itemStack) {
if (ItemUtils.isEmpty(itemStack)) return null;
return this.factory.wrap(itemStack);
}
@Override
public Key itemId(ItemStack itemStack) {
Item<ItemStack> wrapped = wrap(itemStack);
return wrapped.id();
}
@Override
public Key customItemId(ItemStack itemStack) {
Item<ItemStack> wrapped = wrap(itemStack);
if (!wrapped.hasTag(IdModifier.CRAFT_ENGINE_ID)) return null;
return wrapped.id();
}
@Override
public Optional<List<ItemBehavior>> getItemBehavior(Key key) {
Optional<CustomItem<ItemStack>> customItemOptional = getCustomItem(key);
if (customItemOptional.isPresent()) {
CustomItem<ItemStack> customItem = customItemOptional.get();
Key vanillaMaterial = customItem.material();
List<ItemBehavior> behavior = VANILLA_ITEM_EXTRA_BEHAVIORS.get(vanillaMaterial);
if (behavior != null) {
return Optional.of(Stream.concat(customItem.behaviors().stream(), behavior.stream()).toList());
} else {
return Optional.of(List.copyOf(customItem.behaviors()));
}
} else {
List<ItemBehavior> behavior = VANILLA_ITEM_EXTRA_BEHAVIORS.get(key);
if (behavior != null) {
return Optional.of(List.copyOf(behavior));
} else {
return Optional.empty();
}
}
}
@Override
public Collection<Key> items() {
return new ArrayList<>(customItems.keySet());
}
@Override
public void parseSection(Pack pack, Path path, Key id, Map<String, Object> section) {
// just register and recipes
Holder.Reference<Key> holder = BuiltInRegistries.OPTIMIZED_ITEM_ID.get(id)
.orElseGet(() -> ((WritableRegistry<Key>) BuiltInRegistries.OPTIMIZED_ITEM_ID)
.register(new ResourceKey<>(BuiltInRegistries.OPTIMIZED_ITEM_ID.key().location(), id), id));
String materialStringId = (String) section.get("material");
Material material = MaterialUtils.getMaterial(materialStringId);
if (material == null) {
plugin.logger().warn(path, "material " + Optional.ofNullable(materialStringId).map(it -> it + " ").orElse("") + "does not exist for item " + id);
return;
}
Key materialId = Key.of(material.getKey().namespace(), material.getKey().value());
int customModelData = MiscUtils.getAsInt(section.getOrDefault("custom-model-data", 0));
Key itemModelKey = null;
CustomItem.Builder<ItemStack> itemBuilder = BukkitCustomItem.builder().id(id).material(materialId);
itemBuilder.modifier(new IdModifier<>(id));
// Sets some basic model info
if (customModelData != 0) {
itemBuilder.modifier(new CustomModelDataModifier<>(customModelData));
} else if (section.containsKey("model") && VersionHelper.isVersionNewerThan1_21_2()) {
// check server version here because components require 1.21.2+
// customize or use the id
itemModelKey = Key.from(section.getOrDefault("item-model", id.toString()).toString());
itemBuilder.modifier(new ItemModelModifier<>(itemModelKey));
}
// Get item behaviors
Object behaviorConfig = section.get("behavior");
if (behaviorConfig instanceof List<?>) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> behavior = (List<Map<String, Object>>) behaviorConfig;
List<ItemBehavior> behaviors = new ArrayList<>();
for (Map<String, Object> behaviorMap : behavior) {
behaviors.add(ItemBehaviors.fromMap(pack, path, id, behaviorMap));
}
itemBuilder.behavior(behaviors);
} else if (behaviorConfig instanceof Map<?, ?>) {
Map<String, Object> behaviorSection = MiscUtils.castToMap(section.get("behavior"), true);
if (behaviorSection != null) {
itemBuilder.behavior(ItemBehaviors.fromMap(pack, path, id, behaviorSection));
}
}
// Get item data
Map<String, Object> dataSection = MiscUtils.castToMap(section.get("data"), true);
if (dataSection != null) {
for (Map.Entry<String, Object> dataEntry : dataSection.entrySet()) {
Optional.ofNullable(dataFunctions.get(dataEntry.getKey())).ifPresent(function -> {
try {
itemBuilder.modifier(function.apply(dataEntry.getValue()));
} catch (IllegalArgumentException e) {
plugin.logger().warn("Invalid data format", e);
}
});
}
}
if (section.containsKey("settings")) {
Map<String, Object> settings = MiscUtils.castToMap(section.get("settings"), false);
itemBuilder.settings(ItemSettings.fromMap(settings));
}
CustomItem<ItemStack> customItem = itemBuilder.build();
this.customItems.put(id, customItem);
this.cachedSuggestions.add(Suggestion.suggestion(id.toString()));
// regitser tags
Set<Key> tags = customItem.settings().tags();
for (Key tag : tags) {
this.customItemTags.computeIfAbsent(tag, k -> new ArrayList<>()).add(holder);
}
// model part, can be null
// but if it exists, either custom model data or item model should be configured
Map<String, Object> modelSection = MiscUtils.castToMap(section.get("model"), true);
if (modelSection == null) {
return;
}
if (customModelData != 0) {
// use custom model data
// check conflict
Map<Integer, Key> conflict = this.cmdConflictChecker.computeIfAbsent(materialId, k -> new HashMap<>());
if (conflict.containsKey(customModelData)) {
plugin.logger().warn(path, "Failed to create model for " + id + " because custom-model-data " + customModelData + " already occupied by item " + conflict.get(customModelData).toString());
return;
}
conflict.put(customModelData, id);
// Parse models
ItemModel model = ItemModels.fromMap(modelSection);
for (ModelGeneration generation : model.modelsToGenerate()) {
prepareModelGeneration(generation);
}
if (ConfigManager.packMaxVersion() > 21.39f) {
TreeMap<Integer, ItemModel> map = this.modernOverrides.computeIfAbsent(materialId, k -> new TreeMap<>());
map.put(customModelData, model);
}
if (ConfigManager.packMinVersion() < 21.39f) {
List<LegacyOverridesModel> legacyOverridesModels = new ArrayList<>();
processModelRecursively(model, new LinkedHashMap<>(), legacyOverridesModels, materialId, customModelData);
TreeSet<LegacyOverridesModel> lom = this.legacyOverrides.computeIfAbsent(materialId, k -> new TreeSet<>());
lom.addAll(legacyOverridesModels);
}
} else if (itemModelKey != null) {
// use components
ItemModel model = ItemModels.fromMap(modelSection);
for (ModelGeneration generation : model.modelsToGenerate()) {
prepareModelGeneration(generation);
}
if (ConfigManager.packMaxVersion() > 21.39f) {
this.modernItemModels1_21_4.put(itemModelKey, model);
}
if (ConfigManager.packMaxVersion() > 21.19f && ConfigManager.packMinVersion() < 21.39f) {
List<LegacyOverridesModel> legacyOverridesModels = new ArrayList<>();
processModelRecursively(model, new LinkedHashMap<>(), legacyOverridesModels, materialId, 0);
if (legacyOverridesModels.isEmpty()) {
plugin.logger().warn(path, "Can't convert " + id + "'s model to legacy format.");
return;
}
legacyOverridesModels.sort(LegacyOverridesModel::compareTo);
this.modernItemModels1_21_2.put(itemModelKey, legacyOverridesModels);
}
} else {
if (!VersionHelper.isVersionNewerThan1_21_2()) {
plugin.logger().warn(path, "No custom-model-data configured for " + id);
}
}
}
@Override
public Map<Key, ItemModel> modernItemModels1_21_4() {
return this.modernItemModels1_21_4;
}
@Override
public Map<Key, List<LegacyOverridesModel>> modernItemModels1_21_2() {
return this.modernItemModels1_21_2;
}
@Override
public Collection<Key> vanillaItems() {
return VANILLA_ITEMS;
}
@Override
public Map<Key, TreeSet<LegacyOverridesModel>> legacyItemOverrides() {
return this.legacyOverrides;
}
@Override
public Map<Key, TreeMap<Integer, ItemModel>> modernItemOverrides() {
return this.modernOverrides;
}
private void processModelRecursively(
ItemModel currentModel,
Map<String, Object> accumulatedPredicates,
List<LegacyOverridesModel> resultList,
Key materialId,
int customModelData
) {
if (currentModel instanceof ConditionItemModel conditionModel) {
handleConditionModel(conditionModel, accumulatedPredicates, resultList, materialId, customModelData);
} else if (currentModel instanceof RangeDispatchItemModel rangeModel) {
handleRangeModel(rangeModel, accumulatedPredicates, resultList, materialId, customModelData);
} else if (currentModel instanceof SelectItemModel selectModel) {
handleSelectModel(selectModel, accumulatedPredicates, resultList, materialId, customModelData);
} else if (currentModel instanceof BaseItemModel baseModel) {
resultList.add(new LegacyOverridesModel(
new LinkedHashMap<>(accumulatedPredicates),
baseModel.path(),
customModelData
));
}
}
@SuppressWarnings({"unchecked", "rawtypes"})
private void handleConditionModel(
ConditionItemModel model,
Map<String, Object> parentPredicates,
List<LegacyOverridesModel> resultList,
Key materialId,
int customModelData
) {
if (model.property() instanceof LegacyModelPredicate predicate) {
String predicateId = predicate.legacyPredicateId(materialId);
Map<String, Object> truePredicates = mergePredicates(
parentPredicates,
predicateId,
predicate.toLegacyValue(true)
);
processModelRecursively(
model.onTrue(),
truePredicates,
resultList,
materialId,
customModelData
);
Map<String, Object> falsePredicates = mergePredicates(
parentPredicates,
predicateId,
predicate.toLegacyValue(false)
);
processModelRecursively(
model.onFalse(),
falsePredicates,
resultList,
materialId,
customModelData
);
}
}
@SuppressWarnings({"unchecked", "rawtypes"})
private void handleRangeModel(
RangeDispatchItemModel model,
Map<String, Object> parentPredicates,
List<LegacyOverridesModel> resultList,
Key materialId,
int customModelData
) {
if (model.property() instanceof LegacyModelPredicate predicate) {
String predicateId = predicate.legacyPredicateId(materialId);
for (Map.Entry<Float, ItemModel> entry : model.entries().entrySet()) {
Map<String, Object> merged = mergePredicates(
parentPredicates,
predicateId,
predicate.toLegacyValue(entry.getKey())
);
processModelRecursively(
entry.getValue(),
merged,
resultList,
materialId,
customModelData
);
}
if (model.fallBack() != null) {
Map<String, Object> merged = mergePredicates(
parentPredicates,
predicateId,
predicate.toLegacyValue(0f)
);
processModelRecursively(
model.fallBack(),
merged,
resultList,
materialId,
customModelData
);
}
}
}
@SuppressWarnings({"unchecked", "rawtypes"})
private void handleSelectModel(
SelectItemModel model,
Map<String, Object> parentPredicates,
List<LegacyOverridesModel> resultList,
Key materialId,
int customModelData
) {
if (model.property() instanceof LegacyModelPredicate predicate) {
String predicateId = predicate.legacyPredicateId(materialId);
for (Map.Entry<Either<String, List<String>>, ItemModel> entry : model.whenMap().entrySet()) {
List<String> cases = entry.getKey().fallbackOrMapPrimary(List::of);
for (String caseValue : cases) {
Map<String, Object> merged = mergePredicates(
parentPredicates,
predicateId,
predicate.toLegacyValue(caseValue)
);
// Additional check for crossbow
if (materialId.equals(ItemKeys.CROSSBOW)) {
merged = mergePredicates(
merged,
"charged",
1
);
}
processModelRecursively(
entry.getValue(),
merged,
resultList,
materialId,
customModelData
);
}
}
// Additional check for crossbow
if (model.fallBack() != null && materialId.equals(ItemKeys.CROSSBOW)) {
Map<String, Object> merged = mergePredicates(
parentPredicates,
"charged",
0
);
processModelRecursively(
model.fallBack(),
merged,
resultList,
materialId,
customModelData
);
}
}
}
private Map<String, Object> mergePredicates(
Map<String, Object> existing,
String newKey,
Number newValue
) {
Map<String, Object> merged = new LinkedHashMap<>(existing);
if (newKey == null) return merged;
merged.put(newKey, newValue);
return merged;
}
@SuppressWarnings("unchecked")
private void registerAllVanillaItems() {
try {
for (Material material : Registry.MATERIAL) {
if (material.getKey().namespace().equals("minecraft")) {
if (!material.isLegacy() && material.isItem()) {
Key id = Key.from(material.getKey().asString());
VANILLA_ITEMS.add(id);
Holder.Reference<Key> holder = BuiltInRegistries.OPTIMIZED_ITEM_ID.get(id)
.orElseGet(() -> ((WritableRegistry<Key>) BuiltInRegistries.OPTIMIZED_ITEM_ID)
.register(new ResourceKey<>(BuiltInRegistries.OPTIMIZED_ITEM_ID.key().location(), id), id));
Object resourceLocation = Reflections.method$ResourceLocation$fromNamespaceAndPath.invoke(null, id.namespace(), id.value());
Object mcHolder = ((Optional<Object>) Reflections.method$Registry$getHolder1.invoke(Reflections.instance$BuiltInRegistries$ITEM, Reflections.method$ResourceKey$create.invoke(null, Reflections.instance$Registries$ITEM, resourceLocation))).get();
Set<Object> tags = (Set<Object>) Reflections.field$Holder$Reference$tags.get(mcHolder);
for (Object tag : tags) {
Key tagId = Key.of(Reflections.field$TagKey$location.get(tag).toString());
this.vanillaItemTags.computeIfAbsent(tagId, (key) -> new ArrayList<>()).add(holder);
}
}
}
}
} catch (ReflectiveOperationException e) {
plugin.logger().warn("Failed to init vanilla items", e);
}
}
}

View File

@@ -0,0 +1,28 @@
package net.momirealms.craftengine.bukkit.item;
import net.momirealms.craftengine.core.item.BuildableItem;
import net.momirealms.craftengine.core.item.ItemBuildContext;
import net.momirealms.craftengine.core.util.Key;
import org.bukkit.inventory.ItemStack;
public class CloneableConstantItem implements BuildableItem<ItemStack> {
private final ItemStack item;
private final Key id;
public CloneableConstantItem(Key id, ItemStack item) {
this.item = item;
this.id = id;
}
@Override
public Key id() {
return this.id;
}
@Override
public ItemStack buildItemStack(ItemBuildContext context, int count) {
ItemStack itemStack = this.item.clone();
itemStack.setAmount(count);
return itemStack;
}
}

View File

@@ -0,0 +1,127 @@
package net.momirealms.craftengine.bukkit.item;
import net.kyori.adventure.text.Component;
import net.momirealms.craftengine.bukkit.api.CraftEngineBlocks;
import net.momirealms.craftengine.bukkit.block.BukkitBlockManager;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.bukkit.plugin.user.BukkitServerPlayer;
import net.momirealms.craftengine.bukkit.util.BlockStateUtils;
import net.momirealms.craftengine.bukkit.util.ComponentUtils;
import net.momirealms.craftengine.bukkit.util.Reflections;
import net.momirealms.craftengine.core.block.CustomBlock;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.block.UpdateOption;
import net.momirealms.craftengine.core.block.properties.Property;
import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.util.MCUtils;
import net.momirealms.craftengine.core.util.MiscUtils;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import javax.annotation.Nullable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
public class DebugStickListener implements Listener {
private final BukkitCraftEngine plugin;
public DebugStickListener(BukkitCraftEngine plugin) {
this.plugin = plugin;
}
@EventHandler(ignoreCancelled = true)
public void onUseDebugStick(PlayerInteractEvent event) {
Block clickedBlock = event.getClickedBlock();
if (clickedBlock == null) return;
ItemStack itemInHand = event.getItem();
if (itemInHand == null) return;
Material material = itemInHand.getType();
if (material != Material.DEBUG_STICK) return;
Player bukkitPlayer = event.getPlayer();
BukkitServerPlayer player = this.plugin.adapt(bukkitPlayer);
if (!(player.canInstabuild() && player.hasPermission("minecraft.debugstick")) && !player.hasPermission("minecraft.debugstick.always")) {
return;
}
if (event.getHand() == EquipmentSlot.OFF_HAND) {
int currentTicks = player.gameTicks();
if (!player.updateLastSuccessfulInteractionTick(currentTicks)) {
event.setCancelled(true);
return;
}
}
Object blockState = BlockStateUtils.blockDataToBlockState(clickedBlock.getBlockData());
int stateId = BlockStateUtils.blockStateToId(blockState);
if (!BlockStateUtils.isVanillaBlock(stateId)) {
event.setCancelled(true);
boolean update = event.getAction() == Action.RIGHT_CLICK_BLOCK;
ImmutableBlockState clickedCustomBlock = BukkitBlockManager.instance().getImmutableBlockStateUnsafe(stateId);
CustomBlock block = clickedCustomBlock.owner().value();
Collection<Property<?>> properties = block.properties();
String blockId = block.id().toString();
try {
if (properties.isEmpty()) {
Object systemChatPacket = Reflections.constructor$ClientboundSystemChatPacket.newInstance(
ComponentUtils.adventureToMinecraft(Component.translatable("item.minecraft.debug_stick.empty").arguments(Component.text(blockId))), true);
player.sendPacket(systemChatPacket, false);
} else {
Item<ItemStack> wrapped = BukkitItemManager.instance().wrap(itemInHand);
Object storedData = wrapped.getTag("craftengine:debug_stick_state");
if (storedData == null) storedData = new HashMap<>();
if (storedData instanceof Map<?,?> map) {
Map<String, Object> data = MiscUtils.castToMap(map, false);
String currentPropertyName = (String) data.get(blockId);
Property<?> currentProperty = block.getProperty(currentPropertyName);
if (currentProperty == null) {
currentProperty = properties.iterator().next();
}
if (update) {
ImmutableBlockState nextState = cycleState(clickedCustomBlock, currentProperty, player.isSecondaryUseActive());
CraftEngineBlocks.place(clickedBlock.getLocation(), nextState, new UpdateOption.Builder().updateClients().updateKnownShape().build(), false);
Object systemChatPacket = Reflections.constructor$ClientboundSystemChatPacket.newInstance(
ComponentUtils.adventureToMinecraft(Component.translatable("item.minecraft.debug_stick.update")
.arguments(
Component.text(currentProperty.name()),
Component.text(getNameHelper(nextState, currentProperty))
)), true);
player.sendPacket(systemChatPacket, false);
} else {
currentProperty = getRelative(properties, currentProperty, player.isSecondaryUseActive());
data.put(blockId, currentProperty.name());
wrapped.setTag(data, "craftengine:debug_stick_state");
wrapped.load();
Object systemChatPacket = Reflections.constructor$ClientboundSystemChatPacket.newInstance(
ComponentUtils.adventureToMinecraft(Component.translatable("item.minecraft.debug_stick.select")
.arguments(
Component.text(currentProperty.name()),
Component.text(getNameHelper(clickedCustomBlock, currentProperty))
)), true);
player.sendPacket(systemChatPacket, false);
}
}
}
} catch (ReflectiveOperationException e) {
plugin.logger().warn("Failed to send system chat packet", e);
}
}
}
private static <T extends Comparable<T>> ImmutableBlockState cycleState(ImmutableBlockState state, Property<T> property, boolean inverse) {
return state.with(property, getRelative(property.possibleValues(), state.get(property), inverse));
}
private static <T> T getRelative(Iterable<T> elements, @Nullable T current, boolean inverse) {
return inverse ? MCUtils.findPreviousInIterable(elements, current) : MCUtils.findNextInIterable(elements, current);
}
private static <T extends Comparable<T>> String getNameHelper(ImmutableBlockState state, Property<T> property) {
return property.valueName(state.get(property));
}
}

View File

@@ -0,0 +1,224 @@
package net.momirealms.craftengine.bukkit.item;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.momirealms.craftengine.bukkit.api.event.CustomBlockInteractEvent;
import net.momirealms.craftengine.bukkit.block.BukkitBlockManager;
import net.momirealms.craftengine.bukkit.item.behavior.BlockItemBehavior;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.bukkit.plugin.user.BukkitServerPlayer;
import net.momirealms.craftengine.bukkit.util.*;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.entity.player.InteractionHand;
import net.momirealms.craftengine.core.entity.player.InteractionResult;
import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.item.behavior.ItemBehavior;
import net.momirealms.craftengine.core.item.context.UseOnContext;
import net.momirealms.craftengine.core.util.Direction;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.world.BlockHitResult;
import net.momirealms.craftengine.core.world.BlockPos;
import net.momirealms.craftengine.core.world.Vec3d;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
public class ItemEventListener implements Listener {
private final BukkitCraftEngine plugin;
public ItemEventListener(BukkitCraftEngine plugin) {
this.plugin = plugin;
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH)
public void onInteractBlock(PlayerInteractEvent event) {
Action action = event.getAction();
if (action != Action.LEFT_CLICK_BLOCK && action != Action.RIGHT_CLICK_BLOCK) {
return;
}
Block block = Objects.requireNonNull(event.getClickedBlock());
Object blockState = BlockStateUtils.blockDataToBlockState(block.getBlockData());
int stateId = BlockStateUtils.blockStateToId(blockState);
if (BlockStateUtils.isVanillaBlock(stateId)) {
return;
}
// it's breaking the block
if (action == Action.LEFT_CLICK_BLOCK && event.getPlayer().getGameMode() == GameMode.CREATIVE) {
return;
}
CustomBlockInteractEvent interactEvent = new CustomBlockInteractEvent(
event.getPlayer(),
block.getLocation(),
event.getInteractionPoint(),
BukkitBlockManager.instance().getImmutableBlockStateUnsafe(stateId),
block,
event.getBlockFace(),
event.getHand() == EquipmentSlot.HAND ? InteractionHand.MAIN_HAND : InteractionHand.OFF_HAND,
action == Action.RIGHT_CLICK_BLOCK ? CustomBlockInteractEvent.Action.RIGHT_CLICK : CustomBlockInteractEvent.Action.LEFT_CLICK
);
if (EventUtils.fireAndCheckCancel(interactEvent)) {
event.setCancelled(true);
}
}
@EventHandler
public void onInteractAir(PlayerInteractEvent event) {
if (event.getAction() != Action.RIGHT_CLICK_AIR) return;
Player bukkitPlayer = event.getPlayer();
BukkitServerPlayer player = this.plugin.adapt(bukkitPlayer);
InteractionHand hand = event.getHand() == EquipmentSlot.HAND ? InteractionHand.MAIN_HAND : InteractionHand.OFF_HAND;
if (cancelEventIfHasInteraction(event, player, hand)) {
return;
}
if (player.isSpectatorMode() || player.isAdventureMode()) {
return;
}
// Gets the item in hand
Item<ItemStack> itemInHand = player.getItemInHand(hand);
// should never be null
if (itemInHand == null) return;
Optional<List<ItemBehavior>> optionalItemBehaviors = itemInHand.getItemBehavior();
if (optionalItemBehaviors.isPresent()) {
for (ItemBehavior itemBehavior : optionalItemBehaviors.get()) {
InteractionResult result = itemBehavior.use(player.level(), player, hand);
if (result == InteractionResult.SUCCESS_AND_CANCEL) {
event.setCancelled(true);
return;
}
if (result != InteractionResult.PASS) {
return;
}
}
}
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onInteractAtBlock(PlayerInteractEvent event) {
if (event.getAction() != Action.RIGHT_CLICK_BLOCK) return;
if (event.useItemInHand() == Event.Result.DENY || event.useInteractedBlock() == Event.Result.DENY) return;
Location interactionPoint = event.getInteractionPoint();
if (interactionPoint == null) return;
Player bukkitPlayer = event.getPlayer();
Block clickedBlock = Objects.requireNonNull(event.getClickedBlock());
BukkitServerPlayer player = this.plugin.adapt(bukkitPlayer);
InteractionHand hand = event.getHand() == EquipmentSlot.HAND ? InteractionHand.MAIN_HAND : InteractionHand.OFF_HAND;
if (cancelEventIfHasInteraction(event, player, hand)) {
return;
}
// Gets the item in hand
Item<ItemStack> itemInHand = player.getItemInHand(hand);
if (itemInHand == null) return;
Optional<List<ItemBehavior>> optionalItemBehaviors = itemInHand.getItemBehavior();
// has custom item behavior
if (optionalItemBehaviors.isPresent()) {
BlockPos pos = LocationUtils.toBlockPos(clickedBlock.getLocation());
Vec3d vec3d = new Vec3d(interactionPoint.getX(), interactionPoint.getY(), interactionPoint.getZ());
Direction direction = DirectionUtils.toDirection(event.getBlockFace());
BlockHitResult hitResult = new BlockHitResult(vec3d, direction, pos, false);
boolean interactable = InteractUtils.isInteractable(BlockStateUtils.getBlockOwnerId(clickedBlock), bukkitPlayer, clickedBlock.getBlockData(), hitResult, itemInHand);
// do not allow to place block if it's a vanilla block
if (itemInHand.isBlockItem() && itemInHand.isCustomItem()) {
if (!interactable || player.isSecondaryUseActive()) {
event.setCancelled(true);
}
}
if (!player.isSecondaryUseActive() && interactable) {
// if it's interactable on server, cancel the custom behaviors
return;
}
// TODO We need to further investigate how to handle adventure mode
// no spectator interactions
if (player.isSpectatorMode() || player.isAdventureMode()) {
return;
}
for (ItemBehavior itemBehavior : optionalItemBehaviors.get()) {
InteractionResult result = itemBehavior.useOnBlock(new UseOnContext(player, hand, hitResult));
if (result == InteractionResult.SUCCESS_AND_CANCEL) {
event.setCancelled(true);
return;
}
int maxY = player.level().worldHeight().getMaxBuildHeight() - 1;
if (direction == Direction.UP
&& result != InteractionResult.SUCCESS
&& pos.y() >= maxY
&& itemBehavior instanceof BlockItemBehavior
) {
player.sendActionBar(Component.translatable("build.tooHigh").arguments(Component.text(maxY)).color(NamedTextColor.RED));
return;
}
if (result != InteractionResult.PASS) {
return;
}
}
return;
}
// it's a vanilla block
if (itemInHand.isBlockItem() && !itemInHand.isCustomItem()) {
// client won't have sounds if the fake block is interactable
// so we should check and resend sounds on interact
Object blockState = BlockStateUtils.blockDataToBlockState(clickedBlock.getBlockData());
int stateId = BlockStateUtils.blockStateToId(blockState);
ImmutableBlockState againCustomBlock = BukkitBlockManager.instance().getImmutableBlockState(stateId);
if (againCustomBlock == null || againCustomBlock.isEmpty()) {
return;
}
BlockPos pos = LocationUtils.toBlockPos(clickedBlock.getLocation());
Vec3d vec3d = new Vec3d(interactionPoint.getX(), interactionPoint.getY(), interactionPoint.getZ());
Direction direction = DirectionUtils.toDirection(event.getBlockFace());
BlockHitResult hitResult = new BlockHitResult(vec3d, direction, pos, false);
try {
BlockData craftBlockData = BlockStateUtils.createBlockData(againCustomBlock.vanillaBlockState().handle());
if (InteractUtils.isInteractable(Key.of(clickedBlock.getType().getKey().asString()), bukkitPlayer, craftBlockData, hitResult, itemInHand)) {
if (!player.isSecondaryUseActive()) {
player.setResendSound();
}
} else {
if (BlockStateUtils.isReplaceable(againCustomBlock.customBlockState().handle()) && !BlockStateUtils.isReplaceable(againCustomBlock.vanillaBlockState().handle())) {
player.setResendSwing();
}
}
} catch (ReflectiveOperationException e) {
plugin.logger().warn("Failed to get CraftBlockData", e);
}
}
}
private boolean cancelEventIfHasInteraction(PlayerInteractEvent event, BukkitServerPlayer player, InteractionHand hand) {
if (hand == InteractionHand.OFF_HAND) {
int currentTicks = player.gameTicks();
// The client will send multiple packets to the server if the client thinks it should
// However, if the main hand item interaction is successful, the off-hand item should be blocked.
if (!player.updateLastSuccessfulInteractionTick(currentTicks)) {
event.setCancelled(true);
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,113 @@
package net.momirealms.craftengine.bukkit.item;
import com.saicone.rtag.RtagItem;
import net.momirealms.craftengine.core.item.ItemWrapper;
import org.bukkit.inventory.ItemStack;
@SuppressWarnings("UnstableApiUsage")
public class RTagItemWrapper implements ItemWrapper<ItemStack> {
private final RtagItem rtagItem;
private int count;
public RTagItemWrapper(RtagItem rtagItem, int count) {
this.rtagItem = rtagItem;
this.count = count;
}
@Override
public ItemStack getItem() {
ItemStack itemStack = this.rtagItem.getItem();
itemStack.setAmount(this.count);
return itemStack;
}
@Override
public boolean set(Object value, Object... path) {
return this.rtagItem.set(value, path);
}
@Override
public boolean add(Object value, Object... path) {
return this.rtagItem.add(value, path);
}
@Override
public <V> V get(Object... path) {
return this.rtagItem.get(path);
}
@Override
public void setComponent(Object path, Object value) {
this.rtagItem.setComponent(path, value);
}
@Override
public Object getComponent(Object path) {
return this.rtagItem.getComponent(path);
}
@Override
public int count() {
return this.count;
}
@Override
public void count(int amount) {
if (amount < 0) amount = 0;
this.count = amount;
}
@Override
public <V> V getExact(Object... path) {
return this.rtagItem.get(path);
}
@Override
public void removeComponent(Object path) {
this.rtagItem.removeComponent(path);
}
@Override
public boolean hasComponent(Object path) {
return this.rtagItem.hasComponent(path);
}
@Override
public void update() {
this.rtagItem.update();
}
@Override
public boolean remove(Object... path) {
return this.rtagItem.remove(path);
}
@Override
public boolean hasTag(Object... path) {
return this.rtagItem.hasTag(path);
}
@Override
public ItemStack load() {
ItemStack itemStack = this.rtagItem.load();
itemStack.setAmount(this.count);
return itemStack;
}
@Override
public ItemStack loadCopy() {
ItemStack itemStack = this.rtagItem.loadCopy();
itemStack.setAmount(this.count);
return itemStack;
}
@Override
public Object getLiteralObject() {
return this.rtagItem.getLiteralObject();
}
@Override
public ItemWrapper<ItemStack> copyWithCount(int count) {
return new RTagItemWrapper(new RtagItem(this.rtagItem.loadCopy()), count);
}
}

View File

@@ -0,0 +1,118 @@
package net.momirealms.craftengine.bukkit.item.behavior;
import net.momirealms.craftengine.bukkit.api.CraftEngineBlocks;
import net.momirealms.craftengine.bukkit.block.BukkitBlockManager;
import net.momirealms.craftengine.bukkit.block.behavior.StrippableBlockBehavior;
import net.momirealms.craftengine.bukkit.util.*;
import net.momirealms.craftengine.bukkit.world.BukkitWorldBlock;
import net.momirealms.craftengine.core.block.CustomBlock;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.block.UpdateOption;
import net.momirealms.craftengine.core.entity.player.InteractionHand;
import net.momirealms.craftengine.core.entity.player.InteractionResult;
import net.momirealms.craftengine.core.entity.player.Player;
import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.item.ItemKeys;
import net.momirealms.craftengine.core.item.behavior.ItemBehavior;
import net.momirealms.craftengine.core.item.behavior.ItemBehaviorFactory;
import net.momirealms.craftengine.core.item.context.UseOnContext;
import net.momirealms.craftengine.core.pack.Pack;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.VersionHelper;
import net.momirealms.craftengine.core.world.BlockPos;
import net.momirealms.craftengine.core.world.Vec3d;
import net.momirealms.sparrow.nbt.CompoundTag;
import org.bukkit.GameEvent;
import org.bukkit.Material;
import org.bukkit.Statistic;
import org.bukkit.block.Block;
import org.bukkit.event.entity.EntityChangeBlockEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.Vector;
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
public class AxeItemBehavior extends ItemBehavior {
public static final Factory FACTORY = new Factory();
public static final AxeItemBehavior INSTANCE = new AxeItemBehavior();
private static final Key AXE_STRIP_SOUND = Key.of("minecraft:item.axe.strip");
@SuppressWarnings("unchecked")
@Override
public InteractionResult useOnBlock(UseOnContext context) {
BukkitWorldBlock clicked = (BukkitWorldBlock) context.getLevel().getBlockAt(context.getClickedPos());
Block block = clicked.block();
ImmutableBlockState state = BukkitBlockManager.instance().getImmutableBlockState(BlockStateUtils.blockDataToId(block.getBlockData()));
if (state == null || state.isEmpty()) return InteractionResult.PASS;
if (!(state.behavior() instanceof StrippableBlockBehavior blockBehavior)) {
return InteractionResult.PASS;
}
Player player = context.getPlayer();
Item<ItemStack> offHandItem = (Item<ItemStack>) player.getItemInHand(InteractionHand.OFF_HAND);
// is using a shield
if (context.getHand() == InteractionHand.MAIN_HAND && offHandItem != null && offHandItem.vanillaId().equals(ItemKeys.SHIELD) && !player.isSecondaryUseActive()) {
return InteractionResult.PASS;
}
Optional<CustomBlock> optionalNewCustomBlock = BukkitBlockManager.instance().getBlock(blockBehavior.stripped());
if (optionalNewCustomBlock.isEmpty()) {
CraftEngine.instance().logger().warn("stripped block " + blockBehavior.stripped() + " does not exist");
return InteractionResult.FAIL;
}
CustomBlock newCustomBlock = optionalNewCustomBlock.get();
CompoundTag compoundTag = state.propertiesNbt();
ImmutableBlockState newState = newCustomBlock.getBlockState(compoundTag);
org.bukkit.entity.Player bukkitPlayer = ((org.bukkit.entity.Player) player.platformPlayer());
// Call bukkit event
EntityChangeBlockEvent event = new EntityChangeBlockEvent(bukkitPlayer, block, BlockStateUtils.createBlockData(newState.customBlockState().handle()));
if (EventUtils.fireAndCheckCancel(event)) {
return InteractionResult.PASS;
}
BlockPos pos = context.getClickedPos();
context.getLevel().playBlockSound(Vec3d.atCenterOf(pos), AXE_STRIP_SOUND, 1, 1);
CraftEngineBlocks.place(block.getLocation(), newState, UpdateOption.UPDATE_ALL_IMMEDIATE, false);
block.getWorld().sendGameEvent(bukkitPlayer, GameEvent.BLOCK_CHANGE, new Vector(pos.x(), pos.y(), pos.z()));
Item<ItemStack> item = (Item<ItemStack>) context.getItem();
Material material = MaterialUtils.getMaterial(item.vanillaId());
bukkitPlayer.setStatistic(Statistic.USE_ITEM, material, bukkitPlayer.getStatistic(Statistic.USE_ITEM, material) + 1);
// resend swing if it's not interactable on client side
if (!InteractUtils.isInteractable(BlockStateUtils.getBlockOwnerIdFromState(state.vanillaBlockState().handle()),
bukkitPlayer, BlockStateUtils.fromBlockData(state.vanillaBlockState().handle()),
context.getHitResult(), item
)) {
player.swingHand(context.getHand());
}
// shrink item amount
if (VersionHelper.isVersionNewerThan1_20_5()) {
Object itemStack = item.getLiteralObject();
Object serverPlayer = player.serverPlayer();
Object equipmentSlot = context.getHand() == InteractionHand.MAIN_HAND ? Reflections.instance$EquipmentSlot$MAINHAND : Reflections.instance$EquipmentSlot$OFFHAND;
try {
Reflections.method$ItemStack$hurtAndBreak.invoke(itemStack, 1, serverPlayer, equipmentSlot);
} catch (ReflectiveOperationException e) {
CraftEngine.instance().logger().warn("Failed to hurt itemStack", e);
}
} else {
ItemStack itemStack = item.getItem();
itemStack.damage(1, bukkitPlayer);
}
return InteractionResult.SUCCESS;
}
public static class Factory implements ItemBehaviorFactory {
@Override
public ItemBehavior create(Pack pack, Path path, Key id, Map<String, Object> arguments) {
return INSTANCE;
}
}
}

View File

@@ -0,0 +1,186 @@
package net.momirealms.craftengine.bukkit.item.behavior;
import net.momirealms.craftengine.bukkit.api.CraftEngineBlocks;
import net.momirealms.craftengine.bukkit.api.event.CustomBlockAttemptPlaceEvent;
import net.momirealms.craftengine.bukkit.api.event.CustomBlockPlaceEvent;
import net.momirealms.craftengine.bukkit.block.BukkitBlockManager;
import net.momirealms.craftengine.bukkit.util.DirectionUtils;
import net.momirealms.craftengine.bukkit.util.EventUtils;
import net.momirealms.craftengine.bukkit.util.LocationUtils;
import net.momirealms.craftengine.bukkit.util.Reflections;
import net.momirealms.craftengine.core.block.CustomBlock;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.block.UpdateOption;
import net.momirealms.craftengine.core.entity.player.InteractionHand;
import net.momirealms.craftengine.core.entity.player.InteractionResult;
import net.momirealms.craftengine.core.entity.player.Player;
import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.item.behavior.ItemBehavior;
import net.momirealms.craftengine.core.item.behavior.ItemBehaviorFactory;
import net.momirealms.craftengine.core.item.context.BlockPlaceContext;
import net.momirealms.craftengine.core.item.context.UseOnContext;
import net.momirealms.craftengine.core.pack.Pack;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.MiscUtils;
import net.momirealms.craftengine.core.world.BlockPos;
import org.bukkit.*;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.block.data.BlockData;
import org.bukkit.event.block.BlockCanBuildEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.Vector;
import javax.annotation.Nullable;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
public class BlockItemBehavior extends ItemBehavior {
public static final Factory FACTORY = new Factory();
private final Key blockId;
public BlockItemBehavior(Key blockId) {
this.blockId = blockId;
}
@Override
public InteractionResult useOnBlock(UseOnContext context) {
return this.place(new BlockPlaceContext(context));
}
public InteractionResult place(BlockPlaceContext context) {
if (!context.canPlace()) {
return InteractionResult.FAIL;
}
Optional<CustomBlock> optionalBlock = BukkitBlockManager.instance().getBlock(this.blockId);
if (optionalBlock.isEmpty()) {
CraftEngine.instance().logger().warn("Failed to place unknown block " + this.blockId);
return InteractionResult.FAIL;
}
CustomBlock block = optionalBlock.get();
BlockPlaceContext placeContext = updatePlacementContext(context);
if (placeContext == null) {
return InteractionResult.FAIL;
}
ImmutableBlockState blockStateToPlace = getPlacementState(placeContext, block);
if (blockStateToPlace == null) {
return InteractionResult.FAIL;
}
Player player = placeContext.getPlayer();
int gameTicks = player.gameTicks();
if (!player.updateLastSuccessfulInteractionTick(gameTicks)) {
return InteractionResult.FAIL;
}
BlockPos pos = placeContext.getClickedPos();
BlockPos againstPos = placeContext.getAgainstPos();
World world = (World) placeContext.getLevel().getHandle();
Location placeLocation = new Location(world, pos.x(), pos.y(), pos.z());
Block bukkitBlock = world.getBlockAt(placeLocation);
Block againstBlock = world.getBlockAt(againstPos.x(), againstPos.y(), againstPos.z());
org.bukkit.entity.Player bukkitPlayer = (org.bukkit.entity.Player) player.platformPlayer();
// trigger event
CustomBlockAttemptPlaceEvent attemptPlaceEvent = new CustomBlockAttemptPlaceEvent(bukkitPlayer, placeLocation.clone(), blockStateToPlace,
DirectionUtils.toBlockFace(context.getClickedFace()), bukkitBlock, context.getHand());
if (EventUtils.fireAndCheckCancel(attemptPlaceEvent)) {
return InteractionResult.FAIL;
}
// it's just world + pos
BlockState previousState = bukkitBlock.getState();
// place custom block
CraftEngineBlocks.place(placeLocation, blockStateToPlace, UpdateOption.UPDATE_ALL_IMMEDIATE, false);
// call bukkit event
BlockPlaceEvent bukkitPlaceEvent = new BlockPlaceEvent(bukkitBlock, previousState, againstBlock, (ItemStack) placeContext.getItem().getItem(), bukkitPlayer, true, context.getHand() == InteractionHand.MAIN_HAND ? EquipmentSlot.HAND : EquipmentSlot.OFF_HAND);
if (EventUtils.fireAndCheckCancel(bukkitPlaceEvent)) {
// revert changes
previousState.update(true, false);
return InteractionResult.FAIL;
}
// call custom event
CustomBlockPlaceEvent customPlaceEvent = new CustomBlockPlaceEvent(bukkitPlayer, placeLocation.clone(), blockStateToPlace, world.getBlockAt(placeLocation), context.getHand());
if (EventUtils.fireAndCheckCancel(customPlaceEvent)) {
// revert changes
previousState.update(true, false);
return InteractionResult.FAIL;
}
if (!player.isCreativeMode()) {
Item<?> item = placeContext.getItem();
item.count(item.count() - 1);
item.load();
}
player.swingHand(placeContext.getHand());
world.playSound(new Location(world, pos.x(), pos.y(), pos.z()), blockStateToPlace.sounds().placeSound().toString(), SoundCategory.BLOCKS, 1f, 0.8f);
world.sendGameEvent(bukkitPlayer, GameEvent.BLOCK_PLACE, new Vector(pos.x(), pos.y(), pos.z()));
return InteractionResult.SUCCESS;
}
@Nullable
public BlockPlaceContext updatePlacementContext(BlockPlaceContext context) {
return context;
}
protected ImmutableBlockState getPlacementState(BlockPlaceContext context, CustomBlock block) {
ImmutableBlockState state = block.getStateForPlacement(context);
return state != null && this.canPlace(context, state) ? state : null;
}
protected boolean checkStatePlacement() {
return true;
}
protected boolean canPlace(BlockPlaceContext context, ImmutableBlockState state) {
try {
Object player;
try {
player = Reflections.method$CraftPlayer$getHandle.invoke(context.getPlayer().platformPlayer());
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException("Failed to get server player", e);
}
Object blockState = state.customBlockState().handle();
Object blockPos = LocationUtils.toBlockPos(context.getClickedPos());
Object voxelShape = Reflections.method$CollisionContext$of.invoke(null, player);
Object world = Reflections.field$CraftWorld$ServerLevel.get(context.getLevel().getHandle());
boolean defaultReturn = ((!this.checkStatePlacement() || (boolean) Reflections.method$BlockStateBase$canSurvive.invoke(blockState, world, blockPos))
&& (boolean) Reflections.method$ServerLevel$checkEntityCollision.invoke(world, blockState, player, voxelShape, blockPos, true));
Block block = (Block) Reflections.method$CraftBlock$at.invoke(null, world, blockPos);
BlockData blockData = (BlockData) Reflections.method$CraftBlockData$fromData.invoke(null, blockState);
BlockCanBuildEvent canBuildEvent = new BlockCanBuildEvent(block, (org.bukkit.entity.Player) context.getPlayer().platformPlayer(), blockData, defaultReturn, context.getHand() == InteractionHand.MAIN_HAND ? EquipmentSlot.HAND : EquipmentSlot.OFF_HAND);
Bukkit.getPluginManager().callEvent(canBuildEvent);
return canBuildEvent.isBuildable();
} catch (ReflectiveOperationException e) {
CraftEngine.instance().logger().warn("Failed to check canPlace", e);
return false;
}
}
public Key blockId() {
return this.blockId;
}
public static class Factory implements ItemBehaviorFactory {
@Override
public ItemBehavior create(Pack pack, Path path, Key key, Map<String, Object> arguments) {
Object id = arguments.get("block");
if (id == null) {
throw new IllegalArgumentException("Missing required parameter 'block' for block_item behavior");
}
if (id instanceof Map<?, ?> map) {
BukkitBlockManager.instance().parseSection(pack, path, key, MiscUtils.castToMap(map, false));
return new BlockItemBehavior(key);
} else {
return new BlockItemBehavior(Key.of(id.toString()));
}
}
}
}

View File

@@ -0,0 +1,27 @@
package net.momirealms.craftengine.bukkit.item.behavior;
import net.momirealms.craftengine.core.entity.player.InteractionResult;
import net.momirealms.craftengine.core.item.behavior.ItemBehavior;
import net.momirealms.craftengine.core.item.behavior.ItemBehaviorFactory;
import net.momirealms.craftengine.core.item.context.UseOnContext;
import net.momirealms.craftengine.core.pack.Pack;
import net.momirealms.craftengine.core.util.Key;
import java.nio.file.Path;
import java.util.Map;
public class BoneMealBehavior extends ItemBehavior {
public static final BoneMealBehavior INSTANCE = new BoneMealBehavior();
@Override
public InteractionResult useOnBlock(UseOnContext context) {
return super.useOnBlock(context);
}
public static class Factory implements ItemBehaviorFactory {
@Override
public ItemBehavior create(Pack pack, Path path, Key id, Map<String, Object> arguments) {
return INSTANCE;
}
}
}

View File

@@ -0,0 +1,71 @@
package net.momirealms.craftengine.bukkit.item.behavior;
import net.momirealms.craftengine.bukkit.api.CraftEngineBlocks;
import net.momirealms.craftengine.bukkit.block.BukkitBlockManager;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.bukkit.util.BlockStateUtils;
import net.momirealms.craftengine.bukkit.world.BukkitWorldBlock;
import net.momirealms.craftengine.core.block.CustomBlock;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.block.UpdateOption;
import net.momirealms.craftengine.core.block.properties.Property;
import net.momirealms.craftengine.core.entity.player.InteractionHand;
import net.momirealms.craftengine.core.entity.player.InteractionResult;
import net.momirealms.craftengine.core.item.behavior.ItemBehavior;
import net.momirealms.craftengine.core.item.behavior.ItemBehaviorFactory;
import net.momirealms.craftengine.core.item.context.UseOnContext;
import net.momirealms.craftengine.core.pack.Pack;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.world.BlockPos;
import org.bukkit.*;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import java.nio.file.Path;
import java.util.Map;
public class BucketItemBehavior extends ItemBehavior {
public static final BucketItemBehavior INSTANCE = new BucketItemBehavior();
public static final Factory FACTORY = new Factory();
private static final Key ITEM_BUCKET_FILL = Key.of("item.bucket.fill");
@SuppressWarnings("unchecked")
@Override
public InteractionResult useOnBlock(UseOnContext context) {
BukkitWorldBlock clicked = (BukkitWorldBlock) context.getLevel().getBlockAt(context.getClickedPos());
Block block = clicked.block();
ImmutableBlockState state = BukkitBlockManager.instance().getImmutableBlockState(BlockStateUtils.blockDataToId(block.getBlockData()));
if (state == null || state.isEmpty()) return InteractionResult.PASS;
CustomBlock customBlock = state.owner().value();
Property<Boolean> waterlogged = (Property<Boolean>) customBlock.getProperty("waterlogged");
if (waterlogged == null) return InteractionResult.PASS;
boolean waterloggedState = state.get(waterlogged);
if (!waterloggedState) return InteractionResult.PASS;
BlockPos pos = context.getClickedPos();
Player player = (Player) context.getPlayer().platformPlayer();
World world = player.getWorld();
Location location = new Location(world, pos.x(), pos.y(), pos.z());
EquipmentSlot slot = context.getHand() == InteractionHand.MAIN_HAND ? EquipmentSlot.HAND : EquipmentSlot.OFF_HAND;
CraftEngineBlocks.place(location, state.with(waterlogged, false), UpdateOption.UPDATE_ALL, false);
if (player.getGameMode() == GameMode.SURVIVAL) {
// to prevent dupe in moment
player.getInventory().setItem(slot, new ItemStack(Material.AIR));
BukkitCraftEngine.instance().scheduler().sync().runDelayed(() ->
player.getInventory().setItem(slot, new ItemStack(Material.WATER_BUCKET)), world, location.getBlockX() >> 4, location.getBlockZ() >> 4);
}
player.setStatistic(Statistic.USE_ITEM, Material.BUCKET, player.getStatistic(Statistic.USE_ITEM, Material.BUCKET) + 1);
// client will assume it has sounds
// context.getPlayer().level().playBlockSound(Vec3d.atCenterOf(context.getClickedPos()), ITEM_BUCKET_FILL, 1, 1);
return InteractionResult.SUCCESS_AND_CANCEL;
}
public static class Factory implements ItemBehaviorFactory {
@Override
public ItemBehavior create(Pack pack, Path path, Key id, Map<String, Object> arguments) {
return INSTANCE;
}
}
}

View File

@@ -0,0 +1,23 @@
package net.momirealms.craftengine.bukkit.item.behavior;
import net.momirealms.craftengine.core.item.behavior.EmptyItemBehavior;
import net.momirealms.craftengine.core.item.behavior.ItemBehaviors;
import net.momirealms.craftengine.core.util.Key;
public class BukkitItemBehaviors extends ItemBehaviors {
public static final Key EMPTY = Key.from("craftengine:empty");
public static final Key BLOCK_ITEM = Key.from("craftengine:block_item");
public static final Key FURNITURE_ITEM = Key.from("craftengine:furniture_item");
public static final Key AXE_ITEM = Key.from("craftengine:axe_item");
public static final Key WATER_BUCKET_ITEM = Key.from("craftengine:water_bucket_item");
public static final Key BUCKET_ITEM = Key.from("craftengine:bucket_item");
public static void init() {
register(EMPTY, (pack, path, args, id) -> EmptyItemBehavior.INSTANCE);
register(BLOCK_ITEM, BlockItemBehavior.FACTORY);
register(FURNITURE_ITEM, FurnitureItemBehavior.FACTORY);
register(AXE_ITEM, AxeItemBehavior.FACTORY);
register(WATER_BUCKET_ITEM, WaterBucketItemBehavior.FACTORY);
register(BUCKET_ITEM, BucketItemBehavior.FACTORY);
}
}

View File

@@ -0,0 +1,150 @@
package net.momirealms.craftengine.bukkit.item.behavior;
import net.momirealms.craftengine.bukkit.api.event.FurnitureAttemptPlaceEvent;
import net.momirealms.craftengine.bukkit.api.event.FurniturePlaceEvent;
import net.momirealms.craftengine.bukkit.entity.furniture.BukkitFurnitureManager;
import net.momirealms.craftengine.bukkit.entity.furniture.LoadedFurniture;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.bukkit.util.DirectionUtils;
import net.momirealms.craftengine.bukkit.util.EventUtils;
import net.momirealms.craftengine.core.entity.furniture.AnchorType;
import net.momirealms.craftengine.core.entity.furniture.CustomFurniture;
import net.momirealms.craftengine.core.entity.player.InteractionResult;
import net.momirealms.craftengine.core.entity.player.Player;
import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.item.behavior.ItemBehavior;
import net.momirealms.craftengine.core.item.behavior.ItemBehaviorFactory;
import net.momirealms.craftengine.core.item.context.UseOnContext;
import net.momirealms.craftengine.core.pack.Pack;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.util.Direction;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.MiscUtils;
import net.momirealms.craftengine.core.util.Pair;
import net.momirealms.craftengine.core.world.Vec3d;
import org.bukkit.Location;
import org.bukkit.SoundCategory;
import org.bukkit.World;
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
public class FurnitureItemBehavior extends ItemBehavior {
public static final Factory FACTORY = new Factory();
private final Key id;
public FurnitureItemBehavior(Key id) {
this.id = id;
}
public Key furnitureId() {
return id;
}
@Override
public InteractionResult useOnBlock(UseOnContext context) {
return this.place(context);
}
public InteractionResult place(UseOnContext context) {
Optional<CustomFurniture> optionalCustomFurniture = BukkitFurnitureManager.instance().getFurniture(this.id);
if (optionalCustomFurniture.isEmpty()) {
CraftEngine.instance().logger().warn("Furniture " + this.id + " not found");
return InteractionResult.FAIL;
}
CustomFurniture customFurniture = optionalCustomFurniture.get();
Direction clickedFace = context.getClickedFace();
AnchorType anchorType = switch (clickedFace) {
case EAST, WEST, NORTH, SOUTH -> AnchorType.WALL;
case UP -> AnchorType.GROUND;
case DOWN -> AnchorType.CEILING;
};
CustomFurniture.Placement placement = customFurniture.getPlacement(anchorType);
if (placement == null) {
return InteractionResult.FAIL;
}
Player player = context.getPlayer();
int gameTicks = player.gameTicks();
if (!player.updateLastSuccessfulInteractionTick(gameTicks)) {
return InteractionResult.FAIL;
}
Vec3d clickedPosition = context.getClickLocation();
// trigger event
org.bukkit.entity.Player bukkitPlayer = (org.bukkit.entity.Player) player.platformPlayer();
World world = (World) context.getLevel().getHandle();
// get position and rotation for placement
Vec3d finalPlacePosition;
double furnitureYaw;
if (anchorType == AnchorType.WALL) {
furnitureYaw = Direction.getYaw(clickedFace);
if (clickedFace == Direction.EAST || clickedFace == Direction.WEST) {
Pair<Double, Double> xz = placement.alignmentRule().apply(Pair.of(clickedPosition.y(), clickedPosition.z()));
finalPlacePosition = new Vec3d(clickedPosition.x(), xz.left(), xz.right());
} else {
Pair<Double, Double> xz = placement.alignmentRule().apply(Pair.of(clickedPosition.x(), clickedPosition.y()));
finalPlacePosition = new Vec3d(xz.left(), xz.right(), clickedPosition.z());
}
} else {
furnitureYaw = placement.rotationRule().apply(180 + player.getXRot());
Pair<Double, Double> xz = placement.alignmentRule().apply(Pair.of(clickedPosition.x(), clickedPosition.z()));
finalPlacePosition = new Vec3d(xz.left(), clickedPosition.y(), xz.right());
}
Location furnitureLocation = new Location(world, finalPlacePosition.x(), finalPlacePosition.y(), finalPlacePosition.z(), (float) furnitureYaw, 0);
if (!BukkitCraftEngine.instance().antiGrief().canPlace(bukkitPlayer, furnitureLocation)) {
return InteractionResult.FAIL;
}
if (!BukkitCraftEngine.instance().antiGrief().canPlace(bukkitPlayer, furnitureLocation)) {
return InteractionResult.FAIL;
}
FurnitureAttemptPlaceEvent attemptPlaceEvent = new FurnitureAttemptPlaceEvent(bukkitPlayer, customFurniture, anchorType, furnitureLocation.clone(),
DirectionUtils.toBlockFace(clickedFace), context.getHand(), world.getBlockAt(context.getClickedPos().x(), context.getClickedPos().y(), context.getClickedPos().z()));
if (EventUtils.fireAndCheckCancel(attemptPlaceEvent)) {
return InteractionResult.FAIL;
}
LoadedFurniture loadedFurniture = BukkitFurnitureManager.instance().place(customFurniture, furnitureLocation.clone(), anchorType, false);
FurniturePlaceEvent placeEvent = new FurniturePlaceEvent(bukkitPlayer, loadedFurniture, furnitureLocation, context.getHand());
if (EventUtils.fireAndCheckCancel(placeEvent)) {
loadedFurniture.destroy();
return InteractionResult.FAIL;
}
if (!player.isCreativeMode()) {
Item<?> item = context.getItem();
item.count(item.count() - 1);
item.load();
}
furnitureLocation.getWorld().playSound(furnitureLocation, customFurniture.settings().sounds().placeSound().toString(), SoundCategory.BLOCKS,1f, 1f);
player.swingHand(context.getHand());
return InteractionResult.SUCCESS;
}
public static class Factory implements ItemBehaviorFactory {
@Override
public ItemBehavior create(Pack pack, Path path, Key key, Map<String, Object> arguments) {
Object id = arguments.get("furniture");
if (id == null) {
throw new IllegalArgumentException("Missing required parameter 'furniture' for furniture_item behavior");
}
if (id instanceof Map<?,?> map) {
BukkitFurnitureManager.instance().parseSection(pack, path, key, MiscUtils.castToMap(map, false));
return new FurnitureItemBehavior(key);
} else {
return new FurnitureItemBehavior(Key.of(id.toString()));
}
}
}
}

View File

@@ -0,0 +1,64 @@
package net.momirealms.craftengine.bukkit.item.behavior;
import net.momirealms.craftengine.bukkit.api.CraftEngineBlocks;
import net.momirealms.craftengine.bukkit.block.BukkitBlockManager;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.bukkit.util.BlockStateUtils;
import net.momirealms.craftengine.bukkit.world.BukkitWorldBlock;
import net.momirealms.craftengine.core.block.CustomBlock;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.block.UpdateOption;
import net.momirealms.craftengine.core.block.properties.Property;
import net.momirealms.craftengine.core.entity.player.InteractionResult;
import net.momirealms.craftengine.core.item.behavior.ItemBehavior;
import net.momirealms.craftengine.core.item.behavior.ItemBehaviorFactory;
import net.momirealms.craftengine.core.item.context.UseOnContext;
import net.momirealms.craftengine.core.pack.Pack;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.world.BlockPos;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import java.nio.file.Path;
import java.util.Map;
public class WaterBucketItemBehavior extends ItemBehavior {
public static final WaterBucketItemBehavior INSTANCE = new WaterBucketItemBehavior();
public static final Factory FACTORY = new Factory();
@SuppressWarnings("unchecked")
@Override
public InteractionResult useOnBlock(UseOnContext context) {
BlockPos pos = context.getClickedPos();
BukkitWorldBlock clicked = (BukkitWorldBlock) context.getLevel().getBlockAt(pos);
Block block = clicked.block();
ImmutableBlockState state = BukkitBlockManager.instance().getImmutableBlockState(BlockStateUtils.blockDataToId(block.getBlockData()));
if (state == null || state.isEmpty()) return InteractionResult.PASS;
CustomBlock customBlock = state.owner().value();
Property<Boolean> waterlogged = (Property<Boolean>) customBlock.getProperty("waterlogged");
if (waterlogged == null) return InteractionResult.PASS;
Player player = (Player) context.getPlayer().platformPlayer();
World world = player.getWorld();
Location location = new Location(world, pos.x(), pos.y(), pos.z());
// TODO Refactor all of this because it's playing a trick with the server
ImmutableBlockState nextState = state.with(waterlogged, true);
block.setBlockData(BlockStateUtils.createBlockData(nextState.vanillaBlockState().handle()), false);
// actually we should broadcast this change
context.getPlayer().sendPacket(BlockStateUtils.createBlockUpdatePacket(pos, state), true);
BukkitCraftEngine.instance().scheduler().sync().runDelayed(() ->
CraftEngineBlocks.place(location, nextState, UpdateOption.UPDATE_ALL, false), world, location.getBlockX() >> 4, location.getBlockZ() >> 4);
return InteractionResult.SUCCESS;
}
public static class Factory implements ItemBehaviorFactory {
@Override
public ItemBehavior create(Pack pack, Path path, Key id, Map<String, Object> arguments) {
return INSTANCE;
}
}
}

View File

@@ -0,0 +1,137 @@
package net.momirealms.craftengine.bukkit.item.factory;
import com.saicone.rtag.RtagItem;
import net.momirealms.craftengine.bukkit.item.RTagItemWrapper;
import net.momirealms.craftengine.bukkit.util.ItemTags;
import net.momirealms.craftengine.bukkit.util.Reflections;
import net.momirealms.craftengine.core.item.ItemFactory;
import net.momirealms.craftengine.core.item.ItemWrapper;
import net.momirealms.craftengine.core.item.modifier.IdModifier;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.util.Key;
import org.bukkit.inventory.ItemStack;
import java.util.Objects;
import java.util.Optional;
public abstract class BukkitItemFactory extends ItemFactory<CraftEngine, RTagItemWrapper, ItemStack> {
protected BukkitItemFactory(CraftEngine plugin) {
super(plugin);
}
public static BukkitItemFactory create(CraftEngine plugin) {
Objects.requireNonNull(plugin, "plugin");
switch (plugin.serverVersion()) {
case "1.20", "1.20.1", "1.20.2", "1.20.3", "1.20.4" -> {
return new UniversalItemFactory(plugin);
}
case "1.20.5", "1.20.6",
"1.21", "1.21.1", "1.21.2", "1.21.3", "1.21.4", "1.21.5",
"1.22", "1.22.1" -> {
return new ComponentItemFactory(plugin);
}
default -> throw new IllegalStateException("Unsupported server version: " + plugin.serverVersion());
}
}
@Override
protected Key id(ItemWrapper<ItemStack> item) {
Object id = item.get(IdModifier.CRAFT_ENGINE_ID);
if (id == null) return Key.of(item.getItem().getType().getKey().asString());
return Key.of(id.toString());
}
@Override
protected Optional<Key> customId(ItemWrapper<ItemStack> item) {
Object id = item.get(IdModifier.CRAFT_ENGINE_ID);
if (id == null) return Optional.empty();
return Optional.of(Key.of(id.toString()));
}
@Override
protected boolean isBlockItem(ItemWrapper<ItemStack> item) {
return item.getItem().getType().isBlock();
}
@Override
protected Key vanillaId(ItemWrapper<ItemStack> item) {
return Key.of(item.getItem().getType().getKey().asString());
}
@Override
protected ItemWrapper<ItemStack> wrapInternal(ItemStack item) {
return new RTagItemWrapper(new RtagItem(item), item.getAmount());
}
@Override
protected void setTag(ItemWrapper<ItemStack> item, Object value, Object... path) {
item.set(value, path);
}
@Override
protected Object getTag(ItemWrapper<ItemStack> item, Object... path) {
return item.get(path);
}
@Override
protected boolean hasTag(ItemWrapper<ItemStack> item, Object... path) {
return item.hasTag(path);
}
@Override
protected boolean removeTag(ItemWrapper<ItemStack> item, Object... path) {
return item.remove(path);
}
@Override
protected void setComponent(ItemWrapper<ItemStack> item, String type, Object value) {
item.setComponent(type, value);
}
@Override
protected Object getComponent(ItemWrapper<ItemStack> item, String type) {
return item.getComponent(type);
}
@Override
protected boolean hasComponent(ItemWrapper<ItemStack> item, String type) {
return item.hasComponent(type);
}
@Override
protected void removeComponent(ItemWrapper<ItemStack> item, String type) {
item.removeComponent(type);
}
@Override
protected void update(ItemWrapper<ItemStack> item) {
item.update();
}
@Override
protected ItemStack load(ItemWrapper<ItemStack> item) {
return item.load();
}
@Override
protected ItemStack getItem(ItemWrapper<ItemStack> item) {
return item.getItem();
}
@Override
protected ItemStack loadCopy(ItemWrapper<ItemStack> item) {
return item.loadCopy();
}
@Override
protected boolean is(ItemWrapper<ItemStack> item, Key itemTag) {
Object literalObject = item.getLiteralObject();
Object tag = ItemTags.getOrCreate(itemTag);
try {
return (boolean) Reflections.method$ItemStack$isTag.invoke(literalObject, tag);
} catch (ReflectiveOperationException e) {
return false;
}
}
}

View File

@@ -0,0 +1,275 @@
package net.momirealms.craftengine.bukkit.item.factory;
import com.saicone.rtag.data.ComponentType;
import net.momirealms.craftengine.bukkit.util.EnchantmentUtils;
import net.momirealms.craftengine.core.item.ComponentKeys;
import net.momirealms.craftengine.core.item.Enchantment;
import net.momirealms.craftengine.core.item.ItemWrapper;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.VersionHelper;
import org.bukkit.inventory.ItemStack;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Function;
@SuppressWarnings("UnstableApiUsage")
public class ComponentItemFactory extends BukkitItemFactory {
private final BiConsumer<ItemWrapper<ItemStack>, Integer> customModelDataSetter;
private final Function<ItemWrapper<ItemStack>, Optional<Integer>> customModelDataGetter;
public ComponentItemFactory(CraftEngine plugin) {
super(plugin);
this.customModelDataSetter = VersionHelper.isVersionNewerThan1_21_4() ?
((item, data) -> item.setComponent(ComponentKeys.CUSTOM_MODEL_DATA,
Map.of("floats", List.of(data.floatValue())))) : ((item, data) -> item.setComponent(ComponentKeys.CUSTOM_MODEL_DATA, data));
this.customModelDataGetter = VersionHelper.isVersionNewerThan1_21_4() ?
(item) -> {
Optional<Object> optional = ComponentType.encodeJava(ComponentKeys.CUSTOM_MODEL_DATA, item.getComponent(ComponentKeys.CUSTOM_MODEL_DATA));
if (optional.isEmpty()) return Optional.empty();
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) optional.get();
@SuppressWarnings("unchecked")
List<Float> floats = (List<Float>) data.get("floats");
if (floats == null || floats.isEmpty()) return Optional.empty();
return Optional.of((int) Math.floor(floats.get(0)));
} : (item) -> Optional.ofNullable(
(Integer) ComponentType.encodeJava(
ComponentKeys.CUSTOM_MODEL_DATA,
item.getComponent(ComponentKeys.CUSTOM_MODEL_DATA)
).orElse(null)
);
}
@Override
protected void customModelData(ItemWrapper<ItemStack> item, Integer data) {
if (data == null) {
item.removeComponent(ComponentKeys.CUSTOM_MODEL_DATA);
} else {
this.customModelDataSetter.accept(item, data);
}
}
@Override
protected Optional<Integer> customModelData(ItemWrapper<ItemStack> item) {
if (!item.hasComponent(ComponentKeys.CUSTOM_MODEL_DATA)) return Optional.empty();
return this.customModelDataGetter.apply(item);
}
@Override
protected void displayName(ItemWrapper<ItemStack> item, String json) {
if (json == null) {
item.removeComponent(ComponentKeys.CUSTOM_NAME);
} else {
item.setComponent(ComponentKeys.CUSTOM_NAME, json);
}
}
@Override
protected Optional<String> displayName(ItemWrapper<ItemStack> item) {
if (!item.hasComponent(ComponentKeys.CUSTOM_NAME)) return Optional.empty();
return Optional.ofNullable(
(String) ComponentType.encodeJava(
ComponentKeys.CUSTOM_NAME,
item.getComponent(ComponentKeys.CUSTOM_NAME)
).orElse(null)
);
}
@Override
protected void itemName(ItemWrapper<ItemStack> item, String json) {
if (json == null) {
item.removeComponent(ComponentKeys.ITEM_NAME);
} else {
item.setComponent(ComponentKeys.ITEM_NAME, json);
}
}
@Override
protected Optional<String> itemName(ItemWrapper<ItemStack> item) {
if (!item.hasComponent(ComponentKeys.ITEM_NAME)) return Optional.empty();
return Optional.ofNullable(
(String) ComponentType.encodeJava(
ComponentKeys.ITEM_NAME,
item.getComponent(ComponentKeys.ITEM_NAME)
).orElse(null)
);
}
@Override
protected void skull(ItemWrapper<ItemStack> item, String skullData) {
final Map<String, Object> profile = Map.of(
"properties", List.of(
Map.of(
"name", "textures",
"value", skullData
)
)
);
item.setComponent("minecraft:profile", profile);
}
@SuppressWarnings("unchecked")
@Override
protected Optional<List<String>> lore(ItemWrapper<ItemStack> item) {
if (!item.hasComponent(ComponentKeys.LORE)) return Optional.empty();
return Optional.ofNullable(
(List<String>) ComponentType.encodeJava(
ComponentKeys.LORE,
item.getComponent(ComponentKeys.LORE)
).orElse(null)
);
}
@Override
protected void lore(ItemWrapper<ItemStack> item, List<String> lore) {
if (lore == null || lore.isEmpty()) {
item.removeComponent(ComponentKeys.LORE);
} else {
item.setComponent(ComponentKeys.LORE, lore);
}
}
@Override
protected boolean unbreakable(ItemWrapper<ItemStack> item) {
return item.hasComponent(ComponentKeys.UNBREAKABLE);
}
@Override
protected void unbreakable(ItemWrapper<ItemStack> item, boolean unbreakable) {
if (unbreakable) {
item.removeComponent(ComponentKeys.UNBREAKABLE);
} else {
item.setComponent(ComponentKeys.UNBREAKABLE, true);
}
}
@Override
protected Optional<Boolean> glint(ItemWrapper<ItemStack> item) {
return Optional.ofNullable((Boolean) item.getComponent(ComponentKeys.ENCHANTMENT_GLINT_OVERRIDE));
}
@Override
protected void glint(ItemWrapper<ItemStack> item, Boolean glint) {
item.setComponent(ComponentKeys.ENCHANTMENT_GLINT_OVERRIDE, glint);
}
@Override
protected Optional<Integer> damage(ItemWrapper<ItemStack> item) {
if (!item.hasComponent(ComponentKeys.DAMAGE)) return Optional.empty();
return Optional.ofNullable(
(Integer) ComponentType.encodeJava(
ComponentKeys.DAMAGE,
item.getComponent(ComponentKeys.DAMAGE)
).orElse(null)
);
}
@Override
protected void damage(ItemWrapper<ItemStack> item, Integer damage) {
if (damage == null) damage = 0;
item.setComponent(ComponentKeys.DAMAGE, damage);
}
@Override
protected Optional<Integer> maxDamage(ItemWrapper<ItemStack> item) {
if (!item.hasComponent(ComponentKeys.MAX_DAMAGE)) return Optional.of((int) item.getItem().getType().getMaxDurability());
return Optional.ofNullable(
(Integer) ComponentType.encodeJava(
ComponentKeys.MAX_DAMAGE,
item.getComponent(ComponentKeys.MAX_DAMAGE)
).orElse(null)
);
}
@Override
protected void maxDamage(ItemWrapper<ItemStack> item, Integer damage) {
if (damage == null) {
item.removeComponent(ComponentKeys.MAX_DAMAGE);
} else {
item.setComponent(ComponentKeys.MAX_DAMAGE, damage);
}
}
@Override
protected Optional<Enchantment> getEnchantment(ItemWrapper<ItemStack> item, Key key) {
Object enchant = item.getComponent(ComponentKeys.ENCHANTMENTS);
try {
Map<String, Integer> map = EnchantmentUtils.toMap(enchant);
Integer level = map.get(key.toString());
if (level == null) return Optional.empty();
return Optional.of(new Enchantment(key, level));
} catch (ReflectiveOperationException e) {
plugin.logger().warn("Failed to get enchantment " + key, e);
return Optional.empty();
}
}
@Override
protected void enchantments(ItemWrapper<ItemStack> item, List<Enchantment> enchantments) {
Map<String, Integer> enchants = new HashMap<>();
for (Enchantment enchantment : enchantments) {
enchants.put(enchantment.id().toString(), enchantment.level());
}
item.setComponent(ComponentKeys.ENCHANTMENTS, enchants);
}
@Override
protected void storedEnchantments(ItemWrapper<ItemStack> item, List<Enchantment> enchantments) {
Map<String, Integer> enchants = new HashMap<>();
for (Enchantment enchantment : enchantments) {
enchants.put(enchantment.id().toString(), enchantment.level());
}
item.setComponent(ComponentKeys.STORED_ENCHANTMENTS, enchants);
}
@Override
protected void addEnchantment(ItemWrapper<ItemStack> item, Enchantment enchantment) {
Object enchant = item.getComponent(ComponentKeys.ENCHANTMENTS);
try {
Map<String, Integer> map = EnchantmentUtils.toMap(enchant);
map.put(enchantment.toString(), enchantment.level());
item.setComponent(ComponentKeys.ENCHANTMENTS, map);
} catch (ReflectiveOperationException e) {
plugin.logger().warn("Failed to add enchantment", e);
}
}
@Override
protected void addStoredEnchantment(ItemWrapper<ItemStack> item, Enchantment enchantment) {
Object enchant = item.getComponent(ComponentKeys.STORED_ENCHANTMENTS);
try {
Map<String, Integer> map = EnchantmentUtils.toMap(enchant);
map.put(enchantment.toString(), enchantment.level());
item.setComponent(ComponentKeys.STORED_ENCHANTMENTS, map);
} catch (ReflectiveOperationException e) {
plugin.logger().warn("Failed to add stored enchantment", e);
}
}
@Override
protected void itemFlags(ItemWrapper<ItemStack> item, List<String> flags) {
throw new UnsupportedOperationException("This feature is not available on 1.20.5+");
}
@Override
protected int maxStackSize(ItemWrapper<ItemStack> item) {
if (!item.hasComponent(ComponentKeys.MAX_STACK_SIZE)) return item.getItem().getType().getMaxStackSize();
return Optional.ofNullable((Integer) ComponentType.encodeJava(ComponentKeys.MAX_STACK_SIZE, item.getComponent(ComponentKeys.MAX_STACK_SIZE)).orElse(null))
.orElse(item.getItem().getType().getMaxStackSize());
}
@Override
protected void maxStackSize(ItemWrapper<ItemStack> item, Integer maxStackSize) {
if (maxStackSize == null) {
item.removeComponent(ComponentKeys.MAX_STACK_SIZE);
} else {
item.setComponent(ComponentKeys.MAX_STACK_SIZE, maxStackSize);
}
}
}

View File

@@ -0,0 +1,221 @@
package net.momirealms.craftengine.bukkit.item.factory;
import com.saicone.rtag.tag.TagBase;
import com.saicone.rtag.tag.TagCompound;
import com.saicone.rtag.tag.TagList;
import net.momirealms.craftengine.core.item.Enchantment;
import net.momirealms.craftengine.core.item.ItemWrapper;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.SkullUtils;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import java.nio.charset.StandardCharsets;
import java.util.*;
public class UniversalItemFactory extends BukkitItemFactory {
public UniversalItemFactory(CraftEngine plugin) {
super(plugin);
}
@Override
protected void displayName(ItemWrapper<ItemStack> item, String json) {
if (json != null) {
item.set(json, "display", "Name");
} else {
item.remove("display", "Name");
}
}
@Override
protected Optional<String> displayName(ItemWrapper<ItemStack> item) {
if (!item.hasTag("display", "Name")) return Optional.empty();
return Optional.of(item.get("display", "Name"));
}
@Override
protected void itemName(ItemWrapper<ItemStack> item, String json) {
throw new UnsupportedOperationException("This feature is only available on 1.20.5+");
}
@Override
protected Optional<String> itemName(ItemWrapper<ItemStack> item) {
throw new UnsupportedOperationException("This feature is only available on 1.20.5+");
}
@Override
protected void customModelData(ItemWrapper<ItemStack> item, Integer data) {
if (data == null) {
item.remove("CustomModelData");
} else {
item.set(data, "CustomModelData");
}
}
@Override
protected Optional<Integer> customModelData(ItemWrapper<ItemStack> item) {
if (!item.hasTag("CustomModelData")) return Optional.empty();
return Optional.of(item.get("CustomModelData"));
}
@Override
protected void skull(ItemWrapper<ItemStack> item, String skullData) {
if (skullData == null) {
item.remove("SkullOwner");
} else {
item.set(UUID.nameUUIDFromBytes(SkullUtils.identifierFromBase64(skullData).getBytes(StandardCharsets.UTF_8)), "SkullOwner", "Id");
item.set(
List.of(Map.of("Value", skullData)),
"SkullOwner", "Properties", "textures"
);
}
}
@Override
protected Optional<List<String>> lore(ItemWrapper<ItemStack> item) {
if (!item.hasTag("display", "Lore")) return Optional.empty();
return Optional.of(item.get("display", "Lore"));
}
@Override
protected void lore(ItemWrapper<ItemStack> item, List<String> lore) {
if (lore == null || lore.isEmpty()) {
item.remove("display", "Lore");
} else {
item.set(lore, "display", "Lore");
}
}
@Override
protected boolean unbreakable(ItemWrapper<ItemStack> item) {
return Optional.ofNullable((Boolean) item.get("Unbreakable")).orElse(false);
}
@Override
protected void unbreakable(ItemWrapper<ItemStack> item, boolean unbreakable) {
item.set(unbreakable, "Unbreakable");
}
@Override
protected Optional<Boolean> glint(ItemWrapper<ItemStack> item) {
return Optional.of(false);
}
@Override
protected void glint(ItemWrapper<ItemStack> item, Boolean glint) {
throw new UnsupportedOperationException("This feature is only available on 1.20.5+");
}
@Override
protected Optional<Integer> damage(ItemWrapper<ItemStack> item) {
if (!item.hasTag("Damage")) return Optional.empty();
return Optional.of(item.get("Damage"));
}
@Override
protected void damage(ItemWrapper<ItemStack> item, Integer damage) {
item.set(damage, "Damage");
}
@Override
protected Optional<Integer> maxDamage(ItemWrapper<ItemStack> item) {
// if (!item.hasTag("CustomFishing", "max_dur")) return Optional.empty();
// return Optional.of(item.get("CustomFishing", "max_dur"));
return Optional.of((int) item.getItem().getType().getMaxDurability());
}
@Override
protected void maxDamage(ItemWrapper<ItemStack> item, Integer damage) {
// if (damage == null) {
// item.remove("CustomFishing", "max_dur");
// } else {
// item.set(damage, "CustomFishing", "max_dur");
// }
throw new UnsupportedOperationException("This feature is only available on 1.20.5+");
}
@Override
protected void enchantments(ItemWrapper<ItemStack> item, List<Enchantment> enchantments) {
ArrayList<Object> tags = new ArrayList<>();
for (Enchantment enchantment : enchantments) {
tags.add((Map.of("id", enchantment.id().toString(), "lvl", (short) enchantment.level())));
}
item.set(tags, "Enchantments");
}
@Override
protected void storedEnchantments(ItemWrapper<ItemStack> item, List<Enchantment> enchantments) {
ArrayList<Object> tags = new ArrayList<>();
for (Enchantment enchantment : enchantments) {
tags.add((Map.of("id", enchantment.id().toString(), "lvl", (short) enchantment.level())));
}
item.set(tags, "StoredEnchantments");
}
@Override
protected void addEnchantment(ItemWrapper<ItemStack> item, Enchantment enchantment) {
Object enchantments = item.getExact("Enchantments");
if (enchantments != null) {
for (Object enchant : TagList.getValue(enchantments)) {
if (TagBase.getValue(TagCompound.get(enchant, "id")).equals(enchant.toString())) {
TagCompound.set(enchant, "lvl", TagBase.newTag(enchantment.level()));
return;
}
}
item.add(Map.of("id", enchantment.toString(), "lvl", (short) enchantment.level()), "Enchantments");
} else {
item.set(List.of(Map.of("id", enchantment.toString(), "lvl", (short) enchantment.level())), "Enchantments");
}
}
@Override
protected void addStoredEnchantment(ItemWrapper<ItemStack> item, Enchantment enchantment) {
Object enchantments = item.getExact("StoredEnchantments");
if (enchantments != null) {
for (Object enchant : TagList.getValue(enchantments)) {
if (TagBase.getValue(TagCompound.get(enchant, "id")).equals(enchant.toString())) {
TagCompound.set(enchant, "lvl", TagBase.newTag(enchantment.level()));
return;
}
}
item.add(Map.of("id", enchantment.toString(), "lvl", (short) enchantment.level()), "StoredEnchantments");
} else {
item.set(List.of(Map.of("id", enchantment.toString(), "lvl", (short) enchantment.level())), "StoredEnchantments");
}
}
@SuppressWarnings("deprecation")
@Override
protected Optional<Enchantment> getEnchantment(ItemWrapper<ItemStack> item, Key key) {
int level = item.getItem().getEnchantmentLevel(Objects.requireNonNull(Registry.ENCHANTMENT.get(new NamespacedKey(key.namespace(), key.value()))));
if (level <= 0) return Optional.empty();
return Optional.of(new Enchantment(key, level));
}
@Override
protected void itemFlags(ItemWrapper<ItemStack> item, List<String> flags) {
if (flags == null || flags.isEmpty()) {
item.remove("HideFlags");
return;
}
int f = 0;
for (String flag : flags) {
ItemFlag itemFlag = ItemFlag.valueOf(flag);
f = f | 1 << itemFlag.ordinal();
}
item.set(f, "HideFlags");
}
@Override
protected int maxStackSize(ItemWrapper<ItemStack> item) {
return item.getItem().getType().getMaxStackSize();
}
@Override
protected void maxStackSize(ItemWrapper<ItemStack> item, Integer maxStackSize) {
}
}

View File

@@ -0,0 +1,892 @@
package net.momirealms.craftengine.bukkit.item.recipe;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.saicone.rtag.item.ItemObject;
import com.saicone.rtag.tag.TagCompound;
import net.momirealms.craftengine.bukkit.item.BukkitItemManager;
import net.momirealms.craftengine.bukkit.item.CloneableConstantItem;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.bukkit.util.MaterialUtils;
import net.momirealms.craftengine.bukkit.util.RecipeUtils;
import net.momirealms.craftengine.bukkit.util.Reflections;
import net.momirealms.craftengine.core.item.CustomItem;
import net.momirealms.craftengine.core.item.ItemBuildContext;
import net.momirealms.craftengine.core.item.recipe.Recipe;
import net.momirealms.craftengine.core.item.recipe.*;
import net.momirealms.craftengine.core.item.recipe.input.RecipeInput;
import net.momirealms.craftengine.core.item.recipe.vanilla.*;
import net.momirealms.craftengine.core.item.recipe.vanilla.reader.VanillaRecipeReader1_20;
import net.momirealms.craftengine.core.item.recipe.vanilla.reader.VanillaRecipeReader1_20_5;
import net.momirealms.craftengine.core.item.recipe.vanilla.reader.VanillaRecipeReader1_21_2;
import net.momirealms.craftengine.core.pack.Pack;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.config.ConfigManager;
import net.momirealms.craftengine.core.registry.BuiltInRegistries;
import net.momirealms.craftengine.core.registry.Holder;
import net.momirealms.craftengine.core.util.HeptaFunction;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.PentaFunction;
import net.momirealms.craftengine.core.util.VersionHelper;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.event.HandlerList;
import org.bukkit.inventory.*;
import org.bukkit.inventory.recipe.CookingBookCategory;
import org.bukkit.inventory.recipe.CraftingBookCategory;
import org.jetbrains.annotations.Nullable;
import java.io.Reader;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
public class BukkitRecipeManager implements RecipeManager<ItemStack> {
private static final Map<Key, BiConsumer<NamespacedKey, Recipe<ItemStack>>> BUKKIT_RECIPE_FACTORIES = new HashMap<>();
private static Object minecraftRecipeManager;
private static final List<Object> injectedIngredients = new ArrayList<>();
private static final IdentityHashMap<Recipe<ItemStack>, Object> recipeToMcRecipeHolder = new IdentityHashMap<>();
private static BukkitRecipeManager instance;
static {
BUKKIT_RECIPE_FACTORIES.put(RecipeTypes.SHAPED, (key, recipe) -> {
CustomShapedRecipe<ItemStack> ceRecipe = (CustomShapedRecipe<ItemStack>) recipe;
ShapedRecipe shapedRecipe = new ShapedRecipe(key, ceRecipe.result(ItemBuildContext.EMPTY));
if (ceRecipe.group() != null) {
shapedRecipe.setGroup(Objects.requireNonNull(ceRecipe.group()));
}
if (ceRecipe.category() != null) {
shapedRecipe.setCategory(CraftingBookCategory.valueOf(Objects.requireNonNull(ceRecipe.category()).name()));
}
shapedRecipe.shape(ceRecipe.pattern().pattern());
for (Map.Entry<Character, Ingredient<ItemStack>> entry : ceRecipe.pattern().ingredients().entrySet()) {
shapedRecipe.setIngredient(entry.getKey(), new RecipeChoice.MaterialChoice(ingredientToBukkitMaterials(entry.getValue())));
}
try {
Object craftRecipe = Reflections.method$CraftShapedRecipe$fromBukkitRecipe.invoke(null, shapedRecipe);
Reflections.method$CraftRecipe$addToCraftingManager.invoke(craftRecipe);
injectShapedRecipe(new Key(key.namespace(), key.value()), ceRecipe);
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to convert shaped recipe", e);
}
});
BUKKIT_RECIPE_FACTORIES.put(RecipeTypes.SHAPELESS, (key, recipe) -> {
CustomShapelessRecipe<ItemStack> ceRecipe = (CustomShapelessRecipe<ItemStack>) recipe;
ShapelessRecipe shapelessRecipe = new ShapelessRecipe(key, ceRecipe.result(ItemBuildContext.EMPTY));
if (ceRecipe.group() != null) {
shapelessRecipe.setGroup(Objects.requireNonNull(ceRecipe.group()));
}
if (ceRecipe.category() != null) {
shapelessRecipe.setCategory(CraftingBookCategory.valueOf(Objects.requireNonNull(ceRecipe.category()).name()));
}
for (Ingredient<ItemStack> ingredient : ceRecipe.ingredients()) {
shapelessRecipe.addIngredient(new RecipeChoice.MaterialChoice(ingredientToBukkitMaterials(ingredient)));
}
try {
Object craftRecipe = Reflections.method$CraftShapelessRecipe$fromBukkitRecipe.invoke(null, shapelessRecipe);
Reflections.method$CraftRecipe$addToCraftingManager.invoke(craftRecipe);
injectShapelessRecipe(new Key(key.namespace(), key.value()), ceRecipe);
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to convert shapeless recipe", e);
}
});
BUKKIT_RECIPE_FACTORIES.put(RecipeTypes.SMELTING, (key, recipe) -> {
CustomSmeltingRecipe<ItemStack> ceRecipe = (CustomSmeltingRecipe<ItemStack>) recipe;
FurnaceRecipe furnaceRecipe = new FurnaceRecipe(
key, ceRecipe.result(ItemBuildContext.EMPTY),
new RecipeChoice.MaterialChoice(ingredientToBukkitMaterials(ceRecipe.ingredient())),
ceRecipe.experience(), ceRecipe.cookingTime()
);
if (ceRecipe.group() != null) {
furnaceRecipe.setGroup(Objects.requireNonNull(ceRecipe.group()));
}
if (ceRecipe.category() != null) {
furnaceRecipe.setCategory(CookingBookCategory.valueOf(Objects.requireNonNull(ceRecipe.category()).name()));
}
try {
Object craftRecipe = Reflections.method$CraftFurnaceRecipe$fromBukkitRecipe.invoke(null, furnaceRecipe);
Reflections.method$CraftRecipe$addToCraftingManager.invoke(craftRecipe);
injectCookingRecipe(new Key(key.namespace(), key.value()), ceRecipe);
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to convert smelting recipe", e);
}
});
BUKKIT_RECIPE_FACTORIES.put(RecipeTypes.SMOKING, (key, recipe) -> {
CustomSmokingRecipe<ItemStack> ceRecipe = (CustomSmokingRecipe<ItemStack>) recipe;
SmokingRecipe smokingRecipe = new SmokingRecipe(
key, ceRecipe.result(ItemBuildContext.EMPTY),
new RecipeChoice.MaterialChoice(ingredientToBukkitMaterials(ceRecipe.ingredient())),
ceRecipe.experience(), ceRecipe.cookingTime()
);
if (ceRecipe.group() != null) {
smokingRecipe.setGroup(Objects.requireNonNull(ceRecipe.group()));
}
if (ceRecipe.category() != null) {
smokingRecipe.setCategory(CookingBookCategory.valueOf(Objects.requireNonNull(ceRecipe.category()).name()));
}
try {
Object craftRecipe = Reflections.method$CraftSmokingRecipe$fromBukkitRecipe.invoke(null, smokingRecipe);
Reflections.method$CraftRecipe$addToCraftingManager.invoke(craftRecipe);
injectCookingRecipe(new Key(key.namespace(), key.value()), ceRecipe);
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to convert smoking recipe", e);
}
});
BUKKIT_RECIPE_FACTORIES.put(RecipeTypes.BLASTING, (key, recipe) -> {
CustomBlastingRecipe<ItemStack> ceRecipe = (CustomBlastingRecipe<ItemStack>) recipe;
BlastingRecipe blastingRecipe = new BlastingRecipe(
key, ceRecipe.result(ItemBuildContext.EMPTY),
new RecipeChoice.MaterialChoice(ingredientToBukkitMaterials(ceRecipe.ingredient())),
ceRecipe.experience(), ceRecipe.cookingTime()
);
if (ceRecipe.group() != null) {
blastingRecipe.setGroup(Objects.requireNonNull(ceRecipe.group()));
}
if (ceRecipe.category() != null) {
blastingRecipe.setCategory(CookingBookCategory.valueOf(Objects.requireNonNull(ceRecipe.category()).name()));
}
try {
Object craftRecipe = Reflections.method$CraftBlastingRecipe$fromBukkitRecipe.invoke(null, blastingRecipe);
Reflections.method$CraftRecipe$addToCraftingManager.invoke(craftRecipe);
injectCookingRecipe(new Key(key.namespace(), key.value()), ceRecipe);
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to convert blasting recipe", e);
}
});
BUKKIT_RECIPE_FACTORIES.put(RecipeTypes.CAMPFIRE_COOKING, (key, recipe) -> {
CustomCampfireRecipe<ItemStack> ceRecipe = (CustomCampfireRecipe<ItemStack>) recipe;
CampfireRecipe campfireRecipe = new CampfireRecipe(
key, ceRecipe.result(ItemBuildContext.EMPTY),
new RecipeChoice.MaterialChoice(ingredientToBukkitMaterials(ceRecipe.ingredient())),
ceRecipe.experience(), ceRecipe.cookingTime()
);
if (ceRecipe.group() != null) {
campfireRecipe.setGroup(Objects.requireNonNull(ceRecipe.group()));
}
if (ceRecipe.category() != null) {
campfireRecipe.setCategory(CookingBookCategory.valueOf(Objects.requireNonNull(ceRecipe.category()).name()));
}
try {
Object craftRecipe = Reflections.method$CraftCampfireRecipe$fromBukkitRecipe.invoke(null, campfireRecipe);
Reflections.method$CraftRecipe$addToCraftingManager.invoke(craftRecipe);
injectCookingRecipe(new Key(key.namespace(), key.value()), ceRecipe);
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to convert campfire recipe", e);
}
});
BUKKIT_RECIPE_FACTORIES.put(RecipeTypes.STONE_CUTTING, (key, recipe) -> {
CustomStoneCuttingRecipe<ItemStack> ceRecipe = (CustomStoneCuttingRecipe<ItemStack>) recipe;
List<ItemStack> itemStacks = new ArrayList<>();
for (Holder<Key> item : ceRecipe.ingredient().items()) {
itemStacks.add(BukkitItemManager.instance().buildItemStack(item.value(), null));
}
StonecuttingRecipe stonecuttingRecipe = new StonecuttingRecipe(
key, ceRecipe.result(ItemBuildContext.EMPTY),
new RecipeChoice.ExactChoice(itemStacks)
);
if (ceRecipe.group() != null) {
stonecuttingRecipe.setGroup(Objects.requireNonNull(ceRecipe.group()));
}
try {
Object craftRecipe = Reflections.method$CraftStonecuttingRecipe$fromBukkitRecipe.invoke(null, stonecuttingRecipe);
Reflections.method$CraftRecipe$addToCraftingManager.invoke(craftRecipe);
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to convert stone cutting recipe", e);
}
});
}
private final BukkitCraftEngine plugin;
private final RecipeEventListener recipeEventListener;
private final CrafterEventListener crafterEventListener;
private final Map<Key, List<Recipe<ItemStack>>> byType;
private final Map<Key, Recipe<ItemStack>> byId;
private final Map<Key, List<Recipe<ItemStack>>> byResult;
private final VanillaRecipeReader recipeReader;
private final List<NamespacedKey> injectedDataPackRecipes;
private final List<NamespacedKey> registeredCustomRecipes;
// data pack recipe resource locations [minecraft:xxx]
private final Set<Key> dataPackRecipes;
private Object stolenFeatureFlagSet;
public BukkitRecipeManager(BukkitCraftEngine plugin) {
instance = this;
this.plugin = plugin;
this.byType = new HashMap<>();
this.byId = new HashMap<>();
this.byResult = new HashMap<>();
this.injectedDataPackRecipes = new ArrayList<>();
this.registeredCustomRecipes = new ArrayList<>();
this.dataPackRecipes = new HashSet<>();
this.recipeEventListener = new RecipeEventListener(plugin, this, plugin.itemManager());
if (VersionHelper.isVersionNewerThan1_21()) {
this.crafterEventListener = new CrafterEventListener(plugin, this, plugin.itemManager());
} else {
this.crafterEventListener = null;
}
if (VersionHelper.isVersionNewerThan1_21_2()) {
this.recipeReader = new VanillaRecipeReader1_21_2();
} else if (VersionHelper.isVersionNewerThan1_20_5()) {
this.recipeReader = new VanillaRecipeReader1_20_5();
} else {
this.recipeReader = new VanillaRecipeReader1_20();
}
try {
minecraftRecipeManager = Reflections.method$MinecraftServer$getRecipeManager.invoke(Reflections.method$MinecraftServer$getServer.invoke(null));
} catch (ReflectiveOperationException e) {
plugin.logger().warn("Failed to get minecraft recipe manager", e);
}
}
@Override
public boolean isDataPackRecipe(Key key) {
return this.dataPackRecipes.contains(key);
}
@Override
public boolean isCustomRecipe(Key key) {
return this.byId.containsKey(key);
}
@Override
public Optional<Recipe<ItemStack>> getRecipeById(Key key) {
return Optional.ofNullable(this.byId.get(key));
}
@Override
public void delayedInit() {
Bukkit.getPluginManager().registerEvents(this.recipeEventListener, this.plugin.bootstrap());
if (this.crafterEventListener != null) {
Bukkit.getPluginManager().registerEvents(this.crafterEventListener, this.plugin.bootstrap());
}
}
@Override
public void load() {
if (!ConfigManager.enableRecipeSystem()) return;
if (VersionHelper.isVersionNewerThan1_21_2()) {
try {
this.stolenFeatureFlagSet = Reflections.field$RecipeManager$featureflagset.get(minecraftRecipeManager);
Reflections.field$RecipeManager$featureflagset.set(minecraftRecipeManager, null);
} catch (ReflectiveOperationException e) {
this.plugin.logger().warn("Failed to steal featureflagset", e);
}
}
}
@Override
public void unload() {
this.byType.clear();
this.byId.clear();
this.byResult.clear();
this.dataPackRecipes.clear();
try {
// do not unregister them
// for (NamespacedKey key : this.injectedDataPackRecipes) {
// unregisterRecipe(key);
// }
for (NamespacedKey key : this.registeredCustomRecipes) {
unregisterRecipe(key);
}
if (VersionHelper.isVersionNewerThan1_21_2()) {
Reflections.method$RecipeManager$finalizeRecipeLoading.invoke(minecraftRecipeManager);
}
} catch (ReflectiveOperationException e) {
plugin.logger().warn("Failed to unregister recipes", e);
}
this.registeredCustomRecipes.clear();
this.injectedDataPackRecipes.clear();
recipeToMcRecipeHolder.clear();
}
@Override
public void disable() {
HandlerList.unregisterAll(this.recipeEventListener);
if (this.crafterEventListener != null) {
HandlerList.unregisterAll(this.crafterEventListener);
}
unload();
}
private void unregisterRecipe(NamespacedKey key) throws ReflectiveOperationException {
if (VersionHelper.isVersionNewerThan1_21_2()) {
Object recipeMap = Reflections.field$RecipeManager$recipes.get(minecraftRecipeManager);
Reflections.method$RecipeMap$removeRecipe.invoke(recipeMap, Reflections.method$CraftRecipe$toMinecraft.invoke(null, key));
} else {
Bukkit.removeRecipe(key);
}
}
@Override
public void parseSection(Pack pack, Path path, Key id, Map<String, Object> section) {
if (!ConfigManager.enableRecipeSystem()) return;
if (this.byId.containsKey(id)) {
this.plugin.logger().warn(path, "Duplicated recipe " + id);
return;
}
Recipe<ItemStack> recipe = RecipeTypes.fromMap(id, section);
NamespacedKey key = NamespacedKey.fromString(id.toString());
BUKKIT_RECIPE_FACTORIES.get(recipe.type()).accept(key, recipe);
try {
this.registeredCustomRecipes.add(key);
this.byType.computeIfAbsent(recipe.type(), k -> new ArrayList<>()).add(recipe);
this.byId.put(id, recipe);
this.byResult.computeIfAbsent(recipe.result().item().id(), k -> new ArrayList<>()).add(recipe);
} catch (Exception e) {
plugin.logger().warn("Failed to add custom recipe " + id, e);
}
}
@Override
public List<Recipe<ItemStack>> getRecipes(Key type) {
return this.byType.getOrDefault(type, List.of());
}
@Override
public List<Recipe<ItemStack>> getRecipeByResult(Key result) {
return this.byResult.getOrDefault(result, List.of());
}
// example: stone button
public void addVanillaInternalRecipe(Key id, Recipe<ItemStack> recipe) {
this.byType.computeIfAbsent(recipe.type(), k -> new ArrayList<>()).add(recipe);
this.byId.put(id, recipe);
this.byResult.computeIfAbsent(recipe.result().item().id(), k -> new ArrayList<>()).add(recipe);
}
@Nullable
@Override
public Recipe<ItemStack> getRecipe(Key type, RecipeInput input) {
List<Recipe<ItemStack>> recipes = this.byType.get(type);
if (recipes == null) return null;
for (Recipe<ItemStack> recipe : recipes) {
if (recipe.matches(input)) {
return recipe;
}
}
return null;
}
@Nullable
@Override
public Recipe<ItemStack> getRecipe(Key type, RecipeInput input, Key lastRecipe) {
if (lastRecipe != null) {
Recipe<ItemStack> last = byId.get(lastRecipe);
if (last != null && last.matches(input)) {
return last;
}
}
return getRecipe(type, input);
}
@Override
public CompletableFuture<Void> delayedLoad() {
if (!ConfigManager.enableRecipeSystem()) return CompletableFuture.completedFuture(null);
return this.processVanillaRecipes().thenRun(() -> {
try {
// give flags back on 1.21.2+
if (VersionHelper.isVersionNewerThan1_21_2() && this.stolenFeatureFlagSet != null) {
Reflections.field$RecipeManager$featureflagset.set(minecraftRecipeManager, this.stolenFeatureFlagSet);
this.stolenFeatureFlagSet = false;
}
// refresh recipes
if (VersionHelper.isVersionNewerThan1_21_2()) {
Reflections.method$RecipeManager$finalizeRecipeLoading.invoke(minecraftRecipeManager);
}
// send to players
Reflections.method$DedicatedPlayerList$reloadRecipes.invoke(Reflections.field$CraftServer$playerList.get(Bukkit.getServer()));
// now we need to remove the fake `exact`
if (VersionHelper.isVersionNewerThan1_21_4()) {
for (Object ingredient : injectedIngredients) {
Reflections.field$Ingredient$itemStacks1_21_4.set(ingredient, null);
}
} else if (VersionHelper.isVersionNewerThan1_21_2()) {
for (Object ingredient : injectedIngredients) {
Reflections.field$Ingredient$itemStacks1_21_2.set(ingredient, null);
}
}
// clear cache
injectedIngredients.clear();
} catch (Exception e) {
this.plugin.logger().warn("Failed to run delayed recipe tasks", e);
}
});
}
@SuppressWarnings("unchecked")
private CompletableFuture<Void> processVanillaRecipes() {
CompletableFuture<Void> future = new CompletableFuture<>();
try {
List<Runnable> injectLogics = new ArrayList<>();
plugin.scheduler().async().execute(() -> {
try {
Object fileToIdConverter = Reflections.method$FileToIdConverter$json.invoke(null, VersionHelper.isVersionNewerThan1_21() ? "recipe" : "recipes");
Object minecraftServer = Reflections.method$MinecraftServer$getServer.invoke(null);
Object packRepository = Reflections.method$MinecraftServer$getPackRepository.invoke(minecraftServer);
List<Object> selected = (List<Object>) Reflections.field$PackRepository$selected.get(packRepository);
List<Object> packResources = new ArrayList<>();
for (Object pack : selected) {
packResources.add(Reflections.method$Pack$open.invoke(pack));
}
try (AutoCloseable resourceManager = (AutoCloseable) Reflections.constructor$MultiPackResourceManager.newInstance(Reflections.instance$PackType$SERVER_DATA, packResources)) {
Map<Object, Object> scannedResources = (Map<Object, Object>) Reflections.method$FileToIdConverter$listMatchingResources.invoke(fileToIdConverter, resourceManager);
for (Map.Entry<Object, Object> entry : scannedResources.entrySet()) {
Key id = extractKeyFromResourceLocation(entry.getKey().toString());
this.dataPackRecipes.add(id);
// Maybe it's unregistered by other plugins
if (Bukkit.getRecipe(new NamespacedKey(id.namespace(), id.value())) == null) {
continue;
}
Reader reader = (Reader) Reflections.method$Resource$openAsReader.invoke(entry.getValue());
JsonObject jsonObject = JsonParser.parseReader(reader).getAsJsonObject();
String type = jsonObject.get("type").getAsString();
switch (type) {
case "minecraft:crafting_shaped" -> {
VanillaShapedRecipe recipe = this.recipeReader.readShaped(jsonObject);
handleDataPackShapedRecipe(id, recipe, (injectLogics::add));
}
case "minecraft:crafting_shapeless" -> {
VanillaShapelessRecipe recipe = this.recipeReader.readShapeless(jsonObject);
handleDataPackShapelessRecipe(id, recipe, (injectLogics::add));
}
case "minecraft:smelting" -> {
VanillaSmeltingRecipe recipe = this.recipeReader.readSmelting(jsonObject);
handleDataPackCookingRecipe(id, recipe, FurnaceRecipe::new, CustomSmeltingRecipe::new, Reflections.method$CraftFurnaceRecipe$fromBukkitRecipe, (injectLogics::add));
}
case "minecraft:blasting" -> {
VanillaBlastingRecipe recipe = this.recipeReader.readBlasting(jsonObject);
handleDataPackCookingRecipe(id, recipe, BlastingRecipe::new, CustomBlastingRecipe::new, Reflections.method$CraftBlastingRecipe$fromBukkitRecipe, (injectLogics::add));
}
case "minecraft:smoking" -> {
VanillaSmokingRecipe recipe = this.recipeReader.readSmoking(jsonObject);
handleDataPackCookingRecipe(id, recipe, SmokingRecipe::new, CustomSmokingRecipe::new, Reflections.method$CraftSmokingRecipe$fromBukkitRecipe, (injectLogics::add));
}
case "minecraft:campfire_cooking" -> {
VanillaCampfireRecipe recipe = this.recipeReader.readCampfire(jsonObject);
handleDataPackCookingRecipe(id, recipe, CampfireRecipe::new, CustomCampfireRecipe::new, Reflections.method$CraftCampfireRecipe$fromBukkitRecipe, (injectLogics::add));
}
case "minecraft:stonecutting" -> {
VanillaStoneCuttingRecipe recipe = this.recipeReader.readStoneCutting(jsonObject);
handleDataPackStoneCuttingRecipe(id, recipe);
}
}
}
}
} catch (Exception e) {
plugin.logger().warn("Failed to read data pack recipes", e);
} finally {
plugin.scheduler().sync().run(() -> {
try {
for (Runnable runnable : injectLogics) {
runnable.run();
}
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to register recipes", e);
} finally {
future.complete(null);
}
});
}
});
} catch (Exception e) {
this.plugin.logger().warn("Failed to inject vanilla recipes", e);
}
return future;
}
private void handleDataPackShapelessRecipe(Key id, VanillaShapelessRecipe recipe, Consumer<Runnable> callback) {
NamespacedKey key = new NamespacedKey(id.namespace(), id.value());
ItemStack result = createResultStack(recipe.result());
ShapelessRecipe shapelessRecipe = new ShapelessRecipe(key, result);
if (recipe.group() != null) {
shapelessRecipe.setGroup(recipe.group());
}
if (recipe.category() != null) {
shapelessRecipe.setCategory(CraftingBookCategory.valueOf(recipe.category().name()));
}
boolean hasCustomItemInTag = false;
List<Ingredient<ItemStack>> ingredientList = new ArrayList<>();
for (List<String> list : recipe.ingredients()) {
Set<Material> materials = new HashSet<>();
Set<Holder<Key>> holders = new HashSet<>();
for (String item : list) {
if (item.charAt(0) == '#') {
Key tag = Key.of(item.substring(1));
materials.addAll(tagToMaterials(tag));
if (!hasCustomItemInTag) {
if (!plugin.itemManager().tagToCustomItems(tag).isEmpty()) {
hasCustomItemInTag = true;
}
}
holders.addAll(plugin.itemManager().tagToItems(tag));
} else {
materials.add(MaterialUtils.getMaterial(item));
holders.add(BuiltInRegistries.OPTIMIZED_ITEM_ID.get(Key.from(item)).orElseThrow());
}
}
shapelessRecipe.addIngredient(new RecipeChoice.MaterialChoice(new ArrayList<>(materials)));
ingredientList.add(Ingredient.of(holders));
}
CustomShapelessRecipe<ItemStack> ceRecipe = new CustomShapelessRecipe<>(
id,
recipe.category(),
recipe.group(),
ingredientList,
new CustomRecipeResult<>(new CloneableConstantItem(recipe.result().isCustom() ? Key.of("!internal:custom") : Key.of(recipe.result().id()), result), recipe.result().count())
);
if (hasCustomItemInTag) {
callback.accept(() -> {
try {
unregisterRecipe(key);
Reflections.method$CraftRecipe$addToCraftingManager.invoke(Reflections.method$CraftShapelessRecipe$fromBukkitRecipe.invoke(null, shapelessRecipe));
injectShapelessRecipe(id, ceRecipe);
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to convert shapeless recipe", e);
}
});
this.injectedDataPackRecipes.add(key);
}
this.addVanillaInternalRecipe(id, ceRecipe);
}
private void handleDataPackStoneCuttingRecipe(Key id, VanillaStoneCuttingRecipe recipe) {
ItemStack result = createResultStack(recipe.result());
Set<Holder<Key>> holders = new HashSet<>();
for (String item : recipe.ingredient()) {
if (item.charAt(0) == '#') {
Key tag = Key.from(item.substring(1));
holders.addAll(plugin.itemManager().tagToItems(tag));
} else {
holders.add(BuiltInRegistries.OPTIMIZED_ITEM_ID.get(Key.from(item)).orElseThrow());
}
}
CustomStoneCuttingRecipe<ItemStack> ceRecipe = new CustomStoneCuttingRecipe<>(
id,
recipe.group(),
Ingredient.of(holders),
new CustomRecipeResult<>(new CloneableConstantItem(recipe.result().isCustom() ? Key.of("!internal:custom") : Key.of(recipe.result().id()), result), recipe.result().count())
);
this.addVanillaInternalRecipe(id, ceRecipe);
}
private void handleDataPackShapedRecipe(Key id, VanillaShapedRecipe recipe, Consumer<Runnable> callback) {
NamespacedKey key = new NamespacedKey(id.namespace(), id.value());
ItemStack result = createResultStack(recipe.result());
ShapedRecipe shapedRecipe = new ShapedRecipe(key, result);
if (recipe.group() != null) {
shapedRecipe.setGroup(recipe.group());
}
if (recipe.category() != null) {
shapedRecipe.setCategory(CraftingBookCategory.valueOf(recipe.category().name()));
}
shapedRecipe.shape(recipe.pattern());
boolean hasCustomItemInTag = false;
Map<Character, Ingredient<ItemStack>> ingredients = new HashMap<>();
for (Map.Entry<Character, List<String>> entry : recipe.ingredients().entrySet()) {
Set<Material> materials = new HashSet<>();
Set<Holder<Key>> holders = new HashSet<>();
for (String item : entry.getValue()) {
if (item.charAt(0) == '#') {
Key tag = Key.from(item.substring(1));
materials.addAll(tagToMaterials(tag));
if (!hasCustomItemInTag) {
if (!plugin.itemManager().tagToCustomItems(tag).isEmpty()) {
hasCustomItemInTag = true;
}
}
holders.addAll(plugin.itemManager().tagToItems(tag));
} else {
materials.add(MaterialUtils.getMaterial(item));
holders.add(BuiltInRegistries.OPTIMIZED_ITEM_ID.get(Key.from(item)).orElseThrow());
}
}
shapedRecipe.setIngredient(entry.getKey(), new RecipeChoice.MaterialChoice(new ArrayList<>(materials)));
ingredients.put(entry.getKey(), Ingredient.of(holders));
}
CustomShapedRecipe<ItemStack> ceRecipe = new CustomShapedRecipe<>(
id,
recipe.category(),
recipe.group(),
new CustomShapedRecipe.Pattern<>(recipe.pattern(), ingredients),
new CustomRecipeResult<>(new CloneableConstantItem(recipe.result().isCustom() ? Key.of("!internal:custom") : Key.of(recipe.result().id()), result), recipe.result().count())
);
if (hasCustomItemInTag) {
callback.accept(() -> {
try {
unregisterRecipe(key);
Reflections.method$CraftRecipe$addToCraftingManager.invoke(Reflections.method$CraftShapedRecipe$fromBukkitRecipe.invoke(null, shapedRecipe));
injectShapedRecipe(id, ceRecipe);
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to convert shaped recipe", e);
}
});
this.injectedDataPackRecipes.add(key);
}
this.addVanillaInternalRecipe(id, ceRecipe);
}
private void handleDataPackCookingRecipe(Key id,
VanillaCookingRecipe recipe,
PentaFunction<NamespacedKey, ItemStack, RecipeChoice, Float, Integer, org.bukkit.inventory.CookingRecipe<?>> constructor1,
HeptaFunction<Key, CookingRecipeCategory, String, Ingredient<ItemStack>, Integer, Float, CustomRecipeResult<ItemStack>, CustomCookingRecipe<ItemStack>> constructor2,
Method fromBukkitRecipeMethod,
Consumer<Runnable> callback) {
NamespacedKey key = new NamespacedKey(id.namespace(), id.value());
ItemStack result = createResultStack(recipe.result());
Set<Material> materials = new HashSet<>();
Set<Holder<Key>> holders = new HashSet<>();
boolean hasCustomItemInTag = false;
for (String item : recipe.ingredient()) {
if (item.charAt(0) == '#') {
Key tag = Key.from(item.substring(1));
materials.addAll(tagToMaterials(tag));
if (!hasCustomItemInTag) {
if (!plugin.itemManager().tagToCustomItems(tag).isEmpty()) {
hasCustomItemInTag = true;
}
}
holders.addAll(plugin.itemManager().tagToItems(tag));
} else {
materials.add(MaterialUtils.getMaterial(item));
holders.add(BuiltInRegistries.OPTIMIZED_ITEM_ID.get(Key.from(item)).orElseThrow());
}
}
org.bukkit.inventory.CookingRecipe<?> cookingRecipe = constructor1.apply(key, result, new RecipeChoice.MaterialChoice(new ArrayList<>(materials)), recipe.experience(), recipe.cookingTime());
if (recipe.group() != null) {
cookingRecipe.setGroup(recipe.group());
}
if (recipe.category() != null) {
cookingRecipe.setCategory(CookingBookCategory.valueOf(recipe.category().name()));
}
CustomCookingRecipe<ItemStack> ceRecipe = constructor2.apply(
id,
recipe.category(),
recipe.group(),
Ingredient.of(holders),
recipe.cookingTime(),
recipe.experience(),
new CustomRecipeResult<>(new CloneableConstantItem(recipe.result().isCustom() ? Key.of("!internal:custom") : Key.of(recipe.result().id()), result), recipe.result().count())
);
if (hasCustomItemInTag) {
callback.accept(() -> {
try {
unregisterRecipe(key);
Reflections.method$CraftRecipe$addToCraftingManager.invoke(fromBukkitRecipeMethod.invoke(null, cookingRecipe));
injectCookingRecipe(id, ceRecipe);
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to convert smelting recipe", e);
}
});
this.injectedDataPackRecipes.add(key);
}
this.addVanillaInternalRecipe(id, ceRecipe);
}
private List<Material> tagToMaterials(Key tag) {
Set<Material> materials = new HashSet<>();
List<Holder<Key>> holders = this.plugin.itemManager().tagToVanillaItems(tag);
if (holders != null) {
for (Holder<Key> holder : holders) {
materials.add(MaterialUtils.getMaterial(holder.value()));
}
}
List<Holder<Key>> customItems = this.plugin.itemManager().tagToCustomItems(tag);
if (customItems != null) {
for (Holder<Key> holder : customItems) {
this.plugin.itemManager().getCustomItem(holder.value()).ifPresent(it -> {
materials.add(MaterialUtils.getMaterial(it.material()));
});
}
}
return new ArrayList<>(materials);
}
private ItemStack createResultStack(RecipeResult result) {
ItemStack itemStack;
if (result.components() == null) {
itemStack = new ItemStack(Objects.requireNonNull(MaterialUtils.getMaterial(result.id())));
itemStack.setAmount(result.count());
} else {
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("id", result.id());
jsonObject.addProperty("count", result.count());
jsonObject.add("components", result.components());
Object nmsStack = ItemObject.newItem(TagCompound.newTag(jsonObject.toString()));
try {
itemStack = (ItemStack) Reflections.method$CraftItemStack$asCraftMirror.invoke(null, nmsStack);
} catch (Exception e) {
this.plugin.logger().warn("Failed to create ItemStack mirror", e);
return new ItemStack(Material.STICK);
}
}
return itemStack;
}
private Key extractKeyFromResourceLocation(String input) {
int prefixEndIndex = input.indexOf(':');
String prefix = input.substring(0, prefixEndIndex);
int lastSlashIndex = input.lastIndexOf('/');
int lastDotIndex = input.lastIndexOf('.');
String fileName = input.substring(lastSlashIndex + 1, lastDotIndex);
return Key.of(prefix, fileName);
}
private static List<Material> ingredientToBukkitMaterials(Ingredient<ItemStack> ingredient) {
Set<Material> materials = new HashSet<>();
for (Holder<Key> holder : ingredient.items()) {
materials.add(getMaterialById(holder.value()));
}
return new ArrayList<>(materials);
}
private static Material getMaterialById(Key key) {
Material material = MaterialUtils.getMaterial(key);
if (material != null) {
return material;
}
Optional<CustomItem<ItemStack>> optionalItem = BukkitItemManager.instance().getCustomItem(key);
return optionalItem.map(itemStackCustomItem -> MaterialUtils.getMaterial(itemStackCustomItem.material())).orElse(null);
}
private static List<Object> getIngredientLooks(List<Holder<Key>> holders) throws ReflectiveOperationException {
List<Object> itemStacks = new ArrayList<>();
for (Holder<Key> holder : holders) {
ItemStack itemStack = BukkitItemManager.instance().getBuildableItem(holder.value()).get().buildItemStack(ItemBuildContext.EMPTY, 1);
Object nmsStack = Reflections.method$CraftItemStack$asNMSMirror.invoke(null, itemStack);
itemStacks.add(nmsStack);
}
return itemStacks;
}
private static void injectShapedRecipe(Key id, CustomShapedRecipe<ItemStack> recipe) throws ReflectiveOperationException {
List<Ingredient<ItemStack>> actualIngredients = recipe.parsedPattern().ingredients()
.stream()
.filter(Optional::isPresent)
.map(Optional::get)
.toList();
Object shapedRecipe = getNMSRecipe(id);
recipeToMcRecipeHolder.put(recipe, shapedRecipe);
if (VersionHelper.isVersionNewerThan1_20_2()) {
shapedRecipe = Reflections.field$RecipeHolder$recipe.get(shapedRecipe);
}
if (VersionHelper.isVersionNewerThan1_21_2()) {
Reflections.field$ShapedRecipe$placementInfo.set(shapedRecipe, null);
}
List<Object> ingredients = RecipeUtils.getIngredientsFromShapedRecipe(shapedRecipe);
injectIngredients(ingredients, actualIngredients);
}
@SuppressWarnings("unchecked")
private static void injectShapelessRecipe(Key id, CustomShapelessRecipe<ItemStack> recipe) throws ReflectiveOperationException {
List<Ingredient<ItemStack>> actualIngredients = recipe.ingredients();
Object shapelessRecipe = getNMSRecipe(id);
recipeToMcRecipeHolder.put(recipe, shapelessRecipe);
if (VersionHelper.isVersionNewerThan1_20_2()) {
shapelessRecipe = Reflections.field$RecipeHolder$recipe.get(shapelessRecipe);
}
if (VersionHelper.isVersionNewerThan1_21_2()) {
Reflections.field$ShapelessRecipe$placementInfo.set(shapelessRecipe, null);
}
List<Object> ingredients = (List<Object>) Reflections.field$ShapelessRecipe$ingredients.get(shapelessRecipe);
injectIngredients(ingredients, actualIngredients);
}
private static void injectCookingRecipe(Key id, CustomCookingRecipe<ItemStack> recipe) throws ReflectiveOperationException {
Ingredient<ItemStack> actualIngredient = recipe.ingredient();
Object smeltingRecipe = getNMSRecipe(id);
recipeToMcRecipeHolder.put(recipe, smeltingRecipe);
if (VersionHelper.isVersionNewerThan1_20_2()) {
smeltingRecipe = Reflections.field$RecipeHolder$recipe.get(smeltingRecipe);
}
Object ingredient;
if (VersionHelper.isVersionNewerThan1_21_2()) {
ingredient = Reflections.field$SingleItemRecipe$input.get(smeltingRecipe);
} else {
ingredient = Reflections.field$AbstractCookingRecipe$input.get(smeltingRecipe);
}
injectIngredients(List.of(ingredient), List.of(actualIngredient));
}
// recipe on 1.20.1 and holder on 1.20.2+
private static Object getNMSRecipe(Key id) throws ReflectiveOperationException {
if (VersionHelper.isVersionNewerThan1_21_2()) {
Object resourceKey = Reflections.method$CraftRecipe$toMinecraft.invoke(null, new NamespacedKey(id.namespace(), id.value()));
@SuppressWarnings("unchecked")
Optional<Object> optional = (Optional<Object>) Reflections.method$RecipeManager$byKey.invoke(minecraftRecipeManager, resourceKey);
if (optional.isEmpty()) {
throw new IllegalArgumentException("Recipe " + id + " not found");
}
return optional.get();
} else {
Object resourceLocation = Reflections.method$ResourceLocation$fromNamespaceAndPath.invoke(null, id.namespace(), id.value());
@SuppressWarnings("unchecked")
Optional<Object> optional = (Optional<Object>) Reflections.method$RecipeManager$byKey.invoke(minecraftRecipeManager, resourceLocation);
if (optional.isEmpty()) {
throw new IllegalArgumentException("Recipe " + id + " not found");
}
return optional.get();
}
}
private static void injectIngredients(List<Object> ingredients, List<Ingredient<ItemStack>> actualIngredients) throws ReflectiveOperationException {
if (ingredients.size() != actualIngredients.size()) {
throw new IllegalArgumentException("Ingredient count mismatch");
}
for (int i = 0; i < ingredients.size(); i++) {
Object ingredient = ingredients.get(i);
Ingredient<ItemStack> actualIngredient = actualIngredients.get(i);
List<Object> items = getIngredientLooks(actualIngredient.items());
if (VersionHelper.isVersionNewerThan1_21_4()) {
Reflections.field$Ingredient$itemStacks1_21_4.set(ingredient, new HashSet<>(items));
} else if (VersionHelper.isVersionNewerThan1_21_2()) {
Reflections.field$Ingredient$itemStacks1_21_2.set(ingredient, items);
} else {
Object itemStackArray = Array.newInstance(Reflections.clazz$ItemStack, items.size());
for (int j = 0; j < items.size(); j++) {
Array.set(itemStackArray, j, items.get(j));
}
Reflections.field$Ingredient$itemStacks1_20_1.set(ingredient, itemStackArray);
}
injectedIngredients.add(ingredient);
}
}
public Object getRecipeHolderByRecipe(Recipe<ItemStack> recipe) {
return recipeToMcRecipeHolder.get(recipe);
}
public static Object minecraftRecipeManager() {
return minecraftRecipeManager;
}
public static BukkitRecipeManager instance() {
return instance;
}
}

View File

@@ -0,0 +1,98 @@
package net.momirealms.craftengine.bukkit.item.recipe;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.bukkit.util.ItemUtils;
import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.item.ItemBuildContext;
import net.momirealms.craftengine.core.item.ItemManager;
import net.momirealms.craftengine.core.item.recipe.OptimizedIDItem;
import net.momirealms.craftengine.core.item.recipe.Recipe;
import net.momirealms.craftengine.core.item.recipe.RecipeTypes;
import net.momirealms.craftengine.core.item.recipe.input.CraftingInput;
import net.momirealms.craftengine.core.plugin.config.ConfigManager;
import net.momirealms.craftengine.core.registry.BuiltInRegistries;
import net.momirealms.craftengine.core.registry.Holder;
import net.momirealms.craftengine.core.util.Key;
import org.bukkit.block.Crafter;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.CrafterCraftEvent;
import org.bukkit.inventory.CraftingRecipe;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class CrafterEventListener implements Listener {
private static final OptimizedIDItem<ItemStack> EMPTY = new OptimizedIDItem<>(null, null);
private final ItemManager<ItemStack> itemManager;
private final BukkitRecipeManager recipeManager;
private final BukkitCraftEngine plugin;
public CrafterEventListener(BukkitCraftEngine plugin, BukkitRecipeManager recipeManager, ItemManager<ItemStack> itemManager) {
this.itemManager = itemManager;
this.recipeManager = recipeManager;
this.plugin = plugin;
}
@EventHandler
public void onCrafting(CrafterCraftEvent event) {
if (!ConfigManager.enableRecipeSystem()) return;
CraftingRecipe recipe = event.getRecipe();
if (!(event.getBlock().getState() instanceof Crafter crafter)) {
return;
}
Key recipeId = Key.of(recipe.getKey().namespace(), recipe.getKey().value());
boolean isCustom = this.recipeManager.isCustomRecipe(recipeId);
// Maybe it's recipe from other plugins, then we ignore it
if (!isCustom) {
return;
}
Inventory inventory = crafter.getInventory();
ItemStack[] ingredients = inventory.getStorageContents();
List<OptimizedIDItem<ItemStack>> optimizedIDItems = new ArrayList<>();
for (ItemStack itemStack : ingredients) {
if (ItemUtils.isEmpty(itemStack)) {
optimizedIDItems.add(EMPTY);
} else {
Item<ItemStack> wrappedItem = this.itemManager.wrap(itemStack);
Optional<Holder.Reference<Key>> idHolder = BuiltInRegistries.OPTIMIZED_ITEM_ID.get(wrappedItem.id());
if (idHolder.isEmpty()) {
// an invalid item is used in recipe, we disallow it
event.setCancelled(true);
return;
} else {
optimizedIDItems.add(new OptimizedIDItem<>(idHolder.get(), itemStack));
}
}
}
CraftingInput<ItemStack> input;
if (ingredients.length == 9) {
input = CraftingInput.of(3, 3, optimizedIDItems);
} else if (ingredients.length == 4) {
input = CraftingInput.of(2, 2, optimizedIDItems);
} else {
return;
}
Recipe<ItemStack> ceRecipe = this.recipeManager.getRecipe(RecipeTypes.SHAPELESS, input);
if (ceRecipe != null) {
event.setResult(ceRecipe.result(ItemBuildContext.EMPTY));
return;
}
ceRecipe = this.recipeManager.getRecipe(RecipeTypes.SHAPED, input);
if (ceRecipe != null) {
event.setResult(ceRecipe.result(ItemBuildContext.EMPTY));
return;
}
// clear result if not met
event.setCancelled(true);
}
}

View File

@@ -0,0 +1,547 @@
package net.momirealms.craftengine.bukkit.item.recipe;
import com.destroystokyo.paper.event.inventory.PrepareResultEvent;
import net.momirealms.craftengine.bukkit.item.BukkitItemManager;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.bukkit.plugin.injector.BukkitInjector;
import net.momirealms.craftengine.bukkit.plugin.user.BukkitServerPlayer;
import net.momirealms.craftengine.bukkit.util.ItemUtils;
import net.momirealms.craftengine.bukkit.util.Reflections;
import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.item.ItemBuildContext;
import net.momirealms.craftengine.core.item.ItemManager;
import net.momirealms.craftengine.core.item.recipe.CustomCampfireRecipe;
import net.momirealms.craftengine.core.item.recipe.OptimizedIDItem;
import net.momirealms.craftengine.core.item.recipe.Recipe;
import net.momirealms.craftengine.core.item.recipe.RecipeTypes;
import net.momirealms.craftengine.core.item.recipe.input.CraftingInput;
import net.momirealms.craftengine.core.item.recipe.input.SingleItemInput;
import net.momirealms.craftengine.core.plugin.config.ConfigManager;
import net.momirealms.craftengine.core.registry.BuiltInRegistries;
import net.momirealms.craftengine.core.registry.Holder;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.VersionHelper;
import net.momirealms.craftengine.core.util.context.ContextHolder;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.Campfire;
import org.bukkit.block.Furnace;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.*;
import org.bukkit.event.inventory.*;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class RecipeEventListener implements Listener {
private static final OptimizedIDItem<ItemStack> EMPTY = new OptimizedIDItem<>(null, null);
private final ItemManager<ItemStack> itemManager;
private final BukkitRecipeManager recipeManager;
private final BukkitCraftEngine plugin;
public RecipeEventListener(BukkitCraftEngine plugin, BukkitRecipeManager recipeManager, ItemManager<ItemStack> itemManager) {
this.itemManager = itemManager;
this.recipeManager = recipeManager;
this.plugin = plugin;
}
@SuppressWarnings("deprecation")
@EventHandler(ignoreCancelled = true)
public void onClickInventoryWithFuel(InventoryClickEvent event) {
Inventory inventory = event.getInventory();
if (!(inventory instanceof FurnaceInventory furnaceInventory)) return;
ItemStack fuelStack = furnaceInventory.getFuel();
Inventory clickedInventory = event.getClickedInventory();
Player player = (Player) event.getWhoClicked();
if (clickedInventory == player.getInventory()) {
if (event.getClick() == ClickType.SHIFT_LEFT || event.getClick() == ClickType.SHIFT_RIGHT) {
ItemStack item = event.getCurrentItem();
if (ItemUtils.isEmpty(item)) return;
if (fuelStack == null || fuelStack.getType() == Material.AIR) {
Item<ItemStack> wrappedItem = BukkitItemManager.instance().wrap(item);
Optional<Holder.Reference<Key>> idHolder = BuiltInRegistries.OPTIMIZED_ITEM_ID.get(wrappedItem.id());
if (idHolder.isEmpty()) return;
SingleItemInput<ItemStack> input = new SingleItemInput<>(new OptimizedIDItem<>(idHolder.get(), item));
Key recipeType;
if (furnaceInventory.getType() == InventoryType.FURNACE) {
recipeType = RecipeTypes.SMELTING;
} else if (furnaceInventory.getType() == InventoryType.BLAST_FURNACE) {
recipeType = RecipeTypes.BLASTING;
} else {
recipeType = RecipeTypes.SMOKING;
}
Recipe<ItemStack> ceRecipe = recipeManager.getRecipe(recipeType, input);
// The item is an ingredient, we should never consider it as fuel firstly
if (ceRecipe != null) return;
int fuelTime = this.itemManager.fuelTime(item);
if (fuelTime == 0) {
if (ItemUtils.isCustomItem(item) && item.getType().isFuel()) {
event.setCancelled(true);
ItemStack smelting = furnaceInventory.getSmelting();
if (ItemUtils.isEmpty(smelting)) {
furnaceInventory.setSmelting(item.clone());
item.setAmount(0);
} else if (smelting.isSimilar(item)) {
int maxStackSize = smelting.getMaxStackSize();
int canGiveMaxCount = item.getAmount();
if (maxStackSize > smelting.getAmount()) {
if (canGiveMaxCount + smelting.getAmount() >= maxStackSize) {
int givenCount = maxStackSize - smelting.getAmount();
smelting.setAmount(maxStackSize);
item.setAmount(item.getAmount() - givenCount);
} else {
smelting.setAmount(smelting.getAmount() + canGiveMaxCount);
item.setAmount(0);
}
}
}
player.updateInventory();
}
return;
}
event.setCancelled(true);
furnaceInventory.setFuel(item.clone());
item.setAmount(0);
player.updateInventory();
} else {
if (fuelStack.isSimilar(item)) {
event.setCancelled(true);
int maxStackSize = fuelStack.getMaxStackSize();
int canGiveMaxCount = item.getAmount();
if (maxStackSize > fuelStack.getAmount()) {
if (canGiveMaxCount + fuelStack.getAmount() >= maxStackSize) {
int givenCount = maxStackSize - fuelStack.getAmount();
fuelStack.setAmount(maxStackSize);
item.setAmount(item.getAmount() - givenCount);
} else {
fuelStack.setAmount(fuelStack.getAmount() + canGiveMaxCount);
item.setAmount(0);
}
player.updateInventory();
}
}
}
}
} else {
// click the furnace inventory
int slot = event.getSlot();
// click the fuel slot
if (slot != 1) {
return;
}
ClickType clickType = event.getClick();
switch (clickType) {
case SWAP_OFFHAND, NUMBER_KEY -> {
ItemStack item;
int hotBarSlot = event.getHotbarButton();
if (clickType == ClickType.SWAP_OFFHAND) {
item = player.getInventory().getItemInOffHand();
} else {
item = player.getInventory().getItem(hotBarSlot);
}
if (item == null) return;
int fuelTime = this.plugin.itemManager().fuelTime(item);
// only handle custom items
if (fuelTime == 0) {
if (ItemUtils.isCustomItem(item) && item.getType().isFuel()) {
event.setCancelled(true);
}
return;
}
event.setCancelled(true);
if (fuelStack == null || fuelStack.getType() == Material.AIR) {
furnaceInventory.setFuel(item.clone());
item.setAmount(0);
} else {
if (clickType == ClickType.SWAP_OFFHAND) {
player.getInventory().setItemInOffHand(fuelStack);
} else {
player.getInventory().setItem(hotBarSlot, fuelStack);
}
furnaceInventory.setFuel(item.clone());
}
player.updateInventory();
}
case LEFT, RIGHT -> {
ItemStack itemOnCursor = event.getCursor();
// pick item
if (ItemUtils.isEmpty(itemOnCursor)) return;
int fuelTime = this.plugin.itemManager().fuelTime(itemOnCursor);
// only handle custom items
if (fuelTime == 0) {
if (ItemUtils.isCustomItem(itemOnCursor) && itemOnCursor.getType().isFuel()) {
event.setCancelled(true);
}
return;
}
event.setCancelled(true);
// The slot is empty
if (fuelStack == null || fuelStack.getType() == Material.AIR) {
if (clickType == ClickType.LEFT) {
furnaceInventory.setFuel(itemOnCursor.clone());
itemOnCursor.setAmount(0);
player.updateInventory();
} else {
ItemStack cloned = itemOnCursor.clone();
cloned.setAmount(1);
furnaceInventory.setFuel(cloned);
itemOnCursor.setAmount(itemOnCursor.getAmount() - 1);
player.updateInventory();
}
} else {
boolean isSimilar = itemOnCursor.isSimilar(fuelStack);
if (clickType == ClickType.LEFT) {
if (isSimilar) {
int maxStackSize = fuelStack.getMaxStackSize();
int canGiveMaxCount = itemOnCursor.getAmount();
if (maxStackSize > fuelStack.getAmount()) {
if (canGiveMaxCount + fuelStack.getAmount() >= maxStackSize) {
int givenCount = maxStackSize - fuelStack.getAmount();
fuelStack.setAmount(maxStackSize);
itemOnCursor.setAmount(itemOnCursor.getAmount() - givenCount);
} else {
fuelStack.setAmount(fuelStack.getAmount() + canGiveMaxCount);
itemOnCursor.setAmount(0);
}
player.updateInventory();
}
} else {
// swap item
event.setCursor(fuelStack);
furnaceInventory.setFuel(itemOnCursor.clone());
player.updateInventory();
}
} else {
if (isSimilar) {
int maxStackSize = fuelStack.getMaxStackSize();
if (maxStackSize > fuelStack.getAmount()) {
fuelStack.setAmount(fuelStack.getAmount() + 1);
itemOnCursor.setAmount(itemOnCursor.getAmount() - 1);
player.updateInventory();
}
} else {
// swap item
event.setCursor(fuelStack);
furnaceInventory.setFuel(itemOnCursor.clone());
player.updateInventory();
}
}
}
}
}
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.HIGH)
public void onFurnaceBurn(FurnaceBurnEvent event) {
ItemStack fuel = event.getFuel();
int fuelTime = this.itemManager.fuelTime(fuel);
if (fuelTime != 0) {
event.setBurnTime(fuelTime);
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onFurnaceInventoryOpen(InventoryOpenEvent event) {
if (!ConfigManager.enableRecipeSystem()) return;
if (!(event.getInventory() instanceof FurnaceInventory furnaceInventory)) {
return;
}
Furnace furnace = furnaceInventory.getHolder();
try {
Object blockEntity = Reflections.field$CraftBlockEntityState$tileEntity.get(furnace);
BukkitInjector.injectCookingBlockEntity(blockEntity);
} catch (Exception e) {
this.plugin.logger().warn("Failed to inject cooking block entity", e);
}
}
// for 1.20.1-1.21.1
@EventHandler(ignoreCancelled = true)
public void onBlockIgnite(BlockIgniteEvent event) {
if (!ConfigManager.enableRecipeSystem()) return;
if (VersionHelper.isVersionNewerThan1_21_2()) return;
Block block = event.getBlock();
Material material = block.getType();
if (material == Material.CAMPFIRE) {
if (block.getState() instanceof Campfire campfire) {
try {
Object blockEntity = Reflections.field$CraftBlockEntityState$tileEntity.get(campfire);
BukkitInjector.injectCookingBlockEntity(blockEntity);
} catch (Exception e) {
this.plugin.logger().warn("Failed to inject cooking block entity", e);
}
}
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onPlaceBlock(BlockPlaceEvent event) {
if (!ConfigManager.enableRecipeSystem()) return;
Block block = event.getBlock();
Material material = block.getType();
if (material == Material.FURNACE || material == Material.BLAST_FURNACE || material == Material.SMOKER) {
if (block.getState() instanceof Furnace furnace) {
try {
Object blockEntity = Reflections.field$CraftBlockEntityState$tileEntity.get(furnace);
BukkitInjector.injectCookingBlockEntity(blockEntity);
} catch (Exception e) {
plugin.logger().warn("Failed to inject cooking block entity", e);
}
}
} else if (!VersionHelper.isVersionNewerThan1_21_2() && material == Material.CAMPFIRE) {
if (block.getState() instanceof Campfire campfire) {
try {
Object blockEntity = Reflections.field$CraftBlockEntityState$tileEntity.get(campfire);
BukkitInjector.injectCookingBlockEntity(blockEntity);
} catch (Exception e) {
this.plugin.logger().warn("Failed to inject cooking block entity", e);
}
}
}
}
// for 1.21.2+
@EventHandler(ignoreCancelled = true)
public void onPutItemOnCampfire(PlayerInteractEvent event) {
if (!ConfigManager.enableRecipeSystem()) return;
if (!VersionHelper.isVersionNewerThan1_21_2()) return;
if (event.getAction() != Action.RIGHT_CLICK_BLOCK) return;
Block clicked = event.getClickedBlock();
if (clicked == null) return;
Material type = clicked.getType();
if (type != Material.CAMPFIRE && type != Material.SOUL_CAMPFIRE) return;
if (!VersionHelper.isVersionNewerThan1_21_2()) {
if (clicked.getState() instanceof Campfire campfire) {
try {
Object blockEntity = Reflections.field$CraftBlockEntityState$tileEntity.get(campfire);
BukkitInjector.injectCookingBlockEntity(blockEntity);
} catch (Exception e) {
this.plugin.logger().warn("Failed to inject cooking block entity", e);
}
}
}
ItemStack itemStack = event.getItem();
if (ItemUtils.isEmpty(itemStack)) return;
try {
@SuppressWarnings("unchecked")
Optional<Object> optionalMCRecipe = (Optional<Object>) Reflections.method$RecipeManager$getRecipeFor1.invoke(
BukkitRecipeManager.minecraftRecipeManager(),
Reflections.instance$RecipeType$CAMPFIRE_COOKING,
Reflections.constructor$SingleRecipeInput.newInstance(Reflections.method$CraftItemStack$asNMSMirror.invoke(null, itemStack)),
Reflections.field$CraftWorld$ServerLevel.get(event.getPlayer().getWorld()),
null
);
if (optionalMCRecipe.isEmpty()) {
return;
}
Item<ItemStack> wrappedItem = BukkitItemManager.instance().wrap(itemStack);
Optional<Holder.Reference<Key>> idHolder = BuiltInRegistries.OPTIMIZED_ITEM_ID.get(wrappedItem.id());
if (idHolder.isEmpty()) {
return;
}
SingleItemInput<ItemStack> input = new SingleItemInput<>(new OptimizedIDItem<>(idHolder.get(), itemStack));
CustomCampfireRecipe<ItemStack> ceRecipe = (CustomCampfireRecipe<ItemStack>) this.recipeManager.getRecipe(RecipeTypes.CAMPFIRE_COOKING, input);
if (ceRecipe == null) {
event.setCancelled(true);
}
} catch (Exception e) {
this.plugin.logger().warn("Failed to handle interact campfire", e);
}
}
// for 1.21.2+
@EventHandler(ignoreCancelled = true)
public void onCampfireCook(CampfireStartEvent event) {
if (!ConfigManager.enableRecipeSystem()) return;
if (!VersionHelper.isVersionNewerThan1_21_2()) return;
CampfireRecipe recipe = event.getRecipe();
Key recipeId = new Key(recipe.getKey().namespace(), recipe.getKey().value());
boolean isCustom = this.recipeManager.isCustomRecipe(recipeId);
if (!isCustom) {
return;
}
ItemStack itemStack = event.getSource();
Item<ItemStack> wrappedItem = BukkitItemManager.instance().wrap(itemStack);
Optional<Holder.Reference<Key>> idHolder = BuiltInRegistries.OPTIMIZED_ITEM_ID.get(wrappedItem.id());
if (idHolder.isEmpty()) {
event.setTotalCookTime(Integer.MAX_VALUE);
return;
}
SingleItemInput<ItemStack> input = new SingleItemInput<>(new OptimizedIDItem<>(idHolder.get(), itemStack));
CustomCampfireRecipe<ItemStack> ceRecipe = (CustomCampfireRecipe<ItemStack>) this.recipeManager.getRecipe(RecipeTypes.CAMPFIRE_COOKING, input);
if (ceRecipe == null) {
event.setTotalCookTime(Integer.MAX_VALUE);
return;
}
event.setTotalCookTime(ceRecipe.cookingTime());
}
// for 1.21.2+
@EventHandler(ignoreCancelled = true)
public void onCampfireCook(BlockCookEvent event) {
if (!ConfigManager.enableRecipeSystem()) return;
if (!VersionHelper.isVersionNewerThan1_21_2()) return;
Material type = event.getBlock().getType();
if (type != Material.CAMPFIRE && type != Material.SOUL_CAMPFIRE) return;
CampfireRecipe recipe = (CampfireRecipe) event.getRecipe();
Key recipeId = new Key(recipe.getKey().namespace(), recipe.getKey().value());
boolean isCustom = this.recipeManager.isCustomRecipe(recipeId);
if (!isCustom) {
return;
}
ItemStack itemStack = event.getSource();
Item<ItemStack> wrappedItem = BukkitItemManager.instance().wrap(itemStack);
Optional<Holder.Reference<Key>> idHolder = BuiltInRegistries.OPTIMIZED_ITEM_ID.get(wrappedItem.id());
if (idHolder.isEmpty()) {
event.setCancelled(true);
return;
}
SingleItemInput<ItemStack> input = new SingleItemInput<>(new OptimizedIDItem<>(idHolder.get(), itemStack));
CustomCampfireRecipe<ItemStack> ceRecipe = (CustomCampfireRecipe<ItemStack>) this.recipeManager.getRecipe(RecipeTypes.CAMPFIRE_COOKING, input);
if (ceRecipe == null) {
event.setCancelled(true);
return;
}
event.setResult(ceRecipe.result(ItemBuildContext.EMPTY));
}
// Paper only
@EventHandler
public void onPrepareResult(PrepareResultEvent event) {
if (!ConfigManager.enableRecipeSystem()) return;
if (event.getInventory() instanceof CartographyInventory cartographyInventory) {
if (ItemUtils.hasCustomItem(cartographyInventory.getStorageContents())) {
event.setResult(new ItemStack(Material.AIR));
}
}
}
@EventHandler(ignoreCancelled = true)
public void onSpecialRecipe(PrepareItemCraftEvent event) {
if (!ConfigManager.enableRecipeSystem()) return;
org.bukkit.inventory.Recipe recipe = event.getRecipe();
if (recipe == null)
return;
if (!(recipe instanceof ComplexRecipe))
return;
CraftingInventory inventory = event.getInventory();
if (ItemUtils.hasCustomItem(inventory.getMatrix())) {
inventory.setResult(null);
}
}
@EventHandler(ignoreCancelled = true)
public void onCraftingRecipe(PrepareItemCraftEvent event) {
if (!ConfigManager.enableRecipeSystem()) return;
org.bukkit.inventory.Recipe recipe = event.getRecipe();
if (recipe == null)
return;
// we only handle shaped and shapeless recipes
boolean shapeless = event.getRecipe() instanceof ShapelessRecipe;
boolean shaped = event.getRecipe() instanceof ShapedRecipe;
if (!shaped && !shapeless) return;
CraftingRecipe craftingRecipe = (CraftingRecipe) recipe;
Key recipeId = Key.of(craftingRecipe.getKey().namespace(), craftingRecipe.getKey().value());
boolean isCustom = this.recipeManager.isCustomRecipe(recipeId);
// Maybe it's recipe from other plugins, then we ignore it
if (!isCustom) {
return;
}
CraftingInventory inventory = event.getInventory();
ItemStack[] ingredients = inventory.getMatrix();
List<OptimizedIDItem<ItemStack>> optimizedIDItems = new ArrayList<>();
for (ItemStack itemStack : ingredients) {
if (ItemUtils.isEmpty(itemStack)) {
optimizedIDItems.add(EMPTY);
} else {
Item<ItemStack> wrappedItem = this.itemManager.wrap(itemStack);
Optional<Holder.Reference<Key>> idHolder = BuiltInRegistries.OPTIMIZED_ITEM_ID.get(wrappedItem.id());
if (idHolder.isEmpty()) {
// an invalid item is used in recipe, we disallow it
inventory.setResult(null);
return;
} else {
optimizedIDItems.add(new OptimizedIDItem<>(idHolder.get(), itemStack));
}
}
}
CraftingInput<ItemStack> input;
if (ingredients.length == 9) {
input = CraftingInput.of(3, 3, optimizedIDItems);
} else if (ingredients.length == 4) {
input = CraftingInput.of(2, 2, optimizedIDItems);
} else {
return;
}
Player player;
try {
player = (Player) Reflections.method$InventoryView$getPlayer.invoke(event.getView());
} catch (ReflectiveOperationException e) {
plugin.logger().warn("Failed to get inventory viewer", e);
return;
}
BukkitServerPlayer serverPlayer = this.plugin.adapt(player);
Key lastRecipe = serverPlayer.lastUsedRecipe();
Recipe<ItemStack> ceRecipe = this.recipeManager.getRecipe(RecipeTypes.SHAPELESS, input, lastRecipe);
if (ceRecipe != null) {
inventory.setResult(ceRecipe.result(new ItemBuildContext(serverPlayer, ContextHolder.EMPTY)));
serverPlayer.setLastUsedRecipe(ceRecipe.id());
correctCraftingRecipeUsed(inventory, ceRecipe);
return;
}
ceRecipe = this.recipeManager.getRecipe(RecipeTypes.SHAPED, input, lastRecipe);
if (ceRecipe != null) {
inventory.setResult(ceRecipe.result(new ItemBuildContext(serverPlayer, ContextHolder.EMPTY)));
serverPlayer.setLastUsedRecipe(ceRecipe.id());
correctCraftingRecipeUsed(inventory, ceRecipe);
return;
}
// clear result if not met
inventory.setResult(null);
}
private void correctCraftingRecipeUsed(CraftingInventory inventory, Recipe<ItemStack> recipe) {
Object holderOrRecipe = recipeManager.getRecipeHolderByRecipe(recipe);
if (holderOrRecipe == null) {
// it's a vanilla recipe but not injected
return;
}
try {
Object resultInventory = Reflections.field$CraftInventoryCrafting$resultInventory.get(inventory);
Reflections.field$ResultContainer$recipeUsed.set(resultInventory, holderOrRecipe);
} catch (ReflectiveOperationException e) {
plugin.logger().warn("Failed to correct used recipe", e);
}
}
}

View File

@@ -0,0 +1,239 @@
package net.momirealms.craftengine.bukkit.pack;
import net.kyori.adventure.text.Component;
import net.momirealms.craftengine.bukkit.api.event.AsyncResourcePackGenerateEvent;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.bukkit.plugin.command.feature.ReloadCommand;
import net.momirealms.craftengine.bukkit.plugin.user.BukkitServerPlayer;
import net.momirealms.craftengine.bukkit.util.ComponentUtils;
import net.momirealms.craftengine.bukkit.util.EventUtils;
import net.momirealms.craftengine.bukkit.util.Reflections;
import net.momirealms.craftengine.core.pack.AbstractPackManager;
import net.momirealms.craftengine.core.pack.host.HostMode;
import net.momirealms.craftengine.core.pack.host.ResourcePackHost;
import net.momirealms.craftengine.core.plugin.config.ConfigManager;
import net.momirealms.craftengine.core.plugin.network.ConnectionState;
import net.momirealms.craftengine.core.plugin.network.NetWorkUser;
import net.momirealms.craftengine.core.util.VersionHelper;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerResourcePackStatusEvent;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Files;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public class BukkitPackManager extends AbstractPackManager implements Listener {
private final BukkitCraftEngine plugin;
private HostMode previousHostMode = HostMode.NONE;
private UUID previousHostUUID;
public BukkitPackManager(BukkitCraftEngine plugin) {
super(plugin, (rf, zp) -> {
AsyncResourcePackGenerateEvent endEvent = new AsyncResourcePackGenerateEvent(rf, zp);
EventUtils.fireAndForget(endEvent);
});
this.plugin = plugin;
}
@Override
public void delayedInit() {
super.delayedInit();
Bukkit.getPluginManager().registerEvents(this, plugin.bootstrap());
}
@EventHandler(priority = EventPriority.LOW)
public void onPlayerJoin(PlayerJoinEvent event) {
// for 1.20.1 servers, not recommended to use
if (ConfigManager.sendPackOnJoin() && !VersionHelper.isVersionNewerThan1_20_2()) {
this.sendResourcePack(plugin.networkManager().getUser(event.getPlayer()), null);
}
}
@EventHandler(priority = EventPriority.LOW)
public void onResourcePackStatus(PlayerResourcePackStatusEvent event) {
// for 1.20.1 servers, not recommended to use
if (ConfigManager.sendPackOnJoin() && ConfigManager.kickOnDeclined() && !VersionHelper.isVersionNewerThan1_20_2()) {
if (event.getStatus() == PlayerResourcePackStatusEvent.Status.DECLINED || event.getStatus() == PlayerResourcePackStatusEvent.Status.FAILED_DOWNLOAD) {
event.getPlayer().kick();
}
}
}
@Override
public void load() {
super.load();
// update server properties
if (VersionHelper.isVersionNewerThan1_20_2()) {
if (ConfigManager.hostMode() == HostMode.SELF_HOST) {
if (Files.exists(resourcePackPath())) {
updateResourcePackSettings(super.packUUID, ResourcePackHost.instance().url(), super.packHash, ConfigManager.kickOnDeclined(), ConfigManager.resourcePackPrompt());
}
} else if (ConfigManager.hostMode() == HostMode.EXTERNAL_HOST) {
updateResourcePackSettings(ConfigManager.externalPackUUID(), ConfigManager.externalPackUrl(), ConfigManager.externalPackSha1(), ConfigManager.kickOnDeclined(), ConfigManager.resourcePackPrompt());
}
}
if (ConfigManager.sendPackOnReload()) {
if (this.previousHostMode == HostMode.SELF_HOST) {
this.previousHostUUID = super.packUUID;
}
// unload packs if user changed to none host
if (ConfigManager.hostMode() == HostMode.NONE && this.previousHostMode != HostMode.NONE) {
unloadResourcePackForOnlinePlayers(this.previousHostUUID);
}
// load new external resource pack on reload
if (ConfigManager.hostMode() == HostMode.EXTERNAL_HOST) {
if (this.previousHostMode == HostMode.NONE) {
updateResourcePackForOnlinePlayers(null);
} else {
updateResourcePackForOnlinePlayers(this.previousHostUUID);
}
// record previous host uuid here
this.previousHostUUID = ConfigManager.externalPackUUID();
}
if (ConfigManager.hostMode() == HostMode.SELF_HOST && this.previousHostMode != HostMode.SELF_HOST) {
if (ReloadCommand.RELOAD_PACK_FLAG) {
ReloadCommand.RELOAD_PACK_FLAG = false;
if (this.previousHostMode == HostMode.NONE) {
updateResourcePackForOnlinePlayers(null);
} else if (this.previousHostMode == HostMode.EXTERNAL_HOST) {
updateResourcePackForOnlinePlayers(this.previousHostUUID);
}
}
}
}
this.previousHostMode = ConfigManager.hostMode();
}
@Override
public void unload() {
super.unload();
if (VersionHelper.isVersionNewerThan1_20_2() && this.previousHostMode != HostMode.NONE) {
resetResourcePackSettings();
}
}
@Override
public void disable() {
super.disable();
HandlerList.unregisterAll(this);
}
@Override
public void generateResourcePack() {
// generate pack
super.generateResourcePack();
// update server properties
if (VersionHelper.isVersionNewerThan1_20_2()) {
if (ConfigManager.hostMode() == HostMode.SELF_HOST) {
updateResourcePackSettings(super.packUUID, ResourcePackHost.instance().url(), super.packHash, ConfigManager.kickOnDeclined(), ConfigManager.resourcePackPrompt());
}
}
// resend packs
if (ConfigManager.hostMode() == HostMode.SELF_HOST && ConfigManager.sendPackOnReload()) {
updateResourcePackForOnlinePlayers(this.previousHostUUID);
}
}
protected void updateResourcePackForOnlinePlayers(UUID previousUUID) {
for (Player player : Bukkit.getOnlinePlayers()) {
BukkitServerPlayer serverPlayer = plugin.adapt(player);
sendResourcePack(serverPlayer, previousUUID);
}
}
private void resetResourcePackSettings() {
try {
Object settings = Reflections.field$DedicatedServer$settings.get(Reflections.method$MinecraftServer$getServer.invoke(null));
Object properties = Reflections.field$DedicatedServerSettings$properties.get(settings);
Reflections.field$DedicatedServerProperties$serverResourcePackInfo.set(properties, Optional.empty());
} catch (Exception e) {
this.plugin.logger().warn("Failed to update resource pack settings", e);
}
}
private void updateResourcePackSettings(UUID uuid, String url, String sha1, boolean required, Component prompt) {
try {
Object settings = Reflections.field$DedicatedServer$settings.get(Reflections.method$MinecraftServer$getServer.invoke(null));
Object properties = Reflections.field$DedicatedServerSettings$properties.get(settings);
Object info;
if (VersionHelper.isVersionNewerThan1_20_3()) {
info = Reflections.constructor$ServerResourcePackInfo.newInstance(uuid, url, sha1, required, ComponentUtils.adventureToMinecraft(prompt));
} else {
info = Reflections.constructor$ServerResourcePackInfo.newInstance(url + uuid, sha1, required, ComponentUtils.adventureToMinecraft(prompt));
}
Reflections.field$DedicatedServerProperties$serverResourcePackInfo.set(properties, Optional.of(info));
} catch (Exception e) {
this.plugin.logger().warn("Failed to update resource pack settings", e);
}
}
public void sendResourcePack(NetWorkUser user, @Nullable UUID previousPack) {
if (ConfigManager.hostMode() == HostMode.NONE) return;
String url;
String sha1;
UUID uuid;
if (ConfigManager.hostMode() == HostMode.SELF_HOST) {
url = ResourcePackHost.instance().url();
sha1 = super.packHash;
uuid = super.packUUID;
if (!Files.exists(resourcePackPath())) return;
} else {
url = ConfigManager.externalPackUrl();
sha1 = ConfigManager.externalPackSha1();
uuid = ConfigManager.externalPackUUID();
if (uuid.equals(previousPack)) return;
}
Object packPrompt = ComponentUtils.adventureToMinecraft(ConfigManager.resourcePackPrompt());
try {
Object packPacket;
if (VersionHelper.isVersionNewerThan1_20_5()) {
packPacket = Reflections.constructor$ClientboundResourcePackPushPacket.newInstance(
uuid, url, sha1, ConfigManager.kickOnDeclined(), Optional.of(packPrompt)
);
} else if (VersionHelper.isVersionNewerThan1_20_3()) {
packPacket = Reflections.constructor$ClientboundResourcePackPushPacket.newInstance(
uuid, url, sha1, ConfigManager.kickOnDeclined(), packPrompt
);
} else {
packPacket = Reflections.constructor$ClientboundResourcePackPushPacket.newInstance(
url + uuid, sha1, ConfigManager.kickOnDeclined(), packPrompt
);
}
if (user.decoderState() == ConnectionState.PLAY) {
if (previousPack != null && VersionHelper.isVersionNewerThan1_20_3()) {
plugin.networkManager().sendPackets(user, List.of(Reflections.constructor$ClientboundResourcePackPopPacket.newInstance(Optional.of(previousPack)), packPacket));
} else {
user.sendPacket(packPacket, false);
}
} else {
user.nettyChannel().writeAndFlush(packPacket);
}
} catch (Exception e) {
this.plugin.logger().warn("Failed to send resource pack", e);
}
}
public void unloadResourcePackForOnlinePlayers(UUID uuid) {
try {
for (Player player : Bukkit.getOnlinePlayers()) {
BukkitServerPlayer serverPlayer = plugin.adapt(player);
if (serverPlayer.decoderState() == ConnectionState.PLAY) {
serverPlayer.sendPacket(Reflections.constructor$ClientboundResourcePackPopPacket.newInstance(Optional.of(uuid)), true);
}
}
} catch (Exception e) {
this.plugin.logger().warn("Failed to unload online player resource pack", e);
}
}
}

View File

@@ -0,0 +1,355 @@
package net.momirealms.craftengine.bukkit.plugin;
import net.momirealms.antigrieflib.AntiGriefLib;
import net.momirealms.craftengine.bukkit.api.event.CraftEngineReloadEvent;
import net.momirealms.craftengine.bukkit.block.BukkitBlockManager;
import net.momirealms.craftengine.bukkit.block.behavior.BukkitBlockBehaviors;
import net.momirealms.craftengine.bukkit.entity.furniture.BukkitFurnitureManager;
import net.momirealms.craftengine.bukkit.item.BukkitItemManager;
import net.momirealms.craftengine.bukkit.item.behavior.BukkitItemBehaviors;
import net.momirealms.craftengine.bukkit.item.recipe.BukkitRecipeManager;
import net.momirealms.craftengine.bukkit.pack.BukkitPackManager;
import net.momirealms.craftengine.bukkit.plugin.command.BukkitCommandManager;
import net.momirealms.craftengine.bukkit.plugin.command.BukkitSenderFactory;
import net.momirealms.craftengine.bukkit.plugin.gui.BukkitGuiManager;
import net.momirealms.craftengine.bukkit.plugin.injector.BukkitInjector;
import net.momirealms.craftengine.bukkit.plugin.network.BukkitNetworkManager;
import net.momirealms.craftengine.bukkit.plugin.papi.ImageExpansion;
import net.momirealms.craftengine.bukkit.plugin.papi.ShiftExpansion;
import net.momirealms.craftengine.bukkit.plugin.scheduler.BukkitSchedulerAdapter;
import net.momirealms.craftengine.bukkit.plugin.user.BukkitServerPlayer;
import net.momirealms.craftengine.bukkit.sound.BukkitSoundManager;
import net.momirealms.craftengine.bukkit.util.EventUtils;
import net.momirealms.craftengine.bukkit.util.PlaceholderAPIUtils;
import net.momirealms.craftengine.bukkit.util.Reflections;
import net.momirealms.craftengine.bukkit.world.BukkitWorldManager;
import net.momirealms.craftengine.core.entity.player.Player;
import net.momirealms.craftengine.core.item.ItemManager;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.classpath.ReflectionClassPathAppender;
import net.momirealms.craftengine.core.plugin.command.sender.SenderFactory;
import net.momirealms.craftengine.core.plugin.dependency.Dependencies;
import net.momirealms.craftengine.core.plugin.dependency.Dependency;
import net.momirealms.craftengine.core.plugin.gui.category.ItemBrowserManagerImpl;
import net.momirealms.craftengine.core.plugin.logger.JavaPluginLogger;
import net.momirealms.craftengine.core.plugin.scheduler.SchedulerAdapter;
import net.momirealms.craftengine.core.plugin.scheduler.SchedulerTask;
import net.momirealms.craftengine.core.util.ReflectionUtils;
import net.momirealms.craftengine.core.util.VersionHelper;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.command.CommandSender;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
@SuppressWarnings("unchecked")
public class BukkitCraftEngine extends CraftEngine {
private static BukkitCraftEngine instance;
private final JavaPlugin bootstrap;
private SchedulerTask tickTask;
private boolean successfullyLoaded = false;
private boolean requiresRestart = false;
private boolean hasMod = false;
private AntiGriefLib antiGrief;
private boolean hasPlaceholderAPI;
public BukkitCraftEngine(JavaPlugin bootstrap) {
VersionHelper.init(serverVersion());
instance = this;
this.bootstrap = bootstrap;
super.classPathAppender = new ReflectionClassPathAppender(this);
super.scheduler = new BukkitSchedulerAdapter(this);
super.logger = new JavaPluginLogger(bootstrap.getLogger());
Class<?> modClass = ReflectionUtils.getClazz(MOD_CLASS);
if (modClass != null) {
Field isSuccessfullyRegistered = ReflectionUtils.getDeclaredField(modClass, "isSuccessfullyRegistered");
try {
assert isSuccessfullyRegistered != null;
requiresRestart = !(boolean) isSuccessfullyRegistered.get(null);
hasMod = true;
} catch (Exception ignore) {
}
}
}
@Override
public void load() {
super.load();
Reflections.init();
BukkitInjector.init();
super.networkManager = new BukkitNetworkManager(this);
super.networkManager.init();
// load mappings and inject blocks
super.blockManager = new BukkitBlockManager(this);
super.furnitureManager = new BukkitFurnitureManager(this);
this.successfullyLoaded = true;
}
@Override
protected List<Dependency> platformDependencies() {
return List.of(
Dependencies.BSTATS_BUKKIT,
Dependencies.CLOUD_BUKKIT, Dependencies.CLOUD_PAPER, Dependencies.CLOUD_BRIGADIER, Dependencies.CLOUD_MINECRAFT_EXTRAS
);
}
@Override
public void enable() {
if (this.hasMod && this.requiresRestart) {
logger().warn(" ");
logger().warn(" ");
logger().warn(" ");
logger().warn("This is the first time you have installed CraftEngine. A restart is required to apply the changes.");
logger().warn(" ");
logger().warn(" ");
logger().warn(" ");
Bukkit.getServer().shutdown();
return;
}
if (!this.successfullyLoaded) {
logger().severe(" ");
logger().severe(" ");
logger().severe(" ");
logger().severe("Failed to enable CraftEngine. Please check the log on loading stage.");
logger().severe("To reduce the loss caused by plugin not loaded, now shutting down the server");
logger().severe(" ");
logger().severe(" ");
logger().severe(" ");
Bukkit.getServer().shutdown();
return;
}
BukkitBlockBehaviors.init();
BukkitItemBehaviors.init();
super.packManager = new BukkitPackManager(this);
super.senderFactory = new BukkitSenderFactory(this);
super.itemManager = new BukkitItemManager(this);
super.recipeManager = new BukkitRecipeManager(this);
super.commandManager = new BukkitCommandManager(this);
super.itemBrowserManager = new ItemBrowserManagerImpl(this);
super.guiManager = new BukkitGuiManager(this);
super.worldManager = new BukkitWorldManager(this);
super.soundManager = new BukkitSoundManager(this);
super.enable();
// tick task
if (VersionHelper.isFolia()) {
this.tickTask = this.scheduler().sync().runRepeating(() -> {
for (BukkitServerPlayer serverPlayer : networkManager().onlineUsers()) {
org.bukkit.entity.Player player = serverPlayer.platformPlayer();
Location location = player.getLocation();
scheduler().sync().run(serverPlayer::tick, player.getWorld(), location.getBlockX() >> 4, location.getBlockZ() >> 4);
}
}, 1, 1);
} else {
this.tickTask = this.scheduler().sync().runRepeating(() -> {
for (BukkitServerPlayer serverPlayer : networkManager().onlineUsers()) {
serverPlayer.tick();
}
}, 1, 1);
}
// compatibility
// register expansion
if (this.isPluginEnabled("PlaceholderAPI")) {
new ShiftExpansion(this).register();
new ImageExpansion(this).register();
this.hasPlaceholderAPI = true;
}
// WorldEdit
if (this.isPluginEnabled("FastAsyncWorldEdit")) {
this.blockManager().initFastAsyncWorldEditHook();
} else if (this.isPluginEnabled("WorldEdit")) {
this.blockManager().initWorldEditHook();
}
}
@Override
public void disable() {
super.disable();
if (this.tickTask != null) this.tickTask.cancel();
}
@Override
public void reload() {
super.reload();
CraftEngineReloadEvent event = new CraftEngineReloadEvent(this);
EventUtils.fireAndForget(event);
}
@Override
protected void registerParsers() {
// register template parser
this.packManager.registerConfigSectionParser(this.templateManager);
// register font parser
this.packManager.registerConfigSectionParser(this.imageManager);
// register item parser
this.packManager.registerConfigSectionParser(this.itemManager);
// register furniture parser
this.packManager.registerConfigSectionParser(this.furnitureManager);
// register block parser
this.packManager.registerConfigSectionParser(this.blockManager);
// register recipe parser
this.packManager.registerConfigSectionParser(this.recipeManager);
// register category parser
this.packManager.registerConfigSectionParser(this.itemBrowserManager);
// register translation parser
this.packManager.registerConfigSectionParser(this.translationManager);
this.packManager.registerConfigSectionParser(this.translationManager.clientLangManager());
// register sound parser
this.packManager.registerConfigSectionParser(this.soundManager);
this.packManager.registerConfigSectionParser(this.soundManager.jukeboxSongManager());
}
@Override
public InputStream resourceStream(String filePath) {
return bootstrap.getResource(filePath.replace("\\", "/"));
}
@Override
public File dataFolderFile() {
return bootstrap().getDataFolder();
}
@Override
public Path dataFolderPath() {
return bootstrap().getDataFolder().toPath().toAbsolutePath();
}
@SuppressWarnings("deprecation")
@Override
public String pluginVersion() {
return bootstrap().getDescription().getVersion();
}
@Override
public String serverVersion() {
return Bukkit.getServer().getBukkitVersion().split("-")[0];
}
@Override
public SchedulerAdapter<World> scheduler() {
return (SchedulerAdapter<World>) scheduler;
}
@Override
public ItemManager<ItemStack> itemManager() {
return (ItemManager<ItemStack>) itemManager;
}
@Override
public BukkitBlockManager blockManager() {
return (BukkitBlockManager) blockManager;
}
@Override
public BukkitFurnitureManager furnitureManager() {
return (BukkitFurnitureManager) furnitureManager;
}
@Override
public SenderFactory<CraftEngine, CommandSender> senderFactory() {
return (SenderFactory<CraftEngine, CommandSender>) senderFactory;
}
public JavaPlugin bootstrap() {
return bootstrap;
}
public static BukkitCraftEngine instance() {
return instance;
}
@Override
public boolean hasPlaceholderAPI() {
return this.hasPlaceholderAPI;
}
@Override
public boolean isPluginEnabled(String plugin) {
return Bukkit.getPluginManager().isPluginEnabled(plugin);
}
@Override
public String parse(Player player, String text) {
return PlaceholderAPIUtils.parse((org.bukkit.entity.Player) player.platformPlayer(), text);
}
@Override
public BukkitNetworkManager networkManager() {
return (BukkitNetworkManager) networkManager;
}
@Override
public BukkitPackManager packManager() {
return (BukkitPackManager) packManager;
}
@SuppressWarnings("ResultOfMethodCallIgnored")
@Override
public void saveResource(String resourcePath) {
if (resourcePath.isEmpty()) {
throw new IllegalArgumentException("ResourcePath cannot be null or empty");
}
File outFile = new File(dataFolderFile(), resourcePath);
if (outFile.exists())
return;
resourcePath = resourcePath.replace('\\', '/');
InputStream in = resourceStream(resourcePath);
if (in == null)
return;
int lastIndex = resourcePath.lastIndexOf('/');
File outDir = new File(dataFolderFile(), resourcePath.substring(0, Math.max(lastIndex, 0)));
if (!outDir.exists()) {
outDir.mkdirs();
}
try {
OutputStream out = new FileOutputStream(outFile);
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
out.close();
in.close();
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
public BukkitServerPlayer adapt(org.bukkit.entity.Player player) {
if (player == null) return null;
return Optional.ofNullable((BukkitServerPlayer) networkManager().getOnlineUser(player)).orElseGet(
() -> (BukkitServerPlayer) networkManager().getUser(player)
);
}
public boolean hasMod() {
return hasMod;
}
public boolean requiresRestart() {
return requiresRestart;
}
public AntiGriefLib antiGrief() {
if (this.antiGrief == null) {
this.antiGrief = AntiGriefLib.builder(this.bootstrap)
.ignoreOP(true)
.silentLogs(true)
.build();
}
return this.antiGrief;
}
}

View File

@@ -0,0 +1,43 @@
package net.momirealms.craftengine.bukkit.plugin.command;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TranslatableComponent;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.command.AbstractCommandFeature;
import net.momirealms.craftengine.core.plugin.command.CraftEngineCommandManager;
import net.momirealms.craftengine.core.util.Pair;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Entity;
import org.incendo.cloud.bukkit.data.Selector;
import java.util.Collection;
public abstract class BukkitCommandFeature<C extends CommandSender> extends AbstractCommandFeature<C> {
public BukkitCommandFeature(CraftEngineCommandManager<C> commandManager, CraftEngine plugin) {
super(commandManager, plugin);
}
public Pair<TranslatableComponent.Builder, Component> resolveSelector(Selector<? extends Entity> selector, TranslatableComponent.Builder single, TranslatableComponent.Builder multiple) {
Collection<? extends Entity> entities = selector.values();
if (entities.size() == 1) {
return Pair.of(single, Component.text(entities.iterator().next().getName()));
} else {
return Pair.of(multiple, Component.text(entities.size()));
}
}
public Pair<TranslatableComponent.Builder, Component> resolveSelector(Collection<? extends Entity> selector, TranslatableComponent.Builder single, TranslatableComponent.Builder multiple) {
if (selector.size() == 1) {
return Pair.of(single, Component.text(selector.iterator().next().getName()));
} else {
return Pair.of(multiple, Component.text(selector.size()));
}
}
@Override
public BukkitCraftEngine plugin() {
return (BukkitCraftEngine) super.plugin();
}
}

View File

@@ -0,0 +1,64 @@
package net.momirealms.craftengine.bukkit.plugin.command;
import net.kyori.adventure.util.Index;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.bukkit.plugin.command.feature.*;
import net.momirealms.craftengine.core.plugin.command.AbstractCommandManager;
import net.momirealms.craftengine.core.plugin.command.CommandFeature;
import net.momirealms.craftengine.core.plugin.command.parser.BlockStateParser;
import net.momirealms.craftengine.core.plugin.command.sender.Sender;
import org.bukkit.command.CommandSender;
import org.incendo.cloud.SenderMapper;
import org.incendo.cloud.bukkit.CloudBukkitCapabilities;
import org.incendo.cloud.execution.ExecutionCoordinator;
import org.incendo.cloud.paper.LegacyPaperCommandManager;
import org.incendo.cloud.setting.ManagerSetting;
import java.util.List;
public class BukkitCommandManager extends AbstractCommandManager<CommandSender> {
private final BukkitCraftEngine plugin;
private final Index<String, CommandFeature<CommandSender>> INDEX;
public BukkitCommandManager(BukkitCraftEngine plugin) {
super(plugin, new LegacyPaperCommandManager<>(
plugin.bootstrap(),
ExecutionCoordinator.simpleCoordinator(),
SenderMapper.identity()
));
this.plugin = plugin;
this.INDEX = Index.create(CommandFeature::getFeatureID, List.of(
new ReloadCommand(this, plugin),
new GetItemCommand(this, plugin),
new GiveItemCommand(this, plugin),
new ItemBrowserCommand(this, plugin),
new TestCommand(this, plugin),
new DebugGetBlockStateRegistryIdCommand(this, plugin),
new DebugGetBlockInternalIdCommand(this, plugin),
new DebugAppearanceStateUsageCommand(this, plugin),
new DebugRealStateUsageCommand(this, plugin),
new DebugItemDataCommand(this, plugin),
new DebugSetBlockCommand(this, plugin),
new DebugTargetBlockCommand(this, plugin)
));
final LegacyPaperCommandManager<CommandSender> manager = (LegacyPaperCommandManager<CommandSender>) getCommandManager();
manager.settings().set(ManagerSetting.ALLOW_UNSAFE_REGISTRATION, true);
manager.parserRegistry().registerParser(BlockStateParser.blockStateParser());
if (manager.hasCapability(CloudBukkitCapabilities.NATIVE_BRIGADIER)) {
manager.registerBrigadier();
manager.brigadierManager().setNativeNumberSuggestions(true);
} else if (manager.hasCapability(CloudBukkitCapabilities.ASYNCHRONOUS_COMPLETION)) {
manager.registerAsynchronousCompletions();
}
}
@Override
protected Sender wrapSender(CommandSender sender) {
return plugin.senderFactory().wrap(sender);
}
@Override
public Index<String, CommandFeature<CommandSender>> features() {
return INDEX;
}
}

View File

@@ -0,0 +1,87 @@
package net.momirealms.craftengine.bukkit.plugin.command;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import net.kyori.adventure.text.Component;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.core.plugin.command.sender.Sender;
import net.momirealms.craftengine.core.plugin.command.sender.SenderFactory;
import net.momirealms.craftengine.core.util.Tristate;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.command.RemoteConsoleCommandSender;
import org.bukkit.entity.Player;
import java.util.UUID;
public class BukkitSenderFactory extends SenderFactory<BukkitCraftEngine, CommandSender> {
private final BukkitAudiences audiences;
public BukkitSenderFactory(BukkitCraftEngine plugin) {
super(plugin);
this.audiences = BukkitAudiences.create(plugin.bootstrap());
}
@Override
protected String name(CommandSender sender) {
if (sender instanceof Player) {
return sender.getName();
}
return Sender.CONSOLE_NAME;
}
@Override
protected UUID uniqueId(CommandSender sender) {
if (sender instanceof Player) {
return ((Player) sender).getUniqueId();
}
return Sender.CONSOLE_UUID;
}
@Override
public Audience audience(CommandSender sender) {
return this.audiences.sender(sender);
}
@Override
protected void sendMessage(CommandSender sender, Component message) {
// we can safely send async for players and the console - otherwise, send it sync
if (sender instanceof Player || sender instanceof ConsoleCommandSender || sender instanceof RemoteConsoleCommandSender) {
audience(sender).sendMessage(message);
} else {
plugin().scheduler().executeSync(() -> audience(sender).sendMessage(message));
}
}
@Override
protected Tristate permissionState(CommandSender sender, String node) {
if (sender.hasPermission(node)) {
return Tristate.TRUE;
} else if (sender.isPermissionSet(node)) {
return Tristate.FALSE;
} else {
return Tristate.UNDEFINED;
}
}
@Override
protected boolean hasPermission(CommandSender sender, String node) {
return sender.hasPermission(node);
}
@Override
protected void performCommand(CommandSender sender, String command) {
plugin().bootstrap().getServer().dispatchCommand(sender, command);
}
@Override
protected boolean isConsole(CommandSender sender) {
return sender instanceof ConsoleCommandSender || sender instanceof RemoteConsoleCommandSender;
}
@Override
public void close() {
super.close();
this.audiences.close();
}
}

View File

@@ -0,0 +1,79 @@
package net.momirealms.craftengine.bukkit.plugin.command.feature;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.momirealms.craftengine.bukkit.block.BukkitBlockManager;
import net.momirealms.craftengine.bukkit.plugin.command.BukkitCommandFeature;
import net.momirealms.craftengine.bukkit.util.BlockStateUtils;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.command.CraftEngineCommandManager;
import net.momirealms.craftengine.core.util.Key;
import org.bukkit.command.CommandSender;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.incendo.cloud.Command;
import org.incendo.cloud.context.CommandContext;
import org.incendo.cloud.context.CommandInput;
import org.incendo.cloud.parser.standard.StringParser;
import org.incendo.cloud.suggestion.Suggestion;
import org.incendo.cloud.suggestion.SuggestionProvider;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class DebugAppearanceStateUsageCommand extends BukkitCommandFeature<CommandSender> {
public DebugAppearanceStateUsageCommand(CraftEngineCommandManager<CommandSender> commandManager, CraftEngine plugin) {
super(commandManager, plugin);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(org.incendo.cloud.CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.required("id", StringParser.stringComponent(StringParser.StringMode.GREEDY_FLAG_YIELDING).suggestionProvider(new SuggestionProvider<>() {
@Override
public @NonNull CompletableFuture<? extends @NonNull Iterable<? extends @NonNull Suggestion>> suggestionsFuture(@NonNull CommandContext<Object> context, @NonNull CommandInput input) {
return CompletableFuture.completedFuture(plugin().blockManager().blockAppearanceArranger().keySet().stream().map(it -> Suggestion.suggestion(it.toString())).toList());
}
}))
.handler(context -> {
String data = context.get("id");
BukkitBlockManager blockManager = plugin().blockManager();
Key baseBlockId = Key.of(data);
List<Integer> appearances = blockManager.blockAppearanceArranger().get(baseBlockId);
if (appearances == null) return;
int i = 0;
Component component = Component.text(baseBlockId + ": ");
List<Component> children = new ArrayList<>();
for (int appearance : appearances) {
Component text = Component.text("|");
List<Integer> reals = blockManager.appearanceToRealStates(appearance);
if (reals == null) {
Component hover = Component.text(baseBlockId.value() + ":" + i).color(NamedTextColor.GREEN);
hover = hover.append(Component.newline()).append(Component.text(BlockStateUtils.fromBlockData(BlockStateUtils.idToBlockState(appearance)).getAsString()).color(NamedTextColor.GREEN));
text = text.color(NamedTextColor.GREEN).hoverEvent(HoverEvent.showText(hover));
} else {
Component hover = Component.text(baseBlockId.value() + ":" + i).color(NamedTextColor.RED);
List<Component> hoverChildren = new ArrayList<>();
hoverChildren.add(Component.newline());
hoverChildren.add(Component.text(BlockStateUtils.fromBlockData(BlockStateUtils.idToBlockState(appearance)).getAsString()).color(NamedTextColor.RED));
for (int real : reals) {
hoverChildren.add(Component.newline());
hoverChildren.add(Component.text(blockManager.getImmutableBlockStateUnsafe(real).toString()).color(NamedTextColor.GRAY));
}
text = text.color(NamedTextColor.RED).hoverEvent(HoverEvent.showText(hover.children(hoverChildren)));
}
children.add(text);
i++;
}
plugin().senderFactory().wrap(context.sender())
.sendMessage(component.children(children));
});
}
@Override
public String getFeatureID() {
return "debug_appearance_state_usage";
}
}

View File

@@ -0,0 +1,49 @@
package net.momirealms.craftengine.bukkit.plugin.command.feature;
import net.momirealms.craftengine.bukkit.plugin.command.BukkitCommandFeature;
import net.momirealms.craftengine.bukkit.util.BlockStateUtils;
import net.momirealms.craftengine.core.block.BlockStateParser;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.command.CraftEngineCommandManager;
import net.momirealms.craftengine.core.plugin.command.FlagKeys;
import org.bukkit.command.CommandSender;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.incendo.cloud.Command;
import org.incendo.cloud.context.CommandContext;
import org.incendo.cloud.context.CommandInput;
import org.incendo.cloud.parser.standard.StringParser;
import org.incendo.cloud.suggestion.Suggestion;
import org.incendo.cloud.suggestion.SuggestionProvider;
import java.util.concurrent.CompletableFuture;
public class DebugGetBlockInternalIdCommand extends BukkitCommandFeature<CommandSender> {
public DebugGetBlockInternalIdCommand(CraftEngineCommandManager<CommandSender> commandManager, CraftEngine plugin) {
super(commandManager, plugin);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(org.incendo.cloud.CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.flag(FlagKeys.SILENT_FLAG)
.required("id", StringParser.stringComponent(StringParser.StringMode.GREEDY_FLAG_YIELDING).suggestionProvider(new SuggestionProvider<>() {
@Override
public @NonNull CompletableFuture<? extends @NonNull Iterable<? extends @NonNull Suggestion>> suggestionsFuture(@NonNull CommandContext<Object> context, @NonNull CommandInput input) {
return CompletableFuture.completedFuture(plugin().blockManager().cachedSuggestions());
}
}))
.handler(context -> {
String data = context.get("id");
ImmutableBlockState state = BlockStateParser.deserialize(data);
if (state == null) return;
context.sender().sendMessage(BlockStateUtils.getBlockOwnerIdFromState(state.customBlockState().handle()).toString());
});
}
@Override
public String getFeatureID() {
return "debug_get_block_internal_id";
}
}

View File

@@ -0,0 +1,37 @@
package net.momirealms.craftengine.bukkit.plugin.command.feature;
import net.momirealms.craftengine.bukkit.plugin.command.BukkitCommandFeature;
import net.momirealms.craftengine.bukkit.util.BlockStateUtils;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.command.CraftEngineCommandManager;
import org.bukkit.Bukkit;
import org.bukkit.block.data.BlockData;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.incendo.cloud.Command;
import org.incendo.cloud.parser.standard.StringParser;
public class DebugGetBlockStateRegistryIdCommand extends BukkitCommandFeature<CommandSender> {
public DebugGetBlockStateRegistryIdCommand(CraftEngineCommandManager<CommandSender> commandManager, CraftEngine plugin) {
super(commandManager, plugin);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(org.incendo.cloud.CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.senderType(Player.class)
.required("state", StringParser.greedyStringParser())
.handler(context -> {
String state = context.get("state");
BlockData blockData = Bukkit.createBlockData(state);
int id = BlockStateUtils.blockDataToId(blockData);
context.sender().sendMessage(String.valueOf(id));
});
}
@Override
public String getFeatureID() {
return "debug_get_block_state_registry_id";
}
}

View File

@@ -0,0 +1,160 @@
package net.momirealms.craftengine.bukkit.plugin.command.feature;
import com.saicone.rtag.RtagMirror;
import com.saicone.rtag.item.ItemObject;
import com.saicone.rtag.tag.TagCompound;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.momirealms.craftengine.bukkit.plugin.command.BukkitCommandFeature;
import net.momirealms.craftengine.bukkit.util.ItemUtils;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.command.CraftEngineCommandManager;
import net.momirealms.craftengine.core.util.AdventureHelper;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.incendo.cloud.Command;
import java.util.*;
public class DebugItemDataCommand extends BukkitCommandFeature<CommandSender> {
public DebugItemDataCommand(CraftEngineCommandManager<CommandSender> commandManager, CraftEngine plugin) {
super(commandManager, plugin);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(org.incendo.cloud.CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.senderType(Player.class)
.handler(context -> {
ItemStack itemInHand = context.sender().getInventory().getItemInMainHand();
if (ItemUtils.isEmpty(itemInHand)) {
plugin().senderFactory().wrap(context.sender()).sendMessage(Component.text("Please hold an item").color(NamedTextColor.RED));
return;
}
Map<String, Object> readableMap = toReadableMap(itemInHand);
readableMap.remove("rtagDataVersion");
List<String> readableList = mapToList(readableMap);
StringJoiner joiner = new StringJoiner("<newline><reset>");
for (String text : readableList) {
joiner.add(text);
}
plugin().senderFactory().wrap(context.sender()).sendMessage(AdventureHelper.miniMessage(joiner.toString()));
});
}
@Override
public String getFeatureID() {
return "debug_item_data";
}
public static Map<String, Object> toReadableMap(ItemStack item) {
return toMap(item);
}
private static Map<String, Object> toMap(ItemStack object) {
return TagCompound.getValue(RtagMirror.INSTANCE, toCompound(object));
}
private static Object toCompound(ItemStack object) {
if (object == null) {
return null;
} else {
Object compound = extract(object);
return TagCompound.isTagCompound(compound) ? compound : null;
}
}
private static Object extract(ItemStack object) {
return ItemObject.save(ItemObject.asNMSCopy(object));
}
private List<String> mapToList(Map<String, Object> readableDataMap) {
List<String> list = new ArrayList<>();
mapToList(readableDataMap, list, 0, false);
return list;
}
@SuppressWarnings("unchecked")
private void mapToList(Map<String, Object> map, List<String> readableList, int loopTimes, boolean isMapList) {
boolean first = true;
for (Map.Entry<String, Object> entry : map.entrySet()) {
Object nbt = entry.getValue();
if (nbt instanceof List<?> list) {
if (isMapList && first) {
first = false;
readableList.add(" ".repeat(loopTimes - 1) +
"<#F5F5F5>- <gradient:#FFD700:#FFFACD><hover:show_text:'<yellow>List'>" + entry.getKey() + "</hover></gradient>:</#F5F5F5>");
} else {
readableList.add(" ".repeat(loopTimes) +
"<#F5F5F5><gradient:#FFD700:#FFFACD><hover:show_text:'<yellow>List'>" + entry.getKey() + "</hover></gradient>:</#F5F5F5>");
}
for (Object value : list) {
if (value instanceof Map<?,?> innerDataMap) {
mapToList((Map<String, Object>) innerDataMap, readableList, loopTimes + 2, true);
} else {
if (value instanceof String string) {
readableList.add(" ".repeat(loopTimes + 1)
+ "<#F5F5F5>- <hover:show_text:'<yellow>Copy'><click:suggest_command:'" + string.replace("'", "\\'") + "'>" + value + "</click></hover></#F5F5F5>");
} else {
readableList.add(" ".repeat(loopTimes + 1)
+ "<#F5F5F5>- <hover:show_text:'<yellow>Copy'><click:suggest_command:'" + value + "'>" + value + "</click></hover></#F5F5F5>");
}
}
}
} else if (nbt instanceof Map<?,?> innerMap) {
if (isMapList && first) {
first = false;
readableList.add(" ".repeat(loopTimes - 1) +
"<#F5F5F5>- <gradient:#FFD700:#FFFACD><hover:show_text:'<yellow>Map'>" + entry.getKey() + "</hover></gradient>:</#F5F5F5>");
} else {
readableList.add(" ".repeat(loopTimes) +
"<#F5F5F5><gradient:#FFD700:#FFFACD><hover:show_text:'<yellow>Map'>" + entry.getKey() + "</hover></gradient>:");
}
mapToList((Map<String, Object>) innerMap, readableList, loopTimes + 1, false);
} else {
String value;
if (nbt.getClass().isArray()) {
if (nbt instanceof Object[]) {
value = Arrays.deepToString((Object[]) nbt);
} else if (nbt instanceof int[]) {
value = Arrays.toString((int[]) nbt);
} else if (nbt instanceof long[]) {
value = Arrays.toString((long[]) nbt);
} else if (nbt instanceof double[]) {
value = Arrays.toString((double[]) nbt);
} else if (nbt instanceof float[]) {
value = Arrays.toString((float[]) nbt);
} else if (nbt instanceof boolean[]) {
value = Arrays.toString((boolean[]) nbt);
} else if (nbt instanceof byte[]) {
value = Arrays.toString((byte[]) nbt);
} else if (nbt instanceof char[]) {
value = Arrays.toString((char[]) nbt);
} else if (nbt instanceof short[]) {
value = Arrays.toString((short[]) nbt);
} else {
value = "Unknown array type";
}
} else {
value = nbt.toString();
}
if (isMapList && first) {
first = false;
readableList.add(" ".repeat(loopTimes - 1) +
"<#F5F5F5>- <gradient:#FFD700:#FFFACD><hover:show_text:'<yellow>" + nbt.getClass().getSimpleName() + "'>" + entry.getKey() + "</hover></gradient>" +
": " +
"<#F5F5F5><hover:show_text:'<yellow>Copy'><click:suggest_command:'" + value.replace("'", "\\'") + "'>" + value + "</click></hover></#F5F5F5>");
} else {
readableList.add(" ".repeat(loopTimes) +
"<#F5F5F5><gradient:#FFD700:#FFFACD><hover:show_text:'<yellow>" + nbt.getClass().getSimpleName() + "'>" + entry.getKey() + "</hover></gradient>" +
": " +
"<hover:show_text:'<yellow>Copy'><click:suggest_command:'" + value.replace("'", "\\'") + "'>" + value + "</click></hover></#F5F5F5>");
}
}
}
}
}

View File

@@ -0,0 +1,70 @@
package net.momirealms.craftengine.bukkit.plugin.command.feature;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.momirealms.craftengine.bukkit.block.BukkitBlockManager;
import net.momirealms.craftengine.bukkit.plugin.command.BukkitCommandFeature;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.command.CraftEngineCommandManager;
import net.momirealms.craftengine.core.util.Key;
import org.bukkit.command.CommandSender;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.incendo.cloud.Command;
import org.incendo.cloud.context.CommandContext;
import org.incendo.cloud.context.CommandInput;
import org.incendo.cloud.parser.standard.StringParser;
import org.incendo.cloud.suggestion.Suggestion;
import org.incendo.cloud.suggestion.SuggestionProvider;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class DebugRealStateUsageCommand extends BukkitCommandFeature<CommandSender> {
public DebugRealStateUsageCommand(CraftEngineCommandManager<CommandSender> commandManager, CraftEngine plugin) {
super(commandManager, plugin);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(org.incendo.cloud.CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.required("id", StringParser.stringComponent(StringParser.StringMode.GREEDY_FLAG_YIELDING).suggestionProvider(new SuggestionProvider<>() {
@Override
public @NonNull CompletableFuture<? extends @NonNull Iterable<? extends @NonNull Suggestion>> suggestionsFuture(@NonNull CommandContext<Object> context, @NonNull CommandInput input) {
return CompletableFuture.completedFuture(plugin().blockManager().blockAppearanceArranger().keySet().stream().map(it -> Suggestion.suggestion(it.toString())).toList());
}
}))
.handler(context -> {
String data = context.get("id");
BukkitBlockManager blockManager = plugin().blockManager();
Key baseBlockId = Key.of(data);
List<Integer> reals = blockManager.realBlockArranger().get(baseBlockId);
if (reals == null) return;
int i = 0;
Component component = Component.text(baseBlockId + ": ");
List<Component> children = new ArrayList<>();
for (int real : reals) {
ImmutableBlockState state = blockManager.getImmutableBlockStateUnsafe(real);
if (state.isEmpty()) {
Component hover = Component.text("craftengine:" + baseBlockId.value() + "_" + i).color(NamedTextColor.GREEN);
children.add(Component.text("|").color(NamedTextColor.GREEN).hoverEvent(HoverEvent.showText(hover)));
} else {
Component hover = Component.text("craftengine:" + baseBlockId.value() + "_" + i).color(NamedTextColor.RED);
hover = hover.append(Component.newline()).append(Component.text(state.toString()).color(NamedTextColor.GRAY));
children.add(Component.text("|").color(NamedTextColor.RED).hoverEvent(HoverEvent.showText(hover)));
}
i++;
}
plugin().senderFactory().wrap(context.sender())
.sendMessage(component.children(children));
});
}
@Override
public String getFeatureID() {
return "debug_real_state_usage";
}
}

View File

@@ -0,0 +1,52 @@
package net.momirealms.craftengine.bukkit.plugin.command.feature;
import net.momirealms.craftengine.bukkit.api.CraftEngineBlocks;
import net.momirealms.craftengine.bukkit.plugin.command.BukkitCommandFeature;
import net.momirealms.craftengine.core.block.BlockStateParser;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.block.UpdateOption;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.command.CraftEngineCommandManager;
import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.incendo.cloud.Command;
import org.incendo.cloud.bukkit.parser.location.LocationParser;
import org.incendo.cloud.context.CommandContext;
import org.incendo.cloud.context.CommandInput;
import org.incendo.cloud.parser.standard.StringParser;
import org.incendo.cloud.suggestion.Suggestion;
import org.incendo.cloud.suggestion.SuggestionProvider;
import java.util.concurrent.CompletableFuture;
public class DebugSetBlockCommand extends BukkitCommandFeature<CommandSender> {
public DebugSetBlockCommand(CraftEngineCommandManager<CommandSender> commandManager, CraftEngine plugin) {
super(commandManager, plugin);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(org.incendo.cloud.CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.required("location", LocationParser.locationParser())
.required("id", StringParser.stringComponent(StringParser.StringMode.GREEDY_FLAG_YIELDING).suggestionProvider(new SuggestionProvider<>() {
@Override
public @NonNull CompletableFuture<? extends @NonNull Iterable<? extends @NonNull Suggestion>> suggestionsFuture(@NonNull CommandContext<Object> context, @NonNull CommandInput input) {
return CompletableFuture.completedFuture(plugin().blockManager().cachedSuggestions());
}
}))
.handler(context -> {
String data = context.get("id");
ImmutableBlockState state = BlockStateParser.deserialize(data);
if (state == null) return;
Location location = context.get("location");
CraftEngineBlocks.place(location, state, UpdateOption.UPDATE_ALL, false);
});
}
@Override
public String getFeatureID() {
return "debug_set_block";
}
}

View File

@@ -0,0 +1,73 @@
package net.momirealms.craftengine.bukkit.plugin.command.feature;
import net.kyori.adventure.text.Component;
import net.momirealms.craftengine.bukkit.block.BukkitBlockManager;
import net.momirealms.craftengine.bukkit.plugin.command.BukkitCommandFeature;
import net.momirealms.craftengine.bukkit.util.BlockStateUtils;
import net.momirealms.craftengine.bukkit.util.Reflections;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.command.CraftEngineCommandManager;
import net.momirealms.craftengine.core.plugin.command.sender.Sender;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.incendo.cloud.Command;
import java.util.Set;
public class DebugTargetBlockCommand extends BukkitCommandFeature<CommandSender> {
public DebugTargetBlockCommand(CraftEngineCommandManager<CommandSender> commandManager, CraftEngine plugin) {
super(commandManager, plugin);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(org.incendo.cloud.CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.senderType(Player.class)
.flag(manager.flagBuilder("this").build())
.handler(context -> {
Player player = context.sender();
Block block;
if (context.flags().hasFlag("this")) {
Location location = player.getLocation();
block = location.getBlock();
} else {
block = player.getTargetBlockExact(10);
if (block == null) return;
}
String bData = block.getBlockData().getAsString();
Object blockState = BlockStateUtils.blockDataToBlockState(block.getBlockData());
Sender sender = plugin().senderFactory().wrap(context.sender());
sender.sendMessage(Component.text(bData));
int id = BlockStateUtils.blockStateToId(blockState);
Object holder = BukkitBlockManager.instance().getMinecraftBlockHolder(id);
if (holder != null) {
ImmutableBlockState immutableBlockState = BukkitBlockManager.instance().getImmutableBlockState(id);
if (immutableBlockState != null) {
sender.sendMessage(Component.text(immutableBlockState.toString()));
}
try {
@SuppressWarnings("unchecked")
Set<Object> tags = (Set<Object>) Reflections.field$Holder$Reference$tags.get(holder);
if (!tags.isEmpty()) {
sender.sendMessage(Component.text("tags: "));
for (Object tag : tags) {
sender.sendMessage(Component.text(" - " + Reflections.field$TagKey$location.get(tag).toString()));
}
}
} catch (ReflectiveOperationException e) {
plugin().logger().warn("Could not get tags", e);
}
}
});
}
@Override
public String getFeatureID() {
return "debug_target_block";
}
}

View File

@@ -0,0 +1,77 @@
package net.momirealms.craftengine.bukkit.plugin.command.feature;
import net.kyori.adventure.text.Component;
import net.momirealms.craftengine.bukkit.plugin.command.BukkitCommandFeature;
import net.momirealms.craftengine.bukkit.util.PlayerUtils;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.command.CraftEngineCommandManager;
import net.momirealms.craftengine.core.plugin.command.FlagKeys;
import net.momirealms.craftengine.core.plugin.locale.MessageConstants;
import net.momirealms.craftengine.core.util.Key;
import org.bukkit.NamespacedKey;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.incendo.cloud.Command;
import org.incendo.cloud.bukkit.parser.NamespacedKeyParser;
import org.incendo.cloud.context.CommandContext;
import org.incendo.cloud.context.CommandInput;
import org.incendo.cloud.parser.standard.IntegerParser;
import org.incendo.cloud.suggestion.Suggestion;
import org.incendo.cloud.suggestion.SuggestionProvider;
import java.util.concurrent.CompletableFuture;
public class GetItemCommand extends BukkitCommandFeature<CommandSender> {
public GetItemCommand(CraftEngineCommandManager<CommandSender> commandManager, CraftEngine plugin) {
super(commandManager, plugin);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(org.incendo.cloud.CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.senderType(Player.class)
.flag(FlagKeys.SILENT_FLAG)
.flag(FlagKeys.TO_INVENTORY_FLAG)
.required("id", NamespacedKeyParser.namespacedKeyComponent().suggestionProvider(new SuggestionProvider<>() {
@Override
public @NonNull CompletableFuture<? extends @NonNull Iterable<? extends @NonNull Suggestion>> suggestionsFuture(@NonNull CommandContext<Object> context, @NonNull CommandInput input) {
return CompletableFuture.completedFuture(plugin().itemManager().cachedSuggestions());
}
}))
.optional("amount", IntegerParser.integerParser(1, 6400))
.handler(context -> {
Player player = context.sender();
int amount = context.getOrDefault("amount", 1);
boolean toInv = context.flags().hasFlag(FlagKeys.TO_INVENTORY);
NamespacedKey namespacedKey = context.get("id");
Key key = Key.of(namespacedKey.namespace(), namespacedKey.value());
ItemStack builtItem = plugin().itemManager().buildCustomItemStack(key, plugin().adapt(context.sender()));
if (builtItem == null) {
handleFeedback(context, MessageConstants.COMMAND_ITEM_GET_FAILURE_NOT_EXIST, Component.text(key.toString()));
return;
}
int amountToGive = amount;
int maxStack = builtItem.getMaxStackSize();
while (amountToGive > 0) {
int perStackSize = Math.min(maxStack, amountToGive);
amountToGive -= perStackSize;
ItemStack more = builtItem.clone();
more.setAmount(perStackSize);
if (toInv) {
PlayerUtils.putItemsToInventory(player.getInventory(), more, more.getAmount());
} else {
PlayerUtils.dropItem(player, more, false, true, false);
}
}
handleFeedback(context, MessageConstants.COMMAND_ITEM_GET_SUCCESS, Component.text(amount), Component.text(key.toString()));
});
}
@Override
public String getFeatureID() {
return "get_item";
}
}

View File

@@ -0,0 +1,95 @@
package net.momirealms.craftengine.bukkit.plugin.command.feature;
import net.kyori.adventure.text.Component;
import net.momirealms.craftengine.bukkit.item.BukkitItemManager;
import net.momirealms.craftengine.bukkit.plugin.command.BukkitCommandFeature;
import net.momirealms.craftengine.bukkit.util.PlayerUtils;
import net.momirealms.craftengine.core.item.CustomItem;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.command.CraftEngineCommandManager;
import net.momirealms.craftengine.core.plugin.command.FlagKeys;
import net.momirealms.craftengine.core.plugin.locale.MessageConstants;
import net.momirealms.craftengine.core.util.Key;
import org.bukkit.NamespacedKey;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.incendo.cloud.Command;
import org.incendo.cloud.bukkit.data.MultiplePlayerSelector;
import org.incendo.cloud.bukkit.parser.NamespacedKeyParser;
import org.incendo.cloud.bukkit.parser.selector.MultiplePlayerSelectorParser;
import org.incendo.cloud.context.CommandContext;
import org.incendo.cloud.context.CommandInput;
import org.incendo.cloud.parser.standard.IntegerParser;
import org.incendo.cloud.suggestion.Suggestion;
import org.incendo.cloud.suggestion.SuggestionProvider;
import java.util.Collection;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
public class GiveItemCommand extends BukkitCommandFeature<CommandSender> {
public GiveItemCommand(CraftEngineCommandManager<CommandSender> commandManager, CraftEngine plugin) {
super(commandManager, plugin);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(org.incendo.cloud.CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.flag(FlagKeys.SILENT_FLAG)
.flag(FlagKeys.TO_INVENTORY_FLAG)
.required("player", MultiplePlayerSelectorParser.multiplePlayerSelectorParser(true))
.required("id", NamespacedKeyParser.namespacedKeyComponent().suggestionProvider(new SuggestionProvider<>() {
@Override
public @NonNull CompletableFuture<? extends @NonNull Iterable<? extends @NonNull Suggestion>> suggestionsFuture(@NonNull CommandContext<Object> context, @NonNull CommandInput input) {
return CompletableFuture.completedFuture(plugin().itemManager().cachedSuggestions());
}
}))
.optional("amount", IntegerParser.integerParser(1, 6400))
.handler(context -> {
MultiplePlayerSelector selector = context.get("player");
int amount = context.getOrDefault("amount", 1);
boolean toInv = context.flags().hasFlag(FlagKeys.TO_INVENTORY);
NamespacedKey namespacedKey = context.get("id");
Key key = Key.of(namespacedKey.namespace(), namespacedKey.value());
Optional<CustomItem<ItemStack>> optionalItem = BukkitItemManager.instance().getCustomItem(key);
if (optionalItem.isEmpty()) {
handleFeedback(context, MessageConstants.COMMAND_ITEM_GIVE_FAILURE_NOT_EXIST, Component.text(key.toString()));
return;
}
Collection<Player> players = selector.values();
for (Player player : players) {
ItemStack builtItem = optionalItem.get().buildItemStack(plugin().adapt(player));
if (builtItem == null) {
return;
}
int amountToGive = amount;
int maxStack = builtItem.getMaxStackSize();
while (amountToGive > 0) {
int perStackSize = Math.min(maxStack, amountToGive);
amountToGive -= perStackSize;
ItemStack more = builtItem.clone();
more.setAmount(perStackSize);
if (toInv) {
PlayerUtils.putItemsToInventory(player.getInventory(), more, more.getAmount());
} else {
PlayerUtils.dropItem(player, more, false, true, false);
}
}
}
if (players.size() == 1) {
handleFeedback(context, MessageConstants.COMMAND_ITEM_GIVE_SUCCESS_SINGLE, Component.text(amount), Component.text(key.toString()), Component.text(players.iterator().next().getName()));
} else if (players.size() > 1) {
handleFeedback(context, MessageConstants.COMMAND_ITEM_GIVE_SUCCESS_MULTIPLE, Component.text(amount), Component.text(key.toString()), Component.text(players.size()));
}
});
}
@Override
public String getFeatureID() {
return "give_item";
}
}

View File

@@ -0,0 +1,32 @@
package net.momirealms.craftengine.bukkit.plugin.command.feature;
import net.momirealms.craftengine.bukkit.plugin.command.BukkitCommandFeature;
import net.momirealms.craftengine.bukkit.plugin.user.BukkitServerPlayer;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.command.CraftEngineCommandManager;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.incendo.cloud.Command;
public class ItemBrowserCommand extends BukkitCommandFeature<CommandSender> {
public ItemBrowserCommand(CraftEngineCommandManager<CommandSender> commandManager, CraftEngine plugin) {
super(commandManager, plugin);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(org.incendo.cloud.CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.senderType(Player.class)
.handler(context -> {
Player player = context.sender();
BukkitServerPlayer serverPlayer = plugin().adapt(player);
plugin().itemBrowserManager().open(serverPlayer);
});
}
@Override
public String getFeatureID() {
return "item_browser";
}
}

View File

@@ -0,0 +1,91 @@
package net.momirealms.craftengine.bukkit.plugin.command.feature;
import net.kyori.adventure.text.Component;
import net.momirealms.craftengine.bukkit.plugin.command.BukkitCommandFeature;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.command.CraftEngineCommandManager;
import net.momirealms.craftengine.core.plugin.locale.MessageConstants;
import org.bukkit.command.CommandSender;
import org.incendo.cloud.Command;
import org.incendo.cloud.parser.standard.EnumParser;
import java.util.Optional;
public class ReloadCommand extends BukkitCommandFeature<CommandSender> {
public static boolean RELOAD_PACK_FLAG = false;
public ReloadCommand(CraftEngineCommandManager<CommandSender> commandManager, CraftEngine plugin) {
super(commandManager, plugin);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(org.incendo.cloud.CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.flag(manager.flagBuilder("silent").withAliases("s"))
.optional("content", EnumParser.enumParser(ReloadArgument.class))
.handler(context -> {
if (plugin().isReloading()) {
handleFeedback(context, MessageConstants.COMMAND_RELOAD_FAILURE_IS_LOADING);
return;
}
Optional<ReloadArgument> optional = context.optional("content");
ReloadArgument argument = ReloadArgument.CONFIG;
if (optional.isPresent()) {
argument = optional.get();
}
if (argument == ReloadArgument.CONFIG) {
try {
RELOAD_PACK_FLAG = true;
long time1 = System.currentTimeMillis();
plugin().reload();
long time2 = System.currentTimeMillis();
handleFeedback(context, MessageConstants.COMMAND_RELOAD_CONFIG_SUCCESS, Component.text(time2 - time1));
} catch (Exception e) {
handleFeedback(context, MessageConstants.COMMAND_RELOAD_CONFIG_FAILURE);
plugin().logger().warn("Failed to reload config", e);
}
} else if (argument == ReloadArgument.PACK) {
plugin().scheduler().executeAsync(() -> {
try {
long time1 = System.currentTimeMillis();
plugin().packManager().generateResourcePack();
long time2 = System.currentTimeMillis();
handleFeedback(context, MessageConstants.COMMAND_RELOAD_PACK_SUCCESS, Component.text(time2 - time1));
} catch (Exception e) {
handleFeedback(context, MessageConstants.COMMAND_RELOAD_PACK_FAILURE);
plugin().logger().warn("Failed to generate resource pack", e);
}
});
} else if (argument == ReloadArgument.ALL) {
long time1 = System.currentTimeMillis();
try {
plugin().reload();
plugin().scheduler().executeAsync(() -> {
try {
plugin().packManager().generateResourcePack();
long time2 = System.currentTimeMillis();
handleFeedback(context, MessageConstants.COMMAND_RELOAD_ALL_SUCCESS, Component.text(time2 - time1));
} catch (Exception e) {
handleFeedback(context, MessageConstants.COMMAND_RELOAD_ALL_FAILURE);
plugin().logger().warn("Failed to generate resource pack", e);
}
});
} catch (Exception e) {
handleFeedback(context, MessageConstants.COMMAND_RELOAD_ALL_FAILURE);
plugin().logger().warn("Failed to reload config", e);
}
}
});
}
@Override
public String getFeatureID() {
return "reload";
}
public enum ReloadArgument {
CONFIG,
PACK,
ALL
}
}

View File

@@ -0,0 +1,34 @@
package net.momirealms.craftengine.bukkit.plugin.command.feature;
import net.momirealms.craftengine.bukkit.plugin.command.BukkitCommandFeature;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.command.CraftEngineCommandManager;
import net.momirealms.craftengine.core.registry.Holder;
import net.momirealms.craftengine.core.util.Key;
import org.bukkit.command.CommandSender;
import org.incendo.cloud.Command;
import java.util.List;
public class TestCommand extends BukkitCommandFeature<CommandSender> {
public TestCommand(CraftEngineCommandManager<CommandSender> commandManager, CraftEngine plugin) {
super(commandManager, plugin);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(org.incendo.cloud.CommandManager<CommandSender> manager, Command.Builder<CommandSender> builder) {
return builder
.handler(context -> {
List<Holder<Key>> holders = plugin().itemManager().tagToItems(Key.of("minecraft:planks"));
for (Holder<Key> holder : holders) {
context.sender().sendMessage(holder.registeredName());
}
});
}
@Override
public String getFeatureID() {
return "debug_test";
}
}

View File

@@ -0,0 +1,81 @@
package net.momirealms.craftengine.bukkit.plugin.gui;
import net.momirealms.craftengine.bukkit.item.BukkitItemManager;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.bukkit.util.ItemUtils;
import net.momirealms.craftengine.core.entity.player.Player;
import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.plugin.gui.Click;
import net.momirealms.craftengine.core.plugin.gui.Gui;
import net.momirealms.craftengine.core.plugin.gui.Inventory;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.ItemStack;
public class BukkitClick implements Click {
private final InventoryClickEvent event;
private final Inventory inventory;
private final Gui gui;
public BukkitClick(InventoryClickEvent event, Gui gui, Inventory inventory) {
this.event = event;
this.inventory = inventory;
this.gui = gui;
}
@Override
public Gui gui() {
return this.gui;
}
@Override
public Inventory clickedInventory() {
return this.inventory;
}
@Override
public int slot() {
return this.event.getSlot();
}
@Override
public void cancel() {
this.event.setCancelled(true);
}
@Override
public boolean isCancelled() {
return this.event.isCancelled();
}
@Override
public String type() {
return this.event.getClick().name();
}
@Override
public int hotBarKey() {
return this.event.getHotbarButton();
}
@Override
public Player clicker() {
return BukkitCraftEngine.instance().adapt((org.bukkit.entity.Player) event.getWhoClicked());
}
@SuppressWarnings("deprecation")
@Override
public void setItemOnCursor(Item<?> item) {
this.event.setCursor((ItemStack) item.getItem());
}
@Override
public Item<?> itemOnCursor() {
ItemStack itemStack = this.event.getCursor();
if (ItemUtils.isEmpty(itemStack)) return null;
return BukkitItemManager.instance().wrap(itemStack);
}
public InventoryClickEvent event() {
return event;
}
}

View File

@@ -0,0 +1,87 @@
package net.momirealms.craftengine.bukkit.plugin.gui;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.bukkit.util.LegacyInventoryUtils;
import net.momirealms.craftengine.core.plugin.gui.AbstractGui;
import net.momirealms.craftengine.core.plugin.gui.Gui;
import net.momirealms.craftengine.core.plugin.gui.GuiManager;
import net.momirealms.craftengine.core.plugin.gui.Inventory;
import net.momirealms.craftengine.core.plugin.scheduler.SchedulerTask;
import net.momirealms.craftengine.core.util.VersionHelper;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryDragEvent;
public class BukkitGuiManager implements GuiManager, Listener {
private final BukkitCraftEngine plugin;
private SchedulerTask timerTask;
public BukkitGuiManager(BukkitCraftEngine plugin) {
this.plugin = plugin;
}
@Override
public void delayedInit() {
Bukkit.getPluginManager().registerEvents(this, plugin.bootstrap());
this.timerTask = plugin.scheduler().sync().runRepeating(this::timerTask, 20, 20);
}
@Override
public void disable() {
HandlerList.unregisterAll(this);
if (this.timerTask != null && !this.timerTask.cancelled()) {
this.timerTask.cancel();
}
}
public void timerTask() {
for (Player player : Bukkit.getOnlinePlayers()) {
org.bukkit.inventory.Inventory top = !VersionHelper.isVersionNewerThan1_21() ? LegacyInventoryUtils.getTopInventory(player) : player.getOpenInventory().getTopInventory();
if (top.getHolder() instanceof CraftEngineInventoryHolder holder) {
holder.gui().onTimer();
}
}
}
@Override
public Inventory createInventory(Gui gui, int size) {
CraftEngineInventoryHolder holder = new CraftEngineInventoryHolder(gui);
org.bukkit.inventory.Inventory inventory = Bukkit.createInventory(holder, size);
holder.holder().bindValue(inventory);
return new BukkitInventory(inventory);
}
@EventHandler(ignoreCancelled = true)
public void onInventoryClick(InventoryClickEvent event) {
org.bukkit.inventory.Inventory inventory = event.getInventory();
if (!(inventory.getHolder() instanceof CraftEngineInventoryHolder craftEngineInventoryHolder)) {
return;
}
AbstractGui gui = (AbstractGui) craftEngineInventoryHolder.gui();
Player player = (Player) event.getWhoClicked();
if (event.getClickedInventory() == player.getInventory()) {
gui.handleInventoryClick(new BukkitClick(event, gui, new BukkitInventory(player.getInventory())));
} else if (event.getClickedInventory() == inventory) {
gui.handleGuiClick(new BukkitClick(event, gui, new BukkitInventory(inventory)));
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOW)
public void onInventoryDrag(InventoryDragEvent event) {
org.bukkit.inventory.Inventory inventory = event.getInventory();
if (!(inventory.getHolder() instanceof CraftEngineInventoryHolder)) {
return;
}
for (int raw : event.getRawSlots()) {
if (raw < inventory.getSize()) {
event.setCancelled(true);
return;
}
}
}
}

View File

@@ -0,0 +1,42 @@
package net.momirealms.craftengine.bukkit.plugin.gui;
import net.kyori.adventure.text.Component;
import net.momirealms.craftengine.bukkit.plugin.user.BukkitServerPlayer;
import net.momirealms.craftengine.bukkit.util.ComponentUtils;
import net.momirealms.craftengine.bukkit.util.Reflections;
import net.momirealms.craftengine.core.entity.player.Player;
import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.gui.Inventory;
import org.bukkit.inventory.ItemStack;
public class BukkitInventory implements Inventory {
private final org.bukkit.inventory.Inventory inventory;
public BukkitInventory(org.bukkit.inventory.Inventory inventory) {
this.inventory = inventory;
}
@Override
public void open(Player player, Component title) {
BukkitServerPlayer serverPlayer = (BukkitServerPlayer) player;
Object nmsPlayer = serverPlayer.serverPlayer();
try {
Object menuType = Reflections.method$CraftContainer$getNotchInventoryType.invoke(null, inventory);
int nextId = (int) Reflections.method$ServerPlayer$nextContainerCounter.invoke(nmsPlayer);
Object menu = Reflections.constructor$CraftContainer.newInstance(inventory, nmsPlayer, nextId);
Reflections.field$AbstractContainerMenu$checkReachable.set(menu, false);
Object packet = Reflections.constructor$ClientboundOpenScreenPacket.newInstance(nextId, menuType, ComponentUtils.adventureToMinecraft(title));
serverPlayer.sendPacket(packet, false);
Reflections.field$Player$containerMenu.set(nmsPlayer, menu);
Reflections.method$ServerPlayer$initMenu.invoke(nmsPlayer, menu);
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to update inventory title", e);
}
}
@Override
public void setItem(int index, Item<?> item) {
this.inventory.setItem(index, item == null ? null : (ItemStack) item.getItem());
}
}

View File

@@ -0,0 +1,34 @@
package net.momirealms.craftengine.bukkit.plugin.gui;
import net.momirealms.craftengine.core.plugin.gui.Gui;
import net.momirealms.craftengine.shared.ObjectHolder;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
import org.jetbrains.annotations.NotNull;
public class CraftEngineInventoryHolder implements InventoryHolder {
private final ObjectHolder<Inventory> inventory;
private final Gui gui;
public CraftEngineInventoryHolder(Gui gui) {
this.inventory = new ObjectHolder<>();
this.gui = gui;
}
public ObjectHolder<Inventory> holder() {
return inventory;
}
public Gui gui() {
return gui;
}
public Inventory inventory() {
return inventory.value();
}
@Override
public @NotNull Inventory getInventory() {
return inventory();
}
}

View File

@@ -0,0 +1,831 @@
package net.momirealms.craftengine.bukkit.plugin.injector;
import com.mojang.datafixers.util.Pair;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.ClassFileVersion;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.dynamic.scaffold.subclass.ConstructorStrategy;
import net.bytebuddy.implementation.FieldAccessor;
import net.bytebuddy.implementation.FixedValue;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.annotation.*;
import net.bytebuddy.matcher.ElementMatchers;
import net.momirealms.craftengine.bukkit.block.BukkitBlockManager;
import net.momirealms.craftengine.bukkit.block.BukkitBlockShape;
import net.momirealms.craftengine.bukkit.item.BukkitItemManager;
import net.momirealms.craftengine.bukkit.item.recipe.BukkitRecipeManager;
import net.momirealms.craftengine.bukkit.util.BlockStateUtils;
import net.momirealms.craftengine.bukkit.util.NoteBlockChainUpdateUtils;
import net.momirealms.craftengine.bukkit.util.Reflections;
import net.momirealms.craftengine.core.block.BlockKeys;
import net.momirealms.craftengine.core.block.EmptyBlock;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.block.StatePredicate;
import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.item.recipe.CustomCookingRecipe;
import net.momirealms.craftengine.core.item.recipe.OptimizedIDItem;
import net.momirealms.craftengine.core.item.recipe.RecipeTypes;
import net.momirealms.craftengine.core.item.recipe.input.SingleItemInput;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.config.ConfigManager;
import net.momirealms.craftengine.core.registry.BuiltInRegistries;
import net.momirealms.craftengine.core.registry.Holder;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.ReflectionUtils;
import net.momirealms.craftengine.core.util.SectionPosUtils;
import net.momirealms.craftengine.core.util.VersionHelper;
import net.momirealms.craftengine.core.world.CEWorld;
import net.momirealms.craftengine.core.world.SectionPos;
import net.momirealms.craftengine.core.world.chunk.CESection;
import net.momirealms.craftengine.core.world.chunk.InjectedPalettedContainerHolder;
import net.momirealms.craftengine.shared.ObjectHolder;
import net.momirealms.craftengine.shared.block.*;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.Nullable;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
public class BukkitInjector {
private static final ByteBuddy byteBuddy = new ByteBuddy(ClassFileVersion.JAVA_V17);
private static final BukkitBlockShape STONE_SHAPE = new BukkitBlockShape(Reflections.instance$Blocks$STONE$defaultState);
private static Class<?> clazz$InjectedPalettedContainer;
private static Field field$InjectedPalettedContainer$target;
private static Field field$InjectedPalettedContainer$section;
private static Field field$InjectedPalettedContainer$world;
private static Field field$InjectedPalettedContainer$pos;
private static Class<?> clazz$OptimizedItemDisplay;
private static Constructor<?> constructor$OptimizedItemDisplay;
private static Class<?> clazz$OptimizedItemDisplayFatory;
private static Object instance$OptimizedItemDisplayFactory;
private static Class<?> clazz$CraftEngineBlock;
private static MethodHandle constructor$CraftEngineBlock;
private static Field field$CraftEngineBlock$behavior;
private static Field field$CraftEngineBlock$shape;
private static Field field$CraftEngineBlock$isNoteBlock;
private static Class<?> clazz$InjectedCacheChecker;
private static Field field$InjectedCacheChecker$recipeType;
private static Field field$InjectedCacheChecker$lastRecipe;
private static Field field$InjectedCacheChecker$lastCustomRecipe;
public static void init() {
try {
MethodHandles.Lookup lookup = MethodHandles.lookup();
clazz$InjectedCacheChecker = byteBuddy
.subclass(Object.class, ConstructorStrategy.Default.IMITATE_SUPER_CLASS_OPENING)
.implement(Reflections.clazz$RecipeManager$CachedCheck)
.defineField("recipeType", Reflections.clazz$RecipeType, Visibility.PUBLIC)
.defineField("lastRecipe", Object.class, Visibility.PUBLIC)
.defineField("lastCustomRecipe", Key.class, Visibility.PUBLIC)
.method(ElementMatchers.named("getRecipeFor").or(ElementMatchers.named("a")))
.intercept(MethodDelegation.to(
VersionHelper.isVersionNewerThan1_21_2() ?
GetRecipeForMethodInterceptor1_21_2.INSTANCE :
(VersionHelper.isVersionNewerThan1_21() ?
GetRecipeForMethodInterceptor1_21.INSTANCE :
VersionHelper.isVersionNewerThan1_20_5() ?
GetRecipeForMethodInterceptor1_20_5.INSTANCE :
GetRecipeForMethodInterceptor1_20.INSTANCE)
))
.make()
.load(BukkitInjector.class.getClassLoader())
.getLoaded();
field$InjectedCacheChecker$recipeType = clazz$InjectedCacheChecker.getDeclaredField("recipeType");
field$InjectedCacheChecker$lastRecipe = clazz$InjectedCacheChecker.getDeclaredField("lastRecipe");
field$InjectedCacheChecker$lastCustomRecipe = clazz$InjectedCacheChecker.getDeclaredField("lastCustomRecipe");
// Paletted Container
clazz$InjectedPalettedContainer = byteBuddy
.subclass(Reflections.clazz$PalettedContainer)
.name("net.minecraft.world.level.chunk.InjectedPalettedContainer")
.implement(InjectedPalettedContainerHolder.class)
.defineField("target", Reflections.clazz$PalettedContainer, Visibility.PRIVATE)
.defineField("cesection", CESection.class, Visibility.PRIVATE)
.defineField("ceworld", CEWorld.class, Visibility.PRIVATE)
.defineField("cepos", SectionPos.class, Visibility.PRIVATE)
.method(ElementMatchers.any()
.and(ElementMatchers.not(ElementMatchers.is(Reflections.method$PalettedContainer$getAndSet)))
.and(ElementMatchers.not(ElementMatchers.isDeclaredBy(Object.class)))
)
.intercept(MethodDelegation.toField("target"))
.method(ElementMatchers.is(Reflections.method$PalettedContainer$getAndSet))
.intercept(MethodDelegation.to(GetAndSetInterceptor.INSTANCE))
.method(ElementMatchers.named("target"))
.intercept(FieldAccessor.ofField("target"))
.method(ElementMatchers.named("ceSection"))
.intercept(FieldAccessor.ofField("cesection"))
.method(ElementMatchers.named("ceWorld"))
.intercept(FieldAccessor.ofField("ceworld"))
.method(ElementMatchers.named("cePos"))
.intercept(FieldAccessor.ofField("cepos"))
.make()
.load(BukkitInjector.class.getClassLoader())
.getLoaded();
field$InjectedPalettedContainer$target = ReflectionUtils.getDeclaredField(clazz$InjectedPalettedContainer, "target");
field$InjectedPalettedContainer$section = ReflectionUtils.getDeclaredField(clazz$InjectedPalettedContainer, "cesection");
field$InjectedPalettedContainer$world = ReflectionUtils.getDeclaredField(clazz$InjectedPalettedContainer, "ceworld");
field$InjectedPalettedContainer$pos = ReflectionUtils.getDeclaredField(clazz$InjectedPalettedContainer, "cepos");
// State Predicate
DynamicType.Unloaded<?> alwaysTrue = byteBuddy
.subclass(Reflections.clazz$StatePredicate)
.method(ElementMatchers.named("test"))
.intercept(FixedValue.value(true))
.make();
Class<?> alwaysTrueClass = alwaysTrue.load(BukkitInjector.class.getClassLoader()).getLoaded();
DynamicType.Unloaded<?> alwaysFalse = byteBuddy
.subclass(Reflections.clazz$StatePredicate)
.method(ElementMatchers.named("test"))
.intercept(FixedValue.value(false))
.make();
Class<?> alwaysFalseClass = alwaysFalse.load(BukkitInjector.class.getClassLoader()).getLoaded();
StatePredicate.init(alwaysTrueClass.getDeclaredConstructor().newInstance(), alwaysFalseClass.getDeclaredConstructor().newInstance());
// Optimized Item Display
clazz$OptimizedItemDisplay = byteBuddy
.subclass(Reflections.clazz$Display$ItemDisplay, ConstructorStrategy.Default.IMITATE_SUPER_CLASS_OPENING)
.name("net.minecraft.world.entity.OptimizedItemDisplay")
.make()
.load(BukkitInjector.class.getClassLoader())
.getLoaded();
constructor$OptimizedItemDisplay = ReflectionUtils.getConstructor(clazz$OptimizedItemDisplay, Reflections.clazz$EntityType, Reflections.clazz$Level);
clazz$OptimizedItemDisplayFatory = byteBuddy
.subclass(Object.class, ConstructorStrategy.Default.IMITATE_SUPER_CLASS_OPENING)
.name("net.momirealms.craftengine.bukkit.entity.OptimizedItemDisplayFactory")
.implement(Reflections.clazz$EntityType$EntityFactory)
.method(ElementMatchers.named("create"))
.intercept(MethodDelegation.to(OptimizedItemDisplayMethodInterceptor.INSTANCE))
.make()
.load(BukkitInjector.class.getClassLoader())
.getLoaded();
instance$OptimizedItemDisplayFactory = Objects.requireNonNull(ReflectionUtils.getConstructor(clazz$OptimizedItemDisplayFatory, 0)).newInstance();
String packageWithName = BukkitInjector.class.getName();
String generatedClassName = packageWithName.substring(0, packageWithName.lastIndexOf('.')) + ".CraftEngineBlock";
DynamicType.Builder<?> builder = byteBuddy
.subclass(Reflections.clazz$Block, ConstructorStrategy.Default.IMITATE_SUPER_CLASS_OPENING)
.name(generatedClassName)
.defineField("behaviorHolder", ObjectHolder.class, Visibility.PUBLIC)
.defineField("shapeHolder", ObjectHolder.class, Visibility.PUBLIC)
.defineField("isClientSideNoteBlock", boolean.class, Visibility.PUBLIC)
// should always implement this interface
.implement(Reflections.clazz$Fallable)
.implement(Reflections.clazz$BonemealableBlock)
// internal interfaces
.implement(BehaviorHolder.class)
.implement(ShapeHolder.class)
.implement(NoteBlockIndicator.class)
.method(ElementMatchers.named("getBehaviorHolder"))
.intercept(FieldAccessor.ofField("behaviorHolder"))
.method(ElementMatchers.named("getShapeHolder"))
.intercept(FieldAccessor.ofField("shapeHolder"))
.method(ElementMatchers.named("isNoteBlock"))
.intercept(FieldAccessor.ofField("isClientSideNoteBlock"))
// getShape
.method(ElementMatchers.is(Reflections.method$BlockBehaviour$getShape))
.intercept(MethodDelegation.to(GetShapeInterceptor.INSTANCE))
// tick
.method(ElementMatchers.is(Reflections.method$BlockBehaviour$tick))
.intercept(MethodDelegation.to(TickInterceptor.INSTANCE))
// isValidBoneMealTarget
.method(ElementMatchers.is(Reflections.method$BonemealableBlock$isValidBonemealTarget))
.intercept(MethodDelegation.to(IsValidBoneMealTargetInterceptor.INSTANCE))
// isBoneMealSuccess
.method(ElementMatchers.is(Reflections.method$BonemealableBlock$isBonemealSuccess))
.intercept(MethodDelegation.to(IsBoneMealSuccessInterceptor.INSTANCE))
// performBoneMeal
.method(ElementMatchers.is(Reflections.method$BonemealableBlock$performBonemeal))
.intercept(MethodDelegation.to(PerformBoneMealInterceptor.INSTANCE))
// random tick
.method(ElementMatchers.is(Reflections.method$BlockBehaviour$randomTick))
.intercept(MethodDelegation.to(RandomTickInterceptor.INSTANCE))
// onPlace
.method(ElementMatchers.takesArguments(5)
.and(ElementMatchers.takesArgument(0, Reflections.clazz$BlockState))
.and(ElementMatchers.takesArgument(1, Reflections.clazz$Level))
.and(ElementMatchers.takesArgument(2, Reflections.clazz$BlockPos))
.and(ElementMatchers.takesArgument(3, Reflections.clazz$BlockState))
.and(ElementMatchers.takesArgument(4, boolean.class))
.and(ElementMatchers.named("onPlace").or(ElementMatchers.named("a")))
)
.intercept(MethodDelegation.to(OnPlaceInterceptor.INSTANCE))
// onBrokenAfterFall
.method(ElementMatchers.takesArguments(3)
.and(ElementMatchers.takesArgument(0, Reflections.clazz$Level))
.and(ElementMatchers.takesArgument(1, Reflections.clazz$BlockPos))
.and(ElementMatchers.takesArgument(2, Reflections.clazz$FallingBlockEntity))
)
.intercept(MethodDelegation.to(OnBrokenAfterFallInterceptor.INSTANCE))
// canSurvive
.method(ElementMatchers.takesArguments(3)
.and(ElementMatchers.takesArgument(0, Reflections.clazz$BlockState))
.and(ElementMatchers.takesArgument(1, Reflections.clazz$LevelReader))
.and(ElementMatchers.takesArgument(2, Reflections.clazz$BlockPos))
)
.intercept(MethodDelegation.to(CanSurviveInterceptor.INSTANCE))
// updateShape
.method(ElementMatchers.returns(Reflections.clazz$BlockState)
.and(ElementMatchers.takesArgument(0, Reflections.clazz$BlockState))
// LevelReader 1.21.3+ // 1.20-1.12.2
.and(ElementMatchers.takesArgument(1, Reflections.clazz$LevelReader).or(ElementMatchers.takesArgument(1, Reflections.clazz$Direction)))
.and(ElementMatchers.named("updateShape").or(ElementMatchers.named("a"))))
.intercept(MethodDelegation.to(UpdateShapeInterceptor.INSTANCE))
// getFluidState
.method(ElementMatchers.returns(Reflections.clazz$FluidState)
.and(ElementMatchers.takesArgument(0, Reflections.clazz$BlockState)))
.intercept(MethodDelegation.to(FluidStateInterceptor.INSTANCE));
clazz$CraftEngineBlock = builder.make().load(BukkitInjector.class.getClassLoader()).getLoaded();
constructor$CraftEngineBlock = MethodHandles.publicLookup().in(clazz$CraftEngineBlock)
.findConstructor(clazz$CraftEngineBlock, MethodType.methodType(void.class, Reflections.clazz$BlockBehaviour$Properties))
.asType(MethodType.methodType(Reflections.clazz$Block, Reflections.clazz$BlockBehaviour$Properties));
field$CraftEngineBlock$behavior = clazz$CraftEngineBlock.getField("behaviorHolder");
field$CraftEngineBlock$shape = clazz$CraftEngineBlock.getField("shapeHolder");
field$CraftEngineBlock$isNoteBlock = clazz$CraftEngineBlock.getField("isClientSideNoteBlock");
} catch (Throwable e) {
CraftEngine.instance().logger().severe("Failed to init injector", e);
}
}
public static void injectCookingBlockEntity(Object entity) throws ReflectiveOperationException {
if (Reflections.clazz$AbstractFurnaceBlockEntity.isInstance(entity)) {
Object quickCheck = Reflections.field$AbstractFurnaceBlockEntity$quickCheck.get(entity);
if (clazz$InjectedCacheChecker.isInstance(quickCheck)) return; // already injected
Object recipeType = Reflections.field$AbstractFurnaceBlockEntity$recipeType.get(entity);
Object injectedChecker = Reflections.UNSAFE.allocateInstance(clazz$InjectedCacheChecker);
field$InjectedCacheChecker$recipeType.set(injectedChecker, recipeType);
Reflections.field$AbstractFurnaceBlockEntity$quickCheck.set(entity, injectedChecker);
} else if (!VersionHelper.isVersionNewerThan1_21_2() && Reflections.clazz$CampfireBlockEntity.isInstance(entity)) {
Object quickCheck = Reflections.field$CampfireBlockEntity$quickCheck.get(entity);
if (clazz$InjectedCacheChecker.isInstance(quickCheck)) return; // already injected
Object injectedChecker = Reflections.UNSAFE.allocateInstance(clazz$InjectedCacheChecker);
field$InjectedCacheChecker$recipeType.set(injectedChecker, Reflections.instance$RecipeType$CAMPFIRE_COOKING);
Reflections.field$CampfireBlockEntity$quickCheck.set(entity, injectedChecker);
}
}
public static Object getOptimizedItemDisplayFactory() {
return instance$OptimizedItemDisplayFactory;
}
public static class OptimizedItemDisplayMethodInterceptor {
public static final OptimizedItemDisplayMethodInterceptor INSTANCE = new OptimizedItemDisplayMethodInterceptor();
@RuntimeType
public Object intercept(@AllArguments Object[] args) throws Exception {
return constructor$OptimizedItemDisplay.newInstance(args[0], args[1]);
}
}
public static void injectLevelChunkSection(Object targetSection, CESection ceSection, CEWorld ceWorld, SectionPos pos) {
try {
Object container = Reflections.field$LevelChunkSection$states.get(targetSection);
if (!clazz$InjectedPalettedContainer.isInstance(container)) {
Object injectedObject = Reflections.UNSAFE.allocateInstance(clazz$InjectedPalettedContainer);
field$InjectedPalettedContainer$target.set(injectedObject, container);
field$InjectedPalettedContainer$section.set(injectedObject, ceSection);
field$InjectedPalettedContainer$world.set(injectedObject, ceWorld);
field$InjectedPalettedContainer$pos.set(injectedObject, pos);
Reflections.field$PalettedContainer$data.set(injectedObject, Reflections.field$PalettedContainer$data.get(container));
Reflections.field$LevelChunkSection$states.set(targetSection, injectedObject);
}
} catch (Exception e) {
CraftEngine.instance().logger().severe("Failed to inject chunk section", e);
}
}
public static void uninjectLevelChunkSection(Object section) {
try {
Object states = Reflections.field$LevelChunkSection$states.get(section);
if (clazz$InjectedPalettedContainer.isInstance(states)) {
Reflections.field$LevelChunkSection$states.set(section, field$InjectedPalettedContainer$target.get(states));
}
} catch (Exception e) {
CraftEngine.instance().logger().severe("Failed to inject chunk section", e);
}
}
public static class GetRecipeForMethodInterceptor1_20 {
public static final GetRecipeForMethodInterceptor1_20 INSTANCE = new GetRecipeForMethodInterceptor1_20();
@SuppressWarnings("unchecked")
@RuntimeType
public Object intercept(@This Object thisObj, @AllArguments Object[] args) throws Exception {
Object mcRecipeManager = BukkitRecipeManager.minecraftRecipeManager();
Object type = field$InjectedCacheChecker$recipeType.get(thisObj);
Object lastRecipe = field$InjectedCacheChecker$lastRecipe.get(thisObj);
Optional<Pair<Object, Object>> optionalRecipe = (Optional<Pair<Object, Object>>) Reflections.method$RecipeManager$getRecipeFor0.invoke(mcRecipeManager, type, args[0], args[1], lastRecipe);
if (optionalRecipe.isPresent()) {
Pair<Object, Object> pair = optionalRecipe.get();
Object resourceLocation = pair.getFirst();
Key recipeId = Key.of(resourceLocation.toString());
BukkitRecipeManager recipeManager = BukkitRecipeManager.instance();
ItemStack itemStack;
List<Object> items;
if (type == Reflections.instance$RecipeType$CAMPFIRE_COOKING) {
items = (List<Object>) Reflections.field$SimpleContainer$items.get(args[0]);
} else {
items = (List<Object>) Reflections.field$AbstractFurnaceBlockEntity$items.get(args[0]);
}
itemStack = (ItemStack) Reflections.method$CraftItemStack$asCraftMirror.invoke(null, items.get(0));
// it's a recipe from other plugins
boolean isCustom = recipeManager.isCustomRecipe(recipeId);
if (!isCustom) {
field$InjectedCacheChecker$lastRecipe.set(thisObj, resourceLocation);
return optionalRecipe;
}
Item<ItemStack> wrappedItem = BukkitItemManager.instance().wrap(itemStack);
Optional<Holder.Reference<Key>> idHolder = BuiltInRegistries.OPTIMIZED_ITEM_ID.get(wrappedItem.id());
if (idHolder.isEmpty()) {
return Optional.empty();
}
SingleItemInput<ItemStack> input = new SingleItemInput<>(new OptimizedIDItem<>(idHolder.get(), itemStack));
CustomCookingRecipe<ItemStack> ceRecipe;
Key lastCustomRecipe = (Key) field$InjectedCacheChecker$lastCustomRecipe.get(thisObj);
if (type == Reflections.instance$RecipeType$SMELTING) {
ceRecipe = (CustomCookingRecipe<ItemStack>) recipeManager.getRecipe(RecipeTypes.SMELTING, input, lastCustomRecipe);
} else if (type == Reflections.instance$RecipeType$BLASTING) {
ceRecipe = (CustomCookingRecipe<ItemStack>) recipeManager.getRecipe(RecipeTypes.BLASTING, input, lastCustomRecipe);
} else if (type == Reflections.instance$RecipeType$SMOKING) {
ceRecipe = (CustomCookingRecipe<ItemStack>) recipeManager.getRecipe(RecipeTypes.SMOKING, input, lastCustomRecipe);
} else if (type == Reflections.instance$RecipeType$CAMPFIRE_COOKING) {
ceRecipe = (CustomCookingRecipe<ItemStack>) recipeManager.getRecipe(RecipeTypes.CAMPFIRE_COOKING, input, lastCustomRecipe);
} else {
return Optional.empty();
}
if (ceRecipe == null) {
return Optional.empty();
}
// Cache recipes, it might be incorrect on reloading
field$InjectedCacheChecker$lastCustomRecipe.set(thisObj, ceRecipe.id());
// It doesn't matter at all
field$InjectedCacheChecker$lastRecipe.set(thisObj, resourceLocation);
return Optional.of(Optional.ofNullable(recipeManager.getRecipeHolderByRecipe(ceRecipe)).orElse(pair.getSecond()));
} else {
return Optional.empty();
}
}
}
public static class GetRecipeForMethodInterceptor1_20_5 {
public static final GetRecipeForMethodInterceptor1_20_5 INSTANCE = new GetRecipeForMethodInterceptor1_20_5();
@SuppressWarnings("unchecked")
@RuntimeType
public Object intercept(@This Object thisObj, @AllArguments Object[] args) throws Exception {
Object mcRecipeManager = BukkitRecipeManager.minecraftRecipeManager();
Object type = field$InjectedCacheChecker$recipeType.get(thisObj);
Object lastRecipe = field$InjectedCacheChecker$lastRecipe.get(thisObj);
Optional<Object> optionalRecipe = (Optional<Object>) Reflections.method$RecipeManager$getRecipeFor0.invoke(mcRecipeManager, type, args[0], args[1], lastRecipe);
if (optionalRecipe.isPresent()) {
Object holder = optionalRecipe.get();
Object id = Reflections.field$RecipeHolder$id.get(holder);
Key recipeId = Key.of(id.toString());
BukkitRecipeManager recipeManager = BukkitRecipeManager.instance();
ItemStack itemStack;
List<Object> items;
if (type == Reflections.instance$RecipeType$CAMPFIRE_COOKING) {
items = (List<Object>) Reflections.field$SimpleContainer$items.get(args[0]);
} else {
items = (List<Object>) Reflections.field$AbstractFurnaceBlockEntity$items.get(args[0]);
}
itemStack = (ItemStack) Reflections.method$CraftItemStack$asCraftMirror.invoke(null, items.get(0));
// it's a recipe from other plugins
boolean isCustom = recipeManager.isCustomRecipe(recipeId);
if (!isCustom) {
field$InjectedCacheChecker$lastRecipe.set(thisObj, id);
return optionalRecipe;
}
Item<ItemStack> wrappedItem = BukkitItemManager.instance().wrap(itemStack);
Optional<Holder.Reference<Key>> idHolder = BuiltInRegistries.OPTIMIZED_ITEM_ID.get(wrappedItem.id());
if (idHolder.isEmpty()) {
return Optional.empty();
}
SingleItemInput<ItemStack> input = new SingleItemInput<>(new OptimizedIDItem<>(idHolder.get(), itemStack));
CustomCookingRecipe<ItemStack> ceRecipe;
Key lastCustomRecipe = (Key) field$InjectedCacheChecker$lastCustomRecipe.get(thisObj);
if (type == Reflections.instance$RecipeType$SMELTING) {
ceRecipe = (CustomCookingRecipe<ItemStack>) recipeManager.getRecipe(RecipeTypes.SMELTING, input, lastCustomRecipe);
} else if (type == Reflections.instance$RecipeType$BLASTING) {
ceRecipe = (CustomCookingRecipe<ItemStack>) recipeManager.getRecipe(RecipeTypes.BLASTING, input, lastCustomRecipe);
} else if (type == Reflections.instance$RecipeType$SMOKING) {
ceRecipe = (CustomCookingRecipe<ItemStack>) recipeManager.getRecipe(RecipeTypes.SMOKING, input, lastCustomRecipe);
} else if (type == Reflections.instance$RecipeType$CAMPFIRE_COOKING) {
ceRecipe = (CustomCookingRecipe<ItemStack>) recipeManager.getRecipe(RecipeTypes.CAMPFIRE_COOKING, input, lastCustomRecipe);
} else {
return Optional.empty();
}
if (ceRecipe == null) {
return Optional.empty();
}
// Cache recipes, it might be incorrect on reloading
field$InjectedCacheChecker$lastCustomRecipe.set(thisObj, ceRecipe.id());
// It doesn't matter at all
field$InjectedCacheChecker$lastRecipe.set(thisObj, id);
return Optional.of(Optional.ofNullable(recipeManager.getRecipeHolderByRecipe(ceRecipe)).orElse(holder));
} else {
return Optional.empty();
}
}
}
public static class GetRecipeForMethodInterceptor1_21 {
public static final GetRecipeForMethodInterceptor1_21 INSTANCE = new GetRecipeForMethodInterceptor1_21();
@SuppressWarnings("unchecked")
@RuntimeType
public Object intercept(@This Object thisObj, @AllArguments Object[] args) throws Exception {
Object mcRecipeManager = BukkitRecipeManager.minecraftRecipeManager();
Object type = field$InjectedCacheChecker$recipeType.get(thisObj);
Object lastRecipe = field$InjectedCacheChecker$lastRecipe.get(thisObj);
Optional<Object> optionalRecipe = (Optional<Object>) Reflections.method$RecipeManager$getRecipeFor0.invoke(mcRecipeManager, type, args[0], args[1], lastRecipe);
if (optionalRecipe.isPresent()) {
Object holder = optionalRecipe.get();
Object id = Reflections.field$RecipeHolder$id.get(holder);
Key recipeId = Key.of(id.toString());
BukkitRecipeManager recipeManager = BukkitRecipeManager.instance();
ItemStack itemStack = (ItemStack) Reflections.method$CraftItemStack$asCraftMirror.invoke(null, Reflections.field$SingleRecipeInput$item.get(args[0]));
// it's a recipe from other plugins
boolean isCustom = recipeManager.isCustomRecipe(recipeId);
if (!isCustom) {
field$InjectedCacheChecker$lastRecipe.set(thisObj, id);
return optionalRecipe;
}
Item<ItemStack> wrappedItem = BukkitItemManager.instance().wrap(itemStack);
Optional<Holder.Reference<Key>> idHolder = BuiltInRegistries.OPTIMIZED_ITEM_ID.get(wrappedItem.id());
if (idHolder.isEmpty()) {
return Optional.empty();
}
SingleItemInput<ItemStack> input = new SingleItemInput<>(new OptimizedIDItem<>(idHolder.get(), itemStack));
CustomCookingRecipe<ItemStack> ceRecipe;
Key lastCustomRecipe = (Key) field$InjectedCacheChecker$lastCustomRecipe.get(thisObj);
if (type == Reflections.instance$RecipeType$SMELTING) {
ceRecipe = (CustomCookingRecipe<ItemStack>) recipeManager.getRecipe(RecipeTypes.SMELTING, input, lastCustomRecipe);
} else if (type == Reflections.instance$RecipeType$BLASTING) {
ceRecipe = (CustomCookingRecipe<ItemStack>) recipeManager.getRecipe(RecipeTypes.BLASTING, input, lastCustomRecipe);
} else if (type == Reflections.instance$RecipeType$SMOKING) {
ceRecipe = (CustomCookingRecipe<ItemStack>) recipeManager.getRecipe(RecipeTypes.SMOKING, input, lastCustomRecipe);
} else if (type == Reflections.instance$RecipeType$CAMPFIRE_COOKING) {
ceRecipe = (CustomCookingRecipe<ItemStack>) recipeManager.getRecipe(RecipeTypes.CAMPFIRE_COOKING, input, lastCustomRecipe);
} else {
return Optional.empty();
}
if (ceRecipe == null) {
return Optional.empty();
}
// Cache recipes, it might be incorrect on reloading
field$InjectedCacheChecker$lastCustomRecipe.set(thisObj, ceRecipe.id());
// It doesn't matter at all
field$InjectedCacheChecker$lastRecipe.set(thisObj, id);
return Optional.of(Optional.ofNullable(recipeManager.getRecipeHolderByRecipe(ceRecipe)).orElse(holder));
} else {
return Optional.empty();
}
}
}
public static class GetRecipeForMethodInterceptor1_21_2 {
public static final GetRecipeForMethodInterceptor1_21_2 INSTANCE = new GetRecipeForMethodInterceptor1_21_2();
@SuppressWarnings("unchecked")
@RuntimeType
public Object intercept(@This Object thisObj, @AllArguments Object[] args) throws Exception {
Object mcRecipeManager = BukkitRecipeManager.minecraftRecipeManager();
Object type = field$InjectedCacheChecker$recipeType.get(thisObj);
Object lastRecipe = field$InjectedCacheChecker$lastRecipe.get(thisObj);
Optional<Object> optionalRecipe = (Optional<Object>) Reflections.method$RecipeManager$getRecipeFor1.invoke(mcRecipeManager, type, args[0], args[1], lastRecipe);
if (optionalRecipe.isPresent()) {
Object holder = optionalRecipe.get();
Object id = Reflections.field$RecipeHolder$id.get(holder);
Object resourceLocation = Reflections.field$ResourceKey$location.get(id);
Key recipeId = Key.of(resourceLocation.toString());
BukkitRecipeManager recipeManager = BukkitRecipeManager.instance();
ItemStack itemStack = (ItemStack) Reflections.method$CraftItemStack$asCraftMirror.invoke(null, Reflections.field$SingleRecipeInput$item.get(args[0]));
// it's a recipe from other plugins
boolean isCustom = recipeManager.isCustomRecipe(recipeId);
if (!isCustom) {
field$InjectedCacheChecker$lastRecipe.set(thisObj, id);
return optionalRecipe;
}
Item<ItemStack> wrappedItem = BukkitItemManager.instance().wrap(itemStack);
Optional<Holder.Reference<Key>> idHolder = BuiltInRegistries.OPTIMIZED_ITEM_ID.get(wrappedItem.id());
if (idHolder.isEmpty()) {
return Optional.empty();
}
SingleItemInput<ItemStack> input = new SingleItemInput<>(new OptimizedIDItem<>(idHolder.get(), itemStack));
CustomCookingRecipe<ItemStack> ceRecipe;
Key lastCustomRecipe = (Key) field$InjectedCacheChecker$lastCustomRecipe.get(thisObj);
if (type == Reflections.instance$RecipeType$SMELTING) {
ceRecipe = (CustomCookingRecipe<ItemStack>) recipeManager.getRecipe(RecipeTypes.SMELTING, input, lastCustomRecipe);
} else if (type == Reflections.instance$RecipeType$BLASTING) {
ceRecipe = (CustomCookingRecipe<ItemStack>) recipeManager.getRecipe(RecipeTypes.BLASTING, input, lastCustomRecipe);
} else if (type == Reflections.instance$RecipeType$SMOKING) {
ceRecipe = (CustomCookingRecipe<ItemStack>) recipeManager.getRecipe(RecipeTypes.SMOKING, input, lastCustomRecipe);
} else {
return Optional.empty();
}
if (ceRecipe == null) {
return Optional.empty();
}
// Cache recipes, it might be incorrect on reloading
field$InjectedCacheChecker$lastCustomRecipe.set(thisObj, ceRecipe.id());
// It doesn't matter at all
field$InjectedCacheChecker$lastRecipe.set(thisObj, id);
return Optional.of(Optional.ofNullable(recipeManager.getRecipeHolderByRecipe(ceRecipe)).orElse(holder));
} else {
return Optional.empty();
}
}
}
public static class PalettedContainerMethodInterceptor {
public static final PalettedContainerMethodInterceptor INSTANCE = new PalettedContainerMethodInterceptor();
@RuntimeType
public Object intercept(@This Object thisObj, @AllArguments Object[] args, @Origin Method method) throws Throwable {
InjectedPalettedContainerHolder holder = (InjectedPalettedContainerHolder) thisObj;
return method.invoke(holder.target(), args);
}
}
public static class GetAndSetInterceptor {
public static final GetAndSetInterceptor INSTANCE = new GetAndSetInterceptor();
@RuntimeType
public Object intercept(@This Object thisObj, @AllArguments Object[] args, @Origin MethodHandle method) throws Throwable {
InjectedPalettedContainerHolder holder = (InjectedPalettedContainerHolder) thisObj;
Object targetStates = holder.target();
int x = (int) args[0];
int y = (int) args[1];
int z = (int) args[2];
Object previousState = method.invoke(targetStates, x, y, z, args[3]);
try {
Object newState = args[3];
int stateId = BlockStateUtils.blockStateToId(newState);
CESection section = holder.ceSection();
if (BlockStateUtils.isVanillaBlock(stateId)) {
section.setBlockState(x, y, z, EmptyBlock.INSTANCE.getDefaultState());
if (ConfigManager.enableLightSystem() && ConfigManager.forceUpdateLight()) {
updateLightIfChanged(holder, previousState, newState, null, y, z, x);
}
} else {
ImmutableBlockState immutableBlockState = BukkitBlockManager.instance().getImmutableBlockStateUnsafe(stateId);
section.setBlockState(x, y, z, immutableBlockState);
if (ConfigManager.enableLightSystem()) {
updateLightIfChanged(holder, previousState, newState, immutableBlockState.vanillaBlockState().handle(), y, z, x);
}
}
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to intercept setBlockState", e);
}
return previousState;
}
private void updateLightIfChanged(@This InjectedPalettedContainerHolder thisObj, Object previousBlockState, Object newState, @Nullable Object clientSideNewState, int y, int z, int x) throws ReflectiveOperationException {
int previousLight = BlockStateUtils.getLightEmission(previousBlockState);
int newLight = BlockStateUtils.getLightEmission(newState);
if (previousLight != newLight || (clientSideNewState != null && (BlockStateUtils.isOcclude(newState) != BlockStateUtils.isOcclude(clientSideNewState)))) {
CEWorld world = thisObj.ceWorld();
SectionPos sectionPos = thisObj.cePos();
Set<SectionPos> posSet = SectionPosUtils.calculateAffectedRegions((sectionPos.x() << 4) + x, (sectionPos.y() << 4) + y, (sectionPos.z() << 4) + z, Math.max(newLight, previousLight));
world.sectionLightUpdated(posSet);
}
}
}
public static Object generateBlock(Key replacedBlock, Object ownerBlock, Object properties) throws Throwable {
Object ownerProperties = Reflections.field$BlockBehaviour$properties.get(ownerBlock);
Reflections.field$BlockBehaviour$Properties$hasCollision.set(properties, Reflections.field$BlockBehaviour$Properties$hasCollision.get(ownerProperties));
ObjectHolder<BlockBehavior> behaviorHolder = new ObjectHolder<>(EmptyBlockBehavior.INSTANCE);
ObjectHolder<BlockShape> shapeHolder = new ObjectHolder<>(STONE_SHAPE);
Object newBlockInstance = constructor$CraftEngineBlock.invoke(properties);
field$CraftEngineBlock$behavior.set(newBlockInstance, behaviorHolder);
field$CraftEngineBlock$shape.set(newBlockInstance, shapeHolder);
field$CraftEngineBlock$isNoteBlock.set(newBlockInstance, replacedBlock.equals(BlockKeys.NOTE_BLOCK));
return newBlockInstance;
}
public static class FluidStateInterceptor {
public static final FluidStateInterceptor INSTANCE = new FluidStateInterceptor();
@RuntimeType
public Object intercept(@This Object thisObj, @AllArguments Object[] args, @SuperCall Callable<Object> superMethod) {
ObjectHolder<BlockBehavior> holder = ((BehaviorHolder) thisObj).getBehaviorHolder();
try {
return holder.value().getFluidState(thisObj, args, superMethod);
} catch (Exception e) {
CraftEngine.instance().logger().severe("Failed to run getFluidState", e);
return args[0];
}
}
}
public static class UpdateShapeInterceptor {
public static final UpdateShapeInterceptor INSTANCE = new UpdateShapeInterceptor();
@RuntimeType
public Object intercept(@This Object thisObj, @AllArguments Object[] args, @SuperCall Callable<Object> superMethod) throws Exception {
ObjectHolder<BlockBehavior> holder = ((BehaviorHolder) thisObj).getBehaviorHolder();
if (((NoteBlockIndicator) thisObj).isNoteBlock()) {
startNoteBlockChain(args);
}
try {
return holder.value().updateShape(thisObj, args, superMethod);
} catch (Exception e) {
CraftEngine.instance().logger().severe("Failed to run updateShape", e);
return args[0];
}
}
}
private static void startNoteBlockChain(Object[] args) throws ReflectiveOperationException {
Object direction;
Object serverLevel;
Object blockPos;
if (VersionHelper.isVersionNewerThan1_21_2()) {
direction = args[4];
serverLevel = args[1];
blockPos = args[3];
} else {
direction = args[1];
serverLevel = args[3];
blockPos = args[4];
}
int id = (int) Reflections.field$Direction$data3d.get(direction);
// Y axis
if (id == 0 || id == 1) {
Object chunkSource = Reflections.field$ServerLevel$chunkSource.get(serverLevel);
Reflections.method$ServerChunkCache$blockChanged.invoke(chunkSource, blockPos);
if (id == 1) {
NoteBlockChainUpdateUtils.noteBlockChainUpdate(serverLevel, chunkSource, Reflections.instance$Direction$DOWN, blockPos, 0);
} else {
NoteBlockChainUpdateUtils.noteBlockChainUpdate(serverLevel, chunkSource, Reflections.instance$Direction$UP, blockPos, 0);
}
}
}
public static class GetShapeInterceptor {
public static final GetShapeInterceptor INSTANCE = new GetShapeInterceptor();
@RuntimeType
public Object intercept(@This Object thisObj, @AllArguments Object[] args, @SuperCall Callable<Object> superMethod) throws Exception {
ObjectHolder<BlockShape> holder = ((ShapeHolder) thisObj).getShapeHolder();
try {
return holder.value().getShape(thisObj, args);
} catch (Exception e) {
CraftEngine.instance().logger().severe("Failed to run getShape", e);
return superMethod.call();
}
}
}
public static class RandomTickInterceptor {
public static final RandomTickInterceptor INSTANCE = new RandomTickInterceptor();
@RuntimeType
public void intercept(@This Object thisObj, @AllArguments Object[] args, @SuperCall Callable<Object> superMethod) {
ObjectHolder<BlockBehavior> holder = ((BehaviorHolder) thisObj).getBehaviorHolder();
try {
holder.value().randomTick(thisObj, args, superMethod);
} catch (Exception e) {
CraftEngine.instance().logger().severe("Failed to run randomTick", e);
}
}
}
public static class TickInterceptor {
public static final TickInterceptor INSTANCE = new TickInterceptor();
@RuntimeType
public void intercept(@This Object thisObj, @AllArguments Object[] args, @SuperCall Callable<Object> superMethod) {
ObjectHolder<BlockBehavior> holder = ((BehaviorHolder) thisObj).getBehaviorHolder();
try {
holder.value().tick(thisObj, args, superMethod);
} catch (Exception e) {
CraftEngine.instance().logger().severe("Failed to run tick", e);
}
}
}
public static class OnPlaceInterceptor {
public static final OnPlaceInterceptor INSTANCE = new OnPlaceInterceptor();
@RuntimeType
public void intercept(@This Object thisObj, @AllArguments Object[] args, @SuperCall Callable<Object> superMethod) {
ObjectHolder<BlockBehavior> holder = ((BehaviorHolder) thisObj).getBehaviorHolder();
try {
holder.value().onPlace(thisObj, args, superMethod);
} catch (Exception e) {
CraftEngine.instance().logger().severe("Failed to run onPlace", e);
}
}
}
public static class OnBrokenAfterFallInterceptor {
public static final OnBrokenAfterFallInterceptor INSTANCE = new OnBrokenAfterFallInterceptor();
@RuntimeType
public void intercept(@This Object thisObj, @AllArguments Object[] args, @SuperCall Callable<Object> superMethod) {
ObjectHolder<BlockBehavior> holder = ((BehaviorHolder) thisObj).getBehaviorHolder();
try {
holder.value().onBrokenAfterFall(thisObj, args, superMethod);
} catch (Exception e) {
CraftEngine.instance().logger().severe("Failed to run onBrokenAfterFall", e);
}
}
}
public static class CanSurviveInterceptor {
public static final CanSurviveInterceptor INSTANCE = new CanSurviveInterceptor();
@RuntimeType
public boolean intercept(@This Object thisObj, @AllArguments Object[] args, @SuperCall Callable<Object> superMethod) {
ObjectHolder<BlockBehavior> holder = ((BehaviorHolder) thisObj).getBehaviorHolder();
try {
return holder.value().canSurvive(thisObj, args, superMethod);
} catch (Exception e) {
CraftEngine.instance().logger().severe("Failed to run canSurvive", e);
return true;
}
}
}
public static class IsBoneMealSuccessInterceptor {
public static final IsBoneMealSuccessInterceptor INSTANCE = new IsBoneMealSuccessInterceptor();
@RuntimeType
public boolean intercept(@This Object thisObj, @AllArguments Object[] args) {
ObjectHolder<BlockBehavior> holder = ((BehaviorHolder) thisObj).getBehaviorHolder();
try {
return holder.value().isBoneMealSuccess(thisObj, args);
} catch (Exception e) {
CraftEngine.instance().logger().severe("Failed to run isBoneMealSuccess", e);
return true;
}
}
}
public static class IsValidBoneMealTargetInterceptor {
public static final IsValidBoneMealTargetInterceptor INSTANCE = new IsValidBoneMealTargetInterceptor();
@RuntimeType
public boolean intercept(@This Object thisObj, @AllArguments Object[] args) {
ObjectHolder<BlockBehavior> holder = ((BehaviorHolder) thisObj).getBehaviorHolder();
try {
return holder.value().isValidBoneMealTarget(thisObj, args);
} catch (Exception e) {
CraftEngine.instance().logger().severe("Failed to run isValidBoneMealTarget", e);
return true;
}
}
}
public static class PerformBoneMealInterceptor {
public static final PerformBoneMealInterceptor INSTANCE = new PerformBoneMealInterceptor();
@RuntimeType
public void intercept(@This Object thisObj, @AllArguments Object[] args) {
ObjectHolder<BlockBehavior> holder = ((BehaviorHolder) thisObj).getBehaviorHolder();
try {
holder.value().performBoneMeal(thisObj, args);
} catch (Exception e) {
CraftEngine.instance().logger().severe("Failed to run performBoneMeal", e);
}
}
}
}

View File

@@ -0,0 +1,570 @@
package net.momirealms.craftengine.bukkit.plugin.network;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.handler.codec.MessageToMessageDecoder;
import io.netty.handler.codec.MessageToMessageEncoder;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.bukkit.plugin.network.impl.*;
import net.momirealms.craftengine.bukkit.plugin.user.BukkitServerPlayer;
import net.momirealms.craftengine.bukkit.util.Reflections;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.network.ConnectionState;
import net.momirealms.craftengine.core.plugin.network.NetWorkUser;
import net.momirealms.craftengine.core.plugin.network.NetworkManager;
import net.momirealms.craftengine.core.util.FriendlyByteBuf;
import net.momirealms.craftengine.core.util.ListMonitor;
import net.momirealms.craftengine.core.util.TriConsumer;
import net.momirealms.craftengine.core.util.VersionHelper;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
public class BukkitNetworkManager implements NetworkManager, Listener {
private static BukkitNetworkManager instance;
private static final Map<Class<?>, TriConsumer<NetWorkUser, NMSPacketEvent, Object>> nmsPacketFunctions = new HashMap<>();
private static final Map<Integer, BiConsumer<NetWorkUser, ByteBufPacketEvent>> byteBufPacketFunctions = new HashMap<>();
private static void registerNMSPacketConsumer(final TriConsumer<NetWorkUser, NMSPacketEvent, Object> function, @Nullable Class<?> packet) {
if (packet == null) return;
nmsPacketFunctions.put(packet, function);
}
private static void registerByteBufPacketConsumer(final BiConsumer<NetWorkUser, ByteBufPacketEvent> function, int id) {
byteBufPacketFunctions.put(id, function);
}
private final BiConsumer<Object, List<Object>> packetsConsumer;
private final BiConsumer<Object, Object> delayedPacketConsumer;
private final BiConsumer<Object, Object> immediatePacketConsumer;
private final BukkitCraftEngine plugin;
private final Map<ChannelPipeline, BukkitServerPlayer> users = new ConcurrentHashMap<>();
private final Map<UUID, BukkitServerPlayer> onlineUsers = new ConcurrentHashMap<>();
private final HashSet<Channel> injectedChannels = new HashSet<>();
private final PacketIds packetIds;
private static final String CONNECTION_HANDLER_NAME = "craftengine_connection_handler";
private static final String SERVER_CHANNEL_HANDLER_NAME = "craftengine_server_channel_handler";
private static final String PLAYER_CHANNEL_HANDLER_NAME = "craftengine_player_packet_handler";
private static final String PACKET_ENCODER = "craftengine_encoder";
private static final String PACKET_DECODER = "craftengine_decoder";
private boolean active;
private boolean init;
public BukkitNetworkManager(BukkitCraftEngine plugin) {
this.plugin = plugin;
if (VersionHelper.isVersionNewerThan1_21_2()) {
this.packetIds = new PacketIds1_21_2();
} else if (VersionHelper.isVersionNewerThan1_20_5()) {
this.packetIds = new PacketIds1_20_5();
} else if (VersionHelper.isVersionNewerThan1_20_3()) {
this.packetIds = new PacketIds1_20_3();
} else if (VersionHelper.isVersionNewerThan1_20_2()) {
this.packetIds = new PacketIds1_20_2();
} else {
this.packetIds = new PacketIds1_20();
}
this.registerConsumers();
this.packetsConsumer = ((serverPlayer, packets) -> {
try {
Object bundle = Reflections.constructor$ClientboundBundlePacket.newInstance(packets);
Reflections.method$ServerGamePacketListenerImpl$sendPacket.invoke(
Reflections.field$ServerPlayer$connection.get(serverPlayer), bundle);
} catch (ReflectiveOperationException e) {
plugin.logger().warn("Failed to create bundle packet", e);
}
});
this.delayedPacketConsumer = (serverPlayer, packet) -> {
try {
Reflections.method$ServerGamePacketListenerImpl$sendPacket.invoke(
Reflections.field$ServerPlayer$connection.get(serverPlayer), packet);
} catch (ReflectiveOperationException e) {
plugin.logger().warn("Failed to invoke send packet", e);
}
};
this.immediatePacketConsumer = (serverPlayer, packet) -> {
try {
Reflections.method$Connection$sendPacketImmediate.invoke(Reflections.field$ServerCommonPacketListenerImpl$connection.get(Reflections.field$ServerPlayer$connection.get(serverPlayer)), packet, null, true);
} catch (ReflectiveOperationException e) {
plugin.logger().warn("Failed to invoke send packet", e);
}
};
this.active = true;
instance = this;
}
private void registerConsumers() {
registerNMSPacketConsumer(PacketConsumers.LEVEL_CHUNK_WITH_LIGHT, Reflections.clazz$ClientboundLevelChunkWithLightPacket);
registerNMSPacketConsumer(PacketConsumers.PLAYER_ACTION, Reflections.clazz$ServerboundPlayerActionPacket);
registerNMSPacketConsumer(PacketConsumers.SWING_HAND, Reflections.clazz$ServerboundSwingPacket);
registerNMSPacketConsumer(PacketConsumers.USE_ITEM_ON, Reflections.clazz$ServerboundUseItemOnPacket);
registerNMSPacketConsumer(PacketConsumers.PICK_ITEM_FROM_BLOCK, Reflections.clazz$ServerboundPickItemFromBlockPacket);
registerNMSPacketConsumer(PacketConsumers.SET_CREATIVE_SLOT, Reflections.clazz$ServerboundSetCreativeModeSlotPacket);
registerNMSPacketConsumer(PacketConsumers.ADD_ENTITY, Reflections.clazz$ClientboundAddEntityPacket);
registerNMSPacketConsumer(PacketConsumers.LOGIN, Reflections.clazz$ClientboundLoginPacket);
registerNMSPacketConsumer(PacketConsumers.RESPAWN, Reflections.clazz$ClientboundRespawnPacket);
registerNMSPacketConsumer(PacketConsumers.INTERACT_ENTITY, Reflections.clazz$ServerboundInteractPacket);
registerNMSPacketConsumer(PacketConsumers.REMOVE_ENTITY, Reflections.clazz$ClientboundRemoveEntitiesPacket);
registerNMSPacketConsumer(PacketConsumers.SYNC_ENTITY_POSITION, Reflections.clazz$ClientboundEntityPositionSyncPacket);
registerNMSPacketConsumer(PacketConsumers.MOVE_ENTITY, Reflections.clazz$ClientboundMoveEntityPacket$Pos);
registerNMSPacketConsumer(PacketConsumers.PICK_ITEM_FROM_ENTITY, Reflections.clazz$ServerboundPickItemFromEntityPacket);
registerNMSPacketConsumer(PacketConsumers.SOUND, Reflections.clazz$ClientboundSoundPacket);
registerByteBufPacketConsumer(PacketConsumers.SECTION_BLOCK_UPDATE, this.packetIds.clientboundSectionBlocksUpdatePacket());
registerByteBufPacketConsumer(PacketConsumers.BLOCK_UPDATE, this.packetIds.clientboundBlockUpdatePacket());
registerByteBufPacketConsumer(PacketConsumers.LEVEL_PARTICLE, this.packetIds.clientboundLevelParticlesPacket());
registerByteBufPacketConsumer(PacketConsumers.LEVEL_EVENT, this.packetIds.clientboundLevelEventPacket());
}
public static BukkitNetworkManager instance() {
return instance;
}
@EventHandler(priority = EventPriority.LOWEST)
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
BukkitServerPlayer user = (BukkitServerPlayer) getUser(player);
if (user != null) {
user.setPlayer(player);
this.onlineUsers.put(player.getUniqueId(), user);
}
}
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
Player player = event.getPlayer();
Channel channel = getChannel(player);
NetWorkUser user = removeUser(channel);
if (user == null) return;
handleDisconnection(channel);
this.onlineUsers.remove(player.getUniqueId());
}
@Override
public Collection<BukkitServerPlayer> onlineUsers() {
return onlineUsers.values();
}
@Override
public void init() {
if (init) return;
try {
Object server = Reflections.method$MinecraftServer$getServer.invoke(null);
Object serverConnection = Reflections.field$MinecraftServer$connection.get(server);
@SuppressWarnings("unchecked")
List<ChannelFuture> channels = (List<ChannelFuture>) Reflections.field$ServerConnectionListener$channels.get(serverConnection);
ListMonitor<ChannelFuture> monitor = new ListMonitor<>(channels, (future) -> {
if (!active) return;
Channel channel = future.channel();
injectServerChannel(channel);
injectedChannels.add(channel);
}, (object) -> {});
Reflections.field$ServerConnectionListener$channels.set(serverConnection, monitor);
init = true;
} catch (ReflectiveOperationException e) {
plugin.logger().warn("Failed to init server connection", e);
}
}
@Override
public void enable() {
Bukkit.getPluginManager().registerEvents(this, plugin.bootstrap());
}
@Override
public void shutdown() {
HandlerList.unregisterAll(this);
for (Channel channel : injectedChannels) {
uninjectServerChannel(channel);
}
for (Player player : Bukkit.getOnlinePlayers()) {
handleDisconnection(getChannel(player));
}
injectedChannels.clear();
active = false;
}
@Override
public void setUser(Channel channel, NetWorkUser user) {
ChannelPipeline pipeline = channel.pipeline();
users.put(pipeline, (BukkitServerPlayer) user);
}
@Override
public NetWorkUser getUser(Channel channel) {
ChannelPipeline pipeline = channel.pipeline();
return users.get(pipeline);
}
@Override
public NetWorkUser removeUser(Channel channel) {
ChannelPipeline pipeline = channel.pipeline();
return users.remove(pipeline);
}
@Override
public Channel getChannel(net.momirealms.craftengine.core.entity.player.Player player) {
return getChannel((Player) player.platformPlayer());
}
public NetWorkUser getUser(Player player) {
return getUser(getChannel(player));
}
public NetWorkUser getOnlineUser(Player player) {
return onlineUsers.get(player.getUniqueId());
}
public Channel getChannel(Player player) {
try {
return (Channel) Reflections.field$Channel.get(
Reflections.field$NetworkManager.get(
Reflections.field$ServerPlayer$connection.get(
Reflections.method$CraftPlayer$getHandle.invoke(player)
)
)
);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
public void sendPacket(@NotNull Player player, @NotNull Object packet) {
try {
Object serverPlayer = Reflections.method$CraftPlayer$getHandle.invoke(player);
this.immediatePacketConsumer.accept(serverPlayer, packet);
} catch (Exception e) {
this.plugin.logger().warn("Failed to send packet", e);
}
}
public void sendPacket(@NotNull NetWorkUser player, Object packet, boolean immediately) {
if (immediately) {
this.immediatePacketConsumer.accept(player.serverPlayer(), packet);
} else {
this.delayedPacketConsumer.accept(player.serverPlayer(), packet);
}
}
public void sendPackets(@NotNull NetWorkUser player, List<Object> packet) {
this.packetsConsumer.accept(player.serverPlayer(), packet);
}
private void injectServerChannel(Channel serverChannel) {
ChannelPipeline pipeline = serverChannel.pipeline();
ChannelHandler connectionHandler = pipeline.get(CONNECTION_HANDLER_NAME);
if (connectionHandler != null) {
pipeline.remove(CONNECTION_HANDLER_NAME);
}
if (pipeline.get("SpigotNettyServerChannelHandler#0") != null) {
pipeline.addAfter("SpigotNettyServerChannelHandler#0", CONNECTION_HANDLER_NAME, new ServerChannelHandler());
} else if (pipeline.get("floodgate-init") != null) {
pipeline.addAfter("floodgate-init", CONNECTION_HANDLER_NAME, new ServerChannelHandler());
} else if (pipeline.get("MinecraftPipeline#0") != null) {
pipeline.addAfter("MinecraftPipeline#0", CONNECTION_HANDLER_NAME, new ServerChannelHandler());
} else {
pipeline.addFirst(CONNECTION_HANDLER_NAME, new ServerChannelHandler());
}
for (Player player : Bukkit.getOnlinePlayers()) {
Channel channel = getChannel(player);
NetWorkUser user = getUser(player);
if (user == null) {
user = new BukkitServerPlayer(plugin, channel);
((BukkitServerPlayer) user).setPlayer(player);
injectChannel(channel, ConnectionState.PLAY);
}
}
}
private void uninjectServerChannel(Channel channel) {
if (channel.pipeline().get(CONNECTION_HANDLER_NAME) != null) {
channel.pipeline().remove(CONNECTION_HANDLER_NAME);
}
}
public void handleDisconnection(Channel channel) {
NetWorkUser user = removeUser(channel);
if (user == null) return;
if (channel.pipeline().get(PLAYER_CHANNEL_HANDLER_NAME) != null) {
channel.pipeline().remove(PLAYER_CHANNEL_HANDLER_NAME);
}
if (channel.pipeline().get(PACKET_ENCODER) != null) {
channel.pipeline().remove(PACKET_ENCODER);
}
if (channel.pipeline().get(PACKET_DECODER) != null) {
channel.pipeline().remove(PACKET_DECODER);
}
}
public class ServerChannelHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(@NotNull ChannelHandlerContext context, @NotNull Object c) throws Exception {
Channel channel = (Channel) c;
channel.pipeline().addLast(SERVER_CHANNEL_HANDLER_NAME, new PreChannelInitializer());
super.channelRead(context, c);
}
}
public class PreChannelInitializer extends ChannelInboundHandlerAdapter {
private static final InternalLogger logger = InternalLoggerFactory.getInstance(ChannelInitializer.class);
@Override
public void channelRegistered(ChannelHandlerContext context) {
try {
injectChannel(context.channel(), ConnectionState.HANDSHAKING);
} catch (Throwable t) {
exceptionCaught(context, t);
} finally {
ChannelPipeline pipeline = context.pipeline();
if (pipeline.context(this) != null) {
pipeline.remove(this);
}
}
context.pipeline().fireChannelRegistered();
}
@Override
public void exceptionCaught(ChannelHandlerContext context, Throwable t) {
PreChannelInitializer.logger.warn("Failed to inject channel: " + context.channel(), t);
context.close();
}
}
public void injectChannel(Channel channel, ConnectionState state) {
if (isFakeChannel(channel)) {
return;
}
BukkitServerPlayer user = new BukkitServerPlayer(plugin, channel);
if (channel.pipeline().get("splitter") == null) {
channel.close();
return;
}
ChannelPipeline pipeline = channel.pipeline();
if (pipeline.get(PACKET_ENCODER) != null) {
pipeline.remove(PACKET_ENCODER);
}
if (pipeline.get(PACKET_DECODER) != null) {
pipeline.remove(PACKET_DECODER);
}
for (Map.Entry<String, ChannelHandler> entry : pipeline.toMap().entrySet()) {
if (Reflections.clazz$Connection.isAssignableFrom(entry.getValue().getClass())) {
pipeline.addBefore(entry.getKey(), PLAYER_CHANNEL_HANDLER_NAME, new PluginChannelHandler(user));
break;
}
}
String decoderName = pipeline.names().contains("inbound_config") ? "inbound_config" : "decoder";
pipeline.addBefore(decoderName, PACKET_DECODER, new PluginChannelDecoder(user));
String encoderName = pipeline.names().contains("outbound_config") ? "outbound_config" : "encoder";
pipeline.addBefore(encoderName, PACKET_ENCODER, new PluginChannelEncoder(user));
channel.closeFuture().addListener((ChannelFutureListener) future -> handleDisconnection(user.nettyChannel()));
setUser(channel, user);
}
public static boolean isFakeChannel(Object channel) {
return channel.getClass().getSimpleName().equals("FakeChannel")
|| channel.getClass().getSimpleName().equals("SpoofedChannel");
}
public class PluginChannelHandler extends ChannelDuplexHandler {
private final NetWorkUser player;
public PluginChannelHandler(NetWorkUser player) {
this.player = player;
}
@Override
public void write(ChannelHandlerContext context, Object packet, ChannelPromise channelPromise) throws Exception {
try {
NMSPacketEvent event = new NMSPacketEvent(packet);
onNMSPacketSend(player, event, packet);
if (event.isCancelled()) return;
super.write(context, packet, channelPromise);
channelPromise.addListener((p) -> {
for (Runnable task : event.getDelayedTasks()) {
task.run();
}
});
} catch (Throwable e) {
plugin.logger().severe("An error occurred when reading packets", e);
super.write(context, packet, channelPromise);
}
}
@Override
public void channelRead(@NotNull ChannelHandlerContext context, @NotNull Object packet) throws Exception {
NMSPacketEvent event = new NMSPacketEvent(packet);
onNMSPacketReceive(player, event, packet);
if (event.isCancelled()) return;
super.channelRead(context, packet);
}
}
public class PluginChannelEncoder extends MessageToMessageEncoder<ByteBuf> {
private final NetWorkUser player;
private boolean handledCompression = false;
public PluginChannelEncoder(NetWorkUser player) {
this.player = player;
}
public PluginChannelEncoder(PluginChannelEncoder encoder) {
this.player = encoder.player;
this.handledCompression = encoder.handledCompression;
}
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) {
boolean needCompression = !handledCompression && handleCompression(channelHandlerContext, byteBuf);
this.onByteBufSend(byteBuf);
if (needCompression) {
compress(channelHandlerContext, byteBuf);
}
if (byteBuf.isReadable()) {
list.add(byteBuf.retain());
}
}
private boolean handleCompression(ChannelHandlerContext ctx, ByteBuf buffer) {
if (handledCompression) return false;
int compressIndex = ctx.pipeline().names().indexOf("compress");
if (compressIndex == -1) return false;
handledCompression = true;
int encoderIndex = ctx.pipeline().names().indexOf(PACKET_ENCODER);
if (encoderIndex == -1) return false;
if (compressIndex > encoderIndex) {
decompress(ctx, buffer, buffer);
PluginChannelEncoder encoder = (PluginChannelEncoder) ctx.pipeline().remove(PACKET_ENCODER);
String encoderName = ctx.pipeline().names().contains("outbound_config") ? "outbound_config" : "encoder";
ctx.pipeline().addBefore(encoderName, PACKET_ENCODER, new PluginChannelEncoder(encoder));
return true;
}
return false;
}
private void onByteBufSend(ByteBuf buffer) {
// I don't care packets before PLAY for the moment
if (player.encoderState() != ConnectionState.PLAY) return;
int size = buffer.readableBytes();
if (size != 0) {
FriendlyByteBuf buf = new FriendlyByteBuf(buffer);
int preProcessIndex = buf.readerIndex();
int packetId = buf.readVarInt();
ByteBufPacketEvent event = new ByteBufPacketEvent(packetId, buf);
BukkitNetworkManager.this.handleByteBufPacket(this.player, event);
if (event.isCancelled()) {
buf.clear();
} else if (!event.changed()) {
buf.readerIndex(preProcessIndex);
}
}
}
}
public class PluginChannelDecoder extends MessageToMessageDecoder<ByteBuf> {
private final NetWorkUser player;
public PluginChannelDecoder(NetWorkUser player) {
this.player = player;
}
@Override
protected void decode(ChannelHandlerContext context, ByteBuf byteBuf, List<Object> list) {
if (byteBuf.isReadable()) {
list.add(byteBuf.retain());
}
}
}
private void onNMSPacketReceive(NetWorkUser user, NMSPacketEvent event, Object packet) {
handleNMSPacket(user, event, packet);
}
@SuppressWarnings("unchecked")
private void onNMSPacketSend(NetWorkUser player, NMSPacketEvent event, Object packet) throws ReflectiveOperationException {
if (Reflections.clazz$ClientboundBundlePacket.isInstance(packet)) {
Iterable<Object> packets = (Iterable<Object>) Reflections.field$BundlePacket$packets.get(packet);
for (Object p : packets) {
onNMSPacketSend(player, event, p);
}
} else {
handleNMSPacket(player, event, packet);
}
}
protected void handleNMSPacket(NetWorkUser user, NMSPacketEvent event, Object packet) {
Optional.ofNullable(nmsPacketFunctions.get(packet.getClass()))
.ifPresent(function -> function.accept(user, event, packet));
}
protected void handleByteBufPacket(NetWorkUser user, ByteBufPacketEvent event) {
int packetID = event.packetID();
Optional.ofNullable(byteBufPacketFunctions.get(packetID))
.ifPresent(function -> function.accept(user, event));
}
private void compress(ChannelHandlerContext ctx, ByteBuf input) {
ChannelHandler compressor = ctx.pipeline().get("compress");
ByteBuf temp = ctx.alloc().buffer();
try {
if (compressor != null) {
callEncode(compressor, ctx, input, temp);
}
} finally {
input.clear().writeBytes(temp);
temp.release();
}
}
private void decompress(ChannelHandlerContext ctx, ByteBuf input, ByteBuf output) {
ChannelHandler decompressor = ctx.pipeline().get("decompress");
if (decompressor != null) {
ByteBuf temp = (ByteBuf) callDecode(decompressor, ctx, input).get(0);
try {
output.clear().writeBytes(temp);
} finally {
temp.release();
}
}
}
private static void callEncode(Object encoder, ChannelHandlerContext ctx, ByteBuf msg, ByteBuf output) {
try {
Reflections.method$messageToByteEncoder$encode.invoke(encoder, ctx, msg, output);
} catch (ReflectiveOperationException e) {
CraftEngine.instance().logger().warn("Failed to call encode", e);
}
}
public static List<Object> callDecode(Object decoder, Object ctx, Object input) {
List<Object> output = new ArrayList<>();
try {
Reflections.method$byteToMessageDecoder$decode.invoke(decoder, ctx, input, output);
} catch (ReflectiveOperationException e) {
CraftEngine.instance().logger().warn("Failed to call decode", e);
}
return output;
}
}

View File

@@ -0,0 +1,59 @@
package net.momirealms.craftengine.bukkit.plugin.network;
import net.momirealms.craftengine.core.util.Cancellable;
import net.momirealms.craftengine.core.util.FriendlyByteBuf;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
public class ByteBufPacketEvent implements Cancellable {
private boolean cancelled;
private List<Runnable> delayedTasks = null;
private final FriendlyByteBuf buf;
private boolean changed;
private final int packetID;
public ByteBufPacketEvent(int packetID, FriendlyByteBuf buf) {
this.buf = buf;
this.packetID = packetID;
}
public int packetID() {
return packetID;
}
public FriendlyByteBuf getBuffer() {
return buf;
}
public void setChanged(boolean dirty) {
this.changed = dirty;
}
public boolean changed() {
return changed;
}
public void addDelayedTask(Runnable task) {
if (delayedTasks == null) {
delayedTasks = new ArrayList<>();
}
delayedTasks.add(task);
}
public List<Runnable> getDelayedTasks() {
return Optional.ofNullable(delayedTasks).orElse(Collections.emptyList());
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public void setCancelled(boolean cancel) {
cancelled = cancel;
}
}

View File

@@ -0,0 +1,43 @@
package net.momirealms.craftengine.bukkit.plugin.network;
import net.momirealms.craftengine.core.util.Cancellable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
public class NMSPacketEvent implements Cancellable {
private boolean cancelled;
private List<Runnable> delayedTasks = null;
private final Object packet;
public NMSPacketEvent(Object packet) {
this.packet = packet;
}
public Object getPacket() {
return packet;
}
public void addDelayedTask(Runnable task) {
if (delayedTasks == null) {
delayedTasks = new ArrayList<>();
}
delayedTasks.add(task);
}
public List<Runnable> getDelayedTasks() {
return Optional.ofNullable(delayedTasks).orElse(Collections.emptyList());
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public void setCancelled(boolean cancel) {
cancelled = cancel;
}
}

View File

@@ -0,0 +1,664 @@
package net.momirealms.craftengine.bukkit.plugin.network;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import it.unimi.dsi.fastutil.ints.IntList;
import net.momirealms.craftengine.bukkit.api.CraftEngineFurniture;
import net.momirealms.craftengine.bukkit.api.event.FurnitureBreakEvent;
import net.momirealms.craftengine.bukkit.api.event.FurnitureInteractEvent;
import net.momirealms.craftengine.bukkit.block.BukkitBlockManager;
import net.momirealms.craftengine.bukkit.entity.furniture.BukkitFurnitureManager;
import net.momirealms.craftengine.bukkit.entity.furniture.LoadedFurniture;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.bukkit.plugin.user.BukkitServerPlayer;
import net.momirealms.craftengine.bukkit.util.*;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.entity.player.InteractionHand;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.config.ConfigManager;
import net.momirealms.craftengine.core.plugin.network.ConnectionState;
import net.momirealms.craftengine.core.plugin.network.NetWorkUser;
import net.momirealms.craftengine.core.util.*;
import net.momirealms.craftengine.core.world.BlockPos;
import net.momirealms.craftengine.core.world.chunk.Palette;
import net.momirealms.craftengine.core.world.chunk.PalettedContainer;
import net.momirealms.craftengine.core.world.chunk.packet.MCSection;
import org.bukkit.*;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.util.RayTraceResult;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiConsumer;
public class PacketConsumers {
private static int[] mappings;
private static IntIdentityList BLOCK_LIST;
private static IntIdentityList BIOME_LIST;
public static void init(Map<Integer, Integer> map, int registrySize) {
mappings = new int[registrySize];
Arrays.fill(mappings, -1);
for (int i = 0; i < registrySize; i++) {
mappings[i] = i;
}
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
mappings[entry.getKey()] = entry.getValue();
}
BLOCK_LIST = new IntIdentityList(registrySize);
BIOME_LIST = new IntIdentityList(RegistryUtils.currentBiomeRegistrySize());
}
public static int remap(int stateId) {
return mappings[stateId];
}
public static final TriConsumer<NetWorkUser, NMSPacketEvent, Object> LEVEL_CHUNK_WITH_LIGHT = (user, event, packet) -> {
try {
BukkitServerPlayer player = (BukkitServerPlayer) user;
Object chunkData = Reflections.field$ClientboundLevelChunkWithLightPacket$chunkData.get(packet);
byte[] buffer = (byte[]) Reflections.field$ClientboundLevelChunkPacketData$buffer.get(chunkData);
ByteBuf buf = Unpooled.copiedBuffer(buffer);
FriendlyByteBuf friendlyByteBuf = new FriendlyByteBuf(buf);
FriendlyByteBuf newBuf = new FriendlyByteBuf(Unpooled.buffer());
for (int i = 0, count = player.clientSideSectionCount(); i < count; i++) {
try {
MCSection mcSection = new MCSection(BLOCK_LIST, BIOME_LIST);
mcSection.readPacket(friendlyByteBuf);
PalettedContainer<Integer> container = mcSection.blockStateContainer();
Palette<Integer> palette = container.data().palette();
if (palette.canRemap()) {
palette.remap(PacketConsumers::remap);
} else {
for (int j = 0; j < 4096; j ++) {
int state = container.get(j);
int newState = remap(state);
if (newState != state) {
container.set(j, newState);
}
}
}
mcSection.writePacket(newBuf);
} catch (Exception e) {
break;
}
}
Reflections.field$ClientboundLevelChunkPacketData$buffer.set(chunkData, newBuf.array());
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to handle ClientboundLevelChunkWithLightPacket", e);
}
};
public static final BiConsumer<NetWorkUser, ByteBufPacketEvent> SECTION_BLOCK_UPDATE = (user, event) -> {
try {
FriendlyByteBuf buf = event.getBuffer();
long pos = buf.readLong();
int blocks = buf.readVarInt();
short[] positions = new short[blocks];
int[] states = new int[blocks];
for (int i = 0; i < blocks; i++) {
long k = buf.readVarLong();
positions[i] = (short) ((int) (k & 4095L));
states[i] = remap((int) (k >>> 12));
}
buf.clear();
buf.writeVarInt(event.packetID());
buf.writeLong(pos);
buf.writeVarInt(blocks);
for (int i = 0; i < blocks; i ++) {
buf.writeVarLong((long) states[i] << 12 | positions[i]);
}
event.setChanged(true);
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to handle ClientboundSectionBlocksUpdatePacket", e);
}
};
public static final BiConsumer<NetWorkUser, ByteBufPacketEvent> BLOCK_UPDATE = (user, event) -> {
try {
FriendlyByteBuf buf = event.getBuffer();
BlockPos pos = buf.readBlockPos(buf);
int before = buf.readVarInt();
int state = remap(before);
if (state == before) {
return;
}
event.setChanged(true);
buf.clear();
buf.writeVarInt(event.packetID());
buf.writeBlockPos(pos);
buf.writeVarInt(state);
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to handle ClientboundBlockUpdatePacket", e);
}
};
public static final BiConsumer<NetWorkUser, ByteBufPacketEvent> LEVEL_EVENT = (user, event) -> {
try {
FriendlyByteBuf buf = event.getBuffer();
int eventId = buf.readInt();
if (eventId != 2001) return;
BlockPos blockPos = buf.readBlockPos(buf);
int state = buf.readInt();
boolean global = buf.readBoolean();
int newState = remap(state);
if (newState == state) {
return;
}
event.setChanged(true);
buf.clear();
buf.writeVarInt(event.packetID());
buf.writeInt(eventId);
buf.writeBlockPos(blockPos);
buf.writeInt(newState);
buf.writeBoolean(global);
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to handle ClientboundLevelEventPacket", e);
}
};
public static final BiConsumer<NetWorkUser, ByteBufPacketEvent> LEVEL_PARTICLE = (user, event) -> {
try {
FriendlyByteBuf buf = event.getBuffer();
Object mcByteBuf;
Method writeMethod;
if (VersionHelper.isVersionNewerThan1_20_5()) {
mcByteBuf = Reflections.constructor$RegistryFriendlyByteBuf.newInstance(buf, Reflections.instance$registryAccess);
writeMethod = Reflections.method$ClientboundLevelParticlesPacket$write;
} else {
mcByteBuf = Reflections.constructor$FriendlyByteBuf.newInstance(event.getBuffer().source());
writeMethod = Reflections.method$Packet$write;
}
Object packet = Reflections.constructor$ClientboundLevelParticlesPacket.newInstance(mcByteBuf);
Object option = Reflections.field$ClientboundLevelParticlesPacket$particle.get(packet);
if (option == null) return;
if (!Reflections.clazz$BlockParticleOption.isInstance(option)) return;
Object blockState = Reflections.field$BlockParticleOption$blockState.get(option);
int id = BlockStateUtils.blockStateToId(blockState);
int remapped = remap(id);
if (remapped == id) return;
Reflections.field$BlockParticleOption$blockState.set(option, BlockStateUtils.idToBlockState(remapped));
event.setChanged(true);
buf.clear();
buf.writeVarInt(event.packetID());
writeMethod.invoke(packet, mcByteBuf);
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to handle ClientboundLevelParticlesPacket", e);
}
};
public static final TriConsumer<NetWorkUser, NMSPacketEvent, Object> PLAYER_ACTION = (user, event, packet) -> {
try {
if (!user.isOnline()) return;
BukkitServerPlayer player = (BukkitServerPlayer) user;
Player platformPlayer = player.platformPlayer();
World world = platformPlayer.getWorld();
Object blockPos = Reflections.field$ServerboundPlayerActionPacket$pos.get(packet);
BlockPos pos = new BlockPos(
(int) Reflections.field$Vec3i$x.get(blockPos),
(int) Reflections.field$Vec3i$y.get(blockPos),
(int) Reflections.field$Vec3i$z.get(blockPos)
);
if (VersionHelper.isFolia()) {
BukkitCraftEngine.instance().scheduler().sync().run(() -> {
try {
handlePlayerActionPacketOnMainThread(player, world, pos, packet);
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to handle ServerboundPlayerActionPacket", e);
}
}, world, pos.x() >> 4, pos.z() >> 4);
} else {
handlePlayerActionPacketOnMainThread(player, world, pos, packet);
}
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to handle ServerboundPlayerActionPacket", e);
}
};
private static void handlePlayerActionPacketOnMainThread(BukkitServerPlayer player, World world, BlockPos pos, Object packet) throws Exception {
Object action = Reflections.field$ServerboundPlayerActionPacket$action.get(packet);
if (action == Reflections.instance$ServerboundPlayerActionPacket$Action$START_DESTROY_BLOCK) {
Object serverLevel = Reflections.field$CraftWorld$ServerLevel.get(world);
Object blockState = Reflections.method$BlockGetter$getBlockState.invoke(serverLevel, LocationUtils.toBlockPos(pos));
int stateId = BlockStateUtils.blockStateToId(blockState);
// not a custom block
if (BlockStateUtils.isVanillaBlock(stateId)) {
if (ConfigManager.enableSoundSystem()) {
Object blockOwner = Reflections.field$StateHolder$owner.get(blockState);
if (BukkitBlockManager.instance().isBlockSoundRemoved(blockOwner)) {
player.startMiningBlock(world, pos, blockState, false, null);
return;
}
}
if (player.isMiningBlock() || player.shouldSyncAttribute()) {
player.stopMiningBlock();
}
return;
}
player.startMiningBlock(world, pos, blockState, true, BukkitBlockManager.instance().getImmutableBlockStateUnsafe(stateId));
} else if (action == Reflections.instance$ServerboundPlayerActionPacket$Action$ABORT_DESTROY_BLOCK) {
if (player.isMiningBlock()) {
player.abortMiningBlock();
}
} else if (action == Reflections.instance$ServerboundPlayerActionPacket$Action$STOP_DESTROY_BLOCK) {
if (player.isMiningBlock()) {
player.stopMiningBlock();
}
}
}
public static final TriConsumer<NetWorkUser, NMSPacketEvent, Object> SWING_HAND = (user, event, packet) -> {
try {
if (!user.isOnline()) return;
BukkitServerPlayer player = (BukkitServerPlayer) user;
if (!player.isMiningBlock()) return;
Object hand = Reflections.field$ServerboundSwingPacket$hand.get(packet);
if (hand == Reflections.instance$InteractionHand$MAIN_HAND) {
player.onSwingHand();
}
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to handle ServerboundSwingPacket", e);
}
};
public static final TriConsumer<NetWorkUser, NMSPacketEvent, Object> USE_ITEM_ON = (user, event, packet) -> {
try {
if (!user.isOnline()) return;
BukkitServerPlayer player = (BukkitServerPlayer) user;
if (player.isMiningBlock()) {
player.stopMiningBlock();
}
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to handle ServerboundUseItemOnPacket", e);
}
};
public static final TriConsumer<NetWorkUser, NMSPacketEvent, Object> RESPAWN = (user, event, packet) -> {
try {
BukkitServerPlayer player = (BukkitServerPlayer) user;
Object dimensionKey;
if (!VersionHelper.isVersionNewerThan1_20_2()) {
dimensionKey = Reflections.field$ClientboundRespawnPacket$dimension.get(packet);
} else {
Object commonInfo = Reflections.field$ClientboundRespawnPacket$commonPlayerSpawnInfo.get(packet);
dimensionKey = Reflections.field$CommonPlayerSpawnInfo$dimension.get(commonInfo);
}
Object location = Reflections.field$ResourceKey$location.get(dimensionKey);
World world = Bukkit.getWorld(Objects.requireNonNull(NamespacedKey.fromString(location.toString())));
if (world != null) {
int sectionCount = (world.getMaxHeight() - world.getMinHeight()) / 16;
player.setClientSideSectionCount(sectionCount);
player.setClientSideDimension(Key.of(location.toString()));
} else {
CraftEngine.instance().logger().warn("Failed to handle ClientboundRespawnPacket: World " + location + " does not exist");
}
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to handle ClientboundRespawnPacket", e);
}
};
public static final TriConsumer<NetWorkUser, NMSPacketEvent, Object> LOGIN = (user, event, packet) -> {
try {
BukkitServerPlayer player = (BukkitServerPlayer) user;
player.setConnectionState(ConnectionState.PLAY);
Object dimensionKey;
if (!VersionHelper.isVersionNewerThan1_20_2()) {
dimensionKey = Reflections.field$ClientboundLoginPacket$dimension.get(packet);
} else {
Object commonInfo = Reflections.field$ClientboundLoginPacket$commonPlayerSpawnInfo.get(packet);
dimensionKey = Reflections.field$CommonPlayerSpawnInfo$dimension.get(commonInfo);
}
Object location = Reflections.field$ResourceKey$location.get(dimensionKey);
World world = Bukkit.getWorld(Objects.requireNonNull(NamespacedKey.fromString(location.toString())));
if (world != null) {
int sectionCount = (world.getMaxHeight() - world.getMinHeight()) / 16;
player.setClientSideSectionCount(sectionCount);
player.setClientSideDimension(Key.of(location.toString()));
} else {
CraftEngine.instance().logger().warn("Failed to handle ClientboundLoginPacket: World " + location + " does not exist");
}
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to handle ClientboundLoginPacket", e);
}
};
// 1.21.4-
// We can't find the best solution, we can only keep the feel as good as possible
// When the hotbar is full, the latest creative mode inventory can only be accessed when the player opens the inventory screen. Currently, it is not worth further handling this issue.
public static final TriConsumer<NetWorkUser, NMSPacketEvent, Object> SET_CREATIVE_SLOT = (user, event, packet) -> {
try {
if (VersionHelper.isVersionNewerThan1_21_4()) return;
if (!user.isOnline()) return;
BukkitServerPlayer player = (BukkitServerPlayer) user;
if (VersionHelper.isFolia()) {
BukkitCraftEngine.instance().scheduler().sync().run(() -> {
try {
handleSetCreativeSlotPacketOnMainThread(player, packet);
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to handle ServerboundSetCreativeModeSlotPacket", e);
}
}, (World) player.level().getHandle(), (MCUtils.fastFloor(player.x())) >> 4, (MCUtils.fastFloor(player.z())) >> 4);
} else {
handleSetCreativeSlotPacketOnMainThread(player, packet);
}
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to handle ServerboundSetCreativeModeSlotPacket", e);
}
};
private static void handleSetCreativeSlotPacketOnMainThread(BukkitServerPlayer player, Object packet) throws Exception {
Player bukkitPlayer = player.platformPlayer();
if (bukkitPlayer == null) return;
if (bukkitPlayer.getGameMode() != GameMode.CREATIVE) return;
int slot = VersionHelper.isVersionNewerThan1_20_5() ? Reflections.field$ServerboundSetCreativeModeSlotPacket$slotNum.getShort(packet) : Reflections.field$ServerboundSetCreativeModeSlotPacket$slotNum.getInt(packet);
if (slot < 36 || slot > 44) return;
ItemStack item = (ItemStack) Reflections.method$CraftItemStack$asCraftMirror.invoke(null, Reflections.field$ServerboundSetCreativeModeSlotPacket$itemStack.get(packet));
if (ItemUtils.isEmpty(item)) return;
if (slot - 36 != bukkitPlayer.getInventory().getHeldItemSlot()) {
return;
}
double interactionRange = player.getInteractionRange();
// do ray trace to get current block
RayTraceResult result = bukkitPlayer.rayTraceBlocks(interactionRange, FluidCollisionMode.NEVER);
if (result == null) return;
org.bukkit.block.Block hitBlock = result.getHitBlock();
if (hitBlock == null) return;
ImmutableBlockState state = BukkitBlockManager.instance().getImmutableBlockState(BlockStateUtils.blockDataToId(hitBlock.getBlockData()));
// not a custom block
if (state == null || state.isEmpty()) return;
Key itemId = state.settings().itemId();
// no item available
if (itemId == null) return;
BlockData data = BlockStateUtils.createBlockData(state.vanillaBlockState().handle());
// compare item
if (data == null || !data.getMaterial().equals(item.getType())) return;
ItemStack itemStack = BukkitCraftEngine.instance().itemManager().buildCustomItemStack(itemId, player);
if (ItemUtils.isEmpty(itemStack)) {
CraftEngine.instance().logger().warn("Item: " + itemId + " is not a valid item");
return;
}
PlayerInventory inventory = bukkitPlayer.getInventory();
int sameItemSlot = -1;
int emptySlot = -1;
for (int i = 0; i < 9 + 27; i++) {
ItemStack invItem = inventory.getItem(i);
if (ItemUtils.isEmpty(invItem)) {
if (emptySlot == -1 && i < 9) emptySlot = i;
continue;
}
if (invItem.getType().equals(itemStack.getType()) && invItem.getItemMeta().equals(itemStack.getItemMeta())) {
if (sameItemSlot == -1) sameItemSlot = i;
}
}
if (sameItemSlot != -1) {
if (sameItemSlot < 9) {
inventory.setHeldItemSlot(sameItemSlot);
ItemStack previousItem = inventory.getItem(slot - 36);
BukkitCraftEngine.instance().scheduler().sync().runDelayed(() -> inventory.setItem(slot - 36, previousItem));
} else {
ItemStack sameItem = inventory.getItem(sameItemSlot);
int finalSameItemSlot = sameItemSlot;
BukkitCraftEngine.instance().scheduler().sync().runDelayed(() -> {
inventory.setItem(finalSameItemSlot, new ItemStack(Material.AIR));
inventory.setItem(slot - 36, sameItem);
});
}
} else {
if (item.getAmount() == 1) {
if (ItemUtils.isEmpty(inventory.getItem(slot - 36))) {
BukkitCraftEngine.instance().scheduler().sync().runDelayed(() -> inventory.setItem(slot - 36, itemStack));
return;
}
if (emptySlot != -1) {
inventory.setHeldItemSlot(emptySlot);
inventory.setItem(emptySlot, itemStack);
} else {
BukkitCraftEngine.instance().scheduler().sync().runDelayed(() -> inventory.setItem(slot - 36, itemStack));
}
}
}
}
// 1.21.4+
public static final TriConsumer<NetWorkUser, NMSPacketEvent, Object> PICK_ITEM_FROM_BLOCK = (user, event, packet) -> {
try {
if (!user.isOnline()) return;
Player player = (Player) user.platformPlayer();
if (player == null) return;
Object pos = Reflections.field$ServerboundPickItemFromBlockPacket$pos.get(packet);
if (VersionHelper.isFolia()) {
int x = (int) Reflections.field$Vec3i$x.get(pos);
int z = (int) Reflections.field$Vec3i$z.get(pos);
BukkitCraftEngine.instance().scheduler().sync().run(() -> {
try {
handlePickItemFromBlockPacketOnMainThread(player, pos);
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to handle ServerboundPickItemFromBlockPacket", e);
}
}, player.getWorld(), x >> 4, z >> 4);
} else {
BukkitCraftEngine.instance().scheduler().sync().run(() -> {
try {
handlePickItemFromBlockPacketOnMainThread(player, pos);
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to handle ServerboundPickItemFromBlockPacket", e);
}
});
}
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to handle ServerboundPickItemFromBlockPacket", e);
}
};
private static void handlePickItemFromBlockPacketOnMainThread(Player player, Object pos) throws Exception {
Object serverLevel = Reflections.field$CraftWorld$ServerLevel.get(player.getWorld());
Object blockState = Reflections.method$BlockGetter$getBlockState.invoke(serverLevel, pos);
ImmutableBlockState state = BukkitBlockManager.instance().getImmutableBlockState(BlockStateUtils.blockStateToId(blockState));
if (state == null) return;
Key itemId = state.settings().itemId();
if (itemId == null) return;
pickItem(player, itemId);
}
// 1.21.4+
public static final TriConsumer<NetWorkUser, NMSPacketEvent, Object> PICK_ITEM_FROM_ENTITY = (user, event, packet) -> {
try {
int entityId = (int) Reflections.field$ServerboundPickItemFromEntityPacket$id.get(packet);
LoadedFurniture furniture = BukkitFurnitureManager.instance().getLoadedFurnitureByInteractionEntityId(entityId);
if (furniture == null) return;
Player player = (Player) user.platformPlayer();
if (player == null) return;
if (VersionHelper.isFolia()) {
Location location = player.getLocation();
int x = location.getBlockX();
int z = location.getBlockZ();
BukkitCraftEngine.instance().scheduler().sync().run(() -> {
try {
handlePickItemFromEntityOnMainThread(player, furniture);
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to handle ServerboundPickItemFromEntityPacket", e);
}
}, player.getWorld(), x >> 4, z >> 4);
} else {
BukkitCraftEngine.instance().scheduler().sync().run(() -> {
try {
handlePickItemFromEntityOnMainThread(player, furniture);
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to handle ServerboundPickItemFromEntityPacket", e);
}
});
}
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to handle ServerboundPickItemFromEntityPacket", e);
}
};
private static void handlePickItemFromEntityOnMainThread(Player player, LoadedFurniture furniture) throws Exception {
Key itemId = furniture.furniture().settings().itemId();
if (itemId == null) return;
pickItem(player, itemId);
}
private static void pickItem(Player player, Key itemId) throws IllegalAccessException, InvocationTargetException {
ItemStack itemStack = BukkitCraftEngine.instance().itemManager().buildCustomItemStack(itemId, BukkitCraftEngine.instance().adapt(player));
if (itemStack == null) {
CraftEngine.instance().logger().warn("Item: " + itemId + " is not a valid item");
return;
}
assert Reflections.method$ServerGamePacketListenerImpl$tryPickItem != null;
Reflections.method$ServerGamePacketListenerImpl$tryPickItem.invoke(
Reflections.field$ServerPlayer$connection.get(Reflections.method$CraftPlayer$getHandle.invoke(player)), Reflections.method$CraftItemStack$asNMSMirror.invoke(null, itemStack));
}
public static final TriConsumer<NetWorkUser, NMSPacketEvent, Object> ADD_ENTITY = (user, event, packet) -> {
try {
Object entityType = Reflections.field$ClientboundAddEntityPacket$type.get(packet);
// Falling blocks
if (entityType == Reflections.instance$EntityType$FALLING_BLOCK) {
int data = Reflections.field$ClientboundAddEntityPacket$data.getInt(packet);
int remapped = remap(data);
if (remapped != data) {
Reflections.field$ClientboundAddEntityPacket$data.set(packet, remapped);
}
} else if (entityType == Reflections.instance$EntityType$ITEM_DISPLAY) {
// Furniture
int entityId = (int) Reflections.field$ClientboundAddEntityPacket$entityId.get(packet);
LoadedFurniture furniture = BukkitFurnitureManager.instance().getLoadedFurnitureByBaseEntityId(entityId);
if (furniture != null) {
user.sendPacket(furniture.spawnPacket(), false);
}
}
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to handle ClientboundAddEntityPacket", e);
}
};
public static final TriConsumer<NetWorkUser, NMSPacketEvent, Object> SYNC_ENTITY_POSITION = (user, event, packet) -> {
try {
int entityId = (int) Reflections.field$ClientboundEntityPositionSyncPacket$id.get(packet);
if (BukkitFurnitureManager.instance().isFurnitureBaseEntity(entityId)) {
event.setCancelled(true);
}
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to handle ClientboundEntityPositionSyncPacket", e);
}
};
public static final TriConsumer<NetWorkUser, NMSPacketEvent, Object> MOVE_ENTITY = (user, event, packet) -> {
try {
int entityId = (int) Reflections.field$ClientboundMoveEntityPacket$entityId.get(packet);
if (BukkitFurnitureManager.instance().isFurnitureBaseEntity(entityId)) {
event.setCancelled(true);
}
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to handle ClientboundMoveEntityPacket$Pos", e);
}
};
public static final TriConsumer<NetWorkUser, NMSPacketEvent, Object> REMOVE_ENTITY = (user, event, packet) -> {
try {
IntList intList = (IntList) Reflections.field$ClientboundRemoveEntitiesPacket$entityIds.get(packet);
for (int i = 0, size = intList.size(); i < size; i++) {
int[] entities = BukkitFurnitureManager.instance().getSubEntityIdsByBaseEntityId(intList.getInt(i));
if (entities == null) continue;
for (int entityId : entities) {
intList.add(entityId);
}
}
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to handle ClientboundRemoveEntitiesPacket", e);
}
};
public static final TriConsumer<NetWorkUser, NMSPacketEvent, Object> INTERACT_ENTITY = (user, event, packet) -> {
try {
Player player = (Player) user.platformPlayer();
if (player == null) return;
int entityId = (int) Reflections.field$ServerboundInteractPacket$entityId.get(packet);
Object action = Reflections.field$ServerboundInteractPacket$action.get(packet);
Object actionType = Reflections.method$ServerboundInteractPacket$Action$getType.invoke(action);
if (actionType == null) return;
LoadedFurniture furniture = BukkitFurnitureManager.instance().getLoadedFurnitureByInteractionEntityId(entityId);
if (furniture == null) return;
Location location = furniture.baseEntity().getLocation();
BukkitServerPlayer serverPlayer = (BukkitServerPlayer) user;
if (serverPlayer.isSpectatorMode() || serverPlayer.isAdventureMode()) return;
BukkitCraftEngine.instance().scheduler().sync().run(() -> {
if (actionType == Reflections.instance$ServerboundInteractPacket$ActionType$ATTACK) {
if (furniture.isValid()) {
if (!BukkitCraftEngine.instance().antiGrief().canBreak(player, location)) {
return;
}
FurnitureBreakEvent breakEvent = new FurnitureBreakEvent(serverPlayer.platformPlayer(), furniture);
if (EventUtils.fireAndCheckCancel(breakEvent)) {
return;
}
CraftEngineFurniture.remove(furniture, serverPlayer, !serverPlayer.isCreativeMode(), true);
}
} else if (actionType == Reflections.instance$ServerboundInteractPacket$ActionType$INTERACT_AT) {
InteractionHand hand;
Location interactionPoint;
try {
Object interactionHand = Reflections.field$ServerboundInteractPacket$InteractionAtLocationAction$hand.get(action);
hand = interactionHand == Reflections.instance$InteractionHand$MAIN_HAND ? InteractionHand.MAIN_HAND : InteractionHand.OFF_HAND;
Object vec3 = Reflections.field$ServerboundInteractPacket$InteractionAtLocationAction$location.get(action);
double x = (double) Reflections.field$Vec3$x.get(vec3);
double y = (double) Reflections.field$Vec3$y.get(vec3);
double z = (double) Reflections.field$Vec3$z.get(vec3);
interactionPoint = new Location(location.getWorld(), x, y, z);
} catch (ReflectiveOperationException e) {
throw new RuntimeException("Failed to get interaction hand from interact packet", e);
}
FurnitureInteractEvent interactEvent = new FurnitureInteractEvent(serverPlayer.platformPlayer(), furniture, hand, interactionPoint);
if (EventUtils.fireAndCheckCancel(interactEvent)) {
return;
}
if (player.isSneaking())
return;
furniture.getAvailableSeat(entityId).ifPresent(seatPos -> {
if (furniture.occupySeat(seatPos)) {
furniture.mountSeat(Objects.requireNonNull(player.getPlayer()), seatPos);
}
});
}
}, player.getWorld(), location.getBlockX() >> 4,location.getBlockZ() >> 4);
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to handle ServerboundInteractPacket", e);
}
};
public static final TriConsumer<NetWorkUser, NMSPacketEvent, Object> SOUND = (user, event, packet) -> {
try {
Object sound = Reflections.field$ClientboundSoundPacket$sound.get(packet);
Object soundEvent = Reflections.method$Holder$value.invoke(sound);
Key mapped = BukkitBlockManager.instance().replaceSoundIfExist(Key.of(Reflections.field$SoundEvent$location.get(soundEvent).toString()));
if (mapped != null) {
event.setCancelled(true);
Object newId = Reflections.method$ResourceLocation$fromNamespaceAndPath.invoke(null, mapped.namespace(), mapped.value());
Object newSoundEvent = VersionHelper.isVersionNewerThan1_21_2() ?
Reflections.constructor$SoundEvent.newInstance(newId, Reflections.field$SoundEvent$fixedRange.get(soundEvent)) :
Reflections.constructor$SoundEvent.newInstance(newId, Reflections.field$SoundEvent$range.get(soundEvent), Reflections.field$SoundEvent$newSystem.get(soundEvent));
Object newSoundPacket = Reflections.constructor$ClientboundSoundPacket.newInstance(
Reflections.method$Holder$direct.invoke(null, newSoundEvent),
Reflections.field$ClientboundSoundPacket$source.get(packet),
(double) Reflections.field$ClientboundSoundPacket$x.getInt(packet) / 8,
(double) Reflections.field$ClientboundSoundPacket$y.getInt(packet) / 8,
(double) Reflections.field$ClientboundSoundPacket$z.getInt(packet) / 8,
Reflections.field$ClientboundSoundPacket$volume.get(packet),
Reflections.field$ClientboundSoundPacket$pitch.get(packet),
Reflections.field$ClientboundSoundPacket$seed.get(packet)
);
user.sendPacket(newSoundPacket, true);
}
} catch (Exception e) {
CraftEngine.instance().logger().warn("Failed to handle ClientboundSoundPacket", e);
}
};
}

View File

@@ -0,0 +1,14 @@
package net.momirealms.craftengine.bukkit.plugin.network;
public interface PacketIds {
int clientboundBlockUpdatePacket();
int clientboundSectionBlocksUpdatePacket();
int clientboundLevelParticlesPacket();
int clientboundLevelEventPacket();
int clientboundAddEntityPacket();
}

View File

@@ -0,0 +1,31 @@
package net.momirealms.craftengine.bukkit.plugin.network.impl;
import net.momirealms.craftengine.bukkit.plugin.network.PacketIds;
public class PacketIds1_20 implements PacketIds {
@Override
public int clientboundBlockUpdatePacket() {
return 10;
}
@Override
public int clientboundSectionBlocksUpdatePacket() {
return 67;
}
@Override
public int clientboundLevelParticlesPacket() {
return 38;
}
@Override
public int clientboundLevelEventPacket() {
return 37;
}
@Override
public int clientboundAddEntityPacket() {
return 1;
}
}

View File

@@ -0,0 +1,31 @@
package net.momirealms.craftengine.bukkit.plugin.network.impl;
import net.momirealms.craftengine.bukkit.plugin.network.PacketIds;
public class PacketIds1_20_2 implements PacketIds {
@Override
public int clientboundBlockUpdatePacket() {
return 9;
}
@Override
public int clientboundSectionBlocksUpdatePacket() {
return 69;
}
@Override
public int clientboundLevelParticlesPacket() {
return 39;
}
@Override
public int clientboundLevelEventPacket() {
return 38;
}
@Override
public int clientboundAddEntityPacket() {
return 1;
}
}

View File

@@ -0,0 +1,31 @@
package net.momirealms.craftengine.bukkit.plugin.network.impl;
import net.momirealms.craftengine.bukkit.plugin.network.PacketIds;
public class PacketIds1_20_3 implements PacketIds {
@Override
public int clientboundBlockUpdatePacket() {
return 9;
}
@Override
public int clientboundSectionBlocksUpdatePacket() {
return 71;
}
@Override
public int clientboundLevelParticlesPacket() {
return 39;
}
@Override
public int clientboundLevelEventPacket() {
return 38;
}
@Override
public int clientboundAddEntityPacket() {
return 1;
}
}

View File

@@ -0,0 +1,31 @@
package net.momirealms.craftengine.bukkit.plugin.network.impl;
import net.momirealms.craftengine.bukkit.plugin.network.PacketIds;
public class PacketIds1_20_5 implements PacketIds {
@Override
public int clientboundBlockUpdatePacket() {
return 9;
}
@Override
public int clientboundSectionBlocksUpdatePacket() {
return 73;
}
@Override
public int clientboundLevelParticlesPacket() {
return 41;
}
@Override
public int clientboundLevelEventPacket() {
return 40;
}
@Override
public int clientboundAddEntityPacket() {
return 1;
}
}

View File

@@ -0,0 +1,31 @@
package net.momirealms.craftengine.bukkit.plugin.network.impl;
import net.momirealms.craftengine.bukkit.plugin.network.PacketIds;
public class PacketIds1_21_2 implements PacketIds {
@Override
public int clientboundBlockUpdatePacket() {
return 9;
}
@Override
public int clientboundSectionBlocksUpdatePacket() {
return 78;
}
@Override
public int clientboundLevelParticlesPacket() {
return 42;
}
@Override
public int clientboundLevelEventPacket() {
return 41;
}
@Override
public int clientboundAddEntityPacket() {
return 1;
}
}

View File

@@ -0,0 +1,87 @@
package net.momirealms.craftengine.bukkit.plugin.papi;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import net.momirealms.craftengine.core.font.BitmapImage;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.util.FormatUtils;
import net.momirealms.craftengine.core.util.Key;
import org.bukkit.OfflinePlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Optional;
public class ImageExpansion extends PlaceholderExpansion {
private final CraftEngine plugin;
public ImageExpansion(CraftEngine plugin) {
this.plugin = plugin;
}
@Override
public @NotNull String getIdentifier() {
return "image";
}
@Override
public @NotNull String getAuthor() {
return "XiaoMoMi";
}
@Override
public @NotNull String getVersion() {
return "1.0";
}
@Override
public boolean persist() {
return true;
}
@Override
public @Nullable String onRequest(OfflinePlayer player, @NotNull String params) {
String[] split = params.split("_", 2);
if (split.length != 2) return null;
String[] param = split[1].split(":", 4);
if (param.length < 2) return null;
Key key;
try {
key = Key.of(param[0], param[1]);
} catch (IllegalArgumentException e) {
plugin.logger().warn("Invalid image namespaced key: " + param[0] + ":" + param[1]);
return null;
}
Optional<BitmapImage> optional = plugin.imageManager().bitmapImageByImageId(key);
if (optional.isEmpty()) {
return null;
}
BitmapImage image = optional.get();
int codepoint;
if (param.length == 4) {
codepoint = image.codepointAt(Integer.parseInt(param[2]), Integer.parseInt(param[3]));
} else if (param.length == 2) {
codepoint = image.codepointAt(0,0);
} else {
return null;
}
try {
switch (split[0]) {
case "mm", "minimessage", "mini" -> {
return FormatUtils.miniMessageFont(new String(Character.toChars(codepoint)), image.font().toString());
}
case "md", "minedown" -> {
return FormatUtils.mineDownFont(new String(Character.toChars(codepoint)), image.font().toString());
}
case "raw" -> {
return new String(Character.toChars(codepoint));
}
default -> {
return null;
}
}
} catch (IndexOutOfBoundsException e) {
return null;
}
}
}

View File

@@ -0,0 +1,75 @@
package net.momirealms.craftengine.bukkit.plugin.papi;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import org.bukkit.OfflinePlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class ShiftExpansion extends PlaceholderExpansion {
private final CraftEngine plugin;
public ShiftExpansion(CraftEngine plugin) {
this.plugin = plugin;
}
@Override
public @NotNull String getIdentifier() {
return "shift";
}
@Override
public @NotNull String getAuthor() {
return "XiaoMoMi";
}
@Override
public @NotNull String getVersion() {
return "1.0";
}
@Override
public boolean persist() {
return true;
}
@Override
public @Nullable String onRequest(OfflinePlayer player, @NotNull String params) {
String[] split = params.split("_", 2);
switch (split[0]) {
case "mini", "minimessage", "mm" -> {
if (split.length != 2) return null;
try {
return plugin.imageManager().createMiniMessageOffsets(Integer.parseInt(split[1]));
} catch (NumberFormatException e) {
return null;
}
}
case "md", "minedown" -> {
if (split.length != 2) return null;
try {
return plugin.imageManager().createMineDownOffsets(Integer.parseInt(split[1]));
} catch (NumberFormatException e) {
return null;
}
}
case "raw" -> {
if (split.length != 2) return null;
try {
return plugin.imageManager().createRawOffsets(Integer.parseInt(split[1]));
} catch (NumberFormatException e) {
return null;
}
}
default -> {
if (split.length != 1) return null;
try {
return plugin.imageManager().createMiniMessageOffsets(Integer.parseInt(split[0]));
} catch (NumberFormatException e) {
return null;
}
}
}
}
}

View File

@@ -0,0 +1,27 @@
package net.momirealms.craftengine.bukkit.plugin.scheduler;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.bukkit.plugin.scheduler.impl.BukkitExecutor;
import net.momirealms.craftengine.bukkit.plugin.scheduler.impl.FoliaExecutor;
import net.momirealms.craftengine.core.plugin.scheduler.AbstractJavaScheduler;
import net.momirealms.craftengine.core.plugin.scheduler.RegionExecutor;
import net.momirealms.craftengine.core.util.VersionHelper;
import org.bukkit.World;
public class BukkitSchedulerAdapter extends AbstractJavaScheduler<World> {
protected RegionExecutor<World> sync;
public BukkitSchedulerAdapter(BukkitCraftEngine plugin) {
super(plugin);
if (VersionHelper.isFolia()) {
this.sync = new FoliaExecutor(plugin.bootstrap());
} else {
this.sync = new BukkitExecutor(plugin.bootstrap());
}
}
@Override
public RegionExecutor<World> sync() {
return this.sync;
}
}

View File

@@ -0,0 +1,64 @@
package net.momirealms.craftengine.bukkit.plugin.scheduler.impl;
import net.momirealms.craftengine.core.plugin.scheduler.DummyTask;
import net.momirealms.craftengine.core.plugin.scheduler.RegionExecutor;
import net.momirealms.craftengine.core.plugin.scheduler.SchedulerTask;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;
public class BukkitExecutor implements RegionExecutor<World> {
private final Plugin plugin;
public BukkitExecutor(Plugin plugin) {
this.plugin = plugin;
}
@Override
public void run(Runnable runnable, World world, int x, int z) {
execute(runnable);
}
@Override
public void runDelayed(Runnable r, World world, int x, int z) {
Bukkit.getScheduler().runTask(plugin, r);
}
@Override
public SchedulerTask runAsyncRepeating(Runnable runnable, long delay, long period) {
return new BukkitTask(Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, runnable, delay, period));
}
@Override
public SchedulerTask runAsyncLater(Runnable runnable, long delay) {
return new BukkitTask(Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, runnable, delay));
}
@Override
public SchedulerTask runLater(Runnable runnable, long delay, World world, int x, int z) {
if (delay <= 0) {
if (Bukkit.isPrimaryThread()) {
runnable.run();
return new DummyTask();
} else {
return new BukkitTask(Bukkit.getScheduler().runTask(plugin, runnable));
}
}
return new BukkitTask(Bukkit.getScheduler().runTaskLater(plugin, runnable, delay));
}
@Override
public SchedulerTask runRepeating(Runnable runnable, long delay, long period, World world, int x, int z) {
return new BukkitTask(Bukkit.getScheduler().runTaskTimer(plugin, runnable, delay, period));
}
@Override
public void execute(@NotNull Runnable runnable) {
if (Bukkit.isPrimaryThread()) {
runnable.run();
return;
}
Bukkit.getScheduler().runTask(plugin, runnable);
}
}

View File

@@ -0,0 +1,21 @@
package net.momirealms.craftengine.bukkit.plugin.scheduler.impl;
import net.momirealms.craftengine.core.plugin.scheduler.SchedulerTask;
public class BukkitTask implements SchedulerTask {
private final org.bukkit.scheduler.BukkitTask bukkitTask;
public BukkitTask(org.bukkit.scheduler.BukkitTask bukkitTask) {
this.bukkitTask = bukkitTask;
}
@Override
public void cancel() {
this.bukkitTask.cancel();
}
@Override
public boolean cancelled() {
return bukkitTask.isCancelled();
}
}

View File

@@ -0,0 +1,72 @@
package net.momirealms.craftengine.bukkit.plugin.scheduler.impl;
import net.momirealms.craftengine.core.plugin.scheduler.RegionExecutor;
import net.momirealms.craftengine.core.plugin.scheduler.SchedulerTask;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
public class FoliaExecutor implements RegionExecutor<World> {
private final Plugin plugin;
public FoliaExecutor(Plugin plugin) {
this.plugin = plugin;
}
@Override
public void run(Runnable runnable, World world, int x, int z) {
Optional.ofNullable(world).ifPresentOrElse(w ->
Bukkit.getRegionScheduler().execute(plugin, w, x, z, runnable),
() -> Bukkit.getGlobalRegionScheduler().execute(plugin, runnable)
);
}
@Override
public void runDelayed(Runnable runnable, World world, int x, int z) {
run(runnable, world, x, z);
}
@Override
public SchedulerTask runAsyncRepeating(Runnable runnable, long delay, long period) {
return runRepeating(runnable, delay, period, null, 0, 0);
}
@Override
public SchedulerTask runAsyncLater(Runnable runnable, long delay) {
return runLater(runnable, delay, null, 0, 0);
}
@Override
public SchedulerTask runLater(Runnable runnable, long delay, World world, int x, int z) {
if (world == null) {
if (delay <= 0) {
return new FoliaTask(Bukkit.getGlobalRegionScheduler().runDelayed(plugin, scheduledTask -> runnable.run(), delay));
} else {
return new FoliaTask(Bukkit.getGlobalRegionScheduler().run(plugin, scheduledTask -> runnable.run()));
}
} else {
if (delay <= 0) {
return new FoliaTask(Bukkit.getRegionScheduler().run(plugin, world, x, z, scheduledTask -> runnable.run()));
} else {
return new FoliaTask(Bukkit.getRegionScheduler().runDelayed(plugin, world, x, z, scheduledTask -> runnable.run(), delay));
}
}
}
@Override
public SchedulerTask runRepeating(Runnable runnable, long delay, long period, World world, int x, int z) {
if (world == null) {
return new FoliaTask(Bukkit.getGlobalRegionScheduler().runAtFixedRate(plugin, scheduledTask -> runnable.run(), delay, period));
} else {
return new FoliaTask(Bukkit.getRegionScheduler().runAtFixedRate(plugin, world, x, z, scheduledTask -> runnable.run(), delay, period));
}
}
@Override
public void execute(@NotNull Runnable runnable) {
Bukkit.getGlobalRegionScheduler().execute(plugin, runnable);
}
}

View File

@@ -0,0 +1,22 @@
package net.momirealms.craftengine.bukkit.plugin.scheduler.impl;
import io.papermc.paper.threadedregions.scheduler.ScheduledTask;
import net.momirealms.craftengine.core.plugin.scheduler.SchedulerTask;
public class FoliaTask implements SchedulerTask {
private final ScheduledTask task;
public FoliaTask(ScheduledTask task) {
this.task = task;
}
@Override
public void cancel() {
this.task.cancel();
}
@Override
public boolean cancelled() {
return task.isCancelled();
}
}

View File

@@ -0,0 +1,574 @@
package net.momirealms.craftengine.bukkit.plugin.user;
import com.google.common.collect.Lists;
import io.netty.channel.Channel;
import net.kyori.adventure.text.Component;
import net.momirealms.craftengine.bukkit.item.BukkitItemManager;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.bukkit.util.*;
import net.momirealms.craftengine.bukkit.world.BukkitWorld;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.entity.player.InteractionHand;
import net.momirealms.craftengine.core.entity.player.Player;
import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.network.ConnectionState;
import net.momirealms.craftengine.core.util.Direction;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.VersionHelper;
import net.momirealms.craftengine.core.world.BlockPos;
import net.momirealms.craftengine.core.world.Vec3d;
import net.momirealms.craftengine.core.world.World;
import org.bukkit.*;
import org.bukkit.block.Block;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.util.RayTraceResult;
import org.jetbrains.annotations.Nullable;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.util.UUID;
public class BukkitServerPlayer extends Player {
private final Channel channel;
private final BukkitCraftEngine plugin;
private ConnectionState decoderState;
private ConnectionState encoderState;
private Reference<org.bukkit.entity.Player> playerRef;
private Reference<Object> serverPlayerRef;
private int sectionCount;
private int lastSuccessfulInteraction;
private long lastAttributeSyncTime;
private Key clientSideDimension;
private int lastSentState = -1;
private int lastHitBlockTime;
private BlockPos destroyPos;
private Object destroyedState;
private boolean isDestroyingBlock;
private boolean isDestroyingCustomBlock;
private boolean swingHandAck;
private float miningProgress;
private int resentSoundTick;
private int resentSwingTick;
private Key lastUsedRecipe = null;
public BukkitServerPlayer(BukkitCraftEngine plugin, Channel channel) {
this.channel = channel;
this.plugin = plugin;
}
public void setPlayer(org.bukkit.entity.Player player) {
playerRef = new WeakReference<>(player);
try {
serverPlayerRef = new WeakReference<>(Reflections.method$CraftPlayer$getHandle.invoke(player));
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
@Override
public Channel nettyChannel() {
return channel;
}
@Override
public CraftEngine plugin() {
return plugin;
}
@Override
public boolean isMiningBlock() {
return destroyPos != null;
}
public void setDestroyedState(Object destroyedState) {
this.destroyedState = destroyedState;
}
public void setDestroyPos(BlockPos destroyPos) {
this.destroyPos = destroyPos;
}
@Override
public boolean shouldSyncAttribute() {
long current = System.currentTimeMillis();
if (current - this.lastAttributeSyncTime > 10000) {
this.lastAttributeSyncTime = current;
return true;
}
return false;
}
@Override
public boolean isSneaking() {
return platformPlayer().isSneaking();
}
@Override
public boolean isCreativeMode() {
return platformPlayer().getGameMode() == GameMode.CREATIVE;
}
@Override
public boolean isSpectatorMode() {
return platformPlayer().getGameMode() == GameMode.SPECTATOR;
}
@Override
public boolean isAdventureMode() {
return platformPlayer().getGameMode() == GameMode.ADVENTURE;
}
@Override
public void sendActionBar(Component text) {
try {
Object packet = Reflections.constructor$ClientboundSetActionBarTextPacket.newInstance(ComponentUtils.adventureToMinecraft(text));
sendPacket(packet, false);
} catch (ReflectiveOperationException e) {
plugin.logger().warn("Failed to send action bar", e);
}
}
@Override
public boolean updateLastSuccessfulInteractionTick(int tick) {
if (lastSuccessfulInteraction != tick) {
lastSuccessfulInteraction = tick;
return true;
} else {
return false;
}
}
@Override
public int gameTicks() {
try {
Object serverPlayer = serverPlayer();
Object gameMode = Reflections.field$ServerPlayer$gameMode.get(serverPlayer);
return (int) Reflections.field$ServerPlayerGameMode$gameTicks.get(gameMode);
} catch (ReflectiveOperationException e) {
throw new RuntimeException("Failed to get current tick", e);
}
}
@Override
public void swingHand(InteractionHand hand) {
platformPlayer().swingHand(hand == InteractionHand.MAIN_HAND ? EquipmentSlot.HAND : EquipmentSlot.OFF_HAND);
}
@Override
public boolean hasPermission(String permission) {
return platformPlayer().hasPermission(permission);
}
@Override
public boolean canInstabuild() {
try {
Object abilities = Reflections.field$Player$abilities.get(serverPlayer());
return (boolean) Reflections.field$Abilities$instabuild.get(abilities);
} catch (ReflectiveOperationException e) {
CraftEngine.instance().logger().warn("Failed to get canInstabuild for " + name(), e);
return false;
}
}
@Override
public String name() {
org.bukkit.entity.Player player = platformPlayer();
if (player == null) return "Unknown";
return player.getName();
}
@Override
public void playSound(Key sound, float volume, float pitch) {
platformPlayer().playSound(platformPlayer(), sound.toString(), SoundCategory.MASTER, volume, pitch);
}
@Override
public void giveItem(Item<?> item) {
PlayerUtils.giveItem(platformPlayer(), (ItemStack) item.getItem(), item.count());
}
@Override
public void sendPacket(Object packet, boolean immediately) {
this.plugin.networkManager().sendPacket(this, packet, immediately);
}
@Override
public ConnectionState decoderState() {
return decoderState;
}
@Override
public ConnectionState encoderState() {
return encoderState;
}
@Override
public int clientSideSectionCount() {
return sectionCount;
}
public void setClientSideSectionCount(int sectionCount) {
this.sectionCount = sectionCount;
}
@Override
public Key clientSideDimension() {
return clientSideDimension;
}
public void setClientSideDimension(Key clientSideDimension) {
this.clientSideDimension = clientSideDimension;
}
public void setConnectionState(ConnectionState connectionState) {
this.encoderState = connectionState;
this.decoderState = connectionState;
}
public void setDecoderState(ConnectionState decoderState) {
this.decoderState = decoderState;
}
public void setEncoderState(ConnectionState encoderState) {
this.encoderState = encoderState;
}
@Override
public void tick() {
// not fully online
if (serverPlayer() == null) return;
if (this.isDestroyingBlock) {
this.tickBlockDestroy();
}
}
@Override
public float getDestroyProgress(Object blockState, BlockPos pos) {
try {
Object serverPlayer = serverPlayer();
Object blockPos = Reflections.constructor$BlockPos.newInstance(pos.x(), pos.y(), pos.z());
return (float) Reflections.method$BlockStateBase$getDestroyProgress.invoke(blockState, serverPlayer, Reflections.method$Entity$level.invoke(serverPlayer), blockPos);
} catch (ReflectiveOperationException e) {
this.plugin.logger().warn("Failed to get destroy progress for player " + platformPlayer().getName());
return 0f;
}
}
public void startMiningBlock(org.bukkit.World world, BlockPos pos, Object state, boolean custom, @Nullable ImmutableBlockState immutableBlockState) {
// instant break
if (custom && getDestroyProgress(state, pos) >= 1f) {
assert immutableBlockState != null;
// not an instant break on client side
if (getDestroyProgress(immutableBlockState.vanillaBlockState().handle(), pos) < 1f) {
try {
Object levelEventPacket = Reflections.constructor$ClientboundLevelEventPacket.newInstance(2001, LocationUtils.toBlockPos(pos), BlockStateUtils.blockStateToId(this.destroyedState), false);
sendPacket(levelEventPacket, false);
} catch (ReflectiveOperationException e) {
this.plugin.logger().warn("Failed to send level event packet", e);
}
}
//ParticleUtils.addBlockBreakParticles(world, LocationUtils.toBlockPos(pos), state);
return;
}
setCanBreakBlock(!custom);
setDestroyPos(pos);
setDestroyedState(state);
setIsDestroyingBlock(true, custom);
}
private void setCanBreakBlock(boolean canBreak) {
try {
if (canBreak) {
if (VersionHelper.isVersionNewerThan1_20_5()) {
Object serverPlayer = serverPlayer();
Object attributeInstance = Reflections.method$ServerPlayer$getAttribute.invoke(serverPlayer, Reflections.instance$Holder$Attribute$block_break_speed);
Object newPacket = Reflections.constructor$ClientboundUpdateAttributesPacket0.newInstance(entityID(), Lists.newArrayList(attributeInstance));
sendPacket(newPacket, true);
} else {
resetEffect(Reflections.instance$MobEffecr$mining_fatigue);
resetEffect(Reflections.instance$MobEffecr$haste);
}
} else {
if (VersionHelper.isVersionNewerThan1_20_5()) {
Object attributeModifier = VersionHelper.isVersionNewerThan1_21() ?
Reflections.constructor$AttributeModifier.newInstance(Reflections.method$ResourceLocation$fromNamespaceAndPath.invoke(null, "craftengine", "custom_hardness"), -9999d, Reflections.instance$AttributeModifier$Operation$ADD_VALUE) :
Reflections.constructor$AttributeModifier.newInstance(UUID.randomUUID(), "craftengine:custom_hardness", -9999d, Reflections.instance$AttributeModifier$Operation$ADD_VALUE);
Object attributeSnapshot = Reflections.constructor$ClientboundUpdateAttributesPacket$AttributeSnapshot.newInstance(Reflections.instance$Holder$Attribute$block_break_speed, 1d, Lists.newArrayList(attributeModifier));
Object newPacket = Reflections.constructor$ClientboundUpdateAttributesPacket1.newInstance(entityID(), Lists.newArrayList(attributeSnapshot));
sendPacket(newPacket, true);
} else {
Object fatiguePacket = MobEffectUtils.createPacket(Reflections.instance$MobEffecr$mining_fatigue, entityID(), (byte) 9, -1, false, false, false);
Object hastePacket = MobEffectUtils.createPacket(Reflections.instance$MobEffecr$haste, entityID(), (byte) 0, -1, false, false, false);
sendPacket(fatiguePacket, true);
sendPacket(hastePacket, true);
}
}
} catch (ReflectiveOperationException e) {
plugin.logger().warn("Failed to set attribute for player " + platformPlayer().getName(), e);
}
}
@Override
public void stopMiningBlock() {
setCanBreakBlock(true);
setIsDestroyingBlock(false, false);
}
private void resetEffect(Object mobEffect) throws ReflectiveOperationException {
Object effectInstance = Reflections.method$ServerPlayer$getEffect.invoke(serverPlayer(), mobEffect);
Object packet;
if (effectInstance != null) {
packet = Reflections.constructor$ClientboundUpdateMobEffectPacket.newInstance(entityID(), effectInstance);
} else {
packet = Reflections.constructor$ClientboundRemoveMobEffectPacket.newInstance(entityID(), mobEffect);
}
sendPacket(packet, true);
}
@Override
public void abortMiningBlock() {
abortDestroyProgress();
}
private void tickBlockDestroy() {
// prevent server from taking over breaking blocks
if (this.isDestroyingCustomBlock) {
try {
Object serverPlayer = serverPlayer();
Object gameMode = Reflections.field$ServerPlayer$gameMode.get(serverPlayer);
Reflections.field$ServerPlayerGameMode$isDestroyingBlock.set(gameMode, false);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
if (!this.swingHandAck) return;
this.swingHandAck = false;
try {
org.bukkit.entity.Player player = platformPlayer();
double range = getInteractionRange();
RayTraceResult result = player.rayTraceBlocks(range, FluidCollisionMode.NEVER);
if (result == null) return;
Block hitBlock = result.getHitBlock();
if (hitBlock == null) return;
Location location = hitBlock.getLocation();
BlockPos hitPos = new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ());
if (!hitPos.equals(this.destroyPos)) {
return;
}
Object blockPos = LocationUtils.toBlockPos(hitPos);
Object serverPlayer = serverPlayer();
Object gameMode = Reflections.field$ServerPlayer$gameMode.get(serverPlayer);
int currentTick = (int) Reflections.field$ServerPlayerGameMode$gameTicks.get(gameMode);
if (currentTick - this.lastHitBlockTime > 3) {
Object blockOwner = Reflections.field$StateHolder$owner.get(this.destroyedState);
Object soundType = Reflections.field$BlockBehaviour$soundType.get(blockOwner);
Object soundEvent = Reflections.field$SoundType$hitSound.get(soundType);
Object soundId = Reflections.field$SoundEvent$location.get(soundEvent);
level().playBlockSound(new Vec3d(this.destroyPos.x(), this.destroyPos.y(), this.destroyPos.z()), Key.of(soundId.toString()), 0.5F, 0.5F);
this.lastHitBlockTime = currentTick;
}
// accumulate progress (custom blocks only)
if (this.isDestroyingCustomBlock) {
Item<ItemStack> item = this.getItemInHand(InteractionHand.MAIN_HAND);
if (item != null) {
Material itemMaterial = item.getItem().getType();
if (canInstabuild() && (itemMaterial == Material.DEBUG_STICK
|| itemMaterial == Material.TRIDENT
|| (VersionHelper.isVersionNewerThan1_20_5() && itemMaterial == MaterialUtils.MACE)
|| item.is(ItemTags.SWORDS))) {
return;
}
}
this.miningProgress = (float) Reflections.method$BlockStateBase$getDestroyProgress.invoke(this.destroyedState, serverPlayer, Reflections.method$Entity$level.invoke(serverPlayer), blockPos) + miningProgress;
int packetStage = (int) (this.miningProgress * 10.0F);
if (packetStage != this.lastSentState) {
this.lastSentState = packetStage;
broadcastDestroyProgress(player, hitPos, blockPos, packetStage);
}
if (this.miningProgress >= 1f) {
//Reflections.method$ServerLevel$levelEvent.invoke(Reflections.field$CraftWorld$ServerLevel.get(player.getWorld()), null, 2001, blockPos, BlockStateUtils.blockStateToId(this.destroyedState));
Reflections.method$ServerPlayerGameMode$destroyBlock.invoke(gameMode, blockPos);
Object levelEventPacket = Reflections.constructor$ClientboundLevelEventPacket.newInstance(2001, blockPos, BlockStateUtils.blockStateToId(this.destroyedState), false);
sendPacket(levelEventPacket, false);
this.stopMiningBlock();
}
}
} catch (Exception e) {
plugin.logger().warn("Failed to tick destroy for player " + platformPlayer().getName(), e);
}
}
private void broadcastDestroyProgress(org.bukkit.entity.Player player, BlockPos hitPos, Object blockPos, int stage) throws ReflectiveOperationException {
Object packet = Reflections.constructor$ClientboundBlockDestructionPacket.newInstance(Integer.MAX_VALUE - entityID(), blockPos, stage);
for (org.bukkit.entity.Player other : player.getWorld().getPlayers()) {
Location otherLocation = other.getLocation();
double d0 = (double) hitPos.x() - otherLocation.getX();
double d1 = (double) hitPos.y() - otherLocation.getY();
double d2 = (double) hitPos.z() - otherLocation.getZ();
if (d0 * d0 + d1 * d1 + d2 * d2 < 1024.0D) {
plugin.networkManager().sendPacket(other, packet);
}
}
}
@Override
public double getInteractionRange() {
try {
if (VersionHelper.isVersionNewerThan1_20_5()) {
Object attributeInstance = Reflections.method$ServerPlayer$getAttribute.invoke(serverPlayer(), Reflections.instance$Holder$Attribute$block_interaction_range);
if (attributeInstance == null) return 4.5d;
return (double) Reflections.method$AttributeInstance$getValue.invoke(attributeInstance);
} else {
return 4.5d;
}
} catch (ReflectiveOperationException e) {
plugin.logger().warn("Failed to get interaction range for player " + platformPlayer().getName(), e);
return 4.5d;
}
}
public void setIsDestroyingBlock(boolean value, boolean custom) {
if (value) {
this.isDestroyingBlock = true;
this.isDestroyingCustomBlock = custom;
this.swingHandAck = true;
this.miningProgress = 0;
} else {
this.isDestroyingBlock = false;
this.swingHandAck = false;
if (this.destroyPos != null) {
try {
this.broadcastDestroyProgress(platformPlayer(), this.destroyPos, LocationUtils.toBlockPos(this.destroyPos), -1);
} catch (ReflectiveOperationException e) {
plugin.logger().warn("Failed to set isDestroyingCustomBlock", e);
}
}
this.destroyPos = null;
this.miningProgress = 0;
this.destroyedState = null;
this.isDestroyingCustomBlock = false;
}
}
@Override
public void abortDestroyProgress() {
this.swingHandAck = false;
this.miningProgress = 0;
if (this.destroyPos == null) return;
try {
this.broadcastDestroyProgress(platformPlayer(), this.destroyPos, LocationUtils.toBlockPos(this.destroyPos), -1);
} catch (ReflectiveOperationException e) {
plugin.logger().warn("Failed to abort destroyProgress", e);
}
}
@Override
public void onSwingHand() {
this.swingHandAck = true;
}
@Override
public int entityID() {
return platformPlayer().getEntityId();
}
@Override
public boolean isOnline() {
org.bukkit.entity.Player player = platformPlayer();
if (player == null) return false;
return player.isOnline();
}
@Override
public float getYRot() {
return platformPlayer().getLocation().getPitch();
}
@Override
public float getXRot() {
return platformPlayer().getLocation().getYaw();
}
@Override
public boolean isSecondaryUseActive() {
return isSneaking();
}
@Override
public Direction getDirection() {
return DirectionUtils.toDirection(platformPlayer().getFacing());
}
@Nullable
@Override
public Item<ItemStack> getItemInHand(InteractionHand hand) {
PlayerInventory inventory = platformPlayer().getInventory();
return BukkitItemManager.instance().wrap(hand == InteractionHand.MAIN_HAND ? inventory.getItemInMainHand() : inventory.getItemInOffHand());
}
@Override
public World level() {
return new BukkitWorld(platformPlayer().getWorld());
}
@Override
public double x() {
return platformPlayer().getLocation().getX();
}
@Override
public double y() {
return platformPlayer().getLocation().getY();
}
@Override
public double z() {
return platformPlayer().getLocation().getZ();
}
@Override
public Object serverPlayer() {
if (serverPlayerRef == null) return null;
return serverPlayerRef.get();
}
@Override
public org.bukkit.entity.Player platformPlayer() {
if (playerRef == null) return null;
return playerRef.get();
}
public void setResendSound() {
resentSoundTick = gameTicks();
}
public void setResendSwing() {
resentSwingTick = gameTicks();
}
public boolean shouldResendSound() {
return resentSoundTick == gameTicks();
}
public boolean shouldResendSwing() {
return resentSwingTick == gameTicks();
}
public Key lastUsedRecipe() {
return lastUsedRecipe;
}
public void setLastUsedRecipe(Key lastUsedRecipe) {
this.lastUsedRecipe = lastUsedRecipe;
}
}

View File

@@ -0,0 +1,58 @@
package net.momirealms.craftengine.bukkit.sound;
import net.momirealms.craftengine.bukkit.util.ComponentUtils;
import net.momirealms.craftengine.bukkit.util.Reflections;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.sound.song.AbstractJukeboxSongManager;
import net.momirealms.craftengine.core.sound.song.JukeboxSong;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.VersionHelper;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
public class BukkitJukeboxSongManager extends AbstractJukeboxSongManager {
public BukkitJukeboxSongManager(CraftEngine plugin) {
super(plugin);
}
@Override
protected void registerSongs(Map<Key, JukeboxSong> songs) {
if (songs.isEmpty()) return;
try {
unfreezeRegistry();
for (Map.Entry<Key, JukeboxSong> entry : songs.entrySet()) {
Key id = entry.getKey();
JukeboxSong jukeboxSong = entry.getValue();
Object resourceLocation = Reflections.method$ResourceLocation$fromNamespaceAndPath.invoke(null, id.namespace(), id.value());
Object soundId = Reflections.method$ResourceLocation$fromNamespaceAndPath.invoke(null, jukeboxSong.sound().namespace(), jukeboxSong.sound().value());
Object song = Reflections.method$Registry$get.invoke(Reflections.instance$InternalRegistries$JUKEBOX_SONG, resourceLocation);
Object soundEvent = VersionHelper.isVersionNewerThan1_21_2() ?
Reflections.constructor$SoundEvent.newInstance(soundId, Optional.of(jukeboxSong.range())) :
Reflections.constructor$SoundEvent.newInstance(soundId, jukeboxSong.range(), false);
Object soundHolder = Reflections.method$Holder$direct.invoke(null, soundEvent);
if (song == null) {
song = Reflections.constructor$JukeboxSong.newInstance(soundHolder, ComponentUtils.adventureToMinecraft(jukeboxSong.description()), jukeboxSong.lengthInSeconds(), jukeboxSong.comparatorOutput());
Object holder = Reflections.method$Registry$registerForHolder.invoke(null, Reflections.instance$InternalRegistries$JUKEBOX_SONG, resourceLocation, song);
Reflections.method$Holder$Reference$bindValue.invoke(holder, song);
Reflections.field$Holder$Reference$tags.set(holder, Set.of());
}
}
freezeRegistry();
} catch (Exception e) {
plugin.logger().warn("Failed to register jukebox songs.", e);
}
}
private void unfreezeRegistry() throws IllegalAccessException {
Reflections.field$MappedRegistry$frozen.set(Reflections.instance$InternalRegistries$JUKEBOX_SONG, false);
}
private void freezeRegistry() throws IllegalAccessException {
Reflections.field$MappedRegistry$frozen.set(Reflections.instance$InternalRegistries$JUKEBOX_SONG, true);
}
}

View File

@@ -0,0 +1,17 @@
package net.momirealms.craftengine.bukkit.sound;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.sound.AbstractSoundManager;
import net.momirealms.craftengine.core.sound.song.JukeboxSongManager;
public class BukkitSoundManager extends AbstractSoundManager {
public BukkitSoundManager(CraftEngine plugin) {
super(plugin);
}
@Override
protected JukeboxSongManager createJukeboxSongManager() {
return new BukkitJukeboxSongManager(super.plugin);
}
}

View File

@@ -0,0 +1,202 @@
package net.momirealms.craftengine.bukkit.util;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.block.PushReaction;
import net.momirealms.craftengine.core.util.Instrument;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.MapColor;
import net.momirealms.craftengine.core.world.BlockPos;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.event.block.BlockPhysicsEvent;
import java.lang.reflect.InvocationTargetException;
import java.util.IdentityHashMap;
public class BlockStateUtils {
public static final IdentityHashMap<Object, Object> CLIENT_SIDE_NOTE_BLOCKS = new IdentityHashMap<>();
private static int vanillaStateSize;
private static boolean hasInit;
public static void init(int size) {
if (hasInit) {
throw new IllegalStateException("BlockStateUtils has already been initialized");
}
vanillaStateSize = size;
hasInit = true;
}
public static Object createBlockUpdatePacket(BlockPos pos, ImmutableBlockState state) {
try {
return Reflections.constructor$ClientboundBlockUpdatePacket.newInstance(LocationUtils.toBlockPos(pos), state.customBlockState().handle());
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
public static BlockData createBlockData(Object blockState) {
try {
return (BlockData) Reflections.method$CraftBlockData$createData.invoke(null, blockState);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public static BlockData fromBlockData(Object blockState) {
try {
return (BlockData) Reflections.method$CraftBlockData$fromData.invoke(null, blockState);
} catch (InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public static int blockDataToId(BlockData blockData) {
try {
Object blockState = Reflections.field$CraftBlockData$data.get(blockData);
return (int) Reflections.method$IdMapper$getId.invoke(Reflections.instance$BLOCK_STATE_REGISTRY, blockState);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
public static Key getBlockOwnerId(Block block) {
BlockData data = block.getBlockData();
Object blockState = blockDataToBlockState(data);
return getBlockOwnerIdFromState(blockState);
}
public static Key getBlockOwnerIdFromState(Object blockState) {
String id = blockState.toString();
int first = id.indexOf('{');
int last = id.indexOf('}');
if (first != -1 && last != -1 && last > first) {
String blockId = id.substring(first + 1, last);
return Key.of(blockId);
} else {
throw new IllegalArgumentException("Invalid block ID format: " + id);
}
}
public static Object blockDataToBlockState(BlockData blockData) {
try {
return Reflections.field$CraftBlockData$data.get(blockData);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
public static Object idToBlockState(int id) {
try {
return Reflections.method$IdMapper$byId.invoke(Reflections.instance$BLOCK_STATE_REGISTRY, id);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
public static int blockStateToId(Object blockState) {
try {
return (int) Reflections.method$IdMapper$getId.invoke(Reflections.instance$BLOCK_STATE_REGISTRY, blockState);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
public static Object getBlockOwner(Object blockState) {
try {
return Reflections.field$StateHolder$owner.get(blockState);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
public static int physicsEventToId(BlockPhysicsEvent event) throws ReflectiveOperationException {
Object blockData = Reflections.field$BlockPhysicsEvent$changed.get(event);
Object blockState = Reflections.field$CraftBlockData$data.get(blockData);
return (int) Reflections.method$IdMapper$getId.invoke(Reflections.instance$BLOCK_STATE_REGISTRY, blockState);
}
public static Object physicsEventToState(BlockPhysicsEvent event) throws ReflectiveOperationException {
Object blockData = Reflections.field$BlockPhysicsEvent$changed.get(event);
return Reflections.field$CraftBlockData$data.get(blockData);
}
public static void setLightEmission(Object state, int emission) throws ReflectiveOperationException {
Reflections.field$BlockStateBase$lightEmission.set(state, emission);
}
public static int getLightEmission(Object state) throws ReflectiveOperationException {
return (int) Reflections.field$BlockStateBase$lightEmission.get(state);
}
public static void setMapColor(Object state, MapColor color) throws ReflectiveOperationException {
Object mcMapColor = Reflections.method$MapColor$byId.invoke(null, color.id);
Reflections.field$BlockStateBase$mapColor.set(state, mcMapColor);
}
public static void setInstrument(Object state, Instrument instrument) throws ReflectiveOperationException {
Object mcInstrument = ((Object[]) Reflections.method$NoteBlockInstrument$values.invoke(null))[instrument.ordinal()];
Reflections.field$BlockStateBase$instrument.set(state, mcInstrument);
}
public static void setHardness(Object state, float hardness) throws ReflectiveOperationException {
Reflections.field$BlockStateBase$hardness.set(state, hardness);
}
public static void setBurnable(Object state, boolean burnable) throws ReflectiveOperationException {
Reflections.field$BlockStateBase$burnable.set(state, burnable);
}
public static void setPushReaction(Object state, PushReaction reaction) throws ReflectiveOperationException {
Object pushReaction = ((Object[]) Reflections.method$PushReaction$values.invoke(null))[reaction.ordinal()];
Reflections.field$BlockStateBase$pushReaction.set(state, pushReaction);
}
public static void setIsRandomlyTicking(Object state, boolean randomlyTicking) throws ReflectiveOperationException {
Reflections.field$BlockStateBase$isRandomlyTicking.set(state, randomlyTicking);
}
public static void setReplaceable(Object state, boolean replaceable) throws ReflectiveOperationException {
Reflections.field$BlockStateBase$replaceable.set(state, replaceable);
}
public static boolean isReplaceable(Object state) throws ReflectiveOperationException {
return (boolean) Reflections.field$BlockStateBase$replaceable.get(state);
}
public static void setCanOcclude(Object state, boolean canOcclude) throws ReflectiveOperationException {
Reflections.field$BlockStateBase$canOcclude.set(state, canOcclude);
}
public static boolean isOcclude(Object state) throws ReflectiveOperationException {
return (boolean) Reflections.field$BlockStateBase$canOcclude.get(state);
}
public static void setIsRedstoneConductor(Object state, Object predicate) throws ReflectiveOperationException {
Reflections.field$BlockStateBase$isRedstoneConductor.set(state, predicate);
}
public static void setIsSuffocating(Object state, Object predicate) throws ReflectiveOperationException {
Reflections.field$BlockStateBase$isSuffocating.set(state, predicate);
}
public static void setIsViewBlocking(Object state, Object predicate) throws ReflectiveOperationException {
Reflections.field$BlockStateBase$isViewBlocking.set(state, predicate);
}
public static boolean isClientSideNoteBlock(Object state) {
return CLIENT_SIDE_NOTE_BLOCKS.containsKey(state);
}
public static boolean isVanillaBlock(Object state) {
int id = blockStateToId(state);
return id >= 0 && id < vanillaStateSize;
}
public static boolean isVanillaBlock(int id) {
return id >= 0 && id < vanillaStateSize;
}
public static int vanillaStateSize() {
return vanillaStateSize;
}
}

View File

@@ -0,0 +1,27 @@
package net.momirealms.craftengine.bukkit.util;
import net.momirealms.craftengine.core.util.Key;
import java.util.HashMap;
import java.util.Map;
public class BlockTags {
private static final Map<Key, Object> CACHE = new HashMap<>();
private BlockTags() {}
public static Object getOrCreate(Key key) {
Object value = CACHE.get(key);
if (value == null) {
try {
value = Reflections.method$TagKey$create.invoke(null, Reflections.instance$Registries$BLOCK, Reflections.method$ResourceLocation$fromNamespaceAndPath.invoke(null, key.namespace(), key.value()));
CACHE.put(key, value);
return value;
} catch (Exception e) {
throw new RuntimeException("Failed to create block tag: " + key, e);
}
} else {
return value;
}
}
}

View File

@@ -0,0 +1,69 @@
package net.momirealms.craftengine.bukkit.util;
import net.momirealms.craftengine.core.util.ReflectionUtils;
import org.bukkit.Bukkit;
import java.lang.reflect.Method;
import java.util.Objects;
public final class BukkitReflectionUtils {
private static final String PREFIX_MC = "net.minecraft.";
private static final String PREFIX_CRAFTBUKKIT = "org.bukkit.craftbukkit";
private static final String CRAFT_SERVER = "CraftServer";
private static final String CB_PKG_VERSION;
public static final int MAJOR_REVISION;
private BukkitReflectionUtils() {}
static {
final Class<?> serverClass;
if (Bukkit.getServer() == null) {
// Paper plugin Bootstrapper 1.20.6+
serverClass = Objects.requireNonNull(ReflectionUtils.getClazz("org.bukkit.craftbukkit.CraftServer"));
} else {
serverClass = Bukkit.getServer().getClass();
}
final String pkg = serverClass.getPackage().getName();
final String nmsVersion = pkg.substring(pkg.lastIndexOf(".") + 1);
if (!nmsVersion.contains("_")) {
int fallbackVersion = -1;
if (Bukkit.getServer() != null) {
try {
final Method getMinecraftVersion = serverClass.getDeclaredMethod("getMinecraftVersion");
fallbackVersion = Integer.parseInt(getMinecraftVersion.invoke(Bukkit.getServer()).toString().split("\\.")[1]);
} catch (final Exception ignored) {
}
} else {
// Paper plugin bootstrapper 1.20.6+
try {
final Class<?> sharedConstants = Objects.requireNonNull(ReflectionUtils.getClazz("net.minecraft.SharedConstants"));
final Method getCurrentVersion = sharedConstants.getDeclaredMethod("getCurrentVersion");
final Object currentVersion = getCurrentVersion.invoke(null);
final Method getName = currentVersion.getClass().getDeclaredMethod("getName");
final String versionName = (String) getName.invoke(currentVersion);
try {
fallbackVersion = Integer.parseInt(versionName.split("\\.")[1]);
} catch (final Exception ignored) {
}
} catch (final ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
MAJOR_REVISION = fallbackVersion;
} else {
MAJOR_REVISION = Integer.parseInt(nmsVersion.split("_")[1]);
}
String name = serverClass.getName();
name = name.substring(PREFIX_CRAFTBUKKIT.length());
name = name.substring(0, name.length() - CRAFT_SERVER.length());
CB_PKG_VERSION = name;
}
public static String assembleCBClass(String className) {
return PREFIX_CRAFTBUKKIT + CB_PKG_VERSION + className;
}
public static String assembleMCClass(String className) {
return PREFIX_MC + className;
}
}

View File

@@ -0,0 +1,27 @@
package net.momirealms.craftengine.bukkit.util;
import net.kyori.adventure.text.Component;
import net.momirealms.craftengine.core.util.AdventureHelper;
import net.momirealms.craftengine.core.util.VersionHelper;
public class ComponentUtils {
private ComponentUtils() {}
public static Object adventureToMinecraft(Component component) {
String json = AdventureHelper.componentToJson(component);
if (VersionHelper.isVersionNewerThan1_20_5()) {
try {
return Reflections.method$Component$Serializer$fromJson.invoke(null, json, Reflections.instance$MinecraftRegistry);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
} else {
try {
return Reflections.method$CraftChatMessage$fromJSON.invoke(null, json);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
}
}

View File

@@ -0,0 +1,32 @@
package net.momirealms.craftengine.bukkit.util;
import net.momirealms.craftengine.core.util.Direction;
import org.bukkit.block.BlockFace;
public class DirectionUtils {
private DirectionUtils() {}
public static Direction toDirection(BlockFace face) {
return switch (face) {
case UP -> Direction.UP;
case DOWN -> Direction.DOWN;
case NORTH -> Direction.NORTH;
case SOUTH -> Direction.SOUTH;
case WEST -> Direction.WEST;
case EAST -> Direction.EAST;
default -> throw new IllegalStateException("Unexpected value: " + face);
};
}
public static BlockFace toBlockFace(Direction direction) {
return switch (direction) {
case UP -> BlockFace.UP;
case DOWN -> BlockFace.DOWN;
case NORTH -> BlockFace.NORTH;
case SOUTH -> BlockFace.SOUTH;
case WEST -> BlockFace.WEST;
case EAST -> BlockFace.EAST;
};
}
}

View File

@@ -0,0 +1,23 @@
package net.momirealms.craftengine.bukkit.util;
import java.util.HashMap;
import java.util.Map;
public class EnchantmentUtils {
private EnchantmentUtils() {}
@SuppressWarnings("unchecked")
public static Map<String, Integer> toMap(Object itemEnchantments) throws ReflectiveOperationException {
Map<String, Integer> map = new HashMap<>();
Map<Object, Integer> enchantments = (Map<Object, Integer>) Reflections.field$ItemEnchantments$enchantments.get(itemEnchantments);
for (Map.Entry<Object, Integer> entry : enchantments.entrySet()) {
Object holder = entry.getKey();
String name = (String) Reflections.method$Holder$getRegisteredName.invoke(holder);
int level = entry.getValue();
map.put(name, level);
}
return map;
}
}

View File

@@ -0,0 +1,84 @@
package net.momirealms.craftengine.bukkit.util;
public class EntityDataUtils {
private EntityDataUtils() {}
private static final int HAS_SHADOW = 0x01; // 1
private static final int IS_SEE_THROUGH = 0x02; // 2
private static final int USE_DEFAULT_BACKGROUND = 0x04; // 4
private static final int LEFT_ALIGNMENT = 0x08; // 8
private static final int RIGHT_ALIGNMENT = 0x10; // 16
public static byte encodeTextDisplayMask(boolean hasShadow, boolean isSeeThrough, boolean useDefaultBackground, int alignment) {
int bitMask = 0;
if (hasShadow) {
bitMask |= HAS_SHADOW;
}
if (isSeeThrough) {
bitMask |= IS_SEE_THROUGH;
}
if (useDefaultBackground) {
bitMask |= USE_DEFAULT_BACKGROUND;
}
switch (alignment) {
case 0: // CENTER
break;
case 1: // LEFT
bitMask |= LEFT_ALIGNMENT;
break;
case 2: // RIGHT
bitMask |= RIGHT_ALIGNMENT;
break;
default:
throw new IllegalArgumentException("Invalid alignment value");
}
return (byte) bitMask;
}
private static final int IS_ON_FIRE = 0x01; // 1
private static final int IS_CROUCHING = 0x02; // 2
private static final int UNUSED = 0x04; // 4
private static final int IS_SPRINTING = 0x08; // 8
private static final int IS_SWIMMING = 0x10; // 16
private static final int IS_INVISIBLE = 0x20; // 32
private static final int HAS_GLOWING_EFFECT = 0x40; // 64
private static final int IS_FLYING_WITH_ELYTRA = 0x80; // 128
public static byte encodeCommonMask(boolean isOnFire, boolean isCrouching, boolean isUnused,
boolean isSprinting, boolean isSwimming, boolean isInvisible,
boolean hasGlowingEffect, boolean isFlyingWithElytra) {
int bitMask = 0;
if (isOnFire) {
bitMask |= IS_ON_FIRE;
}
if (isCrouching) {
bitMask |= IS_CROUCHING;
}
if (isUnused) {
bitMask |= UNUSED;
}
if (isSprinting) {
bitMask |= IS_SPRINTING;
}
if (isSwimming) {
bitMask |= IS_SWIMMING;
}
if (isInvisible) {
bitMask |= IS_INVISIBLE;
}
if (hasGlowingEffect) {
bitMask |= HAS_GLOWING_EFFECT;
}
if (isFlyingWithElytra) {
bitMask |= IS_FLYING_WITH_ELYTRA;
}
return (byte) bitMask;
}
public static boolean isCrouching(byte mask) {
return (mask & IS_CROUCHING) != 0;
}
}

Some files were not shown because too many files have changed in this diff Show More