mirror of
https://github.com/curioustorvald/Terrarum.git
synced 2026-06-13 03:54:06 +09:00
rename 'tvda' -> 'savegame'
This commit is contained in:
535
src/net/torvald/terrarum/savegame/ByteArray64.kt
Normal file
535
src/net/torvald/terrarum/savegame/ByteArray64.kt
Normal file
@@ -0,0 +1,535 @@
|
||||
package net.torvald.terrarum.savegame
|
||||
|
||||
import java.io.*
|
||||
import java.nio.channels.ClosedChannelException
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.UnsupportedCharsetException
|
||||
|
||||
|
||||
/**
|
||||
* ByteArray that can hold larger than 2 GiB of Data.
|
||||
*
|
||||
* Works kind of like Bank Switching of old game console's cartridges which does same thing.
|
||||
*
|
||||
* Note that this class is just a fancy ArrayList. Internal size will grow accordingly
|
||||
*
|
||||
* @param initialSize Initial size of the array. If it's not specified, 8192 will be used instead.
|
||||
*
|
||||
* Created by Minjaesong on 2017-04-12.
|
||||
*/
|
||||
class ByteArray64(initialSize: Long = bankSize.toLong()) {
|
||||
var internalCapacity: Long = initialSize
|
||||
private set
|
||||
|
||||
var size = 0L
|
||||
internal set
|
||||
|
||||
private var finalised = false
|
||||
|
||||
companion object {
|
||||
val bankSize: Int = 8192
|
||||
|
||||
fun fromByteArray(byteArray: ByteArray): ByteArray64 {
|
||||
val ba64 = ByteArray64(byteArray.size.toLong())
|
||||
byteArray.forEachIndexed { i, byte -> ba64[i.toLong()] = byte }
|
||||
return ba64
|
||||
}
|
||||
}
|
||||
|
||||
private val __data: ArrayList<ByteArray>
|
||||
|
||||
private fun checkMutability() {
|
||||
if (finalised) throw IllegalStateException("ByteArray64 is finalised and cannot be modified")
|
||||
}
|
||||
|
||||
init {
|
||||
if (internalCapacity < 0)
|
||||
throw IllegalArgumentException("Invalid array size: $internalCapacity")
|
||||
else if (internalCapacity == 0L) // signalling empty array
|
||||
internalCapacity = bankSize.toLong()
|
||||
|
||||
val requiredBanks: Int = (initialSize - 1).toBankNumber() + 1
|
||||
|
||||
__data = ArrayList<ByteArray>(requiredBanks)
|
||||
repeat(requiredBanks) { __data.add(ByteArray(bankSize)) }
|
||||
}
|
||||
|
||||
private fun Long.toBankNumber(): Int = (this / bankSize).toInt()
|
||||
private fun Long.toBankOffset(): Int = (this % bankSize).toInt()
|
||||
|
||||
operator fun set(index: Long, value: Byte) {
|
||||
checkMutability()
|
||||
ensureCapacity(index + 1)
|
||||
|
||||
try {
|
||||
__data[index.toBankNumber()][index.toBankOffset()] = value
|
||||
size = maxOf(size, index + 1)
|
||||
}
|
||||
catch (e: IndexOutOfBoundsException) {
|
||||
val msg = "index: $index -> bank ${index.toBankNumber()} offset ${index.toBankOffset()}\n" +
|
||||
"But the array only contains ${__data.size} banks.\n" +
|
||||
"InternalCapacity = $internalCapacity, Size = $size"
|
||||
throw IndexOutOfBoundsException(msg)
|
||||
}
|
||||
}
|
||||
|
||||
fun add(value: Byte) = set(size, value)
|
||||
|
||||
operator fun get(index: Long): Byte {
|
||||
if (index < 0 || index >= size)
|
||||
throw ArrayIndexOutOfBoundsException("size $size, index $index")
|
||||
|
||||
try {
|
||||
val r = __data[index.toBankNumber()][index.toBankOffset()]
|
||||
return r
|
||||
}
|
||||
catch (e: IndexOutOfBoundsException) {
|
||||
System.err.println("index: $index -> bank ${index.toBankNumber()} offset ${index.toBankOffset()}")
|
||||
System.err.println("But the array only contains ${__data.size} banks.")
|
||||
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun addOneBank() {
|
||||
__data.add(ByteArray(bankSize))
|
||||
internalCapacity = __data.size * bankSize.toLong()
|
||||
}
|
||||
|
||||
/**
|
||||
* Increases the capacity of it, if necessary, to ensure that it can hold at least the number of elements specified by the minimum capacity argument.
|
||||
*/
|
||||
fun ensureCapacity(minCapacity: Long) {
|
||||
while (minCapacity > internalCapacity) {
|
||||
addOneBank()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
operator fun iterator(): ByteIterator {
|
||||
return object : ByteIterator() {
|
||||
var iterationCounter = 0L
|
||||
|
||||
override fun nextByte(): Byte {
|
||||
iterationCounter += 1
|
||||
return this@ByteArray64[iterationCounter - 1]
|
||||
}
|
||||
|
||||
override fun hasNext() = iterationCounter < this@ByteArray64.size
|
||||
}
|
||||
}
|
||||
|
||||
fun iteratorChoppedToInt(): IntIterator {
|
||||
return object : IntIterator() {
|
||||
var iterationCounter = 0L
|
||||
val iteratorSize = 1 + ((this@ByteArray64.size - 1) / 4).toInt()
|
||||
|
||||
override fun nextInt(): Int {
|
||||
var byteCounter = iterationCounter * 4L
|
||||
var int = 0
|
||||
(0..3).forEach {
|
||||
if (byteCounter + it < this@ByteArray64.size) {
|
||||
int += this@ByteArray64[byteCounter + it].toInt() shl (it * 8)
|
||||
}
|
||||
else {
|
||||
int += 0 shl (it * 8)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
iterationCounter += 1
|
||||
return int
|
||||
}
|
||||
|
||||
override fun hasNext() = iterationCounter < iteratorSize
|
||||
}
|
||||
}
|
||||
|
||||
/** Iterates over all written bytes. */
|
||||
fun forEach(consumer: (Byte) -> Unit) = iterator().forEach { consumer(it) }
|
||||
/** Iterates over all written 32-bit words. */
|
||||
fun forEachInt32(consumer: (Int) -> Unit) = iteratorChoppedToInt().forEach { consumer(it) }
|
||||
/** Iterates over all existing banks, even if they are not used. Please use [forEachUsedBanks] to iterate over banks that are actually been used. */
|
||||
fun forEachBanks(consumer: (ByteArray) -> Unit) = __data.forEach(consumer)
|
||||
/** Iterates over all written bytes. */
|
||||
fun forEachIndexed(consumer: (Long, Byte) -> Unit) {
|
||||
var cnt = 0L
|
||||
iterator().forEach {
|
||||
consumer(cnt, it)
|
||||
cnt += 1
|
||||
}
|
||||
}
|
||||
/** Iterates over all written 32-bit words. */
|
||||
fun forEachInt32Indexed(consumer: (Long, Int) -> Unit) {
|
||||
var cnt = 0L
|
||||
iteratorChoppedToInt().forEach {
|
||||
consumer(cnt, it)
|
||||
cnt += 1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param consumer (Int, Int, ByteArray)-to-Unit function where first Int is index;
|
||||
* second Int is actual number of bytes written in that bank, 0 to BankSize inclusive.
|
||||
*/
|
||||
fun forEachUsedBanksIndexed(consumer: (Int, Int, ByteArray) -> Unit) {
|
||||
__data.forEachIndexed { index, bytes ->
|
||||
consumer(index, (size - bankSize * index).coerceIn(0, bankSize.toLong()).toInt(), bytes)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param consumer (Int, Int, ByteArray)-to-Unit function where Int is actual number of bytes written in that bank, 0 to BankSize inclusive.
|
||||
*/
|
||||
fun forEachUsedBanks(consumer: (Int, ByteArray) -> Unit) {
|
||||
__data.forEachIndexed { index, bytes ->
|
||||
consumer((size - bankSize * index).coerceIn(0, bankSize.toLong()).toInt(), bytes)
|
||||
}
|
||||
}
|
||||
|
||||
fun sliceArray64(range: LongRange): ByteArray64 {
|
||||
val newarr = ByteArray64(range.last - range.first + 1)
|
||||
range.forEach { index ->
|
||||
newarr[index - range.first] = this[index]
|
||||
}
|
||||
return newarr
|
||||
}
|
||||
|
||||
fun sliceArray(range: IntRange): ByteArray {
|
||||
val newarr = ByteArray(range.last - range.first + 1)
|
||||
range.forEach { index ->
|
||||
newarr[index - range.first] = this[index.toLong()]
|
||||
}
|
||||
return newarr
|
||||
}
|
||||
|
||||
fun toByteArray(): ByteArray {
|
||||
if (this.size > Integer.MAX_VALUE - 8) // according to OpenJDK; the size itself is VM-dependent
|
||||
throw TypeCastException("Impossible cast; too large to fit")
|
||||
|
||||
return ByteArray(this.size.toInt()) { this[it.toLong()] }
|
||||
}
|
||||
|
||||
fun writeToFile(file: File) {
|
||||
var fos = FileOutputStream(file, false)
|
||||
// following code writes in-chunk basis
|
||||
/*fos.write(__data[0])
|
||||
fos.flush()
|
||||
fos.close()
|
||||
|
||||
if (__data.size > 1) {
|
||||
fos = FileOutputStream(file, true)
|
||||
for (i in 1..__data.lastIndex) {
|
||||
fos.write(__data[i])
|
||||
fos.flush()
|
||||
}
|
||||
fos.close()
|
||||
}*/
|
||||
|
||||
forEach {
|
||||
fos.write(it.toInt())
|
||||
}
|
||||
fos.flush()
|
||||
fos.close()
|
||||
}
|
||||
|
||||
fun finalise() {
|
||||
this.finalised = true
|
||||
}
|
||||
}
|
||||
|
||||
open class ByteArray64InputStream(val byteArray64: ByteArray64): InputStream() {
|
||||
protected open var readCounter = 0L
|
||||
|
||||
override fun read(): Int {
|
||||
readCounter += 1
|
||||
|
||||
return try {
|
||||
byteArray64[readCounter - 1].toUint()
|
||||
}
|
||||
catch (e: ArrayIndexOutOfBoundsException) {
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Static ByteArray OutputStream. Less leeway, more stable. */
|
||||
open class ByteArray64OutputStream(val byteArray64: ByteArray64): OutputStream() {
|
||||
protected open var writeCounter = 0L
|
||||
|
||||
override fun write(b: Int) {
|
||||
try {
|
||||
byteArray64.add(b.toByte())
|
||||
writeCounter += 1
|
||||
}
|
||||
catch (e: ArrayIndexOutOfBoundsException) {
|
||||
throw IOException(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
byteArray64.finalise()
|
||||
}
|
||||
}
|
||||
|
||||
/** Just like Java's ByteArrayOutputStream, except its size grows if you exceed the initial size
|
||||
*/
|
||||
open class ByteArray64GrowableOutputStream(size: Long = ByteArray64.bankSize.toLong()): OutputStream() {
|
||||
protected open var buf = ByteArray64(size)
|
||||
protected open var count = 0L
|
||||
|
||||
private var finalised = false
|
||||
|
||||
init {
|
||||
if (size <= 0L) throw IllegalArgumentException("Illegal array size: $size")
|
||||
}
|
||||
|
||||
override fun write(b: Int) {
|
||||
if (finalised) {
|
||||
throw IllegalStateException("This output stream is finalised and cannot be modified.")
|
||||
}
|
||||
else {
|
||||
buf.add(b.toByte())
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
|
||||
/** Unlike Java's, this does NOT create a copy of the internal buffer; this just returns its internal.
|
||||
* This method also "finalises" the buffer inside of the output stream, making further modification impossible.
|
||||
*
|
||||
* The output stream must be flushed and closed, warning you of closing the stream is not possible.
|
||||
*/
|
||||
@Synchronized
|
||||
fun toByteArray64(): ByteArray64 {
|
||||
close()
|
||||
buf.size = count
|
||||
return buf
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
finalised = true
|
||||
buf.finalise()
|
||||
}
|
||||
}
|
||||
|
||||
open class ByteArray64Writer(val charset: Charset) : Writer() {
|
||||
|
||||
/* writer must be able to handle nonstandard utf-8 surrogate representation, where
|
||||
* each surrogate is encoded in single code point, resulting six utf-8 bytes instead of four.
|
||||
*/
|
||||
|
||||
private val acceptableCharsets = arrayOf(Charsets.UTF_8, Charset.forName("CP437"))
|
||||
|
||||
init {
|
||||
if (!acceptableCharsets.contains(charset))
|
||||
throw UnsupportedCharsetException(charset.name())
|
||||
}
|
||||
|
||||
private val ba = ByteArray64()
|
||||
private var closed = false
|
||||
private var surrogateBuf = 0
|
||||
|
||||
init {
|
||||
this.lock = ba
|
||||
}
|
||||
|
||||
private fun checkOpen() {
|
||||
if (closed) throw ClosedChannelException()
|
||||
}
|
||||
|
||||
private fun Int.isSurroHigh() = this.ushr(10) == 0b110110
|
||||
private fun Int.isSurroLow() = this.ushr(10) == 0b110111
|
||||
private fun Int.toUcode() = 'u' + this.toString(16).toUpperCase().padStart(4,'0')
|
||||
|
||||
/**
|
||||
* @param c not a freakin' codepoint; just a Java's Char casted into Int
|
||||
*/
|
||||
override fun write(c: Int) {
|
||||
checkOpen()
|
||||
when (charset) {
|
||||
Charsets.UTF_8 -> {
|
||||
if (surrogateBuf == 0 && !c.isSurroHigh() && !c.isSurroLow())
|
||||
writeUtf8Codepoint(c)
|
||||
else if (surrogateBuf == 0 && c.isSurroHigh())
|
||||
surrogateBuf = c
|
||||
else if (surrogateBuf != 0 && c.isSurroLow())
|
||||
writeUtf8Codepoint(65536 + surrogateBuf.and(1023).shl(10) or c.and(1023))
|
||||
// invalid surrogate pair input
|
||||
else
|
||||
throw IllegalStateException("Surrogate high: ${surrogateBuf.toUcode()}, surrogate low: ${c.toUcode()}")
|
||||
}
|
||||
Charset.forName("CP437") -> {
|
||||
ba.add(c.toByte())
|
||||
}
|
||||
else -> throw UnsupportedCharsetException(charset.name())
|
||||
}
|
||||
}
|
||||
|
||||
fun writeUtf8Codepoint(codepoint: Int) {
|
||||
when (codepoint) {
|
||||
in 0..127 -> ba.add(codepoint.toByte())
|
||||
in 128..2047 -> {
|
||||
ba.add((0xC0 or codepoint.ushr(6).and(31)).toByte())
|
||||
ba.add((0x80 or codepoint.and(63)).toByte())
|
||||
}
|
||||
in 2048..65535 -> {
|
||||
ba.add((0xE0 or codepoint.ushr(12).and(15)).toByte())
|
||||
ba.add((0x80 or codepoint.ushr(6).and(63)).toByte())
|
||||
ba.add((0x80 or codepoint.and(63)).toByte())
|
||||
}
|
||||
in 65536..1114111 -> {
|
||||
ba.add((0xF0 or codepoint.ushr(18).and(7)).toByte())
|
||||
ba.add((0x80 or codepoint.ushr(12).and(63)).toByte())
|
||||
ba.add((0x80 or codepoint.ushr(6).and(63)).toByte())
|
||||
ba.add((0x80 or codepoint.and(63)).toByte())
|
||||
}
|
||||
else -> throw IllegalArgumentException("Not a unicode code point: U+${codepoint.toString(16).toUpperCase()}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun write(cbuf: CharArray) {
|
||||
checkOpen()
|
||||
write(String(cbuf))
|
||||
}
|
||||
|
||||
override fun write(str: String) {
|
||||
checkOpen()
|
||||
str.toByteArray(charset).forEach { ba.add(it) }
|
||||
}
|
||||
|
||||
override fun write(cbuf: CharArray, off: Int, len: Int) {
|
||||
write(cbuf.copyOfRange(off, off + len))
|
||||
}
|
||||
|
||||
override fun write(str: String, off: Int, len: Int) {
|
||||
write(str.substring(off, off + len))
|
||||
}
|
||||
|
||||
override fun close() { closed = true }
|
||||
override fun flush() {}
|
||||
|
||||
fun toByteArray64() = if (closed) ba else throw IllegalAccessException("Writer not closed")
|
||||
}
|
||||
|
||||
open class ByteArray64Reader(val ba: ByteArray64, val charset: Charset) : Reader() {
|
||||
|
||||
/* reader must be able to handle nonstandard utf-8 surrogate representation, where
|
||||
* each surrogate is encoded in single code point, resulting six utf-8 bytes instead of four.
|
||||
*/
|
||||
|
||||
private val acceptableCharsets = arrayOf(Charsets.UTF_8, Charset.forName("CP437"))
|
||||
|
||||
init {
|
||||
if (!acceptableCharsets.contains(charset))
|
||||
throw UnsupportedCharsetException(charset.name())
|
||||
}
|
||||
|
||||
private var readCursor = 0L
|
||||
private val remaining
|
||||
get() = ba.size - readCursor
|
||||
|
||||
/**
|
||||
* U+0000 .. U+007F 0xxxxxxx
|
||||
* U+0080 .. U+07FF 110xxxxx 10xxxxxx
|
||||
* U+0800 .. U+FFFF 1110xxxx 10xxxxxx 10xxxxxx
|
||||
* U+10000 .. U+10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
|
||||
*/
|
||||
private fun utf8GetCharLen(head: Byte) = when (head.toInt() and 255) {
|
||||
in 0b11110_000..0b11110_111 -> 4
|
||||
in 0b1110_0000..0b1110_1111 -> 3
|
||||
in 0b110_00000..0b110_11111 -> 2
|
||||
in 0b0_0000000..0b0_1111111 -> 1
|
||||
else -> throw IllegalArgumentException("Invalid UTF-8 Character head byte: ${head.toInt() and 255}")
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list of bytes that encodes one unicode character. Get required byte length using [utf8GetCharLen].
|
||||
* @return A codepoint of the character.
|
||||
*/
|
||||
private fun utf8decode(bytes0: List<Byte>): Int {
|
||||
val bytes = bytes0.map { it.toInt() and 255 }
|
||||
var ret = when (bytes.size) {
|
||||
4 -> (bytes[0] and 7) shl 18
|
||||
3 -> (bytes[0] and 15) shl 12
|
||||
2 -> (bytes[0] and 31) shl 6
|
||||
1 -> (bytes[0] and 127)
|
||||
else -> throw IllegalArgumentException("Expected bytes size: 1..4, got ${bytes.size}")
|
||||
}
|
||||
bytes.subList(1, bytes.size).reversed().forEachIndexed { index, byte ->
|
||||
ret = ret or (byte and 63).shl(6 * index)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
private var surrogateLeftover = ' '
|
||||
|
||||
override fun read(cbuf: CharArray, off: Int, len: Int): Int {
|
||||
var readCount = 0
|
||||
|
||||
if (remaining <= 0L) return -1
|
||||
|
||||
when (charset) {
|
||||
Charsets.UTF_8 -> {
|
||||
while (readCount < len && remaining > 0) {
|
||||
if (surrogateLeftover != ' ') {
|
||||
cbuf[off + readCount] = surrogateLeftover
|
||||
|
||||
readCount += 1
|
||||
surrogateLeftover = ' '
|
||||
}
|
||||
else {
|
||||
val bbuf = (0 until minOf(4L, remaining)).map { ba[readCursor + it] }
|
||||
val charLen = utf8GetCharLen(bbuf[0])
|
||||
val codePoint = utf8decode(bbuf.subList(0, charLen))
|
||||
|
||||
if (codePoint < 65536) {
|
||||
cbuf[off + readCount] = codePoint.toChar()
|
||||
|
||||
readCount += 1
|
||||
readCursor += charLen
|
||||
}
|
||||
else {
|
||||
/*
|
||||
* U' = yyyyyyyyyyxxxxxxxxxx // U - 0x10000
|
||||
* W1 = 110110yyyyyyyyyy // 0xD800 + yyyyyyyyyy
|
||||
* W2 = 110111xxxxxxxxxx // 0xDC00 + xxxxxxxxxx
|
||||
*/
|
||||
val codPoin = codePoint - 65536
|
||||
val surroLead = (0xD800 or codPoin.ushr(10)).toChar()
|
||||
val surroTrail = (0xDC00 or codPoin.and(1023)).toChar()
|
||||
|
||||
cbuf[off + readCount] = surroLead
|
||||
|
||||
if (off + readCount + 1 < cbuf.size) {
|
||||
cbuf[off + readCount + 1] = surroTrail
|
||||
|
||||
readCount += 2
|
||||
readCursor += 4
|
||||
}
|
||||
else {
|
||||
readCount += 1
|
||||
readCursor += 4
|
||||
surrogateLeftover = surroTrail
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Charset.forName("CP437") -> {
|
||||
for (i in 0 until minOf(len.toLong(), remaining)) {
|
||||
cbuf[(off + i).toInt()] = ba[readCursor].toChar()
|
||||
readCursor += 1
|
||||
readCount += 1
|
||||
}
|
||||
}
|
||||
else -> throw UnsupportedCharsetException(charset.name())
|
||||
}
|
||||
|
||||
return readCount
|
||||
}
|
||||
|
||||
override fun close() { readCursor = 0L }
|
||||
override fun reset() { readCursor = 0L }
|
||||
override fun markSupported() = false
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user