Open-source multiplayer game server compatible with the RuneScape client
https://www.openrs2.org/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
371 lines
11 KiB
371 lines
11 KiB
package org.openrs2.cache
|
|
|
|
import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap
|
|
import it.unimi.dsi.fastutil.ints.Int2ObjectMap
|
|
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
|
|
import it.unimi.dsi.fastutil.ints.IntAVLTreeSet
|
|
import it.unimi.dsi.fastutil.ints.IntSortedSet
|
|
import it.unimi.dsi.fastutil.ints.IntSortedSets
|
|
import org.openrs2.util.krHashCode
|
|
|
|
/**
|
|
* A specialist collection type entirely designed for use by the [Js5Index]
|
|
* class.
|
|
*
|
|
* Entries may be accessed by their ID number or by the hash of their name, if
|
|
* they are named.
|
|
*
|
|
* Entries are sorted by their ID. The IDs should be fairly dense starting
|
|
* around 0, as the client stores entries in an array. Nevertheless, this
|
|
* implementation currently supports sparse IDs efficiently as the entries are
|
|
* actually stored in a tree map, primarily to make manipulation of the
|
|
* collection simpler (the client's implementation has the luxury of being
|
|
* read-only!)
|
|
*
|
|
* As the vast majority of [Js5Index.Group]s only have a single [Js5Index.File]
|
|
* entry, there is a special case for single entry collections. This avoids the
|
|
* memory overhead of the underlying collections in this special case, at the
|
|
* expense of making the logic slightly more complicated.
|
|
*
|
|
* Entries without a name have a name hash of -1. This is consistent with the
|
|
* client, which sets the name hash array to -1 before populating it. If the
|
|
* index or group is sparse, any non-existent IDs still have a hash of -1, which
|
|
* is passed into the client's IntHashTable class. This makes -1 unusable, so I
|
|
* feel this choice is justified.
|
|
*
|
|
* In practice, I think indexes generally either have names for every file or
|
|
* none at all, but allowing a mixture in this implementation makes mutating
|
|
* the index simpler.
|
|
*
|
|
* This implementation permits multiple entries to have the same name hash. In
|
|
* the event of a collision, the colliding entry with the lowest ID is returned
|
|
* from methods that look up an entry by its name hash. This implementation
|
|
* still behaves correctly if one of the colliding entries is removed: it will
|
|
* always return the entry with the next lowest ID.
|
|
*
|
|
* This is an edge case that is unlikely to be hit in practice: the 550 cache
|
|
* has no collisions, and the 876 cache only has a single collision (the hash
|
|
* for the empty string).
|
|
*/
|
|
public abstract class MutableNamedEntryCollection<T : MutableNamedEntry>(
|
|
private val entryConstructor: (MutableNamedEntryCollection<T>, Int) -> T
|
|
) : NamedEntryCollection<T>, MutableIterable<T> {
|
|
private var singleEntry: T? = null
|
|
private var entries: Int2ObjectAVLTreeMap<T>? = null
|
|
|
|
// XXX(gpe): unfortunately fastutil doesn't have a multimap type
|
|
private var nameHashTable: Int2ObjectMap<IntSortedSet>? = null
|
|
|
|
public override val size: Int
|
|
get() {
|
|
if (singleEntry != null) {
|
|
return 1
|
|
}
|
|
|
|
val entries = entries ?: return 0
|
|
return entries.size
|
|
}
|
|
|
|
public override val capacity: Int
|
|
get() {
|
|
val entry = singleEntry
|
|
if (entry != null) {
|
|
return entry.id + 1
|
|
}
|
|
|
|
val entries = entries ?: return 0
|
|
assert(entries.isNotEmpty())
|
|
return entries.lastIntKey() + 1
|
|
}
|
|
|
|
public override operator fun contains(id: Int): Boolean {
|
|
require(id >= 0)
|
|
|
|
val entry = singleEntry
|
|
if (entry?.id == id) {
|
|
return true
|
|
}
|
|
|
|
val entries = entries ?: return false
|
|
return entries.containsKey(id)
|
|
}
|
|
|
|
public override fun containsNamed(nameHash: Int): Boolean {
|
|
require(nameHash != -1)
|
|
|
|
val entry = singleEntry
|
|
if (entry?.nameHash == nameHash) {
|
|
return true
|
|
}
|
|
|
|
return nameHashTable?.containsKey(nameHash) ?: false
|
|
}
|
|
|
|
public override operator fun get(id: Int): T? {
|
|
require(id >= 0)
|
|
|
|
val entry = singleEntry
|
|
if (entry?.id == id) {
|
|
return entry
|
|
}
|
|
|
|
val entries = entries ?: return null
|
|
return entries[id]
|
|
}
|
|
|
|
public override fun getNamed(nameHash: Int): T? {
|
|
require(nameHash != -1)
|
|
|
|
val entry = singleEntry
|
|
if (entry?.nameHash == nameHash) {
|
|
return entry
|
|
}
|
|
|
|
val nameHashTable = nameHashTable ?: return null
|
|
val ids = nameHashTable.getOrDefault(nameHash, IntSortedSets.EMPTY_SET)
|
|
return if (ids.isNotEmpty()) {
|
|
get(ids.firstInt())!!
|
|
} else {
|
|
null
|
|
}
|
|
}
|
|
|
|
public fun createOrGet(id: Int): T {
|
|
var entry = get(id)
|
|
if (entry != null) {
|
|
return entry
|
|
}
|
|
|
|
entry = entryConstructor(this, id)
|
|
|
|
val singleEntry = singleEntry
|
|
var entries = entries
|
|
if (singleEntry == null && entries == null) {
|
|
this.singleEntry = entry
|
|
return entry
|
|
}
|
|
|
|
if (entries == null) {
|
|
entries = Int2ObjectAVLTreeMap<T>()
|
|
|
|
if (singleEntry != null) {
|
|
entries[singleEntry.id] = singleEntry
|
|
|
|
if (singleEntry.nameHash != -1) {
|
|
val nameHashTable = Int2ObjectOpenHashMap<IntSortedSet>()
|
|
nameHashTable[singleEntry.nameHash] = IntSortedSets.singleton(singleEntry.id)
|
|
this.nameHashTable = nameHashTable
|
|
}
|
|
}
|
|
|
|
this.singleEntry = null
|
|
this.entries = entries
|
|
}
|
|
|
|
entries[id] = entry
|
|
return entry
|
|
}
|
|
|
|
public fun createOrGetNamed(nameHash: Int): T {
|
|
var entry = getNamed(nameHash)
|
|
if (entry != null) {
|
|
return entry
|
|
}
|
|
|
|
entry = createOrGet(allocateId())
|
|
entry.nameHash = nameHash
|
|
return entry
|
|
}
|
|
|
|
public fun createOrGet(name: String): T {
|
|
return createOrGetNamed(name.krHashCode())
|
|
}
|
|
|
|
public fun remove(id: Int): T? {
|
|
val entry = get(id)
|
|
entry?.remove()
|
|
return entry
|
|
}
|
|
|
|
public fun removeNamed(nameHash: Int): T? {
|
|
val entry = getNamed(nameHash)
|
|
entry?.remove()
|
|
return entry
|
|
}
|
|
|
|
public fun remove(name: String): T? {
|
|
return removeNamed(name.krHashCode())
|
|
}
|
|
|
|
private fun allocateId(): Int {
|
|
val size = size
|
|
val capacity = capacity
|
|
if (size == capacity) {
|
|
return capacity
|
|
}
|
|
|
|
val singleEntry = singleEntry
|
|
if (singleEntry != null) {
|
|
assert(singleEntry.id != 0)
|
|
return 0
|
|
}
|
|
|
|
val entries = entries!!
|
|
for (id in 0 until capacity) {
|
|
if (!entries.containsKey(id)) {
|
|
return id
|
|
}
|
|
}
|
|
|
|
throw AssertionError()
|
|
}
|
|
|
|
internal fun rename(id: Int, prevNameHash: Int, newNameHash: Int) {
|
|
if (prevNameHash == newNameHash || singleEntry != null) {
|
|
return
|
|
}
|
|
|
|
var nameHashTable = nameHashTable
|
|
if (nameHashTable != null && prevNameHash != -1) {
|
|
val set = nameHashTable.get(prevNameHash)
|
|
assert(set != null && set.contains(id))
|
|
|
|
if (set.size > 1) {
|
|
set.remove(id)
|
|
} else {
|
|
nameHashTable.remove(prevNameHash)
|
|
}
|
|
}
|
|
|
|
if (newNameHash != -1) {
|
|
if (nameHashTable == null) {
|
|
nameHashTable = Int2ObjectOpenHashMap()
|
|
}
|
|
|
|
val set = nameHashTable[newNameHash]
|
|
when (set) {
|
|
null -> nameHashTable[newNameHash] = IntSortedSets.singleton(id)
|
|
is IntSortedSets.Singleton -> {
|
|
val newSet = IntAVLTreeSet()
|
|
newSet.add(set.firstInt())
|
|
newSet.add(id)
|
|
nameHashTable[newNameHash] = newSet
|
|
}
|
|
else -> set.add(id)
|
|
}
|
|
|
|
this.nameHashTable = nameHashTable
|
|
}
|
|
}
|
|
|
|
internal fun remove(entry: T) {
|
|
if (singleEntry?.id == entry.id) {
|
|
singleEntry = null
|
|
return
|
|
}
|
|
|
|
val entries = entries
|
|
check(entries != null)
|
|
|
|
val removedEntry = entries.remove(entry.id)
|
|
check(removedEntry != null)
|
|
|
|
if (entries.size == 1) {
|
|
val firstId = entries.firstIntKey()
|
|
singleEntry = entries[firstId]
|
|
this.entries = null
|
|
nameHashTable = null
|
|
return
|
|
}
|
|
|
|
rename(entry.id, entry.nameHash, -1)
|
|
}
|
|
|
|
override fun iterator(): MutableIterator<T> {
|
|
val singleEntry = singleEntry
|
|
if (singleEntry != null) {
|
|
return object : MutableIterator<T> {
|
|
private var pos = 0
|
|
private var removed = false
|
|
|
|
override fun hasNext(): Boolean {
|
|
return pos == 0
|
|
}
|
|
|
|
override fun next(): T {
|
|
if (pos++ != 0) {
|
|
throw NoSuchElementException()
|
|
}
|
|
|
|
return singleEntry
|
|
}
|
|
|
|
override fun remove() {
|
|
check(pos == 1 && !removed)
|
|
removed = true
|
|
|
|
singleEntry.remove()
|
|
}
|
|
}
|
|
}
|
|
|
|
val entries = entries
|
|
if (entries != null) {
|
|
return object : MutableIterator<T> {
|
|
private val it = entries.values.iterator()
|
|
private var last: T? = null
|
|
|
|
override fun hasNext(): Boolean {
|
|
return it.hasNext()
|
|
}
|
|
|
|
override fun next(): T {
|
|
last = null
|
|
|
|
val entry = it.next()
|
|
last = entry
|
|
return entry
|
|
}
|
|
|
|
override fun remove() {
|
|
val last = last
|
|
check(last != null)
|
|
last.remove()
|
|
this.last = null
|
|
}
|
|
}
|
|
}
|
|
|
|
return object : MutableIterator<T> {
|
|
override fun hasNext(): Boolean {
|
|
return false
|
|
}
|
|
|
|
override fun next(): T {
|
|
throw NoSuchElementException()
|
|
}
|
|
|
|
override fun remove() {
|
|
throw IllegalStateException()
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun equals(other: Any?): Boolean {
|
|
if (this === other) return true
|
|
if (javaClass != other?.javaClass) return false
|
|
|
|
other as MutableNamedEntryCollection<*>
|
|
|
|
if (singleEntry != other.singleEntry) return false
|
|
if (entries != other.entries) return false
|
|
|
|
return true
|
|
}
|
|
|
|
override fun hashCode(): Int {
|
|
var result = singleEntry?.hashCode() ?: 0
|
|
result = 31 * result + (entries?.hashCode() ?: 0)
|
|
return result
|
|
}
|
|
}
|
|
|