Skip to content

Commit

Permalink
Wayland display window and icon injection
Browse files Browse the repository at this point in the history
  • Loading branch information
wired-tomato committed Jul 22, 2024
1 parent 4665bb7 commit 786ce20
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,22 @@ private void crashElegantly(String errorDetails) {
System.exit(1);
}

protected void glfwWindowHints(String mcVersion) {
glfwDefaultWindowHints();
glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API);
glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_NATIVE_CONTEXT_API);
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);
if (mcVersion != null) {
// this emulates what we would get without early progress window
// as vanilla never sets these, so GLFW uses the first window title
// set them explicitly to avoid it using "FML early loading progress" as the class
String vanillaWindowTitle = "Minecraft* " + mcVersion;
glfwWindowHintString(GLFW_X11_CLASS_NAME, vanillaWindowTitle);
glfwWindowHintString(GLFW_X11_INSTANCE_NAME, vanillaWindowTitle);
}
}

/**
* Called to initialize the window when preparing for the Render Thread.
*
Expand Down Expand Up @@ -360,19 +376,7 @@ public void initWindow(@Nullable String mcVersion) {
handleLastGLFWError((error, description) -> LOGGER.error(String.format("Suppressing Last GLFW error: [0x%X]%s", error, description)));

// Set window hints for the new window we're gonna create.
glfwDefaultWindowHints();
glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API);
glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_NATIVE_CONTEXT_API);
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);
if (mcVersion != null) {
// this emulates what we would get without early progress window
// as vanilla never sets these, so GLFW uses the first window title
// set them explicitly to avoid it using "FML early loading progress" as the class
String vanillaWindowTitle = "Minecraft* " + mcVersion;
glfwWindowHintString(GLFW_X11_CLASS_NAME, vanillaWindowTitle);
glfwWindowHintString(GLFW_X11_INSTANCE_NAME, vanillaWindowTitle);
}
glfwWindowHints(mcVersion);

long primaryMonitor = glfwGetPrimaryMonitor();
if (primaryMonitor == 0) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/

package net.neoforged.fml.earlydisplay.wayland;

import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.system.MemoryUtil.memAllocInt;

import net.neoforged.fml.earlydisplay.DisplayWindow;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* The Loading Window that is opened Immediately after Forge starts.
* It is called from the ModDirTransformerDiscoverer, the soonest method that ModLauncher calls into Forge code.
* In this way, we can be sure that this will not run before any transformer or injection.
*
* The window itself is spun off into a secondary thread, and is handed off to the main game by Forge.
*
* Because it is created so early, this thread will "absorb" the context from OpenGL.
* Therefore, it is of utmost importance that the Context is made Current for the main thread before handoff,
* otherwise OS X will crash out.
*
* Based on the prior ClientVisualization, with some personal touches.
*/
public class WaylandDisplayWindow extends DisplayWindow {
private static final Logger LOGGER = LoggerFactory.getLogger("WAYLANDEARLYDISPLAY");
private static final int GLFW_WAYLAND_APP_ID = 0x26001;

@Override
public String name() {
return "fmlwaylandearlywindow";
}

@Override
public void initWindow(@Nullable String mcVersion) {
if (glfwPlatformSupported(GLFW_PLATFORM_WAYLAND)) {
glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_WAYLAND);
}

super.initWindow(mcVersion);
}

@Override
protected void glfwWindowHints(String mcVersion) {
glfwDefaultWindowHints();
glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API);
glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_NATIVE_CONTEXT_API);
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);

glfwWindowHint(GLFW_FOCUS_ON_SHOW, GLFW_FALSE);
var major = memAllocInt(1);
var minor = memAllocInt(1);
var rev = memAllocInt(1);
glfwGetVersion(major, minor, rev);
if (major.get(0) >= 3 && minor.get(0) >= 4) {
WaylandIconProvider.injectIcon(mcVersion);
glfwWindowHintString(GLFW_WAYLAND_APP_ID, WaylandIconProvider.APP_ID);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Copyright (c) NeoForged and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/

package net.neoforged.fml.earlydisplay.wayland;

import com.google.common.base.Charsets;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import javax.imageio.ImageIO;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class WaylandIconProvider {
private static final Logger LOGGER = LoggerFactory.getLogger("WAYLANDICONPROVIDER");
public static String APP_ID = "com.mojang.minecraft";
public static String ICON_NAME = "neoforged_inject_icon.png";
public static String DESKTOP_FILE_NAME = APP_ID + ".desktop";
private static final List<Path> injects = new ArrayList<>();

public static void injectIcon(@Nullable String mcVersion) {
Runtime.getRuntime().addShutdownHook(new Thread(WaylandIconProvider::uninjectIcon));

try (var iconRes = WaylandIconProvider.class.getResourceAsStream("/wayland/" + ICON_NAME);
var desktopRes = WaylandIconProvider.class.getResourceAsStream("/wayland/" + DESKTOP_FILE_NAME)) {
if (iconRes == null) throw new IOException("Failed to find icon resource");
if (desktopRes == null) throw new IOException("Failed to find desktop resource");

var iconByteStream = new ByteArrayOutputStream();
iconRes.transferTo(iconByteStream);
var image = ImageIO.read(new ByteArrayInputStream(iconByteStream.toByteArray()));
var iconPath = getIconFileLocation(image.getWidth(), image.getHeight());
if (iconPath == null) throw new IOException("Failed to resolve mc icon path");
injectFile(iconPath, iconByteStream.toByteArray());

var desktopString = String.format(new String(desktopRes.readAllBytes(), Charsets.UTF_8), mcVersion, ICON_NAME.substring(0, ICON_NAME.lastIndexOf(".")));
var desktopPath = getDesktopFileLocation();
if (desktopPath == null) throw new IOException("Failed to resolve desktop icon path");
injectFile(desktopPath, desktopString.getBytes(Charsets.UTF_8));

updateIconSystem();
} catch (IOException | NullPointerException e) {
LOGGER.error("Failed to inject icon", e);
}
}

public static void uninjectIcon() {
injects.forEach((path) -> {
try {
Files.deleteIfExists(path);
} catch (IOException e) {
LOGGER.error("Failed to delete file", e);
}
});
}

private static void injectFile(Path target, byte[] data) {
try {
Files.createDirectories(target.getParent());
Files.write(target, data);
injects.add(target);
} catch (IOException e) {
LOGGER.error("Failed to inject file", e);
}
}

private static void updateIconSystem() {
var procBuilder = new ProcessBuilder("xdg-icon-resource", "forceupdate");
try {
procBuilder.start();
} catch (IOException e) {
LOGGER.error("Failed to update icon system", e);
}
}

@Nullable
private static Path getIconFileLocation(int width, int height) {
var userDataLocation = getUserDataLocation();
if (userDataLocation == null) return null;

return getUserDataLocation()
.resolve("icons/hicolor")
.resolve(width + "x" + height)
.resolve("apps")
.resolve(ICON_NAME);
}

@Nullable
private static Path getDesktopFileLocation() {
var userDataLocation = getUserDataLocation();
if (userDataLocation == null) return null;

return getUserDataLocation()
.resolve("applications")
.resolve(DESKTOP_FILE_NAME);
}

@Nullable
private static Path getHome() {
var home = System.getenv().getOrDefault("HOME", System.getProperty("user.home"));
if (home == null || home.isEmpty()) {
LOGGER.error("Failed to resolve user home");
return null;
}

return Paths.get(home);
}

@Nullable
private static Path getUserDataLocation() {
var xdgDataHome = System.getenv("XDG_DATA_HOME");
if (xdgDataHome == null || xdgDataHome.isEmpty()) {
var home = getHome();
return home != null ? getHome().resolve(".local/share/") : null;
}
return Paths.get(xdgDataHome);
}
}
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
net.neoforged.fml.earlydisplay.DisplayWindow
net.neoforged.fml.earlydisplay.DisplayWindow
net.neoforged.fml.earlydisplay.wayland.WaylandDisplayWindow
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[Desktop Entry]
Name=Minecraft %s
Comment=Minecraft (NeoForge FML)
Icon=%s
Exec=true
Hidden=true
Terminal=false
Type=Application
Categories=Game;
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 786ce20

Please sign in to comment.