Skip to content

Commit f6cd471

Browse files
committed
Support Flattening conversions (Fixes #1)
1 parent 2d11180 commit f6cd471

File tree

13 files changed

+2543
-134
lines changed

13 files changed

+2543
-134
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ collect statistics about inventory contents
1010

1111
## Example Usage
1212

13-
### Searching for Sponges (in 1.12)
13+
### Searching for Sponges
1414
```shell
15-
> java -jar mc-scanner-<version>.jar -i sponge -b 19 <world directory> sponges.zip
15+
> java -jar mc-scanner-<version>.jar -i sponge -i wet_sponge -b sponge -b wet_sponge <world directory> sponges.zip
1616
# ... 20s later ...
1717
# 2671/2671 5.9GiB/5.9GiB 298.6MiB/s 30 results
1818
```

build.gradle.kts

+8-4
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
22
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
33

44
plugins {
5-
kotlin("jvm") version "1.4.21"
5+
kotlin("jvm") version "1.5.31"
6+
kotlin("plugin.serialization") version "1.5.31"
67
id("com.github.johnrengelman.shadow") version "6.1.0"
8+
application
79
}
810

911

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

2023
testImplementation("org.jetbrains.kotlin:kotlin-test")
2124
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
2225
}
2326

27+
application {
28+
mainClassName = "de.skyrising.mc.scanner.ScannerKt"
29+
}
30+
2431
tasks {
2532
named<ShadowJar>("shadowJar") {
2633
classifier = ""
2734
mergeServiceFiles()
2835
minimize()
29-
manifest {
30-
attributes(mapOf("Main-Class" to "de.skyrising.mc.scanner.ScannerKt"))
31-
}
3236
}
3337
}
3438

gradle.properties

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
version = 0.1.0
1+
version = 0.2.0

src/main/kotlin/de/skyrising/mc/scanner/inventories.kt

+41-8
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
package de.skyrising.mc.scanner
22

3+
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap
4+
import it.unimi.dsi.fastutil.objects.Object2IntMap
5+
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
36
import it.unimi.dsi.fastutil.objects.Object2LongMap
47
import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap
58
import java.nio.ByteBuffer
69
import java.nio.file.Files
710
import java.nio.file.Path
811
import java.util.*
12+
import java.util.function.LongPredicate
13+
import kotlin.collections.LinkedHashMap
14+
import kotlin.collections.LinkedHashSet
915

1016
data class PlayerFile(private val path: Path) : Scannable {
1117
override val size: Long = Files.size(path)
@@ -36,30 +42,57 @@ data class PlayerFile(private val path: Path) : Scannable {
3642
}
3743

3844
fun scanInventory(slots: ListTag<CompoundTag>, needles: Collection<ItemType>, statsMode: Boolean): List<Object2LongMap<ItemType>> {
39-
val ids = needles.mapTo(mutableSetOf(), ItemType::id)
45+
val byId = LinkedHashMap<Identifier, MutableSet<ItemType>>()
46+
for (needle in needles) {
47+
byId.computeIfAbsent(needle.id) { LinkedHashSet() }.add(needle)
48+
}
4049
val result = Object2LongOpenHashMap<ItemType>()
4150
val inventories = mutableListOf<Object2LongMap<ItemType>>(result)
4251
for (slot in slots) {
4352
if (!slot.has("id", Tag.STRING)) continue
44-
val id = slot.getString("id")
53+
val id = Identifier(slot.getString("id"))
4554
val contained = getSubResults(slot, needles, statsMode)
46-
if (id in ids || (ids.isEmpty() && statsMode && contained.isEmpty())) {
55+
val matchingTypes = byId[id]
56+
if (matchingTypes != null || (byId.isEmpty() && statsMode && contained.isEmpty())) {
4757
val dmg = if (slot.has("Damage", Tag.INTEGER)) slot.getInt("Damage") else null
48-
if (dmg != null && dmg != 0 && dmg < 16) {
49-
result.addTo(ItemType("$id:$dmg"), slot.getInt("Count").toLong())
50-
} else {
51-
result.addTo(ItemType(id), slot.getInt("Count").toLong())
58+
var bestMatch = if (statsMode) ItemType(id, dmg ?: -1) else null
59+
if (matchingTypes != null) {
60+
for (type in matchingTypes) {
61+
if (dmg == type.damage || (bestMatch == null && type.damage < 0)) {
62+
bestMatch = type
63+
}
64+
}
65+
}
66+
if (bestMatch != null) {
67+
result.addTo(bestMatch, slot.getInt("Count").toLong())
5268
}
5369
}
5470
if (contained.isEmpty()) continue
55-
if (statsMode) inventories.addAll(contained)
71+
if (statsMode) {
72+
contained.forEach(::flatten)
73+
inventories.addAll(contained)
74+
}
5675
for (e in contained[0].object2LongEntrySet()) {
5776
result.addTo(e.key, e.longValue)
5877
}
5978
}
79+
flatten(result)
6080
return inventories
6181
}
6282

83+
fun flatten(items: Object2LongMap<ItemType>) {
84+
val updates = Object2LongOpenHashMap<ItemType>()
85+
for (e in items.object2LongEntrySet()) {
86+
if (e.key.flattened) continue
87+
val flattened = e.key.flatten()
88+
if (flattened == e.key) continue
89+
updates[flattened] = if (flattened in updates) updates.getLong(flattened) else items.getLong(flattened) + e.longValue
90+
e.setValue(0)
91+
}
92+
items.putAll(updates)
93+
items.values.removeIf(LongPredicate{ it == 0L })
94+
}
95+
6396
fun getSubResults(slot: CompoundTag, needles: Collection<ItemType>, statsMode: Boolean): List<Object2LongMap<ItemType>> {
6497
if (!slot.has("tag", Tag.COMPOUND)) return emptyList()
6598
val tag = slot.getCompound("tag")

src/main/kotlin/de/skyrising/mc/scanner/nbt.kt

+1
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ data class CompoundTag(val value: MutableMap<String, Tag>) : Tag(), MutableMap<S
360360
fun getCompound(key: String) = getTyped<CompoundTag, CompoundTag>(key) { it }
361361
fun getByteArray(key: String) = getTyped(key, ByteArrayTag::value)
362362
fun getString(key: String) = getTyped(key, StringTag::value)
363+
fun getLongArray(key: String) = getTyped(key, LongArrayTag::value)
363364

364365
fun getInt(key: String): Int {
365366
val tag = get(key, INTEGER) ?: throw IllegalArgumentException("No int value for $key")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package de.skyrising.mc.scanner
2+
3+
import kotlinx.serialization.*
4+
import kotlinx.serialization.json.*
5+
6+
interface Needle
7+
8+
private val BLOCK_STATE_MAP = readBlockStateMap()
9+
private val ITEM_MAP = readItemMap()
10+
11+
data class Identifier(val namespace: String, val path: String) : Comparable<Identifier> {
12+
constructor(id: String) : this(getNamespace(id), getPath(id))
13+
14+
override fun compareTo(other: Identifier): Int {
15+
val namespaceCompare = namespace.compareTo(other.namespace)
16+
if (namespaceCompare != 0) return namespaceCompare
17+
return path.compareTo(other.path)
18+
}
19+
20+
override fun toString() = "$namespace:$path"
21+
22+
companion object {
23+
fun getNamespace(id: String) = id.substringBefore(':', "minecraft")
24+
fun getPath(id: String) = id.substringAfter(':')
25+
}
26+
}
27+
28+
data class BlockState(val id: Identifier, val properties: Map<String, String> = emptyMap()) : Needle, Comparable<BlockState> {
29+
fun unflatten(): List<BlockIdMask> {
30+
val list = mutableListOf<BlockIdMask>()
31+
var id: Int? = null
32+
var mask = 0
33+
for (i in BLOCK_STATE_MAP.indices) {
34+
val mapped = BLOCK_STATE_MAP[i] ?: continue
35+
if (!mapped.matches(this)) continue
36+
val currentId = i shr 4
37+
if (id != null && currentId != id) {
38+
list.add(BlockIdMask(id, mask, this))
39+
mask = 0
40+
}
41+
id = currentId
42+
mask = mask or (1 shl (i and 0xf))
43+
}
44+
if (id != null) list.add(BlockIdMask(id, mask, this))
45+
return list
46+
}
47+
48+
fun matches(predicate: BlockState): Boolean {
49+
if (id != predicate.id) return false
50+
for (e in predicate.properties.entries) {
51+
if (!properties.containsKey(e.key) || properties[e.key] != e.value) return false
52+
}
53+
return true
54+
}
55+
56+
override fun compareTo(other: BlockState): Int {
57+
val idComp = id.compareTo(other.id)
58+
if (idComp != 0) return idComp
59+
if (properties == other.properties) return 0
60+
return properties.hashCode().compareTo(other.properties.hashCode()) or 1
61+
}
62+
63+
fun format(): String {
64+
if (properties.isEmpty()) return id.toString()
65+
val sb = StringBuilder(id.toString()).append('[')
66+
var first = true
67+
for (e in properties.entries) {
68+
if (!first) sb.append(',')
69+
first = false
70+
sb.append(e.key).append('=').append(e.value)
71+
}
72+
return sb.append(']').toString()
73+
}
74+
75+
override fun toString() = "BlockState(${format()})"
76+
77+
companion object {
78+
fun parse(desc: String): BlockState {
79+
if (!desc.contains('[')) return BlockState(Identifier(desc))
80+
val bracketIndex = desc.indexOf('[')
81+
val closingBracketIndex = desc.indexOf(']', bracketIndex + 1)
82+
if (closingBracketIndex != desc.lastIndex) throw IllegalArgumentException("Expected closing ]")
83+
val id = Identifier(desc.substring(0, bracketIndex))
84+
val properties = LinkedHashMap<String, String>()
85+
for (kvPair in desc.substring(bracketIndex + 1, closingBracketIndex).split(',')) {
86+
val equalsIndex = kvPair.indexOf('=')
87+
if (equalsIndex < 0) throw IllegalArgumentException("Invalid key-value pair")
88+
properties[kvPair.substring(0, equalsIndex)] = kvPair.substring(equalsIndex + 1)
89+
}
90+
return BlockState(id, properties)
91+
}
92+
93+
fun from(nbt: CompoundTag): BlockState {
94+
val id = Identifier(nbt.getString("Name"))
95+
if (!nbt.has("Properties", Tag.COMPOUND)) return BlockState(id)
96+
val properties = LinkedHashMap<String, String>()
97+
for (e in nbt.getCompound("Properties").entries) {
98+
properties[e.key] = (e.value as StringTag).value
99+
}
100+
return BlockState(id, properties)
101+
}
102+
}
103+
}
104+
105+
data class BlockIdMask(val id: Int, val metaMask: Int, val blockState: BlockState? = null) : Needle {
106+
fun matches(id: Int, meta: Int) = this.id == id && (1 shl meta) and metaMask != 0
107+
108+
infix fun or(other: BlockIdMask): BlockIdMask {
109+
if (other.id != id) throw IllegalArgumentException("Cannot combine different ids")
110+
return BlockIdMask(id, metaMask or other.metaMask)
111+
}
112+
113+
override fun toString(): String {
114+
if (blockState == null) return "BlockIdMask(%d:0x%04x)".format(id, metaMask)
115+
return "BlockIdMask(%d:0x%04x %s)".format(id, metaMask, blockState.format())
116+
}
117+
}
118+
119+
data class ItemType(val id: Identifier, val damage: Int = -1, val flattened: Boolean = damage < 0) : Needle, Comparable<ItemType> {
120+
fun flatten(): ItemType {
121+
if (this.flattened) return this
122+
var flattened = ITEM_MAP[this]
123+
if (flattened == null) flattened = ITEM_MAP[ItemType(id, 0)]
124+
if (flattened == null) return ItemType(id, damage, true)
125+
return ItemType(flattened, -1, true)
126+
}
127+
128+
fun unflatten(): List<ItemType> {
129+
if (!flattened) return emptyList()
130+
val list = mutableListOf<ItemType>()
131+
for (e in ITEM_MAP.entries) {
132+
if (e.value == this.id && e.key.id != this.id) {
133+
list.add(e.key)
134+
}
135+
}
136+
return list
137+
}
138+
139+
override fun toString(): String {
140+
return "ItemType(${format()})"
141+
}
142+
143+
fun format() = if (damage < 0 || (flattened && damage == 0)) "$id" else "$id.$damage"
144+
145+
override fun compareTo(other: ItemType): Int {
146+
val idComp = id.compareTo(other.id)
147+
if (idComp != 0) return idComp
148+
return damage.compareTo(other.damage)
149+
}
150+
151+
companion object {
152+
fun parse(str: String): ItemType {
153+
if (!str.contains('.')) return ItemType(Identifier(str))
154+
return ItemType(Identifier(str.substringBefore('.')), str.substringAfter('.').toInt())
155+
}
156+
}
157+
}
158+
159+
private fun getFlatteningMap(name: String): JsonObject = Json.decodeFromString(BlockIdMask::class.java.getResourceAsStream("/flattening/$name.json")!!.reader().readText())
160+
161+
private fun readBlockStateMap(): Array<BlockState?> {
162+
val jsonMap = getFlatteningMap("block_states")
163+
val map = Array<BlockState?>(256 * 16) { null }
164+
for (e in jsonMap.entries) {
165+
val id = if (e.key.contains(':')) {
166+
e.key.substringBefore(':').toInt() shl 4 or e.key.substringAfter(':').toInt()
167+
} else {
168+
e.key.toInt() shl 4
169+
}
170+
map[id] = BlockState.parse(e.value.jsonPrimitive.content)
171+
}
172+
for (i in map.indices) {
173+
if (map[i] != null) continue
174+
map[i] = map[i and 0xff0]
175+
}
176+
return map
177+
}
178+
179+
private fun readItemMap(): Map<ItemType, Identifier> {
180+
val jsonMap = getFlatteningMap("items")
181+
val map = LinkedHashMap<ItemType, Identifier>()
182+
for (e in jsonMap.entries) {
183+
map[ItemType.parse(e.key)] = Identifier(e.value.jsonPrimitive.content)
184+
}
185+
return map
186+
}

0 commit comments

Comments
 (0)