diff --git a/build.gradle.kts b/build.gradle.kts index b3ae178..58762f6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,6 +33,7 @@ val sdkVersion = property("library.sdk").toString() val okaeriConfigsVersion = property("library.okaeri_configs").toString() val fabricApiVersion = property("deps.fabric_api").toString() val yaclVersion = property("deps.yacl").toString() +val owoLibVersion = property("deps.owo_lib").toString() val modMenuVersion = property("deps.modmenu").toString() val placeholderApiVersion = if (hasProperty("deps.placeholder_api")) property("deps.placeholder_api").toString() else null @@ -63,6 +64,7 @@ galaxy { add( "https://pkgs.dev.azure.com/djtheredstoner/DevAuth/_packaging/public/maven/v1", + "https://maven.wispforest.io/releases/", GNOMECRAFT_RELEASES, NUCLEOID, OKAERI_RELEASES, OKAERI_SNAPSHOTS, SRNYX_RELEASES, SRNYX_SNAPSHOTS, @@ -82,6 +84,7 @@ galaxy { "deps_loader" to loaderVersion, "deps_fabric_api" to fabricApiVersion, "deps_yacl" to yaclVersion, + "deps_owo_lib" to owoLibVersion, "deps_modmenu" to modMenuVersion)) platformPublishing { @@ -178,6 +181,8 @@ dependencies { modImplementation("dev.isxander:yet-another-config-lib:$yaclVersion") modImplementation("com.terraformersmc:modmenu:$modMenuVersion") placeholderApiVersion?.let { modImplementation("eu.pb4:placeholder-api:$it") } + modImplementation("io.wispforest:owo-lib:$owoLibVersion") + include("io.wispforest:owo-sentinel:$owoLibVersion") // Dev modRuntimeOnly("me.djtheredstoner:DevAuth-fabric:1.2.2") @@ -200,6 +205,12 @@ stonecutter { replace("GuiGraphics", "GuiGraphicsExtractor") replace("net.minecraft.client.renderer.state.CameraRenderState", "net.minecraft.client.renderer.state.level.CameraRenderState") } + replacements.string(current.parsed >= "1.21.11") { + replace("Components", "UIComponents") + replace("Containers", "UIContainers") + replace("ParentComponent", "ParentUIComponent") + replace("io.wispforest.owo.ui.core.Component", "UIComponent") + } replacements.string(current.parsed < "1.21") { replace("GenericMessageScreen", "GenericDirtMessageScreen") } @@ -274,6 +285,13 @@ tasks { } } +java { + toolchain { + languageVersion = JavaLanguageVersion.of(javaVersionProject.majorVersion) + vendor = JvmVendorSpec.JETBRAINS + } +} + loom { fabricModJsonPath = rootProject.file("src/main/resources/fabric.mod.json") // Useful for interface injection @@ -283,7 +301,7 @@ loom { runConfigs.all { preferGradleTask = true - jvmArguments.add("-Dmixin.debug.export=true") // Exports transformed classes for debugging + jvmArguments.add("-Dmixin.debug.export=true -XX:+AllowEnhancedClassRedefinition") // Exports transformed classes for debugging runDirectory = file("../../run") // Shares the run directory between versions } diff --git a/src/main/java/cc/aabss/eventutils/ModMenuImpl.java b/src/main/java/cc/aabss/eventutils/ModMenuImpl.java index f455719..c144968 100644 --- a/src/main/java/cc/aabss/eventutils/ModMenuImpl.java +++ b/src/main/java/cc/aabss/eventutils/ModMenuImpl.java @@ -1,6 +1,6 @@ package cc.aabss.eventutils; -import cc.aabss.eventutils.screen.config.ConfigScreen; +import cc.aabss.eventutils.screen.MenuScreen; import com.terraformersmc.modmenu.api.ConfigScreenFactory; import com.terraformersmc.modmenu.api.ModMenuApi; import dev.kikugie.fletching_table.fabric.Entrypoint; @@ -11,6 +11,6 @@ public class ModMenuImpl implements ModMenuApi { @Override @NotNull public ConfigScreenFactory getModConfigScreenFactory() { - return ConfigScreen::getConfigScreen; + return MenuScreen::new; } } diff --git a/src/main/java/cc/aabss/eventutils/cache/PlayerCache.java b/src/main/java/cc/aabss/eventutils/cache/PlayerCache.java index 577ad34..2fbd8be 100644 --- a/src/main/java/cc/aabss/eventutils/cache/PlayerCache.java +++ b/src/main/java/cc/aabss/eventutils/cache/PlayerCache.java @@ -23,8 +23,8 @@ public PlayerCache(@NotNull EventUtils mod) { public void addToCache(@NotNull Collection players) { players.forEach(player -> { final EnrichedPlayer enriched = new EnrichedPlayer(player); - if (enriched.player.minecraft != null && enriched.player.minecraft.uuid != null) { - addToCache(enriched.player.minecraft.uuid, enriched); + if (enriched.minecraft != null && enriched.minecraft.uuid != null) { + addToCache(enriched.minecraft.uuid, enriched); } }); } @@ -42,8 +42,8 @@ protected EAAction> fetchImpl(@NotNull Collection result = new HashMap<>(); for (final EAPlayer player : players) { final EnrichedPlayer enriched = new EnrichedPlayer(player); - if (enriched.player.minecraft != null && enriched.player.minecraft.uuid != null) { - result.put(enriched.player.minecraft.uuid, enriched); + if (enriched.minecraft != null && enriched.minecraft.uuid != null) { + result.put(enriched.minecraft.uuid, enriched); } } return result; diff --git a/src/main/java/cc/aabss/eventutils/commands/CommandRegister.java b/src/main/java/cc/aabss/eventutils/commands/CommandRegister.java index 21255f0..283c519 100644 --- a/src/main/java/cc/aabss/eventutils/commands/CommandRegister.java +++ b/src/main/java/cc/aabss/eventutils/commands/CommandRegister.java @@ -15,6 +15,8 @@ public class CommandRegister { public static void register(@NotNull EventUtils mod, @NotNull CommandDispatcher dispatcher) { // Commands final LiteralCommandNode config = new ConfigCmd(mod).build(); + final LiteralCommandNode link = new LinkCmd(mod).build(); + final LiteralCommandNode post = new PostCmd(mod).build(); final LiteralCommandNode teleport = new TeleportCmd(mod).build(); final LiteralCommandNode priority = new PriorityCmd(mod).build(); final LiteralCommandNode priorityTop = new PriorityTopCmd(mod).build(); @@ -32,6 +34,8 @@ public static void register(@NotNull EventUtils mod, @NotNull CommandDispatcher< // Build command tree dispatcher.getRoot().addChild(main); main.addChild(config); + main.addChild(link); + main.addChild(post); main.addChild(teleport); main.addChild(priority); main.addChild(priorityTop); diff --git a/src/main/java/cc/aabss/eventutils/commands/LinkCmd.java b/src/main/java/cc/aabss/eventutils/commands/LinkCmd.java new file mode 100644 index 0000000..eb2f92a --- /dev/null +++ b/src/main/java/cc/aabss/eventutils/commands/LinkCmd.java @@ -0,0 +1,36 @@ +package cc.aabss.eventutils.commands; + +import cc.aabss.eventutils.EventUtils; +import cc.aabss.eventutils.versioning.VersionedClient; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.tree.LiteralCommandNode; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.client.Minecraft; +import org.jetbrains.annotations.NotNull; + + +public class LinkCmd extends EUCommand { + public LinkCmd(@NotNull EventUtils mod) { + super(mod); + } + + @Override @NotNull + public LiteralCommandNode build() { + return ClientCommandManager + .literal("link") + .executes(context -> { + execute(context); + return 0; + }) + .build(); + } + + private void execute(@NotNull CommandContext context) { + final Minecraft client = context.getSource().getClient(); + client.execute(() -> { + final VersionedClient vClient = new VersionedClient(client); + mod.discordLinkManager.startLink(vClient.screen(), () -> vClient.setScreen(null)); + }); + } +} diff --git a/src/main/java/cc/aabss/eventutils/commands/PostCmd.java b/src/main/java/cc/aabss/eventutils/commands/PostCmd.java new file mode 100644 index 0000000..c6fd004 --- /dev/null +++ b/src/main/java/cc/aabss/eventutils/commands/PostCmd.java @@ -0,0 +1,32 @@ +package cc.aabss.eventutils.commands; + +import cc.aabss.eventutils.EventUtils; +import cc.aabss.eventutils.screen.EventBuilder; +import cc.aabss.eventutils.versioning.VersionedClient; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.tree.LiteralCommandNode; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import org.jetbrains.annotations.NotNull; + + +public class PostCmd extends EUCommand { + public PostCmd(@NotNull EventUtils mod) { + super(mod); + } + + @Override @NotNull + public LiteralCommandNode build() { + return ClientCommandManager + .literal("post") + .executes(context -> { + execute(context); + return 0; + }) + .build(); + } + + private static void execute(@NotNull CommandContext context) { + new EventBuilder(new VersionedClient(context.getSource().getClient()).screen()).open(); + } +} diff --git a/src/main/java/cc/aabss/eventutils/discordrpc/DiscordRPC.java b/src/main/java/cc/aabss/eventutils/discordrpc/DiscordRPC.java index 96981de..bc70c15 100644 --- a/src/main/java/cc/aabss/eventutils/discordrpc/DiscordRPC.java +++ b/src/main/java/cc/aabss/eventutils/discordrpc/DiscordRPC.java @@ -43,8 +43,8 @@ public class DiscordRPC { public DiscordRPC(@NotNull EventUtils mod) { this.mod = mod; this.client.setListener(new CustomIPCListener(this)); - this.playerUrl = mod.authManager.player != null && mod.authManager.player.player.discord != null && mod.authManager.player.player.discord.id != null - ? utm("https://eventalerts.gg/players/" + mod.authManager.player.player.discord.id, "player") + this.playerUrl = mod.authManager.player != null && mod.authManager.player.discord != null && mod.authManager.player.discord.id != null + ? utm("https://eventalerts.gg/players/" + mod.authManager.player.discord.id, "player") : "https://namemc.com/profile/" + Minecraft.getInstance().getUser().getProfileId(); refreshConnection(); } @@ -77,7 +77,11 @@ public void refreshConnection() { } public void close() { - client.close(); + try { + client.close(); + } catch (final IllegalStateException ignored) { + // Not connected, ignore + } } public void refresh() { diff --git a/src/main/java/cc/aabss/eventutils/manager/DiscordLinkManager.java b/src/main/java/cc/aabss/eventutils/manager/DiscordLinkManager.java index 9c24f45..6f92f1e 100644 --- a/src/main/java/cc/aabss/eventutils/manager/DiscordLinkManager.java +++ b/src/main/java/cc/aabss/eventutils/manager/DiscordLinkManager.java @@ -87,6 +87,7 @@ public synchronized void startLink(@Nullable Screen browserParent, @Nullable Run fail("eventutils.config.discord_link.toast.start"); return; } + // It's okay for browserParent to be null Minecraft.getInstance().execute(() -> ConfirmLinkScreen.confirmLinkNow(browserParent, url)); }, t -> { @@ -185,7 +186,6 @@ private synchronized void timedOut() { if (server == null) return; EventUtils.LOGGER.warn("[DISCORD_LINK] Linking timed out"); cancel(); - fail("eventutils.config.discord_link.toast.timeout"); } private void fail(@NotNull String descriptionKey) { diff --git a/src/main/java/cc/aabss/eventutils/plustag/PlusTag.java b/src/main/java/cc/aabss/eventutils/plustag/PlusTag.java index 7273508..8a95db9 100644 --- a/src/main/java/cc/aabss/eventutils/plustag/PlusTag.java +++ b/src/main/java/cc/aabss/eventutils/plustag/PlusTag.java @@ -1,10 +1,8 @@ package cc.aabss.eventutils.plustag; -import cc.aabss.eventutils.EventUtils; import cc.aabss.eventutils.versioning.VersionedIdentifier; import gg.eventalerts.sdk.object.EAPlayer; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; //? if >=1.21.11 { /*import net.minecraft.resources.Identifier; *///?} else { @@ -59,18 +57,4 @@ public enum PlusTag { this.textureId = VersionedIdentifier.of("textures/bee/plus/" + name().toLowerCase() + ".png"); this.isUnlocked = isUnlocked; } - - @Nullable - public static PlusTag getBestUnlocked(@Nullable EAPlayer player) { - if (player == null) return null; - PlusTag bestTag = null; - for (final PlusTag tag : PlusTag.values()) { - if (tag.isUnlocked.test(player)) { - bestTag = tag; - break; - } - } - EventUtils.LOGGER.debug("[API] Fetched best tag={} uuid={}", bestTag, player.minecraft != null ? player.minecraft.uuid : "(player.minecraft=null)"); - return bestTag; - } } diff --git a/src/main/java/cc/aabss/eventutils/screen/EventBuilder.java b/src/main/java/cc/aabss/eventutils/screen/EventBuilder.java new file mode 100644 index 0000000..11c0439 --- /dev/null +++ b/src/main/java/cc/aabss/eventutils/screen/EventBuilder.java @@ -0,0 +1,602 @@ +package cc.aabss.eventutils.screen; + +import cc.aabss.eventutils.EventUtils; +import cc.aabss.eventutils.NotificationToast; +import cc.aabss.eventutils.sdk.EnrichedPlayer; +import cc.aabss.eventutils.versioning.VersionedClient; +import cc.aabss.eventutils.versioning.VersionedIdentifier; +import gg.eventalerts.sdk.object.EAEvent; +import gg.eventalerts.sdk.object.EAPartnerServer; +import gg.eventalerts.sdk.object.EAPlayer; +import io.wispforest.owo.ui.component.ButtonComponent; +import io.wispforest.owo.ui.component.DropdownComponent; +import io.wispforest.owo.ui.component.LabelComponent; +import io.wispforest.owo.ui.component.TextAreaComponent; +import io.wispforest.owo.ui.component.TextBoxComponent; +import io.wispforest.owo.ui.component.Components; +import io.wispforest.owo.ui.container.FlowLayout; +import io.wispforest.owo.ui.container.Containers; +import io.wispforest.owo.ui.core.*; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.ConfirmScreen; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.multiplayer.ServerData; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.protocol.status.ServerStatus; +import org.bson.types.ObjectId; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xyz.srnyx.javautilities.MapGenerator; +import xyz.srnyx.javautilities.manipulation.DurationParser; +import xyz.srnyx.javautilities.manipulation.Mapper; + +import java.time.Duration; +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.Function; + +import static net.minecraft.network.chat.Component.literal; +import static net.minecraft.network.chat.Component.translatable; + + +public class EventBuilder extends ScreenWithParent { + public static final int MAX_TITLE_LENGTH = 256; + public static final int MAX_DESCRIPTION_LENGTH = 2000; + public static final int MAX_TIME_LENGTH = 1007; + public static final int MAX_IP_LENGTH = 256; + public static final int MAX_VERSION_LENGTH = 256; + public static final int MAX_PRIZE_LENGTH = 256; + @NotNull private static final List TOGGLEABLE_ROLES = Arrays.stream(EAEvent.PingRole.values()) + .filter(role -> role.partnerToggleable) + .toList(); + @NotNull private static final ObjectId COMMUNITY_EVENT_SENTINEL = new ObjectId("000000000000000000000000"); + + @NotNull private Map partnerServers = new LinkedHashMap<>(); + + // Values + private boolean custom = Defaults.CUSTOM; + @Nullable private ObjectId partnerServer; + @NotNull private Set roles = Defaults.pingRoles(); + @NotNull private String title = Defaults.TITLE; + @NotNull private String time = Defaults.TIME; + @NotNull private String ip = Defaults.ip(); + @NotNull private Set platforms = Defaults.platforms(); + @NotNull private String version = Defaults.version(); + @NotNull private String description = Defaults.DESCRIPTION; + @NotNull private String prize = Defaults.PRIZE; + @Nullable private Integer maxPlayers = Defaults.maxPlayers(); + + public EventBuilder(@Nullable Screen parent) { + super(parent); + confirmClose( + translatable("eventutils.event_builder.confirm_close.title").withStyle(ChatFormatting.RED, ChatFormatting.BOLD), + translatable("eventutils.event_builder.confirm_close.description")); + } + + private EventBuilder(@NotNull EventBuilder builder) { + this(builder.parent); + + this.partnerServers = new LinkedHashMap<>(builder.partnerServers); + + // Values + this.custom = builder.custom; + this.partnerServer = builder.partnerServer; + this.roles = new HashSet<>(builder.roles); + this.title = builder.title; + this.time = builder.time; + this.ip = builder.ip; + this.platforms = new HashSet<>(builder.platforms); + this.version = builder.version; + this.description = builder.description; + this.prize = builder.prize; + this.maxPlayers = builder.maxPlayers; + + // Checks + if (custom && (EventUtils.MOD.authManager.player == null || EventUtils.MOD.authManager.player.subscription == null) && partnerServer == null) { + this.custom = false; + } + } + + public void open() { + final EAPlayer player = EventUtils.MOD.authManager.player; + if (player == null || player.discord == null || player.discord.id == null) return; + + // Fetch Partner Servers + EventUtils.MOD.http.partnerServers.retrieveAll(MapGenerator.HASH_MAP.mapOf( + "enabled", null, + "representatives", Set.of(player.discord.id) + )).queue( + partnerServers -> { + // partnerServers + this.partnerServers = new LinkedHashMap<>(); + for (final EAPartnerServer partnerServer : partnerServers) { + if (partnerServer.id != null) this.partnerServers.put(partnerServer.id, partnerServer); + } + + // No default preset, open immediately + if (player.defaultPreset == null) { + setScreen(); + return; + } + + // Default preset, retrieve -> apply -> open + EventUtils.MOD.http.eventPresets.retrieveOneById(player.defaultPreset).queue( + preset -> { + // Apply preset + if (preset.data != null) { + this.custom = false; + if (preset.data.title != null) this.title = preset.data.title; + if (preset.data.time != null) this.time = preset.data.time; + if (preset.data.ip != null) this.ip = preset.data.ip; + if (preset.data.platforms != null) this.platforms = new HashSet<>(preset.data.platforms); + if (preset.data.version != null) this.version = preset.data.version; + if (preset.data.description != null) this.description = preset.data.description; + if (preset.data.prize != null) this.prize = preset.data.prize; + if (preset.data.maxPlayers != null) this.maxPlayers = preset.data.maxPlayers; + } + + // Open + setScreen(); + }, + failure -> { + EventUtils.LOGGER.error("Failed to retrieve default preset", failure); + error("preset", Component.literal(failure.getMessage())); + setScreen(); // Still open, just without default preset + }); + }, + failure -> { + EventUtils.LOGGER.error("Failed to retrieve partner servers", failure); + error("partner_servers", Component.literal(failure.getMessage())); + }); + } + + private void setScreen() { + final Minecraft client = Minecraft.getInstance(); + client.execute(() -> new VersionedClient(client).setScreen(this)); + } + + private void rebuild() { + new VersionedClient(Minecraft.getInstance()).setScreen(new EventBuilder(this)); + } + + private void error(@NotNull String key, @NotNull Object... args) { + NotificationToast.show( + translatable("eventutils.event_builder.error." + key + ".title").withStyle(ChatFormatting.RED), + translatable("eventutils.event_builder.error." + key + ".description", args)); + setScreen(); + } + + private boolean canPost() { + // Check if host is linked + final EnrichedPlayer player = EventUtils.MOD.authManager.player; + if (player == null || player.discord == null || player.minecraft == null) { + error("not_logged_in"); + return false; + } + + final EAEvent.Type type = partnerServer != null ? EAEvent.Type.PARTNER : EAEvent.Type.COMMUNITY; + final Set missingRequiredFields = new HashSet<>(); + + if (custom) { + // Custom + + // Required fields + if (description.isBlank()) missingRequiredFields.add("description"); + } else { + // Builder + + // Required fields + if (title.isBlank()) missingRequiredFields.add("title"); + if (time.isBlank()) missingRequiredFields.add("time"); + if (ip.isBlank()) missingRequiredFields.add("ip"); + if (version.isBlank()) missingRequiredFields.add("version"); + + // Get time as Duration + final Duration duration = DurationParser.parse(time).orElse(null); + if (duration == null) { + error("invalid_time", time); + return false; + } + // Check if duration is more than 1 day + if (duration.toDays() > 1) { + error("time_too_long", time); + return false; + } + + // Custom COMMUNITY required premium + if (type == EAEvent.Type.COMMUNITY && player.getEffectiveSubscriptionTier() == null) { + error("premium_required", translatable("eventutils.event_builder.error.premium_required.custom_community")); + return false; + } + } + + // Missing required field + if (!missingRequiredFields.isEmpty()) { + final MutableComponent missingFields = Component.empty(); + for (final String field : missingRequiredFields) { + if (!missingFields.getSiblings().isEmpty()) missingFields.append(", "); + missingFields.append(translatable("eventutils.event_builder.values." + field + ".label")); + } + error("missing_required", missingFields); + return false; + } + + return true; + } + + private void post() { + if (!canPost()) return; + final EAEvent event = new EAEvent(); + + // Common + event.type = this.partnerServer != null ? EAEvent.Type.PARTNER : EAEvent.Type.COMMUNITY; + event.custom = this.custom; + event.server = this.partnerServer; + event.description = this.description; + + // Partner-only + if (event.type == EAEvent.Type.PARTNER) event.rolesNamed = this.roles; + + if (!custom) { + // Builder + + // Get time as Duration + final Duration duration = DurationParser.parse(time).orElse(null); + if (duration == null) { + error("invalid_time", time); + return; + } + // Check if duration is more than 1 day + if (duration.toDays() > 1) { + error("time_too_long", time); + return; + } + + event.title = this.title; + event.time = new Date(System.currentTimeMillis() + duration.toMillis()); + event.ip = this.ip; + event.platforms = this.platforms; + event.version = this.version; + event.prize = this.prize; + event.maxPlayers = this.maxPlayers; + } + + // Post + NotificationToast.show(translatable("eventutils.event_builder.actions.post.toast.title"), translatable("eventutils.event_builder.actions.post.toast.description")); + EventUtils.MOD.http.events.postOne(event).queue( + success -> { + // Screen + Minecraft.getInstance().execute(this::closeWithoutConfirmation); + // Toast + NotificationToast.show(translatable("eventutils.event_builder.success.title"), translatable("eventutils.event_builder.success.description")); + // Stat + EventUtils.MOD.stats.eventsPosted.incrementAndGet(); + }, + failure -> { + EventUtils.LOGGER.error("Failed to post event", failure); + error("post", Component.literal(failure.getMessage())); + }); + } + + @Override + protected void build(@NotNull FlowLayout root) { + final EnrichedPlayer player = EventUtils.MOD.authManager.player; + + root.gap(4); + root + .surface(Surface.VANILLA_TRANSLUCENT) + .padding(Insets.of(10)) + .horizontalAlignment(HorizontalAlignment.CENTER) + .verticalAlignment(VerticalAlignment.CENTER); + + final FlowLayout fields = Containers.verticalFlow(Sizing.content(), Sizing.content()); + fields.padding(Insets.right(5)); + fields.gap(4); + + // Partner Server + if (!partnerServers.isEmpty()) { + // Build Partner Server option IDs + final List partnerServerIds = new ArrayList<>(partnerServers.keySet()); + partnerServerIds.add(0, COMMUNITY_EVENT_SENTINEL); // Add community event option (don't use addFirst to support old Java versions) + + fields.child(dropdownRow( + "partner_server", true, partnerServerIds, + Objects.requireNonNullElse(partnerServer, COMMUNITY_EVENT_SENTINEL), id -> { + final ObjectId oldPartnerServer = this.partnerServer; + partnerServer = id == COMMUNITY_EVENT_SENTINEL ? null : id; + + // Rebuild if switching between community and partner (for ping roles) + if ((oldPartnerServer == null && partnerServer != null) || (oldPartnerServer != null && partnerServer == null)) { + rebuild(); + } + }, + id -> { + if (id == null || id == COMMUNITY_EVENT_SENTINEL) { + return translatable("eventutils.event_builder.values.partner_server.community").withStyle(ChatFormatting.GRAY); + } + final EAPartnerServer server = partnerServers.get(id); + return literal(server != null && server.name != null ? server.name : id.toHexString()); + })); + } + // Roles (Partner only) TODO role conditions + if (partnerServer != null) fields.child(checkBoxRow( + "roles", !custom, + TOGGLEABLE_ROLES, roles, + pingRole -> literal(pingRole.displayName), + pingRole -> "textures/event_builder/role/" + pingRole.name().toLowerCase() + ".png")); + + if (!custom) { + fields.child(stringRow("title", true, MAX_TITLE_LENGTH, title, value -> title = value)); + fields.child(stringRow("time", true, MAX_TIME_LENGTH, time, value -> time = value)); + fields.child(stringRow("ip", true, MAX_IP_LENGTH, ip, value -> ip = value)); + fields.child(stringRow("version", true, MAX_VERSION_LENGTH, version, value -> version = value)); + } + fields.child(textAreaRow("description", custom, MAX_DESCRIPTION_LENGTH, custom, description, value -> description = value)); + if (!custom) { + fields.child(stringRow("prize", false, MAX_PRIZE_LENGTH, prize, value -> prize = value)); + fields.child(checkBoxRow( + "platforms", false, + List.of(EAEvent.Platform.values()), platforms, + platform -> literal(platform.displayName), + platform -> "textures/event_builder/platform/" + platform.name().toLowerCase() + ".png")); + //TODO does this allow negatives? + fields.child(integerRow("max_players", false, maxPlayers, value -> maxPlayers = value)); + } + root.child(Containers.verticalScroll(Sizing.content(), Sizing.expand(), fields)); + + final FlowLayout actions = Containers.horizontalFlow(Sizing.content(), Sizing.content()); + actions.gap(6); + actions.margins(Insets.top(12)); + final String customKey = custom ? "custom" : "builder"; + if ((player != null && player.getEffectiveSubscriptionTier() != null) || partnerServer != null) { + actions.child(Components.button(translatable("eventutils.event_builder.actions.custom." + customKey).withStyle(ChatFormatting.AQUA), button -> { + custom = !custom; + rebuild(); + }).horizontalSizing(Sizing.fixed(160))); + } + actions.child(Components.button(translatable("eventutils.event_builder.actions.post.label").withStyle(ChatFormatting.GREEN, ChatFormatting.BOLD), button -> { + if (!canPost()) return; + final VersionedClient vClient = new VersionedClient(Minecraft.getInstance()); + vClient.setScreen(new ConfirmScreen(result -> { + if (result) { + post(); + } else { + vClient.setScreen(this); + } + }, translatable("eventutils.event_builder.actions.post.confirm.title").withStyle(ChatFormatting.GREEN, ChatFormatting.BOLD), translatable("eventutils.event_builder.actions.post.confirm.description"))); + }) + .horizontalSizing(Sizing.fixed(160))); + actions.child(Components.button(translatable("gui.cancel").withStyle(ChatFormatting.RED), button -> onClose()) + .horizontalSizing(Sizing.fixed(90))); + root.child(actions); + } + + @NotNull + private FlowLayout stringRow(@NotNull String key, boolean required, int maxLength, @Nullable String value, @NotNull Consumer setter) { + final String initial = value == null ? "" : value; + final TextBoxComponent box = Components.textBox(Sizing.expand(), initial); + final FlowLayout wrapper = inputWrapper(box, required && initial.isBlank()); + box.onChanged().subscribe(newValue -> { + String result = newValue; + if (newValue.length() > maxLength) { + result = newValue.substring(0, maxLength); + box.text(result); + } + setter.accept(result); + markInvalid(wrapper, required && result.isBlank()); + }); + return row(key, required, wrapper); + } + + @NotNull + private FlowLayout integerRow(@NotNull String key, boolean required, @Nullable Number value, @NotNull Consumer setter) { + final String initial = value == null ? "" : value.toString(); + final TextBoxComponent box = Components.textBox(Sizing.expand(), initial); + final FlowLayout wrapper = inputWrapper(box, required && initial.isBlank()); + box.setFilter(text -> text.isEmpty() || (text.length() <= 9 && text.chars().allMatch(Character::isDigit))); + box.onChanged().subscribe(newValue -> { + setter.accept(Mapper.toInt(newValue).orElse(null)); + markInvalid(wrapper, required && newValue.isBlank()); + }); + return row(key, required, box); + } + + @NotNull + private FlowLayout textAreaRow(@NotNull String key, boolean required, int maxLength, boolean big, @Nullable String value, @NotNull Consumer setter) { + final String initial = value == null ? "" : value; + final TextAreaComponent area = Components.textArea(Sizing.expand(), Sizing.fixed(big ? 200 : 60), initial); + final FlowLayout wrapper = inputWrapper(area, required && initial.isBlank()); + area.margins(Insets.of(1)); + area.onChanged().subscribe(newValue -> { + String result = newValue; + if (newValue.length() > maxLength) { + result = newValue.substring(0, maxLength); + area.text(result); + } + setter.accept(result); + markInvalid(wrapper, required && result.isBlank()); + }); + return row(key, required, wrapper); + } + + @NotNull + private FlowLayout dropdownRow( + @NotNull String key, boolean required, @NotNull Collection values, + @Nullable T value, @NotNull Consumer setter, + @NotNull Function<@Nullable T, @NotNull Component> displayNameMapper + ) { + final ButtonComponent button = Components.button(displayNameMapper.apply(value), b -> {}); + button.horizontalSizing(Sizing.expand()); + button.onPress(b -> { + final Insets rootPadding = this.uiAdapter.rootComponent.padding().get(); + DropdownComponent.openContextMenu( + this, this.uiAdapter.rootComponent, FlowLayout::child, + button.getX() - rootPadding.left(), button.getY() + button.getHeight() - rootPadding.top(), + dropdown -> { + for (final T v : values) dropdown.button(displayNameMapper.apply(v), d -> { + setter.accept(v); + button.setMessage(displayNameMapper.apply(v)); + dropdown.remove(); + }); + }); + }); + return row(key, required, button); + } + + @NotNull + private FlowLayout checkBoxRow( + @NotNull String key, boolean required, + @NotNull Collection availableValues, @NotNull Collection selectedValues, + @NotNull Function displayNameMapper, + @Nullable Function iconPathMapper + ) { + final FlowLayout checkboxes = Containers.horizontalFlow(Sizing.expand(), Sizing.content()); + checkboxes.gap(8); + checkboxes.verticalAlignment(VerticalAlignment.CENTER); + for (final T enumValue : availableValues) { + final String iconPath = iconPathMapper == null ? null : iconPathMapper.apply(enumValue); + checkboxes.child(iconPath == null + ? Components.checkbox(displayNameMapper.apply(enumValue)) + .checked(selectedValues.contains(enumValue)) + .onChanged(checked -> setChecked(checked, enumValue, selectedValues::add, selectedValues::remove)) + : iconToggle(iconPath, displayNameMapper.apply(enumValue), selectedValues.contains(enumValue), + checked -> setChecked(checked, enumValue, selectedValues::add, selectedValues::remove))); + } + return row(key, required, checkboxes); + } + + private static void setChecked(boolean checked, @NotNull T value, @NotNull Consumer adder, @NotNull Consumer remover) { + if (checked) { + adder.accept(value); + } else { + remover.accept(value); + } + } + + private static final int ICON_TOGGLE_SIZE = 16; + + @NotNull + private FlowLayout iconToggle(@NotNull String texturePath, @NotNull Component label, boolean initiallyChecked, @NotNull Consumer onChanged) { + final FlowLayout box = Containers.horizontalFlow(Sizing.content(), Sizing.content()); + box.gap(4); + box.verticalAlignment(VerticalAlignment.CENTER); + box.padding(Insets.of(3)); + box.child(Components.texture(VersionedIdentifier.of(texturePath), 0, 0, ICON_TOGGLE_SIZE, ICON_TOGGLE_SIZE, ICON_TOGGLE_SIZE, ICON_TOGGLE_SIZE)); + box.child(Components.label(label)); + + final AtomicBoolean checked = new AtomicBoolean(initiallyChecked); + final Runnable updateSurface = () -> box.surface(checked.get() ? Surface.outline(0xFF55FF55) : Surface.BLANK); + updateSurface.run(); + //? if >=1.21.11 { + /*box.mouseDown().subscribe((event, doubleClick) -> { + *///?} else { + box.mouseDown().subscribe((mouseX, mouseY, button) -> { + //?} + checked.set(!checked.get()); + onChanged.accept(checked.get()); + updateSurface.run(); + return true; + }); + + cursorAll(box, CursorStyle.HAND); + return box; + } + + @NotNull + private FlowLayout row(@NotNull String key, boolean required, @NotNull io.wispforest.owo.ui.core.Component input) { + final FlowLayout row = Containers.horizontalFlow(Sizing.fixed(500), Sizing.content()); + row.gap(8); + row.verticalAlignment(VerticalAlignment.CENTER); + + final MutableComponent labelText = Component.empty(); + if (required) { + labelText.append("*"); + labelText.withStyle(ChatFormatting.BOLD); + } + labelText.append(translatable("eventutils.event_builder.values." + key + ".label")); + + final LabelComponent label = Components.label(labelText); + label.horizontalTextAlignment(HorizontalAlignment.RIGHT); + label.horizontalSizing(Sizing.fixed(100)); + + row.child(label); + row.child(input); + row.tooltip(translatable("eventutils.event_builder.values." + key + ".description")); + return row; + } + + @NotNull + private static FlowLayout inputWrapper(@NotNull io.wispforest.owo.ui.core.Component input, boolean invalid) { + final FlowLayout wrapper = Containers.horizontalFlow(Sizing.expand(), Sizing.content()); + wrapper.padding(Insets.of(1)); + wrapper.child(input); + markInvalid(wrapper, invalid); + return wrapper; + } + + private static void markInvalid(@NotNull FlowLayout wrapper, boolean invalid) { + wrapper.surface(invalid ? Surface.outline(0xFFFF5555) : Surface.BLANK); + } + + private static void cursorAll(@NotNull io.wispforest.owo.ui.core.Component component, @NotNull CursorStyle style) { + component.cursorStyle(style); + if (component instanceof ParentComponent parent) { + for (final io.wispforest.owo.ui.core.Component child : parent.children()) cursorAll(child, style); + } + } + + @Override @NotNull + protected OwoUIAdapter createAdapter() { + return OwoUIAdapter.create(this, Containers::verticalFlow); + } + + public static class Defaults { + public static final boolean CUSTOM = false; + @NotNull private static final Set PING_ROLES = Set.of(EAEvent.PingRole.PARTNER); + @NotNull public static final String TITLE = Minecraft.getInstance().getUser().getName() + "'s Event"; + @NotNull public static final String TIME = "15m"; + @NotNull private static final String IP = "play.eventalerts.gg"; + @NotNull public static final String DESCRIPTION = ""; + @NotNull private static final Set PLATFORMS = Set.of(EAEvent.Platform.JAVA); + @NotNull public static final String PRIZE = ""; + @Nullable public static final Integer MAX_PLAYERS = null; + + @NotNull + public static Set pingRoles() { + return new HashSet<>(PING_ROLES); + } + + @NotNull + public static String ip() { + final ServerData server = Minecraft.getInstance().getCurrentServer(); + return server != null ? server.ip : IP; + } + + @NotNull + public static Set platforms() { + return new HashSet<>(PLATFORMS); + } + + @NotNull + public static String version() { + return Objects.requireNonNullElse(EventUtils.MC_VERSION, "26.2"); + } + + @NotNull + public static String description() { + final ServerData server = Minecraft.getInstance().getCurrentServer(); + return server != null ? server.motd.getString() : DESCRIPTION; + } + + @Nullable + public static Integer maxPlayers() { + final ServerData server = Minecraft.getInstance().getCurrentServer(); + if (server == null) return Defaults.MAX_PLAYERS; + final ServerStatus.Players players = server.players; + return players != null ? Integer.valueOf(players.max()) : Defaults.MAX_PLAYERS; + } + } +} diff --git a/src/main/java/cc/aabss/eventutils/screen/MenuScreen.java b/src/main/java/cc/aabss/eventutils/screen/MenuScreen.java new file mode 100644 index 0000000..c2d539c --- /dev/null +++ b/src/main/java/cc/aabss/eventutils/screen/MenuScreen.java @@ -0,0 +1,83 @@ +package cc.aabss.eventutils.screen; + +import cc.aabss.eventutils.EventUtils; +import cc.aabss.eventutils.screen.config.ConfigScreen; +import cc.aabss.eventutils.screen.config.group.GroupManagerScreen; +import cc.aabss.eventutils.sdk.EnrichedPlayer; +import cc.aabss.eventutils.versioning.VersionedClient; +import io.wispforest.owo.ui.component.Components; +import io.wispforest.owo.ui.container.FlowLayout; +import io.wispforest.owo.ui.container.Containers; +import io.wispforest.owo.ui.core.*; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static net.minecraft.network.chat.Component.translatable; + + +public class MenuScreen extends ScreenWithParent { + public MenuScreen(@Nullable Screen parent) { + super(parent); + } + + @Override @NotNull + protected OwoUIAdapter createAdapter() { + return OwoUIAdapter.create(this, Containers::verticalFlow); + } + + @Override + protected void build(@NotNull FlowLayout rootComponent) { + final EnrichedPlayer player = EventUtils.MOD.authManager.player; + + rootComponent + .surface(Surface.VANILLA_TRANSLUCENT) + .horizontalAlignment(HorizontalAlignment.CENTER) + .verticalAlignment(VerticalAlignment.CENTER); + + final FlowLayout buttons = Containers.verticalFlow(Sizing.expand(), Sizing.content()); + buttons.horizontalSizing(Sizing.fixed(150)); + buttons.gap(10); + + // Config + buttons.child(Components + .button(translatable("eventutils.menu.config").withStyle(ChatFormatting.GOLD), button -> { + new VersionedClient(Minecraft.getInstance()).setScreen(ConfigScreen.getConfigScreen(this)); + }) + .horizontalSizing(Sizing.expand())); + + // Group Manager + buttons.child(Components + .button( + translatable("eventutils.menu.group_manager").withStyle(ChatFormatting.YELLOW), + button -> new VersionedClient(Minecraft.getInstance()).setScreen(new GroupManagerScreen(EventUtils.MOD, this))) + .horizontalSizing(Sizing.expand())); + + if (player != null) { + // Discord Link + if (!player.isDiscordLinked()) buttons.child(Components + .button( + translatable("eventutils.menu.discord_link").withStyle(ChatFormatting.AQUA), + button -> EventUtils.MOD.discordLinkManager.startLink(this, null)) + .horizontalSizing(Sizing.expand())); + + // Event Builder + if (player.isDiscordLinked()) buttons.child(Components + .button( + translatable("eventutils.menu.event_builder").withStyle(ChatFormatting.GREEN), + button -> new EventBuilder(this).open()) + .horizontalSizing(Sizing.expand())); + } + + // Close + buttons.child(Components + .button( + translatable("eventutils.menu.close"), + button -> onClose()) + .horizontalSizing(Sizing.expand())); + + rootComponent.child(buttons); + } +} diff --git a/src/main/java/cc/aabss/eventutils/screen/ScreenWithParent.java b/src/main/java/cc/aabss/eventutils/screen/ScreenWithParent.java new file mode 100644 index 0000000..acf26ff --- /dev/null +++ b/src/main/java/cc/aabss/eventutils/screen/ScreenWithParent.java @@ -0,0 +1,52 @@ +package cc.aabss.eventutils.screen; + +import cc.aabss.eventutils.versioning.VersionedClient; +import io.wispforest.owo.ui.base.BaseOwoScreen; +import io.wispforest.owo.ui.core.ParentComponent; +import net.minecraft.client.gui.screens.ConfirmScreen; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + + +public abstract class ScreenWithParent extends BaseOwoScreen { + @Nullable protected final Screen parent; + + @Nullable private Component confirmCloseTitle; + @Nullable private Component confirmCloseDescription; + + public ScreenWithParent(@Nullable Screen parent) { + this.parent = parent; + } + + @NotNull + public ScreenWithParent confirmClose(@NotNull Component title, @NotNull Component description) { + this.confirmCloseTitle = title; + this.confirmCloseDescription = description; + return this; + } + + public void closeWithoutConfirmation() { + this.confirmCloseTitle = null; + this.confirmCloseDescription = null; + onClose(); + } + + @Override + public void onClose() { + if (minecraft == null) return; + final VersionedClient vClient = new VersionedClient(minecraft); + + // Confirm close + if (confirmCloseTitle != null && confirmCloseDescription != null) { + vClient.setScreen(new ConfirmScreen( + result -> vClient.setScreen(result ? parent : this), + confirmCloseTitle, confirmCloseDescription)); + return; + } + + // Immediate close + vClient.setScreen(parent); + } +} diff --git a/src/main/java/cc/aabss/eventutils/screen/config/ConfigScreen.java b/src/main/java/cc/aabss/eventutils/screen/config/ConfigScreen.java index 80a38e2..757002e 100644 --- a/src/main/java/cc/aabss/eventutils/screen/config/ConfigScreen.java +++ b/src/main/java/cc/aabss/eventutils/screen/config/ConfigScreen.java @@ -25,124 +25,111 @@ public class ConfigScreen { public static Screen getConfigScreen(@Nullable Screen parent) { final EUConfig config = EventUtils.MOD.config; final YetAnotherConfigLib.Builder builder = YetAnotherConfigLib.createBuilder() - .title(translatable("eventutils.config.title")); - - // General - final ConfigCategory.Builder generalCategory = ConfigCategory.createBuilder().name(translatable("eventutils.config.general")) - .option(Option.createBuilder() - .name(translatable("eventutils.config.discord.label")) - .description(OptionDescription.createBuilder() - .text(translatable("eventutils.config.discord.description")) - .image(VersionedIdentifier.of("textures/config/discord_rpc.png"), 351, 165) + .title(translatable("eventutils.config.title")) + // General + .category(ConfigCategory.createBuilder().name(translatable("eventutils.config.general")) + .option(Option.createBuilder() + .name(translatable("eventutils.config.discord.label")) + .description(OptionDescription.createBuilder() + .text(translatable("eventutils.config.discord.description")) + .image(VersionedIdentifier.of("textures/config/discord_rpc.png"), 351, 165) + .build()) + .binding(EUConfig.Defaults.DISCORD_RPC, () -> config.discord_rpc, newValue -> { + config.discord_rpc = newValue; + config.save(); + EventUtils.MOD.discordRPC.refreshConnection(); + }) + .controller(ConfigScreen::getBooleanBuilder) .build()) - .binding(EUConfig.Defaults.DISCORD_RPC, () -> config.discord_rpc, newValue -> { - config.discord_rpc = newValue; - config.save(); - EventUtils.MOD.discordRPC.refreshConnection(); - }) - .controller(ConfigScreen::getBooleanBuilder) - .build()) - .option(Option.createBuilder() - .name(translatable("eventutils.config.queue.label")) - .description(OptionDescription.of(translatable("eventutils.config.queue.description"))) - .binding(EUConfig.Defaults.SIMPLE_QUEUE_MESSAGE, () -> config.simple_queue_message, newValue -> { - config.simple_queue_message = newValue; - config.save(); - }) - .controller(ConfigScreen::getBooleanBuilder) - .build()) - .option(Option.createBuilder() - .name(translatable("eventutils.config.update.label")) - .description(OptionDescription.of(translatable("eventutils.config.update.description"))) - .binding(EUConfig.Defaults.UPDATE_CHECKER, () -> config.update_checker, newValue -> { - config.update_checker = newValue; - config.save(); - EventUtils.MOD.updateChecker.notifyUpdate(); - }) - .controller(ConfigScreen::getBooleanBuilder) - .build()) - .option(Option.createBuilder() - .name(translatable("eventutils.config.window.label")) - .description(OptionDescription.createBuilder() - .text(translatable("eventutils.config.window.description")) - .image(VersionedIdentifier.of("textures/config/confirm_exit.png"), 950, 272) + .option(Option.createBuilder() + .name(translatable("eventutils.config.queue.label")) + .description(OptionDescription.of(translatable("eventutils.config.queue.description"))) + .binding(EUConfig.Defaults.SIMPLE_QUEUE_MESSAGE, () -> config.simple_queue_message, newValue -> { + config.simple_queue_message = newValue; + config.save(); + }) + .controller(ConfigScreen::getBooleanBuilder) .build()) - .binding(EUConfig.Defaults.CONFIRM_WINDOW_CLOSE, () -> config.confirm_window_close, newValue -> { - config.confirm_window_close = newValue; - config.save(); - }) - .controller(ConfigScreen::getBooleanBuilder) - .build()) - .option(Option.createBuilder() - .name(translatable("eventutils.config.disconnect.label")) - .description(OptionDescription.createBuilder() - .text(translatable("eventutils.config.disconnect.description")) - .image(VersionedIdentifier.of("textures/config/confirm_disconnect.png"), 972, 295) + .option(Option.createBuilder() + .name(translatable("eventutils.config.update.label")) + .description(OptionDescription.of(translatable("eventutils.config.update.description"))) + .binding(EUConfig.Defaults.UPDATE_CHECKER, () -> config.update_checker, newValue -> { + config.update_checker = newValue; + config.save(); + EventUtils.MOD.updateChecker.notifyUpdate(); + }) + .controller(ConfigScreen::getBooleanBuilder) .build()) - .binding(EUConfig.Defaults.CONFIRM_DISCONNECT, () -> config.confirm_disconnect, newValue -> { - config.confirm_disconnect = newValue; - config.save(); - }) - .controller(ConfigScreen::getBooleanBuilder) - .build()) - .option(Option.createBuilder() - .name(translatable("eventutils.config.bee_icons.label")) - .description(OptionDescription.createBuilder() - .text(translatable("eventutils.config.bee_icons.description")) - .image(VersionedIdentifier.of("textures/config/bee_icons.png"), 404, 88) + .option(Option.createBuilder() + .name(translatable("eventutils.config.window.label")) + .description(OptionDescription.createBuilder() + .text(translatable("eventutils.config.window.description")) + .image(VersionedIdentifier.of("textures/config/confirm_exit.png"), 950, 272) + .build()) + .binding(EUConfig.Defaults.CONFIRM_WINDOW_CLOSE, () -> config.confirm_window_close, newValue -> { + config.confirm_window_close = newValue; + config.save(); + }) + .controller(ConfigScreen::getBooleanBuilder) + .build()) + .option(Option.createBuilder() + .name(translatable("eventutils.config.disconnect.label")) + .description(OptionDescription.createBuilder() + .text(translatable("eventutils.config.disconnect.description")) + .image(VersionedIdentifier.of("textures/config/confirm_disconnect.png"), 972, 295) + .build()) + .binding(EUConfig.Defaults.CONFIRM_DISCONNECT, () -> config.confirm_disconnect, newValue -> { + config.confirm_disconnect = newValue; + config.save(); + }) + .controller(ConfigScreen::getBooleanBuilder) + .build()) + .option(Option.createBuilder() + .name(translatable("eventutils.config.bee_icons.label")) + .description(OptionDescription.createBuilder() + .text(translatable("eventutils.config.bee_icons.description")) + .image(VersionedIdentifier.of("textures/config/bee_icons.png"), 404, 88) + .build()) + .binding(EUConfig.Defaults.BEE_ICONS, () -> config.bee_icons, newValue -> { + config.bee_icons = newValue; + config.save(); + }) + .controller(ConfigScreen::getBooleanBuilder) + .build()) + .option(ButtonOption.createBuilder() + .name(translatable("eventutils.config.groups.manage.label")) + .description(OptionDescription.of(translatable("eventutils.config.groups.manage.description"))) + .text(translatable("eventutils.config.groups.manage.button")) + .action((yaclScreen, option) -> new VersionedClient(Minecraft.getInstance()).setScreen(new GroupManagerScreen(EventUtils.MOD, yaclScreen))) + .build()) + .group(OptionGroup.createBuilder() + .name(translatable("eventutils.config.advanced.category")) + .collapsed(true) + .option(Option.createBuilder() + .name(translatable("eventutils.config.advanced.developer_mode.label")) + .description(OptionDescription.of(translatable("eventutils.config.advanced.developer_mode.description"))) + .binding(EUConfig.Defaults.DEVELOPER_MODE, () -> config.developer_mode, newValue -> { + config.developer_mode = newValue; + config.save(); + EventUtils.MOD.updateLogLevel(); + EventUtils.MOD.setupSdk("Developer Mode enabled/disabled"); + EventUtils.MOD.cacheManager.clearAll(); + EventUtils.MOD.authManager.authenticate().queue(); + }) + .controller(ConfigScreen::getBooleanBuilder) + .build()) + .option(Option.createBuilder() + .name(translatable("eventutils.config.advanced.log_level.label")) + .description(OptionDescription.of(translatable("eventutils.config.advanced.log_level.description"))) + .binding(EUConfig.Defaults.LOG_LEVEL, () -> config.log_level, newValue -> { + config.log_level = newValue; + config.save(); + EventUtils.MOD.updateLogLevel(); + }) + .controller(EnumDropdownControllerBuilder::create) + .build()) .build()) - .binding(EUConfig.Defaults.BEE_ICONS, () -> config.bee_icons, newValue -> { - config.bee_icons = newValue; - config.save(); - }) - .controller(ConfigScreen::getBooleanBuilder) - .build()) - .option(ButtonOption.createBuilder() - .name(translatable("eventutils.config.groups.manage.label")) - .description(OptionDescription.of(translatable("eventutils.config.groups.manage.description"))) - .text(translatable("eventutils.config.groups.manage.button")) - .action((yaclScreen, option) -> new VersionedClient(Minecraft.getInstance()).setScreen(new GroupManagerScreen(EventUtils.MOD, yaclScreen))) .build()); - // Discord link - if (EventUtils.MOD.authManager.player != null) generalCategory.option(ButtonOption.createBuilder() - .name(translatable("eventutils.config.discord_link.label")) - .description(OptionDescription.of(translatable("eventutils.config.discord_link.description"))) - .text(EventUtils.MOD.authManager.player.player.discord != null - ? translatable("eventutils.config.discord_link.button.linked", EventUtils.MOD.authManager.player.player.discord.username) - : translatable("eventutils.config.discord_link.button.unlinked")) - .action((yaclScreen, option) -> EventUtils.MOD.discordLinkManager.startLink( - yaclScreen, - () -> new VersionedClient(Minecraft.getInstance()).setScreen(getConfigScreen(parent)))) - .build()); - // Advanced - generalCategory.group(OptionGroup.createBuilder() - .name(translatable("eventutils.config.advanced.category")) - .collapsed(true) - .option(Option.createBuilder() - .name(translatable("eventutils.config.advanced.developer_mode.label")) - .description(OptionDescription.of(translatable("eventutils.config.advanced.developer_mode.description"))) - .binding(EUConfig.Defaults.DEVELOPER_MODE, () -> config.developer_mode, newValue -> { - config.developer_mode = newValue; - config.save(); - EventUtils.MOD.updateLogLevel(); - EventUtils.MOD.setupSdk("Developer Mode enabled/disabled"); - EventUtils.MOD.cacheManager.clearAll(); - EventUtils.MOD.authManager.authenticate().queue(); - }) - .controller(ConfigScreen::getBooleanBuilder) - .build()) - .option(Option.createBuilder() - .name(translatable("eventutils.config.advanced.log_level.label")) - .description(OptionDescription.of(translatable("eventutils.config.advanced.log_level.description"))) - .binding(EUConfig.Defaults.LOG_LEVEL, () -> config.log_level, newValue -> { - config.log_level = newValue; - config.save(); - EventUtils.MOD.updateLogLevel(); - }) - .controller(EnumDropdownControllerBuilder::create) - .build()) - .build()); - builder.category(generalCategory.build()); // Events final ConfigCategory.Builder alertsCategory = ConfigCategory.createBuilder() diff --git a/src/main/java/cc/aabss/eventutils/sdk/EnrichedPlayer.java b/src/main/java/cc/aabss/eventutils/sdk/EnrichedPlayer.java index 223afe4..2ae5933 100644 --- a/src/main/java/cc/aabss/eventutils/sdk/EnrichedPlayer.java +++ b/src/main/java/cc/aabss/eventutils/sdk/EnrichedPlayer.java @@ -1,37 +1,57 @@ package cc.aabss.eventutils.sdk; +import cc.aabss.eventutils.EventUtils; import cc.aabss.eventutils.plustag.PlusTag; import gg.eventalerts.sdk.object.EAPlayer; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import xyz.srnyx.javautilities.parents.Stringable; -public class EnrichedPlayer extends Stringable { - @NotNull public final EAPlayer player; - +public class EnrichedPlayer extends EAPlayer { // --- Enrichment --- private boolean plusTagResolved = false; @Nullable private PlusTag plusTag; public EnrichedPlayer(@NotNull EAPlayer player) { - this.player = player; + super(player); } public boolean isOnline() { - return player.minecraft != null && player.minecraft.eventUtils != null; + return minecraft != null && minecraft.eventUtils != null; } public boolean isDiscordLinked() { - return player.discord != null; + return discord != null; + } + + @Nullable + public EAPlayer.Subscription.Tier getEffectiveSubscriptionTier() { + // Admins get highest subscription + if (discord != null && discord.roles != null && discord.roles.contains(EAPlayer.Discord.Role.ADMIN)) { + return EAPlayer.Subscription.Tier.HORNET; + } + + // Has subscription + if (subscription != null) return subscription.tier; + + // No subscription + return null; } @Nullable public PlusTag getPlusTag() { - if (!plusTagResolved) { - plusTagResolved = true; - plusTag = PlusTag.getBestUnlocked(player); + if (plusTagResolved) return plusTag; + + plusTagResolved = true; + PlusTag bestTag = null; + for (final PlusTag tag : PlusTag.values()) { + if (tag.isUnlocked.test(this)) { + bestTag = tag; + break; + } } - return plusTag; + EventUtils.LOGGER.debug("[API] Fetched best tag={} uuid={}", bestTag, minecraft != null ? minecraft.uuid : "(minecraft=null)"); + plusTag = bestTag; + return bestTag; } } diff --git a/src/main/java/cc/aabss/eventutils/sdk/EventWrapper.java b/src/main/java/cc/aabss/eventutils/sdk/EventWrapper.java index ef80ca8..723d77b 100644 --- a/src/main/java/cc/aabss/eventutils/sdk/EventWrapper.java +++ b/src/main/java/cc/aabss/eventutils/sdk/EventWrapper.java @@ -22,6 +22,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import static net.minecraft.network.chat.Component.translatable; + public class EventWrapper { @NotNull private final EventUtils mod; @@ -181,11 +183,11 @@ public void executeTypeSettings() { // toast if (!toastSent && settings.toasts) { // Get title - final String prizeTranslation = prize != null ? "eventutils.event.toast.prize.text" : "eventutils.event.toast.prize.none"; - final Component title = Component.translatable("eventutils.event.toast.title", eventType.nameTranslation, Component.translatable(prizeTranslation, prize)).withStyle(eventType.color); + final Component prizeTranslatable = prize != null ? translatable("eventutils.event.toast.prize.text", prize) : translatable("eventutils.event.toast.prize.none"); + final Component title = translatable("eventutils.event.toast.title", eventType.nameTranslation, prizeTranslatable).withStyle(eventType.color); // Get description - final Component description = ip != null ? Component.translatable("eventutils.event.teleport.text", Component.translatable("eventutils.event.teleport.command", eventType.name().toLowerCase()).withStyle(eventType.color)) : null; + final Component description = ip != null ? translatable("eventutils.event.teleport.text", translatable("eventutils.event.teleport.command", eventType.name().toLowerCase()).withStyle(eventType.color)) : null; // Update metric mod.stats.eventToastsReceived.incrementAndGet(); diff --git a/src/main/java/cc/aabss/eventutils/stats/Stats.java b/src/main/java/cc/aabss/eventutils/stats/Stats.java index 7f3c0f4..30defa0 100644 --- a/src/main/java/cc/aabss/eventutils/stats/Stats.java +++ b/src/main/java/cc/aabss/eventutils/stats/Stats.java @@ -24,6 +24,7 @@ public class Stats { @NotNull private final AtomicLong socketMessagesSentPrevious = new AtomicLong(); @NotNull public final AtomicInteger eventToastsReceived = new AtomicInteger(); @NotNull public final AtomicInteger eventsJoined = new AtomicInteger(); + @NotNull public final AtomicInteger eventsPosted = new AtomicInteger(); public Stats(@NotNull EventUtils mod) { new FabricContext.Factory(BuildProperties.MOD_ID, "5de470ff674a5d47edfd04ad3f99e230") @@ -35,6 +36,7 @@ public Stats(@NotNull EventUtils mod) { socketMessagesSentPrevious.set(mod.webSocket.messagesSent); eventToastsReceived.set(0); eventsJoined.set(0); + eventsPosted.set(0); }); // Config @@ -51,6 +53,7 @@ public Stats(@NotNull EventUtils mod) { // Misc factory.addMetric(Metric.number("event_toasts_received", eventToastsReceived::get)); factory.addMetric(Metric.number("events_joined", eventsJoined::get)); + factory.addMetric(Metric.number("events_posted", eventsPosted::get)); return factory.create(); }) diff --git a/src/main/resources/assets/eventutils/lang/en_us.json b/src/main/resources/assets/eventutils/lang/en_us.json index f6dbfa0..a3effdf 100644 --- a/src/main/resources/assets/eventutils/lang/en_us.json +++ b/src/main/resources/assets/eventutils/lang/en_us.json @@ -145,6 +145,134 @@ } }, + "menu": { + "config": "Config", + "group_manager": "Group manager", + "discord_link": "Link Discord account", + "event_builder": "Post an event", + "close": "Close" + }, + + "event_builder": { + "confirm_close": { + "title": "Confirm Close", + "description": "Are you sure you want to close the Event Builder? All values will be reset." + }, + + "success": { + "title": "Event posted!", + "description": "The event has been created and posted to Event Alerts" + }, + + "error": { + "not_logged_in": { + "title": "Not logged in", + "description": "You must be logged in to post an event" + }, + "partner_servers": { + "title": "Failed to get Partner Servers", + "description": "%s" + }, + "preset": { + "title": "Failed to get preset", + "description": "%s" + }, + "missing_required": { + "title": "Missing required value(s)", + "description": "%s" + }, + "invalid_time": { + "title": "Invalid time", + "description": "The specified time is invalid: %s" + }, + "time_too_long": { + "title": "Time too long", + "description": "The specified time is too far in the future: %s" + }, + "premium_required": { + "title": "Premium required", + "description": "%s", + "custom_community": "Custom community events require a premium subscription" + }, + "post": { + "title": "Failed to post event", + "description": "%s" + } + }, + + "actions": { + "label": "Actions", + "description": "Post the event, load a preset, etc.", + + "custom": { + "custom": "Switch to builder", + "builder": "Switch to custom", + "description": "Whether this event is a custom event or not" + }, + "post": { + "label": "Post Event", + "description": "Post the event to Event Alerts", + + "confirm": { + "title": "Confirm Post", + "description": "Are you sure you want to post this event?" + }, + + "toast": { + "title": "Posting event...", + "description": "This may take a few seconds" + } + } + }, + + "values": { + "label": "Event Values", + "description": "The actual values of the event", + + "partner_server": { + "label": "Partner Server", + "description": "The server to post this event for", + "community": "COMMUNITY EVENT" + }, + "roles": { + "label": "Ping Roles", + "description": "The roles to ping for this event" + }, + "title": { + "label": "Title", + "description": "REQUIRED: The title of the event" + }, + "time": { + "label": "Time", + "description": "REQUIRED: How long from now until the event starts (e.g. 2h 30m)" + }, + "ip": { + "label": "IP", + "description": "REQUIRED: The IP address players should connect to" + }, + "version": { + "label": "Version", + "description": "REQUIRED: The Minecraft version required to join the event" + }, + "description": { + "label": "Description", + "description": "A description of the event" + }, + "prize": { + "label": "Prize", + "description": "The prize players can win from the event" + }, + "platforms": { + "label": "Platforms", + "description": "The platform(s) supported by the event" + }, + "max_players": { + "label": "Max Players", + "description": "The maximum number of players that can participate in the event" + } + } + }, + "config": { "title": "EventUtils Mod Config", "general": "General", @@ -209,6 +337,11 @@ "merge_failed": "Failed to merge existing accounts" } }, + "event_builder": { + "label": "Event Builder", + "description": "Build and post a new Event Alerts event straight from Minecraft", + "button": "Open a new builder" + }, "groups": { "category": "Groups", diff --git a/src/main/resources/assets/eventutils/textures/event_builder/platform/bedrock.png b/src/main/resources/assets/eventutils/textures/event_builder/platform/bedrock.png new file mode 100644 index 0000000..10f6851 Binary files /dev/null and b/src/main/resources/assets/eventutils/textures/event_builder/platform/bedrock.png differ diff --git a/src/main/resources/assets/eventutils/textures/event_builder/platform/java.png b/src/main/resources/assets/eventutils/textures/event_builder/platform/java.png new file mode 100644 index 0000000..d54e559 Binary files /dev/null and b/src/main/resources/assets/eventutils/textures/event_builder/platform/java.png differ diff --git a/src/main/resources/assets/eventutils/textures/event_builder/role/big_money.png b/src/main/resources/assets/eventutils/textures/event_builder/role/big_money.png new file mode 100644 index 0000000..15a8c9f Binary files /dev/null and b/src/main/resources/assets/eventutils/textures/event_builder/role/big_money.png differ diff --git a/src/main/resources/assets/eventutils/textures/event_builder/role/civilization.png b/src/main/resources/assets/eventutils/textures/event_builder/role/civilization.png new file mode 100644 index 0000000..fdfb032 Binary files /dev/null and b/src/main/resources/assets/eventutils/textures/event_builder/role/civilization.png differ diff --git a/src/main/resources/assets/eventutils/textures/event_builder/role/fun.png b/src/main/resources/assets/eventutils/textures/event_builder/role/fun.png new file mode 100644 index 0000000..467d26b Binary files /dev/null and b/src/main/resources/assets/eventutils/textures/event_builder/role/fun.png differ diff --git a/src/main/resources/assets/eventutils/textures/event_builder/role/housing.png b/src/main/resources/assets/eventutils/textures/event_builder/role/housing.png new file mode 100644 index 0000000..0876e53 Binary files /dev/null and b/src/main/resources/assets/eventutils/textures/event_builder/role/housing.png differ diff --git a/src/main/resources/assets/eventutils/textures/event_builder/role/money.png b/src/main/resources/assets/eventutils/textures/event_builder/role/money.png new file mode 100644 index 0000000..adb001d Binary files /dev/null and b/src/main/resources/assets/eventutils/textures/event_builder/role/money.png differ diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 7c48b40..2e22739 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -4,7 +4,8 @@ "version": "${mod_version}", "name": "${mod_name}", "description": "${description}", - "authors": ["Event Alerts", "aabss", "srnyx", "pvty", "QWERTZ_EXE"], + "authors": ["Event Alerts", "srnyx"], + "contributors": ["aabss", "pvty", "QWERTZ_EXE"], "contact": { "homepage": "https://eventalerts.gg?utm_source=minecraft&utm_medium=fabric_mod_json&utm_campaign=fabric", "sources": "https://github.com/Event-Alerts/EventUtils", @@ -18,7 +19,8 @@ "minecraft": ">=${deps_minecraft}", "fabricloader": ">=${deps_loader}", "${fabric_api_id}": ">=${deps_fabric_api}", - "yet_another_config_lib_v3": ">=${deps_yacl}" + "yet_another_config_lib_v3": ">=${deps_yacl}", + "owo": ">=${deps_owo_lib}" }, "suggests": { "modmenu": ">=${deps_modmenu}" diff --git a/stonecutter.properties.toml b/stonecutter.properties.toml index bb14e41..e797d62 100644 --- a/stonecutter.properties.toml +++ b/stonecutter.properties.toml @@ -14,54 +14,63 @@ library.gson = "2.14.0" library.bson = "5.7.0" library.java_websocket = "1.6.0" library.slf4j = "2.0.15" -library.sdk = "2dcb1b4" +library.sdk = "5bb5854" library.okaeri_configs = "df8ae69" library.discord_ipc = "0.11.3" -library.faststats = "0.30.0" +library.faststats = "0.30.1" ["26.2"] deps.fabric_api="0.152.1+26.2" deps.yacl="3.9.4+26.2-fabric" +deps.owo_lib = "0.13.1+26.2" deps.modmenu="20.0.0" ["26.1.2"] deps.fabric_api="0.145.4+26.1.2" deps.yacl="3.9.2+26.1-fabric" +deps.owo_lib = "0.13.1+26.1" deps.modmenu="18.0.0" ["1.21.11"] deps.fabric_api="0.141.0+1.21.11" deps.yacl="3.8.1+1.21.11-fabric" +deps.owo_lib = "0.13.0+1.21.11" deps.modmenu="17.0.0" ["1.21.6"] deps.fabric_api="0.128.2+1.21.6" deps.yacl="3.7.1+1.21.6-fabric" +deps.owo_lib = "0.12.23+1.21.8" deps.modmenu="15.0.0" ["1.21.5"] deps.fabric_api="0.128.2+1.21.5" deps.yacl="3.7.1+1.21.5-fabric" +deps.owo_lib = "0.12.21+1.21.5" deps.modmenu="14.0.0" ["1.21.4"] deps.fabric_api="0.114.0+1.21.4" deps.yacl="3.6.2+1.21.4-fabric" deps.modmenu="13.0.0" +deps.owo_lib = "0.12.20+1.21.4" ["1.21.1"] deps.fabric_api="0.102.0+1.21.1" deps.yacl="3.6.0+1.21-fabric" +deps.owo_lib = "0.12.15.4+1.21" deps.modmenu="11.0.3" ["1.21"] deps.fabric_api="0.102.0+1.21" deps.yacl="3.6.0+1.21-fabric" +deps.owo_lib = "0.12.15.4+1.21" deps.modmenu="11.0.3" ["1.20.4"] deps.fabric_api="0.97.1+1.20.4" deps.yacl="3.6.0+1.20.4-fabric" +deps.owo_lib = "0.12.6+1.20.3" deps.modmenu="9.2.0" deps.placeholder_api="2.4.0-pre.1+1.20.4"