Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7ff3ffe
feat: Rs3Prox and a fair amount of decoders and transcribers. Current…
AmVoidGuy Aug 22, 2026
5f1597d
chore: Updated server prots with real names
AmVoidGuy Aug 24, 2026
8784783
feat: Cleaning up zones some more, and some alt methods for decoders
AmVoidGuy Aug 27, 2026
c3a28f6
feat: Some map proj anim field identification and cleanup
AmVoidGuy Aug 28, 2026
8daee0b
feat: Linux test
AmVoidGuy Aug 28, 2026
dd9f6be
feat: Cleaned up client loading logic, no longer needs launcher or ho…
AmVoidGuy Aug 29, 2026
dd614fd
feat: Refactored for multiple revision support, synced up 950
AmVoidGuy Sep 11, 2026
5c51ee7
feat: update rs3 packets
Z-Kris Sep 16, 2026
16f76c3
feat: properly map rs3 worlds to loopback addresses
Z-Kris Sep 17, 2026
a7235a4
feat: rs3 binary writing
Z-Kris Sep 17, 2026
3109806
fix: redirect rs3 ports
Z-Kris Sep 18, 2026
ce82cef
feat: rs3 branding/icon
Z-Kris Sep 18, 2026
8300a37
feat: link up rs3's dynamic data (bandwidth, name, status etc)
Z-Kris Sep 18, 2026
f92d441
feat: rs3 reconnect support
Z-Kris Sep 18, 2026
782e2de
feat: sanitize sensitive rs3 packets out of the binary
Z-Kris Sep 18, 2026
e07aea1
fix: grey out rs3 proxy target selection
Z-Kris Sep 18, 2026
de78f71
fix: repair rs3 extended infos and zone prots
Z-Kris Sep 19, 2026
5898d87
feat: load missing gamevals
Z-Kris Sep 19, 2026
d35611d
refactor: hide unused packet variables
Z-Kris Sep 19, 2026
258c6bf
refactor: more sensible defaults for properties
Z-Kris Sep 19, 2026
bad4cfc
refactor: remove partial 949 support
Z-Kris Sep 19, 2026
9b74e2d
refactor: rename obfuscated field in rebuild packets
Z-Kris Sep 19, 2026
1509b15
feat: add a setting to skip rs3 lobby logging
Z-Kris Sep 19, 2026
a729eb9
refactor: proper rebuild_normal logging for worldarea/minmax coords
Z-Kris Sep 19, 2026
c89aad4
refactor: migrate rs3 transcribers to the correct module
Z-Kris Sep 19, 2026
943c27f
feat: add a launcher warning for RS3
Z-Kris Sep 19, 2026
0a2a56b
docs: update docs for rs3 support
Z-Kris Sep 19, 2026
9a7ae66
feat: add proper rs3 launcher support to maintain client properties
Z-Kris Sep 19, 2026
b424827
refactor: lint project
Z-Kris Sep 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
276 changes: 200 additions & 76 deletions README.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package net.rsprox.cache.api.rs3

/** A session-pinned snapshot. Lookups must not perform network I/O. */
public interface Rs3AppearanceDefinitions {
public val equipmentSlotKinds: List<Int>

public fun getItem(id: Int): Rs3AppearanceItem
}

/** Only definition fields which determine appearance wire layout are retained. */
public data class Rs3AppearanceItem(
public val maleBodyModels: List<Int>,
public val femaleBodyModels: List<Int>,
public val maleHeadModels: List<Int>,
public val femaleHeadModels: List<Int>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package net.rsprox.cache.api.rs3

/** Immutable metadata snapshot. Implementations must not perform I/O on packet-decoder threads. */
public interface Rs3PacketDefinitions : Rs3AppearanceDefinitions {
public fun getNpc(id: Int): Rs3NpcDefinition

public fun getVarbit(id: Int): Rs3VarbitDefinition

public fun getQuickChatPhrase(id: Int): Rs3QuickChatPhrase

public fun getVariable(
domain: Rs3VariableDomain,
id: Int,
): Rs3VariableDefinition
}

public enum class Rs3VariableDomain(
public val group: Int,
) {
PLAYER(60),
CLIENT(62),
CLAN(66),
PLAYER_GROUP(80),
}

public data class Rs3NpcDefinition(
public val recolourCount: Int,
public val retextureCount: Int,
public val morph: Rs3NpcMorph? = null,
/** Zero-based palette slots, in customisation wire order (cache opcodes 44/45). */
public val recolourSlots: List<Int> = List(recolourCount) { it },
public val retextureSlots: List<Int> = List(retextureCount) { it },
)

public data class Rs3NpcMorph(
public val varbit: Int,
public val varp: Int,
/** Last entry is the fallback, including -1 (invisible). */
public val types: List<Int>,
)

public data class Rs3VarbitDefinition(
public val domain: Int,
public val base: Int,
public val startBit: Int,
public val endBit: Int,
)

public enum class Rs3BaseVarType {
INT,
LONG,
STRING,
COORDINATE,
}

public data class Rs3VariableDefinition(
public val scriptType: Int,
public val baseType: Rs3BaseVarType,
)

public data class Rs3QuickChatPhrase(
public val template: String,
public val commands: List<Rs3QuickChatCommand>,
)

public data class Rs3QuickChatCommand(
public val id: Int,
public val transmitBytes: Int,
public val receiveBytes: Int,
public val arguments: List<Int>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package net.rsprox.cache.api.type

public fun interface ClientScriptDefinitionProvider {
public fun getClientScriptDefinition(id: Int): ClientScriptDefinition?

public companion object {
public val EMPTY: ClientScriptDefinitionProvider = ClientScriptDefinitionProvider { null }
}
}
3 changes: 2 additions & 1 deletion cache/src/main/kotlin/net/rsprox/cache/OldSchoolCache.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ public class OldSchoolCache(
private lateinit var varbits: Map<Int, VarBitType>
private val clientScripts = RSProxArchiveClientScriptIndex(masterIndex)

override fun getClientScriptDefinition(id: Int): ClientScriptDefinition? = clientScripts.get(id)
override fun getClientScriptDefinition(id: Int): ClientScriptDefinition? =
clientScripts.getClientScriptDefinition(id)

override fun getNpcType(id: Int): NpcType? {
if (!this::npcs.isInitialized) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package net.rsprox.cache.clientscript

public enum class ClientScriptGame(
internal val archiveGame: String,
internal val namespace: String,
) {
OLD_SCHOOL("oldschool", ""),
RUNESCAPE("runescape", "rs3"),
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,33 @@ import net.rsprox.cache.CLIENTSCRIPT_INDEXES_DIRECTORY
import net.rsprox.cache.Js5MasterIndex
import net.rsprox.cache.api.type.ClientScriptArgument
import net.rsprox.cache.api.type.ClientScriptDefinition
import net.rsprox.cache.api.type.ClientScriptDefinitionProvider
import net.rsprox.cache.util.atomicWrite
import net.rsprox.cache.util.mapper
import java.net.URI
import java.nio.ByteBuffer
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.isRegularFile

internal class RSProxArchiveClientScriptIndex(
public class RSProxArchiveClientScriptIndex(
private val masterIndex: Js5MasterIndex,
) {
private val game: ClientScriptGame = ClientScriptGame.OLD_SCHOOL,
) : ClientScriptDefinitionProvider {
private val scripts: Map<Int, ClientScriptDefinition> by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
load()
}

fun get(id: Int): ClientScriptDefinition? = scripts[id]
override fun getClientScriptDefinition(id: Int): ClientScriptDefinition? = scripts[id]

/** Resolve on the launch/transcription worker, not the relay event loop or Swing thread. */
public fun preload() {
scripts
}

private fun load(): Map<Int, ClientScriptDefinition> {
val sha256 = masterIndex.sha256()
val path = CLIENTSCRIPT_INDEXES_DIRECTORY.resolve(sha256).resolve(INDEX_FILE)
val path = CLIENTSCRIPT_INDEXES_DIRECTORY.resolve(game.namespace).resolve(sha256).resolve(INDEX_FILE)
if (path.isRegularFile()) {
try {
return parse(Files.readAllBytes(path), sha256).also { scripts ->
Expand All @@ -51,7 +59,8 @@ internal class RSProxArchiveClientScriptIndex(
path: Path,
sha256: String,
): Map<Int, ClientScriptDefinition> {
val uri = URI("$ARCHIVE_BASE_URL/clientscripts/$sha256/$INDEX_FILE")
val prefix = listOf("clientscripts", game.namespace).filter(String::isNotEmpty).joinToString("/")
val uri = URI("$ARCHIVE_BASE_URL/$prefix/$sha256/$INDEX_FILE")
logger.info { "Downloading clientscript definitions from $uri" }
val connection = uri.toURL().openConnection()
connection.connectTimeout = CONNECT_TIMEOUT_MS
Expand All @@ -72,7 +81,7 @@ internal class RSProxArchiveClientScriptIndex(
require(index.version == INDEX_VERSION && index.kind == INDEX_KIND) {
"Unsupported clientscript index schema ${index.kind} version ${index.version}"
}
require(index.game == OLDSCHOOL_GAME) { "Unexpected clientscript index game: ${index.game}" }
require(index.game == game.archiveGame) { "Unexpected clientscript index game: ${index.game}" }
// Don't allow this check as it breaks during revision transitions.
// require(index.revision == masterIndex.revision) {
// "Clientscript index revision ${index.revision} does not match cache revision ${masterIndex.revision}"
Expand All @@ -96,12 +105,28 @@ internal class RSProxArchiveClientScriptIndex(
}
}

private companion object {
public companion object {
/** RS3 live snapshots and recording headers contain the uncompressed master index. */
public fun forRuneScape(
revision: Int,
masterIndex: ByteArray,
): RSProxArchiveClientScriptIndex {
require(masterIndex.isNotEmpty()) { "Missing RS3 master index for clientscript metadata" }
// OpenRS2/RSProx Archive identify the complete, uncompressed JS5 container (type + length + data).
val container =
ByteBuffer
.allocate(5 + masterIndex.size)
.put(0.toByte())
.putInt(masterIndex.size)
.put(masterIndex)
.array()
return RSProxArchiveClientScriptIndex(Js5MasterIndex(revision, container), ClientScriptGame.RUNESCAPE)
}

private const val ARCHIVE_BASE_URL: String = "https://archive.rsprox.net"
private const val INDEX_FILE: String = "index.json"
private const val INDEX_VERSION: Int = 1
private const val INDEX_KIND: String = "clientscript-index"
private const val OLDSCHOOL_GAME: String = "oldschool"
private const val CONNECT_TIMEOUT_MS: Int = 8_000
private const val READ_TIMEOUT_MS: Int = 30_000
private val logger = InlineLogger()
Expand Down
107 changes: 107 additions & 0 deletions cache/src/main/kotlin/net/rsprox/cache/rs3/Rs3AppearanceItemDecoder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package net.rsprox.cache.rs3

import net.rsprox.cache.api.rs3.Rs3AppearanceItem
import java.nio.ByteBuffer

/** Revision-950 obj definitions. Unused fields are consumed by schema, never by a remainder guess. */
internal object Rs3AppearanceItemDecoder {
fun decode(
id: Int,
bytes: ByteArray,
resolve: ((Int) -> Rs3AppearanceItem)? = null,
): Rs3AppearanceItem {
val input = ByteBuffer.wrap(bytes)

fun byte(): Int = input.get().toInt() and 255

fun word(): Int = input.short.toInt() and 65535

fun smart(): Int =
if (input.get(input.position()) < 0) {
input.int and Int.MAX_VALUE
} else {
word().let {
if (it == 32767) -1 else it
}
}

fun skip(count: Int) {
require(count in 0..input.remaining()) { "Truncated RS3 item $id" }
input.position(input.position() + count)
}

fun string() {
while (byte() != 0) Unit
}
val maleBody = MutableList(3) { -1 }
val femaleBody = MutableList(3) { -1 }
val maleHead = MutableList(2) { -1 }
val femaleHead = MutableList(2) { -1 }

fun tri(): Int = (byte() shl 16) or (byte() shl 8) or byte()
var lendLink = -1
var lendTemplate = -1
var boughtLink = -1
var boughtTemplate = -1
while (true) {
when (val opcode = byte()) {
0 -> {
require(!input.hasRemaining()) { "Trailing RS3 item $id definition bytes" }
// Native 0x78d510 -> 0x78cfa0: lent (kind 1), then bought (kind 2),
// copy all wearable models from the linked item, never the inventory template.
val link =
when {
boughtTemplate != -1 -> boughtLink
lendTemplate != -1 -> lendLink
else -> -1
}
if (boughtTemplate != -1 || lendTemplate != -1) {
require(link >= 0) { "RS3 item $id has a wearable template without a link" }
return checkNotNull(resolve) { "RS3 item $id needs linked item $link" }(link)
}
return Rs3AppearanceItem(maleBody, femaleBody, maleHead, femaleHead)
}
23 -> maleBody[0] = smart()
24 -> maleBody[1] = smart()
78 -> maleBody[2] = smart()
25 -> femaleBody[0] = smart()
26 -> femaleBody[1] = smart()
79 -> femaleBody[2] = smart()
90 -> maleHead[0] = smart()
92 -> maleHead[1] = smart()
91 -> femaleHead[0] = smart()
93 -> femaleHead[1] = smart()
1, in 46..56 -> smart()
9 -> repeat(byte()) { smart() }
2, 3, in 30..39, 131, 164 -> string()
11, 15, 16, 65, 156, 157, 165, 167, 168, 178 -> Unit
13, 14, 27, 96, 113, 114, 115, 134 -> byte()
4, 5, 6, 7, 8, 10, 44, 45, 94, 95, 97, 98, 110, 111, 112,
148, 149, in 142..146, in 150..154, 161, 162, 163,
-> word()
121 -> lendLink = word()
122 -> lendTemplate = word()
139 -> boughtLink = word()
140 -> boughtTemplate = word()
203 -> lendLink = tri()
204 -> lendTemplate = tri()
205 -> boughtLink = tri()
206 -> boughtTemplate = tri()
12, 43, 69, in 100..109 -> skip(4)
40, 41 -> repeat(byte()) { skip(4) }
42 -> skip(byte())
125, 126, in 127..130, 182, 201, 202, 207, 208 -> skip(3)
132 -> repeat(byte()) { word() }
181 -> skip(8)
in 190..199 -> skip(5)
249 ->
repeat(byte()) {
val isString = byte() == 1
skip(3)
if (isString) string() else skip(4)
}
else -> error("Unknown RS3 item $id definition opcode $opcode")
}
}
}
}
Loading
Loading