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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Currently, the plugin supports the following features:
- [**MiniMessage**](https://docs.papermc.io/adventure/minimessage/format/) support for **creating changelogs**
- A **completely customizable** changelog screen
- Customizable **translations** based on the player's **client language**
- Optional support for **legacy `&`-based color/formatting codes** (e.g. `&a`, `&l`, `&r`) alongside MiniMessage
- **Pre-defined authors**, configurable in `config.yml`, that can be quick-selected in the editor dialog

## Supported Server Software & Versions
ServerChangelogs is currently available as:
Expand Down Expand Up @@ -114,7 +116,7 @@ ServerChangelogs was originally made for a **smaller minecraft server** I'm a pa

The roadmap below contains some of the features that I'm **planning to add** in the future:
- [ ] **JDBC (relational) database support**: Currently, the plugin stores all changelog data (including who has already seen what changelogs) in a plain YAML file. The reason for this mostly comes down to the stuff above (we wanted a solution fast), so we didn't prioritize storage. This does start to cause problems over time, since the amount of data that has to be (de-)serialized for each startup/write operation increases by a lot! A proper implementation would also have to consider things like auto-migration from the existing YAML files.
- [ ] **Pre-defined authors**: Rather than having to enter the name of the changelog's author every time (possibly including formatting), there could be a list of pre-defined authors in the plugin config that a user could choose from.
- [x] **Pre-defined authors**: Rather than having to enter the name of the changelog's author every time (possibly including formatting), there could be a list of pre-defined authors in the plugin config that a user could choose from.
- [ ] **Customizable changelog dialog layout**: Currently, the changelog dialog always shows all previous changelogs, with unread ones marked red. In the future, it'd be nice to have an option for this, e.g. so players on join could also only see the most recent changelog.
- [ ] **Paged changelogs**: At the moment, all existing changelogs are just added to the dialog without any additional checks. While this is fine with just a few of them, it could quickly become an issue with a long history of changelogs. So adding a page system with a configurable amount of changelogs per page would be great!
- [ ] **Player first-join storage**: Right now, changelogs are just displayed to players upon joining whenever they haven't read them (except for the **first time** a player joins a server). This can be very confusing to new-ish players, since they weren't there for all the previous changes to begin with! A possible (while not 100% perfect) solution to this would be storing when the player initially joined the server / was first seen by the plugin, to then only display relevant changelogs to them.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.codingcat.changelogs.base.dialog.IDialog;
import com.codingcat.changelogs.base.event.ChangelogJoinListener;
import com.codingcat.changelogs.base.lang.TranslationSource;
import com.codingcat.changelogs.base.util.LegacyColorTranslator;
import com.codingcat.changelogs.base.util.ResourceUtil;
import com.codingcat.changelogs.platformapi.ChangelogsPlatform;
import com.codingcat.changelogs.platformapi.Entrypoint;
Expand Down Expand Up @@ -58,16 +59,17 @@ public ServerChangelogs(@NotNull ChangelogsPlatform platform) {
public void onStart() {
logger = getPlatform().getComponentLogger();
Path translationPath = getPlatform().getDataPath().resolve("lang");
if (translationPath.toFile().mkdirs()) {
logger.info("Creating default translation files...");
ResourceUtil.readResourcesAsString("lang").forEach((fname, contents) -> {
try {
Files.writeString(translationPath.resolve(fname), contents, StandardCharsets.UTF_8, StandardOpenOption.CREATE);
} catch (IOException e) {
logger.warn("Failed to create default translation file \"{}\":", fname, e);
}
});
}
translationPath.toFile().mkdirs();
ResourceUtil.readResourcesAsString("lang").forEach((fname, contents) -> {
Path targetFile = translationPath.resolve(fname);
if (targetFile.toFile().exists()) return;
logger.info("Creating missing default translation file \"{}\"...", fname);
try {
Files.writeString(targetFile, contents, StandardCharsets.UTF_8, StandardOpenOption.CREATE);
} catch (IOException e) {
logger.warn("Failed to create default translation file \"{}\":", fname, e);
}
});
this.translationSource = new TranslationSource(translationPath, getPlatform().getChangelogsMeta(), getPlatform().getPlatformMeta(), logger);
this.translationSource.reload();
info("console.startup");
Expand All @@ -83,6 +85,7 @@ public void onStart() {
}
this.config = new PluginConfig(this, configPath);
this.config.tryReload();
LegacyColorTranslator.setEnabled(this.config.useLegacyColorCodes());
this.changelogStorage = this.config.createChangelogStorage();
info("console.startup_storage", text(this.changelogStorage.getDisplayName()));
this.changelogStorage.init();
Expand Down Expand Up @@ -111,6 +114,7 @@ public void reload(boolean force) throws IOException, YAMLException, IDialog.Des
this.config.reload();
String err = this.config.validate();
if (err != null) throw new YAMLException(err);
LegacyColorTranslator.setEnabled(this.config.useLegacyColorCodes());
this.changelogStorage.shutdown();
this.changelogStorage = this.config.createChangelogStorage();
info("console.startup_storage", text(this.changelogStorage.getDisplayName()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public void reload() throws IOException, YAMLException {
getDateFormatter();
createChangelogHeaderStack();
getEnabledManualWorkarounds();
getPredefinedAuthors();
return null;
} catch (Exception e) {
return e.getMessage();
Expand Down Expand Up @@ -96,6 +97,14 @@ public boolean useNativeFallbackPermissions() {
return getBoolean("use_native_fallback_permissions", false);
}

public boolean useLegacyColorCodes() {
return getBoolean("legacy_color_codes", true);
}

public @NotNull List<String> getPredefinedAuthors() {
return getList("predefined_authors", String.class);
}

public @Nullable ItemStack createChangelogHeaderStack() {
String value = getString("dialog_header_item", null);
return value != null ? createStack(value) : null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ public void recreate() {
this.dialogMap.clear();
register.accept(new ChangelogEditorDialog(
plugin.getChangelogStorage(),
plugin.pluginConfig().useNativeFallbackPermissions()
plugin.pluginConfig().useNativeFallbackPermissions(),
plugin.pluginConfig().getPredefinedAuthors()
));
register.accept(new ChangelogDialog(
plugin.getChangelogStorage(), this,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

Expand All @@ -53,6 +54,7 @@ public class ChangelogEditorDialog implements IDialog {
private final @Getter String id = "changelog_editor";
private final @NotNull ChangelogStorage storage;
private final boolean useFallbackPermissions;
private final @NotNull List<String> predefinedAuthors;

@Override
public @NotNull Dialog build(@NotNull IPlayer p, @NotNull DialogSessionManager sessionManager) {
Expand Down Expand Up @@ -85,14 +87,14 @@ public class ChangelogEditorDialog implements IDialog {
new PlainMessage(translatableManual(p, sessionTranslation + ".subtitle", text(session.getEntryUID() + 1)), 160),
false, false, 15, 15);
PacketEventsFix.fixItemBody(itemBody);
List<DialogBody> body = List.of(itemBody,
List<DialogBody> body = new ArrayList<>(List.of(itemBody,
new PlainMessageDialogBody(new PlainMessage(translatableManual(p, "dialog.editor.hint"), ChangelogDialog.LINE_WIDTH)),
new PlainMessageDialogBody(new PlainMessage(!previewLines.isEmpty() ? createLinesComponent(p, lineMapper, previewLines) : translatableManual(p, "dialog.editor.empty_preview"), ChangelogDialog.LINE_WIDTH))
);
if (session.isShowRestoredMessage()) {
body = new ArrayList<>(body);
));
if (session.isShowRestoredMessage())
body.add(1, new PlainMessageDialogBody(new PlainMessage(translatableManual(p, "dialog.editor.restored_session"), 400)));
}
if (!predefinedAuthors.isEmpty())
body.add(new PlainMessageDialogBody(new PlainMessage(createPredefinedAuthorsComponent(p, sessionManager), ChangelogDialog.LINE_WIDTH)));
List<Input> inputs = List.of(
new Input("line", new TextInputControl(350, translatableManual(p, "dialog.editor.input.contents"),
true, session.getCurrentLine(), 5000, null)),
Expand All @@ -111,6 +113,21 @@ public class ChangelogEditorDialog implements IDialog {
return new MultiActionDialog(common, buttons, closeBtn, 3);
}

private @NotNull Component createPredefinedAuthorsComponent(@NotNull IPlayer p, @NotNull DialogSessionManager sessionManager) {
String prefix = "dialog.editor.predefined_authors.";
Component entries = Component.empty();
for (int i = 0; i < predefinedAuthors.size(); i++) {
Component parsedAuthor = MiniMessage.miniMessage().deserialize(predefinedAuthors.get(i));
NBTCompound payload = new NBTCompound();
payload.setTag("author_index", new NBTInt(i));
Component entry = translatableManual(p, prefix + "entry", parsedAuthor)
.clickEvent(sessionManager.createSessionBasedClickEvent(this, "select_predefined_author", payload));
entries = entries.append(entry);
if (i < predefinedAuthors.size() - 1) entries = entries.appendSpace();
}
return translatableManual(p, prefix + "label", entries);
}

private @NotNull Dialog buildConfirmCloseDialog(@NotNull IPlayer p, @NotNull DialogSessionManager sessionManager) {
EditorSession session = sessionManager.getSessionData(p, EditorSession.class);
String prefix = "dialog.editor.confirm_close.";
Expand Down Expand Up @@ -179,6 +196,13 @@ public void onActionTriggered(@NotNull String action, @Nullable NBTCompound data
session.setCurrentLine("");
this.showTo(source, sessionManager, DialogPackets.PacketPhase.PLAY);
}
case "select_predefined_author" -> {
if (data == null) return;
int authorIndex = data.getNumberTagValueOrThrow("author_index").intValue();
if (authorIndex < 0 || authorIndex >= predefinedAuthors.size()) return;
session.setAuthor(predefinedAuthors.get(authorIndex));
this.showTo(source, sessionManager, DialogPackets.PacketPhase.PLAY);
}
case "start_edit_line", "remove_line" -> {
if (data == null) return;
int lineIdx = data.getNumberTagValueOrThrow("target_line").intValue();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.codingcat.changelogs.base.data.ChangelogEntry;
import com.codingcat.changelogs.base.data.ChangelogStorage;
import com.codingcat.changelogs.base.util.LegacyColorTranslator;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
Expand Down Expand Up @@ -37,12 +38,13 @@ public abstract class EditorSession {

public @NotNull @Unmodifiable List<Component> deserializeLines() {
return getRawLines().stream()
.map(LegacyColorTranslator::translate)
.map(MiniMessage.miniMessage()::deserialize)
.toList();
}

public @Nullable Component deserializeAuthor() {
return !author.isBlank() ? MiniMessage.miniMessage().deserialize(getAuthor()) : null;
return !author.isBlank() ? MiniMessage.miniMessage().deserialize(LegacyColorTranslator.translate(getAuthor())) : null;
}

public static class Create extends EditorSession {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.codingcat.changelogs.base.util;

import org.jetbrains.annotations.NotNull;

import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Translates legacy ampersand-based formatting codes (e.g. {@code &a}, {@code &l}, {@code &r})
* into their MiniMessage tag equivalents, so that changelog authors who are used to the
* legacy formatting system can keep using it, optionally alongside regular MiniMessage tags.
* <p>
* Color codes are translated into a {@code <reset>} tag followed by the matching color tag,
* mirroring vanilla behavior where selecting a new color also clears any previously active
* formatting (bold, italic, etc.). Formatting codes (bold, italic, ...) simply stack on top of
* each other, and {@code &r} maps directly to {@code <reset>}.
*/
public final class LegacyColorTranslator {
private static final @NotNull Pattern LEGACY_CODE_PATTERN = Pattern.compile("&([0-9a-fk-orA-FK-OR])");

private static final @NotNull Map<Character, String> COLORS = Map.ofEntries(
Map.entry('0', "black"), Map.entry('1', "dark_blue"), Map.entry('2', "dark_green"),
Map.entry('3', "dark_aqua"), Map.entry('4', "dark_red"), Map.entry('5', "dark_purple"),
Map.entry('6', "gold"), Map.entry('7', "gray"), Map.entry('8', "dark_gray"),
Map.entry('9', "blue"), Map.entry('a', "green"), Map.entry('b', "aqua"),
Map.entry('c', "red"), Map.entry('d', "light_purple"), Map.entry('e', "yellow"),
Map.entry('f', "white")
);

private static final @NotNull Map<Character, String> FORMATS = Map.of(
'k', "obfuscated", 'l', "bold", 'm', "strikethrough", 'n', "underlined", 'o', "italic"
);

private static volatile boolean enabled = true;

private LegacyColorTranslator() {
}

/**
* Enables/disables legacy code translation plugin-wide. Should be called whenever the
* plugin configuration is (re-)loaded, mirroring the {@code legacy_color_codes} option.
*/
public static void setEnabled(boolean isEnabled) {
enabled = isEnabled;
}

/**
* Replaces every legacy formatting code in the given raw (unparsed) string with its
* MiniMessage tag equivalent. Regular MiniMessage tags already present in the input are
* left untouched, so both can be freely mixed. Returns the input unchanged if legacy code
* translation is currently disabled, or if the input doesn't contain any {@code &} at all.
*/
public static @NotNull String translate(@NotNull String rawInput) {
if (!enabled || rawInput.indexOf('&') == -1) return rawInput;
Matcher matcher = LEGACY_CODE_PATTERN.matcher(rawInput);
StringBuilder result = new StringBuilder();
while (matcher.find()) {
char code = Character.toLowerCase(matcher.group(1).charAt(0));
String replacement;
if (code == 'r') replacement = "<reset>";
else if (COLORS.containsKey(code)) replacement = "<reset><" + COLORS.get(code) + ">";
else replacement = "<" + FORMATS.get(code) + ">";
matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));
}
matcher.appendTail(result);
return result.toString();
}
}
15 changes: 15 additions & 0 deletions base/src/main/resources/defaults/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,21 @@ dialog_header: true
# Example: "minecraft:paper[minecraft:item_model='my_resourcepack:newspaper_item']"
dialog_header_item: "minecraft:written_book"

# Whether legacy "&"-based color/formatting codes (e.g. &a, &l, &r) should automatically
# be translated into their MiniMessage equivalent when writing changelog lines/authors.
# This is purely additive - regular MiniMessage tags can still be used at the same time.
# Disable this if you don't want "&" to be treated as a formatting character.
legacy_color_codes: true

# A list of pre-defined authors that can be quickly selected when creating/editing a
# changelog in the editor dialog, instead of having to type one out (with formatting) every time.
# Entries support MiniMessage formatting, just like the "author" input field itself.
# Leave this empty to not show any quick-select options.
predefined_authors: []
#predefined_authors:
# - "<green><b>Notch"
# - "<aqua>Admin Team"

# -- DO NOT MODIFY UNLESS YOU KNOW WHAT YOU ARE DOING --
# Manually enable workarounds/fixes for certain bugs in e.g. PacketEvents
# that normally don't apply to the current version
Expand Down
3 changes: 3 additions & 0 deletions base/src/main/resources/lang/en_US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ dialog: # Translations for the plugin's dialog UIs
input:
contents: "Line Content"
author: "Author <gray>(optional)</gray>"
predefined_authors:
label: "<translate:icon.info> <gray>Quick-select author: <arg:0>"
entry: "<dark_gray>[<hover:show_text:'<gray>Click to use this author'><arg:0></hover>]"
button:
add_line: "<translate:icon.speech_bubble> <green><b>Add Line"
edit_line: "<translate:icon.confirm> <light_purple><b>Edit Line"
Expand Down
3 changes: 3 additions & 0 deletions base/src/main/resources/lang/ru_RU.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ dialog: # Переводы диалоговых интерфейсов (UI) пл
input:
contents: "Текст строки"
author: "Автор <gray>(необязательно)</gray>"
predefined_authors:
label: "<translate:icon.info> <gray>Быстрый выбор автора: <arg:0>"
entry: "<dark_gray>[<hover:show_text:'<gray>Нажмите, чтобы выбрать этого автора'><arg:0></hover>]"
button:
add_line: "<translate:icon.speech_bubble> <green><b>Добавить строку"
edit_line: "<translate:icon.confirm> <light_purple><b>Изменить строку"
Expand Down