diff --git a/build.gradle.kts b/build.gradle.kts index b068ff6..5596e18 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,7 +33,7 @@ dependencies { modImplementation("com.daqem.uilib:uilib-fabric:${property("deps.uilib_version")}") modImplementation("maven.modrinth:modmenu:${property("deps.modmenu_version")}") - modCompileOnly("maven.modrinth:scamscreener:${property("deps.scamscreener_version")}") + modCompileOnly("maven.modrinth:scamscreener:${property("deps.scamscreener_version")}+${stonecutter.current.version}") modCompileOnly("maven.modrinth:scaleme:${property("deps.scaleme_version")}") modImplementation("net.azureaaron:hm-api:${property("deps.hm_api_version")}") @@ -45,7 +45,7 @@ dependencies { modRuntimeOnly("me.djtheredstoner:DevAuth-fabric:1.2.2") modRuntimeOnly("maven.modrinth:modmenu:${property("deps.modmenu_version")}") - modRuntimeOnly("maven.modrinth:scamscreener:${property("deps.scamscreener_version")}") + modRuntimeOnly("maven.modrinth:scamscreener:${property("deps.scamscreener_version")}+${stonecutter.current.version}") } loom { diff --git a/gradle.properties b/gradle.properties index f203a41..e1721b8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,6 +11,7 @@ mod.name=Pack Core # Global dependencies deps.fabric_loader=0.18.4 +deps.scamscreener_version=2.1.2 # Versioned dependencies deps.fabric_api=[VERSIONED] @@ -18,7 +19,6 @@ deps.uilib_version=[VERSIONED] deps.midnightlib_version=[VERSIONED] deps.hm_api_version=[VERSIONED] deps.modmenu_version=[VERSIONED] -deps.scamscreener_version=[VERSIONED] deps.scaleme_version=[VERSIONED] deps.sodium_version=[VERSIONED] diff --git a/src/main/java/com/github/kd_gaming1/packcore/PackCore.java b/src/main/java/com/github/kd_gaming1/packcore/PackCore.java index a87a89b..05de131 100644 --- a/src/main/java/com/github/kd_gaming1/packcore/PackCore.java +++ b/src/main/java/com/github/kd_gaming1/packcore/PackCore.java @@ -11,9 +11,12 @@ import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.TitleScreen; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,33 +37,27 @@ public void onInitializeClient() { LOGGER.info("[PackCore] Initialized"); RamWarningHelper.init(); - UpdateChecker.checkAsync(); ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> PackCoreCommands.register(dispatcher)); ScreenEvents.BEFORE_INIT.register((client, screen, scaledWidth, scaledHeight) -> { - if (!(screen instanceof TitleScreen) || screen instanceof PackCoreTitleScreen || replacingTitleScreen) return; + if (!(screen instanceof TitleScreen)) return; RamWarningHelper.onMainMenu(); + if (decorateMinimalTitleScreenIfNeeded(screen, scaledWidth, scaledHeight)) return; - if (!PackCoreConfig.successfulWelcomeWizard) { - client.execute(() -> client.setScreen(new WelcomeWizardScreen(screen))); - return; - } + scheduleConfiguredTitleScreen(client, screen); + }); - client.execute(() -> { - replacingTitleScreen = true; - try { - switch (PackCoreConfig.menuStyle) { - case MODERN -> client.setScreen(new SBETitleScreen()); - case MODERN_MINIMAL -> client.setScreen(new SBETitleScreen(false)); - case MINIMAL -> client.setScreen(new PackCoreTitleScreen()); - } - } finally { - replacingTitleScreen = false; - } - }); + ClientTickEvents.END_CLIENT_TICK.register(client -> { + if (decorateMinimalTitleScreenIfNeeded( + client.screen, + client.getWindow().getGuiScaledWidth(), + client.getWindow().getGuiScaledHeight() + )) return; + + scheduleConfiguredTitleScreen(client, client.screen); }); ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> client.execute(RamWarningHelper::onWorldJoin)); @@ -68,4 +65,41 @@ public void onInitializeClient() { ClientLifecycleEvents.CLIENT_STARTED.register(client -> PlaytimeTracker.onSessionStart()); ClientLifecycleEvents.CLIENT_STOPPING.register(client -> PlaytimeTracker.onSessionEnd()); } + + private static boolean decorateMinimalTitleScreenIfNeeded(Screen screen, int scaledWidth, int scaledHeight) { + if (!(screen instanceof TitleScreen titleScreen) + || !PackCoreConfig.successfulWelcomeWizard + || PackCoreConfig.menuStyle != PackCoreConfig.MenuStyle.MINIMAL) { + return false; + } + + if (!(titleScreen instanceof PackCoreTitleScreen)) { + PackCoreTitleScreen.decorateExisting(titleScreen, scaledWidth, scaledHeight); + } + return true; + } + + private static void scheduleConfiguredTitleScreen(Minecraft client, Screen screen) { + if (!(screen instanceof TitleScreen) || screen instanceof PackCoreTitleScreen || replacingTitleScreen) return; + + replacingTitleScreen = true; + client.execute(() -> { + try { + if (client.screen != screen) return; + + if (!PackCoreConfig.successfulWelcomeWizard) { + client.setScreen(new WelcomeWizardScreen(screen)); + return; + } + + switch (PackCoreConfig.menuStyle) { + case MODERN -> client.setScreen(new SBETitleScreen()); + case MODERN_MINIMAL -> client.setScreen(new SBETitleScreen(false)); + case MINIMAL -> client.setScreen(new PackCoreTitleScreen()); + } + } finally { + replacingTitleScreen = false; + } + }); + } } diff --git a/src/main/java/com/github/kd_gaming1/packcore/gui/screen/PackCoreTitleScreen.java b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/PackCoreTitleScreen.java index 0756303..836cc0f 100644 --- a/src/main/java/com/github/kd_gaming1/packcore/gui/screen/PackCoreTitleScreen.java +++ b/src/main/java/com/github/kd_gaming1/packcore/gui/screen/PackCoreTitleScreen.java @@ -6,6 +6,8 @@ import com.github.kd_gaming1.packcore.metadata.ModpackMetadata; import com.github.kd_gaming1.packcore.update.UpdateChecker; import com.github.kd_gaming1.packcore.update.UpdateStatus; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.fabricmc.fabric.api.client.screen.v1.Screens; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.Button; @@ -21,6 +23,10 @@ import net.minecraft.util.Util; import org.jspecify.annotations.NonNull; +import java.util.Collections; +import java.util.Set; +import java.util.WeakHashMap; + import static com.github.kd_gaming1.packcore.PackCore.MOD_ID; /** @@ -34,21 +40,18 @@ public class PackCoreTitleScreen extends TitleScreen { private static final int ICON_SPACING = 4; private static final int BUTTON_HEIGHT = 20; private static final int BUTTON_STRIDE = 24; + private static final String JOIN_HYPIXEL_LABEL = Component.translatable("gui.packcore.button.join_hypixel").getString(); private static boolean updateToastShown = false; + private static final Set VERSION_HOOKED_SCREENS = + Collections.newSetFromMap(new WeakHashMap<>()); @Override protected void init() { super.init(); UpdateStatus status = UpdateChecker.getCachedStatus(); - - if (!updateToastShown) { - if (status.isUpdateAvailable()) { - ToastHelper.showUpdateAvailable(status.latestVersion()); - } - updateToastShown = true; - } + showUpdateToastIfNeeded(); // Join Hypixel — one row above vanilla singleplayer int hypixelY = this.height / 4 + 48 - (BUTTON_STRIDE * 2); @@ -93,6 +96,66 @@ protected void init() { btn -> Minecraft.getInstance().setScreen(new ChangelogScreen(this, status))); } + public static void decorateExisting(TitleScreen screen, int scaledWidth, int scaledHeight) { + showUpdateToastIfNeeded(); + Screens.getButtons(screen).removeIf(button -> + button instanceof PackCoreDecoratedWidget + || JOIN_HYPIXEL_LABEL.equals(button.getMessage().getString()) + ); + + int hypixelY = scaledHeight / 4 + 48 - (BUTTON_STRIDE * 2); + Screens.getButtons(screen).add(Button.builder( + Component.translatable("gui.packcore.button.join_hypixel"), + btn -> connectToHypixel(screen) + ).bounds(scaledWidth / 2 - 100, hypixelY, 200, BUTTON_HEIGHT).build()); + + int vanillaTwoLinesY = scaledHeight - 10 - Minecraft.getInstance().font.lineHeight - 2; + int yourVersionY = vanillaTwoLinesY - Minecraft.getInstance().font.lineHeight - 2; + + int githubY = yourVersionY - MARGIN - ICON_SIZE; + int modrinthY = githubY - ICON_SPACING - ICON_SIZE; + int discordY = modrinthY - ICON_SPACING - ICON_SIZE; + + Screens.getButtons(screen).add(createDecoratedIconButton( + MARGIN, discordY, "menu/discord_icon", + Component.translatable("gui.packcore.tooltip.discord"), + btn -> Util.getPlatform().openUri(ModpackMetadata.getInstance().getDiscordUrl()) + )); + + Screens.getButtons(screen).add(createDecoratedIconButton( + MARGIN, modrinthY, "menu/modrinth_icon", + Component.translatable("gui.packcore.tooltip.modrinth"), + btn -> Util.getPlatform().openUri(ModpackMetadata.getInstance().getWebsiteUrl()) + )); + + Screens.getButtons(screen).add(createDecoratedIconButton( + MARGIN, githubY, "menu/github_icon", + Component.translatable("gui.packcore.tooltip.github"), + btn -> Util.getPlatform().openUri(ModpackMetadata.getInstance().getIssueTrackerUrl()) + )); + + int settingsY = scaledHeight - ICON_SIZE - MARGIN - (Minecraft.getInstance().font.lineHeight * 2) - 4; + Screens.getButtons(screen).add(createDecoratedIconButton( + scaledWidth - ICON_SIZE - MARGIN, settingsY, "menu/settings_icon", + Component.translatable("gui.packcore.tooltip.modpack_config"), + btn -> Minecraft.getInstance().setScreen(new ConfigScreen()) + )); + + UpdateStatus status = UpdateChecker.getCachedStatus(); + boolean hasUpdate = status.isUpdateAvailable(); + String updateIcon = hasUpdate ? "menu/update_icon_available" : "menu/update_icon"; + Component updateTooltip = hasUpdate + ? Component.translatable("gui.packcore.tooltip.update_available", status.latestVersion()) + : Component.translatable("gui.packcore.tooltip.changelog"); + + Screens.getButtons(screen).add(createDecoratedIconButton( + scaledWidth - ICON_SIZE - MARGIN, MARGIN, updateIcon, updateTooltip, + btn -> Minecraft.getInstance().setScreen(new ChangelogScreen(screen, status)) + )); + + registerVersionHook(screen); + } + @Override public void render(@NonNull GuiGraphics graphics, int mouseX, int mouseY, float delta) { super.render(graphics, mouseX, mouseY, delta); @@ -102,13 +165,17 @@ public void render(@NonNull GuiGraphics graphics, int mouseX, int mouseY, float } private void connectToHypixel() { + connectToHypixel(this); + } + + private static void connectToHypixel(TitleScreen screen) { ServerData serverData = new ServerData( "Hypixel", PackCoreConfig.serverAddressForQuickJoinButton, ServerData.Type.OTHER ); ConnectScreen.startConnecting( - this, + screen, Minecraft.getInstance(), ServerAddress.parseString(PackCoreConfig.serverAddressForQuickJoinButton), serverData, @@ -125,6 +192,35 @@ private void addIconButton(int x, int y, String spritePath, Component tooltip, B addRenderableWidget(button); } + private static PackCoreImageButton createDecoratedIconButton(int x, int y, String spritePath, Component tooltip, Button.OnPress onPress) { + Identifier icon = Identifier.fromNamespaceAndPath(MOD_ID, spritePath); + WidgetSprites sprites = new WidgetSprites(icon, icon, icon); + PackCoreImageButton button = new PackCoreImageButton(x, y, ICON_SIZE, ICON_SIZE, sprites, onPress); + button.setTooltip(Tooltip.create(tooltip)); + return button; + } + + private static void registerVersionHook(TitleScreen screen) { + if (!VERSION_HOOKED_SCREENS.add(screen)) return; + + ScreenEvents.afterRender(screen).register((screen1, graphics, mouseX, mouseY, tickDelta) -> { + Minecraft client = Minecraft.getInstance(); + int height = client.getWindow().getGuiScaledHeight(); + int yourVersionY = (height - MARGIN - ICON_SIZE) + (ICON_SIZE - client.font.lineHeight) / 2; + graphics.drawString(client.font, buildVersionText(UpdateChecker.getCachedStatus()), MARGIN, yourVersionY, 0xFFFFFFFF, false); + }); + } + + private static void showUpdateToastIfNeeded() { + if (updateToastShown) return; + + UpdateStatus status = UpdateChecker.getCachedStatus(); + if (status.isUpdateAvailable()) { + ToastHelper.showUpdateAvailable(status.latestVersion()); + } + updateToastShown = true; + } + private static Component buildVersionText(UpdateStatus status) { String installed = status.installedVersion() != null ? status.installedVersion() @@ -135,4 +231,12 @@ private static Component buildVersionText(UpdateStatus status) { } return Component.literal("v" + installed); } -} \ No newline at end of file + + private interface PackCoreDecoratedWidget {} + + private static final class PackCoreImageButton extends ImageButton implements PackCoreDecoratedWidget { + private PackCoreImageButton(int x, int y, int width, int height, WidgetSprites sprites, OnPress onPress) { + super(x, y, width, height, sprites, onPress); + } + } +} diff --git a/versions/1.21.11/gradle.properties b/versions/1.21.11/gradle.properties index c9a876c..5b21535 100644 --- a/versions/1.21.11/gradle.properties +++ b/versions/1.21.11/gradle.properties @@ -4,7 +4,6 @@ deps.midnightlib_version=1.9.2+1.21.11-fabric deps.hm_api_version=1.0.1+1.21.2 deps.modmenu_version=17.0.0-beta.2 deps.uilib_version=19.1.0 -deps.scamscreener_version=uue2ayyA deps.scaleme_version=3.0.0 deps.sodium_version=mc1.21.11-0.8.4-fabric