Skip to content

Commit d43c4fc

Browse files
authored
Merge pull request #60 from zvyap/master
Native Support for Adventure Library
2 parents 434ebac + 2aeee87 commit d43c4fc

20 files changed

Lines changed: 1054 additions & 16 deletions

File tree

README.md

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,9 +392,69 @@ YamlConfigurationProperties properties = ConfigLib.BUKKIT_DEFAULT_PROPERTIES.toB
392392
.build();
393393
```
394394

395-
To get access to this object, you have to import `configlib-paper` instead
395+
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:
396+
397+
```java
398+
YamlConfigurationProperties properties = ConfigLib.PAPER_DEFAULT_PROPERTIES.toBuilder()
399+
// ...further configure the builder...
400+
.build();
401+
```
402+
403+
To get access to these objects, you have to import `configlib-paper` instead
396404
of `configlib-yaml` as described in the [Import](#import) section.
397405

406+
#### Support for Adventure library types
407+
408+
The `configlib-adventure` module provides serializers for Adventure library types
409+
commonly used in PaperMC and Velocity plugins. It supports:
410+
411+
* `Component` - Text components with customizable format (MiniMessage, legacy, JSON)
412+
* `Key` - Namespaced keys (e.g., `minecraft:stone`)
413+
* `Sound` - Sound effects with pitch, volume, and source
414+
415+
To use Adventure types in your configuration, use the helper method that registers all Adventure serializers:
416+
417+
```java
418+
YamlConfigurationProperties.Builder<?> builder = YamlConfigurationProperties.newBuilder();
419+
AdventureConfigLib.addDefaults(builder);
420+
YamlConfigurationProperties properties = builder.build();
421+
```
422+
423+
##### Component Formats
424+
425+
The `AdventureComponentSerializer` supports multiple text formats through the `AdventureComponentFormat` enum:
426+
427+
| Format | Description | Example |
428+
|---------------------|----------------------------------------------------|------------------------------------|
429+
| `MINI_MESSAGE` | MiniMessage tags | `<red>Hello <bold>World</bold>` |
430+
| `LEGACY_AMPERSAND` | Legacy colors with `&` | `&cHello &lWorld` |
431+
| `LEGACY_SECTION` | Legacy colors with `§` | `§cHello §lWorld` |
432+
| `MINECRAFT_JSON` | Minecraft JSON format | `{"text":"Hello","color":"red"}` |
433+
| `TRANSLATION_KEY` | Translation keys | `block.minecraft.stone` |
434+
435+
You can customize the serialization and deserialization format order:
436+
437+
```java
438+
List<AdventureComponentFormat> serializeOrder = List.of(AdventureComponentFormat.MINI_MESSAGE);
439+
List<AdventureComponentFormat> deserializeOrder = List.of(
440+
AdventureComponentFormat.MINI_MESSAGE,
441+
AdventureComponentFormat.LEGACY_AMPERSAND
442+
);
443+
AdventureConfigLib.addDefaults(builder, serializeOrder, deserializeOrder);
444+
```
445+
446+
##### Sound Serialization
447+
448+
Sounds are serialized in a compact string format: `<sound_id> [pitch] [volume] [source]`.
449+
Sound id can be anything even it is not in vanilla Minecraft to support custom sound from texture packs.
450+
```yaml
451+
# Full format
452+
joinSound: "minecraft:entity.experience_orb.pickup 1.0 1.0 MASTER"
453+
454+
# Minimal format (defaults: pitch=1.0, volume=1.0, source=MASTER)
455+
leaveSound: "minecraft:entity.experience_orb.pickup"
456+
```
457+
398458
### Comments
399459

400460
The configuration elements of a configuration type can be annotated with
@@ -1141,6 +1201,10 @@ This project contains three classes of modules:
11411201
* The `configlib-yaml` module contains the classes that can save configuration
11421202
instances as YAML files and instantiate new instances from such files. This
11431203
module does not contain anything Minecraft related, either.
1204+
* The `configlib-adventure` module provides serializers for Adventure library types
1205+
like `Component`, `Key`, and `Sound`. This module is useful for PaperMC and
1206+
Velocity plugins that use the Adventure text API. See the
1207+
[Adventure support section](#support-for-adventure-library-types) for details.
11441208
* The `configlib-paper`, `configlib-velocity`, and `configlib-waterfall` modules
11451209
contain basic plugins that are used to conveniently load this library. These
11461210
three modules shade the `-core` module, the `-yaml` module, and the YAML

buildSrc/src/main/kotlin/libs-config.gradle.kts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,3 @@ dependencies {
77
testImplementation(testFixtures(project(":configlib-core")))
88
}
99

10-
tasks.compileJava {
11-
dependsOn(project(":configlib-core").tasks.check)
12-
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
plugins {
2+
`core-config`
3+
`libs-config`
4+
}
5+
6+
val adventureVersion = "4.26.1"
7+
8+
dependencies {
9+
api(project(":configlib-core"))
10+
compileOnly("net.kyori:adventure-api:$adventureVersion")
11+
compileOnly("net.kyori:adventure-text-minimessage:$adventureVersion")
12+
compileOnly("net.kyori:adventure-text-serializer-legacy:$adventureVersion")
13+
compileOnly("net.kyori:adventure-text-serializer-gson:$adventureVersion")
14+
compileOnly("net.kyori:adventure-text-serializer-plain:${adventureVersion}")
15+
16+
testImplementation(project(":configlib-yaml"))
17+
testImplementation("net.kyori:adventure-api:$adventureVersion")
18+
testImplementation("net.kyori:adventure-text-minimessage:$adventureVersion")
19+
testImplementation("net.kyori:adventure-text-serializer-legacy:$adventureVersion")
20+
testImplementation("net.kyori:adventure-text-serializer-gson:$adventureVersion")
21+
testImplementation("net.kyori:adventure-text-serializer-plain:${adventureVersion}")
22+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package de.exlll.configlib;
2+
3+
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
4+
5+
import java.util.function.Predicate;
6+
import java.util.regex.Pattern;
7+
8+
/**
9+
* Represents the different text formats supported for Adventure Component
10+
* serialization.
11+
*/
12+
public enum AdventureComponentFormat {
13+
/**
14+
* MiniMessage format with tags like {@code <red>} or {@code <bold>}.
15+
*/
16+
MINI_MESSAGE(Patterns.MINI_MESSAGE_PATTERN.asPredicate()),
17+
/**
18+
* Translation key format for translatable components.
19+
*/
20+
TRANSLATION_KEY(input -> true), // translation keys can be any format
21+
/**
22+
* Legacy format using ampersand ({@code &}) as the color code prefix.
23+
*/
24+
LEGACY_AMPERSAND(input ->
25+
input.indexOf(LegacyComponentSerializer.AMPERSAND_CHAR) != -1),
26+
/**
27+
* Legacy format using section symbol ({@code §}) as the color code prefix.
28+
*/
29+
LEGACY_SECTION(input ->
30+
input.indexOf(LegacyComponentSerializer.SECTION_CHAR) != -1),
31+
/**
32+
* Minecraft JSON format for components.
33+
*/
34+
MINECRAFT_JSON(input -> {
35+
input = input.trim();
36+
return input.startsWith("{") && input.endsWith("}");
37+
});
38+
39+
// Hack to avoid compiler error while singleton pattern initialization
40+
private static class Patterns {
41+
// Pattern to detect any <tag> in a string
42+
static final Pattern MINI_MESSAGE_PATTERN =
43+
Pattern.compile("<[a-zA-Z0-9_:-]+(?::[^<>]+)?>");
44+
}
45+
46+
private final Predicate<String> inputPredicate;
47+
48+
AdventureComponentFormat(Predicate<String> inputPredicate) {
49+
this.inputPredicate = inputPredicate;
50+
}
51+
52+
/**
53+
* Checks if the given input string matches this format.
54+
*
55+
* @param input the input string to check
56+
* @return true if the input matches this format, false otherwise
57+
*/
58+
public boolean matches(String input) {
59+
if (input == null) {
60+
return false;
61+
}
62+
return inputPredicate.test(input);
63+
}
64+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package de.exlll.configlib;
2+
3+
import net.kyori.adventure.text.Component;
4+
import net.kyori.adventure.text.TranslatableComponent;
5+
import net.kyori.adventure.text.minimessage.MiniMessage;
6+
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
7+
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
8+
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
9+
10+
import java.util.Arrays;
11+
import java.util.List;
12+
13+
/**
14+
* Serializer for Adventure {@link Component} objects.
15+
* Supports multiple formats including MiniMessage, legacy, and JSON.
16+
*/
17+
public final class AdventureComponentSerializer implements Serializer<Component, String> {
18+
private final AdventureComponentFormat serializeFormat;
19+
private final List<AdventureComponentFormat> deserializeOrder;
20+
21+
/**
22+
* Creates a new ComponentSerializer with separate format orders for
23+
* serialization and deserialization.
24+
*
25+
* @param serializeFormat the format to use when serializing
26+
* @param deserializeOrder the order of formats to try when deserializing
27+
*/
28+
public AdventureComponentSerializer(AdventureComponentFormat serializeFormat,
29+
List<AdventureComponentFormat> deserializeOrder) {
30+
this.serializeFormat = serializeFormat;
31+
this.deserializeOrder = List.copyOf(deserializeOrder);
32+
}
33+
34+
/**
35+
* Creates a new ComponentSerializer using the same format order for
36+
* both serialization and deserialization.
37+
*
38+
* @param serializeFormat the format to use for serialization
39+
* @param deserializeFormats the formats to use for deserialization, in order of
40+
* preference
41+
*/
42+
public AdventureComponentSerializer(AdventureComponentFormat serializeFormat,
43+
AdventureComponentFormat... deserializeFormats) {
44+
this(serializeFormat, deserializeFormats.length == 0
45+
? List.of(serializeFormat)
46+
: Arrays.asList(deserializeFormats));
47+
}
48+
49+
@Override
50+
public String serialize(Component element) {
51+
if (element == null) {
52+
return null;
53+
}
54+
55+
return serialize(element, serializeFormat);
56+
}
57+
58+
@Override
59+
public Component deserialize(String element) {
60+
if (element == null) {
61+
return null;
62+
}
63+
64+
for (AdventureComponentFormat format : deserializeOrder) {
65+
if (!format.matches(element)) {
66+
continue;
67+
}
68+
69+
return deserialize(element, format);
70+
}
71+
72+
// Fallback to MiniMessage
73+
return MiniMessage.miniMessage().deserialize(element);
74+
}
75+
76+
private String serialize(Component component, AdventureComponentFormat format) {
77+
return switch (format) {
78+
case MINI_MESSAGE -> MiniMessage.miniMessage().serialize(component);
79+
case LEGACY_AMPERSAND ->
80+
LegacyComponentSerializer.legacyAmpersand().serialize(component);
81+
case LEGACY_SECTION ->
82+
LegacyComponentSerializer.legacySection().serialize(component);
83+
case MINECRAFT_JSON -> GsonComponentSerializer.gson().serialize(component);
84+
case TRANSLATION_KEY ->
85+
component instanceof TranslatableComponent translatableComponent
86+
? translatableComponent.key()
87+
: PlainTextComponentSerializer.plainText().serialize(component);
88+
};
89+
}
90+
91+
private Component deserialize(String string, AdventureComponentFormat format) {
92+
return switch (format) {
93+
case MINI_MESSAGE ->
94+
MiniMessage.miniMessage().deserialize(string);
95+
case LEGACY_AMPERSAND ->
96+
LegacyComponentSerializer.legacyAmpersand().deserialize(string);
97+
case LEGACY_SECTION ->
98+
LegacyComponentSerializer.legacySection().deserialize(string);
99+
case MINECRAFT_JSON ->
100+
GsonComponentSerializer.gson().deserialize(string);
101+
case TRANSLATION_KEY ->
102+
Component.translatable(string);
103+
};
104+
}
105+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package de.exlll.configlib;
2+
3+
import net.kyori.adventure.key.Key;
4+
import net.kyori.adventure.sound.Sound;
5+
import net.kyori.adventure.text.Component;
6+
7+
import java.util.List;
8+
9+
/**
10+
* Utility class providing default serializers for Adventure library types.
11+
*/
12+
public final class AdventureConfigLib {
13+
// Use MiniMessage as the default format since MiniMessage covered all
14+
// component type
15+
private static final List<AdventureComponentFormat> DEFAULT_FORMAT_ORDER = List.of(
16+
AdventureComponentFormat.MINI_MESSAGE
17+
);
18+
19+
private AdventureConfigLib() {
20+
}
21+
22+
/**
23+
* Adds default Adventure serializers to the configuration builder.
24+
*
25+
* @param builder the configuration properties builder
26+
* @param <B> the builder type
27+
* @return the builder with default serializers added
28+
*/
29+
public static <B extends ConfigurationProperties.Builder<B>>
30+
ConfigurationProperties.Builder<B> addDefaults(
31+
ConfigurationProperties.Builder<B> builder) {
32+
return addDefaults(builder, DEFAULT_FORMAT_ORDER.get(0), DEFAULT_FORMAT_ORDER);
33+
}
34+
35+
/**
36+
* Adds default Adventure serializers to the configuration builder with custom
37+
* format orders.
38+
*
39+
* @param builder the configuration properties builder
40+
* @param serializeFormat the format to use when serializing
41+
* components
42+
* @param deserializeOrder the order of formats to try when deserializing
43+
* components
44+
* @param <B> the builder type
45+
* @return the builder with default serializers added
46+
*/
47+
public static <B extends ConfigurationProperties.Builder<B>>
48+
ConfigurationProperties.Builder<B> addDefaults(
49+
ConfigurationProperties.Builder<B> builder,
50+
AdventureComponentFormat serializeFormat,
51+
List<AdventureComponentFormat> deserializeOrder) {
52+
builder.addSerializer(Component.class,
53+
new AdventureComponentSerializer(serializeFormat, deserializeOrder));
54+
builder.addSerializer(Key.class, new AdventureKeySerializer());
55+
builder.addSerializer(Sound.class, new AdventureSoundSerializer());
56+
return builder;
57+
}
58+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package de.exlll.configlib;
2+
3+
import net.kyori.adventure.key.Key;
4+
5+
import java.util.OptionalInt;
6+
7+
/**
8+
* Serializer for Adventure {@link Key} objects.
9+
*/
10+
public final class AdventureKeySerializer implements Serializer<Key, String> {
11+
12+
private final String defaultNamespace;
13+
14+
/**
15+
* Creates a new KeySerializer with the specified default namespace.
16+
*
17+
* @param defaultNamespace the default namespace to use when deserializing keys
18+
* without a namespace
19+
* @throws IllegalArgumentException if the namespace is invalid
20+
*/
21+
public AdventureKeySerializer(String defaultNamespace) {
22+
this.defaultNamespace = defaultNamespace;
23+
OptionalInt result = Key.checkNamespace(defaultNamespace);
24+
if (result.isPresent()) {
25+
throw new IllegalArgumentException(
26+
"Invalid namespace at index " + result.getAsInt() + ": "
27+
+ defaultNamespace);
28+
}
29+
}
30+
31+
/**
32+
* Creates a new KeySerializer using Adventure's default namespace (minecraft).
33+
*/
34+
public AdventureKeySerializer() {
35+
this.defaultNamespace = null; // Use Adventure's default namespace
36+
}
37+
38+
@Override
39+
public String serialize(Key element) {
40+
return element.asString();
41+
}
42+
43+
@Override
44+
public Key deserialize(String element) {
45+
if (this.defaultNamespace == null) {
46+
return Key.key(element);
47+
}
48+
49+
return Key.key(this.defaultNamespace, element);
50+
}
51+
}

0 commit comments

Comments
 (0)