diff --git a/archive/src/main/kotlin/org/openrs2/archive/map/MapRenderer.kt b/archive/src/main/kotlin/org/openrs2/archive/map/MapRenderer.kt index 74630ef7fe..542ffa6407 100644 --- a/archive/src/main/kotlin/org/openrs2/archive/map/MapRenderer.kt +++ b/archive/src/main/kotlin/org/openrs2/archive/map/MapRenderer.kt @@ -277,7 +277,7 @@ public class MapRenderer @Inject constructor( connection: Connection, masterIndexId: Int, archiveId: Int, - group: Js5Index.Group + group: Js5Index.Group<*> ): Int2ObjectSortedMap? { connection.prepareStatement( """ diff --git a/cache/src/main/kotlin/org/openrs2/cache/Group.kt b/cache/src/main/kotlin/org/openrs2/cache/Group.kt index 23cd3744f9..97b3b6d9f6 100644 --- a/cache/src/main/kotlin/org/openrs2/cache/Group.kt +++ b/cache/src/main/kotlin/org/openrs2/cache/Group.kt @@ -6,7 +6,7 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectSortedMap import org.openrs2.buffer.use public object Group { - public fun unpack(input: ByteBuf, group: Js5Index.Group): Int2ObjectSortedMap { + public fun unpack(input: ByteBuf, group: Js5Index.Group<*>): Int2ObjectSortedMap { require(group.size >= 1) val singleEntry = group.singleOrNull() diff --git a/cache/src/main/kotlin/org/openrs2/cache/Js5Index.kt b/cache/src/main/kotlin/org/openrs2/cache/Js5Index.kt index ea5a105969..908c4e7291 100644 --- a/cache/src/main/kotlin/org/openrs2/cache/Js5Index.kt +++ b/cache/src/main/kotlin/org/openrs2/cache/Js5Index.kt @@ -13,17 +13,25 @@ public class Js5Index( public var hasDigests: Boolean = false, public var hasLengths: Boolean = false, public var hasUncompressedChecksums: Boolean = false -) : NamedEntryCollection(::Group) { - public class Group internal constructor( - parent: NamedEntryCollection, +) : MutableNamedEntryCollection(::MutableGroup) { + public interface Group : NamedEntryCollection, NamedEntry { + public val version: Int + public val checksum: Int + public val uncompressedChecksum: Int + public val length: Int + public val uncompressedLength: Int + } + + public class MutableGroup internal constructor( + parent: MutableNamedEntryCollection, override val id: Int - ) : NamedEntryCollection(::File), NamedEntry { - private var parent: NamedEntryCollection? = parent - public var version: Int = 0 - public var checksum: Int = 0 - public var uncompressedChecksum: Int = 0 - public var length: Int = 0 - public var uncompressedLength: Int = 0 + ) : MutableNamedEntryCollection(::MutableFile), MutableNamedEntry, Group { + private var parent: MutableNamedEntryCollection? = parent + public override var version: Int = 0 + public override var checksum: Int = 0 + public override var uncompressedChecksum: Int = 0 + public override var length: Int = 0 + public override var uncompressedLength: Int = 0 public override var nameHash: Int = -1 set(value) { @@ -47,7 +55,7 @@ public class Js5Index( if (javaClass != other?.javaClass) return false if (!super.equals(other)) return false - other as Group + other as MutableGroup if (id != other.id) return false if (version != other.version) return false @@ -90,11 +98,13 @@ public class Js5Index( } } - public class File internal constructor( - parent: NamedEntryCollection, + public interface File : NamedEntry + + public class MutableFile internal constructor( + parent: MutableNamedEntryCollection, override val id: Int, - ) : NamedEntry { - private var parent: NamedEntryCollection? = parent + ) : MutableNamedEntry, File { + private var parent: MutableNamedEntryCollection? = parent public override var nameHash: Int = -1 set(value) { @@ -111,7 +121,7 @@ public class Js5Index( if (this === other) return true if (javaClass != other?.javaClass) return false - other as File + other as MutableFile if (id != other.id) return false if (nameHash != other.nameHash) return false diff --git a/cache/src/main/kotlin/org/openrs2/cache/Js5MasterIndex.kt b/cache/src/main/kotlin/org/openrs2/cache/Js5MasterIndex.kt index ec0cca4ca6..33944273e3 100644 --- a/cache/src/main/kotlin/org/openrs2/cache/Js5MasterIndex.kt +++ b/cache/src/main/kotlin/org/openrs2/cache/Js5MasterIndex.kt @@ -140,7 +140,7 @@ public data class Js5MasterIndex( val version = index.version val groups = index.size - val totalUncompressedLength = index.sumOf(Js5Index.Group::uncompressedLength) + val totalUncompressedLength = index.sumOf(Js5Index.Group<*>::uncompressedLength) // TODO(gpe): should we throw an exception if there are trailing bytes here or in the block above? Entry(version, checksum, groups, totalUncompressedLength, digest) diff --git a/cache/src/main/kotlin/org/openrs2/cache/MutableNamedEntry.kt b/cache/src/main/kotlin/org/openrs2/cache/MutableNamedEntry.kt new file mode 100644 index 0000000000..8fcb7b9765 --- /dev/null +++ b/cache/src/main/kotlin/org/openrs2/cache/MutableNamedEntry.kt @@ -0,0 +1,17 @@ +package org.openrs2.cache + +import org.openrs2.util.krHashCode + +public interface MutableNamedEntry : NamedEntry { + public override var nameHash: Int + + public fun setName(name: String) { + nameHash = name.krHashCode() + } + + public fun clearName() { + nameHash = -1 + } + + public fun remove() +} diff --git a/cache/src/main/kotlin/org/openrs2/cache/MutableNamedEntryCollection.kt b/cache/src/main/kotlin/org/openrs2/cache/MutableNamedEntryCollection.kt new file mode 100644 index 0000000000..fac6db682e --- /dev/null +++ b/cache/src/main/kotlin/org/openrs2/cache/MutableNamedEntryCollection.kt @@ -0,0 +1,371 @@ +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( + private val entryConstructor: (MutableNamedEntryCollection, Int) -> T +) : NamedEntryCollection, MutableIterable { + private var singleEntry: T? = null + private var entries: Int2ObjectAVLTreeMap? = null + + // XXX(gpe): unfortunately fastutil doesn't have a multimap type + private var nameHashTable: Int2ObjectMap? = 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() + + if (singleEntry != null) { + entries[singleEntry.id] = singleEntry + + if (singleEntry.nameHash != -1) { + val nameHashTable = Int2ObjectOpenHashMap() + 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 { + val singleEntry = singleEntry + if (singleEntry != null) { + return object : MutableIterator { + 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 { + 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 { + 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 + } +} diff --git a/cache/src/main/kotlin/org/openrs2/cache/NamedEntry.kt b/cache/src/main/kotlin/org/openrs2/cache/NamedEntry.kt index 570b59a601..1e0efa7c10 100644 --- a/cache/src/main/kotlin/org/openrs2/cache/NamedEntry.kt +++ b/cache/src/main/kotlin/org/openrs2/cache/NamedEntry.kt @@ -1,18 +1,6 @@ package org.openrs2.cache -import org.openrs2.util.krHashCode - public interface NamedEntry { public val id: Int - public var nameHash: Int - - public fun setName(name: String) { - nameHash = name.krHashCode() - } - - public fun clearName() { - nameHash = -1 - } - - public fun remove() + public val nameHash: Int } diff --git a/cache/src/main/kotlin/org/openrs2/cache/NamedEntryCollection.kt b/cache/src/main/kotlin/org/openrs2/cache/NamedEntryCollection.kt index 88be188db2..46b6414c78 100644 --- a/cache/src/main/kotlin/org/openrs2/cache/NamedEntryCollection.kt +++ b/cache/src/main/kotlin/org/openrs2/cache/NamedEntryCollection.kt @@ -1,379 +1,25 @@ 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). + * A read-only view of a [MutableNamedEntryCollection]. */ -public abstract class NamedEntryCollection( - private val entryConstructor: (NamedEntryCollection, Int) -> T -) : MutableIterable { - private var singleEntry: T? = null - private var entries: Int2ObjectAVLTreeMap? = null - - // XXX(gpe): unfortunately fastutil doesn't have a multimap type - private var nameHashTable: Int2ObjectMap? = null - +public interface NamedEntryCollection : Iterable { public val size: Int - get() { - if (singleEntry != null) { - return 1 - } - - val entries = entries ?: return 0 - return entries.size - } - public 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 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 fun containsNamed(nameHash: Int): Boolean { - require(nameHash != -1) - val entry = singleEntry - if (entry?.nameHash == nameHash) { - return true - } - - return nameHashTable?.containsKey(nameHash) ?: false - } + public operator fun contains(id: Int): Boolean + public fun containsNamed(nameHash: Int): Boolean public operator fun contains(name: String): Boolean { return containsNamed(name.krHashCode()) } - public 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 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 operator fun get(id: Int): T? + public fun getNamed(nameHash: Int): T? public operator fun get(name: String): T? { return getNamed(name.krHashCode()) } - - 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() - - if (singleEntry != null) { - entries[singleEntry.id] = singleEntry - - if (singleEntry.nameHash != -1) { - val nameHashTable = Int2ObjectOpenHashMap() - 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 { - val singleEntry = singleEntry - if (singleEntry != null) { - return object : MutableIterator { - 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 { - 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 { - 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 NamedEntryCollection<*> - - 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 - } } diff --git a/cache/src/test/kotlin/org/openrs2/cache/NamedEntryCollectionTest.kt b/cache/src/test/kotlin/org/openrs2/cache/NamedEntryCollectionTest.kt index 897feaf757..072a8fe7ef 100644 --- a/cache/src/test/kotlin/org/openrs2/cache/NamedEntryCollectionTest.kt +++ b/cache/src/test/kotlin/org/openrs2/cache/NamedEntryCollectionTest.kt @@ -10,10 +10,10 @@ import kotlin.test.assertTrue class NamedEntryCollectionTest { private class TestEntry( - parent: NamedEntryCollection, + parent: MutableNamedEntryCollection, override val id: Int - ) : NamedEntry { - private var parent: NamedEntryCollection? = parent + ) : MutableNamedEntry { + private var parent: MutableNamedEntryCollection? = parent override var nameHash: Int = -1 set(value) { @@ -27,7 +27,7 @@ class NamedEntryCollectionTest { } } - private class TestCollection : NamedEntryCollection(::TestEntry) + private class TestCollection : MutableNamedEntryCollection(::TestEntry) @Test fun testBounds() {