From c3ac3bd34a653c33fd1e52723a9ea8942ad3b6e1 Mon Sep 17 00:00:00 2001 From: Adam <897017+asemy@users.noreply.github.com> Date: Fri, 15 Oct 2021 23:43:42 +0200 Subject: [PATCH] mucking around with sockets and rcon --- README.md | 0 .../archetype/kotlin-jvm.gradle.kts | 10 ++ modules/kt-rcon/build.gradle.kts | 15 ++ .../kotlin/dev/adamko/ktrcon/KtRconClient.kt | 124 ++++++++++++++ .../kotlin/dev/adamko/ktrcon/KtRconClient2.kt | 103 ++++++++++++ .../kotlin/dev/adamko/ktrcon/KtRconClient3.kt | 130 +++++++++++++++ .../main/kotlin/dev/adamko/ktrcon/Packet.kt | 152 ++++++++++++++++++ .../kotlin/dev/adamko/ktrcon/PacketType.kt | 31 ++++ .../main/kotlin/dev/adamko/ktrcon/Password.kt | 16 ++ .../src/main/kotlin/dev/adamko/ktrcon/main.kt | 25 +++ .../test/resources/server/docker-compose.yml | 10 +- settings.gradle.kts | 1 + 12 files changed, 613 insertions(+), 4 deletions(-) create mode 100644 README.md create mode 100644 modules/kt-rcon/build.gradle.kts create mode 100644 modules/kt-rcon/src/main/kotlin/dev/adamko/ktrcon/KtRconClient.kt create mode 100644 modules/kt-rcon/src/main/kotlin/dev/adamko/ktrcon/KtRconClient2.kt create mode 100644 modules/kt-rcon/src/main/kotlin/dev/adamko/ktrcon/KtRconClient3.kt create mode 100644 modules/kt-rcon/src/main/kotlin/dev/adamko/ktrcon/Packet.kt create mode 100644 modules/kt-rcon/src/main/kotlin/dev/adamko/ktrcon/PacketType.kt create mode 100644 modules/kt-rcon/src/main/kotlin/dev/adamko/ktrcon/Password.kt create mode 100644 modules/kt-rcon/src/main/kotlin/dev/adamko/ktrcon/main.kt diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/buildSrc/src/main/kotlin/dev/adamko/factoriowebmap/archetype/kotlin-jvm.gradle.kts b/buildSrc/src/main/kotlin/dev/adamko/factoriowebmap/archetype/kotlin-jvm.gradle.kts index 4ad22ae..5f33e8a 100644 --- a/buildSrc/src/main/kotlin/dev/adamko/factoriowebmap/archetype/kotlin-jvm.gradle.kts +++ b/buildSrc/src/main/kotlin/dev/adamko/factoriowebmap/archetype/kotlin-jvm.gradle.kts @@ -1,5 +1,7 @@ package dev.adamko.factoriowebmap.archetype +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + plugins { id("dev.adamko.factoriowebmap.archetype.base") kotlin("jvm") @@ -11,3 +13,11 @@ kotlin { dependencies { implementation(platform("org.jetbrains.kotlin:kotlin-bom")) } + +tasks.withType().configureEach { + kotlinOptions.freeCompilerArgs += listOf( + "-Xopt-in=kotlin.OptIn", + "-Xopt-in=kotlin.ExperimentalStdlibApi", + "-Xopt-in=kotlin.time.ExperimentalTime", + ) +} \ No newline at end of file diff --git a/modules/kt-rcon/build.gradle.kts b/modules/kt-rcon/build.gradle.kts new file mode 100644 index 0000000..dbd36d2 --- /dev/null +++ b/modules/kt-rcon/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("dev.adamko.factoriowebmap.archetype.kotlin-jvm") +} + +//group = "${rootProject.group}.server" +//version = rootProject.version +// + +dependencies { + + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2") + + implementation("io.ktor:ktor-network:1.6.4") + +} \ No newline at end of file diff --git a/modules/kt-rcon/src/main/kotlin/dev/adamko/ktrcon/KtRconClient.kt b/modules/kt-rcon/src/main/kotlin/dev/adamko/ktrcon/KtRconClient.kt new file mode 100644 index 0000000..c5f4419 --- /dev/null +++ b/modules/kt-rcon/src/main/kotlin/dev/adamko/ktrcon/KtRconClient.kt @@ -0,0 +1,124 @@ +package dev.adamko.ktrcon + +import java.io.* +import java.net.Socket +import java.net.SocketException +import java.nio.BufferUnderflowException +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.channels.AsynchronousSocketChannel +import kotlin.coroutines.coroutineContext +import kotlin.coroutines.suspendCoroutine +import kotlin.random.Random +import kotlin.random.nextUInt +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.supervisorScope +import kotlinx.coroutines.yield + +class KtRconClient( + private val host: String, + private val port: Int, + private val password: Password, +) : AutoCloseable { + + private val rconClientScope = CoroutineScope(SupervisorJob()) + + private val socket: Socket = Socket(host, port) + + init { + + runBlocking { + send(PacketType.AUTH_REQUEST, password()) + } + + require(socket.isConnected) { "Socket must connect" } + + println("socket.isConnected: ${socket.isConnected}") + +// val j = rconClientScope.launch { + + runCatching { + val reader = socket.getInputStream() + .bufferedReader() + .lineSequence() + .asFlow() + .onEach { + println("socket response: [$it]") + } + .launchIn(rconClientScope) + +// while (socket.isConnected) { +// println("response: ${input.read().toByte().toInt().toChar()}") + + println("is listener active? ${reader.isActive}") + + reader.invokeOnCompletion { + println("cancelled input stream job") + } + + } +// } +// yield() +// } +// } + + + } + + suspend fun send(type: PacketType, body: String) = supervisorScope { + + val p = Packet(Random.nextUInt(), type, body) + + println("Sending packet: $p - totalSize: ${p.totalSize}") + println("p.size: ${p.size}") + println("p.id: ${p.id}") + println("p.type.value: ${p.type.value}") +// println("p.bodyBytes: ${p.bodyBytes} / ${p.body}") +// println("p.terminator: ${p.terminator} / ${p.terminator.decodeToString()}") + +// println("p.byteArray: ${p.toByteArray()}") + + val bb = p.toByteBuffer2() +// ByteBuffer.wrap(p.toByteArray()) +// bb.order(ByteOrder.LITTLE_ENDIAN) + +// val bb = ByteBuffer.allocate(p.totalSize * 2).apply { +// order(ByteOrder.LITTLE_ENDIAN) +// putInt(p.size) +// putInt(p.id.toInt()) +// putInt(p.type.value) +// put(p.bodyBytes) +// put(p.terminator) +// } + + runCatching { + socket.getOutputStream().use { os -> + os.write(bb.array()) + os.flush() + } + } +// socket.channel.finishConnect() +// socket.getInputStream().use { input -> +// println("running inputddd") +//// while (true) { +// println("response: ${input.readAllBytes().decodeToString()}") +// +//// yield() +//// } +// } + } + + override fun close() { + rconClientScope.cancel("Closing ${this::class}") + socket.close() + } + + +} \ No newline at end of file diff --git a/modules/kt-rcon/src/main/kotlin/dev/adamko/ktrcon/KtRconClient2.kt b/modules/kt-rcon/src/main/kotlin/dev/adamko/ktrcon/KtRconClient2.kt new file mode 100644 index 0000000..cc4a1bf --- /dev/null +++ b/modules/kt-rcon/src/main/kotlin/dev/adamko/ktrcon/KtRconClient2.kt @@ -0,0 +1,103 @@ +package dev.adamko.ktrcon + +import java.io.* +import java.net.InetSocketAddress +import java.net.Socket +import java.net.SocketException +import java.nio.BufferUnderflowException +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.channels.AsynchronousSocketChannel +import java.nio.channels.CompletionHandler +import java.util.concurrent.TimeUnit +import kotlin.coroutines.coroutineContext +import kotlin.coroutines.suspendCoroutine +import kotlin.random.Random +import kotlin.random.nextUInt +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.supervisorScope +import kotlinx.coroutines.yield + +class KtRconClient2( + private val host: String, + private val port: Int, + private val password: Password, +) : AutoCloseable { + + private val rconClientScope = CoroutineScope(SupervisorJob()) + + private val client: AsynchronousSocketChannel by lazy { + println("Initialising client") + + val client = AsynchronousSocketChannel.open() + val address = InetSocketAddress(host, port) + val result = client.connect(address) + result.get() //.get(2, TimeUnit.SECONDS) + + println("connected to async - $address") + require(client.isOpen) { "Require client is open" } + + client + } + + init { + } + + suspend fun send(type: PacketType, body: String) = supervisorScope { + + val p = Packet(Random.nextUInt(1u..100u), type, body) + + println("Sending packet: $p - totalSize: ${p.totalSize}") + println("p.size: ${p.size}") + println("p.totalSize: ${p.totalSize}") + println("p.id: ${p.id}") + println("p.type.value: ${p.type.value}") +// println("p.bodyBytes: ${p.bodyBytes} / ${p.body}") +// println("p.terminator: ${p.terminator} / ${p.terminator.decodeToString()}") + +// println("p.byteArray: ${p.toByteArray()}") +// println("p.byteArray: ${p.toByteBuffer().array()}") + + + runCatching { + val bb = p.toByteBuffer2() +// ByteBuffer.wrap(p.toByteArray()) +// bb.order(ByteOrder.LITTLE_ENDIAN) + + println("writing bb to client") + bb.compact() + val future = client.write(bb) + + delay(2000) + + val writeResult = future.get() //.get(2, TimeUnit.SECONDS) + delay(2000) + + println("write result: $writeResult") + + + val response = ByteBuffer.allocate(Int.SIZE_BYTES) + val responseStatus = runCatching { + client.read(response).get() //.get(2, TimeUnit.SECONDS) + } + println("responseStatus: $responseStatus") + println("response from server: ${response.array().decodeToString()}") + delay(2000) + } + + } + + override fun close() { + rconClientScope.cancel("Closing ${this::class}") + client.shutdownInput() + client.close() + } + +} \ No newline at end of file diff --git a/modules/kt-rcon/src/main/kotlin/dev/adamko/ktrcon/KtRconClient3.kt b/modules/kt-rcon/src/main/kotlin/dev/adamko/ktrcon/KtRconClient3.kt new file mode 100644 index 0000000..cdf69d0 --- /dev/null +++ b/modules/kt-rcon/src/main/kotlin/dev/adamko/ktrcon/KtRconClient3.kt @@ -0,0 +1,130 @@ +package dev.adamko.ktrcon + +import io.ktor.network.selector.ActorSelectorManager +import io.ktor.network.sockets.aSocket +import io.ktor.network.sockets.openReadChannel +import io.ktor.network.sockets.openWriteChannel +import io.ktor.util.decodeString +import io.ktor.utils.io.ByteReadChannel +import io.ktor.utils.io.ByteWriteChannel +import io.ktor.utils.io.core.isNotEmpty +import io.ktor.utils.io.core.readBytes +import io.ktor.utils.io.readRemaining +import io.ktor.utils.io.readUTF8Line +import io.ktor.utils.io.writeIntLittleEndian +import io.ktor.utils.io.writeStringUtf8 +import java.io.* +import java.net.InetSocketAddress +import java.net.Socket +import java.net.SocketException +import java.nio.BufferUnderflowException +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.channels.AsynchronousSocketChannel +import java.nio.channels.CompletionHandler +import java.util.concurrent.TimeUnit +import kotlin.coroutines.coroutineContext +import kotlin.coroutines.suspendCoroutine +import kotlin.random.Random +import kotlin.random.nextUInt +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.supervisorScope +import kotlinx.coroutines.yield + +class KtRconClient3( + private val host: String, + private val port: Int, + private val password: Password, +) : AutoCloseable { + + private val rconClientJob = SupervisorJob() + private val rconClientScope = CoroutineScope(rconClientJob) + + private val input: ByteReadChannel + private val output: ByteWriteChannel + + init { + val socket = runBlocking { + val selector = ActorSelectorManager(rconClientJob) + aSocket(selector) + .tcp() + .connect(InetSocketAddress(host, port)) + } + input = socket.openReadChannel() + output = socket.openWriteChannel() + +// runBlocking { +// output.writeStringUtf8("test") +// output.flush() +// delay(1000) +// println("availableForRead: ${input.availableForRead}") +// +// } + + rconClientScope.launch { + while (!input.isClosedForRead) { + val packet = input.readRemaining(1) + while (packet.isNotEmpty) { + val bytes = packet.readBytes() + println( + "received ${bytes.size} bytes: ${bytes.joinToString { "$it" }} - ${bytes.decodeToString()}" + ) + yield() + } + yield() + } + } + } + + suspend fun send(type: PacketType, body: String) = supervisorScope { + + val p = Packet(Random.nextUInt(1u..100u), type, body) + + println("Sending packet: $p - totalSize: ${p.totalSize}") + println("p.size: ${p.size}") + println("p.totalSize: ${p.totalSize}") + println("p.id: ${p.id}") + println("p.type.value: ${p.type.value}") +// println("p.bodyBytes: ${p.bodyBytes} / ${p.body}") +// println("p.terminator: ${p.terminator} / ${p.terminator.decodeToString()}") + +// println("p.byteArray: ${p.toByteArray()}") + println("p.byteArray: ${p.toByteBuffer2().array().joinToString { it.toString() }}") + + + val bb = p.toByteBuffer2() + println("bb: ${bb.position()} - ${bb.toString()}") + + bb.array().forEach { + print("-$it") + output.writeByte(it) + } +// output.writeFully(bb) +// output.writeFully(bb) + output.flush() + println("finished writing") + +// if (type == PacketType.EXEC_COMMAND) { +// println("awaiting response from server") +// input.read(0) { +// println("server response: ${it.array()}") +// } +//// val response = input.readUTF8Line() +//// println("Server said $response") +// } + + } + + override fun close() { + rconClientJob.complete() + rconClientScope.cancel("Closing ${this::class}") + } + +} \ No newline at end of file diff --git a/modules/kt-rcon/src/main/kotlin/dev/adamko/ktrcon/Packet.kt b/modules/kt-rcon/src/main/kotlin/dev/adamko/ktrcon/Packet.kt new file mode 100644 index 0000000..baa296b --- /dev/null +++ b/modules/kt-rcon/src/main/kotlin/dev/adamko/ktrcon/Packet.kt @@ -0,0 +1,152 @@ +package dev.adamko.ktrcon + +import io.ktor.utils.io.bits.reverseByteOrder +import java.nio.ByteBuffer +import java.nio.ByteOrder + + +/** + * @param[size] The packet size field is a 32-bit little endian integer, representing the length of the + * request in bytes. Note that the packet size field itself is not included when determining the + * size of the packet, so the value of this field is always 4 less than the packet's actual + * length. + * + * The minimum possible value for packet size is 10. The maximum possible value of packet size + * is 4096. If the response is too large to fit into one packet, it will be split and sent as + * multiple packets. + * + * @param[id] The packet id field is a 32-bit little endian integer chosen by the client for each + * request. It may be set to any positive integer. When the server responds to the request, the + * response packet will have the same packet id as the original request (unless it is a failed + * [PacketType.AUTH_RESPONSE] packet.) It need not be unique, but if a unique packet id is + * assigned, it can be used to match incoming responses to their corresponding requests. + * + * @param[type] See [PacketType] + * + * @param[body] The packet body field is a null-terminated string encoded in ASCII (i.e. ASCIIZ). + * Depending on the packet type, it may contain either the RCON password for the server, the command + * to be executed, or the server's response to a request. + */ +data class Packet( + val id: UInt, + val type: PacketType, + val bodyRaw: String, +) { + private val bodyTerminator: Byte = NULL_CHAR_BYTE + + private val body: ByteArray = bodyRaw.toByteArray(Charsets.UTF_8) + +// val bodyBytes = body.toByteArray(Charsets.UTF_8) + /** The size of this packet (note that the size itself is excluded from the calculation.) */ + val size: Int = run { + val idSize = 1 * Int.SIZE_BYTES + val typeSize = 1 * Int.SIZE_BYTES + val bodySize = body.size * Byte.SIZE_BYTES + val bodyTerminatorSize = 1 * Byte.SIZE_BYTES + val packetTerminatorSize = 1 * Byte.SIZE_BYTES + val total = idSize + typeSize + bodySize + bodyTerminatorSize + packetTerminatorSize + println( + "packetSize: $total = $idSize + $typeSize + $bodySize + $bodyTerminatorSize + $packetTerminatorSize" + ) + + total + } + + val totalSize: Int = size + Int.SIZE_BYTES + + init { + println("totalSize: $totalSize") + println("body: ${body.joinToString { it.toString() }}") + } + +// fun toByteArray(): ByteArray { +// val array = buildList { +// add(this@Packet.size.toByte()) +// add(id.toByte()) +// add(type.value.toByte()) +// addAll(body.toByteArray(Charsets.UTF_8).asIterable()) +// add(bodyTerminator) +// add(RCON_PACKET_TERMINATOR) +// }.toByteArray() +// +// println(array.joinToString { it.toInt().toChar().toString().padStart(3, ' ') }) +// println(array.joinToString { it.toInt().toString().padStart(3, ' ') }) +// println(array.joinToString { it.toUInt().toString().padStart(3, ' ') }) +// println(array.joinToString { it.toString().padStart(3, ' ') }) +// +//// require(array.size == totalSize) { "${array.size} == $totalSize" } +// return array +// } + + fun toByteBuffer2(): ByteBuffer { + + return ByteBuffer + .allocate(totalSize) + .order(ByteOrder.LITTLE_ENDIAN) + // size + .putInt(size) + // ID + .putInt(id.toInt()) + // type + .putInt(type.value) + // body + .put(body) + // body terminator + .put(bodyTerminator) + // packet terminator + .put(RCON_PACKET_TERMINATOR) +// .asReadOnlyBuffer() + + + } + +// fun toByteBuffer(): ByteBuffer { +// +// +// val bodyBA = buildList { +// add(id.toByte()) +// add(type.value.toByte()) +// addAll(bodyBytes.asIterable()) +// add(bodyTerminator) +// add(RCON_PACKET_TERMINATOR) +// }.toByteArray() +//// +//// +//// val body = ByteBuffer.allocate(totalSize + 15).apply { +//// order(ByteOrder.LITTLE_ENDIAN) +////// putInt(this@Packet.size) +//// putInt(this@Packet.id.toInt()) +//// putInt(this@Packet.type.value) +//// put(this@Packet.bodyBytes) +//// put(this@Packet.terminator) +//// } +// +// val bodySize = bodyBA.size //* Byte.SIZE_BYTES +// println("bodySize: $bodySize") +// +// val bb = ByteBuffer.allocate(bodySize + Int.SIZE_BYTES).apply { +// order(ByteOrder.LITTLE_ENDIAN) +// putInt(bodySize) +//// put(body.array()) +// put(bodyBA) +// } +// +// require( +// bb.array().size == totalSize) { "${bb.array().size} == $totalSize ($size + ${Int.SIZE_BYTES})" } +// +// return bb +// } + + companion object { + /** [Char.MIN_VALUE] = null char */ + const val NULL_CHAR_BYTE = Char.MIN_VALUE.code.toByte() + + /** + * The end of a Source RCON Protocol packet is marked by an empty ASCIIZ string - + * essentially just 8 bits of 0. In languages that do not null-terminate strings automatically, + * you can simply write 16 bits of 0 immediately after the packet body (8 to terminate the + * packet body and 8 for the empty string afterwards). + */ + const val RCON_PACKET_TERMINATOR = NULL_CHAR_BYTE + } +} diff --git a/modules/kt-rcon/src/main/kotlin/dev/adamko/ktrcon/PacketType.kt b/modules/kt-rcon/src/main/kotlin/dev/adamko/ktrcon/PacketType.kt new file mode 100644 index 0000000..f2878a1 --- /dev/null +++ b/modules/kt-rcon/src/main/kotlin/dev/adamko/ktrcon/PacketType.kt @@ -0,0 +1,31 @@ +package dev.adamko.ktrcon +/** + * The packet type field is a 32-bit little endian integer, which indicates the purpose of the + * packet. Its value will always be either 0, 2, or 3, depending on which of the request/response + * types the packet represents. + */ +enum class PacketType( + val value: Int, + val descriptor: String, +) { + + /** + * Typically, the first packet sent by the client will be a [AUTH_RESPONSE] packet, which is + * used to authenticate the connection with the server. + * + * The value of the packet's fields are as follows: + * + * * **ID** - any positive integer, chosen by the client + * (will be mirrored back in the server's response) + * * **Type** - 3 + * * **Body** - the RCON password of the server (if this matches the server's *rcon_password* + * cvar, the auth will succeed) + * + * If the rcon_password cvar is not set, or if it is set to empty string, all + * [AUTH_RESPONSE] requests will be refused. + */ + AUTH_REQUEST(3, "SERVERDATA_AUTH"), + AUTH_RESPONSE(2, "SERVERDATA_AUTH_RESPONSE"), + EXEC_COMMAND(2, "SERVERDATA_EXECCOMMAND"), + RESPONSE_VALUE(0, "SERVERDATA_RESPONSE_VALUE"), +} diff --git a/modules/kt-rcon/src/main/kotlin/dev/adamko/ktrcon/Password.kt b/modules/kt-rcon/src/main/kotlin/dev/adamko/ktrcon/Password.kt new file mode 100644 index 0000000..8b8b9b2 --- /dev/null +++ b/modules/kt-rcon/src/main/kotlin/dev/adamko/ktrcon/Password.kt @@ -0,0 +1,16 @@ +package dev.adamko.ktrcon + +class Password constructor(password: String) { + private val password: ByteArray = password.encodeToByteArray() + private val hashcode: Int = password.hashCode() + + operator fun invoke() = password.decodeToString() + + fun access(accessor: (password: String) -> Unit) { + accessor(password.decodeToString()) + } + + override fun equals(other: Any?) = (other as? Password)?.hashcode == this.hashcode + + override fun hashCode() = hashcode +} \ No newline at end of file diff --git a/modules/kt-rcon/src/main/kotlin/dev/adamko/ktrcon/main.kt b/modules/kt-rcon/src/main/kotlin/dev/adamko/ktrcon/main.kt new file mode 100644 index 0000000..946446c --- /dev/null +++ b/modules/kt-rcon/src/main/kotlin/dev/adamko/ktrcon/main.kt @@ -0,0 +1,25 @@ +package dev.adamko.ktrcon + +import kotlin.time.Duration +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking + +fun main(): Unit = runBlocking { + KtRconClient3( + "localhost", + 27715, + Password("coh9oongoojahX4") + ).use { client -> + + +// client.send(PacketType.AUTH_REQUEST, "passwrd") + client.send(PacketType.AUTH_REQUEST, "coh9oongoojahX4") + delay(Duration.seconds(10)) + + client.send(PacketType.EXEC_COMMAND, "/help") + + + delay(Duration.seconds(10)) + + } +} diff --git a/modules/mod/src/test/resources/server/docker-compose.yml b/modules/mod/src/test/resources/server/docker-compose.yml index 78bc9a8..ff0775c 100644 --- a/modules/mod/src/test/resources/server/docker-compose.yml +++ b/modules/mod/src/test/resources/server/docker-compose.yml @@ -2,15 +2,17 @@ version: "3.9" services: factorio: image: factoriotools/factorio:1.1.42 -# logging: -# driver: "json-file" + # logging: + # driver: "json-file" container_name: "factorio" restart: "unless-stopped" ports: - - "34197:34197/udp" - - "27015:27015/tcp" + - "34197:34197/udp" # factorio + - "27715:27715/tcp" # rcon volumes: - "./data:/factorio" + environment: + RCON_PORT: 27715 # environment: # - PUID=1000 # - PGID=1000 diff --git a/settings.gradle.kts b/settings.gradle.kts index d94594e..f77beda 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,6 +4,7 @@ include( ":modules:mod", // ":modules:mod2", ":modules:server", + ":modules:kt-rcon", ) enableFeaturePreview("VERSION_CATALOGS")