nosingleplayer 1.1.8 : menu titre rework (boutons icônes, Discord, statut serveur)
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).
This commit is contained in:
@@ -42,6 +42,9 @@ tasks.register('downloadMcLibs') {
|
|||||||
new File(outDir, 'slf4j-api.jar'))
|
new File(outDir, 'slf4j-api.jar'))
|
||||||
dl('https://libraries.minecraft.net/com/mojang/brigadier/1.3.11/brigadier-1.3.11.jar',
|
dl('https://libraries.minecraft.net/com/mojang/brigadier/1.3.11/brigadier-1.3.11.jar',
|
||||||
new File(outDir, 'brigadier.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'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ loader_version=0.20.0
|
|||||||
loom_version=1.19.1
|
loom_version=1.19.1
|
||||||
|
|
||||||
# Mod Properties
|
# Mod Properties
|
||||||
mod_version=1.1.7
|
mod_version=1.1.8
|
||||||
maven_group=fr.surviepote
|
maven_group=fr.surviepote
|
||||||
archives_base_name=nosingleplayer
|
archives_base_name=nosingleplayer
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>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<Identifier, DynamicTexture> 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<String> name, NativeImage image) {
|
||||||
|
super(name, image);
|
||||||
|
this.sampler = RenderSystem.getSamplerCache().getClampToEdge(FilterMode.LINEAR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+127
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
@@ -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();
|
||||||
|
}
|
||||||
+66
@@ -1,5 +1,10 @@
|
|||||||
package fr.surviepote.nosingleplayer.mixin;
|
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.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import net.minecraft.client.Minecraft;
|
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 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
|
@Unique
|
||||||
private static void surviepote$removeWidget(TitleScreen self, GuiEventListener widget) {
|
private static void surviepote$removeWidget(TitleScreen self, GuiEventListener widget) {
|
||||||
self.children().remove(widget);
|
self.children().remove(widget);
|
||||||
@@ -40,12 +51,16 @@ public abstract class TitleScreenMixin {
|
|||||||
String solo = Component.translatable("menu.singleplayer").getString();
|
String solo = Component.translatable("menu.singleplayer").getString();
|
||||||
String multi = Component.translatable("menu.multiplayer").getString();
|
String multi = Component.translatable("menu.multiplayer").getString();
|
||||||
String realms = Component.translatable("menu.online").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 joinX = minecraft.getWindow().getGuiScaledWidth() / 2 - 100;
|
||||||
int joinY = minecraft.getWindow().getGuiScaledHeight() / 4 + 72;
|
int joinY = minecraft.getWindow().getGuiScaledHeight() / 4 + 72;
|
||||||
boolean foundMulti = false;
|
boolean foundMulti = false;
|
||||||
|
|
||||||
List<GuiEventListener> toRemove = new ArrayList<>();
|
List<GuiEventListener> toRemove = new ArrayList<>();
|
||||||
|
Button optionsButton = null;
|
||||||
|
Button quitButton = null;
|
||||||
for (GuiEventListener child : new ArrayList<>(self.children())) {
|
for (GuiEventListener child : new ArrayList<>(self.children())) {
|
||||||
// ATTENTION : FriendsButton/SpriteIconButton/PlainTextButton héritent tous
|
// ATTENTION : FriendsButton/SpriteIconButton/PlainTextButton héritent tous
|
||||||
// de Button. Il faut donc les tester AVANT "instanceof Button", sinon la
|
// de Button. Il faut donc les tester AVANT "instanceof Button", sinon la
|
||||||
@@ -71,6 +86,14 @@ public abstract class TitleScreenMixin {
|
|||||||
toRemove.add(button);
|
toRemove.add(button);
|
||||||
} else if (msg.equals(realms)) {
|
} else if (msg.equals(realms)) {
|
||||||
toRemove.add(button);
|
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" })
|
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||||
List rawChildren = (List) self.children();
|
List rawChildren = (List) self.children();
|
||||||
rawChildren.add(join);
|
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) {
|
if (join instanceof AbstractWidget w) {
|
||||||
w.visible = true;
|
w.visible = true;
|
||||||
w.active = true;
|
w.active = true;
|
||||||
}
|
}
|
||||||
|
if (discord instanceof AbstractWidget w) {
|
||||||
|
w.visible = true;
|
||||||
|
w.active = true;
|
||||||
|
}
|
||||||
if (!foundMulti) {
|
if (!foundMulti) {
|
||||||
NoSingleplayer.LOGGER.warn("[SurviePote] Bouton Multi introuvable, Join placé par défaut.");
|
NoSingleplayer.LOGGER.warn("[SurviePote] Bouton Multi introuvable, Join placé par défaut.");
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-32
@@ -1,9 +1,7 @@
|
|||||||
package fr.surviepote.nosingleplayer.mixin;
|
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.realmsclient.gui.screens.RealmsNotificationsScreen;
|
||||||
import com.mojang.renderpearl.api.textures.FilterMode;
|
import fr.surviepote.nosingleplayer.ModTextures;
|
||||||
import fr.surviepote.nosingleplayer.NoSingleplayer;
|
import fr.surviepote.nosingleplayer.NoSingleplayer;
|
||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
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.At;
|
||||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
@Mixin(TitleScreen.class)
|
@Mixin(TitleScreen.class)
|
||||||
public abstract class TitleScreenRenderMixin {
|
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
|
* 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
|
* 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
|
* 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
|
* magenta). Le PNG est donc lu dans le jar du mod (voir ModTextures).
|
||||||
* DynamicTexture dans le TextureManager.
|
|
||||||
*/
|
*/
|
||||||
@Unique
|
@Unique
|
||||||
private static void surviepote$loadBackground() {
|
private static void surviepote$loadBackground() {
|
||||||
var textures = Minecraft.getInstance().getTextureManager();
|
DynamicTexture texture = ModTextures.get(SURVIEPOTE_BACKGROUND, SURVIEPOTE_BACKGROUND_RESOURCE,
|
||||||
if (surviepote$background != null
|
"SurviePote Background", true);
|
||||||
&& textures.getTexture(SURVIEPOTE_BACKGROUND) == surviepote$background) {
|
if (texture == null) {
|
||||||
return; // déjà chargé et toujours enregistré
|
surviepote$background = null;
|
||||||
}
|
|
||||||
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;
|
return;
|
||||||
}
|
}
|
||||||
NativeImage image = NativeImage.read(in);
|
if (surviepote$background != texture) {
|
||||||
surviepote$backgroundWidth = image.getWidth();
|
surviepote$background = texture;
|
||||||
surviepote$backgroundHeight = image.getHeight();
|
surviepote$backgroundWidth = texture.getPixels().getWidth();
|
||||||
surviepote$background = new LinearDynamicTexture(() -> "SurviePote Background", image);
|
surviepote$backgroundHeight = texture.getPixels().getHeight();
|
||||||
textures.register(SURVIEPOTE_BACKGROUND, surviepote$background);
|
|
||||||
NoSingleplayer.LOGGER.info("[SurviePote] Fond chargé : {}x{}",
|
NoSingleplayer.LOGGER.info("[SurviePote] Fond chargé : {}x{}",
|
||||||
surviepote$backgroundWidth, surviepote$backgroundHeight);
|
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. */
|
// Pas de texte de version ("26.3 mc") en bas à gauche.
|
||||||
@Unique
|
@Redirect(method = "extractRenderState",
|
||||||
private static final class LinearDynamicTexture extends DynamicTexture {
|
at = @At(value = "INVOKE",
|
||||||
LinearDynamicTexture(Supplier<String> name, NativeImage image) {
|
target = "Lnet/minecraft/client/gui/GuiGraphicsExtractor;text(Lnet/minecraft/client/gui/Font;Ljava/lang/String;III)V"))
|
||||||
super(name, image);
|
private void surviepote$skipVersionText(GuiGraphicsExtractor extractor, Font font, String text, int x, int y, int color) {
|
||||||
this.sampler = RenderSystem.getSamplerCache().getClampToEdge(FilterMode.LINEAR);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pas de texte de splash (message rotatif) sur le menu principal.
|
// Pas de texte de splash (message rotatif) sur le menu principal.
|
||||||
|
|||||||
-26
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 904 B |
Binary file not shown.
|
After Width: | Height: | Size: 645 B |
Binary file not shown.
|
After Width: | Height: | Size: 704 B |
@@ -5,8 +5,8 @@
|
|||||||
"client": [
|
"client": [
|
||||||
"TitleScreenMixin",
|
"TitleScreenMixin",
|
||||||
"TitleScreenRenderMixin",
|
"TitleScreenRenderMixin",
|
||||||
"TitleScreenTextMixin",
|
|
||||||
"MinecraftTitleMixin",
|
"MinecraftTitleMixin",
|
||||||
|
"ButtonAccessor",
|
||||||
"RealmsNotificationsMixin",
|
"RealmsNotificationsMixin",
|
||||||
"PauseScreenMixin",
|
"PauseScreenMixin",
|
||||||
"ScreenAccessor"
|
"ScreenAccessor"
|
||||||
|
|||||||
Reference in New Issue
Block a user