diff --git a/build.gradle.kts b/build.gradle.kts index b609129..c462e7e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -43,13 +43,20 @@ dependencies { compileOnly(libs.kotlinx.coroutines.core) compileOnly(libs.kotlinx.coroutines.jdk8) compileOnly(libs.kotlinx.serialization.json) + compileOnly(toxopid.dependencies.arcCore) + testImplementation(toxopid.dependencies.arcCore) compileOnly(toxopid.dependencies.mindustryCore) + testImplementation(toxopid.dependencies.mindustryCore) + compileOnly(libs.slf4j.api) testImplementation(libs.slf4j.simple) - compileOnly(libs.okhttp) + + implementation(libs.okhttp) implementation(libs.hoplite.core) implementation(libs.hoplite.yaml) + implementation(libs.guava) + testImplementation(libs.junit.api) testRuntimeOnly(libs.junit.engine) } @@ -119,11 +126,17 @@ val generateMetadataFile by tasks.registering { tasks.shadowJar { archiveFileName = "${metadata.name}.jar" archiveClassifier = "plugin" + from(generateMetadataFile) from(rootProject.file("LICENSE.md")) { into("META-INF") } + val shadowPackage = "$rootPackage.shadow" kotlinRelocate("com.sksamuel.hoplite", "$shadowPackage.hoplite") + kotlinRelocate("okhttp3", "$shadowPackage.okhttp3") + kotlinRelocate("okio", "$shadowPackage.okio") relocate("org.yaml.snakeyaml", "$shadowPackage.snakeyaml") + relocate("com.google.common", "$shadowPackage.guava") + mergeServiceFiles() minimize { exclude(dependency("com.sksamuel.hoplite:hoplite-.*:.*")) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7d21954..70d58c4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,7 @@ slf4md = "1.0.1" slf4j = "2.0.16" hoplite = "2.8.2" okhttp = "4.12.0" +guava = "33.3.1-jre" # testing junit = "5.11.2" @@ -34,6 +35,7 @@ slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" } hoplite-core = { module = "com.sksamuel.hoplite:hoplite-core", version.ref = "hoplite" } hoplite-yaml = { module = "com.sksamuel.hoplite:hoplite-yaml", version.ref = "hoplite" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } +guava = { module = "com.google.guava:guava", version.ref = "guava" } # testing junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } diff --git a/src/main/kotlin/com/xpdustry/nohorny/NoHornyLogger.kt b/src/main/kotlin/com/xpdustry/nohorny/NoHornyLogger.kt index 12368d4..944c9b1 100644 --- a/src/main/kotlin/com/xpdustry/nohorny/NoHornyLogger.kt +++ b/src/main/kotlin/com/xpdustry/nohorny/NoHornyLogger.kt @@ -29,6 +29,7 @@ import arc.util.Log import mindustry.Vars import org.slf4j.LoggerFactory +@Deprecated("no") internal interface NoHornyLogger { fun error( text: String, diff --git a/src/main/kotlin/com/xpdustry/nohorny/geometry/AdjacencyIndex.kt b/src/main/kotlin/com/xpdustry/nohorny/geometry/AdjacencyIndex.kt new file mode 100644 index 0000000..2c7b647 --- /dev/null +++ b/src/main/kotlin/com/xpdustry/nohorny/geometry/AdjacencyIndex.kt @@ -0,0 +1,135 @@ +/* + * This file is part of NoHorny. The plugin securing your server against nsfw builds. + * + * MIT License + * + * Copyright (c) 2024 Xpdustry + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.xpdustry.nohorny.geometry + +import arc.math.geom.Point2 +import arc.struct.IntMap +import arc.struct.IntQueue +import arc.struct.IntSet +import com.google.common.graph.ElementOrder +import com.google.common.graph.GraphBuilder +import kotlin.math.max +import kotlin.math.min + +internal data class IndexBlock( + override val x: Int, + override val y: Int, + val size: Int, + val data: T, +) : Rectangle { + override val w: Int get() = size + override val h: Int get() = size +} + +internal data class IndexCluster( + override val x: Int, + override val y: Int, + override val w: Int, + override val h: Int, + val blocks: List>, +) : Rectangle + +@Suppress("UnstableApiUsage") +internal class AdjacencyIndex { + private val index = IntMap>() + private val links = + GraphBuilder.undirected() + .nodeOrder(ElementOrder.unordered()) + .build() + + fun upsert(block: IndexBlock) { + if (select(block.x, block.y) != null) { + remove(block.x, block.y) + } + for (x in block.x until block.x + block.size) { + for (y in block.y until block.y + block.size) { + val packed = Point2.pack(x, y) + index.put(packed, block) + + if (x == block.x && select(x - 1, y) != null) { + links.putEdge(packed, Point2.pack(x - 1, y)) + } else if (x == block.x + block.size - 1 && select(x + 1, y) != null) { + links.putEdge(packed, Point2.pack(x + 1, y)) + } + + if (y == block.y && select(x, y - 1) != null) { + links.putEdge(packed, Point2.pack(x, y - 1)) + } else if (y == block.y + block.size - 1 && select(x, y + 1) != null) { + links.putEdge(packed, Point2.pack(x, y + 1)) + } + } + } + } + + fun select( + x: Int, + y: Int, + ): IndexBlock? = index[Point2.pack(x, y)] + + fun remove( + x: Int, + y: Int, + ) { + val packed = Point2.pack(x, y) + val block = select(x, y) ?: return + for (i in block.x until block.x + block.size) { + for (j in block.y until block.y + block.size) { + index.remove(Point2.pack(i, j)) + } + } + links.removeNode(packed) + } + + fun adjacents(): List> { + val clusters = mutableListOf>() + val visited = IntSet() + for (node in links.nodes()) { + if (node in visited) continue + var x = Point2.x(node).toInt() + var y = Point2.y(node).toInt() + var w = index[node]!!.size + var h = w + val blocks = mutableListOf>() + val queue = IntQueue() + queue.addLast(node) + while (!queue.isEmpty) { + val current = queue.removeFirst() + if (!visited.add(current)) continue + val data = index[current]!! + blocks += data + x = min(x, data.x) + y = min(y, data.y) + w = max(w, data.x + data.size - x) + h = max(h, data.y + data.size - y) + for (neighbor in links.adjacentNodes(current)) { + queue.addLast(neighbor) + } + } + clusters += IndexCluster(x, y, w, h, blocks) + } + return clusters + } +} diff --git a/src/main/kotlin/com/xpdustry/nohorny/tracker/LogicDisplayTracker.kt b/src/main/kotlin/com/xpdustry/nohorny/tracker/LogicDisplayTracker.kt new file mode 100644 index 0000000..b35335b --- /dev/null +++ b/src/main/kotlin/com/xpdustry/nohorny/tracker/LogicDisplayTracker.kt @@ -0,0 +1,129 @@ +/* + * This file is part of NoHorny. The plugin securing your server against nsfw builds. + * + * MIT License + * + * Copyright (c) 2024 Xpdustry + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.xpdustry.nohorny.tracker + +import com.xpdustry.nohorny.NoHornyImage +import com.xpdustry.nohorny.NoHornyListener +import com.xpdustry.nohorny.extension.onBuildingLifecycleEvent +import com.xpdustry.nohorny.extension.rx +import com.xpdustry.nohorny.extension.ry +import com.xpdustry.nohorny.geometry.AdjacencyIndex +import com.xpdustry.nohorny.geometry.ImmutablePoint +import com.xpdustry.nohorny.geometry.IndexBlock +import mindustry.Vars +import mindustry.logic.LExecutor +import mindustry.world.blocks.logic.LogicDisplay + +private typealias ProcessorWithLinks = Pair> + +internal data class LogicDisplayConfig( + val processorSearchRadius: Int = 10, +) + +internal class LogicDisplayTracker : NoHornyListener { + private val config = LogicDisplayConfig() + private val processors = mutableMapOf() + private val displays = AdjacencyIndex() + + override fun onInit() { + onBuildingLifecycleEvent( + insert = { display, _, _ -> + val resolution = (display.block as LogicDisplay).displaySize + val map = mutableMapOf() + val block = + IndexBlock( + display.rx, + display.ry, + display.block.size, + NoHornyImage.Display(resolution, map), + ) + + for ((position, data) in processors) { + if (display.within( + position.x.toFloat() * Vars.tilesize, + position.y.toFloat() * Vars.tilesize, + config.processorSearchRadius.toFloat() * Vars.tilesize, + ) + ) { + val (processor, links) = data + for (link in links) { + if (block.contains(link.x, link.y)) { + map[position] = processor + } + } + } + } + + displays.upsert(block) + }, + remove = { x, y -> + displays.remove(x, y) + }, + ) + } + + private fun readInstructions(executor: LExecutor): List { + val instructions = mutableListOf() + for (instruction in executor.instructions) { + if (instruction !is LExecutor.DrawI) { + continue + } + instructions += + when (instruction.type) { + LogicDisplay.commandColor -> { + val r = normalizeColorValue(executor.numi(instruction.x)) + val g = normalizeColorValue(executor.numi(instruction.y)) + val b = normalizeColorValue(executor.numi(instruction.p1)) + val a = normalizeColorValue(executor.numi(instruction.p2)) + NoHornyImage.Instruction.Color(r, g, b, a) + } + LogicDisplay.commandRect -> { + val x = executor.numi(instruction.x) + val y = executor.numi(instruction.y) + val w = executor.numi(instruction.p1) + val h = executor.numi(instruction.p2) + NoHornyImage.Instruction.Rect(x, y, w, h) + } + LogicDisplay.commandTriangle -> { + val x1 = executor.numi(instruction.x) + val y1 = executor.numi(instruction.y) + val x2 = executor.numi(instruction.p1) + val y2 = executor.numi(instruction.p2) + val x3 = executor.numi(instruction.p3) + val y3 = executor.numi(instruction.p4) + NoHornyImage.Instruction.Triangle(x1, y1, x2, y2, x3, y3) + } + else -> continue + } + } + return instructions + } + + private fun normalizeColorValue(value: Int): Int { + val result = value % 256 + return if (result < 0) result + 256 else result + } +}