Added .bin and .dat parsers and Buffer.

This commit is contained in:
Daan Vanden Bosch 2020-10-19 17:40:33 +02:00
parent d94560c8e0
commit f2532de792
16 changed files with 1088 additions and 17 deletions

View File

@ -14,7 +14,7 @@ sealed class PwResult<out T>(val problems: List<Problem>) {
}
}
class Success<T>(val value: T, problems: List<Problem>) : PwResult<T>(problems)
class Success<T>(val value: T, problems: List<Problem> = emptyList()) : PwResult<T>(problems)
class Failure(problems: List<Problem>) : PwResult<Nothing>(problems)

View File

@ -1,4 +1,4 @@
package world.phantasmal.lib.cursor
package world.phantasmal.lib
enum class Endianness {
Little,

View File

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

View File

@ -2,6 +2,7 @@ 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
@ -15,6 +16,9 @@ protected constructor(protected val offset: UInt) : WritableCursor {
protected val absolutePosition: UInt
get() = offset + position
override fun hasBytesLeft(bytes: UInt): Boolean =
bytesLeft >= bytes
override fun seek(offset: Int): WritableCursor =
seekStart((position.toInt() + offset).toUInt())

View File

@ -0,0 +1,249 @@
package world.phantasmal.lib.cursor
import world.phantasmal.lib.Endianness
import world.phantasmal.lib.buffer.Buffer
/**
* @param buffer The Buffer to read from and write to.
* @param offset The start offset of the part that will be read from.
* @param size The size of the part that will be read from.
*/
class BufferCursor(
private val buffer: Buffer,
offset: UInt = 0u,
size: UInt = buffer.size - offset,
) : AbstractWritableCursor(offset) {
private var _size = size
override var size: UInt
get() = _size
set(value) {
if (value > _size) {
ensureSpace(value)
} else {
_size = value
}
}
/**
* Mirrors the underlying buffer's endianness.
*/
override var endianness: Endianness
get() = buffer.endianness
set(value) {
buffer.endianness = value
}
init {
require(offset <= buffer.size) {
"Offset $offset is out of bounds."
}
require(offset + size <= buffer.size) {
"Size $size is out of bounds."
}
}
override fun u8(): UByte {
val r = buffer.getU8(absolutePosition)
position++
return r
}
override fun u16(): UShort {
val r = buffer.getU16(absolutePosition)
position += 2u
return r
}
override fun u32(): UInt {
val r = buffer.getU32(absolutePosition)
position += 4u
return r
}
override fun i8(): Byte {
val r = buffer.getI8(absolutePosition)
position++
return r
}
override fun i16(): Short {
val r = buffer.getI16(absolutePosition)
position += 2u
return r
}
override fun i32(): Int {
val r = buffer.getI32(absolutePosition)
position += 4u
return r
}
override fun f32(): Float {
val r = buffer.getF32(absolutePosition)
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] = buffer.getU8(absolutePosition)
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] = buffer.getU16(absolutePosition)
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] = buffer.getU32(absolutePosition)
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] = buffer.getI32(absolutePosition)
position += 4u
}
return array
}
override fun take(size: UInt): Cursor {
val wrapper = BufferCursor(buffer, offset = absolutePosition, size)
position += size
return wrapper
}
override fun buffer(size: UInt): Buffer {
val wrapper = buffer.slice(offset = absolutePosition, size)
position += size
return wrapper
}
override fun writeU8(value: UByte): WritableCursor {
ensureSpace(1u)
buffer.setU8(absolutePosition, value)
position++
return this
}
override fun writeU16(value: UShort): WritableCursor {
ensureSpace(2u)
buffer.setU16(absolutePosition, value)
position += 2u
return this
}
override fun writeU32(value: UInt): WritableCursor {
ensureSpace(4u)
buffer.setU32(absolutePosition, value)
position += 4u
return this
}
override fun writeI8(value: Byte): WritableCursor {
ensureSpace(1u)
buffer.setI8(absolutePosition, value)
position++
return this
}
override fun writeI16(value: Short): WritableCursor {
ensureSpace(2u)
buffer.setI16(absolutePosition, value)
position += 2u
return this
}
override fun writeI32(value: Int): WritableCursor {
ensureSpace(4u)
buffer.setI32(absolutePosition, value)
position += 4u
return this
}
override fun writeF32(value: Float): WritableCursor {
ensureSpace(4u)
buffer.setF32(absolutePosition, value)
position += 4u
return this
}
override fun writeU8Array(array: UByteArray): WritableCursor {
ensureSpace(array.size.toUInt())
return super.writeU8Array(array)
}
override fun writeU16Array(array: UShortArray): WritableCursor {
ensureSpace(2u * array.size.toUInt())
return super.writeU16Array(array)
}
override fun writeU32Array(array: UIntArray): WritableCursor {
ensureSpace(4u * array.size.toUInt())
return super.writeU32Array(array)
}
override fun writeI32Array(array: IntArray): WritableCursor {
ensureSpace(4u * array.size.toUInt())
return super.writeI32Array(array)
}
override fun writeCursor(other: Cursor): WritableCursor {
val size = other.size - other.position
ensureSpace(size)
return super.writeCursor(other)
}
override fun writeStringAscii(str: String, byteLength: UInt): WritableCursor {
ensureSpace(byteLength)
return super.writeStringAscii(str, byteLength)
}
override fun writeStringUtf16(str: String, byteLength: UInt): WritableCursor {
ensureSpace(byteLength)
return super.writeStringUtf16(str, byteLength)
}
private fun ensureSpace(size: UInt) {
val needed = (position + size).toInt() - _size.toInt()
if (needed > 0) {
_size += needed.toUInt()
if (buffer.size < offset + _size) {
buffer.size = offset + _size
}
}
}
}

View File

@ -1,5 +1,8 @@
package world.phantasmal.lib.cursor
import world.phantasmal.lib.Endianness
import world.phantasmal.lib.buffer.Buffer
/**
* A cursor for reading binary data.
*/
@ -18,6 +21,8 @@ interface Cursor {
val bytesLeft: UInt
fun hasBytesLeft(bytes: UInt = 1u): Boolean
/**
* Seek forward or backward by a number of bytes.
*
@ -120,4 +125,9 @@ interface Cursor {
nullTerminated: Boolean,
dropRemaining: Boolean,
): String
/**
* Returns a buffer with a copy of [size] bytes at [position].
*/
fun buffer(size: UInt): Buffer
}

View File

@ -0,0 +1,109 @@
package world.phantasmal.lib.fileFormats.quest
import mu.KotlinLogging
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
class BinFile(
val format: BinFormat,
val questId: UInt,
val language: UInt,
val questName: String,
val shortDescription: String,
val longDescription: String,
// val objectCode: ArrayBuffer,
val labelOffsets: IntArray,
val shopItems: UIntArray,
)
enum class BinFormat {
/**
* Dreamcast/GameCube
*/
DC_GC,
/**
* Desktop
*/
PC,
/**
* BlueBurst
*/
BB,
}
fun parseBin(cursor: Cursor): BinFile {
val objectCodeOffset = cursor.u32()
val labelOffsetTableOffset = cursor.u32() // Relative offsets
val size = cursor.u32()
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
else -> {
logger.warn { "Object code at unexpected offset, assuming file is a PC file." }
BinFormat.PC
}
}
val questId: UInt
val language: UInt
val questName: String
val shortDescription: String
val longDescription: String
if (format == BinFormat.DC_GC) {
cursor.seek(1)
language = cursor.u8().toUInt()
questId = cursor.u16().toUInt()
questName = cursor.stringAscii(32u, true, true)
shortDescription = cursor.stringAscii(128u, true, true)
longDescription = cursor.stringAscii(288u, true, true)
} else {
questId = cursor.u32()
language = cursor.u32()
questName = cursor.stringUtf16(64u, true, true)
shortDescription = cursor.stringUtf16(256u, true, true)
longDescription = cursor.stringUtf16(576u, true, true)
}
if (size != cursor.size) {
logger.warn { "Value $size in bin size field does not match actual size ${cursor.size}." }
}
val shopItems = if (format == BinFormat.BB) {
cursor.seek(4) // Skip padding.
cursor.u32Array(932u)
} else {
UIntArray(0)
}
val labelOffsetCount = (cursor.size - labelOffsetTableOffset) / 4u
val labelOffsets = cursor
.seekStart(labelOffsetTableOffset)
.i32Array(labelOffsetCount)
// val objectCode = cursor
// .seekStart(objectCodeOffset)
// .arrayBuffer(labelOffsetTableOffset - objectCodeOffset);
return BinFile(
format,
questId,
language,
questName,
shortDescription,
longDescription,
// objectCode,
labelOffsets,
shopItems,
)
}

View File

@ -0,0 +1,238 @@
package world.phantasmal.lib.fileFormats.quest
import mu.KotlinLogging
import world.phantasmal.lib.buffer.Buffer
import world.phantasmal.lib.cursor.Cursor
private val logger = KotlinLogging.logger {}
private const val OBJECT_BYTE_SIZE = 68u;
private const val NPC_BYTE_SIZE = 72u;
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;
class DatFile(
val objs: List<DatEntity>,
val npcs: List<DatEntity>,
val events: List<DatEvent>,
val unknowns: List<DatUnknown>,
)
class DatEntity(
var areaId: UInt,
val data: Buffer,
)
class DatEvent(
var id: UInt,
var sectionId: UShort,
var wave: UShort,
var delay: UShort,
val actions: MutableList<DatEventAction>,
val areaId: UInt,
val unknown: UShort,
)
sealed class DatEventAction {
class SpawnNpcs(
val sectionId: UShort,
val appearFlag: UShort,
) : DatEventAction()
class Unlock(
val doorId: UShort,
) : DatEventAction()
class Lock(
val doorId: UShort,
) : DatEventAction()
class TriggerEvent(
val eventId: UInt,
) : DatEventAction()
}
class DatUnknown(
val entityType: UInt,
val totalSize: UInt,
val areaId: UInt,
val entitiesSize: UInt,
val data: UByteArray,
)
fun parseDat(cursor: Cursor): DatFile {
val objs = mutableListOf<DatEntity>()
val npcs = mutableListOf<DatEntity>()
val events = mutableListOf<DatEvent>()
val unknowns = mutableListOf<DatUnknown>()
while (cursor.hasBytesLeft()) {
val entityType = cursor.u32();
val totalSize = cursor.u32();
val areaId = cursor.u32();
val entitiesSize = cursor.u32();
if (entityType == 0u) {
break;
} else {
require(entitiesSize == totalSize - 16u) {
"Malformed DAT file. Expected an entities size of ${totalSize - 16u}, 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);
else -> {
// Unknown entity types 4 and 5 (challenge mode).
unknowns.add(DatUnknown(
entityType,
totalSize,
areaId,
entitiesSize,
data = cursor.u8Array(entitiesSize),
))
}
}
require(!entitiesCursor.hasBytesLeft()) {
logger.warn {
"Read ${entitiesCursor.position} bytes instead of expected ${entitiesCursor.size} for entity type ${entityType}."
}
}
}
}
return DatFile(
objs,
npcs,
events,
unknowns
)
}
private fun parseEntities(
cursor: Cursor,
areaId: UInt,
entities: MutableList<DatEntity>,
entitySize: UInt,
) {
val entityCount = cursor.size / entitySize
repeat(entityCount.toInt()) {
entities.add(DatEntity(
areaId,
data = cursor.buffer(entitySize),
));
}
}
private fun parseEvents(cursor: Cursor, areaId: UInt, events: MutableList<DatEvent>) {
val actionsOffset = cursor.u32();
cursor.seek(4); // Always 0x10
val eventCount = cursor.u32();
cursor.seek(3); // Always 0
val eventType = cursor.u8();
require(eventType == (0x32u).toUByte()) {
"Can't parse challenge mode quests yet."
}
cursor.seekStart(actionsOffset);
val actionsCursor = cursor.take(cursor.bytesLeft);
cursor.seekStart(16u);
repeat(eventCount.toInt()) {
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 actions: MutableList<DatEventAction> =
if (eventActionsOffset < actionsCursor.size) {
actionsCursor.seekStart(eventActionsOffset);
parseEventActions(actionsCursor);
} else {
logger.warn { "Invalid event actions offset $eventActionsOffset for event ${id}." }
mutableListOf()
}
events.add(DatEvent(
id,
sectionId,
wave,
delay,
actions,
areaId,
unknown,
))
}
if (cursor.position != actionsOffset) {
logger.warn {
"Read ${cursor.position - 16u} bytes of event data instead of expected ${actionsOffset - 16u}."
}
}
var lastU8: UByte = 0xffu;
while (actionsCursor.hasBytesLeft()) {
lastU8 = actionsCursor.u8();
if (lastU8 != (0xffu).toUByte()) {
break;
}
}
if (lastU8 != (0xffu).toUByte()) {
actionsCursor.seek(-1);
}
// Make sure the cursor position represents the amount of bytes we've consumed.
cursor.seekStart(actionsOffset + actionsCursor.position);
}
private fun parseEventActions(cursor: Cursor): MutableList<DatEventAction> {
val actions = mutableListOf<DatEventAction>()
outer@ while (cursor.hasBytesLeft()) {
when (val type = cursor.u8()) {
(1u).toUByte() -> break@outer;
EVENT_ACTION_SPAWN_NPCS ->
actions.add(DatEventAction.SpawnNpcs(
sectionId = cursor.u16(),
appearFlag = cursor.u16(),
))
EVENT_ACTION_UNLOCK ->
actions.add(DatEventAction.Unlock(
doorId = cursor.u16(),
))
EVENT_ACTION_LOCK ->
actions.add(DatEventAction.Lock(
doorId = cursor.u16(),
))
EVENT_ACTION_TRIGGER_EVENT ->
actions.add(DatEventAction.TriggerEvent(
eventId = cursor.u32(),
))
else -> {
logger.warn { "Unexpected event action type ${type}." }
break@outer;
}
}
}
return actions;
}

View File

@ -0,0 +1,35 @@
package world.phantasmal.lib.buffer
import world.phantasmal.lib.Endianness
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class BufferTests {
@Test
fun simple_properties_and_invariants() {
val capacity = 500u
val buffer = Buffer.withCapacity(capacity)
assertEquals(0u, buffer.size)
assertEquals(capacity, buffer.capacity)
assertEquals(Endianness.Little, buffer.endianness)
}
@Test
fun reallocates_internal_storage_when_necessary() {
val buffer = Buffer.withCapacity(100u)
assertEquals(0u, buffer.size)
assertEquals(100u, buffer.capacity)
buffer.size = 101u
assertEquals(101u, buffer.size)
assertTrue(buffer.capacity >= 101u)
buffer.setU8(100u, (0xABu).toUByte())
assertEquals(0xABu, buffer.getU8(100u).toUInt())
}
}

View File

@ -0,0 +1,72 @@
package world.phantasmal.lib.cursor
import world.phantasmal.lib.Endianness
import world.phantasmal.lib.buffer.Buffer
import kotlin.test.Test
import kotlin.test.assertEquals
class BufferCursorTests : WritableCursorTests() {
override fun createCursor(bytes: ByteArray, endianness: Endianness) =
BufferCursor(Buffer.fromByteArray(bytes, endianness))
@Test
fun writeU8_increases_size_correctly() {
testIntegerWriteSize(1, { writeU8(it.toUByte()) }, Endianness.Little)
testIntegerWriteSize(1, { writeU8(it.toUByte()) }, Endianness.Big)
}
@Test
fun writeU16_increases_size_correctly() {
testIntegerWriteSize(2, { writeU16(it.toUShort()) }, Endianness.Little)
testIntegerWriteSize(2, { writeU16(it.toUShort()) }, Endianness.Big)
}
@Test
fun writeU32_increases_size_correctly() {
testIntegerWriteSize(4, { writeU32(it.toUInt()) }, Endianness.Little)
testIntegerWriteSize(4, { writeU32(it.toUInt()) }, Endianness.Big)
}
@Test
fun writeI8_increases_size_correctly() {
testIntegerWriteSize(1, { writeI8(it.toByte()) }, Endianness.Little)
testIntegerWriteSize(1, { writeI8(it.toByte()) }, Endianness.Big)
}
@Test
fun writeI16_increases_size_correctly() {
testIntegerWriteSize(2, { writeI16(it.toShort()) }, Endianness.Little)
testIntegerWriteSize(2, { writeI16(it.toShort()) }, Endianness.Big)
}
@Test
fun writeI32_increases_size_correctly() {
testIntegerWriteSize(4, { writeI32(it) }, Endianness.Little)
testIntegerWriteSize(4, { writeI32(it) }, Endianness.Big)
}
private fun testIntegerWriteSize(
byteCount: Int,
write: BufferCursor.(Int) -> Unit,
endianness: Endianness,
) {
val expectedNumber1 = 7891378
val expectedNumber2 = 893894273
val buffer = Buffer.withCapacity(8u, endianness)
val cursor = BufferCursor(buffer)
assertEquals(0u, buffer.size)
assertEquals(0u, cursor.size)
cursor.write(expectedNumber1)
assertEquals(byteCount.toUInt(), buffer.size)
assertEquals(byteCount.toUInt(), cursor.size)
cursor.write(expectedNumber2)
assertEquals(2u * byteCount.toUInt(), buffer.size)
assertEquals(2u * byteCount.toUInt(), cursor.size)
}
}

View File

@ -1,5 +1,6 @@
package world.phantasmal.lib.cursor
import world.phantasmal.lib.Endianness
import kotlin.test.Test
import kotlin.test.assertEquals
@ -8,7 +9,7 @@ import kotlin.test.assertEquals
* implementation.
*/
abstract class CursorTests {
abstract fun createCursor(bytes: Array<Byte>, endianness: Endianness): Cursor
abstract fun createCursor(bytes: ByteArray, endianness: Endianness): Cursor
@Test
fun simple_cursor_properties_and_invariants() {
@ -17,7 +18,7 @@ abstract class CursorTests {
}
private fun simple_cursor_properties_and_invariants(endianness: Endianness) {
val cursor = createCursor(arrayOf(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(
0 to 0u,
@ -42,7 +43,7 @@ abstract class CursorTests {
}
private fun cursor_handles_byte_order_correctly(endianness: Endianness) {
val cursor = createCursor(arrayOf(1, 2, 3, 4), endianness)
val cursor = createCursor(byteArrayOf(1, 2, 3, 4), endianness)
if (endianness == Endianness.Little) {
assertEquals(0x04030201u, cursor.u32())
@ -96,7 +97,7 @@ abstract class CursorTests {
val expectedNumber2 = 0x05060708 shr (8 * (4 - byteCount))
// Put them in a byte array.
val bytes = Array<Byte>(2 * byteCount) { 0 }
val bytes = ByteArray(2 * byteCount)
for (i in 0 until byteCount) {
val shift =
@ -128,7 +129,7 @@ abstract class CursorTests {
}
private fun f32(endianness: Endianness) {
val bytes = arrayOf<Byte>(0x40, 0x20, 0, 0, 0x42, 1, 0, 0)
val bytes = byteArrayOf(0x40, 0x20, 0, 0, 0x42, 1, 0, 0)
if (endianness == Endianness.Little) {
bytes.reverse(0, 4)
@ -194,7 +195,7 @@ abstract class CursorTests {
endianness: Endianness,
) {
// Generate array of the form 1, 2, 0xFF, 4, 5, 6, 7, 8.
val bytes = Array<Byte>(8 * byteCount) { 0 }
val bytes = ByteArray(8 * byteCount)
for (i in 0 until 8) {
if (i == 2) {
@ -260,7 +261,7 @@ abstract class CursorTests {
endianness: Endianness,
) {
val chars = byteArrayOf(7, 65, 66, 0, (255).toByte(), 13)
val bytes = Array<Byte>(chars.size * byteCount) { 0 }
val bytes = ByteArray(chars.size * byteCount)
for (i in 0..chars.size) {
if (endianness == Endianness.Little) {
@ -295,4 +296,25 @@ abstract class CursorTests {
assertEquals("AB\u0000ÿ", cursor.read(4u * bc, false, false))
assertEquals(5u * bc, cursor.position)
}
@Test
fun buffer() {
testBuffer(Endianness.Little)
testBuffer(Endianness.Big)
}
private fun testBuffer(endianness: Endianness) {
val bytes = byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8)
val cursor = createCursor(bytes, endianness)
val buf = cursor.seek(2).buffer(4u)
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))
}
}

View File

@ -1,12 +1,13 @@
package world.phantasmal.lib.cursor
import world.phantasmal.lib.Endianness
import kotlin.math.abs
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
abstract class WritableCursorTests : CursorTests() {
abstract override fun createCursor(bytes: Array<Byte>, endianness: Endianness): WritableCursor
abstract override fun createCursor(bytes: ByteArray, endianness: Endianness): WritableCursor
@Test
fun simple_WritableCursor_properties_and_invariants() {
@ -15,7 +16,7 @@ abstract class WritableCursorTests : CursorTests() {
}
private fun simple_WritableCursor_properties_and_invariants(endianness: Endianness) {
val cursor = createCursor(arrayOf(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)
@ -77,7 +78,7 @@ abstract class WritableCursorTests : CursorTests() {
val expectedNumber1 = 0x01020304 shr (8 * (4 - byteCount))
val expectedNumber2 = 0x05060708 shr (8 * (4 - byteCount))
val cursor = createCursor(Array(2 * byteCount) { 0 }, endianness)
val cursor = createCursor(ByteArray(2 * byteCount), endianness)
cursor.write(expectedNumber1)
cursor.write(expectedNumber2)
@ -100,7 +101,7 @@ abstract class WritableCursorTests : CursorTests() {
* Writes and reads two floats.
*/
private fun writeF32(endianness: Endianness) {
val cursor = createCursor(Array(8) { 0 }, endianness)
val cursor = createCursor(ByteArray(8), endianness)
cursor.writeF32(1337.9001f)
cursor.writeF32(103.502f)
@ -181,7 +182,7 @@ abstract class WritableCursorTests : CursorTests() {
val testArray1 = IntArray(10) { it }
val testArray2 = IntArray(10) { it + 10 }
val cursor = createCursor(Array(20 * byteCount) { 0 }, endianness)
val cursor = createCursor(ByteArray(20 * byteCount), endianness)
cursor.write(testArray1)
assertEquals(10u * byteCount.toUInt(), cursor.position)
@ -203,7 +204,7 @@ abstract class WritableCursorTests : CursorTests() {
}
private fun write_seek_backwards_then_take(endianness: Endianness) {
val cursor = createCursor(Array(16) { 0 }, endianness)
val cursor = createCursor(ByteArray(16), endianness)
cursor.writeU32(1u).writeU32(2u).writeU32(3u).writeU32(4u)
cursor.seek(-8)

View File

@ -0,0 +1,209 @@
package world.phantasmal.lib.buffer
import org.khronos.webgl.ArrayBuffer
import org.khronos.webgl.DataView
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,
endianness: Endianness,
) {
private var dataView = DataView(arrayBuffer)
private var littleEndian = endianness == Endianness.Little
actual var size: UInt = size
set(value) {
ensureCapacity(value)
field = value
}
actual var endianness: Endianness
get() = if (littleEndian) Endianness.Little else Endianness.Big
set(value) {
littleEndian = value == Endianness.Little
}
actual val capacity: UInt
get() = arrayBuffer.byteLength.toUInt()
actual fun getU8(offset: UInt): UByte {
checkOffset(offset, 1u)
return dataView.getUint8(offset.toInt()).toUByte()
}
actual fun getU16(offset: UInt): UShort {
checkOffset(offset, 2u)
return dataView.getUint16(offset.toInt(), littleEndian).toUShort()
}
actual fun getU32(offset: UInt): UInt {
checkOffset(offset, 4u)
return dataView.getUint32(offset.toInt(), littleEndian).toUInt()
}
actual fun getI8(offset: UInt): Byte {
checkOffset(offset, 1u)
return dataView.getInt8(offset.toInt())
}
actual fun getI16(offset: UInt): Short {
checkOffset(offset, 2u)
return dataView.getInt16(offset.toInt(), littleEndian)
}
actual fun getI32(offset: UInt): Int {
checkOffset(offset, 4u)
return dataView.getInt32(offset.toInt(), littleEndian)
}
actual fun getF32(offset: UInt): Float {
checkOffset(offset, 4u)
return dataView.getFloat32(offset.toInt(), littleEndian)
}
actual fun getStringUtf16(
offset: UInt,
maxByteLength: UInt,
nullTerminated: Boolean,
): String =
buildString {
val len = maxByteLength / 2u
for (i in 0u until len) {
val codePoint = getU16(offset + i * 2u)
if (nullTerminated && codePoint == ZERO_U16) {
break
}
append(codePoint.toShort().toChar())
}
}
actual fun slice(offset: UInt, size: UInt): Buffer {
checkOffset(offset, size)
return fromArrayBuffer(
arrayBuffer.slice(offset.toInt(), (offset + size).toInt()),
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())
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)
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)
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)
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)
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)
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)
return this
}
/**
* Writes 0 bytes to the entire buffer.
*/
actual fun zero(): Buffer {
(Uint8Array(arrayBuffer).asDynamic()).fill(0)
return this
}
/**
* Checks whether we can read [size] bytes at [offset].
*/
private fun checkOffset(offset: UInt, size: UInt) {
require(offset + size <= this.size) {
"Offset $offset is out of bounds."
}
}
/**
* Reallocates the underlying ArrayBuffer if necessary.
*/
private fun ensureCapacity(minNewSize: UInt) {
if (minNewSize > capacity) {
var newSize = if (capacity == 0u) minNewSize else capacity;
do {
newSize *= 2u;
} while (newSize < minNewSize);
val newBuffer = ArrayBuffer(newSize.toInt());
Uint8Array(newBuffer).set(Uint8Array(arrayBuffer, 0, size.toInt()));
arrayBuffer = newBuffer;
dataView = DataView(arrayBuffer);
}
}
actual companion object {
actual fun withCapacity(
initialCapacity: UInt,
endianness: Endianness,
): Buffer =
Buffer(ArrayBuffer(initialCapacity.toInt()), size = 0u, 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)
}
fun fromArrayBuffer(arrayBuffer: ArrayBuffer, endianness: Endianness): Buffer {
return Buffer(arrayBuffer, arrayBuffer.byteLength.toUInt(), endianness)
}
}
}

View File

@ -2,6 +2,8 @@ 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) {
@ -116,6 +118,16 @@ protected constructor(endianness: Endianness, offset: UInt) : AbstractWritableCu
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())

View File

@ -2,6 +2,7 @@ package world.phantasmal.lib.cursor
import org.khronos.webgl.ArrayBuffer
import org.khronos.webgl.DataView
import world.phantasmal.lib.Endianness
/**
* A cursor for reading from an array buffer or part of an array buffer.

View File

@ -1,8 +1,9 @@
package world.phantasmal.lib.cursor
import org.khronos.webgl.Uint8Array
import world.phantasmal.lib.Endianness
class ArrayBufferCursorTests : WritableCursorTests() {
override fun createCursor(bytes: Array<Byte>, endianness: Endianness) =
ArrayBufferCursor(Uint8Array(bytes).buffer, endianness)
override fun createCursor(bytes: ByteArray, endianness: Endianness) =
ArrayBufferCursor(Uint8Array(bytes.toTypedArray()).buffer, endianness)
}