diff --git a/atlas/root/distribution.json b/atlas/root/distribution.json index 09bb1bf..58d846c 100644 --- a/atlas/root/distribution.json +++ b/atlas/root/distribution.json @@ -487,13 +487,13 @@ } }, { - "id": "fr.surviepote:nosingleplayer:1.1.8@jar", + "id": "fr.surviepote:nosingleplayer:1.1.9@jar", "name": "NoSingleplayer (SurviePote)", "type": "FabricMod", "artifact": { - "size": 3637392, - "url": "https://git.kisakay.com/k/mc/raw/branch/main/atlas/root/servers/SurviePote-26.3/fabricmods/required/nosingleplayer-1.1.8.jar", - "MD5": "dc69cd7ebc5d10ba4797c4e212e47e0c" + "size": 3639116, + "url": "https://git.kisakay.com/k/mc/raw/branch/main/atlas/root/servers/SurviePote-26.3/fabricmods/required/nosingleplayer-1.1.9.jar", + "MD5": "d9666a2698e9bcb3d69e03097dc081b4" } }, { diff --git a/atlas/root/servers/SurviePote-26.3/fabricmods/required/nosingleplayer-1.1.8.jar b/atlas/root/servers/SurviePote-26.3/fabricmods/required/nosingleplayer-1.1.9.jar similarity index 99% rename from atlas/root/servers/SurviePote-26.3/fabricmods/required/nosingleplayer-1.1.8.jar rename to atlas/root/servers/SurviePote-26.3/fabricmods/required/nosingleplayer-1.1.9.jar index c0b90fd..6c47547 100644 Binary files a/atlas/root/servers/SurviePote-26.3/fabricmods/required/nosingleplayer-1.1.8.jar and b/atlas/root/servers/SurviePote-26.3/fabricmods/required/nosingleplayer-1.1.9.jar differ diff --git a/launcher/app/assets/js/processbuilder.js b/launcher/app/assets/js/processbuilder.js index 7795c2f..5adf0fb 100644 --- a/launcher/app/assets/js/processbuilder.js +++ b/launcher/app/assets/js/processbuilder.js @@ -185,6 +185,10 @@ class ProcessBuilder { const child = child_process.spawn(ConfigManager.getJavaExecutable(this.server.rawServer.id), args, { cwd: this.gameDir, detached: ConfigManager.getLaunchDetached(), + // Windows : sans ça, java.exe ouvre sa propre console (detached => nouvelle + // console) : une fenêtre noire, vide (stdout est pipe vers le launcher), + // qui traîne par-dessus la fenêtre du jeu pendant toute la session. + windowsHide: true, env: gameEnv }) diff --git a/mods/nosingleplayer/README.md b/mods/nosingleplayer/README.md index 6fafdb6..3349a67 100644 --- a/mods/nosingleplayer/README.md +++ b/mods/nosingleplayer/README.md @@ -13,4 +13,14 @@ au serveur unique : pas de refmap nécessaire. Build : `cd mods/nosingleplayer && ./gradlew build` (dans nix-shell racine pour Java 25). -Jar : `build/libs/nosingleplayer-1.1.4.jar` → copié dans `atlas/.../fabricmods/required/`. +Jar : `build/libs/nosingleplayer-1.1.9.jar` → copié dans `atlas/.../fabricmods/required/`. + +## Diagnostic + +- `-Dsurviepote.customTitle=false` (options JVM du launcher) : repasser au menu **vanilla** + (panorama vanilla au lieu du fond custom). Sert à isoler un souci de rendu du fond custom. +- Le mod log au démarrage du menu : `[SurviePote] GPU: | | pilote | backend ` + et `[SurviePote] Fond chargé : x` (ou l'erreur de chargement de la texture). +- Si le fond custom échoue (texture refusée par le GPU, jar corrompu…), le mod se repli + automatiquement sur le panorama vanilla au lieu de laisser un écran noir. + diff --git a/mods/nosingleplayer/gradle.properties b/mods/nosingleplayer/gradle.properties index 3b623e8..4a289e5 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.8 +mod_version=1.1.9 maven_group=fr.surviepote archives_base_name=nosingleplayer 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 index 056d277..0cfff86 100644 --- a/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/gui/ServerStatusWidget.java +++ b/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/gui/ServerStatusWidget.java @@ -26,6 +26,14 @@ public class ServerStatusWidget extends AbstractWidget { private static final long REFRESH_MS = 15000L; private static final long TIMEOUT_MS = 8000L; + /** Thread dédié : le ping ne doit jamais bloquer le thread de rendu. */ + private static final java.util.concurrent.ExecutorService PING_EXECUTOR = + java.util.concurrent.Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "SurviePote-ServerStatus"); + t.setDaemon(true); + return t; + }); + private static final int BG_COLOR = 0xB0000000; private static final int ONLINE_COLOR = 0xFF4CD964; private static final int OFFLINE_COLOR = 0xFFE04B4B; @@ -106,12 +114,25 @@ public class ServerStatusWidget extends AbstractWidget { } private void refresh() { + // ATTENTION : pingServer() fait une résolution DNS + ouvre une connexion. + // Appelé depuis extractWidgetRenderState (thread de rendu), ça BLOQUE le + // rendu du menu (écran figé/noir) le temps du timeout DNS. On le lance + // donc sur un thread dédié ; l'état lu par le rendu est "volatile". try { this.pingStartedAt = System.currentTimeMillis(); this.pending = true; - this.pinger.pingServer(this.data, () -> { - }, () -> { - }, EventLoopGroupHolder.remote(Minecraft.getInstance().options.useNativeTransport())); + PING_EXECUTOR.execute(() -> { + try { + 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; + } + }); } catch (Exception e) { NoSingleplayer.LOGGER.warn("[SurviePote] Ping serveur impossible : {}", e.toString()); this.pending = false; diff --git a/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/mixin/ScreenAccessor.java b/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/mixin/ScreenAccessor.java index 50cf1bb..f7a790d 100644 --- a/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/mixin/ScreenAccessor.java +++ b/mods/nosingleplayer/src/main/java/fr/surviepote/nosingleplayer/mixin/ScreenAccessor.java @@ -1,14 +1,24 @@ package fr.surviepote.nosingleplayer.mixin; import java.util.List; +import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.components.Renderable; import net.minecraft.client.gui.screens.Screen; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; @Mixin(Screen.class) public interface ScreenAccessor { @Accessor("renderables") List surviepote$getRenderables(); + + /** + * Panorama vanilla (déclaré dans Screen, appelé par TitleScreen). + * Utilisé en repli quand le fond custom de SurviePote n'a pas pu être chargé : + * sans ça, la redirection de l'appel laisse l'écran entièrement noir. + */ + @Invoker("extractPanorama") + void surviepote$extractVanillaPanorama(GuiGraphicsExtractor extractor, float partialTick); } 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 d6c81cf..4db3088 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 @@ -45,7 +45,18 @@ public abstract class TitleScreenMixin { @Inject(method = "init", at = @At("TAIL")) private void surviepote$customizeMenu(CallbackInfo ci) { - TitleScreen self = (TitleScreen) (Object) this; + // Garde-fou : si la personnalisation échoue, on NE DOIT PAS avoir retiré les + // boutons vanilla (menu vide + fond noir = "écran cassé"). Les retraits ne + // sont appliqués qu'après la création des widgets, en fin de méthode. + try { + surviepote$buildMenu((TitleScreen) (Object) this); + } catch (Throwable t) { + NoSingleplayer.LOGGER.error("[SurviePote] Personnalisation du menu échouée, menu vanilla conservé.", t); + } + } + + @Unique + private void surviepote$buildMenu(TitleScreen self) { Minecraft minecraft = Minecraft.getInstance(); String solo = Component.translatable("menu.singleplayer").getString(); @@ -97,9 +108,9 @@ public abstract class TitleScreenMixin { } } } - for (GuiEventListener widget : toRemove) { - surviepote$removeWidget(self, widget); - } + // ATTENTION : les retraits des widgets vanilla ne sont appliqués qu'APRÈS la + // création des widgets SurviePote (fin de méthode). Si une exception survient + // entre-temps, le menu vanilla est conservé au lieu d'être vidé. Button join = Button.builder(Component.literal("Rejoindre SurviePote"), pressed -> { ServerData data = new ServerData("SurviePote", SERVER_HOST, ServerData.Type.OTHER); @@ -158,6 +169,13 @@ public abstract class TitleScreenMixin { w.visible = true; w.active = true; } + + // Tout est prêt : on retire enfin les widgets vanilla (Solo, Multi, Realms, + // réseau social, icônes bas) remplacés ci-dessus. + for (GuiEventListener widget : toRemove) { + surviepote$removeWidget(self, widget); + } + 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 bb1250a..67591ce 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,5 +1,6 @@ package fr.surviepote.nosingleplayer.mixin; +import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.realmsclient.gui.screens.RealmsNotificationsScreen; import fr.surviepote.nosingleplayer.ModTextures; import fr.surviepote.nosingleplayer.NoSingleplayer; @@ -29,6 +30,15 @@ public abstract class TitleScreenRenderMixin { private static final String SURVIEPOTE_BACKGROUND_RESOURCE = "/assets/nosingleplayer/textures/gui/background.png"; + /** + * Option de diagnostic : lancer le jeu avec + * {@code -Dsurviepote.customTitle=false} pour repasser au menu vanilla + * (utilisable dans les options JVM du launcher). Sert à isoler un éventuel + * souci de rendu du fond custom sur un poste donné. + */ + @Unique + private static final String DISABLE_CUSTOM_TITLE_PROPERTY = "surviepote.customTitle"; + @Unique private static DynamicTexture surviepote$background; @@ -38,6 +48,13 @@ public abstract class TitleScreenRenderMixin { @Unique private static int surviepote$backgroundHeight; + // Le fond custom a déjà échoué : on ne réessaie plus (le menu reste noir sinon). + @Unique + private static boolean surviepote$backgroundFailed; + + @Unique + private static boolean surviepote$gpuLogged; + // Pas de panorama dynamique : fond statique custom à la place. // Note : extractPanorama est déclarée dans Screen (héritée), on redirige donc // l'APPEL dans TitleScreen plutôt que la méthode elle-même. @@ -46,39 +63,90 @@ public abstract class TitleScreenRenderMixin { at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/TitleScreen;extractPanorama(Lnet/minecraft/client/gui/GuiGraphicsExtractor;F)V")) private void surviepote$customBackground(TitleScreen self, GuiGraphicsExtractor extractor, float partialTick) { - int w = Minecraft.getInstance().getWindow().getGuiScaledWidth(); - int h = Minecraft.getInstance().getWindow().getGuiScaledHeight(); + // Les widgets du menu sont dessinés par super.extractRenderState(), juste APRÈS + // cet appel. Toute exception levée ici interromp le rendu du menu entier => + // écran 100 % noir (sans boutons). D'où les garde-fous ci-dessous. + surviepote$logGpuOnce(); - surviepote$loadBackground(); - if (surviepote$background == null) { + // Menu vanilla si le fond custom a déjà échoué, ou si l'utilisateur a forcé + // -Dsurviepote.customTitle=false (option de diagnostic). + final boolean customEnabled = !"false".equalsIgnoreCase( + System.getProperty(DISABLE_CUSTOM_TITLE_PROPERTY, "true")); + if (surviepote$backgroundFailed || !customEnabled) { + surviepote$vanillaPanorama(self, extractor, partialTick); return; } - float screenAspect = (float) w / (float) h; - float texAspect = (float) surviepote$backgroundWidth / (float) surviepote$backgroundHeight; + try { + int w = Minecraft.getInstance().getWindow().getGuiScaledWidth(); + int h = Minecraft.getInstance().getWindow().getGuiScaledHeight(); - int u, v, srcW, srcH; - if (screenAspect > texAspect) { - // Écran plus large que l'image : on rogne en haut/bas. - srcW = surviepote$backgroundWidth; - srcH = Math.round(surviepote$backgroundWidth / screenAspect); - u = 0; - v = (surviepote$backgroundHeight - srcH) / 2; - } else { - // Écran plus haut que l'image : on rogne à gauche/droite. - srcH = surviepote$backgroundHeight; - srcW = Math.round(surviepote$backgroundHeight * screenAspect); - u = (surviepote$backgroundWidth - srcW) / 2; - v = 0; + surviepote$loadBackground(); + if (surviepote$background == null) { + surviepote$vanillaPanorama(self, extractor, partialTick); + return; + } + + float screenAspect = (float) w / (float) h; + float texAspect = (float) surviepote$backgroundWidth / (float) surviepote$backgroundHeight; + + int u, v, srcW, srcH; + if (screenAspect > texAspect) { + // Écran plus large que l'image : on rogne en haut/bas. + srcW = surviepote$backgroundWidth; + srcH = Math.round(surviepote$backgroundWidth / screenAspect); + u = 0; + v = (surviepote$backgroundHeight - srcH) / 2; + } else { + // Écran plus haut que l'image : on rogne à gauche/droite. + srcH = surviepote$backgroundHeight; + srcW = Math.round(surviepote$backgroundHeight * screenAspect); + u = (surviepote$backgroundWidth - srcW) / 2; + v = 0; + } + + // Surcharge complète : (u, v) = origine dans la texture, (w, h) = taille à + // l'écran, (srcW, srcH) = taille de la région source dans la texture. + // L'overload court réutilise w/h comme région source (=> zoom sur un coin). + extractor.blit(RenderPipelines.GUI_TEXTURED, SURVIEPOTE_BACKGROUND, + 0, 0, (float) u, (float) v, + w, h, srcW, srcH, + surviepote$backgroundWidth, surviepote$backgroundHeight, -1); + } catch (Throwable t) { + // Repli : on bascule sur le panorama vanilla pour ne pas laisser un écran noir. + surviepote$backgroundFailed = true; + NoSingleplayer.LOGGER.error("[SurviePote] Échec du fond custom, repli sur le panorama vanilla.", t); + surviepote$vanillaPanorama(self, extractor, partialTick); } + } - // Surcharge complète : (u, v) = origine dans la texture, (w, h) = taille à - // l'écran, (srcW, srcH) = taille de la région source dans la texture. - // L'overload court réutilise w/h comme région source (=> zoom sur un coin). - extractor.blit(RenderPipelines.GUI_TEXTURED, SURVIEPOTE_BACKGROUND, - 0, 0, (float) u, (float) v, - w, h, srcW, srcH, - surviepote$backgroundWidth, surviepote$backgroundHeight, -1); + /** + * Redessine le panorama vanilla (appelé dans Screen, pas dans TitleScreen : + * la redirection porte sur l'appel, donc pas de récursion). + */ + @Unique + private void surviepote$vanillaPanorama(TitleScreen self, GuiGraphicsExtractor extractor, float partialTick) { + try { + ((ScreenAccessor) self).surviepote$extractVanillaPanorama(extractor, partialTick); + } catch (Throwable t) { + NoSingleplayer.LOGGER.error("[SurviePote] Échec du panorama vanilla.", t); + } + } + + /** Log une fois les infos GPU : permet de comparer un poste qui plante d'un poste qui marche. */ + @Unique + private static void surviepote$logGpuOnce() { + if (surviepote$gpuLogged) { + return; + } + surviepote$gpuLogged = true; + try { + var info = RenderSystem.getDevice().getDeviceInfo(); + NoSingleplayer.LOGGER.info("[SurviePote] GPU: {} | {} | pilote {} | backend {}", + info.name(), info.vendorName(), info.driverInfo(), info.backendName()); + } catch (Throwable t) { + NoSingleplayer.LOGGER.warn("[SurviePote] Infos GPU indisponibles : {}", t.toString()); + } } /**