├── .gitignore ├── README.md ├── _config.yml ├── content ├── combat │ └── index.md ├── index.md ├── poison │ └── index.md └── queue │ └── index.md ├── functionality ├── index.md └── map │ ├── game-map.png │ └── index.md ├── index.md ├── naming └── index.md ├── protocol ├── data-types │ ├── ModifiedEndianIO.kts │ ├── endian.png │ └── index.md ├── index.md └── rs2011 │ ├── index.md │ └── rs667 │ ├── rs667-client-protocol.md │ └── rs667-server-protocol.md └── runescript └── index.md /.gitignore: -------------------------------------------------------------------------------- 1 | # ide files 2 | /target 3 | *.iml 4 | *.project 5 | *.classpath 6 | .idea/* 7 | build/* 8 | build 9 | rebel.xml 10 | rebel-remote.xml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## RuneDocs 2 | _Documented information about the entire RuneScape game._ 3 | 4 | #### [Content](./content) 5 | Combat 6 | 7 | Poison 8 | 9 | Queue 10 | 11 | #### [Functionality](./functionality) 12 | 13 | 14 | 15 | #### [Naming](./naming) 16 | 17 | #### [Protocol](./protocol) 18 | 19 | #### [RuneScript](./runescript) 20 | 21 | 22 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /content/combat/index.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuneDocs/docs/9985421bfdccf0d1cae8c87bbdc7a95af0ca4934/content/combat/index.md -------------------------------------------------------------------------------- /content/index.md: -------------------------------------------------------------------------------- 1 | # Content 2 | 3 | #### [Combat](./combat) 4 | 5 | #### [Poison](./poison) 6 | 7 | #### [Queue](./queue) -------------------------------------------------------------------------------- /content/poison/index.md: -------------------------------------------------------------------------------- 1 | # Poison 2 | 3 | Poison is a suspendable action, in that it can be paused and resumed. 4 | 5 | Known interactions that pause poison: 6 | 1. Main screen interface being open 7 | 8 | Example: https://streamable.com/ivwzky 9 | -------------------------------------------------------------------------------- /content/queue/index.md: -------------------------------------------------------------------------------- 1 | # Queues 2 | 3 | Most in game actions are queued. 4 | 5 | The priority of a queued action is either weak, normal, strong 6 | 7 | > A weakqueue is cancelled if the player clicks away - commonly used for Make-X. Queues are weak / strong / normal. 8 | > https://github.com/RuneStar/leaks/blob/master/153.txt 9 | > 10 | > A standard one will wait if you have a menu open, and execute when it closes. A strong one will close menus to execute itself sooner. 11 | > https://github.com/RuneStar/leaks/blob/master/142.txt 12 | 13 | 14 | Both npcs and players have queues 15 | 16 | Runescript queue has the format `queue(type, tick delay)(parameters)` 17 | 18 | [npc_queue(npc_say,2)](https://github.com/RuneStar/leaks/blob/master/102.0.jpg) 19 | 20 | [combat_clearqueue & player_end_poison](https://github.com/RuneStar/leaks/blob/master/176.0.jpg) 21 | 22 | [weakqueue*(smith_generic,3)](https://github.com/RuneStar/leaks/blob/master/205.0.png) 23 | 24 | [clearqueue(fade_clear)](https://github.com/RuneStar/leaks/blob/master/242.spawns-runescript.png) 25 | 26 | [soul-wars bandage heal queue](https://github.com/RuneStar/leaks/blob/master/319.SoulWars8.png) 27 | 28 | ## Ai queues 29 | 30 | Are they related? 31 | 32 | | Id | Description | 33 | |---|---| 34 | | 1 | Retaliation | 35 | | 2 | Damage | 36 | | 3 | Bind effects | 37 | | 4..20 | Custom | 38 | [ai_queue](https://twitter.com/Chrischis2/status/644620927519617024) 39 | 40 | 41 | ## Weak - cancelled by movement 42 | 43 | * Make-X 44 | * Smithing 45 | * Crafting 46 | * Fletching 47 | * Herblore 48 | * Bones on altar 49 | * Runecrafting 50 | * Cooking 51 | * Fishing 52 | * Mining 53 | * Farming 54 | * Summoning infuse pouches 55 | * Gathering 56 | * Woodcutting 57 | * Firemaking 58 | * Normal emotes 59 | * Sitting home teleport 60 | * Movement - (interaction) 61 | * Filling vials 62 | 63 | ## Normal - Waits for interfaces 64 | 65 | * Level up (gfx, sound, dialogue) 66 | * Poison - Not cancelled 67 | * ~~XP drops - Not cancelled~~ 68 | * ~~Consuming food and potions - Not cancelled~~ 69 | * ~~Container modifications - Not cancelled~~ 70 | * Movement - (walk to tile, follow) 71 | * Combat - Cancellable (attacking) 72 | * ~~Most dialogues~~ 73 | 74 | ## Strong - Closes interfaces 75 | Cancels Weak, suspends normal 76 | * Hits 77 | * Death 78 | * Teleports 79 | 80 | ## None 81 | 82 | * Hitpoint renewal 83 | 84 | ## Interaction Movement 85 | Can't be suspended if right next too - means there's a pre-movement check 86 | Can be suspended if have to move to reach target - Movement isn't suspended like walk to tile 87 | Only walk to tile can have movement-suspended on the first tick 88 | 89 | ## Food 90 | Doesn't stop movement, stops action at end of movement 91 | 92 | Pot + food doesn't work in the same tick, only food + pot 93 | 94 | Kara + food takes normal delay 95 | 1 tick healing must be Food + Pot + Karambwan 96 | 97 | ## Bury bones 98 | Stops movement, but movement is allowed to start after first tick (animations combine and still get xp later) 99 | 100 | ## Drop item & alching 101 | Doesn't stop movement, does stop action at the end of movement 102 | 103 | All actions cancels walking and combat, closes interfaces 104 | 105 | * Emotes 106 | * Bury bones 107 | * Dropping items 108 | * Using one item on another 109 | * Using item on a npc 110 | 111 | > Appears all player actions close interfaces 112 | 113 | ## Interfaces 114 | 115 | ### Interfaces that pause movement 116 | 117 | * Skill guides 118 | * ~~Dialogues~~ (Is it dialogues or is it leveling up itself?) 119 | 120 | > Opening a skill guide will stall an action from performing e.g attacking, picking up an item until the interface is closed 121 | 122 | ### Interfaces that don't stop movement 123 | 124 | * Quest guides 125 | * Achievement guides 126 | * Clan setup 127 | 128 | ### Interfaces that stop movement 129 | 130 | * Settings 131 | * Equipment/Price Checker/Items kept on death 132 | 133 | # Questions 134 | 135 | * Are delays handled before or as part of the queue? 136 | * If all actions clear queues, what about poison? 137 | * What queue priorities cancel what? - Cancels all priority queues below it, except poisons? see above 138 | * Are interfaces part of the queue? 139 | 140 | * How do actions which a player can't click out of work? (Obstacles, Ectofunctus) - Just queue 3 actions with the correct delays, unfreezing the player after 141 | * How do animations play into this? (Teleporting, Emotes) - Not all emotes suspend hits, most suspend movement? 142 | * Are dialogues part of the queue? - Yes definitely, level up suspends combat 143 | * If something is added to the queue by a queue action, does it activate on the same tick or the next one? - Same, delay depending. 144 | * When is a queue processed, is it each tick or more frequently? 145 | 146 | # Notes 147 | 148 | * Effects 149 | * Passive 150 | * Godwars snow 151 | * Monkey madness tunnel rock-fall 152 | * Barrows crypt crumble 153 | * Active 154 | * Spells (confuse, bind, etc..) 155 | * Prayers (leech, boost etc..) 156 | * Poison 157 | * Granite maul special; click the spec bar twice, switch to a different weapon, hit, switch back to granite maul and it would auto spec 158 | * Karambwan/pizzas/pies/chocolate bomb/tangled toad legs - doesn't clear queue, separate from regular food 159 | * Potions and food are separate actions but on the same priority 160 | * Equipping and 1-tick changing 161 | * D-Spear (suspends hits) 162 | * Level teleport interface stall - configure exp drops 163 | 164 | first two foods are cooked at 3 ticks ea, ones after that cook at 4 ticks 165 | most hits are determined upon added to damage queue 166 | combat delay is that of the last weapon attacked with 167 | -------------------------------------------------------------------------------- /functionality/index.md: -------------------------------------------------------------------------------- 1 | # Functionality 2 | The term functionality in RuneScape applies to how things function in the game. For example, when a player interacts with an object in a client, an `Action`. 3 | 4 | #### [Map](/combat) -------------------------------------------------------------------------------- /functionality/map/game-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuneDocs/docs/9985421bfdccf0d1cae8c87bbdc7a95af0ca4934/functionality/map/game-map.png -------------------------------------------------------------------------------- /functionality/map/index.md: -------------------------------------------------------------------------------- 1 | # Game Map 2 | 3 | **Note**: At the time of this writing, all code examples are in Java. This is to be updated to Kotlin later. 4 | 5 | ## Layout 6 | 7 | The game map in RuneScape is made out of a three dimensional box of exactly four altitudes or height levels where each height level holds a symmetrical two dimensional grid of 16384 x 16384 tiles. Each grid on a height level is divided into two separate static units: 8 | 9 | - Map squares 10 | - Zones 11 | 12 | A map square is a 64 x 64 area of tiles. In contrast with a map square, a zone is a more fine-grained area consisting of 8 x 8 tiles. This means that each map square can hold up to 8 x 8 zones. 13 | 14 | ![Game Map](game-map.png) 15 | - *Credits to Greg for the illustration* 16 | 17 | The RuneScape game cache stores all objects, obstacles, buildings, tiles and such per map squares. For efficiency however, the server should store all deserialized objects per zone. This allows the server to efficiently read-and manipulate zones to provide certain features of the game. This includes (but is not limited to) instancing (player owned houses, minigames, random events), searching for nearby players / npcs etc. 18 | 19 | Conceptually, the grid of the game world can be modeled as a three dimensional array of: 20 | 21 | ```java 22 | private static final int 23 | SIZE = 16384, 24 | ZONE_COUNT = SIZE / 8, // 16384 tiles on each axis divided by 8 tiles per zone 25 | ALTITUDE_COUNT = 4; 26 | 27 | private final Zone[][][] zones = new Zone[ALTITUDE_COUNT][ZONE_COUNT][ZONE_COUNT]; 28 | ``` 29 | 30 | Alternative and arguably more memory efficient ways involve using a `HashMap` but that's a mere detail. 31 | 32 | ## Build Area 33 | 34 | The build area is a limited view of the map the player can roam freely. Once the player reaches the edge of the build area, a rebuild is required which is to update the player's view. The build area consists of 104 x 104 tiles and showcases events that are currently happening in the game in that particular part of the map. As the build area consists of 104 x 104 tiles and each zone being 8 x 8 tiles, our build area consists of 13 x 13 zones. When a rebuild occurs, the build area recenters itself around the player's current position. The center of the build area is calculated by dividing the view in two which equals 6 zones (48 tiles, exclusively). This means that the player's position local to the build area, right after a rebuild will always be in zone 7, 7 (48, 48). 35 | 36 | The margin that defines the edges of the build area is 16 tiles, which means that we can check if a rebuild is required by: 37 | 38 | ```java 39 | private boolean rebuildRequired(Position position) { 40 | int x = getX(position); 41 | int z = getZ(position); 42 | 43 | boolean reachedLowerEdge = x < 16 || z < 16; 44 | boolean reachedUpperEdge = x >= 88 || z >= 88; 45 | 46 | return reachedLowerEdge || reachedUpperEdge; 47 | } 48 | 49 | public int getX(Position p) { 50 | return p.getX() - ((lastRebuild.getZoneX() - RADIUS) * Zone.SIZE); 51 | } 52 | 53 | public int getZ(Position p) { 54 | return p.getZ() - ((lastRebuild.getZoneZ() - RADIUS) * Zone.SIZE); 55 | } 56 | ``` 57 | 58 | The methods `getX()` and `getZ()` get the player's current x and z coordinates within the build area, respectively. As is noticable by the `lastRebuild` field, the `BuildArea` keeps track of where it last has been updated to compare whether it should rebuild again using a given `Position` as its new center. 59 | 60 | ```java 61 | private static final int RADIUS = 6; 62 | 63 | private void doRebuild(World world, Position position) { 64 | int centerZoneX = position.getZoneX(); 65 | int centerZoneZ = position.getZoneZ(); 66 | 67 | int bottomLeftZoneX = centerZoneX - RADIUS; 68 | int bottomLeftZoneZ = centerZoneZ - RADIUS; 69 | 70 | for (int altitude = 0; altitude < ALTITUDE_COUNT; altitude++) { 71 | for (int localZoneX = 0; localZoneX < ZONE_COUNT; localZoneX++) { 72 | for (int localZoneZ = 0; localZoneZ < ZONE_COUNT; localZoneZ++) { 73 | int zoneX = bottomLeftZoneX + localZoneX; 74 | int zoneZ = bottomLeftZoneZ + localZoneZ; 75 | 76 | zones[altitude][localZoneX][localZoneZ] = world.get(altitude, zoneX, zoneZ); 77 | } 78 | } 79 | } 80 | 81 | lastRebuild = position; 82 | 83 | notifyRebuild(centerZoneX, centerZoneZ); 84 | } 85 | ``` 86 | 87 | A rebuild will also involve refreshing the entities ALL zones within an arbitrary radius in the new build area. Every element within each zone (floor item stacks and objects) is visually removed from the client and re-applied. The server doesn't actually manually remove each element per-packet but rather sends a packet that clears an entire zone of its elements. After sending this packet, all elements that are still within that zone are re-sent. Post-rebuild the server will keep track of updates within the avatar's 7 x 7 area of zones until another rebuild occurs. 88 | 89 | ```java 90 | private void refreshAllZones(Position center) { 91 | clearPendingUpdates(); 92 | 93 | for (int localZoneX = 0; localZoneX < ZONE_COUNT; localZoneX++) { 94 | for (int localZoneZ = 0; localZoneZ < ZONE_COUNT; localZoneZ++) { 95 | Zone zone = zones[center.getAltitude()][localZoneX][localZoneZ]; 96 | if (zone == null) { 97 | continue; 98 | } 99 | 100 | refreshZone(zone); 101 | } 102 | } 103 | } 104 | 105 | private void refreshZone(Zone zone) { 106 | clearZone(zone.getPosition()); 107 | 108 | for (int tileX = 0; tileX < Zone.SIZE; tileX++) { 109 | for (int tileZ = 0; tileZ < Zone.SIZE; tileZ++) { 110 | FloorItemStack itemStack = zone.getFloorItemStack(tileX, tileZ); 111 | if (itemStack == null || itemStack.isEmpty()) { 112 | continue; 113 | } 114 | 115 | for (Item item : itemStack) { 116 | spawnFloorItem(itemStack.getPosition(), item); 117 | } 118 | } 119 | } 120 | 121 | Collection dynamicLocs = zone.getLocs(); 122 | for (Loc loc : dynamicLocs) { 123 | spawnLoc(loc); 124 | } 125 | } 126 | ``` 127 | 128 | ## Instancing 129 | 130 | In the past, RuneScape private servers would attempt to emulate instancing by placing players on specific height levels by using a bit value overflow trick that the client ended up treating as the bottom height level (0). However, this isn't how Jagex solved the instancing problem. An instance is generated by searching the map for an empty spot. The spot should be the size of the instance in tiles. So for example, if an instance such as the Drill Demon event occupies exactly one map square, we are to copy over all 8 x 8 zones within that map square to our empty area. The empty area may be at 0,0 which means that our event map is copied over to 0,0 and our player is to be teleported there as well when the event occurs. Once the player is done with the event, the player is teleported out of there back to the main land and the temporary instance is cleared from our reserved area to be reused by another event or instance. **Note** that OldSchool RuneScape starts searching for empty spots at an X coordinate of 6400 as CS2 scripts require this for minigames such as Theatre of Blood. 131 | 132 | In order to create an instance, the game searches for an empty spot that zones can be copied over to. In many cases, the game will prematurely calculate how many zones or map squares an instance will occupy. This may vary however in areas such as Chambers of Xeric and Theatre of Blood. 133 | 134 | ```java 135 | private static final int 136 | OFFSET_X = 6400, 137 | OFFSET_Z = 0; 138 | 139 | public Position findEmptyArea(int altitude, int width, int length) { 140 | int mapSpanX = width / MapSquare.SIZE; 141 | int mapSpanZ = length / MapSquare.SIZE; 142 | 143 | VerticalSearch: 144 | for (int z = OFFSET_Z; z < SIZE; z += length) { 145 | HorizontalSearch: 146 | for (int x = OFFSET_X; x < SIZE; x += width) { 147 | int mapX = x / MapSquare.SIZE; 148 | int mapZ = z / MapSquare.SIZE; 149 | 150 | for (int i = mapX; i < mapX + mapSpanX; i++) { 151 | int zoneX = i * Zone.SIZE; 152 | int zoneZ = mapZ * Zone.SIZE; 153 | 154 | Zone zone = zones[altitude][zoneX][zoneZ]; 155 | if (zone != null) { 156 | continue HorizontalSearch; 157 | } 158 | } 159 | 160 | for (int i = mapZ; i < mapZ + mapSpanZ; i++) { 161 | int zoneX = mapX * Zone.SIZE; 162 | int zoneZ = i * Zone.SIZE; 163 | 164 | Zone zone = zones[altitude][zoneX][zoneZ]; 165 | if (zone != null) { 166 | continue VerticalSearch; 167 | } 168 | } 169 | 170 | return Position.abs(x, z, altitude); 171 | } 172 | } 173 | 174 | return null; 175 | } 176 | ``` 177 | 178 | Once a suitable area has been found somewhere on the map, the target zone can be copied. It is important that the collision matrix is included in the copy. 179 | 180 | ```java 181 | public Zone copy(Position position, Rotation rotation) { 182 | Zone original = Objects.requireNonNull(get(position)); 183 | Zone copy = Zone.at(true, position, rotation); 184 | for (int x = 0; x < Zone.SIZE; x++) { 185 | for (int z = 0; z < Zone.SIZE; z++) { 186 | copy.getCollisionMatrix().set(x, z, original.getCollisionMatrix().get(x, z)); 187 | } 188 | } 189 | 190 | return copy; 191 | } 192 | ``` 193 | 194 | And the copied zone can then be pasted at the designated location of our empty area. 195 | 196 | ```java 197 | public void paste(Position target, Zone zone) { 198 | if (get(target) != null) { 199 | throw new IllegalArgumentException(); 200 | } 201 | 202 | zone.setPosition(Position.abs(target.getZoneX() * Zone.SIZE, target.getZoneZ() * Zone.SIZE, target.getAltitude())); 203 | put(zone); 204 | } 205 | ``` 206 | 207 | A zone can also be rotated in our dynamic construct (for rooms in Player Owned Houses). This means that every flag in the collision matrix has to be shifted by X degrees as well. For example, clockwise rotating a matrix by 90 degrees can be implemented as: 208 | 209 | ```java 210 | public void rotate() { 211 | int[][] updated = new int[getWidth()][getLength()]; 212 | for (int x = 0; x < getWidth(); ++x) { 213 | for (int z = 0; z < getLength(); ++z) { 214 | updated[x][z] = flags[getWidth() - z - 1][x]; 215 | } 216 | } 217 | 218 | flags = updated; 219 | } 220 | ``` 221 | 222 | The client then needs to showcase these changes. There are two types of packets that the client supports. These are called static rebuild and dynamic rebuild, respectively. The static rebuild packet is used when there are no dynamic changes (such as zones being copied over) to the map. This will tell the client to draw the regular map based on a given tile position. For most of the time, the static rebuild packet is used. 223 | 224 | ```java 225 | public static EventEncoder encoder() { 226 | return (out, evt) -> { 227 | out.writeShort(evt.getCenterZoneX()); 228 | out.writeShort(evt.getCenterZoneZ()); 229 | 230 | out.writeShort(evt.getKeySets().size()); 231 | for (MapSquareConfig.KeySet keySet : evt.getKeySets()) { 232 | for (int key : keySet) { 233 | out.writeInt(key); 234 | } 235 | } 236 | }; 237 | } 238 | ``` 239 | 240 | The dynamic rebuild packet on the other hand will draw the zones that have been copied over to our empty area. 241 | 242 | ```java 243 | public static EventEncoder encoder() { 244 | return (out, evt) -> { 245 | out 246 | .writeBoolean(evt.requireImmediateRebuild()) 247 | .writeShort(evt.getCenterZoneX()) 248 | .writeShort(evt.getCenterZoneZ()) 249 | .writeShort(evt.getKeySets().size()); 250 | 251 | bitBlock(BitAccessType.WRITE, out, bitIndex -> { 252 | for (int altitude = 0; altitude < evt.getPalette().getHeight(); altitude++) { 253 | for (int localZoneX = 0; localZoneX < evt.getPalette().getWidth(); localZoneX++) { 254 | for (int localZoneZ = 0; localZoneZ < evt.getPalette().getLength(); localZoneZ++) { 255 | BuildAreaPalette.Zone zone = evt.getPalette().get(altitude, localZoneX, localZoneZ); 256 | 257 | writeBit(out, bitIndex, zone != null); 258 | if (zone != null) { 259 | writeBits(out, bitIndex, 26, zone.getAltitude() << 24 | 260 | zone.getRotation() << 1 | 261 | zone.getOriginX() << 14 | 262 | zone.getOriginZ() << 3); 263 | } 264 | } 265 | } 266 | } 267 | }); 268 | 269 | for (MapSquareConfig.KeySet keySet : evt.getKeySets()) { 270 | for (int key : keySet) { 271 | out.writeInt(key); 272 | } 273 | } 274 | }; 275 | } 276 | ``` -------------------------------------------------------------------------------- /index.md: -------------------------------------------------------------------------------- 1 | ## RuneDocs 2 | _Documented information about the entire RuneScape game._ 3 | 4 | #### Content 5 | 1. [Combat](./content/combat) 6 | 2. [Poison](./content/poison) 7 | 3. [Queue](./content/queue) 8 | 9 | #### Functionality 10 | 11 | 1. [Map](./functionality/map) 12 | 13 | #### Naming 14 | 15 | #### Protocol 16 | 17 | 1. [Data Types](./protocol/data-types) 18 | 2. [RS2011](./protocol/rs2011) 19 | 20 | #### RuneScript 21 | 22 | 23 | -------------------------------------------------------------------------------- /naming/index.md: -------------------------------------------------------------------------------- 1 | ## Glossary 2 | 3 | The table below provides an explanation of the terms used within the RuneScape client. 4 | 5 | Term: the name chosen by Jagex to define something
6 | Synonym: the word(s) chosen by the community to reference a Jagex term
7 | Explanation: the context in which the term is used, as well as an example 8 | ----- 9 | 10 | | Term | Synonyms | Explanation | 11 | |---------- | :-------------: | :-------------: | 12 | | item 1 | synonym 1 | explanation 1 | 13 | | item 2 | synonym 2 | explanation 2 | 14 | | item 3 | synonym 3 | explanation 3 | -------------------------------------------------------------------------------- /protocol/data-types/ModifiedEndianIO.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("NAME_SHADOWING") 2 | 3 | import java.nio.ByteBuffer 4 | 5 | enum class Endian { 6 | BIG, 7 | LITTLE, 8 | MIDDLE; 9 | 10 | fun getRange(modifier: Modifier, byteCount: Int) = when (this) { 11 | BIG -> byteCount - 1 downTo 0 12 | LITTLE -> 0 until byteCount 13 | MIDDLE -> if (modifier == Modifier.INVERSE) MID_ENDIAN_INVERSE else MID_ENDIAN_ORDER 14 | } 15 | 16 | companion object { 17 | private val MID_ENDIAN_ORDER = listOf(1, 0, 3, 2) 18 | private val MID_ENDIAN_INVERSE = MID_ENDIAN_ORDER.reversed() 19 | } 20 | } 21 | 22 | enum class Modifier { 23 | NONE, 24 | ADD, 25 | INVERSE, 26 | SUBTRACT; 27 | } 28 | 29 | enum class DataType(val byteCount: Int) { 30 | BYTE(1), 31 | SHORT(2), 32 | MEDIUM(3), 33 | INT(4), 34 | LONG(8); 35 | } 36 | 37 | @Throws(IllegalStateException::class) 38 | fun check(type: DataType, modifier: Modifier, order: Endian) { 39 | if (order == Endian.MIDDLE) { 40 | check(modifier == Modifier.NONE || modifier == Modifier.INVERSE) { 41 | "Middle endian doesn't support variable modifier $modifier" 42 | } 43 | check(type == DataType.INT) { 44 | "Middle endian can only be used with an integer" 45 | } 46 | } 47 | } 48 | 49 | fun read(buffer: ByteBuffer, type: DataType, modifier: Modifier = Modifier.NONE, order: Endian = Endian.BIG): Long { 50 | check(buffer.remaining() >= type.byteCount) { 51 | "Not enough allocated buffer remaining $type." 52 | } 53 | 54 | check(type, modifier, order) 55 | 56 | var longValue: Long = 0 57 | var read: Int 58 | for (index in order.getRange(modifier, type.byteCount)) { 59 | read = buffer.get().toInt() 60 | read = when (if(index == 0 && order != Endian.MIDDLE) modifier else Modifier.NONE) { 61 | Modifier.ADD -> read - 128 and 0xff 62 | Modifier.INVERSE -> -read and 0xff 63 | Modifier.SUBTRACT -> 128 - read and 0xff 64 | else -> read and 0xff shl index * 8 65 | } 66 | longValue = longValue or read.toLong() 67 | } 68 | return longValue 69 | } 70 | 71 | fun write(buffer: ByteBuffer, type: DataType, value: Number, modifier: Modifier = Modifier.NONE, order: Endian = Endian.BIG) { 72 | check(type, modifier, order) 73 | 74 | for (index in order.getRange(modifier, type.byteCount)) { 75 | val modifiedValue = when (if (index == 0 && order != Endian.MIDDLE) modifier else Modifier.NONE) { 76 | Modifier.ADD -> value.toInt() + 128 77 | Modifier.INVERSE -> -value.toInt() 78 | Modifier.SUBTRACT -> 128 - value.toInt() 79 | else -> (value.toLong() shr index * 8).toInt() 80 | } 81 | buffer.put(modifiedValue.toByte()) 82 | } 83 | } -------------------------------------------------------------------------------- /protocol/data-types/endian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuneDocs/docs/9985421bfdccf0d1cae8c87bbdc7a95af0ca4934/protocol/data-types/endian.png -------------------------------------------------------------------------------- /protocol/data-types/index.md: -------------------------------------------------------------------------------- 1 | # Data Types 2 | 3 | RuneScape uses a number of uncommon and bespoke data formats to obsfuscate communication and reduce cache file size. 4 | 5 | Each client revision randomises the [endianness](#endianness) and [modifications](#modifications) used for decoding client packets. The OSRS protocol scrambling follows a decipherable order (citation needed). 6 | 7 | ## Standard Java Data Types 8 | RuneScape client uses [Java's primitive data types](https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html) to store and transmit information, understanding these limitations and sizes are important regardless of client revision. 9 | 10 | | Type | Size | Minimum Value | Maximum Value | 11 | |---|---|---|---| 12 | | byte | 1 byte | -128 | 127 | 13 | | short | 2 bytes | -32,768 | 32,767 | 14 | | int | 4 bytes | -2,147,483,648 | 2,147,483,647 | 15 | | long | 8 bytes | -9,223,372,036,854,775,808 | 9,223,372,036,854,775,807 | 16 | | float | 4 bytes | | | 17 | | double | 8 bytes | | | 18 | | boolean | 1 bit | false | true | 19 | | char | 2 bytes | 0 | 65535 | 20 | 21 | > 2,147,483,647 is the maximum stack a player can have of one item because item amounts are stored as integers. 22 | 23 | ### Signed and unsigned 24 | 25 | Data types typically are Signed, easily thought of as a single bit assigned as a binary flag denoting a positive or negative number. However when only positive values are necessary the flag can be unsigned allowing a larger number to be stored in the same amount of space. 26 | 27 | | Type | Size | Minimum Unsigned Value | Maximum Unsigned Value | 28 | |---|---|---|---| 29 | | byte | 1 byte | 0 | 255 | 30 | | short | 2 bytes | 0 | 65,535 | 31 | | int | 4 bytes | 0 | ‭4,294,967,296‬ | 32 | | long | 8 bytes | 0 | ‭18,446,744,073,709,551,616‬ | 33 | 34 | > Methods utilising unsigned integers commonly use 'U' as a prefix e.g. `readUByte` 35 | 36 | ### Bit Access 37 | 38 | The client also utilises reading and writing individual bits interchangeably with other data types. 39 | 40 | > TODO code example needed for both read and write which cleanly breaks down how it works 41 | 42 | ## Obfuscation methods 43 | 44 | ### Endianness 45 | 46 | The read and write order of byte data is called Endianness 47 | RuneScape client occasionally uses a middle-endian (integer's only) the order of which is determined by an [inverse modification](#modifications). 48 | 49 | ![Endian order](endian.png) 50 | 51 | * Big - Most significant bit first 52 | * Little - Least significant bit first 53 | * Middle - Most significant bit central 54 | 55 | ### Modifications 56 | 57 | Several further modifications can be made to bytes before and reversed afterwards as obfuscation 58 | 59 | | Name | Read | Write | 60 | |---|---|---| 61 | | A | `value - 128` | `value + 128` | 62 | | C | `-value` | `-value` | 63 | | S | `128 - value` | `128 - value` | 64 | 65 | ### Implementation 66 | 67 | [Kotlin](ModifiedEndianIO.kts) (bespoke type support needed) 68 | 69 | ## Bespoke types 70 | 71 | ### Medium/Tribyte 72 | 73 | A custom data type for storing 3 byte numbers between -8,388,608 and 8,388,607 74 | 75 | ## Smart 76 | Functions that "smartly" choose the smallest data type necessary to store the value provided 77 | 78 | e.g 79 | ```text 80 | if value < byte max 81 | write byte 82 | else 83 | write short 84 | ``` 85 | -------------------------------------------------------------------------------- /protocol/index.md: -------------------------------------------------------------------------------- 1 | # Protocol 2 | 3 | #### [Data Types](./data-types) 4 | 5 | #### [RuneScape 2011](./rs2011) -------------------------------------------------------------------------------- /protocol/rs2011/index.md: -------------------------------------------------------------------------------- 1 | Many components of the protocol have not changed significantly. Specifiations are outlined in this directory. -------------------------------------------------------------------------------- /protocol/rs2011/rs667/rs667-client-protocol.md: -------------------------------------------------------------------------------- 1 | ## Client → Server 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /protocol/rs2011/rs667/rs667-server-protocol.md: -------------------------------------------------------------------------------- 1 | ## Server → Client 2 | 3 | -------------------------------------------------------------------------------- /runescript/index.md: -------------------------------------------------------------------------------- 1 | # RuneScript --------------------------------------------------------------------------------