Added PRS compression code and fixed several bugs and performance issues.

This commit is contained in:
Daan Vanden Bosch 2020-10-26 23:08:44 +01:00
parent b810e45fb3
commit 3416ef5676
27 changed files with 1019 additions and 657 deletions

View File

@ -177,10 +177,10 @@ private class Assembler(private val assembly: List<String>, private val manualSt
is DataSegment -> { is DataSegment -> {
val oldSize = seg.data.size val oldSize = seg.data.size
seg.data.size += bytes.size.toUInt() seg.data.size += bytes.size
for (i in bytes.indices) { for (i in bytes.indices) {
seg.data.setI8(i.toUInt() + oldSize, bytes[i]) seg.data.setI8(i + oldSize, bytes[i])
} }
} }
@ -231,7 +231,9 @@ private class Assembler(private val assembly: List<String>, private val manualSt
} }
private fun addUnexpectedTokenError(token: Token) { private fun addUnexpectedTokenError(token: Token) {
addError(token, "Unexpected token.", "Unexpected ${token::class.simpleName} at ${token.srcLoc()}.") addError(token,
"Unexpected token.",
"Unexpected ${token::class.simpleName} at ${token.srcLoc()}.")
} }
private fun addWarning(token: Token, uiMessage: String) { private fun addWarning(token: Token, uiMessage: String) {
@ -288,7 +290,7 @@ private class Assembler(private val assembly: List<String>, private val manualSt
if (!prevLineHadLabel) { if (!prevLineHadLabel) {
segment = DataSegment( segment = DataSegment(
labels = mutableListOf(label), labels = mutableListOf(label),
data = Buffer.withCapacity(0u), data = Buffer.withCapacity(0),
srcLoc = SegmentSrcLoc(labels = mutableListOf(srcLoc)), srcLoc = SegmentSrcLoc(labels = mutableListOf(srcLoc)),
) )
objectCode.add(segment!!) objectCode.add(segment!!)

View File

@ -6,102 +6,109 @@ import world.phantasmal.lib.Endianness
* Resizable, continuous block of bytes which is reallocated when necessary. * Resizable, continuous block of bytes which is reallocated when necessary.
*/ */
expect class Buffer { expect class Buffer {
var size: UInt var size: Int
/** /**
* Byte order mode. * Byte order mode.
*/ */
var endianness: Endianness var endianness: Endianness
val capacity: UInt val capacity: Int
/** /**
* Reads an unsigned 8-bit integer at the given offset. * Reads an unsigned 8-bit integer at the given offset.
*/ */
fun getU8(offset: UInt): UByte fun getU8(offset: Int): UByte
/** /**
* Reads an unsigned 16-bit integer at the given offset. * Reads an unsigned 16-bit integer at the given offset.
*/ */
fun getU16(offset: UInt): UShort fun getU16(offset: Int): UShort
/** /**
* Reads an unsigned 32-bit integer at the given offset. * Reads an unsigned 32-bit integer at the given offset.
*/ */
fun getU32(offset: UInt): UInt fun getU32(offset: Int): UInt
/** /**
* Reads a signed 8-bit integer at the given offset. * Reads a signed 8-bit integer at the given offset.
*/ */
fun getI8(offset: UInt): Byte fun getI8(offset: Int): Byte
/** /**
* Reads a signed 16-bit integer at the given offset. * Reads a signed 16-bit integer at the given offset.
*/ */
fun getI16(offset: UInt): Short fun getI16(offset: Int): Short
/** /**
* Reads a signed 32-bit integer at the given offset. * Reads a signed 32-bit integer at the given offset.
*/ */
fun getI32(offset: UInt): Int fun getI32(offset: Int): Int
/** /**
* Reads a 32-bit floating point number at the given offset. * Reads a 32-bit floating point number at the given offset.
*/ */
fun getF32(offset: UInt): Float fun getF32(offset: Int): Float
/** /**
* Reads a UTF-16-encoded string at the given offset. * Reads a UTF-16-encoded string at the given offset.
*/ */
fun getStringUtf16(offset: UInt, maxByteLength: UInt, nullTerminated: Boolean): String fun getStringUtf16(offset: Int, maxByteLength: Int, nullTerminated: Boolean): String
/** /**
* Returns a copy of this buffer at the given offset with the given size. * Returns a copy of this buffer at the given offset with the given size.
*/ */
fun slice(offset: UInt, size: UInt): Buffer fun slice(offset: Int, size: Int): Buffer
/** /**
* Writes an unsigned 8-bit integer at the given offset. * Writes an unsigned 8-bit integer at the given offset.
*/ */
fun setU8(offset: UInt, value: UByte): Buffer fun setU8(offset: Int, value: UByte): Buffer
/** /**
* Writes an unsigned 16-bit integer at the given offset. * Writes an unsigned 16-bit integer at the given offset.
*/ */
fun setU16(offset: UInt, value: UShort): Buffer fun setU16(offset: Int, value: UShort): Buffer
/** /**
* Writes an unsigned 32-bit integer at the given offset. * Writes an unsigned 32-bit integer at the given offset.
*/ */
fun setU32(offset: UInt, value: UInt): Buffer fun setU32(offset: Int, value: UInt): Buffer
/** /**
* Writes a signed 8-bit integer at the given offset. * Writes a signed 8-bit integer at the given offset.
*/ */
fun setI8(offset: UInt, value: Byte): Buffer fun setI8(offset: Int, value: Byte): Buffer
/** /**
* Writes a signed 16-bit integer at the given offset. * Writes a signed 16-bit integer at the given offset.
*/ */
fun setI16(offset: UInt, value: Short): Buffer fun setI16(offset: Int, value: Short): Buffer
/** /**
* Writes a signed 32-bit integer at the given offset. * Writes a signed 32-bit integer at the given offset.
*/ */
fun setI32(offset: UInt, value: Int): Buffer fun setI32(offset: Int, value: Int): Buffer
/** /**
* Writes a 32-bit floating point number at the given offset. * Writes a 32-bit floating point number at the given offset.
*/ */
fun setF32(offset: UInt, value: Float): Buffer fun setF32(offset: Int, value: Float): Buffer
/** /**
* Writes 0 bytes to the entire buffer. * Writes 0 bytes to the entire buffer.
*/ */
fun zero(): Buffer fun zero(): Buffer
/**
* Writes [value] to every byte in the buffer.
*/
fun fill(value: Byte): Buffer
companion object { companion object {
fun withCapacity(initialCapacity: UInt, endianness: Endianness = Endianness.Little): Buffer fun withCapacity(initialCapacity: Int, endianness: Endianness = Endianness.Little): Buffer
fun withSize(initialSize: Int, endianness: Endianness = Endianness.Little): Buffer
fun fromByteArray(array: ByteArray, endianness: Endianness = Endianness.Little): Buffer fun fromByteArray(array: ByteArray, endianness: Endianness = Endianness.Little): Buffer
} }

View File

@ -0,0 +1,138 @@
package world.phantasmal.lib.compression.prs
import world.phantasmal.lib.Endianness
import world.phantasmal.lib.buffer.Buffer
import world.phantasmal.lib.cursor.Cursor
import world.phantasmal.lib.cursor.WritableCursor
import world.phantasmal.lib.cursor.cursor
import kotlin.math.max
import kotlin.math.min
fun prsCompress(cursor: Cursor): Cursor {
val compressor = PrsCompressor(cursor.size, cursor.endianness)
val comparisonCursor = cursor.take(cursor.size)
cursor.seekStart(0)
while (cursor.hasBytesLeft()) {
// Find the longest match.
var bestOffset = 0
var bestSize = 0
val startPos = cursor.position
val minOffset = max(0, startPos - min(0x800, cursor.bytesLeft))
for (i in startPos - 255 downTo minOffset) {
comparisonCursor.seekStart(i)
var size = 0
while (cursor.hasBytesLeft() && size <= 254 && cursor.u8() == comparisonCursor.u8()) {
size++
}
cursor.seekStart(startPos)
if (size >= bestSize) {
bestOffset = i
bestSize = size
if (size >= 255) {
break
}
}
}
if (bestSize < 3) {
compressor.addU8(cursor.u8())
} else {
compressor.copy(bestOffset - cursor.position, bestSize)
cursor.seek(bestSize)
}
}
return compressor.finalize()
}
private class PrsCompressor(capacity: Int, endianness: Endianness) {
private val output: WritableCursor = Buffer.withCapacity(capacity, endianness).cursor()
private var flags = 0
private var flagBitsLeft = 0
private var flagOffset = 0
fun addU8(value: UByte) {
writeControlBit(1)
writeU8(value)
}
fun copy(offset: Int, size: Int) {
if (offset > -256 && size <= 5) {
shortCopy(offset, size)
} else {
longCopy(offset, size)
}
}
fun finalize(): Cursor {
writeControlBit(0)
writeControlBit(1)
flags = flags ushr flagBitsLeft
val pos = output.position
output.seekStart(flagOffset).writeU8(flags.toUByte()).seekStart(pos)
writeU8(0u)
writeU8(0u)
return output.seekStart(0)
}
private fun writeControlBit(bit: Int) {
if (flagBitsLeft == 0) {
// Write out the flags to their position in the file, and store the next flags byte
// position.
val pos = output.position
output.seekStart(flagOffset)
output.writeU8(flags.toUByte())
output.seekStart(pos)
output.writeU8(0u) // Placeholder for the next flags byte.
flagOffset = pos
flagBitsLeft = 8
}
flags = flags ushr 1
if (bit!=0) {
flags = flags or 0x80
}
flagBitsLeft--
}
private fun writeU8(data: UByte) {
output.writeU8(data)
}
private fun writeU8(data: Int) {
output.writeU8(data.toUByte())
}
private fun shortCopy(offset: Int, size: Int) {
val s = size - 2
writeControlBit(0)
writeControlBit(0)
writeControlBit(((s ushr 1) and 1) )
writeControlBit((s and 1))
writeU8(offset and 0xFF)
}
private fun longCopy(offset: Int, size: Int) {
writeControlBit(0)
writeControlBit(1)
if (size <= 9) {
writeU8(((offset shl 3) and 0xF8) or ((size - 2) and 0x07))
writeU8((offset ushr 5) and 0xFF)
} else {
writeU8((offset shl 3) and 0xF8)
writeU8((offset ushr 5) and 0xFF)
writeU8(size - 1)
}
}
}

View File

@ -6,9 +6,9 @@ import world.phantasmal.core.PwResultBuilder
import world.phantasmal.core.Severity import world.phantasmal.core.Severity
import world.phantasmal.core.Success import world.phantasmal.core.Success
import world.phantasmal.lib.buffer.Buffer import world.phantasmal.lib.buffer.Buffer
import world.phantasmal.lib.cursor.BufferCursor
import world.phantasmal.lib.cursor.Cursor import world.phantasmal.lib.cursor.Cursor
import world.phantasmal.lib.cursor.WritableCursor import world.phantasmal.lib.cursor.WritableCursor
import world.phantasmal.lib.cursor.cursor
import kotlin.math.floor import kotlin.math.floor
import kotlin.math.min import kotlin.math.min
@ -16,26 +16,27 @@ private val logger = KotlinLogging.logger {}
fun prsDecompress(cursor: Cursor): PwResult<Cursor> { fun prsDecompress(cursor: Cursor): PwResult<Cursor> {
try { try {
val ctx = Context(cursor) val decompressor = PrsDecompressor(cursor)
var i = 0
while (true) { while (true) {
if (ctx.readFlagBit() == 1u) { if (decompressor.readFlagBit() == 1) {
// Single byte copy. // Single byte copy.
ctx.copyU8() decompressor.copyU8()
} else { } else {
// Multi byte copy. // Multi byte copy.
var length: UInt var length: Int
var offset: Int var offset: Int
if (ctx.readFlagBit() == 0u) { if (decompressor.readFlagBit() == 0) {
// Short copy. // Short copy.
length = (ctx.readFlagBit() shl 1) or ctx.readFlagBit() length = (decompressor.readFlagBit() shl 1) or decompressor.readFlagBit()
length += 2u length += 2
offset = ctx.readU8().toInt() - 256 offset = decompressor.readU8().toInt() - 256
} else { } else {
// Long copy or end of file. // Long copy or end of file.
offset = ctx.readU16().toInt() offset = decompressor.readU16().toInt()
// Two zero bytes implies that this is the end of the file. // Two zero bytes implies that this is the end of the file.
if (offset == 0) { if (offset == 0) {
@ -43,24 +44,26 @@ fun prsDecompress(cursor: Cursor): PwResult<Cursor> {
} }
// Do we need to read a length byte, or is it encoded in what we already have? // Do we need to read a length byte, or is it encoded in what we already have?
length = (offset and 0b111).toUInt() length = offset and 0b111
offset = offset shr 3 offset = offset ushr 3
if (length == 0u) { if (length == 0) {
length = ctx.readU8().toUInt() length = decompressor.readU8().toInt()
length += 1u length += 1
} else { } else {
length += 2u length += 2
} }
offset -= 8192 offset -= 8192
} }
ctx.offsetCopy(offset, length) decompressor.offsetCopy(offset, length)
}
} }
return Success(ctx.dst.seekStart(0u)) i++
}
return Success(decompressor.dst.seekStart(0))
} catch (e: Throwable) { } catch (e: Throwable) {
return PwResultBuilder<Cursor>(logger) return PwResultBuilder<Cursor>(logger)
.addProblem(Severity.Error, "PRS-compressed stream is corrupt.", cause = e) .addProblem(Severity.Error, "PRS-compressed stream is corrupt.", cause = e)
@ -68,23 +71,22 @@ fun prsDecompress(cursor: Cursor): PwResult<Cursor> {
} }
} }
class Context(cursor: Cursor) { private class PrsDecompressor(cursor: Cursor) {
private val src: Cursor = cursor private val src: Cursor = cursor
val dst: WritableCursor = BufferCursor( val dst: WritableCursor =
Buffer.withCapacity(floor(1.5 * cursor.size.toDouble()).toUInt(), cursor.endianness), Buffer.withCapacity(floor(1.5 * cursor.size.toDouble()).toInt(), cursor.endianness).cursor()
) private var flags = 0
private var flags = 0u
private var flagBitsLeft = 0 private var flagBitsLeft = 0
fun readFlagBit(): UInt { fun readFlagBit(): Int {
// Fetch a new flag byte when the previous byte has been processed. // Fetch a new flag byte when the previous byte has been processed.
if (flagBitsLeft == 0) { if (flagBitsLeft == 0) {
flags = readU8().toUInt() flags = readU8().toInt()
flagBitsLeft = 8 flagBitsLeft = 8
} }
val bit = flags and 1u val bit = flags and 1
flags = flags shr 1 flags = flags ushr 1
flagBitsLeft -= 1 flagBitsLeft -= 1
return bit return bit
} }
@ -97,25 +99,26 @@ class Context(cursor: Cursor) {
fun readU16(): UShort = src.u16() fun readU16(): UShort = src.u16()
fun offsetCopy(offset: Int, length: UInt) { fun offsetCopy(offset: Int, length: Int) {
require(offset in -8192..0) { require(offset in -8192..0) {
"offset was ${offset}, should be between -8192 and 0." "offset was ${offset}, should be between -8192 and 0."
} }
require(length in 1u..256u) { require(length in 1..256) {
"length was ${length}, should be between 1 and 256." "length was ${length}, should be between 1 and 256."
} }
// The length can be larger than -offset, in that case we copy -offset bytes size/-offset times. // The length can be larger than -offset, in that case we copy -offset bytes size/-offset
val bufSize = min((-offset).toUInt(), length) // times.
val bufSize = min(-offset, length)
dst.seek(offset) dst.seek(offset)
val buf = dst.take(bufSize) val buf = dst.take(bufSize)
dst.seek(-offset - bufSize.toInt()) dst.seek(-offset - bufSize)
repeat((length / bufSize).toInt()) { repeat(length / bufSize) {
dst.writeCursor(buf) dst.writeCursor(buf)
buf.seekStart(0u) buf.seekStart(0)
} }
dst.writeCursor(buf.take(length % bufSize)) dst.writeCursor(buf.take(length % bufSize))

View File

@ -2,52 +2,51 @@ package world.phantasmal.lib.cursor
import world.phantasmal.lib.ZERO_U16 import world.phantasmal.lib.ZERO_U16
import world.phantasmal.lib.ZERO_U8 import world.phantasmal.lib.ZERO_U8
import world.phantasmal.lib.buffer.Buffer
import kotlin.math.min import kotlin.math.min
abstract class AbstractWritableCursor abstract class AbstractWritableCursor
protected constructor(protected val offset: UInt) : WritableCursor { protected constructor(protected val offset: Int) : WritableCursor {
override var position: UInt = 0u override var position: Int = 0
protected set protected set
override val bytesLeft: UInt override val bytesLeft: Int
get() = size - position get() = size - position
protected val absolutePosition: UInt protected val absolutePosition: Int
get() = offset + position get() = offset + position
override fun hasBytesLeft(bytes: UInt): Boolean = override fun hasBytesLeft(bytes: Int): Boolean =
bytesLeft >= bytes bytesLeft >= bytes
override fun seek(offset: Int): WritableCursor = override fun seek(offset: Int): WritableCursor =
seekStart((position.toInt() + offset).toUInt()) seekStart(position + offset)
override fun seekStart(offset: UInt): WritableCursor { override fun seekStart(offset: Int): WritableCursor {
require(offset <= size) { "Offset $offset is out of bounds." } require(offset >= 0 || offset <= size) { "Offset $offset is out of bounds." }
position = offset position = offset
return this return this
} }
override fun seekEnd(offset: UInt): WritableCursor { override fun seekEnd(offset: Int): WritableCursor {
require(offset <= size) { "Offset $offset is out of bounds." } require(offset >= 0 || offset <= size) { "Offset $offset is out of bounds." }
position = size - offset position = size - offset
return this return this
} }
override fun stringAscii( override fun stringAscii(
maxByteLength: UInt, maxByteLength: Int,
nullTerminated: Boolean, nullTerminated: Boolean,
dropRemaining: Boolean, dropRemaining: Boolean,
): String = ): String =
buildString { buildString {
for (i in 0u until maxByteLength) { for (i in 0 until maxByteLength) {
val codePoint = u8() val codePoint = u8()
if (nullTerminated && codePoint == ZERO_U8) { if (nullTerminated && codePoint == ZERO_U8) {
if (dropRemaining) { if (dropRemaining) {
seek((maxByteLength - i - 1u).toInt()) seek(maxByteLength - i - 1)
} }
break break
@ -58,19 +57,19 @@ protected constructor(protected val offset: UInt) : WritableCursor {
} }
override fun stringUtf16( override fun stringUtf16(
maxByteLength: UInt, maxByteLength: Int,
nullTerminated: Boolean, nullTerminated: Boolean,
dropRemaining: Boolean, dropRemaining: Boolean,
): String = ): String =
buildString { buildString {
val len = maxByteLength / 2u val len = maxByteLength / 2
for (i in 0u until len) { for (i in 0 until len) {
val codePoint = u16() val codePoint = u16()
if (nullTerminated && codePoint == ZERO_U16) { if (nullTerminated && codePoint == ZERO_U16) {
if (dropRemaining) { if (dropRemaining) {
seek((maxByteLength - 2u * i - 2u).toInt()) seek(maxByteLength - 2 * i - 2)
} }
break break
@ -82,7 +81,7 @@ protected constructor(protected val offset: UInt) : WritableCursor {
override fun writeU8Array(array: UByteArray): WritableCursor { override fun writeU8Array(array: UByteArray): WritableCursor {
val len = array.size val len = array.size
requireSize(len.toUInt()) requireSize(len)
for (i in 0 until len) { for (i in 0 until len) {
writeU8(array[i]) writeU8(array[i])
@ -93,7 +92,7 @@ protected constructor(protected val offset: UInt) : WritableCursor {
override fun writeU16Array(array: UShortArray): WritableCursor { override fun writeU16Array(array: UShortArray): WritableCursor {
val len = array.size val len = array.size
requireSize(2u * len.toUInt()) requireSize(2 * len)
for (i in 0 until len) { for (i in 0 until len) {
writeU16(array[i]) writeU16(array[i])
@ -104,7 +103,7 @@ protected constructor(protected val offset: UInt) : WritableCursor {
override fun writeU32Array(array: UIntArray): WritableCursor { override fun writeU32Array(array: UIntArray): WritableCursor {
val len = array.size val len = array.size
requireSize(4u * len.toUInt()) requireSize(4 * len)
for (i in 0 until len) { for (i in 0 until len) {
writeU32(array[i]) writeU32(array[i])
@ -115,7 +114,7 @@ protected constructor(protected val offset: UInt) : WritableCursor {
override fun writeI32Array(array: IntArray): WritableCursor { override fun writeI32Array(array: IntArray): WritableCursor {
val len = array.size val len = array.size
requireSize(4u * len.toUInt()) requireSize(4 * len)
for (i in 0 until len) { for (i in 0 until len) {
writeI32(array[i]) writeI32(array[i])
@ -127,51 +126,45 @@ protected constructor(protected val offset: UInt) : WritableCursor {
override fun writeCursor(other: Cursor): WritableCursor { override fun writeCursor(other: Cursor): WritableCursor {
val size = other.bytesLeft val size = other.bytesLeft
requireSize(size) requireSize(size)
for (i in 0 until size) {
for (i in 0u until (size / 4u)) { writeI8(other.i8())
writeU32(other.u32())
} }
for (i in 0u until (size % 4u)) {
writeU8(other.u8())
}
position += size
return this return this
} }
override fun writeStringAscii(str: String, byteLength: UInt): WritableCursor { override fun writeStringAscii(str: String, byteLength: Int): WritableCursor {
requireSize(byteLength) requireSize(byteLength)
val len = min(byteLength.toInt(), str.length) val len = min(byteLength, str.length)
for (i in 0 until len) { for (i in 0 until len) {
writeU8(str[i].toByte().toUByte()) writeI8(str[i].toByte())
} }
val padLen = byteLength.toInt() - len val padLen = byteLength - len
for (i in 0 until padLen) { for (i in 0 until padLen) {
writeU8(0u) writeI8(0)
} }
return this return this
} }
override fun writeStringUtf16(str: String, byteLength: UInt): WritableCursor { override fun writeStringUtf16(str: String, byteLength: Int): WritableCursor {
requireSize(byteLength) requireSize(byteLength)
val maxLen = byteLength.toInt() / 2 val maxLen = byteLength / 2
val len = min(maxLen, str.length) val len = min(maxLen, str.length)
for (i in 0 until len) { for (i in 0 until len) {
writeU16(str[i].toShort().toUShort()) writeI16(str[i].toShort())
} }
val padLen = maxLen - len val padLen = maxLen - len
for (i in 0 until padLen) { for (i in 0 until padLen) {
writeU16(0u) writeI16(0)
} }
return this return this
@ -180,7 +173,7 @@ protected constructor(protected val offset: UInt) : WritableCursor {
/** /**
* Throws an error if less than [size] bytes are left at [position]. * Throws an error if less than [size] bytes are left at [position].
*/ */
protected fun requireSize(size: UInt) { protected fun requireSize(size: Int) {
val left = this.size - position val left = this.size - position
require(size <= left) { "$size Bytes required but only $left available." } require(size <= left) { "$size Bytes required but only $left available." }

View File

@ -10,12 +10,12 @@ import world.phantasmal.lib.buffer.Buffer
*/ */
class BufferCursor( class BufferCursor(
private val buffer: Buffer, private val buffer: Buffer,
offset: UInt = 0u, offset: Int = 0,
size: UInt = buffer.size - offset, size: Int = buffer.size - offset,
) : AbstractWritableCursor(offset) { ) : AbstractWritableCursor(offset) {
private var _size = size private var _size = size
override var size: UInt override var size: Int
get() = _size get() = _size
set(value) { set(value) {
if (value > _size) { if (value > _size) {
@ -52,13 +52,13 @@ class BufferCursor(
override fun u16(): UShort { override fun u16(): UShort {
val r = buffer.getU16(absolutePosition) val r = buffer.getU16(absolutePosition)
position += 2u position += 2
return r return r
} }
override fun u32(): UInt { override fun u32(): UInt {
val r = buffer.getU32(absolutePosition) val r = buffer.getU32(absolutePosition)
position += 4u position += 4
return r return r
} }
@ -70,28 +70,28 @@ class BufferCursor(
override fun i16(): Short { override fun i16(): Short {
val r = buffer.getI16(absolutePosition) val r = buffer.getI16(absolutePosition)
position += 2u position += 2
return r return r
} }
override fun i32(): Int { override fun i32(): Int {
val r = buffer.getI32(absolutePosition) val r = buffer.getI32(absolutePosition)
position += 4u position += 4
return r return r
} }
override fun f32(): Float { override fun f32(): Float {
val r = buffer.getF32(absolutePosition) val r = buffer.getF32(absolutePosition)
position += 4u position += 4
return r return r
} }
override fun u8Array(n: UInt): UByteArray { override fun u8Array(n: Int): UByteArray {
requireSize(n) requireSize(n)
val array = UByteArray(n.toInt()) val array = UByteArray(n)
for (i in 0 until n.toInt()) { for (i in 0 until n) {
array[i] = buffer.getU8(absolutePosition) array[i] = buffer.getU8(absolutePosition)
position++ position++
} }
@ -99,123 +99,123 @@ class BufferCursor(
return array return array
} }
override fun u16Array(n: UInt): UShortArray { override fun u16Array(n: Int): UShortArray {
requireSize(2u * n) requireSize(2 * n)
val array = UShortArray(n.toInt()) val array = UShortArray(n)
for (i in 0 until n.toInt()) { for (i in 0 until n) {
array[i] = buffer.getU16(absolutePosition) array[i] = buffer.getU16(absolutePosition)
position += 2u position += 2
} }
return array return array
} }
override fun u32Array(n: UInt): UIntArray { override fun u32Array(n: Int): UIntArray {
requireSize(4u * n) requireSize(4 * n)
val array = UIntArray(n.toInt()) val array = UIntArray(n)
for (i in 0 until n.toInt()) { for (i in 0 until n) {
array[i] = buffer.getU32(absolutePosition) array[i] = buffer.getU32(absolutePosition)
position += 4u position += 4
} }
return array return array
} }
override fun i32Array(n: UInt): IntArray { override fun i32Array(n: Int): IntArray {
requireSize(4u * n) requireSize(4 * n)
val array = IntArray(n.toInt()) val array = IntArray(n)
for (i in 0 until n.toInt()) { for (i in 0 until n) {
array[i] = buffer.getI32(absolutePosition) array[i] = buffer.getI32(absolutePosition)
position += 4u position += 4
} }
return array return array
} }
override fun take(size: UInt): Cursor { override fun take(size: Int): Cursor {
val wrapper = BufferCursor(buffer, offset = absolutePosition, size) val wrapper = BufferCursor(buffer, offset = absolutePosition, size)
position += size position += size
return wrapper return wrapper
} }
override fun buffer(size: UInt): Buffer { override fun buffer(size: Int): Buffer {
val wrapper = buffer.slice(offset = absolutePosition, size) val wrapper = buffer.slice(offset = absolutePosition, size)
position += size position += size
return wrapper return wrapper
} }
override fun writeU8(value: UByte): WritableCursor { override fun writeU8(value: UByte): WritableCursor {
ensureSpace(1u) ensureSpace(1)
buffer.setU8(absolutePosition, value) buffer.setU8(absolutePosition, value)
position++ position++
return this return this
} }
override fun writeU16(value: UShort): WritableCursor { override fun writeU16(value: UShort): WritableCursor {
ensureSpace(2u) ensureSpace(2)
buffer.setU16(absolutePosition, value) buffer.setU16(absolutePosition, value)
position += 2u position += 2
return this return this
} }
override fun writeU32(value: UInt): WritableCursor { override fun writeU32(value: UInt): WritableCursor {
ensureSpace(4u) ensureSpace(4)
buffer.setU32(absolutePosition, value) buffer.setU32(absolutePosition, value)
position += 4u position += 4
return this return this
} }
override fun writeI8(value: Byte): WritableCursor { override fun writeI8(value: Byte): WritableCursor {
ensureSpace(1u) ensureSpace(1)
buffer.setI8(absolutePosition, value) buffer.setI8(absolutePosition, value)
position++ position++
return this return this
} }
override fun writeI16(value: Short): WritableCursor { override fun writeI16(value: Short): WritableCursor {
ensureSpace(2u) ensureSpace(2)
buffer.setI16(absolutePosition, value) buffer.setI16(absolutePosition, value)
position += 2u position += 2
return this return this
} }
override fun writeI32(value: Int): WritableCursor { override fun writeI32(value: Int): WritableCursor {
ensureSpace(4u) ensureSpace(4)
buffer.setI32(absolutePosition, value) buffer.setI32(absolutePosition, value)
position += 4u position += 4
return this return this
} }
override fun writeF32(value: Float): WritableCursor { override fun writeF32(value: Float): WritableCursor {
ensureSpace(4u) ensureSpace(4)
buffer.setF32(absolutePosition, value) buffer.setF32(absolutePosition, value)
position += 4u position += 4
return this return this
} }
override fun writeU8Array(array: UByteArray): WritableCursor { override fun writeU8Array(array: UByteArray): WritableCursor {
ensureSpace(array.size.toUInt()) ensureSpace(array.size)
return super.writeU8Array(array) return super.writeU8Array(array)
} }
override fun writeU16Array(array: UShortArray): WritableCursor { override fun writeU16Array(array: UShortArray): WritableCursor {
ensureSpace(2u * array.size.toUInt()) ensureSpace(2 * array.size)
return super.writeU16Array(array) return super.writeU16Array(array)
} }
override fun writeU32Array(array: UIntArray): WritableCursor { override fun writeU32Array(array: UIntArray): WritableCursor {
ensureSpace(4u * array.size.toUInt()) ensureSpace(4 * array.size)
return super.writeU32Array(array) return super.writeU32Array(array)
} }
override fun writeI32Array(array: IntArray): WritableCursor { override fun writeI32Array(array: IntArray): WritableCursor {
ensureSpace(4u * array.size.toUInt()) ensureSpace(4 * array.size)
return super.writeI32Array(array) return super.writeI32Array(array)
} }
@ -225,21 +225,21 @@ class BufferCursor(
return super.writeCursor(other) return super.writeCursor(other)
} }
override fun writeStringAscii(str: String, byteLength: UInt): WritableCursor { override fun writeStringAscii(str: String, byteLength: Int): WritableCursor {
ensureSpace(byteLength) ensureSpace(byteLength)
return super.writeStringAscii(str, byteLength) return super.writeStringAscii(str, byteLength)
} }
override fun writeStringUtf16(str: String, byteLength: UInt): WritableCursor { override fun writeStringUtf16(str: String, byteLength: Int): WritableCursor {
ensureSpace(byteLength) ensureSpace(byteLength)
return super.writeStringUtf16(str, byteLength) return super.writeStringUtf16(str, byteLength)
} }
private fun ensureSpace(size: UInt) { private fun ensureSpace(size: Int) {
val needed = (position + size).toInt() - _size.toInt() val needed = (position + size) - _size
if (needed > 0) { if (needed > 0) {
_size += needed.toUInt() _size += needed
if (buffer.size < offset + _size) { if (buffer.size < offset + _size) {
buffer.size = offset + _size buffer.size = offset + _size
@ -247,3 +247,6 @@ class BufferCursor(
} }
} }
} }
fun Buffer.cursor(): BufferCursor =
BufferCursor(this)

View File

@ -7,21 +7,21 @@ import world.phantasmal.lib.buffer.Buffer
* A cursor for reading binary data. * A cursor for reading binary data.
*/ */
interface Cursor { interface Cursor {
val size: UInt val size: Int
/** /**
* The position from where bytes will be read or written. * The position from where bytes will be read or written.
*/ */
val position: UInt val position: Int
/** /**
* Byte order mode. * Byte order mode.
*/ */
var endianness: Endianness var endianness: Endianness
val bytesLeft: UInt val bytesLeft: Int
fun hasBytesLeft(bytes: UInt = 1u): Boolean fun hasBytesLeft(bytes: Int = 1): Boolean
/** /**
* Seek forward or backward by a number of bytes. * Seek forward or backward by a number of bytes.
@ -34,16 +34,16 @@ interface Cursor {
/** /**
* Seek forward from the start of the cursor by a number of bytes. * Seek forward from the start of the cursor by a number of bytes.
* *
* @param offset smaller than size * @param offset greater or equal to 0 and smaller than size
*/ */
fun seekStart(offset: UInt): Cursor fun seekStart(offset: Int): Cursor
/** /**
* Seek backward from the end of the cursor by a number of bytes. * Seek backward from the end of the cursor by a number of bytes.
* *
* @param offset smaller than size * @param offset greater or equal to 0 and smaller than size
*/ */
fun seekEnd(offset: UInt): Cursor fun seekEnd(offset: Int): Cursor
/** /**
* Reads an unsigned 8-bit integer and increments position by 1. * Reads an unsigned 8-bit integer and increments position by 1.
@ -83,36 +83,36 @@ interface Cursor {
/** /**
* Reads [n] unsigned 8-bit integers and increments position by [n]. * Reads [n] unsigned 8-bit integers and increments position by [n].
*/ */
fun u8Array(n: UInt): UByteArray fun u8Array(n: Int): UByteArray
/** /**
* Reads [n] unsigned 16-bit integers and increments position by 2[n]. * Reads [n] unsigned 16-bit integers and increments position by 2[n].
*/ */
fun u16Array(n: UInt): UShortArray fun u16Array(n: Int): UShortArray
/** /**
* Reads [n] unsigned 32-bit integers and increments position by 4[n]. * Reads [n] unsigned 32-bit integers and increments position by 4[n].
*/ */
fun u32Array(n: UInt): UIntArray fun u32Array(n: Int): UIntArray
/** /**
* Reads [n] signed 32-bit integers and increments position by 4[n]. * Reads [n] signed 32-bit integers and increments position by 4[n].
*/ */
fun i32Array(n: UInt): IntArray fun i32Array(n: Int): IntArray
/** /**
* Consumes a variable number of bytes. * Consumes a variable number of bytes.
* *
* @param size the amount bytes to consume. * @param size the amount bytes to consume.
* @return a write-through view containing size bytes. * @return a view containing size bytes.
*/ */
fun take(size: UInt): Cursor fun take(size: Int): Cursor
/** /**
* Consumes up to [maxByteLength] bytes. * Consumes up to [maxByteLength] bytes.
*/ */
fun stringAscii( fun stringAscii(
maxByteLength: UInt, maxByteLength: Int,
nullTerminated: Boolean, nullTerminated: Boolean,
dropRemaining: Boolean, dropRemaining: Boolean,
): String ): String
@ -121,7 +121,7 @@ interface Cursor {
* Consumes up to [maxByteLength] bytes. * Consumes up to [maxByteLength] bytes.
*/ */
fun stringUtf16( fun stringUtf16(
maxByteLength: UInt, maxByteLength: Int,
nullTerminated: Boolean, nullTerminated: Boolean,
dropRemaining: Boolean, dropRemaining: Boolean,
): String ): String
@ -129,5 +129,5 @@ interface Cursor {
/** /**
* Returns a buffer with a copy of [size] bytes at [position]. * Returns a buffer with a copy of [size] bytes at [position].
*/ */
fun buffer(size: UInt): Buffer fun buffer(size: Int): Buffer
} }

View File

@ -4,7 +4,13 @@ package world.phantasmal.lib.cursor
* A cursor for reading and writing binary data. * A cursor for reading and writing binary data.
*/ */
interface WritableCursor : Cursor { interface WritableCursor : Cursor {
override var size: UInt override var size: Int
override fun seek(offset: Int): WritableCursor
override fun seekStart(offset: Int): WritableCursor
override fun seekEnd(offset: Int): WritableCursor
/** /**
* Writes an unsigned 8-bit integer and increments position by 1. * Writes an unsigned 8-bit integer and increments position by 1.
@ -74,12 +80,12 @@ interface WritableCursor : Cursor {
* Writes [byteLength] characters of [str]. If [str] is shorter than [byteLength], nul bytes * Writes [byteLength] characters of [str]. If [str] is shorter than [byteLength], nul bytes
* will be inserted until [byteLength] bytes have been written. * will be inserted until [byteLength] bytes have been written.
*/ */
fun writeStringAscii(str: String, byteLength: UInt): WritableCursor fun writeStringAscii(str: String, byteLength: Int): WritableCursor
/** /**
* Writes characters of [str] without writing more than [byteLength] bytes. If less than * Writes characters of [str] without writing more than [byteLength] bytes. If less than
* [byteLength] bytes can be written this way, nul bytes will be inserted until [byteLength] * [byteLength] bytes can be written this way, nul bytes will be inserted until [byteLength]
* bytes have been written. * bytes have been written.
*/ */
fun writeStringUtf16(str: String, byteLength: UInt): WritableCursor fun writeStringUtf16(str: String, byteLength: Int): WritableCursor
} }

View File

@ -7,9 +7,9 @@ import world.phantasmal.lib.cursor.Cursor
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
class IffChunk(val type: UInt, val data: Cursor) class IffChunk(val type: Int, val data: Cursor)
class IffChunkHeader(val type: UInt, val size: UInt) class IffChunkHeader(val type: Int, val size: Int)
/** /**
* PSO uses a little endian variant of the IFF format. * PSO uses a little endian variant of the IFF format.
@ -27,16 +27,16 @@ fun parseIffHeaders(cursor: Cursor): PwResult<List<IffChunkHeader>> =
private fun <T> parse( private fun <T> parse(
cursor: Cursor, cursor: Cursor,
getChunk: (Cursor, type: UInt, size: UInt) -> T, getChunk: (Cursor, type: Int, size: Int) -> T,
): PwResult<List<T>> { ): PwResult<List<T>> {
val result = PwResult.build<List<T>>(logger) val result = PwResult.build<List<T>>(logger)
val chunks = mutableListOf<T>() val chunks = mutableListOf<T>()
var corrupted = false var corrupted = false
while (cursor.bytesLeft >= 8u) { while (cursor.bytesLeft >= 8) {
val type = cursor.u32() val type = cursor.i32()
val sizePos = cursor.position val sizePos = cursor.position
val size = cursor.u32() val size = cursor.i32()
if (size > cursor.bytesLeft) { if (size > cursor.bytesLeft) {
corrupted = true corrupted = true

View File

@ -8,7 +8,7 @@ import world.phantasmal.lib.fileFormats.Vec3
import world.phantasmal.lib.fileFormats.parseIff import world.phantasmal.lib.fileFormats.parseIff
import world.phantasmal.lib.fileFormats.vec3F32 import world.phantasmal.lib.fileFormats.vec3F32
private const val NJCM: UInt = 0x4D434A4Eu private const val NJCM: Int = 0x4D434A4E
class NjObject<Model>( class NjObject<Model>(
val evaluationFlags: NjEvaluationFlags, val evaluationFlags: NjEvaluationFlags,
@ -72,7 +72,7 @@ private fun <Model, Context> parseSiblingObjects(
val skip = (evalFlags and 0b1000000u) != 0u val skip = (evalFlags and 0b1000000u) != 0u
val shapeSkip = (evalFlags and 0b10000000u) != 0u val shapeSkip = (evalFlags and 0b10000000u) != 0u
val modelOffset = cursor.u32() val modelOffset = cursor.i32()
val pos = cursor.vec3F32() val pos = cursor.vec3F32()
val rotation = Vec3( val rotation = Vec3(
angleToRad(cursor.i32()), angleToRad(cursor.i32()),
@ -80,24 +80,24 @@ private fun <Model, Context> parseSiblingObjects(
angleToRad(cursor.i32()), angleToRad(cursor.i32()),
) )
val scale = cursor.vec3F32() val scale = cursor.vec3F32()
val childOffset = cursor.u32() val childOffset = cursor.i32()
val siblingOffset = cursor.u32() val siblingOffset = cursor.i32()
val model = if (modelOffset == 0u) { val model = if (modelOffset == 0) {
null null
} else { } else {
cursor.seekStart(modelOffset) cursor.seekStart(modelOffset)
parse_model(cursor, context) parse_model(cursor, context)
} }
val children = if (childOffset == 0u) { val children = if (childOffset == 0) {
emptyList() emptyList()
} else { } else {
cursor.seekStart(childOffset) cursor.seekStart(childOffset)
parseSiblingObjects(cursor, parse_model, context) parseSiblingObjects(cursor, parse_model, context)
} }
val siblings = if (siblingOffset == 0u) { val siblings = if (siblingOffset == 0) {
emptyList() emptyList()
} else { } else {
cursor.seekStart(siblingOffset) cursor.seekStart(siblingOffset)

View File

@ -62,7 +62,7 @@ sealed class NjcmChunk(val typeId: UByte) {
class Bits(typeId: UByte, val srcAlpha: UByte, val dstAlpha: UByte) : NjcmChunk(typeId) class Bits(typeId: UByte, val srcAlpha: UByte, val dstAlpha: UByte) : NjcmChunk(typeId)
class CachePolygonList(val cacheIndex: UByte, val offset: UInt) : NjcmChunk(4u) class CachePolygonList(val cacheIndex: UByte, val offset: Int) : NjcmChunk(4u)
class DrawPolygonList(val cacheIndex: UByte) : NjcmChunk(5u) class DrawPolygonList(val cacheIndex: UByte) : NjcmChunk(5u)
@ -122,15 +122,15 @@ class NjcmErgb(
val b: UByte, val b: UByte,
) )
fun parseNjcmModel(cursor: Cursor, cachedChunkOffsets: MutableMap<UByte, UInt>): NjcmModel { fun parseNjcmModel(cursor: Cursor, cachedChunkOffsets: MutableMap<UByte, Int>): NjcmModel {
val vlistOffset = cursor.u32() // Vertex list val vlistOffset = cursor.i32() // Vertex list
val plistOffset = cursor.u32() // Triangle strip index list val plistOffset = cursor.i32() // Triangle strip index list
val boundingSphereCenter = cursor.vec3F32() val boundingSphereCenter = cursor.vec3F32()
val boundingSphereRadius = cursor.f32() val boundingSphereRadius = cursor.f32()
val vertices: MutableList<NjcmVertex> = mutableListOf() val vertices: MutableList<NjcmVertex> = mutableListOf()
val meshes: MutableList<NjcmTriangleStrip> = mutableListOf() val meshes: MutableList<NjcmTriangleStrip> = mutableListOf()
if (vlistOffset != 0u) { if (vlistOffset != 0) {
cursor.seekStart(vlistOffset) cursor.seekStart(vlistOffset)
for (chunk in parseChunks(cursor, cachedChunkOffsets, true)) { for (chunk in parseChunks(cursor, cachedChunkOffsets, true)) {
@ -148,7 +148,7 @@ fun parseNjcmModel(cursor: Cursor, cachedChunkOffsets: MutableMap<UByte, UInt>):
} }
} }
if (plistOffset != 0u) { if (plistOffset != 0) {
cursor.seekStart(plistOffset) cursor.seekStart(plistOffset)
var textureId: UInt? = null var textureId: UInt? = null
@ -203,7 +203,7 @@ fun parseNjcmModel(cursor: Cursor, cachedChunkOffsets: MutableMap<UByte, UInt>):
// TODO: don't reparse when DrawPolygonList chunk is encountered. // TODO: don't reparse when DrawPolygonList chunk is encountered.
private fun parseChunks( private fun parseChunks(
cursor: Cursor, cursor: Cursor,
cachedChunkOffsets: MutableMap<UByte, UInt>, cachedChunkOffsets: MutableMap<UByte, Int>,
wideEndChunks: Boolean, wideEndChunks: Boolean,
): List<NjcmChunk> { ): List<NjcmChunk> {
val chunks: MutableList<NjcmChunk> = mutableListOf() val chunks: MutableList<NjcmChunk> = mutableListOf()
@ -214,7 +214,7 @@ private fun parseChunks(
val flags = cursor.u8() val flags = cursor.u8()
val flagsUInt = flags.toUInt() val flagsUInt = flags.toUInt()
val chunkStartPosition = cursor.position val chunkStartPosition = cursor.position
var size = 0u var size = 0
when (typeId.toInt()) { when (typeId.toInt()) {
0 -> { 0 -> {
@ -253,7 +253,7 @@ private fun parseChunks(
)) ))
} }
in 8..9 -> { in 8..9 -> {
size = 2u size = 2
val textureBitsAndId = cursor.u16().toUInt() val textureBitsAndId = cursor.u16().toUInt()
chunks.add(NjcmChunk.Tiny( chunks.add(NjcmChunk.Tiny(
@ -269,7 +269,7 @@ private fun parseChunks(
)) ))
} }
in 17..31 -> { in 17..31 -> {
size = 2u + 2u * cursor.u16() size = 2 + 2 * cursor.i16()
var diffuse: NjcmArgb? = null var diffuse: NjcmArgb? = null
var ambient: NjcmArgb? = null var ambient: NjcmArgb? = null
@ -312,32 +312,32 @@ private fun parseChunks(
)) ))
} }
in 32..50 -> { in 32..50 -> {
size = 2u + 4u * cursor.u16() size = 2 + 4 * cursor.i16()
chunks.add(NjcmChunk.Vertex( chunks.add(NjcmChunk.Vertex(
typeId, typeId,
vertices = parseVertexChunk(cursor, typeId, flags), vertices = parseVertexChunk(cursor, typeId, flags),
)) ))
} }
in 56..58 -> { in 56..58 -> {
size = 2u + 2u * cursor.u16() size = 2 + 2 * cursor.i16()
chunks.add(NjcmChunk.Volume( chunks.add(NjcmChunk.Volume(
typeId, typeId,
)) ))
} }
in 64..75 -> { in 64..75 -> {
size = 2u + 2u * cursor.u16() size = 2 + 2 * cursor.i16()
chunks.add(NjcmChunk.Strip( chunks.add(NjcmChunk.Strip(
typeId, typeId,
triangleStrips = parseTriangleStripChunk(cursor, typeId, flags), triangleStrips = parseTriangleStripChunk(cursor, typeId, flags),
)) ))
} }
255 -> { 255 -> {
size = if (wideEndChunks) 2u else 0u size = if (wideEndChunks) 2 else 0
chunks.add(NjcmChunk.End) chunks.add(NjcmChunk.End)
loop = false loop = false
} }
else -> { else -> {
size = 2u + 2u * cursor.u16() size = 2 + 2 * cursor.i16()
chunks.add(NjcmChunk.Unknown( chunks.add(NjcmChunk.Unknown(
typeId, typeId,
)) ))

View File

@ -6,9 +6,9 @@ import world.phantasmal.lib.cursor.Cursor
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
private const val DC_GC_OBJECT_CODE_OFFSET = 468u private const val DC_GC_OBJECT_CODE_OFFSET = 468
private const val PC_OBJECT_CODE_OFFSET = 920u private const val PC_OBJECT_CODE_OFFSET = 920
private const val BB_OBJECT_CODE_OFFSET = 4652u private const val BB_OBJECT_CODE_OFFSET = 4652
class BinFile( class BinFile(
val format: BinFormat, val format: BinFormat,
@ -40,17 +40,19 @@ enum class BinFormat {
} }
fun parseBin(cursor: Cursor): BinFile { fun parseBin(cursor: Cursor): BinFile {
val objectCodeOffset = cursor.u32() val objectCodeOffset = cursor.i32()
val labelOffsetTableOffset = cursor.u32() // Relative offsets val labelOffsetTableOffset = cursor.i32() // Relative offsets
val size = cursor.u32() val size = cursor.i32()
cursor.seek(4) // Always seems to be 0xFFFFFFFF. cursor.seek(4) // Always seems to be 0xFFFFFFFF.
val format = when (objectCodeOffset) { val format = when (objectCodeOffset) {
DC_GC_OBJECT_CODE_OFFSET -> BinFormat.DC_GC DC_GC_OBJECT_CODE_OFFSET -> BinFormat.DC_GC
BB_OBJECT_CODE_OFFSET -> BinFormat.BB
PC_OBJECT_CODE_OFFSET -> BinFormat.PC PC_OBJECT_CODE_OFFSET -> BinFormat.PC
BB_OBJECT_CODE_OFFSET -> BinFormat.BB
else -> { else -> {
logger.warn { "Object code at unexpected offset, assuming file is a PC file." } logger.warn {
"Object code at unexpected offset $objectCodeOffset, assuming file is a PC file."
}
BinFormat.PC BinFormat.PC
} }
} }
@ -65,15 +67,15 @@ fun parseBin(cursor: Cursor): BinFile {
cursor.seek(1) cursor.seek(1)
language = cursor.u8().toUInt() language = cursor.u8().toUInt()
questId = cursor.u16().toUInt() questId = cursor.u16().toUInt()
questName = cursor.stringAscii(32u, nullTerminated = true, dropRemaining = true) questName = cursor.stringAscii(32, nullTerminated = true, dropRemaining = true)
shortDescription = cursor.stringAscii(128u, nullTerminated = true, dropRemaining = true) shortDescription = cursor.stringAscii(128, nullTerminated = true, dropRemaining = true)
longDescription = cursor.stringAscii(288u, nullTerminated = true, dropRemaining = true) longDescription = cursor.stringAscii(288, nullTerminated = true, dropRemaining = true)
} else { } else {
questId = cursor.u32() questId = cursor.u32()
language = cursor.u32() language = cursor.u32()
questName = cursor.stringUtf16(64u, nullTerminated = true, dropRemaining = true) questName = cursor.stringUtf16(64, nullTerminated = true, dropRemaining = true)
shortDescription = cursor.stringUtf16(256u, nullTerminated = true, dropRemaining = true) shortDescription = cursor.stringUtf16(256, nullTerminated = true, dropRemaining = true)
longDescription = cursor.stringUtf16(576u, nullTerminated = true, dropRemaining = true) longDescription = cursor.stringUtf16(576, nullTerminated = true, dropRemaining = true)
} }
if (size != cursor.size) { if (size != cursor.size) {
@ -82,12 +84,12 @@ fun parseBin(cursor: Cursor): BinFile {
val shopItems = if (format == BinFormat.BB) { val shopItems = if (format == BinFormat.BB) {
cursor.seek(4) // Skip padding. cursor.seek(4) // Skip padding.
cursor.u32Array(932u) cursor.u32Array(932)
} else { } else {
UIntArray(0) UIntArray(0)
} }
val labelOffsetCount = (cursor.size - labelOffsetTableOffset) / 4u val labelOffsetCount = (cursor.size - labelOffsetTableOffset) / 4
val labelOffsets = cursor val labelOffsets = cursor
.seekStart(labelOffsetTableOffset) .seekStart(labelOffsetTableOffset)
.i32Array(labelOffsetCount) .i32Array(labelOffsetCount)

View File

@ -6,13 +6,13 @@ import world.phantasmal.lib.cursor.Cursor
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
private const val EVENT_ACTION_SPAWN_NPCS: UByte = 0x8u private const val EVENT_ACTION_SPAWN_NPCS = 0x8
private const val EVENT_ACTION_UNLOCK: UByte = 0xAu private const val EVENT_ACTION_UNLOCK = 0xA
private const val EVENT_ACTION_LOCK: UByte = 0xBu private const val EVENT_ACTION_LOCK = 0xB
private const val EVENT_ACTION_TRIGGER_EVENT: UByte = 0xCu private const val EVENT_ACTION_TRIGGER_EVENT = 0xC
const val OBJECT_BYTE_SIZE = 68u const val OBJECT_BYTE_SIZE = 68
const val NPC_BYTE_SIZE = 72u const val NPC_BYTE_SIZE = 72
class DatFile( class DatFile(
val objs: List<DatEntity>, val objs: List<DatEntity>,
@ -22,7 +22,7 @@ class DatFile(
) )
class DatEntity( class DatEntity(
var areaId: UInt, var areaId: Int,
val data: Buffer, val data: Buffer,
) )
@ -32,7 +32,7 @@ class DatEvent(
var wave: UShort, var wave: UShort,
var delay: UShort, var delay: UShort,
val actions: MutableList<DatEventAction>, val actions: MutableList<DatEventAction>,
val areaId: UInt, val areaId: Int,
val unknown: UShort, val unknown: UShort,
) )
@ -56,10 +56,10 @@ sealed class DatEventAction {
} }
class DatUnknown( class DatUnknown(
val entityType: UInt, val entityType: Int,
val totalSize: UInt, val totalSize: Int,
val areaId: UInt, val areaId: Int,
val entitiesSize: UInt, val entitiesSize: Int,
val data: UByteArray, val data: UByteArray,
) )
@ -70,24 +70,24 @@ fun parseDat(cursor: Cursor): DatFile {
val unknowns = mutableListOf<DatUnknown>() val unknowns = mutableListOf<DatUnknown>()
while (cursor.hasBytesLeft()) { while (cursor.hasBytesLeft()) {
val entityType = cursor.u32() val entityType = cursor.i32()
val totalSize = cursor.u32() val totalSize = cursor.i32()
val areaId = cursor.u32() val areaId = cursor.i32()
val entitiesSize = cursor.u32() val entitiesSize = cursor.i32()
if (entityType == 0u) { if (entityType == 0) {
break break
} else { } else {
require(entitiesSize == totalSize - 16u) { require(entitiesSize == totalSize - 16) {
"Malformed DAT file. Expected an entities size of ${totalSize - 16u}, got ${entitiesSize}." "Malformed DAT file. Expected an entities size of ${totalSize - 16}, got ${entitiesSize}."
} }
val entitiesCursor = cursor.take(entitiesSize) val entitiesCursor = cursor.take(entitiesSize)
when (entityType) { when (entityType) {
1u -> parseEntities(entitiesCursor, areaId, objs, OBJECT_BYTE_SIZE) 1 -> parseEntities(entitiesCursor, areaId, objs, OBJECT_BYTE_SIZE)
2u -> parseEntities(entitiesCursor, areaId, npcs, NPC_BYTE_SIZE) 2 -> parseEntities(entitiesCursor, areaId, npcs, NPC_BYTE_SIZE)
3u -> parseEvents(entitiesCursor, areaId, events) 3 -> parseEvents(entitiesCursor, areaId, events)
else -> { else -> {
// Unknown entity types 4 and 5 (challenge mode). // Unknown entity types 4 and 5 (challenge mode).
unknowns.add(DatUnknown( unknowns.add(DatUnknown(
@ -118,13 +118,13 @@ fun parseDat(cursor: Cursor): DatFile {
private fun parseEntities( private fun parseEntities(
cursor: Cursor, cursor: Cursor,
areaId: UInt, areaId: Int,
entities: MutableList<DatEntity>, entities: MutableList<DatEntity>,
entitySize: UInt, entitySize: Int,
) { ) {
val entityCount = cursor.size / entitySize val entityCount = cursor.size / entitySize
repeat(entityCount.toInt()) { repeat(entityCount) {
entities.add(DatEntity( entities.add(DatEntity(
areaId, areaId,
data = cursor.buffer(entitySize), data = cursor.buffer(entitySize),
@ -132,29 +132,29 @@ private fun parseEntities(
} }
} }
private fun parseEvents(cursor: Cursor, areaId: UInt, events: MutableList<DatEvent>) { private fun parseEvents(cursor: Cursor, areaId: Int, events: MutableList<DatEvent>) {
val actionsOffset = cursor.u32() val actionsOffset = cursor.i32()
cursor.seek(4) // Always 0x10 cursor.seek(4) // Always 0x10
val eventCount = cursor.u32() val eventCount = cursor.i32()
cursor.seek(3) // Always 0 cursor.seek(3) // Always 0
val eventType = cursor.u8() val eventType = cursor.u8()
require(eventType == (0x32u).toUByte()) { require(eventType != (0x32u).toUByte()) {
"Can't parse challenge mode quests yet." "Can't parse challenge mode quests yet."
} }
cursor.seekStart(actionsOffset) cursor.seekStart(actionsOffset)
val actionsCursor = cursor.take(cursor.bytesLeft) val actionsCursor = cursor.take(cursor.bytesLeft)
cursor.seekStart(16u) cursor.seekStart(16)
repeat(eventCount.toInt()) { repeat(eventCount) {
val id = cursor.u32() val id = cursor.u32()
cursor.seek(4) // Always 0x100 cursor.seek(4) // Always 0x100
val sectionId = cursor.u16() val sectionId = cursor.u16()
val wave = cursor.u16() val wave = cursor.u16()
val delay = cursor.u16() val delay = cursor.u16()
val unknown = cursor.u16() // "wavesetting"? val unknown = cursor.u16() // "wavesetting"?
val eventActionsOffset = cursor.u32() val eventActionsOffset = cursor.i32()
val actions: MutableList<DatEventAction> = val actions: MutableList<DatEventAction> =
if (eventActionsOffset < actionsCursor.size) { if (eventActionsOffset < actionsCursor.size) {
@ -178,7 +178,7 @@ private fun parseEvents(cursor: Cursor, areaId: UInt, events: MutableList<DatEve
if (cursor.position != actionsOffset) { if (cursor.position != actionsOffset) {
logger.warn { logger.warn {
"Read ${cursor.position - 16u} bytes of event data instead of expected ${actionsOffset - 16u}." "Read ${cursor.position - 16} bytes of event data instead of expected ${actionsOffset - 16}."
} }
} }
@ -204,8 +204,8 @@ private fun parseEventActions(cursor: Cursor): MutableList<DatEventAction> {
val actions = mutableListOf<DatEventAction>() val actions = mutableListOf<DatEventAction>()
outer@ while (cursor.hasBytesLeft()) { outer@ while (cursor.hasBytesLeft()) {
when (val type = cursor.u8()) { when (val type = cursor.u8().toInt()) {
(1u).toUByte() -> break@outer 1 -> break@outer
EVENT_ACTION_SPAWN_NPCS -> EVENT_ACTION_SPAWN_NPCS ->
actions.add(DatEventAction.SpawnNpcs( actions.add(DatEventAction.SpawnNpcs(

View File

@ -67,7 +67,7 @@ fun parseObjectCode(
// Put segments in an array and parse left-over segments as data. // Put segments in an array and parse left-over segments as data.
var offset = 0 var offset = 0
while (offset < cursor.size.toInt()) { while (offset < cursor.size) {
var segment: Segment? = offsetToSegment[offset] var segment: Segment? = offsetToSegment[offset]
// If we have a segment, add it. Otherwise create a new data segment. // If we have a segment, add it. Otherwise create a new data segment.
@ -76,7 +76,7 @@ fun parseObjectCode(
var endOffset: Int var endOffset: Int
if (labels == null) { if (labels == null) {
endOffset = cursor.size.toInt() endOffset = cursor.size
for (label in labelHolder.labels) { for (label in labelHolder.labels) {
if (label.offset > offset) { if (label.offset > offset) {
@ -86,10 +86,10 @@ fun parseObjectCode(
} }
} else { } else {
val info = labelHolder.getInfo(labels[0])!! val info = labelHolder.getInfo(labels[0])!!
endOffset = info.next?.offset ?: cursor.size.toInt() endOffset = info.next?.offset ?: cursor.size
} }
cursor.seekStart(offset.toUInt()) cursor.seekStart(offset)
parseDataSegment( parseDataSegment(
offsetToSegment, offsetToSegment,
cursor, cursor,
@ -110,7 +110,7 @@ fun parseObjectCode(
offset += when (segment) { offset += when (segment) {
is InstructionSegment -> segment.instructions.sumBy { instructionSize(it, dcGcFormat) } is InstructionSegment -> segment.instructions.sumBy { instructionSize(it, dcGcFormat) }
is DataSegment -> segment.data.size.toInt() is DataSegment -> segment.data.size
// String segments should be multiples of 4 bytes. // String segments should be multiples of 4 bytes.
is StringSegment -> 4 * ceil((segment.value.length + 1) / 2.0).toInt() is StringSegment -> 4 * ceil((segment.value.length + 1) / 2.0).toInt()
@ -136,7 +136,7 @@ fun parseObjectCode(
} }
// Sanity check parsed object code. // Sanity check parsed object code.
if (cursor.size != offset.toUInt()) { if (cursor.size != offset) {
result.addProblem( result.addProblem(
Severity.Error, Severity.Error,
"The script code is corrupt.", "The script code is corrupt.",
@ -201,7 +201,8 @@ private fun findAndParseSegments(
// Never on the stack. // Never on the stack.
// Eat all remaining arguments. // Eat all remaining arguments.
while (i < instruction.args.size) { while (i < instruction.args.size) {
newLabels[instruction.args[i].value as Int] = SegmentType.Instructions newLabels[instruction.args[i].value as Int] =
SegmentType.Instructions
i++ i++
} }
} }
@ -322,8 +323,8 @@ private fun parseSegment(
} }
} }
val endOffset = info.next?.offset ?: cursor.size.toInt() val endOffset = info.next?.offset ?: cursor.size
cursor.seekStart(info.offset.toUInt()) cursor.seekStart(info.offset)
return when (type) { return when (type) {
SegmentType.Instructions -> SegmentType.Instructions ->
@ -370,9 +371,9 @@ private fun parseInstructionsSegment(
instructions, instructions,
SegmentSrcLoc() SegmentSrcLoc()
) )
offsetToSegment[cursor.position.toInt()] = segment offsetToSegment[cursor.position] = segment
while (cursor.position < endOffset.toUInt()) { while (cursor.position < endOffset) {
// Parse the opcode. // Parse the opcode.
val mainOpcode = cursor.u8() val mainOpcode = cursor.u8()
@ -436,10 +437,10 @@ private fun parseDataSegment(
val startOffset = cursor.position val startOffset = cursor.position
val segment = DataSegment( val segment = DataSegment(
labels, labels,
cursor.buffer(endOffset.toUInt() - startOffset), cursor.buffer(endOffset - startOffset),
SegmentSrcLoc(), SegmentSrcLoc(),
) )
offsetToSegment[startOffset.toInt()] = segment offsetToSegment[startOffset] = segment
} }
private fun parseStringSegment( private fun parseStringSegment(
@ -454,20 +455,20 @@ private fun parseStringSegment(
labels, labels,
if (dcGcFormat) { if (dcGcFormat) {
cursor.stringAscii( cursor.stringAscii(
endOffset.toUInt() - startOffset, endOffset - startOffset,
nullTerminated = true, nullTerminated = true,
dropRemaining = true dropRemaining = true
) )
} else { } else {
cursor.stringUtf16( cursor.stringUtf16(
endOffset.toUInt() - startOffset, endOffset - startOffset,
nullTerminated = true, nullTerminated = true,
dropRemaining = true dropRemaining = true
) )
}, },
SegmentSrcLoc() SegmentSrcLoc()
) )
offsetToSegment[startOffset.toInt()] = segment offsetToSegment[startOffset] = segment
} }
private fun parseInstructionArguments( private fun parseInstructionArguments(
@ -501,7 +502,7 @@ private fun parseInstructionArguments(
} }
is StringType -> { is StringType -> {
val maxBytes = min(4096u, cursor.bytesLeft) val maxBytes = min(4096, cursor.bytesLeft)
args.add(Arg( args.add(Arg(
if (dcGcFormat) { if (dcGcFormat) {
cursor.stringAscii( cursor.stringAscii(
@ -521,7 +522,7 @@ private fun parseInstructionArguments(
is ILabelVarType -> { is ILabelVarType -> {
val argSize = cursor.u8() val argSize = cursor.u8()
args.addAll(cursor.u16Array(argSize.toUInt()).map { Arg(it.toInt()) }) args.addAll(cursor.u16Array(argSize.toInt()).map { Arg(it.toInt()) })
} }
is RegRefType, is RegRefType,
@ -532,7 +533,7 @@ private fun parseInstructionArguments(
is RegRefVarType -> { is RegRefVarType -> {
val argSize = cursor.u8() val argSize = cursor.u8()
args.addAll(cursor.u8Array(argSize.toUInt()).map { Arg(it.toInt()) }) args.addAll(cursor.u8Array(argSize.toInt()).map { Arg(it.toInt()) })
} }
else -> error("Parameter type ${param.type} not implemented.") else -> error("Parameter type ${param.type} not implemented.")

View File

@ -8,15 +8,15 @@ class QuestNpc(var episode: Episode, var areaId: Int, val data: Buffer) {
* Only seems to be valid for non-enemies. * Only seems to be valid for non-enemies.
*/ */
var scriptLabel: Int var scriptLabel: Int
get() = data.getF32(60u).roundToInt() get() = data.getF32(60).roundToInt()
set(value) { set(value) {
data.setF32(60u, value.toFloat()) data.setF32(60, value.toFloat())
} }
var skin: Int var skin: Int
get() = data.getI32(64u) get() = data.getI32(64)
set(value) { set(value) {
data.setI32(64u, value) data.setI32(64, value)
} }
init { init {

View File

@ -3,9 +3,11 @@ package world.phantasmal.lib.fileFormats.quest
import world.phantasmal.lib.buffer.Buffer import world.phantasmal.lib.buffer.Buffer
class QuestObject(var areaId: Int, val data: Buffer) { class QuestObject(var areaId: Int, val data: Buffer) {
var type: ObjectType = TODO() var type: ObjectType
val scriptLabel: Int? = TODO() get() = TODO()
val scriptLabel2: Int? = TODO() set(_) = TODO()
val scriptLabel: Int? = null // TODO Implement scriptLabel.
val scriptLabel2: Int? = null // TODO Implement scriptLabel2.
init { init {
require(data.size == OBJECT_BYTE_SIZE) { require(data.size == OBJECT_BYTE_SIZE) {

View File

@ -7,29 +7,56 @@ import kotlin.test.assertTrue
class BufferTests { class BufferTests {
@Test @Test
fun simple_properties_and_invariants() { fun withCapacity() {
val capacity = 500u val capacity = 500
val buffer = Buffer.withCapacity(capacity) val buffer = Buffer.withCapacity(capacity)
assertEquals(0u, buffer.size) assertEquals(0, buffer.size)
assertEquals(capacity, buffer.capacity) assertEquals(capacity, buffer.capacity)
assertEquals(Endianness.Little, buffer.endianness) assertEquals(Endianness.Little, buffer.endianness)
} }
@Test
fun withSize() {
val size = 500
val buffer = Buffer.withSize(size)
assertEquals(size, buffer.size)
assertEquals(size, buffer.capacity)
assertEquals(Endianness.Little, buffer.endianness)
}
@Test @Test
fun reallocates_internal_storage_when_necessary() { fun reallocates_internal_storage_when_necessary() {
val buffer = Buffer.withCapacity(100u) val buffer = Buffer.withCapacity(100)
assertEquals(0u, buffer.size) assertEquals(0, buffer.size)
assertEquals(100u, buffer.capacity) assertEquals(100, buffer.capacity)
buffer.size = 101u buffer.size = 101
assertEquals(101u, buffer.size) assertEquals(101, buffer.size)
assertTrue(buffer.capacity >= 101u) assertTrue(buffer.capacity >= 101)
buffer.setU8(100u, (0xABu).toUByte()) buffer.setU8(100, (0xABu).toUByte())
assertEquals(0xABu, buffer.getU8(100u).toUInt()) assertEquals(0xABu, buffer.getU8(100).toUInt())
}
@Test
fun fill_and_zero() {
val buffer = Buffer.withSize(100)
buffer.fill(100)
for (i in 0 until buffer.size) {
assertEquals(100u, buffer.getU8(i))
}
buffer.zero()
for (i in 0 until buffer.size) {
assertEquals(0u, buffer.getU8(i))
}
} }
} }

View File

@ -0,0 +1,74 @@
package world.phantasmal.lib.compression.prs
import world.phantasmal.lib.buffer.Buffer
import world.phantasmal.lib.cursor.cursor
import kotlin.random.Random
import kotlin.random.nextUInt
import kotlin.test.Test
import kotlin.test.assertEquals
class PrsCompressTests {
@Test
fun edge_case_0_bytes() {
val compressed = prsCompress(Buffer.withSize(0).cursor())
assertEquals(3, compressed.size)
}
@Test
fun edge_case_1_byte() {
val compressed = prsCompress(Buffer.withSize(1).fill(111).cursor())
assertEquals(4, compressed.size)
}
@Test
fun edge_case_2_bytes() {
val compressed = prsCompress(Buffer.fromByteArray(byteArrayOf(7, 111)).cursor())
assertEquals(5, compressed.size)
}
@Test
fun edge_case_3_bytes() {
val compressed = prsCompress(Buffer.fromByteArray(byteArrayOf(7, 55, 120)).cursor())
assertEquals(6, compressed.size)
}
@Test
fun best_case() {
val compressed = prsCompress(Buffer.withSize(10_000).fill(127).cursor())
assertEquals(475, compressed.size)
}
@Test
fun worst_case() {
val random = Random(37)
val buffer = Buffer.withSize(10_000)
for (i in 0 until buffer.size step 4) {
buffer.setU32(i, random.nextUInt())
}
val compressed = prsCompress(buffer.cursor())
assertEquals(11252, compressed.size)
}
@Test
fun typical_case() {
val random = Random(37)
val pattern = byteArrayOf(0, 0, 2, 0, 3, 0, 5, 0, 0, 0, 7, 9, 11, 13, 0, 0)
val buffer = Buffer.withSize(1000 * pattern.size)
for (i in 0 until buffer.size) {
buffer.setI8(i, (pattern[i % pattern.size] + random.nextInt(10)).toByte())
}
val compressed = prsCompress(buffer.cursor())
assertEquals(14549, compressed.size)
}
}

View File

@ -0,0 +1,84 @@
package world.phantasmal.lib.compression.prs
import world.phantasmal.lib.buffer.Buffer
import world.phantasmal.lib.cursor.cursor
import kotlin.random.Random
import kotlin.random.nextUInt
import kotlin.test.Test
import kotlin.test.assertEquals
class PrsDecompressTests {
@Test
fun edge_case_0_bytes() {
testWithBuffer(Buffer.withSize(0))
}
@Test
fun edge_case_1_byte() {
testWithBuffer(Buffer.withSize(1).fill(111))
}
@Test
fun edge_case_2_bytes() {
testWithBuffer(Buffer.fromByteArray(byteArrayOf(7, 111)))
}
@Test
fun edge_case_3_bytes() {
testWithBuffer(Buffer.fromByteArray(byteArrayOf(7, 55, 120)))
}
@Test
fun best_case() {
testWithBuffer(Buffer.withSize(10_000).fill(127))
}
@Test
fun worst_case() {
val random = Random(37)
val buffer = Buffer.withSize(10_000)
for (i in 0 until buffer.size step 4) {
buffer.setU32(i, random.nextUInt())
}
testWithBuffer(buffer)
}
@Test
fun typical_case() {
val random = Random(37)
val pattern = byteArrayOf(0, 0, 2, 0, 3, 0, 5, 0, 0, 0, 7, 9, 11, 13, 0, 0)
val buffer = Buffer.withSize(1000 * pattern.size)
for (i in 0 until buffer.size) {
buffer.setI8(i, (pattern[i % pattern.size] + random.nextInt(10)).toByte())
}
testWithBuffer(buffer)
}
private fun testWithBuffer(buffer: Buffer) {
val cursor = buffer.cursor()
val compressedCursor = prsCompress(cursor)
val decompressedCursor = prsDecompress(compressedCursor).unwrap()
cursor.seekStart(0)
assertEquals(cursor.size, decompressedCursor.size)
while (cursor.hasBytesLeft()) {
val expected = cursor.i8()
val actual = decompressedCursor.i8()
if (expected != actual) {
// Assert after check for performance.
assertEquals(
expected,
actual,
"Got $actual, expected $expected at ${cursor.position - 1}."
)
}
}
}
}

View File

@ -53,20 +53,20 @@ class BufferCursorTests : WritableCursorTests() {
val expectedNumber1 = 7891378 val expectedNumber1 = 7891378
val expectedNumber2 = 893894273 val expectedNumber2 = 893894273
val buffer = Buffer.withCapacity(8u, endianness) val buffer = Buffer.withCapacity(8, endianness)
val cursor = BufferCursor(buffer) val cursor = BufferCursor(buffer)
assertEquals(0u, buffer.size) assertEquals(0, buffer.size)
assertEquals(0u, cursor.size) assertEquals(0, cursor.size)
cursor.write(expectedNumber1) cursor.write(expectedNumber1)
assertEquals(byteCount.toUInt(), buffer.size) assertEquals(byteCount, buffer.size)
assertEquals(byteCount.toUInt(), cursor.size) assertEquals(byteCount, cursor.size)
cursor.write(expectedNumber2) cursor.write(expectedNumber2)
assertEquals(2u * byteCount.toUInt(), buffer.size) assertEquals(2 * byteCount, buffer.size)
assertEquals(2u * byteCount.toUInt(), cursor.size) assertEquals(2 * byteCount, cursor.size)
} }
} }

View File

@ -21,15 +21,15 @@ abstract class CursorTests {
val cursor = createCursor(byteArrayOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), endianness) val cursor = createCursor(byteArrayOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), endianness)
for ((seek_to, expectedPos) in listOf( for ((seek_to, expectedPos) in listOf(
0 to 0u, 0 to 0,
3 to 3u, 3 to 3,
5 to 8u, 5 to 8,
2 to 10u, 2 to 10,
-10 to 0u, -10 to 0,
)) { )) {
cursor.seek(seek_to) cursor.seek(seek_to)
assertEquals(10u, cursor.size) assertEquals(10, cursor.size)
assertEquals(expectedPos, cursor.position) assertEquals(expectedPos, cursor.position)
assertEquals(cursor.position + cursor.bytesLeft, cursor.size) assertEquals(cursor.position + cursor.bytesLeft, cursor.size)
assertEquals(endianness, cursor.endianness) assertEquals(endianness, cursor.endianness)
@ -116,10 +116,10 @@ abstract class CursorTests {
val cursor = createCursor(bytes, endianness) val cursor = createCursor(bytes, endianness)
assertEquals(expectedNumber1, cursor.read()) assertEquals(expectedNumber1, cursor.read())
assertEquals(byteCount.toUInt(), cursor.position) assertEquals(byteCount, cursor.position)
assertEquals(expectedNumber2, cursor.read()) assertEquals(expectedNumber2, cursor.read())
assertEquals(2u * byteCount.toUInt(), cursor.position) assertEquals(2 * byteCount, cursor.position)
} }
@Test @Test
@ -139,17 +139,17 @@ abstract class CursorTests {
val cursor = createCursor(bytes, endianness) val cursor = createCursor(bytes, endianness)
assertEquals(2.5f, cursor.f32()) assertEquals(2.5f, cursor.f32())
assertEquals(4u, cursor.position) assertEquals(4, cursor.position)
assertEquals(32.25f, cursor.f32()) assertEquals(32.25f, cursor.f32())
assertEquals(8u, cursor.position) assertEquals(8, cursor.position)
} }
@Test @Test
fun u8Array() { fun u8Array() {
val read: Cursor.(UInt) -> IntArray = { n -> val read: Cursor.(Int) -> IntArray = { n ->
val arr = u8Array(n) val arr = u8Array(n)
IntArray(n.toInt()) { arr[it].toInt() } IntArray(n) { arr[it].toInt() }
} }
testIntegerArrayRead(1, read, Endianness.Little) testIntegerArrayRead(1, read, Endianness.Little)
@ -158,9 +158,9 @@ abstract class CursorTests {
@Test @Test
fun u16Array() { fun u16Array() {
val read: Cursor.(UInt) -> IntArray = { n -> val read: Cursor.(Int) -> IntArray = { n ->
val arr = u16Array(n) val arr = u16Array(n)
IntArray(n.toInt()) { arr[it].toInt() } IntArray(n) { arr[it].toInt() }
} }
testIntegerArrayRead(2, read, Endianness.Little) testIntegerArrayRead(2, read, Endianness.Little)
@ -169,9 +169,9 @@ abstract class CursorTests {
@Test @Test
fun u32Array() { fun u32Array() {
val read: Cursor.(UInt) -> IntArray = { n -> val read: Cursor.(Int) -> IntArray = { n ->
val arr = u32Array(n) val arr = u32Array(n)
IntArray(n.toInt()) { arr[it].toInt() } IntArray(n) { arr[it].toInt() }
} }
testIntegerArrayRead(4, read, Endianness.Little) testIntegerArrayRead(4, read, Endianness.Little)
@ -180,9 +180,9 @@ abstract class CursorTests {
@Test @Test
fun i32Array() { fun i32Array() {
val read: Cursor.(UInt) -> IntArray = { n -> val read: Cursor.(Int) -> IntArray = { n ->
val arr = i32Array(n) val arr = i32Array(n)
IntArray(n.toInt()) { arr[it] } IntArray(n) { arr[it] }
} }
testIntegerArrayRead(4, read, Endianness.Little) testIntegerArrayRead(4, read, Endianness.Little)
@ -191,7 +191,7 @@ abstract class CursorTests {
private fun testIntegerArrayRead( private fun testIntegerArrayRead(
byteCount: Int, byteCount: Int,
read: Cursor.(UInt) -> IntArray, read: Cursor.(Int) -> IntArray,
endianness: Endianness, endianness: Endianness,
) { ) {
// Generate array of the form 1, 2, 0xFF, 4, 5, 6, 7, 8. // Generate array of the form 1, 2, 0xFF, 4, 5, 6, 7, 8.
@ -217,26 +217,47 @@ abstract class CursorTests {
// Test cursor. // Test cursor.
val cursor = createCursor(bytes, endianness) val cursor = createCursor(bytes, endianness)
val array1 = cursor.read(3u) val array1 = cursor.read(3)
assertEquals(1, array1[0]) assertEquals(1, array1[0])
assertEquals(2, array1[1]) assertEquals(2, array1[1])
assertEquals(allOnes, array1[2]) assertEquals(allOnes, array1[2])
assertEquals(3u * byteCount.toUInt(), cursor.position) assertEquals(3 * byteCount, cursor.position)
cursor.seekStart((2 * byteCount).toUInt()) cursor.seekStart(2 * byteCount)
val array2 = cursor.read(4u) val array2 = cursor.read(4)
assertEquals(allOnes, array2[0]) assertEquals(allOnes, array2[0])
assertEquals(4, array2[1]) assertEquals(4, array2[1])
assertEquals(5, array2[2]) assertEquals(5, array2[2])
assertEquals(6, array2[3]) assertEquals(6, array2[3])
assertEquals(6u * byteCount.toUInt(), cursor.position) assertEquals(6 * byteCount, cursor.position)
cursor.seekStart((5 * byteCount).toUInt()) cursor.seekStart(5 * byteCount)
val array3 = cursor.read(3u) val array3 = cursor.read(3)
assertEquals(6, array3[0]) assertEquals(6, array3[0])
assertEquals(7, array3[1]) assertEquals(7, array3[1])
assertEquals(8, array3[2]) assertEquals(8, array3[2])
assertEquals(8u * byteCount.toUInt(), cursor.position) assertEquals(8 * byteCount, cursor.position)
}
@Test
fun take() {
testTake(Endianness.Little)
testTake(Endianness.Big)
}
private fun testTake(endianness: Endianness) {
val bytes = byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8)
val cursor = createCursor(bytes, endianness)
val newCursor = cursor.seek(2).take(4)
assertEquals(6, cursor.position)
assertEquals(4, newCursor.size)
assertEquals(3u, newCursor.u8())
assertEquals(4u, newCursor.u8())
assertEquals(5u, newCursor.u8())
assertEquals(6u, newCursor.u8())
} }
@Test @Test
@ -254,7 +275,7 @@ abstract class CursorTests {
private fun testStringRead( private fun testStringRead(
byteCount: Int, byteCount: Int,
read: Cursor.( read: Cursor.(
maxByteLength: UInt, maxByteLength: Int,
nullTerminated: Boolean, nullTerminated: Boolean,
dropRemaining: Boolean, dropRemaining: Boolean,
) -> String, ) -> String,
@ -271,30 +292,29 @@ abstract class CursorTests {
} }
} }
val bc = byteCount.toUInt()
val cursor = createCursor(bytes, endianness) val cursor = createCursor(bytes, endianness)
cursor.seekStart(bc) cursor.seekStart(byteCount)
assertEquals("AB", cursor.read(4u * bc, true, true)) assertEquals("AB", cursor.read(4 * byteCount, true, true))
assertEquals(5u * bc, cursor.position) assertEquals(5 * byteCount, cursor.position)
cursor.seekStart(bc) cursor.seekStart(byteCount)
assertEquals("AB", cursor.read(2u * bc, true, true)) assertEquals("AB", cursor.read(2 * byteCount, true, true))
assertEquals(3u * bc, cursor.position) assertEquals(3 * byteCount, cursor.position)
cursor.seekStart(bc) cursor.seekStart(byteCount)
assertEquals("AB", cursor.read(4u * bc, true, false)) assertEquals("AB", cursor.read(4 * byteCount, true, false))
assertEquals(4u * bc, cursor.position) assertEquals(4 * byteCount, cursor.position)
cursor.seekStart(bc) cursor.seekStart(byteCount)
assertEquals("AB", cursor.read(2u * bc, true, false)) assertEquals("AB", cursor.read(2 * byteCount, true, false))
assertEquals(3u * bc, cursor.position) assertEquals(3 * byteCount, cursor.position)
cursor.seekStart(bc) cursor.seekStart(byteCount)
assertEquals("AB\u0000ÿ", cursor.read(4u * bc, false, true)) assertEquals("AB\u0000ÿ", cursor.read(4 * byteCount, false, true))
assertEquals(5u * bc, cursor.position) assertEquals(5 * byteCount, cursor.position)
cursor.seekStart(bc) cursor.seekStart(byteCount)
assertEquals("AB\u0000ÿ", cursor.read(4u * bc, false, false)) assertEquals("AB\u0000ÿ", cursor.read(4 * byteCount, false, false))
assertEquals(5u * bc, cursor.position) assertEquals(5 * byteCount, cursor.position)
} }
@Test @Test
@ -308,13 +328,13 @@ abstract class CursorTests {
val cursor = createCursor(bytes, endianness) val cursor = createCursor(bytes, endianness)
val buf = cursor.seek(2).buffer(4u) val buf = cursor.seek(2).buffer(4)
assertEquals(6u, cursor.position) assertEquals(6, cursor.position)
assertEquals(4u, buf.size) assertEquals(4, buf.size)
assertEquals(3u, buf.getU8(0u)) assertEquals(3u, buf.getU8(0))
assertEquals(4u, buf.getU8(1u)) assertEquals(4u, buf.getU8(1))
assertEquals(5u, buf.getU8(2u)) assertEquals(5u, buf.getU8(2))
assertEquals(6u, buf.getU8(3u)) assertEquals(6u, buf.getU8(3))
} }
} }

View File

@ -1,6 +1,7 @@
package world.phantasmal.lib.cursor package world.phantasmal.lib.cursor
import world.phantasmal.lib.Endianness import world.phantasmal.lib.Endianness
import world.phantasmal.lib.buffer.Buffer
import kotlin.math.abs import kotlin.math.abs
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -18,15 +19,15 @@ abstract class WritableCursorTests : CursorTests() {
private fun simple_WritableCursor_properties_and_invariants(endianness: Endianness) { private fun simple_WritableCursor_properties_and_invariants(endianness: Endianness) {
val cursor = createCursor(byteArrayOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), endianness) val cursor = createCursor(byteArrayOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), endianness)
assertEquals(0u, cursor.position) assertEquals(0, cursor.position)
cursor.writeU8(99u).writeU8(99u).writeU8(99u).writeU8(99u) cursor.writeU8(99u).writeU8(99u).writeU8(99u).writeU8(99u)
cursor.seek(-1) cursor.seek(-1)
assertEquals(cursor.position + cursor.bytesLeft, cursor.size) assertEquals(cursor.position + cursor.bytesLeft, cursor.size)
assertEquals(10u, cursor.size) assertEquals(10, cursor.size)
assertEquals(3u, cursor.position) assertEquals(3, cursor.position)
assertEquals(7u, cursor.bytesLeft) assertEquals(7, cursor.bytesLeft)
assertEquals(endianness, cursor.endianness) assertEquals(endianness, cursor.endianness)
} }
@ -83,9 +84,9 @@ abstract class WritableCursorTests : CursorTests() {
cursor.write(expectedNumber1) cursor.write(expectedNumber1)
cursor.write(expectedNumber2) cursor.write(expectedNumber2)
assertEquals((2 * byteCount).toUInt(), cursor.position) assertEquals(2 * byteCount, cursor.position)
cursor.seekStart(0u) cursor.seekStart(0)
assertEquals(expectedNumber1, cursor.read()) assertEquals(expectedNumber1, cursor.read())
assertEquals(expectedNumber2, cursor.read()) assertEquals(expectedNumber2, cursor.read())
@ -106,23 +107,23 @@ abstract class WritableCursorTests : CursorTests() {
cursor.writeF32(1337.9001f) cursor.writeF32(1337.9001f)
cursor.writeF32(103.502f) cursor.writeF32(103.502f)
assertEquals(8u, cursor.position) assertEquals(8, cursor.position)
cursor.seekStart(0u) cursor.seekStart(0)
// The read floats won't be exactly the same as the written floats in Kotlin JS, because // The read floats won't be exactly the same as the written floats in Kotlin JS, because
// they're backed by numbers (64-bit floats). // they're backed by numbers (64-bit floats).
assertTrue(abs(1337.9001f - cursor.f32()) < 0.001) assertTrue(abs(1337.9001f - cursor.f32()) < 0.001)
assertTrue(abs(103.502f - cursor.f32()) < 0.001) assertTrue(abs(103.502f - cursor.f32()) < 0.001)
assertEquals(8u, cursor.position) assertEquals(8, cursor.position)
} }
@Test @Test
fun writeU8Array() { fun writeU8Array() {
val read: Cursor.(UInt) -> IntArray = { n -> val read: Cursor.(Int) -> IntArray = { n ->
val arr = u8Array(n) val arr = u8Array(n)
IntArray(n.toInt()) { arr[it].toInt() } IntArray(n) { arr[it].toInt() }
} }
val write: WritableCursor.(IntArray) -> Unit = { a -> val write: WritableCursor.(IntArray) -> Unit = { a ->
writeU8Array(UByteArray(a.size) { a[it].toUByte() }) writeU8Array(UByteArray(a.size) { a[it].toUByte() })
@ -134,9 +135,9 @@ abstract class WritableCursorTests : CursorTests() {
@Test @Test
fun writeU16Array() { fun writeU16Array() {
val read: Cursor.(UInt) -> IntArray = { n -> val read: Cursor.(Int) -> IntArray = { n ->
val arr = u16Array(n) val arr = u16Array(n)
IntArray(n.toInt()) { arr[it].toInt() } IntArray(n) { arr[it].toInt() }
} }
val write: WritableCursor.(IntArray) -> Unit = { a -> val write: WritableCursor.(IntArray) -> Unit = { a ->
writeU16Array(UShortArray(a.size) { a[it].toUShort() }) writeU16Array(UShortArray(a.size) { a[it].toUShort() })
@ -148,9 +149,9 @@ abstract class WritableCursorTests : CursorTests() {
@Test @Test
fun writeU32Array() { fun writeU32Array() {
val read: Cursor.(UInt) -> IntArray = { n -> val read: Cursor.(Int) -> IntArray = { n ->
val arr = u32Array(n) val arr = u32Array(n)
IntArray(n.toInt()) { arr[it].toInt() } IntArray(n) { arr[it].toInt() }
} }
val write: WritableCursor.(IntArray) -> Unit = { a -> val write: WritableCursor.(IntArray) -> Unit = { a ->
writeU32Array(UIntArray(a.size) { a[it].toUInt() }) writeU32Array(UIntArray(a.size) { a[it].toUInt() })
@ -162,7 +163,7 @@ abstract class WritableCursorTests : CursorTests() {
@Test @Test
fun writeI32Array() { fun writeI32Array() {
val read: Cursor.(UInt) -> IntArray = { n -> val read: Cursor.(Int) -> IntArray = { n ->
i32Array(n) i32Array(n)
} }
val write: WritableCursor.(IntArray) -> Unit = { a -> val write: WritableCursor.(IntArray) -> Unit = { a ->
@ -175,7 +176,7 @@ abstract class WritableCursorTests : CursorTests() {
private fun testIntegerArrayWrite( private fun testIntegerArrayWrite(
byteCount: Int, byteCount: Int,
read: Cursor.(UInt) -> IntArray, read: Cursor.(Int) -> IntArray,
write: WritableCursor.(IntArray) -> Unit, write: WritableCursor.(IntArray) -> Unit,
endianness: Endianness, endianness: Endianness,
) { ) {
@ -185,16 +186,42 @@ abstract class WritableCursorTests : CursorTests() {
val cursor = createCursor(ByteArray(20 * byteCount), endianness) val cursor = createCursor(ByteArray(20 * byteCount), endianness)
cursor.write(testArray1) cursor.write(testArray1)
assertEquals(10u * byteCount.toUInt(), cursor.position) assertEquals(10 * byteCount, cursor.position)
cursor.write(testArray2) cursor.write(testArray2)
assertEquals(20u * byteCount.toUInt(), cursor.position) assertEquals(20 * byteCount, cursor.position)
cursor.seekStart(0u) cursor.seekStart(0)
assertTrue(testArray1.contentEquals(cursor.read(10u))) assertTrue(testArray1.contentEquals(cursor.read(10)))
assertTrue(testArray2.contentEquals(cursor.read(10u))) assertTrue(testArray2.contentEquals(cursor.read(10)))
assertEquals(20u * byteCount.toUInt(), cursor.position) assertEquals(20 * byteCount, cursor.position)
}
@Test
fun writeCursor() {
testWriteCursor(Endianness.Little)
testWriteCursor(Endianness.Big)
}
private fun testWriteCursor(endianness: Endianness) {
val cursor = createCursor(ByteArray(8), endianness)
cursor.seek(2)
cursor.writeCursor(Buffer.fromByteArray(byteArrayOf(1, 2, 3, 4)).cursor())
assertEquals(6, cursor.position)
cursor.seekStart(0)
assertEquals(0, cursor.i8())
assertEquals(0, cursor.i8())
assertEquals(1, cursor.i8())
assertEquals(2, cursor.i8())
assertEquals(3, cursor.i8())
assertEquals(4, cursor.i8())
assertEquals(0, cursor.i8())
assertEquals(0, cursor.i8())
} }
@Test @Test
@ -208,10 +235,11 @@ abstract class WritableCursorTests : CursorTests() {
cursor.writeU32(1u).writeU32(2u).writeU32(3u).writeU32(4u) cursor.writeU32(1u).writeU32(2u).writeU32(3u).writeU32(4u)
cursor.seek(-8) cursor.seek(-8)
val newCursor = cursor.take(8u) val newCursor = cursor.take(8)
assertEquals(8u, newCursor.size) assertEquals(16, cursor.position)
assertEquals(0u, newCursor.position) assertEquals(8, newCursor.size)
assertEquals(0, newCursor.position)
assertEquals(3u, newCursor.u32()) assertEquals(3u, newCursor.u32())
assertEquals(4u, newCursor.u32()) assertEquals(4u, newCursor.u32())
} }

View File

@ -2,19 +2,20 @@ package world.phantasmal.lib.buffer
import org.khronos.webgl.ArrayBuffer import org.khronos.webgl.ArrayBuffer
import org.khronos.webgl.DataView import org.khronos.webgl.DataView
import org.khronos.webgl.Int8Array
import org.khronos.webgl.Uint8Array import org.khronos.webgl.Uint8Array
import world.phantasmal.lib.Endianness import world.phantasmal.lib.Endianness
import world.phantasmal.lib.ZERO_U16 import world.phantasmal.lib.ZERO_U16
actual class Buffer private constructor( actual class Buffer private constructor(
private var arrayBuffer: ArrayBuffer, private var arrayBuffer: ArrayBuffer,
size: UInt, size: Int,
endianness: Endianness, endianness: Endianness,
) { ) {
private var dataView = DataView(arrayBuffer) private var dataView = DataView(arrayBuffer)
private var littleEndian = endianness == Endianness.Little private var littleEndian = endianness == Endianness.Little
actual var size: UInt = size actual var size: Int = size
set(value) { set(value) {
ensureCapacity(value) ensureCapacity(value)
field = value field = value
@ -26,54 +27,54 @@ actual class Buffer private constructor(
littleEndian = value == Endianness.Little littleEndian = value == Endianness.Little
} }
actual val capacity: UInt actual val capacity: Int
get() = arrayBuffer.byteLength.toUInt() get() = arrayBuffer.byteLength
actual fun getU8(offset: UInt): UByte { actual fun getU8(offset: Int): UByte {
checkOffset(offset, 1u) checkOffset(offset, 1)
return dataView.getUint8(offset.toInt()).toUByte() return dataView.getUint8(offset).toUByte()
} }
actual fun getU16(offset: UInt): UShort { actual fun getU16(offset: Int): UShort {
checkOffset(offset, 2u) checkOffset(offset, 2)
return dataView.getUint16(offset.toInt(), littleEndian).toUShort() return dataView.getUint16(offset, littleEndian).toUShort()
} }
actual fun getU32(offset: UInt): UInt { actual fun getU32(offset: Int): UInt {
checkOffset(offset, 4u) checkOffset(offset, 4)
return dataView.getUint32(offset.toInt(), littleEndian).toUInt() return dataView.getUint32(offset, littleEndian).toUInt()
} }
actual fun getI8(offset: UInt): Byte { actual fun getI8(offset: Int): Byte {
checkOffset(offset, 1u) checkOffset(offset, 1)
return dataView.getInt8(offset.toInt()) return dataView.getInt8(offset)
} }
actual fun getI16(offset: UInt): Short { actual fun getI16(offset: Int): Short {
checkOffset(offset, 2u) checkOffset(offset, 2)
return dataView.getInt16(offset.toInt(), littleEndian) return dataView.getInt16(offset, littleEndian)
} }
actual fun getI32(offset: UInt): Int { actual fun getI32(offset: Int): Int {
checkOffset(offset, 4u) checkOffset(offset, 4)
return dataView.getInt32(offset.toInt(), littleEndian) return dataView.getInt32(offset, littleEndian)
} }
actual fun getF32(offset: UInt): Float { actual fun getF32(offset: Int): Float {
checkOffset(offset, 4u) checkOffset(offset, 4)
return dataView.getFloat32(offset.toInt(), littleEndian) return dataView.getFloat32(offset, littleEndian)
} }
actual fun getStringUtf16( actual fun getStringUtf16(
offset: UInt, offset: Int,
maxByteLength: UInt, maxByteLength: Int,
nullTerminated: Boolean, nullTerminated: Boolean,
): String = ): String =
buildString { buildString {
val len = maxByteLength / 2u val len = maxByteLength / 2
for (i in 0u until len) { for (i in 0 until len) {
val codePoint = getU16(offset + i * 2u) val codePoint = getU16(offset + i * 2)
if (nullTerminated && codePoint == ZERO_U16) { if (nullTerminated && codePoint == ZERO_U16) {
break break
@ -83,90 +84,69 @@ actual class Buffer private constructor(
} }
} }
actual fun slice(offset: UInt, size: UInt): Buffer { actual fun slice(offset: Int, size: Int): Buffer {
checkOffset(offset, size) checkOffset(offset, size)
return fromArrayBuffer( return fromArrayBuffer(
arrayBuffer.slice(offset.toInt(), (offset + size).toInt()), arrayBuffer.slice(offset, (offset + size)),
endianness endianness
) )
} }
/** actual fun setU8(offset: Int, value: UByte): Buffer {
* Writes an unsigned 8-bit integer at the given offset. checkOffset(offset, 1)
*/ dataView.setUint8(offset, value.toByte())
actual fun setU8(offset: UInt, value: UByte): Buffer {
checkOffset(offset, 1u)
dataView.setUint8(offset.toInt(), value.toByte())
return this return this
} }
/** actual fun setU16(offset: Int, value: UShort): Buffer {
* Writes an unsigned 16-bit integer at the given offset. checkOffset(offset, 2)
*/ dataView.setUint16(offset, value.toShort(), littleEndian)
actual fun setU16(offset: UInt, value: UShort): Buffer {
checkOffset(offset, 2u)
dataView.setUint16(offset.toInt(), value.toShort(), littleEndian)
return this return this
} }
/** actual fun setU32(offset: Int, value: UInt): Buffer {
* Writes an unsigned 32-bit integer at the given offset. checkOffset(offset, 4)
*/ dataView.setUint32(offset, value.toInt(), littleEndian)
actual fun setU32(offset: UInt, value: UInt): Buffer {
checkOffset(offset, 4u)
dataView.setUint32(offset.toInt(), value.toInt(), littleEndian)
return this return this
} }
/** actual fun setI8(offset: Int, value: Byte): Buffer {
* Writes a signed 8-bit integer at the given offset. checkOffset(offset, 1)
*/ dataView.setInt8(offset, value)
actual fun setI8(offset: UInt, value: Byte): Buffer {
checkOffset(offset, 1u)
dataView.setInt8(offset.toInt(), value)
return this return this
} }
/** actual fun setI16(offset: Int, value: Short): Buffer {
* Writes a signed 16-bit integer at the given offset. checkOffset(offset, 2)
*/ dataView.setInt16(offset, value, littleEndian)
actual fun setI16(offset: UInt, value: Short): Buffer {
checkOffset(offset, 2u)
dataView.setInt16(offset.toInt(), value, littleEndian)
return this return this
} }
/** actual fun setI32(offset: Int, value: Int): Buffer {
* Writes a signed 32-bit integer at the given offset. checkOffset(offset, 4)
*/ dataView.setInt32(offset, value, littleEndian)
actual fun setI32(offset: UInt, value: Int): Buffer {
checkOffset(offset, 4u)
dataView.setInt32(offset.toInt(), value, littleEndian)
return this return this
} }
/** actual fun setF32(offset: Int, value: Float): Buffer {
* Writes a 32-bit floating point number at the given offset. checkOffset(offset, 4)
*/ dataView.setFloat32(offset, value, littleEndian)
actual fun setF32(offset: UInt, value: Float): Buffer {
checkOffset(offset, 4u)
dataView.setFloat32(offset.toInt(), value, littleEndian)
return this return this
} }
/** actual fun zero(): Buffer =
* Writes 0 bytes to the entire buffer. fill(0)
*/
actual fun zero(): Buffer { actual fun fill(value: Byte): Buffer {
(Uint8Array(arrayBuffer).asDynamic()).fill(0) (Int8Array(arrayBuffer).asDynamic()).fill(value)
return this return this
} }
/** /**
* Checks whether we can read [size] bytes at [offset]. * Checks whether we can read [size] bytes at [offset].
*/ */
private fun checkOffset(offset: UInt, size: UInt) { private fun checkOffset(offset: Int, size: Int) {
require(offset + size <= this.size) { require(offset >= 0 && offset + size <= this.size) {
"Offset $offset is out of bounds." "Offset $offset is out of bounds."
} }
} }
@ -174,16 +154,16 @@ actual class Buffer private constructor(
/** /**
* Reallocates the underlying ArrayBuffer if necessary. * Reallocates the underlying ArrayBuffer if necessary.
*/ */
private fun ensureCapacity(minNewSize: UInt) { private fun ensureCapacity(minNewSize: Int) {
if (minNewSize > capacity) { if (minNewSize > capacity) {
var newSize = if (capacity == 0u) minNewSize else capacity; var newSize = if (capacity == 0) minNewSize else capacity;
do { do {
newSize *= 2u; newSize *= 2;
} while (newSize < minNewSize); } while (newSize < minNewSize);
val newBuffer = ArrayBuffer(newSize.toInt()); val newBuffer = ArrayBuffer(newSize);
Uint8Array(newBuffer).set(Uint8Array(arrayBuffer, 0, size.toInt())); Uint8Array(newBuffer).set(Uint8Array(arrayBuffer, 0, size));
arrayBuffer = newBuffer; arrayBuffer = newBuffer;
dataView = DataView(arrayBuffer); dataView = DataView(arrayBuffer);
} }
@ -191,19 +171,21 @@ actual class Buffer private constructor(
actual companion object { actual companion object {
actual fun withCapacity( actual fun withCapacity(
initialCapacity: UInt, initialCapacity: Int,
endianness: Endianness, endianness: Endianness,
): Buffer = ): Buffer =
Buffer(ArrayBuffer(initialCapacity.toInt()), size = 0u, endianness) Buffer(ArrayBuffer(initialCapacity), size = 0, endianness)
actual fun withSize(initialSize: Int, endianness: Endianness): Buffer =
Buffer(ArrayBuffer(initialSize), initialSize, endianness)
actual fun fromByteArray(array: ByteArray, endianness: Endianness): Buffer { actual fun fromByteArray(array: ByteArray, endianness: Endianness): Buffer {
val arrayBuffer = ArrayBuffer(array.size) val arrayBuffer = ArrayBuffer(array.size)
Uint8Array(arrayBuffer).set(array.toTypedArray()) Int8Array(arrayBuffer).set(array.toTypedArray())
return Buffer(arrayBuffer, array.size.toUInt(), endianness) return Buffer(arrayBuffer, array.size, endianness)
} }
fun fromArrayBuffer(arrayBuffer: ArrayBuffer, endianness: Endianness): Buffer { fun fromArrayBuffer(arrayBuffer: ArrayBuffer, endianness: Endianness): Buffer =
return Buffer(arrayBuffer, arrayBuffer.byteLength.toUInt(), endianness) Buffer(arrayBuffer, arrayBuffer.byteLength, endianness)
}
} }
} }

View File

@ -1,179 +0,0 @@
package world.phantasmal.lib.cursor
import org.khronos.webgl.ArrayBuffer
import org.khronos.webgl.DataView
import world.phantasmal.lib.Endianness
import world.phantasmal.lib.buffer.Buffer
abstract class AbstractArrayBufferCursor
protected constructor(endianness: Endianness, offset: UInt) : AbstractWritableCursor(offset) {
private var littleEndian: Boolean = endianness == Endianness.Little
protected abstract val backingBuffer: ArrayBuffer
protected abstract val dv: DataView
override var endianness: Endianness
get() = if (littleEndian) Endianness.Little else Endianness.Big
set(value) {
littleEndian = value == Endianness.Little
}
override fun u8(): UByte {
requireSize(1u)
val r = dv.getUint8(absolutePosition.toInt())
position++
return r.toUByte()
}
override fun u16(): UShort {
requireSize(2u)
val r = dv.getUint16(absolutePosition.toInt(), littleEndian)
position += 2u
return r.toUShort()
}
override fun u32(): UInt {
requireSize(4u)
val r = dv.getUint32(absolutePosition.toInt(), littleEndian)
position += 4u
return r.toUInt()
}
override fun i8(): Byte {
requireSize(1u)
val r = dv.getInt8(absolutePosition.toInt())
position++
return r
}
override fun i16(): Short {
requireSize(2u)
val r = dv.getInt16(absolutePosition.toInt(), littleEndian)
position += 2u
return r
}
override fun i32(): Int {
requireSize(4u)
val r = dv.getInt32(absolutePosition.toInt(), littleEndian)
position += 4u
return r
}
override fun f32(): Float {
requireSize(4u)
val r = dv.getFloat32(absolutePosition.toInt(), littleEndian)
position += 4u
return r
}
override fun u8Array(n: UInt): UByteArray {
requireSize(n)
val array = UByteArray(n.toInt())
for (i in 0 until n.toInt()) {
array[i] = dv.getUint8(absolutePosition.toInt()).toUByte()
position++
}
return array
}
override fun u16Array(n: UInt): UShortArray {
requireSize(2u * n)
val array = UShortArray(n.toInt())
for (i in 0 until n.toInt()) {
array[i] = dv.getUint16(absolutePosition.toInt(), littleEndian).toUShort()
position += 2u
}
return array
}
override fun u32Array(n: UInt): UIntArray {
requireSize(4u * n)
val array = UIntArray(n.toInt())
for (i in 0 until n.toInt()) {
array[i] = dv.getUint32(absolutePosition.toInt(), littleEndian).toUInt()
position += 4u
}
return array
}
override fun i32Array(n: UInt): IntArray {
requireSize(4u * n)
val array = IntArray(n.toInt())
for (i in 0 until n.toInt()) {
array[i] = dv.getInt32(absolutePosition.toInt(), littleEndian)
position += 4u
}
return array
}
override fun buffer(size: UInt): Buffer {
requireSize(size)
val r = Buffer.fromArrayBuffer(
backingBuffer.slice(absolutePosition.toInt(), (absolutePosition + size).toInt()),
endianness
)
position += size
return r
}
override fun writeU8(value: UByte): WritableCursor {
requireSize(1u)
dv.setUint8(absolutePosition.toInt(), value.toByte())
position++
return this
}
override fun writeU16(value: UShort): WritableCursor {
requireSize(2u)
dv.setUint16(absolutePosition.toInt(), value.toShort(), littleEndian)
position += 2u
return this
}
override fun writeU32(value: UInt): WritableCursor {
requireSize(4u)
dv.setUint32(absolutePosition.toInt(), value.toInt(), littleEndian)
position += 4u
return this
}
override fun writeI8(value: Byte): WritableCursor {
requireSize(1u)
dv.setInt8(absolutePosition.toInt(), value)
position++
return this
}
override fun writeI16(value: Short): WritableCursor {
requireSize(2u)
dv.setInt16(absolutePosition.toInt(), value, littleEndian)
position += 2u
return this
}
override fun writeI32(value: Int): WritableCursor {
requireSize(4u)
dv.setInt32(absolutePosition.toInt(), value, littleEndian)
position += 4u
return this
}
override fun writeF32(value: Float): WritableCursor {
requireSize(4u)
dv.setFloat32(absolutePosition.toInt(), value, littleEndian)
position += 4u
return this
}
}

View File

@ -3,6 +3,7 @@ package world.phantasmal.lib.cursor
import org.khronos.webgl.ArrayBuffer import org.khronos.webgl.ArrayBuffer
import org.khronos.webgl.DataView import org.khronos.webgl.DataView
import world.phantasmal.lib.Endianness import world.phantasmal.lib.Endianness
import world.phantasmal.lib.buffer.Buffer
/** /**
* A cursor for reading from an array buffer or part of an array buffer. * A cursor for reading from an array buffer or part of an array buffer.
@ -15,22 +16,192 @@ import world.phantasmal.lib.Endianness
class ArrayBufferCursor( class ArrayBufferCursor(
buffer: ArrayBuffer, buffer: ArrayBuffer,
endianness: Endianness, endianness: Endianness,
offset: UInt = 0u, offset: Int = 0,
size: UInt = buffer.byteLength.toUInt() - offset, size: Int = buffer.byteLength - offset,
) : AbstractArrayBufferCursor(endianness, offset) { ) : AbstractWritableCursor(offset) {
override val backingBuffer = buffer private var littleEndian: Boolean = endianness == Endianness.Little
override val dv = DataView(buffer, 0, buffer.byteLength) private val backingBuffer = buffer
private val dv = DataView(buffer)
override var size: UInt = size override var size: Int = size
set(value) { set(value) {
require(size <= backingBuffer.byteLength.toUInt() - offset) require(size <= backingBuffer.byteLength - offset)
field = value field = value
} }
override fun take(size: UInt): ArrayBufferCursor { override var endianness: Endianness
get() = if (littleEndian) Endianness.Little else Endianness.Big
set(value) {
littleEndian = value == Endianness.Little
}
override fun u8(): UByte {
requireSize(1)
val r = dv.getUint8(absolutePosition)
position++
return r.toUByte()
}
override fun u16(): UShort {
requireSize(2)
val r = dv.getUint16(absolutePosition, littleEndian)
position += 2
return r.toUShort()
}
override fun u32(): UInt {
requireSize(4)
val r = dv.getUint32(absolutePosition, littleEndian)
position += 4
return r.toUInt()
}
override fun i8(): Byte {
requireSize(1)
val r = dv.getInt8(absolutePosition)
position++
return r
}
override fun i16(): Short {
requireSize(2)
val r = dv.getInt16(absolutePosition, littleEndian)
position += 2
return r
}
override fun i32(): Int {
requireSize(4)
val r = dv.getInt32(absolutePosition, littleEndian)
position += 4
return r
}
override fun f32(): Float {
requireSize(4)
val r = dv.getFloat32(absolutePosition, littleEndian)
position += 4
return r
}
override fun u8Array(n: Int): UByteArray {
requireSize(n)
val array = UByteArray(n)
for (i in 0 until n) {
array[i] = dv.getUint8(absolutePosition).toUByte()
position++
}
return array
}
override fun u16Array(n: Int): UShortArray {
requireSize(2 * n)
val array = UShortArray(n)
for (i in 0 until n) {
array[i] = dv.getUint16(absolutePosition, littleEndian).toUShort()
position += 2
}
return array
}
override fun u32Array(n: Int): UIntArray {
requireSize(4 * n)
val array = UIntArray(n)
for (i in 0 until n) {
array[i] = dv.getUint32(absolutePosition, littleEndian).toUInt()
position += 4
}
return array
}
override fun i32Array(n: Int): IntArray {
requireSize(4 * n)
val array = IntArray(n)
for (i in 0 until n) {
array[i] = dv.getInt32(absolutePosition, littleEndian)
position += 4
}
return array
}
override fun take(size: Int): Cursor {
val offset = offset + position val offset = offset + position
val wrapper = ArrayBufferCursor(backingBuffer, endianness, offset, size) val wrapper = ArrayBufferCursor(backingBuffer, endianness, offset, size)
this.position += size this.position += size
return wrapper return wrapper
} }
override fun buffer(size: Int): Buffer {
requireSize(size)
val r = Buffer.fromArrayBuffer(
backingBuffer.slice(absolutePosition, (absolutePosition + size)),
endianness
)
position += size
return r
}
override fun writeU8(value: UByte): WritableCursor {
requireSize(1)
dv.setUint8(absolutePosition, value.toByte())
position++
return this
}
override fun writeU16(value: UShort): WritableCursor {
requireSize(2)
dv.setUint16(absolutePosition, value.toShort(), littleEndian)
position += 2
return this
}
override fun writeU32(value: UInt): WritableCursor {
requireSize(4)
dv.setUint32(absolutePosition, value.toInt(), littleEndian)
position += 4
return this
}
override fun writeI8(value: Byte): WritableCursor {
requireSize(1)
dv.setInt8(absolutePosition, value)
position++
return this
}
override fun writeI16(value: Short): WritableCursor {
requireSize(2)
dv.setInt16(absolutePosition, value, littleEndian)
position += 2
return this
}
override fun writeI32(value: Int): WritableCursor {
requireSize(4)
dv.setInt32(absolutePosition, value, littleEndian)
position += 4
return this
}
override fun writeF32(value: Float): WritableCursor {
requireSize(4)
dv.setFloat32(absolutePosition, value, littleEndian)
position += 4
return this
}
} }
fun ArrayBuffer.cursor(endianness: Endianness): ArrayBufferCursor =
ArrayBufferCursor(this, endianness)

View File

@ -6,7 +6,7 @@ import io.ktor.client.features.json.serializer.*
import kotlinx.browser.document import kotlinx.browser.document
import kotlinx.browser.window import kotlinx.browser.window
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import org.w3c.dom.PopStateEvent import org.w3c.dom.PopStateEvent
import world.phantasmal.core.disposable.Disposable import world.phantasmal.core.disposable.Disposable
@ -47,7 +47,7 @@ private fun init(): Disposable {
val basePath = window.location.origin + val basePath = window.location.origin +
(if (pathname.lastOrNull() == '/') pathname.dropLast(1) else pathname) (if (pathname.lastOrNull() == '/') pathname.dropLast(1) else pathname)
val scope = CoroutineScope(Job()) val scope = CoroutineScope(SupervisorJob())
disposer.add(disposable { scope.cancel() }) disposer.add(disposable { scope.cancel() })
disposer.add( disposer.add(

View File

@ -5,7 +5,7 @@ import kotlinx.coroutines.launch
import org.w3c.files.File import org.w3c.files.File
import world.phantasmal.core.* import world.phantasmal.core.*
import world.phantasmal.lib.Endianness import world.phantasmal.lib.Endianness
import world.phantasmal.lib.cursor.ArrayBufferCursor import world.phantasmal.lib.cursor.cursor
import world.phantasmal.lib.fileFormats.quest.Quest import world.phantasmal.lib.fileFormats.quest.Quest
import world.phantasmal.lib.fileFormats.quest.parseBinDatToQuest import world.phantasmal.lib.fileFormats.quest.parseBinDatToQuest
import world.phantasmal.observable.value.Val import world.phantasmal.observable.value.Val
@ -17,7 +17,7 @@ import world.phantasmal.webui.readFile
class QuestEditorToolbarController( class QuestEditorToolbarController(
scope: CoroutineScope, scope: CoroutineScope,
private val questEditorStore: QuestEditorStore private val questEditorStore: QuestEditorStore,
) : Controller(scope) { ) : Controller(scope) {
private val _resultDialogVisible = mutableVal(false) private val _resultDialogVisible = mutableVal(false)
private val _result = mutableVal<PwResult<*>?>(null) private val _result = mutableVal<PwResult<*>?>(null)
@ -46,11 +46,9 @@ class QuestEditorToolbarController(
return@launch return@launch
} }
val binBuffer = readFile(bin)
val datBuffer = readFile(dat)
val parseResult = parseBinDatToQuest( val parseResult = parseBinDatToQuest(
ArrayBufferCursor(binBuffer, Endianness.Little), readFile(bin).cursor(Endianness.Little),
ArrayBufferCursor(datBuffer, Endianness.Little) readFile(dat).cursor(Endianness.Little),
) )
setResult(parseResult) setResult(parseResult)