diff --git a/build-logic/src/main/kotlin/buildlogic.common.gradle.kts b/build-logic/src/main/kotlin/buildlogic.common.gradle.kts index bc5bea09bf..e9747e9fd8 100644 --- a/build-logic/src/main/kotlin/buildlogic.common.gradle.kts +++ b/build-logic/src/main/kotlin/buildlogic.common.gradle.kts @@ -49,7 +49,7 @@ license { header(rootProject.file("HEADER.txt")) include("**/*.java") include("**/*.kt") - exclude("**/com/sk89q/worldedit/util/formatting/text/serializer/gson/StyleSerializer.java") + exclude("**/com/sk89q/worldedit/util/formatting/text/**/*.java") } plugins.withId("idea") { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c3d1d36f0b..a21076f609 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -76,7 +76,7 @@ mockito-junit-jupiter.module = "org.mockito:mockito-junit-jupiter" commonsCli = "commons-cli:commons-cli:1.4" # https://repo.papermc.io/service/rest/repository/browse/maven-public/io/papermc/paper/paper-api/ -paperApi = "io.papermc.paper:paper-api:1.21.4-R0.1-20250327.133756-218" +paperApi = "io.papermc.paper:paper-api:1.21.5-R0.1-20250416.183508-24" paperLib = "io.papermc:paperlib:1.0.8" dummypermscompat = "com.sk89q:dummypermscompat:1.10" diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/util/formatting/text/adapter/bukkit/SpigotAdapter.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/util/formatting/text/adapter/bukkit/SpigotAdapter.java new file mode 100644 index 0000000000..9213cdfaef --- /dev/null +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/util/formatting/text/adapter/bukkit/SpigotAdapter.java @@ -0,0 +1,200 @@ +/* + * This file is part of text-extras, licensed under the MIT License. + * + * Copyright (c) 2018-2020 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package com.sk89q.worldedit.util.formatting.text.adapter.bukkit; + +import com.google.common.collect.Lists; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.internal.Excluder; +import com.google.gson.reflect.TypeToken; +import com.sk89q.worldedit.util.formatting.text.Component; +import com.sk89q.worldedit.util.formatting.text.serializer.gson.GsonComponentSerializer; +import com.sk89q.worldedit.util.formatting.text.serializer.legacy.LegacyComponentSerializer; +import net.md_5.bungee.api.ChatMessageType; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.chat.ChatVersion; +import net.md_5.bungee.chat.ComponentSerializer; +import net.md_5.bungee.chat.VersionedComponentSerializer; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.function.BiConsumer; + + +@SuppressWarnings({"deprecation", "UnstableApiUsage", "unchecked"}) +final class SpigotAdapter implements Adapter { + private static final boolean BOUND = bind(); + + private static boolean bind() { + try { + final Field factoriesField = field(Gson.class, "factories"); + final Field builderFactoriesField = field(GsonBuilder.class, "factories"); + final Field builderHierarchyFactoriesField = field(GsonBuilder.class, "hierarchyFactories"); + + // WorldEdit start - use different gson depending on MC version + final Gson gson; + Gson tmpGson; + try { + final Field gsonField = field(ComponentSerializer.class, "gson"); + tmpGson = (Gson) gsonField.get(null); + } catch (NoSuchFieldException ignored) { + tmpGson = VersionedComponentSerializer.forVersion(ChatVersion.V1_21_5).getGson(); + } + gson = tmpGson; + // WorldEdit end + + final GsonBuilder builder = GsonComponentSerializer.populate(new GsonBuilder()); + + final List existingFactories = (List) factoriesField.get(gson); + final List newFactories = new ArrayList<>(); + newFactories.addAll((List) builderFactoriesField.get(builder)); + Collections.reverse(newFactories); + newFactories.addAll((List) builderHierarchyFactoriesField.get(builder)); + + final List modifiedFactories = new ArrayList<>(existingFactories); + + // the excluder must precede all adapters that handle user-defined types + final int index = findExcluderIndex(modifiedFactories); + + for (final TypeAdapterFactory newFactory : Lists.reverse(newFactories)) { + modifiedFactories.add(index, newFactory); + } + + Class treeTypeAdapterClass; + try { + // newer gson releases + treeTypeAdapterClass = Class.forName("com.google.gson.internal.bind.TreeTypeAdapter"); + } catch (final ClassNotFoundException e) { + // old gson releases + treeTypeAdapterClass = Class.forName("com.google.gson.TreeTypeAdapter"); + } + + final Method newFactoryWithMatchRawTypeMethod = treeTypeAdapterClass.getMethod("newFactoryWithMatchRawType", TypeToken.class, Object.class); + final TypeAdapterFactory adapterComponentFactory = (TypeAdapterFactory) newFactoryWithMatchRawTypeMethod.invoke(null, TypeToken.get(AdapterComponent.class), new Serializer()); + modifiedFactories.add(index, adapterComponentFactory); + + factoriesField.set(gson, modifiedFactories); + return true; + } catch (final Throwable e) { + return false; + } + } + + private static Field field(final Class klass, final String name) throws NoSuchFieldException { + final Field field = klass.getDeclaredField(name); + field.setAccessible(true); + return field; + } + + private static int findExcluderIndex(final List factories) { + for (int i = 0, size = factories.size(); i < size; i++) { + final TypeAdapterFactory factory = factories.get(i); + if (factory instanceof Excluder) { + return i + 1; + } + } + return 0; + } + + @Override + public void sendMessage(final List viewers, final Component component) { + if (!BOUND) { + return; + } + send(viewers, component, (viewer, components) -> viewer.spigot().sendMessage(components)); + } + + @Override + public void sendActionBar(final List viewers, final Component component) { + if (!BOUND) { + return; + } + send(viewers, component, (viewer, components) -> viewer.spigot().sendMessage(ChatMessageType.ACTION_BAR, components)); + } + + private static void send(final List viewers, final Component component, final BiConsumer consumer) { + if (!BOUND) { + return; + } + final BaseComponent[] components = {new AdapterComponent(component)}; + for (final Iterator it = viewers.iterator(); it.hasNext(); ) { + final CommandSender viewer = it.next(); + if (viewer instanceof Player) { + try { + consumer.accept((Player) viewer, components); + it.remove(); + } catch (final Throwable e) { + e.printStackTrace(); + } + } + } + } + + static BaseComponent[] toBungeeCord(final Component component) { + if (BOUND) { + return new BaseComponent[]{new AdapterComponent(component)}; + } else { + return ComponentSerializer.parse(GsonComponentSerializer.INSTANCE.serialize(component)); + } + } + + public static final class AdapterComponent extends BaseComponent { + private final Component component; + + @SuppressWarnings("deprecation") + // weeeee + AdapterComponent(final Component component) { + this.component = component; + } + + @Override + public BaseComponent duplicate() { + return this; + } + + @Override + public String toLegacyText() { + return LegacyComponentSerializer.legacy().serialize(this.component); + } + } + + public static class Serializer implements JsonSerializer { + @Override + public JsonElement serialize(final AdapterComponent src, final Type typeOfSrc, final JsonSerializationContext context) { + return context.serialize(src.component); + } + } +}