Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 65 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -392,9 +392,69 @@ YamlConfigurationProperties properties = ConfigLib.BUKKIT_DEFAULT_PROPERTIES.toB
.build();
```

To get access to this object, you have to import `configlib-paper` instead
If you are using **Paper** software, you can use `PAPER_DEFAULT_PROPERTIES` which includes both Bukkit `ConfigurationSerializable` support (e.g. `ItemStack`) and Adventure support pre-configured:

```java
YamlConfigurationProperties properties = ConfigLib.PAPER_DEFAULT_PROPERTIES.toBuilder()
// ...further configure the builder...
.build();
```

To get access to these objects, you have to import `configlib-paper` instead
of `configlib-yaml` as described in the [Import](#import) section.

#### Support for Adventure library types

The `configlib-adventure` module provides serializers for Adventure library types
commonly used in PaperMC and Velocity plugins. It supports:

* `Component` - Text components with customizable format (MiniMessage, legacy, JSON)
* `Key` - Namespaced keys (e.g., `minecraft:stone`)
* `Sound` - Sound effects with pitch, volume, and source

To use Adventure types in your configuration, use the helper method that registers all Adventure serializers:

```java
YamlConfigurationProperties.Builder<?> builder = YamlConfigurationProperties.newBuilder();
AdventureConfigLib.addDefaults(builder);
YamlConfigurationProperties properties = builder.build();
```

##### Component Formats

The `AdventureComponentSerializer` supports multiple text formats through the `AdventureComponentFormat` enum:

| Format | Description | Example |
|---------------------|----------------------------------------------------|------------------------------------|
| `MINI_MESSAGE` | MiniMessage tags | `<red>Hello <bold>World</bold>` |
| `LEGACY_AMPERSAND` | Legacy colors with `&` | `&cHello &lWorld` |
| `LEGACY_SECTION` | Legacy colors with `§` | `§cHello §lWorld` |
| `MINECRAFT_JSON` | Minecraft JSON format | `{"text":"Hello","color":"red"}` |
| `TRANSLATION_KEY` | Translation keys | `block.minecraft.stone` |

You can customize the serialization and deserialization format order:

```java
List<AdventureComponentFormat> serializeOrder = List.of(AdventureComponentFormat.MINI_MESSAGE);
List<AdventureComponentFormat> deserializeOrder = List.of(
AdventureComponentFormat.MINI_MESSAGE,
AdventureComponentFormat.LEGACY_AMPERSAND
);
AdventureConfigLib.addDefaults(builder, serializeOrder, deserializeOrder);
```

##### Sound Serialization

Sounds are serialized in a compact string format: `<sound_id> [pitch] [volume] [source]`.
Sound id can be anything even it is not in vanilla Minecraft to support custom sound from texture packs.
```yaml
# Full format
joinSound: "minecraft:entity.experience_orb.pickup 1.0 1.0 MASTER"

# Minimal format (defaults: pitch=1.0, volume=1.0, source=MASTER)
leaveSound: "minecraft:entity.experience_orb.pickup"
```

### Comments

The configuration elements of a configuration type can be annotated with
Expand Down Expand Up @@ -1141,6 +1201,10 @@ This project contains three classes of modules:
* The `configlib-yaml` module contains the classes that can save configuration
instances as YAML files and instantiate new instances from such files. This
module does not contain anything Minecraft related, either.
* The `configlib-adventure` module provides serializers for Adventure library types
like `Component`, `Key`, and `Sound`. This module is useful for PaperMC and
Velocity plugins that use the Adventure text API. See the
[Adventure support section](#support-for-adventure-library-types) for details.
* The `configlib-paper`, `configlib-velocity`, and `configlib-waterfall` modules
contain basic plugins that are used to conveniently load this library. These
three modules shade the `-core` module, the `-yaml` module, and the YAML
Expand Down
3 changes: 0 additions & 3 deletions buildSrc/src/main/kotlin/libs-config.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,3 @@ dependencies {
testImplementation(testFixtures(project(":configlib-core")))
}

tasks.compileJava {
dependsOn(project(":configlib-core").tasks.check)
}
22 changes: 22 additions & 0 deletions configlib-adventure/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
plugins {
`core-config`
`libs-config`
}

val adventureVersion = "4.26.1"

dependencies {
api(project(":configlib-core"))
compileOnly("net.kyori:adventure-api:$adventureVersion")
compileOnly("net.kyori:adventure-text-minimessage:$adventureVersion")
compileOnly("net.kyori:adventure-text-serializer-legacy:$adventureVersion")
compileOnly("net.kyori:adventure-text-serializer-gson:$adventureVersion")
compileOnly("net.kyori:adventure-text-serializer-plain:${adventureVersion}")

testImplementation(project(":configlib-yaml"))
testImplementation("net.kyori:adventure-api:$adventureVersion")
testImplementation("net.kyori:adventure-text-minimessage:$adventureVersion")
testImplementation("net.kyori:adventure-text-serializer-legacy:$adventureVersion")
testImplementation("net.kyori:adventure-text-serializer-gson:$adventureVersion")
testImplementation("net.kyori:adventure-text-serializer-plain:${adventureVersion}")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package de.exlll.configlib;

import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;

import java.util.function.Predicate;
import java.util.regex.Pattern;

/**
* Represents the different text formats supported for Adventure Component
* serialization.
*/
public enum AdventureComponentFormat {
/**
* MiniMessage format with tags like {@code <red>} or {@code <bold>}.
*/
MINI_MESSAGE(Patterns.MINI_MESSAGE_PATTERN.asPredicate()),
/**
* Translation key format for translatable components.
*/
TRANSLATION_KEY(input -> true), // translation keys can be any format
/**
* Legacy format using ampersand ({@code &}) as the color code prefix.
*/
LEGACY_AMPERSAND(input ->
input.indexOf(LegacyComponentSerializer.AMPERSAND_CHAR) != -1),
/**
* Legacy format using section symbol ({@code §}) as the color code prefix.
*/
LEGACY_SECTION(input ->
input.indexOf(LegacyComponentSerializer.SECTION_CHAR) != -1),
/**
* Minecraft JSON format for components.
*/
MINECRAFT_JSON(input -> {
input = input.trim();
return input.startsWith("{") && input.endsWith("}");
});

// Hack to avoid compiler error while singleton pattern initialization
private static class Patterns {
// Pattern to detect any <tag> in a string
static final Pattern MINI_MESSAGE_PATTERN =
Pattern.compile("<[a-zA-Z0-9_:-]+(?::[^<>]+)?>");
}

private final Predicate<String> inputPredicate;

AdventureComponentFormat(Predicate<String> inputPredicate) {
this.inputPredicate = inputPredicate;
}

/**
* Checks if the given input string matches this format.
*
* @param input the input string to check
* @return true if the input matches this format, false otherwise
*/
public boolean matches(String input) {
if (input == null) {
return false;
}
return inputPredicate.test(input);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package de.exlll.configlib;

import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;

import java.util.Arrays;
import java.util.List;

/**
* Serializer for Adventure {@link Component} objects.
* Supports multiple formats including MiniMessage, legacy, and JSON.
*/
public final class AdventureComponentSerializer implements Serializer<Component, String> {
private final List<AdventureComponentFormat> serializeOrder;
private final List<AdventureComponentFormat> deserializeOrder;

/**
* Creates a new ComponentSerializer with separate format orders for
* serialization and deserialization.
*
* @param serializeOrder the order of formats to try when serializing
* @param deserializeOrder the order of formats to try when deserializing
*/
public AdventureComponentSerializer(List<AdventureComponentFormat> serializeOrder,
List<AdventureComponentFormat> deserializeOrder) {
this.serializeOrder = List.copyOf(serializeOrder);
this.deserializeOrder = List.copyOf(deserializeOrder);
}

/**
* Creates a new ComponentSerializer using the same format order for
* both serialization and deserialization.
*
* @param formats the formats to use, in order of preference
*/
public AdventureComponentSerializer(AdventureComponentFormat... formats) {
this(Arrays.asList(formats), Arrays.asList(formats));
Copy link
Owner

Choose a reason for hiding this comment

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

You should make a defensive copy here, too. Calling asList is not enough.

Copy link
Author

Choose a reason for hiding this comment

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

I already called List.copyOf in another constructor. This constructor just converts the array to a List to pass it to the other constructor. or I miss understanding here?

}

@Override
public String serialize(Component element) {
if (element == null) {
return null;
}

for (AdventureComponentFormat format : serializeOrder) {
return serialize(element, format);
}

// Fallback to MiniMessage
return MiniMessage.miniMessage().serialize(element);
}

@Override
public Component deserialize(String element) {
if (element == null) {
return null;
}

for (AdventureComponentFormat format : deserializeOrder) {
if (!format.matches(element)) {
continue;
}

return deserialize(element, format);
}

// Fallback to MiniMessage
return MiniMessage.miniMessage().deserialize(element);
}

private String serialize(Component component, AdventureComponentFormat format) {
return switch (format) {
case MINI_MESSAGE -> MiniMessage.miniMessage().serialize(component);
case LEGACY_AMPERSAND ->
LegacyComponentSerializer.legacyAmpersand().serialize(component);
case LEGACY_SECTION ->
LegacyComponentSerializer.legacySection().serialize(component);
case MINECRAFT_JSON -> GsonComponentSerializer.gson().serialize(component);
case TRANSLATION_KEY ->
component instanceof TranslatableComponent translatableComponent
? translatableComponent.key()
: PlainTextComponentSerializer.plainText().serialize(component);
};
}

private Component deserialize(String string, AdventureComponentFormat format) {
return switch (format) {
case MINI_MESSAGE ->
MiniMessage.miniMessage().deserialize(string);
case LEGACY_AMPERSAND ->
LegacyComponentSerializer.legacyAmpersand().deserialize(string);
case LEGACY_SECTION ->
LegacyComponentSerializer.legacySection().deserialize(string);
case MINECRAFT_JSON ->
GsonComponentSerializer.gson().deserialize(string);
case TRANSLATION_KEY ->
Component.translatable(string);
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package de.exlll.configlib;

import net.kyori.adventure.key.Key;
import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.text.Component;

import java.util.List;

/**
* Utility class providing default serializers for Adventure library types.
*/
public final class AdventureConfigLib {
// Use MiniMessage as the default format since MiniMessage covered all
// component type
private static final List<AdventureComponentFormat> DEFAULT_FORMAT_ORDER = List.of(
AdventureComponentFormat.MINI_MESSAGE
);

private AdventureConfigLib() {
}

/**
* Adds default Adventure serializers to the configuration builder.
*
* @param builder the configuration properties builder
* @param <B> the builder type
* @return the builder with default serializers added
*/
public static <B extends ConfigurationProperties.Builder<B>>
ConfigurationProperties.Builder<B> addDefaults(
ConfigurationProperties.Builder<B> builder) {
return addDefaults(builder, DEFAULT_FORMAT_ORDER, DEFAULT_FORMAT_ORDER);
}

/**
* Adds default Adventure serializers to the configuration builder with custom
* format orders.
*
* @param builder the configuration properties builder
* @param serializeOrder the order of formats to try when serializing
* components
* @param deserializeOrder the order of formats to try when deserializing
* components
* @param <B> the builder type
* @return the builder with default serializers added
*/
public static <B extends ConfigurationProperties.Builder<B>>
ConfigurationProperties.Builder<B> addDefaults(
ConfigurationProperties.Builder<B> builder,
List<AdventureComponentFormat> serializeOrder,
List<AdventureComponentFormat> deserializeOrder) {
builder.addSerializer(Component.class,
new AdventureComponentSerializer(serializeOrder, deserializeOrder));
builder.addSerializer(Key.class, new AdventureKeySerializer());
builder.addSerializer(Sound.class, new AdventureSoundSerializer());
return builder;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package de.exlll.configlib;

import net.kyori.adventure.key.Key;

import java.util.OptionalInt;

/**
* Serializer for Adventure {@link Key} objects.
*/
public final class AdventureKeySerializer implements Serializer<Key, String> {

private final String defaultNamespace;

/**
* Creates a new KeySerializer with the specified default namespace.
*
* @param defaultNamespace the default namespace to use when deserializing keys
* without a namespace
* @throws IllegalArgumentException if the namespace is invalid
*/
public AdventureKeySerializer(String defaultNamespace) {
this.defaultNamespace = defaultNamespace;
OptionalInt result = Key.checkNamespace(defaultNamespace);
if (result.isPresent()) {
throw new IllegalArgumentException(
"Invalid namespace at index " + result.getAsInt() + ": "
+ defaultNamespace);
}
}

/**
* Creates a new KeySerializer using Adventure's default namespace (minecraft).
*/
public AdventureKeySerializer() {
this.defaultNamespace = null; // Use Adventure's default namespace
}

@Override
public String serialize(Key element) {
return element.asString();
}

@Override
public Key deserialize(String element) {
if (this.defaultNamespace == null) {
return Key.key(element);
}

return Key.key(this.defaultNamespace, element);
}
}
Loading