Skip to content
Merged
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
3 changes: 2 additions & 1 deletion resources/assets/edenmod/lang/en_us.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"key.edenmod.open_config": "Open EdenMod config",
"key.edenmod.open_config": "Open Config",
"key.edenmod.open_emote_picker": "Open Emote Picker",
"key.edenmod.open_menu": "Open Menu",
"key.category.minecraft.edenmod": "EdenMod"
}
1 change: 1 addition & 0 deletions resources/edenmod.mixins.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"package": "tel.eden.mod.mixin",
"compatibilityLevel": "JAVA_21",
"client": [
"ChatScreenMixin",
"ClientPacketListenerCommandAliasMixin",
"ClientPacketListenerMixin",
"GuiGraphicsMixin",
Expand Down
66 changes: 50 additions & 16 deletions src/tel/eden/mod/EdenModClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
import net.minecraft.client.KeyMapping;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.ChatScreen;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.ClickEvent;
import net.minecraft.network.chat.Component;
Expand Down Expand Up @@ -135,6 +136,7 @@ public final class EdenModClient implements ClientModInitializer {
private final List<TrackedCommandKeybind> trackedCommandKeybinds = new ArrayList<>();
private KeyMapping openConfigKey;
private KeyMapping createPartyKey;
private KeyMapping openEmotePickerKey;
private BridgeWebSocketClient socket;

public BridgeWebSocketClient socket() {
Expand All @@ -153,6 +155,7 @@ public BridgeWebSocketClient socket() {
private volatile boolean updateChecked;
private volatile boolean updateStaged;
private volatile boolean pendingUpdateNotification;
private volatile boolean pendingCenteredEmotePicker;
// Non-null when the bridge rejected our connection; holds the error code
// ("version_rejected", "not_member", "http_401", etc.) so onClientTick can
// show the right message once the player is loaded.
Expand Down Expand Up @@ -189,6 +192,42 @@ public tel.eden.mod.update.UpdateInfo getPendingUpdate() {
return pendingUpdate;
}

public void requestCenteredEmotePicker() {
pendingCenteredEmotePicker = true;
}

public boolean consumeCenteredEmotePickerRequest() {
boolean pending = pendingCenteredEmotePicker;
pendingCenteredEmotePicker = false;
return pending;
}

public boolean matchesOpenEmotePickerMouse(net.minecraft.client.input.MouseButtonEvent event) {
return openEmotePickerKey.matchesMouse(event);
}

public boolean isOpenEmotePickerMouseBound() {
return openEmotePickerKey.saveString().startsWith("key.mouse.");
}

public boolean shouldOpenEmotePickerOnChatOpen() {
Minecraft mc = Minecraft.getInstance();
if (isOpenEmotePickerMouseBound() || mc.options == null) {
return false;
}
return openEmotePickerKey.saveString().equals(mc.options.keyChat.saveString()) && openEmotePickerKey.isDown();
}

public void openCenteredEmotePicker() {
Minecraft mc = Minecraft.getInstance();
requestCenteredEmotePicker();
mc.execute(() -> {
if (!(mc.screen instanceof ChatScreen)) {
mc.setScreen(new ChatScreen("", false));
}
});
}

/** An immutable snapshot of the currently known parties, safe to read off-thread. */
public java.util.List<PartyInfo> knownParties() {
return java.util.List.copyOf(knownParties);
Expand All @@ -215,6 +254,7 @@ public void onInitializeClient() {
KeyMapping.Category edenCategory = new KeyMapping.Category(net.minecraft.resources.Identifier.parse("edenmod"));
openConfigKey = KeyBindingHelper.registerKeyBinding(new KeyMapping("key.edenmod.open_config", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_B, edenCategory));
createPartyKey = KeyBindingHelper.registerKeyBinding(new KeyMapping("key.edenmod.open_menu", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_L, edenCategory));
openEmotePickerKey = KeyBindingHelper.registerKeyBinding(new KeyMapping("key.edenmod.open_emote_picker", InputConstants.Type.MOUSE, GLFW.GLFW_MOUSE_BUTTON_MIDDLE, edenCategory));

ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> {
loginPending = true;
Expand Down Expand Up @@ -278,6 +318,12 @@ private void onClientTick(Minecraft client) {
display(() -> Component.literal("You must be on Wynncraft to open the Eden menu.").withStyle(net.minecraft.ChatFormatting.RED));
}
}
while (openEmotePickerKey.consumeClick()) {
if (client.screen instanceof ChatScreen && isOpenEmotePickerMouseBound()) {
continue;
}
openCenteredEmotePicker();
}
pollCommandKeybinds(client);
if (pendingUpdateNotification && client.player != null) {
pendingUpdateNotification = false;
Expand Down Expand Up @@ -1300,34 +1346,21 @@ private static String normalizeCommandDisplay(String command) {
return normalized == null ? "/" : normalized;
}

/** List every loaded chat emote with its inline glyph and clickable ``:shortcode:``. */
/** Open the chat emote picker, centered in the chat screen. */
private int showEmojis(FabricClientCommandSource source) {
java.util.List<String> codes = EmoteRegistry.shortcodes();
if (codes.isEmpty()) {
source.sendFeedback(DiscordChatFormatter.systemLine("No chat emojis are loaded.", ChatFormatting.YELLOW));
return 1;
}
MutableComponent out = Component.literal("Eden chat emojis (" + codes.size() + ") — click one to insert it, or type :name: in chat:").withStyle(ChatFormatting.GREEN);
for (String code : codes) {
MutableComponent line = Component.literal("\n ");
Integer cp = EmoteRegistry.codepointFor(code);
if (cp != null) {
String glyph = new String(Character.toChars(cp));
line.append(Component.literal(glyph).withStyle(Style.EMPTY.withFont(EmoteRegistry.font()).withColor(ChatFormatting.WHITE)));
line.append(Component.literal(" "));
}
Style click = Style.EMPTY.withColor(ChatFormatting.AQUA).withClickEvent(new ClickEvent.SuggestCommand(":" + code + ":")).withHoverEvent(new HoverEvent.ShowText(Component.literal("Click to put :" + code + ": in your chat box")));
line.append(Component.literal(":" + code + ":").setStyle(click));
out.append(line);
}
source.sendFeedback(out);
openCenteredEmotePicker();
return 1;
}

private record HelpEntry(String command, String description) {
}

private static final List<HelpEntry> HELP_ENTRIES = List.of(new HelpEntry("/eden config", "open the config screen"), new HelpEntry("/eden online", "who's connected to the bridge"), new HelpEntry("/eden cf", "flip a coin"), new HelpEntry("/eden diceroll", "roll a die"), new HelpEntry("/eden emojis", "list all chat emojis and their :syntax:"), new HelpEntry("/eden party", "list open parties (click to join)"), new HelpEntry("/eden party create <raid> [note]", "open a raid party"), new HelpEntry("/eden party join <id>", "join a party"), new HelpEntry("/eden party leave [id]", "leave your party"), new HelpEntry("/eden anni <size> [note]", "open an Annihilation party (2-10)"), new HelpEntry("/eden command alias", "open the command alias editor"), new HelpEntry("/eden command keybind", "open the command keybind editor"), new HelpEntry("/eden update", "check for a pending update"), new HelpEntry("/eden update download", "download the update now (applies on exit)"), new HelpEntry("/eden aspects pending", "members' pending aspects — Chiefs only"), new HelpEntry("/eden gift <member> <aspect|emerald|tome> <amount>", "gift guild rewards — Chiefs only"), new HelpEntry("/eden dump <member>", "gift all guild-bank emeralds to a member — Chiefs only"), new HelpEntry("/eden help", "this help screen"));
private static final List<HelpEntry> HELP_ENTRIES = List.of(new HelpEntry("/eden config", "open the config screen"), new HelpEntry("/eden online", "who's connected to the bridge"), new HelpEntry("/eden cf", "flip a coin"), new HelpEntry("/eden diceroll", "roll a die"), new HelpEntry("/eden emojis", "open the chat emote picker"), new HelpEntry("/eden party", "list open parties (click to join)"), new HelpEntry("/eden party create <raid> [note]", "open a raid party"), new HelpEntry("/eden party join <id>", "join a party"), new HelpEntry("/eden party leave [id]", "leave your party"), new HelpEntry("/eden anni <size> [note]", "open an Annihilation party (2-10)"), new HelpEntry("/eden command alias", "open the command alias editor"), new HelpEntry("/eden command keybind", "open the command keybind editor"), new HelpEntry("/eden update", "check for a pending update"), new HelpEntry("/eden update download", "download the update now (applies on exit)"), new HelpEntry("/eden aspects pending", "members' pending aspects — Chiefs only"), new HelpEntry("/eden gift <member> <aspect|emerald|tome> <amount>", "gift guild rewards — Chiefs only"), new HelpEntry("/eden dump <member>", "gift all guild-bank emeralds to a member — Chiefs only"), new HelpEntry("/eden help", "this help screen"));

private static final class TrackedCommandKeybind {
private final String input;
Expand Down Expand Up @@ -1696,3 +1729,4 @@ public void onError(String messageText) {
});
}
}

64 changes: 64 additions & 0 deletions src/tel/eden/mod/chat/ChatEmoteFormatter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package tel.eden.mod.chat;

import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.HoverEvent;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.network.chat.Style;
import net.minecraft.util.FormattedCharSequence;

/**
* Formats raw chat-editor text so complete {@code :shortcode:} tokens render as
* inline bitmap emotes while the underlying value stays plain text.
*/
public final class ChatEmoteFormatter {
private static final Pattern EMOTE_PATTERN = Pattern.compile(":(?<emote>[a-zA-Z0-9_+\\-]{2,32}):");

private ChatEmoteFormatter() {
}

/**
* Format the visible editor text for an {@link net.minecraft.client.gui.components.EditBox}.
* Returns {@code null} when the line has no complete emote token so vanilla rendering
* can handle it unchanged.
*/
public static FormattedCharSequence format(String text) {
if (!EMOTE_PATTERN.matcher(text).find()) {
return null;
}
return toComponent(text).getVisualOrderText();
}

private static MutableComponent toComponent(String text) {
MutableComponent out = Component.empty();
Matcher matcher = EMOTE_PATTERN.matcher(text);
int last = 0;
while (matcher.find()) {
if (matcher.start() > last) {
out.append(Component.literal(text.substring(last, matcher.start())));
}
String shortcode = matcher.group("emote");
Integer codepoint = EmoteRegistry.codepointFor(shortcode);
if (codepoint != null) {
String glyph = new String(Character.toChars(codepoint));
Style emoteStyle = Style.EMPTY.withFont(EmoteRegistry.font()).withColor(ChatFormatting.WHITE).withHoverEvent(new HoverEvent.ShowText(Component.literal(":" + shortcode + ":").withStyle(ChatFormatting.GRAY)));
out.append(Component.literal(glyph).withStyle(emoteStyle));
} else {
out.append(Component.literal(matcher.group()));
}
last = matcher.end();
}
if (last < text.length()) {
out.append(Component.literal(text.substring(last)));
}
return out;
}

/** Convert a formatted sequence back into a component, mainly for picker/suggestion previews. */
public static Component previewComponent(String text) {
return Optional.ofNullable(format(text)).map(seq -> toComponent(text)).orElseGet(() -> Component.literal(text));
}
}
71 changes: 71 additions & 0 deletions src/tel/eden/mod/config/BridgeConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,62 @@ public String toString() {
/** Client-side bindings that run commands from keyboard or mouse input. */
public List<CommandKeybind> commandKeybinds = new ArrayList<>();

/** Favorited chat emotes shown by the right-click picker when the star filter is enabled. */
public List<String> favoriteEmotes = new ArrayList<>();

public enum ChatEmoteToolsMode {
UI("UI"), AUTO("Auto"), UI_AND_AUTO("UI & Auto"), NONE("None");

private final String label;

ChatEmoteToolsMode(String label) {
this.label = label;
}

public String label() {
return label;
}
}

/** Legacy toggle kept only to migrate older configs into {@link #chatEmoteToolsMode}. */
@Deprecated
public Boolean chatEmoteUiEnabled = null;

/** Which chat emote tools are enabled: inline/picker UI, autocomplete, both, or none. */
public ChatEmoteToolsMode chatEmoteToolsMode = ChatEmoteToolsMode.UI_AND_AUTO;

/** Visible emote-picker columns in the chat overlay. */
public int emotePickerColumns = 5;

/** Visible emote-picker rows in the chat overlay before scrolling. */
public int emotePickerRows = 4;

/** Whether chat emote autocomplete should only suggest favorited emotes. */
public boolean autocompleteFavoriteEmotes = false;

public enum EmotePickerOpenMode {
CURSOR("Cursor"), CENTER("Center"), CUSTOM("Custom");

private final String label;

EmotePickerOpenMode(String label) {
this.label = label;
}

public String label() {
return label;
}
}

/** Where the chat emote picker should open when triggered. */
public EmotePickerOpenMode emotePickerOpenMode = EmotePickerOpenMode.CURSOR;

/** Saved custom top-left X position for the chat emote picker. */
public int emotePickerCustomX = -1;

/** Saved custom top-left Y position for the chat emote picker. */
public int emotePickerCustomY = -1;

public static final class CommandAlias {
public String alias = "";
public String command = "";
Expand Down Expand Up @@ -133,6 +189,21 @@ public static BridgeConfig load() {
if (config.commandKeybinds == null) {
config.commandKeybinds = new ArrayList<>();
}
if (config.favoriteEmotes == null) {
config.favoriteEmotes = new ArrayList<>();
}
if (config.chatEmoteUiEnabled != null) {
config.chatEmoteToolsMode = config.chatEmoteUiEnabled ? ChatEmoteToolsMode.UI_AND_AUTO : ChatEmoteToolsMode.NONE;
config.chatEmoteUiEnabled = null;
}
if (config.chatEmoteToolsMode == null) {
config.chatEmoteToolsMode = ChatEmoteToolsMode.UI_AND_AUTO;
}
if (config.emotePickerOpenMode == null) {
config.emotePickerOpenMode = EmotePickerOpenMode.CURSOR;
}
config.emotePickerColumns = Math.max(1, Math.min(10, config.emotePickerColumns));
config.emotePickerRows = Math.max(1, Math.min(10, config.emotePickerRows));
config.imagePreviewSize = Math.max(1, Math.min(100, config.imagePreviewSize));
return config;
}
Expand Down
6 changes: 6 additions & 0 deletions src/tel/eden/mod/gui/BridgeConfigScreen.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ protected void init() {
addToggleRow("Bridge", () -> config.enabled, v -> config.enabled = v, "Enabled", "Disabled", true);
addToggleRow("My login/logout messages", () -> config.announceSelfPresence, v -> config.announceSelfPresence = v, "On", "Off", true);
addToggleRow("Party feed", () -> config.partyAnnounce, v -> config.partyAnnounce = v, "On", "Off", true);
addCycleRow("Chat emote tools", () -> config.chatEmoteToolsMode.label(), () -> config.chatEmoteToolsMode = nextChatEmoteToolsMode(config.chatEmoteToolsMode), () -> config.chatEmoteToolsMode = BridgeConfig.ChatEmoteToolsMode.UI_AND_AUTO);
addCycleRow("Game messages", () -> shortGameModeLabel(config.gameDisplayMode), () -> config.gameDisplayMode = nextGameMode(config.gameDisplayMode), () -> config.gameDisplayMode = BridgeConfig.GameDisplayMode.ALL);
PreviewSizeSlider slider = new PreviewSizeSlider(CONTROL_W, 20);
addSliderRow("Image preview size", slider, slider::syncFromConfig, () -> config.imagePreviewSize = 40);
Expand Down Expand Up @@ -148,6 +149,11 @@ private BridgeConfig.GameDisplayMode nextGameMode(BridgeConfig.GameDisplayMode c
return values[(current.ordinal() + 1) % values.length];
}

private BridgeConfig.ChatEmoteToolsMode nextChatEmoteToolsMode(BridgeConfig.ChatEmoteToolsMode current) {
BridgeConfig.ChatEmoteToolsMode[] values = BridgeConfig.ChatEmoteToolsMode.values();
return values[(current.ordinal() + 1) % values.length];
}

private String shortGameModeLabel(BridgeConfig.GameDisplayMode mode) {
return switch (mode) {
case ALL -> "Shown";
Expand Down
Loading
Loading