Skip to content

Conversation

@Janmm14
Copy link
Contributor

@Janmm14 Janmm14 commented Sep 29, 2025

Introducing the ChatColor#closestDefaultColor(java.awt.Color) method:

It computes the closest legacy default ChatColor (§1, §a, §e...) by using squared Euclidean distance calculation. It can be very useful if one just wants to translate the new hex color back for old clients.

Based on idea from Outfluencer in #3887

Introducing the ChatColor#closestDefaultColor(java.awt.Color) method:

It computes the closest legacy default ChatColor (§1, §a, §e...) by using squared Euclidean distance calculation.
It can be very useful if one just wants to translate the new hex color back for old clients.

Co-authored-by: Outfluencer <[email protected]>
Comment on lines 247 to 276
public static ChatColor closestDefaultColor(Color target)
{
Preconditions.checkNotNull( target, "target cannot be null" );
int targetRed = target.getRed();
int targetGreen = target.getGreen();
int targetBlue = target.getBlue();

ChatColor result = null;
int smallestDistance = Integer.MAX_VALUE;

for ( int i = 0, length = COLORS_HEX.length; i < length; i++ )
{
int colorValue = COLORS_HEX[ i ];
int redDiff = ( colorValue >> 16 & 0xFF ) - targetRed;
int greenDiff = ( colorValue >> 8 & 0xFF ) - targetGreen;
int blueDiff = ( colorValue & 0xFF ) - targetBlue;
int distance = redDiff * redDiff + greenDiff * greenDiff + blueDiff * blueDiff;

if ( distance < smallestDistance )
{
smallestDistance = distance;
result = COLORS[ i ];
}
if ( distance < MAX_CLOSEST_COLOR_DIST_SQ )
{
break;
}
}
return Preconditions.checkNotNull( result, "Could not find a match for " + target );
}
Copy link
Contributor Author

@Janmm14 Janmm14 Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its late, I think MAX_CLOSEST_COLOR_DIST_SQ + 1 can be the initial value of smallestDistance (or not +1 and we change comparision to <=), but I'm not 100% sure.

@Janmm14 Janmm14 changed the title Add api to get the closest default ChatColor for a specific color Add api to get the closest default ChatColor for a specific color (faster) Sep 29, 2025
@Janmm14
Copy link
Contributor Author

Janmm14 commented Sep 30, 2025

If needed, I can also do a microbenhcmark. These optimizations are based on educated guesses (like to not iterate over formattings in the first place).

@nellieldev
Copy link

nellieldev commented Sep 30, 2025

  • Hacky, undynamic way of accumulating colors (fixed 16-sized) -> Add one after 16 and get an ArrayIndexOutOfBoundsException in your for-loop, ever heard of Java Stream API?
  • Hacky way of bit-shifting hex colors to R, G and B-values instead of using java.awt.Color like everywhere
  • Updates made to file that are not mentioned in PR
  • for-i can be replaced with for-each loop like in modern java applications
  • MAX_CLOSEST_COLOR_DIST_SQ seems like a neat, resource-friendly way of handling over-the-top rotations over colors but is hard to understand / might be calculated at begin for clearance
  • It would be better to prioritize code readability over "performance" that is getting beat by the JIT compiler and initial compuations that are done one time and cost ~3ms
    => referring to Add api to get the closest default ChatColor for a specific Color #3887

smallestDistance = distance;
result = COLORS[ i ];
}
if ( distance < MAX_CLOSEST_COLOR_DIST_SQ )
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this correct? Can you please explain?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, would also appreciate an explaination for that "magic" value

@Janmm14
Copy link
Contributor Author

Janmm14 commented Sep 30, 2025

It was too late in the night, the algorithm is wrong. The constant is too high (see how I calculated the constant in the test file).

@Janmm14 Janmm14 marked this pull request as draft September 30, 2025 12:09
@Janmm14
Copy link
Contributor Author

Janmm14 commented Sep 30, 2025

@nellieldev

  • Hacky, undynamic way of accumulating colors (fixed 16-sized) -> Add one after 16 and get an ArrayIndexOutOfBoundsException in your for-loop, ever heard of Java Stream API?

This is meant to support legacy minecraft versions, those will never get a new color. Thanks LLM AI for not knowing the context.

  • Hacky way of bit-shifting hex colors to R, G and B-values instead of using java.awt.Color like everywhere

It is exactly how awt color saves / their getters work. For cache locality I got rid of the Color object.

  • Updates made to file that are not mentioned in PR

Some small updates to the file are not noteworthy enough to mention them. I suggest evaluating AI LLM responses yourself before posting them.

  • for-i can be replaced with for-each loop like in modern java applications

No, the index is needed.

  • MAX_CLOSEST_COLOR_DIST_SQ seems like a neat, resource-friendly way of handling over-the-top rotations over colors but is hard to understand / might be calculated at begin for clearance

Unfortunaly the algorithm I wrote in the test file calculated this constant wrong, I will revise it and add a comment to this constant.

I think the code is still perfectly readable, thanks LLM AI. The time of initial computations is irrelevant.

…erence in speed.

Added testClosestDefaultColor to ensure correctness of abort constant.
@Outfluencer
Copy link
Collaborator

Outfluencer commented Sep 30, 2025

I have bruteforced the exact value now

and it seems like for the specific legacy colors its 3697 so we can fast break even more.
But i am not sure why your calculation doesnt provide that value.
By iterating through all colors it happens that there is a distance between 3613 and 3697.

But i am not sure if we should use any of those precalculated values at all.

Here how i tested it btw if you increment the magic value by one it will fail

public static void main(String[] args)
{
    for(int i = 0x000000; i < 0xFFFFFF; i++)
    {
        Color c = new Color( i );
        Preconditions.checkArgument( closestDefaultColor(c, false) == closestDefaultColor(c, true) );
    }
}


public static ChatColor closestDefaultColor(final Color target, boolean fast)
{
    Preconditions.checkNotNull( target, "target cannot be null" );
    final int targetRed = target.getRed();
    final int targetGreen = target.getGreen();
    final int targetBlue = target.getBlue();

    ChatColor result = null;
    int smallestDistance = Integer.MAX_VALUE;

    for ( ChatColor value : DEFAULT_COLORS )
    {
        final Color color = value.getColor();

        int redDiff = color.getRed() - targetRed;
        int greenDiff = color.getGreen() - targetGreen;
        int blueDiff = color.getBlue() - targetBlue;
        int distance = redDiff * redDiff + greenDiff * greenDiff + blueDiff * blueDiff;

        if ( distance < smallestDistance )
        {
            smallestDistance = distance;
            result = value;

            if (fast && distance <= 3697)
            {
                break;
            }
        }
    }
    return Preconditions.checkNotNull( result, "Could not find a match for " + target );
}

@Janmm14
Copy link
Contributor Author

Janmm14 commented Sep 30, 2025

I have bruteforced the exact value now

and it seems like for the specific legacy colors its 3697 so we can fast break even more. But i am not sure why your calculation doesnt provide that value. By iterating through all colors it happens that there is a distance between 3613 and 3697.

But i am not sure if we should use any of those precalculated values at all.

Here how i tested it btw if you increment the magic value by one it will fail

public static void main(String[] args)
{
    for(int i = 0x000000; i < 0xFFFFFF; i++)
    {
        Color c = new Color( i );
        Preconditions.checkArgument( closestDefaultColor(c, false) == closestDefaultColor(c, true) );
    }
}


public static ChatColor closestDefaultColor(final Color target, boolean fast)
{
    Preconditions.checkNotNull( target, "target cannot be null" );
    final int targetRed = target.getRed();
    final int targetGreen = target.getGreen();
    final int targetBlue = target.getBlue();

    ChatColor result = null;
    int smallestDistance = Integer.MAX_VALUE;

    for ( ChatColor value : DEFAULT_COLORS )
    {
        final Color color = value.getColor();

        int redDiff = color.getRed() - targetRed;
        int greenDiff = color.getGreen() - targetGreen;
        int blueDiff = color.getBlue() - targetBlue;
        int distance = redDiff * redDiff + greenDiff * greenDiff + blueDiff * blueDiff;

        if ( distance < smallestDistance )
        {
            smallestDistance = distance;
            result = value;

            if (fast && distance <= 3697)
            {
                break;
            }
        }
    }
    return Preconditions.checkNotNull( result, "Could not find a match for " + target );
}

I investigated, this happens because there are some specific rgb values which have the same distance to 2 default colors.

If, in my method, I change if ( secondClosestDistSq < minDistSq ) to if ( closestDistSq != secondClosestDistSq && secondClosestDistSq < minDistSq ), I get the result of 3698.
And I noticed this way, and playing with order of chat colors, that actually my method outputs exactly 1 too high, as it uses the 2nd closest distance aka it searches the smallest offending value.

…al-distance colors, only check for abort distance if we found a new smallest distance.
@NotYourReturn
Copy link

why copy paste? i have seen the same commit in bungeecord?

@Janmm14 Janmm14 marked this pull request as ready for review October 10, 2025 13:55
@Janmm14
Copy link
Contributor Author

Janmm14 commented Oct 10, 2025

why copy paste? i have seen the same commit in bungeecord?
by @NotYourReturn

9VRh86H-LNpj3IpntusAyg

Based on idea from Outfluencer in #3887

Add api to get the closest default ChatColor for a specific color (faster)

@NotYourReturn
Copy link

why copy paste? i have seen the same commit in bungeecord?
by @NotYourReturn

Based on idea from Outfluencer in #3887

Add api to get the closest default ChatColor for a specific color (faster)

It may result in IndexOutOfBoundsException.java so this is not a good code.

@Janmm14
Copy link
Contributor Author

Janmm14 commented Oct 10, 2025

It may result in IndexOutOfBoundsException.java so this is not a good code.

It does not. legacy colors do not change.

@NotYourReturn
Copy link

It may result in IndexOutOfBoundsException.java so this is not a good code.

It does not. legacy colors do not change.

what if they do?

@Janmm14
Copy link
Contributor Author

Janmm14 commented Oct 10, 2025

what if they do?

Legacy mc versions are not changed ever anymore by minecraft, so the amount of named colors this method needs will not change, especially named colors will never get less.

@NotYourReturn
Copy link

what if they do?

Legacy mc versions are not changed.

what about bedrock?

@Janmm14
Copy link
Contributor Author

Janmm14 commented Oct 10, 2025

what if they do?

Legacy mc versions are not changed.

what about bedrock?

Bedrcok is not the target of Bungeecord.

@NotYourReturn
Copy link

what if they do?

Legacy mc versions are not changed.

what about bedrock?

Bedrcok is not the target of Bungeecord.

when does bungeecord support bedrock?

@Janmm14
Copy link
Contributor Author

Janmm14 commented Oct 10, 2025

when does bungeecord support bedrock?

never. and fix your formatting, dont put your new text into quote.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants