From 82262e9bba2b657d392ae9e86cb4503d26f74c02 Mon Sep 17 00:00:00 2001 From: Greg Date: Mon, 22 Jan 2024 16:02:36 +0000 Subject: [PATCH] Add file server to networking module (#423) * Add version table builder class * Add version tables to Cache * Slightly better but still not perfect dialogue converter * Add cache sector loading * Add file provider to get and store cache sectors * Add better version table support to Cache * Add file server support to Network.kt * Convert protocol to use an array * Tidy up Main.kt * Move remaining network processing into LoginServer class * Tidy up network module packages * Rename Network to GameServer * Update cache builder * Close trade on x-log, add trade messages * Picking failed message * Fix statement line wrap width * Make file server usage optional * Disable removing bzip2 for now, writing doesn't seem to work * Reuse PrefetchKeyGeneration * Make VersionTableBuilder thread safe by using ByteArray instead of BufferWriter * Update prefetch keys --- .../kotlin/world/gregs/voidps/cache/Cache.kt | 14 + .../world/gregs/voidps/cache/CacheDelegate.kt | 26 +- .../world/gregs/voidps/cache/CacheLoader.kt | 28 +- .../world/gregs/voidps/cache/FileCache.kt | 51 +++- .../world/gregs/voidps/cache/MemoryCache.kt | 55 +++- .../world/gregs/voidps/cache/ReadOnlyCache.kt | 49 +-- .../cache/compress/DecompressionContext.kt | 2 +- .../world/gregs/voidps/cache/secure/CRC.kt | 77 +---- .../world/gregs/voidps/cache/secure/RSA.kt | 4 +- .../cache/secure/VersionTableBuilder.kt | 93 ++++++ .../gregs/voidps/cache/secure/Whirlpool.kt | 221 ++++++++++++++ .../cache/secure/VersionTableBuilderTest.kt | 116 +++++++ .../world/gregs/voidps/engine/TimedLoader.kt | 8 + .../engine/client/PlayerAccountLoader.kt | 6 +- .../voidps/engine/client/ui/Interfaces.kt | 2 +- .../engine/client/variable/PlayerVariables.kt | 2 +- .../engine/data/config/VariableDefinition.kt | 2 +- .../world/gregs/voidps/engine/entity/World.kt | 19 +- .../engine/entity/character/player/Player.kt | 4 +- .../engine/client/PlayerAccountLoaderTest.kt | 2 +- .../voidps/engine/client/ui/InterfaceTest.kt | 2 +- .../update/player/PlayerUpdateTaskTest.kt | 6 +- .../engine/client/variable/VariablesTest.kt | 2 +- .../data/config/VariableDefinitionTest.kt | 2 +- .../character/player/PlayerOptionsTest.kt | 2 +- .../engine/map/zone/ZoneBatchUpdatesTest.kt | 2 +- file-server.properties | 6 - .../kotlin/world/gregs/voidps/GameTick.kt | 34 ++- .../main/kotlin/world/gregs/voidps/Main.kt | 78 ++--- .../kotlin/world/gregs/voidps/Properties.kt | 10 + .../world/command/admin/BotCommands.kts | 2 +- .../world/community/trade/TradeConfirm.kts | 3 + .../world/community/trade/TradeDecline.kts | 14 + .../world/interact/dialogue/type/Statement.kt | 2 +- .../world/interact/entity/item/ItemPickup.kts | 2 + game/src/main/resources/game.properties | 51 ++-- game/src/main/resources/private.properties | 3 - .../community/friend/PrivateChatStatusTest.kt | 2 +- .../world/interact/dialogue/NPCChatTest.kt | 2 +- .../world/interact/dialogue/PlayerChatTest.kt | 2 +- .../world/interact/dialogue/StatementTest.kt | 9 +- .../gregs/voidps/world/script/WorldTest.kt | 4 +- .../gregs/voidps/network/AccountLoader.kt | 1 + .../world/gregs/voidps/network/FileServer.kt | 126 ++++++++ .../world/gregs/voidps/network/GameServer.kt | 93 ++++++ .../gregs/voidps/network/JagExtensions.kt | 22 ++ .../network/{Network.kt => LoginServer.kt} | 118 ++----- .../world/gregs/voidps/network/Protocol.kt | 176 +++++------ .../world/gregs/voidps/network/Request.kt | 17 ++ .../world/gregs/voidps/network/Server.kt | 7 + .../voidps/network/{ => client}/Client.kt | 3 +- .../network/{ => client}/ClientState.kt | 2 +- .../network/{ => client}/DummyClient.kt | 2 +- .../network/{ => client}/IsaacCipher.kt | 2 +- .../network/{ => encode}/ByteArrayChannel.kt | 2 +- .../voidps/network/encode/CameraEncoders.kt | 1 + .../voidps/network/encode/ChatEncoder.kt | 11 +- .../network/encode/ChatStatusEncoder.kt | 2 +- .../voidps/network/encode/ClanEncoder.kt | 9 +- .../network/encode/ContainerEncoders.kt | 8 +- .../encode/ContextMenuOptionEncoder.kt | 10 +- .../voidps/network/encode/FriendsEncoder.kt | 4 +- .../voidps/network/encode/IgnoreEncoder.kt | 4 +- .../network/encode/InterfaceEncoders.kt | 7 +- .../voidps/network/encode/LoginEncoder.kt | 6 +- .../voidps/network/encode/LogoutEncoder.kt | 2 +- .../network/encode/MapRegionEncoders.kt | 5 +- .../voidps/network/encode/MinimapEncoders.kt | 2 +- .../voidps/network/encode/NPCUpdateEncoder.kt | 4 +- .../voidps/network/encode/ObjectEncoders.kt | 6 +- .../network/encode/PlayerUpdateEncoder.kt | 4 +- .../network/encode/ProjectileEncoders.kt | 2 +- .../voidps/network/encode/RunEnergyEncoder.kt | 2 +- .../voidps/network/encode/ScriptEncoder.kt | 6 +- .../network/encode/SkillLevelEncoder.kt | 2 +- .../voidps/network/encode/SoundEncoder.kt | 6 +- .../voidps/network/encode/TextTileEncoder.kt | 6 +- .../voidps/network/encode/VarbitEncoder.kt | 6 +- .../voidps/network/encode/VarcEncoder.kt | 2 +- .../voidps/network/encode/VarcStrEncoder.kt | 8 +- .../voidps/network/encode/VarpEncoder.kt | 2 +- .../voidps/network/encode/WeightEncoder.kt | 2 +- .../voidps/network/encode/ZoneEncoders.kt | 2 +- .../network/encode/ZoneUpdateEncoders.kt | 1 + .../voidps/network/file/CacheFileProvider.kt | 38 +++ .../gregs/voidps/network/file/FileProvider.kt | 72 +++++ .../voidps/network/file/MemoryFileProvider.kt | 37 +++ .../gregs/voidps/network/file/PrefetchKeys.kt | 69 +++++ .../gregs/voidps/network/GameServerTest.kt | 63 ++++ .../{NetworkTest.kt => LoginServerTest.kt} | 30 +- .../voidps/tools/DropTableDefinitions.kt | 2 +- .../gregs/voidps/tools/cache/CacheBuilder.kt | 2 +- .../tools/cache/PrefetchKeyGeneration.kt | 55 +--- .../gregs/voidps/tools/cache/RemoveBzip2.kt | 3 - .../tools/wiki/dialogue/DialogueConverter.kt | 287 ++++++++++++++++++ .../tools/wiki/dialogue/DialogueParsing.kt | 1 + 96 files changed, 1804 insertions(+), 579 deletions(-) create mode 100644 cache/src/main/kotlin/world/gregs/voidps/cache/secure/VersionTableBuilder.kt create mode 100644 cache/src/main/kotlin/world/gregs/voidps/cache/secure/Whirlpool.kt create mode 100644 cache/src/test/kotlin/world/gregs/voidps/cache/secure/VersionTableBuilderTest.kt delete mode 100644 file-server.properties create mode 100644 game/src/main/kotlin/world/gregs/voidps/Properties.kt delete mode 100644 game/src/main/resources/private.properties create mode 100644 network/src/main/kotlin/world/gregs/voidps/network/FileServer.kt create mode 100644 network/src/main/kotlin/world/gregs/voidps/network/GameServer.kt rename network/src/main/kotlin/world/gregs/voidps/network/{Network.kt => LoginServer.kt} (58%) create mode 100644 network/src/main/kotlin/world/gregs/voidps/network/Request.kt create mode 100644 network/src/main/kotlin/world/gregs/voidps/network/Server.kt rename network/src/main/kotlin/world/gregs/voidps/network/{ => client}/Client.kt (97%) rename network/src/main/kotlin/world/gregs/voidps/network/{ => client}/ClientState.kt (83%) rename network/src/main/kotlin/world/gregs/voidps/network/{ => client}/DummyClient.kt (89%) rename network/src/main/kotlin/world/gregs/voidps/network/{ => client}/IsaacCipher.kt (99%) rename network/src/main/kotlin/world/gregs/voidps/network/{ => encode}/ByteArrayChannel.kt (98%) create mode 100644 network/src/main/kotlin/world/gregs/voidps/network/file/CacheFileProvider.kt create mode 100644 network/src/main/kotlin/world/gregs/voidps/network/file/FileProvider.kt create mode 100644 network/src/main/kotlin/world/gregs/voidps/network/file/MemoryFileProvider.kt create mode 100644 network/src/main/kotlin/world/gregs/voidps/network/file/PrefetchKeys.kt create mode 100644 network/src/test/kotlin/world/gregs/voidps/network/GameServerTest.kt rename network/src/test/kotlin/world/gregs/voidps/network/{NetworkTest.kt => LoginServerTest.kt} (88%) create mode 100644 tools/src/main/kotlin/world/gregs/voidps/tools/wiki/dialogue/DialogueConverter.kt diff --git a/cache/src/main/kotlin/world/gregs/voidps/cache/Cache.kt b/cache/src/main/kotlin/world/gregs/voidps/cache/Cache.kt index 5e78bbbb45..5e3744b326 100644 --- a/cache/src/main/kotlin/world/gregs/voidps/cache/Cache.kt +++ b/cache/src/main/kotlin/world/gregs/voidps/cache/Cache.kt @@ -1,11 +1,17 @@ package world.gregs.voidps.cache +import java.util.* + interface Cache { + val versionTable: ByteArray + fun indexCount(): Int fun indices(): IntArray + fun sector(index: Int, archive: Int): ByteArray? + fun archives(index: Int): IntArray fun archiveCount(index: Int): Int @@ -34,4 +40,12 @@ interface Cache { fun close() + + companion object { + fun load(properties: Properties): Cache { + val live = properties.getProperty("live").toBoolean() + val loader = if (live) MemoryCache else FileCache + return loader.load(properties) + } + } } \ No newline at end of file diff --git a/cache/src/main/kotlin/world/gregs/voidps/cache/CacheDelegate.kt b/cache/src/main/kotlin/world/gregs/voidps/cache/CacheDelegate.kt index 17313fdc27..548b41329f 100644 --- a/cache/src/main/kotlin/world/gregs/voidps/cache/CacheDelegate.kt +++ b/cache/src/main/kotlin/world/gregs/voidps/cache/CacheDelegate.kt @@ -2,21 +2,28 @@ package world.gregs.voidps.cache import com.displee.cache.CacheLibrary import com.github.michaelbull.logging.InlineLogger +import java.math.BigInteger -class CacheDelegate(directory: String) : Cache { +class CacheDelegate(private val library: CacheLibrary, exponent: BigInteger? = null, modulus: BigInteger? = null) : Cache { - private val library: CacheLibrary + constructor(directory: String, exponent: BigInteger? = null, modulus: BigInteger? = null) : this(timed(directory), exponent, modulus) - init { - val start = System.currentTimeMillis() - library = CacheLibrary(directory) - logger.info { "Cache read from $directory in ${System.currentTimeMillis() - start}ms" } + override val versionTable: ByteArray = if (exponent == null || modulus == null) ByteArray(0) else { + library.generateNewUkeys(exponent, modulus) } override fun indexCount() = library.indices().size override fun indices() = library.indices().map { it.id }.toIntArray() + override fun sector(index: Int, archive: Int): ByteArray? { + return if (index == 255) { + library.index255 + } else { + library.index(index) + }?.readArchiveSector(archive)?.data + } + override fun archives(index: Int) = library.index(index).archiveIds() override fun archiveCount(index: Int) = library.index(index).archiveIds().size @@ -63,5 +70,12 @@ class CacheDelegate(directory: String) : Cache { companion object { private val logger = InlineLogger() + + private fun timed(directory: String): CacheLibrary { + val start = System.currentTimeMillis() + val library = CacheLibrary(directory) + logger.info { "Cache read from $directory in ${System.currentTimeMillis() - start}ms" } + return library + } } } \ No newline at end of file diff --git a/cache/src/main/kotlin/world/gregs/voidps/cache/CacheLoader.kt b/cache/src/main/kotlin/world/gregs/voidps/cache/CacheLoader.kt index 9d306688ed..2f43ac9e0e 100644 --- a/cache/src/main/kotlin/world/gregs/voidps/cache/CacheLoader.kt +++ b/cache/src/main/kotlin/world/gregs/voidps/cache/CacheLoader.kt @@ -1,12 +1,23 @@ package world.gregs.voidps.cache +import world.gregs.voidps.cache.secure.VersionTableBuilder import java.io.File import java.io.FileNotFoundException import java.io.RandomAccessFile +import java.math.BigInteger +import java.util.* interface CacheLoader { - fun load(path: String, xteas: Map? = null, threadUsage: Double = 1.0): Cache { + fun load(properties: Properties, xteas: Map? = null): Cache { + val fileModulus = BigInteger(properties.getProperty("fileModulus"), 16) + val filePrivate = BigInteger(properties.getProperty("filePrivate"), 16) + val cachePath = properties.getProperty("cachePath") + val threadUsage = properties.getProperty("threadUsage", "1.0").toDouble() + return load(cachePath, filePrivate, fileModulus, threadUsage = threadUsage) + } + + fun load(path: String, exponent: BigInteger? = null, modulus: BigInteger? = null, xteas: Map? = null, threadUsage: Double = 1.0): Cache { val mainFile = File(path, "${FileCache.CACHE_FILE_NAME}.dat2") if (!mainFile.exists()) { throw FileNotFoundException("Main file not found at '${mainFile.absolutePath}'.") @@ -18,8 +29,19 @@ interface CacheLoader { } val index255 = RandomAccessFile(index255File, "r") val indexCount = index255.length().toInt() / ReadOnlyCache.INDEX_SIZE - return load(path, mainFile, main, index255File, index255, indexCount, xteas, threadUsage) + val versionTable = if (exponent != null && modulus != null) VersionTableBuilder(exponent, modulus, indexCount) else null + return load(path, mainFile, main, index255File, index255, indexCount, versionTable, xteas, threadUsage) } - fun load(path: String, mainFile: File, main: RandomAccessFile, index255File: File, index255: RandomAccessFile, indexCount: Int, xteas: Map? = null, threadUsage: Double = 1.0): Cache + fun load( + path: String, + mainFile: File, + main: RandomAccessFile, + index255File: File, + index255: RandomAccessFile, + indexCount: Int, + versionTable: VersionTableBuilder? = null, + xteas: Map? = null, + threadUsage: Double = 1.0 + ): Cache } \ No newline at end of file diff --git a/cache/src/main/kotlin/world/gregs/voidps/cache/FileCache.kt b/cache/src/main/kotlin/world/gregs/voidps/cache/FileCache.kt index 90a6d0bf3a..ea81cad2fd 100644 --- a/cache/src/main/kotlin/world/gregs/voidps/cache/FileCache.kt +++ b/cache/src/main/kotlin/world/gregs/voidps/cache/FileCache.kt @@ -1,8 +1,11 @@ package world.gregs.voidps.cache import world.gregs.voidps.cache.compress.DecompressionContext +import world.gregs.voidps.cache.secure.VersionTableBuilder +import world.gregs.voidps.cache.secure.Whirlpool import java.io.File import java.io.RandomAccessFile +import java.math.BigInteger /** * [Cache] which reads data directly from file @@ -10,19 +13,32 @@ import java.io.RandomAccessFile */ class FileCache( private val main: RandomAccessFile, + private val index255: RandomAccessFile, private val indexes: Array, indexCount: Int, val xteas: Map? ) : ReadOnlyCache(indexCount) { private val dataCache = object : LinkedHashMap>(16, 0.75f, true) { - override fun removeEldestEntry(eldest: MutableMap.MutableEntry>?): Boolean { + override fun removeEldestEntry(eldest: MutableMap.MutableEntry>): Boolean { + return size > 12 + } + } + private val sectorCache = object : LinkedHashMap(16, 0.75f, true) { + override fun removeEldestEntry(eldest: MutableMap.MutableEntry): Boolean { return size > 12 } } private val length = main.length() private val context = DecompressionContext() + override fun sector(index: Int, archive: Int): ByteArray? { + val indexRaf = if (index == 255) index255 else indexes[index] ?: return null + return sectorCache.getOrPut(index + (archive shl 6)) { + readSector(main, length, indexRaf, index, archive) + } + } + override fun data(index: Int, archive: Int, file: Int, xtea: IntArray?): ByteArray? { val matchingIndex = files.getOrNull(index)?.getOrNull(archive)?.indexOf(file) ?: -1 if (matchingIndex == -1) { @@ -31,7 +47,7 @@ class FileCache( val hash = index + (archive shl 6) val files = dataCache.getOrPut(hash) { val indexRaf = indexes[index] ?: return null - readFileData(context, main, length, indexRaf, index, archive, xteas) ?: return null + fileData(context, main, length, indexRaf, index, archive, xteas) ?: return null } return files[matchingIndex] } @@ -46,34 +62,37 @@ class FileCache( companion object : CacheLoader { const val CACHE_FILE_NAME = "main_file_cache" - operator fun invoke(path: String, xteas: Map? = null): Cache { - return load(path, xteas) + operator fun invoke(path: String, exponent: BigInteger? = null, modulus: BigInteger? = null, xteas: Map? = null): Cache { + return load(path, exponent, modulus, xteas) } /** * Create [RandomAccessFile]'s for each index file, load only the archive data into memory */ - override fun load(path: String, mainFile: File, main: RandomAccessFile, index255File: File, index255: RandomAccessFile, indexCount: Int, xteas: Map?, threadUsage: Double): Cache { + override fun load( + path: String, + mainFile: File, + main: RandomAccessFile, + index255File: File, + index255: RandomAccessFile, + indexCount: Int, + versionTable: VersionTableBuilder?, + xteas: Map?, + threadUsage: Double + ): Cache { val length = mainFile.length() val context = DecompressionContext() val indices = Array(indexCount) { indexId -> val file = File(path, "${CACHE_FILE_NAME}.idx$indexId") if (file.exists()) RandomAccessFile(file, "r") else null } - val cache = FileCache(main, indices, indexCount, xteas) + val whirlpool = Whirlpool() + val cache = FileCache(main, index255, indices, indexCount, xteas) for (indexId in 0 until indexCount) { - cache.readArchiveData(context, main, length, index255, indexId) + cache.archiveData(context, main, length, index255, indexId, versionTable, whirlpool) } + cache.versionTable = versionTable?.build(whirlpool) ?: ByteArray(0) return cache } - - @JvmStatic - fun main(args: Array) { - val path = "./data/cache/" - - val start = System.currentTimeMillis() - val cache = load(path) - println("Loaded cache in ${System.currentTimeMillis() - start}ms") - } } } \ No newline at end of file diff --git a/cache/src/main/kotlin/world/gregs/voidps/cache/MemoryCache.kt b/cache/src/main/kotlin/world/gregs/voidps/cache/MemoryCache.kt index 054d325cf6..a60b90a483 100644 --- a/cache/src/main/kotlin/world/gregs/voidps/cache/MemoryCache.kt +++ b/cache/src/main/kotlin/world/gregs/voidps/cache/MemoryCache.kt @@ -3,17 +3,34 @@ package world.gregs.voidps.cache import com.github.michaelbull.logging.InlineLogger import kotlinx.coroutines.* import world.gregs.voidps.cache.compress.DecompressionContext +import world.gregs.voidps.cache.secure.VersionTableBuilder +import world.gregs.voidps.cache.secure.Whirlpool import java.io.File import java.io.RandomAccessFile +import java.math.BigInteger /** * [Cache] that holds all data in memory * Read speeds are as fast, loading is slow and memory usage is high but stable. * Loading is done in parallel as it is much slower to load than [FileCache] + * + * Not much benefit of using this in the live game as file providers cache the + * sector data independently for the file server; so the only use after startup is + * reading dynamic map regions in MapDefinitions. + * It is however very useful for integration tests to speed world resetting. */ class MemoryCache(indexCount: Int) : ReadOnlyCache(indexCount) { val data: Array?>?> = arrayOfNulls(indexCount) + val sectors: Array?> = arrayOfNulls(indexCount) + val index255: Array = arrayOfNulls(indexCount) + + override fun sector(index: Int, archive: Int): ByteArray? { + if (index == 255) { + return index255.getOrNull(archive) + } + return sectors.getOrNull(index)?.getOrNull(archive) + } override fun data(index: Int, archive: Int, file: Int, xtea: IntArray?): ByteArray? { return data.getOrNull(index)?.getOrNull(archive)?.getOrNull(file) @@ -22,15 +39,25 @@ class MemoryCache(indexCount: Int) : ReadOnlyCache(indexCount) { companion object : CacheLoader { private val logger = InlineLogger() - operator fun invoke(path: String, threadUsage: Double = 1.0, xteas: Map? = null): Cache { - return load(path, xteas, threadUsage) + operator fun invoke(path: String, threadUsage: Double = 1.0, exponent: BigInteger? = null, modulus: BigInteger? = null, xteas: Map? = null): Cache { + return load(path, exponent, modulus, xteas, threadUsage) as ReadOnlyCache } /** * Load each index in parallel using a percentage of cpu cores */ @OptIn(DelicateCoroutinesApi::class) - override fun load(path: String, mainFile: File, main: RandomAccessFile, index255File: File, index255: RandomAccessFile, indexCount: Int, xteas: Map?, threadUsage: Double): Cache { + override fun load( + path: String, + mainFile: File, + main: RandomAccessFile, + index255File: File, + index255: RandomAccessFile, + indexCount: Int, + versionTable: VersionTableBuilder?, + xteas: Map?, + threadUsage: Double + ): Cache { val cache = MemoryCache(indexCount) val processors = (Runtime.getRuntime().availableProcessors() * threadUsage).toInt().coerceAtLeast(1) newFixedThreadPoolContext(processors, "cache-loader").use { dispatcher -> @@ -39,12 +66,13 @@ class MemoryCache(indexCount: Int) : ReadOnlyCache(indexCount) { val fileLength = mainFile.length() for (indexId in 0 until indexCount) { launch { - loadIndex(path, indexId, mainFile, fileLength, index255File, xteas, processors, cache) + loadIndex(path, indexId, mainFile, fileLength, index255File, xteas, processors, cache, versionTable) } } } } } + cache.versionTable = versionTable?.build() ?: ByteArray(0) return cache } @@ -59,7 +87,8 @@ class MemoryCache(indexCount: Int) : ReadOnlyCache(indexCount) { index255File: File, xteas: Map?, processors: Int, - cache: MemoryCache + cache: MemoryCache, + versionTable: VersionTableBuilder? ) { val file = File(path, "${FileCache.CACHE_FILE_NAME}.idx$indexId") if (!file.exists()) { @@ -75,10 +104,12 @@ class MemoryCache(indexCount: Int) : ReadOnlyCache(indexCount) { RandomAccessFile(index255File, "r") } val context = DecompressionContext() - val highest = cache.readArchiveData(context, main, mainFileLength, index255, indexId) + val whirlpool = Whirlpool() + val highest = cache.archiveData(context, main, mainFileLength, index255, indexId, versionTable, whirlpool, cache.index255) if (highest == -1) { return } + cache.sectors[indexId] = arrayOfNulls(highest + 1) cache.data[indexId] = arrayOfNulls?>(highest + 1) coroutineScope { if (processors in 2 until highest) { @@ -114,7 +145,9 @@ class MemoryCache(indexCount: Int) : ReadOnlyCache(indexCount) { val raf = RandomAccessFile(file, "r") val main = RandomAccessFile(mainFile, "r") for (archiveId in archives) { - val archiveFiles = cache.readFileData(context, main, mainFileLength, raf, indexId, archiveId, xteas) ?: continue + val archiveFiles = cache.fileData( + context, main, mainFileLength, raf, indexId, archiveId, xteas, cache.sectors + ) ?: continue val archiveFileIds = cache.files[indexId]?.get(archiveId) ?: continue val fileId = archiveFileIds.last() val fileCount = cache.fileCounts[indexId]?.getOrNull(archiveId) ?: continue @@ -126,13 +159,5 @@ class MemoryCache(indexCount: Int) : ReadOnlyCache(indexCount) { cache.data[indexId]!![archiveId] = archiveData } } - - @JvmStatic - fun main(args: Array) { - val path = "./data/cache/" - var start = System.currentTimeMillis() - val cache = load(path, null) - println("Loaded cache in ${System.currentTimeMillis() - start}ms") - } } } \ No newline at end of file diff --git a/cache/src/main/kotlin/world/gregs/voidps/cache/ReadOnlyCache.kt b/cache/src/main/kotlin/world/gregs/voidps/cache/ReadOnlyCache.kt index a1d2aebbe4..b925fb9229 100644 --- a/cache/src/main/kotlin/world/gregs/voidps/cache/ReadOnlyCache.kt +++ b/cache/src/main/kotlin/world/gregs/voidps/cache/ReadOnlyCache.kt @@ -4,38 +4,42 @@ import com.github.michaelbull.logging.InlineLogger import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap import world.gregs.voidps.buffer.read.BufferReader import world.gregs.voidps.cache.compress.DecompressionContext +import world.gregs.voidps.cache.secure.VersionTableBuilder +import world.gregs.voidps.cache.secure.Whirlpool import java.io.RandomAccessFile /** * [Cache] which efficiently stores information about its indexes, archives and files. */ -abstract class ReadOnlyCache( - val indices: IntArray, - val archives: Array, - val fileCounts: Array, - val files: Array?>, - private val hashes: MutableMap -) : Cache { +abstract class ReadOnlyCache(indexCount: Int) : Cache { + val indices: IntArray = IntArray(indexCount) { it } + val archives: Array = arrayOfNulls(indexCount) + val fileCounts: Array = arrayOfNulls(indexCount) + val files: Array?> = arrayOfNulls(indexCount) + private val hashes: MutableMap = Int2IntOpenHashMap(16384) - constructor(indexCount: Int) : this(IntArray(indexCount) { it }, arrayOfNulls(indexCount), arrayOfNulls(indexCount), arrayOfNulls(indexCount), Int2IntOpenHashMap(16384)) + override lateinit var versionTable: ByteArray @Suppress("UNCHECKED_CAST") - internal fun readFileData( + internal fun fileData( context: DecompressionContext, main: RandomAccessFile, mainLength: Long, indexRaf: RandomAccessFile, indexId: Int, archiveId: Int, - xteas: Map? + xteas: Map?, + sectors: Array?>? = null ): Array? { val fileCounts = fileCounts[indexId] ?: return null val fileIds = files[indexId] ?: return null val fileCount = fileCounts.getOrNull(archiveId) ?: return null val sectorData = readSector(main, mainLength, indexRaf, indexId, archiveId) ?: return null + if (sectors != null) { + sectors[indexId]!![archiveId] = sectorData + } val keys = if (xteas != null && indexId == Index.MAPS) xteas[archiveId] else null val decompressed = context.decompress(sectorData, keys) ?: return null - if (fileCount == 1) { val fileId = fileIds[archiveId]?.last() ?: return null return Array(fileId + 1) { @@ -78,18 +82,26 @@ abstract class ReadOnlyCache( return archiveFiles as Array } - internal fun readArchiveData( + internal fun archiveData( context: DecompressionContext, main: RandomAccessFile, length: Long, index255: RandomAccessFile, - indexId: Int + indexId: Int, + versionTable: VersionTableBuilder?, + whirlpool: Whirlpool, + sectors: Array? = null ): Int { val archiveSector = readSector(main, length, index255, 255, indexId) + if (sectors != null) { + sectors[indexId] = archiveSector + } if (archiveSector == null) { logger.trace { "Empty index $indexId." } + versionTable?.skip(indexId) return -1 } + versionTable?.sector(indexId, archiveSector, whirlpool) val decompressed = context.decompress(archiveSector) ?: return -1 val reader = BufferReader(decompressed) val version = reader.readUnsignedByte() @@ -97,7 +109,8 @@ abstract class ReadOnlyCache( throw RuntimeException("Unknown version: $version") } if (version >= 6) { - reader.skip(4) // revision + val revision = reader.readInt() + versionTable?.revision(indexId, revision) } val flags = reader.readByte() val archiveCount = reader.readSmart(version) @@ -151,7 +164,7 @@ abstract class ReadOnlyCache( override fun archiveCount(index: Int) = archives.size - override fun lastArchiveId(indexId: Int) = archives.getOrNull(indexId)?.last() ?: -1 + override fun lastArchiveId(indexId: Int) = archives.getOrNull(indexId)?.lastOrNull() ?: -1 override fun archiveId(index: Int, hash: Int) = hashes[hash] ?: -1 @@ -159,7 +172,7 @@ abstract class ReadOnlyCache( override fun fileCount(indexId: Int, archiveId: Int) = fileCounts.getOrNull(indexId)?.getOrNull(archiveId) ?: 0 - override fun lastFileId(indexId: Int, archive: Int) = files.getOrNull(indexId)?.getOrNull(archive)?.last() ?: -1 + override fun lastFileId(indexId: Int, archive: Int) = files.getOrNull(indexId)?.getOrNull(archive)?.lastOrNull() ?: -1 override fun write(index: Int, archive: Int, file: Int, data: ByteArray, xteas: IntArray?) { throw UnsupportedOperationException("Read only cache.") @@ -182,7 +195,7 @@ abstract class ReadOnlyCache( private const val WHIRLPOOL_FLAG = 0x2 const val INDEX_SIZE = 6 - private const val WHIRLPOOL_SIZE = 64 + const val WHIRLPOOL_SIZE = 64 private const val SECTOR_SIZE = 520 private const val SECTOR_HEADER_SIZE_SMALL = 8 private const val SECTOR_DATA_SIZE_SMALL = 512 @@ -194,7 +207,7 @@ abstract class ReadOnlyCache( /** * Reads a section of a cache's archive */ - private fun readSector(mainFile: RandomAccessFile, length: Long, raf: RandomAccessFile, indexId: Int, sectorId: Int): ByteArray? { + internal fun readSector(mainFile: RandomAccessFile, length: Long, raf: RandomAccessFile, indexId: Int, sectorId: Int): ByteArray? { if (length < INDEX_SIZE * sectorId + INDEX_SIZE) { return null } diff --git a/cache/src/main/kotlin/world/gregs/voidps/cache/compress/DecompressionContext.kt b/cache/src/main/kotlin/world/gregs/voidps/cache/compress/DecompressionContext.kt index 2e0fa0a37f..fc122746d7 100644 --- a/cache/src/main/kotlin/world/gregs/voidps/cache/compress/DecompressionContext.kt +++ b/cache/src/main/kotlin/world/gregs/voidps/cache/compress/DecompressionContext.kt @@ -36,7 +36,7 @@ internal class DecompressionContext { } BZIP2 -> { if (!warned.get()) { - logger.warn { "GZIP2 Compression found - replace to improve read performance." } + logger.warn { "BZIP2 Compression found - replace to improve read performance." } warned.set(true) } val decompressed = ByteArray(decompressedSize) diff --git a/cache/src/main/kotlin/world/gregs/voidps/cache/secure/CRC.kt b/cache/src/main/kotlin/world/gregs/voidps/cache/secure/CRC.kt index 7e2be8f0d4..7e1e5e970f 100644 --- a/cache/src/main/kotlin/world/gregs/voidps/cache/secure/CRC.kt +++ b/cache/src/main/kotlin/world/gregs/voidps/cache/secure/CRC.kt @@ -1,26 +1,6 @@ package world.gregs.voidps.cache.secure -import com.displee.cache.index.Index -import world.gregs.voidps.buffer.read.BufferReader -import java.io.RandomAccessFile - -/** - * Quickly reads cache index crc without loading the full [CacheDelegate] - */ -class CRC( - private val mainFile: RandomAccessFile, - private val raf: RandomAccessFile -) { - - fun close() { - mainFile.close() - raf.close() - } - - fun read(index: Int): Int { - return generateCrc(readSector(index)!!) - } - +class CRC { private val table = IntArray(256) { var crc = it for (i in 0..7) { @@ -33,7 +13,7 @@ class CRC( crc } - private fun generateCrc(data: ByteArray, offset: Int = 0, length: Int = data.size): Int { + fun calculate(data: ByteArray, offset: Int = 0, length: Int = data.size): Int { var crc = -1 for (i in offset until length) { crc = crc ushr 8 xor table[crc xor data[i].toInt() and 0xff] @@ -41,57 +21,4 @@ class CRC( crc = crc xor -0x1 return crc } - - private fun readSector(id: Int): ByteArray? { - if (mainFile.length() < Index.INDEX_SIZE * id + Index.INDEX_SIZE) { - return null - } - val sectorData = ByteArray(Index.SECTOR_SIZE) - raf.seek(id.toLong() * Index.INDEX_SIZE) - raf.read(sectorData, 0, Index.INDEX_SIZE) - val bigSector = id > 65535 - val buffer = BufferReader(sectorData) - val size = buffer.readUnsignedMedium() - var position = buffer.readUnsignedMedium() - if (size < 0 || position <= 0 || position > mainFile.length() / Index.SECTOR_SIZE) { - return null - } - val data = ByteArray(size) - var read = 0 - var zoneCount = 0 - val sectorHeaderSize = if (bigSector) Index.SECTOR_HEADER_SIZE_BIG else Index.SECTOR_HEADER_SIZE_SMALL - val sectorDataSize = if (bigSector) Index.SECTOR_DATA_SIZE_BIG else Index.SECTOR_DATA_SIZE_SMALL - while (read < size) { - if (position == 0) { - return null - } - var requiredToRead = size - read - if (requiredToRead > sectorDataSize) { - requiredToRead = sectorDataSize - } - mainFile.seek(position.toLong() * Index.SECTOR_SIZE) - mainFile.read(buffer.array(), 0, requiredToRead + sectorHeaderSize) - buffer.position(0) - val sectorId = if (bigSector) { - buffer.readInt() - } else { - buffer.readUnsignedShort() - } - val zone = buffer.readUnsignedShort() - val nextPosition = buffer.readUnsignedMedium() - val index = buffer.readUnsignedByte() - if (index != 255 || id != sectorId || zoneCount != zone) { - return null - } else if (nextPosition < 0 || nextPosition > mainFile.length() / Index.SECTOR_SIZE) { - return null - } - val bufferData = buffer.array() - for (i in 0 until requiredToRead) { - data[read++] = bufferData[i + sectorHeaderSize] - } - position = nextPosition - zoneCount++ - } - return data - } } \ No newline at end of file diff --git a/cache/src/main/kotlin/world/gregs/voidps/cache/secure/RSA.kt b/cache/src/main/kotlin/world/gregs/voidps/cache/secure/RSA.kt index e66e2029d5..63409f507b 100644 --- a/cache/src/main/kotlin/world/gregs/voidps/cache/secure/RSA.kt +++ b/cache/src/main/kotlin/world/gregs/voidps/cache/secure/RSA.kt @@ -9,8 +9,8 @@ object RSA { /** * Encrypt/decrypts bytes with key and modulus */ - fun crypt(data: ByteArray, modulus: BigInteger, key: BigInteger): ByteArray { - return BigInteger(data).modPow(key, modulus).toByteArray() + fun crypt(data: ByteArray, modulus: BigInteger, exponent: BigInteger): ByteArray { + return BigInteger(data).modPow(exponent, modulus).toByteArray() } /** diff --git a/cache/src/main/kotlin/world/gregs/voidps/cache/secure/VersionTableBuilder.kt b/cache/src/main/kotlin/world/gregs/voidps/cache/secure/VersionTableBuilder.kt new file mode 100644 index 0000000000..8b89fa12de --- /dev/null +++ b/cache/src/main/kotlin/world/gregs/voidps/cache/secure/VersionTableBuilder.kt @@ -0,0 +1,93 @@ +package world.gregs.voidps.cache.secure + +import world.gregs.voidps.cache.ReadOnlyCache +import java.math.BigInteger + +class VersionTableBuilder( + private val exponent: BigInteger, + private val modulus: BigInteger, + private val indexCount: Int +) { + + private val crc = CRC() + private val versionTable = ByteArray(positionFor(indexCount) + MAX_RSA_SIZE) + private var built = false + + init { + versionTable[5] = indexCount.toByte() + } + + fun skip(index: Int) { + val pos = positionFor(index) + for (i in 0 until TABLE_INDEX_OFFSET) { + versionTable[pos + i] = 0 + } + } + + fun sector(index: Int, sectorData: ByteArray, whirlpool: Whirlpool) { + val crc = crc.calculate(sectorData) + crc(index, crc) + val output = ByteArray(ReadOnlyCache.WHIRLPOOL_SIZE) + whirlpool.reset() + whirlpool.add(sectorData) + whirlpool.finalize(output) + whirlpool(index, output) + } + + fun crc(index: Int, crc: Int) { + val pos = positionFor(index) + versionTable[pos] = (crc shr 24).toByte() + versionTable[pos + 1] = (crc shr 16).toByte() + versionTable[pos + 2] = (crc shr 8).toByte() + versionTable[pos + 3] = (crc).toByte() + } + + fun revision(index: Int, revision: Int) { + val pos = positionFor(index) + 4 + versionTable[pos] = (revision shr 24).toByte() + versionTable[pos + 1] = (revision shr 16).toByte() + versionTable[pos + 2] = (revision shr 8).toByte() + versionTable[pos + 3] = (revision).toByte() + } + + + fun whirlpool(index: Int, whirlpool: ByteArray) { + val pos = positionFor(index) + 8 + for (i in whirlpool.indices) { + versionTable[pos + i] = whirlpool[i] + } + } + + fun build(whirlpool: Whirlpool = Whirlpool()): ByteArray { + if (built) { + return versionTable + } + built = true + val output = ByteArray(ReadOnlyCache.WHIRLPOOL_SIZE + 1) + output[0] = 1 + whirlpool.reset() + whirlpool.add(versionTable, 5, positionFor(indexCount) - 5) + whirlpool.finalize(output, 1) + val rsa = RSA.crypt(output, modulus, exponent) + val pos = positionFor(indexCount) + for(i in rsa.indices) { + versionTable[pos + i] = rsa[i] + } + val end = pos + rsa.size + versionTable[0] = 0 + val value = end - 5 + versionTable[1] = (value shr 24).toByte() + versionTable[2] = (value shr 16).toByte() + versionTable[3] = (value shr 8).toByte() + versionTable[4] = (value).toByte() + val data = ByteArray(end) + System.arraycopy(versionTable, 0, data, 0, data.size) + return data + } + + companion object { + private fun positionFor(index: Int) = 6 + index * TABLE_INDEX_OFFSET + private const val TABLE_INDEX_OFFSET = ReadOnlyCache.WHIRLPOOL_SIZE + 8 + private const val MAX_RSA_SIZE = 256 + } +} \ No newline at end of file diff --git a/cache/src/main/kotlin/world/gregs/voidps/cache/secure/Whirlpool.kt b/cache/src/main/kotlin/world/gregs/voidps/cache/secure/Whirlpool.kt new file mode 100644 index 0000000000..271cee3c10 --- /dev/null +++ b/cache/src/main/kotlin/world/gregs/voidps/cache/secure/Whirlpool.kt @@ -0,0 +1,221 @@ +package world.gregs.voidps.cache.secure + +class Whirlpool { + private val block = LongArray(8) + private val hash = LongArray(8) + private val aLongArray6637 = LongArray(8) + private val aLongArray6638 = LongArray(8) + private val state = LongArray(8) + private val bitLength = ByteArray(32) + private val buffer = ByteArray(64) + private var bufferBits = 0 + private var bufferPosition = 0 + private val aLongArrayArray6630 = Array(8) { LongArray(256) } + private val aLongArray6631 = LongArray(11) + + init { + val chars = + "\u1823\uc6e8\u87b8\u014f\u36a6\ud2f5\u796f\u9152\u60bc\u9b8e\ua30c\u7b35\u1de0\ud7c2\u2e4b\ufe57\u1577\u37e5\u9ff0\u4ada\u58c9\u290a\ub1a0\u6b85\ubd5d\u10f4\ucb3e\u0567\ue427\u418b\ua77d\u95d8\ufbee\u7c66\udd17\u479e\uca2d\ubf07\uad5a\u8333\u6302\uaa71\uc819\u49d9\uf2e3\u5b88\u9a26\u32b0\ue90f\ud580\ubecd\u3448\uff7a\u905f\u2068\u1aae\ub454\u9322\u64f1\u7312\u4008\uc3ec\udba1\u8d3d\u9700\ucf2b\u7682\ud61b\ub5af\u6a50\u45f3\u30ef\u3f55\ua2ea\u65ba\u2fc0\ude1c\ufd4d\u9275\u068a\ub2e6\u0e1f\u62d4\ua896\uf9c5\u2559\u8472\u394c\u5e78\u388c\ud1a5\ue261\ub321\u9c1e\u43c7\ufc04\u5199\u6d0d\ufadf\u7e24\u3bab\uce11\u8f4e\ub7eb\u3c81\u94f7\ub913\u2cd3\ue76e\uc403\u5644\u7fa9\u2abb\uc153\udc0b\u9d6c\u3174\uf646\uac89\u14e1\u163a\u6909\u70b6\ud0ed\ucc42\u98a4\u285c\uf886" + for (i in 0..255) { + val code = chars[i / 2].code + val l = if (i and 0x1 == 0) (code ushr 8).toLong() else (code and 0xff).toLong() + var l_38_ = l shl 1 + if (l_38_ >= 256L) { + l_38_ = l_38_ xor 0x11dL + } + var l_39_ = l_38_ shl 1 + if (l_39_ >= 256L) { + l_39_ = l_39_ xor 0x11dL + } + val l_40_ = l_39_ xor l + var l_41_ = l_39_ shl 1 + if (l_41_ >= 256L) { + l_41_ = l_41_ xor 0x11dL + } + val l_42_ = l_41_ xor l + aLongArrayArray6630[0][i] = l shl 56 or (l shl 48) or (l_39_ shl 40) or (l shl 32) or (l_41_ shl 24) or (l_40_ shl 16) or (l_38_ shl 8) or l_42_ + for (i_43_ in 1..7) { + aLongArrayArray6630[i_43_][i] = aLongArrayArray6630[i_43_ - 1][i] ushr 8 or (aLongArrayArray6630[i_43_ - 1][i] shl 56) + } + } + aLongArray6631[0] = 0L + for (i in 1..10) { + val i_44_ = 8 * (i - 1) + aLongArray6631[i] = aLongArrayArray6630[0][i_44_] and 0xffffffffffffffL.inv() xor + (aLongArrayArray6630[1][i_44_ + 1] and 0xff000000000000L) xor + (aLongArrayArray6630[2][i_44_ + 2] and 0xff0000000000L) xor + (aLongArrayArray6630[3][i_44_ + 3] and 0xff00000000L) xor + (aLongArrayArray6630[4][4 + i_44_] and 0xff000000L) xor + (aLongArrayArray6630[5][5 + i_44_] and 0xff0000L) xor + (aLongArrayArray6630[6][6 + i_44_] and 0xff00L) xor + (aLongArrayArray6630[7][7 + i_44_] and 0xffL) + } + reset() + } + + fun reset() { + for (i in 0..31) { + bitLength[i] = 0.toByte() + } + bufferPosition = 0 + bufferBits = 0 + buffer[0] = 0.toByte() + for (i_3_ in 0..7) { + hash[i_3_] = 0L + } + } + + private fun processBuffer() { + var i_4_ = 0 + var i_5_ = 0 + while (i_4_ < 8) { + block[i_4_] = buffer[i_5_].toLong() shl 56 xor + (buffer[i_5_ + 1].toLong() and 0xffL shl 48) xor + (buffer[2 + i_5_].toLong() and 0xffL shl 40) xor + (buffer[i_5_ + 3].toLong() and 0xffL shl 32) xor + (buffer[i_5_ + 4].toLong() and 0xffL shl 24) xor + (buffer[5 + i_5_].toLong() and 0xffL shl 16) xor + (buffer[i_5_ + 6].toLong() and 0xffL shl 8) xor + (buffer[7 + i_5_].toLong() and 0xffL) + i_4_++ + i_5_ += 8 + } + i_4_ = 0 + while (i_4_ < 8) { + state[i_4_] = block[i_4_] xor hash[i_4_].also { aLongArray6637[i_4_] = it } + i_4_++ + } + i_4_ = 1 + while (i_4_ <= 10) { + i_5_ = 0 + while (i_5_ < 8) { + aLongArray6638[i_5_] = 0L + var i_6_ = 0 + var i_7_ = 56 + while (i_6_ < 8) { + aLongArray6638[i_5_] = aLongArray6638[i_5_] xor aLongArrayArray6630[i_6_][(aLongArray6637[i_5_ - i_6_ and 0x7] ushr i_7_).toInt() and 0xff] + i_6_++ + i_7_ -= 8 + } + i_5_++ + } + i_5_ = 0 + while (i_5_ < 8) { + aLongArray6637[i_5_] = aLongArray6638[i_5_] + i_5_++ + } + aLongArray6637[0] = aLongArray6637[0] xor aLongArray6631[i_4_] + i_5_ = 0 + while (i_5_ < 8) { + aLongArray6638[i_5_] = aLongArray6637[i_5_] + var i_8_ = 0 + var i_9_ = 56 + while (i_8_ < 8) { + aLongArray6638[i_5_] = aLongArray6638[i_5_] xor aLongArrayArray6630[i_8_][(state[i_5_ - i_8_ and 0x7] ushr i_9_).toInt() and 0xff] + i_8_++ + i_9_ -= 8 + } + i_5_++ + } + i_5_ = 0 + while (i_5_ < 8) { + state[i_5_] = aLongArray6638[i_5_] + i_5_++ + } + i_4_++ + } + i_4_ = 0 + while (i_4_ < 8) { + hash[i_4_] = hash[i_4_] xor (state[i_4_] xor block[i_4_]) + i_4_++ + } + } + + fun finalize(digest: ByteArray, offset: Int = 0) { + buffer[bufferPosition] = (buffer[bufferPosition].toInt() or (128 ushr (bufferBits and 0x7))).toByte() + ++bufferPosition + if (bufferPosition > 32) { + while (bufferPosition < 64) { + buffer[bufferPosition++] = 0.toByte() + } + processBuffer() + bufferPosition = 0 + } + while (bufferPosition < 32) { + buffer[bufferPosition++] = 0.toByte() + } + System.arraycopy(bitLength, 0, buffer, 32, 32) + processBuffer() + var readIndex = 0 + var writeIndex = offset + while (readIndex < 8) { + val l = hash[readIndex] + digest[writeIndex] = (l ushr 56).toInt().toByte() + digest[writeIndex + 1] = (l ushr 48).toInt().toByte() + digest[writeIndex + 2] = (l ushr 40).toInt().toByte() + digest[writeIndex + 3] = (l ushr 32).toInt().toByte() + digest[writeIndex + 4] = (l ushr 24).toInt().toByte() + digest[writeIndex + 5] = (l ushr 16).toInt().toByte() + digest[writeIndex + 6] = (l ushr 8).toInt().toByte() + digest[writeIndex + 7] = l.toInt().toByte() + readIndex++ + writeIndex += 8 + } + } + + fun add(source: ByteArray, offset: Int = 0, length: Int = source.size) { + var sourceBits = length * 8L + var i = offset + val i_23_ = 8 - (sourceBits.toInt() and 0x7) and 0x7 + val i_24_ = bufferBits and 0x7 + var l_25_ = sourceBits + var i_26_ = 31 + var i_27_ = 0 + while ( /**/i_26_ >= 0) { + i_27_ += (bitLength[i_26_].toInt() and 0xff) + (l_25_.toInt() and 0xff) + bitLength[i_26_] = i_27_.toByte() + i_27_ = i_27_ ushr 8 + l_25_ = l_25_ ushr 8 + i_26_-- + } + while (sourceBits > 8L) { + val i_28_ = source[i].toInt() shl i_23_ and 0xff or (source[i + 1].toInt() and 0xff ushr 8 - i_23_) + if (i_28_ < 0 || i_28_ >= 256) { + throw RuntimeException() + } + buffer[bufferPosition] = (buffer[bufferPosition].toInt() or (i_28_ ushr i_24_)).toByte() + ++bufferPosition + bufferBits += 8 - i_24_ + if (bufferBits == 512) { + processBuffer() + bufferPosition = 0 + bufferBits = 0 + } + buffer[bufferPosition] = (i_28_ shl 8 - i_24_ and 0xff).toByte() + bufferBits += i_24_ + sourceBits -= 8L + i++ + } + val i_29_: Int + if (sourceBits > 0L) { + i_29_ = source[i].toInt() shl i_23_ and 0xff + buffer[bufferPosition] = (buffer[bufferPosition].toInt() or (i_29_ ushr i_24_)).toByte() + } else { + i_29_ = 0 + } + if (i_24_.toLong() + sourceBits < 8L) { + bufferBits += sourceBits.toInt() + } else { + ++bufferPosition + bufferBits += 8 - i_24_ + sourceBits -= (8 - i_24_).toLong() + if (bufferBits == 512) { + processBuffer() + bufferPosition = 0 + bufferBits = 0 + } + buffer[bufferPosition] = (i_29_ shl 8 - i_24_ and 0xff).toByte() + bufferBits += sourceBits.toInt() + } + } +} \ No newline at end of file diff --git a/cache/src/test/kotlin/world/gregs/voidps/cache/secure/VersionTableBuilderTest.kt b/cache/src/test/kotlin/world/gregs/voidps/cache/secure/VersionTableBuilderTest.kt new file mode 100644 index 0000000000..77b0d2eebc --- /dev/null +++ b/cache/src/test/kotlin/world/gregs/voidps/cache/secure/VersionTableBuilderTest.kt @@ -0,0 +1,116 @@ +package world.gregs.voidps.cache.secure + +import com.displee.cache.CacheLibrary +import org.junit.jupiter.api.Assertions.assertArrayEquals +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import world.gregs.voidps.buffer.write.BufferWriter +import java.io.FileInputStream +import java.math.BigInteger +import java.util.* + +class VersionTableBuilderTest { + + @Test + fun `Build small version table`() { + val indexCount = 5 + val table = VersionTableBuilder(BigInteger("12345"), BigInteger("54321"), indexCount) + + for (i in 0 until indexCount) { + table.crc(i, i) + table.whirlpool(i, ByteArray(64) { it.toByte() }) + table.revision(i, i) + } + + val rsa = byteArrayOf(73, 58) + val expected = expected(indexCount, rsa) + val actual = table.build() + assertEquals(expected, actual) + } + + @Test + fun `Build large version table`() { + val indexCount = 50 + val table = VersionTableBuilder(BigInteger("1234567891011121314151617181920"), BigInteger("2019181716151413121110987654321"), indexCount) + for (i in 0 until indexCount) { + table.crc(i, i) + table.whirlpool(i, ByteArray(64) { it.toByte() }) + table.revision(i, i) + } + + val rsa = byteArrayOf(18, -4, -73, 59, -128, -51, -117, 60, 66, 21, 127, -17, -54) + val expected = expected(indexCount, rsa) + val actual = table.build() + assertEquals(expected, actual) + } + + @Test + fun `Build version table with large RSA numbers`() { + val indexCount = 50 + + val random = java.util.Random(0) + val exponent = BigInteger(256, random) + val modulus = BigInteger(256, random) + val table = VersionTableBuilder(exponent, modulus, indexCount) + for (i in 0 until indexCount) { + table.crc(i, i) + table.whirlpool(i, ByteArray(64) { it.toByte() }) + table.revision(i, i) + } + + val rsa = byteArrayOf(46, 32, 78, -33, -33, -50, 69, -78, 13, 77, 66, -120, 95, -104, -92, 62, 53, 121, -16, 1, 116, -100, 53, -36, 72, -124, 116, 113, 27, 88, -106, -19) + val expected = expected(indexCount, rsa) + val actual = table.build() + assertEquals(expected, actual) + } + + @Disabled + @Test + fun `Check actual cache`() { + val library = CacheLibrary("../data/cache/") + val indexCount = library.indices().size + + val properties = Properties() + properties.load(FileInputStream("../game/src/main/resources/private.properties")) + val exponent = BigInteger(properties.getProperty("gamePrivate"), 16) + val modulus = BigInteger(properties.getProperty("gameModulus"), 16) + val table = VersionTableBuilder(exponent, modulus, indexCount) + for (i in 0 until indexCount) { + val index = library.index(i) + table.crc(i, index.crc) + table.revision(i, index.revision) + table.whirlpool(i, index.whirlpool ?: ByteArray(64)) + } + + val expected = library.generateNewUkeys(exponent, modulus) + val actual = table.build() + assertEquals(expected, actual) + } + + private fun assertEquals(expected: ByteArray, actual: ByteArray) { + if (!expected.contentEquals(actual)) { + println(expected.contentToString()) + println(actual.contentToString()) + } + assertArrayEquals(expected, actual) + } + + private fun expected(indexCount: Int, rsa: ByteArray): ByteArray { + val size = 6 + rsa.size + indexCount * 72 + val writer = BufferWriter(size) + writer.writeByte(0) + writer.writeInt(size - 5) + writer.writeByte(indexCount) + for (i in 0 until indexCount) { + writer.skip(3) + writer.writeByte(i) + writer.skip(3) + writer.writeByte(i) + for (j in 0 until 64) { + writer.writeByte(j) + } + } + writer.writeBytes(rsa) + return writer.toArray() + } +} \ No newline at end of file diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/TimedLoader.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/TimedLoader.kt index d91675a137..c8f815d1a0 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/TimedLoader.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/TimedLoader.kt @@ -5,6 +5,14 @@ import world.gregs.voidps.engine.client.ui.chat.plural private val logger = InlineLogger("TimedLoader") +fun timed(name: String, block: () -> R): R { + val start = System.currentTimeMillis() + val result = block.invoke() + val duration = System.currentTimeMillis() - start + logger.info { "Loaded $name in ${duration}ms" } + return result +} + fun timedLoad(name: String, block: () -> Int) { val start = System.currentTimeMillis() val result = block.invoke() diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/PlayerAccountLoader.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/PlayerAccountLoader.kt index 162dabb68a..0dcd3bddf6 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/PlayerAccountLoader.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/PlayerAccountLoader.kt @@ -7,7 +7,11 @@ import kotlinx.coroutines.withContext import org.mindrot.jbcrypt.BCrypt import world.gregs.voidps.engine.data.PlayerAccounts import world.gregs.voidps.engine.entity.character.player.Player -import world.gregs.voidps.network.* +import world.gregs.voidps.network.AccountLoader +import world.gregs.voidps.network.Instruction +import world.gregs.voidps.network.NetworkQueue +import world.gregs.voidps.network.Response +import world.gregs.voidps.network.client.Client /** * Checks password is valid for a player account before logging in diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/ui/Interfaces.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/ui/Interfaces.kt index 1ffe38a41e..d603844ecd 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/ui/Interfaces.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/ui/Interfaces.kt @@ -14,7 +14,7 @@ import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.item.Item import world.gregs.voidps.engine.event.Events import world.gregs.voidps.engine.get -import world.gregs.voidps.network.Client +import world.gregs.voidps.network.client.Client import world.gregs.voidps.network.encode.* /** diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/client/variable/PlayerVariables.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/client/variable/PlayerVariables.kt index cda6249e7b..bb32a8a62e 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/client/variable/PlayerVariables.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/client/variable/PlayerVariables.kt @@ -3,7 +3,7 @@ package world.gregs.voidps.engine.client.variable import world.gregs.voidps.engine.data.config.VariableDefinition.Companion.persist import world.gregs.voidps.engine.data.definition.VariableDefinitions import world.gregs.voidps.engine.event.Events -import world.gregs.voidps.network.Client +import world.gregs.voidps.network.client.Client class PlayerVariables( events: Events, diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/data/config/VariableDefinition.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/data/config/VariableDefinition.kt index 9d29151bda..7f2c00c99b 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/data/config/VariableDefinition.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/data/config/VariableDefinition.kt @@ -4,7 +4,7 @@ import com.github.michaelbull.logging.InlineLogger import world.gregs.voidps.cache.Definition import world.gregs.voidps.engine.client.variable.StringValues import world.gregs.voidps.engine.client.variable.VariableValues -import world.gregs.voidps.network.Client +import world.gregs.voidps.network.client.Client import world.gregs.voidps.network.encode.sendVarbit import world.gregs.voidps.network.encode.sendVarc import world.gregs.voidps.network.encode.sendVarcStr diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/World.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/World.kt index 8a22a84a2b..2aa70b1990 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/World.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/World.kt @@ -9,6 +9,7 @@ import world.gregs.voidps.engine.event.EventHandlerStore import world.gregs.voidps.engine.event.Events import world.gregs.voidps.engine.get import world.gregs.voidps.type.Tile +import java.util.* import java.util.concurrent.ConcurrentHashMap const val MAX_PLAYERS = 0x800 // 2048 @@ -20,13 +21,25 @@ object World : Entity, Variable, EventDispatcher, Runnable, KoinComponent { override val variables = Variables(events) - const val id = 16 - const val name = "World $id" + var id = 0 + private set(value) { + field = value + name = "World $value" + } + var name: String = "World" + private set var members: Boolean = false private set - fun start(members: Boolean) { + fun start(properties: Properties) { + val members = properties.getProperty("members").toBoolean() + val id = properties.getProperty("world").toInt() + start(members, id) + } + + fun start(members: Boolean = true, id: Int = 16) { this.members = members + this.id = id val store: EventHandlerStore = get() store.populate(World) events.emit(Registered) diff --git a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/player/Player.kt b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/player/Player.kt index 8adb37f573..f6bdd4f528 100644 --- a/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/player/Player.kt +++ b/engine/src/main/kotlin/world/gregs/voidps/engine/entity/character/player/Player.kt @@ -32,9 +32,9 @@ import world.gregs.voidps.engine.queue.strongQueue import world.gregs.voidps.engine.suspend.Suspension import world.gregs.voidps.engine.timer.TimerQueue import world.gregs.voidps.engine.timer.Timers -import world.gregs.voidps.network.Client -import world.gregs.voidps.network.ClientState import world.gregs.voidps.network.Instruction +import world.gregs.voidps.network.client.Client +import world.gregs.voidps.network.client.ClientState import world.gregs.voidps.network.encode.login import world.gregs.voidps.network.encode.logout import world.gregs.voidps.network.visual.PlayerVisuals diff --git a/engine/src/test/kotlin/world/gregs/voidps/engine/client/PlayerAccountLoaderTest.kt b/engine/src/test/kotlin/world/gregs/voidps/engine/client/PlayerAccountLoaderTest.kt index b3ec62ec12..2af88bfebd 100644 --- a/engine/src/test/kotlin/world/gregs/voidps/engine/client/PlayerAccountLoaderTest.kt +++ b/engine/src/test/kotlin/world/gregs/voidps/engine/client/PlayerAccountLoaderTest.kt @@ -16,9 +16,9 @@ import org.junit.jupiter.api.extension.ExtendWith import world.gregs.voidps.engine.data.PlayerAccounts import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.script.KoinMock -import world.gregs.voidps.network.Client import world.gregs.voidps.network.NetworkQueue import world.gregs.voidps.network.Response +import world.gregs.voidps.network.client.Client @ExtendWith(MockKExtension::class) @OptIn(ExperimentalCoroutinesApi::class) diff --git a/engine/src/test/kotlin/world/gregs/voidps/engine/client/ui/InterfaceTest.kt b/engine/src/test/kotlin/world/gregs/voidps/engine/client/ui/InterfaceTest.kt index ad29bcf50b..d703d9f37f 100644 --- a/engine/src/test/kotlin/world/gregs/voidps/engine/client/ui/InterfaceTest.kt +++ b/engine/src/test/kotlin/world/gregs/voidps/engine/client/ui/InterfaceTest.kt @@ -8,7 +8,7 @@ import org.koin.test.mock.declare import world.gregs.voidps.engine.data.definition.InterfaceDefinitions import world.gregs.voidps.engine.event.Events import world.gregs.voidps.engine.script.KoinMock -import world.gregs.voidps.network.Client +import world.gregs.voidps.network.client.Client abstract class InterfaceTest : KoinMock() { diff --git a/engine/src/test/kotlin/world/gregs/voidps/engine/client/update/player/PlayerUpdateTaskTest.kt b/engine/src/test/kotlin/world/gregs/voidps/engine/client/update/player/PlayerUpdateTaskTest.kt index d414a4b537..b4b1214b8d 100644 --- a/engine/src/test/kotlin/world/gregs/voidps/engine/client/update/player/PlayerUpdateTaskTest.kt +++ b/engine/src/test/kotlin/world/gregs/voidps/engine/client/update/player/PlayerUpdateTaskTest.kt @@ -18,14 +18,14 @@ import world.gregs.voidps.engine.client.update.view.Viewport import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.Players import world.gregs.voidps.engine.event.EventHandlerStore -import world.gregs.voidps.type.Delta -import world.gregs.voidps.type.Tile import world.gregs.voidps.engine.script.KoinMock import world.gregs.voidps.engine.value -import world.gregs.voidps.network.Client +import world.gregs.voidps.network.client.Client import world.gregs.voidps.network.encode.updatePlayers import world.gregs.voidps.network.visual.PlayerVisuals import world.gregs.voidps.network.visual.VisualEncoder +import world.gregs.voidps.type.Delta +import world.gregs.voidps.type.Tile internal class PlayerUpdateTaskTest : KoinMock() { diff --git a/engine/src/test/kotlin/world/gregs/voidps/engine/client/variable/VariablesTest.kt b/engine/src/test/kotlin/world/gregs/voidps/engine/client/variable/VariablesTest.kt index 5285af1255..9938c39754 100644 --- a/engine/src/test/kotlin/world/gregs/voidps/engine/client/variable/VariablesTest.kt +++ b/engine/src/test/kotlin/world/gregs/voidps/engine/client/variable/VariablesTest.kt @@ -8,7 +8,7 @@ import world.gregs.voidps.engine.data.config.VariableDefinition import world.gregs.voidps.engine.data.definition.VariableDefinitions import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.event.Events -import world.gregs.voidps.network.Client +import world.gregs.voidps.network.client.Client internal class VariablesTest { diff --git a/engine/src/test/kotlin/world/gregs/voidps/engine/data/config/VariableDefinitionTest.kt b/engine/src/test/kotlin/world/gregs/voidps/engine/data/config/VariableDefinitionTest.kt index cf55cb0e9b..8b114b2a9a 100644 --- a/engine/src/test/kotlin/world/gregs/voidps/engine/data/config/VariableDefinitionTest.kt +++ b/engine/src/test/kotlin/world/gregs/voidps/engine/data/config/VariableDefinitionTest.kt @@ -5,7 +5,7 @@ import io.mockk.mockkStatic import io.mockk.verify import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import world.gregs.voidps.network.Client +import world.gregs.voidps.network.client.Client import world.gregs.voidps.network.encode.sendVarbit import world.gregs.voidps.network.encode.sendVarc import world.gregs.voidps.network.encode.sendVarcStr diff --git a/engine/src/test/kotlin/world/gregs/voidps/engine/entity/character/player/PlayerOptionsTest.kt b/engine/src/test/kotlin/world/gregs/voidps/engine/entity/character/player/PlayerOptionsTest.kt index f21a220f0e..e4eeece5f8 100644 --- a/engine/src/test/kotlin/world/gregs/voidps/engine/entity/character/player/PlayerOptionsTest.kt +++ b/engine/src/test/kotlin/world/gregs/voidps/engine/entity/character/player/PlayerOptionsTest.kt @@ -4,7 +4,7 @@ import io.mockk.* import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import world.gregs.voidps.network.Client +import world.gregs.voidps.network.client.Client import world.gregs.voidps.network.encode.contextMenuOption internal class PlayerOptionsTest { diff --git a/engine/src/test/kotlin/world/gregs/voidps/engine/map/zone/ZoneBatchUpdatesTest.kt b/engine/src/test/kotlin/world/gregs/voidps/engine/map/zone/ZoneBatchUpdatesTest.kt index bce4a8a726..285becb1b9 100644 --- a/engine/src/test/kotlin/world/gregs/voidps/engine/map/zone/ZoneBatchUpdatesTest.kt +++ b/engine/src/test/kotlin/world/gregs/voidps/engine/map/zone/ZoneBatchUpdatesTest.kt @@ -19,7 +19,7 @@ import world.gregs.voidps.engine.event.EventHandlerStore import world.gregs.voidps.engine.map.collision.Collisions import world.gregs.voidps.engine.map.collision.GameObjectCollision import world.gregs.voidps.engine.script.KoinMock -import world.gregs.voidps.network.Client +import world.gregs.voidps.network.client.Client import world.gregs.voidps.network.encode.clearZone import world.gregs.voidps.network.encode.send import world.gregs.voidps.network.encode.sendBatch diff --git a/file-server.properties b/file-server.properties deleted file mode 100644 index 06f125986a..0000000000 --- a/file-server.properties +++ /dev/null @@ -1,6 +0,0 @@ -port=50016 -revision=634 -cachePath=./data/cache/ -rsaModulus=d6808be939bbfd2ec4e96b1581ce3e1144b526e7643a72e3c64fbb902724fbfcf14ab601da6d6f8dbb57d1c369d080d9fc392abeb7886e0076d07f2aea5810e540d2817fd1967e35b39cc95cf7c9170b5fb55f5bf95524b60e938f0d64614bc365b87d66963a8cc8664e32875366099ef297180d01c7c3842162865e11d92299 -rsaPrivate=bd7a119cf43de5f90141fb30a5582ca58e5ec2bdd560780a522c2e4fb8f4478f790978db0c3a6d36f28d31a2ff7e89c384b46ed8c740c182b1719d53a86c2086f376d1c213785fd35c2aac5648195d10681d00a8c801dcebc1c7645daad5824c95430324a71228bb43be1bb7df6ac6ca8587f0848cf765fb850f40486b5475ed -prefetchKeys=104,79328,55571,46770,24563,299978,44375,0,4177,2822,102323,618372,170616,332545,388299,705815,18893,22788,18115,1269,6254,532,119,756180,821696,3673,2908 \ No newline at end of file diff --git a/game/src/main/kotlin/world/gregs/voidps/GameTick.kt b/game/src/main/kotlin/world/gregs/voidps/GameTick.kt index 1a0752b7f1..bd5fedf512 100644 --- a/game/src/main/kotlin/world/gregs/voidps/GameTick.kt +++ b/game/src/main/kotlin/world/gregs/voidps/GameTick.kt @@ -1,5 +1,6 @@ package world.gregs.voidps +import world.gregs.voidps.engine.client.ConnectionQueue import world.gregs.voidps.engine.client.instruction.InstructionTask import world.gregs.voidps.engine.client.instruction.InterfaceHandler import world.gregs.voidps.engine.client.update.CharacterUpdateTask @@ -27,6 +28,7 @@ import world.gregs.voidps.engine.entity.character.player.Players import world.gregs.voidps.engine.entity.item.floor.FloorItemTracking import world.gregs.voidps.engine.entity.item.floor.FloorItems import world.gregs.voidps.engine.entity.obj.GameObjects +import world.gregs.voidps.engine.get import world.gregs.voidps.network.NetworkQueue import world.gregs.voidps.network.visual.NPCVisuals import world.gregs.voidps.network.visual.PlayerVisuals @@ -42,21 +44,21 @@ import world.gregs.voidps.network.visual.encode.npc.* import world.gregs.voidps.network.visual.encode.player.* fun getTickStages( - players: Players, - npcs: NPCs, - items: FloorItems, - floorItems: FloorItemTracking, - objects: GameObjects, - queue: NetworkQueue, - factory: PlayerAccounts, - batches: ZoneBatchUpdates, - itemDefinitions: ItemDefinitions, - objectDefinitions: ObjectDefinitions, - npcDefinitions: NPCDefinitions, - interfaceDefinitions: InterfaceDefinitions, - hunting: Hunting, - handler: InterfaceHandler, - parallelPlayer: TaskIterator + players: Players = get(), + npcs: NPCs = get(), + items: FloorItems = get(), + floorItems: FloorItemTracking = get(), + objects: GameObjects = get(), + queue: NetworkQueue = get(), + factory: PlayerAccounts = get(), + batches: ZoneBatchUpdates = get(), + itemDefinitions: ItemDefinitions = get(), + objectDefinitions: ObjectDefinitions = get(), + npcDefinitions: NPCDefinitions = get(), + interfaceDefinitions: InterfaceDefinitions = get(), + hunting: Hunting = get(), + handler: InterfaceHandler = InterfaceHandler(get(), get(), get()), + iterator: TaskIterator ): List { val sequentialNpc: TaskIterator = SequentialIterator() val sequentialPlayer: TaskIterator = SequentialIterator() @@ -77,7 +79,7 @@ fun getTickStages( // Update batches, CharacterUpdateTask( - parallelPlayer, + iterator, players, PlayerUpdateTask(players, playerVisualEncoders()), NPCUpdateTask(npcs, npcVisualEncoders()), diff --git a/game/src/main/kotlin/world/gregs/voidps/Main.kt b/game/src/main/kotlin/world/gregs/voidps/Main.kt index 6f2eb000f7..a482da118b 100644 --- a/game/src/main/kotlin/world/gregs/voidps/Main.kt +++ b/game/src/main/kotlin/world/gregs/voidps/Main.kt @@ -3,14 +3,11 @@ package world.gregs.voidps import com.github.michaelbull.logging.InlineLogger import org.koin.core.context.startKoin import org.koin.core.logger.Level -import org.koin.core.module.Module import org.koin.dsl.module import org.koin.fileProperties import org.koin.logger.slf4jLogger import world.gregs.voidps.cache.Cache -import world.gregs.voidps.cache.FileCache import world.gregs.voidps.cache.Index -import world.gregs.voidps.cache.MemoryCache import world.gregs.voidps.cache.config.decoder.InventoryDecoder import world.gregs.voidps.cache.config.decoder.StructDecoder import world.gregs.voidps.cache.definition.decoder.* @@ -19,22 +16,18 @@ import world.gregs.voidps.engine.* import world.gregs.voidps.engine.client.ConnectionGatekeeper import world.gregs.voidps.engine.client.ConnectionQueue import world.gregs.voidps.engine.client.PlayerAccountLoader -import world.gregs.voidps.engine.client.instruction.InterfaceHandler import world.gregs.voidps.engine.client.update.CharacterTask import world.gregs.voidps.engine.client.update.iterator.ParallelIterator import world.gregs.voidps.engine.client.update.iterator.SequentialIterator -import world.gregs.voidps.engine.data.PlayerAccounts import world.gregs.voidps.engine.data.definition.* import world.gregs.voidps.engine.entity.World -import world.gregs.voidps.engine.entity.character.npc.NPCs -import world.gregs.voidps.engine.entity.character.player.Players -import world.gregs.voidps.engine.entity.item.floor.FloorItems import world.gregs.voidps.engine.map.collision.CollisionDecoder -import world.gregs.voidps.network.Network +import world.gregs.voidps.network.GameServer +import world.gregs.voidps.network.LoginServer import world.gregs.voidps.network.protocol import world.gregs.voidps.script.loadScripts import java.io.File -import java.math.BigInteger +import java.util.* /** * @author GregHib @@ -44,66 +37,38 @@ object Main { lateinit var name: String private val logger = InlineLogger() - private const val USE_MEMORY_CACHE = false @OptIn(ExperimentalUnsignedTypes::class) @JvmStatic fun main(args: Array) { val startTime = System.currentTimeMillis() - val module = cache((if (USE_MEMORY_CACHE) MemoryCache else FileCache).load("./data/cache/")) - logger.info { "Cache loaded in ${System.currentTimeMillis() - startTime}ms" } - preload(module) - name = getProperty("name") - val revision = getProperty("revision").toInt() - val limit = getProperty("loginLimit").toInt() - val modulus = BigInteger(getProperty("rsaModulus"), 16) - val private = BigInteger(getProperty("rsaPrivate"), 16) + val properties = properties("/game.properties") + name = properties.getProperty("name") - val huffman: Huffman = get() - val players: Players = get() - val accounts: PlayerAccounts = get() - val queue: ConnectionQueue = get() - val gatekeeper: ConnectionGatekeeper = get() + val cache = timed("cache") { Cache.load(properties) } + preload(cache, properties) - val accountLoader = PlayerAccountLoader(queue, accounts, Contexts.Game) - val protocol = protocol(huffman) - val server = Network(revision, modulus, private, gatekeeper, accountLoader, limit, Contexts.Game, protocol) + val accountLoader = PlayerAccountLoader(get(), get(), Contexts.Game) + val protocol = protocol(get()) - val interfaceDefinitions: InterfaceDefinitions = get() - val npcs: NPCs = get() - val items: FloorItems = get() - val objectDefinitions: ObjectDefinitions = get() + val gatekeeper: ConnectionGatekeeper = get() + val loginServer = LoginServer.load(properties, protocol, gatekeeper, accountLoader, Contexts.Game) + val server = GameServer.load(cache, properties, gatekeeper, loginServer) - val handler = InterfaceHandler(get(), interfaceDefinitions, get()) - val tickStages = getTickStages( - players, - npcs, - items, - get(), - get(), - queue, - get(), - get(), - get(), - objectDefinitions, - get(), - interfaceDefinitions, - get(), - handler, - if (CharacterTask.DEBUG) SequentialIterator() else ParallelIterator()) + val tickStages = getTickStages(iterator = if (CharacterTask.DEBUG) SequentialIterator() else ParallelIterator()) val engine = GameLoop(tickStages) - - World.start(getProperty("members") == "true") + World.start(properties) engine.start() - logger.info { "${getProperty("name")} loaded in ${System.currentTimeMillis() - startTime}ms" } + + logger.info { "$name loaded in ${System.currentTimeMillis() - startTime}ms" } server.start(getIntProperty("port")) } - private fun preload(module: Module) { + private fun preload(cache: Cache, properties: Properties) { + val module = cache(cache, properties) startKoin { slf4jLogger(level = Level.ERROR) fileProperties("/game.properties") - fileProperties("/private.properties") modules(engineModule, gameModule, module) } val saves = File(getProperty("savePath")) @@ -113,11 +78,12 @@ object Main { loadScripts(getProperty("scriptModule")) } - private fun cache(cache: Cache) = module { + private fun cache(cache: Cache, properties: Properties) = module { + val members = properties.getProperty("members").toBoolean() single(createdAtStart = true) { MapDefinitions(CollisionDecoder(get()), get(), get(), cache).loadCache() } single(createdAtStart = true) { Huffman().load(cache.data(Index.HUFFMAN, 1)!!) } - single(createdAtStart = true) { ObjectDefinitions(ObjectDecoder(member = getProperty("members") == "true", lowDetail = false, get()).load(cache)).load() } - single(createdAtStart = true) { NPCDefinitions(NPCDecoder(member = getProperty("members") == "true", get()).load(cache)).load() } + single(createdAtStart = true) { ObjectDefinitions(ObjectDecoder(members, lowDetail = false, get()).load(cache)).load() } + single(createdAtStart = true) { NPCDefinitions(NPCDecoder(members, get()).load(cache)).load() } single(createdAtStart = true) { ItemDefinitions(ItemDecoder(get()).load(cache)).load() } single(createdAtStart = true) { AnimationDefinitions(AnimationDecoder().load(cache)).load() } single(createdAtStart = true) { EnumDefinitions(EnumDecoder().load(cache), get()).load() } diff --git a/game/src/main/kotlin/world/gregs/voidps/Properties.kt b/game/src/main/kotlin/world/gregs/voidps/Properties.kt new file mode 100644 index 0000000000..8a8d866693 --- /dev/null +++ b/game/src/main/kotlin/world/gregs/voidps/Properties.kt @@ -0,0 +1,10 @@ +package world.gregs.voidps + +import world.gregs.voidps.engine.timed +import java.util.* + +fun properties(path: String) : Properties = timed("properties") { + val properties = Properties() + properties.load(Main::class.java.getResourceAsStream(path)) + return@timed properties +} \ No newline at end of file diff --git a/game/src/main/kotlin/world/gregs/voidps/world/command/admin/BotCommands.kts b/game/src/main/kotlin/world/gregs/voidps/world/command/admin/BotCommands.kts index db9a02c36c..c12c492308 100644 --- a/game/src/main/kotlin/world/gregs/voidps/world/command/admin/BotCommands.kts +++ b/game/src/main/kotlin/world/gregs/voidps/world/command/admin/BotCommands.kts @@ -28,7 +28,7 @@ import world.gregs.voidps.engine.inject import world.gregs.voidps.engine.inv.add import world.gregs.voidps.engine.inv.inventory import world.gregs.voidps.engine.suspend.pause -import world.gregs.voidps.network.DummyClient +import world.gregs.voidps.network.client.DummyClient import world.gregs.voidps.network.visual.update.player.BodyColour import world.gregs.voidps.network.visual.update.player.BodyPart import world.gregs.voidps.type.area.Rectangle diff --git a/game/src/main/kotlin/world/gregs/voidps/world/community/trade/TradeConfirm.kts b/game/src/main/kotlin/world/gregs/voidps/world/community/trade/TradeConfirm.kts index 1f41072a01..d8ce82577d 100644 --- a/game/src/main/kotlin/world/gregs/voidps/world/community/trade/TradeConfirm.kts +++ b/game/src/main/kotlin/world/gregs/voidps/world/community/trade/TradeConfirm.kts @@ -8,6 +8,7 @@ import world.gregs.voidps.engine.client.ui.chat.toDigitGroupString import world.gregs.voidps.engine.client.ui.chat.toTag import world.gregs.voidps.engine.client.ui.closeMenu import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.player.chat.ChatType import world.gregs.voidps.engine.entity.character.player.req.request import world.gregs.voidps.engine.entity.item.Item import world.gregs.voidps.engine.event.on @@ -70,6 +71,8 @@ on({ id == "trade_confirm" && component == "accept" && option = requester.closeMenu() return@request } + acceptor.message("Accepted trade.", ChatType.Trade) + requester.message("Accepted trade.", ChatType.Trade) loanItem(requester, acceptorLoan, acceptor) loanItem(acceptor, requesterLoan, requester) requester.closeMenu() diff --git a/game/src/main/kotlin/world/gregs/voidps/world/community/trade/TradeDecline.kts b/game/src/main/kotlin/world/gregs/voidps/world/community/trade/TradeDecline.kts index 4e7ed62d55..4871db1be3 100644 --- a/game/src/main/kotlin/world/gregs/voidps/world/community/trade/TradeDecline.kts +++ b/game/src/main/kotlin/world/gregs/voidps/world/community/trade/TradeDecline.kts @@ -1,9 +1,14 @@ package world.gregs.voidps.world.community.trade +import world.gregs.voidps.engine.client.message import world.gregs.voidps.engine.client.ui.InterfaceOption import world.gregs.voidps.engine.client.ui.closeMenu +import world.gregs.voidps.engine.client.ui.menu +import world.gregs.voidps.engine.entity.Unregistered import world.gregs.voidps.engine.entity.character.player.Player +import world.gregs.voidps.engine.entity.character.player.chat.ChatType import world.gregs.voidps.engine.event.on +import world.gregs.voidps.world.community.trade.Trade.getPartner import world.gregs.voidps.world.community.trade.Trade.isTradeInterface /** @@ -15,5 +20,14 @@ fun isDecline(component: String, option: String) = component == "decline" && opt fun isClose(component: String, option: String) = component == "close" && option == "Close" on({ isTradeInterface(id) && (isDecline(component, option) || isClose(component, option)) }) { player: Player -> + val other = getPartner(player) + player.message("Declined trade.", ChatType.Trade) player.closeMenu() + other?.message("Other player declined trade.", ChatType.Trade) +} + +on({ isTradeInterface(it.menu) }) { player: Player -> + val other = getPartner(player) + player.closeMenu() + other?.message("Other player declined trade.", ChatType.Trade) } \ No newline at end of file diff --git a/game/src/main/kotlin/world/gregs/voidps/world/interact/dialogue/type/Statement.kt b/game/src/main/kotlin/world/gregs/voidps/world/interact/dialogue/type/Statement.kt index 934b73f8ad..759f4b5fb4 100644 --- a/game/src/main/kotlin/world/gregs/voidps/world/interact/dialogue/type/Statement.kt +++ b/game/src/main/kotlin/world/gregs/voidps/world/interact/dialogue/type/Statement.kt @@ -11,7 +11,7 @@ import world.gregs.voidps.world.interact.dialogue.sendLines private const val MAXIMUM_STATEMENT_SIZE = 5 suspend fun CharacterContext.statement(text: String, clickToContinue: Boolean = true) { - val lines = if (text.contains("\n")) text.trimIndent().lines() else get().get("q8_full").splitLines(text, 380) + val lines = if (text.contains("\n")) text.trimIndent().lines() else get().get("q8_full").splitLines(text, 470) check(lines.size <= MAXIMUM_STATEMENT_SIZE) { "Maximum statement lines exceeded ${lines.size} for $player" } val id = getInterfaceId(lines.size, clickToContinue) check(player.open(id)) { "Unable to open statement dialogue $id for $player" } diff --git a/game/src/main/kotlin/world/gregs/voidps/world/interact/entity/item/ItemPickup.kts b/game/src/main/kotlin/world/gregs/voidps/world/interact/entity/item/ItemPickup.kts index b5f0f9c45d..b13efd921a 100644 --- a/game/src/main/kotlin/world/gregs/voidps/world/interact/entity/item/ItemPickup.kts +++ b/game/src/main/kotlin/world/gregs/voidps/world/interact/entity/item/ItemPickup.kts @@ -1,6 +1,7 @@ package world.gregs.voidps.world.interact.entity.item import com.github.michaelbull.logging.InlineLogger +import world.gregs.voidps.engine.client.message import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.chat.inventoryFull @@ -28,6 +29,7 @@ on({ operate && option == "Take" }) { player: Player -> return@on } if (!floorItems.remove(target)) { + player.message("Too late - it's gone!") return@on } player.inventory.add(target.id, target.amount) diff --git a/game/src/main/resources/game.properties b/game/src/main/resources/game.properties index 09c4eea629..f09a8a6229 100644 --- a/game/src/main/resources/game.properties +++ b/game/src/main/resources/game.properties @@ -1,19 +1,39 @@ # Game Settings name=Void -port=50015 +port=43594 +revision=634 +# World +live=false +world=1 +members=true +homeX=3221 +homeY=3219 +loginLimit=10 +randomWalk=true +characterCollision=true +loadUnusedObjects=true +connectionPerTickCap=25 +# Files +fileServer=true cachePath=./data/cache/ savePath=./data/saves/ -interfacesPath=./data/definitions/interfaces.yml -interfaceTypesPath=./data/definitions/interface-types.yml +scriptModule=.\\game\\src\\main\\kotlin\\ +## Spawns objectsPath=./data/spawns/object-spawns.yml +npcSpawnsPath=./data/spawns/npc-spawns.yml +itemSpawnsPath=./data/spawns/item-spawns.yml +dropsPath=./data/spawns/drops.yml +## Map +areaPath=./data/map/areas.yml teleportsPath=./data/map/teleports.yml musicPath=./data/map/music-tracks.yml navGraphPath=./data/map/nav-graph.yml -areaPath=./data/map/areas.yml +## Definitions definitionsPath=./data/definitions/ +bookPath=./data/definitions/books.yml huntPath=./data/definitions/hunt-modes.yml -npcSpawnsPath=./data/spawns/npc-spawns.yml -itemSpawnsPath=./data/spawns/item-spawns.yml +interfacesPath=./data/definitions/interfaces.yml +interfaceTypesPath=./data/definitions/interface-types.yml objectDefinitionsPath=./data/definitions/objects.yml npcDefinitionsPath=./data/definitions/npcs.yml itemDefinitionsPath=./data/definitions/items.yml @@ -33,15 +53,10 @@ weaponStyleDefinitionsPath=./data/definitions/weapon-styles.yml ammoDefinitionsPath=./data/definitions/ammo-groups.yml categoryDefinitionsPath=./data/definitions/categories.yml parameterDefinitionsPath=./data/definitions/parameters.yml -dropsPath=./data/spawns/drops.yml -bookPath=./data/definitions/books.yml -revision=634 -loginLimit=5 -scriptModule=.\\game\\src\\main\\kotlin\\ -homeX=3221 -homeY=3219 -connectionPerTickCap=25 -randomWalk=true -members=true -characterCollision=true -loadUnusedObjects=true \ No newline at end of file +# RSA Keys +gameModulus=ea3680fdebf2621da7a33601ba39925ee203b3fc80775cd3727bf27fd8c0791c803e0bdb42b8b5257567177f8569024569da9147cef59009ed016af6007e57a556f1754f09ca84dd39a03287f7e41e8626fd78ab3b53262bd63f2e37403a549980bf3077bd402b82ef5fac269eb3c04d2a9b7712a67a018321ceba6c3bfb8f7f +gamePrivate=8330565e649c16d32f841f0b26a97ad044def821164045b176adf0ae25d5e1c0d2206ef9b8ccc7429d194ab33622149096f3436f2a80a7d6b77794d7087dbc4f21239a4012b18afa3d1bede29d63f33bc553885f7117aa5d842231fae613d6e612c651249e66b7c67d565b21e68202798ccdbd0cc6dea3f6d033e719cb75ea01 +fileModulus=d6808be939bbfd2ec4e96b1581ce3e1144b526e7643a72e3c64fbb902724fbfcf14ab601da6d6f8dbb57d1c369d080d9fc392abeb7886e0076d07f2aea5810e540d2817fd1967e35b39cc95cf7c9170b5fb55f5bf95524b60e938f0d64614bc365b87d66963a8cc8664e32875366099ef297180d01c7c3842162865e11d92299 +filePrivate=bd7a119cf43de5f90141fb30a5582ca58e5ec2bdd560780a522c2e4fb8f4478f790978db0c3a6d36f28d31a2ff7e89c384b46ed8c740c182b1719d53a86c2086f376d1c213785fd35c2aac5648195d10681d00a8c801dcebc1c7645daad5824c95430324a71228bb43be1bb7df6ac6ca8587f0848cf765fb850f40486b5475ed +# File server prefetch file list +prefetchKeys=104,79328,55571,46770,24563,299978,44375,0,4177,2822,99949,618327,155210,332223,383345,682590,18893,19068,16187,1248,6254,526,119,741324,821696,3671,2908 \ No newline at end of file diff --git a/game/src/main/resources/private.properties b/game/src/main/resources/private.properties deleted file mode 100644 index f51b8be49b..0000000000 --- a/game/src/main/resources/private.properties +++ /dev/null @@ -1,3 +0,0 @@ -# RSA Keys -rsaModulus=ea3680fdebf2621da7a33601ba39925ee203b3fc80775cd3727bf27fd8c0791c803e0bdb42b8b5257567177f8569024569da9147cef59009ed016af6007e57a556f1754f09ca84dd39a03287f7e41e8626fd78ab3b53262bd63f2e37403a549980bf3077bd402b82ef5fac269eb3c04d2a9b7712a67a018321ceba6c3bfb8f7f -rsaPrivate=8330565e649c16d32f841f0b26a97ad044def821164045b176adf0ae25d5e1c0d2206ef9b8ccc7429d194ab33622149096f3436f2a80a7d6b77794d7087dbc4f21239a4012b18afa3d1bede29d63f33bc553885f7117aa5d842231fae613d6e612c651249e66b7c67d565b21e68202798ccdbd0cc6dea3f6d033e719cb75ea01 \ No newline at end of file diff --git a/game/src/test/kotlin/world/gregs/voidps/world/community/friend/PrivateChatStatusTest.kt b/game/src/test/kotlin/world/gregs/voidps/world/community/friend/PrivateChatStatusTest.kt index e197e50745..e99e3df594 100644 --- a/game/src/test/kotlin/world/gregs/voidps/world/community/friend/PrivateChatStatusTest.kt +++ b/game/src/test/kotlin/world/gregs/voidps/world/community/friend/PrivateChatStatusTest.kt @@ -11,7 +11,7 @@ import world.gregs.voidps.engine.entity.Registered import world.gregs.voidps.engine.entity.World import world.gregs.voidps.engine.entity.character.player.Player import world.gregs.voidps.engine.entity.character.player.chat.clan.ClanRank -import world.gregs.voidps.network.Client +import world.gregs.voidps.network.client.Client import world.gregs.voidps.network.encode.Friend import world.gregs.voidps.network.encode.sendFriendsList import world.gregs.voidps.world.script.WorldTest diff --git a/game/src/test/kotlin/world/gregs/voidps/world/interact/dialogue/NPCChatTest.kt b/game/src/test/kotlin/world/gregs/voidps/world/interact/dialogue/NPCChatTest.kt index 73ea962a8e..90964dae7d 100644 --- a/game/src/test/kotlin/world/gregs/voidps/world/interact/dialogue/NPCChatTest.kt +++ b/game/src/test/kotlin/world/gregs/voidps/world/interact/dialogue/NPCChatTest.kt @@ -18,7 +18,7 @@ import world.gregs.voidps.engine.data.definition.AnimationDefinitions import world.gregs.voidps.engine.data.definition.NPCDefinitions import world.gregs.voidps.engine.entity.character.npc.NPC import world.gregs.voidps.engine.suspend.dialogue.ContinueSuspension -import world.gregs.voidps.network.Client +import world.gregs.voidps.network.client.Client import world.gregs.voidps.network.encode.npcDialogueHead import world.gregs.voidps.world.interact.dialogue.type.npc import kotlin.test.assertTrue diff --git a/game/src/test/kotlin/world/gregs/voidps/world/interact/dialogue/PlayerChatTest.kt b/game/src/test/kotlin/world/gregs/voidps/world/interact/dialogue/PlayerChatTest.kt index 2a03ffabef..9ee2bd2b49 100644 --- a/game/src/test/kotlin/world/gregs/voidps/world/interact/dialogue/PlayerChatTest.kt +++ b/game/src/test/kotlin/world/gregs/voidps/world/interact/dialogue/PlayerChatTest.kt @@ -16,7 +16,7 @@ import world.gregs.voidps.engine.Contexts import world.gregs.voidps.engine.client.ui.open import world.gregs.voidps.engine.data.definition.AnimationDefinitions import world.gregs.voidps.engine.suspend.dialogue.ContinueSuspension -import world.gregs.voidps.network.Client +import world.gregs.voidps.network.client.Client import world.gregs.voidps.network.encode.playerDialogueHead import world.gregs.voidps.world.interact.dialogue.type.player import kotlin.test.assertTrue diff --git a/game/src/test/kotlin/world/gregs/voidps/world/interact/dialogue/StatementTest.kt b/game/src/test/kotlin/world/gregs/voidps/world/interact/dialogue/StatementTest.kt index 5745d10fbd..54ab691ae3 100644 --- a/game/src/test/kotlin/world/gregs/voidps/world/interact/dialogue/StatementTest.kt +++ b/game/src/test/kotlin/world/gregs/voidps/world/interact/dialogue/StatementTest.kt @@ -14,7 +14,6 @@ import kotlin.test.assertTrue internal class StatementTest : DialogueTest() { - @TestFactory fun `Send statement lines`() = arrayOf( "One line" to "dialogue_message1", @@ -41,15 +40,15 @@ internal class StatementTest : DialogueTest() { @Test fun `Long line wraps statement`() { - val text = "This is one very long statement dialogue text line which should be wrapped into at least three lines ideally more than even that." + val text = "This is one very long statement dialogue text paragraph which should be wrapped into at least three lines but ideally even more than three, maybe four." dialogue { statement(text = text, clickToContinue = true) } verify { player.open("dialogue_message3") - interfaces.sendText("dialogue_message3", "line1", "This is one very long statement dialogue text line which") - interfaces.sendText("dialogue_message3", "line2", "should be wrapped into at least three lines ideally more") - interfaces.sendText("dialogue_message3", "line3", "than even that.") + interfaces.sendText("dialogue_message3", "line1", "This is one very long statement dialogue text paragraph which should") + interfaces.sendText("dialogue_message3", "line2", "be wrapped into at least three lines but ideally even more than three,") + interfaces.sendText("dialogue_message3", "line3", "maybe four.") } } diff --git a/game/src/test/kotlin/world/gregs/voidps/world/script/WorldTest.kt b/game/src/test/kotlin/world/gregs/voidps/world/script/WorldTest.kt index df7a13c352..7316c43806 100644 --- a/game/src/test/kotlin/world/gregs/voidps/world/script/WorldTest.kt +++ b/game/src/test/kotlin/world/gregs/voidps/world/script/WorldTest.kt @@ -45,8 +45,8 @@ import world.gregs.voidps.engine.map.collision.Collisions import world.gregs.voidps.engine.map.collision.GameObjectCollision import world.gregs.voidps.gameModule import world.gregs.voidps.getTickStages -import world.gregs.voidps.network.Client import world.gregs.voidps.network.NetworkGatekeeper +import world.gregs.voidps.network.client.Client import world.gregs.voidps.script.loadScripts import world.gregs.voidps.type.Tile import world.gregs.voidps.type.setRandom @@ -186,7 +186,7 @@ abstract class WorldTest : KoinTest { get(), get(), handler, - parallelPlayer = SequentialIterator()) + iterator = SequentialIterator()) engine = GameLoop(tickStages, mockk(relaxed = true)) store.populate(World) World.start(true) diff --git a/network/src/main/kotlin/world/gregs/voidps/network/AccountLoader.kt b/network/src/main/kotlin/world/gregs/voidps/network/AccountLoader.kt index cb69abdca7..e8c4ba9392 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/AccountLoader.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/AccountLoader.kt @@ -1,6 +1,7 @@ package world.gregs.voidps.network import kotlinx.coroutines.flow.MutableSharedFlow +import world.gregs.voidps.network.client.Client /** * Loads and setups account from data on file diff --git a/network/src/main/kotlin/world/gregs/voidps/network/FileServer.kt b/network/src/main/kotlin/world/gregs/voidps/network/FileServer.kt new file mode 100644 index 0000000000..c7ca5dbb56 --- /dev/null +++ b/network/src/main/kotlin/world/gregs/voidps/network/FileServer.kt @@ -0,0 +1,126 @@ +package world.gregs.voidps.network + +import com.github.michaelbull.logging.InlineLogger +import io.ktor.utils.io.* +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.isActive +import world.gregs.voidps.cache.Cache +import world.gregs.voidps.network.file.FileProvider +import world.gregs.voidps.network.file.prefetchKeys +import java.util.* + +/** + * Serves the client with the sector of cache files it has requested + */ +class FileServer( + private val revision: Int, + private val prefetchKeys: IntArray, + private val provider: FileProvider +) : Server { + + val logger = InlineLogger() + + override suspend fun connect(read: ByteReadChannel, write: ByteWriteChannel, hostname: String) { + synchronise(read, write) + if (acknowledge(read, write)) { + logger.trace { "Client synchronisation complete: $hostname" } + readRequests(read, write, hostname) + } + } + + /** + * Confirm the client got our message and is ready to start sending file requests + */ + private suspend fun acknowledge(read: ByteReadChannel, write: ByteWriteChannel): Boolean { + val opcode = read.readByte().toInt() + if (opcode != Request.ACKNOWLEDGE) { + logger.trace { "Invalid ack opcode: $opcode" } + write.writeByte(Response.LOGIN_SERVER_REJECTED_SESSION) + write.close() + return false + } + + val id = read.readMedium() + if (id != ACKNOWLEDGE_ID) { + logger.trace { "Invalid session id expected: $ACKNOWLEDGE_ID actual: $id" } + write.writeByte(Response.BAD_SESSION_ID) + write.close() + return false + } + return true + } + + /** + * If the client is up-to-date and in the correct state send it the [prefetchKeys] list so it knows what indices are available to request + */ + private suspend fun synchronise(read: ByteReadChannel, write: ByteWriteChannel) { + val revision = read.readInt() + if (revision != this.revision) { + logger.trace { "Invalid game revision: $revision" } + write.writeByte(Response.GAME_UPDATE) + write.close() + return + } + + write.writeByte(Response.DATA_CHANGE) + for (key in prefetchKeys) { + write.writeInt(key) + } + write.flush() + } + + private suspend fun readRequests(read: ByteReadChannel, write: ByteWriteChannel, hostname: String) = coroutineScope { + try { + while (isActive) { + when (val opcode = read.readByte().toInt()) { + Request.STATUS_LOGGED_OUT, Request.STATUS_LOGGED_IN -> verify(read, write, Request.PREFETCH_REQUEST) + Request.PRIORITY_REQUEST, Request.PREFETCH_REQUEST -> { + val value = read.readUMedium() + provider.serve(write, value, opcode == Request.PREFETCH_REQUEST) + } + Request.ENCRYPTION_KEY_UPDATE -> read.readUByte() + else -> { + logger.warn { "Unknown file-server request $opcode." } + write.close() + } + } + } + } finally { + logger.trace { "Client disconnected: ${hostname}." } + } + } + + + + /** + * Confirm a session value send by the client is as the server [expected] + */ + suspend fun verify(read: ByteReadChannel, write: ByteWriteChannel, expected: Int): Boolean { + val id = read.readMedium() + if (id != expected) { + logger.trace { "Invalid session id expected: $expected actual: $id" } + write.writeByte(Response.BAD_SESSION_ID) + write.close() + return false + } + return true + } + + companion object { + private const val ACKNOWLEDGE_ID = 3 + + fun load(cache: Cache, properties: Properties): Server { + val fileServer = properties.getProperty("fileServer").toBoolean() + if (!fileServer) { + return object : Server { + override suspend fun connect(read: ByteReadChannel, write: ByteWriteChannel, hostname: String) { + } + } + } + val fileProvider: FileProvider = FileProvider.load(cache, properties) + val revision = properties.getProperty("revision").toInt() + val prefetchKeys = prefetchKeys(cache, properties) + return FileServer(revision, prefetchKeys, fileProvider) + } + } +} \ No newline at end of file diff --git a/network/src/main/kotlin/world/gregs/voidps/network/GameServer.kt b/network/src/main/kotlin/world/gregs/voidps/network/GameServer.kt new file mode 100644 index 0000000000..625f7262e1 --- /dev/null +++ b/network/src/main/kotlin/world/gregs/voidps/network/GameServer.kt @@ -0,0 +1,93 @@ +package world.gregs.voidps.network + +import com.github.michaelbull.logging.InlineLogger +import io.ktor.network.selector.* +import io.ktor.network.sockets.* +import io.ktor.util.network.* +import io.ktor.utils.io.* +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.ClosedReceiveChannelException +import world.gregs.voidps.cache.Cache +import world.gregs.voidps.network.client.Client +import java.net.SocketException +import java.util.* +import java.util.concurrent.Executors +import kotlin.concurrent.thread + +/** + * A network server for client's to connect to the game with + */ +class GameServer( + private val gatekeeper: NetworkGatekeeper, + private val loginLimit: Int, + private val loginServer: Server, + private val fileServer: Server +) { + + private lateinit var dispatcher: ExecutorCoroutineDispatcher + private var running = false + + fun start(port: Int) = runBlocking { + Runtime.getRuntime().addShutdownHook(thread(start = false) { stop() }) + val executor = Executors.newCachedThreadPool() + dispatcher = executor.asCoroutineDispatcher() + val selector = ActorSelectorManager(dispatcher) + val supervisor = SupervisorJob() + val exceptionHandler = CoroutineExceptionHandler { context, throwable -> + if (throwable is SocketException && throwable.message == "Connection reset") { + logger.trace { "Connection reset: ${context.job}" } + } else if (throwable is ClosedReceiveChannelException && throwable.message == "EOF while 1 bytes expected") { + logger.trace { "EOF disconnection: ${context.job}" } + } else { + logger.error(throwable) { "Connection error" } + } + } + val scope = CoroutineScope(coroutineContext + supervisor + exceptionHandler) + with(scope) { + val server = aSocket(selector).tcp().bind(port = port) + running = true + logger.info { "Listening for requests on port ${port}..." } + while (running) { + val socket = server.accept() + logger.trace { "New connection accepted ${socket.remoteAddress}" } + val read = socket.openReadChannel() + val write = socket.openWriteChannel(autoFlush = false) + launch(Client.context) { + connect(read, write, socket.remoteAddress.toJavaAddress().hostname) + } + } + } + } + + suspend fun connect(read: ByteReadChannel, write: ByteWriteChannel, hostname: String) { + if (gatekeeper.connections(hostname) >= loginLimit) { + write.finish(Response.LOGIN_LIMIT_EXCEEDED) + return + } + when (val opcode = read.readByte().toInt()) { + Request.CONNECT_LOGIN -> loginServer.connect(read, write, hostname) + Request.CONNECT_JS5 -> fileServer.connect(read, write, hostname) + else -> { + logger.trace { "Invalid sync session id: $opcode" } + write.finish(Response.INVALID_LOGIN_SERVER) + } + } + } + + fun stop() { + running = false + dispatcher.close() + } + + companion object { + + @ExperimentalUnsignedTypes + fun load(cache: Cache, properties: Properties, gatekeeper: NetworkGatekeeper, loginServer: LoginServer): GameServer { + val limit = properties.getProperty("loginLimit").toInt() + val fileServer = FileServer.load(cache, properties) + return GameServer(gatekeeper, limit, loginServer, fileServer) + } + + private val logger = InlineLogger() + } +} \ No newline at end of file diff --git a/network/src/main/kotlin/world/gregs/voidps/network/JagExtensions.kt b/network/src/main/kotlin/world/gregs/voidps/network/JagExtensions.kt index fa8637c10f..948259cc98 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/JagExtensions.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/JagExtensions.kt @@ -6,6 +6,18 @@ import world.gregs.voidps.buffer.write.BufferWriter import kotlin.random.Random import kotlin.text.toByteArray +suspend fun ByteReadChannel.readUByte(): Int = readByte().toInt() and 0xff + +suspend fun ByteReadChannel.readUShort(): Int = (readUByte() shl 8) or readUByte() + +suspend fun ByteReadChannel.readMedium(): Int { + return (readByte().toInt() shl 16) + (readByte().toInt() shl 8) + readByte().toInt() +} + +suspend fun ByteReadChannel.readUMedium(): Int { + return (readUByte() shl 16) + (readUByte() shl 8) + readUByte() +} + suspend fun ByteWriteChannel.writeByte(value: Boolean) = writeByte(if (value) 1 else 0) suspend fun ByteWriteChannel.writeByteAdd(value: Boolean) = writeByteAdd(if (value) 1 else 0) @@ -122,6 +134,16 @@ suspend fun ByteWriteChannel.bitAccess(block: BitAccessor.() -> Unit) { accessor.write(this) } +suspend fun ByteWriteChannel.respond(value: Int) { + writeByte(value) + flush() +} + +suspend fun ByteWriteChannel.finish(value: Int) { + respond(value) + close() +} + fun ByteReadPacket.readString(): String { val sb = StringBuilder() var b: Int diff --git a/network/src/main/kotlin/world/gregs/voidps/network/Network.kt b/network/src/main/kotlin/world/gregs/voidps/network/LoginServer.kt similarity index 58% rename from network/src/main/kotlin/world/gregs/voidps/network/Network.kt rename to network/src/main/kotlin/world/gregs/voidps/network/LoginServer.kt index efd469b0a2..bec350ccc9 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/Network.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/LoginServer.kt @@ -1,82 +1,36 @@ package world.gregs.voidps.network import com.github.michaelbull.logging.InlineLogger -import io.ktor.network.selector.* -import io.ktor.network.sockets.* -import io.ktor.util.network.* import io.ktor.utils.io.* import io.ktor.utils.io.core.* -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableSharedFlow import world.gregs.voidps.cache.secure.RSA import world.gregs.voidps.cache.secure.Xtea -import world.gregs.voidps.network.Decoder.Companion.BYTE -import world.gregs.voidps.network.Decoder.Companion.SHORT +import world.gregs.voidps.network.client.Client +import world.gregs.voidps.network.client.ClientState +import world.gregs.voidps.network.client.IsaacCipher import java.math.BigInteger -import java.util.concurrent.Executors +import java.util.* +/** + * Connects a client to their account in the game world + */ @ExperimentalUnsignedTypes -class Network( +class LoginServer( + private val protocol: Array, private val revision: Int, private val modulus: BigInteger, private val private: BigInteger, private val gatekeeper: NetworkGatekeeper, private val loader: AccountLoader, - private val loginLimit: Int, - private val disconnectContext: CoroutineDispatcher, - private val protocol: Map -) { + private val disconnectContext: CoroutineDispatcher +) : Server { - private lateinit var dispatcher: ExecutorCoroutineDispatcher - private var running = false - - fun start(port: Int) = runBlocking { - val executor = Executors.newCachedThreadPool() - dispatcher = executor.asCoroutineDispatcher() - val selector = ActorSelectorManager(dispatcher) - val supervisor = SupervisorJob() - val exceptionHandler = CoroutineExceptionHandler { _, throwable -> - logger.error(throwable) { "Connection error" } - } - val scope = CoroutineScope(coroutineContext + supervisor + exceptionHandler) - with(scope) { - val server = aSocket(selector).tcp().bind(port = port) - running = true - logger.info { "Listening for requests on port ${port}..." } - while (running) { - val socket = server.accept() - logger.trace { "New connection accepted ${socket.remoteAddress}" } - val read = socket.openReadChannel() - val write = socket.openWriteChannel(autoFlush = false) - launch(Client.context) { - connect(read, write, socket.remoteAddress.toJavaAddress().hostname) - } - } - } - } - - suspend fun connect(read: ByteReadChannel, write: ByteWriteChannel, hostname: String) { - if (gatekeeper.connections(hostname) >= loginLimit) { - write.finish(Response.LOGIN_LIMIT_EXCEEDED) - return - } - synchronise(read, write) - login(read, write, hostname) - } - - private suspend fun synchronise(read: ByteReadChannel, write: ByteWriteChannel) { + override suspend fun connect(read: ByteReadChannel, write: ByteWriteChannel, hostname: String) { + write.respond(Response.DATA_CHANGE) val opcode = read.readByte().toInt() - if (opcode != SYNCHRONISE) { - logger.trace { "Invalid sync session id: $opcode" } - write.finish(Response.LOGIN_SERVER_REJECTED_SESSION) - return - } - write.respond(ACCEPT_SESSION) - } - - private suspend fun login(read: ByteReadChannel, write: ByteWriteChannel, hostname: String) { - val opcode = read.readByte().toInt() - if (opcode != LOGIN && opcode != RECONNECT) { + if (opcode != Request.LOGIN && opcode != Request.RECONNECT) { logger.trace { "Invalid request id: $opcode" } write.finish(Response.LOGIN_SERVER_REJECTED_SESSION) return @@ -89,7 +43,7 @@ class Network( private suspend fun checkClientVersion(read: ByteReadChannel, packet: ByteReadPacket, write: ByteWriteChannel, hostname: String) { val version = packet.readInt() if (version != revision) { - logger.trace { "Invalid revision: $version" } + logger.trace { "Invalid client revision: $version" } write.finish(Response.GAME_UPDATE) return } @@ -106,7 +60,7 @@ class Network( suspend fun validateSession(read: ByteReadChannel, rsa: ByteReadPacket, packet: ByteReadPacket, write: ByteWriteChannel, hostname: String) { val sessionId = rsa.readUByte().toInt() - if (sessionId != SESSION) { + if (sessionId != Request.SESSION) { logger.debug { "Bad session id $sessionId" } write.finish(Response.BAD_SESSION_ID) return @@ -180,43 +134,23 @@ class Network( return } val size = when (decoder.length) { - BYTE -> read.readUByte() - SHORT -> read.readUShort() + Decoder.BYTE -> read.readUByte() + Decoder.SHORT -> read.readUShort() else -> decoder.length } val packet = read.readPacket(size = size) decoder.decode(instructions, packet) } } - - fun shutdown() { - running = false - dispatcher.close() - } - - private suspend fun ByteReadChannel.readUByte(): Int = readByte().toInt() and 0xff - private suspend fun ByteReadChannel.readUShort(): Int = (readUByte() shl 8) or readUByte() - + companion object { - - private suspend fun ByteWriteChannel.respond(value: Int) { - writeByte(value) - flush() - } - - private suspend fun ByteWriteChannel.finish(value: Int) { - respond(value) - close() - } - private val logger = InlineLogger() - private const val SYNCHRONISE = 14 - private const val LOGIN = 16 - private const val RECONNECT = 18 - private const val SESSION = 10 - private const val SIGN_UP = 28 - - private const val ACCEPT_SESSION = 0 + fun load(properties: Properties, protocol: Array, gatekeeper: NetworkGatekeeper, loader: AccountLoader, disconnectContext: CoroutineDispatcher): LoginServer { + val gameModulus = BigInteger(properties.getProperty("gameModulus"), 16) + val gamePrivate = BigInteger(properties.getProperty("gamePrivate"), 16) + val revision = properties.getProperty("revision").toInt() + return LoginServer(protocol, revision, gameModulus, gamePrivate, gatekeeper, loader, disconnectContext) + } } } \ No newline at end of file diff --git a/network/src/main/kotlin/world/gregs/voidps/network/Protocol.kt b/network/src/main/kotlin/world/gregs/voidps/network/Protocol.kt index 8e71ded5d4..47d0ea3afc 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/Protocol.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/Protocol.kt @@ -153,90 +153,92 @@ object Protocol { const val UNKNOWN_64 = 42 // size -1 } -fun protocol(huffman: Huffman): Map = mapOf( - 22 to FloorItemOption1Decoder(), - 16 to FloorItemOption2Decoder(), - 45 to FloorItemOption3Decoder(), - 24 to FloorItemOption4Decoder(), - 26 to FloorItemOption5Decoder(), - 53 to ConsoleCommandDecoder(), - 2 to DialogueContinueDecoder(), - 32 to IntegerEntryDecoder(), - 70 to InterfaceClosedDecoder(), - 72 to InterfaceOnInterfaceDecoder(), - 61 to InterfaceOnNpcDecoder(), - 54 to InterfaceOnObjectDecoder(), - 48 to InterfaceOnPlayerDecoder(), - 23 to InterfaceOptionDecoder(0), - 59 to InterfaceOptionDecoder(1), - 9 to InterfaceOptionDecoder(2), - 15 to InterfaceOptionDecoder(3), - 17 to InterfaceOptionDecoder(4), - 39 to InterfaceOptionDecoder(5), - 33 to InterfaceOptionDecoder(6), - 60 to InterfaceOptionDecoder(7), - 11 to InterfaceOptionDecoder(8), - 42 to InterfaceOptionDecoder(9), - 78 to InterfaceSwitchComponentsDecoder(), - 55 to MovedCameraDecoder(), - 69 to KeysPressedDecoder(), - 83 to MovedMouseDecoder(), - 63 to NPCOption1Decoder(), - 29 to NPCOption2Decoder(), - 5 to NPCOption3Decoder(), - 62 to NPCOption4Decoder(), - 65 to NPCOption5Decoder(), - 68 to NPCExamineDecoder(), - 27 to ObjectOption1Decoder(), - 36 to ObjectOption2Decoder(), - 80 to ObjectOption3Decoder(), - 56 to ObjectOption4Decoder(), - 38 to ObjectOption5Decoder(), - 46 to ObjectExamineDecoder(), - 0 to PingDecoder(), - 21 to LatencyDecoder(), - 25 to PlayerOption1Decoder(), - 12 to PlayerOption2Decoder(), - 79 to PlayerOption3Decoder(), - 44 to PlayerOption4Decoder(), - 81 to PlayerOption5Decoder(), - 51 to PlayerOption6Decoder(), - 57 to PlayerOption7Decoder(), - 18 to PlayerOption8Decoder(), - 4 to RegionLoadedDecoder(), - 47 to RegionLoadingDecoder(), - 7 to ScreenChangeDecoder(), - 43 to StringEntryDecoder(), - 35 to WalkMapDecoder(), - 82 to WalkMiniMapDecoder(), - 49 to WindowClickDecoder(), - 8 to WindowFocusDecoder(), - 41 to PublicDecoder(huffman), - 3 to PublicQuickChatDecoder(), - 10 to AddFriendDecoder(), - 14 to AddIgnoreDecoder(), - 6 to DeleteFriendDecoder(), - 73 to DeleteIgnoreDecoder(), - 20 to PrivateDecoder(huffman), - 19 to PrivateQuickChatDecoder(), - 31 to ChatTypeDecoder(), - 50 to ClanChatJoinDecoder(), - 64 to ClanChatKickDecoder(), - 66 to ReportAbuseDecoder(), - 13 to AntiCheatDecoder(), - 67 to HyperlinkDecoder(), - 75 to LobbyOnlineStatusDecoder(), - 30 to LobbyWorldListRefreshDecoder(), - 58 to WorldMapCloseDecoder(), - 74 to ClanChatRankDecoder(), - 77 to ReflectionResponseDecoder(), - 76 to SecondaryTeleportDecoder(), - 1 to ClanChatNameDecoder(), - 34 to InterfaceOnFloorItemDecoder(), - 40 to APCoordinateDecoder(), - 28 to ResumeObjDialogueDecoder(), - 84 to ToolkitPreferencesDecoder(), - 52 to WindowHoveredDecoder(), - 37 to FloorItemExamineDecoder(), - 71 to UnknownDecoder() -) \ No newline at end of file +fun protocol(huffman: Huffman): Array { + val array = arrayOfNulls(256) + array[22] = FloorItemOption1Decoder() + array[16] = FloorItemOption2Decoder() + array[45] = FloorItemOption3Decoder() + array[24] = FloorItemOption4Decoder() + array[26] = FloorItemOption5Decoder() + array[53] = ConsoleCommandDecoder() + array[2] = DialogueContinueDecoder() + array[32] = IntegerEntryDecoder() + array[70] = InterfaceClosedDecoder() + array[72] = InterfaceOnInterfaceDecoder() + array[61] = InterfaceOnNpcDecoder() + array[54] = InterfaceOnObjectDecoder() + array[48] = InterfaceOnPlayerDecoder() + array[23] = InterfaceOptionDecoder(0) + array[59] = InterfaceOptionDecoder(1) + array[9] = InterfaceOptionDecoder(2) + array[15] = InterfaceOptionDecoder(3) + array[17] = InterfaceOptionDecoder(4) + array[39] = InterfaceOptionDecoder(5) + array[33] = InterfaceOptionDecoder(6) + array[60] = InterfaceOptionDecoder(7) + array[11] = InterfaceOptionDecoder(8) + array[42] = InterfaceOptionDecoder(9) + array[78] = InterfaceSwitchComponentsDecoder() + array[55] = MovedCameraDecoder() + array[69] = KeysPressedDecoder() + array[83] = MovedMouseDecoder() + array[63] = NPCOption1Decoder() + array[29] = NPCOption2Decoder() + array[5] = NPCOption3Decoder() + array[62] = NPCOption4Decoder() + array[65] = NPCOption5Decoder() + array[68] = NPCExamineDecoder() + array[27] = ObjectOption1Decoder() + array[36] = ObjectOption2Decoder() + array[80] = ObjectOption3Decoder() + array[56] = ObjectOption4Decoder() + array[38] = ObjectOption5Decoder() + array[46] = ObjectExamineDecoder() + array[0] = PingDecoder() + array[21] = LatencyDecoder() + array[25] = PlayerOption1Decoder() + array[12] = PlayerOption2Decoder() + array[79] = PlayerOption3Decoder() + array[44] = PlayerOption4Decoder() + array[81] = PlayerOption5Decoder() + array[51] = PlayerOption6Decoder() + array[57] = PlayerOption7Decoder() + array[18] = PlayerOption8Decoder() + array[4] = RegionLoadedDecoder() + array[47] = RegionLoadingDecoder() + array[7] = ScreenChangeDecoder() + array[43] = StringEntryDecoder() + array[35] = WalkMapDecoder() + array[82] = WalkMiniMapDecoder() + array[49] = WindowClickDecoder() + array[8] = WindowFocusDecoder() + array[41] = PublicDecoder(huffman) + array[3] = PublicQuickChatDecoder() + array[10] = AddFriendDecoder() + array[14] = AddIgnoreDecoder() + array[6] = DeleteFriendDecoder() + array[73] = DeleteIgnoreDecoder() + array[20] = PrivateDecoder(huffman) + array[19] = PrivateQuickChatDecoder() + array[31] = ChatTypeDecoder() + array[50] = ClanChatJoinDecoder() + array[64] = ClanChatKickDecoder() + array[66] = ReportAbuseDecoder() + array[13] = AntiCheatDecoder() + array[67] = HyperlinkDecoder() + array[75] = LobbyOnlineStatusDecoder() + array[30] = LobbyWorldListRefreshDecoder() + array[58] = WorldMapCloseDecoder() + array[74] = ClanChatRankDecoder() + array[77] = ReflectionResponseDecoder() + array[76] = SecondaryTeleportDecoder() + array[1] = ClanChatNameDecoder() + array[34] = InterfaceOnFloorItemDecoder() + array[40] = APCoordinateDecoder() + array[28] = ResumeObjDialogueDecoder() + array[84] = ToolkitPreferencesDecoder() + array[52] = WindowHoveredDecoder() + array[37] = FloorItemExamineDecoder() + array[71] = UnknownDecoder() + return array +} \ No newline at end of file diff --git a/network/src/main/kotlin/world/gregs/voidps/network/Request.kt b/network/src/main/kotlin/world/gregs/voidps/network/Request.kt new file mode 100644 index 0000000000..5f79b2605d --- /dev/null +++ b/network/src/main/kotlin/world/gregs/voidps/network/Request.kt @@ -0,0 +1,17 @@ +package world.gregs.voidps.network + +object Request { + const val PREFETCH_REQUEST = 0 + const val PRIORITY_REQUEST = 1 + const val STATUS_LOGGED_IN = 2 + const val STATUS_LOGGED_OUT = 3 + const val ENCRYPTION_KEY_UPDATE = 4 + const val ACKNOWLEDGE = 6 + const val SESSION = 10 + const val CONNECT_LOGIN = 14 + const val CONNECT_JS5 = 15 + const val LOGIN = 16 + const val RECONNECT = 18 + const val LOBBY = 19 + const val SIGN_UP = 28 +} \ No newline at end of file diff --git a/network/src/main/kotlin/world/gregs/voidps/network/Server.kt b/network/src/main/kotlin/world/gregs/voidps/network/Server.kt new file mode 100644 index 0000000000..84d2226b32 --- /dev/null +++ b/network/src/main/kotlin/world/gregs/voidps/network/Server.kt @@ -0,0 +1,7 @@ +package world.gregs.voidps.network + +import io.ktor.utils.io.* + +interface Server { + suspend fun connect(read: ByteReadChannel, write: ByteWriteChannel, hostname: String) +} \ No newline at end of file diff --git a/network/src/main/kotlin/world/gregs/voidps/network/Client.kt b/network/src/main/kotlin/world/gregs/voidps/network/client/Client.kt similarity index 97% rename from network/src/main/kotlin/world/gregs/voidps/network/Client.kt rename to network/src/main/kotlin/world/gregs/voidps/network/client/Client.kt index db6b75e703..fa040fc646 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/Client.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/client/Client.kt @@ -1,4 +1,4 @@ -package world.gregs.voidps.network +package world.gregs.voidps.network.client import com.github.michaelbull.logging.InlineLogger import io.ktor.utils.io.* @@ -6,6 +6,7 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first +import world.gregs.voidps.network.writeSmart open class Client( private val write: ByteWriteChannel, diff --git a/network/src/main/kotlin/world/gregs/voidps/network/ClientState.kt b/network/src/main/kotlin/world/gregs/voidps/network/client/ClientState.kt similarity index 83% rename from network/src/main/kotlin/world/gregs/voidps/network/ClientState.kt rename to network/src/main/kotlin/world/gregs/voidps/network/client/ClientState.kt index cb22ce7419..4cb0abd97b 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/ClientState.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/client/ClientState.kt @@ -1,4 +1,4 @@ -package world.gregs.voidps.network +package world.gregs.voidps.network.client sealed class ClientState { object Connected : ClientState() diff --git a/network/src/main/kotlin/world/gregs/voidps/network/DummyClient.kt b/network/src/main/kotlin/world/gregs/voidps/network/client/DummyClient.kt similarity index 89% rename from network/src/main/kotlin/world/gregs/voidps/network/DummyClient.kt rename to network/src/main/kotlin/world/gregs/voidps/network/client/DummyClient.kt index 3bd21b31e1..bb78027bb6 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/DummyClient.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/client/DummyClient.kt @@ -1,4 +1,4 @@ -package world.gregs.voidps.network +package world.gregs.voidps.network.client import io.ktor.utils.io.* diff --git a/network/src/main/kotlin/world/gregs/voidps/network/IsaacCipher.kt b/network/src/main/kotlin/world/gregs/voidps/network/client/IsaacCipher.kt similarity index 99% rename from network/src/main/kotlin/world/gregs/voidps/network/IsaacCipher.kt rename to network/src/main/kotlin/world/gregs/voidps/network/client/IsaacCipher.kt index 03972cc5a3..255c434fd6 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/IsaacCipher.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/client/IsaacCipher.kt @@ -1,4 +1,4 @@ -package world.gregs.voidps.network +package world.gregs.voidps.network.client /** * An implementation of an ISAAC cipher. See diff --git a/network/src/main/kotlin/world/gregs/voidps/network/ByteArrayChannel.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/ByteArrayChannel.kt similarity index 98% rename from network/src/main/kotlin/world/gregs/voidps/network/ByteArrayChannel.kt rename to network/src/main/kotlin/world/gregs/voidps/network/encode/ByteArrayChannel.kt index 89331a7e58..34764d2730 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/ByteArrayChannel.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/ByteArrayChannel.kt @@ -1,4 +1,4 @@ -package world.gregs.voidps.network +package world.gregs.voidps.network.encode import io.ktor.utils.io.* import io.ktor.utils.io.bits.* diff --git a/network/src/main/kotlin/world/gregs/voidps/network/encode/CameraEncoders.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/CameraEncoders.kt index 6017543589..f61a37db4b 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/encode/CameraEncoders.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/CameraEncoders.kt @@ -6,6 +6,7 @@ import world.gregs.voidps.network.Protocol.CAMERA_MOVE import world.gregs.voidps.network.Protocol.CAMERA_RESET import world.gregs.voidps.network.Protocol.CAMERA_SHAKE import world.gregs.voidps.network.Protocol.CAMERA_TURN +import world.gregs.voidps.network.client.Client /** * @param localX localX diff --git a/network/src/main/kotlin/world/gregs/voidps/network/encode/ChatEncoder.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/ChatEncoder.kt index 2e5c3390c6..842f4c010b 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/encode/ChatEncoder.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/ChatEncoder.kt @@ -2,16 +2,17 @@ package world.gregs.voidps.network.encode import io.ktor.utils.io.* import world.gregs.voidps.network.* -import world.gregs.voidps.network.Client.Companion.BYTE -import world.gregs.voidps.network.Client.Companion.name -import world.gregs.voidps.network.Client.Companion.smart -import world.gregs.voidps.network.Client.Companion.string import world.gregs.voidps.network.Protocol.GAME_MESSAGE import world.gregs.voidps.network.Protocol.PRIVATE_CHAT_FROM import world.gregs.voidps.network.Protocol.PRIVATE_CHAT_TO import world.gregs.voidps.network.Protocol.PRIVATE_QUICK_CHAT_FROM import world.gregs.voidps.network.Protocol.PRIVATE_QUICK_CHAT_TO import world.gregs.voidps.network.Protocol.PUBLIC_CHAT +import world.gregs.voidps.network.client.Client +import world.gregs.voidps.network.client.Client.Companion.BYTE +import world.gregs.voidps.network.client.Client.Companion.name +import world.gregs.voidps.network.client.Client.Companion.smart +import world.gregs.voidps.network.client.Client.Companion.string /** * A chat box message to display @@ -67,7 +68,7 @@ fun Client.publicChat(index: Int, effects: Int, rights: Int, message: ByteArray) } } -fun Client.publicQuickChat(index: Int, effects: Int, rights: Int, file: Int, data: ByteArray) { +fun Client.publicQuickChat(index: Int, effects: Int, rights: Int, file: Int, data: ByteArray) { send(PUBLIC_CHAT, data.size + 7, BYTE) { writeShort(index) writeShort(effects) diff --git a/network/src/main/kotlin/world/gregs/voidps/network/encode/ChatStatusEncoder.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/ChatStatusEncoder.kt index 9943e742cd..d5b7fe88f3 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/encode/ChatStatusEncoder.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/ChatStatusEncoder.kt @@ -1,9 +1,9 @@ package world.gregs.voidps.network.encode import io.ktor.utils.io.* -import world.gregs.voidps.network.Client import world.gregs.voidps.network.Protocol.PRIVATE_STATUS import world.gregs.voidps.network.Protocol.PUBLIC_STATUS +import world.gregs.voidps.network.client.Client import world.gregs.voidps.network.writeByteAdd import world.gregs.voidps.network.writeByteSubtract diff --git a/network/src/main/kotlin/world/gregs/voidps/network/encode/ClanEncoder.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/ClanEncoder.kt index 7b3cb8f4ee..1699447579 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/encode/ClanEncoder.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/ClanEncoder.kt @@ -2,14 +2,15 @@ package world.gregs.voidps.network.encode import io.ktor.utils.io.* import world.gregs.voidps.network.* -import world.gregs.voidps.network.Client.Companion.BYTE -import world.gregs.voidps.network.Client.Companion.SHORT -import world.gregs.voidps.network.Client.Companion.name -import world.gregs.voidps.network.Client.Companion.string import world.gregs.voidps.network.Protocol.APPEND_CLAN_CHAT import world.gregs.voidps.network.Protocol.CLAN_CHAT import world.gregs.voidps.network.Protocol.CLAN_QUICK_CHAT import world.gregs.voidps.network.Protocol.UPDATE_CLAN_CHAT +import world.gregs.voidps.network.client.Client +import world.gregs.voidps.network.client.Client.Companion.BYTE +import world.gregs.voidps.network.client.Client.Companion.SHORT +import world.gregs.voidps.network.client.Client.Companion.name +import world.gregs.voidps.network.client.Client.Companion.string fun Client.clanChat(displayName: String, clan: String, rights: Int, data: ByteArray, responseName: String = displayName) { send(CLAN_CHAT, name(displayName, responseName) + 14 + data.size, BYTE) { diff --git a/network/src/main/kotlin/world/gregs/voidps/network/encode/ContainerEncoders.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/ContainerEncoders.kt index c1582f5270..bc966922e6 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/encode/ContainerEncoders.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/ContainerEncoders.kt @@ -1,9 +1,13 @@ package world.gregs.voidps.network.encode import io.ktor.utils.io.* -import world.gregs.voidps.network.* -import world.gregs.voidps.network.Client.Companion.SHORT import world.gregs.voidps.network.Protocol.INTERFACE_ITEMS +import world.gregs.voidps.network.client.Client +import world.gregs.voidps.network.client.Client.Companion.SHORT +import world.gregs.voidps.network.writeByte +import world.gregs.voidps.network.writeByteAdd +import world.gregs.voidps.network.writeIntMiddle +import world.gregs.voidps.network.writeShortAdd /** * Sends a list of items to display on an interface item group component diff --git a/network/src/main/kotlin/world/gregs/voidps/network/encode/ContextMenuOptionEncoder.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/ContextMenuOptionEncoder.kt index 9a6dcc8c76..0f4c24dc9f 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/encode/ContextMenuOptionEncoder.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/ContextMenuOptionEncoder.kt @@ -1,9 +1,13 @@ package world.gregs.voidps.network.encode -import world.gregs.voidps.network.* -import world.gregs.voidps.network.Client.Companion.BYTE -import world.gregs.voidps.network.Client.Companion.string import world.gregs.voidps.network.Protocol.PLAYER_OPTION +import world.gregs.voidps.network.client.Client +import world.gregs.voidps.network.client.Client.Companion.BYTE +import world.gregs.voidps.network.client.Client.Companion.string +import world.gregs.voidps.network.writeByteAdd +import world.gregs.voidps.network.writeByteSubtract +import world.gregs.voidps.network.writeShortAddLittle +import world.gregs.voidps.network.writeString /** * Sends a player right click option diff --git a/network/src/main/kotlin/world/gregs/voidps/network/encode/FriendsEncoder.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/FriendsEncoder.kt index 6e6f21ea66..dff713dffc 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/encode/FriendsEncoder.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/FriendsEncoder.kt @@ -1,9 +1,9 @@ package world.gregs.voidps.network.encode import io.ktor.utils.io.* -import world.gregs.voidps.network.Client -import world.gregs.voidps.network.Client.Companion.string import world.gregs.voidps.network.Protocol.UPDATE_FRIENDS +import world.gregs.voidps.network.client.Client +import world.gregs.voidps.network.client.Client.Companion.string import world.gregs.voidps.network.writeByte import world.gregs.voidps.network.writeString diff --git a/network/src/main/kotlin/world/gregs/voidps/network/encode/IgnoreEncoder.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/IgnoreEncoder.kt index bbfb85ba1d..07bac174f6 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/encode/IgnoreEncoder.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/IgnoreEncoder.kt @@ -1,10 +1,10 @@ package world.gregs.voidps.network.encode import io.ktor.utils.io.* -import world.gregs.voidps.network.Client -import world.gregs.voidps.network.Client.Companion.string import world.gregs.voidps.network.Protocol.UNLOCK_IGNORES import world.gregs.voidps.network.Protocol.UPDATE_IGNORE +import world.gregs.voidps.network.client.Client +import world.gregs.voidps.network.client.Client.Companion.string import world.gregs.voidps.network.writeByte import world.gregs.voidps.network.writeString diff --git a/network/src/main/kotlin/world/gregs/voidps/network/encode/InterfaceEncoders.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/InterfaceEncoders.kt index 3d785df072..934eb7735c 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/encode/InterfaceEncoders.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/InterfaceEncoders.kt @@ -2,10 +2,11 @@ package world.gregs.voidps.network.encode import io.ktor.utils.io.* import world.gregs.voidps.network.* -import world.gregs.voidps.network.Client.Companion.SHORT -import world.gregs.voidps.network.Client.Companion.smart -import world.gregs.voidps.network.Client.Companion.string import world.gregs.voidps.network.Protocol.INTERFACE_ANIMATION +import world.gregs.voidps.network.client.Client +import world.gregs.voidps.network.client.Client.Companion.SHORT +import world.gregs.voidps.network.client.Client.Companion.smart +import world.gregs.voidps.network.client.Client.Companion.string /** * Sends an animation to a interface component diff --git a/network/src/main/kotlin/world/gregs/voidps/network/encode/LoginEncoder.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/LoginEncoder.kt index 05a17a4a71..588aa1504c 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/encode/LoginEncoder.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/LoginEncoder.kt @@ -1,7 +1,11 @@ package world.gregs.voidps.network.encode import io.ktor.utils.io.* -import world.gregs.voidps.network.* +import world.gregs.voidps.network.Response +import world.gregs.voidps.network.client.Client +import world.gregs.voidps.network.writeByte +import world.gregs.voidps.network.writeMedium +import world.gregs.voidps.network.writeString fun Client.login(username: String, index: Int, rights: Int, member: Boolean = true, membersWorld: Boolean = true) = send(-1) { writeByte(Response.SUCCESS) diff --git a/network/src/main/kotlin/world/gregs/voidps/network/encode/LogoutEncoder.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/LogoutEncoder.kt index 3952f86d49..45e5fa33a1 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/encode/LogoutEncoder.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/LogoutEncoder.kt @@ -1,6 +1,6 @@ package world.gregs.voidps.network.encode -import world.gregs.voidps.network.Client import world.gregs.voidps.network.Protocol.LOGOUT +import world.gregs.voidps.network.client.Client fun Client.logout() = send(LOGOUT) {} \ No newline at end of file diff --git a/network/src/main/kotlin/world/gregs/voidps/network/encode/MapRegionEncoders.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/MapRegionEncoders.kt index 114e511bb1..8aebb6e6f0 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/encode/MapRegionEncoders.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/MapRegionEncoders.kt @@ -2,9 +2,10 @@ package world.gregs.voidps.network.encode import io.ktor.utils.io.* import world.gregs.voidps.network.* -import world.gregs.voidps.network.Client.Companion.SHORT -import world.gregs.voidps.network.Client.Companion.bits import world.gregs.voidps.network.Protocol.REGION +import world.gregs.voidps.network.client.Client +import world.gregs.voidps.network.client.Client.Companion.SHORT +import world.gregs.voidps.network.client.Client.Companion.bits fun Client.mapRegion( zoneX: Int, diff --git a/network/src/main/kotlin/world/gregs/voidps/network/encode/MinimapEncoders.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/MinimapEncoders.kt index 679bada030..807eabdf31 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/encode/MinimapEncoders.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/MinimapEncoders.kt @@ -1,8 +1,8 @@ package world.gregs.voidps.network.encode import io.ktor.utils.io.* -import world.gregs.voidps.network.Client import world.gregs.voidps.network.Protocol.MINIMAP_STATE +import world.gregs.voidps.network.client.Client fun Client.sendMinimapState(state: Int) { send(MINIMAP_STATE) { diff --git a/network/src/main/kotlin/world/gregs/voidps/network/encode/NPCUpdateEncoder.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/NPCUpdateEncoder.kt index 5b3ce19bd8..1a29d63fd2 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/encode/NPCUpdateEncoder.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/NPCUpdateEncoder.kt @@ -1,9 +1,9 @@ package world.gregs.voidps.network.encode import world.gregs.voidps.buffer.write.BufferWriter -import world.gregs.voidps.network.Client -import world.gregs.voidps.network.Client.Companion.SHORT import world.gregs.voidps.network.Protocol.NPC_UPDATING +import world.gregs.voidps.network.client.Client +import world.gregs.voidps.network.client.Client.Companion.SHORT import world.gregs.voidps.network.writeBytes fun Client.updateNPCs( diff --git a/network/src/main/kotlin/world/gregs/voidps/network/encode/ObjectEncoders.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/ObjectEncoders.kt index 6d0f7bd74e..6bc83aa499 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/encode/ObjectEncoders.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/ObjectEncoders.kt @@ -1,7 +1,11 @@ package world.gregs.voidps.network.encode import io.ktor.utils.io.* -import world.gregs.voidps.network.* +import world.gregs.voidps.network.Protocol +import world.gregs.voidps.network.client.Client +import world.gregs.voidps.network.writeByteAdd +import world.gregs.voidps.network.writeIntInverseMiddle +import world.gregs.voidps.network.writeShortAddLittle /** * Show animation of an object for a single client diff --git a/network/src/main/kotlin/world/gregs/voidps/network/encode/PlayerUpdateEncoder.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/PlayerUpdateEncoder.kt index ac7589cc51..7f4e1aa2ff 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/encode/PlayerUpdateEncoder.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/PlayerUpdateEncoder.kt @@ -1,9 +1,9 @@ package world.gregs.voidps.network.encode import world.gregs.voidps.buffer.write.BufferWriter -import world.gregs.voidps.network.Client -import world.gregs.voidps.network.Client.Companion.SHORT import world.gregs.voidps.network.Protocol.PLAYER_UPDATING +import world.gregs.voidps.network.client.Client +import world.gregs.voidps.network.client.Client.Companion.SHORT import world.gregs.voidps.network.writeBytes fun Client.updatePlayers( diff --git a/network/src/main/kotlin/world/gregs/voidps/network/encode/ProjectileEncoders.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/ProjectileEncoders.kt index 40b898731a..7cef33710d 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/encode/ProjectileEncoders.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/ProjectileEncoders.kt @@ -1,8 +1,8 @@ package world.gregs.voidps.network.encode import io.ktor.utils.io.* -import world.gregs.voidps.network.Client import world.gregs.voidps.network.Protocol +import world.gregs.voidps.network.client.Client /** * @param offset The tile offset from the zone update sent (encoded with 3 rather than the usual 4) diff --git a/network/src/main/kotlin/world/gregs/voidps/network/encode/RunEnergyEncoder.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/RunEnergyEncoder.kt index 252481ecde..f2cd540fc6 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/encode/RunEnergyEncoder.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/RunEnergyEncoder.kt @@ -1,8 +1,8 @@ package world.gregs.voidps.network.encode import io.ktor.utils.io.* -import world.gregs.voidps.network.Client import world.gregs.voidps.network.Protocol.RUN_ENERGY +import world.gregs.voidps.network.client.Client /** * Sends run energy diff --git a/network/src/main/kotlin/world/gregs/voidps/network/encode/ScriptEncoder.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/ScriptEncoder.kt index 9d66f0c3ac..2de0bf23a6 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/encode/ScriptEncoder.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/ScriptEncoder.kt @@ -1,9 +1,9 @@ package world.gregs.voidps.network.encode -import world.gregs.voidps.network.Client -import world.gregs.voidps.network.Client.Companion.SHORT -import world.gregs.voidps.network.Client.Companion.string import world.gregs.voidps.network.Protocol.SCRIPT +import world.gregs.voidps.network.client.Client +import world.gregs.voidps.network.client.Client.Companion.SHORT +import world.gregs.voidps.network.client.Client.Companion.string import world.gregs.voidps.network.writeString /** diff --git a/network/src/main/kotlin/world/gregs/voidps/network/encode/SkillLevelEncoder.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/SkillLevelEncoder.kt index 981f848df5..b2b4875a97 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/encode/SkillLevelEncoder.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/SkillLevelEncoder.kt @@ -1,7 +1,7 @@ package world.gregs.voidps.network.encode -import world.gregs.voidps.network.Client import world.gregs.voidps.network.Protocol.SKILL_LEVEL +import world.gregs.voidps.network.client.Client import world.gregs.voidps.network.writeByteInverse import world.gregs.voidps.network.writeByteSubtract import world.gregs.voidps.network.writeIntMiddle diff --git a/network/src/main/kotlin/world/gregs/voidps/network/encode/SoundEncoder.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/SoundEncoder.kt index bd70ff3ad6..d72f5deba0 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/encode/SoundEncoder.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/SoundEncoder.kt @@ -1,11 +1,15 @@ package world.gregs.voidps.network.encode import io.ktor.utils.io.* -import world.gregs.voidps.network.* import world.gregs.voidps.network.Protocol.JINGLE import world.gregs.voidps.network.Protocol.MIDI_SOUND import world.gregs.voidps.network.Protocol.PLAY_MUSIC import world.gregs.voidps.network.Protocol.SOUND_EFFECT +import world.gregs.voidps.network.client.Client +import world.gregs.voidps.network.writeByteInverse +import world.gregs.voidps.network.writeByteSubtract +import world.gregs.voidps.network.writeMedium +import world.gregs.voidps.network.writeShortAddLittle fun Client.playMusicTrack( music: Int, diff --git a/network/src/main/kotlin/world/gregs/voidps/network/encode/TextTileEncoder.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/TextTileEncoder.kt index 9cce33cbd3..0210d71e1a 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/encode/TextTileEncoder.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/TextTileEncoder.kt @@ -1,10 +1,10 @@ package world.gregs.voidps.network.encode import io.ktor.utils.io.* -import world.gregs.voidps.network.Client -import world.gregs.voidps.network.Client.Companion.BYTE -import world.gregs.voidps.network.Client.Companion.string import world.gregs.voidps.network.Protocol.TILE_TEXT +import world.gregs.voidps.network.client.Client +import world.gregs.voidps.network.client.Client.Companion.BYTE +import world.gregs.voidps.network.client.Client.Companion.string import world.gregs.voidps.network.writeMedium import world.gregs.voidps.network.writeString diff --git a/network/src/main/kotlin/world/gregs/voidps/network/encode/VarbitEncoder.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/VarbitEncoder.kt index 72fcd9a2a4..48623679d2 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/encode/VarbitEncoder.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/VarbitEncoder.kt @@ -1,8 +1,12 @@ package world.gregs.voidps.network.encode -import world.gregs.voidps.network.* import world.gregs.voidps.network.Protocol.CLIENT_VARBIT import world.gregs.voidps.network.Protocol.CLIENT_VARBIT_LARGE +import world.gregs.voidps.network.client.Client +import world.gregs.voidps.network.writeByteAdd +import world.gregs.voidps.network.writeIntInverseMiddle +import world.gregs.voidps.network.writeShortAdd +import world.gregs.voidps.network.writeShortLittle /** * A variable bit; also known as "ConfigFile", known in the client as "clientvarpbit" diff --git a/network/src/main/kotlin/world/gregs/voidps/network/encode/VarcEncoder.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/VarcEncoder.kt index 0d969997da..7ad84c9ca1 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/encode/VarcEncoder.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/VarcEncoder.kt @@ -1,9 +1,9 @@ package world.gregs.voidps.network.encode import io.ktor.utils.io.* -import world.gregs.voidps.network.Client import world.gregs.voidps.network.Protocol.CLIENT_VARC import world.gregs.voidps.network.Protocol.CLIENT_VARC_LARGE +import world.gregs.voidps.network.client.Client import world.gregs.voidps.network.writeIntLittle import world.gregs.voidps.network.writeShortAddLittle import world.gregs.voidps.network.writeShortLittle diff --git a/network/src/main/kotlin/world/gregs/voidps/network/encode/VarcStrEncoder.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/VarcStrEncoder.kt index 84c71bcbff..7f92b01dcf 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/encode/VarcStrEncoder.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/VarcStrEncoder.kt @@ -1,11 +1,11 @@ package world.gregs.voidps.network.encode -import world.gregs.voidps.network.Client -import world.gregs.voidps.network.Client.Companion.BYTE -import world.gregs.voidps.network.Client.Companion.SHORT -import world.gregs.voidps.network.Client.Companion.string import world.gregs.voidps.network.Protocol.CLIENT_VARC_STR import world.gregs.voidps.network.Protocol.CLIENT_VARC_STR_LARGE +import world.gregs.voidps.network.client.Client +import world.gregs.voidps.network.client.Client.Companion.BYTE +import world.gregs.voidps.network.client.Client.Companion.SHORT +import world.gregs.voidps.network.client.Client.Companion.string import world.gregs.voidps.network.writeShortAdd import world.gregs.voidps.network.writeShortAddLittle import world.gregs.voidps.network.writeString diff --git a/network/src/main/kotlin/world/gregs/voidps/network/encode/VarpEncoder.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/VarpEncoder.kt index 4fcba87205..1bf34068e6 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/encode/VarpEncoder.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/VarpEncoder.kt @@ -1,9 +1,9 @@ package world.gregs.voidps.network.encode import io.ktor.utils.io.* -import world.gregs.voidps.network.Client import world.gregs.voidps.network.Protocol.CLIENT_VARP import world.gregs.voidps.network.Protocol.CLIENT_VARP_LARGE +import world.gregs.voidps.network.client.Client import world.gregs.voidps.network.writeIntInverseMiddle import world.gregs.voidps.network.writeShortAdd diff --git a/network/src/main/kotlin/world/gregs/voidps/network/encode/WeightEncoder.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/WeightEncoder.kt index 97e02ef77e..62d2c5634e 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/encode/WeightEncoder.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/WeightEncoder.kt @@ -1,8 +1,8 @@ package world.gregs.voidps.network.encode import io.ktor.utils.io.* -import world.gregs.voidps.network.Client import world.gregs.voidps.network.Protocol.PLAYER_WEIGHT +import world.gregs.voidps.network.client.Client /** * Updates player weight for equipment screen diff --git a/network/src/main/kotlin/world/gregs/voidps/network/encode/ZoneEncoders.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/ZoneEncoders.kt index 5afaf66322..3bdb72e3e2 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/encode/ZoneEncoders.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/ZoneEncoders.kt @@ -1,8 +1,8 @@ package world.gregs.voidps.network.encode -import world.gregs.voidps.network.Client import world.gregs.voidps.network.Protocol import world.gregs.voidps.network.Protocol.CLEAR_ZONE +import world.gregs.voidps.network.client.Client import world.gregs.voidps.network.writeByteAdd import world.gregs.voidps.network.writeByteInverse diff --git a/network/src/main/kotlin/world/gregs/voidps/network/encode/ZoneUpdateEncoders.kt b/network/src/main/kotlin/world/gregs/voidps/network/encode/ZoneUpdateEncoders.kt index fc836f88e0..187fb1daf1 100644 --- a/network/src/main/kotlin/world/gregs/voidps/network/encode/ZoneUpdateEncoders.kt +++ b/network/src/main/kotlin/world/gregs/voidps/network/encode/ZoneUpdateEncoders.kt @@ -3,6 +3,7 @@ package world.gregs.voidps.network.encode import io.ktor.utils.io.* import kotlinx.coroutines.runBlocking import world.gregs.voidps.network.* +import world.gregs.voidps.network.client.Client import world.gregs.voidps.network.encode.zone.* fun encodeBatch(messages: Collection): ByteArray { diff --git a/network/src/main/kotlin/world/gregs/voidps/network/file/CacheFileProvider.kt b/network/src/main/kotlin/world/gregs/voidps/network/file/CacheFileProvider.kt new file mode 100644 index 0000000000..33dcd9a6c8 --- /dev/null +++ b/network/src/main/kotlin/world/gregs/voidps/network/file/CacheFileProvider.kt @@ -0,0 +1,38 @@ +package world.gregs.voidps.network.file + +import io.ktor.utils.io.* +import world.gregs.voidps.cache.Cache +import world.gregs.voidps.network.file.FileProvider.Companion.LARGEST_BLOCK +import world.gregs.voidps.network.file.FileProvider.Companion.OFFSET +import world.gregs.voidps.network.file.FileProvider.Companion.SEPARATOR +import world.gregs.voidps.network.file.FileProvider.Companion.SPLIT +import world.gregs.voidps.network.file.FileProvider.Companion.getInt +import kotlin.math.min + +/** + * Reads sectors directly from the [cache] + * Average read speeds, low memory usage + */ +class CacheFileProvider(private val cache: Cache) : FileProvider { + + override fun data(index: Int, archive: Int): ByteArray? { + if (index == 255 && archive == 255) { + return cache.versionTable + } + return cache.sector(index, archive) + } + + override suspend fun encode(write: ByteWriteChannel, data: ByteArray) { + val compression = data[0].toInt() + val size = getInt(data[1], data[2], data[3], data[4]) + if (compression != 0) 8 else 4 + var length = min(size, LARGEST_BLOCK) + write.writeFully(data, OFFSET, length) + var written = length + while (written < size) { + write.writeByte(SEPARATOR) + length = if (size - written < SPLIT) size - written else SPLIT - 1 + write.writeFully(data, written + OFFSET, length) + written += length + } + } +} \ No newline at end of file diff --git a/network/src/main/kotlin/world/gregs/voidps/network/file/FileProvider.kt b/network/src/main/kotlin/world/gregs/voidps/network/file/FileProvider.kt new file mode 100644 index 0000000000..05fa551c54 --- /dev/null +++ b/network/src/main/kotlin/world/gregs/voidps/network/file/FileProvider.kt @@ -0,0 +1,72 @@ +package world.gregs.voidps.network.file + +import com.github.michaelbull.logging.InlineLogger +import io.ktor.utils.io.* +import world.gregs.voidps.buffer.write.BufferWriter +import world.gregs.voidps.cache.Cache +import java.util.* +import kotlin.math.min + +/** + * Provides cache sectors requested by the client + */ +interface FileProvider { + + fun data(index: Int, archive: Int): ByteArray? + + suspend fun encode(write: ByteWriteChannel, data: ByteArray) + + suspend fun serve(write: ByteWriteChannel, request: Int, prefetch: Boolean): Boolean { + val index = request shr 16 + val archive = request and 0xffff + val data = data(index, archive) + if (data == null || data.size < 4) { + logger.warn { "Unable to fulfill request $index $archive $prefetch." } + return false + } + val compression = data[0].toInt() + write.writeByte(index) + write.writeShort(archive) + write.writeByte(if (prefetch) compression or 0x80 else compression) + encode(write, data) + write.flush() + return true + } + + companion object { + private val logger = InlineLogger() + + fun load(cache: Cache, properties: Properties): FileProvider { + val start = System.currentTimeMillis() + val live = properties.getProperty("live").toBoolean() + val provider = if (live) MemoryFileProvider(cache) else CacheFileProvider(cache) + logger.info { "Loaded file provider in ${System.currentTimeMillis() - start}ms" } + return provider + } + + internal fun getInt(b1: Byte, b2: Byte, b3: Byte, b4: Byte) = b1.toInt() shl 24 or (b2.toInt() and 0xff shl 16) or (b3.toInt() and 0xff shl 8) or (b4.toInt() and 0xff) + + internal const val SEPARATOR = 255 + private const val HEADER = 4 + internal const val SPLIT = 512 + internal const val LARGEST_BLOCK = SPLIT - HEADER + internal const val OFFSET = 1 + + internal fun encode(data: ByteArray): ByteArray { + val compression = data[0].toInt() + val size = getInt(data[1], data[2], data[3], data[4]) + if (compression != 0) 8 else 4 + val write = BufferWriter(data.size + (size / SPLIT) + 1) + write.writeByte(compression) + var length = min(size, LARGEST_BLOCK) + write.writeBytes(data, OFFSET, length) + var written = length + while (written < size) { + write.writeByte(SEPARATOR) + length = if (size - written < SPLIT) size - written else SPLIT - 1 + write.writeBytes(data, written + OFFSET, length) + written += length + } + return write.toArray() + } + } +} \ No newline at end of file diff --git a/network/src/main/kotlin/world/gregs/voidps/network/file/MemoryFileProvider.kt b/network/src/main/kotlin/world/gregs/voidps/network/file/MemoryFileProvider.kt new file mode 100644 index 0000000000..7b75cfdae0 --- /dev/null +++ b/network/src/main/kotlin/world/gregs/voidps/network/file/MemoryFileProvider.kt @@ -0,0 +1,37 @@ +package world.gregs.voidps.network.file + +import io.ktor.utils.io.* +import world.gregs.voidps.cache.Cache + +/** + * Reads all [cache] sectors into memory + * Fast read speed, high memory usage + */ +class MemoryFileProvider(cache: Cache) : FileProvider { + + private val sectors: Array?> = arrayOfNulls(256) + + init { + val index255 = arrayOfNulls(256) + sectors[255] = index255 + index255[255] = FileProvider.encode(cache.versionTable) + for (index in cache.indices()) { + val archives = arrayOfNulls(cache.lastArchiveId(index) + 1) + sectors[index] = archives + for (archive in cache.archives(index)) { + val data = cache.sector(index, archive) ?: continue + archives[archive] = FileProvider.encode(data) + } + index255[index] = FileProvider.encode(cache.sector(255, index) ?: continue) + } + } + + override fun data(index: Int, archive: Int): ByteArray? { + return sectors.getOrNull(index)?.getOrNull(archive) + } + + override suspend fun encode(write: ByteWriteChannel, data: ByteArray) { + write.writeFully(data, 1, data.size - 1) + } + +} \ No newline at end of file diff --git a/network/src/main/kotlin/world/gregs/voidps/network/file/PrefetchKeys.kt b/network/src/main/kotlin/world/gregs/voidps/network/file/PrefetchKeys.kt new file mode 100644 index 0000000000..84ccc23c1b --- /dev/null +++ b/network/src/main/kotlin/world/gregs/voidps/network/file/PrefetchKeys.kt @@ -0,0 +1,69 @@ +package world.gregs.voidps.network.file + +import world.gregs.voidps.cache.Cache +import world.gregs.voidps.cache.Index +import java.util.* + +fun prefetchKeys(cache: Cache, properties: Properties): IntArray { + val existing = properties["prefetchKeys"] as? String + return existing?.split(",")?.map { it.toInt() }?.toIntArray() ?: generatePrefetchKeys(cache) +} + +/** + * Generates cache prefetch keys used to determine total cache download percentage + * Note: Can vary between revisions, compare with your client. + */ +fun generatePrefetchKeys(cache: Cache) = intArrayOf( + archive(cache, Index.DEFAULTS), + native(cache, "jaclib"), + native(cache, "jaggl"), + native(cache, "jagdx"), + native(cache, "jagmisc"), + native(cache, "sw3d"), + native(cache, "hw3d"), + native(cache, "jagtheora"), + archive(cache, Index.SHADERS), + archive(cache, Index.TEXTURE_DEFINITIONS), + archive(cache, Index.CONFIGS), + archive(cache, Index.OBJECTS), + archive(cache, Index.ENUMS), + archive(cache, Index.NPCS), + archive(cache, Index.ITEMS), + archive(cache, Index.ANIMATIONS), + archive(cache, Index.GRAPHICS), + archive(cache, Index.VAR_BIT), + archive(cache, Index.QUICK_CHAT_MESSAGES), + archive(cache, Index.QUICK_CHAT_MENUS), + archive(cache, Index.PARTICLES), + archive(cache, Index.BILLBOARDS), + file(cache, Index.HUFFMAN, "huffman"), + archive(cache, Index.INTERFACES), + archive(cache, Index.CLIENT_SCRIPTS), + archive(cache, Index.FONT_METRICS), + file(cache, Index.WORLD_MAP, "details"), +) + +/** + * Length of archive with [name] in [index] + */ +fun file(cache: Cache, index: Int, name: String): Int { + val archive = cache.archiveId(index, name) + if (archive == -1) { + return 0 + } + return (cache.sector(index, archive)?.size ?: 2) - 2 +} + +/** + * Length of all archives in [index] + */ +fun archive(cache: Cache, index: Int): Int { + var total = 0 + for(archive in cache.archives(index)) { + total += cache.sector(index, archive)?.size ?: 0 + } + total += cache.sector(255, index)?.size ?: 0 + return total +} + +fun native(cache: Cache, name: String) = file(cache, Index.NATIVE_LIBRARIES, "windows/x86/$name.dll") \ No newline at end of file diff --git a/network/src/test/kotlin/world/gregs/voidps/network/GameServerTest.kt b/network/src/test/kotlin/world/gregs/voidps/network/GameServerTest.kt new file mode 100644 index 0000000000..2bb468f88d --- /dev/null +++ b/network/src/test/kotlin/world/gregs/voidps/network/GameServerTest.kt @@ -0,0 +1,63 @@ +package world.gregs.voidps.network + +import io.ktor.utils.io.* +import io.mockk.* +import io.mockk.impl.annotations.MockK +import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.junit5.MockKExtension +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(MockKExtension::class) +@ExperimentalUnsignedTypes +internal class GameServerTest { + @MockK + lateinit var server: GameServer + + @RelaxedMockK + lateinit var gatekeeper: NetworkGatekeeper + + @RelaxedMockK + lateinit var read: ByteReadChannel + + @RelaxedMockK + lateinit var write: ByteWriteChannel + + @BeforeEach + fun setup() { + server = spyk( + GameServer( + gatekeeper, + 2, + mockk(relaxed = true), + mockk(relaxed = true) + ) + ) + } + + @Test + fun `Login limit exceeded`() = runTest { + every { gatekeeper.connections("") } returns 1000 + + server.connect(read, write, "") + + coVerify { + write.writeByte(Response.LOGIN_LIMIT_EXCEEDED) + write.close() + } + } + + @Test + fun `Network rejected synchronisation`() = runTest { + coEvery { read.readByte() } returns 12 + + server.connect(read, write, "") + + coVerify { + write.writeByte(Response.INVALID_LOGIN_SERVER) + write.close() + } + } +} diff --git a/network/src/test/kotlin/world/gregs/voidps/network/NetworkTest.kt b/network/src/test/kotlin/world/gregs/voidps/network/LoginServerTest.kt similarity index 88% rename from network/src/test/kotlin/world/gregs/voidps/network/NetworkTest.kt rename to network/src/test/kotlin/world/gregs/voidps/network/LoginServerTest.kt index 5156a1c5c0..f9fb3efb7b 100644 --- a/network/src/test/kotlin/world/gregs/voidps/network/NetworkTest.kt +++ b/network/src/test/kotlin/world/gregs/voidps/network/LoginServerTest.kt @@ -12,14 +12,15 @@ import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith +import world.gregs.voidps.network.client.Client import java.math.BigInteger @ExtendWith(MockKExtension::class) @OptIn(ExperimentalCoroutinesApi::class) @ExperimentalUnsignedTypes -internal class NetworkTest { +internal class LoginServerTest { @MockK - lateinit var network: Network + lateinit var network: LoginServer @RelaxedMockK lateinit var gatekeeper: NetworkGatekeeper @@ -36,31 +37,18 @@ internal class NetworkTest { @BeforeEach fun setup() { network = spyk( - Network( + LoginServer( + protocol(mockk()), 123, BigInteger.ONE, BigInteger.valueOf(2), gatekeeper, loader, - 2, - UnconfinedTestDispatcher(), - protocol(mockk()) + UnconfinedTestDispatcher() ) ) } - @Test - fun `Login limit exceeded`() = runTest { - every { gatekeeper.connections("") } returns 1000 - - network.connect(read, write, "") - - coVerify { - write.writeByte(Response.LOGIN_LIMIT_EXCEEDED) - write.close() - } - } - @Test fun `Login server rejected synchronisation`() = runTest { coEvery { read.readByte() } returns 15 @@ -92,11 +80,7 @@ internal class NetworkTest { @Test fun `Game update`() = runTest { - var index = 0 - val array = arrayOf(14, 16) - coEvery { read.readByte() } answers { - array[index++].toByte() - } + coEvery { read.readByte() } returns 16 coEvery { read.readShort() } returns 4 coEvery { read.readPacket(4) } returns ByteReadPacket(byteArrayOf(0, 0, 2, 123)) diff --git a/tools/src/main/kotlin/world/gregs/voidps/tools/DropTableDefinitions.kt b/tools/src/main/kotlin/world/gregs/voidps/tools/DropTableDefinitions.kt index 5fc1f3f042..4e8c6502fe 100644 --- a/tools/src/main/kotlin/world/gregs/voidps/tools/DropTableDefinitions.kt +++ b/tools/src/main/kotlin/world/gregs/voidps/tools/DropTableDefinitions.kt @@ -19,7 +19,7 @@ object DropTableDefinitions { val koin = startKoin { fileProperties("/tool.properties") modules(module { - single { CacheDelegate(getProperty("cachePath")) as Cache } + single { CacheDelegate(getProperty("cachePath")) as Cache } single { ItemDefinitions(ItemDecoder().load(get())).load(Yaml()) } }) }.koin diff --git a/tools/src/main/kotlin/world/gregs/voidps/tools/cache/CacheBuilder.kt b/tools/src/main/kotlin/world/gregs/voidps/tools/cache/CacheBuilder.kt index 2d5e8333ad..c6868c6aa2 100644 --- a/tools/src/main/kotlin/world/gregs/voidps/tools/cache/CacheBuilder.kt +++ b/tools/src/main/kotlin/world/gregs/voidps/tools/cache/CacheBuilder.kt @@ -55,7 +55,7 @@ object CacheBuilder { val cache667 = OpenRS2.downloadCache(temp.resolve("cache-667/"), 1473) val library = CacheLibrary(path.path) RemoveXteas.remove(library, xteas) - RemoveBzip2.remove(library) +// RemoveBzip2.remove(library) MoveCameraClientScript.convert(library, cache667) println("Rebuilding cache.") library.rebuild(target) diff --git a/tools/src/main/kotlin/world/gregs/voidps/tools/cache/PrefetchKeyGeneration.kt b/tools/src/main/kotlin/world/gregs/voidps/tools/cache/PrefetchKeyGeneration.kt index f8a4de6307..c0ed8fb4d8 100644 --- a/tools/src/main/kotlin/world/gregs/voidps/tools/cache/PrefetchKeyGeneration.kt +++ b/tools/src/main/kotlin/world/gregs/voidps/tools/cache/PrefetchKeyGeneration.kt @@ -1,7 +1,8 @@ package world.gregs.voidps.tools.cache import com.displee.cache.CacheLibrary -import world.gregs.voidps.cache.Index +import world.gregs.voidps.cache.CacheDelegate +import world.gregs.voidps.network.file.generatePrefetchKeys object PrefetchKeyGeneration { @@ -12,55 +13,7 @@ object PrefetchKeyGeneration { } fun print(cache: CacheLibrary) { - print("prefetchKeys=") - print("${cache.archive(Index.DEFAULTS)},") - print("${cache.native("jaclib")},") - print("${cache.native("jaggl")},") - print("${cache.native("jagdx")},") - print("${cache.native("jagmisc")},") - print("${cache.native("sw3d")},") - print("${cache.native("hw3d")},") - print("${cache.native("jagtheora")},") - print("${cache.archive(Index.SHADERS)},") - print("${cache.archive(Index.TEXTURE_DEFINITIONS)},") - print("${cache.archive(Index.CONFIGS)},") - print("${cache.archive(Index.OBJECTS)},") - print("${cache.archive(Index.ENUMS)},") - print("${cache.archive(Index.NPCS)},") - print("${cache.archive(Index.ITEMS)},") - print("${cache.archive(Index.ANIMATIONS)},") - print("${cache.archive(Index.GRAPHICS)},") - print("${cache.archive(Index.VAR_BIT)},") - print("${cache.archive(Index.QUICK_CHAT_MESSAGES)},") - print("${cache.archive(Index.QUICK_CHAT_MENUS)},") - print("${cache.archive(Index.PARTICLES)},") - print("${cache.archive(Index.BILLBOARDS)},") - print("${cache.group(Index.HUFFMAN, "huffman")},") - print("${cache.archive(Index.INTERFACES)},") - print("${cache.archive(Index.CLIENT_SCRIPTS)},") - print("${cache.archive(Index.FONT_METRICS)},") - println(cache.group(Index.WORLD_MAP, "details")) - } - - private fun CacheLibrary.group(index: Int, name: String): Int { - val idx = index(index) - val archive = idx.archiveId(name) - if(archive == -1) { - return 0 - } - return (idx.readArchiveSector(archive)?.size ?: 2) - 2 - } - - private fun CacheLibrary.archive(index: Int): Int { - var total = 0 - index(index).archiveIds().forEach { archive -> - total += index(index).readArchiveSector(archive)?.size ?: 0 - } - total += index255?.readArchiveSector(index)?.size ?: 0 - return total - } - - private fun CacheLibrary.native(name: String): Int { - return group(Index.NATIVE_LIBRARIES, "windows/x86/$name.dll") + val keys = generatePrefetchKeys(CacheDelegate(cache)) + println("prefetchKeys=${keys.joinToString(",")}") } } \ No newline at end of file diff --git a/tools/src/main/kotlin/world/gregs/voidps/tools/cache/RemoveBzip2.kt b/tools/src/main/kotlin/world/gregs/voidps/tools/cache/RemoveBzip2.kt index 69c6d2c0b0..3ffff6e121 100644 --- a/tools/src/main/kotlin/world/gregs/voidps/tools/cache/RemoveBzip2.kt +++ b/tools/src/main/kotlin/world/gregs/voidps/tools/cache/RemoveBzip2.kt @@ -18,9 +18,6 @@ object RemoveBzip2 { indices++ } for (archive in index.archives()) { - for (file in archive.files) { - lib.data(index.id, archive.id, file.key) - } if (archive.compressionType == CompressionType.BZIP2) { archive.compressionType = CompressionType.GZIP archive.flag() diff --git a/tools/src/main/kotlin/world/gregs/voidps/tools/wiki/dialogue/DialogueConverter.kt b/tools/src/main/kotlin/world/gregs/voidps/tools/wiki/dialogue/DialogueConverter.kt new file mode 100644 index 0000000000..5794e413ea --- /dev/null +++ b/tools/src/main/kotlin/world/gregs/voidps/tools/wiki/dialogue/DialogueConverter.kt @@ -0,0 +1,287 @@ +package world.gregs.voidps.tools.wiki.dialogue + +import com.google.common.base.CaseFormat +import com.google.common.base.CharMatcher +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet +import net.pearx.kasechange.toPascalCase +import net.pearx.kasechange.toSnakeCase +import world.gregs.yaml.Yaml +import world.gregs.yaml.read.YamlReaderConfiguration +import java.io.File +import java.util.* + +object DialogueConverter { + + data class DialogueOption( + val child: DialogueUnion? = null, + val message: String? = null + ) { + companion object { + + operator fun invoke(map: Map): DialogueOption { + return DialogueOption( + map["child"] as? DialogueUnion, + map["message"] as? String + ) + } + } + } + + data class DialogueUnion( + var text: String?, + var name: String?, + var npc: Int?, + var animation: Int?, + var neighbors: List? = null, + var options: List? = null + ) { + + val builder: StringBuilder by lazy { StringBuilder() } + + override fun hashCode(): Int { + return if (isRootNode) { + Objects.hash(neighbors) + } else Objects.hash(npc, text, name, options) + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + if (other == null || javaClass != other.javaClass) { + return false + } + val (text1, name1, npc1, _, neighbors1, options1) = other as DialogueUnion + return if (isRootNode) { + if (neighbors == neighbors1) { + return !(neighbors != null && neighbors1 != null && neighbors1.containsAll(neighbors!!)) + } + false + } else { + npc == npc1 && text == text1 && name == name1 && options == options1 + } + } + + val isNPCDialogue: Boolean + get() = npc != null && animation != null && text != null && name != null + val isOptionsDialogue: Boolean + get() = options != null + val isPlayerDialogue: Boolean + get() = animation != null && text != null + val isMessageDialogue: Boolean + get() = text != null && animation == null && npc == null && options == null + val isRootNode: Boolean + get() = npc == null && animation == null && text == null && name == null && neighbors != null + + + companion object { + + operator fun invoke(map: Map): DialogueUnion { + return DialogueUnion( + map["text"] as? String, + map["name"] as? String, + map["npc"] as? Int, + map["animation"] as? Int, + map["neighbors"] as? List, + map["options"] as? List + ) + } + } + } + + private fun getParents(node: DialogueUnion): ObjectOpenHashSet { + val queue = ArrayDeque() + val parentQueue = ArrayDeque() + val branchToFunction = Object2ObjectOpenHashMap() + val parents = ObjectOpenHashSet() + queue.push(node) + parentQueue.push(node) + branchToFunction[node] = "startDialogue" + var previousParent = node + while (!queue.isEmpty()) { + val front = queue.poll() + val parent = parentQueue.poll() + printMembers(node.npc!!, previousParent, parent, parentQueue.peekFirst(), front, branchToFunction, parents) + previousParent = parent + val neighbors = front.neighbors ?: continue + for (neighbor in neighbors) { + queue.addLast(neighbor) + parentQueue.addLast(front) + val options = neighbor.options ?: continue + for (option in options) { + val child = option.child ?: continue + queue.addLast(child) + parentQueue.addLast(neighbor) + } + } + } + return parents + } + + @JvmStatic + fun main(args: Array) { + val input = File("${System.getProperty("user.home")}/Downloads/complete-dialogues/") + val yaml = Yaml() + val reader = object : YamlReaderConfiguration() { + override fun add(list: MutableList, value: Any, parentMap: String?) { + if (value is Map<*, *>) { + value as Map + super.add(list, if (value.containsKey("child")) DialogueOption(value) else DialogueUnion(value), parentMap) + } else { + super.add(list, value, parentMap) + } + } + + override fun set(map: MutableMap, key: String, value: Any, indent: Int, parentMap: String?) { + if (value is Map<*, *>) { + value as Map + super.set(map, key, if (value.containsKey("child")) DialogueOption(value) else DialogueUnion(value), indent, parentMap) + } else { + super.set(map, key, value, indent, parentMap) + } + } + } + val outputPath = File("./temp/dialogue-scripts/") + outputPath.mkdirs() + input.walkTopDown().forEach { file -> + if (!file.isFile || file.extension != "json") { + return@forEach + } + val index = file.nameWithoutExtension.indexOfLast { it == '-' } + val name = file.nameWithoutExtension.substring(0, index) + val relative = file.parent.replace(input.path, "").removePrefix("\\") + val text = file.readText().replace("\" + player.getName() + \"", "{\$player_name}").replace("{\$player_name}", "") + val root = DialogueUnion(yaml.read(text, reader) as Map) + val out = outputPath.resolve(relative).resolve("${name.replace("'", "").toPascalCase()}.kts") + out.parentFile.mkdirs() + writeAll(out, name.toSnakeCase(), getParents(root)) + } + } + + private fun getNpcName(id: Int): String { + return "$id" + } + + private fun writeAll(file: File, name: String, functions: ObjectOpenHashSet) { + val sorted = functions.sortedBy { it.neighbors?.size ?: 0 }.toMutableList() + val existing = ObjectOpenHashSet() + sorted.removeIf { parent: DialogueUnion -> + val builder: StringBuilder = parent.builder + val functionName: String = builder.substring(0, builder.indexOf("\n")) + if (!existing.contains(functionName)) { + existing.add(functionName) + return@removeIf false + } + true + } + + if (file.exists()) { + file.delete() + } + file.appendText(""" + package world.gregs.voidps.world.map + + import world.gregs.voidps.engine.entity.character.CharacterContext + import world.gregs.voidps.engine.entity.character.npc.NPCOption + import world.gregs.voidps.engine.entity.character.player.Player + import world.gregs.voidps.engine.event.on + import world.gregs.voidps.world.interact.dialogue.* + import world.gregs.voidps.world.interact.dialogue.type.* + + on({ operate && target.id == "$name" && option == "Talk-to" }) { player: Player -> + startDialogue() + } + """.trimIndent()) + file.appendText("\n\n") + for (parent: DialogueUnion in sorted) { + file.appendText(parent.builder.toString().replace("\t", " ")) + } + } + + private fun printMembers( + npc: Int, + previousParent: DialogueUnion, + parent: DialogueUnion, + nextParent: DialogueUnion?, + front: DialogueUnion, + branchToFunction: Object2ObjectOpenHashMap, + parents: ObjectOpenHashSet + ) { + val lastNode = parent != nextParent + val builder = parent.builder + if (parent != previousParent) { + val function = branchToFunction[parent] + if (function != null) { + builder.append("suspend fun CharacterContext.").append(function).append("() {\n") + } + } + if (front.isNPCDialogue) { + val anim = getAnimName(front.animation) + builder.append("\tnpc").append("<").append(anim).append(">").append("(") + if (npc != front.npc) { + builder.append(getNpcName(front.npc!!)).append(", ") + } + builder.append("\"").append(front.text).append("\"").append(")") + + } else if (front.isPlayerDialogue) { + val anim = getAnimName(front.animation) + builder.append("\tplayer").append("<").append(anim).append(">").append("(").append("\"").append(front.text).append("\"").append(")") + } else if (front.isMessageDialogue) { + builder.append("\tmessage").append("(").append("\"").append(front.text).append("\")") + } else if (front.isOptionsDialogue) { + val options = front.options +// branchToFunction[front] = toFunction(options?.first()?.message) + builder.append("\tchoice {\n") + var count = 0 + for (option: DialogueOption in options!!) { + count++ + val function = toFunction(option.message) + branchToFunction[option.child] = function + builder.append("\t\toption(\"").append(option.message).append("\") {\n\t\t\t").append(function).append("()\n\t\t}") + if (count == options.size) { + continue + } + builder.append("\n") + } + builder.append("\n\t}") + } else { + val function = branchToFunction[parent] + if (function != null) { + builder.append("suspend fun CharacterContext.").append(function).append("() {\n") + } + return + } + if (!lastNode) { + builder.append("\n") + } else { + builder.append("\n}\n\n") + parents.add(parent) + } + } + + private fun getAnimName(animation: Int?): String { + return when (animation) { + 554, 555, 556, 557 -> "Unsure" + 562, 563, 564, 565 -> "RollEyes" + 567, 568, 569, 570 -> "Cheerful" + 571, 572, 573, 574 -> "Surprised" + 575, 576, 577, 578 -> "Uncertain" + 588, 589, 590, 591 -> "Talk" + 592, 593, 594, 595 -> "Suspicious" + 596, 597, 598, 599 -> "Afraid" + 600, 601, 602, 603 -> "Drunk" + 605, 606, 607, 608 -> "Chuckle" + 609 -> "EvilLaugh" + 610, 611, 612, 613 -> "Upset" + 614, 615, 616, 617 -> "Furious" + else -> animation.toString() + } + } + + private fun toFunction(message: String?): String { + val t = CharMatcher.javaLetterOrDigit().or(CharMatcher.whitespace()).retainFrom(message).replace("\\s".toRegex(), "_").uppercase(Locale.getDefault()) + return CaseFormat.UPPER_UNDERSCORE + .to(CaseFormat.LOWER_CAMEL, t) + } +} \ No newline at end of file diff --git a/tools/src/main/kotlin/world/gregs/voidps/tools/wiki/dialogue/DialogueParsing.kt b/tools/src/main/kotlin/world/gregs/voidps/tools/wiki/dialogue/DialogueParsing.kt index 331ff26386..2cd1241ca9 100644 --- a/tools/src/main/kotlin/world/gregs/voidps/tools/wiki/dialogue/DialogueParsing.kt +++ b/tools/src/main/kotlin/world/gregs/voidps/tools/wiki/dialogue/DialogueParsing.kt @@ -9,6 +9,7 @@ import java.util.* /** * Parses dialogue yaml files into code to be queried + * https://mega.nz/file/sJsRlDzI#E5U0ObIAz9CrAD2HWmJLaALdvrQyR2oaF2gcbWe5qVE */ object DialogueParsing {