From 4f95e8281296ddde73c5d35eb6dbb35c9b8b8b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ana=C3=AFs=20Saraiva?= <53227802+Kisakay@users.noreply.github.com> Date: Mon, 19 Oct 2026 22:08:21 +0200 Subject: [PATCH] =?UTF-8?q?nosingleplayer=201.1.8=20:=20menu=20titre=20rew?= =?UTF-8?q?ork=20(boutons=20ic=C3=B4nes,=20Discord,=20statut=20serveur)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Menu principal : - Bouton "Join Discord" (logo Discord lissé + texte) sous "Rejoindre SurviePote", ouvre https://discord.gg/EpkgEPv5c. - "Options..." et "Quit Game" remplacés par des boutons icônes sans texte (engrenage / symbole power) qui conservent l'action du bouton vanilla (ButtonAccessor -> onPress). - Panneau de statut du serveur en haut à gauche : En ligne / Hors ligne, joueurs current/max et ping. Attention : dans ServerStatusPinger.pingServer les 2 Runnable ne sont pas des callbacks succès/échec (le 1er ne fire que si le favicon change, le 2nd est le pong) -> on lit directement ServerData.status ("0/20" quand le statut est arrivé, vidé en cas d'échec) + timeout 8s. - Suppression du texte "26.3 mc" (bas à gauche) : le mixin TitleScreenTextMixin qui le dessinait est supprimé, la redirection masque le texte de version. - Suppression du splash, du logo Mojang, des boutons amis/icônes et du copyright (TitleScreenMixin : FriendsButton/SpriteIconButton/PlainTextButton sont des sous-classes de Button, ils étaient avalés par "instanceof Button"). Icônes : PNG lisses 64x64 (supersampling 8x8) au lieu du pixel art. Chargement des textures du mod via ModTextures (les assets des mods ne sont plus exposés au ResourceManager en MC 26.3). --- mods/nosingleplayer/build.gradle | 3 + mods/nosingleplayer/gradle.properties | 2 +- .../nosingleplayer/ModTextures.java | 69 ++++++++++ .../nosingleplayer/gui/DiscordButton.java | 65 +++++++++ .../nosingleplayer/gui/IconButton.java | 54 ++++++++ .../gui/ServerStatusWidget.java | 127 ++++++++++++++++++ .../nosingleplayer/mixin/ButtonAccessor.java | 13 ++ .../mixin/TitleScreenMixin.java | 66 +++++++++ .../mixin/TitleScreenRenderMixin.java | 49 +++---- .../mixin/TitleScreenTextMixin.java | 26 ---- .../nosingleplayer/textures/gui/discord.png | Bin 0 -> 904 bytes .../nosingleplayer/textures/gui/options.png | Bin 0 -> 645 bytes .../nosingleplayer/textures/gui/quit.png | Bin 0 -> 704 bytes .../main/resources/nosingleplayer.mixins.json | 2 +- 14 files changed, 415 insertions(+), 61 deletions(-) create mode 100644 mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/ModTextures.java create mode 100644 mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/gui/DiscordButton.java create mode 100644 mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/gui/IconButton.java create mode 100644 mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/gui/ServerStatusWidget.java create mode 100644 mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/mixin/ButtonAccessor.java delete mode 100644 mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/mixin/TitleScreenTextMixin.java create mode 100644 mods/nosingleplayer/src/main/resources/assets/nosingleplayer/textures/gui/discord.png create mode 100644 mods/nosingleplayer/src/main/resources/assets/nosingleplayer/textures/gui/options.png create mode 100644 mods/nosingleplayer/src/main/resources/assets/nosingleplayer/textures/gui/quit.png diff --git a/mods/nosingleplayer/build.gradle b/mods/nosingleplayer/build.gradle index d73948b..f05472e 100644 --- a/mods/nosingleplayer/build.gradle +++ b/mods/nosingleplayer/build.gradle @@ -42,6 +42,9 @@ tasks.register('downloadMcLibs') { new File(outDir, 'slf4j-api.jar')) dl('https://libraries.minecraft.net/com/mojang/brigadier/1.3.11/brigadier-1.3.11.jar', new File(outDir, 'brigadier.jar')) + // Annotations JSpecify : referencées par les signatures de MC 26.3 (@Nullable...). + dl('https://repo1.maven.org/maven2/org/jspecify/jspecify/1.0.0/jspecify-1.0.0.jar', + new File(outDir, 'jspecify.jar')) } } diff --git a/mods/nosingleplayer/gradle.properties b/mods/nosingleplayer/gradle.properties index 3c803b0..3b623e8 100644 --- a/mods/nosingleplayer/gradle.properties +++ b/mods/nosingleplayer/gradle.properties @@ -5,7 +5,7 @@ loader_version=0.20.0 loom_version=1.19.1 # Mod Properties -mod_version=1.1.7 +mod_version=1.1.8 maven_group=fr.surviepote archives_base_name=nosingleplayer diff --git a/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/ModTextures.java b/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/ModTextures.java new file mode 100644 index 0000000..2df0ca7 --- /dev/null +++ b/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/ModTextures.java @@ -0,0 +1,69 @@ +package fr.surviepote.nosingleplayer; + +import com.mojang.blaze3d.platform.NativeImage; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.renderpearl.api.textures.FilterMode; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.resources.Identifier; + +/** + * Chargement des textures du mod. + * + *

MC 26.3 n'expose plus les ressources des mods au ResourceManager : les packs ne + * sont lus que depuis des dossiers, pas depuis les jars du classpath. Une texture + * référencée par son Identifier est donc introuvable (placeholder magenta). On lit + * donc le PNG directement dans le jar du mod et on enregistre une DynamicTexture dans + * le TextureManager. + */ +public final class ModTextures { + + private static final Map CACHE = new HashMap<>(); + + private ModTextures() { + } + + /** + * Renvoie la texture (chargée depuis le jar du mod), ou null si le fichier est absent. + * + * @param id identifiant sous lequel la texture est enregistrée + * @param resourcePath chemin dans le jar, ex {@code /assets/nosingleplayer/...} + * @param name nom affiché dans les crash reports GPU + * @param linear true = filtrage linéaire (images agrandies), false = nearest (pixel art) + */ + public static DynamicTexture get(Identifier id, String resourcePath, String name, boolean linear) { + DynamicTexture texture = CACHE.get(id); + var textures = Minecraft.getInstance().getTextureManager(); + if (texture != null && textures.getTexture(id) == texture) { + return texture; // déjà chargée et toujours enregistrée + } + try (InputStream in = ModTextures.class.getResourceAsStream(resourcePath)) { + if (in == null) { + NoSingleplayer.LOGGER.error("[SurviePote] Texture introuvable dans le jar : {}", resourcePath); + return null; + } + NativeImage image = NativeImage.read(in); + texture = linear + ? new LinearTexture(() -> name, image) + : new DynamicTexture(() -> name, image); + CACHE.put(id, texture); + textures.register(id, texture); + return texture; + } catch (IOException e) { + NoSingleplayer.LOGGER.error("[SurviePote] Échec de chargement de la texture " + resourcePath, e); + return null; + } + } + + /** DynamicTexture avec filtrage linéaire (utile quand l'image est réduite à l'écran). */ + private static final class LinearTexture extends DynamicTexture { + LinearTexture(java.util.function.Supplier name, NativeImage image) { + super(name, image); + this.sampler = RenderSystem.getSamplerCache().getClampToEdge(FilterMode.LINEAR); + } + } +} diff --git a/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/gui/DiscordButton.java b/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/gui/DiscordButton.java new file mode 100644 index 0000000..9eea142 --- /dev/null +++ b/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/gui/DiscordButton.java @@ -0,0 +1,65 @@ +package fr.surviepote.nosingleplayer.gui; + +import fr.surviepote.nosingleplayer.ModTextures; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.components.AbstractButton; +import net.minecraft.client.input.InputWithModifiers; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; + +/** + * Bouton "Join Discord" : fond vanilla, logo Discord en pixel art et libellé. + * Ouvre l'invite Discord au clic. + */ +public class DiscordButton extends AbstractButton { + + public static final Identifier TEXTURE = Identifier.fromNamespaceAndPath( + "nosingleplayer", "textures/gui/discord.png"); + + private static final String TEXTURE_PATH = "/assets/nosingleplayer/textures/gui/discord.png"; + + private static final int ICON_SIZE = 16; + private static final int ICON_GAP = 5; + private static final int TEXT_COLOR = 0xFFFFFFFF; + private static final int TEXT_HEIGHT = 9; + + private final Runnable action; + + public DiscordButton(int x, int y, int width, int height, String label, Runnable action) { + super(x, y, width, height, Component.literal(label)); + this.action = action; + // La texture doit être enregistrée avant le premier rendu. + ModTextures.get(TEXTURE, TEXTURE_PATH, "SurviePote Discord", false); + } + + @Override + protected void extractContents(GuiGraphicsExtractor extractor, int mouseX, int mouseY, float partialTick) { + extractDefaultSprite(extractor); // fond standard (survol/clic inclus) + + Font font = Minecraft.getInstance().font; + String label = getMessage().getString(); + int textWidth = font.width(label); + int groupWidth = ICON_SIZE + ICON_GAP + textWidth; + int startX = getX() + (getWidth() - groupWidth) / 2; + int iconY = getY() + (getHeight() - ICON_SIZE) / 2; + int textY = getY() + (getHeight() - TEXT_HEIGHT) / 2; + + extractor.blit(RenderPipelines.GUI_TEXTURED, TEXTURE, + startX, iconY, 0.0F, 0.0F, ICON_SIZE, ICON_SIZE, ICON_SIZE, ICON_SIZE, -1); + extractor.text(font, label, startX + ICON_SIZE + ICON_GAP, textY, TEXT_COLOR, true); + } + + @Override + public void onPress(InputWithModifiers input) { + this.action.run(); + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput output) { + defaultButtonNarrationText(output); + } +} diff --git a/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/gui/IconButton.java b/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/gui/IconButton.java new file mode 100644 index 0000000..3c75882 --- /dev/null +++ b/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/gui/IconButton.java @@ -0,0 +1,54 @@ +package fr.surviepote.nosingleplayer.gui; + +import fr.surviepote.nosingleplayer.ModTextures; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.components.AbstractButton; +import net.minecraft.client.input.InputWithModifiers; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; + +/** + * Bouton icône vanilla (fond standard) avec une texture du mod, sans texte. + */ +public class IconButton extends AbstractButton { + + public static final Identifier OPTIONS_TEXTURE = Identifier.fromNamespaceAndPath( + "nosingleplayer", "textures/gui/options.png"); + public static final Identifier QUIT_TEXTURE = Identifier.fromNamespaceAndPath( + "nosingleplayer", "textures/gui/quit.png"); + + private static final int ICON_SIZE = 16; + + private final Identifier texture; + private final Runnable action; + + public IconButton(int x, int y, int width, int height, Component message, + Identifier texture, String resourcePath, Runnable action) { + super(x, y, width, height, message); + this.texture = texture; + this.action = action; + // La texture doit être enregistrée avant le premier rendu. + ModTextures.get(texture, resourcePath, message.getString(), false); + } + + @Override + protected void extractContents(GuiGraphicsExtractor extractor, int mouseX, int mouseY, float partialTick) { + extractDefaultSprite(extractor); // fond standard (survol/clic inclus) + int iconX = getX() + (getWidth() - ICON_SIZE) / 2; + int iconY = getY() + (getHeight() - ICON_SIZE) / 2; + extractor.blit(RenderPipelines.GUI_TEXTURED, this.texture, + iconX, iconY, 0.0F, 0.0F, ICON_SIZE, ICON_SIZE, ICON_SIZE, ICON_SIZE, -1); + } + + @Override + public void onPress(InputWithModifiers input) { + this.action.run(); + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput output) { + defaultButtonNarrationText(output); + } +} diff --git a/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/gui/ServerStatusWidget.java b/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/gui/ServerStatusWidget.java new file mode 100644 index 0000000..056d277 --- /dev/null +++ b/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/gui/ServerStatusWidget.java @@ -0,0 +1,127 @@ +package fr.surviepote.nosingleplayer.gui; + +import fr.surviepote.nosingleplayer.NoSingleplayer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.multiplayer.ServerData; +import net.minecraft.client.multiplayer.ServerStatusPinger; +import net.minecraft.network.chat.Component; +import net.minecraft.server.network.EventLoopGroupHolder; + +/** + * Panneau "statut du serveur" affiché en haut à gauche du menu principal : + * état en ligne / hors ligne, nombre de joueurs et ping. + * + *

Note : dans ServerStatusPinger.pingServer, les deux Runnable passés ne sont pas + * des callbacks de succès/échec (le 1er ne fire que si le favicon change, le 2nd est + * le pong). On lit donc directement l'état dans ServerData : `status` contient le + * compteur de joueurs ("0/20") quand le statut est arrivé, et est vidé en cas d'échec. + */ +public class ServerStatusWidget extends AbstractWidget { + + private static final String HOST = "mcpv.ihorizon.org:25565"; + private static final long REFRESH_MS = 15000L; + private static final long TIMEOUT_MS = 8000L; + + private static final int BG_COLOR = 0xB0000000; + private static final int ONLINE_COLOR = 0xFF4CD964; + private static final int OFFLINE_COLOR = 0xFFE04B4B; + private static final int PENDING_COLOR = 0xFFAAAAAA; + private static final int TEXT_COLOR = 0xFFFFFFFF; + + private final ServerStatusPinger pinger = new ServerStatusPinger(); + private final ServerData data = new ServerData("SurviePote", HOST, ServerData.Type.OTHER); + + private volatile boolean resolved; + private volatile boolean online; + private volatile String playersText = ""; + private volatile long ping = -1L; + + private long nextRefresh; + private long pingStartedAt; + private boolean pending; + + public ServerStatusWidget(int x, int y, int width, int height) { + super(x, y, width, height, Component.literal("Statut du serveur SurviePote")); + } + + @Override + protected void extractWidgetRenderState(GuiGraphicsExtractor extractor, int mouseX, int mouseY, float partialTick) { + long now = System.currentTimeMillis(); + if (now >= this.nextRefresh) { + this.nextRefresh = now + REFRESH_MS; + refresh(); + } + if (this.pending && now - this.pingStartedAt > TIMEOUT_MS) { + this.pending = false; // aucune réponse du serveur + } + this.pinger.tick(); + readStatus(); + + Font font = Minecraft.getInstance().font; + boolean online = this.online; + boolean resolved = this.resolved; + + String statusText = !resolved ? "Connexion..." : (online ? "En ligne" : "Hors ligne"); + int statusColor = !resolved ? PENDING_COLOR : (online ? ONLINE_COLOR : OFFLINE_COLOR); + String playersText = online + ? (this.playersText.matches("\\d+/\\d+") ? this.playersText + " joueurs" : this.playersText) + : (resolved ? "Joueurs : -" : ""); + String pingText = online && this.ping > 0 ? this.ping + " ms" : ""; + + // Fond du panneau. + extractor.fill(getX(), getY(), getX() + getWidth(), getY() + getHeight(), BG_COLOR); + // Pastille d'état. + extractor.fill(getX() + 4, getY() + 4, getX() + 9, getY() + 9, statusColor); + // Textes. + extractor.text(font, statusText, getX() + 12, getY() + 3, statusColor, true); + if (!playersText.isEmpty()) { + extractor.text(font, playersText, getX() + 12, getY() + 13, TEXT_COLOR, true); + } + if (!pingText.isEmpty()) { + extractor.text(font, pingText, + getX() + getWidth() - font.width(pingText) - 4, getY() + 3, PENDING_COLOR, true); + } + } + + /** Lit l'état réel du ping dans ServerData (mis à jour par le thread réseau). */ + private void readStatus() { + Component status = this.data.status; + String text = status != null ? status.getString() : ""; + if (!text.isEmpty()) { + this.online = true; + this.resolved = true; + this.playersText = text; // "0/20" + } else if (this.resolved) { + this.online = false; // le status a été vidé => échec du ping + } else if (!this.pending) { + this.resolved = true; // timeout : serveur injoignable + } + if (this.data.ping > 0) { + this.ping = this.data.ping; + } + } + + private void refresh() { + try { + this.pingStartedAt = System.currentTimeMillis(); + this.pending = true; + this.pinger.pingServer(this.data, () -> { + }, () -> { + }, EventLoopGroupHolder.remote(Minecraft.getInstance().options.useNativeTransport())); + } catch (Exception e) { + NoSingleplayer.LOGGER.warn("[SurviePote] Ping serveur impossible : {}", e.toString()); + this.pending = false; + this.resolved = true; + this.online = false; + } + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput output) { + defaultButtonNarrationText(output); + } +} diff --git a/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/mixin/ButtonAccessor.java b/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/mixin/ButtonAccessor.java new file mode 100644 index 0000000..6daed3c --- /dev/null +++ b/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/mixin/ButtonAccessor.java @@ -0,0 +1,13 @@ +package fr.surviepote.nosingleplayer.mixin; + +import net.minecraft.client.gui.components.Button; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +/** Permet de récupérer l'action d'un bouton vanilla (pour la conserver). */ +@Mixin(Button.class) +public interface ButtonAccessor { + + @Accessor("onPress") + Button.OnPress surviepote$getOnPress(); +} diff --git a/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/mixin/TitleScreenMixin.java b/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/mixin/TitleScreenMixin.java index a63c20b..d6c81cf 100644 --- a/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/mixin/TitleScreenMixin.java +++ b/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/mixin/TitleScreenMixin.java @@ -1,5 +1,10 @@ package fr.surviepote.nosingleplayer.mixin; +import com.mojang.blaze3d.Blaze3D; +import fr.surviepote.nosingleplayer.gui.DiscordButton; +import fr.surviepote.nosingleplayer.gui.IconButton; +import fr.surviepote.nosingleplayer.gui.ServerStatusWidget; +import java.net.URI; import java.util.ArrayList; import java.util.List; import net.minecraft.client.Minecraft; @@ -26,6 +31,12 @@ public abstract class TitleScreenMixin { private static final String SERVER_HOST = "mcpv.ihorizon.org:25565"; + private static final String DISCORD_URL = "https://discord.gg/EpkgEPv5c"; + + private static final int ICON_SIZE = 20; + private static final String OPTIONS_TEXTURE_PATH = "/assets/nosingleplayer/textures/gui/options.png"; + private static final String QUIT_TEXTURE_PATH = "/assets/nosingleplayer/textures/gui/quit.png"; + @Unique private static void surviepote$removeWidget(TitleScreen self, GuiEventListener widget) { self.children().remove(widget); @@ -40,12 +51,16 @@ public abstract class TitleScreenMixin { String solo = Component.translatable("menu.singleplayer").getString(); String multi = Component.translatable("menu.multiplayer").getString(); String realms = Component.translatable("menu.online").getString(); + String options = Component.translatable("menu.options").getString(); + String quit = Component.translatable("menu.quit").getString(); int joinX = minecraft.getWindow().getGuiScaledWidth() / 2 - 100; int joinY = minecraft.getWindow().getGuiScaledHeight() / 4 + 72; boolean foundMulti = false; List toRemove = new ArrayList<>(); + Button optionsButton = null; + Button quitButton = null; for (GuiEventListener child : new ArrayList<>(self.children())) { // ATTENTION : FriendsButton/SpriteIconButton/PlainTextButton héritent tous // de Button. Il faut donc les tester AVANT "instanceof Button", sinon la @@ -71,6 +86,14 @@ public abstract class TitleScreenMixin { toRemove.add(button); } else if (msg.equals(realms)) { toRemove.add(button); + } else if (msg.equals(options)) { + // Remplacé plus bas par un bouton icône (engrenage). + optionsButton = button; + toRemove.add(button); + } else if (msg.equals(quit)) { + // Remplacé plus bas par un bouton icône (power). + quitButton = button; + toRemove.add(button); } } } @@ -88,10 +111,53 @@ public abstract class TitleScreenMixin { @SuppressWarnings({ "unchecked", "rawtypes" }) List rawChildren = (List) self.children(); rawChildren.add(join); + + // Bouton Discord (logo pixel art + texte) juste sous "Rejoindre SurviePote". + DiscordButton discord = new DiscordButton(joinX, joinY + 24, 200, 20, "Join Discord", () -> + Blaze3D.openUri(URI.create(DISCORD_URL))); + ((ScreenAccessor) self).surviepote$getRenderables().add(discord); + rawChildren.add(discord); + + // Options / Quit Game : boutons icônes (engrenage / power) sans texte. + // On garde l'action du bouton vanilla d'origine. + if (optionsButton != null) { + final Button vanillaOptions = optionsButton; + Button.OnPress press = ((ButtonAccessor) vanillaOptions).surviepote$getOnPress(); + IconButton icon = new IconButton( + optionsButton.getX() + optionsButton.getWidth() / 2 - ICON_SIZE / 2, + optionsButton.getY(), ICON_SIZE, ICON_SIZE, + Component.literal("Options"), + IconButton.OPTIONS_TEXTURE, OPTIONS_TEXTURE_PATH, + () -> press.onPress(vanillaOptions)); + ((ScreenAccessor) self).surviepote$getRenderables().add(icon); + rawChildren.add(icon); + } + if (quitButton != null) { + final Button vanillaQuit = quitButton; + Button.OnPress press = ((ButtonAccessor) vanillaQuit).surviepote$getOnPress(); + IconButton icon = new IconButton( + quitButton.getX() + quitButton.getWidth() / 2 - ICON_SIZE / 2, + quitButton.getY(), ICON_SIZE, ICON_SIZE, + Component.literal("Quit Game"), + IconButton.QUIT_TEXTURE, QUIT_TEXTURE_PATH, + () -> press.onPress(vanillaQuit)); + ((ScreenAccessor) self).surviepote$getRenderables().add(icon); + rawChildren.add(icon); + } + + // Panneau de statut du serveur en haut à gauche (en ligne / joueurs / ping). + ServerStatusWidget status = new ServerStatusWidget(4, 4, 120, 24); + ((ScreenAccessor) self).surviepote$getRenderables().add(status); + rawChildren.add(status); + if (join instanceof AbstractWidget w) { w.visible = true; w.active = true; } + if (discord instanceof AbstractWidget w) { + w.visible = true; + w.active = true; + } if (!foundMulti) { NoSingleplayer.LOGGER.warn("[SurviePote] Bouton Multi introuvable, Join placé par défaut."); } diff --git a/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/mixin/TitleScreenRenderMixin.java b/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/mixin/TitleScreenRenderMixin.java index a5448f9..bb1250a 100644 --- a/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/mixin/TitleScreenRenderMixin.java +++ b/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/mixin/TitleScreenRenderMixin.java @@ -1,9 +1,7 @@ package fr.surviepote.nosingleplayer.mixin; -import com.mojang.blaze3d.platform.NativeImage; -import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.realmsclient.gui.screens.RealmsNotificationsScreen; -import com.mojang.renderpearl.api.textures.FilterMode; +import fr.surviepote.nosingleplayer.ModTextures; import fr.surviepote.nosingleplayer.NoSingleplayer; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; @@ -19,10 +17,6 @@ import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Redirect; -import java.io.IOException; -import java.io.InputStream; -import java.util.function.Supplier; - @Mixin(TitleScreen.class) public abstract class TitleScreenRenderMixin { @@ -91,41 +85,30 @@ public abstract class TitleScreenRenderMixin { * MC 26.3 n'expose plus les ressources des mods au ResourceManager : les packs * ne sont lus que depuis des dossiers, pas depuis les jars du classpath. Une * texture référencée par son Identifier est donc introuvable (placeholder - * magenta). On charge donc le PNG depuis le jar du mod et on enregistre une - * DynamicTexture dans le TextureManager. + * magenta). Le PNG est donc lu dans le jar du mod (voir ModTextures). */ @Unique private static void surviepote$loadBackground() { - var textures = Minecraft.getInstance().getTextureManager(); - if (surviepote$background != null - && textures.getTexture(SURVIEPOTE_BACKGROUND) == surviepote$background) { - return; // déjà chargé et toujours enregistré + DynamicTexture texture = ModTextures.get(SURVIEPOTE_BACKGROUND, SURVIEPOTE_BACKGROUND_RESOURCE, + "SurviePote Background", true); + if (texture == null) { + surviepote$background = null; + return; } - try (InputStream in = NoSingleplayer.class.getResourceAsStream(SURVIEPOTE_BACKGROUND_RESOURCE)) { - if (in == null) { - NoSingleplayer.LOGGER.error("[SurviePote] Fond introuvable dans le jar : {}", - SURVIEPOTE_BACKGROUND_RESOURCE); - return; - } - NativeImage image = NativeImage.read(in); - surviepote$backgroundWidth = image.getWidth(); - surviepote$backgroundHeight = image.getHeight(); - surviepote$background = new LinearDynamicTexture(() -> "SurviePote Background", image); - textures.register(SURVIEPOTE_BACKGROUND, surviepote$background); + if (surviepote$background != texture) { + surviepote$background = texture; + surviepote$backgroundWidth = texture.getPixels().getWidth(); + surviepote$backgroundHeight = texture.getPixels().getHeight(); NoSingleplayer.LOGGER.info("[SurviePote] Fond chargé : {}x{}", surviepote$backgroundWidth, surviepote$backgroundHeight); - } catch (IOException e) { - NoSingleplayer.LOGGER.error("[SurviePote] Échec de chargement du fond", e); } } - /** DynamicTexture avec filtrage linéaire : le fond est réduit à la taille de l'écran. */ - @Unique - private static final class LinearDynamicTexture extends DynamicTexture { - LinearDynamicTexture(Supplier name, NativeImage image) { - super(name, image); - this.sampler = RenderSystem.getSamplerCache().getClampToEdge(FilterMode.LINEAR); - } + // Pas de texte de version ("26.3 mc") en bas à gauche. + @Redirect(method = "extractRenderState", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/gui/GuiGraphicsExtractor;text(Lnet/minecraft/client/gui/Font;Ljava/lang/String;III)V")) + private void surviepote$skipVersionText(GuiGraphicsExtractor extractor, Font font, String text, int x, int y, int color) { } // Pas de texte de splash (message rotatif) sur le menu principal. diff --git a/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/mixin/TitleScreenTextMixin.java b/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/mixin/TitleScreenTextMixin.java deleted file mode 100644 index 5d9fff1..0000000 --- a/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/mixin/TitleScreenTextMixin.java +++ /dev/null @@ -1,26 +0,0 @@ -package fr.surviepote.nosingleplayer.mixin; - -import net.minecraft.client.gui.Font; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.client.gui.screens.TitleScreen; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Redirect; - -@Mixin(TitleScreen.class) -public class TitleScreenTextMixin { - - @Redirect( - method = "extractRenderState", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/client/gui/GuiGraphicsExtractor;text(Lnet/minecraft/client/gui/Font;Ljava/lang/String;III)V" - ) - ) - private void surviepote$versionLine(GuiGraphicsExtractor extractor, Font font, String text, int x, int y, int color) { - if (text.contains("Copyright") || text.contains("Mojang")) { - return; - } - extractor.text(font, "26.3 mc", x, y, color); - } -} diff --git a/mods/nosingleplayer/src/main/resources/assets/nosingleplayer/textures/gui/discord.png b/mods/nosingleplayer/src/main/resources/assets/nosingleplayer/textures/gui/discord.png new file mode 100644 index 0000000000000000000000000000000000000000..38b1e3012e5bde64ec27705dfa1b7877c435e9ef GIT binary patch literal 904 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9o4mJh`hE~)ovm2#%0Zw_ zTWQ{k6&)*9R2VH{3KVQ~2nkb|=MWNdVWzs4mVju-83ipZffHRGEJ}+OEmC3%3~Y2! zP}0&8IAg=2)Kot6<{5)Cg*)$BJ56%_GwFZX{+L5|@4fYW{qKI~Nvk6Zmiz24f3f8Z z!=wl6XUffF^Lg(hysSerElOGRaK8EjQ-=51a~Fj0>dmxS=ASevs6MdD|`o%UWr?L%>AmErpu7Ctgv&EziHczsSI-reZ6PYtkQhq zz%;>U;f5n8mvgzUZ{|2q%sR>cYY%I|)X)E`n}S@l*iKMffO*9v`(vwjGCnDksxFamr~iv2Tl> zq`Hc^c?bG8`oufT)b}|SS<7Oy^truitAO*Hs9&s;xZ6GpsoFn}?FxA-VW~bxLEg#H z%`$dp`ij5{Y+W1Q1Yf^u`s@(zxu1>8f31F^dFkFSHeHJ|Zw`Iw{x+k4CAlQgXLaa> za~I&>aW5PdXVJ?+DycLJr85kHC NJYD@<);T3K0RVh8he7}V literal 0 HcmV?d00001 diff --git a/mods/nosingleplayer/src/main/resources/assets/nosingleplayer/textures/gui/options.png b/mods/nosingleplayer/src/main/resources/assets/nosingleplayer/textures/gui/options.png new file mode 100644 index 0000000000000000000000000000000000000000..e71ce2203d3b1ba562566ee323c89de1349f5b33 GIT binary patch literal 645 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9o4mJh`hE~)oxOHavw=vf zwwCrpjwfARU8n4r7EL z)Z^XXSuK308FMtaUwcPUB<9q7`0%5+bA zk9ZaXhv=!>#qXDh%noI+_kE|8xhZ#Qz6nE)$Um0A>ASK@JaZZ1efrxLJZ8&Kti2z~ znDV7hXXcKFciE3-i8S1v^yl=69Lvv>%Ow8JZ(8;@uWOr-@=@mkU&aW}S$jRim&pd_ zDHO(8POS~B`M1hQZ3Tmeu8Q%LOZylWajO3hx*~cYYTS<*iyNiulMVms2h z=%I%CjEa>>7fSBL+f3983|^xx?N_k+qokCx__^+gncQ#eBo5?toDBGPkfG={!ynEb z#(lmFYO#q2OO!;NckN%t5W{>h^vM|+$De$E!z5Q6f6H`W`P3T@o8-j5@x0RUjA40n zXY;y%O?r|6efu~ZbPR;twQ?3)&KFnRe(YtzJ5FspHC^KhIrt z7j6;>R%0rVPBt^Ul=DT6DdF@pD_aH&vA3V(8O*e_cyi)IrX~GmJ+L|C@JMmzGNZ-ka3sm|} zJ=$Q>clz-mmUU+yToPG#_TeLyb>|*^`ttvR`Rg~XN$)02J~)ovl6Tkb^+m zL_rP}rpAsFib@ttP0R%hEKH3@iaGiOHYzclR1|m}xs$1}BcXlOvMlYXXTBKxw<*i~ z7#`?4<6O<>H|)))pBh!9^RhHB2rw)(4Zig?;~O7?ob2B38R`sR!T*d0jxaZzjAxjj z9qM)8OIe{oaBJkmUKPfiDS18T_Aov8o}ZVZEy>V-aiz7@WpnNvMjQA2e{_etPH8x!6ABO+qrLN zzs*bFUXWB$_vWLPO`5}s8b%Gt%lkMUaEtUj4fw^7AobVPAw8pU7OUAg-{c!_ovWL7 zU0(Rm%0DJKx_P4wQ^OqtjcAny;jGJy$C*n`e`;YeFqtsVoyEZX%o#-{hs+y4H+L`? z9qtTJVpwCs*2md$q;J-@Fb;<5-o{!chB^28lOH^AvS47i%VYb4ZG#WPfglDKmJedy z58j$PelfIIYEm(MnWkLxi*A7+M?I$(s||u2*B#18=UAz5PT-cT+cU-Horg~}aCkW~ zE1j4i;dIbB>BNE+s~54l1|}9>uSwam!9vUMk>%1bNWSM3Cc#?PD zt!2lhqox->+iJNuUHkAwX;a&Cx)WYRNx6Tz6!zq_{>#tv_U(?^Cb`t%5Gzwqpm&Oo zYtX)_E)%D$^Eo$tlJ?}+T2sDU{keo&_4f6&*|BqqyQbLPyZU`^tyA91lR|FJ`)~eQ zCD<3JYqun@My|4b+C2B@1A`;fmnQ{R$-fq}u()z4*} HQ$iB}@*FZy literal 0 HcmV?d00001 diff --git a/mods/nosingleplayer/src/main/resources/nosingleplayer.mixins.json b/mods/nosingleplayer/src/main/resources/nosingleplayer.mixins.json index 22bc3d5..89144bf 100644 --- a/mods/nosingleplayer/src/main/resources/nosingleplayer.mixins.json +++ b/mods/nosingleplayer/src/main/resources/nosingleplayer.mixins.json @@ -5,8 +5,8 @@ "client": [ "TitleScreenMixin", "TitleScreenRenderMixin", - "TitleScreenTextMixin", "MinecraftTitleMixin", + "ButtonAccessor", "RealmsNotificationsMixin", "PauseScreenMixin", "ScreenAccessor"