Support custom formatting for pronouns, along with a new database format using a palette

This commit is contained in:
Ash B 2021-08-07 16:12:23 +01:00
parent 9f9977a3da
commit 915fd82f15
No known key found for this signature in database
GPG key ID: 1AE71DC3E127235F
12 changed files with 416 additions and 39 deletions

View file

@ -4,7 +4,7 @@ plugins {
`maven-publish`
}
version = "1.0.0"
version = "1.1.0"
group = "io.github.ashisbored"
repositories {
@ -27,6 +27,10 @@ dependencies {
// placeholder-api
modImplementation(libs.placeholder.api)
include(libs.placeholder.api)
// more-codecs
modImplementation(libs.more.codecs)
include(libs.more.codecs)
}
tasks.processResources {

View file

@ -6,6 +6,7 @@ fabric-loader = "0.11.6"
fabric-api = "0.37.1+1.17"
placeholder-api = "1.0.1+1.17"
more-codecs = "0.2.0"
[libraries]
minecraft = { module = "com.mojang:minecraft", version.ref = "minecraft" }
@ -15,3 +16,4 @@ fabric-loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric-l
fabric-api = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fabric-api" }
placeholder-api = { module = "eu.pb4:placeholder-api", version.ref = "placeholder-api" }
more-codecs = { module = "xyz.nucleoid:more-codecs", version.ref = "more-codecs" }

View file

@ -5,8 +5,10 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.mojang.serialization.JsonOps;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import io.github.ashisbored.playerpronouns.data.Pronoun;
import net.fabricmc.loader.api.FabricLoader;
import java.io.IOException;
@ -19,15 +21,15 @@ import java.util.Optional;
public class Config {
private static final Codec<Config> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Codec.BOOL.fieldOf("allow_custom").forGetter(config -> config.allowCustom),
Codec.STRING.listOf().fieldOf("single").forGetter(config -> config.single),
Codec.STRING.listOf().fieldOf("pairs").forGetter(config -> config.pairs)
Pronoun.CODEC.listOf().fieldOf("single").forGetter(config -> config.single),
Pronoun.CODEC.listOf().fieldOf("pairs").forGetter(config -> config.pairs)
).apply(instance, Config::new));
private final boolean allowCustom;
private final List<String> single;
private final List<String> pairs;
private final List<Pronoun> single;
private final List<Pronoun> pairs;
private Config(boolean allowCustom, List<String> single, List<String> pairs) {
private Config(boolean allowCustom, List<Pronoun> single, List<Pronoun> pairs) {
this.allowCustom = allowCustom;
this.single = single;
this.pairs = pairs;
@ -41,11 +43,11 @@ public class Config {
return allowCustom;
}
public List<String> getSingle() {
public List<Pronoun> getSingle() {
return single;
}
public List<String> getPairs() {
public List<Pronoun> getPairs() {
return pairs;
}
@ -69,7 +71,10 @@ public class Config {
String s = Files.readString(path);
JsonParser parser = new JsonParser();
JsonElement ele = parser.parse(s);
return CODEC.decode(JsonOps.INSTANCE, ele).map(Pair::getFirst).result().orElseGet(Config::new);
DataResult<Config> result = CODEC.decode(JsonOps.INSTANCE, ele).map(Pair::getFirst);
Optional<DataResult.PartialResult<Config>> err = result.error();
err.ifPresent(e -> PlayerPronouns.LOGGER.warn("Failed to load config: {}", e.message()));
return result.result().orElseGet(Config::new);
} catch (IOException e) {
PlayerPronouns.LOGGER.warn("Failed to load config!", e);
return new Config();

View file

@ -3,8 +3,9 @@ package io.github.ashisbored.playerpronouns;
import eu.pb4.placeholders.PlaceholderAPI;
import eu.pb4.placeholders.PlaceholderResult;
import io.github.ashisbored.playerpronouns.command.PronounsCommand;
import io.github.ashisbored.playerpronouns.data.BinaryPronounDatabase;
import io.github.ashisbored.playerpronouns.data.PronounDatabase;
import io.github.ashisbored.playerpronouns.data.PronounList;
import io.github.ashisbored.playerpronouns.data.Pronouns;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.command.v1.CommandRegistrationCallback;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
@ -24,7 +25,7 @@ public class PlayerPronouns implements ModInitializer {
public static final Logger LOGGER = LogManager.getLogger();
public static final String MOD_ID = "playerpronouns";
private static BinaryPronounDatabase pronounDatabase;
private static PronounDatabase pronounDatabase;
public static Config config;
@Override
@ -40,7 +41,7 @@ public class PlayerPronouns implements ModInitializer {
if (!Files.exists(playerData)) {
Files.createDirectories(playerData);
}
pronounDatabase = BinaryPronounDatabase.load(playerData.resolve("pronouns.dat"));
pronounDatabase = PronounDatabase.load(playerData.resolve("pronouns.dat"));
} catch (IOException e) {
LOGGER.error("Failed to create/load pronoun database!", e);
}
@ -69,8 +70,26 @@ public class PlayerPronouns implements ModInitializer {
if (pronounDatabase == null) {
return PlaceholderResult.value("Unknown");
}
String pronouns = pronounDatabase.get(player.getUuid());
return PlaceholderResult.value(Objects.requireNonNullElse(pronouns, "Unknown"));
Pronouns pronouns = pronounDatabase.get(player.getUuid());
if (pronouns == null) {
return PlaceholderResult.value("Unknown");
}
return PlaceholderResult.value(pronouns.formatted());
});
PlaceholderAPI.register(new Identifier(MOD_ID, "raw_pronouns"), ctx -> {
if (!ctx.hasPlayer()) {
return PlaceholderResult.invalid("missing player");
}
ServerPlayerEntity player = ctx.getPlayer();
if (pronounDatabase == null) {
return PlaceholderResult.value("Unknown");
}
Pronouns pronouns = pronounDatabase.get(player.getUuid());
if (pronouns == null) {
return PlaceholderResult.value("Unknown");
}
return PlaceholderResult.value(pronouns.raw());
});
}
@ -82,7 +101,7 @@ public class PlayerPronouns implements ModInitializer {
pronounDatabase.save(playerData.resolve("pronouns.dat"));
}
public static boolean setPronouns(ServerPlayerEntity player, String pronouns) {
public static boolean setPronouns(ServerPlayerEntity player, Pronouns pronouns) {
if (pronounDatabase == null) return false;
pronounDatabase.put(player.getUuid(), pronouns);

View file

@ -15,7 +15,7 @@ public class PronounsArgument {
.suggests((ctx, builder) -> {
String remaining = builder.getRemainingLowerCase();
for (String pronouns : PronounList.get().getCalculatedPronounStrings()) {
for (String pronouns : PronounList.get().getCalculatedPronounStrings().keySet()) {
if (pronouns.toLowerCase(Locale.ROOT).startsWith(remaining)) {
builder.suggest(pronouns);
}

View file

@ -4,11 +4,15 @@ import com.mojang.brigadier.Command;
import com.mojang.brigadier.CommandDispatcher;
import io.github.ashisbored.playerpronouns.PlayerPronouns;
import io.github.ashisbored.playerpronouns.data.PronounList;
import io.github.ashisbored.playerpronouns.data.Pronouns;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.LiteralText;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import java.util.Map;
import static com.mojang.brigadier.arguments.StringArgumentType.getString;
import static io.github.ashisbored.playerpronouns.command.PronounsArgument.pronouns;
import static net.minecraft.server.command.CommandManager.literal;
@ -19,17 +23,26 @@ public class PronounsCommand {
.then(pronouns("pronouns")
.executes(ctx -> {
ServerPlayerEntity player = ctx.getSource().getPlayer();
String pronouns = getString(ctx, "pronouns");
String pronounsString = getString(ctx, "pronouns");
if (!PlayerPronouns.config.allowCustom() && !PronounList.get().getCalculatedPronounStrings().contains(pronouns)) {
Map<String, Text> pronounTexts = PronounList.get().getCalculatedPronounStrings();
if (!PlayerPronouns.config.allowCustom() && !pronounTexts.containsKey(pronounsString)) {
ctx.getSource().sendError(new LiteralText("Custom pronouns have been disabled by the server administrator."));
return 0;
}
Pronouns pronouns;
if (pronounTexts.containsKey(pronounsString)) {
pronouns = new Pronouns(pronounsString, pronounTexts.get(pronounsString));
} else {
pronouns = new Pronouns(pronounsString, new LiteralText(pronounsString));
}
if (!PlayerPronouns.setPronouns(player, pronouns)) {
ctx.getSource().sendError(new LiteralText("Failed to update pronouns, sorry"));
} else {
ctx.getSource().sendFeedback(new LiteralText("Updated your pronouns to " + pronouns + "!")
ctx.getSource().sendFeedback(new LiteralText("Updated your pronouns to ")
.append(pronouns.formatted())
.formatted(Formatting.GREEN), false);
}

View file

@ -2,11 +2,14 @@ package io.github.ashisbored.playerpronouns.data;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.text.LiteralText;
import net.minecraft.text.Text;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.UUID;
public class BinaryPronounDatabase {
@ -48,6 +51,19 @@ public class BinaryPronounDatabase {
}
}
public static PalettePronounDatabase convert(Path path) throws IOException {
Object2ObjectMap<UUID, Pronouns> pronouns = new Object2ObjectOpenHashMap<>();
Map<String, Text> pronounStrings = PronounList.get().getCalculatedPronounStrings();
for (var entry : BinaryPronounDatabase.load(path).data.entrySet()) {
Text formatted = new LiteralText(entry.getValue());
if (pronounStrings.containsKey(entry.getValue())) {
formatted = pronounStrings.get(entry.getValue());
}
pronouns.put(entry.getKey(), new Pronouns(entry.getValue(), formatted));
}
return new PalettePronounDatabase(pronouns);
}
public static BinaryPronounDatabase load(Path path) throws IOException {
if (!Files.exists(path)) {
return new BinaryPronounDatabase();

View file

@ -0,0 +1,139 @@
package io.github.ashisbored.playerpronouns.data;
import com.google.gson.JsonParser;
import com.mojang.serialization.JsonOps;
import io.github.ashisbored.playerpronouns.PlayerPronouns;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.util.Pair;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
/**
* An improved version of {@link io.github.ashisbored.playerpronouns.data.BinaryPronounDatabase} that uses a palette
* for efficiency when storing lots of players. It also supports versioning of the file to allow for changes to be
* made to the format in the future.
*/
public class PalettePronounDatabase implements PronounDatabase {
public static final int VERSION_NUMBER = 1;
private static final JsonParser JSON_PARSER = new JsonParser();
private final Object2ObjectMap<UUID, Pronouns> data;
protected PalettePronounDatabase(Object2ObjectMap<UUID, Pronouns> data) {
this.data = data;
}
private PalettePronounDatabase() {
this(new Object2ObjectOpenHashMap<>());
}
@Override
public void put(UUID player, Pronouns pronouns) {
this.data.put(player, pronouns);
}
@Override
public @Nullable Pronouns get(UUID player) {
return this.data.get(player);
}
@Override
public synchronized void save(Path path) throws IOException {
try (OutputStream os = Files.newOutputStream(path);
DataOutputStream out = new DataOutputStream(os)) {
out.writeShort(0x4568);
out.writeInt(VERSION_NUMBER);
Pair<List<Pronouns>, Object2IntMap<UUID>> pair = this.convertToPalette();
List<Pronouns> palette = pair.getLeft();
Object2IntMap<UUID> values = pair.getRight();
out.writeInt(palette.size());
for (Pronouns pronouns : palette) {
String p = Pronouns.CODEC.encodeStart(JsonOps.INSTANCE, pronouns).result().orElseThrow().toString();
out.writeUTF(p);
}
out.writeInt(values.size());
for (var entry : values.object2IntEntrySet()) {
out.writeLong(entry.getKey().getMostSignificantBits());
out.writeLong(entry.getKey().getLeastSignificantBits());
out.writeInt(entry.getIntValue());
}
}
}
private Pair<List<Pronouns>, Object2IntMap<UUID>> convertToPalette() {
List<Pronouns> palette = new ArrayList<>();
Object2IntMap<UUID> values = new Object2IntOpenHashMap<>();
for (var entry : this.data.entrySet()) {
if (!palette.contains(entry.getValue())) {
palette.add(entry.getValue());
}
values.put(entry.getKey(), palette.indexOf(entry.getValue()));
}
return new Pair<>(palette, values);
}
public static PalettePronounDatabase load(Path path) throws IOException {
if (!Files.exists(path)) {
return new PalettePronounDatabase();
}
try (InputStream is = Files.newInputStream(path);
DataInputStream in = new DataInputStream(is)) {
short magic = in.readShort();
if (magic != 0x4568) {
throw new IOException("Invalid DB magic: " + magic);
}
int version = in.readInt();
if (version > VERSION_NUMBER) {
throw new IOException("DB version " + version + " is greater than the latest supported: " + version);
}
List<Pronouns> palette = new ArrayList<>();
int paletteSize = in.readInt();
for (int i = 0; i < paletteSize; i++) {
String s = in.readUTF();
Optional<Pronouns> optionalPronouns = Pronouns.CODEC.decode(JsonOps.INSTANCE, JSON_PARSER.parse(s)).resultOrPartial(e -> {
throw new RuntimeException(new IOException("Invalid pronouns in database: " + s));
}).map(com.mojang.datafixers.util.Pair::getFirst);
if (optionalPronouns.isEmpty()) {
throw new IOException("Invalid pronouns in database: " + s);
}
palette.add(optionalPronouns.get());
}
Object2ObjectMap<UUID, Pronouns> data = new Object2ObjectOpenHashMap<>();
// V1 Parsing
if (version == 1) {
int playerCount = in.readInt();
for (int i = 0; i < playerCount; i++) {
long mostSigBits = in.readLong();
long leastSigBits = in.readLong();
UUID uuid = new UUID(mostSigBits, leastSigBits);
int pronounIndex = in.readInt();
Pronouns old = data.put(uuid, palette.get(pronounIndex));
if (old != null) {
PlayerPronouns.LOGGER.warn("Duplicate UUID in database: " + uuid);
}
}
}
return new PalettePronounDatabase(data);
}
}
}

View file

@ -0,0 +1,107 @@
package io.github.ashisbored.playerpronouns.data;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.mojang.serialization.DynamicOps;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import io.github.ashisbored.playerpronouns.PlayerPronouns;
import net.minecraft.text.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public record Pronoun(
String pronoun,
Style style
) {
public static final Codec<Pronoun> CODEC = new PronounCodec();
private static class PronounCodec implements Codec<Pronoun> {
private static final Codec<Pronoun> OBJECT_CODEC = RecordCodecBuilder.create(instance -> instance.group(
Codec.STRING.fieldOf("pronoun").forGetter(Pronoun::pronoun),
Codec.STRING.listOf().xmap(Pronoun::styleFrom, Pronoun::fromStyle).fieldOf("style").forGetter(Pronoun::style)
).apply(instance, Pronoun::new));
private PronounCodec() { }
@Override
public <T> DataResult<Pair<Pronoun, T>> decode(DynamicOps<T> ops, T input) {
Optional<String> asString = ops.getStringValue(input).result();
return asString.map(s -> DataResult.success(new Pair<>(new Pronoun(s, Style.EMPTY), ops.empty())))
.orElseGet(() -> OBJECT_CODEC.decode(ops, input));
}
@Override
public <T> DataResult<T> encode(Pronoun input, DynamicOps<T> ops, T prefix) {
if (input.style.isEmpty()) {
return ops.mergeToPrimitive(prefix, ops.createString(input.pronoun));
} else {
return OBJECT_CODEC.encode(input, ops, prefix);
}
}
}
private static Style styleFrom(List<String> formatting) {
Style style = Style.EMPTY;
for (String format : formatting) {
switch (format) {
case "bold" -> style = style.withBold(true);
case "italic" -> style = style.withItalic(true);
case "strikethrough" -> style = style.withStrikethrough(true);
case "underline" -> style = style.withUnderline(true);
case "obfuscated" -> style = style.obfuscated(true);
default -> {
TextColor col = TextColor.parse(format);
if (col != null) {
style = style.withColor(col);
} else {
PlayerPronouns.LOGGER.warn("Invalid formatting: {}", format);
}
}
}
}
return style;
}
private static List<String> fromStyle(Style style) {
List<String> ret = new ArrayList<>();
TextColor colour = style.getColor();
if (colour != null) {
ret.add(colour.toString());
}
if (style.isBold()) {
ret.add("bold");
}
if (style.isItalic()) {
ret.add("italic");
}
if (style.isStrikethrough()) {
ret.add("strikethrough");
}
if (style.isUnderlined()) {
ret.add("underline");
}
if (style.isObfuscated()) {
ret.add("obfuscated");
}
return ret;
}
public MutableText toText() {
return new LiteralText(this.pronoun).setStyle(this.style);
}
@Override
public String toString() {
return this.pronoun;
}
@Override
public int hashCode() {
return this.pronoun.hashCode();
}
}

View file

@ -0,0 +1,43 @@
package io.github.ashisbored.playerpronouns.data;
import io.github.ashisbored.playerpronouns.PlayerPronouns;
import org.jetbrains.annotations.Nullable;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;
public interface PronounDatabase {
void put(UUID player, Pronouns pronouns);
@Nullable Pronouns get(UUID player);
void save(Path path) throws IOException;
static PronounDatabase load(Path path) throws IOException {
if (!Files.exists(path)) {
// Will create a new empty database.
return PalettePronounDatabase.load(path);
}
boolean legacy = false;
try (InputStream is = Files.newInputStream(path);
DataInputStream in = new DataInputStream(is)) {
short magic = in.readShort();
if (magic == 0x4567) {
legacy = true;
}
}
if (legacy) {
PlayerPronouns.LOGGER.info("Old (1.0.0) format pronoun database found, converting...");
Path backupPath = path.getParent().resolve(path.getFileName().toString() + ".bak");
Files.copy(path, backupPath);
PlayerPronouns.LOGGER.info("Old database backed up to {}", backupPath);
return BinaryPronounDatabase.convert(path);
}
return PalettePronounDatabase.load(path);
}
}

View file

@ -5,6 +5,10 @@ import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import io.github.ashisbored.playerpronouns.Config;
import io.github.ashisbored.playerpronouns.PlayerPronouns;
import net.minecraft.text.LiteralText;
import net.minecraft.text.MutableText;
import net.minecraft.text.Style;
import net.minecraft.text.Text;
import net.minecraft.util.Pair;
import java.io.IOException;
@ -15,13 +19,13 @@ import java.util.*;
public class PronounList {
private static PronounList INSTANCE;
private final List<String> defaultSingle;
private final List<String> defaultPairs;
private final List<String> customSingle;
private final List<String> customPairs;
private final List<String> calculatedPronounStrings;
private final List<Pronoun> defaultSingle;
private final List<Pronoun> defaultPairs;
private final List<Pronoun> customSingle;
private final List<Pronoun> customPairs;
private final Map<String, Text> calculatedPronounStrings;
public PronounList(List<String> defaultSingle, List<String> defaultPairs, List<String> customSingle, List<String> customPairs) {
public PronounList(List<Pronoun> defaultSingle, List<Pronoun> defaultPairs, List<Pronoun> customSingle, List<Pronoun> customPairs) {
this.defaultSingle = defaultSingle;
this.defaultPairs = defaultPairs;
this.customSingle = customSingle;
@ -29,24 +33,33 @@ public class PronounList {
this.calculatedPronounStrings = this.computePossibleCombinations();
}
public List<String> getCalculatedPronounStrings() {
public Map<String, Text> getCalculatedPronounStrings() {
return this.calculatedPronounStrings;
}
private List<String> computePossibleCombinations() {
List<String> ret = new ArrayList<>();
ret.addAll(this.defaultSingle);
ret.addAll(this.customSingle);
List<String> combinedPairs = new ArrayList<>();
private Map<String, Text> computePossibleCombinations() {
Map<String, Text> ret = new HashMap<>();
for (Pronoun pronoun : this.defaultSingle) {
ret.put(pronoun.pronoun(), pronoun.toText());
}
for (Pronoun pronoun : this.customSingle) {
ret.put(pronoun.pronoun(), pronoun.toText());
}
List<Pronoun> combinedPairs = new ArrayList<>();
combinedPairs.addAll(this.defaultPairs);
combinedPairs.addAll(this.customPairs);
for (int i = 0; i < combinedPairs.size(); i++) {
for (int j = 0; j < combinedPairs.size(); j++) {
if (i == j) continue;
ret.add(combinedPairs.get(i) + "/" + combinedPairs.get(j));
Pronoun a = combinedPairs.get(i);
Pronoun b = combinedPairs.get(j);
MutableText combined = new LiteralText("");
combined.append(a.toText());
combined.append(new LiteralText("/"));
combined.append(b.toText());
ret.put(a.pronoun() + "/" + b.pronoun(), combined);
}
}
ret.sort(Comparator.naturalOrder());
return ret;
}
@ -55,7 +68,7 @@ public class PronounList {
throw new IllegalStateException("PronounList has already been loaded!");
}
Pair<List<String>, List<String>> defaults = loadDefaults();
Pair<List<Pronoun>, List<Pronoun>> defaults = loadDefaults();
INSTANCE = new PronounList(
defaults.getLeft(),
defaults.getRight(),
@ -71,16 +84,16 @@ public class PronounList {
return INSTANCE;
}
private static Pair<List<String>, List<String>> loadDefaults() {
private static Pair<List<Pronoun>, List<Pronoun>> loadDefaults() {
try (InputStream is = Objects.requireNonNull(PronounList.class.getResourceAsStream("/default_pronouns.json"));
InputStreamReader reader = new InputStreamReader(is)) {
JsonObject ele = new JsonParser().parse(reader).getAsJsonObject();
JsonArray jsonSingle = ele.getAsJsonArray("single");
JsonArray jsonPairs = ele.getAsJsonArray("pairs");
List<String> single = new ArrayList<>();
List<String> pairs = new ArrayList<>();
jsonSingle.forEach(e -> single.add(e.getAsString()));
jsonPairs.forEach(e -> pairs.add(e.getAsString()));
List<Pronoun> single = new ArrayList<>();
List<Pronoun> pairs = new ArrayList<>();
jsonSingle.forEach(e -> single.add(new Pronoun(e.getAsString(), Style.EMPTY)));
jsonPairs.forEach(e -> pairs.add(new Pronoun(e.getAsString(), Style.EMPTY)));
return new Pair<>(single, pairs);
} catch (IOException e) {
PlayerPronouns.LOGGER.error("Failed to load default pronouns!", e);

View file

@ -0,0 +1,16 @@
package io.github.ashisbored.playerpronouns.data;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.text.Text;
import xyz.nucleoid.codecs.MoreCodecs;
public record Pronouns(
String raw,
Text formatted
) {
public static final Codec<Pronouns> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Codec.STRING.fieldOf("raw").forGetter(Pronouns::raw),
MoreCodecs.TEXT.fieldOf("formatted").forGetter(Pronouns::formatted)
).apply(instance, Pronouns::new));
}