Hello,
I have recently been messing with tablist headers and footers, and noticed that when you just send a header and no footer has been sent (same goes for footer), the footer is sent as blank. I think the intended functionality is to not send anything, but because in implementation it sends a headerandfooter packet rather than individual ones when individual ones are set, it can send a blank space. This is what it looks like:

The only method I called was p.getTabList().setHeader(...), and only once. This is on a test server run through my IDE with no other plugins installed. I noticed that, in the code for SpongeTabList, it sends EMPTY_COMPONENT in the packet, and was wondering if sending a null value would work instead.
EDIT: The tablist displayname was set by the same plugin but it should not affect the header and footer.
You cannot send a null header or footer, but an empty translation component {"translate":""} will actually remove the header or footer.
Alright, I will try that in my code; however this is an issue in the SpongeCommon code, which has a section that sends a TextComponentString with an empty string (see here), which results in the blank space seen in the image.
@Wundero You cannot send a null header or footer. https://bugs.mojang.com/browse/MC-98180
That wasn't the point of the issue; the issue is that the SpongePowered code sends a blank string, which causes what the image shows. The reason I said null was because I figured that might have been a solution, however I now know that it is not. The solution that @Cybermaxke suggested will probably work, but don't just close the issue without reading into the problem.
@Cybermaxke In addition to my previous message, sending an empty component does not work. packetIn.getHeader().getFormattedText().isEmpty() is checked, which is _always_ false:
/**
* Gets the text of this component, with formatting codes added for rendering.
*/
public final String getFormattedText()
{
StringBuilder stringbuilder = new StringBuilder();
for (ITextComponent itextcomponent : this)
{
stringbuilder.append(itextcomponent.getStyle().getFormattingCode());
stringbuilder.append(itextcomponent.getUnformattedComponentText());
stringbuilder.append((Object)TextFormatting.RESET);
}
return stringbuilder.toString();
}
Ok, but that still doesn't solve the issue that blank space is appearing in tab. Based on what you suggest, the only solution is to submit a pull request that adds conditional packet sending, such that when either header or footer is null in the TabList class (they are Nullable as declared), it will only send the packet that changes the ones that are not null.
There is only one packet, used for both header and footer.
Then how would you suggest fixing the blank space that appears when I only change the header? I know it's possible.
public void handlePlayerListHeaderFooter(SPacketPlayerListHeaderFooter packetIn)
{
this.gameController.ingameGUI.getTabList().setHeader(packetIn.getHeader().getFormattedText().isEmpty() ? null : packetIn.getHeader());
this.gameController.ingameGUI.getTabList().setFooter(packetIn.getFooter().getFormattedText().isEmpty() ? null : packetIn.getFooter());
}
/**
* Gets the text of this component, with formatting codes added for rendering.
*/
public final String getFormattedText()
{
StringBuilder stringbuilder = new StringBuilder();
for (ITextComponent itextcomponent : this)
{
stringbuilder.append(itextcomponent.getStyle().getFormattingCode());
stringbuilder.append(itextcomponent.getUnformattedComponentText());
stringbuilder.append((Object)TextFormatting.RESET);
}
return stringbuilder.toString();
}
You cannot send a null header because getFormattedText would be called on a null.
You cannot send a null footer because getFormattedText would be called on a null.
You cannot send an empty header to remove the header, because getFormattedText is _never_ empty.
You cannot send an empty footer to remove the footer, because getFormattedText is _never_ empty.
@kashike
The default implementation TextComponentBase includes the itself in the iterator, but this method is overridden in the TextComponentTranslation, where itself is not included. So if you don't attach children to a translation component, the iterator will be empty, and the StringBuilder will be empty.
TextComponentBase:
public Iterator<ITextComponent> iterator()
{
return Iterators.concat(Iterators.forArray(new TextComponentBase[] { this }), createDeepCopyIterator(this.siblings));
}
TextComponentTranslation:
public Iterator<ITextComponent> iterator()
{
ensureInitialized();
return Iterators.concat(createDeepCopyIterator(this.children), createDeepCopyIterator(this.siblings));
}
@Cybermaxke Uh huh, but see that ensureInitialized there? That populates this.children.
/**
* ensures that our children are initialized from the most recent string translation mapping.
*/
synchronized void ensureInitialized()
{
synchronized (this.syncLock)
{
long i = I18n.getLastTranslationUpdateTimeInMilliseconds();
if (i == this.lastTranslationUpdateTimeInMilliseconds)
{
return;
}
this.lastTranslationUpdateTimeInMilliseconds = i;
this.children.clear();
}
try
{
this.initializeFromFormat(I18n.translateToLocal(this.key));
}
catch (TextComponentTranslationFormatException textcomponenttranslationformatexception)
{
this.children.clear();
try
{
this.initializeFromFormat(I18n.translateToFallback(this.key));
}
catch (TextComponentTranslationFormatException var5)
{
throw textcomponenttranslationformatexception;
}
}
}
/**
* initializes our children from a format string, using the format args to fill in the placeholder variables.
*/
protected void initializeFromFormat(String format)
{
boolean flag = false;
Matcher matcher = STRING_VARIABLE_PATTERN.matcher(format);
int i = 0;
int j = 0;
try
{
int l;
for (; matcher.find(j); j = l)
{
int k = matcher.start();
l = matcher.end();
if (k > j)
{
TextComponentString textcomponentstring = new TextComponentString(String.format(format.substring(j, k), new Object[0]));
textcomponentstring.getStyle().setParentStyle(this.getStyle());
this.children.add(textcomponentstring);
}
String s2 = matcher.group(2);
String s = format.substring(k, l);
if ("%".equals(s2) && "%%".equals(s))
{
TextComponentString textcomponentstring2 = new TextComponentString("%");
textcomponentstring2.getStyle().setParentStyle(this.getStyle());
this.children.add(textcomponentstring2);
}
else
{
if (!"s".equals(s2))
{
throw new TextComponentTranslationFormatException(this, "Unsupported format: \'" + s + "\'");
}
String s1 = matcher.group(1);
int i1 = s1 != null ? Integer.parseInt(s1) - 1 : i++;
if (i1 < this.formatArgs.length)
{
this.children.add(this.getFormatArgumentAsComponent(i1));
}
}
}
if (j < format.length())
{
TextComponentString textcomponentstring1 = new TextComponentString(String.format(format.substring(j), new Object[0]));
textcomponentstring1.getStyle().setParentStyle(this.getStyle());
this.children.add(textcomponentstring1);
}
}
catch (IllegalFormatException illegalformatexception)
{
throw new TextComponentTranslationFormatException(this, illegalformatexception);
}
}
@kashike
key is empty so translateToLocal will also return a empty format.t find any matches, because theformat` is empty.if (j < format.length()) will always be false because j = 0 and format.length() = 0 -> No children
If TranslatableComponent will work, then feel free to PR a change, then, @Cybermaxke - after testing.
I tested what @Cybermaxke suggested, by sending a Text with value TextSerializers.JSON.deserialize("{\"translate\":\"\"}"); and it worked. See
.
Most helpful comment
You cannot send a
nullheader becausegetFormattedTextwould be called on anull.You cannot send a
nullfooter becausegetFormattedTextwould be called on anull.You cannot send an empty header to remove the header, because
getFormattedTextis _never_ empty.You cannot send an empty footer to remove the footer, because
getFormattedTextis _never_ empty.