mirror of
https://github.com/IntellectualSites/PlotSquared.git
synced 2025-10-24 15:13:44 +02:00
fix/chore: support signs in spigot
no need to use different implementations for spigot vs paper anymore
This commit is contained in:
@@ -46,6 +46,7 @@ import com.plotsquared.bukkit.listener.WorldEvents;
|
|||||||
import com.plotsquared.bukkit.placeholder.PAPIPlaceholders;
|
import com.plotsquared.bukkit.placeholder.PAPIPlaceholders;
|
||||||
import com.plotsquared.bukkit.placeholder.PlaceholderFormatter;
|
import com.plotsquared.bukkit.placeholder.PlaceholderFormatter;
|
||||||
import com.plotsquared.bukkit.player.BukkitPlayerManager;
|
import com.plotsquared.bukkit.player.BukkitPlayerManager;
|
||||||
|
import com.plotsquared.bukkit.schematic.StateWrapper;
|
||||||
import com.plotsquared.bukkit.util.BukkitUtil;
|
import com.plotsquared.bukkit.util.BukkitUtil;
|
||||||
import com.plotsquared.bukkit.util.BukkitWorld;
|
import com.plotsquared.bukkit.util.BukkitWorld;
|
||||||
import com.plotsquared.bukkit.util.SetGenCB;
|
import com.plotsquared.bukkit.util.SetGenCB;
|
||||||
@@ -289,6 +290,18 @@ public final class BukkitPlatform extends JavaPlugin implements Listener, PlotPl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate compatibility of StateWrapper with the current running server version
|
||||||
|
// Do this always, even if it's not required, to prevent running servers which fail to restore plot backups or
|
||||||
|
// inserting broken plot / road templates.
|
||||||
|
try {
|
||||||
|
var instance = StateWrapper.INSTANCE;
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error("Failed to initialize required classes for restoring tile entities. " +
|
||||||
|
"PlotSquared will disable itself to prevent possible damages.", e);
|
||||||
|
getServer().getPluginManager().disablePlugin(this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// We create the injector after PlotSquared has been initialized, so that we have access
|
// We create the injector after PlotSquared has been initialized, so that we have access
|
||||||
// to generated instances and settings
|
// to generated instances and settings
|
||||||
this.injector = Guice
|
this.injector = Guice
|
||||||
|
|||||||
@@ -19,18 +19,14 @@
|
|||||||
package com.plotsquared.bukkit.schematic;
|
package com.plotsquared.bukkit.schematic;
|
||||||
|
|
||||||
import com.plotsquared.bukkit.util.BukkitUtil;
|
import com.plotsquared.bukkit.util.BukkitUtil;
|
||||||
import com.plotsquared.core.PlotSquared;
|
|
||||||
import com.sk89q.jnbt.CompoundTag;
|
import com.sk89q.jnbt.CompoundTag;
|
||||||
import io.papermc.lib.PaperLib;
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
|
||||||
import org.apache.logging.log4j.Logger;
|
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.Block;
|
||||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||||
import org.jetbrains.annotations.ApiStatus;
|
import org.jetbrains.annotations.ApiStatus;
|
||||||
|
|
||||||
@ApiStatus.Internal
|
@ApiStatus.Internal
|
||||||
public sealed interface StateWrapper permits StateWrapperSpigot, StateWrapper.Factory.NoopStateWrapper {
|
public sealed interface StateWrapper permits StateWrapperSpigot {
|
||||||
|
|
||||||
StateWrapper INSTANCE = Factory.createStateWrapper();
|
StateWrapper INSTANCE = Factory.createStateWrapper();
|
||||||
|
|
||||||
@@ -47,40 +43,8 @@ public sealed interface StateWrapper permits StateWrapperSpigot, StateWrapper.Fa
|
|||||||
@ApiStatus.Internal
|
@ApiStatus.Internal
|
||||||
final class Factory {
|
final class Factory {
|
||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + StateWrapper.class.getSimpleName());
|
|
||||||
|
|
||||||
private static final String INITIALIZATION_ERROR_TEMPLATE = """
|
|
||||||
Failed to initialize StateWrapper: {}
|
|
||||||
Block-/Tile-Entities, pasted by schematics for example, won't be updated with their respective block data. This affects things like sign text, banner patterns, skulls, etc.
|
|
||||||
Try updating your Server Software, PlotSquared and WorldEdit / FastAsyncWorldEdit first. If the issue persists, report it on the issue tracker.
|
|
||||||
""";
|
|
||||||
|
|
||||||
private static StateWrapper createStateWrapper() {
|
private static StateWrapper createStateWrapper() {
|
||||||
int[] serverVersion = PlotSquared.platform().serverVersion();
|
return new StateWrapperSpigot();
|
||||||
if (PaperLib.isPaper() && (serverVersion[1] == 21 && serverVersion[2] >= 5) || serverVersion[1] > 21) {
|
|
||||||
try {
|
|
||||||
return new StateWrapperPaper1_21_5();
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOGGER.error("Failed to initialize Paper-specific state wrapper, falling back to Spigot", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return new StateWrapperSpigot();
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOGGER.error(INITIALIZATION_ERROR_TEMPLATE, StateWrapperSpigot.class.getSimpleName(), e);
|
|
||||||
}
|
|
||||||
return new NoopStateWrapper();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@ApiStatus.Internal
|
|
||||||
static final class NoopStateWrapper implements StateWrapper {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean restore(final @NonNull Block block, final @NonNull CompoundTag data) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,155 +0,0 @@
|
|||||||
/*
|
|
||||||
* PlotSquared, a land and world management plugin for Minecraft.
|
|
||||||
* Copyright (C) IntellectualSites <https://intellectualsites.com>
|
|
||||||
* Copyright (C) IntellectualSites team and contributors
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
package com.plotsquared.bukkit.schematic;
|
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import com.google.gson.GsonBuilder;
|
|
||||||
import com.google.gson.JsonElement;
|
|
||||||
import com.google.gson.JsonParser;
|
|
||||||
import com.google.gson.JsonSyntaxException;
|
|
||||||
import com.sk89q.jnbt.CompoundTag;
|
|
||||||
import com.sk89q.jnbt.Tag;
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
|
||||||
import org.apache.logging.log4j.Logger;
|
|
||||||
import org.bukkit.DyeColor;
|
|
||||||
import org.bukkit.block.BlockState;
|
|
||||||
import org.bukkit.block.Sign;
|
|
||||||
import org.bukkit.block.sign.Side;
|
|
||||||
import org.bukkit.block.sign.SignSide;
|
|
||||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
|
||||||
|
|
||||||
import java.lang.invoke.MethodHandle;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
final class StateWrapperPaper1_21_5 extends StateWrapperSpigot {
|
|
||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + StateWrapperPaper1_21_5.class.getSimpleName());
|
|
||||||
|
|
||||||
private static final Gson GSON = new GsonBuilder().registerTypeHierarchyAdapter(Tag.class, new NbtGsonSerializer()).create();
|
|
||||||
private static Object KYORI_GSON_SERIALIZER = null;
|
|
||||||
private static MethodHandle GSON_SERIALIZER_DESERIALIZE_TREE = null;
|
|
||||||
private static MethodHandle BUKKIT_SIGN_SIDE_LINE_SET = null;
|
|
||||||
|
|
||||||
public StateWrapperPaper1_21_5() {
|
|
||||||
super();
|
|
||||||
try {
|
|
||||||
initializeSignHack();
|
|
||||||
LOGGER.info("Using {} for block data population", StateWrapperPaper1_21_5.class.getSimpleName());
|
|
||||||
} catch (Throwable e) {
|
|
||||||
throw new RuntimeException("Failed to initialize sign hack", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void postEntityStateLoad(final @NonNull BlockState blockState, final @NonNull CompoundTag data) throws Throwable {
|
|
||||||
// signs need special handling during generation
|
|
||||||
if (blockState instanceof Sign sign) {
|
|
||||||
if (data.getValue().get("front_text") instanceof CompoundTag textTag) {
|
|
||||||
setSignTextHack(sign.getSide(Side.FRONT), textTag);
|
|
||||||
}
|
|
||||||
if (data.getValue().get("back_text") instanceof CompoundTag textTag) {
|
|
||||||
setSignTextHack(sign.getSide(Side.BACK), textTag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Logger logger() {
|
|
||||||
return StateWrapperPaper1_21_5.LOGGER;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set sign content on the bukkit tile entity. The server does not load sign content applied via the main logic
|
|
||||||
* (CraftBlockEntity#load), as the SignEntity needs to have a valid ServerLevel assigned to it.
|
|
||||||
* That's not possible on worldgen; therefore, this hack has to be used additionally.
|
|
||||||
* <br />
|
|
||||||
* Modern sign content (non-plain-text sign lines) require Paper.
|
|
||||||
*
|
|
||||||
* @param side The sign side to apply data onto.
|
|
||||||
* @param text The compound tag containing the data for the sign side ({@code front_text} / {@code back_text})
|
|
||||||
* @throws Throwable if something went wrong when reflectively updating the sign.
|
|
||||||
*/
|
|
||||||
private static void setSignTextHack(SignSide side, CompoundTag text) throws Throwable {
|
|
||||||
if (text.containsKey("color")) {
|
|
||||||
//noinspection UnstableApiUsage
|
|
||||||
side.setColor(DyeColor.legacyValueOf(text.getString("color").toUpperCase(Locale.ROOT)));
|
|
||||||
}
|
|
||||||
if (text.containsKey("has_glowing_text")) {
|
|
||||||
side.setGlowingText(text.getByte("has_glowing_text") == 1);
|
|
||||||
}
|
|
||||||
List<Tag> lines = text.getList("messages");
|
|
||||||
if (lines != null) {
|
|
||||||
for (int i = 0; i < Math.min(lines.size(), 3); i++) {
|
|
||||||
Tag line = lines.get(i);
|
|
||||||
Object content = line.getValue();
|
|
||||||
// Minecraft uses mixed lists / arrays in their sign texts. One line can be a complex component, whereas
|
|
||||||
// the following line could simply be a string. Those simpler lines are represented as `{"": ""}` (only in
|
|
||||||
// SNBT those will be shown as a standard string).
|
|
||||||
if (line instanceof CompoundTag compoundTag && compoundTag.getValue().containsKey("")) {
|
|
||||||
content = compoundTag.getValue().get("");
|
|
||||||
}
|
|
||||||
// absolute garbage way to try to handle stringified components (pre 1.21.5)
|
|
||||||
else if (content instanceof String contentAsString && (contentAsString.startsWith("{") || contentAsString.startsWith("["))) {
|
|
||||||
try {
|
|
||||||
content = JsonParser.parseString(contentAsString);
|
|
||||||
} catch (JsonSyntaxException e) {
|
|
||||||
// well, it wasn't JSON after all
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// serializes the line content from JNBT to Gson JSON objects, passes that to adventure and deserializes
|
|
||||||
// into an adventure component.
|
|
||||||
// pass all possible types of content into the deserializer (Strings, Compounds, Arrays), even though Strings
|
|
||||||
// could be set directly via Sign#setLine(int, String). The overhead is minimal, the serializer can handle
|
|
||||||
// strings - and we don't have to use the deprecated method.
|
|
||||||
BUKKIT_SIGN_SIDE_LINE_SET.invoke(
|
|
||||||
side, i, GSON_SERIALIZER_DESERIALIZE_TREE.invoke(
|
|
||||||
KYORI_GSON_SERIALIZER,
|
|
||||||
content instanceof JsonElement ? content : GSON.toJsonTree(content)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void initializeSignHack() throws Throwable {
|
|
||||||
char[] dontObfuscate = new char[]{
|
|
||||||
'n', 'e', 't', '.', 'k', 'y', 'o', 'r', 'i', '.', 'a', 'd', 'v', 'e', 'n', 't', 'u', 'r', 'e', '.',
|
|
||||||
't', 'e', 'x', 't', '.', 's', 'e', 'r', 'i', 'a', 'l', 'i', 'z', 'e', 'r', '.', 'g', 's', 'o', 'n', '.',
|
|
||||||
'G', 's', 'o', 'n', 'C', 'o', 'm', 'p', 'o', 'n', 'e', 'n', 't', 'S', 'e', 'r', 'i', 'a', 'l', 'i', 'z', 'e', 'r'
|
|
||||||
};
|
|
||||||
Class<?> gsonComponentSerializerClass = Class.forName(new String(dontObfuscate));
|
|
||||||
KYORI_GSON_SERIALIZER = Arrays.stream(gsonComponentSerializerClass.getMethods())
|
|
||||||
.filter(method -> method.getName().equals("gson"))
|
|
||||||
.findFirst()
|
|
||||||
.orElseThrow().invoke(null);
|
|
||||||
GSON_SERIALIZER_DESERIALIZE_TREE = LOOKUP.unreflect(Arrays
|
|
||||||
.stream(gsonComponentSerializerClass.getMethods())
|
|
||||||
.filter(method -> method.getName().equals("deserializeFromTree") && method.getParameterCount() == 1)
|
|
||||||
.findFirst()
|
|
||||||
.orElseThrow());
|
|
||||||
BUKKIT_SIGN_SIDE_LINE_SET = LOOKUP.unreflect(Arrays.stream(SignSide.class.getMethods())
|
|
||||||
.filter(method -> method.getName().equals("line") && method.getParameterCount() == 2)
|
|
||||||
.findFirst()
|
|
||||||
.orElseThrow());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -18,42 +18,66 @@
|
|||||||
*/
|
*/
|
||||||
package com.plotsquared.bukkit.schematic;
|
package com.plotsquared.bukkit.schematic;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
import com.plotsquared.core.util.ReflectionUtils;
|
import com.plotsquared.core.util.ReflectionUtils;
|
||||||
import com.sk89q.jnbt.CompoundTag;
|
import com.sk89q.jnbt.CompoundTag;
|
||||||
|
import com.sk89q.jnbt.Tag;
|
||||||
import com.sk89q.worldedit.bukkit.WorldEditPlugin;
|
import com.sk89q.worldedit.bukkit.WorldEditPlugin;
|
||||||
import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter;
|
import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter;
|
||||||
|
import com.sk89q.worldedit.bukkit.adapter.Refraction;
|
||||||
import com.sk89q.worldedit.extension.platform.NoCapablePlatformException;
|
import com.sk89q.worldedit.extension.platform.NoCapablePlatformException;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.DyeColor;
|
||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.Block;
|
||||||
import org.bukkit.block.BlockState;
|
import org.bukkit.block.BlockState;
|
||||||
|
import org.bukkit.block.Sign;
|
||||||
|
import org.bukkit.block.sign.Side;
|
||||||
|
import org.bukkit.block.sign.SignSide;
|
||||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||||
|
|
||||||
import java.lang.invoke.MethodHandle;
|
import java.lang.invoke.MethodHandle;
|
||||||
import java.lang.invoke.MethodHandles;
|
import java.lang.invoke.MethodHandles;
|
||||||
import java.lang.invoke.MethodType;
|
import java.lang.invoke.MethodType;
|
||||||
|
import java.lang.reflect.Array;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
sealed class StateWrapperSpigot implements StateWrapper permits StateWrapperPaper1_21_5 {
|
final class StateWrapperSpigot implements StateWrapper {
|
||||||
|
|
||||||
private static final boolean FORCE_UPDATE_STATE = true;
|
private static final boolean FORCE_UPDATE_STATE = true;
|
||||||
private static final boolean UPDATE_TRIGGER_PHYSICS = false;
|
private static final boolean UPDATE_TRIGGER_PHYSICS = false;
|
||||||
private static final String CRAFTBUKKIT_PACKAGE = Bukkit.getServer().getClass().getPackageName();
|
private static final String CRAFTBUKKIT_PACKAGE = Bukkit.getServer().getClass().getPackageName();
|
||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + StateWrapperSpigot.class.getSimpleName());
|
private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + StateWrapperSpigot.class.getSimpleName());
|
||||||
static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
|
private static final Gson GSON = new GsonBuilder().registerTypeHierarchyAdapter(Tag.class, new NbtGsonSerializer()).create();
|
||||||
|
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
|
||||||
|
|
||||||
private static BukkitImplAdapter ADAPTER = null;
|
private static BukkitImplAdapter ADAPTER = null;
|
||||||
private static Class<?> LIN_TAG_CLASS = null;
|
private static Class<?> LIN_TAG_CLASS = null;
|
||||||
private static Class<?> JNBT_TAG_CLASS = null;
|
private static Class<?> JNBT_TAG_CLASS = null;
|
||||||
private static Class<?> CRAFT_BLOCK_ENTITY_STATE_CLASS = null;
|
private static Class<?> CRAFT_BLOCK_ENTITY_STATE_CLASS = null;
|
||||||
|
private static Class<?> MINECRAFT_CHAT_COMPONENT_CLASS = null;
|
||||||
|
private static Field CRAFT_SIGN_SIDE_SIGN_TEXT = null;
|
||||||
private static MethodHandle PAPERWEIGHT_ADAPTER_FROM_NATIVE = null;
|
private static MethodHandle PAPERWEIGHT_ADAPTER_FROM_NATIVE = null;
|
||||||
private static MethodHandle CRAFT_BLOCK_ENTITY_STATE_LOAD_DATA = null;
|
private static MethodHandle CRAFT_BLOCK_ENTITY_STATE_LOAD_DATA = null;
|
||||||
private static MethodHandle CRAFT_BLOCK_ENTITY_STATE_UPDATE = null;
|
private static MethodHandle CRAFT_BLOCK_ENTITY_STATE_UPDATE = null;
|
||||||
|
private static MethodHandle CRAFT_BLOCK_ENTITY_STATE_GET_SNAPSHOT = null;
|
||||||
|
private static MethodHandle SIGN_BLOCK_ENTITY_SET_TEXT = null;
|
||||||
|
private static MethodHandle MINECRAFT_DYE_COLOR_BY_NAME = null;
|
||||||
|
private static MethodHandle CRAFT_CHAT_MESSAGE_FROM_JSON_OR_STRING = null;
|
||||||
|
private static MethodHandle SIGN_TEXT_CONSTRUCTOR = null;
|
||||||
private static MethodHandle TO_LIN_TAG = null;
|
private static MethodHandle TO_LIN_TAG = null;
|
||||||
|
|
||||||
|
private static Object DYE_COLOR_BLACK = null;
|
||||||
|
|
||||||
public StateWrapperSpigot() {
|
public StateWrapperSpigot() {
|
||||||
try {
|
try {
|
||||||
findNbtCompoundClassType(clazz -> LIN_TAG_CLASS = clazz, clazz -> JNBT_TAG_CLASS = clazz);
|
findNbtCompoundClassType(clazz -> LIN_TAG_CLASS = clazz, clazz -> JNBT_TAG_CLASS = clazz);
|
||||||
@@ -74,10 +98,39 @@ sealed class StateWrapperSpigot implements StateWrapper permits StateWrapperPape
|
|||||||
throw new RuntimeException("Failed to access required WorldEdit methods", e);
|
throw new RuntimeException("Failed to access required WorldEdit methods", e);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
final Class<?> SIGN_TEXT_CLASS = Class.forName("net.minecraft.world.level.block.entity.SignText");
|
||||||
|
MINECRAFT_CHAT_COMPONENT_CLASS = Class.forName(Refraction.pickName(
|
||||||
|
"net.minecraft.network.chat.Component",
|
||||||
|
"net.minecraft.network.chat.IChatBaseComponent"
|
||||||
|
));
|
||||||
|
final Class<?> MINECRAFT_DYE_COLOR_CLASS = Class.forName(Refraction.pickName(
|
||||||
|
"net.minecraft.world.item.DyeColor",
|
||||||
|
"net.minecraft.world.item.EnumColor"
|
||||||
|
));
|
||||||
|
CRAFT_SIGN_SIDE_SIGN_TEXT = Class.forName(CRAFTBUKKIT_PACKAGE + ".block.sign.CraftSignSide")
|
||||||
|
.getDeclaredField("signText");
|
||||||
|
CRAFT_SIGN_SIDE_SIGN_TEXT.setAccessible(true);
|
||||||
CRAFT_BLOCK_ENTITY_STATE_CLASS = Class.forName(CRAFTBUKKIT_PACKAGE + ".block.CraftBlockEntityState");
|
CRAFT_BLOCK_ENTITY_STATE_CLASS = Class.forName(CRAFTBUKKIT_PACKAGE + ".block.CraftBlockEntityState");
|
||||||
CRAFT_BLOCK_ENTITY_STATE_LOAD_DATA = findCraftBlockEntityStateLoadDataMethodHandle(CRAFT_BLOCK_ENTITY_STATE_CLASS);
|
CRAFT_BLOCK_ENTITY_STATE_LOAD_DATA = findCraftBlockEntityStateLoadDataMethodHandle(CRAFT_BLOCK_ENTITY_STATE_CLASS);
|
||||||
CRAFT_BLOCK_ENTITY_STATE_UPDATE = findCraftBlockEntityStateUpdateMethodHandle(CRAFT_BLOCK_ENTITY_STATE_CLASS);
|
CRAFT_BLOCK_ENTITY_STATE_UPDATE = findCraftBlockEntityStateUpdateMethodHandle(CRAFT_BLOCK_ENTITY_STATE_CLASS);
|
||||||
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) {
|
CRAFT_BLOCK_ENTITY_STATE_GET_SNAPSHOT = findCraftBlockEntityStateSnapshotMethodHandle(CRAFT_BLOCK_ENTITY_STATE_CLASS);
|
||||||
|
SIGN_BLOCK_ENTITY_SET_TEXT = findSignBlockEntitySetTextMethodHandle(
|
||||||
|
Class.forName(Refraction.pickName(
|
||||||
|
"net.minecraft.world.level.block.entity.SignBlockEntity",
|
||||||
|
"net.minecraft.world.level.block.entity.TileEntitySign"
|
||||||
|
)),
|
||||||
|
SIGN_TEXT_CLASS
|
||||||
|
);
|
||||||
|
CRAFT_CHAT_MESSAGE_FROM_JSON_OR_STRING = findCraftChatMessageFromJsonOrStringMethodHandle(
|
||||||
|
MINECRAFT_CHAT_COMPONENT_CLASS);
|
||||||
|
SIGN_TEXT_CONSTRUCTOR = findSignTextConstructor(
|
||||||
|
SIGN_TEXT_CLASS, MINECRAFT_CHAT_COMPONENT_CLASS, MINECRAFT_DYE_COLOR_CLASS
|
||||||
|
);
|
||||||
|
MINECRAFT_DYE_COLOR_BY_NAME = findDyeColorByNameMethodHandle(MINECRAFT_DYE_COLOR_CLASS);
|
||||||
|
DYE_COLOR_BLACK = Objects.requireNonNull(
|
||||||
|
MINECRAFT_DYE_COLOR_BY_NAME.invoke("black", null), "couldn't find black dye color"
|
||||||
|
);
|
||||||
|
} catch (Throwable e) {
|
||||||
throw new RuntimeException("Failed to initialize required native method accessors", e);
|
throw new RuntimeException("Failed to initialize required native method accessors", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,17 +154,67 @@ sealed class StateWrapperSpigot implements StateWrapper permits StateWrapperPape
|
|||||||
|
|
||||||
CRAFT_BLOCK_ENTITY_STATE_UPDATE.invoke(blockState, FORCE_UPDATE_STATE, UPDATE_TRIGGER_PHYSICS);
|
CRAFT_BLOCK_ENTITY_STATE_UPDATE.invoke(blockState, FORCE_UPDATE_STATE, UPDATE_TRIGGER_PHYSICS);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
logger().error("Failed to update tile entity", e);
|
LOGGER.error("Failed to update tile entity", e);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void postEntityStateLoad(final @NonNull BlockState blockState, final @NonNull CompoundTag data) throws Throwable {
|
public void postEntityStateLoad(final @NonNull BlockState blockState, final @NonNull CompoundTag data) throws Throwable {
|
||||||
|
if (blockState instanceof Sign sign) {
|
||||||
|
if (data.getValue().get("front_text") instanceof CompoundTag textTag) {
|
||||||
|
setSignContents(true, sign.getSide(Side.FRONT), blockState, textTag);
|
||||||
|
}
|
||||||
|
if (data.getValue().get("back_text") instanceof CompoundTag textTag) {
|
||||||
|
setSignContents(false, sign.getSide(Side.BACK), blockState, textTag);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Logger logger() {
|
private static void setSignContents(boolean front, SignSide side, BlockState blockState, CompoundTag data) throws Throwable {
|
||||||
return StateWrapperSpigot.LOGGER;
|
final List<Tag> messages = data.getList("messages");
|
||||||
|
if (messages.size() != 4) {
|
||||||
|
if (data.containsKey("color")) {
|
||||||
|
//noinspection UnstableApiUsage
|
||||||
|
side.setColor(DyeColor.legacyValueOf(data.getString("color").toUpperCase(Locale.ROOT)));
|
||||||
|
}
|
||||||
|
side.setGlowingText(data.getByte("has_glowing_text") == 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String color = data.getString("color");
|
||||||
|
final boolean glowing = data.getByte("has_glowing_text") == 1;
|
||||||
|
final Object dyeColor = color.isEmpty() ? DYE_COLOR_BLACK : MINECRAFT_DYE_COLOR_BY_NAME.invoke(
|
||||||
|
color.equalsIgnoreCase("silver") ? "light_gray" : color,
|
||||||
|
DYE_COLOR_BLACK // fallback
|
||||||
|
);
|
||||||
|
|
||||||
|
Object[] components = new Object[messages.size()];
|
||||||
|
for (int i = 0; i < components.length; i++) {
|
||||||
|
final Tag message = messages.get(i);
|
||||||
|
Object content;
|
||||||
|
// unwrap possible nested entry for mixed array types in later versions
|
||||||
|
if (message instanceof CompoundTag tag && tag.containsKey("")) {
|
||||||
|
content = tag.getString("");
|
||||||
|
} else {
|
||||||
|
content = message.getValue();
|
||||||
|
}
|
||||||
|
// if the value is not a string, make it one so it can be converted to a chat component
|
||||||
|
if (!(content instanceof String)) {
|
||||||
|
content = GSON.toJson(content);
|
||||||
|
LOGGER.info("GSON serialized to {}", content);
|
||||||
|
LOGGER.info("factory to {}", CRAFT_CHAT_MESSAGE_FROM_JSON_OR_STRING.invoke((String) content));
|
||||||
|
}
|
||||||
|
// chat.Component
|
||||||
|
components[i] = CRAFT_CHAT_MESSAGE_FROM_JSON_OR_STRING.invoke((String) content);
|
||||||
|
}
|
||||||
|
final Object typedComponents = Array.newInstance(MINECRAFT_CHAT_COMPONENT_CLASS, components.length);
|
||||||
|
System.arraycopy(components, 0, typedComponents, 0, components.length);
|
||||||
|
final Object signText = SIGN_TEXT_CONSTRUCTOR.invoke(typedComponents, typedComponents, dyeColor, glowing);
|
||||||
|
|
||||||
|
// blockState == org.bukkit.craftbukkit.block.CraftBlockEntityState
|
||||||
|
// --> net.minecraft.world.level.block.entity.SignBlockEntity
|
||||||
|
SIGN_BLOCK_ENTITY_SET_TEXT.invoke(CRAFT_BLOCK_ENTITY_STATE_GET_SNAPSHOT.invoke(blockState), signText, front);
|
||||||
|
CRAFT_SIGN_SIDE_SIGN_TEXT.set(side, signText);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -187,7 +290,7 @@ sealed class StateWrapperSpigot implements StateWrapper permits StateWrapperPape
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static MethodHandle findCraftBlockEntityStateLoadDataMethodHandle(Class<?> craftBlockEntityStateClass) throws
|
private static MethodHandle findCraftBlockEntityStateLoadDataMethodHandle(Class<?> craftBlockEntityStateClass) throws
|
||||||
NoSuchMethodException, IllegalAccessException, ClassNotFoundException {
|
NoSuchMethodException, IllegalAccessException {
|
||||||
for (final Method method : craftBlockEntityStateClass.getMethods()) {
|
for (final Method method : craftBlockEntityStateClass.getMethods()) {
|
||||||
if (method.getName().equals("loadData") && method.getParameterCount() == 1) {
|
if (method.getName().equals("loadData") && method.getParameterCount() == 1) {
|
||||||
return LOOKUP.unreflect(method);
|
return LOOKUP.unreflect(method);
|
||||||
@@ -197,7 +300,7 @@ sealed class StateWrapperSpigot implements StateWrapper permits StateWrapperPape
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static MethodHandle findCraftBlockEntityStateUpdateMethodHandle(Class<?> craftBlockEntityStateClass) throws
|
private static MethodHandle findCraftBlockEntityStateUpdateMethodHandle(Class<?> craftBlockEntityStateClass) throws
|
||||||
NoSuchMethodException, IllegalAccessException, ClassNotFoundException {
|
NoSuchMethodException, IllegalAccessException {
|
||||||
for (final Method method : craftBlockEntityStateClass.getMethods()) {
|
for (final Method method : craftBlockEntityStateClass.getMethods()) {
|
||||||
if (method.getReturnType().equals(Boolean.TYPE) && method.getParameterCount() == 2 &&
|
if (method.getReturnType().equals(Boolean.TYPE) && method.getParameterCount() == 2 &&
|
||||||
method.getParameterTypes()[0] == Boolean.TYPE && method.getParameterTypes()[1] == Boolean.TYPE) {
|
method.getParameterTypes()[0] == Boolean.TYPE && method.getParameterTypes()[1] == Boolean.TYPE) {
|
||||||
@@ -207,4 +310,52 @@ sealed class StateWrapperSpigot implements StateWrapper permits StateWrapperPape
|
|||||||
throw new NoSuchMethodException("Couldn't find method for #update(boolean, boolean) in " + craftBlockEntityStateClass.getName());
|
throw new NoSuchMethodException("Couldn't find method for #update(boolean, boolean) in " + craftBlockEntityStateClass.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static MethodHandle findCraftBlockEntityStateSnapshotMethodHandle(Class<?> craftBlockEntityStateClass) throws
|
||||||
|
IllegalAccessException, NoSuchMethodException {
|
||||||
|
// doesn't seem to be obfuscated, but protected
|
||||||
|
final MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(craftBlockEntityStateClass, LOOKUP);
|
||||||
|
return lookup.unreflect(craftBlockEntityStateClass.getDeclaredMethod("getSnapshot"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MethodHandle findSignBlockEntitySetTextMethodHandle(Class<?> signBlockEntity, Class<?> signText) throws
|
||||||
|
NoSuchMethodException, IllegalAccessException {
|
||||||
|
for (final Method method : signBlockEntity.getMethods()) {
|
||||||
|
if (method.getReturnType() == Boolean.TYPE && method.getParameterCount() == 2
|
||||||
|
&& method.getParameterTypes()[0] == signText && method.getParameterTypes()[1] == Boolean.TYPE) {
|
||||||
|
return LOOKUP.unreflect(method);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new NoSuchMethodException("Couldn't lookup SignBlockEntity#setText(SignText, boolean) boolean");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MethodHandle findCraftChatMessageFromJsonOrStringMethodHandle(Class<?> minecraftChatComponent)
|
||||||
|
throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException {
|
||||||
|
// public static IChatBaseComponent fromJSONOrString(String message)
|
||||||
|
return LOOKUP.findStatic(
|
||||||
|
Class.forName(CRAFTBUKKIT_PACKAGE + ".util.CraftChatMessage"),
|
||||||
|
"fromJSONOrString",
|
||||||
|
MethodType.methodType(minecraftChatComponent, String.class)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MethodHandle findSignTextConstructor(Class<?> signText, Class<?> chatComponent, Class<?> dyeColorEnum) throws
|
||||||
|
NoSuchMethodException, IllegalAccessException {
|
||||||
|
return LOOKUP.findConstructor(
|
||||||
|
signText, MethodType.methodType(
|
||||||
|
void.class,
|
||||||
|
chatComponent.arrayType(), chatComponent.arrayType(), dyeColorEnum, Boolean.TYPE
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MethodHandle findDyeColorByNameMethodHandle(Class<?> dyeColorClass) throws
|
||||||
|
NoSuchMethodException, IllegalAccessException {
|
||||||
|
for (final Method method : dyeColorClass.getMethods()) {
|
||||||
|
if (Modifier.isStatic(method.getModifiers()) && method.getParameterCount() == 2 && method.getParameterTypes()[0] == String.class) {
|
||||||
|
return LOOKUP.unreflect(method);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new NoSuchMethodException("Couldn't lookup static DyeColor.byName(String, DyeColor)");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user