Skip to content

Commit

Permalink
Support Flattening conversions (Fixes #1)
Browse files Browse the repository at this point in the history
  • Loading branch information
skyrising committed Oct 17, 2021
1 parent 2d11180 commit f6cd471
Show file tree
Hide file tree
Showing 13 changed files with 2,543 additions and 134 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ collect statistics about inventory contents

## Example Usage

### Searching for Sponges (in 1.12)
### Searching for Sponges
```shell
> java -jar mc-scanner-<version>.jar -i sponge -b 19 <world directory> sponges.zip
> java -jar mc-scanner-<version>.jar -i sponge -i wet_sponge -b sponge -b wet_sponge <world directory> sponges.zip
# ... 20s later ...
# 2671/2671 5.9GiB/5.9GiB 298.6MiB/s 30 results
```
Expand Down
12 changes: 8 additions & 4 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar

plugins {
kotlin("jvm") version "1.4.21"
kotlin("jvm") version "1.5.31"
kotlin("plugin.serialization") version "1.5.31"
id("com.github.johnrengelman.shadow") version "6.1.0"
application
}


Expand All @@ -16,19 +18,21 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("it.unimi.dsi:fastutil:8.5.2")
implementation("net.sf.jopt-simple:jopt-simple:6.0-alpha-3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0")

testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
}

application {
mainClassName = "de.skyrising.mc.scanner.ScannerKt"
}

tasks {
named<ShadowJar>("shadowJar") {
classifier = ""
mergeServiceFiles()
minimize()
manifest {
attributes(mapOf("Main-Class" to "de.skyrising.mc.scanner.ScannerKt"))
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version = 0.1.0
version = 0.2.0
49 changes: 41 additions & 8 deletions src/main/kotlin/de/skyrising/mc/scanner/inventories.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
package de.skyrising.mc.scanner

import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap
import it.unimi.dsi.fastutil.objects.Object2IntMap
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
import it.unimi.dsi.fastutil.objects.Object2LongMap
import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap
import java.nio.ByteBuffer
import java.nio.file.Files
import java.nio.file.Path
import java.util.*
import java.util.function.LongPredicate
import kotlin.collections.LinkedHashMap
import kotlin.collections.LinkedHashSet

data class PlayerFile(private val path: Path) : Scannable {
override val size: Long = Files.size(path)
Expand Down Expand Up @@ -36,30 +42,57 @@ data class PlayerFile(private val path: Path) : Scannable {
}

fun scanInventory(slots: ListTag<CompoundTag>, needles: Collection<ItemType>, statsMode: Boolean): List<Object2LongMap<ItemType>> {
val ids = needles.mapTo(mutableSetOf(), ItemType::id)
val byId = LinkedHashMap<Identifier, MutableSet<ItemType>>()
for (needle in needles) {
byId.computeIfAbsent(needle.id) { LinkedHashSet() }.add(needle)
}
val result = Object2LongOpenHashMap<ItemType>()
val inventories = mutableListOf<Object2LongMap<ItemType>>(result)
for (slot in slots) {
if (!slot.has("id", Tag.STRING)) continue
val id = slot.getString("id")
val id = Identifier(slot.getString("id"))
val contained = getSubResults(slot, needles, statsMode)
if (id in ids || (ids.isEmpty() && statsMode && contained.isEmpty())) {
val matchingTypes = byId[id]
if (matchingTypes != null || (byId.isEmpty() && statsMode && contained.isEmpty())) {
val dmg = if (slot.has("Damage", Tag.INTEGER)) slot.getInt("Damage") else null
if (dmg != null && dmg != 0 && dmg < 16) {
result.addTo(ItemType("$id:$dmg"), slot.getInt("Count").toLong())
} else {
result.addTo(ItemType(id), slot.getInt("Count").toLong())
var bestMatch = if (statsMode) ItemType(id, dmg ?: -1) else null
if (matchingTypes != null) {
for (type in matchingTypes) {
if (dmg == type.damage || (bestMatch == null && type.damage < 0)) {
bestMatch = type
}
}
}
if (bestMatch != null) {
result.addTo(bestMatch, slot.getInt("Count").toLong())
}
}
if (contained.isEmpty()) continue
if (statsMode) inventories.addAll(contained)
if (statsMode) {
contained.forEach(::flatten)
inventories.addAll(contained)
}
for (e in contained[0].object2LongEntrySet()) {
result.addTo(e.key, e.longValue)
}
}
flatten(result)
return inventories
}

fun flatten(items: Object2LongMap<ItemType>) {
val updates = Object2LongOpenHashMap<ItemType>()
for (e in items.object2LongEntrySet()) {
if (e.key.flattened) continue
val flattened = e.key.flatten()
if (flattened == e.key) continue
updates[flattened] = if (flattened in updates) updates.getLong(flattened) else items.getLong(flattened) + e.longValue
e.setValue(0)
}
items.putAll(updates)
items.values.removeIf(LongPredicate{ it == 0L })
}

fun getSubResults(slot: CompoundTag, needles: Collection<ItemType>, statsMode: Boolean): List<Object2LongMap<ItemType>> {
if (!slot.has("tag", Tag.COMPOUND)) return emptyList()
val tag = slot.getCompound("tag")
Expand Down
1 change: 1 addition & 0 deletions src/main/kotlin/de/skyrising/mc/scanner/nbt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ data class CompoundTag(val value: MutableMap<String, Tag>) : Tag(), MutableMap<S
fun getCompound(key: String) = getTyped<CompoundTag, CompoundTag>(key) { it }
fun getByteArray(key: String) = getTyped(key, ByteArrayTag::value)
fun getString(key: String) = getTyped(key, StringTag::value)
fun getLongArray(key: String) = getTyped(key, LongArrayTag::value)

fun getInt(key: String): Int {
val tag = get(key, INTEGER) ?: throw IllegalArgumentException("No int value for $key")
Expand Down
186 changes: 186 additions & 0 deletions src/main/kotlin/de/skyrising/mc/scanner/needles.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package de.skyrising.mc.scanner

import kotlinx.serialization.*
import kotlinx.serialization.json.*

interface Needle

private val BLOCK_STATE_MAP = readBlockStateMap()
private val ITEM_MAP = readItemMap()

data class Identifier(val namespace: String, val path: String) : Comparable<Identifier> {
constructor(id: String) : this(getNamespace(id), getPath(id))

override fun compareTo(other: Identifier): Int {
val namespaceCompare = namespace.compareTo(other.namespace)
if (namespaceCompare != 0) return namespaceCompare
return path.compareTo(other.path)
}

override fun toString() = "$namespace:$path"

companion object {
fun getNamespace(id: String) = id.substringBefore(':', "minecraft")
fun getPath(id: String) = id.substringAfter(':')
}
}

data class BlockState(val id: Identifier, val properties: Map<String, String> = emptyMap()) : Needle, Comparable<BlockState> {
fun unflatten(): List<BlockIdMask> {
val list = mutableListOf<BlockIdMask>()
var id: Int? = null
var mask = 0
for (i in BLOCK_STATE_MAP.indices) {
val mapped = BLOCK_STATE_MAP[i] ?: continue
if (!mapped.matches(this)) continue
val currentId = i shr 4
if (id != null && currentId != id) {
list.add(BlockIdMask(id, mask, this))
mask = 0
}
id = currentId
mask = mask or (1 shl (i and 0xf))
}
if (id != null) list.add(BlockIdMask(id, mask, this))
return list
}

fun matches(predicate: BlockState): Boolean {
if (id != predicate.id) return false
for (e in predicate.properties.entries) {
if (!properties.containsKey(e.key) || properties[e.key] != e.value) return false
}
return true
}

override fun compareTo(other: BlockState): Int {
val idComp = id.compareTo(other.id)
if (idComp != 0) return idComp
if (properties == other.properties) return 0
return properties.hashCode().compareTo(other.properties.hashCode()) or 1
}

fun format(): String {
if (properties.isEmpty()) return id.toString()
val sb = StringBuilder(id.toString()).append('[')
var first = true
for (e in properties.entries) {
if (!first) sb.append(',')
first = false
sb.append(e.key).append('=').append(e.value)
}
return sb.append(']').toString()
}

override fun toString() = "BlockState(${format()})"

companion object {
fun parse(desc: String): BlockState {
if (!desc.contains('[')) return BlockState(Identifier(desc))
val bracketIndex = desc.indexOf('[')
val closingBracketIndex = desc.indexOf(']', bracketIndex + 1)
if (closingBracketIndex != desc.lastIndex) throw IllegalArgumentException("Expected closing ]")
val id = Identifier(desc.substring(0, bracketIndex))
val properties = LinkedHashMap<String, String>()
for (kvPair in desc.substring(bracketIndex + 1, closingBracketIndex).split(',')) {
val equalsIndex = kvPair.indexOf('=')
if (equalsIndex < 0) throw IllegalArgumentException("Invalid key-value pair")
properties[kvPair.substring(0, equalsIndex)] = kvPair.substring(equalsIndex + 1)
}
return BlockState(id, properties)
}

fun from(nbt: CompoundTag): BlockState {
val id = Identifier(nbt.getString("Name"))
if (!nbt.has("Properties", Tag.COMPOUND)) return BlockState(id)
val properties = LinkedHashMap<String, String>()
for (e in nbt.getCompound("Properties").entries) {
properties[e.key] = (e.value as StringTag).value
}
return BlockState(id, properties)
}
}
}

data class BlockIdMask(val id: Int, val metaMask: Int, val blockState: BlockState? = null) : Needle {
fun matches(id: Int, meta: Int) = this.id == id && (1 shl meta) and metaMask != 0

infix fun or(other: BlockIdMask): BlockIdMask {
if (other.id != id) throw IllegalArgumentException("Cannot combine different ids")
return BlockIdMask(id, metaMask or other.metaMask)
}

override fun toString(): String {
if (blockState == null) return "BlockIdMask(%d:0x%04x)".format(id, metaMask)
return "BlockIdMask(%d:0x%04x %s)".format(id, metaMask, blockState.format())
}
}

data class ItemType(val id: Identifier, val damage: Int = -1, val flattened: Boolean = damage < 0) : Needle, Comparable<ItemType> {
fun flatten(): ItemType {
if (this.flattened) return this
var flattened = ITEM_MAP[this]
if (flattened == null) flattened = ITEM_MAP[ItemType(id, 0)]
if (flattened == null) return ItemType(id, damage, true)
return ItemType(flattened, -1, true)
}

fun unflatten(): List<ItemType> {
if (!flattened) return emptyList()
val list = mutableListOf<ItemType>()
for (e in ITEM_MAP.entries) {
if (e.value == this.id && e.key.id != this.id) {
list.add(e.key)
}
}
return list
}

override fun toString(): String {
return "ItemType(${format()})"
}

fun format() = if (damage < 0 || (flattened && damage == 0)) "$id" else "$id.$damage"

override fun compareTo(other: ItemType): Int {
val idComp = id.compareTo(other.id)
if (idComp != 0) return idComp
return damage.compareTo(other.damage)
}

companion object {
fun parse(str: String): ItemType {
if (!str.contains('.')) return ItemType(Identifier(str))
return ItemType(Identifier(str.substringBefore('.')), str.substringAfter('.').toInt())
}
}
}

private fun getFlatteningMap(name: String): JsonObject = Json.decodeFromString(BlockIdMask::class.java.getResourceAsStream("/flattening/$name.json")!!.reader().readText())

private fun readBlockStateMap(): Array<BlockState?> {
val jsonMap = getFlatteningMap("block_states")
val map = Array<BlockState?>(256 * 16) { null }
for (e in jsonMap.entries) {
val id = if (e.key.contains(':')) {
e.key.substringBefore(':').toInt() shl 4 or e.key.substringAfter(':').toInt()
} else {
e.key.toInt() shl 4
}
map[id] = BlockState.parse(e.value.jsonPrimitive.content)
}
for (i in map.indices) {
if (map[i] != null) continue
map[i] = map[i and 0xff0]
}
return map
}

private fun readItemMap(): Map<ItemType, Identifier> {
val jsonMap = getFlatteningMap("items")
val map = LinkedHashMap<ItemType, Identifier>()
for (e in jsonMap.entries) {
map[ItemType.parse(e.key)] = Identifier(e.value.jsonPrimitive.content)
}
return map
}
Loading

0 comments on commit f6cd471

Please sign in to comment.