|
49 | 49 | import org.eclipse.jdt.annotation.Nullable; |
50 | 50 |
|
51 | 51 | import net.kyori.adventure.text.Component; |
| 52 | +import net.kyori.adventure.text.TextComponent; |
| 53 | +import net.kyori.adventure.text.format.NamedTextColor; |
| 54 | +import net.kyori.adventure.text.format.Style; |
| 55 | +import net.kyori.adventure.text.format.TextColor; |
| 56 | +import net.kyori.adventure.text.format.TextDecoration; |
52 | 57 | import net.kyori.adventure.text.minimessage.MiniMessage; |
53 | 58 | import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; |
54 | 59 | import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; |
@@ -1327,7 +1332,133 @@ public static String convertInlineCommandsToMiniMessage(@NonNull String message) |
1327 | 1332 | */ |
1328 | 1333 | @NonNull |
1329 | 1334 | public static String componentToLegacy(@NonNull Component component) { |
1330 | | - return SECTION_SERIALIZER.serialize(component); |
| 1335 | + StringBuilder sb = new StringBuilder(); |
| 1336 | + // EmittedState[0] holds the last-emitted style (color + decorations) so the walker |
| 1337 | + // can compute transitions and emit §r where Adventure's serializer would not. |
| 1338 | + EmittedState state = new EmittedState(); |
| 1339 | + appendComponentLegacy(sb, component, Style.empty(), state); |
| 1340 | + return sb.toString(); |
| 1341 | + } |
| 1342 | + |
| 1343 | + /** |
| 1344 | + * Mutable state used by {@link #appendComponentLegacy(StringBuilder, Component, Style, EmittedState)} |
| 1345 | + * to track the most recently emitted color and decorations. Adventure's |
| 1346 | + * {@link LegacyComponentSerializer} silently drops decoration-off transitions because legacy |
| 1347 | + * color codes have no "turn this decoration off" code — only §r resets everything. We work |
| 1348 | + * around that by tracking what is currently active and emitting §r ourselves when needed. |
| 1349 | + */ |
| 1350 | + private static final class EmittedState { |
| 1351 | + TextColor color; |
| 1352 | + boolean bold; |
| 1353 | + boolean italic; |
| 1354 | + boolean underlined; |
| 1355 | + boolean strikethrough; |
| 1356 | + boolean obfuscated; |
| 1357 | + boolean isFresh = true; |
| 1358 | + } |
| 1359 | + |
| 1360 | + private static void appendComponentLegacy(StringBuilder sb, Component component, Style inherited, EmittedState state) { |
| 1361 | + // merge() with default strategy lets the child component override inherited fields, |
| 1362 | + // and inherits the parent's fields where the child leaves them unset. |
| 1363 | + Style effective = inherited.merge(component.style()); |
| 1364 | + if (component instanceof TextComponent text && !text.content().isEmpty()) { |
| 1365 | + emitStyleTransition(sb, effective, state); |
| 1366 | + sb.append(text.content()); |
| 1367 | + } |
| 1368 | + for (Component child : component.children()) { |
| 1369 | + appendComponentLegacy(sb, child, effective, state); |
| 1370 | + } |
| 1371 | + } |
| 1372 | + |
| 1373 | + private static void emitStyleTransition(StringBuilder sb, Style style, EmittedState state) { |
| 1374 | + boolean wantBold = style.decoration(TextDecoration.BOLD) == TextDecoration.State.TRUE; |
| 1375 | + boolean wantItalic = style.decoration(TextDecoration.ITALIC) == TextDecoration.State.TRUE; |
| 1376 | + boolean wantUnderlined = style.decoration(TextDecoration.UNDERLINED) == TextDecoration.State.TRUE; |
| 1377 | + boolean wantStrikethrough = style.decoration(TextDecoration.STRIKETHROUGH) == TextDecoration.State.TRUE; |
| 1378 | + boolean wantObfuscated = style.decoration(TextDecoration.OBFUSCATED) == TextDecoration.State.TRUE; |
| 1379 | + TextColor wantColor = style.color(); |
| 1380 | + |
| 1381 | + // Determine if we need a hard reset: any decoration that was on must turn off, |
| 1382 | + // or the color must change to "no color" while one was previously active. |
| 1383 | + boolean needReset = (state.bold && !wantBold) |
| 1384 | + || (state.italic && !wantItalic) |
| 1385 | + || (state.underlined && !wantUnderlined) |
| 1386 | + || (state.strikethrough && !wantStrikethrough) |
| 1387 | + || (state.obfuscated && !wantObfuscated) |
| 1388 | + || (state.color != null && wantColor == null); |
| 1389 | + |
| 1390 | + if (needReset) { |
| 1391 | + sb.append(COLOR_CHAR).append('r'); |
| 1392 | + state.color = null; |
| 1393 | + state.bold = false; |
| 1394 | + state.italic = false; |
| 1395 | + state.underlined = false; |
| 1396 | + state.strikethrough = false; |
| 1397 | + state.obfuscated = false; |
| 1398 | + } |
| 1399 | + |
| 1400 | + // Emit color if it changed (or after a reset) |
| 1401 | + if (wantColor != null && (state.isFresh || !wantColor.equals(state.color) || needReset)) { |
| 1402 | + sb.append(legacyColorCode(wantColor)); |
| 1403 | + state.color = wantColor; |
| 1404 | + } |
| 1405 | + |
| 1406 | + // Emit decorations that should now be on but aren't yet |
| 1407 | + if (wantBold && !state.bold) { |
| 1408 | + sb.append(COLOR_CHAR).append('l'); |
| 1409 | + state.bold = true; |
| 1410 | + } |
| 1411 | + if (wantItalic && !state.italic) { |
| 1412 | + sb.append(COLOR_CHAR).append('o'); |
| 1413 | + state.italic = true; |
| 1414 | + } |
| 1415 | + if (wantUnderlined && !state.underlined) { |
| 1416 | + sb.append(COLOR_CHAR).append('n'); |
| 1417 | + state.underlined = true; |
| 1418 | + } |
| 1419 | + if (wantStrikethrough && !state.strikethrough) { |
| 1420 | + sb.append(COLOR_CHAR).append('m'); |
| 1421 | + state.strikethrough = true; |
| 1422 | + } |
| 1423 | + if (wantObfuscated && !state.obfuscated) { |
| 1424 | + sb.append(COLOR_CHAR).append('k'); |
| 1425 | + state.obfuscated = true; |
| 1426 | + } |
| 1427 | + state.isFresh = false; |
| 1428 | + } |
| 1429 | + |
| 1430 | + private static String legacyColorCode(TextColor color) { |
| 1431 | + // For named colors, use the standard single-character legacy code. |
| 1432 | + NamedTextColor named = NamedTextColor.nearestTo(color); |
| 1433 | + char code; |
| 1434 | + if (named == NamedTextColor.BLACK) code = '0'; |
| 1435 | + else if (named == NamedTextColor.DARK_BLUE) code = '1'; |
| 1436 | + else if (named == NamedTextColor.DARK_GREEN) code = '2'; |
| 1437 | + else if (named == NamedTextColor.DARK_AQUA) code = '3'; |
| 1438 | + else if (named == NamedTextColor.DARK_RED) code = '4'; |
| 1439 | + else if (named == NamedTextColor.DARK_PURPLE) code = '5'; |
| 1440 | + else if (named == NamedTextColor.GOLD) code = '6'; |
| 1441 | + else if (named == NamedTextColor.GRAY) code = '7'; |
| 1442 | + else if (named == NamedTextColor.DARK_GRAY) code = '8'; |
| 1443 | + else if (named == NamedTextColor.BLUE) code = '9'; |
| 1444 | + else if (named == NamedTextColor.GREEN) code = 'a'; |
| 1445 | + else if (named == NamedTextColor.AQUA) code = 'b'; |
| 1446 | + else if (named == NamedTextColor.RED) code = 'c'; |
| 1447 | + else if (named == NamedTextColor.LIGHT_PURPLE) code = 'd'; |
| 1448 | + else if (named == NamedTextColor.YELLOW) code = 'e'; |
| 1449 | + else code = 'f'; |
| 1450 | + // If the original color was a true hex (not a named color), emit the §x§R§R... form |
| 1451 | + // so it round-trips. Otherwise just emit the named code. |
| 1452 | + if (!(color instanceof NamedTextColor)) { |
| 1453 | + String hex = String.format("%06X", color.value()); |
| 1454 | + StringBuilder out = new StringBuilder(); |
| 1455 | + out.append(COLOR_CHAR).append('x'); |
| 1456 | + for (int i = 0; i < 6; i++) { |
| 1457 | + out.append(COLOR_CHAR).append(hex.charAt(i)); |
| 1458 | + } |
| 1459 | + return out.toString(); |
| 1460 | + } |
| 1461 | + return COLOR_CHAR + Character.toString(code); |
1331 | 1462 | } |
1332 | 1463 |
|
1333 | 1464 | /** |
|
0 commit comments