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"