Skip to content

Commit 9d29fe5

Browse files
committed
Merge remote-tracking branch 'origin/master' into develop
# Conflicts: # build.gradle.kts
2 parents 4b7a03a + f5ed1f2 commit 9d29fe5

File tree

3 files changed

+101
-3
lines changed

3 files changed

+101
-3
lines changed

src/main/java/world/bentobox/bentobox/managers/LocalesManager.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -272,9 +272,28 @@ public void loadLocalesFromFile(String localeFolder) {
272272
String tag = language.getName().substring(0, language.getName().length() - 4);
273273
Locale localeObject = Locale.forLanguageTag(tag);
274274

275-
// Skip files whose name does not parse to a real BCP-47 language tag.
276-
// e.g. "zh_CN.yml" (underscore) yields Locale.ROOT, which would otherwise show
277-
// up as a blank entry in the language selector panel.
275+
// If the tag uses underscores (e.g. pt_BR) it won't parse as a valid BCP-47 tag.
276+
// Silently fix it: rename the file to use '-' and load it under the corrected locale.
277+
if (localeObject.getLanguage().isEmpty() && tag.contains("_")) {
278+
String fixedTag = tag.replace('_', '-');
279+
File fixedFile = new File(language.getParentFile(), fixedTag + ".yml");
280+
if (!fixedFile.exists() && language.renameTo(fixedFile)) {
281+
plugin.logWarning("Locale file '" + localeFolder + "/" + language.getName()
282+
+ "' has been renamed to '" + fixedTag + ".yml' to conform to BCP-47 (use '-' not '_').");
283+
language = fixedFile;
284+
} else if (fixedFile.exists()) {
285+
plugin.logWarning("Duplicate locale file '" + localeFolder + "/" + language.getName()
286+
+ "': '" + fixedTag + ".yml' already exists and will be used instead. Please remove the underscore version.");
287+
continue;
288+
} else {
289+
plugin.logWarning("Locale file '" + localeFolder + "/" + language.getName()
290+
+ "' uses '_' instead of '-' and could not be renamed; loading it as '" + fixedTag + "'.");
291+
}
292+
tag = fixedTag;
293+
localeObject = Locale.forLanguageTag(tag);
294+
}
295+
296+
// Skip files that still don't parse to a real BCP-47 language tag after the fix attempt.
278297
if (localeObject.getLanguage().isEmpty()) {
279298
plugin.logWarning("Ignoring locale file '" + localeFolder + "/" + language.getName()
280299
+ "': '" + tag + "' is not a valid BCP-47 language tag (use '-' not '_').");

src/main/java/world/bentobox/bentobox/util/Util.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,13 @@ public class Util {
9898
*/
9999
private static final Pattern LEGACY_HEX_CODE_PATTERN = Pattern.compile("&#[0-9a-fA-F]{3,6}|\u00A7x(\u00A7[0-9a-fA-F]){6}");
100100

101+
/**
102+
* Pattern to match the BungeeCord/Spigot {@code &x&R&R&G&G&B&B} hex format
103+
* (after {@code §} has been normalised to {@code &}).
104+
* Produced by {@link LegacyComponentSerializer} with {@code useUnusualXRepeatedCharacterHexFormat()}.
105+
*/
106+
private static final Pattern BUNGEE_HEX_PATTERN = Pattern.compile("&x(&[0-9a-fA-F]){6}");
107+
101108
/**
102109
* MiniMessage instance for parsing MiniMessage-formatted strings.
103110
*/
@@ -1047,6 +1054,10 @@ public static String legacyToMiniMessage(@NonNull String legacy) {
10471054
// First, normalize § to & for uniform processing
10481055
String text = legacy.replace('\u00A7', '&');
10491056

1057+
// Convert BungeeCord/Spigot §x§R§R§G§G§B§B hex format (now &x&R&R...) to &#RRGGBB
1058+
// so the HEX_PATTERN step below handles all hex input uniformly.
1059+
text = normalizeBungeeHex(text);
1060+
10501061
// Convert hex codes &#RRGGBB → <color:#RRGGBB>
10511062
Matcher hexMatcher = HEX_PATTERN.matcher(text);
10521063
StringBuilder sb = new StringBuilder();
@@ -1211,6 +1222,8 @@ public static String legacyToMiniMessage(@NonNull String legacy) {
12111222
public static String replaceLegacyCodesInline(@NonNull String text) {
12121223
// Normalize § to &
12131224
text = text.replace('\u00A7', '&');
1225+
// Convert BungeeCord/Spigot &x&R&R&G&G&B&B hex format to &#RRGGBB (see legacyToMiniMessage)
1226+
text = normalizeBungeeHex(text);
12141227
// Replace hex codes &#RRGGBB → <color:#RRGGBB>
12151228
Matcher hexMatcher = HEX_PATTERN.matcher(text);
12161229
StringBuilder sb = new StringBuilder();
@@ -1532,6 +1545,30 @@ private static String legacyColorCode(TextColor color) {
15321545
return COLOR_CHAR + Character.toString(code);
15331546
}
15341547

1548+
/**
1549+
* Converts the BungeeCord/Spigot {@code &x&R&R&G&G&B&B} repeated-character hex format
1550+
* (produced after {@code §} → {@code &} normalisation) to the {@code &#RRGGBB} form
1551+
* that {@link #HEX_PATTERN} understands.
1552+
*
1553+
* @param text input string with {@code &} normalised from {@code §}
1554+
* @return string with {@code &x&R&R&G&G&B&B} sequences replaced by {@code &#RRGGBB}
1555+
*/
1556+
private static String normalizeBungeeHex(@NonNull String text) {
1557+
Matcher m = BUNGEE_HEX_PATTERN.matcher(text);
1558+
if (!m.find()) {
1559+
return text;
1560+
}
1561+
StringBuilder sb = new StringBuilder();
1562+
m.reset();
1563+
while (m.find()) {
1564+
// "&x&2&3&8&a&f&0" → strip "&x" prefix and remaining "&" chars → "238af0"
1565+
String digits = m.group(0).substring(2).replace("&", "");
1566+
m.appendReplacement(sb, "&#" + digits);
1567+
}
1568+
m.appendTail(sb);
1569+
return sb.toString();
1570+
}
1571+
15351572
/**
15361573
* Serializes an Adventure Component to plain text with no formatting.
15371574
*

src/test/java/world/bentobox/bentobox/util/LegacyToMiniMessageTest.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static org.junit.jupiter.api.Assertions.assertEquals;
44
import static org.junit.jupiter.api.Assertions.assertFalse;
5+
import static org.junit.jupiter.api.Assertions.assertNotNull;
56
import static org.junit.jupiter.api.Assertions.assertTrue;
67

78
import net.kyori.adventure.text.Component;
@@ -285,4 +286,45 @@ void testStripSpaceAfterColorCodesRespectsBoundary() {
285286
// Reset must NOT strip
286287
assertEquals("\u00A7r world", Util.stripSpaceAfterColorCodes("\u00A7r world"));
287288
}
289+
290+
/**
291+
* Regression for <a href="https://github.com/BentoBoxWorld/BentoBox/issues/2943">BentoBox#2943</a>:
292+
* hex colors using the {@code &#RRGGBB} format were broken because
293+
* {@code translateColorCodes} serialises them to the BungeeCord
294+
* {@code §x§R§R§G§G§B§B} format, which {@code legacyToMiniMessage} (and
295+
* {@code replaceLegacyCodesInline}) did not recognise. The colour was then
296+
* corrupted to a sequence of named colours (&amp;2, &amp;3, …) instead of
297+
* the intended hex value.
298+
*/
299+
@Test
300+
void testBungeeCordHexFormatRoundTrip() {
301+
// Simulate the full path: user writes &#238af0&l in a locale file.
302+
// convertToLegacy (pure-legacy path) calls translateColorCodes which
303+
// serialises to §x§2§3§8§a§f§0§l. sendRawMessage then calls
304+
// parseMiniMessageOrLegacy which must reconstruct the original colour.
305+
String bungeeHex = "\u00A7x\u00A72\u00A73\u00A78\u00A7a\u00A7f\u00A70\u00A7l";
306+
Component component = Util.parseMiniMessageOrLegacy(bungeeHex + "test");
307+
// The component must carry the original hex colour, not a named-colour approximation.
308+
net.kyori.adventure.text.format.TextColor color = component.children().isEmpty()
309+
? component.color()
310+
: component.children().get(0).color();
311+
assertNotNull(color, "Expected a colour on the component");
312+
assertEquals(0x238af0, color.value(), "Hex colour #238af0 must survive the BungeeCord round-trip");
313+
}
314+
315+
/**
316+
* {@code &#RRGGBB&l} (the raw user-written format) must also round-trip correctly
317+
* through {@code legacyToMiniMessage}.
318+
*/
319+
@Test
320+
void testRawHexWithBoldRoundTrip() {
321+
String mm = Util.legacyToMiniMessage("&#238af0&lBold text");
322+
Component component = Util.parseMiniMessage(mm);
323+
// Must have the correct hex colour
324+
net.kyori.adventure.text.format.TextColor color = component.color() != null
325+
? component.color()
326+
: component.children().isEmpty() ? null : component.children().get(0).color();
327+
assertNotNull(color, "Expected a colour");
328+
assertEquals(0x238af0, color.value());
329+
}
288330
}

0 commit comments

Comments
 (0)