Skip to content

Commit 0ade4b8

Browse files
committed
Add GUI
1 parent 89c42d8 commit 0ade4b8

35 files changed

Lines changed: 1323 additions & 90 deletions

.github/workflows/client.yaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ jobs:
1313
distribution: "temurin"
1414
java-version: "23"
1515
- uses: gradle/actions/setup-gradle@v4
16-
- run: ./gradlew client:packageDist
16+
- run: ./gradlew client:packageReleaseDistributionForCurrentOS
17+
- run: mv client-*.msi client.msi
18+
working-directory: client/build/compose/binaries/main-release/msi/
1719
- name: Release
1820
uses: softprops/action-gh-release@v2
1921
if: ${{ github.ref == 'refs/heads/main' }}
2022
with:
2123
tag_name: ${{ github.run_number }}
22-
files: client/build/dist/*.tar.gz
24+
files: client/build/compose/binaries/main-release/msi/*.msi

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,6 @@ gradle-app.setting
116116
# Java heap dump
117117
*.hprof
118118

119-
# End of https://www.toptal.com/developers/gitignore/api/intellij+all,gradle
119+
# End of https://www.toptal.com/developers/gitignore/api/intellij+all,gradle
120+
121+
store/

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2021 Michael Rittmeister
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

client/build.gradle.kts

Lines changed: 45 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,68 @@
1-
import org.panteleyev.jpackage.ImageType
1+
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
22

33
plugins {
44
id("buildsrc.convention.kotlin-jvm")
5-
id("org.panteleyev.jpackageplugin") version "1.6.1"
5+
alias(libs.plugins.kotlin.compose)
6+
alias(libs.plugins.kotlin.serialization)
7+
alias(libs.plugins.compose)
68
}
79

810
version = "1.0.0"
911

12+
repositories {
13+
mavenCentral()
14+
google()
15+
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
16+
}
17+
1018
dependencies {
1119
implementation(projects.common)
1220
implementation(libs.kotlinx.serialization.json)
1321
implementation(libs.jnativehook)
14-
implementation(libs.ktor.client.cio)
22+
implementation(libs.ktor.serialization.kotlinx.json)
23+
implementation(libs.ktor.client.okhttp)
1524
implementation(libs.ktor.client.websockets)
1625
implementation(libs.ktor.client.resources)
26+
implementation(libs.ktor.client.content.negotiation)
1727
implementation(libs.ktor.serialization.kotlinx.json)
1828
implementation(libs.kotlin.logging)
19-
implementation(libs.slf4j.simple)
20-
}
21-
22-
tasks {
23-
val copyDependencies by registering(Copy::class) {
24-
from(configurations.runtimeClasspath).into(layout.buildDirectory.dir("jars"))
25-
}
29+
implementation(libs.logback.classic)
2630

27-
val copyJar by registering(Copy::class) {
28-
from(jar).into(layout.buildDirectory.dir("jars"))
29-
}
30-
31-
jpackage {
32-
dependsOn(build, copyDependencies, copyJar)
33-
winConsole = true
34-
35-
input = layout.buildDirectory.dir("jars").get().asFile.absolutePath
31+
implementation(compose.desktop.currentOs)
32+
implementation(compose.foundation)
33+
implementation(compose.runtime)
34+
implementation(compose.ui)
35+
implementation(compose.material3)
36+
implementation(compose.materialIconsExtended)
37+
implementation(libs.compose.navigation)
38+
implementation(libs.androidx.lifecycle.viewmodel.compose)
39+
}
3640

37-
appName = "GTA KILL"
38-
vendor = "Schlaubi"
39-
type = ImageType.APP_IMAGE
41+
compose.desktop {
42+
application {
43+
mainClass = "dev.schlaubi.mastermind.LauncherKt"
4044

41-
mainJar = jar.get().archiveFile.get().asFile.absolutePath
42-
mainClass = "dev.schlaubi.mastermind.MainKt"
45+
nativeDistributions {
46+
targetFormats(TargetFormat.Msi)
4347

44-
destination = layout.buildDirectory.dir("dist").get().asFile.absolutePath
45-
}
48+
licenseFile = rootProject.file("LICENSE")
49+
vendor = "Schlaubi"
50+
description = "GTA kill script"
51+
copyright = "(c) 2025 Michael Rittmeister"
4652

47-
register<Tar>("packageDist") {
48-
dependsOn(jpackage)
49-
from(jpackage.get().destination + "/" + jpackage.get().appName)
53+
windows {
54+
menuGroup = "GTA Killer"
55+
upgradeUuid = "8193b8f9-1355-4d0f-9c6f-6619d0f18604"
56+
}
57+
}
5058

51-
compression = Compression.GZIP
52-
archiveExtension = "tar.gz"
53-
destinationDirectory = layout.buildDirectory.dir("dist")
59+
buildTypes {
60+
release {
61+
proguard {
62+
version = libs.versions.proguard
63+
configurationFiles.from(project.file("rules.pro"))
64+
}
65+
}
66+
}
5467
}
5568
}

client/rules.pro

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Ktor
2+
-keepclassmembers class io.ktor.** { volatile <fields>; }
3+
-keep class io.ktor.client.engine.okhttp.OkHttpEngineContainer
4+
-keep class io.ktor.serialization.kotlinx.json.KotlinxSerializationJsonExtensionProvider
5+
6+
# SLF4j
7+
-keep class org.slf4j.simple.SimpleServiceProvider
8+
-dontwarn io.github.oshai.kotlinlogging.logback.**
9+
10+
# serialization
11+
# For some reason if we don't do this, we get a VerifyError at runtime
12+
# Serializer for classes with named companion objects are retrieved using `getDeclaredClasses`.
13+
# If you have any, replace classes with those containing named companion objects.
14+
-keepattributes InnerClasses # Needed for `getDeclaredClasses`.
15+
16+
# Kotlin serialization looks up the generated serializer classes through a function on companion
17+
# objects. The companions are looked up reflectively so we need to explicitly keep these functions.
18+
-keepclasseswithmembers class **.*$Companion {
19+
kotlinx.serialization.KSerializer serializer(...);
20+
}
21+
# If a companion has the serializer function, keep the companion field on the original type so that
22+
# the reflective lookup succeeds.
23+
-if class **.*$Companion {
24+
kotlinx.serialization.KSerializer serializer(...);
25+
}
26+
-keepclassmembers class <1>.<2> {
27+
<1>.<2>$Companion Companion;
28+
}
29+
30+
-dontwarn kotlinx.atomicfu.**
31+
-dontwarn io.netty.**
32+
-dontwarn com.typesafe.**
33+
-dontwarn org.slf4j.**
34+
-dontwarn ch.qos.logback.**
35+
-dontwarn okhttp3.**
36+
-dontwarn io.ktor.**
37+
38+
# hotkeys
39+
-keep class com.github.kwhat.jnativehook.** { *; } # Preserve all native hook classes and their methods
40+
41+
# compose
42+
-dontoptimize

client/src/main/kotlin/Launcher.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package dev.schlaubi.mastermind
2+
3+
import androidx.compose.ui.window.singleWindowApplication
4+
import com.github.kwhat.jnativehook.GlobalScreen
5+
import dev.schlaubi.mastermind.core.registerKeyBoardListener
6+
import dev.schlaubi.mastermind.ui.GTAKiller
7+
8+
fun main() {
9+
GlobalScreen.registerNativeHook()
10+
registerKeyBoardListener()
11+
12+
singleWindowApplication(title = "GTA Killer") {
13+
GTAKiller()
14+
}
15+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package dev.schlaubi.mastermind.core
2+
3+
import androidx.compose.runtime.getValue
4+
import androidx.compose.runtime.mutableStateOf
5+
import androidx.compose.runtime.setValue
6+
import dev.schlaubi.gtakiller.common.Event
7+
import dev.schlaubi.gtakiller.common.KillGtaEvent
8+
import dev.schlaubi.gtakiller.common.Route
9+
import dev.schlaubi.gtakiller.common.Status
10+
import dev.schlaubi.gtakiller.common.Username
11+
import dev.schlaubi.mastermind.core.settings.settings
12+
import io.github.oshai.kotlinlogging.KotlinLogging
13+
import io.ktor.client.*
14+
import io.ktor.client.call.*
15+
import io.ktor.client.plugins.*
16+
import io.ktor.client.plugins.contentnegotiation.*
17+
import io.ktor.client.plugins.resources.*
18+
import io.ktor.client.plugins.websocket.*
19+
import io.ktor.http.*
20+
import io.ktor.serialization.kotlinx.*
21+
import io.ktor.serialization.kotlinx.json.*
22+
import io.ktor.websocket.*
23+
import kotlinx.coroutines.*
24+
import kotlinx.coroutines.flow.MutableSharedFlow
25+
import kotlinx.coroutines.flow.asSharedFlow
26+
import kotlinx.serialization.json.Json
27+
import kotlin.coroutines.CoroutineContext
28+
29+
var currentApi by mutableStateOf<APIClient?>(null)
30+
31+
val safeApi get() = currentApi ?: error("No API client set")
32+
33+
private val LOG = KotlinLogging.logger { }
34+
35+
class APIClient(val url: Url) : CoroutineScope {
36+
override val coroutineContext: CoroutineContext = Dispatchers.IO + SupervisorJob()
37+
38+
private val client = HttpClient {
39+
install(ContentNegotiation) {
40+
json()
41+
}
42+
install(WebSockets) {
43+
contentConverter = KotlinxWebsocketSerializationConverter(Json)
44+
}
45+
install(Resources)
46+
47+
defaultRequest {
48+
url.takeFrom(this@APIClient.url)
49+
}
50+
}
51+
52+
private var webSocketSession: DefaultClientWebSocketSession? = null
53+
private val _events = MutableSharedFlow<Event>()
54+
val events = _events.asSharedFlow()
55+
56+
suspend fun connectToWebSocket() {
57+
webSocketSession?.close()
58+
val session = client.webSocketSession {
59+
url {
60+
url.takeFrom(this@APIClient.url)
61+
protocol = if (url.protocol.isSecure()) {
62+
URLProtocol.WSS
63+
} else {
64+
URLProtocol.WS
65+
}
66+
67+
client.href(Route.Events(), this)
68+
}
69+
70+
headers.append(HttpHeaders.Username, settings.userName)
71+
}
72+
73+
webSocketSession = session
74+
75+
session.launch {
76+
launch {
77+
while (isActive) {
78+
val event = session.receiveDeserialized<Event>()
79+
LOG.debug { "Received event: $event" }
80+
_events.emit(event)
81+
handleEvent(event)
82+
}
83+
}
84+
85+
val reason = session.closeReason.await()
86+
LOG.info { "Lost connection to websocket: ${reason?.message}" }
87+
}
88+
}
89+
90+
suspend fun getCurrentStatus() = client.get(Route.Status()).body<Status>()
91+
92+
suspend fun sendEvent(event: Event) {
93+
LOG.debug { "Sending event: $event" }
94+
webSocketSession?.sendSerialized(event) ?: error("Not connected")
95+
}
96+
97+
fun disconnect() {
98+
client.close()
99+
webSocketSession?.cancel()
100+
}
101+
}
102+
103+
suspend fun reportKillCommand() = safeApi.sendEvent(KillGtaEvent)
104+
105+
private suspend fun handleEvent(event: Event) {
106+
when (event) {
107+
is KillGtaEvent -> killGta()
108+
else -> {}
109+
}
110+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package dev.schlaubi.mastermind.core
2+
3+
import com.github.kwhat.jnativehook.GlobalScreen
4+
import com.github.kwhat.jnativehook.keyboard.NativeKeyEvent
5+
import com.github.kwhat.jnativehook.keyboard.NativeKeyListener
6+
import dev.schlaubi.mastermind.core.settings.settings
7+
import dev.schlaubi.mastermind.util.Loom
8+
import kotlinx.coroutines.Dispatchers
9+
import kotlinx.coroutines.runBlocking
10+
11+
fun registerKeyBoardListener() = GlobalScreen.addNativeKeyListener(object : NativeKeyListener {
12+
override fun nativeKeyPressed(nativeEvent: NativeKeyEvent) {
13+
if (nativeEvent.keyCode == settings.hotkey) {
14+
runBlocking(Dispatchers.Loom) {
15+
reportAndKill()
16+
}
17+
}
18+
}
19+
})
20+
21+
suspend fun reportAndKill() {
22+
reportKillCommand()
23+
killGta()
24+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package dev.schlaubi.mastermind.core
2+
3+
import io.github.oshai.kotlinlogging.KotlinLogging
4+
import kotlinx.coroutines.channels.Channel
5+
import kotlinx.coroutines.flow.MutableSharedFlow
6+
import kotlinx.coroutines.flow.asSharedFlow
7+
import kotlin.jvm.optionals.getOrNull
8+
9+
private val LOG = KotlinLogging.logger { }
10+
11+
private val _events = MutableSharedFlow<Unit>(extraBufferCapacity = Channel.UNLIMITED)
12+
val gtaKillErrors = _events.asSharedFlow()
13+
14+
suspend fun killGta() {
15+
LOG.info { "Trying to kill GTA5.exe" }
16+
17+
val gtaProcess = ProcessHandle.allProcesses()
18+
.filter { it.info().command().getOrNull()?.contains("GTA5.exe") == true }
19+
.findFirst()
20+
if (gtaProcess.isPresent) {
21+
gtaProcess.get().destroyForcibly()
22+
} else {
23+
LOG.error { "GTA5.exe not found" }
24+
_events.emit(Unit)
25+
}
26+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package dev.schlaubi.mastermind.core.settings
2+
3+
import com.github.kwhat.jnativehook.keyboard.NativeKeyEvent
4+
import io.ktor.http.Url
5+
import kotlinx.serialization.Serializable
6+
7+
@Serializable
8+
data class Settings(
9+
val currentUrl: Url?,
10+
val pastUrls: Set<Url>,
11+
val userName: String,
12+
val hotkey: Int = NativeKeyEvent.VC_F3
13+
)
14+
15+
fun Settings.addServerOrMoveToTop(serverUrl: Url): Settings {
16+
val servers = if (serverUrl in pastUrls) (pastUrls - serverUrl) + serverUrl else pastUrls + serverUrl
17+
18+
return copy(pastUrls = servers, currentUrl = serverUrl)
19+
}

0 commit comments

Comments
 (0)