From d80798307b3ebc1233a3c086bbe9db50ec1cb159 Mon Sep 17 00:00:00 2001 From: VixidDev <52578495+VixidDev@users.noreply.github.com> Date: Wed, 3 Jul 2024 00:13:06 +0100 Subject: [PATCH] Use JNA to query window title instead --- .../dev/vixid/vsm/mixin/MixinKeyboard.java | 2 +- src/main/kotlin/dev/vixid/vsm/VSM.kt | 9 +- .../{ => features}/spotify/ControlUtils.kt | 2 +- .../{ => features}/spotify/SpotifyDisplay.kt | 40 ++---- .../kotlin/dev/vixid/vsm/utils/JNAHelper.kt | 120 ++++++++++++++++++ 5 files changed, 136 insertions(+), 37 deletions(-) rename src/main/kotlin/dev/vixid/vsm/{ => features}/spotify/ControlUtils.kt (97%) rename src/main/kotlin/dev/vixid/vsm/{ => features}/spotify/SpotifyDisplay.kt (67%) create mode 100644 src/main/kotlin/dev/vixid/vsm/utils/JNAHelper.kt diff --git a/src/main/java/dev/vixid/vsm/mixin/MixinKeyboard.java b/src/main/java/dev/vixid/vsm/mixin/MixinKeyboard.java index acbe3c4..cb6a5bb 100644 --- a/src/main/java/dev/vixid/vsm/mixin/MixinKeyboard.java +++ b/src/main/java/dev/vixid/vsm/mixin/MixinKeyboard.java @@ -1,7 +1,7 @@ package dev.vixid.vsm.mixin; import dev.vixid.vsm.events.KeyPress; -import dev.vixid.vsm.spotify.ControlUtils; +import dev.vixid.vsm.features.spotify.ControlUtils; import net.minecraft.client.Keyboard; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; diff --git a/src/main/kotlin/dev/vixid/vsm/VSM.kt b/src/main/kotlin/dev/vixid/vsm/VSM.kt index 2d1477c..f44498d 100644 --- a/src/main/kotlin/dev/vixid/vsm/VSM.kt +++ b/src/main/kotlin/dev/vixid/vsm/VSM.kt @@ -6,13 +6,9 @@ import dev.vixid.vsm.config.core.VSMGsonMapper import dev.vixid.vsm.config.core.annotations.ConfigEditorKeybind import dev.vixid.vsm.config.core.gui.GuiOptionEditorKeybind import dev.vixid.vsm.overlays.OverlayPositions -import dev.vixid.vsm.spotify.SpotifyDisplay +import dev.vixid.vsm.features.spotify.SpotifyDisplay import io.github.notenoughupdates.moulconfig.managed.ManagedConfig import java.io.File -import kotlinx.coroutines.CoroutineName -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob import net.fabricmc.api.ClientModInitializer import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -31,9 +27,6 @@ object VSM : ClientModInitializer { } } - private val globalJob = Job() - val coroutineScope = CoroutineScope(CoroutineName("VSM") + SupervisorJob(globalJob)) - override fun onInitializeClient() { GlobalScreen.registerNativeHook() diff --git a/src/main/kotlin/dev/vixid/vsm/spotify/ControlUtils.kt b/src/main/kotlin/dev/vixid/vsm/features/spotify/ControlUtils.kt similarity index 97% rename from src/main/kotlin/dev/vixid/vsm/spotify/ControlUtils.kt rename to src/main/kotlin/dev/vixid/vsm/features/spotify/ControlUtils.kt index e61d5a0..54248d0 100644 --- a/src/main/kotlin/dev/vixid/vsm/spotify/ControlUtils.kt +++ b/src/main/kotlin/dev/vixid/vsm/features/spotify/ControlUtils.kt @@ -1,4 +1,4 @@ -package dev.vixid.vsm.spotify +package dev.vixid.vsm.features.spotify import com.github.kwhat.jnativehook.GlobalScreen import com.github.kwhat.jnativehook.keyboard.NativeKeyEvent diff --git a/src/main/kotlin/dev/vixid/vsm/spotify/SpotifyDisplay.kt b/src/main/kotlin/dev/vixid/vsm/features/spotify/SpotifyDisplay.kt similarity index 67% rename from src/main/kotlin/dev/vixid/vsm/spotify/SpotifyDisplay.kt rename to src/main/kotlin/dev/vixid/vsm/features/spotify/SpotifyDisplay.kt index 460cedc..8e6c983 100644 --- a/src/main/kotlin/dev/vixid/vsm/spotify/SpotifyDisplay.kt +++ b/src/main/kotlin/dev/vixid/vsm/features/spotify/SpotifyDisplay.kt @@ -1,4 +1,4 @@ -package dev.vixid.vsm.spotify +package dev.vixid.vsm.features.spotify import dev.vixid.vsm.VSM import dev.vixid.vsm.config.SpotifyConfig @@ -7,10 +7,8 @@ import dev.vixid.vsm.events.MousePress import dev.vixid.vsm.overlays.Overlay import dev.vixid.vsm.overlays.OverlayPositions import dev.vixid.vsm.utils.ChatUtils +import dev.vixid.vsm.utils.JNAHelper import dev.vixid.vsm.utils.RenderUtils.drawTextWithShadow -import java.io.BufferedReader -import java.io.InputStreamReader -import kotlinx.coroutines.launch import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents import net.minecraft.client.MinecraftClient import net.minecraft.client.gui.DrawContext @@ -42,9 +40,18 @@ object SpotifyDisplay : Overlay() { totalTicks++ if (totalTicks % 20 == 0) { - VSM.coroutineScope.launch { - songName = getSongFromSpotifyProcess() + var windowTitle = JNAHelper.getProcessWindowTitle("Spotify.exe") + + if (windowTitle == "Spotify") { + windowTitle = songName + } else if (windowTitle.isNotEmpty()) { + windowTitle = "§a${windowTitle}".replace(" - ", " §f-§b ") + + if (songName != windowTitle) { + ChatUtils.chat("§bVSM §f> $windowTitle") + } } + songName = windowTitle } } @@ -67,26 +74,5 @@ object SpotifyDisplay : Overlay() { } } - private fun getSongFromSpotifyProcess() : String { - return try { - val process = Runtime.getRuntime().exec("powershell.exe (ps Spotify | ? MainWindowTitle | select MainWindowTitle).MainWindowTitle") - val reader = BufferedReader(InputStreamReader(process.inputStream)) - var line = reader.readLine() - reader.close() - if (line == "Spotify") { - line = songName - } else if (line != null) { - line = "§a${line}".replace(" - ", " §f-§b ") - if (!line.equals(songName)) { - ChatUtils.chat("§bVSM §f> $line") - } - } - line ?: "§cCannot detect song name!" - } catch (error: Exception) { - error.printStackTrace() - "§cCannot detect song name!" - } - } - private fun isEnabled(client: MinecraftClient) = client.world != null && config.enabled } \ No newline at end of file diff --git a/src/main/kotlin/dev/vixid/vsm/utils/JNAHelper.kt b/src/main/kotlin/dev/vixid/vsm/utils/JNAHelper.kt new file mode 100644 index 0000000..a922b39 --- /dev/null +++ b/src/main/kotlin/dev/vixid/vsm/utils/JNAHelper.kt @@ -0,0 +1,120 @@ +@file:Suppress("FunctionName", "LocalVariableName") + +package dev.vixid.vsm.utils + +import com.sun.jna.Native +import com.sun.jna.Platform +import com.sun.jna.Pointer +import com.sun.jna.platform.win32.WinDef +import com.sun.jna.platform.win32.WinNT +import com.sun.jna.platform.win32.WinUser +import com.sun.jna.ptr.IntByReference +import com.sun.jna.win32.StdCallLibrary +import com.sun.jna.win32.W32APIOptions + +object JNAHelper { + + private val INSTANCE: NativeJNAHelper by lazy { + if (Platform.isWindows()) { + WindowsJNAHelper + } else if (Platform.isX11()) { + X11JNAHelper + } else if (Platform.isMac()) { + MacJNAHelper + } else { + val os = System.getProperty("os.name") + throw UnsupportedOperationException("No support for $os") + } + } + + private object WindowsJNAHelper : NativeJNAHelper() { + interface User32 : StdCallLibrary { + companion object { + val INSTANCE = Native.load("user32", User32::class.java, W32APIOptions.DEFAULT_OPTIONS) as User32 + } + + fun EnumWindows(lpEnumFunc: WinUser.WNDENUMPROC?, arg: Pointer?): Boolean + fun GetWindowThreadProcessId(hWnd: WinDef.HWND?, lpdwProcessId: IntByReference?): Int + fun GetWindowTextW(hwnd: WinDef.HWND?, lpString: CharArray?, nMaxCount: Int): Int + } + + interface Kernel32 : StdCallLibrary { + companion object { + val INSTANCE = Native.load("Kernel32", Kernel32::class.java, W32APIOptions.DEFAULT_OPTIONS) as Kernel32 + } + + fun OpenProcess(fdwAccess: Int, fInherit: Boolean, IDProcess: Int): WinNT.HANDLE? + } + + interface Psapi : StdCallLibrary { + companion object { + val INSTANCE = Native.load("Psapi", Psapi::class.java, W32APIOptions.DEFAULT_OPTIONS) as Psapi + } + + fun GetModuleBaseNameW(hProcess: Pointer, hModule: Pointer?, lpBaseName: CharArray, nSize: Int): WinDef.DWORD + } + + private val ignoredStrings = arrayOf("Default IME", "MSCTFIME UI", "GDI+ Window") + + override fun getProcessWindowTitle(process: String): String { + val PROCESS_VM_READ = 0x0010 + val PROCESS_QUERY_INFORMATION = 0x0400 + + val user32 = User32.INSTANCE + val kernel32 = Kernel32.INSTANCE + val psapi = Psapi.INSTANCE + + var title = "" + + user32.EnumWindows({ hwnd: WinDef.HWND, _: Pointer? -> + val pid = IntByReference() + user32.GetWindowThreadProcessId(hwnd, pid) + val processHandle = kernel32.OpenProcess(PROCESS_VM_READ or PROCESS_QUERY_INFORMATION, false, pid.value) + + if (processHandle != null) { + val moduleNameBuf = CharArray(512) + psapi.GetModuleBaseNameW(processHandle.pointer, Pointer.NULL, moduleNameBuf, 512) + val moduleName = Native.toString(moduleNameBuf) + + if (moduleName.equals(process)) { + val titleBuf = CharArray(512) + user32.GetWindowTextW(hwnd, titleBuf, 512) + val windowTitle = Native.toString(titleBuf) + + if (windowTitle.isNotEmpty() && !ignore(windowTitle)) { + title = windowTitle + } + } + } + true + }, null) + + return title + } + + private fun ignore(title: String): Boolean { + for (string in ignoredStrings) { + if (title.contains(string)) return true + } + return false + } + } + + private object X11JNAHelper : NativeJNAHelper() { + override fun getProcessWindowTitle(process: String): String { + throw UnsupportedOperationException("This function has not been implemented for this platform yet!") + } + } + + private object MacJNAHelper : NativeJNAHelper() { + override fun getProcessWindowTitle(process: String): String { + throw UnsupportedOperationException("This function has not been implemented for this platform yet!") + } + } + + private abstract class NativeJNAHelper { + abstract fun getProcessWindowTitle(process: String): String + } + + fun getProcessWindowTitle(process: String): String = INSTANCE.getProcessWindowTitle(process) +} \ No newline at end of file