diff --git a/api/src/main/java/net/md_5/bungee/api/CommandSender.java b/api/src/main/java/net/md_5/bungee/api/CommandSender.java index 0a51b57f..4a8f6bcd 100644 --- a/api/src/main/java/net/md_5/bungee/api/CommandSender.java +++ b/api/src/main/java/net/md_5/bungee/api/CommandSender.java @@ -1,5 +1,7 @@ package net.md_5.bungee.api; +import net.md_5.bungee.api.chat.BaseComponent; + import java.util.Collection; public interface CommandSender @@ -17,6 +19,7 @@ public interface CommandSender * * @param message the message to send */ + @Deprecated public void sendMessage(String message); /** @@ -25,8 +28,23 @@ public interface CommandSender * * @param messages the messages to send */ + @Deprecated public void sendMessages(String... messages); + /** + * Send a message to this sender. + * + * @param message the message to send + */ + public void sendMessage(BaseComponent... message); + + /** + * Send a message to this sender. + * + * @param message the message to send + */ + public void sendMessage(BaseComponent message); + /** * Get all groups this user is part of. This returns an unmodifiable * collection. diff --git a/api/src/main/java/net/md_5/bungee/api/ProxyServer.java b/api/src/main/java/net/md_5/bungee/api/ProxyServer.java index 65d20404..f2ef12d0 100644 --- a/api/src/main/java/net/md_5/bungee/api/ProxyServer.java +++ b/api/src/main/java/net/md_5/bungee/api/ProxyServer.java @@ -1,5 +1,6 @@ package net.md_5.bungee.api; +import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.plugin.PluginManager; import com.google.common.base.Preconditions; import java.io.File; @@ -231,8 +232,23 @@ public abstract class ProxyServer * * @param message the message to broadcast */ + @Deprecated public abstract void broadcast(String message); + /** + * Send the specified message to the console and all connected players. + * + * @param message the message to broadcast + */ + public abstract void broadcast(BaseComponent... message); + + /** + * Send the specified message to the console and all connected players. + * + * @param message the message to broadcast + */ + public abstract void broadcast(BaseComponent message); + /** * Gets a new instance of this proxies custom tab list. * diff --git a/api/src/main/java/net/md_5/bungee/api/chat/BaseComponent.java b/api/src/main/java/net/md_5/bungee/api/chat/BaseComponent.java new file mode 100644 index 00000000..9ef3cc30 --- /dev/null +++ b/api/src/main/java/net/md_5/bungee/api/chat/BaseComponent.java @@ -0,0 +1,392 @@ +package net.md_5.bungee.api.chat; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import net.md_5.bungee.api.ChatColor; + +import java.util.ArrayList; +import java.util.List; + +@Setter +@NoArgsConstructor +public abstract class BaseComponent +{ + + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + BaseComponent parent; + + /** + * The color of this component and any child + * components (unless overridden) + */ + @Getter(AccessLevel.NONE) + private ChatColor color; + /** + * Whether this component and any child + * components (unless overridden) is bold + */ + @Getter(AccessLevel.NONE) + private Boolean bold; + /** + * Whether this component and any child + * components (unless overridden) is italic + */ + @Getter(AccessLevel.NONE) + private Boolean italic; + /** + * Whether this component and any child + * components (unless overridden) is underlined + */ + @Getter(AccessLevel.NONE) + private Boolean underlined; + /** + * Whether this component and any child + * components (unless overridden) is strikethrough + */ + @Getter(AccessLevel.NONE) + private Boolean strikethrough; + /** + * Whether this component and any child + * components (unless overridden) is obfuscated + */ + @Getter(AccessLevel.NONE) + private Boolean obfuscated; + + /** + * Appended components that inherit this component's + * formatting and events + */ + @Getter + private List extra; + + /** + * The action to preform when this component (and + * child components) are clicked + */ + @Getter + private ClickEvent clickEvent; + /** + * The action to preform when this component (and + * child components) are hovered over + */ + @Getter + private HoverEvent hoverEvent; + + protected BaseComponent(BaseComponent old) + { + setColor( old.getColorRaw() ); + setBold( old.isBoldRaw() ); + setItalic( old.isItalicRaw() ); + setUnderlined( old.isUnderlined() ); + setStrikethrough( old.isStrikethroughRaw() ); + setObfuscated( old.isObfuscatedRaw() ); + setClickEvent( old.getClickEvent() ); + setHoverEvent( old.getHoverEvent() ); + } + + /** + * Converts the components to a string that uses the + * old formatting codes ({@link net.md_5.bungee.api.ChatColor#COLOR_CHAR} + * @param components the components to convert + * @return the string in the old format + */ + public static String toLegacyText(BaseComponent... components) + { + StringBuilder builder = new StringBuilder(); + for ( BaseComponent msg : components ) + { + builder.append( msg.toLegacyText() ); + } + return builder.toString(); + } + + /** + * Converts the components into a string without + * any formatting + * @param components the components to convert + * @return the string as plain text + */ + public static String toPlainText(BaseComponent... components) + { + StringBuilder builder = new StringBuilder(); + for ( BaseComponent msg : components ) + { + builder.append( msg.toPlainText() ); + } + return builder.toString(); + } + + + /** + * Returns the color of this component. This uses the parent's color + * if this component doesn't have one. {@link net.md_5.bungee.api.ChatColor#WHITE} + * is returned if no color is found. + * + * @return the color of this component + */ + public ChatColor getColor() + { + if ( color == null ) + { + if ( parent == null ) + { + return ChatColor.WHITE; + } + return parent.getColor(); + } + return color; + } + + /** + * Returns the color of this component without checking the parents + * color. May return null + * + * @return the color of this component + */ + public ChatColor getColorRaw() + { + return color; + } + + /** + * Returns whether this component is bold. This uses the parent's + * setting if this component hasn't been set. false is returned + * if none of the parent chain has been set. + * + * @return whether the component is bold + */ + public boolean isBold() + { + if ( bold == null ) + { + return parent != null && parent.isBold(); + } + return bold; + } + + /** + * Returns whether this component is bold without checking + * the parents setting. May return null + * + * @return whether the component is bold + */ + public Boolean isBoldRaw() + { + return bold; + } + + /** + * Returns whether this component is italic. This uses the parent's + * setting if this component hasn't been set. false is returned + * if none of the parent chain has been set. + * + * @return whether the component is italic + */ + public boolean isItalic() + { + if ( italic == null ) + { + return parent != null && parent.isItalic(); + } + return italic; + } + + /** + * Returns whether this component is italic without checking + * the parents setting. May return null + * + * @return whether the component is italic + */ + public Boolean isItalicRaw() + { + return italic; + } + + /** + * Returns whether this component is underlined. This uses the parent's + * setting if this component hasn't been set. false is returned + * if none of the parent chain has been set. + * + * @return whether the component is underlined + */ + public boolean isUnderlined() + { + if ( underlined == null ) + { + return parent != null && parent.isUnderlined(); + } + return underlined; + } + + /** + * Returns whether this component is underlined without checking + * the parents setting. May return null + * + * @return whether the component is underlined + */ + public Boolean isUnderlinedRaw() + { + return underlined; + } + + /** + * Returns whether this component is strikethrough. This uses the parent's + * setting if this component hasn't been set. false is returned + * if none of the parent chain has been set. + * + * @return whether the component is strikethrough + */ + public boolean isStrikethrough() + { + if ( strikethrough == null ) + { + return parent != null && parent.isStrikethrough(); + } + return strikethrough; + } + + /** + * Returns whether this component is strikethrough without checking + * the parents setting. May return null + * + * @return whether the component is strikethrough + */ + public Boolean isStrikethroughRaw() + { + return strikethrough; + } + + /** + * Returns whether this component is obfuscated. This uses the parent's + * setting if this component hasn't been set. false is returned + * if none of the parent chain has been set. + * + * @return whether the component is obfuscated + */ + public boolean isObfuscated() + { + if ( obfuscated == null ) + { + return parent != null && parent.isObfuscated(); + } + return obfuscated; + } + + /** + * Returns whether this component is obfuscated without checking + * the parents setting. May return null + * + * @return whether the component is obfuscated + */ + public Boolean isObfuscatedRaw() + { + return obfuscated; + } + + public void setExtra(List components) + { + for ( BaseComponent component : components ) + { + component.parent = this; + } + extra = components; + } + + /** + * Appends a text element to the component. The text will + * inherit this component's formatting + * + * @param text the text to append + */ + public void addExtra(String text) + { + addExtra( new TextComponent( text ) ); + } + + /** + * Appends a component to the component. The text will + * inherit this component's formatting + * + * @param component the component to append + */ + public void addExtra(BaseComponent component) + { + if ( extra == null ) + { + extra = new ArrayList<>(); + } + component.parent = this; + extra.add( component ); + } + + /** + * Returns whether the component has any formatting + * or events applied to it + * @return + */ + public boolean hasFormatting() + { + return color != null || bold != null || + italic != null || underlined != null || + strikethrough != null || obfuscated != null || + hoverEvent != null || clickEvent != null; + } + + + /** + * Converts the component into a string without + * any formatting + * @return the string as plain text + */ + public String toPlainText() + { + StringBuilder builder = new StringBuilder(); + toPlainText( builder ); + return builder.toString(); + } + + protected void toPlainText(StringBuilder builder) + { + if ( extra != null ) + { + for ( BaseComponent e : extra ) + { + e.toPlainText( builder ); + } + } + } + + + /** + * Converts the component to a string that uses the + * old formatting codes ({@link net.md_5.bungee.api.ChatColor#COLOR_CHAR} + * @return the string in the old format + */ + public String toLegacyText() + { + StringBuilder builder = new StringBuilder(); + toLegacyText( builder ); + return builder.toString(); + } + + protected void toLegacyText(StringBuilder builder) + { + if ( extra != null ) + { + for ( BaseComponent e : extra ) + { + e.toLegacyText( builder ); + } + } + } + + + @Override + public String toString() + { + return String.format( "BaseComponent{color=%s, bold=%b, italic=%b, underlined=%b, strikethrough=%b, obfuscated=%b, clickEvent=%s, hoverEvent=%s}", getColor().getName(), isBold(), isItalic(), isUnderlined(), isStrikethrough(), isObfuscated(), getClickEvent(), getHoverEvent() ); + } +} diff --git a/api/src/main/java/net/md_5/bungee/api/chat/ClickEvent.java b/api/src/main/java/net/md_5/bungee/api/chat/ClickEvent.java new file mode 100644 index 00000000..55c66f30 --- /dev/null +++ b/api/src/main/java/net/md_5/bungee/api/chat/ClickEvent.java @@ -0,0 +1,53 @@ +package net.md_5.bungee.api.chat; + + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@AllArgsConstructor +final public class ClickEvent +{ + /** + * The type of action to preform on click + */ + private final Action action; + /** + * Depends on action + * @see net.md_5.bungee.api.chat.ClickEvent.Action + */ + private final String value; + + public enum Action + { + /** + * Open a url at the path given by + * {@link net.md_5.bungee.api.chat.ClickEvent#getValue()} + */ + OPEN_URL, + /** + * Open a file at the path given by + * {@link net.md_5.bungee.api.chat.ClickEvent#getValue()} + */ + OPEN_FILE, + /** + * Run the command given by + * {@link net.md_5.bungee.api.chat.ClickEvent#getValue()} + */ + RUN_COMMAND, + /** + * Inserts the string given by + * {@link net.md_5.bungee.api.chat.ClickEvent#getValue()} + * into the players text box + */ + SUGGEST_COMMAND + } + + @Override + public String toString() + { + return String.format( "ClickEvent{action=%s, value=%s}", action, value ); + } +} diff --git a/api/src/main/java/net/md_5/bungee/api/chat/ComponentBuilder.java b/api/src/main/java/net/md_5/bungee/api/chat/ComponentBuilder.java new file mode 100644 index 00000000..038ae5c7 --- /dev/null +++ b/api/src/main/java/net/md_5/bungee/api/chat/ComponentBuilder.java @@ -0,0 +1,157 @@ +package net.md_5.bungee.api.chat; + +import lombok.NoArgsConstructor; +import net.md_5.bungee.api.ChatColor; + +import java.util.ArrayList; +import java.util.List; + +/** + * ComponentBuilder simplifies creating basic messages by allowing + * the use of a chainable builder. + *

+ *

+ *     new ComponentBuilder("Hello ").color(ChatColor.RED).
+ *         append("World").color(ChatColor.BLUE).
+ *         append("!").bold(true).create();
+ * 
+ *

+ * All methods (excluding {@link #append(String)} and {@link #create()} + * work on the last part appended to the builder, so in the example + * above "Hello " would be {@link net.md_5.bungee.api.ChatColor#RED} + * and "World" would be {@link net.md_5.bungee.api.ChatColor#BLUE} but + * "!" would be bold and {@link net.md_5.bungee.api.ChatColor#BLUE} + * because append copies the previous part's formatting + */ +public class ComponentBuilder +{ + private TextComponent current; + private List parts = new ArrayList<>(); + + /** + * Creates a componentBuilder with the given text as the + * first part. + * + * @param text the first text element + */ + public ComponentBuilder(String text) + { + current = new TextComponent( text ); + } + + /** + * Appends the text to the builder and makes it the current + * target for formatting. The text will have all the + * formatting from the previous part. + * + * @param text the text to append + * @return this ComponentBuilder for chaining + */ + public ComponentBuilder append(String text) + { + parts.add( current ); + current = new TextComponent( current ); + current.setText( text ); + return this; + } + + /** + * Sets the color of the current part. + * + * @param color the new color + * @return this ComponentBuilder for chaining + */ + public ComponentBuilder color(ChatColor color) + { + current.setColor( color ); + return this; + } + + /** + * Sets whether the current part is bold. + * + * @param bold whether this part is bold + * @return this ComponentBuilder for chaining + */ + public ComponentBuilder bold(boolean bold) + { + current.setBold( bold ); + return this; + } + + /** + * Sets whether the current part is italic + * + * @param italic whether this part is italic + * @return this ComponentBuilder for chaining + */ + public ComponentBuilder italic(boolean italic) + { + current.setItalic( italic ); + return this; + } + + /** + * Sets whether the current part is underlined + * + * @param underlined whether this part is underlined + * @return this ComponentBuilder for chaining + */ + public ComponentBuilder underlined(boolean underlined) + { + current.setUnderlined( underlined ); + return this; + } + + /** + * Sets whether the current part is strikethrough + * + * @param strikethrough whether this part is strikethrough + * @return this ComponentBuilder for chaining + */ + public ComponentBuilder strikethrough(boolean strikethrough) + { + current.setStrikethrough( strikethrough ); + return this; + } + + /** + * Sets whether the current pat is obfuscated + * + * @param obfuscated whether this part is obfuscated + * @return this ComponentBuilder for chaining + */ + public ComponentBuilder obfuscated(boolean obfuscated) + { + current.setObfuscated( obfuscated ); + return this; + } + + /** + * Sets the click event for the current part. + * @param clickEvent + * @return + */ + public ComponentBuilder event(ClickEvent clickEvent) + { + current.setClickEvent( clickEvent ); + return this; + } + + public ComponentBuilder event(HoverEvent hoverEvent) + { + current.setHoverEvent( hoverEvent ); + return this; + } + + /** + * Returns the components needed to display the message + * created by this builder + * @return the created components + */ + public BaseComponent[] create() + { + parts.add( current ); + return parts.toArray( new BaseComponent[parts.size()] ); + } +} diff --git a/api/src/main/java/net/md_5/bungee/api/chat/HoverEvent.java b/api/src/main/java/net/md_5/bungee/api/chat/HoverEvent.java new file mode 100644 index 00000000..c2df8f9c --- /dev/null +++ b/api/src/main/java/net/md_5/bungee/api/chat/HoverEvent.java @@ -0,0 +1,27 @@ +package net.md_5.bungee.api.chat; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@AllArgsConstructor +final public class HoverEvent +{ + private final Action action; + private final BaseComponent[] value; + + public enum Action + { + SHOW_TEXT, + SHOW_ACHIEVEMENT, + SHOW_ITEM + } + + @Override + public String toString() + { + return String.format( "HoverEvent{action=%s, value=%s}", action, value ); + } +} diff --git a/api/src/main/java/net/md_5/bungee/api/chat/TextComponent.java b/api/src/main/java/net/md_5/bungee/api/chat/TextComponent.java new file mode 100644 index 00000000..e1c68c07 --- /dev/null +++ b/api/src/main/java/net/md_5/bungee/api/chat/TextComponent.java @@ -0,0 +1,158 @@ +package net.md_5.bungee.api.chat; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import net.md_5.bungee.api.ChatColor; + +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class TextComponent extends BaseComponent +{ + + private static final Pattern url = Pattern.compile( "^(?:(https?)://)?([-\\w_\\.]{2,}\\.[a-z]{2,4})(/\\S*)?$" ); + + /** + * Converts the old formatting system that used {@link net.md_5.bungee.api.ChatColor#COLOR_CHAR} + * into the new json based system. + * @param message the text to convert + * @return the components needed to print the message to the client + */ + public static BaseComponent[] fromLegacyText(String message) + { + ArrayList components = new ArrayList<>(); + StringBuilder builder = new StringBuilder(); + TextComponent component = new TextComponent(); + Matcher matcher = url.matcher( message ); + + for ( int i = 0; i < message.length(); i++ ) + { + char c = message.charAt( i ); + if ( c == ChatColor.COLOR_CHAR ) + { + i++; + c = message.charAt( i ); + if ( c >= 'A' && c <= 'Z' ) + { + c += 32; + } + if ( builder.length() > 0 ) + { + TextComponent old = component; + component = new TextComponent( old ); + old.setText( builder.toString() ); + builder = new StringBuilder(); + components.add( old ); + } + ChatColor format = ChatColor.getByChar( c ); + switch ( format ) + { + case BOLD: + component.setBold( true ); + break; + case ITALIC: + component.setItalic( true ); + break; + case UNDERLINE: + component.setUnderlined( true ); + break; + case STRIKETHROUGH: + component.setStrikethrough( true ); + break; + case MAGIC: + component.setObfuscated( true ); + break; + case RESET: + format = ChatColor.WHITE; + default: + component = new TextComponent(); + component.setColor( format ); + break; + } + continue; + } + int pos = message.indexOf( ' ', i ); + if ( pos == -1 ) pos = message.length(); + if ( matcher.region( i, pos ).find() ) + { //Web link handling + + if ( builder.length() > 0 ) + { + TextComponent old = component; + component = new TextComponent( old ); + old.setText( builder.toString() ); + builder = new StringBuilder(); + components.add( old ); + } + + TextComponent old = component; + component = new TextComponent( old ); + String urlString = message.substring( i, pos ); + component.setText( urlString ); + component.setClickEvent( new ClickEvent( ClickEvent.Action.OPEN_URL, + urlString.startsWith( "http" ) ? urlString : "http://" + urlString ) ); + components.add( component ); + i += pos - i - 1; + component = old; + continue; + } + builder.append( c ); + } + if ( builder.length() > 0 ) + { + component.setText( builder.toString() ); + components.add( component ); + } + return components.toArray( new BaseComponent[components.size()] ); + } + + /** + * The text of the component that will be + * displayed to the client + */ + private String text; + + /** + * Creates a TextComponent with formatting and text + * from the passed component + * @param textComponent the component to copy from + */ + public TextComponent(TextComponent textComponent) + { + super( textComponent ); + setText( textComponent.getText() ); + } + + @Override + protected void toPlainText(StringBuilder builder) + { + builder.append( text ); + super.toPlainText( builder ); + } + + @Override + protected void toLegacyText(StringBuilder builder) + { + builder.append( getColor() ); + if ( isBold() ) builder.append( ChatColor.BOLD ); + if ( isItalic() ) builder.append( ChatColor.ITALIC ); + if ( isUnderlined() ) builder.append( ChatColor.UNDERLINE ); + if ( isStrikethrough() ) builder.append( ChatColor.STRIKETHROUGH ); + if ( isObfuscated() ) builder.append( ChatColor.MAGIC ); + builder.append( text ); + super.toLegacyText( builder ); + } + + @Override + public String toString() + { + return String.format( "TextComponent{text=%s, %s}", text, super.toString() ); + } +} diff --git a/api/src/main/java/net/md_5/bungee/api/chat/TranslatableComponent.java b/api/src/main/java/net/md_5/bungee/api/chat/TranslatableComponent.java new file mode 100644 index 00000000..f24d252d --- /dev/null +++ b/api/src/main/java/net/md_5/bungee/api/chat/TranslatableComponent.java @@ -0,0 +1,204 @@ +package net.md_5.bungee.api.chat; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import net.md_5.bungee.api.ChatColor; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.ResourceBundle; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Getter +@Setter +@NoArgsConstructor +public class TranslatableComponent extends BaseComponent +{ + private final ResourceBundle locales = ResourceBundle.getBundle( "mojang-translations/en_US" ); + private final Pattern format = Pattern.compile( "%(?:(\\d+)\\$)?([A-Za-z%]|$)" ); + + /** + * The key into the Minecraft locale files to use for the + * translation. The text depends on the client's locale setting. + * The console is always en_US + */ + private String translate; + /** + * The components to substitute into the translation + */ + private List with; + + /** + * Creates a translatable component with the passed substitutions + * @see #setTranslate(String) + * @see #setWith(java.util.List) + * @param translate the translation key + * @param with the {@link java.lang.String}s and {@link net.md_5.bungee.api.chat.BaseComponent}s + * to use into the translation + */ + public TranslatableComponent(String translate, Object... with) + { + setTranslate( translate ); + List temp = new ArrayList<>(); + for ( Object w : with ) + { + if ( w instanceof String ) + { + temp.add( new TextComponent( (String) w ) ); + } else + { + temp.add( (BaseComponent) w ); + } + } + setWith( temp ); + } + + /** + * Sets the translation substitutions to be used in + * this component. Removes any previously set + * substitutions + * @param components the components to substitute + */ + public void setWith(List components) + { + for ( BaseComponent component : components ) + { + component.parent = this; + } + with = components; + } + + /** + * Adds a text substitution to the component. The text will + * inherit this component's formatting + * + * @param text the text to substitute + */ + public void addWith(String text) + { + addWith( new TextComponent( text ) ); + } + + /** + * Adds a component substitution to the component. The text will + * inherit this component's formatting + * + * @param component the component to substitute + */ + public void addWith(BaseComponent component) + { + if ( with == null ) + { + with = new ArrayList<>(); + } + component.parent = this; + with.add( component ); + } + + @Override + protected void toPlainText(StringBuilder builder) + { + String trans = locales.getString( translate ); + if ( trans == null ) + { + builder.append( translate ); + } else + { + Matcher matcher = format.matcher( trans ); + int position = 0; + int i = 0; + while ( matcher.find( position ) ) + { + int pos = matcher.start(); + if ( pos != position ) + { + builder.append( trans.substring( position, pos ) ); + } + position = matcher.end(); + + String formatCode = matcher.group( 2 ); + switch ( formatCode.charAt( 0 ) ) + { + case 's': + case 'd': + String withIndex = matcher.group( 1 ); + with.get( withIndex != null ? Integer.parseInt( withIndex ) - 1 : i++ ).toPlainText( builder ); + break; + case '%': + builder.append( '%' ); + break; + } + } + if ( trans.length() != position ) + { + builder.append( trans.substring( position, trans.length() ) ); + } + } + super.toPlainText( builder ); + } + + @Override + protected void toLegacyText(StringBuilder builder) + { + String trans = locales.getString( translate ); + if ( trans == null ) + { + addFormat( builder ); + builder.append( translate ); + } else + { + Matcher matcher = format.matcher( trans ); + int position = 0; + int i = 0; + while ( matcher.find( position ) ) + { + int pos = matcher.start(); + if ( pos != position ) + { + addFormat( builder ); + builder.append( trans.substring( position, pos ) ); + } + position = matcher.end(); + + String formatCode = matcher.group( 2 ); + switch ( formatCode.charAt( 0 ) ) + { + case 's': + case 'd': + String withIndex = matcher.group( 1 ); + with.get( withIndex != null ? Integer.parseInt( withIndex ) - 1 : i++ ).toLegacyText( builder ); + break; + case '%': + addFormat( builder ); + builder.append( '%' ); + break; + } + } + if ( trans.length() != position ) + { + addFormat( builder ); + builder.append( trans.substring( position, trans.length() ) ); + } + } + super.toLegacyText( builder ); + } + + private void addFormat(StringBuilder builder) + { + builder.append( getColor() ); + if ( isBold() ) builder.append( ChatColor.BOLD ); + if ( isItalic() ) builder.append( ChatColor.ITALIC ); + if ( isUnderlined() ) builder.append( ChatColor.UNDERLINE ); + if ( isStrikethrough() ) builder.append( ChatColor.STRIKETHROUGH ); + if ( isObfuscated() ) builder.append( ChatColor.MAGIC ); + } + + @Override + public String toString() + { + return String.format( "TranslatableComponent{translate=%s, with=%s, %s}", translate, with, super.toString() ); + } +} diff --git a/api/src/main/java/net/md_5/bungee/api/connection/Connection.java b/api/src/main/java/net/md_5/bungee/api/connection/Connection.java index 2c9cae14..4e2068b9 100644 --- a/api/src/main/java/net/md_5/bungee/api/connection/Connection.java +++ b/api/src/main/java/net/md_5/bungee/api/connection/Connection.java @@ -1,6 +1,8 @@ package net.md_5.bungee.api.connection; import java.net.InetSocketAddress; + +import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.protocol.DefinedPacket; /** @@ -26,8 +28,29 @@ public interface Connection * @param reason the reason shown to the player / sent to the server on * disconnect */ + @Deprecated void disconnect(String reason); + /** + * Disconnects this end of the connection for the specified reason. If this + * is an {@link ProxiedPlayer} the respective server connection will be + * closed too. + * + * @param reason the reason shown to the player / sent to the server on + * disconnect + */ + void disconnect(BaseComponent... reason); + + /** + * Disconnects this end of the connection for the specified reason. If this + * is an {@link ProxiedPlayer} the respective server connection will be + * closed too. + * + * @param reason the reason shown to the player / sent to the server on + * disconnect + */ + void disconnect(BaseComponent reason); + /** * Get the unsafe methods of this class. * diff --git a/api/src/main/java/net/md_5/bungee/api/event/ServerKickEvent.java b/api/src/main/java/net/md_5/bungee/api/event/ServerKickEvent.java index 50a4cc93..2208b414 100644 --- a/api/src/main/java/net/md_5/bungee/api/event/ServerKickEvent.java +++ b/api/src/main/java/net/md_5/bungee/api/event/ServerKickEvent.java @@ -3,6 +3,8 @@ package net.md_5.bungee.api.event; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.config.ServerInfo; import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.plugin.Cancellable; @@ -28,7 +30,7 @@ public class ServerKickEvent extends Event implements Cancellable /** * Kick reason. */ - private String kickReason; + private BaseComponent[] kickReasonComponent; /** * Server to send player to if this event is cancelled. */ @@ -44,16 +46,26 @@ public class ServerKickEvent extends Event implements Cancellable CONNECTING, CONNECTED, UNKNOWN; } - public ServerKickEvent(ProxiedPlayer player, String kickReason, ServerInfo cancelServer) + public ServerKickEvent(ProxiedPlayer player, BaseComponent[] kickReasonComponent, ServerInfo cancelServer) { - this( player, kickReason, cancelServer, State.UNKNOWN ); + this( player, kickReasonComponent, cancelServer, State.UNKNOWN ); } - public ServerKickEvent(ProxiedPlayer player, String kickReason, ServerInfo cancelServer, State state) + public ServerKickEvent(ProxiedPlayer player, BaseComponent[] kickReasonComponent, ServerInfo cancelServer, State state) { this.player = player; - this.kickReason = kickReason; + this.kickReasonComponent = kickReasonComponent; this.cancelServer = cancelServer; this.state = state; } + + @Deprecated + public String getKickReason() { + return BaseComponent.toLegacyText( kickReasonComponent ); + } + + @Deprecated + public void setKickReason(String reason) { + kickReasonComponent = TextComponent.fromLegacyText( reason ); + } } diff --git a/proxy/src/main/java/net/md_5/bungee/BungeeCord.java b/proxy/src/main/java/net/md_5/bungee/BungeeCord.java index a1d3b127..37a754ee 100644 --- a/proxy/src/main/java/net/md_5/bungee/BungeeCord.java +++ b/proxy/src/main/java/net/md_5/bungee/BungeeCord.java @@ -1,6 +1,9 @@ package net.md_5.bungee; import com.google.common.io.ByteStreams; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.chat.ComponentSerializer; import net.md_5.bungee.log.BungeeLogger; import net.md_5.bungee.reconnect.YamlReconnectHandler; import net.md_5.bungee.scheduler.BungeeScheduler; @@ -134,6 +137,7 @@ public class BungeeCord extends ProxyServer getPluginManager().registerCommand( null, new CommandPerms() ); getPluginManager().registerCommand( null, new CommandSend() ); getPluginManager().registerCommand( null, new CommandFind() ); + getPluginManager().registerCommand( null, new CommandAlertRaw() ); registerChannel( "BungeeCord" ); } @@ -486,11 +490,21 @@ public class BungeeCord extends ProxyServer @Override public void broadcast(String message) { - getConsole().sendMessage( message ); - // TODO: Here too - for (String msg : ChatConverter.toJSONChat( message )) { - broadcast( new Chat( msg ) ); - } + broadcast( TextComponent.fromLegacyText( message ) ); + } + + @Override + public void broadcast(BaseComponent... message) + { + getConsole().sendMessage( BaseComponent.toLegacyText( message ) ); + broadcast( new Chat( ComponentSerializer.toString( message ) ) ); + } + + @Override + public void broadcast(BaseComponent message) + { + getConsole().sendMessage( message.toLegacyText() ); + broadcast( new Chat( ComponentSerializer.toString( message ) ) ); } public void addConnection(UserConnection con) diff --git a/proxy/src/main/java/net/md_5/bungee/ChatConverter.java b/proxy/src/main/java/net/md_5/bungee/ChatConverter.java deleted file mode 100644 index 83aa21cc..00000000 --- a/proxy/src/main/java/net/md_5/bungee/ChatConverter.java +++ /dev/null @@ -1,197 +0,0 @@ -package net.md_5.bungee; - -import com.google.gson.Gson; -import com.google.gson.annotations.SerializedName; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class ChatConverter { - - private static final Gson gson = new Gson(); - private static final char COLOR_CHAR = '\u00A7'; - private static final Pattern url = Pattern.compile("^(?:(https?)://)?([-\\w_\\.]{2,}\\.[a-z]{2,4})(/\\S*)?$"); - - public static String[] toJSONChat(String txt) { - Message msg = new Message(); - ArrayList parts = new ArrayList(); - StringBuilder outBuffer = new StringBuilder("["); - StringBuilder buf = new StringBuilder(); - Matcher matcher = url.matcher(txt); - for (int i = 0; i < txt.length(); i++) { - char c = txt.charAt(i); - if (c != COLOR_CHAR) { - int pos = txt.indexOf(' ', i); - if (pos == -1) pos = txt.length(); - if (matcher.region(i, pos).find()) { //Web link handling - msg.text = buf.toString(); - buf = new StringBuilder(); - outBuffer = append(parts, outBuffer, msg); - Message old = msg; - msg = new Message(old); - msg.clickEvent = new ClickEvent(); - msg.clickEvent.action = "open_url"; - String urlString = txt.substring(i, pos); - if (urlString.startsWith("http")) { - msg.text = msg.clickEvent.value = urlString; - } else { - msg.text = urlString; - msg.clickEvent.value = "http://" + urlString; - } - outBuffer = append(parts, outBuffer, msg); - i += pos - i - 1; - msg = new Message(old); - continue; - } - buf.append(c); - continue; - } - i++; - c = txt.charAt(i); - if (c >= 'A' && c <= 'Z') { - c += 32; - } - msg.text = buf.toString(); - buf = new StringBuilder(); - outBuffer = append(parts, outBuffer, msg); - msg = new Message(msg); - switch(c) { - case 'k': - msg.obfuscated = Boolean.TRUE; - break; - case 'l': - msg.bold = Boolean.TRUE; - break; - case 'm': - msg.strikethrough = Boolean.TRUE; - break; - case 'n': - msg.underlined = Boolean.TRUE; - break; - case 'o': - msg.italic = Boolean.TRUE; - break; - default: - msg.obfuscated = null; - msg.bold = null; - msg.strikethrough = null; - msg.underlined = null; - msg.italic = null; - if (c != 'r') { - msg.color = Color.fromCode(Character.toString(c)); - } else { - msg.color = Color.WHITE; - } - break; - } - } - msg.text = buf.toString(); - append(parts, outBuffer, msg); - - parts.add(outBuffer.append("]").toString()); - String[] pArray = new String[parts.size()]; - parts.toArray(pArray); - return pArray; - } - - private static StringBuilder append(ArrayList parts, StringBuilder outBuffer, Message part) { - String p = gson.toJson(part); - if (p.length() + outBuffer.length() + 1 >= Short.MAX_VALUE - 20) { - outBuffer.append("]"); - parts.add(outBuffer.toString()); - outBuffer = new StringBuilder("["); - } - if (outBuffer.length() != 1) { - outBuffer.append(","); - } - outBuffer.append(p); - return outBuffer; - } -} - -class Message { - public String text; - - public Boolean bold; - public Boolean italic; - public Boolean underlined; - public Boolean strikethrough; - public Boolean obfuscated; - - public Color color; - - public ClickEvent clickEvent; - - public Message() { - - } - - public Message(Message old) { - this.bold = old.bold; - this.italic = old.italic; - this.underlined = old.underlined; - this.strikethrough = old.strikethrough; - this.color = old.color; - } -} - -class ClickEvent { - public String action; - public String value; -} - -enum Color { - @SerializedName("black") - BLACK("0"), - @SerializedName("dark_blue") - DARK_BLUE("1"), - @SerializedName("dark_green") - DARK_GREEN("2"), - @SerializedName("dark_aqua") - DARK_AQUA("3"), - @SerializedName("dark_red") - DARK_RED("4"), - @SerializedName("dark_purple") - DARK_PURPLE("5"), - @SerializedName("gold") - GOLD("6"), - @SerializedName("gray") - GRAY("7"), - @SerializedName("dark_gray") - DARK_GRAY("8"), - @SerializedName("blue") - BLUE("9"), - @SerializedName("green") - GREEN("a"), - @SerializedName("aqua") - AQUA("b"), - @SerializedName("red") - RED("c"), - @SerializedName("light_purple") - LIGHT_PURPLE("d"), - @SerializedName("yellow") - YELLOW("e"), - @SerializedName("white") - WHITE("f"); - - public String code; - - Color(String code) { - this.code = code; - } - - - private static HashMap codeMap = new HashMap(); - - public static Color fromCode(String code) { - return codeMap.get(code); - } - - static { - for (Color color : values()) { - codeMap.put(color.code, color); - } - } -} diff --git a/proxy/src/main/java/net/md_5/bungee/ServerConnection.java b/proxy/src/main/java/net/md_5/bungee/ServerConnection.java index b5c5af50..b8e35b12 100644 --- a/proxy/src/main/java/net/md_5/bungee/ServerConnection.java +++ b/proxy/src/main/java/net/md_5/bungee/ServerConnection.java @@ -5,7 +5,10 @@ import java.util.concurrent.TimeUnit; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.connection.Server; +import net.md_5.bungee.chat.ComponentSerializer; import net.md_5.bungee.netty.ChannelWrapper; import net.md_5.bungee.protocol.DefinedPacket; import net.md_5.bungee.protocol.packet.PluginMessage; @@ -39,11 +42,17 @@ public class ServerConnection implements Server @Override public synchronized void disconnect(String reason) + { + disconnect( TextComponent.fromLegacyText( reason ) ); + } + + @Override + public void disconnect(BaseComponent... reason) { if ( !ch.isClosed() ) { // TODO: Can we just use a future here? - unsafe().sendPacket( new Kick( reason ) ); + unsafe().sendPacket( new Kick( ComponentSerializer.toString( reason ) ) ); ch.getHandle().eventLoop().schedule( new Runnable() { @Override @@ -53,6 +62,13 @@ public class ServerConnection implements Server } }, 100, TimeUnit.MILLISECONDS ); } + + } + + @Override + public void disconnect(BaseComponent reason) + { + disconnect( new BaseComponent[]{reason} ); } @Override diff --git a/proxy/src/main/java/net/md_5/bungee/ServerConnector.java b/proxy/src/main/java/net/md_5/bungee/ServerConnector.java index f3c3e93f..0d317c93 100644 --- a/proxy/src/main/java/net/md_5/bungee/ServerConnector.java +++ b/proxy/src/main/java/net/md_5/bungee/ServerConnector.java @@ -15,6 +15,7 @@ import net.md_5.bungee.api.event.ServerSwitchEvent; import net.md_5.bungee.api.score.Objective; import net.md_5.bungee.api.score.Scoreboard; import net.md_5.bungee.api.score.Team; +import net.md_5.bungee.chat.ComponentSerializer; import net.md_5.bungee.connection.CancelSendSignal; import net.md_5.bungee.connection.DownstreamBridge; import net.md_5.bungee.netty.HandlerBoss; @@ -211,7 +212,7 @@ public class ServerConnector extends PacketHandler { def = null; } - ServerKickEvent event = bungee.getPluginManager().callEvent( new ServerKickEvent( user, kick.getMessage(), def, ServerKickEvent.State.CONNECTING ) ); + ServerKickEvent event = bungee.getPluginManager().callEvent( new ServerKickEvent( user, ComponentSerializer.parse(kick.getMessage()), def, ServerKickEvent.State.CONNECTING ) ); if ( event.isCancelled() && event.getCancelServer() != null ) { user.connect( event.getCancelServer() ); diff --git a/proxy/src/main/java/net/md_5/bungee/UserConnection.java b/proxy/src/main/java/net/md_5/bungee/UserConnection.java index 1e4919b1..e51ec343 100644 --- a/proxy/src/main/java/net/md_5/bungee/UserConnection.java +++ b/proxy/src/main/java/net/md_5/bungee/UserConnection.java @@ -20,12 +20,15 @@ import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.Setter; import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.config.ServerInfo; import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.event.PermissionCheckEvent; import net.md_5.bungee.api.event.ServerConnectEvent; import net.md_5.bungee.api.score.Scoreboard; import net.md_5.bungee.api.tab.TabListHandler; +import net.md_5.bungee.chat.ComponentSerializer; import net.md_5.bungee.connection.InitialHandler; import net.md_5.bungee.netty.ChannelWrapper; import net.md_5.bungee.netty.HandlerBoss; @@ -256,15 +259,27 @@ public final class UserConnection implements ProxiedPlayer @Override public synchronized void disconnect(String reason) { - disconnect0( ChatConverter.toJSONChat( reason )[0] ); + disconnect0( TextComponent.fromLegacyText( reason ) ); } - public synchronized void disconnect0(String reason) + @Override + public void disconnect(BaseComponent... reason) + { + disconnect0( reason ); + } + + @Override + public void disconnect(BaseComponent reason) + { + disconnect0( reason ); + } + + public synchronized void disconnect0(BaseComponent ...reason) { if ( ch.getHandle().isActive() ) { - bungee.getLogger().log( Level.INFO, "[" + getName() + "] disconnected with: " + reason ); - unsafe().sendPacket( new Kick( reason ) ); + bungee.getLogger().log( Level.INFO, "[" + getName() + "] disconnected with: " + BaseComponent.toLegacyText( reason ) ); + unsafe().sendPacket( new Kick( ComponentSerializer.toString( reason ) ) ); ch.close(); if ( server != null ) { @@ -283,9 +298,7 @@ public final class UserConnection implements ProxiedPlayer @Override public void sendMessage(String message) { - for(String msg : ChatConverter.toJSONChat( message )) { - unsafe().sendPacket( new Chat( msg ) ); - } + sendMessage( TextComponent.fromLegacyText( message ) ); } @Override @@ -297,6 +310,18 @@ public final class UserConnection implements ProxiedPlayer } } + @Override + public void sendMessage(BaseComponent... message) + { + unsafe().sendPacket( new Chat( ComponentSerializer.toString( message ) ) ); + } + + @Override + public void sendMessage(BaseComponent message) + { + unsafe().sendPacket( new Chat( ComponentSerializer.toString( message ) ) ); + } + @Override public void sendData(String channel, byte[] data) { diff --git a/proxy/src/main/java/net/md_5/bungee/chat/BaseComponentSerializer.java b/proxy/src/main/java/net/md_5/bungee/chat/BaseComponentSerializer.java new file mode 100644 index 00000000..7128dab9 --- /dev/null +++ b/proxy/src/main/java/net/md_5/bungee/chat/BaseComponentSerializer.java @@ -0,0 +1,116 @@ +package net.md_5.bungee.chat; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.chat.HoverEvent; + +import java.util.Arrays; + +public class BaseComponentSerializer +{ + + protected void deserialize(JsonObject object, BaseComponent component, JsonDeserializationContext context) + { + if ( object.has( "color" ) ) + { + component.setColor( ChatColor.valueOf( object.get( "color" ).getAsString().toUpperCase() ) ); + } + if ( object.has( "bold" ) ) + { + component.setBold( object.get( "bold" ).getAsBoolean() ); + } + if ( object.has( "italic" ) ) + { + component.setItalic( object.get( "italic" ).getAsBoolean() ); + } + if ( object.has( "underlined" ) ) + { + component.setUnderlined( object.get( "underlined" ).getAsBoolean() ); + } + if ( object.has( "strikethrough" ) ) + { + component.setUnderlined( object.get( "strikethrough" ).getAsBoolean() ); + } + if ( object.has( "obfuscated" ) ) + { + component.setUnderlined( object.get( "obfuscated" ).getAsBoolean() ); + } + if ( object.has( "extra" ) ) + { + component.setExtra( Arrays.asList( context.deserialize( object.get( "extra" ), BaseComponent[].class ) ) ); + } + + //Events + if ( object.has( "clickEvent" ) ) + { + JsonObject event = object.getAsJsonObject( "clickEvent" ); + component.setClickEvent( new ClickEvent( + ClickEvent.Action.valueOf( event.get( "action" ).getAsString().toUpperCase() ), + event.get( "value" ).getAsString() ) ); + } + if ( object.has( "hoverEvent" ) ) + { + JsonObject event = object.getAsJsonObject( "hoverEvent" ); + BaseComponent[] res; + if (event.get("value").isJsonArray()) { + res = context.deserialize( event.get( "value" ), BaseComponent[].class ); + } else { + res = new BaseComponent[]{context.deserialize( event.get( "value" ), BaseComponent.class )}; + } + component.setHoverEvent( new HoverEvent( HoverEvent.Action.valueOf( event.get( "action" ).getAsString().toUpperCase() ), res ) ); + } + } + + protected void serialize(JsonObject object, BaseComponent component, JsonSerializationContext context) + { + if ( component.getColorRaw() != null ) + { + object.addProperty( "color", component.getColorRaw().getName() ); + } + if ( component.isBoldRaw() != null ) + { + object.addProperty( "bold", component.isBoldRaw() ); + } + if ( component.isItalicRaw() != null ) + { + object.addProperty( "italic", component.isItalicRaw() ); + } + if ( component.isUnderlinedRaw() != null ) + { + object.addProperty( "underlined", component.isUnderlinedRaw() ); + } + if ( component.isStrikethroughRaw() != null ) + { + object.addProperty( "strikethrough", component.isStrikethroughRaw() ); + } + if ( component.isObfuscatedRaw() != null ) + { + object.addProperty( "obfuscated", component.isObfuscatedRaw() ); + } + + if ( component.getExtra() != null ) + { + object.add( "extra", context.serialize( component.getExtra() ) ); + } + + //Events + if ( component.getClickEvent() != null ) + { + JsonObject clickEvent = new JsonObject(); + clickEvent.addProperty( "action", component.getClickEvent().getAction().toString().toLowerCase() ); + clickEvent.addProperty( "value", component.getClickEvent().getValue() ); + object.add( "clickEvent", clickEvent ); + } + if ( component.getHoverEvent() != null ) + { + JsonObject hoverEvent = new JsonObject(); + hoverEvent.addProperty( "action", component.getHoverEvent().getAction().toString().toLowerCase() ); + hoverEvent.add( "value", context.serialize( component.getHoverEvent().getValue() ) ); + object.add( "hoverEvent", hoverEvent ); + } + } +} diff --git a/proxy/src/main/java/net/md_5/bungee/chat/ComponentSerializer.java b/proxy/src/main/java/net/md_5/bungee/chat/ComponentSerializer.java new file mode 100644 index 00000000..c019f7ac --- /dev/null +++ b/proxy/src/main/java/net/md_5/bungee/chat/ComponentSerializer.java @@ -0,0 +1,66 @@ +package net.md_5.bungee.chat; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +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 net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.chat.TranslatableComponent; + +import java.lang.reflect.Type; + +public class ComponentSerializer implements JsonSerializer, JsonDeserializer +{ + + private final static Gson gson = new GsonBuilder(). + registerTypeAdapter( BaseComponent.class, new ComponentSerializer() ). + registerTypeAdapter( TextComponent.class, new TextComponentSerializer() ). + registerTypeAdapter( TranslatableComponent.class, new TranslatableComponentSerializer() ). + create(); + + public static BaseComponent[] parse(String json) + { + if ( json.startsWith( "[" ) ) + { //Array + return gson.fromJson( json, BaseComponent[].class ); + } + return new BaseComponent[]{gson.fromJson( json, BaseComponent.class )}; + } + + public static String toString(BaseComponent component) + { + return gson.toJson( component ); + } + + public static String toString(BaseComponent... components) + { + return gson.toJson( components ); + } + + @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 ); + } + return context.deserialize( json, TextComponent.class ); + } + + @Override + public JsonElement serialize(BaseComponent src, Type typeOfSrc, JsonSerializationContext context) + { + return context.serialize( src, src.getClass() ); + } +} diff --git a/proxy/src/main/java/net/md_5/bungee/chat/TextComponentSerializer.java b/proxy/src/main/java/net/md_5/bungee/chat/TextComponentSerializer.java new file mode 100644 index 00000000..031590d6 --- /dev/null +++ b/proxy/src/main/java/net/md_5/bungee/chat/TextComponentSerializer.java @@ -0,0 +1,39 @@ +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.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import net.md_5.bungee.api.chat.TextComponent; + +import java.lang.reflect.Type; + +public class TextComponentSerializer extends BaseComponentSerializer implements JsonSerializer, JsonDeserializer +{ + @Override + public TextComponent deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException + { + TextComponent component = new TextComponent(); + JsonObject object = json.getAsJsonObject(); + deserialize( object, component, context ); + component.setText( object.get( "text" ).getAsString() ); + return component; + } + + @Override + public JsonElement serialize(TextComponent src, Type typeOfSrc, JsonSerializationContext context) + { + if ( !src.hasFormatting() ) + { + return new JsonPrimitive( src.getText() ); + } + JsonObject object = new JsonObject(); + serialize( object, src, context ); + object.addProperty( "text", src.getText() ); + return object; + } +} diff --git a/proxy/src/main/java/net/md_5/bungee/chat/TranslatableComponentSerializer.java b/proxy/src/main/java/net/md_5/bungee/chat/TranslatableComponentSerializer.java new file mode 100644 index 00000000..f48954a1 --- /dev/null +++ b/proxy/src/main/java/net/md_5/bungee/chat/TranslatableComponentSerializer.java @@ -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 net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.TranslatableComponent; + +import java.lang.reflect.Type; +import java.util.Arrays; + +public class TranslatableComponentSerializer extends BaseComponentSerializer implements JsonSerializer, JsonDeserializer +{ + @Override + public TranslatableComponent deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException + { + TranslatableComponent component = new TranslatableComponent(); + JsonObject object = json.getAsJsonObject(); + deserialize( object, component, context ); + component.setTranslate( object.get( "translate" ).getAsString() ); + if ( object.has( "with" ) ) + { + component.setWith( Arrays.asList( (BaseComponent[]) context.deserialize( object.get( "with" ), BaseComponent[].class ) ) ); + } + 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() ) ); + } + return object; + } +} diff --git a/proxy/src/main/java/net/md_5/bungee/command/CommandAlertRaw.java b/proxy/src/main/java/net/md_5/bungee/command/CommandAlertRaw.java new file mode 100644 index 00000000..39f12ba1 --- /dev/null +++ b/proxy/src/main/java/net/md_5/bungee/command/CommandAlertRaw.java @@ -0,0 +1,47 @@ +package net.md_5.bungee.command; + +import com.google.common.base.Joiner; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.chat.ComponentBuilder; +import net.md_5.bungee.api.chat.HoverEvent; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.plugin.Command; +import net.md_5.bungee.chat.ComponentSerializer; + +import java.util.Arrays; + +public class CommandAlertRaw extends Command +{ + + public CommandAlertRaw() + { + super( "alertraw", "bungeecord.command.alert" ); + } + + @Override + public void execute(CommandSender sender, String[] args) + { + if ( args.length == 0 ) + { + sender.sendMessage( ChatColor.RED + "You must supply a message." ); + } else + { + String message = Joiner.on(' ').join( args ); + + try + { + ProxyServer.getInstance().broadcast( ComponentSerializer.parse( message ) ); + } catch ( Exception e ) + { + sender.sendMessage( + new ComponentBuilder( "An error occured while parsing your message. (Hover for details)" ). + color( ChatColor.RED ).underlined( true ). + event( new HoverEvent( HoverEvent.Action.SHOW_TEXT, + new ComponentBuilder( e.getMessage() ).color( ChatColor.RED ).create() ) ). + create() ); + } + } + } +} diff --git a/proxy/src/main/java/net/md_5/bungee/command/ConsoleCommandSender.java b/proxy/src/main/java/net/md_5/bungee/command/ConsoleCommandSender.java index 65a2505a..d6e8b79f 100644 --- a/proxy/src/main/java/net/md_5/bungee/command/ConsoleCommandSender.java +++ b/proxy/src/main/java/net/md_5/bungee/command/ConsoleCommandSender.java @@ -5,6 +5,7 @@ import java.util.Collections; import lombok.Getter; import net.md_5.bungee.api.CommandSender; import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.chat.BaseComponent; /** * Command sender representing the proxy console. @@ -34,6 +35,18 @@ public class ConsoleCommandSender implements CommandSender } } + @Override + public void sendMessage(BaseComponent... message) + { + sendMessage( BaseComponent.toLegacyText( message ) ); + } + + @Override + public void sendMessage(BaseComponent message) + { + sendMessage( message.toLegacyText() ); + } + @Override public String getName() { diff --git a/proxy/src/main/java/net/md_5/bungee/connection/DownstreamBridge.java b/proxy/src/main/java/net/md_5/bungee/connection/DownstreamBridge.java index cf0ff3bb..0e9ce932 100644 --- a/proxy/src/main/java/net/md_5/bungee/connection/DownstreamBridge.java +++ b/proxy/src/main/java/net/md_5/bungee/connection/DownstreamBridge.java @@ -20,6 +20,7 @@ import net.md_5.bungee.api.score.Position; import net.md_5.bungee.api.score.Score; import net.md_5.bungee.api.score.Scoreboard; import net.md_5.bungee.api.score.Team; +import net.md_5.bungee.chat.ComponentSerializer; import net.md_5.bungee.netty.ChannelWrapper; import net.md_5.bungee.netty.PacketHandler; import net.md_5.bungee.protocol.PacketWrapper; @@ -355,13 +356,13 @@ public class DownstreamBridge extends PacketHandler { def = null; } - ServerKickEvent event = bungee.getPluginManager().callEvent( new ServerKickEvent( con, kick.getMessage(), def, ServerKickEvent.State.CONNECTED ) ); + ServerKickEvent event = bungee.getPluginManager().callEvent( new ServerKickEvent( con, ComponentSerializer.parse(kick.getMessage()), def, ServerKickEvent.State.CONNECTED ) ); if ( event.isCancelled() && event.getCancelServer() != null ) { con.connectNow( event.getCancelServer() ); } else { - con.disconnect0( event.getKickReason() ); // TODO: Json concat util method // TODO: Prefix our own stuff. + con.disconnect0( event.getKickReasonComponent() ); // TODO: Prefix our own stuff. } server.setObsolete( true ); throw new CancelSendSignal(); diff --git a/proxy/src/main/java/net/md_5/bungee/connection/InitialHandler.java b/proxy/src/main/java/net/md_5/bungee/connection/InitialHandler.java index 05ab8e98..9d33c05e 100644 --- a/proxy/src/main/java/net/md_5/bungee/connection/InitialHandler.java +++ b/proxy/src/main/java/net/md_5/bungee/connection/InitialHandler.java @@ -16,6 +16,8 @@ import net.md_5.bungee.api.Callback; import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.ServerPing; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.config.ListenerInfo; import net.md_5.bungee.api.config.ServerInfo; import net.md_5.bungee.api.connection.PendingConnection; @@ -23,6 +25,7 @@ import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.event.LoginEvent; import net.md_5.bungee.api.event.PostLoginEvent; import net.md_5.bungee.api.event.ProxyPingEvent; +import net.md_5.bungee.chat.ComponentSerializer; import net.md_5.bungee.http.HttpClient; import net.md_5.bungee.netty.HandlerBoss; import net.md_5.bungee.netty.ChannelWrapper; @@ -395,13 +398,29 @@ public class InitialHandler extends PacketHandler implements PendingConnection @Override public synchronized void disconnect(String reason) { - if ( !ch.isClosed() ) + if (!ch.isClosed()) { - unsafe().sendPacket( new Kick( ChatConverter.toJSONChat( reason )[0] ) ); + unsafe().sendPacket( new Kick( ComponentSerializer.toString( TextComponent.fromLegacyText( reason ) ) ) ); ch.close(); } } + @Override + public void disconnect(BaseComponent... reason) + { + if ( !ch.isClosed() ) + { + unsafe().sendPacket( new Kick( ComponentSerializer.toString( reason ) ) ); + ch.close(); + } + } + + @Override + public void disconnect(BaseComponent reason) + { + disconnect( new BaseComponent[]{reason} ); + } + @Override public String getName() { diff --git a/proxy/src/main/resources/mojang-translations/en_US.properties b/proxy/src/main/resources/mojang-translations/en_US.properties new file mode 100644 index 00000000..4feb71f7 --- /dev/null +++ b/proxy/src/main/resources/mojang-translations/en_US.properties @@ -0,0 +1,1685 @@ + +language.name=English +language.region=US +language.code=en_US + +gui.done=Done +gui.cancel=Cancel +gui.back=Back +gui.toMenu=Back to title screen +gui.up=Up +gui.down=Down +gui.yes=Yes +gui.no=No + +translation.test.none=Hello, world! +translation.test.complex=Prefix, %s%2$s again %s and %1$s lastly %s and also %1$s again! +translation.test.escape=%%s %%%s %%%%s %%%%%s +translation.test.invalid=hi % +translation.test.invalid2=hi % s +translation.test.args=%s %s +translation.test.world=world + +menu.singleplayer=Singleplayer +menu.multiplayer=Multiplayer +menu.online=Minecraft Realms +menu.options=Options... +menu.quit=Quit Game +menu.returnToMenu=Save and Quit to Title +menu.disconnect=Disconnect +menu.returnToGame=Back to Game +menu.switchingLevel=Switching worlds +menu.generatingLevel=Generating world +menu.loadingLevel=Loading world +menu.generatingTerrain=Building terrain +menu.convertingLevel=Converting world +menu.simulating=Simulating the world for a bit +menu.respawning=Respawning +menu.shareToLan=Open to LAN + +selectWorld.title=Select World +selectWorld.empty=empty +selectWorld.world=World +selectWorld.select=Play Selected World +selectWorld.create=Create New World +selectWorld.recreate=Re-Create +selectWorld.createDemo=Play New Demo World +selectWorld.delete=Delete +selectWorld.rename=Rename +selectWorld.deleteQuestion=Are you sure you want to delete this world? +selectWorld.deleteWarning=will be lost forever! (A long time!) +selectWorld.deleteButton=Delete +selectWorld.renameButton=Rename +selectWorld.renameTitle=Rename World +selectWorld.conversion=Must be converted! +selectWorld.newWorld=New World +selectWorld.newWorld.copyOf=Copy of %s +selectWorld.enterName=World Name +selectWorld.resultFolder=Will be saved in: +selectWorld.enterSeed=Seed for the World Generator +selectWorld.seedInfo=Leave blank for a random seed +selectWorld.cheats=Cheats +selectWorld.customizeType=Customize + +createWorld.customize.presets=Presets +createWorld.customize.presets.title=Select a Preset +createWorld.customize.presets.select=Use Preset +createWorld.customize.presets.share=Want to share your preset with someone? Use the below box! +createWorld.customize.presets.list=Alternatively, here's some we made earlier! +createWorld.customize.flat.title=Superflat Customization +createWorld.customize.flat.tile=Layer Material +createWorld.customize.flat.height=Height +createWorld.customize.flat.addLayer=Add Layer +createWorld.customize.flat.editLayer=Edit Layer +createWorld.customize.flat.removeLayer=Remove Layer +createWorld.customize.flat.layer.top=Top - %d +createWorld.customize.flat.layer=%d +createWorld.customize.flat.layer.bottom=Bottom - %d + +gameMode.survival=Survival Mode +gameMode.creative=Creative Mode +gameMode.adventure=Adventure Mode +gameMode.hardcore=Hardcore Mode! +gameMode.changed=Your game mode has been updated + +selectWorld.gameMode=Game Mode +selectWorld.gameMode.survival=Survival +selectWorld.gameMode.survival.line1=Search for resources, crafting, gain +selectWorld.gameMode.survival.line2=levels, health and hunger +selectWorld.gameMode.creative=Creative +selectWorld.gameMode.creative.line1=Unlimited resources, free flying and +selectWorld.gameMode.creative.line2=destroy blocks instantly +selectWorld.gameMode.hardcore=Hardcore +selectWorld.gameMode.hardcore.line1=Same as survival mode, locked at hardest +selectWorld.gameMode.hardcore.line2=difficulty, and one life only +selectWorld.gameMode.adventure=Adventure +selectWorld.gameMode.adventure.line1=Same as survival mode, but blocks can't +selectWorld.gameMode.adventure.line2=be added or removed +selectWorld.moreWorldOptions=More World Options... +selectWorld.mapFeatures=Generate Structures: +selectWorld.mapFeatures.info=Villages, dungeons etc +selectWorld.mapType=World Type: +selectWorld.mapType.normal=Normal +selectWorld.allowCommands=Allow Cheats: +selectWorld.allowCommands.info=Commands like /gamemode, /xp +selectWorld.hardcoreMode=Hardcore: +selectWorld.hardcoreMode.info=World is deleted upon death +selectWorld.bonusItems=Bonus Chest: + +generator.default=Default +generator.flat=Superflat +generator.largeBiomes=Large Biomes +generator.amplified=AMPLIFIED + +generator.amplified.info=Notice: Just for fun, requires beefy computer + +selectServer.title=Select Server +selectServer.empty=empty +selectServer.select=Join Server +selectServer.direct=Direct Connect +selectServer.edit=Edit +selectServer.delete=Delete +selectServer.add=Add server +selectServer.defaultName=Minecraft Server +selectServer.deleteQuestion=Are you sure you want to remove this server? +selectServer.deleteWarning=will be lost forever! (A long time!) +selectServer.deleteButton=Delete +selectServer.refresh=Refresh +selectServer.hiddenAddress=(Hidden) +addServer.title=Edit Server Info +addServer.enterName=Server Name +addServer.enterIp=Server Address +addServer.add=Done +addServer.hideAddress=Hide Address +lanServer.title=LAN World +lanServer.scanning=Scanning for games on your local network +lanServer.start=Start LAN World +lanServer.otherPlayers=Settings for Other Players +mcoServer.title=Minecraft Online World + +multiplayer.title=Play Multiplayer +multiplayer.connect=Connect +multiplayer.info1=Minecraft Multiplayer is currently not finished, but there +multiplayer.info2=is some buggy early testing going on. +multiplayer.ipinfo=Enter the IP of a server to connect to it: +multiplayer.texturePrompt.line1=This server recommends the use of a custom resource pack. +multiplayer.texturePrompt.line2=Would you like to download and install it automagically? +multiplayer.downloadingTerrain=Downloading terrain +multiplayer.downloadingStats=Downloading statistics & achievements... +multiplayer.stopSleeping=Leave Bed +multiplayer.player.joined=%s joined the game +multiplayer.player.left=%s left the game + +chat.cannotSend=Cannot send chat message +chat.type.text=<%s> %s +chat.type.emote=* %s %s +chat.type.announcement=[%s] %s +chat.type.admin=[%s: %s] +chat.type.achievement=%s has just earned the achievement %s +chat.link.confirm=Are you sure you want to open the following website? +chat.link.warning=Never open links from people that you don't trust! +chat.copy=Copy to Clipboard +chat.link.confirmTrusted=Do you want to open this link or copy it to your clipboard? +chat.link.open=Open in browser + +$o=Play Demo World +menu.resetdemo=Reset Demo World + +demo.day.1=This demo will last five game days, do your best! +demo.day.2=Day Two +demo.day.3=Day Three +demo.day.4=Day Four +demo.day.5=This is your last day! +demo.day.warning=Your time is almost up! +demo.day.6=You have passed your fifth day, use F2 to save a screenshot of your creation +demo.reminder=The demo time has expired, buy the game to continue or start a new world! +demo.remainingTime=Remaining time: %s +demo.demoExpired=Demo time's up! +demo.help.movement=Use %1$s, %2$s, %3$s, %4$s and the mouse to move around +demo.help.movementShort=Move by pressing %1$s, %2$s, %3$s, %4$s +demo.help.movementMouse=Look around using the mouse +demo.help.jump=Jump by pressing %1$s +demo.help.inventory=Use %1$s to open your inventory +demo.help.title=Minecraft Demo Mode +demo.help.fullWrapped=This demo will last 5 ingame days (about 1 hour and 40 minutes of real time). Check the achievements for hints! Have fun! +demo.help.buy=Purchase Now! +demo.help.later=Continue Playing! + +connect.connecting=Connecting to the server... +connect.authorizing=Logging in... +connect.failed=Failed to connect to the server + +disconnect.genericReason=%s +disconnect.disconnected=Disconnected by Server +disconnect.lost=Connection Lost +disconnect.kicked=Was kicked from the game +disconnect.timeout=Timed out +disconnect.closed=Connection closed +disconnect.loginFailed=Failed to login +disconnect.loginFailedInfo=Failed to login: %s +disconnect.loginFailedInfo.serversUnavailable=The authentication are currently down for maintenance. +disconnect.loginFailedInfo.invalidSession=Invalid session (Try restarting your game) +disconnect.quitting=Quitting +disconnect.endOfStream=End of stream +disconnect.overflow=Buffer overflow +disconnect.spam=Kicked for spamming + +soundCategory.master=Master Volume +soundCategory.music=Music +soundCategory.record=Jukebox/Noteblocks +soundCategory.weather=Weather +soundCategory.hostile=Hostile Creatures +soundCategory.neutral=Friendly Creatures +soundCategory.player=Players +soundCategory.block=Blocks +soundCategory.ambient=Ambient/Environment + +options.off=OFF +options.on=ON +options.visible=Shown +options.hidden=Hidden +options.title=Options +options.controls=Controls... +options.video=Video Settings... +options.language=Language... +options.sounds=Music & Sounds... +options.sounds.title=Music & Sound Options +options.languageWarning=Language translations may not be 100%% accurate +options.videoTitle=Video Settings +options.music=Music +options.sound=Sound +options.invertMouse=Invert Mouse +options.fov=FOV +options.fov.min=Normal +options.fov.max=Quake Pro +options.saturation=Saturation +options.gamma=Brightness +options.gamma.min=Moody +options.gamma.max=Bright +options.sensitivity=Sensitivity +options.sensitivity.min=*yawn* +options.sensitivity.max=HYPERSPEED!!! +options.renderDistance=Render Distance +options.renderDistance.tiny=Tiny +options.renderDistance.short=Short +options.renderDistance.normal=Normal +options.renderDistance.far=Far +options.viewBobbing=View Bobbing +options.ao=Smooth Lighting +options.ao.off=OFF +options.ao.min=Minimum +options.ao.max=Maximum +options.anaglyph=3D Anaglyph +options.framerateLimit=Max Framerate +options.framerateLimit.max=Unlimited +options.difficulty=Difficulty +options.difficulty.peaceful=Peaceful +options.difficulty.easy=Easy +options.difficulty.normal=Normal +options.difficulty.hard=Hard +options.difficulty.hardcore=Hardcore +options.graphics=Graphics +options.graphics.fancy=Fancy +options.graphics.fast=Fast +options.guiScale=GUI Scale +options.guiScale.auto=Auto +options.guiScale.small=Small +options.guiScale.normal=Normal +options.guiScale.large=Large +options.advancedOpengl=Advanced OpenGL +options.fboEnable=Enable FBOs +options.postProcessEnable=Enable Post-Processing +options.aoDesc0=Enable faux ambient occlusion on blocks. +options.aoDesc1= +options.framerateLimitDesc0=Selects the maximum frame rate: +options.framerateLimitDesc1=35fps, 120fps, or 200+fps. +options.viewBobbingDesc0=Enables view-bob when moving. +options.viewBobbingDesc1= +options.renderCloudsDesc0=Enables the rendering of clouds. +options.renderCloudsDesc1= +options.graphicsDesc0='Fancy': Enables extra transparency. +options.graphicsDesc1='Fast': Suggested for lower-end hardware. +options.renderDistanceDesc0=Maximum render distance. Smaller values +options.renderDistanceDesc1=run better on lower-end hardware. +options.particlesDesc0=Selects the overall amount of particles. +options.particlesDesc1=On lower-end hardware, less is better. +options.advancedOpenglDesc0=Enables occlusion queries. On AMD and Intel +options.advancedOpenglDesc1=hardware, this may decrease performance. +options.fboEnableDesc0=Enables the use of Framebuffer Objects. +options.fboEnableDesc1=Necessary for certain Minecraft features. +options.postProcessEnableDesc0=Enables post-processing. Disabling will +options.postProcessEnableDesc1=result in reduction in Awesome Levels. +options.renderClouds=Clouds +options.qualityButton=Video Quality Settings... +options.qualityVideoTitle=Video Quality Settings +options.performanceButton=Video Performance Settings... +options.performanceVideoTitle=Video Performance Settings +options.advancedButton=Advanced Video Settings... +options.advancedVideoTitle=Advanced Video Settings +options.postButton=Post-Processing Settings... +options.postVideoTitle=Post-Processing Settings +options.farWarning1=A 64 bit Java installation is recommended +options.farWarning2=for 'Far' render distance (you have 32 bit) +options.particles=Particles +options.particles.all=All +options.particles.decreased=Decreased +options.particles.minimal=Minimal +options.multiplayer.title=Multiplayer Settings... +options.chat.title=Chat Settings... +options.chat.visibility=Chat +options.chat.visibility.full=Shown +options.chat.visibility.system=Commands Only +options.chat.visibility.hidden=Hidden +options.chat.color=Colors +options.chat.opacity=Opacity +options.chat.links=Web Links +options.chat.links.prompt=Prompt on Links +options.chat.scale=Scale +options.chat.width=Width +options.chat.height.focused=Focused Height +options.chat.height.unfocused=Unfocused Height +options.showCape=Show Cape +options.serverTextures=Server Textures +options.snooper=Allow Snooper +options.snooper.view=Snooper Settings... +options.snooper.title=Machine Specs Collection +options.snooper.desc=We want to collect information about your machine to help improve Minecraft by knowing what we can support and where the biggest problems are. All of this information is completely anonymous and viewable below. We promise we won't do anything bad with this data, but if you want to opt out then feel free to toggle it off! +options.resourcepack=Resource Packs... +options.fullscreen=Fullscreen +options.vsync=Use VSync +options.touchscreen=Touchscreen Mode +options.mipmapLevels=Mipmap Levels +options.anisotropicFiltering=Anisotropic Filtering +options.forceUnicodeFont=Force Unicode Font + +controls.title=Controls +controls.reset=Reset +controls.resetAll=Reset Keys + +key.sprint=Sprint +key.forward=Walk Forwards +key.left=Strafe Left +key.back=Walk Backwards +key.right=Strafe Right +key.jump=Jump +key.inventory=Inventory +key.drop=Drop Item +key.chat=Open Chat +key.sneak=Sneak +key.playerlist=List Players +key.attack=Attack/Destroy +key.use=Use Item/Place Block +key.pickItem=Pick Block +key.mouseButton=Button %1$s +key.command=Open Command +key.screenshot=Take Screenshot +key.togglePerspective=Toggle Perspective +key.smoothCamera=Toggle Cinematic Camera +key.hotbar.1=Hotbar Slot 1 +key.hotbar.2=Hotbar Slot 2 +key.hotbar.3=Hotbar Slot 3 +key.hotbar.4=Hotbar Slot 4 +key.hotbar.5=Hotbar Slot 5 +key.hotbar.6=Hotbar Slot 6 +key.hotbar.7=Hotbar Slot 7 +key.hotbar.8=Hotbar Slot 8 +key.hotbar.9=Hotbar Slot 9 + +key.categories.movement=Movement +key.categories.misc=Miscellaneous +key.categories.multiplayer=Multiplayer +key.categories.gameplay=Gameplay +key.categories.ui=Game Interface +key.categories.inventory=Inventory + +resourcePack.openFolder=Open resource pack folder +resourcePack.title=Select Resource Packs +resourcePack.available.title=Available Resource Packs +resourcePack.selected.title=Selected Resource Packs +resourcePack.folderInfo=(Place resource pack files here) + +book.pageIndicator=Page %1$s of %2$s +book.byAuthor=by %1$s +book.signButton=Sign +book.editTitle=Enter Book Title: +book.finalizeButton=Sign and Close +book.finalizeWarning=Note! When you sign the book, it will no longer be editable. + +tile.stone.name=Stone +tile.hayBlock.name=Hay Bale +tile.grass.name=Grass Block +tile.dirt.default.name=Dirt +tile.dirt.podzol.name=Podzol +tile.stonebrick.name=Cobblestone +tile.wood.name=Wooden Planks +tile.wood.oak.name=Oak Wood Planks +tile.wood.spruce.name=Spruce Wood Planks +tile.wood.birch.name=Birch Wood Planks +tile.wood.jungle.name=Jungle Wood Planks +tile.wood.acacia.name=Acacia Wood Planks +tile.wood.big_oak.name=Dark Oak Wood Planks +tile.sapling.oak.name=Oak Sapling +tile.sapling.spruce.name=Spruce Sapling +tile.sapling.birch.name=Birch Sapling +tile.sapling.jungle.name=Jungle Sapling +tile.sapling.acacia.name=Acacia Sapling +tile.sapling.roofed_oak.name=Dark Oak Sapling +tile.deadbush.name=Dead Bush +tile.bedrock.name=Bedrock +tile.water.name=Water +tile.lava.name=Lava +tile.sand.default.name=Sand +tile.sand.red.name=Red Sand +tile.sandStone.name=Sandstone +tile.sandStone.default.name=Sandstone +tile.sandStone.chiseled.name=Chiseled Sandstone +tile.sandStone.smooth.name=Smooth Sandstone +tile.gravel.name=Gravel +tile.oreGold.name=Gold Ore +tile.oreIron.name=Iron Ore +tile.oreCoal.name=Coal Ore +tile.log.name=Wood +tile.log.oak.name=Oak Wood +tile.log.spruce.name=Spruce Wood +tile.log.birch.name=Birch Wood +tile.log.jungle.name=Jungle Wood +tile.log.acacia.name=Acacia Wood +tile.log.big_oak.name=Dark Oak Wood +tile.leaves.name=Leaves +tile.leaves.oak.name=Oak Leaves +tile.leaves.spruce.name=Spruce Leaves +tile.leaves.birch.name=Birch Leaves +tile.leaves.jungle.name=Jungle Leaves +tile.leaves.acacia.name=Acacia Leaves +tile.leaves.big_oak.name=Dark Oak Leaves +tile.tallgrass.name=Grass +tile.tallgrass.shrub.name=Shrub +tile.tallgrass.grass.name=Grass +tile.tallgrass.fern.name=Fern +tile.sponge.name=Sponge +tile.glass.name=Glass +tile.stainedGlass.name=Stained Glass +tile.stainedGlass.black.name=Black Stained Glass +tile.stainedGlass.red.name=Red Stained Glass +tile.stainedGlass.green.name=Green Stained Glass +tile.stainedGlass.brown.name=Brown Stained Glass +tile.stainedGlass.blue.name=Blue Stained Glass +tile.stainedGlass.purple.name=Purple Stained Glass +tile.stainedGlass.cyan.name=Cyan Stained Glass +tile.stainedGlass.silver.name=Light Gray Stained Glass +tile.stainedGlass.gray.name=Gray Stained Glass +tile.stainedGlass.pink.name=Pink Stained Glass +tile.stainedGlass.lime.name=Lime Stained Glass +tile.stainedGlass.yellow.name=Yellow Stained Glass +tile.stainedGlass.lightBlue.name=Light Blue Stained Glass +tile.stainedGlass.magenta.name=Magenta Stained Glass +tile.stainedGlass.orange.name=Orange Stained Glass +tile.stainedGlass.white.name=White Stained Glass +tile.thinStainedGlass.name=Stained Glass Pane +tile.thinStainedGlass.black.name=Black Stained Glass Pane +tile.thinStainedGlass.red.name=Red Stained Glass Pane +tile.thinStainedGlass.green.name=Green Stained Glass Pane +tile.thinStainedGlass.brown.name=Brown Stained Glass Pane +tile.thinStainedGlass.blue.name=Blue Stained Glass Pane +tile.thinStainedGlass.purple.name=Purple Stained Glass Pane +tile.thinStainedGlass.cyan.name=Cyan Stained Glass Pane +tile.thinStainedGlass.silver.name=Light Gray Stained Glass Pane +tile.thinStainedGlass.gray.name=Gray Stained Glass Pane +tile.thinStainedGlass.pink.name=Pink Stained Glass Pane +tile.thinStainedGlass.lime.name=Lime Stained Glass Pane +tile.thinStainedGlass.yellow.name=Yellow Stained Glass Pane +tile.thinStainedGlass.lightBlue.name=Light Blue Stained Glass Pane +tile.thinStainedGlass.magenta.name=Magenta Stained Glass Pane +tile.thinStainedGlass.orange.name=Orange Stained Glass Pane +tile.thinStainedGlass.white.name=White Stained Glass Pane +tile.thinGlass.name=Glass Pane +tile.cloth.name=Wool +tile.flower1.dandelion.name=Dandelion +tile.flower2.poppy.name=Poppy +tile.flower2.blueOrchid.name=Blue Orchid +tile.flower2.allium.name=Allium +tile.flower2.houstonia.name=Azure Bluet +tile.flower2.tulipRed.name=Red Tulip +tile.flower2.tulipOrange.name=Orange Tulip +tile.flower2.tulipWhite.name=White Tulip +tile.flower2.tulipPink.name=Pink Tulip +tile.flower2.oxeyeDaisy.name=Oxeye Daisy +tile.doublePlant.sunflower.name=Sunflower +tile.doublePlant.syringa.name=Lilac +tile.doublePlant.grass.name=Double Tallgrass +tile.doublePlant.fern.name=Large Fern +tile.doublePlant.rose.name=Rose Bush +tile.doublePlant.paeonia.name=Peony +tile.mushroom.name=Mushroom +tile.blockGold.name=Block of Gold +tile.blockIron.name=Block of Iron +tile.stoneSlab.stone.name=Stone Slab +tile.stoneSlab.sand.name=Sandstone Slab +tile.stoneSlab.wood.name=Wooden Slab +tile.stoneSlab.cobble.name=Cobblestone Slab +tile.stoneSlab.brick.name=Bricks Slab +tile.stoneSlab.smoothStoneBrick.name=Stone Bricks Slab +tile.stoneSlab.netherBrick.name=Nether Brick Slab +tile.stoneSlab.quartz.name=Quartz Slab +tile.woodSlab.oak.name=Oak Wood Slab +tile.woodSlab.spruce.name=Spruce Wood Slab +tile.woodSlab.birch.name=Birch Wood Slab +tile.woodSlab.jungle.name=Jungle Wood Slab +tile.woodSlab.acacia.name=Acacia Wood Slab +tile.woodSlab.big_oak.name=Dark Oak Wood Slab +tile.brick.name=Bricks +tile.tnt.name=TNT +tile.bookshelf.name=Bookshelf +tile.stoneMoss.name=Moss Stone +tile.obsidian.name=Obsidian +tile.torch.name=Torch +tile.fire.name=Fire +tile.mobSpawner.name=Monster Spawner +tile.stairsWood.name=Oak Wood Stairs +tile.stairsWoodSpruce.name=Spruce Wood Stairs +tile.stairsWoodBirch.name=Birch Wood Stairs +tile.stairsWoodJungle.name=Jungle Wood Stairs +tile.stairsWoodAcacia.name=Acacia Wood Stairs +tile.stairsWoodDarkOak.name=Dark Oak Wood Stairs +tile.chest.name=Chest +tile.chestTrap.name=Trapped Chest +tile.redstoneDust.name=Redstone Dust +tile.oreDiamond.name=Diamond Ore +tile.blockCoal.name=Block of Coal +tile.blockDiamond.name=Block of Diamond +tile.workbench.name=Crafting Table +tile.crops.name=Crops +tile.farmland.name=Farmland +tile.furnace.name=Furnace +tile.sign.name=Sign +tile.doorWood.name=Wooden Door +tile.ladder.name=Ladder +tile.rail.name=Rail +tile.goldenRail.name=Powered Rail +tile.activatorRail.name=Activator Rail +tile.detectorRail.name=Detector Rail +tile.stairsStone.name=Stone Stairs +tile.stairsSandStone.name=Sandstone Stairs +tile.lever.name=Lever +tile.pressurePlate.name=Pressure Plate +tile.weightedPlate_light.name=Weighted Pressure Plate (Light) +tile.weightedPlate_heavy.name=Weighted Pressure Plate (Heavy) +tile.doorIron.name=Iron Door +tile.oreRedstone.name=Redstone Ore +tile.notGate.name=Redstone Torch +tile.button.name=Button +tile.snow.name=Snow +tile.woolCarpet.black.name=Black Carpet +tile.woolCarpet.red.name=Red Carpet +tile.woolCarpet.green.name=Green Carpet +tile.woolCarpet.brown.name=Brown Carpet +tile.woolCarpet.blue.name=Blue Carpet +tile.woolCarpet.purple.name=Purple Carpet +tile.woolCarpet.cyan.name=Cyan Carpet +tile.woolCarpet.silver.name=Light Gray Carpet +tile.woolCarpet.gray.name=Gray Carpet +tile.woolCarpet.pink.name=Pink Carpet +tile.woolCarpet.lime.name=Lime Carpet +tile.woolCarpet.yellow.name=Yellow Carpet +tile.woolCarpet.lightBlue.name=Light Blue Carpet +tile.woolCarpet.magenta.name=Magenta Carpet +tile.woolCarpet.orange.name=Orange Carpet +tile.woolCarpet.white.name=Carpet +tile.ice.name=Ice +tile.icePacked.name=Packed Ice +tile.cactus.name=Cactus +tile.clay.name=Clay +tile.clayHardenedStained.black.name=Black Stained Clay +tile.clayHardenedStained.red.name=Red Stained Clay +tile.clayHardenedStained.green.name=Green Stained Clay +tile.clayHardenedStained.brown.name=Brown Stained Clay +tile.clayHardenedStained.blue.name=Blue Stained Clay +tile.clayHardenedStained.purple.name=Purple Stained Clay +tile.clayHardenedStained.cyan.name=Cyan Stained Clay +tile.clayHardenedStained.silver.name=Light Gray Stained Clay +tile.clayHardenedStained.gray.name=Gray Stained Clay +tile.clayHardenedStained.pink.name=Pink Stained Clay +tile.clayHardenedStained.lime.name=Lime Stained Clay +tile.clayHardenedStained.yellow.name=Yellow Stained Clay +tile.clayHardenedStained.lightBlue.name=Light Blue Stained Clay +tile.clayHardenedStained.magenta.name=Magenta Stained Clay +tile.clayHardenedStained.orange.name=Orange Stained Clay +tile.clayHardenedStained.white.name=White Stained Clay +tile.clayHardened.name=Hardened Clay +tile.reeds.name=Sugar cane +tile.jukebox.name=Jukebox +tile.fence.name=Fence +tile.fenceGate.name=Fence Gate +tile.pumpkin.name=Pumpkin +tile.litpumpkin.name=Jack o'Lantern +tile.hellrock.name=Netherrack +tile.hellsand.name=Soul Sand +tile.lightgem.name=Glowstone +tile.portal.name=Portal +tile.cloth.black.name=Black Wool +tile.cloth.red.name=Red Wool +tile.cloth.green.name=Green Wool +tile.cloth.brown.name=Brown Wool +tile.cloth.blue.name=Blue Wool +tile.cloth.purple.name=Purple Wool +tile.cloth.cyan.name=Cyan Wool +tile.cloth.silver.name=Light Gray Wool +tile.cloth.gray.name=Gray Wool +tile.cloth.pink.name=Pink Wool +tile.cloth.lime.name=Lime Wool +tile.cloth.yellow.name=Yellow Wool +tile.cloth.lightBlue.name=Light Blue Wool +tile.cloth.magenta.name=Magenta Wool +tile.cloth.orange.name=Orange Wool +tile.cloth.white.name=Wool +tile.oreLapis.name=Lapis Lazuli Ore +tile.blockLapis.name=Lapis Lazuli Block +tile.dispenser.name=Dispenser +tile.dropper.name=Dropper +tile.musicBlock.name=Note Block +tile.cake.name=Cake +tile.bed.name=Bed +tile.bed.occupied=This bed is occupied +tile.bed.noSleep=You can only sleep at night +tile.bed.notSafe=You may not rest now, there are monsters nearby +tile.bed.notValid=Your home bed was missing or obstructed +tile.lockedchest.name=Locked chest +tile.trapdoor.name=Trapdoor +tile.web.name=Cobweb +tile.stonebricksmooth.name=Stone Bricks +tile.stonebricksmooth.default.name=Stone Bricks +tile.stonebricksmooth.mossy.name=Mossy Stone Bricks +tile.stonebricksmooth.cracked.name=Cracked Stone Bricks +tile.stonebricksmooth.chiseled.name=Chiseled Stone Bricks +tile.monsterStoneEgg.stone.name=Stone Monster Egg +tile.monsterStoneEgg.cobble.name=Cobblestone Monster Egg +tile.monsterStoneEgg.brick.name=Stone Brick Monster Egg +tile.monsterStoneEgg.mossybrick.name=Mossy Stone Brick Monster Egg +tile.monsterStoneEgg.crackedbrick.name=Cracked Stone Brick Monster Egg +tile.monsterStoneEgg.chiseledbrick.name=Chiseled Stone Brick Monster Egg +tile.pistonBase.name=Piston +tile.pistonStickyBase.name=Sticky Piston +tile.fenceIron.name=Iron Bars +tile.melon.name=Melon +tile.stairsBrick.name=Brick Stairs +tile.stairsStoneBrickSmooth.name=Stone Brick Stairs +tile.vine.name=Vines +tile.netherBrick.name=Nether Brick +tile.netherFence.name=Nether Brick Fence +tile.stairsNetherBrick.name=Nether Brick Stairs +tile.netherStalk.name=Nether Wart +tile.cauldron.name=Cauldron +tile.enchantmentTable.name=Enchantment Table +tile.anvil.name=Anvil +tile.anvil.intact.name=Anvil +tile.anvil.slightlyDamaged.name=Slightly Damaged Anvil +tile.anvil.veryDamaged.name=Very Damaged Anvil +tile.whiteStone.name=End Stone +tile.endPortalFrame.name=End Portal +tile.mycel.name=Mycelium +tile.waterlily.name=Lily Pad +tile.dragonEgg.name=Dragon Egg +tile.redstoneLight.name=Redstone Lamp +tile.cocoa.name=Cocoa +tile.enderChest.name=Ender Chest +tile.oreRuby.name=Ruby Ore +tile.oreEmerald.name=Emerald Ore +tile.blockEmerald.name=Block of Emerald +tile.blockRedstone.name=Block of Redstone +tile.tripWire.name=Tripwire +tile.tripWireSource.name=Tripwire Hook +tile.commandBlock.name=Command Block +tile.beacon.name=Beacon +tile.beacon.primary=Primary Power +tile.beacon.secondary=Secondary Power +tile.cobbleWall.normal.name=Cobblestone Wall +tile.cobbleWall.mossy.name=Mossy Cobblestone Wall +tile.carrots.name=Carrots +tile.potatoes.name=Potatoes +tile.daylightDetector.name=Daylight Sensor +tile.netherquartz.name=Nether Quartz Ore +tile.hopper.name=Hopper +tile.quartzBlock.default.name=Block of Quartz +tile.quartzBlock.chiseled.name=Chiseled Quartz Block +tile.quartzBlock.lines.name=Pillar Quartz Block +tile.stairsQuartz.name=Quartz Stairs + +item.nameTag.name=Name Tag +item.leash.name=Lead +item.shovelIron.name=Iron Shovel +item.pickaxeIron.name=Iron Pickaxe +item.hatchetIron.name=Iron Axe +item.flintAndSteel.name=Flint and Steel +item.apple.name=Apple +item.cookie.name=Cookie +item.bow.name=Bow +item.arrow.name=Arrow +item.coal.name=Coal +item.charcoal.name=Charcoal +item.diamond.name=Diamond +item.emerald.name=Emerald +item.ingotIron.name=Iron Ingot +item.ingotGold.name=Gold Ingot +item.swordIron.name=Iron Sword +item.swordWood.name=Wooden Sword +item.shovelWood.name=Wooden Shovel +item.pickaxeWood.name=Wooden Pickaxe +item.hatchetWood.name=Wooden Axe +item.swordStone.name=Stone Sword +item.shovelStone.name=Stone Shovel +item.pickaxeStone.name=Stone Pickaxe +item.hatchetStone.name=Stone Axe +item.swordDiamond.name=Diamond Sword +item.shovelDiamond.name=Diamond Shovel +item.pickaxeDiamond.name=Diamond Pickaxe +item.hatchetDiamond.name=Diamond Axe +item.stick.name=Stick +item.bowl.name=Bowl +item.mushroomStew.name=Mushroom Stew +item.swordGold.name=Golden Sword +item.shovelGold.name=Golden Shovel +item.pickaxeGold.name=Golden Pickaxe +item.hatchetGold.name=Golden Axe +item.string.name=String +item.feather.name=Feather +item.sulphur.name=Gunpowder +item.hoeWood.name=Wooden Hoe +item.hoeStone.name=Stone Hoe +item.hoeIron.name=Iron Hoe +item.hoeDiamond.name=Diamond Hoe +item.hoeGold.name=Golden Hoe +item.seeds.name=Seeds +item.seeds_pumpkin.name=Pumpkin Seeds +item.seeds_melon.name=Melon Seeds +item.melon.name=Melon +item.wheat.name=Wheat +item.bread.name=Bread +item.helmetCloth.name=Leather Cap +item.chestplateCloth.name=Leather Tunic +item.leggingsCloth.name=Leather Pants +item.bootsCloth.name=Leather Boots +item.helmetChain.name=Chain Helmet +item.chestplateChain.name=Chain Chestplate +item.leggingsChain.name=Chain Leggings +item.bootsChain.name=Chain Boots +item.helmetIron.name=Iron Helmet +item.chestplateIron.name=Iron Chestplate +item.leggingsIron.name=Iron Leggings +item.bootsIron.name=Iron Boots +item.helmetDiamond.name=Diamond Helmet +item.chestplateDiamond.name=Diamond Chestplate +item.leggingsDiamond.name=Diamond Leggings +item.bootsDiamond.name=Diamond Boots +item.helmetGold.name=Golden Helmet +item.chestplateGold.name=Golden Chestplate +item.leggingsGold.name=Golden Leggings +item.bootsGold.name=Golden Boots +item.flint.name=Flint +item.porkchopRaw.name=Raw Porkchop +item.porkchopCooked.name=Cooked Porkchop +item.chickenRaw.name=Raw Chicken +item.chickenCooked.name=Cooked Chicken +item.beefRaw.name=Raw Beef +item.beefCooked.name=Steak +item.painting.name=Painting +item.frame.name=Item Frame +item.appleGold.name=Golden Apple +item.sign.name=Sign +item.doorWood.name=Wooden Door +item.bucket.name=Bucket +item.bucketWater.name=Water Bucket +item.bucketLava.name=Lava Bucket +item.minecart.name=Minecart +item.saddle.name=Saddle +item.doorIron.name=Iron Door +item.redstone.name=Redstone +item.snowball.name=Snowball +item.boat.name=Boat +item.leather.name=Leather +item.milk.name=Milk +item.brick.name=Brick +item.clay.name=Clay +item.reeds.name=Sugar Canes +item.paper.name=Paper +item.book.name=Book +item.slimeball.name=Slimeball +item.minecartChest.name=Minecart with Chest +item.minecartFurnace.name=Minecart with Furnace +item.minecartTnt.name=Minecart with TNT +item.minecartHopper.name=Minecart with Hopper +item.minecartCommandBlock.name=Minecart with Command Block +item.egg.name=Egg +item.compass.name=Compass +item.fishingRod.name=Fishing Rod +item.clock.name=Clock +item.yellowDust.name=Glowstone Dust +item.fish.cod.raw.name=Raw Fish +item.fish.salmon.raw.name=Raw Salmon +item.fish.pufferfish.raw.name=Pufferfish +item.fish.clownfish.raw.name=Clownfish +item.fish.cod.cooked.name=Cooked Fish +item.fish.salmon.cooked.name=Cooked Salmon +item.record.name=Music Disc +item.record.13.desc=C418 - 13 +item.record.cat.desc=C418 - cat +item.record.blocks.desc=C418 - blocks +item.record.chirp.desc=C418 - chirp +item.record.far.desc=C418 - far +item.record.mall.desc=C418 - mall +item.record.mellohi.desc=C418 - mellohi +item.record.stal.desc=C418 - stal +item.record.strad.desc=C418 - strad +item.record.ward.desc=C418 - ward +item.record.11.desc=C418 - 11 +item.record.wait.desc=C418 - wait +item.bone.name=Bone +item.dyePowder.black.name=Ink Sac +item.dyePowder.red.name=Rose Red +item.dyePowder.green.name=Cactus Green +item.dyePowder.brown.name=Cocoa Beans +item.dyePowder.blue.name=Lapis Lazuli +item.dyePowder.purple.name=Purple Dye +item.dyePowder.cyan.name=Cyan Dye +item.dyePowder.silver.name=Light Gray Dye +item.dyePowder.gray.name=Gray Dye +item.dyePowder.pink.name=Pink Dye +item.dyePowder.lime.name=Lime Dye +item.dyePowder.yellow.name=Dandelion Yellow +item.dyePowder.lightBlue.name=Light Blue Dye +item.dyePowder.magenta.name=Magenta Dye +item.dyePowder.orange.name=Orange Dye +item.dyePowder.white.name=Bone Meal +item.sugar.name=Sugar +item.cake.name=Cake +item.bed.name=Bed +item.diode.name=Redstone Repeater +item.comparator.name=Redstone Comparator +item.map.name=Map +item.leaves.name=Leaves +item.shears.name=Shears +item.rottenFlesh.name=Rotten Flesh +item.enderPearl.name=Ender Pearl +item.blazeRod.name=Blaze Rod +item.ghastTear.name=Ghast Tear +item.netherStalkSeeds.name=Nether Wart +item.potion.name=Potion +item.emptyPotion.name=Water Bottle +item.goldNugget.name=Gold Nugget +item.glassBottle.name=Glass Bottle +item.spiderEye.name=Spider Eye +item.fermentedSpiderEye.name=Fermented Spider Eye +item.blazePowder.name=Blaze Powder +item.magmaCream.name=Magma Cream +item.cauldron.name=Cauldron +item.brewingStand.name=Brewing Stand +item.eyeOfEnder.name=Eye of Ender +item.speckledMelon.name=Glistering Melon +item.monsterPlacer.name=Spawn +item.expBottle.name=Bottle o' Enchanting +item.fireball.name=Fire Charge +item.writingBook.name=Book and Quill +item.writtenBook.name=Written Book +item.ruby.name=Ruby +item.flowerPot.name=Flower Pot +item.emptyMap.name=Empty Map +item.carrots.name=Carrot +item.carrotGolden.name=Golden Carrot +item.potato.name=Potato +item.potatoBaked.name=Baked Potato +item.potatoPoisonous.name=Poisonous Potato +item.skull.skeleton.name=Skeleton Skull +item.skull.wither.name=Wither Skeleton Skull +item.skull.zombie.name=Zombie Head +item.skull.char.name=Head +item.skull.player.name=%s's Head +item.skull.creeper.name=Creeper Head +item.carrotOnAStick.name=Carrot on a Stick +item.netherStar.name=Nether Star +item.pumpkinPie.name=Pumpkin Pie +item.enchantedBook.name=Enchanted Book +item.fireworks.name=Firework Rocket +item.fireworks.flight=Flight Duration: +item.fireworksCharge.name=Firework Star +item.fireworksCharge.black=Black +item.fireworksCharge.red=Red +item.fireworksCharge.green=Green +item.fireworksCharge.brown=Brown +item.fireworksCharge.blue=Blue +item.fireworksCharge.purple=Purple +item.fireworksCharge.cyan=Cyan +item.fireworksCharge.silver=Light Gray +item.fireworksCharge.gray=Gray +item.fireworksCharge.pink=Pink +item.fireworksCharge.lime=Lime +item.fireworksCharge.yellow=Yellow +item.fireworksCharge.lightBlue=Light Blue +item.fireworksCharge.magenta=Magenta +item.fireworksCharge.orange=Orange +item.fireworksCharge.white=White +item.fireworksCharge.customColor=Custom +item.fireworksCharge.fadeTo=Fade to +item.fireworksCharge.flicker=Twinkle +item.fireworksCharge.trail=Trail +item.fireworksCharge.type.0=Small Ball +item.fireworksCharge.type.1=Large Ball +item.fireworksCharge.type.2=Star-shaped +item.fireworksCharge.type.3=Creeper-shaped +item.fireworksCharge.type.4=Burst +item.fireworksCharge.type=Unknown Shape +item.netherbrick.name=Nether Brick +item.netherquartz.name=Nether Quartz +item.horsearmormetal.name=Iron Horse Armor +item.horsearmorgold.name=Gold Horse Armor +item.horsearmordiamond.name=Diamond Horse Armor + +container.inventory=Inventory +container.hopper=Item Hopper +container.crafting=Crafting +container.dispenser=Dispenser +container.dropper=Dropper +container.furnace=Furnace +container.enchant=Enchant +container.repair=Repair & Name +container.repair.cost=Enchantment Cost: %1$d +container.repair.expensive=Too Expensive! +container.creative=Item Selection +container.brewing=Brewing Stand +container.chest=Chest +container.chestDouble=Large Chest +container.minecart=Minecart +container.enderchest=Ender Chest + +item.dyed=Dyed +item.unbreakable=Unbreakable + +entity.Item.name=Item +entity.XPOrb.name=Experience Orb +entity.SmallFireball.name=Small Fireball +entity.Fireball.name=Fireball + +entity.Arrow.name=Arrow +entity.Snowball.name=Snowball +entity.Painting.name=Painting + +entity.Mob.name=Mob +entity.Monster.name=Monster + +entity.Creeper.name=Creeper +entity.Skeleton.name=Skeleton +entity.Spider.name=Spider +entity.Giant.name=Giant +entity.Zombie.name=Zombie +entity.Slime.name=Slime +entity.Ghast.name=Ghast +entity.PigZombie.name=Zombie Pigman +entity.Enderman.name=Enderman +entity.Silverfish.name=Silverfish +entity.CaveSpider.name=Cave Spider +entity.Blaze.name=Blaze +entity.LavaSlime.name=Magma Cube +entity.MushroomCow.name=Mooshroom +entity.Villager.name=Villager +entity.VillagerGolem.name=Iron Golem +entity.SnowMan.name=Snow Golem +entity.EnderDragon.name=Ender Dragon +entity.WitherBoss.name=Wither +entity.Witch.name=Witch + +entity.Pig.name=Pig +entity.Sheep.name=Sheep +entity.Cow.name=Cow +entity.Chicken.name=Chicken +entity.Squid.name=Squid +entity.Wolf.name=Wolf +entity.Ozelot.name=Ocelot +entity.Cat.name=Cat +entity.Bat.name=Bat +entity.EntityHorse.name=Horse +entity.horse.name=Horse +entity.donkey.name=Donkey +entity.mule.name=Mule +entity.skeletonhorse.name=Skeleton Horse +entity.zombiehorse.name=Zombie Horse + +entity.PrimedTnt.name=Block of TNT +entity.FallingSand.name=Falling Block + +entity.Minecart.name=Minecart +entity.Boat.name=Boat + +entity.Arrow.name=arrow +entity.generic.name=unknown + +death.fell.accident.ladder=%1$s fell off a ladder +death.fell.accident.vines=%1$s fell off some vines +death.fell.accident.water=%1$s fell out of the water +death.fell.accident.generic=%1$s fell from a high place +death.fell.killer=%1$s was doomed to fall +death.fell.assist=%1$s was doomed to fall by %2$s +death.fell.assist.item=%1$s was doomed to fall by %2$s using %3$s +death.fell.finish=%1$s fell too far and was finished by %2$s +death.fell.finish.item=%1$s fell too far and was finished by %2$s using %3$s + +death.attack.inFire=%1$s went up in flames +death.attack.inFire.player=%1$s walked into fire whilst fighting %2$s +death.attack.onFire=%1$s burned to death +death.attack.onFire.player=%1$s was burnt to a crisp whilst fighting %2$s +death.attack.lava=%1$s tried to swim in lava +death.attack.lava.player=%1$s tried to swim in lava to escape %2$s +death.attack.inWall=%1$s suffocated in a wall +death.attack.drown=%1$s drowned +death.attack.drown.player=%1$s drowned whilst trying to escape %2$s +death.attack.starve=%1$s starved to death +death.attack.cactus=%1$s was pricked to death +death.attack.cactus.player=%1$s walked into a cactus whilst trying to escape %2$s +death.attack.generic=%1$s died +death.attack.explosion=%1$s blew up +death.attack.explosion.player=%1$s was blown up by %2$s +death.attack.magic=%1$s was killed by magic +death.attack.wither=%1$s withered away +death.attack.anvil=%1$s was squashed by a falling anvil +death.attack.fallingBlock=%1$s was squashed by a falling block +death.attack.mob=%1$s was slain by %2$s +death.attack.player=%1$s was slain by %2$s +death.attack.player.item=%1$s was slain by %2$s using %3$s +death.attack.arrow=%1$s was shot by %2$s +death.attack.arrow.item=%1$s was shot by %2$s using %3$s +death.attack.fireball=%1$s was fireballed by %2$s +death.attack.fireball.item=%1$s was fireballed by %2$s using %3$s +death.attack.thrown=%1$s was pummeled by %2$s +death.attack.thrown.item=%1$s was pummeled by %2$s using %3$s +death.attack.indirectMagic=%1$s was killed by %2$s using magic +death.attack.indirectMagic.item=%1$s was killed by %2$s using %3$s +death.attack.thorns=%1$s was killed trying to hurt %2$s +death.attack.fall=%1$s hit the ground too hard +death.attack.outOfWorld=%1$s fell out of the world + +deathScreen.respawn=Respawn +deathScreen.deleteWorld=Delete world +deathScreen.titleScreen=Title screen +deathScreen.score=Score +deathScreen.title.hardcore=Game over! +deathScreen.hardcoreInfo=You cannot respawn in hardcore mode! +deathScreen.title=You died! +deathScreen.leaveServer=Leave server +deathScreen.quit.confirm=Are you sure you want to quit? + +potion.effects.whenDrank=When Applied: +potion.empty=No Effects +potion.moveSpeed=Speed +potion.moveSlowdown=Slowness +potion.digSpeed=Haste +potion.digSlowDown=Mining Fatigue +potion.damageBoost=Strength +potion.weakness=Weakness +potion.heal=Instant Health +potion.harm=Instant Damage +potion.jump=Jump Boost +potion.confusion=Nausea +potion.regeneration=Regeneration +potion.resistance=Resistance +potion.fireResistance=Fire Resistance +potion.waterBreathing=Water Breathing +potion.invisibility=Invisibility +potion.blindness=Blindness +potion.nightVision=Night Vision +potion.hunger=Hunger +potion.poison=Poison +potion.wither=Wither +potion.healthBoost=Health Boost +potion.absorption=Absorption +potion.saturation=Saturation + +potion.moveSpeed.postfix=Potion of Swiftness +potion.moveSlowdown.postfix=Potion of Slowness +potion.digSpeed.postfix=Potion of Haste +potion.digSlowDown.postfix=Potion of Dullness +potion.damageBoost.postfix=Potion of Strength +potion.weakness.postfix=Potion of Weakness +potion.heal.postfix=Potion of Healing +potion.harm.postfix=Potion of Harming +potion.jump.postfix=Potion of Leaping +potion.confusion.postfix=Potion of Nausea +potion.regeneration.postfix=Potion of Regeneration +potion.resistance.postfix=Potion of Resistance +potion.fireResistance.postfix=Potion of Fire Resistance +potion.waterBreathing.postfix=Potion of Water Breathing +potion.invisibility.postfix=Potion of Invisibility +potion.blindness.postfix=Potion of Blindness +potion.nightVision.postfix=Potion of Night Vision +potion.hunger.postfix=Potion of Hunger +potion.poison.postfix=Potion of Poison +potion.wither.postfix=Potion of Decay +potion.healthBoost.postfix=Potion of Health Boost +potion.absorption.postfix=Potion of Absorption +potion.saturation.postfix=Potion of Saturation + +potion.potency.0= +potion.potency.1=II +potion.potency.2=III +potion.potency.3=IV + +potion.prefix.grenade=Splash +potion.prefix.mundane=Mundane +potion.prefix.uninteresting=Uninteresting +potion.prefix.bland=Bland +potion.prefix.clear=Clear +potion.prefix.milky=Milky +potion.prefix.diffuse=Diffuse +potion.prefix.artless=Artless +potion.prefix.thin=Thin +potion.prefix.awkward=Awkward +potion.prefix.flat=Flat +potion.prefix.bulky=Bulky +potion.prefix.bungling=Bungling +potion.prefix.buttered=Buttered +potion.prefix.smooth=Smooth +potion.prefix.suave=Suave +potion.prefix.debonair=Debonair +potion.prefix.thick=Thick +potion.prefix.elegant=Elegant +potion.prefix.fancy=Fancy +potion.prefix.charming=Charming +potion.prefix.dashing=Dashing +potion.prefix.refined=Refined +potion.prefix.cordial=Cordial +potion.prefix.sparkling=Sparkling +potion.prefix.potent=Potent +potion.prefix.foul=Foul +potion.prefix.odorless=Odorless +potion.prefix.rank=Rank +potion.prefix.harsh=Harsh +potion.prefix.acrid=Acrid +potion.prefix.gross=Gross +potion.prefix.stinky=Stinky + +enchantment.damage.all=Sharpness +enchantment.damage.undead=Smite +enchantment.damage.arthropods=Bane of Arthropods +enchantment.knockback=Knockback +enchantment.fire=Fire Aspect +enchantment.protect.all=Protection +enchantment.protect.fire=Fire Protection +enchantment.protect.fall=Feather Falling +enchantment.protect.explosion=Blast Protection +enchantment.protect.projectile=Projectile Protection +enchantment.oxygen=Respiration +enchantment.waterWorker=Aqua Affinity +enchantment.digging=Efficiency +enchantment.untouching=Silk Touch +enchantment.durability=Unbreaking +enchantment.lootBonus=Looting +enchantment.lootBonusDigger=Fortune +enchantment.lootBonusFishing=Luck of the Sea +enchantment.fishingSpeed=Lure +enchantment.arrowDamage=Power +enchantment.arrowFire=Flame +enchantment.arrowKnockback=Punch +enchantment.arrowInfinite=Infinity +enchantment.thorns=Thorns + +enchantment.level.1=I +enchantment.level.2=II +enchantment.level.3=III +enchantment.level.4=IV +enchantment.level.5=V +enchantment.level.6=VI +enchantment.level.7=VII +enchantment.level.8=VIII +enchantment.level.9=IX +enchantment.level.10=X + +gui.achievements=Achievements +gui.stats=Statistics + +stats.tooltip.type.achievement=Achievement +stats.tooltip.type.statistic=Statistic +stat.generalButton=General +stat.blocksButton=Blocks +stat.itemsButton=Items +stat.mobsButton=Mobs + +stat.used=Times Used +stat.mined=Times Mined +stat.depleted=Times Depleted +stat.crafted=Times Crafted +stat.entityKills=You killed %d %s +stat.entityKilledBy=%s killed you %d time(s) +stat.entityKills.none=You have never killed %s +stat.entityKilledBy.none=You have never been killed by %s + +stat.startGame=Times played +stat.createWorld=Worlds created +stat.loadWorld=Saves loaded +stat.joinMultiplayer=Multiplayer joins +stat.leaveGame=Games quit + +stat.playOneMinute=Minutes Played + +stat.walkOneCm=Distance Walked +stat.fallOneCm=Distance Fallen +stat.swimOneCm=Distance Swum +stat.flyOneCm=Distance Flown +stat.climbOneCm=Distance Climbed +stat.diveOneCm=Distance Dove +stat.minecartOneCm=Distance by Minecart +stat.boatOneCm=Distance by Boat +stat.pigOneCm=Distance by Pig +stat.horseOneCm=Distance by Horse +stat.jump=Jumps +stat.drop=Items Dropped + +stat.damageDealt=Damage Dealt +stat.damageTaken=Damage Taken +stat.deaths=Number of Deaths +stat.mobKills=Mob Kills +stat.animalsBred=Animals Bred +stat.playerKills=Player Kills +stat.fishCaught=Fish Caught +stat.treasureFished=Treasure Fished +stat.junkFished=Junk Fished + +stat.mineBlock=%1$s Mined +stat.craftItem=%1$s Crafted +stat.useItem=%1$s Used +stat.breakItem=%1$s Depleted + +achievement.get=Achievement get! + +achievement.taken=Taken! +achievement.unknown=??? + +achievement.requires=Requires '%1$s' +achievement.openInventory=Taking Inventory +achievement.openInventory.desc=Press '%1$s' to open your inventory. +achievement.mineWood=Getting Wood +achievement.mineWood.desc=Attack a tree until a block of wood pops out +achievement.buildWorkBench=Benchmarking +achievement.buildWorkBench.desc=Craft a workbench with four blocks of planks +achievement.buildPickaxe=Time to Mine! +achievement.buildPickaxe.desc=Use planks and sticks to make a pickaxe +achievement.buildFurnace=Hot Topic +achievement.buildFurnace.desc=Construct a furnace out of eight stone blocks +achievement.acquireIron=Acquire Hardware +achievement.acquireIron.desc=Smelt an iron ingot +achievement.buildHoe=Time to Farm! +achievement.buildHoe.desc=Use planks and sticks to make a hoe +achievement.makeBread=Bake Bread +achievement.makeBread.desc=Turn wheat into bread +achievement.bakeCake=The Lie +achievement.bakeCake.desc=Wheat, sugar, milk and eggs! +achievement.buildBetterPickaxe=Getting an Upgrade +achievement.buildBetterPickaxe.desc=Construct a better pickaxe +achievement.cookFish=Delicious Fish +achievement.cookFish.desc=Catch and cook fish! +achievement.onARail=On A Rail +achievement.onARail.desc=Travel by minecart at least 1 km from where you started +achievement.buildSword=Time to Strike! +achievement.buildSword.desc=Use planks and sticks to make a sword +achievement.killEnemy=Monster Hunter +achievement.killEnemy.desc=Attack and destroy a monster +achievement.killCow=Cow Tipper +achievement.killCow.desc=Harvest some leather +achievement.breedCow=Repopulation +achievement.breedCow.desc=Breed two cows with wheat +achievement.flyPig=When Pigs Fly +achievement.flyPig.desc=Fly a pig off a cliff +achievement.snipeSkeleton=Sniper Duel +achievement.snipeSkeleton.desc=Kill a skeleton with an arrow from more than 50 meters +achievement.diamonds=DIAMONDS! +achievement.diamonds.desc=Acquire diamonds with your iron tools +achievement.diamondsToYou=Diamonds to you! +achievement.diamondsToYou.desc=Throw diamonds at another player. +achievement.portal=We Need to Go Deeper +achievement.portal.desc=Build a portal to the Nether +achievement.ghast=Return to Sender +achievement.ghast.desc=Destroy a Ghast with a fireball +achievement.blazeRod=Into Fire +achievement.blazeRod.desc=Relieve a Blaze of its rod +achievement.potion=Local Brewery +achievement.potion.desc=Brew a potion +achievement.theEnd=The End? +achievement.theEnd.desc=Locate the End +achievement.theEnd2=The End. +achievement.theEnd2.desc=Defeat the Ender Dragon +achievement.spawnWither=The Beginning? +achievement.spawnWither.desc=Spawn the Wither +achievement.killWither=The Beginning. +achievement.killWither.desc=Kill the Wither +achievement.fullBeacon=Beaconator +achievement.fullBeacon.desc=Create a full beacon +achievement.exploreAllBiomes=Adventuring Time +achievement.exploreAllBiomes.desc=Discover all biomes +achievement.enchantments=Enchanter +achievement.enchantments.desc=Use a book, obsidian and diamonds to construct an enchantment table +achievement.overkill=Overkill +achievement.overkill.desc=Deal eight hearts of damage in a single hit +achievement.bookcase=Librarian +achievement.bookcase.desc=Build some bookshelves to improve your enchantment table + +commands.generic.exception=An unknown error occurred while attempting to perform this command +commands.generic.permission=You do not have permission to use this command +commands.generic.syntax=Invalid command syntax +commands.generic.player.notFound=That player cannot be found +commands.generic.notFound=Unknown command. Try /help for a list of commands +commands.generic.num.invalid='%s' is not a valid number +commands.generic.boolean.invalid='%s' is not true or false +commands.generic.num.tooSmall=The number you have entered (%d) is too small, it must be at least %d +commands.generic.num.tooBig=The number you have entered (%d) is too big, it must be at most %d +commands.generic.double.tooSmall=The number you have entered (%.2f) is too small, it must be at least %.2f +commands.generic.double.tooBig=The number you have entered (%.2f) is too big, it must be at most %.2f +commands.generic.usage=Usage: %s +commands.generic.deprecatedId=Warning: Using numeric IDs will not be supported in the future. Please use names, such as '%s' + +commands.setidletimeout.usage=/setidletimeout +commands.setidletimeout.success=Successfully set the idle timeout to %d minutes. +commands.xp.failure.widthdrawXp=Cannot give player negative experience points +commands.xp.success=Given %d experience to %s +commands.xp.success.levels=Given %d levels to %s +commands.xp.success.negative.levels=Taken %d levels from %s +commands.xp.usage=/xp [player] OR /xp L [player] +commands.playsound.usage=/playsound [x] [y] [z] [volume] [pitch] [minimumVolume] +commands.playsound.success=Played sound '%s' to %s +commands.playsound.playerTooFar=Player %s is too far away to hear the sound +commands.give.usage=/give [amount] [data] [dataTag] +commands.give.notFound=There is no such item with ID %d +commands.give.success=Given %s * %d to %s +commands.give.tagError=Data tag parsing failed: %s +commands.summon.usage=/summon [x] [y] [z] [dataTag] +commands.summon.success=Object successfully summoned +commands.summon.failed=Unable to summon object +commands.summon.tagError=Data tag parsing failed: %s +commands.summon.outOfWorld=Cannot summon the object out of the world +commands.testforblock.usage=/testforblock [dataValue] [dataTag] +commands.testforblock.failed.tile=The block at %d,%d,%d is %s (expected: %s). +commands.testforblock.failed.data=The block at %d,%d,%d had the data value of %s (expected: %s). +commands.testforblock.failed.nbt=The block at %d,%d,%d did not have the required NBT keys. +commands.testforblock.failed.tileEntity=The block at %d,%d,%d is not a tile entity and cannot support tag matching. +commands.testforblock.success=Successfully found the block at %d,%d,%d. +commands.testforblock.outOfWorld=Cannot test for block outside of the world +commands.setblock.usage=/setblock [dataValue] [oldBlockHandling] [dataTag] +commands.setblock.success=Block placed +commands.setblock.failed=Unable to place block +commands.setblock.tagError=Data tag parsing failed: %s +commands.setblock.outOfWorld=Cannot place block outside of the world +commands.setblock.notFound=There is no such block with ID/name %s +commands.setblock.noChange=The block couldn't be placed +commands.effect.usage=/effect [seconds] [amplifier] +commands.effect.notFound=There is no such mob effect with ID %d +commands.effect.success=Given %1$s (ID %2$d) * %3$d to %4$s for %5$d seconds +commands.effect.success.removed=Took %1$s from %2$s +commands.effect.success.removed.all=Took all effects from %s +commands.effect.failure.notActive=Couldn't take %1$s from %2$s as they do not have the effect +commands.effect.failure.notActive.all=Couldn't take any effects from %s as they do not have any +commands.enchant.usage=/enchant [level] +commands.enchant.notFound=There is no such enchantment with ID %d +commands.enchant.noItem=The target doesn't hold an item +commands.enchant.cantEnchant=The selected enchantment can't be added to the target item +commands.enchant.cantCombine=%1$s can't be combined with %2$s +commands.enchant.success=Enchanting succeeded +commands.clear.usage=/clear [item] [data] +commands.clear.success=Cleared the inventory of %s, removing %d items +commands.clear.failure=Could not clear the inventory of %s, no items to remove +commands.downfall.usage=/toggledownfall +commands.downfall.success=Toggled downfall +commands.time.usage=/time +commands.time.added=Added %d to the time +commands.time.set=Set the time to %d +commands.players.usage=/list +commands.players.list=There are %d/%d players online: +commands.banlist.ips=There are %d total banned IP addresses: +commands.banlist.players=There are %d total banned players: +commands.banlist.usage=/banlist [ips|players] +commands.kill.usage=/kill +commands.kill.success=Ouch! That looked like it hurt +commands.kick.success=Kicked %s from the game +commands.kick.success.reason=Kicked %s from the game: '%s' +commands.kick.usage=/kick [reason ...] +commands.op.success=Opped %s +commands.op.usage=/op +commands.deop.success=De-opped %s +commands.deop.usage=/deop +commands.say.usage=/say +commands.ban.success=Banned player %s +commands.ban.usage=/ban [reason ...] +commands.unban.success=Unbanned player %s +commands.unban.usage=/pardon +commands.banip.invalid=You have entered an invalid IP address or a player that is not online +commands.banip.success=Banned IP address %s +commands.banip.success.players=Banned IP address %s belonging to %s +commands.banip.usage=/ban-ip [reason ...] +commands.unbanip.invalid=You have entered an invalid IP address +commands.unbanip.success=Unbanned IP address %s +commands.unbanip.usage=/pardon-ip

+commands.save.usage=/save-all +commands.save-on.alreadyOn=Saving is already turned on. +commands.save-on.usage=/save-on +commands.save-off.alreadyOff=Saving is already turned off. +commands.save-off.usage=/save-off +commands.save.enabled=Turned on world auto-saving +commands.save.disabled=Turned off world auto-saving +commands.save.start=Saving... +commands.save.success=Saved the world +commands.save.failed=Saving failed: %s +commands.stop.usage=/stop +commands.stop.start=Stopping the server +commands.tp.success=Teleported %s to %s +commands.tp.success.coordinates=Teleported %s to %.2f,%.2f,%.2f +commands.tp.usage=/tp [target player] OR /tp [target player] +commands.tp.notSameDimension=Unable to teleport because players are not in the same dimension +commands.whitelist.list=There are %d (out of %d seen) whitelisted players: +commands.whitelist.enabled=Turned on the whitelist +commands.whitelist.disabled=Turned off the whitelist +commands.whitelist.reloaded=Reloaded the whitelist +commands.whitelist.add.success=Added %s to the whitelist +commands.whitelist.add.usage=/whitelist add +commands.whitelist.remove.success=Removed %s from the whitelist +commands.whitelist.remove.usage=/whitelist remove +commands.whitelist.usage=/whitelist +commands.scoreboard.usage=/scoreboard +commands.scoreboard.teamNotFound=No team was found by the name '%s' +commands.scoreboard.objectiveNotFound=No objective was found by the name '%s' +commands.scoreboard.objectiveReadOnly=The objective '%s' is read-only and cannot be set +commands.scoreboard.objectives.usage=/scoreboard objectives +commands.scoreboard.objectives.setdisplay.usage=/scoreboard objectives setdisplay [objective] +commands.scoreboard.objectives.setdisplay.invalidSlot=No such display slot '%s' +commands.scoreboard.objectives.setdisplay.successCleared=Cleared objective display slot '%s' +commands.scoreboard.objectives.setdisplay.successSet=Set the display objective in slot '%s' to '%s' +commands.scoreboard.objectives.add.usage=/scoreboard objectives add [display name ...] +commands.scoreboard.objectives.add.wrongType=Invalid objective criteria type '%s' +commands.scoreboard.objectives.add.alreadyExists=An objective with the name '%s' already exists +commands.scoreboard.objectives.add.tooLong=The name '%s' is too long for an objective, it can be at most %d characters long +commands.scoreboard.objectives.add.displayTooLong=The display name '%s' is too long for an objective, it can be at most %d characters long +commands.scoreboard.objectives.add.success=Added new objective '%s' successfully +commands.scoreboard.objectives.remove.usage=/scoreboard objectives remove +commands.scoreboard.objectives.remove.success=Removed objective '%s' successfully +commands.scoreboard.objectives.list.count=Showing %d objective(s) on scoreboard: +commands.scoreboard.objectives.list.entry=- %s: displays as '%s' and is type '%s' +commands.scoreboard.objectives.list.empty=There are no objectives on the scoreboard +commands.scoreboard.players.usage=/scoreboard players +commands.scoreboard.players.set.success=Set score of %s for player %s to %d +commands.scoreboard.players.set.usage=/scoreboard players set +commands.scoreboard.players.add.usage=/scoreboard players add +commands.scoreboard.players.remove.usage=/scoreboard players remove +commands.scoreboard.players.reset.usage=/scoreboard players reset +commands.scoreboard.players.reset.success=Reset all scores of player %s +commands.scoreboard.players.list.usage=/scoreboard players list [name] +commands.scoreboard.players.list.count=Showing %d tracked players on the scoreboard: +commands.scoreboard.players.list.empty=There are no tracked players on the scoreboard +commands.scoreboard.players.list.player.count=Showing %d tracked objective(s) for %s: +commands.scoreboard.players.list.player.entry=- %2$s: %1$d (%3$s) +commands.scoreboard.players.list.player.empty=Player %s has no scores recorded +commands.scoreboard.teams.usage=/scoreboard teams +commands.scoreboard.teams.add.usage=/scoreboard teams add [display name ...] +commands.scoreboard.teams.add.alreadyExists=A team with the name '%s' already exists +commands.scoreboard.teams.add.tooLong=The name '%s' is too long for a team, it can be at most %d characters long +commands.scoreboard.teams.add.displayTooLong=The display name '%s' is too long for a team, it can be at most %d characters long +commands.scoreboard.teams.add.success=Added new team '%s' successfully +commands.scoreboard.teams.list.usage=/scoreboard teams list [name] +commands.scoreboard.teams.list.count=Showing %d teams on the scoreboard: +commands.scoreboard.teams.list.entry=- %1$s: '%2$s' has %3$d players +commands.scoreboard.teams.list.empty=There are no teams registered on the scoreboard +commands.scoreboard.teams.list.player.count=Showing %d player(s) in team %s: +commands.scoreboard.teams.list.player.entry=- %2$s: %1$d (%3$s) +commands.scoreboard.teams.list.player.empty=Team %s has no players +commands.scoreboard.teams.empty.usage=/scoreboard teams empty +commands.scoreboard.teams.empty.alreadyEmpty=Team %s is already empty, cannot remove nonexistant players +commands.scoreboard.teams.empty.success=Removed all %d player(s) from team %s +commands.scoreboard.teams.remove.usage=/scoreboard teams remove +commands.scoreboard.teams.remove.success=Removed team %s +commands.scoreboard.teams.join.usage=/scoreboard teams join [player] +commands.scoreboard.teams.join.success=Added %d player(s) to team %s: %s +commands.scoreboard.teams.join.failure=Could not add %d player(s) to team %s: %s +commands.scoreboard.teams.leave.usage=/scoreboard teams leave [player] +commands.scoreboard.teams.leave.success=Removed %d player(s) from their teams: %s +commands.scoreboard.teams.leave.failure=Could not remove %d player(s) from their teams: %s +commands.scoreboard.teams.leave.noTeam=You are not in a team +commands.scoreboard.teams.option.usage=/scoreboard teams option +commands.scoreboard.teams.option.noValue=Valid values for option %s are: %s +commands.scoreboard.teams.option.success=Set option %s for team %s to %s +commands.gamemode.success.self=Set own game mode to %s +commands.gamemode.success.other=Set %s's game mode to %s +commands.gamemode.usage=/gamemode [player] +commands.defaultgamemode.usage=/defaultgamemode +commands.defaultgamemode.success=The world's default game mode is now %s +commands.me.usage=/me +commands.help.header=--- Showing help page %d of %d (/help ) --- +commands.help.footer=Tip: Use the key while typing a command to auto-complete the command or its arguments +commands.help.usage=/help [page|command name] +commands.publish.usage=/publish +commands.publish.started=Local game hosted on port %s +commands.publish.failed=Unable to host local game +commands.debug.start=Started debug profiling +commands.debug.stop=Stopped debug profiling after %.2f seconds (%d ticks) +commands.debug.notStarted=Can't stop profiling when we haven't started yet! +commands.debug.usage=/debug +commands.tellraw.usage=/tellraw +commands.tellraw.jsonException=Invalid json: %s +commands.message.usage=/tell +commands.message.sameTarget=You can't send a private message to yourself! +commands.message.display.outgoing=You whisper to %s: %s +commands.message.display.incoming=%s whispers to you: %s +commands.difficulty.usage=/difficulty +commands.difficulty.success=Set game difficulty to %s +commands.spawnpoint.usage=/spawnpoint OR /spawnpoint OR /spawnpoint +commands.spawnpoint.success=Set %s's spawn point to (%d, %d, %d) +commands.setworldspawn.usage=/setworldspawn OR /setworldspawn +commands.setworldspawn.success=Set the world spawn point to (%d, %d, %d) +commands.gamerule.usage=/gamerule OR /gamerule +commands.gamerule.success=Game rule has been updated +commands.gamerule.norule=No game rule called '%s' is available +commands.weather.usage=/weather [duration in seconds] +commands.weather.clear=Changing to clear weather +commands.weather.rain=Changing to rainy weather +commands.weather.thunder=Changing to rain and thunder +commands.testfor.usage=/testfor +commands.testfor.failed=/testfor is only usable by commandblocks with analog output +commands.seed.usage=/seed +commands.seed.success=Seed: %s +commands.spreadplayers.usage=/spreadplayers +commands.spreadplayers.spreading.teams=Spreading %s teams %s blocks around %s,%s (min %s blocks apart) +commands.spreadplayers.spreading.players=Spreading %s players %s blocks around %s,%s (min %s blocks apart) +commands.spreadplayers.success.teams=Successfully spread %s teams around %s,%s +commands.spreadplayers.success.players=Successfully spread %s players around %s,%s +commands.spreadplayers.info.teams=(Average distance between teams is %s blocks apart after %s iterations) +commands.spreadplayers.info.players=(Average distance between players is %s blocks apart after %s iterations) +commands.spreadplayers.failure.teams=Could not spread %s teams around %s,%s (too many players for space - try using spread of at most %s) +commands.spreadplayers.failure.players=Could not spread %s players around %s,%s (too many players for space - try using spread of at most %s) +commands.achievement.usage=/achievement give [player] +commands.achievement.unknownAchievement=Unknown achievement or statistic '%s' +commands.achievement.give.success.all=Successfully given all achievements to %s +commands.achievement.give.success.one=Successfully given %s the stat %s +commands.achievement.statTooLow=Player %s does not have the stat %s + +itemGroup.buildingBlocks=Building Blocks +itemGroup.decorations=Decoration Blocks +itemGroup.redstone=Redstone +itemGroup.transportation=Transportation +itemGroup.misc=Miscellaneous +itemGroup.search=Search Items +itemGroup.food=Foodstuffs +itemGroup.tools=Tools +itemGroup.combat=Combat +itemGroup.brewing=Brewing +itemGroup.materials=Materials +itemGroup.inventory=Survival Inventory + +inventory.binSlot=Destroy Item + +advMode.setCommand=Set Console Command for Block +advMode.setCommand.success=Command set: %s +advMode.command=Console Command +advMode.nearestPlayer=Use "@p" to target nearest player +advMode.randomPlayer=Use "@r" to target random player +advMode.allPlayers=Use "@a" to target all players +advMode.previousOutput=Previous Output + +advMode.notEnabled=Command blocks are not enabled on this server +advMode.notAllowed=Must be an opped player in creative mode + +mco.title=Minecraft Realms +mount.onboard=Press %1$s to dismount + +mco.terms.buttons.agree=Agree +mco.terms.buttons.disagree=Don't Agree +mco.terms.title=Realms Terms of Service +mco.terms.sentence.1=I agree to Minecraft Realms +mco.terms.sentence.2=Terms of Service + +mco.buy.realms.title=Buy a Realm +mco.buy.realms.buy=I want one! + +mco.selectServer.play=Play +mco.selectServer.configure=Configure +mco.selectServer.leave=Leave Realm +mco.selectServer.create=Create Realm +mco.selectServer.buy=Buy Realm +mco.selectServer.moreinfo=More Info + +mco.selectServer.expired=Expired Server +mco.selectServer.open=Open Server +mco.selectServer.closed=Closed Server +mco.selectServer.locked=Locked Server + +mco.selectServer.expires.days=Expires in %s days +mco.selectServer.expires.day=Expires in a day +mco.selectServer.expires.soon=Expires soon + +mco.configure.world.edit.title=Edit Realm + +mco.configure.world.title=Configure Realm +mco.configure.world.name=Name +mco.configure.world.description=Description +mco.configure.world.location=Location +mco.configure.world.invited=Invited +mco.configure.world.buttons.edit=Edit +mco.configure.world.buttons.reset=Reset Realm +mco.configure.world.buttons.done=Done +mco.configure.world.buttons.delete=Delete +mco.configure.world.buttons.open=Open Realm +mco.configure.world.buttons.close=Close Realm +mco.configure.world.buttons.invite=Invite +mco.configure.world.buttons.uninvite=Uninvite +mco.configure.world.buttons.backup=Backups +mco.configure.world.buttons.subscription=Subscription +mco.configure.world.invite.profile.name=Name +mco.configure.world.uninvite.question=Are you sure that you want to uninvite +mco.configure.world.status=Status + +mco.configure.world.subscription.title=Subscription Info +mco.configure.world.subscription.daysleft=Days Left +mco.configure.world.subscription.start=Start Date +mco.configure.world.subscription.extend=Extend Subscription + +mco.create.world.location.title=Locations +mco.create.world.location.warning=You may not get the exact location you select +mco.create.world.wait=Creating the realm... +mco.create.world.seed=Seed (Optional) + +mco.reset.world.title=Reset Realm +mco.reset.world.warning=This will permanently delete your realm! +mco.reset.world.seed=Seed (Optional) +mco.reset.world.resetting.screen.title=Resetting Realm... + +mco.configure.world.close.question.line1=Your realm will become unavailable. +mco.configure.world.close.question.line2=Are you sure you want to do that? + +mco.configure.world.leave.question.line1=If you leave this realm you won't see it unless invited again +mco.configure.world.leave.question.line2=Are you sure you want to do that? + +mco.configure.world.reset.question.line1=Your realm will be regenerated and your current realm will be lost +mco.configure.world.reset.question.line2=Are you sure you want to do that? + +mco.configure.world.restore.question.line1=Your realm will be restored to date +mco.configure.world.restore.question.line2=Are you sure you want to do that? + +mco.configure.world.restore.download.question.line1=You will be redirected to your default browser to download your world map. +mco.configure.world.restore.download.question.line2=Do you want to continue? + +mco.more.info.question.line1=You will be redirected to your default browser to see the page. +mco.more.info.question.line2=Do you want to continue? + +mco.connect.connecting=Connecting to the online server... +mco.connect.authorizing=Logging in... +mco.connect.failed=Failed to connect to the online server + +mco.create.world=Create + +mco.client.outdated.title=Client Outdated! +mco.client.outdated.msg=Your client is outdated, please consider updating it to use Realms + +mco.backup.title=Backups +mco.backup.button.restore=Restore +mco.backup.restoring=Restoring your realm +mco.backup.button.download=Download Latest + +mco.template.title=Realm Templates +mco.template.button.select=Select +mco.template.default.name=Select Template (Optional) +mco.template.name=Template + +mco.invites.button.accept=Accept +mco.invites.button.reject=Reject +mco.invites.title=Pending Invitations +mco.invites.pending=New invitations! +mco.invites.nopending=No pending invitations! + +build.tooHigh=Height limit for building is %s blocks + +attribute.modifier.plus.0=+%d %s +attribute.modifier.plus.1=+%d%% %s +attribute.modifier.plus.2=+%d%% %s +attribute.modifier.take.0=-%d %s +attribute.modifier.take.1=-%d%% %s +attribute.modifier.take.2=-%d%% %s + +attribute.name.horse.jumpStrength=Horse Jump Strength +attribute.name.zombie.spawnReinforcements=Zombie Reinforcements +attribute.name.generic.maxHealth=Max Health +attribute.name.generic.followRange=Mob Follow Range +attribute.name.generic.knockbackResistance=Knockback Resistance +attribute.name.generic.movementSpeed=Speed +attribute.name.generic.attackDamage=Attack Damage + +screenshot.success=Saved screenshot as %s +screenshot.failure=Couldn't save screenshot: %s diff --git a/proxy/src/test/java/net/md_5/bungee/chat/ComponentsTest.java b/proxy/src/test/java/net/md_5/bungee/chat/ComponentsTest.java new file mode 100644 index 00000000..898c2b5f --- /dev/null +++ b/proxy/src/test/java/net/md_5/bungee/chat/ComponentsTest.java @@ -0,0 +1,82 @@ +package net.md_5.bungee.chat; + +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.chat.ComponentBuilder; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.chat.TranslatableComponent; +import org.junit.Assert; +import org.junit.Test; + +public class ComponentsTest +{ + @Test + public void testBasicComponent() + { + TextComponent textComponent = new TextComponent( "Hello world" ); + textComponent.setColor( ChatColor.RED ); + + Assert.assertEquals( "Hello world", textComponent.toPlainText() ); + Assert.assertEquals( ChatColor.RED + "Hello world", textComponent.toLegacyText() ); + } + + @Test + public void testLegacyConverter() + { + BaseComponent[] test1 = TextComponent.fromLegacyText( ChatColor.AQUA + "Aqua " + ChatColor.RED + ChatColor.BOLD + "RedBold" ); + + Assert.assertEquals( "Aqua RedBold", BaseComponent.toPlainText( test1 ) ); + Assert.assertEquals( ChatColor.AQUA + "Aqua " + ChatColor.RED + ChatColor.BOLD + "RedBold", BaseComponent.toLegacyText( test1 ) ); + + BaseComponent[] test2 = TextComponent.fromLegacyText( "Text http://spigotmc.org " + ChatColor.GREEN + "google.com/test" ); + + Assert.assertEquals( "Text http://spigotmc.org google.com/test", BaseComponent.toPlainText( test2 ) ); + //The extra ChatColor.WHITEs are sometimes inserted when not needed but it doesn't change the result + Assert.assertEquals( ChatColor.WHITE + "Text " + ChatColor.WHITE + "http://spigotmc.org" + ChatColor.WHITE + + " " + ChatColor.GREEN + "google.com/test", BaseComponent.toLegacyText( test2 ) ); + + ClickEvent url1 = test2[1].getClickEvent(); + Assert.assertNotNull( url1 ); + Assert.assertTrue( url1.getAction() == ClickEvent.Action.OPEN_URL ); + Assert.assertEquals( "http://spigotmc.org", url1.getValue() ); + + ClickEvent url2 = test2[3].getClickEvent(); + Assert.assertNotNull( url2 ); + Assert.assertTrue( url2.getAction() == ClickEvent.Action.OPEN_URL ); + Assert.assertEquals( "http://google.com/test", url2.getValue() ); + } + + @Test + public void testTranslateComponent() + { + TranslatableComponent item = new TranslatableComponent( "item.swordGold.name" ); + item.setColor( ChatColor.AQUA ); + TranslatableComponent translatableComponent = new TranslatableComponent( "commands.give.success", + item, "5", + "thinkofdeath" ); + + Assert.assertEquals( "Given Golden Sword * 5 to thinkofdeath", translatableComponent.toPlainText() ); + Assert.assertEquals( ChatColor.WHITE + "Given " + ChatColor.AQUA + "Golden Sword" + ChatColor.WHITE + + " * " + ChatColor.WHITE + "5" + ChatColor.WHITE + " to " + ChatColor.WHITE + "thinkofdeath", + translatableComponent.toLegacyText() ); + + TranslatableComponent positional = new TranslatableComponent( "book.pageIndicator", "5", "50" ); + + Assert.assertEquals( "Page 5 of 50", positional.toPlainText() ); + Assert.assertEquals( ChatColor.WHITE + "Page " + ChatColor.WHITE + "5" + ChatColor.WHITE + " of " + ChatColor.WHITE + "50", positional.toLegacyText() ); + } + + @Test + public void testBuilder() + { + BaseComponent[] components = new ComponentBuilder( "Hello " ).color( ChatColor.RED ). + append( "World" ).bold( true ).color( ChatColor.BLUE ). + append( "!" ).color( ChatColor.YELLOW ).create(); + + Assert.assertEquals( "Hello World!", BaseComponent.toPlainText( components ) ); + Assert.assertEquals( ChatColor.RED + "Hello " + ChatColor.BLUE + ChatColor.BOLD + + "World" + ChatColor.YELLOW + ChatColor.BOLD + "!", BaseComponent.toLegacyText( components ) ); + } + +}