Minecraft 25w20a protocol support

This commit is contained in:
md_5
2025-05-17 16:01:00 +10:00
parent a336efb8fa
commit 69e4872f40
67 changed files with 1530 additions and 41 deletions

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<project-shared-configuration>
<!--
This file contains additional configuration written by modules in the NetBeans IDE.
The configuration is intended to be shared among all the users of project and
therefore it is assumed to be part of version control checkout.
Without this configuration present, some functionality in the IDE may be limited or fail altogether.
-->
<properties xmlns="http://www.netbeans.org/ns/maven-properties-data/1">
<!--
Properties that influence various parts of the IDE, especially code formatting and the like.
You can copy and paste the single properties, into the pom.xml file and the IDE will pick them up.
That way multiple projects can share the same settings (useful for formatting rules for example).
Any value defined here will override the pom.xml file value but is only applicable to the current project.
-->
<org-netbeans-modules-editor-indent.CodeStyle.usedProfile>project</org-netbeans-modules-editor-indent.CodeStyle.usedProfile>
<org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.classDeclBracePlacement>NEW_LINE</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.classDeclBracePlacement>
<org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.otherBracePlacement>NEW_LINE</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.otherBracePlacement>
<org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.methodDeclBracePlacement>NEW_LINE</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.methodDeclBracePlacement>
<org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.spaceWithinMethodCallParens>true</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.spaceWithinMethodCallParens>
<org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.spaceWithinSwitchParens>true</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.spaceWithinSwitchParens>
<org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.spaceWithinCatchParens>true</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.spaceWithinCatchParens>
<org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.spaceWithinTryParens>true</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.spaceWithinTryParens>
<org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.spaceWithinSynchronizedParens>true</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.spaceWithinSynchronizedParens>
<org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.spaceWithinArrayInitBrackets>true</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.spaceWithinArrayInitBrackets>
<org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.spaceWithinParens>true</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.spaceWithinParens>
<org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.spaceWithinWhileParens>true</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.spaceWithinWhileParens>
<org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.spaceWithinIfParens>true</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.spaceWithinIfParens>
<org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.spaceWithinForParens>true</org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.spaceWithinForParens>
</properties>
</project-shared-configuration>

41
serializer/pom.xml Normal file
View File

@@ -0,0 +1,41 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-parent</artifactId>
<version>1.21-R0.3-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-serializer</artifactId>
<version>1.21-R0.3-SNAPSHOT</version>
<packaging>jar</packaging>
<name>BungeeCord-Serializer</name>
<description>Minecraft JSON serializer intended for use with BungeeCord</description>
<dependencies>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.11.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>bungeecord-chat</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>bungeecord-dialog</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,78 @@
package net.md_5.bungee.api.chat.hover.content;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import java.lang.reflect.Type;
import java.util.UUID;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.chat.BaseComponentSerializer;
import net.md_5.bungee.chat.VersionedComponentSerializer;
public class EntitySerializer extends BaseComponentSerializer implements JsonSerializer<Entity>, JsonDeserializer<Entity>
{
public EntitySerializer(VersionedComponentSerializer serializer)
{
super( serializer );
}
@Override
public Entity deserialize(JsonElement element, Type type, JsonDeserializationContext context) throws JsonParseException
{
JsonObject value = element.getAsJsonObject();
boolean newEntity = value.has( "uuid" );
String idString;
JsonElement uuid = value.get( newEntity ? "uuid" : "id" );
if ( uuid.isJsonArray() )
{
idString = parseUUID( context.deserialize( uuid, int[].class ) ).toString();
} else
{
idString = uuid.getAsString();
}
return new Entity(
( value.has( newEntity ? "id" : "type" ) ) ? value.get( newEntity ? "id" : "type" ).getAsString() : null,
idString,
( value.has( "name" ) ) ? context.deserialize( value.get( "name" ), BaseComponent.class ) : null
);
}
@Override
public JsonElement serialize(Entity content, Type type, JsonSerializationContext context)
{
JsonObject object = new JsonObject();
switch ( serializer.getVersion() )
{
case V1_21_5:
object.addProperty( "id", ( content.getType() != null ) ? content.getType() : "minecraft:pig" );
object.addProperty( "uuid", content.getId() );
break;
case V1_16:
object.addProperty( "type", ( content.getType() != null ) ? content.getType() : "minecraft:pig" );
object.addProperty( "id", content.getId() );
break;
default:
throw new IllegalArgumentException( "Unknown version " + serializer.getVersion() );
}
if ( content.getName() != null )
{
object.add( "name", context.serialize( content.getName() ) );
}
return object;
}
private static UUID parseUUID(int[] array)
{
return new UUID( (long) array[0] << 32 | (long) array[1] & 0XFFFFFFFFL, (long) array[2] << 32 | (long) array[3] & 0XFFFFFFFFL );
}
}

View File

@@ -0,0 +1,71 @@
package net.md_5.bungee.api.chat.hover.content;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import java.lang.reflect.Type;
import net.md_5.bungee.api.chat.ItemTag;
public class ItemSerializer implements JsonSerializer<Item>, JsonDeserializer<Item>
{
@Override
public Item deserialize(JsonElement element, Type type, JsonDeserializationContext context) throws JsonParseException
{
JsonObject value = element.getAsJsonObject();
int count = -1;
if ( value.has( "Count" ) )
{
JsonPrimitive countObj = value.get( "Count" ).getAsJsonPrimitive();
if ( countObj.isNumber() )
{
count = countObj.getAsInt();
} else if ( countObj.isString() )
{
String cString = countObj.getAsString();
char last = cString.charAt( cString.length() - 1 );
// Check for all number suffixes
if ( last == 'b' || last == 's' || last == 'l' || last == 'f' || last == 'd' )
{
cString = cString.substring( 0, cString.length() - 1 );
}
try
{
count = Integer.parseInt( cString );
} catch ( NumberFormatException ex )
{
throw new JsonParseException( "Could not parse count: " + ex );
}
}
}
return new Item(
( value.has( "id" ) ) ? value.get( "id" ).getAsString() : null,
count,
( value.has( "tag" ) ) ? context.deserialize( value.get( "tag" ), ItemTag.class ) : null
);
}
@Override
public JsonElement serialize(Item content, Type type, JsonSerializationContext context)
{
JsonObject object = new JsonObject();
object.addProperty( "id", ( content.getId() == null ) ? "minecraft:air" : content.getId() );
if ( content.getCount() != -1 )
{
object.addProperty( "Count", content.getCount() );
}
if ( content.getTag() != null )
{
object.add( "tag", context.serialize( content.getTag() ) );
}
return object;
}
}

View File

@@ -0,0 +1,38 @@
package net.md_5.bungee.api.chat.hover.content;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import java.lang.reflect.Type;
import net.md_5.bungee.api.chat.BaseComponent;
public class TextSerializer implements JsonSerializer<Text>, JsonDeserializer<Text>
{
@Override
public Text deserialize(JsonElement element, Type type, JsonDeserializationContext context) throws JsonParseException
{
if ( element.isJsonArray() )
{
return new Text( context.<BaseComponent[]>deserialize( element, BaseComponent[].class ) );
} else if ( element.isJsonPrimitive() )
{
return new Text( element.getAsJsonPrimitive().getAsString() );
} else
{
return new Text( new BaseComponent[]
{
context.deserialize( element, BaseComponent.class )
} );
}
}
@Override
public JsonElement serialize(Text content, Type type, JsonSerializationContext context)
{
return context.serialize( content.getValue() );
}
}

View File

@@ -0,0 +1,276 @@
package net.md_5.bungee.chat;
import com.google.common.base.Preconditions;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSerializationContext;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Locale;
import lombok.RequiredArgsConstructor;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.ClickEventCustom;
import net.md_5.bungee.api.chat.ComponentStyle;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.hover.content.Content;
import net.md_5.bungee.api.dialog.chat.ShowDialogClickEvent;
@RequiredArgsConstructor
public class BaseComponentSerializer
{
protected final VersionedComponentSerializer serializer;
protected void deserialize(JsonObject object, BaseComponent component, JsonDeserializationContext context)
{
component.applyStyle( context.deserialize( object, ComponentStyle.class ) );
JsonElement insertion = object.get( "insertion" );
if ( insertion != null )
{
component.setInsertion( insertion.getAsString() );
}
//Events
JsonObject clickEvent;
boolean newClickEvent = ( clickEvent = object.getAsJsonObject( "click_event" ) ) != null;
if ( !newClickEvent )
{
clickEvent = object.getAsJsonObject( "clickEvent" );
}
if ( clickEvent != null )
{
ClickEvent.Action action = ClickEvent.Action.valueOf( clickEvent.get( "action" ).getAsString().toUpperCase( Locale.ROOT ) );
if ( newClickEvent )
{
switch ( action )
{
case OPEN_URL:
component.setClickEvent( new ClickEvent( action, clickEvent.get( "url" ).getAsString() ) );
break;
case RUN_COMMAND:
case SUGGEST_COMMAND:
component.setClickEvent( new ClickEvent( action, clickEvent.get( "command" ).getAsString() ) );
break;
case CHANGE_PAGE:
int page = clickEvent.get( "page" ).getAsInt();
Preconditions.checkArgument( page >= 0, "Page number has to be positive" );
component.setClickEvent( new ClickEvent( action, Integer.toString( page ) ) );
break;
case SHOW_DIALOG:
component.setClickEvent( context.deserialize( clickEvent.get( "dialog" ), ShowDialogClickEvent.class ) );
break;
case CUSTOM:
component.setClickEvent( new ClickEventCustom( clickEvent.get( "id" ).getAsString(), ( clickEvent.has( "payload" ) ) ? clickEvent.get( "payload" ).getAsString() : null ) );
break;
default:
component.setClickEvent( new ClickEvent( action, ( clickEvent.has( "value" ) ) ? clickEvent.get( "value" ).getAsString() : "" ) );
break;
}
} else
{
component.setClickEvent( new ClickEvent( action, ( clickEvent.has( "value" ) ) ? clickEvent.get( "value" ).getAsString() : "" ) );
}
}
JsonObject hoverEventJson;
boolean newHoverEvent = ( hoverEventJson = object.getAsJsonObject( "hover_event" ) ) != null;
if ( !newHoverEvent )
{
hoverEventJson = object.getAsJsonObject( "hoverEvent" );
}
if ( hoverEventJson != null )
{
HoverEvent hoverEvent = null;
HoverEvent.Action action = HoverEvent.Action.valueOf( hoverEventJson.get( "action" ).getAsString().toUpperCase( Locale.ROOT ) );
if ( newHoverEvent || hoverEventJson.has( "contents" ) )
{
// value is only used for text in >= 1.21.5 (its inlined now)
JsonElement contents = hoverEventJson.get( newHoverEvent ? "value" : "contents" );
if ( contents != null || ( newHoverEvent && ( action == HoverEvent.Action.SHOW_ITEM || action == HoverEvent.Action.SHOW_ENTITY ) ) )
{
if ( contents == null )
{
// this is the new inline for SHOW_ITEM and SHOW_ENTITY
contents = hoverEventJson;
}
Content[] list;
if ( contents.isJsonArray() )
{
list = context.deserialize( contents, HoverEvent.getClass( action, true ) );
} else
{
list = new Content[]
{
context.deserialize( contents, HoverEvent.getClass( action, false ) )
};
}
hoverEvent = new HoverEvent( action, new ArrayList<>( Arrays.asList( list ) ) );
}
} else
{
JsonElement value = hoverEventJson.get( "value" );
if ( value != null )
{
// Plugins previously had support to pass BaseComponent[] into any action.
// If the GSON is possible to be parsed as BaseComponent, attempt to parse as so.
BaseComponent[] components;
if ( value.isJsonArray() )
{
components = context.deserialize( value, BaseComponent[].class );
} else
{
components = new BaseComponent[]
{
context.deserialize( value, BaseComponent.class )
};
}
hoverEvent = new HoverEvent( action, components );
}
}
if ( hoverEvent != null )
{
component.setHoverEvent( hoverEvent );
}
}
JsonElement extra = object.get( "extra" );
if ( extra != null )
{
component.setExtra( Arrays.asList( context.deserialize( extra, BaseComponent[].class ) ) );
}
}
protected void serialize(JsonObject object, BaseComponent component, JsonSerializationContext context)
{
boolean first = false;
if ( VersionedComponentSerializer.serializedComponents.get() == null )
{
first = true;
VersionedComponentSerializer.serializedComponents.set( Collections.newSetFromMap( new IdentityHashMap<BaseComponent, Boolean>() ) );
}
try
{
Preconditions.checkArgument( !VersionedComponentSerializer.serializedComponents.get().contains( component ), "Component loop" );
VersionedComponentSerializer.serializedComponents.get().add( component );
ComponentStyleSerializer.serializeTo( component.getStyle(), object );
if ( component.getInsertion() != null )
{
object.addProperty( "insertion", component.getInsertion() );
}
//Events
if ( component.getClickEvent() != null )
{
JsonObject clickEvent = new JsonObject();
String actionName = component.getClickEvent().getAction().toString().toLowerCase( Locale.ROOT );
clickEvent.addProperty( "action", actionName.toLowerCase( Locale.ROOT ) );
switch ( serializer.getVersion() )
{
case V1_21_5:
ClickEvent.Action action = ClickEvent.Action.valueOf( actionName.toUpperCase( Locale.ROOT ) );
switch ( action )
{
case OPEN_URL:
clickEvent.addProperty( "url", component.getClickEvent().getValue() );
break;
case RUN_COMMAND:
case SUGGEST_COMMAND:
clickEvent.addProperty( "command", component.getClickEvent().getValue() );
break;
case CHANGE_PAGE:
clickEvent.addProperty( "page", Integer.parseInt( component.getClickEvent().getValue() ) );
break;
case SHOW_DIALOG:
clickEvent.add( "dialog", context.serialize( component.getClickEvent() ) );
break;
case CUSTOM:
ClickEventCustom custom = (ClickEventCustom) component.getClickEvent();
clickEvent.addProperty( "id", custom.getValue() );
if ( custom.getPayload() != null )
{
clickEvent.addProperty( "payload", custom.getPayload() );
}
break;
default:
clickEvent.addProperty( "value", component.getClickEvent().getValue() );
break;
}
object.add( "click_event", clickEvent );
break;
case V1_16:
clickEvent.addProperty( "value", component.getClickEvent().getValue() );
object.add( "clickEvent", clickEvent );
break;
default:
throw new IllegalArgumentException( "Unknown version " + serializer.getVersion() );
}
}
if ( component.getHoverEvent() != null )
{
JsonObject hoverEvent = new JsonObject();
hoverEvent.addProperty( "action", component.getHoverEvent().getAction().toString().toLowerCase( Locale.ROOT ) );
if ( component.getHoverEvent().isLegacy() )
{
hoverEvent.add( "value", context.serialize( component.getHoverEvent().getContents().get( 0 ) ) );
} else
{
switch ( serializer.getVersion() )
{
case V1_21_5:
if ( component.getHoverEvent().getAction() == HoverEvent.Action.SHOW_ITEM || component.getHoverEvent().getAction() == HoverEvent.Action.SHOW_ENTITY )
{
JsonObject inlined = context.serialize( ( component.getHoverEvent().getContents().size() == 1 )
? component.getHoverEvent().getContents().get( 0 ) : component.getHoverEvent().getContents() ).getAsJsonObject();
inlined.entrySet().forEach( entry -> hoverEvent.add( entry.getKey(), entry.getValue() ) );
} else
{
hoverEvent.add( "value", context.serialize( ( component.getHoverEvent().getContents().size() == 1 )
? component.getHoverEvent().getContents().get( 0 ) : component.getHoverEvent().getContents() ) );
}
break;
case V1_16:
hoverEvent.add( "contents", context.serialize( ( component.getHoverEvent().getContents().size() == 1 )
? component.getHoverEvent().getContents().get( 0 ) : component.getHoverEvent().getContents() ) );
break;
default:
throw new IllegalArgumentException( "Unknown version " + serializer.getVersion() );
}
}
switch ( serializer.getVersion() )
{
case V1_21_5:
object.add( "hover_event", hoverEvent );
break;
case V1_16:
object.add( "hoverEvent", hoverEvent );
break;
default:
throw new IllegalArgumentException( "Unknown version " + serializer.getVersion() );
}
}
if ( component.getExtra() != null )
{
object.add( "extra", context.serialize( component.getExtra() ) );
}
} finally
{
VersionedComponentSerializer.serializedComponents.get().remove( component );
if ( first )
{
VersionedComponentSerializer.serializedComponents.set( null );
}
}
}
}

View File

@@ -0,0 +1,10 @@
package net.md_5.bungee.chat;
import org.jetbrains.annotations.ApiStatus;
@ApiStatus.Internal
public enum ChatVersion
{
V1_16,
V1_21_5;
}

View File

@@ -0,0 +1,141 @@
package net.md_5.bungee.chat;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import java.lang.reflect.Type;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.ComponentStyle;
import net.md_5.bungee.api.chat.hover.content.Content;
public class ComponentSerializer implements JsonDeserializer<BaseComponent>
{
/**
* Parse a JSON-compliant String as an array of base components. The input
* can be one of either an array of components, or a single component
* object. If the input is an array, each component will be parsed
* individually and returned in the order that they were parsed. If the
* input is a single component object, a single-valued array with the
* component will be returned.
* <p>
* <strong>NOTE:</strong> {@link #deserialize(String)} is preferred as it
* will parse only one component as opposed to an array of components which
* is non- standard behavior. This method is still appropriate for parsing
* multiple components at once, although such use case is rarely (if at all)
* exhibited in vanilla Minecraft.
*
* @param json the component json to parse
* @return an array of all parsed components
*/
public static BaseComponent[] parse(String json)
{
return VersionedComponentSerializer.getDefault().parse( json );
}
/**
* Deserialize a JSON-compliant String as a single component.
*
* @param json the component json to parse
* @return the deserialized component
* @throws IllegalArgumentException if anything other than a valid JSON
* component string is passed as input
*/
public static BaseComponent deserialize(String json)
{
return VersionedComponentSerializer.getDefault().deserialize( json );
}
/**
* Deserialize a JSON element as a single component.
*
* @param jsonElement the component json to parse
* @return the deserialized component
* @throws IllegalArgumentException if anything other than a valid JSON
* component is passed as input
*/
public static BaseComponent deserialize(JsonElement jsonElement)
{
return VersionedComponentSerializer.getDefault().deserialize( jsonElement );
}
/**
* Deserialize a JSON-compliant String as a component style.
*
* @param json the component style json to parse
* @return the deserialized component style
* @throws IllegalArgumentException if anything other than a valid JSON
* component style string is passed as input
*/
public static ComponentStyle deserializeStyle(String json)
{
return VersionedComponentSerializer.getDefault().deserializeStyle( json );
}
/**
* Deserialize a JSON element as a component style.
*
* @param jsonElement the component style json to parse
* @return the deserialized component style
* @throws IllegalArgumentException if anything other than a valid JSON
* component style is passed as input
*/
public static ComponentStyle deserializeStyle(JsonElement jsonElement)
{
return VersionedComponentSerializer.getDefault().deserializeStyle( jsonElement );
}
public static JsonElement toJson(BaseComponent component)
{
return VersionedComponentSerializer.getDefault().toJson( component );
}
public static JsonElement toJson(ComponentStyle style)
{
return VersionedComponentSerializer.getDefault().toJson( style );
}
/**
* @param object the object to serialize
* @return the JSON string representation of the object
* @deprecated Error-prone, be careful which object you input here
*/
@Deprecated
public static String toString(Object object)
{
return VersionedComponentSerializer.getDefault().toString( object );
}
/**
* @param content the content to serialize
* @return the JSON string representation of the object
* @deprecated for legacy internal use only
*/
@Deprecated
public static String toString(Content content)
{
return VersionedComponentSerializer.getDefault().toString( content );
}
public static String toString(BaseComponent component)
{
return VersionedComponentSerializer.getDefault().toString( component );
}
public static String toString(BaseComponent... components)
{
return VersionedComponentSerializer.getDefault().toString( components );
}
public static String toString(ComponentStyle style)
{
return VersionedComponentSerializer.getDefault().toString( style );
}
@Override
public BaseComponent deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException
{
return VersionedComponentSerializer.getDefault().deserialize( json, typeOfT, context );
}
}

View File

@@ -0,0 +1,137 @@
package net.md_5.bungee.chat;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import java.awt.Color;
import java.lang.reflect.Type;
import java.util.Map;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.ComponentStyle;
import net.md_5.bungee.api.chat.ComponentStyleBuilder;
public class ComponentStyleSerializer implements JsonSerializer<ComponentStyle>, JsonDeserializer<ComponentStyle>
{
private static boolean getAsBoolean(JsonElement el)
{
if ( el.isJsonPrimitive() )
{
JsonPrimitive primitive = (JsonPrimitive) el;
if ( primitive.isBoolean() )
{
return primitive.getAsBoolean();
}
if ( primitive.isNumber() )
{
Number number = primitive.getAsNumber();
if ( number instanceof Byte )
{
return number.byteValue() != 0;
}
}
}
return false;
}
static void serializeTo(ComponentStyle style, JsonObject object)
{
if ( style.isBoldRaw() != null )
{
object.addProperty( "bold", style.isBoldRaw() );
}
if ( style.isItalicRaw() != null )
{
object.addProperty( "italic", style.isItalicRaw() );
}
if ( style.isUnderlinedRaw() != null )
{
object.addProperty( "underlined", style.isUnderlinedRaw() );
}
if ( style.isStrikethroughRaw() != null )
{
object.addProperty( "strikethrough", style.isStrikethroughRaw() );
}
if ( style.isObfuscatedRaw() != null )
{
object.addProperty( "obfuscated", style.isObfuscatedRaw() );
}
if ( style.hasColor() && style.getColor().getColor() != null )
{
object.addProperty( "color", style.getColor().getName() );
}
if ( style.hasShadowColor() )
{
object.addProperty( "shadow_color", style.getShadowColor().getRGB() );
}
if ( style.hasFont() )
{
object.addProperty( "font", style.getFont() );
}
}
@Override
public ComponentStyle deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException
{
ComponentStyleBuilder builder = ComponentStyle.builder();
JsonObject object = json.getAsJsonObject();
for ( Map.Entry<String, JsonElement> entry : object.entrySet() )
{
String name = entry.getKey();
JsonElement value = entry.getValue();
switch ( name )
{
case "bold":
builder.bold( getAsBoolean( value ) );
break;
case "italic":
builder.italic( getAsBoolean( value ) );
break;
case "underlined":
builder.underlined( getAsBoolean( value ) );
break;
case "strikethrough":
builder.strikethrough( getAsBoolean( value ) );
break;
case "obfuscated":
builder.obfuscated( getAsBoolean( value ) );
break;
case "color":
builder.color( ChatColor.of( value.getAsString() ) );
break;
case "shadow_color":
if ( value.isJsonArray() )
{
JsonArray array = value.getAsJsonArray();
builder.shadowColor( new Color( array.get( 0 ).getAsFloat(), array.get( 1 ).getAsFloat(), array.get( 2 ).getAsFloat(), array.get( 3 ).getAsFloat() ) );
} else if ( value.isJsonPrimitive() )
{
builder.shadowColor( new Color( value.getAsNumber().intValue(), true ) );
}
break;
case "font":
builder.font( value.getAsString() );
break;
}
}
return builder.build();
}
@Override
public JsonElement serialize(ComponentStyle src, Type typeOfSrc, JsonSerializationContext context)
{
JsonObject object = new JsonObject();
serializeTo( src, object );
return object;
}
}

View File

@@ -0,0 +1,44 @@
package net.md_5.bungee.chat;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import java.lang.reflect.Type;
import net.md_5.bungee.api.chat.KeybindComponent;
public class KeybindComponentSerializer extends BaseComponentSerializer implements JsonSerializer<KeybindComponent>, JsonDeserializer<KeybindComponent>
{
public KeybindComponentSerializer(VersionedComponentSerializer serializer)
{
super( serializer );
}
@Override
public KeybindComponent deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException
{
JsonObject object = json.getAsJsonObject();
JsonElement keybind = object.get( "keybind" );
if ( keybind == null )
{
throw new JsonParseException( "Could not parse JSON: missing 'keybind' property" );
}
KeybindComponent component = new KeybindComponent();
deserialize( object, component, context );
component.setKeybind( keybind.getAsString() );
return component;
}
@Override
public JsonElement serialize(KeybindComponent src, Type typeOfSrc, JsonSerializationContext context)
{
JsonObject object = new JsonObject();
serialize( object, src, context );
object.addProperty( "keybind", src.getKeybind() );
return object;
}
}

View File

@@ -0,0 +1,66 @@
package net.md_5.bungee.chat;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import java.lang.reflect.Type;
import net.md_5.bungee.api.chat.ScoreComponent;
public class ScoreComponentSerializer extends BaseComponentSerializer implements JsonSerializer<ScoreComponent>, JsonDeserializer<ScoreComponent>
{
public ScoreComponentSerializer(VersionedComponentSerializer serializer)
{
super( serializer );
}
@Override
public ScoreComponent deserialize(JsonElement element, Type type, JsonDeserializationContext context) throws JsonParseException
{
JsonObject json = element.getAsJsonObject();
JsonObject score = json.getAsJsonObject( "score" );
if ( score == null )
{
throw new JsonParseException( "Could not parse JSON: missing 'score' property" );
}
JsonElement nameJson = score.get( "name" );
if ( nameJson == null )
{
throw new JsonParseException( "A score component needs at least a name (and an objective)" );
}
JsonElement objectiveJson = score.get( "objective" );
if ( objectiveJson == null )
{
throw new JsonParseException( "A score component needs at least a name and an objective" );
}
String name = nameJson.getAsString();
String objective = objectiveJson.getAsString();
ScoreComponent component = new ScoreComponent( name, objective );
JsonElement value = score.get( "value" );
if ( value != null && !value.getAsString().isEmpty() )
{
component.setValue( value.getAsString() );
}
deserialize( json, component, context );
return component;
}
@Override
public JsonElement serialize(ScoreComponent component, Type type, JsonSerializationContext context)
{
JsonObject root = new JsonObject();
serialize( root, component, context );
JsonObject json = new JsonObject();
json.addProperty( "name", component.getName() );
json.addProperty( "objective", component.getObjective() );
json.addProperty( "value", component.getValue() );
root.add( "score", json );
return root;
}
}

View File

@@ -0,0 +1,55 @@
package net.md_5.bungee.chat;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import java.lang.reflect.Type;
import net.md_5.bungee.api.chat.SelectorComponent;
public class SelectorComponentSerializer extends BaseComponentSerializer implements JsonSerializer<SelectorComponent>, JsonDeserializer<SelectorComponent>
{
public SelectorComponentSerializer(VersionedComponentSerializer serializer)
{
super( serializer );
}
@Override
public SelectorComponent deserialize(JsonElement element, Type type, JsonDeserializationContext context) throws JsonParseException
{
JsonObject object = element.getAsJsonObject();
JsonElement selector = object.get( "selector" );
if ( selector == null )
{
throw new JsonParseException( "Could not parse JSON: missing 'selector' property" );
}
SelectorComponent component = new SelectorComponent( selector.getAsString() );
JsonElement separator = object.get( "separator" );
if ( separator != null )
{
component.setSeparator( serializer.deserialize( separator.getAsString() ) );
}
deserialize( object, component, context );
return component;
}
@Override
public JsonElement serialize(SelectorComponent component, Type type, JsonSerializationContext context)
{
JsonObject object = new JsonObject();
serialize( object, component, context );
object.addProperty( "selector", component.getSelector() );
if ( component.getSeparator() != null )
{
object.addProperty( "separator", serializer.toString( component.getSeparator() ) );
}
return object;
}
}

View File

@@ -0,0 +1,43 @@
package net.md_5.bungee.chat;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import java.lang.reflect.Type;
import net.md_5.bungee.api.chat.TextComponent;
public class TextComponentSerializer extends BaseComponentSerializer implements JsonSerializer<TextComponent>, JsonDeserializer<TextComponent>
{
public TextComponentSerializer(VersionedComponentSerializer serializer)
{
super( serializer );
}
@Override
public TextComponent deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException
{
TextComponent component = new TextComponent();
JsonObject object = json.getAsJsonObject();
JsonElement text = object.get( "text" );
if ( text != null )
{
component.setText( text.getAsString() );
}
deserialize( object, component, context );
return component;
}
@Override
public JsonElement serialize(TextComponent src, Type typeOfSrc, JsonSerializationContext context)
{
JsonObject object = new JsonObject();
serialize( object, src, context );
object.addProperty( "text", src.getText() );
return object;
}
}

View File

@@ -0,0 +1,64 @@
package net.md_5.bungee.chat;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import java.lang.reflect.Type;
import java.util.Arrays;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TranslatableComponent;
public class TranslatableComponentSerializer extends BaseComponentSerializer implements JsonSerializer<TranslatableComponent>, JsonDeserializer<TranslatableComponent>
{
public TranslatableComponentSerializer(VersionedComponentSerializer serializer)
{
super( serializer );
}
@Override
public TranslatableComponent deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException
{
TranslatableComponent component = new TranslatableComponent();
JsonObject object = json.getAsJsonObject();
deserialize( object, component, context );
JsonElement translate = object.get( "translate" );
if ( translate == null )
{
throw new JsonParseException( "Could not parse JSON: missing 'translate' property" );
}
component.setTranslate( translate.getAsString() );
JsonElement with = object.get( "with" );
if ( with != null )
{
component.setWith( Arrays.asList( context.deserialize( with, BaseComponent[].class ) ) );
}
JsonElement fallback = object.get( "fallback" );
if ( fallback != null )
{
component.setFallback( fallback.getAsString() );
}
return component;
}
@Override
public JsonElement serialize(TranslatableComponent src, Type typeOfSrc, JsonSerializationContext context)
{
JsonObject object = new JsonObject();
serialize( object, src, context );
object.addProperty( "translate", src.getTranslate() );
if ( src.getWith() != null )
{
object.add( "with", context.serialize( src.getWith() ) );
}
if ( src.getFallback() != null )
{
object.addProperty( "fallback", src.getFallback() );
}
return object;
}
}

View File

@@ -0,0 +1,280 @@
package net.md_5.bungee.chat;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import java.lang.reflect.Type;
import java.util.Set;
import lombok.Getter;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.ComponentStyle;
import net.md_5.bungee.api.chat.ItemTag;
import net.md_5.bungee.api.chat.KeybindComponent;
import net.md_5.bungee.api.chat.ScoreComponent;
import net.md_5.bungee.api.chat.SelectorComponent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.chat.TranslatableComponent;
import net.md_5.bungee.api.chat.hover.content.Content;
import net.md_5.bungee.api.chat.hover.content.Entity;
import net.md_5.bungee.api.chat.hover.content.EntitySerializer;
import net.md_5.bungee.api.chat.hover.content.Item;
import net.md_5.bungee.api.chat.hover.content.ItemSerializer;
import net.md_5.bungee.api.chat.hover.content.Text;
import net.md_5.bungee.api.chat.hover.content.TextSerializer;
import net.md_5.bungee.api.dialog.Dialog;
import net.md_5.bungee.api.dialog.chat.ShowDialogClickEvent;
import net.md_5.bungee.dialog.DialogSerializer;
import net.md_5.bungee.dialog.ShowDialogClickEventSerializer;
import org.jetbrains.annotations.ApiStatus;
@ApiStatus.Experimental
public class VersionedComponentSerializer implements JsonDeserializer<BaseComponent>
{
@Getter
@ApiStatus.Internal
private final Gson gson;
@Getter
@ApiStatus.Internal
private final ChatVersion version;
@Getter
@ApiStatus.Internal
private final DialogSerializer dialogSerializer;
public VersionedComponentSerializer(ChatVersion version)
{
this.version = version;
this.dialogSerializer = new DialogSerializer( this );
this.gson = new GsonBuilder().
registerTypeAdapter( BaseComponent.class, this ).
registerTypeAdapter( TextComponent.class, new TextComponentSerializer( this ) ).
registerTypeAdapter( TranslatableComponent.class, new TranslatableComponentSerializer( this ) ).
registerTypeAdapter( KeybindComponent.class, new KeybindComponentSerializer( this ) ).
registerTypeAdapter( ScoreComponent.class, new ScoreComponentSerializer( this ) ).
registerTypeAdapter( SelectorComponent.class, new SelectorComponentSerializer( this ) ).
registerTypeAdapter( ComponentStyle.class, new ComponentStyleSerializer() ).
registerTypeAdapter( Entity.class, new EntitySerializer( this ) ).
registerTypeAdapter( Text.class, new TextSerializer() ).
registerTypeAdapter( Item.class, new ItemSerializer() ).
registerTypeAdapter( ItemTag.class, new ItemTag.Serializer() ).
// Dialogs
registerTypeAdapter( Dialog.class, dialogSerializer ).
registerTypeAdapter( ShowDialogClickEvent.class, new ShowDialogClickEventSerializer() ).
create();
}
private static final VersionedComponentSerializer v1_16 = new VersionedComponentSerializer( ChatVersion.V1_16 );
private static final VersionedComponentSerializer v1_21_5 = new VersionedComponentSerializer( ChatVersion.V1_21_5 );
public static VersionedComponentSerializer forVersion(ChatVersion version)
{
switch ( version )
{
case V1_16:
return v1_16;
case V1_21_5:
return v1_21_5;
default:
throw new IllegalArgumentException( "Unknown version " + version );
}
}
@Deprecated
@ApiStatus.Internal
public static VersionedComponentSerializer getDefault()
{
return v1_16;
}
@ApiStatus.Internal
public static final ThreadLocal<Set<BaseComponent>> serializedComponents = new ThreadLocal<Set<BaseComponent>>();
/**
* Parse a JSON-compliant String as an array of base components. The input
* can be one of either an array of components, or a single component
* object. If the input is an array, each component will be parsed
* individually and returned in the order that they were parsed. If the
* input is a single component object, a single-valued array with the
* component will be returned.
* <p>
* <strong>NOTE:</strong> {@link #deserialize(String)} is preferred as it
* will parse only one component as opposed to an array of components which
* is non- standard behavior. This method is still appropriate for parsing
* multiple components at once, although such use case is rarely (if at all)
* exhibited in vanilla Minecraft.
*
* @param json the component json to parse
* @return an array of all parsed components
*/
public BaseComponent[] parse(String json)
{
JsonElement jsonElement = JsonParser.parseString( json );
if ( jsonElement.isJsonArray() )
{
return gson.fromJson( jsonElement, BaseComponent[].class );
} else
{
return new BaseComponent[]
{
gson.fromJson( jsonElement, BaseComponent.class )
};
}
}
/**
* Deserialize a JSON-compliant String as a single component.
*
* @param json the component json to parse
* @return the deserialized component
* @throws IllegalArgumentException if anything other than a valid JSON
* component string is passed as input
*/
public BaseComponent deserialize(String json)
{
JsonElement jsonElement = JsonParser.parseString( json );
return deserialize( jsonElement );
}
/**
* Deserialize a JSON element as a single component.
*
* @param jsonElement the component json to parse
* @return the deserialized component
* @throws IllegalArgumentException if anything other than a valid JSON
* component is passed as input
*/
public BaseComponent deserialize(JsonElement jsonElement)
{
if ( jsonElement instanceof JsonPrimitive )
{
JsonPrimitive primitive = (JsonPrimitive) jsonElement;
if ( primitive.isString() )
{
return new TextComponent( primitive.getAsString() );
}
} else if ( jsonElement instanceof JsonArray )
{
BaseComponent[] array = gson.fromJson( jsonElement, BaseComponent[].class );
return TextComponent.fromArray( array );
}
return gson.fromJson( jsonElement, BaseComponent.class );
}
/**
* Deserialize a JSON-compliant String as a component style.
*
* @param json the component style json to parse
* @return the deserialized component style
* @throws IllegalArgumentException if anything other than a valid JSON
* component style string is passed as input
*/
public ComponentStyle deserializeStyle(String json)
{
JsonElement jsonElement = JsonParser.parseString( json );
return deserializeStyle( jsonElement );
}
/**
* Deserialize a JSON element as a component style.
*
* @param jsonElement the component style json to parse
* @return the deserialized component style
* @throws IllegalArgumentException if anything other than a valid JSON
* component style is passed as input
*/
public ComponentStyle deserializeStyle(JsonElement jsonElement)
{
return gson.fromJson( jsonElement, ComponentStyle.class );
}
public JsonElement toJson(BaseComponent component)
{
return gson.toJsonTree( component );
}
public JsonElement toJson(ComponentStyle style)
{
return gson.toJsonTree( style );
}
/**
* @param object the object to serialize
* @return the JSON string representation of the object
* @deprecated Error-prone, be careful which object you input here
*/
@Deprecated
public String toString(Object object)
{
return gson.toJson( object );
}
/**
* @param content the content to serialize
* @return the JSON string representation of the object
* @deprecated for legacy internal use only
*/
@Deprecated
public String toString(Content content)
{
return gson.toJson( content );
}
public String toString(BaseComponent component)
{
return gson.toJson( component );
}
public String toString(BaseComponent... components)
{
if ( components.length == 1 )
{
return gson.toJson( components[0] );
} else
{
return gson.toJson( new TextComponent( components ) );
}
}
public String toString(ComponentStyle style)
{
return gson.toJson( style );
}
@Override
public BaseComponent deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException
{
if ( json.isJsonPrimitive() )
{
return new TextComponent( json.getAsString() );
}
JsonObject object = json.getAsJsonObject();
if ( object.has( "translate" ) )
{
return context.deserialize( json, TranslatableComponent.class );
}
if ( object.has( "keybind" ) )
{
return context.deserialize( json, KeybindComponent.class );
}
if ( object.has( "score" ) )
{
return context.deserialize( json, ScoreComponent.class );
}
if ( object.has( "selector" ) )
{
return context.deserialize( json, SelectorComponent.class );
}
return context.deserialize( json, TextComponent.class );
}
}

View File

@@ -0,0 +1,117 @@
package net.md_5.bungee.dialog;
import com.google.common.base.Preconditions;
import com.google.common.collect.BiMap;
import com.google.common.collect.ImmutableBiMap;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import java.lang.reflect.Type;
import lombok.RequiredArgsConstructor;
import net.md_5.bungee.api.dialog.ConfirmationDialog;
import net.md_5.bungee.api.dialog.Dialog;
import net.md_5.bungee.api.dialog.DialogBase;
import net.md_5.bungee.api.dialog.DialogListDialog;
import net.md_5.bungee.api.dialog.MultiActionDialog;
import net.md_5.bungee.api.dialog.MultiActionInputFormDialog;
import net.md_5.bungee.api.dialog.NoticeDialog;
import net.md_5.bungee.api.dialog.ServerLinksDialog;
import net.md_5.bungee.api.dialog.SimpleInputFormDialog;
import net.md_5.bungee.chat.VersionedComponentSerializer;
@RequiredArgsConstructor
public class DialogSerializer implements JsonDeserializer<Dialog>, JsonSerializer<Dialog>
{
private static final BiMap<String, Class<? extends Dialog>> TYPES;
private final VersionedComponentSerializer serializer;
static
{
ImmutableBiMap.Builder<String, Class<? extends Dialog>> builder = ImmutableBiMap.builder();
builder.put( "minecraft:notice", NoticeDialog.class );
builder.put( "minecraft:confirmation", ConfirmationDialog.class );
builder.put( "minecraft:multi_action", MultiActionDialog.class );
builder.put( "minecraft:server_links", ServerLinksDialog.class );
builder.put( "minecraft:dialog_list", DialogListDialog.class );
builder.put( "minecraft:simple_input_form", SimpleInputFormDialog.class );
builder.put( "minecraft:multi_action_input_form", MultiActionInputFormDialog.class );
TYPES = builder.build();
}
public JsonElement toJson(Dialog dialog)
{
return serializer.getGson().toJsonTree( dialog, Dialog.class );
}
public String toString(Dialog dialog)
{
return serializer.getGson().toJson( dialog, Dialog.class );
}
public Dialog deserialize(JsonElement jsonElement)
{
return serializer.getGson().fromJson( jsonElement, Dialog.class );
}
public Dialog deserialize(String json)
{
JsonElement jsonElement = JsonParser.parseString( json );
return deserialize( jsonElement );
}
@Override
public Dialog deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException
{
JsonObject object = json.getAsJsonObject();
String type = object.get( "type" ).getAsString();
if ( object.has( "base" ) )
{
throw new JsonParseException( "Cannot explicitly specify base" );
}
Type realType = TYPES.get( type );
if ( realType == null )
{
throw new JsonParseException( "Unknown type " + type );
}
Dialog dialog = context.deserialize( json, realType );
DialogBase base = context.deserialize( json, DialogBase.class );
dialog.setBase( base );
return dialog;
}
@Override
public JsonElement serialize(Dialog src, Type typeOfSrc, JsonSerializationContext context)
{
if ( src == null )
{
return JsonNull.INSTANCE;
}
Class<? extends Dialog> realType = src.getClass();
String type = TYPES.inverse().get( realType );
Preconditions.checkArgument( type != null, "Unknown type %s", typeOfSrc );
JsonObject object = (JsonObject) context.serialize( src, realType );
object.addProperty( "type", type );
JsonObject base = (JsonObject) context.serialize( src.getBase() );
object.asMap().putAll( base.asMap() );
return object;
}
}

View File

@@ -0,0 +1,38 @@
package net.md_5.bungee.dialog;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import java.lang.reflect.Type;
import net.md_5.bungee.api.dialog.Dialog;
import net.md_5.bungee.api.dialog.chat.ShowDialogClickEvent;
public class ShowDialogClickEventSerializer implements JsonDeserializer<ShowDialogClickEvent>, JsonSerializer<ShowDialogClickEvent>
{
@Override
public ShowDialogClickEvent deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException
{
if ( json.isJsonPrimitive() && json.getAsJsonPrimitive().isString() )
{
return new ShowDialogClickEvent( json.getAsJsonPrimitive().getAsString() );
}
return new ShowDialogClickEvent( (Dialog) context.deserialize( json, Dialog.class ) );
}
@Override
public JsonElement serialize(ShowDialogClickEvent src, Type typeOfSrc, JsonSerializationContext context)
{
if ( src.getReference() != null )
{
return new JsonPrimitive( src.getReference() );
}
return context.serialize( src.getDialog(), Dialog.class );
}
}

View File

@@ -0,0 +1,871 @@
package net.md_5.bungee.api.chat;
import static net.md_5.bungee.api.ChatColor.*;
import static org.junit.jupiter.api.Assertions.*;
import java.awt.Color;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.ObjIntConsumer;
import java.util.function.Supplier;
import net.md_5.bungee.api.chat.hover.content.Entity;
import net.md_5.bungee.api.chat.hover.content.Text;
import net.md_5.bungee.chat.ComponentSerializer;
import org.junit.jupiter.api.Test;
public class ComponentsTest
{
public static void testDissembleReassemble(BaseComponent[] components)
{
String json = ComponentSerializer.toString( components );
BaseComponent[] parsed = ComponentSerializer.parse( json );
assertEquals( BaseComponent.toLegacyText( parsed ), BaseComponent.toLegacyText( components ) );
}
public static void testDissembleReassemble(BaseComponent component)
{
String json = ComponentSerializer.toString( component );
BaseComponent[] parsed = ComponentSerializer.parse( json );
assertEquals( BaseComponent.toLegacyText( parsed ), BaseComponent.toLegacyText( component ) );
}
public static void testAssembleDissemble(String json, boolean modern)
{
if ( modern )
{
BaseComponent deserialized = ComponentSerializer.deserialize( json );
assertEquals( json, ComponentSerializer.toString( deserialized ) );
} else
{
BaseComponent[] parsed = ComponentSerializer.parse( json );
assertEquals( json, ComponentSerializer.toString( parsed ) );
}
}
@Test
public void testItemParse()
{
// Declare all commonly used variables for reuse.
BaseComponent[] components;
TextComponent textComponent;
String json;
textComponent = new TextComponent( "Test" );
textComponent.setHoverEvent( new HoverEvent( HoverEvent.Action.SHOW_ITEM, new BaseComponent[]
{
new TextComponent( "{id:\"minecraft:netherrack\",Count:47b}" )
} ) );
testDissembleReassemble( new BaseComponent[]
{
textComponent
} );
testDissembleReassemble( textComponent );
json = "{\"hoverEvent\":{\"action\":\"show_item\",\"value\":[{\"text\":\"{id:\\\"minecraft:netherrack\\\",Count:47b}\"}]},\"text\":\"Test\"}";
testAssembleDissemble( json, false );
testAssembleDissemble( json, true );
//////////
String hoverVal = "{\"text\":\"{id:\\\"minecraft:dirt\\\",Count:1b}\"}";
json = "{\"extra\":[{\"text\":\"[\"},{\"extra\":[{\"translate\":\"block.minecraft.dirt\"}],\"text\":\"\"},{\"text\":\"]\"}],\"hoverEvent\":{\"action\":\"show_item\",\"value\":[" + hoverVal + "]},\"text\":\"\"}";
components = ComponentSerializer.parse( json );
Text contentText = ( (Text) components[0].getHoverEvent().getContents().get( 0 ) );
assertEquals( hoverVal, ComponentSerializer.toString( (BaseComponent[]) contentText.getValue() ) );
testDissembleReassemble( components );
//////////
// TODO: now ambiguous since "text" to distinguish Text from Item is not required
/*
TextComponent component1 = new TextComponent( "HoverableText" );
String nbt = "{display:{Name:{text:Hello},Lore:[{text:Line_1},{text:Line_2}]},ench:[{id:49,lvl:5}],Unbreakable:1}}";
Item contentItem = new Item( "minecraft:wood", 1, ItemTag.ofNbt( nbt ) );
HoverEvent hoverEvent = new HoverEvent( HoverEvent.Action.SHOW_ITEM, contentItem );
component1.setHoverEvent( hoverEvent );
json = ComponentSerializer.toString( component1 );
components = ComponentSerializer.parse( json );
Item parsedContentItem = ( (Item) components[0].getHoverEvent().getContents().get( 0 ) );
assertEquals( contentItem, parsedContentItem );
assertEquals( contentItem.getCount(), parsedContentItem.getCount() );
assertEquals( contentItem.getId(), parsedContentItem.getId() );
assertEquals( nbt, parsedContentItem.getTag().getNbt() );
*/
}
@Test
public void testArrayUUIDParse()
{
BaseComponent[] uuidComponent = ComponentSerializer.parse( "{\"translate\":\"multiplayer.player.joined\",\"with\":[{\"text\":\"Rexcantor64\",\"hoverEvent\":{\"contents\":{\"type\":\"minecraft:player\",\"id\":[1328556382,-2138814985,-1895806765,-1039963041],\"name\":\"Rexcantor64\"},\"action\":\"show_entity\"},\"insertion\":\"Rexcantor64\",\"clickEvent\":{\"action\":\"suggest_command\",\"value\":\"/tell Rexcantor64 \"}}],\"color\":\"yellow\"}" );
assertEquals( "4f30295e-8084-45f7-8f00-48d3c2036c5f", ( (Entity) ( (TranslatableComponent) uuidComponent[0] ).getWith().get( 0 ).getHoverEvent().getContents().get( 0 ) ).getId() );
testDissembleReassemble( uuidComponent );
}
@Test
public void testEmptyComponentBuilderCreate()
{
testEmptyComponentBuilder(
ComponentBuilder::create,
(components) -> assertEquals( components.length, 0 ),
(components, size) -> assertEquals( size, components.length )
);
}
@Test
public void testEmptyComponentBuilderBuild()
{
testEmptyComponentBuilder(
ComponentBuilder::build,
(component) -> assertNull( component.getExtra() ),
(component, size) -> assertEquals( component.getExtra().size(), size )
);
}
private static <T> void testEmptyComponentBuilder(Function<ComponentBuilder, T> componentBuilder, Consumer<T> emptyAssertion, ObjIntConsumer<T> sizedAssertion)
{
ComponentBuilder builder = new ComponentBuilder();
T component = componentBuilder.apply( builder );
emptyAssertion.accept( component );
for ( int i = 0; i < 3; i++ )
{
builder.append( "part:" + i );
component = componentBuilder.apply( builder );
sizedAssertion.accept( component, i + 1 );
}
}
@Test
public void testDummyRetaining()
{
ComponentBuilder builder = new ComponentBuilder();
assertNotNull( builder.getCurrentComponent() );
builder.color( GREEN );
builder.append( "test ", ComponentBuilder.FormatRetention.ALL );
assertEquals( builder.getCurrentComponent().getColor(), GREEN );
}
@Test
public void testComponentGettingExceptions()
{
ComponentBuilder builder = new ComponentBuilder();
assertThrows( IndexOutOfBoundsException.class, () -> builder.getComponent( -1 ) );
assertThrows( IndexOutOfBoundsException.class, () -> builder.getComponent( 0 ) );
assertThrows( IndexOutOfBoundsException.class, () -> builder.getComponent( 1 ) );
BaseComponent component = new TextComponent( "Hello" );
builder.append( component );
assertEquals( builder.getComponent( 0 ), component );
assertThrows( IndexOutOfBoundsException.class, () -> builder.getComponent( 1 ) );
}
@Test
public void testFormatNotColor()
{
BaseComponent[] component = new ComponentBuilder().color( BOLD ).append( "Test" ).create();
String json = ComponentSerializer.toString( component );
BaseComponent[] parsed = ComponentSerializer.parse( json );
assertNull( parsed[0].getColorRaw(), "Format should not be preserved as color" );
}
@Test
public void testComponentParting()
{
ComponentBuilder builder = new ComponentBuilder();
TextComponent apple = new TextComponent( "apple" );
builder.append( apple );
assertEquals( builder.getCurrentComponent(), apple );
assertEquals( builder.getComponent( 0 ), apple );
TextComponent mango = new TextComponent( "mango" );
TextComponent orange = new TextComponent( "orange" );
builder.append( mango );
builder.append( orange );
builder.removeComponent( 1 ); // Removing mango
assertEquals( builder.getComponent( 0 ), apple );
assertEquals( builder.getComponent( 1 ), orange );
}
@Test
public void testToLegacyFromLegacy()
{
String text = "" + GREEN + BOLD + "Hello " + WHITE + MAGIC + "world" + GRAY + "!";
assertEquals( text, BaseComponent.toLegacyText( TextComponent.fromLegacyText( text ) ) );
}
@Test
public void testComponentBuilderCursorInvalidPos()
{
ComponentBuilder builder = new ComponentBuilder();
builder.append( new TextComponent( "Apple, " ) );
builder.append( new TextComponent( "Orange, " ) );
assertThrows( IndexOutOfBoundsException.class, () -> builder.setCursor( -1 ) );
assertThrows( IndexOutOfBoundsException.class, () -> builder.setCursor( 2 ) );
}
@Test
public void testComponentBuilderCursor()
{
TextComponent t1, t2, t3;
ComponentBuilder builder = new ComponentBuilder();
assertEquals( builder.getCursor(), -1 );
builder.append( t1 = new TextComponent( "Apple, " ) );
assertEquals( builder.getCursor(), 0 );
builder.append( t2 = new TextComponent( "Orange, " ) );
builder.append( t3 = new TextComponent( "Mango, " ) );
assertEquals( builder.getCursor(), 2 );
builder.setCursor( 0 );
assertEquals( builder.getCurrentComponent(), t1 );
// Test that appending new components updates the position to the new list size
// after having previously set it to 0 (first component)
builder.append( new TextComponent( "and Grapefruit" ) );
assertEquals( builder.getCursor(), 3 );
builder.setCursor( 0 );
builder.resetCursor();
assertEquals( builder.getCursor(), 3 );
}
@Test
public void testLegacyComponentBuilderAppend()
{
String text = "" + GREEN + BOLD + "Hello " + RESET + MAGIC + "world" + GRAY + "!";
BaseComponent[] components = TextComponent.fromLegacyText( text );
BaseComponent[] builderComponents = new ComponentBuilder().append( components ).create();
assertArrayEquals( components, builderComponents );
}
/*
@Test
public void testItemTag()
{
TextComponent component = new TextComponent( "Hello world" );
HoverEvent.ContentItem content = new HoverEvent.ContentItem();
content.setId( "minecraft:diamond_sword" );
content.setCount( 1 );
content.setTag( ItemTag.builder()
.ench( new ItemTag.Enchantment( 5, 16 ) )
.name( new TextComponent( "Sharp Sword" ) )
.unbreakable( true )
.lore( new ComponentBuilder( "Line1" ).create() )
.lore( new ComponentBuilder( "Line2" ).create() )
.build() );
HoverEvent event = new HoverEvent( HoverEvent.Action.SHOW_ITEM, content );
component.setHoverEvent( event );
String serialised = ComponentSerializer.toString( component );
BaseComponent[] deserialised = ComponentSerializer.parse( serialised );
assertEquals( TextComponent.toLegacyText( deserialised ), TextComponent.toLegacyText( component ) );
}
*/
@Test
public void testModernShowAdvancement()
{
String advancement = "achievement.openInventory";
// First do the text using the newer contents system
HoverEvent hoverEvent = new HoverEvent(
HoverEvent.Action.SHOW_TEXT,
new Text( advancement )
);
TextComponent component = new TextComponent( "test" );
component.setHoverEvent( hoverEvent );
assertEquals( component.getHoverEvent().getContents().size(), 1 );
assertTrue( component.getHoverEvent().getContents().get( 0 ) instanceof Text );
assertEquals( ( (Text) component.getHoverEvent().getContents().get( 0 ) ).getValue(), advancement );
}
@Test
public void testHoverEventContentsCreate()
{
// First do the text using the newer contents system
HoverEvent hoverEvent = new HoverEvent(
HoverEvent.Action.SHOW_TEXT,
new Text( new ComponentBuilder( "First" ).create() ),
new Text( new ComponentBuilder( "Second" ).create() )
);
this.testHoverEventContents(
hoverEvent,
ComponentSerializer::parse,
(components) -> components[0].getHoverEvent(),
ComponentsTest::testDissembleReassemble // BaseComponent
);
// check the test still works with the value method
hoverEvent = new HoverEvent( HoverEvent.Action.SHOW_TEXT, new ComponentBuilder( "Sample text" ).create() );
TextComponent component = new TextComponent( "Sample text" );
component.setHoverEvent( hoverEvent );
assertEquals( hoverEvent.getContents().size(), 1 );
assertTrue( hoverEvent.isLegacy() );
String serialized = ComponentSerializer.toString( component );
BaseComponent[] deserialized = ComponentSerializer.parse( serialized );
assertEquals( component.getHoverEvent(), deserialized[0].getHoverEvent() );
}
@Test
public void testHoverEventContentsBuild()
{
// First do the text using the newer contents system
HoverEvent hoverEvent = new HoverEvent(
HoverEvent.Action.SHOW_TEXT,
new Text( new ComponentBuilder( "First" ).build() ),
new Text( new ComponentBuilder( "Second" ).build() )
);
this.testHoverEventContents(
hoverEvent,
ComponentSerializer::deserialize,
BaseComponent::getHoverEvent,
ComponentsTest::testDissembleReassemble // BaseComponent
);
}
private <T> void testHoverEventContents(HoverEvent hoverEvent, Function<String, T> deserializer, Function<T, HoverEvent> hoverEventGetter, Consumer<T> dissembleReassembleTest)
{
TextComponent component = new TextComponent( "Sample text" );
component.setHoverEvent( hoverEvent );
assertEquals( hoverEvent.getContents().size(), 2 );
assertFalse( hoverEvent.isLegacy() );
String serialized = ComponentSerializer.toString( component );
T deserialized = deserializer.apply( serialized );
assertEquals( component.getHoverEvent(), hoverEventGetter.apply( deserialized ) );
// Test single content:
String json = "{\"italic\":true,\"color\":\"gray\",\"translate\":\"chat.type.admin\",\"with\":[{\"text\":\"@\"}"
+ ",{\"translate\":\"commands.give.success.single\",\"with\":[\"1\",{\"color\":\"white\""
+ ",\"hoverEvent\":{\"action\":\"show_item\",\"contents\":{\"id\":\"minecraft:diamond_sword\",\"tag\":\""
+ "{Damage:0,display:{Lore:['\\\"test lore'!\\\"'],Name:'\\\"test\\\"'}}\"}},"
+ "\"extra\":[{\"italic\":true,\"extra\":[{\"text\":\"test\"}],\"text\":\"\"},{\"text\":\"]\"}],"
+ "\"text\":\"[\"},{\"insertion\":\"Name\",\"clickEvent\":{\"action\":\"suggest_command\",\"value\":"
+ "\"/tell Name \"},\"hoverEvent\":{\"action\":\"show_entity\",\"contents\":"
+ "{\"type\":\"minecraft:player\",\"id\":\"00000000-0000-0000-0000-00000000000000\",\"name\":"
+ "{\"text\":\"Name\"}}},\"text\":\"Name\"}]}]}";
dissembleReassembleTest.accept( deserializer.apply( json ) );
}
@Test
public void testFormatRetentionCopyFormattingCreate()
{
testFormatRetentionCopyFormatting( () -> new HoverEvent( HoverEvent.Action.SHOW_TEXT, new ComponentBuilder( "Test" ).create() ) );
}
@Test
public void testFormatRetentionCopyFormattingBuild()
{
testFormatRetentionCopyFormatting( () -> new HoverEvent( HoverEvent.Action.SHOW_TEXT, new Text( new ComponentBuilder( "Test" ).build() ) ) );
}
private static void testFormatRetentionCopyFormatting(Supplier<HoverEvent> hoverEventSupplier)
{
TextComponent first = new TextComponent( "Hello" );
first.setBold( true );
first.setColor( RED );
first.setClickEvent( new ClickEvent( ClickEvent.Action.RUN_COMMAND, "test" ) );
first.setHoverEvent( hoverEventSupplier.get() );
TextComponent second = new TextComponent( " world" );
second.copyFormatting( first, ComponentBuilder.FormatRetention.ALL, true );
assertEquals( first.isBold(), second.isBold() );
assertEquals( first.getColor(), second.getColor() );
assertEquals( first.getClickEvent(), second.getClickEvent() );
assertEquals( first.getHoverEvent(), second.getHoverEvent() );
}
@Test
public void testBuilderCloneCreate()
{
testBuilderClone( (builder) -> BaseComponent.toLegacyText( builder.create() ) );
}
@Test
public void testBuilderCloneBuild()
{
testBuilderClone( (builder) -> BaseComponent.toLegacyText( builder.build() ) );
}
private static void testBuilderClone(Function<ComponentBuilder, String> legacyTextFunction)
{
ComponentBuilder builder = new ComponentBuilder( "Hello " ).color( RED ).append( "world" ).color( DARK_RED );
ComponentBuilder cloned = new ComponentBuilder( builder );
assertEquals( legacyTextFunction.apply( builder ), legacyTextFunction.apply( cloned ) );
}
@Test
public void testBuilderAppendCreateMixedComponents()
{
testBuilderAppendMixedComponents(
ComponentBuilder::create,
(components, index) -> components[index]
);
}
@Test
public void testBuilderAppendBuildMixedComponents()
{
testBuilderAppendMixedComponents(
ComponentBuilder::build,
(component, index) -> component.getExtra().get( index )
);
}
private static <T> void testBuilderAppendMixedComponents(Function<ComponentBuilder, T> componentBuilder, BiFunction<T, Integer, BaseComponent> extraGetter)
{
ComponentBuilder builder = new ComponentBuilder( "Hello " );
TextComponent textComponent = new TextComponent( "world " );
TranslatableComponent translatableComponent = new TranslatableComponent( "item.swordGold.name" );
// array based BaseComponent append
builder.append( new BaseComponent[]
{
textComponent,
translatableComponent
} );
ScoreComponent scoreComponent = new ScoreComponent( "myscore", "myobjective" );
builder.append( scoreComponent ); // non array based BaseComponent append
T component = componentBuilder.apply( builder );
assertEquals( "Hello ", extraGetter.apply( component, 0 ).toPlainText() );
assertEquals( textComponent.toPlainText(), extraGetter.apply( component, 1 ).toPlainText() );
assertEquals( translatableComponent.toPlainText(), extraGetter.apply( component, 2 ).toPlainText() );
assertEquals( scoreComponent.toPlainText(), extraGetter.apply( component, 3 ).toPlainText() );
}
@Test
public void testScore()
{
BaseComponent[] component = ComponentSerializer.parse( "{\"score\":{\"name\":\"@p\",\"objective\":\"TEST\",\"value\":\"hello\"}}" );
String text = ComponentSerializer.toString( component );
BaseComponent[] reparsed = ComponentSerializer.parse( text );
assertArrayEquals( component, reparsed );
}
@Test
public void testStyle()
{
ComponentStyle style = ComponentSerializer.deserializeStyle( "{\"color\":\"red\",\"font\":\"minecraft:example\",\"bold\":true,\"italic\":false,\"obfuscated\":true}" );
String text = ComponentSerializer.toString( style );
ComponentStyle reparsed = ComponentSerializer.deserializeStyle( text );
assertEquals( style, reparsed );
}
@Test
public void testBuilderAppendCreate()
{
testBuilderAppend(
() -> new HoverEvent( HoverEvent.Action.SHOW_TEXT, new ComponentBuilder( "Hello world" ).create() ),
ComponentBuilder::create,
(components, index) -> components[index],
BaseComponent::toPlainText,
YELLOW + "Hello " + GREEN + "world!",
BaseComponent::toLegacyText
);
}
@Test
public void testBuilderAppendBuild()
{
testBuilderAppend(
() -> new HoverEvent( HoverEvent.Action.SHOW_TEXT, new Text( new ComponentBuilder( "Hello world" ).build() ) ),
ComponentBuilder::build,
(component, index) -> component.getExtra().get( index ),
(component) -> BaseComponent.toPlainText( component ),
// An extra format code is appended to the beginning because there is an empty TextComponent at the start of every component
WHITE.toString() + YELLOW + "Hello " + GREEN + "world!",
(component) -> BaseComponent.toLegacyText( component )
);
}
private static <T> void testBuilderAppend(Supplier<HoverEvent> hoverEventSupplier, Function<ComponentBuilder, T> componentBuilder, BiFunction<T, Integer, BaseComponent> extraGetter, Function<T, String> toPlainTextFunction, String expectedLegacyText, Function<T, String> toLegacyTextFunction)
{
ClickEvent clickEvent = new ClickEvent( ClickEvent.Action.RUN_COMMAND, "/help " );
HoverEvent hoverEvent = hoverEventSupplier.get();
ComponentBuilder builder = new ComponentBuilder( "Hello " ).color( YELLOW );
builder.append( new ComponentBuilder( "world!" ).color( GREEN ).event( hoverEvent ).event( clickEvent ).create() ); // Intentionally using create() to append multiple individual components
T component = componentBuilder.apply( builder );
assertEquals( extraGetter.apply( component, 1 ).getHoverEvent(), hoverEvent );
assertEquals( extraGetter.apply( component, 1 ).getClickEvent(), clickEvent );
assertEquals( "Hello world!", toPlainTextFunction.apply( component ) );
assertEquals( expectedLegacyText, toLegacyTextFunction.apply( component ) );
}
@Test
public void testBuilderAppendLegacyCreate()
{
testBuilderAppendLegacy(
ComponentBuilder::create,
BaseComponent::toPlainText,
YELLOW + "Hello " + GREEN + "world!",
BaseComponent::toLegacyText
);
}
@Test
public void testBuilderAppendLegacyBuild()
{
testBuilderAppendLegacy(
ComponentBuilder::build,
(component) -> BaseComponent.toPlainText( component ),
// An extra format code is appended to the beginning because there is an empty TextComponent at the start of every component
WHITE.toString() + YELLOW + "Hello " + GREEN + "world!",
(component) -> BaseComponent.toLegacyText( component )
);
}
private static <T> void testBuilderAppendLegacy(Function<ComponentBuilder, T> componentBuilder, Function<T, String> toPlainTextFunction, String expectedLegacyString, Function<T, String> toLegacyTextFunction)
{
ComponentBuilder builder = new ComponentBuilder( "Hello " ).color( YELLOW );
builder.appendLegacy( GREEN + "world!" );
T component = componentBuilder.apply( builder );
assertEquals( "Hello world!", toPlainTextFunction.apply( component ) );
assertEquals( expectedLegacyString, toLegacyTextFunction.apply( component ) );
}
@Test
public void testBasicComponent()
{
TextComponent textComponent = new TextComponent( "Hello world" );
textComponent.setColor( RED );
assertEquals( "Hello world", textComponent.toPlainText() );
assertEquals( RED + "Hello world", textComponent.toLegacyText() );
}
@Test
public void testLegacyConverter()
{
BaseComponent[] test1 = TextComponent.fromLegacyText( AQUA + "Aqua " + RED + BOLD + "RedBold" );
assertEquals( "Aqua RedBold", BaseComponent.toPlainText( test1 ) );
assertEquals( AQUA + "Aqua " + RED + BOLD + "RedBold", BaseComponent.toLegacyText( test1 ) );
BaseComponent[] test2 = TextComponent.fromLegacyText( "Text http://spigotmc.org " + GREEN + "google.com/test" );
assertEquals( "Text http://spigotmc.org google.com/test", BaseComponent.toPlainText( test2 ) );
//The extra ChatColor instances are sometimes inserted when not needed but it doesn't change the result
assertEquals( WHITE + "Text " + WHITE + "http://spigotmc.org" + WHITE
+ " " + GREEN + "google.com/test" + GREEN, BaseComponent.toLegacyText( test2 ) );
ClickEvent url1 = test2[1].getClickEvent();
assertNotNull( url1 );
assertTrue( url1.getAction() == ClickEvent.Action.OPEN_URL );
assertEquals( "http://spigotmc.org", url1.getValue() );
ClickEvent url2 = test2[3].getClickEvent();
assertNotNull( url2 );
assertTrue( url2.getAction() == ClickEvent.Action.OPEN_URL );
assertEquals( "http://google.com/test", url2.getValue() );
}
@Test
public void testBuilderCreate()
{
testBuilder(
ComponentBuilder::create,
BaseComponent::toPlainText,
RED + "Hello " + BLUE + BOLD + "World" + YELLOW + BOLD + "!",
BaseComponent::toLegacyText
);
}
@Test
public void testBuilderBuild()
{
testBuilder(
ComponentBuilder::build,
(component) -> BaseComponent.toPlainText( component ),
// An extra format code is appended to the beginning because there is an empty TextComponent at the start of every component
WHITE.toString() + RED + "Hello " + BLUE + BOLD + "World" + YELLOW + BOLD + "!",
(component) -> BaseComponent.toLegacyText( component )
);
}
private static <T> void testBuilder(Function<ComponentBuilder, T> componentBuilder, Function<T, String> toPlainTextFunction, String expectedLegacyString, Function<T, String> toLegacyTextFunction)
{
T component = componentBuilder.apply( new ComponentBuilder( "Hello " ).color( RED ).
append( "World" ).bold( true ).color( BLUE ).
append( "!" ).color( YELLOW ) );
assertEquals( "Hello World!", toPlainTextFunction.apply( component ) );
assertEquals( expectedLegacyString, toLegacyTextFunction.apply( component ) );
}
@Test
public void testBuilderCreateReset()
{
testBuilderReset(
ComponentBuilder::create,
(components, index) -> components[index]
);
}
@Test
public void testBuilderBuildReset()
{
testBuilderReset(
ComponentBuilder::build,
(component, index) -> component.getExtra().get( index )
);
}
private static <T> void testBuilderReset(Function<ComponentBuilder, T> componentBuilder, BiFunction<T, Integer, BaseComponent> extraGetter)
{
T component = componentBuilder.apply( new ComponentBuilder( "Hello " ).color( RED )
.append( "World" ).reset() );
assertEquals( RED, extraGetter.apply( component, 0 ).getColor() );
assertEquals( WHITE, extraGetter.apply( component, 1 ).getColor() );
}
@Test
public void testBuilderCreateFormatRetention()
{
testBuilderFormatRetention(
ComponentBuilder::create,
(components, index) -> components[index]
);
}
@Test
public void testBuilderBuildFormatRetention()
{
testBuilderFormatRetention(
ComponentBuilder::build,
(component, index) -> component.getExtra().get( index )
);
}
private static <T> void testBuilderFormatRetention(Function<ComponentBuilder, T> componentBuilder, BiFunction<T, Integer, BaseComponent> extraGetter)
{
T noneRetention = componentBuilder.apply( new ComponentBuilder( "Hello " ).color( RED )
.append( "World", ComponentBuilder.FormatRetention.NONE ) );
assertEquals( RED, extraGetter.apply( noneRetention, 0 ).getColor() );
assertEquals( WHITE, extraGetter.apply( noneRetention, 1 ).getColor() );
HoverEvent testEvent = new HoverEvent( HoverEvent.Action.SHOW_TEXT, new Text( new ComponentBuilder( "test" ).build() ) );
T formattingRetention = componentBuilder.apply( new ComponentBuilder( "Hello " ).color( RED )
.event( testEvent ).append( "World", ComponentBuilder.FormatRetention.FORMATTING ) );
assertEquals( RED, extraGetter.apply( formattingRetention, 0 ).getColor() );
assertEquals( testEvent, extraGetter.apply( formattingRetention, 0 ).getHoverEvent() );
assertEquals( RED, extraGetter.apply( formattingRetention, 1 ).getColor() );
assertNull( extraGetter.apply( formattingRetention, 1 ).getHoverEvent() );
ClickEvent testClickEvent = new ClickEvent( ClickEvent.Action.OPEN_URL, "http://www.example.com" );
T eventRetention = componentBuilder.apply( new ComponentBuilder( "Hello " ).color( RED )
.event( testEvent ).event( testClickEvent ).append( "World", ComponentBuilder.FormatRetention.EVENTS ) );
assertEquals( RED, extraGetter.apply( eventRetention, 0 ).getColor() );
assertEquals( testEvent, extraGetter.apply( eventRetention, 0 ).getHoverEvent() );
assertEquals( testClickEvent, extraGetter.apply( eventRetention, 0 ).getClickEvent() );
assertEquals( WHITE, extraGetter.apply( eventRetention, 1 ).getColor() );
assertEquals( testEvent, extraGetter.apply( eventRetention, 1 ).getHoverEvent() );
assertEquals( testClickEvent, extraGetter.apply( eventRetention, 1 ).getClickEvent() );
}
@Test
public void testLoopSimple()
{
TextComponent component = new TextComponent( "Testing" );
component.addExtra( component );
assertThrows( IllegalArgumentException.class, () -> ComponentSerializer.toString( component ) );
}
@Test
public void testLoopComplex()
{
TextComponent a = new TextComponent( "A" );
TextComponent b = new TextComponent( "B" );
b.setColor( AQUA );
TextComponent c = new TextComponent( "C" );
c.setColor( RED );
a.addExtra( b );
b.addExtra( c );
c.addExtra( a );
assertThrows( IllegalArgumentException.class, () -> ComponentSerializer.toString( a ) );
}
@Test
public void testRepeated()
{
TextComponent a = new TextComponent( "A" );
TextComponent b = new TextComponent( "B" );
b.setColor( AQUA );
a.addExtra( b );
a.addExtra( b );
ComponentSerializer.toString( a );
}
@Test
public void testRepeatedError()
{
TextComponent a = new TextComponent( "A" );
TextComponent b = new TextComponent( "B" );
b.setColor( AQUA );
TextComponent c = new TextComponent( "C" );
c.setColor( RED );
a.addExtra( b );
a.addExtra( c );
c.addExtra( a );
a.addExtra( b );
assertThrows( IllegalArgumentException.class, () -> ComponentSerializer.toString( a ) );
}
@Test
public void testInvalidColorCodes()
{
StringBuilder allInvalidColorCodes = new StringBuilder();
// collect all invalid color codes (e.g. §z, §g, ...)
for ( char alphChar : "0123456789abcdefghijklmnopqrstuvwxyz".toCharArray() )
{
if ( ALL_CODES.indexOf( alphChar ) == -1 )
{
allInvalidColorCodes.append( COLOR_CHAR );
allInvalidColorCodes.append( alphChar );
}
}
// last char is a single '§'
allInvalidColorCodes.append( COLOR_CHAR );
String invalidColorCodesLegacyText = fromAndToLegacyText( allInvalidColorCodes.toString() );
String emptyLegacyText = fromAndToLegacyText( "" );
// all invalid color codes and the trailing '§' should be ignored
assertEquals( emptyLegacyText, invalidColorCodesLegacyText );
}
@Test
public void testFormattingOnlyTextConversion()
{
String text = "" + GREEN;
BaseComponent[] converted = TextComponent.fromLegacyText( text );
assertEquals( GREEN, converted[0].getColor() );
String roundtripLegacyText = BaseComponent.toLegacyText( converted );
// color code should not be lost during conversion
assertEquals( text, roundtripLegacyText );
}
@Test
public void testEquals()
{
TextComponent first = new TextComponent( "Hello, " );
first.addExtra( new TextComponent( "World!" ) );
TextComponent second = new TextComponent( "Hello, " );
second.addExtra( new TextComponent( "World!" ) );
assertEquals( first, second );
}
@Test
public void testNotEquals()
{
TextComponent first = new TextComponent( "Hello, " );
first.addExtra( new TextComponent( "World." ) );
TextComponent second = new TextComponent( "Hello, " );
second.addExtra( new TextComponent( "World!" ) );
assertNotEquals( first, second );
}
@Test
public void testLegacyHack()
{
BaseComponent[] hexColored = new ComponentBuilder().color( of( Color.GRAY ) ).append( "Test" ).create();
String legacy = BaseComponent.toLegacyText( hexColored );
BaseComponent[] reColored = TextComponent.fromLegacyText( legacy );
assertArrayEquals( hexColored, reColored );
}
@Test
public void testLegacyResetInBuilderCreate()
{
testLegacyResetInBuilder(
ComponentBuilder::create,
ComponentSerializer::toString
);
}
@Test
public void testLegacyResetInBuilderBuild()
{
testLegacyResetInBuilder(
ComponentBuilder::build,
ComponentSerializer::toString
);
}
@Test
public void testHasFormatting()
{
BaseComponent component = new TextComponent();
assertFalse( component.hasFormatting() );
component.setBold( true );
assertTrue( component.hasFormatting() );
}
@Test
public void testStyleIsEmpty()
{
ComponentStyle style = ComponentStyle.builder().build();
assertTrue( style.isEmpty() );
style = ComponentStyle.builder()
.bold( true )
.build();
assertFalse( style.isEmpty() );
}
/*
* In legacy chat, colors and reset both reset all formatting.
* Make sure it works in combination with ComponentBuilder.
*/
private static <T> void testLegacyResetInBuilder(Function<ComponentBuilder, T> componentBuilder, Function<T, String> componentSerializer)
{
ComponentBuilder builder = new ComponentBuilder();
BaseComponent[] a = TextComponent.fromLegacyText( "" + DARK_RED + UNDERLINE + "44444" + RESET + "dd" + GOLD + BOLD + "6666" );
String expected = "{\"extra\":[{\"underlined\":true,\"color\":\"dark_red\",\"text\":\"44444\"},{\"color\":"
+ "\"white\",\"text\":\"dd\"},{\"bold\":true,\"color\":\"gold\",\"text\":\"6666\"}],\"text\":\"\"}";
assertEquals( expected, ComponentSerializer.toString( a ) );
builder.append( a );
String test1 = componentSerializer.apply( componentBuilder.apply( builder ) );
assertEquals( expected, test1 );
BaseComponent[] b = TextComponent.fromLegacyText( RESET + "rrrr" );
builder.append( b );
String test2 = componentSerializer.apply( componentBuilder.apply( builder ) );
assertEquals(
"{\"extra\":[{\"underlined\":true,\"color\":\"dark_red\",\"text\":\"44444\"},"
+ "{\"color\":\"white\",\"text\":\"dd\"},{\"bold\":true,\"color\":\"gold\",\"text\":\"6666\"},"
+ "{\"color\":\"white\",\"text\":\"rrrr\"}],\"text\":\"\"}",
test2 );
}
private static String fromAndToLegacyText(String legacyText)
{
return BaseComponent.toLegacyText( TextComponent.fromLegacyText( legacyText ) );
}
}

View File

@@ -0,0 +1,51 @@
package net.md_5.bungee.api.chat;
import static net.md_5.bungee.api.ChatColor.*;
import static org.junit.jupiter.api.Assertions.*;
import net.md_5.bungee.chat.ComponentSerializer;
import org.junit.jupiter.api.Test;
public class TranslatableComponentTest
{
@Test
public void testMissingPlaceholdersAdded()
{
TranslatableComponent testComponent = new TranslatableComponent( "Test string with %s placeholders: %s", 2, "aoeu" );
assertEquals( "Test string with 2 placeholders: aoeu", testComponent.toPlainText() );
assertEquals( WHITE + "Test string with " + WHITE + "2" + WHITE + " placeholders: " + WHITE + "aoeu", testComponent.toLegacyText() );
}
@Test
public void testJsonSerialisation()
{
TranslatableComponent testComponent = new TranslatableComponent( "Test string with %s placeholder", "a" );
String jsonString = ComponentSerializer.toString( testComponent );
BaseComponent[] baseComponents = ComponentSerializer.parse( jsonString );
assertEquals( "Test string with a placeholder", BaseComponent.toPlainText( baseComponents ) );
assertEquals( WHITE + "Test string with " + WHITE + "a" + WHITE + " placeholder", BaseComponent.toLegacyText( baseComponents ) );
}
@Test
public void testTranslateComponent()
{
TranslatableComponent item = new TranslatableComponent( "item.swordGold.name" );
item.setColor( AQUA );
TranslatableComponent translatableComponent = new TranslatableComponent( "commands.give.success",
item, "5", "thinkofdeath" );
assertEquals( "Given Golden Sword * 5 to thinkofdeath", translatableComponent.toPlainText() );
assertEquals( WHITE + "Given " + AQUA + "Golden Sword" + WHITE + " * " + WHITE + "5" + WHITE + " to " + WHITE + "thinkofdeath",
translatableComponent.toLegacyText() );
TranslatableComponent positional = new TranslatableComponent( "book.pageIndicator", "5", "50" );
assertEquals( "Page 5 of 50", positional.toPlainText() );
assertEquals( WHITE + "Page " + WHITE + "5" + WHITE + " of " + WHITE + "50", positional.toLegacyText() );
TranslatableComponent one_four_two = new TranslatableComponent( "filled_map.buried_treasure" );
assertEquals( "Buried Treasure Map", one_four_two.toPlainText() );
}
}

View File

@@ -0,0 +1,25 @@
package net.md_5.bungee.dialog;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.ComponentBuilder;
import net.md_5.bungee.api.dialog.Dialog;
import net.md_5.bungee.api.dialog.DialogBase;
import net.md_5.bungee.api.dialog.NoticeDialog;
import net.md_5.bungee.chat.VersionedComponentSerializer;
import org.junit.jupiter.api.Test;
public class SimpleTest
{
@Test
public void testNotice()
{
String json = "{type:\"minecraft:notice\",title:\"Hello\"}";
Dialog deserialized = VersionedComponentSerializer.getDefault().getDialogSerializer().deserialize( json );
System.err.println( deserialized );
Dialog notice = new NoticeDialog( new DialogBase( new ComponentBuilder( "Hello" ).color( ChatColor.RED ).build() ) );
String newJson = VersionedComponentSerializer.getDefault().getDialogSerializer().toString( notice );
System.err.println( newJson );
}
}