Make MinecraftComponent immutable

This commit is contained in:
Vankka 2024-07-26 23:38:56 +03:00
parent 295bc1c6bf
commit 6f8b5bd023
No known key found for this signature in database
GPG Key ID: 62E48025ED4E7EBB
18 changed files with 232 additions and 281 deletions

View File

@ -5,6 +5,9 @@ dependencies {
// Annotations // Annotations
compileOnlyApi(libs.jetbrains.annotations) compileOnlyApi(libs.jetbrains.annotations)
// Unrelocate
compileOnly(project(':common:common-unrelocate'))
// JDA // JDA
api(libs.jda) { api(libs.jda) {
// We don't use audio // We don't use audio

View File

@ -43,7 +43,7 @@ import java.util.function.Predicate;
/** /**
* The DiscordSRV API. * The DiscordSRV API.
* * <p>
* Use your platform's service provider or {@link #get()} / {@link #optional()} to get the instance. * Use your platform's service provider or {@link #get()} / {@link #optional()} to get the instance.
*/ */
@SuppressWarnings("unused") // API @SuppressWarnings("unused") // API
@ -51,12 +51,17 @@ public interface DiscordSRVApi {
/** /**
* Gets the instance of {@link DiscordSRVApi}. * Gets the instance of {@link DiscordSRVApi}.
* @return the DiscordSRV api, or {@code null} if not available * @return the DiscordSRV api
* @see #isAvailable() * @see #isAvailable()
* @throws IllegalStateException if DiscordSRV has not been initialized yet
*/ */
@Nullable @NotNull
static DiscordSRVApi get() { static DiscordSRVApi get() {
return InstanceHolder.API; DiscordSRVApi api = InstanceHolder.API;
if (api == null) {
throw new IllegalStateException("DiscordSRV has not been initialized yet");
}
return api;
} }
/** /**

View File

@ -24,6 +24,7 @@
package com.discordsrv.api.component; package com.discordsrv.api.component;
import com.discordsrv.api.DiscordSRVApi; import com.discordsrv.api.DiscordSRVApi;
import com.discordsrv.unrelocate.net.kyori.adventure.text.Component;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -31,7 +32,7 @@ import org.jetbrains.annotations.Nullable;
/** /**
* A Minecraft json text component. Use {@link DiscordSRVApi#componentFactory()} to get an instance.<br/> * A Minecraft json text component. Use {@link DiscordSRVApi#componentFactory()} to get an instance.<br/>
* <br/> * <br/>
* This is designed to work with Adventure, see {@link #adventureAdapter(Class, Class)} and {@link #adventureAdapter(MinecraftComponentAdapter)} * This is designed to work with Adventure, see {@link #asAdventure(Class, Class)} and {@link #asAdventure(MinecraftComponentAdapter)}
* but is compatible with anything able to handle Minecraft's json format. * but is compatible with anything able to handle Minecraft's json format.
* Legacy is <b>not supported</b>. * Legacy is <b>not supported</b>.
*/ */
@ -39,6 +40,34 @@ import org.jetbrains.annotations.Nullable;
@ApiStatus.NonExtendable @ApiStatus.NonExtendable
public interface MinecraftComponent { public interface MinecraftComponent {
/**
* Creates a {@link MinecraftComponent} from the provided JSON.
*
* @param json the json
* @return a new {@link MinecraftComponent}
*/
@NotNull
static MinecraftComponent fromJson(@NotNull String json) {
return DiscordSRVApi.get()
.componentFactory()
.fromJson(json);
}
/**
* Creates a {@link MinecraftComponent} from the given unrelocated Adventure Component.
*
* @param unrelocatedAdventureComponent the <b>unrelocated</b> adventure Component
* @return a new {@link MinecraftComponent}
*/
@NotNull
static MinecraftComponent fromAdventure(@NotNull Component unrelocatedAdventureComponent) {
MinecraftComponentAdapter<Component> adapter = MinecraftComponentAdapter.unrelocated();
if (adapter == null) {
throw new IllegalStateException("Unrelocated Adventure GSON serializer not available");
}
return adapter.toDiscordSRV(unrelocatedAdventureComponent);
}
/** /**
* Gets this component as json. * Gets this component as json.
* @return json of this component * @return json of this component
@ -46,13 +75,6 @@ public interface MinecraftComponent {
@NotNull @NotNull
String asJson(); String asJson();
/**
* Sets this component from json.
* @param json valid Minecraft message component json
* @throws IllegalArgumentException if the provided json is not valid
*/
void setJson(@NotNull String json);
/** /**
* Gets this message as a plain {@link String} losing any coloring, click and hover components it may have had. * Gets this message as a plain {@link String} losing any coloring, click and hover components it may have had.
* Use for reference only, <b>this is not a substitute for legacy</b>. * Use for reference only, <b>this is not a substitute for legacy</b>.
@ -62,72 +84,59 @@ public interface MinecraftComponent {
String asPlainString(); String asPlainString();
/** /**
* Creates an Adventure adapter for convenience. * Converts this {@link MinecraftComponent} to an Adventure Component of the provided GSON serializer.
* Prefer using {@link #asAdventure(MinecraftComponentAdapter)}.
* *
* @param gsonSerializerClass the gson serializer class * @param gsonSerializerClass the gson serializer class
* @return an adapter that will convert to/from relocated or unrelocated adventure classes to/from json * @return an adapter that will convert to/from relocated or unrelocated adventure classes to/from json
* @throws IllegalArgumentException if the provided class is not an Adventure GsonComponentSerializer * @throws IllegalArgumentException if the provided class is not an Adventure GsonComponentSerializer
* @see #adventureAdapter(Class, Class) * @see #asAdventure(Class, Class)
*/ */
@NotNull @NotNull
default Adapter<Object> adventureAdapter(@NotNull Class<?> gsonSerializerClass) { default Object asAdventure(@NotNull Class<?> gsonSerializerClass) {
return adventureAdapter(gsonSerializerClass, null); return asAdventure(gsonSerializerClass, null);
} }
/** /**
* Creates an Adventure adapter for convenience. * Converts this {@link MinecraftComponent} to an Adventure Component of the provided GSON serializer.
* Prefer using {@link #asAdventure(MinecraftComponentAdapter)}.
* *
* @param gsonSerializerClass the {@code GsonComponentSerializer} class * @param gsonSerializerClass the {@code GsonComponentSerializer} class
* @param componentClass the {@code Component} class that's returned by the given gson component serializer * @param componentClass the {@code Component} class that's returned by the given gson component serializer
* @return an adapter that will convert to/from relocated or unrelocated adventure classes to/from json * @return the component from the GSON serializers output
* @throws IllegalArgumentException if the provided class is not an Adventure {@code GsonComponentSerializer} * @throws IllegalArgumentException if the provided class is not an Adventure {@code GsonComponentSerializer}
* or if the provided {@code Component} class isn't the one returned by the serializer * or if the provided {@code Component} class isn't the one returned by the serializer
*/ */
@SuppressWarnings("unchecked")
@NotNull @NotNull
<T> Adapter<T> adventureAdapter(@NotNull Class<?> gsonSerializerClass, Class<T> componentClass); default <T> T asAdventure(@NotNull Class<?> gsonSerializerClass, Class<T> componentClass) {
return (T) MinecraftComponentAdapter.create(gsonSerializerClass).toAdventure(this);
}
/** /**
* Creates an Adventure adapter from a {@link MinecraftComponentAdapter} for convenience. * Creates an Adventure adapter from a {@link MinecraftComponentAdapter} for convenience.
* *
* @param adapter the pre-made {@link MinecraftComponentAdapter} * @param adapter the pre-made {@link MinecraftComponentAdapter}
* @return a {@link Adapter} for this component using the given {@link MinecraftComponentAdapter} * @return a new Adventure Component
*/ */
@NotNull @NotNull
<T> Adapter<T> adventureAdapter(@NotNull MinecraftComponentAdapter<T> adapter); default <T> T asAdventure(@NotNull MinecraftComponentAdapter<T> adapter) {
return adapter.toAdventure(this);
}
/** /**
* Creates an Adventure adapter for the unrelocated adventure. * Creates an Adventure adapter for the unrelocated adventure.
* *
* @return a {@link Adapter} for this component using the unrelocated adventure, {@code null} if not available * @return the <b>unrelocated</b> Adventure Component, {@code null} if not available
*/ */
@Nullable @Nullable
@ApiStatus.NonExtendable @ApiStatus.NonExtendable
default Adapter<Object> unrelocatedAdapter() { default Component asAdventure() {
MinecraftComponentAdapter<Object> adapter = MinecraftComponentAdapter.UNRELOCATED; MinecraftComponentAdapter<Component> adapter = MinecraftComponentAdapter.unrelocated();
if (adapter == null) { if (adapter == null) {
return null; return null;
} }
return adventureAdapter(adapter); return asAdventure(adapter);
}
/**
* An Adventure adapter, converts from/to given adventure components from/to json.
*/
interface Adapter<Component> {
/**
* Returns the Adventure Component returned by the gson serializer of this adapter.
* @return the {@code net.kyori.adventure.text.Component} (or relocated), cast this to your end class
*/
@NotNull
Component getComponent();
/**
* Sets the component to the component that can be serialized by the gson serializer for this class.
* @param adventureComponent the component
* @throws IllegalArgumentException if the provided component cannot be processed by the gson serializer of this adapter
*/
void setComponent(@NotNull Component adventureComponent);
} }
} }

View File

@ -23,97 +23,66 @@
package com.discordsrv.api.component; package com.discordsrv.api.component;
import java.lang.reflect.InvocationTargetException; import com.discordsrv.api.DiscordSRVApi;
import java.lang.reflect.Method; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/** /**
* A persistent Adventure adapter for {@link MinecraftComponent}s, this is more efficient than using {@link MinecraftComponent#adventureAdapter(Class)}. * A helper class to make {@link MinecraftComponent}s using Adventure.
* @see MinecraftComponent#adventureAdapter(MinecraftComponentAdapter) * @param <Component> the Adventure Component type, relocated or unrelocated
*/ */
public class MinecraftComponentAdapter<Component> { public interface MinecraftComponentAdapter<Component> {
public static final MinecraftComponentAdapter<Object> UNRELOCATED;
static {
MinecraftComponentAdapter<Object> unrelocated = null;
try {
unrelocated = MinecraftComponentAdapter.create(
Class.forName("net.ky".concat("ori.adventure.text.serializer.gson.GsonComponentSerializer"))
);
} catch (ClassNotFoundException ignored) {}
UNRELOCATED = unrelocated;
}
/** /**
* Creates a new {@link MinecraftComponentAdapter} that can be used with {@link MinecraftComponent}s. * Create a new {@link MinecraftComponentAdapter} for the given GSONComponentSerializer class.
* *
* @param gsonSerializerClass a GsonComponentSerializer class * @param gsonSerializerClass the serializer class
* @return a new {@link MinecraftComponentAdapter} with the provided GsonComponentSerializer * @return a new {@link MinecraftComponentAdapter}
* @throws IllegalArgumentException if the provided argument is not a GsonComponentSerialize class * @param <Component> the type of Adventure Component the serializer handles
*/ */
public static MinecraftComponentAdapter<Object> create(Class<?> gsonSerializerClass) { @NotNull
return new MinecraftComponentAdapter<>(gsonSerializerClass, null); static <Component> MinecraftComponentAdapter<Component> create(Class<?> gsonSerializerClass) {
return create(gsonSerializerClass, null);
} }
/** /**
* Creates a new {@link MinecraftComponentAdapter} that can be used with {@link MinecraftComponent}s. * Create a new {@link MinecraftComponentAdapter} for the given GSONComponentSerializer class.
* *
* @param gsonSerializerClass a GsonComponentSerializer class * @param gsonSerializerClass the serializer class
* @param componentClass the Component class returned by the GsonComponentSerializer * @param componentClass the {@code Component} class that's returned by the given gson component serializer
* @return a new {@link MinecraftComponentAdapter} with the provided GsonComponentSerializer * @return a new {@link MinecraftComponentAdapter}
* @throws IllegalArgumentException if the provided argument is not a GsonComponentSerialize class * @param <T> the type of Adventure Component the serializer returns
* @throws IllegalArgumentException if the provided componentClass does not match the gsonSerializerClasses Component
*/ */
public static <Component> MinecraftComponentAdapter<Component> create(Class<?> gsonSerializerClass, Class<Component> componentClass) { @NotNull
return new MinecraftComponentAdapter<>(gsonSerializerClass, componentClass); static <T> MinecraftComponentAdapter<T> create(Class<?> gsonSerializerClass, Class<T> componentClass) {
return DiscordSRVApi.get()
.componentFactory()
.makeAdapter(gsonSerializerClass, componentClass);
} }
private final Class<?> gsonSerializerClass; /**
private final Object instance; * Create a new {@link MinecraftComponentAdapter} for unrelocated Adventure.
private final Method deserialize; *
private final Method serialize; * @return the shared instance of {@link MinecraftComponentAdapter} unrelocated Adventure, or {@code null}
*/
private MinecraftComponentAdapter(Class<?> gsonSerializerClass, Class<Component> providedComponentClass) { @SuppressWarnings("unchecked")
try { @Nullable
this.gsonSerializerClass = gsonSerializerClass; static MinecraftComponentAdapter<com.discordsrv.unrelocate.net.kyori.adventure.text.Component> unrelocated() {
this.instance = gsonSerializerClass.getDeclaredMethod("gson").invoke(null); return (MinecraftComponentAdapter<com.discordsrv.unrelocate.net.kyori.adventure.text.Component>) (Object) MinecraftComponentAdapterUnrelocated.INSTANCE;
this.deserialize = gsonSerializerClass.getMethod("deserialize", Object.class);
Class<?> componentClass = deserialize.getReturnType();
checkComponentClass(providedComponentClass, componentClass);
this.serialize = gsonSerializerClass.getMethod("serialize", componentClass);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
throw new IllegalArgumentException("The provided class is not a GsonComponentSerializer", e);
}
} }
private static void checkComponentClass(Class<?> provided, Class<?> actual) { /**
if (provided == null) { * Converts a {@link MinecraftComponent} to an Adventure Component.
// Ignore null * @param component the {@link MinecraftComponent}
return; * @return a new Adventure Component
} */
Component toAdventure(MinecraftComponent component);
String providedName = provided.getName(); /**
String actualName = actual.getName(); * Converts an Adventure Component into a {@link MinecraftComponent}.
if (!providedName.equals(actualName)) { * @param component the Adventure Component
throw new IllegalArgumentException( * @return a new {@link MinecraftComponent}
"The provided Component class (" + providedName */
+ ") does not match the one returned by the serializer: " + actualName MinecraftComponent toDiscordSRV(Component component);
);
}
}
public Class<?> serializerClass() {
return gsonSerializerClass;
}
public Object serializerInstance() {
return instance;
}
public Method deserializeMethod() {
return deserialize;
}
public Method serializeMethod() {
return serialize;
}
} }

View File

@ -25,6 +25,7 @@ package com.discordsrv.api.component;
import com.discordsrv.api.DiscordSRVApi; import com.discordsrv.api.DiscordSRVApi;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/** /**
* A factory for creating {@link MinecraftComponent}s. * A factory for creating {@link MinecraftComponent}s.
@ -33,12 +34,21 @@ import org.jetbrains.annotations.NotNull;
public interface MinecraftComponentFactory { public interface MinecraftComponentFactory {
/** /**
* Creates an empty {@link MinecraftComponent}. * Creates an {@link MinecraftComponent} from JSON.
* *
* @param json the Minecraft component json
* @return a new {@link MinecraftComponent} * @return a new {@link MinecraftComponent}
*/ */
@NotNull @NotNull
MinecraftComponent empty(); MinecraftComponent fromJson(@NotNull String json);
/**
* Create an {@link MinecraftComponentAdapter} for the given GSONComponentSerializer class.
* @param gsonSerializerClass the serializer class
* @return a new {@link MinecraftComponentAdapter}
*/
@NotNull
<T> MinecraftComponentAdapter<T> makeAdapter(Class<?> gsonSerializerClass, @Nullable Class<T> componentClass);
/** /**
* Creates a EnhancedLegacyText {@link GameTextBuilder} based on the given input. * Creates a EnhancedLegacyText {@link GameTextBuilder} based on the given input.

View File

@ -355,9 +355,6 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
@Override @Override
public @NotNull Formatter applyPlaceholderService() { public @NotNull Formatter applyPlaceholderService() {
DiscordSRVApi api = DiscordSRVApi.get(); DiscordSRVApi api = DiscordSRVApi.get();
if (api == null) {
throw new IllegalStateException("DiscordSRVApi not available");
}
this.replacements.put( this.replacements.put(
PlaceholderService.PATTERN, PlaceholderService.PATTERN,
wrapFunction(matcher -> api.placeholderService().getResultAsCharSequence(matcher, context)) wrapFunction(matcher -> api.placeholderService().getResultAsCharSequence(matcher, context))
@ -384,9 +381,6 @@ public class SendableDiscordMessageImpl implements SendableDiscordMessage {
@Override @Override
public @NotNull SendableDiscordMessage build() { public @NotNull SendableDiscordMessage build() {
DiscordSRVApi api = DiscordSRVApi.get(); DiscordSRVApi api = DiscordSRVApi.get();
if (api == null) {
throw new IllegalStateException("DiscordSRVApi not available");
}
Function<String, String> placeholders = input -> { Function<String, String> placeholders = input -> {
if (input == null) { if (input == null) {

View File

@ -67,9 +67,6 @@ public interface Module {
@NotNull @NotNull
default Collection<DiscordGatewayIntent> requiredIntents() { default Collection<DiscordGatewayIntent> requiredIntents() {
DiscordSRVApi api = DiscordSRVApi.get(); DiscordSRVApi api = DiscordSRVApi.get();
if (api == null) {
return Collections.emptyList();
}
Collection<? extends EventListener> listeners = api.eventBus().getListeners(this); Collection<? extends EventListener> listeners = api.eventBus().getListeners(this);
EnumSet<DiscordGatewayIntent> intents = EnumSet.noneOf(DiscordGatewayIntent.class); EnumSet<DiscordGatewayIntent> intents = EnumSet.noneOf(DiscordGatewayIntent.class);

View File

@ -66,7 +66,7 @@ public class PaperComponentHandle<T> {
unrelocated = handle.invoke(target); unrelocated = handle.invoke(target);
} catch (Throwable ignored) {} } catch (Throwable ignored) {}
if (unrelocated != null) { if (unrelocated != null) {
return ComponentUtil.fromUnrelocated((com.discordsrv.unrelocate.net.kyori.adventure.text.Component) unrelocated); return MinecraftComponent.fromAdventure((com.discordsrv.unrelocate.net.kyori.adventure.text.Component) unrelocated);
} }
} }

View File

@ -19,7 +19,6 @@
package com.discordsrv.bukkit.console.executor; package com.discordsrv.bukkit.console.executor;
import com.discordsrv.api.component.MinecraftComponent; import com.discordsrv.api.component.MinecraftComponent;
import com.discordsrv.common.component.util.ComponentUtil;
import com.discordsrv.unrelocate.net.kyori.adventure.text.Component; import com.discordsrv.unrelocate.net.kyori.adventure.text.Component;
import org.bukkit.Server; import org.bukkit.Server;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
@ -43,7 +42,6 @@ public class PaperCommandFeedbackExecutor implements Consumer<Component> {
@Override @Override
public void accept(Component component) { public void accept(Component component) {
MinecraftComponent minecraftComponent = ComponentUtil.fromUnrelocated(component); componentConsumer.accept(MinecraftComponent.fromAdventure(component));
componentConsumer.accept(minecraftComponent);
} }
} }

View File

@ -79,7 +79,7 @@ public final class PaperPlayer {
public static void kick(Player player, Component reason) { public static void kick(Player player, Component reason) {
try { try {
KICK_COMPONENT_HANDLE.invokeExact(player, ComponentUtil.toUnrelocated(ComponentUtil.toAPI(reason))); KICK_COMPONENT_HANDLE.invokeExact(player, ComponentUtil.toAPI(reason).asAdventure());
} catch (Throwable e) { } catch (Throwable e) {
throw new RuntimeException("Failed to kick player", e); throw new RuntimeException("Failed to kick player", e);
} }

View File

@ -124,7 +124,7 @@ public class BukkitPlayer extends BukkitCommandSender implements IPlayer {
return; return;
} }
try { try {
PAPER_SEND_MESSAGE_HANDLE.invoke(player, ComponentUtil.toUnrelocated(component)); PAPER_SEND_MESSAGE_HANDLE.invoke(player, component.asAdventure());
} catch (Throwable ignored) { } catch (Throwable ignored) {
super.sendMessage(ComponentUtil.fromAPI(component)); super.sendMessage(ComponentUtil.fromAPI(component));
} }

View File

@ -20,6 +20,7 @@ package com.discordsrv.common.component;
import com.discordsrv.api.component.GameTextBuilder; import com.discordsrv.api.component.GameTextBuilder;
import com.discordsrv.api.component.MinecraftComponent; import com.discordsrv.api.component.MinecraftComponent;
import com.discordsrv.api.component.MinecraftComponentAdapter;
import com.discordsrv.api.component.MinecraftComponentFactory; import com.discordsrv.api.component.MinecraftComponentFactory;
import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.component.renderer.DiscordSRVMinecraftRenderer; import com.discordsrv.common.component.renderer.DiscordSRVMinecraftRenderer;
@ -40,6 +41,7 @@ import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import net.kyori.ansi.ColorLevel; import net.kyori.ansi.ColorLevel;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@ -129,8 +131,13 @@ public class ComponentFactory implements MinecraftComponentFactory {
} }
@Override @Override
public @NotNull MinecraftComponent empty() { public @NotNull MinecraftComponent fromJson(@NotNull String json) {
return MinecraftComponentImpl.empty(); return new MinecraftComponentImpl(json);
}
@Override
public <T> @NotNull MinecraftComponentAdapter<T> makeAdapter(Class<?> gsonSerializerClass, @Nullable Class<T> componentClass) {
return new MinecraftComponentAdapterImpl<>(gsonSerializerClass, componentClass);
} }
@Override @Override

View File

@ -0,0 +1,85 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2024 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV 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.discordsrv.common.component;
import com.discordsrv.api.component.MinecraftComponent;
import com.discordsrv.api.component.MinecraftComponentAdapter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class MinecraftComponentAdapterImpl<T> implements MinecraftComponentAdapter<T> {
private final Class<?> gsonSerializerClass;
private final Object instance;
private final Method deserialize;
private final Method serialize;
public MinecraftComponentAdapterImpl(Class<?> gsonSerializerClass, Class<T> providedComponentClass) {
try {
this.gsonSerializerClass = gsonSerializerClass;
this.instance = gsonSerializerClass.getDeclaredMethod("gson").invoke(null);
this.deserialize = gsonSerializerClass.getMethod("deserialize", Object.class);
Class<?> componentClass = deserialize.getReturnType();
checkComponentClass(providedComponentClass, componentClass);
this.serialize = gsonSerializerClass.getMethod("serialize", componentClass);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
throw new IllegalArgumentException("The provided class is not a GsonComponentSerializer", e);
}
}
private static void checkComponentClass(Class<?> provided, Class<?> actual) {
if (provided == null) {
// Ignore null
return;
}
String providedName = provided.getName();
String actualName = actual.getName();
if (!providedName.equals(actualName)) {
throw new IllegalArgumentException(
"The provided Component class (" + providedName
+ ") does not match the one returned by the serializer: " + actualName
);
}
}
@SuppressWarnings("unchecked")
private <R> R execute(Method method, Object input) {
try {
return (R) method.invoke(instance, input);
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException("Failed to invoke " + gsonSerializerClass.getName() + "." + method.getName(), e.getCause());
}
}
@Override
public T toAdventure(MinecraftComponent component) {
String json = component.asJson();
return execute(deserialize, json);
}
@Override
public MinecraftComponent toDiscordSRV(T o) {
String json = execute(serialize, o);
return MinecraftComponent.fromJson(json);
}
}

View File

@ -19,116 +19,38 @@
package com.discordsrv.common.component; package com.discordsrv.common.component;
import com.discordsrv.api.component.MinecraftComponent; import com.discordsrv.api.component.MinecraftComponent;
import com.discordsrv.api.component.MinecraftComponentAdapter;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.lang.reflect.InvocationTargetException;
public class MinecraftComponentImpl implements MinecraftComponent { public class MinecraftComponentImpl implements MinecraftComponent {
private String json; private final String json;
private Component component; private final Component component;
public static MinecraftComponentImpl empty() {
return new MinecraftComponentImpl("{text:\"\"}");
}
public MinecraftComponentImpl(String json) { public MinecraftComponentImpl(String json) {
setJson(json); this(GsonComponentSerializer.gson().deserialize(json));
} }
public MinecraftComponentImpl(@NotNull Component component) { public MinecraftComponentImpl(@NotNull Component component) {
setComponent(component); this.component = component;
this.json = GsonComponentSerializer.gson().serialize(component);
} }
public Component getComponent() { public Component getComponent() {
return component; return component;
} }
public void setComponent(@NotNull Component component) {
this.component = component;
this.json = GsonComponentSerializer.gson().serialize(component);
}
@Override @Override
public @NotNull String asJson() { public @NotNull String asJson() {
return json; return json;
} }
@Override
public void setJson(@NotNull String json) throws IllegalArgumentException {
Component component;
try {
component = GsonComponentSerializer.gson().deserialize(json);
} catch (Throwable t) {
throw new IllegalArgumentException("Provided json is not valid: " + json, t);
}
this.component = component;
this.json = json;
}
@Override @Override
public @NotNull String asPlainString() { public @NotNull String asPlainString() {
return PlainTextComponentSerializer.plainText().serialize(component); return PlainTextComponentSerializer.plainText().serialize(component);
} }
@Override
public <T> MinecraftComponent.@NotNull Adapter<T> adventureAdapter(
@NotNull Class<?> gsonSerializerClass, @NotNull Class<T> componentClass
) {
return new Adapter<>(gsonSerializerClass, componentClass);
}
@Override
public <T> MinecraftComponent.@NotNull Adapter<T> adventureAdapter(@NotNull MinecraftComponentAdapter<T> adapter) {
return new Adapter<>(adapter);
}
@SuppressWarnings("unchecked")
public class Adapter<T> implements MinecraftComponent.Adapter<T> {
private final MinecraftComponentAdapter<T> adapter;
private Adapter(Class<?> gsonSerializerClass, Class<T> componentClass) {
this(MinecraftComponentAdapter.create(gsonSerializerClass, componentClass));
}
private Adapter(MinecraftComponentAdapter<T> adapter) {
this.adapter = adapter;
}
@Override
public @NotNull T getComponent() {
try {
return (T) adapter.deserializeMethod()
.invoke(
adapter.serializerInstance(),
json
);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException("Failed to convert to adventure component", e);
}
}
@Override
public void setComponent(@NotNull Object adventureComponent) {
try {
setJson(
(String) adapter.serializeMethod()
.invoke(
adapter.serializerInstance(),
adventureComponent
)
);
} catch (InvocationTargetException e) {
throw new IllegalArgumentException("The provided class is not a Component for the GsonComponentSerializer " + adapter.serializerClass().getName(), e);
} catch (IllegalAccessException e) {
throw new RuntimeException("Failed to convert from adventure component", e);
}
}
}
} }

View File

@ -22,30 +22,26 @@ import com.discordsrv.api.component.MinecraftComponent;
import com.discordsrv.api.component.MinecraftComponentAdapter; import com.discordsrv.api.component.MinecraftComponentAdapter;
import com.discordsrv.common.component.MinecraftComponentImpl; import com.discordsrv.common.component.MinecraftComponentImpl;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.Collection;
/** /**
* An util class for {@link Component}s and {@link MinecraftComponent}s. * A util class for {@link Component}s and {@link MinecraftComponent}s.
*/ */
public final class ComponentUtil { public final class ComponentUtil {
private ComponentUtil() {}
private static MinecraftComponentAdapter<Component> ADAPTER; private static MinecraftComponentAdapter<Component> ADAPTER;
@NotNull @NotNull
private static MinecraftComponentAdapter<Component> getAdapter() { private static MinecraftComponentAdapter<Component> adapter() {
return ADAPTER != null ? ADAPTER : (ADAPTER = MinecraftComponentAdapter.create(GsonComponentSerializer.class, Component.class)); return ADAPTER != null ? ADAPTER : (ADAPTER = MinecraftComponentAdapter.create(GsonComponentSerializer.class, Component.class));
} }
private ComponentUtil() {}
public static boolean isEmpty(@NotNull Component component) { public static boolean isEmpty(@NotNull Component component) {
return PlainTextComponentSerializer.plainText().serialize(component).isEmpty(); return PlainTextComponentSerializer.plainText().serialize(component).isEmpty();
} }
@ -78,48 +74,7 @@ public final class ComponentUtil {
if (component instanceof MinecraftComponentImpl) { if (component instanceof MinecraftComponentImpl) {
return ((MinecraftComponentImpl) component).getComponent(); return ((MinecraftComponentImpl) component).getComponent();
} else { } else {
return component.adventureAdapter(getAdapter()).getComponent(); return component.asAdventure(adapter());
} }
} }
public static void set(MinecraftComponent minecraftComponent, Component component) {
if (component instanceof MinecraftComponentImpl) {
((MinecraftComponentImpl) component).setComponent(component);
} else {
minecraftComponent.adventureAdapter(getAdapter()).setComponent(component);
}
}
public static MinecraftComponent fromUnrelocated(@NotNull com.discordsrv.unrelocate.net.kyori.adventure.text.Component unrelocatedAdventure) {
MinecraftComponentImpl component = MinecraftComponentImpl.empty();
MinecraftComponent.Adapter<Object> adapter = component.unrelocatedAdapter();
if (adapter == null) {
throw new IllegalStateException("Could not get unrelocated adventure gson serializer");
}
adapter.setComponent(unrelocatedAdventure);
return component;
}
public static com.discordsrv.unrelocate.net.kyori.adventure.text.Component toUnrelocated(MinecraftComponent component) {
MinecraftComponent.Adapter<Object> adapter = component.unrelocatedAdapter();
if (adapter == null) {
throw new IllegalStateException("Could not get unrelocated adventure gson serializer");
}
return (com.discordsrv.unrelocate.net.kyori.adventure.text.Component) adapter.getComponent();
}
public static Component join(@NotNull Component delimiter, @NotNull Collection<? extends ComponentLike> components) {
return join(delimiter, components.toArray(new ComponentLike[0]));
}
public static Component join(@NotNull Component delimiter, @NotNull ComponentLike[] components) {
TextComponent.Builder builder = Component.text();
for (int i = 0; i < components.length; i++) {
builder.append(components[i]);
if (i < components.length - 1) {
builder.append(delimiter);
}
}
return builder.build();
}
} }

View File

@ -37,11 +37,7 @@ public class MinecraftMessage {
} }
public GameTextBuilder textBuilder() { public GameTextBuilder textBuilder() {
DiscordSRVApi discordSRV = DiscordSRVApi.get(); return DiscordSRVApi.get().componentFactory().textBuilder(rawFormat);
if (discordSRV == null) {
throw new IllegalStateException("DiscordSRVApi == null");
}
return discordSRV.componentFactory().textBuilder(rawFormat);
} }
public MinecraftComponent make() { public MinecraftComponent make() {

View File

@ -27,10 +27,10 @@ import com.discordsrv.api.placeholder.annotation.Placeholder;
import com.discordsrv.api.placeholder.annotation.PlaceholderPrefix; import com.discordsrv.api.placeholder.annotation.PlaceholderPrefix;
import com.discordsrv.api.placeholder.annotation.PlaceholderRemainder; import com.discordsrv.api.placeholder.annotation.PlaceholderRemainder;
import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.component.util.ComponentUtil;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.entities.Role;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.JoinConfiguration;
import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextColor;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -159,7 +159,7 @@ public class DiscordGuildMemberImpl implements DiscordGuildMember {
components.add(Component.text(role.getName()).color(TextColor.color(role.getColor().rgb()))); components.add(Component.text(role.getName()).color(TextColor.color(role.getColor().rgb())));
} }
return ComponentUtil.join(Component.text(suffix), components); return Component.join(JoinConfiguration.separator(Component.text(suffix)), components);
} }
@Override @Override

View File

@ -42,6 +42,7 @@ import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.WebhookClient; import net.dv8tion.jda.api.entities.WebhookClient;
import net.dv8tion.jda.api.requests.ErrorResponse; import net.dv8tion.jda.api.requests.ErrorResponse;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.JoinConfiguration;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable; import org.jetbrains.annotations.Unmodifiable;
@ -307,7 +308,7 @@ public class ReceivedDiscordMessageImpl implements ReceivedDiscordMessage {
)); ));
} }
return ComponentUtil.join(Component.text(suffix), components); return Component.join(JoinConfiguration.separator(Component.text(suffix)), components);
} }
@Override @Override