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 0000000..38b1e30
Binary files /dev/null and b/mods/nosingleplayer/src/main/resources/assets/nosingleplayer/textures/gui/discord.png differ
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 0000000..e71ce22
Binary files /dev/null and b/mods/nosingleplayer/src/main/resources/assets/nosingleplayer/textures/gui/options.png differ
diff --git a/mods/nosingleplayer/src/main/resources/assets/nosingleplayer/textures/gui/quit.png b/mods/nosingleplayer/src/main/resources/assets/nosingleplayer/textures/gui/quit.png
new file mode 100644
index 0000000..219a99e
Binary files /dev/null and b/mods/nosingleplayer/src/main/resources/assets/nosingleplayer/textures/gui/quit.png differ
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"