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 -> {
val oldSize = seg.data.size
seg.data.size += bytes.size.toUInt()
seg.data.size += bytes.size
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) {
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) {
@ -288,7 +290,7 @@ private class Assembler(private val assembly: List<String>, private val manualSt
if (!prevLineHadLabel) {
segment = DataSegment(
labels = mutableListOf(label),
data = Buffer.withCapacity(0u),
data = Buffer.withCapacity(0),
srcLoc = SegmentSrcLoc(labels = mutableListOf(srcLoc)),
)
objectCode.add(segment!!)

View File

@ -6,102 +6,109 @@ import world.phantasmal.lib.Endianness
* Resizable, continuous block of bytes which is reallocated when necessary.
*/
expect class Buffer {
var size: UInt
var size: Int
/**
* Byte order mode.
*/
var endianness: Endianness
val capacity: UInt
val capacity: Int
/**
* 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.
*/
fun getU16(offset: UInt): UShort
fun getU16(offset: Int): UShort
/**
* 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.
*/
fun getI8(offset: UInt): Byte
fun getI8(offset: Int): Byte
/**
* 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.
*/
fun getI32(offset: UInt): Int
fun getI32(offset: Int): Int
/**
* 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.
*/
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.
*/
fun slice(offset: UInt, size: UInt): Buffer
fun slice(offset: Int, size: Int): Buffer
/**
* 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.
*/
fun setU16(offset: UInt, value: UShort): Buffer
fun setU16(offset: Int, value: UShort): Buffer
/**
* 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.
*/
fun setI8(offset: UInt, value: Byte): Buffer
fun setI8(offset: Int, value: Byte): Buffer
/**
* 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.
*/
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.
*/
fun setF32(offset: UInt, value: Float): Buffer
fun setF32(offset: Int, value: Float): Buffer
/**
* Writes 0 bytes to the entire buffer.
*/
fun zero(): Buffer
/**
* Writes [value] to every byte in the buffer.
*/
fun fill(value: Byte): Buffer
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
}

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.Success
import world.phantasmal.lib.buffer.Buffer
import world.phantasmal.lib.cursor.BufferCursor
import world.phantasmal.lib.cursor.Cursor
import world.phantasmal.lib.cursor.WritableCursor
import world.phantasmal.lib.cursor.cursor
import kotlin.math.floor
import kotlin.math.min
@ -16,26 +16,27 @@ private val logger = KotlinLogging.logger {}
fun prsDecompress(cursor: Cursor): PwResult<Cursor> {
try {
val ctx = Context(cursor)
val decompressor = PrsDecompressor(cursor)
var i = 0
while (true) {
if (ctx.readFlagBit() == 1u) {
if (decompressor.readFlagBit() == 1) {
// Single byte copy.
ctx.copyU8()
decompressor.copyU8()
} else {
// Multi byte copy.
var length: UInt
var length: Int
var offset: Int
if (ctx.readFlagBit() == 0u) {
if (decompressor.readFlagBit() == 0) {
// Short copy.
length = (ctx.readFlagBit() shl 1) or ctx.readFlagBit()
length += 2u
length = (decompressor.readFlagBit() shl 1) or decompressor.readFlagBit()
length += 2
offset = ctx.readU8().toInt() - 256
offset = decompressor.readU8().toInt() - 256
} else {
// 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.
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?
length = (offset and 0b111).toUInt()
offset = offset shr 3
length = offset and 0b111
offset = offset ushr 3
if (length == 0u) {
length = ctx.readU8().toUInt()
length += 1u
if (length == 0) {
length = decompressor.readU8().toInt()
length += 1
} else {
length += 2u
length += 2
}
offset -= 8192
}
ctx.offsetCopy(offset, length)
decompressor.offsetCopy(offset, length)
}
i++
}
return Success(ctx.dst.seekStart(0u))
return Success(decompressor.dst.seekStart(0))
} catch (e: Throwable) {
return PwResultBuilder<Cursor>(logger)
.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
val dst: WritableCursor = BufferCursor(
Buffer.withCapacity(floor(1.5 * cursor.size.toDouble()).toUInt(), cursor.endianness),
)
private var flags = 0u
val dst: WritableCursor =
Buffer.withCapacity(floor(1.5 * cursor.size.toDouble()).toInt(), cursor.endianness).cursor()
private var flags = 0
private var flagBitsLeft = 0
fun readFlagBit(): UInt {
fun readFlagBit(): Int {
// Fetch a new flag byte when the previous byte has been processed.
if (flagBitsLeft == 0) {
flags = readU8().toUInt()
flags = readU8().toInt()
flagBitsLeft = 8
}
val bit = flags and 1u
flags = flags shr 1
val bit = flags and 1
flags = flags ushr 1
flagBitsLeft -= 1
return bit
}
@ -97,25 +99,26 @@ class Context(cursor: Cursor) {
fun readU16(): UShort = src.u16()
fun offsetCopy(offset: Int, length: UInt) {
fun offsetCopy(offset: Int, length: Int) {
require(offset in -8192..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."
}
// The length can be larger than -offset, in that case we copy -offset bytes size/-offset times.
val bufSize = min((-offset).toUInt(), length)
// The length can be larger than -offset, in that case we copy -offset bytes size/-offset
// times.
val bufSize = min(-offset, length)
dst.seek(offset)
val buf = dst.take(bufSize)
dst.seek(-offset - bufSize.toInt())
dst.seek(-offset - bufSize)
repeat((length / bufSize).toInt()) {
repeat(length / bufSize) {
dst.writeCursor(buf)
buf.seekStart(0u)
buf.seekStart(0)
}
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_U8
import world.phantasmal.lib.buffer.Buffer
import kotlin.math.min
abstract class AbstractWritableCursor
protected constructor(protected val offset: UInt) : WritableCursor {
override var position: UInt = 0u
protected constructor(protected val offset: Int) : WritableCursor {
override var position: Int = 0
protected set
override val bytesLeft: UInt
override val bytesLeft: Int
get() = size - position
protected val absolutePosition: UInt
protected val absolutePosition: Int
get() = offset + position
override fun hasBytesLeft(bytes: UInt): Boolean =
override fun hasBytesLeft(bytes: Int): Boolean =
bytesLeft >= bytes
override fun seek(offset: Int): WritableCursor =
seekStart((position.toInt() + offset).toUInt())
seekStart(position + offset)
override fun seekStart(offset: UInt): WritableCursor {
require(offset <= size) { "Offset $offset is out of bounds." }
override fun seekStart(offset: Int): WritableCursor {
require(offset >= 0 || offset <= size) { "Offset $offset is out of bounds." }
position = offset
return this
}
override fun seekEnd(offset: UInt): WritableCursor {
require(offset <= size) { "Offset $offset is out of bounds." }
override fun seekEnd(offset: Int): WritableCursor {
require(offset >= 0 || offset <= size) { "Offset $offset is out of bounds." }
position = size - offset
return this
}
override fun stringAscii(
maxByteLength: UInt,
maxByteLength: Int,
nullTerminated: Boolean,
dropRemaining: Boolean,
): String =
buildString {
for (i in 0u until maxByteLength) {
for (i in 0 until maxByteLength) {
val codePoint = u8()
if (nullTerminated && codePoint == ZERO_U8) {
if (dropRemaining) {
seek((maxByteLength - i - 1u).toInt())
seek(maxByteLength - i - 1)
}
break
@ -58,19 +57,19 @@ protected constructor(protected val offset: UInt) : WritableCursor {
}
override fun stringUtf16(
maxByteLength: UInt,
maxByteLength: Int,
nullTerminated: Boolean,
dropRemaining: Boolean,
): String =
buildString {
val len = maxByteLength / 2u
val len = maxByteLength / 2
for (i in 0u until len) {
for (i in 0 until len) {
val codePoint = u16()
if (nullTerminated && codePoint == ZERO_U16) {
if (dropRemaining) {
seek((maxByteLength - 2u * i - 2u).toInt())
seek(maxByteLength - 2 * i - 2)
}
break
@ -82,7 +81,7 @@ protected constructor(protected val offset: UInt) : WritableCursor {
override fun writeU8Array(array: UByteArray): WritableCursor {
val len = array.size
requireSize(len.toUInt())
requireSize(len)
for (i in 0 until len) {
writeU8(array[i])
@ -93,7 +92,7 @@ protected constructor(protected val offset: UInt) : WritableCursor {
override fun writeU16Array(array: UShortArray): WritableCursor {
val len = array.size
requireSize(2u * len.toUInt())
requireSize(2 * len)
for (i in 0 until len) {
writeU16(array[i])
@ -104,7 +103,7 @@ protected constructor(protected val offset: UInt) : WritableCursor {
override fun writeU32Array(array: UIntArray): WritableCursor {
val len = array.size
requireSize(4u * len.toUInt())
requireSize(4 * len)
for (i in 0 until len) {
writeU32(array[i])
@ -115,7 +114,7 @@ protected constructor(protected val offset: UInt) : WritableCursor {
override fun writeI32Array(array: IntArray): WritableCursor {
val len = array.size
requireSize(4u * len.toUInt())
requireSize(4 * len)
for (i in 0 until len) {
writeI32(array[i])
@ -127,51 +126,45 @@ protected constructor(protected val offset: UInt) : WritableCursor {
override fun writeCursor(other: Cursor): WritableCursor {
val size = other.bytesLeft
requireSize(size)
for (i in 0u until (size / 4u)) {
writeU32(other.u32())
for (i in 0 until size) {
writeI8(other.i8())
}
for (i in 0u until (size % 4u)) {
writeU8(other.u8())
}
position += size
return this
}
override fun writeStringAscii(str: String, byteLength: UInt): WritableCursor {
override fun writeStringAscii(str: String, byteLength: Int): WritableCursor {
requireSize(byteLength)
val len = min(byteLength.toInt(), str.length)
val len = min(byteLength, str.length)
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) {
writeU8(0u)
writeI8(0)
}
return this
}
override fun writeStringUtf16(str: String, byteLength: UInt): WritableCursor {
override fun writeStringUtf16(str: String, byteLength: Int): WritableCursor {
requireSize(byteLength)
val maxLen = byteLength.toInt() / 2
val maxLen = byteLength / 2
val len = min(maxLen, str.length)
for (i in 0 until len) {
writeU16(str[i].toShort().toUShort())
writeI16(str[i].toShort())
}
val padLen = maxLen - len
for (i in 0 until padLen) {
writeU16(0u)
writeI16(0)
}
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].
*/
protected fun requireSize(size: UInt) {
protected fun requireSize(size: Int) {
val left = this.size - position
require(size <= left) { "$size Bytes required but only $left available." }

View File

@ -10,12 +10,12 @@ import world.phantasmal.lib.buffer.Buffer
*/
class BufferCursor(
private val buffer: Buffer,
offset: UInt = 0u,
size: UInt = buffer.size - offset,
offset: Int = 0,
size: Int = buffer.size - offset,
) : AbstractWritableCursor(offset) {
private var _size = size
override var size: UInt
override var size: Int
get() = _size
set(value) {
if (value > _size) {
@ -52,13 +52,13 @@ class BufferCursor(
override fun u16(): UShort {
val r = buffer.getU16(absolutePosition)
position += 2u
position += 2
return r
}
override fun u32(): UInt {
val r = buffer.getU32(absolutePosition)
position += 4u
position += 4
return r
}
@ -70,28 +70,28 @@ class BufferCursor(
override fun i16(): Short {
val r = buffer.getI16(absolutePosition)
position += 2u
position += 2
return r
}
override fun i32(): Int {
val r = buffer.getI32(absolutePosition)
position += 4u
position += 4
return r
}
override fun f32(): Float {
val r = buffer.getF32(absolutePosition)
position += 4u
position += 4
return r
}
override fun u8Array(n: UInt): UByteArray {
override fun u8Array(n: Int): UByteArray {
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)
position++
}
@ -99,123 +99,123 @@ class BufferCursor(
return array
}
override fun u16Array(n: UInt): UShortArray {
requireSize(2u * n)
override fun u16Array(n: Int): UShortArray {
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)
position += 2u
position += 2
}
return array
}
override fun u32Array(n: UInt): UIntArray {
requireSize(4u * n)
override fun u32Array(n: Int): UIntArray {
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)
position += 4u
position += 4
}
return array
}
override fun i32Array(n: UInt): IntArray {
requireSize(4u * n)
override fun i32Array(n: Int): IntArray {
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)
position += 4u
position += 4
}
return array
}
override fun take(size: UInt): Cursor {
override fun take(size: Int): Cursor {
val wrapper = BufferCursor(buffer, offset = absolutePosition, size)
position += size
return wrapper
}
override fun buffer(size: UInt): Buffer {
override fun buffer(size: Int): Buffer {
val wrapper = buffer.slice(offset = absolutePosition, size)
position += size
return wrapper
}
override fun writeU8(value: UByte): WritableCursor {
ensureSpace(1u)
ensureSpace(1)
buffer.setU8(absolutePosition, value)
position++
return this
}
override fun writeU16(value: UShort): WritableCursor {
ensureSpace(2u)
ensureSpace(2)
buffer.setU16(absolutePosition, value)
position += 2u
position += 2
return this
}
override fun writeU32(value: UInt): WritableCursor {
ensureSpace(4u)
ensureSpace(4)
buffer.setU32(absolutePosition, value)
position += 4u
position += 4
return this
}
override fun writeI8(value: Byte): WritableCursor {
ensureSpace(1u)
ensureSpace(1)
buffer.setI8(absolutePosition, value)
position++
return this
}
override fun writeI16(value: Short): WritableCursor {
ensureSpace(2u)
ensureSpace(2)
buffer.setI16(absolutePosition, value)
position += 2u
position += 2
return this
}
override fun writeI32(value: Int): WritableCursor {
ensureSpace(4u)
ensureSpace(4)
buffer.setI32(absolutePosition, value)
position += 4u
position += 4
return this
}
override fun writeF32(value: Float): WritableCursor {
ensureSpace(4u)
ensureSpace(4)
buffer.setF32(absolutePosition, value)
position += 4u
position += 4
return this
}
override fun writeU8Array(array: UByteArray): WritableCursor {
ensureSpace(array.size.toUInt())
ensureSpace(array.size)
return super.writeU8Array(array)
}
override fun writeU16Array(array: UShortArray): WritableCursor {
ensureSpace(2u * array.size.toUInt())
ensureSpace(2 * array.size)
return super.writeU16Array(array)
}
override fun writeU32Array(array: UIntArray): WritableCursor {
ensureSpace(4u * array.size.toUInt())
ensureSpace(4 * array.size)
return super.writeU32Array(array)
}
override fun writeI32Array(array: IntArray): WritableCursor {
ensureSpace(4u * array.size.toUInt())
ensureSpace(4 * array.size)
return super.writeI32Array(array)
}
@ -225,21 +225,21 @@ class BufferCursor(
return super.writeCursor(other)
}
override fun writeStringAscii(str: String, byteLength: UInt): WritableCursor {
override fun writeStringAscii(str: String, byteLength: Int): WritableCursor {
ensureSpace(byteLength)
return super.writeStringAscii(str, byteLength)
}
override fun writeStringUtf16(str: String, byteLength: UInt): WritableCursor {
override fun writeStringUtf16(str: String, byteLength: Int): WritableCursor {
ensureSpace(byteLength)
return super.writeStringUtf16(str, byteLength)
}
private fun ensureSpace(size: UInt) {
val needed = (position + size).toInt() - _size.toInt()
private fun ensureSpace(size: Int) {
val needed = (position + size) - _size
if (needed > 0) {
_size += needed.toUInt()
_size += needed
if (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.
*/
interface Cursor {
val size: UInt
val size: Int
/**
* The position from where bytes will be read or written.
*/
val position: UInt
val position: Int
/**
* Byte order mode.
*/
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.
@ -34,16 +34,16 @@ interface Cursor {
/**
* 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.
*
* @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.
@ -83,36 +83,36 @@ interface Cursor {
/**
* 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].
*/
fun u16Array(n: UInt): UShortArray
fun u16Array(n: Int): UShortArray
/**
* 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].
*/
fun i32Array(n: UInt): IntArray
fun i32Array(n: Int): IntArray
/**
* Consumes a variable number of bytes.
*
* @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.
*/
fun stringAscii(
maxByteLength: UInt,
maxByteLength: Int,
nullTerminated: Boolean,
dropRemaining: Boolean,
): String
@ -121,7 +121,7 @@ interface Cursor {
* Consumes up to [maxByteLength] bytes.
*/
fun stringUtf16(
maxByteLength: UInt,
maxByteLength: Int,
nullTerminated: Boolean,
dropRemaining: Boolean,
): String
@ -129,5 +129,5 @@ interface Cursor {
/**
* 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.
*/
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.
@ -74,12 +80,12 @@ interface WritableCursor : Cursor {
* Writes [byteLength] characters of [str]. If [str] is shorter than [byteLength], nul bytes
* 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
* [byteLength] bytes can be written this way, nul bytes will be inserted until [byteLength]
* 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 {}
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.
@ -27,16 +27,16 @@ fun parseIffHeaders(cursor: Cursor): PwResult<List<IffChunkHeader>> =
private fun <T> parse(
cursor: Cursor,
getChunk: (Cursor, type: UInt, size: UInt) -> T,
getChunk: (Cursor, type: Int, size: Int) -> T,
): PwResult<List<T>> {
val result = PwResult.build<List<T>>(logger)
val chunks = mutableListOf<T>()
var corrupted = false
while (cursor.bytesLeft >= 8u) {
val type = cursor.u32()
while (cursor.bytesLeft >= 8) {
val type = cursor.i32()
val sizePos = cursor.position
val size = cursor.u32()
val size = cursor.i32()
if (size > cursor.bytesLeft) {
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.vec3F32
private const val NJCM: UInt = 0x4D434A4Eu
private const val NJCM: Int = 0x4D434A4E
class NjObject<Model>(
val evaluationFlags: NjEvaluationFlags,
@ -72,7 +72,7 @@ private fun <Model, Context> parseSiblingObjects(
val skip = (evalFlags and 0b1000000u) != 0u
val shapeSkip = (evalFlags and 0b10000000u) != 0u
val modelOffset = cursor.u32()
val modelOffset = cursor.i32()
val pos = cursor.vec3F32()
val rotation = Vec3(
angleToRad(cursor.i32()),
@ -80,24 +80,24 @@ private fun <Model, Context> parseSiblingObjects(
angleToRad(cursor.i32()),
)
val scale = cursor.vec3F32()
val childOffset = cursor.u32()
val siblingOffset = cursor.u32()
val childOffset = cursor.i32()
val siblingOffset = cursor.i32()
val model = if (modelOffset == 0u) {
val model = if (modelOffset == 0) {
null
} else {
cursor.seekStart(modelOffset)
parse_model(cursor, context)
}
val children = if (childOffset == 0u) {
val children = if (childOffset == 0) {
emptyList()
} else {
cursor.seekStart(childOffset)
parseSiblingObjects(cursor, parse_model, context)
}
val siblings = if (siblingOffset == 0u) {
val siblings = if (siblingOffset == 0) {
emptyList()
} else {
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 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)
@ -122,15 +122,15 @@ class NjcmErgb(
val b: UByte,
)
fun parseNjcmModel(cursor: Cursor, cachedChunkOffsets: MutableMap<UByte, UInt>): NjcmModel {
val vlistOffset = cursor.u32() // Vertex list
val plistOffset = cursor.u32() // Triangle strip index list
fun parseNjcmModel(cursor: Cursor, cachedChunkOffsets: MutableMap<UByte, Int>): NjcmModel {
val vlistOffset = cursor.i32() // Vertex list
val plistOffset = cursor.i32() // Triangle strip index list
val boundingSphereCenter = cursor.vec3F32()
val boundingSphereRadius = cursor.f32()
val vertices: MutableList<NjcmVertex> = mutableListOf()
val meshes: MutableList<NjcmTriangleStrip> = mutableListOf()
if (vlistOffset != 0u) {
if (vlistOffset != 0) {
cursor.seekStart(vlistOffset)
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)
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.
private fun parseChunks(
cursor: Cursor,
cachedChunkOffsets: MutableMap<UByte, UInt>,
cachedChunkOffsets: MutableMap<UByte, Int>,
wideEndChunks: Boolean,
): List<NjcmChunk> {
val chunks: MutableList<NjcmChunk> = mutableListOf()
@ -214,7 +214,7 @@ private fun parseChunks(
val flags = cursor.u8()
val flagsUInt = flags.toUInt()
val chunkStartPosition = cursor.position
var size = 0u
var size = 0
when (typeId.toInt()) {
0 -> {
@ -253,7 +253,7 @@ private fun parseChunks(
))
}
in 8..9 -> {
size = 2u
size = 2
val textureBitsAndId = cursor.u16().toUInt()
chunks.add(NjcmChunk.Tiny(
@ -269,7 +269,7 @@ private fun parseChunks(
))
}
in 17..31 -> {
size = 2u + 2u * cursor.u16()
size = 2 + 2 * cursor.i16()
var diffuse: NjcmArgb? = null
var ambient: NjcmArgb? = null
@ -312,32 +312,32 @@ private fun parseChunks(
))
}
in 32..50 -> {
size = 2u + 4u * cursor.u16()
size = 2 + 4 * cursor.i16()
chunks.add(NjcmChunk.Vertex(
typeId,
vertices = parseVertexChunk(cursor, typeId, flags),
))
}
in 56..58 -> {
size = 2u + 2u * cursor.u16()
size = 2 + 2 * cursor.i16()
chunks.add(NjcmChunk.Volume(
typeId,
))
}
in 64..75 -> {
size = 2u + 2u * cursor.u16()
size = 2 + 2 * cursor.i16()
chunks.add(NjcmChunk.Strip(
typeId,
triangleStrips = parseTriangleStripChunk(cursor, typeId, flags),
))
}
255 -> {
size = if (wideEndChunks) 2u else 0u
size = if (wideEndChunks) 2 else 0
chunks.add(NjcmChunk.End)
loop = false
}
else -> {
size = 2u + 2u * cursor.u16()
size = 2 + 2 * cursor.i16()
chunks.add(NjcmChunk.Unknown(
typeId,
))

View File

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

View File

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

View File

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

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.
*/
var scriptLabel: Int
get() = data.getF32(60u).roundToInt()
get() = data.getF32(60).roundToInt()
set(value) {
data.setF32(60u, value.toFloat())
data.setF32(60, value.toFloat())
}
var skin: Int
get() = data.getI32(64u)
get() = data.getI32(64)
set(value) {
data.setI32(64u, value)
data.setI32(64, value)
}
init {

View File

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

View File

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

View File

@ -1,6 +1,7 @@
package world.phantasmal.lib.cursor
import world.phantasmal.lib.Endianness
import world.phantasmal.lib.buffer.Buffer
import kotlin.math.abs
import kotlin.test.Test
import kotlin.test.assertEquals
@ -18,15 +19,15 @@ abstract class WritableCursorTests : CursorTests() {
private fun simple_WritableCursor_properties_and_invariants(endianness: 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.seek(-1)
assertEquals(cursor.position + cursor.bytesLeft, cursor.size)
assertEquals(10u, cursor.size)
assertEquals(3u, cursor.position)
assertEquals(7u, cursor.bytesLeft)
assertEquals(10, cursor.size)
assertEquals(3, cursor.position)
assertEquals(7, cursor.bytesLeft)
assertEquals(endianness, cursor.endianness)
}
@ -83,9 +84,9 @@ abstract class WritableCursorTests : CursorTests() {
cursor.write(expectedNumber1)
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(expectedNumber2, cursor.read())
@ -106,23 +107,23 @@ abstract class WritableCursorTests : CursorTests() {
cursor.writeF32(1337.9001f)
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
// they're backed by numbers (64-bit floats).
assertTrue(abs(1337.9001f - cursor.f32()) < 0.001)
assertTrue(abs(103.502f - cursor.f32()) < 0.001)
assertEquals(8u, cursor.position)
assertEquals(8, cursor.position)
}
@Test
fun writeU8Array() {
val read: Cursor.(UInt) -> IntArray = { n ->
val read: Cursor.(Int) -> IntArray = { n ->
val arr = u8Array(n)
IntArray(n.toInt()) { arr[it].toInt() }
IntArray(n) { arr[it].toInt() }
}
val write: WritableCursor.(IntArray) -> Unit = { a ->
writeU8Array(UByteArray(a.size) { a[it].toUByte() })
@ -134,9 +135,9 @@ abstract class WritableCursorTests : CursorTests() {
@Test
fun writeU16Array() {
val read: Cursor.(UInt) -> IntArray = { n ->
val read: Cursor.(Int) -> IntArray = { n ->
val arr = u16Array(n)
IntArray(n.toInt()) { arr[it].toInt() }
IntArray(n) { arr[it].toInt() }
}
val write: WritableCursor.(IntArray) -> Unit = { a ->
writeU16Array(UShortArray(a.size) { a[it].toUShort() })
@ -148,9 +149,9 @@ abstract class WritableCursorTests : CursorTests() {
@Test
fun writeU32Array() {
val read: Cursor.(UInt) -> IntArray = { n ->
val read: Cursor.(Int) -> IntArray = { n ->
val arr = u32Array(n)
IntArray(n.toInt()) { arr[it].toInt() }
IntArray(n) { arr[it].toInt() }
}
val write: WritableCursor.(IntArray) -> Unit = { a ->
writeU32Array(UIntArray(a.size) { a[it].toUInt() })
@ -162,7 +163,7 @@ abstract class WritableCursorTests : CursorTests() {
@Test
fun writeI32Array() {
val read: Cursor.(UInt) -> IntArray = { n ->
val read: Cursor.(Int) -> IntArray = { n ->
i32Array(n)
}
val write: WritableCursor.(IntArray) -> Unit = { a ->
@ -175,7 +176,7 @@ abstract class WritableCursorTests : CursorTests() {
private fun testIntegerArrayWrite(
byteCount: Int,
read: Cursor.(UInt) -> IntArray,
read: Cursor.(Int) -> IntArray,
write: WritableCursor.(IntArray) -> Unit,
endianness: Endianness,
) {
@ -185,16 +186,42 @@ abstract class WritableCursorTests : CursorTests() {
val cursor = createCursor(ByteArray(20 * byteCount), endianness)
cursor.write(testArray1)
assertEquals(10u * byteCount.toUInt(), cursor.position)
assertEquals(10 * byteCount, cursor.position)
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(testArray2.contentEquals(cursor.read(10u)))
assertEquals(20u * byteCount.toUInt(), cursor.position)
assertTrue(testArray1.contentEquals(cursor.read(10)))
assertTrue(testArray2.contentEquals(cursor.read(10)))
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
@ -208,10 +235,11 @@ abstract class WritableCursorTests : CursorTests() {
cursor.writeU32(1u).writeU32(2u).writeU32(3u).writeU32(4u)
cursor.seek(-8)
val newCursor = cursor.take(8u)
val newCursor = cursor.take(8)
assertEquals(8u, newCursor.size)
assertEquals(0u, newCursor.position)
assertEquals(16, cursor.position)
assertEquals(8, newCursor.size)
assertEquals(0, newCursor.position)
assertEquals(3u, 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.DataView
import org.khronos.webgl.Int8Array
import org.khronos.webgl.Uint8Array
import world.phantasmal.lib.Endianness
import world.phantasmal.lib.ZERO_U16
actual class Buffer private constructor(
private var arrayBuffer: ArrayBuffer,
size: UInt,
size: Int,
endianness: Endianness,
) {
private var dataView = DataView(arrayBuffer)
private var littleEndian = endianness == Endianness.Little
actual var size: UInt = size
actual var size: Int = size
set(value) {
ensureCapacity(value)
field = value
@ -26,54 +27,54 @@ actual class Buffer private constructor(
littleEndian = value == Endianness.Little
}
actual val capacity: UInt
get() = arrayBuffer.byteLength.toUInt()
actual val capacity: Int
get() = arrayBuffer.byteLength
actual fun getU8(offset: UInt): UByte {
checkOffset(offset, 1u)
return dataView.getUint8(offset.toInt()).toUByte()
actual fun getU8(offset: Int): UByte {
checkOffset(offset, 1)
return dataView.getUint8(offset).toUByte()
}
actual fun getU16(offset: UInt): UShort {
checkOffset(offset, 2u)
return dataView.getUint16(offset.toInt(), littleEndian).toUShort()
actual fun getU16(offset: Int): UShort {
checkOffset(offset, 2)
return dataView.getUint16(offset, littleEndian).toUShort()
}
actual fun getU32(offset: UInt): UInt {
checkOffset(offset, 4u)
return dataView.getUint32(offset.toInt(), littleEndian).toUInt()
actual fun getU32(offset: Int): UInt {
checkOffset(offset, 4)
return dataView.getUint32(offset, littleEndian).toUInt()
}
actual fun getI8(offset: UInt): Byte {
checkOffset(offset, 1u)
return dataView.getInt8(offset.toInt())
actual fun getI8(offset: Int): Byte {
checkOffset(offset, 1)
return dataView.getInt8(offset)
}
actual fun getI16(offset: UInt): Short {
checkOffset(offset, 2u)
return dataView.getInt16(offset.toInt(), littleEndian)
actual fun getI16(offset: Int): Short {
checkOffset(offset, 2)
return dataView.getInt16(offset, littleEndian)
}
actual fun getI32(offset: UInt): Int {
checkOffset(offset, 4u)
return dataView.getInt32(offset.toInt(), littleEndian)
actual fun getI32(offset: Int): Int {
checkOffset(offset, 4)
return dataView.getInt32(offset, littleEndian)
}
actual fun getF32(offset: UInt): Float {
checkOffset(offset, 4u)
return dataView.getFloat32(offset.toInt(), littleEndian)
actual fun getF32(offset: Int): Float {
checkOffset(offset, 4)
return dataView.getFloat32(offset, littleEndian)
}
actual fun getStringUtf16(
offset: UInt,
maxByteLength: UInt,
offset: Int,
maxByteLength: Int,
nullTerminated: Boolean,
): String =
buildString {
val len = maxByteLength / 2u
val len = maxByteLength / 2
for (i in 0u until len) {
val codePoint = getU16(offset + i * 2u)
for (i in 0 until len) {
val codePoint = getU16(offset + i * 2)
if (nullTerminated && codePoint == ZERO_U16) {
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)
return fromArrayBuffer(
arrayBuffer.slice(offset.toInt(), (offset + size).toInt()),
arrayBuffer.slice(offset, (offset + size)),
endianness
)
}
/**
* Writes an unsigned 8-bit integer at the given offset.
*/
actual fun setU8(offset: UInt, value: UByte): Buffer {
checkOffset(offset, 1u)
dataView.setUint8(offset.toInt(), value.toByte())
actual fun setU8(offset: Int, value: UByte): Buffer {
checkOffset(offset, 1)
dataView.setUint8(offset, value.toByte())
return this
}
/**
* Writes an unsigned 16-bit integer at the given offset.
*/
actual fun setU16(offset: UInt, value: UShort): Buffer {
checkOffset(offset, 2u)
dataView.setUint16(offset.toInt(), value.toShort(), littleEndian)
actual fun setU16(offset: Int, value: UShort): Buffer {
checkOffset(offset, 2)
dataView.setUint16(offset, value.toShort(), littleEndian)
return this
}
/**
* Writes an unsigned 32-bit integer at the given offset.
*/
actual fun setU32(offset: UInt, value: UInt): Buffer {
checkOffset(offset, 4u)
dataView.setUint32(offset.toInt(), value.toInt(), littleEndian)
actual fun setU32(offset: Int, value: UInt): Buffer {
checkOffset(offset, 4)
dataView.setUint32(offset, value.toInt(), littleEndian)
return this
}
/**
* Writes a signed 8-bit integer at the given offset.
*/
actual fun setI8(offset: UInt, value: Byte): Buffer {
checkOffset(offset, 1u)
dataView.setInt8(offset.toInt(), value)
actual fun setI8(offset: Int, value: Byte): Buffer {
checkOffset(offset, 1)
dataView.setInt8(offset, value)
return this
}
/**
* Writes a signed 16-bit integer at the given offset.
*/
actual fun setI16(offset: UInt, value: Short): Buffer {
checkOffset(offset, 2u)
dataView.setInt16(offset.toInt(), value, littleEndian)
actual fun setI16(offset: Int, value: Short): Buffer {
checkOffset(offset, 2)
dataView.setInt16(offset, value, littleEndian)
return this
}
/**
* Writes a signed 32-bit integer at the given offset.
*/
actual fun setI32(offset: UInt, value: Int): Buffer {
checkOffset(offset, 4u)
dataView.setInt32(offset.toInt(), value, littleEndian)
actual fun setI32(offset: Int, value: Int): Buffer {
checkOffset(offset, 4)
dataView.setInt32(offset, value, littleEndian)
return this
}
/**
* Writes a 32-bit floating point number at the given offset.
*/
actual fun setF32(offset: UInt, value: Float): Buffer {
checkOffset(offset, 4u)
dataView.setFloat32(offset.toInt(), value, littleEndian)
actual fun setF32(offset: Int, value: Float): Buffer {
checkOffset(offset, 4)
dataView.setFloat32(offset, value, littleEndian)
return this
}
/**
* Writes 0 bytes to the entire buffer.
*/
actual fun zero(): Buffer {
(Uint8Array(arrayBuffer).asDynamic()).fill(0)
actual fun zero(): Buffer =
fill(0)
actual fun fill(value: Byte): Buffer {
(Int8Array(arrayBuffer).asDynamic()).fill(value)
return this
}
/**
* Checks whether we can read [size] bytes at [offset].
*/
private fun checkOffset(offset: UInt, size: UInt) {
require(offset + size <= this.size) {
private fun checkOffset(offset: Int, size: Int) {
require(offset >= 0 && offset + size <= this.size) {
"Offset $offset is out of bounds."
}
}
@ -174,16 +154,16 @@ actual class Buffer private constructor(
/**
* Reallocates the underlying ArrayBuffer if necessary.
*/
private fun ensureCapacity(minNewSize: UInt) {
private fun ensureCapacity(minNewSize: Int) {
if (minNewSize > capacity) {
var newSize = if (capacity == 0u) minNewSize else capacity;
var newSize = if (capacity == 0) minNewSize else capacity;
do {
newSize *= 2u;
newSize *= 2;
} while (newSize < minNewSize);
val newBuffer = ArrayBuffer(newSize.toInt());
Uint8Array(newBuffer).set(Uint8Array(arrayBuffer, 0, size.toInt()));
val newBuffer = ArrayBuffer(newSize);
Uint8Array(newBuffer).set(Uint8Array(arrayBuffer, 0, size));
arrayBuffer = newBuffer;
dataView = DataView(arrayBuffer);
}
@ -191,19 +171,21 @@ actual class Buffer private constructor(
actual companion object {
actual fun withCapacity(
initialCapacity: UInt,
initialCapacity: Int,
endianness: Endianness,
): 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 {
val arrayBuffer = ArrayBuffer(array.size)
Uint8Array(arrayBuffer).set(array.toTypedArray())
return Buffer(arrayBuffer, array.size.toUInt(), endianness)
Int8Array(arrayBuffer).set(array.toTypedArray())
return Buffer(arrayBuffer, array.size, endianness)
}
fun fromArrayBuffer(arrayBuffer: ArrayBuffer, endianness: Endianness): Buffer {
return Buffer(arrayBuffer, arrayBuffer.byteLength.toUInt(), endianness)
}
fun fromArrayBuffer(arrayBuffer: ArrayBuffer, endianness: Endianness): Buffer =
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.DataView
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.
@ -15,22 +16,192 @@ import world.phantasmal.lib.Endianness
class ArrayBufferCursor(
buffer: ArrayBuffer,
endianness: Endianness,
offset: UInt = 0u,
size: UInt = buffer.byteLength.toUInt() - offset,
) : AbstractArrayBufferCursor(endianness, offset) {
override val backingBuffer = buffer
override val dv = DataView(buffer, 0, buffer.byteLength)
offset: Int = 0,
size: Int = buffer.byteLength - offset,
) : AbstractWritableCursor(offset) {
private var littleEndian: Boolean = endianness == Endianness.Little
private val backingBuffer = buffer
private val dv = DataView(buffer)
override var size: UInt = size
override var size: Int = size
set(value) {
require(size <= backingBuffer.byteLength.toUInt() - offset)
require(size <= backingBuffer.byteLength - offset)
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 wrapper = ArrayBufferCursor(backingBuffer, endianness, offset, size)
this.position += size
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.window
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import org.w3c.dom.PopStateEvent
import world.phantasmal.core.disposable.Disposable
@ -47,7 +47,7 @@ private fun init(): Disposable {
val basePath = window.location.origin +
(if (pathname.lastOrNull() == '/') pathname.dropLast(1) else pathname)
val scope = CoroutineScope(Job())
val scope = CoroutineScope(SupervisorJob())
disposer.add(disposable { scope.cancel() })
disposer.add(

View File

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