├── content
├── combat
│ └── index.md
├── index.md
├── poison
│ └── index.md
└── queue
│ └── index.md
├── runescript
└── index.md
├── _config.yml
├── protocol
├── rs2011
│ ├── rs667
│ │ ├── rs667-server-protocol.md
│ │ └── rs667-client-protocol.md
│ └── index.md
├── data-types
│ ├── endian.png
│ ├── ModifiedEndianIO.kts
│ └── index.md
└── index.md
├── functionality
├── map
│ ├── game-map.png
│ └── index.md
└── index.md
├── .gitignore
├── README.md
├── index.md
└── naming
└── index.md
/content/combat/index.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/runescript/index.md:
--------------------------------------------------------------------------------
1 | # RuneScript
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-minimal
--------------------------------------------------------------------------------
/protocol/rs2011/rs667/rs667-server-protocol.md:
--------------------------------------------------------------------------------
1 | ## Server → Client
2 |
3 |
--------------------------------------------------------------------------------
/protocol/rs2011/rs667/rs667-client-protocol.md:
--------------------------------------------------------------------------------
1 | ## Client → Server
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/functionality/map/game-map.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RuneDocs/docs/HEAD/functionality/map/game-map.png
--------------------------------------------------------------------------------
/protocol/data-types/endian.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RuneDocs/docs/HEAD/protocol/data-types/endian.png
--------------------------------------------------------------------------------
/protocol/index.md:
--------------------------------------------------------------------------------
1 | # Protocol
2 |
3 | #### [Data Types](./data-types)
4 |
5 | #### [RuneScape 2011](./rs2011)
--------------------------------------------------------------------------------
/content/index.md:
--------------------------------------------------------------------------------
1 | # Content
2 |
3 | #### [Combat](./combat)
4 |
5 | #### [Poison](./poison)
6 |
7 | #### [Queue](./queue)
--------------------------------------------------------------------------------
/protocol/rs2011/index.md:
--------------------------------------------------------------------------------
1 | Many components of the protocol have not changed significantly. Specifiations are outlined in this directory.
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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)
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 | 
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 |
--------------------------------------------------------------------------------
/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/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 | 
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 | ```
--------------------------------------------------------------------------------