13
0
geforkt von Mirrors/Paper

#707, SPIGOT-5063, SPIGOT-5304, SPIGOT-5656, SPIGOT-3206, SPIGOT-5350, SPIGOT-5980, SPIGOT-4672: Persist the exact internal text representation where possible.

Issues resolved by this:
* SPIGOT-5063: Internal text representation of ItemStacks changes during ItemStack serialization. This issue was initially primarily concerned with the conversion between color text attributes to legacy color codes.
* SPIGOT-5304: Internal text representation of ItemStacks changes when opening the inventory (in creative mode). In particularly, this issue is also concerned with the conversion between plain text representations to non-plain ones.
* SPIGOT-5656: Internal text representation of ItemStacks changes during ItemStack serialization. This issue is particularly concerned with reordering of text attributes in the text's Json representation.
* SPIGOT-3206: Internal text representation of book pages changes during ItemStack serialization.
* SPIGOT-5350: Any non-plain text features are stripped from books during ItemStack serialization.
* SPIGOT-5980: Written books are marked as 'resolved' during ItemStack serialization and on various inventory interactions, even though they aren't, and thereby breaking any non-resolved page contents.
* SPIGOT-4672: Since item display names are serialized in their internal Json representation, any translatable components get properly persisted as well now.

---------

Minecraft uses text components to represent text. Internally Minecraft stores these components as Json formatted Strings and dynamically parses the text components from this Json representation whenever required.

In some cases Minecraft will create the text components and then convert them to Json itself for the internal storage. In other cases the Json representation is specified by users (eg. in Minecraft give commands, loot tables, mob equipment specified via Minecraft's summon commands, etc.).
There are many different ways in which the same text components can be represented in Json. When Minecraft compares objects which store this textual information, it takes the exact Json representation into account to determine whether the objects are considered equal. For example, ItemStacks will not match (and therefore not stack) if there is a difference in this internal Json representation for at least one if the item's text attributes (such as display name, lore, book pages, etc.). And when specifying nbt data in command selectors (eg. to only match entities/players which hold an item with specific name), the selector compares the raw Json representation as well.

As long as the Json representation is valid and can be parsed, Minecraft will not modify or normalize it.
However, under various circumstances Spigot converts this text information from the internal Json representation to text components (and in some cases even to plain text with legacy color codes) and then later tries to convert the text from these representations back to text components in the Json representation. Because this backwards conversion is in many cases not able to reproduce the original Json representation, the internal data of some affected Minecraft objects (ItemStacks, TileEntities, Entities, etc.) will in some cases get modified.

One especially notable situation in which this issue can come up is Bukkit's configuration serialization of ItemStacks: When a plugin serializes and later deserializes ItemStacks with display name, localized name, lore, or book pages of signed books, Spigot would convert these textual ItemStack attributes to plain text with legacy color codes and later try to convert those back to chat components in the Json representation. If the reconstructed Json representation does not match the original representation, the deserialized ItemStacks would no longer match nor stack with any original ItemStacks.

This case is particularly common if the original ItemStacks are created by users via some vanilla Minecraft mechanism (eg. Minecraft's give command, loot tables, mob equipment specified via Minecraft's summon command, etc.) and the used internal text representation for the created ItemStacks does not match the text representation produced by Spigot. This is also quite likely to be case, because the internal text representation produced by Spigot can sometimes be slightly verbose and, until recently, contained legacy color codes which cannot be used in Minecraft commands in-game.
However, this issue is not limited to items created by users, but affects items created by Minecraft itself as well.

Other cases in which Spigot itself (without any plugins involved) will convert between these text representations include dragging items around inside the inventory or opening the inventory while in creative mode. In these cases Spigot creates Bukkit representations of the affected items for use in Bukkit events and then, after the events have been handled, converts these Bukkit representations back to Minecraft items. See for example SPIGOT-5656 and SPIGOT-5304.

The idea of these changes is to avoid this back and forth conversion between the internal Json representation and the text component or plain text representations in various situations in which it is not actually required:
* CraftMetaItem stores the raw original Json representation for the display name, localized name, lore and pages of signed books now. As long as no plugin modifies these text attributes via the API, they can be reapplied in their original form to an ItemStack.
* The configuration serialization will serialize the original Json representation for these text attributes now so that it can also be restored during deserialization.
* However, in order to still be able to deserialize previously serialized items, and in order to allow users to specify text in the more simple plain representation in configuration files, we also still accept plain text during deserialization. Our approach is to check if the serialized text contains legacy color codes, in which case we convert it to chat components using our own converter and then to Json. Otherwise we try to parse it via Minecraft's Json parser. If the parsing fails due to the text not being valid Json, we interpret the text as plain text and convert it via our own converter as well.
* Various duplicated code has been removed from CraftMetaBookSigned and instead the base CraftMetaBook class allows sub classes to override the relevant aspects of how pages are parsed, serialized and deserialized.
* The BlockStates for command blocks and signs preemptively retrieved the custom name and sign line components, converted them to plain text and later converted them back to text components when applying the BlockState. We now only perform this conversion if a plugin has explicitly modified these texts.

Other changes:
* Minor: We also retrieve, convert and update a few other BlockState attributes directly from the underlying snapshot and only when requested by plugins now.
* SPIGOT-5980: Written books did not properly persist their 'resolved' attribute, resulting in unresolved book pages not getting resolved.
* There are methods to get and set the resolved value for books. However, these are not yet exposed in Bukkit.
* Minor fix: CraftMetaBook#isBookEmpty did not check some of the book attributes. This is probably a minor issue, but for consistency reasons there are checks for the missing attribute(s) now.

----
Covered cases
---
* By remembering the raw original String data, we can persist the exact text representation (eg. the ordering of elements within the Json text object (SPIGOT-5656), used style of escaping quotes (single quotes, escaped double quotes, etc.), use of plain texts (SPIGOT-5304), used boolean style, modern text component features such as translatable texts (SPIGOT-4672), etc.). All of these differences would otherwise cause the ItemStack to no longer be considered equal to its original.
* An empty String in the serialized config data results in no display name rather than an empty display name, like before. An item with explicitly empty display name (`{display: {Name: '""'}}`) is saved as `'""'` and can also be loaded from that representation again.
* Any plain texts, with or without color codes, which don't parse as Json (eg. `display-name: 'Bla'`) are still getting run through Spigot's text to components converter, like before.
* We can now also persist empty but explicitly present lore (`{display:{Lore:[]}}`). Previously this would get removed when the ItemMeta gets reapplied to the item. And ItemMeta#equals would return true for items with and without this empty lore data, even though Minecraft considers them to be different. For plugins using the API there should be no change: #hasLore still checks whether the lore is both present and not empty, and #getLore returns an empty list instead of null in this case (however, it previously already returned an empty list in this case). And setting the lore to an empty list via #setLore will still result in an item with no lore.
* Similarly, we can also persist explicitly specified but empty lists of book pages now.

----
Cases that are not covered (i.e. which may lead to changes in items), but were already not covered previously:
----
* NBT data for text that is not actually of type String.
* Empty or unexpected entries within the display compound.
* Variations in the NBT data representation in item features other than the above mentioned ones.
* Texts containing color codes. During deserialization these texts get interpreted as plain text and converted to a text component representation. This will break the serialization of any ItemStacks which actually use a text component representation with embedded color codes for some reason. Usually the likelihood for encountering such items in practice would probably be small. However, in the past (pre MC 1.16) Spigot would actually produce such items during ItemStack deserialization or when plugins created ItemStack via the Bukkit API. However, Spigot has changed the text representation it produces in MC 1.16, so any previously created and still existing items with this text representation are already problematic anyways now. See SPIGOT-5964. A fix for this linked issue (eg. the automatic conversion of these items) would probably resolve this deficit here as well.
* Spigot's String to text components converter produces quite verbose components since 1.16. See SPIGOT-5964 as well. However, this applies regardless of the changes of this PR.
* Book ItemStacks with more pages than 100 pages or oversized pages are truncated (like before) and may therefore change.
hange.

By: blablubbabc <lukas@wirsindwir.de>
Dieser Commit ist enthalten in:
CraftBukkit/Spigot 2021-01-01 08:53:14 +11:00
Ursprung 9bda60d8b4
Commit cf47c45e20
8 geänderte Dateien mit 393 neuen und 253 gelöschten Zeilen

Datei anzeigen

@ -8,9 +8,6 @@ import org.bukkit.craftbukkit.util.CraftChatMessage;
public class CraftCommandBlock extends CraftBlockEntityState<TileEntityCommand> implements CommandBlock {
private String command;
private String name;
public CraftCommandBlock(Block block) {
super(block, TileEntityCommand.class);
}
@ -19,39 +16,23 @@ public class CraftCommandBlock extends CraftBlockEntityState<TileEntityCommand>
super(material, te);
}
@Override
public void load(TileEntityCommand commandBlock) {
super.load(commandBlock);
command = commandBlock.getCommandBlock().getCommand();
name = CraftChatMessage.fromComponent(commandBlock.getCommandBlock().getName());
}
@Override
public String getCommand() {
return command;
return getSnapshot().getCommandBlock().getCommand();
}
@Override
public void setCommand(String command) {
this.command = command != null ? command : "";
getSnapshot().getCommandBlock().setCommand(command != null ? command : "");
}
@Override
public String getName() {
return name;
return CraftChatMessage.fromComponent(getSnapshot().getCommandBlock().getName());
}
@Override
public void setName(String name) {
this.name = name != null ? name : "@";
}
@Override
public void applyTo(TileEntityCommand commandBlock) {
super.applyTo(commandBlock);
commandBlock.getCommandBlock().setCommand(command);
commandBlock.getCommandBlock().setName(CraftChatMessage.fromStringOrNull(name));
getSnapshot().getCommandBlock().setName(CraftChatMessage.fromStringOrNull(name != null ? name : "@"));
}
}

Datei anzeigen

@ -12,8 +12,9 @@ import org.bukkit.craftbukkit.util.CraftChatMessage;
public class CraftSign extends CraftBlockEntityState<TileEntitySign> implements Sign {
private String[] lines;
private boolean editable;
// Lazily initialized only if requested:
private String[] originalLines = null;
private String[] lines = null;
public CraftSign(final Block block) {
super(block, TileEntitySign.class);
@ -24,37 +25,36 @@ public class CraftSign extends CraftBlockEntityState<TileEntitySign> implements
}
@Override
public void load(TileEntitySign sign) {
super.load(sign);
public String[] getLines() {
if (lines == null) {
// Lazy initialization:
TileEntitySign sign = this.getSnapshot();
lines = new String[sign.lines.length];
System.arraycopy(revertComponents(sign.lines), 0, lines, 0, lines.length);
editable = sign.isEditable;
originalLines = new String[lines.length];
System.arraycopy(lines, 0, originalLines, 0, originalLines.length);
}
@Override
public String[] getLines() {
return lines;
}
@Override
public String getLine(int index) throws IndexOutOfBoundsException {
return lines[index];
return getLines()[index];
}
@Override
public void setLine(int index, String line) throws IndexOutOfBoundsException {
lines[index] = line;
getLines()[index] = line;
}
@Override
public boolean isEditable() {
return this.editable;
return getSnapshot().isEditable;
}
@Override
public void setEditable(boolean editable) {
this.editable = editable;
getSnapshot().isEditable = editable;
}
@Override
@ -71,9 +71,15 @@ public class CraftSign extends CraftBlockEntityState<TileEntitySign> implements
public void applyTo(TileEntitySign sign) {
super.applyTo(sign);
IChatBaseComponent[] newLines = sanitizeLines(lines);
System.arraycopy(newLines, 0, sign.lines, 0, 4);
sign.isEditable = editable;
if (lines != null) {
for (int i = 0; i < lines.length; i++) {
String line = (lines[i] == null) ? "" : lines[i];
if (line.equals(originalLines[i])) {
continue; // The line contents are still the same, skip.
}
sign.lines[i] = CraftChatMessage.fromString(line)[0];
}
}
}
public static IChatBaseComponent[] sanitizeLines(String[] lines) {

Datei anzeigen

@ -14,8 +14,6 @@ import java.util.stream.Collectors;
import javax.annotation.Nullable;
import net.minecraft.server.BlockPosition;
import net.minecraft.server.BlockPropertyInstrument;
import net.minecraft.server.ChatMessage;
import net.minecraft.server.ChatModifier;
import net.minecraft.server.Container;
import net.minecraft.server.ContainerMerchant;
import net.minecraft.server.DamageSource;
@ -1290,10 +1288,6 @@ public class CraftEventFactory {
itemInHand.setItem(Items.WRITTEN_BOOK);
}
CraftMetaBook meta = (CraftMetaBook) editBookEvent.getNewBookMeta();
List<IChatBaseComponent> pages = meta.pages;
for (int i = 0; i < pages.size(); i++) {
pages.set(i, stripEvents(pages.get(i)));
}
CraftItemStack.setItemMeta(itemInHand, meta);
}
}
@ -1301,31 +1295,6 @@ public class CraftEventFactory {
return itemInHand;
}
private static IChatBaseComponent stripEvents(IChatBaseComponent c) {
ChatModifier modi = c.getChatModifier();
if (modi != null) {
modi = modi.setChatClickable(null);
modi = modi.setChatHoverable(null);
}
if (c instanceof ChatMessage) {
ChatMessage cm = (ChatMessage) c;
Object[] oo = cm.getArgs();
for (int i = 0; i < oo.length; i++) {
Object o = oo[i];
if (o instanceof IChatBaseComponent) {
oo[i] = stripEvents((IChatBaseComponent) o);
}
}
}
List<IChatBaseComponent> ls = c.getSiblings();
if (ls != null) {
for (int i = 0; i < ls.size(); i++) {
ls.set(i, stripEvents(ls.get(i)));
}
}
return c.mutableCopy().setChatModifier(modi);
}
public static PlayerUnleashEntityEvent callPlayerUnleashEntityEvent(EntityInsentient entity, EntityHuman player) {
PlayerUnleashEntityEvent event = new PlayerUnleashEntityEvent(entity.getBukkitEntity(), (Player) player.getBukkitEntity());
entity.world.getServer().getPluginManager().callEvent(event);

Datei anzeigen

@ -1,12 +1,14 @@
package org.bukkit.craftbukkit.inventory;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.ImmutableMap.Builder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import net.minecraft.server.IChatBaseComponent;
import net.minecraft.server.IChatBaseComponent.ChatSerializer;
import net.minecraft.server.NBTTagCompound;
import net.minecraft.server.NBTTagList;
import net.minecraft.server.NBTTagString;
@ -31,7 +33,11 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta {
protected String title;
protected String author;
public List<IChatBaseComponent> pages = new ArrayList<IChatBaseComponent>();
// We store the pages in their raw original text representation. See SPIGOT-5063, SPIGOT-5350, SPIGOT-3206
// For writable books (CraftMetaBook) the pages are stored as plain Strings.
// For written books (CraftMetaBookSigned) the pages are stored in Minecraft's JSON format.
protected List<String> pages; // null and empty are two different states internally
protected Boolean resolved = null;
protected Integer generation;
CraftMetaBook(CraftMetaItem meta) {
@ -41,16 +47,36 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta {
CraftMetaBook bookMeta = (CraftMetaBook) meta;
this.title = bookMeta.title;
this.author = bookMeta.author;
pages.addAll(bookMeta.pages);
this.resolved = bookMeta.resolved;
this.generation = bookMeta.generation;
if (bookMeta.pages != null) {
this.pages = new ArrayList<String>(bookMeta.pages.size());
if (meta instanceof CraftMetaBookSigned) {
if (this instanceof CraftMetaBookSigned) {
pages.addAll(bookMeta.pages);
} else {
// Convert from JSON to plain Strings:
pages.addAll(Lists.transform(bookMeta.pages, CraftChatMessage::fromJSONComponent));
}
} else {
if (this instanceof CraftMetaBookSigned) {
// Convert from plain Strings to JSON:
// This happens for example during book signing.
for (String page : bookMeta.pages) {
// We don't insert any non-plain text features (such as clickable links) during this conversion.
IChatBaseComponent component = CraftChatMessage.fromString(page, true, true)[0];
pages.add(CraftChatMessage.toJSON(component));
}
} else {
pages.addAll(bookMeta.pages);
}
}
}
}
}
CraftMetaBook(NBTTagCompound tag) {
this(tag, true);
}
CraftMetaBook(NBTTagCompound tag, boolean handlePages) {
super(tag);
if (tag.hasKey(BOOK_TITLE.NBT)) {
@ -61,29 +87,32 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta {
this.author = tag.getString(BOOK_AUTHOR.NBT);
}
boolean resolved = false;
if (tag.hasKey(RESOLVED.NBT)) {
resolved = tag.getBoolean(RESOLVED.NBT);
this.resolved = tag.getBoolean(RESOLVED.NBT);
}
if (tag.hasKey(GENERATION.NBT)) {
generation = tag.getInt(GENERATION.NBT);
}
if (tag.hasKey(BOOK_PAGES.NBT) && handlePages) {
if (tag.hasKey(BOOK_PAGES.NBT)) {
NBTTagList pages = tag.getList(BOOK_PAGES.NBT, CraftMagicNumbers.NBT.TAG_STRING);
this.pages = new ArrayList<String>(pages.size());
boolean expectJson = (this instanceof CraftMetaBookSigned);
// Note: We explicitly check for and truncate oversized books and pages,
// because they can come directly from clients when handling book edits.
for (int i = 0; i < Math.min(pages.size(), MAX_PAGES); i++) {
String page = pages.getString(i);
if (resolved) {
try {
this.pages.add(ChatSerializer.a(page));
continue;
} catch (Exception e) {
// Ignore and treat as an old book
// There was an issue on previous Spigot versions which would
// result in book items with pages in the wrong text
// representation. See SPIGOT-182, SPIGOT-164
if (expectJson) {
page = CraftChatMessage.fromJSONOrStringToJSON(page, false, true, MAX_PAGE_LENGTH, false);
} else {
page = validatePage(page);
}
}
addPage(page);
this.pages.add(page);
}
}
}
@ -97,22 +126,35 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta {
Iterable<?> pages = SerializableMeta.getObject(Iterable.class, map, BOOK_PAGES.BUKKIT, true);
if (pages != null) {
this.pages = new ArrayList<String>();
for (Object page : pages) {
if (page instanceof String) {
addPage((String) page);
internalAddPage(deserializePage((String) page));
}
}
}
resolved = SerializableMeta.getObject(Boolean.class, map, RESOLVED.BUKKIT, true);
generation = SerializableMeta.getObject(Integer.class, map, GENERATION.BUKKIT, true);
}
protected String deserializePage(String pageData) {
// We expect the page data to already be a plain String.
return validatePage(pageData);
}
protected String convertPlainPageToData(String page) {
// Writable books store their data as plain Strings, so we don't need to convert anything.
return page;
}
protected String convertDataToPlainPage(String pageData) {
// pageData is expected to already be a plain String.
return pageData;
}
@Override
void applyToItem(NBTTagCompound itemData) {
applyToItem(itemData, true);
}
void applyToItem(NBTTagCompound itemData, boolean handlePages) {
super.applyToItem(itemData);
if (hasTitle()) {
@ -123,16 +165,16 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta {
itemData.setString(BOOK_AUTHOR.NBT, this.author);
}
if (handlePages) {
if (hasPages()) {
if (pages != null) {
NBTTagList list = new NBTTagList();
for (IChatBaseComponent page : pages) {
list.add(NBTTagString.a(page == null ? "" : CraftChatMessage.fromComponent(page)));
for (String page : pages) {
list.add(NBTTagString.a(page));
}
itemData.set(BOOK_PAGES.NBT, list);
}
itemData.remove(RESOLVED.NBT);
if (resolved != null) {
itemData.setBoolean(RESOLVED.NBT, resolved);
}
if (generation != null) {
@ -146,7 +188,7 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta {
}
boolean isBookEmpty() {
return !(hasPages() || hasAuthor() || hasTitle());
return !((pages != null) || hasAuthor() || hasTitle() || hasGeneration() || (resolved != null));
}
@Override
@ -172,7 +214,7 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta {
@Override
public boolean hasPages() {
return !pages.isEmpty();
return (pages != null) && !pages.isEmpty();
}
@Override
@ -221,69 +263,98 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta {
@Override
public String getPage(final int page) {
Validate.isTrue(isValidPage(page), "Invalid page number");
return CraftChatMessage.fromComponent(pages.get(page - 1));
// assert: pages != null
return convertDataToPlainPage(pages.get(page - 1));
}
@Override
public void setPage(final int page, final String text) {
if (!isValidPage(page)) {
throw new IllegalArgumentException("Invalid page number " + page + "/" + pages.size());
throw new IllegalArgumentException("Invalid page number " + page + "/" + getPageCount());
}
// assert: pages != null
String newText = text == null ? "" : text.length() > MAX_PAGE_LENGTH ? text.substring(0, MAX_PAGE_LENGTH) : text;
pages.set(page - 1, CraftChatMessage.fromString(newText, true)[0]);
String newText = validatePage(text);
pages.set(page - 1, convertPlainPageToData(newText));
}
@Override
public void setPages(final String... pages) {
this.pages.clear();
addPage(pages);
setPages(Arrays.asList(pages));
}
@Override
public void addPage(final String... pages) {
for (String page : pages) {
if (this.pages.size() >= MAX_PAGES) {
return;
page = validatePage(page);
internalAddPage(convertPlainPageToData(page));
}
}
String validatePage(String page) {
if (page == null) {
page = "";
} else if (page.length() > MAX_PAGE_LENGTH) {
page = page.substring(0, MAX_PAGE_LENGTH);
}
this.pages.add(CraftChatMessage.fromString(page, true)[0]);
return page;
}
private void internalAddPage(String page) {
// asserted: page != null
if (this.pages == null) {
this.pages = new ArrayList<String>();
} else if (this.pages.size() >= MAX_PAGES) {
return;
}
this.pages.add(page);
}
@Override
public int getPageCount() {
return pages.size();
return (pages == null) ? 0 : pages.size();
}
@Override
public List<String> getPages() {
return pages.stream().map(CraftChatMessage::fromComponent).collect(ImmutableList.toImmutableList());
if (pages == null) return ImmutableList.of();
return pages.stream().map(this::convertDataToPlainPage).collect(ImmutableList.toImmutableList());
}
@Override
public void setPages(List<String> pages) {
if (pages.isEmpty()) {
this.pages = null;
return;
}
if (this.pages != null) {
this.pages.clear();
}
for (String page : pages) {
addPage(page);
}
}
private boolean isValidPage(int page) {
return page > 0 && page <= pages.size();
return page > 0 && page <= getPageCount();
}
// TODO Expose this attribute in Bukkit?
public boolean isResolved() {
return (resolved == null) ? false : resolved;
}
public void setResolved(boolean resolved) {
this.resolved = resolved;
}
@Override
public CraftMetaBook clone() {
CraftMetaBook meta = (CraftMetaBook) super.clone();
meta.pages = new ArrayList<IChatBaseComponent>(pages);
if (this.pages != null) {
meta.pages = new ArrayList<String>(this.pages);
}
return meta;
}
@ -297,9 +368,12 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta {
if (hasAuthor()) {
hash = 61 * hash + 13 * this.author.hashCode();
}
if (hasPages()) {
if (this.pages != null) {
hash = 61 * hash + 17 * this.pages.hashCode();
}
if (this.resolved != null) {
hash = 61 * hash + 17 * this.resolved.hashCode();
}
if (hasGeneration()) {
hash = 61 * hash + 19 * this.generation.hashCode();
}
@ -316,7 +390,8 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta {
return (hasTitle() ? that.hasTitle() && this.title.equals(that.title) : !that.hasTitle())
&& (hasAuthor() ? that.hasAuthor() && this.author.equals(that.author) : !that.hasAuthor())
&& (hasPages() ? that.hasPages() && this.pages.equals(that.pages) : !that.hasPages())
&& (Objects.equals(this.pages, that.pages))
&& (Objects.equals(this.resolved, that.resolved))
&& (hasGeneration() ? that.hasGeneration() && this.generation.equals(that.generation) : !that.hasGeneration());
}
return true;
@ -339,12 +414,12 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta {
builder.put(BOOK_AUTHOR.BUKKIT, author);
}
if (hasPages()) {
List<String> pagesString = new ArrayList<String>();
for (IChatBaseComponent comp : pages) {
pagesString.add(CraftChatMessage.fromComponent(comp));
if (pages != null) {
builder.put(BOOK_PAGES.BUKKIT, ImmutableList.copyOf(pages));
}
builder.put(BOOK_PAGES.BUKKIT, pagesString);
if (resolved != null) {
builder.put(RESOLVED.BUKKIT, resolved);
}
if (generation != null) {

Datei anzeigen

@ -2,15 +2,11 @@ package org.bukkit.craftbukkit.inventory;
import com.google.common.collect.ImmutableMap.Builder;
import java.util.Map;
import net.minecraft.server.IChatBaseComponent;
import net.minecraft.server.IChatBaseComponent.ChatSerializer;
import net.minecraft.server.NBTTagCompound;
import net.minecraft.server.NBTTagList;
import net.minecraft.server.NBTTagString;
import org.bukkit.Material;
import org.bukkit.configuration.serialization.DelegateDeserialization;
import org.bukkit.craftbukkit.inventory.CraftMetaItem.SerializableMeta;
import org.bukkit.craftbukkit.util.CraftMagicNumbers;
import org.bukkit.craftbukkit.util.CraftChatMessage;
import org.bukkit.inventory.meta.BookMeta;
@DelegateDeserialization(SerializableMeta.class)
@ -21,61 +17,31 @@ class CraftMetaBookSigned extends CraftMetaBook implements BookMeta {
}
CraftMetaBookSigned(NBTTagCompound tag) {
super(tag, false);
boolean resolved = true;
if (tag.hasKey(RESOLVED.NBT)) {
resolved = tag.getBoolean(RESOLVED.NBT);
}
if (tag.hasKey(BOOK_PAGES.NBT)) {
NBTTagList pages = tag.getList(BOOK_PAGES.NBT, CraftMagicNumbers.NBT.TAG_STRING);
for (int i = 0; i < Math.min(pages.size(), MAX_PAGES); i++) {
String page = pages.getString(i);
if (resolved) {
try {
this.pages.add(ChatSerializer.a(page));
continue;
} catch (Exception e) {
// Ignore and treat as an old book
}
}
addPage(page);
}
}
super(tag);
}
CraftMetaBookSigned(Map<String, Object> map) {
super(map);
}
@Override
protected String deserializePage(String pageData) {
return CraftChatMessage.fromJSONOrStringToJSON(pageData, false, true, MAX_PAGE_LENGTH, false);
}
@Override
protected String convertPlainPageToData(String page) {
return CraftChatMessage.fromStringToJSON(page, true);
}
@Override
protected String convertDataToPlainPage(String pageData) {
return CraftChatMessage.fromJSONComponent(pageData);
}
@Override
void applyToItem(NBTTagCompound itemData) {
super.applyToItem(itemData, false);
if (hasTitle()) {
itemData.setString(BOOK_TITLE.NBT, this.title);
}
if (hasAuthor()) {
itemData.setString(BOOK_AUTHOR.NBT, this.author);
}
if (hasPages()) {
NBTTagList list = new NBTTagList();
for (IChatBaseComponent page : pages) {
list.add(NBTTagString.a(
ChatSerializer.a(page)
));
}
itemData.set(BOOK_PAGES.NBT, list);
}
itemData.setBoolean(RESOLVED.NBT, true);
if (generation != null) {
itemData.setInt(GENERATION.NBT, generation);
}
super.applyToItem(itemData);
}
@Override

Datei anzeigen

@ -10,7 +10,6 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import com.google.gson.JsonParseException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@ -31,6 +30,7 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -38,7 +38,6 @@ import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import net.minecraft.server.ChatComponentText;
import net.minecraft.server.EnumItemSlot;
import net.minecraft.server.IChatBaseComponent;
import net.minecraft.server.ItemBlock;
import net.minecraft.server.NBTBase;
import net.minecraft.server.NBTCompressedStreamTools;
@ -262,9 +261,10 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
static final ItemMetaKey BLOCK_DATA = new ItemMetaKey("BlockStateTag");
static final ItemMetaKey BUKKIT_CUSTOM_TAG = new ItemMetaKey("PublicBukkitValues");
private IChatBaseComponent displayName;
private IChatBaseComponent locName;
private List<IChatBaseComponent> lore;
// We store the raw original JSON representation of all text data. See SPIGOT-5063, SPIGOT-5656, SPIGOT-5304
private String displayName;
private String locName;
private List<String> lore; // null and empty are two different states internally
private Integer customModelData;
private NBTTagCompound blockData;
private Map<Enchantment, Integer> enchantments;
@ -291,8 +291,8 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
this.displayName = meta.displayName;
this.locName = meta.locName;
if (meta.hasLore()) {
this.lore = new ArrayList<IChatBaseComponent>(meta.lore);
if (meta.lore != null) {
this.lore = new ArrayList<String>(meta.lore);
}
this.customModelData = meta.customModelData;
@ -326,32 +326,19 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
NBTTagCompound display = tag.getCompound(DISPLAY.NBT);
if (display.hasKey(NAME.NBT)) {
try {
displayName = IChatBaseComponent.ChatSerializer.a(display.getString(NAME.NBT));
} catch (JsonParseException ex) {
// Ignore (stripped like Vanilla)
}
displayName = display.getString(NAME.NBT);
}
if (display.hasKey(LOCNAME.NBT)) {
try {
locName = IChatBaseComponent.ChatSerializer.a(display.getString(LOCNAME.NBT));
} catch (JsonParseException ex) {
// Ignore (stripped like Vanilla)
}
locName = display.getString(LOCNAME.NBT);
}
if (display.hasKey(LORE.NBT)) {
NBTTagList list = display.getList(LORE.NBT, CraftMagicNumbers.NBT.TAG_STRING);
lore = new ArrayList<IChatBaseComponent>(list.size());
lore = new ArrayList<String>(list.size());
for (int index = 0; index < list.size(); index++) {
String line = list.getString(index);
try {
lore.add(IChatBaseComponent.ChatSerializer.a(line));
} catch (JsonParseException ex) {
// Ignore (stripped like Vanilla)
}
lore.add(line);
}
}
}
@ -474,12 +461,13 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
}
CraftMetaItem(Map<String, Object> map) {
setDisplayName(SerializableMeta.getString(map, NAME.BUKKIT, true));
setLocalizedName(SerializableMeta.getString(map, LOCNAME.BUKKIT, true));
displayName = CraftChatMessage.fromJSONOrStringOrNullToJSON(SerializableMeta.getString(map, NAME.BUKKIT, true));
locName = CraftChatMessage.fromJSONOrStringOrNullToJSON(SerializableMeta.getString(map, LOCNAME.BUKKIT, true));
Iterable<?> lore = SerializableMeta.getObject(Iterable.class, map, LORE.BUKKIT, true);
if (lore != null) {
safelyAdd(lore, this.lore = new ArrayList<IChatBaseComponent>(), Integer.MAX_VALUE);
safelyAdd(lore, this.lore = new ArrayList<String>(), true);
}
Integer customModelData = SerializableMeta.getObject(Integer.class, map, CUSTOM_MODEL_DATA.BUKKIT, true);
@ -615,13 +603,13 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
@Overridden
void applyToItem(NBTTagCompound itemTag) {
if (hasDisplayName()) {
setDisplayTag(itemTag, NAME.NBT, NBTTagString.a(CraftChatMessage.toJSON(displayName)));
setDisplayTag(itemTag, NAME.NBT, NBTTagString.a(displayName));
}
if (hasLocalizedName()) {
setDisplayTag(itemTag, LOCNAME.NBT, NBTTagString.a(CraftChatMessage.toJSON(locName)));
setDisplayTag(itemTag, LOCNAME.NBT, NBTTagString.a(locName));
}
if (hasLore()) {
if (lore != null) {
setDisplayTag(itemTag, LORE.NBT, createStringList(lore));
}
@ -667,15 +655,15 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
}
}
NBTTagList createStringList(List<IChatBaseComponent> list) {
if (list == null || list.isEmpty()) {
NBTTagList createStringList(List<String> list) {
if (list == null) {
return null;
}
NBTTagList tagList = new NBTTagList();
for (IChatBaseComponent value : list) {
for (String value : list) {
// SPIGOT-5342 - horrible hack as 0 version does not go through the Mojang updater
tagList.add(NBTTagString.a(version <= 0 || version >= 1803 ? CraftChatMessage.toJSON(value) : CraftChatMessage.fromComponent(value))); // SPIGOT-4935
tagList.add(NBTTagString.a(version <= 0 || version >= 1803 ? value : CraftChatMessage.fromJSONComponent(value))); // SPIGOT-4935
}
return tagList;
@ -750,17 +738,17 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
@Overridden
boolean isEmpty() {
return !(hasDisplayName() || hasLocalizedName() || hasEnchants() || hasLore() || hasCustomModelData() || hasBlockData() || hasRepairCost() || !unhandledTags.isEmpty() || !persistentDataContainer.isEmpty() || hideFlag != 0 || isUnbreakable() || hasDamage() || hasAttributeModifiers());
return !(hasDisplayName() || hasLocalizedName() || hasEnchants() || (lore != null) || hasCustomModelData() || hasBlockData() || hasRepairCost() || !unhandledTags.isEmpty() || !persistentDataContainer.isEmpty() || hideFlag != 0 || isUnbreakable() || hasDamage() || hasAttributeModifiers());
}
@Override
public String getDisplayName() {
return CraftChatMessage.fromComponent(displayName);
return CraftChatMessage.fromJSONComponent(displayName);
}
@Override
public final void setDisplayName(String name) {
this.displayName = CraftChatMessage.fromStringOrNull(name);
this.displayName = CraftChatMessage.fromStringOrNullToJSON(name);
}
@Override
@ -770,12 +758,12 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
@Override
public String getLocalizedName() {
return CraftChatMessage.fromComponent(locName);
return CraftChatMessage.fromJSONComponent(locName);
}
@Override
public void setLocalizedName(String name) {
this.locName = CraftChatMessage.fromStringOrNull(name);
this.locName = CraftChatMessage.fromStringOrNullToJSON(name);
}
@Override
@ -883,20 +871,20 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
@Override
public List<String> getLore() {
return this.lore == null ? null : new ArrayList<String>(Lists.transform(this.lore, CraftChatMessage::fromComponent));
return this.lore == null ? null : new ArrayList<String>(Lists.transform(this.lore, CraftChatMessage::fromJSONComponent));
}
@Override
public void setLore(List<String> lore) { // too tired to think if .clone is better
if (lore == null) {
public void setLore(List<String> lore) {
if (lore == null || lore.isEmpty()) {
this.lore = null;
} else {
if (this.lore == null) {
safelyAdd(lore, this.lore = new ArrayList<IChatBaseComponent>(lore.size()), Integer.MAX_VALUE);
this.lore = new ArrayList<String>(lore.size());
} else {
this.lore.clear();
safelyAdd(lore, this.lore, Integer.MAX_VALUE);
}
safelyAdd(lore, this.lore, false);
}
}
@ -1133,7 +1121,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
return ((this.hasDisplayName() ? that.hasDisplayName() && this.displayName.equals(that.displayName) : !that.hasDisplayName()))
&& (this.hasLocalizedName() ? that.hasLocalizedName() && this.locName.equals(that.locName) : !that.hasLocalizedName())
&& (this.hasEnchants() ? that.hasEnchants() && this.enchantments.equals(that.enchantments) : !that.hasEnchants())
&& (this.hasLore() ? that.hasLore() && this.lore.equals(that.lore) : !that.hasLore())
&& (Objects.equals(this.lore, that.lore))
&& (this.hasCustomModelData() ? that.hasCustomModelData() && this.customModelData.equals(that.customModelData) : !that.hasCustomModelData())
&& (this.hasBlockData() ? that.hasBlockData() && this.blockData.equals(that.blockData) : !that.hasBlockData())
&& (this.hasRepairCost() ? that.hasRepairCost() && this.repairCost == that.repairCost : !that.hasRepairCost())
@ -1166,7 +1154,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
int hash = 3;
hash = 61 * hash + (hasDisplayName() ? this.displayName.hashCode() : 0);
hash = 61 * hash + (hasLocalizedName() ? this.locName.hashCode() : 0);
hash = 61 * hash + (hasLore() ? this.lore.hashCode() : 0);
hash = 61 * hash + ((lore != null) ? this.lore.hashCode() : 0);
hash = 61 * hash + (hasCustomModelData() ? this.customModelData.hashCode() : 0);
hash = 61 * hash + (hasBlockData() ? this.blockData.hashCode() : 0);
hash = 61 * hash + (hasEnchants() ? this.enchantments.hashCode() : 0);
@ -1187,7 +1175,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
try {
CraftMetaItem clone = (CraftMetaItem) super.clone();
if (this.lore != null) {
clone.lore = new ArrayList<IChatBaseComponent>(this.lore);
clone.lore = new ArrayList<String>(this.lore);
}
clone.customModelData = this.customModelData;
clone.blockData = this.blockData;
@ -1219,14 +1207,14 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
@Overridden
ImmutableMap.Builder<String, Object> serialize(ImmutableMap.Builder<String, Object> builder) {
if (hasDisplayName()) {
builder.put(NAME.BUKKIT, CraftChatMessage.fromComponent(displayName));
builder.put(NAME.BUKKIT, displayName);
}
if (hasLocalizedName()) {
builder.put(LOCNAME.BUKKIT, CraftChatMessage.fromComponent(locName));
builder.put(LOCNAME.BUKKIT, locName);
}
if (hasLore()) {
builder.put(LORE.BUKKIT, ImmutableList.copyOf(Lists.transform(lore, CraftChatMessage::fromComponent)));
if (lore != null) {
builder.put(LORE.BUKKIT, ImmutableList.copyOf(lore));
}
if (hasCustomModelData()) {
@ -1321,7 +1309,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
builder.put(key.BUKKIT, mods);
}
static void safelyAdd(Iterable<?> addFrom, Collection<IChatBaseComponent> addTo, int maxItemLength) {
static void safelyAdd(Iterable<?> addFrom, Collection<String> addTo, boolean possiblyJsonInput) {
if (addFrom == null) {
return;
}
@ -1332,15 +1320,15 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
throw new IllegalArgumentException(addFrom + " cannot contain non-string " + object.getClass().getName());
}
addTo.add(new ChatComponentText(""));
addTo.add(CraftChatMessage.toJSON(new ChatComponentText("")));
} else {
String page = object.toString();
String entry = object.toString();
if (page.length() > maxItemLength) {
page = page.substring(0, maxItemLength);
if (possiblyJsonInput) {
addTo.add(CraftChatMessage.fromJSONOrStringToJSON(entry));
} else {
addTo.add(CraftChatMessage.fromStringToJSON(entry));
}
addTo.add(CraftChatMessage.fromString(page)[0]);
}
}
}

Datei anzeigen

@ -2,6 +2,7 @@ package org.bukkit.craftbukkit.util;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.gson.JsonParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@ -23,6 +24,7 @@ public final class CraftChatMessage {
private static final Pattern LINK_PATTERN = Pattern.compile("((?:(?:https?):\\/\\/)?(?:[-\\w_\\.]{2,}\\.[a-z]{2,4}.*?(?=[\\.\\?!,;:]?(?:[" + String.valueOf(org.bukkit.ChatColor.COLOR_CHAR) + " \\n]|$))))");
private static final Map<Character, EnumChatFormat> formatMap;
private static final String COLOR_CHAR_STRING = String.valueOf(ChatColor.COLOR_CHAR);
static {
Builder<Character, EnumChatFormat> builder = ImmutableMap.builder();
@ -55,7 +57,7 @@ public final class CraftChatMessage {
private StringBuilder hex;
private final String message;
private StringMessage(String message, boolean keepNewlines) {
private StringMessage(String message, boolean keepNewlines, boolean plain) {
this.message = message;
if (message == null) {
output = new IChatBaseComponent[]{currentChatComponent};
@ -116,12 +118,16 @@ public final class CraftChatMessage {
needsAdd = true;
break;
case 2:
if (plain) {
appendNewComponent(matcher.end(groupId));
} else {
if (!(match.startsWith("http://") || match.startsWith("https://"))) {
match = "http://" + match;
}
modifier = modifier.setChatClickable(new ChatClickable(EnumClickAction.OPEN_URL, match));
appendNewComponent(matcher.end(groupId));
modifier = modifier.setChatClickable((ChatClickable) null);
}
break;
case 3:
if (needsAdd) {
@ -168,13 +174,135 @@ public final class CraftChatMessage {
}
public static IChatBaseComponent[] fromString(String message, boolean keepNewlines) {
return new StringMessage(message, keepNewlines).getOutput();
return fromString(message, keepNewlines, false);
}
public static IChatBaseComponent[] fromString(String message, boolean keepNewlines, boolean plain) {
return new StringMessage(message, keepNewlines, plain).getOutput();
}
public static String toJSON(IChatBaseComponent component) {
return IChatBaseComponent.ChatSerializer.a(component);
}
public static String toJSONOrNull(IChatBaseComponent component) {
if (component == null) return null;
return toJSON(component);
}
public static IChatBaseComponent fromJSON(String jsonMessage) throws JsonParseException {
// Note: This also parses plain Strings to text components.
return IChatBaseComponent.ChatSerializer.a(jsonMessage);
}
public static IChatBaseComponent fromJSONOrNull(String jsonMessage) {
// Note: An empty message is parsed to an empty text component instead of null.
if (jsonMessage == null) return null;
try {
return fromJSON(jsonMessage);
} catch (JsonParseException ex) {
return null;
}
}
public static IChatBaseComponent fromJSONOrString(String message) {
return fromJSONOrString(message, false);
}
public static IChatBaseComponent fromJSONOrString(String message, boolean keepNewlines) {
return fromJSONOrString(message, false, keepNewlines);
}
private static IChatBaseComponent fromJSONOrString(String message, boolean nullable, boolean keepNewlines) {
if (message == null) message = "";
if (nullable && message.isEmpty()) return null;
// If the message contains color codes, we convert it ourselves:
if (containsColorCodes(message)) {
return fromString(message, keepNewlines)[0];
} else {
try {
return fromJSON(message);
} catch (JsonParseException ex) {
return fromString(message, keepNewlines)[0];
}
}
}
public static String fromJSONOrStringToJSON(String message) {
return fromJSONOrStringToJSON(message, false);
}
public static String fromJSONOrStringToJSON(String message, boolean keepNewlines) {
return fromJSONOrStringToJSON(message, false, keepNewlines, Integer.MAX_VALUE, false);
}
public static String fromJSONOrStringOrNullToJSON(String message) {
return fromJSONOrStringOrNullToJSON(message, false);
}
public static String fromJSONOrStringOrNullToJSON(String message, boolean keepNewlines) {
return fromJSONOrStringToJSON(message, true, keepNewlines, Integer.MAX_VALUE, false);
}
public static String fromJSONOrStringToJSON(String message, boolean nullable, boolean keepNewlines, int maxLength, boolean checkJsonContentLength) {
if (message == null) message = "";
if (nullable && message.isEmpty()) return null;
// If the message contains color codes, we convert it ourselves:
if (containsColorCodes(message)) {
message = trimMessage(message, maxLength);
return fromStringToJSON(message, keepNewlines);
} else {
try {
// If the input can be parsed as JSON, we use that:
IChatBaseComponent component = fromJSON(message);
if (checkJsonContentLength) {
String content = fromComponent(component);
String trimmedContent = trimMessage(content, maxLength);
if (content != trimmedContent) { // identity comparison is fine here
// Note: The resulting text has all non-plain text features stripped.
return fromStringToJSON(trimmedContent, keepNewlines);
}
}
return message;
} catch (JsonParseException ex) {
// Else we convert the input:
message = trimMessage(message, maxLength);
return fromStringToJSON(message, keepNewlines);
}
}
}
public static String trimMessage(String message, int maxLength) {
if (message != null && message.length() > maxLength) {
return message.substring(0, maxLength);
} else {
return message;
}
}
public static boolean containsColorCodes(String message) {
return message != null && message.contains(COLOR_CHAR_STRING);
}
public static String fromStringToJSON(String message) {
return fromStringToJSON(message, false);
}
public static String fromStringToJSON(String message, boolean keepNewlines) {
IChatBaseComponent component = CraftChatMessage.fromString(message, keepNewlines)[0];
return CraftChatMessage.toJSON(component);
}
public static String fromStringOrNullToJSON(String message) {
IChatBaseComponent component = CraftChatMessage.fromStringOrNull(message);
return CraftChatMessage.toJSONOrNull(component);
}
public static String fromJSONComponent(String jsonMessage) {
IChatBaseComponent component = CraftChatMessage.fromJSONOrNull(jsonMessage);
return CraftChatMessage.fromComponent(component);
}
public static String fromComponent(IChatBaseComponent component) {
if (component == null) return "";
StringBuilder out = new StringBuilder();

Datei anzeigen

@ -1,6 +1,8 @@
package org.bukkit.craftbukkit.util;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import net.minecraft.server.ChatComponentText;
import net.minecraft.server.IChatBaseComponent;
import net.minecraft.server.IChatMutableComponent;
import org.junit.Test;
@ -50,6 +52,15 @@ public class CraftChatMessageTest {
testComponent("F§foo§bBar§rBaz", create("F§foo", "§bBar", "Baz"));
}
@Test
public void testPlainText() {
testPlainString("");
testPlainString("Foo§f§mBar§0");
testPlainString("Link to https://www.spigotmc.org/ ...");
testPlainString("Link to http://www.spigotmc.org/ ...");
testPlainString("Link to www.spigotmc.org ...");
}
private IChatBaseComponent create(String txt, String... rest) {
IChatMutableComponent cmp = CraftChatMessage.fromString(txt, false)[0].mutableCopy();
for (String s : rest) {
@ -77,6 +88,22 @@ public class CraftChatMessageTest {
assertEquals("\nComponent: " + cmp + "\n", expected, actual);
}
private void testPlainString(String expected) {
IChatBaseComponent component = CraftChatMessage.fromString(expected, false, true)[0];
String actual = CraftChatMessage.fromComponent(component);
assertEquals("fromComponent does not match input: " + component, expected, actual);
assertTrue("Non-plain component: " + component, !containsNonPlainComponent(component));
}
private boolean containsNonPlainComponent(IChatBaseComponent component) {
for (IChatBaseComponent c : component) {
if (!(c instanceof ChatComponentText)) {
return true;
}
}
return false;
}
private void testComponent(String expected, IChatBaseComponent cmp) {
String actual = CraftChatMessage.fromComponent(cmp);
assertEquals("\nComponent: " + cmp + "\n", expected, actual);