Component API by Vankka (#2665)

Adds a new Component API that allows converting between color code representations in Strings
This commit is contained in:
Henri Schubin 2022-10-16 13:21:49 +03:00 committed by GitHub
parent 900ce7c49c
commit c49276369f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 627 additions and 0 deletions

View File

@ -0,0 +1,30 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.component;
/**
* A Minecraft message component. You should <b>not</b> implement this,
* you can use {@link ComponentService} to a get an instance.
*
* @author Vankka
*/
public interface Component {
char AMPERSAND = '&';
char SECTION = '\u00A7';
}

View File

@ -0,0 +1,178 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.component;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
/**
* A utility api class for dealing with rich and less-rich Minecraft message formats.
*
* @author Vankka
* @see #getInstance()
*/
public interface ComponentService {
/**
* Obtain the instance of ComponentService.
*
* @return ComponentService implementation.
* @throws NoClassDefFoundError If Plan is not installed and this class can not be found or if older Plan version is installed.
* @throws IllegalStateException If Plan is installed, but not enabled.
*/
static ComponentService getInstance() {
return Optional.ofNullable(ComponentService.Holder.service.get())
.orElseThrow(() -> new IllegalStateException("ComponentService has not been initialised yet."));
}
/**
* Translates ampersands into section signs for color codes. Defaults to {@link Component#AMPERSAND} to {@link Component#SECTION}.
* Example: <code>&ctext</code> to <code>§ctext</code>.
*
* @param input the input color string with color codes using ampersands (<code>&</code>)
* @return the same input string with ampersands (<code>&</code>) for color codes replaced with section signs (<code>§</code>)
* @see #translateLegacy(String, char, char)
*/
default String translateLegacy(String input) {
return translateLegacy(input, Component.AMPERSAND, Component.SECTION);
}
/**
* Translates ampersands into section signs for color codes. Example: <code>&ctext</code> to <code>§ctext</code>.
*
* @param input the input color string with color codes using ampersands (<code>&</code>)
* @param inputCharacter the input character for translation, usually <code>&</code>.
* @param outputCharacter the output character for translation, usually <code>§</code>.
* @return the same input string with input characters for color codes replaced with output characters
* @see #translateLegacy(String)
* @see Component#SECTION
* @see Component#AMPERSAND
*/
String translateLegacy(String input, char inputCharacter, char outputCharacter);
/**
* Converts the given input into a {@link Component}.
* Converts an unknown format legacy/minimessage string into a component by attempting to guess the input format.
*
* @param unknown the unknown format input string
* @return a {@link Component}
*/
Component fromAutoDetermine(String unknown);
/**
* Converts the given input into a {@link Component}.
* Input example <code>§ctext</code>.
*
* @param legacy the input legacy
* @return a {@link Component}
* @see #fromLegacy(String, char)
*/
default Component fromLegacy(String legacy) {
return fromLegacy(legacy, Component.SECTION);
}
/**
* Converts the given input into a {@link Component}.
* Input example <code>§ctext</code>.
*
* @param legacy the input legacy
* @param character the character to use as the color prefix, usually <code>§</code>.
* @return a {@link Component}
* @see #fromLegacy(String)
* @see Component#SECTION
* @see Component#AMPERSAND
*/
Component fromLegacy(String legacy, char character);
/**
* Converts the given input into a {@link Component}.
* Input example: <code>&#rrggbbtext</code>.
*
* @param adventureLegacy the input adventure legacy
* @return a {@link Component}
* @see #fromAdventureLegacy(String, char)
*/
default Component fromAdventureLegacy(String adventureLegacy) {
return fromAdventureLegacy(adventureLegacy, Component.AMPERSAND);
}
/**
* Converts the given input into a {@link Component}.
* Input example: <code>&#rrggbbtext</code>.
*
* @param adventureLegacy the input adventure legacy
* @param character the character to use as the color prefix, usually <code>&</code>.
* @return a {@link Component}
* @see #fromAdventureLegacy(String)
* @see Component#SECTION
* @see Component#AMPERSAND
*/
Component fromAdventureLegacy(String adventureLegacy, char character);
/**
* Converts the given input into a {@link Component}.
* Input example: <code>§x§r§r§g§g§b§btext</code>.
*
* @param bungeeLegacy the input bungee legacy
* @return a {@link Component}
*/
default Component fromBungeeLegacy(String bungeeLegacy) {
return fromBungeeLegacy(bungeeLegacy, Component.SECTION);
}
/**
* Converts the given input into a {@link Component}.
* Input example: <code>§x§r§r§g§g§b§btext</code>.
*
* @param bungeeLegacy the input bungee legacy
* @param character the character to use as the color prefix, usually <code>§</code>.
* @return a {@link Component}
* @see Component#SECTION
* @see Component#AMPERSAND
*/
Component fromBungeeLegacy(String bungeeLegacy, char character);
/**
* Converts the given input into a {@link Component}.
* Input example: {@code <red>text</red>}.
*
* @param miniMessage the input minimessage
* @return a {@link Component}
*/
Component fromMiniMessage(String miniMessage);
/**
* Converts the given input into a {@link Component}.
* Input example: <code>{text:"text",color:"red"}</code> (standard Minecraft json).
*
* @param json the input json
* @return a {@link Component}
*/
Component fromJson(String json);
class Holder {
static final AtomicReference<ComponentService> service = new AtomicReference<>();
private Holder() {
/* Static variable holder */
}
static void set(ComponentService service) {
ComponentService.Holder.service.set(service);
}
}
}

View File

@ -0,0 +1,4 @@
/**
* {@link com.djrapitops.plan.component.ComponentService} providing an api for Minecraft message components.
*/
package com.djrapitops.plan.component;

View File

@ -89,6 +89,7 @@ subprojects {
caffeineVersion = "2.9.2"
mysqlVersion = "8.0.30"
sqliteVersion = "3.39.3.0"
adventureVersion = "4.11.0"
hikariVersion = "5.0.1"
slf4jVersion = "2.0.3"
geoIpVersion = "3.0.1"

View File

@ -66,6 +66,8 @@ dependencies {
implementation "io.swagger.core.v3:swagger-jaxrs2-jakarta:$swaggerVersion"
testImplementation project(":api")
testArtifacts project(":extensions:adventure")
testImplementation project(":extensions:adventure")
testImplementation "com.google.code.gson:gson:$gsonVersion"
testImplementation "org.seleniumhq.selenium:selenium-java:4.5.0"
testImplementation "org.testcontainers:testcontainers:$testContainersVersion"

View File

@ -17,6 +17,7 @@
package com.djrapitops.plan;
import com.djrapitops.plan.api.PlanAPI;
import com.djrapitops.plan.component.ComponentSvc;
import com.djrapitops.plan.delivery.DeliveryUtilities;
import com.djrapitops.plan.delivery.export.ExportSystem;
import com.djrapitops.plan.delivery.formatting.Formatters;
@ -76,6 +77,7 @@ public class PlanSystem implements SubSystem {
private final ImportSystem importSystem;
private final ExportSystem exportSystem;
private final DeliveryUtilities deliveryUtilities;
private final ComponentSvc componentService;
private final ResolverSvc resolverService;
private final ResourceSvc resourceService;
private final ExtensionSvc extensionService;
@ -102,6 +104,7 @@ public class PlanSystem implements SubSystem {
ImportSystem importSystem,
ExportSystem exportSystem,
DeliveryUtilities deliveryUtilities,
ComponentSvc componentService,
ResolverSvc resolverService,
ResourceSvc resourceService,
ExtensionSvc extensionService,
@ -127,6 +130,7 @@ public class PlanSystem implements SubSystem {
this.importSystem = importSystem;
this.exportSystem = exportSystem;
this.deliveryUtilities = deliveryUtilities;
this.componentService = componentService;
this.resolverService = resolverService;
this.resourceService = resourceService;
this.extensionService = extensionService;
@ -167,6 +171,7 @@ public class PlanSystem implements SubSystem {
*/
public void enableOtherThanCommands() {
extensionService.register();
componentService.register();
resolverService.register();
resourceService.register();
listenerService.register();

View File

@ -0,0 +1,23 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.component;
public interface ComponentConverter {
String convert(ComponentImpl component, ComponentOperation outputOperation, char outputCharacter);
String translate(String input, char inputCharacter, char outputCharacter);
}

View File

@ -0,0 +1,46 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.component;
public class ComponentImpl implements Component {
private final ComponentOperation inputOperation;
private final String input;
private final char inputCharacter;
public ComponentImpl(ComponentOperation inputOperation, String input) {
this(inputOperation, input, Character.MIN_VALUE);
}
public ComponentImpl(ComponentOperation inputOperation, String input, char inputCharacter) {
this.inputOperation = inputOperation;
this.input = input;
this.inputCharacter = inputCharacter;
}
public ComponentOperation getInputOperation() {
return inputOperation;
}
public String getInput() {
return input;
}
public char getInputCharacter() {
return inputCharacter;
}
}

View File

@ -0,0 +1,26 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.component;
public enum ComponentOperation {
AUTO_DETERMINE,
LEGACY,
ADVENTURE_LEGACY,
BUNGEE_LEGACY,
MINIMESSAGE,
JSON
}

View File

@ -0,0 +1,101 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.component;
import com.djrapitops.plan.exceptions.EnableException;
import javax.inject.Inject;
import javax.inject.Singleton;
/**
* Implementation for {@link ComponentService}.
*
* @author Vankka
*/
@Singleton
public class ComponentSvc implements ComponentService {
private ComponentConverter converter;
// This is required to inject
@Inject
public ComponentSvc() {}
@Override
public String translateLegacy(String input, char inputCharacter, char outputCharacter) {
if (converter != null) {
return converter.translate(input, inputCharacter, outputCharacter);
}
return input.replace(Component.AMPERSAND, Component.SECTION);
}
public String convert(Component component, ComponentOperation operation) {
return convert(component, operation, Character.MIN_VALUE);
}
public String convert(Component component, ComponentOperation operation, char inputCharacter) {
if (!(component instanceof ComponentImpl)) {
throw new IllegalArgumentException("Component was not made by ComponentService, but was of type " + component.getClass().getName());
}
ComponentImpl impl = (ComponentImpl) component;
if (converter != null) {
return converter.convert(impl, operation, inputCharacter);
}
return impl.getInput();
}
@Override
public Component fromAutoDetermine(String unknown) {
return new ComponentImpl(ComponentOperation.AUTO_DETERMINE, unknown);
}
@Override
public Component fromLegacy(String legacy, char character) {
return new ComponentImpl(ComponentOperation.LEGACY, legacy, character);
}
@Override
public Component fromAdventureLegacy(String adventureLegacy, char character) {
return new ComponentImpl(ComponentOperation.ADVENTURE_LEGACY, adventureLegacy, character);
}
@Override
public Component fromBungeeLegacy(String bungeeLegacy, char character) {
return new ComponentImpl(ComponentOperation.BUNGEE_LEGACY, bungeeLegacy, character);
}
@Override
public Component fromMiniMessage(String miniMessage) {
return new ComponentImpl(ComponentOperation.MINIMESSAGE, miniMessage);
}
@Override
public Component fromJson(String json) {
return new ComponentImpl(ComponentOperation.JSON, json);
}
public void register() {
try {
Class<?> clazz = Class.forName("com.djrapitops.plan.component.ComponentConverterImpl");
this.converter = (ComponentConverter) clazz.getDeclaredConstructor().newInstance();
} catch (ReflectiveOperationException e) {
throw new EnableException("Could not initialize ComponentConverter", e);
}
ComponentService.Holder.set(this);
}
}

View File

@ -0,0 +1,64 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.component;
import com.djrapitops.plan.PlanSystem;
import extension.FullSystemExtension;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ExtendWith(FullSystemExtension.class)
public class ComponentServiceTest {
static ComponentSvc service;
@BeforeAll
public static void enableSystem(PlanSystem system) {
system.enable();
service = (ComponentSvc) ComponentService.getInstance();
}
@AfterAll
public static void tearDown(PlanSystem system) {
service = null;
system.disable();
}
@Test
public void translateTest() {
assertEquals("§cred", service.translateLegacy("&cred"));
}
@Test
public void invalidTranslateTest() {
assertEquals("&zinvalid color code", service.translateLegacy("&zinvalid color code"));
}
@Test
public void testAutoDetermine() {
assertEquals("§cred", service.convert(service.fromAutoDetermine("<red>red"), ComponentOperation.LEGACY, Component.SECTION));
assertEquals("§cred", service.convert(service.fromAutoDetermine("&cred"), ComponentOperation.LEGACY, Component.SECTION));
assertEquals("&cred", service.convert(service.fromAutoDetermine("§cred"), ComponentOperation.LEGACY, Component.AMPERSAND));
assertEquals("§cred", service.convert(service.fromAutoDetermine("&#ff5555red"), ComponentOperation.LEGACY, Component.SECTION));
assertEquals("§cred", service.convert(service.fromAutoDetermine("§x§f§f§5§5§5§5red"), ComponentOperation.LEGACY, Component.SECTION));
}
}

View File

@ -0,0 +1,18 @@
// :extensions:adventure is used to avoid relocating 'net.kyori.adventure.*'
// as it is used & provided natively on some platforms
dependencies {
compileOnly project(':api')
compileOnly project(':common')
shadow "net.kyori:adventure-text-serializer-gson:$adventureVersion"
shadow "net.kyori:adventure-text-serializer-legacy:$adventureVersion"
shadow "net.kyori:adventure-text-minimessage:$adventureVersion"
}
shadowJar {
relocate 'net.kyori', 'plan.net.kyori'
// Exclude some stuff included from the root build.gradle
exclude 'dagger/**'
exclude 'javax/inject/**'
}

View File

@ -0,0 +1,123 @@
/*
* This file is part of Player Analytics (Plan).
*
* Plan is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License v3 as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Plan is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.component;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.ComponentSerializer;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
@SuppressWarnings("unused") // Accessed through Reflection
public class ComponentConverterImpl implements ComponentConverter {
private static final char AMPERSAND = com.djrapitops.plan.component.Component.AMPERSAND;
private static final char SECTION = com.djrapitops.plan.component.Component.SECTION;
private final ComponentSerializer<Component, ?, String> legacyAdventureAmpersand;
private final ComponentSerializer<Component, ?, String> legacyBungeeSection;
public ComponentConverterImpl() {
this.legacyAdventureAmpersand = makeAdventureLegacy(AMPERSAND);
this.legacyBungeeSection = makeBungeeLegacy(SECTION);
}
private LegacyComponentSerializer makeAdventureLegacy(char character) {
return LegacyComponentSerializer.builder()
.character(character)
.hexColors()
.build();
}
private LegacyComponentSerializer makeBungeeLegacy(char character) {
return LegacyComponentSerializer.builder()
.character(character)
.hexColors()
.useUnusualXRepeatedCharacterHexFormat() // Bungee
.build();
}
@Override
public String convert(ComponentImpl inputComponent, ComponentOperation outputOperation, char outputCharacter) {
Component component = makeIntoComponent(inputComponent);
return getSerializer(outputOperation, outputCharacter)
.serialize(component);
}
private Component makeIntoComponent(ComponentImpl component) {
ComponentOperation inputOperation = component.getInputOperation();
String input = component.getInput();
char inputCharacter = component.getInputCharacter();
ComponentSerializer<Component, ? extends Component, String> serializer;
if (inputOperation == ComponentOperation.AUTO_DETERMINE) {
boolean isMM = false;
try {
isMM = MiniMessage.miniMessage().stripTags(input).length() != input.length();
} catch (Throwable ignored) {
// MiniMessage may in some cases throw an exception, for example when it is given a legacy section.
}
if (isMM) {
serializer = MiniMessage.miniMessage();
} else if (input.contains(AMPERSAND + "#")) { // &#
serializer = legacyAdventureAmpersand;
} else if (input.contains(SECTION + "x" + SECTION)) { // §x§
serializer = legacyBungeeSection;
} else if (input.contains(Character.toString(SECTION))) { // §
serializer = LegacyComponentSerializer.legacySection();
} else {
serializer = LegacyComponentSerializer.legacyAmpersand();
}
} else {
serializer = getSerializer(inputOperation, inputCharacter);
}
return serializer.deserialize(input);
}
private ComponentSerializer<Component, ? extends Component, String> getSerializer(ComponentOperation operation, char character) {
switch (operation) {
case JSON:
return GsonComponentSerializer.gson();
case LEGACY:
return LegacyComponentSerializer.legacy(character);
case MINIMESSAGE:
return MiniMessage.miniMessage();
case ADVENTURE_LEGACY:
if (character == AMPERSAND) {
return legacyAdventureAmpersand;
}
return makeAdventureLegacy(character);
case BUNGEE_LEGACY:
if (character == SECTION) {
return legacyBungeeSection;
}
return makeBungeeLegacy(character);
case AUTO_DETERMINE:
default:
throw new IllegalStateException("Cannot get serializer for " + operation.name());
}
}
@Override
public String translate(String input, char inputCharacter, char outputCharacter) {
Component component = LegacyComponentSerializer.legacy(inputCharacter).deserialize(input);
return LegacyComponentSerializer.legacy(outputCharacter).serialize(component);
}
}

View File

@ -44,6 +44,9 @@ processResources {
shadowJar {
configurations = [project.configurations.shadow]
from findProject(':extensions:adventure').tasks.shadowJar.archiveFile
exclude('net.fabricmc:*')
exclude('/mappings/')

View File

@ -27,6 +27,8 @@ shadowJar {
dependsOn processResources
configurations = [project.configurations.shadow]
from findProject(':extensions:adventure').tasks.shadowJar.archiveFile
// Exclude these files
exclude "**/*.svg"
exclude "**/*.psd"

View File

@ -24,4 +24,5 @@ include 'bungeecord'
include 'velocity'
include 'plugin'
include 'extensions'
include 'extensions:adventure'
include 'fabric'