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"