diff --git a/conf/src/main/kotlin/org/openrs2/conf/Config.kt b/conf/src/main/kotlin/org/openrs2/conf/Config.kt index 4c8bcdbf..2fec3b63 100644 --- a/conf/src/main/kotlin/org/openrs2/conf/Config.kt +++ b/conf/src/main/kotlin/org/openrs2/conf/Config.kt @@ -3,7 +3,15 @@ package org.openrs2.conf public data class Config( val game: String, val operator: String, - val domain: String + val domain: String, + val world: Int, + val hostname: String, + val country: CountryCode, + val activity: String, + val members: Boolean, + val quickChat: Boolean, + val pvp: Boolean, + val lootShare: Boolean ) { val internalGame: String = game.toInternalName() val internalOperator: String = operator.toInternalName() diff --git a/conf/src/main/kotlin/org/openrs2/conf/CountryCode.kt b/conf/src/main/kotlin/org/openrs2/conf/CountryCode.kt new file mode 100644 index 00000000..c166d09e --- /dev/null +++ b/conf/src/main/kotlin/org/openrs2/conf/CountryCode.kt @@ -0,0 +1,249 @@ +package org.openrs2.conf + +public enum class CountryCode { + /* + * The order of the country codes in this enum MUST match enum 1626 in the + * cache. + */ + AF, // Afghanistan + AL, // Albania + DZ, // Algeria + AS, // American Samoa + AD, // Andorra + AO, // Angola + AI, // Anguilla + AQ, // Antarctica + AG, // Antigua and Barbuda + AR, // Argentina + AM, // Armenia + AW, // Aruba + AU, // Australia + AT, // Austria + AZ, // Azerbaijan + BS, // Bahamas + BH, // Bahrain + BD, // Bangladesh + BB, // Barbados + BY, // Belarus + BE, // Belgium + BZ, // Belize + BJ, // Benin + BM, // Bermuda + BT, // Bhutan + BO, // Bolivia + BA, // Bosnia and Herzegovina + BW, // Botswana + BV, // Bouvet Island + BR, // Brazil + IO, // British Indian Ocean Territory + BN, // Brunei Darussalam + BG, // Bulgaria + BF, // Burkina Faso + BI, // Burundi + KH, // Cambodia + CM, // Cameroon + CA, // Canada + CV, // Cape Verde + KY, // Cayman Islands + CF, // Central African Republic + TD, // Chad + CL, // Chile + CN, // China + CX, // Christmas Island + CC, // Cocos (Keeling) Islands + CO, // Colombia + KM, // Comoros + CG, // Congo + CD, // Congo, The Democratic Republic of the + CK, // Cook Islands + CR, // Costa Rica + CI, // Cote D'Ivoire + HR, // Croatia + CU, // Cuba + CY, // Cyprus + CZ, // Czech Republic + DK, // Denmark + DJ, // Djibouti + DM, // Dominica + DO, // Dominican Republic + TL, // East Timor + EC, // Ecuador + EG, // Egypt + SV, // El Salvador + GQ, // Equatorial Guinea + ER, // Eritrea + EE, // Estonia + ET, // Ethiopia + FK, // Falkland Islands (Malvinas) + FO, // Faroe Islands + FJ, // Fiji + FI, // Finland + FR, // France + FX, // France, Metropolitan + GF, // French Guiana + PF, // French Polynesia + TF, // French Southern Territories + GA, // Gabon + GM, // Gambia + GE, // Georgia + DE, // Germany + GH, // Ghana + GI, // Gibraltar + GR, // Greece + GL, // Greenland + GD, // Grenada + GP, // Guadeloupe + GU, // Guam + GT, // Guatemala + GN, // Guinea + GW, // Guinea-Bissau + GY, // Guyana + HT, // Haiti + HM, // Heard Island and McDonald Islands + VA, // Holy See (Vatican City State) + HN, // Honduras + HK, // Hong Kong + HU, // Hungary + IS, // Iceland + IN, // India + ID, // Indonesia + IR, // Iran, Islamic Republic of + IQ, // Iraq + IE, // Ireland + IL, // Israel + IT, // Italy + JM, // Jamaica + JP, // Japan + JO, // Jordan + KZ, // Kazakstan + KE, // Kenya + KI, // Kiribati + KP, // Korea, Democratic People's Republic of + KR, // Korea, Republic of + KW, // Kuwait + KG, // Kyrgyzstan + LA, // Lao People's Democratic Republic + LV, // Latvia + LB, // Lebanon + LS, // Lesotho + LR, // Liberia + LY, // Libyan Arab Jamahiriya + LI, // Liechtenstein + LT, // Lithuania + LU, // Luxembourg + MO, // Macau + MK, // Macedonia, the Former Yugoslav Republic of + MG, // Madagascar + MW, // Malawi + MY, // Malaysia + MV, // Maldives + ML, // Mali + MT, // Malta + MH, // Marshall Islands + MQ, // Martinique + MR, // Mauritania + MU, // Mauritius + YT, // Mayotte + MX, // Mexico + FM, // Micronesia, Federated States of + MD, // Moldova, Republic of + MC, // Monaco + MN, // Mongolia + ME, // Montenegro + MS, // Montserrat + MA, // Morocco + MZ, // Mozambique + MM, // Myanmar + NA, // Namibia + NR, // Nauru + NP, // Nepal + NL, // Netherlands + AN, // Netherlands Antilles + NC, // New Caledonia + NZ, // New Zealand + NI, // Nicaragua + NE, // Niger + NG, // Nigeria + NU, // Niue + NF, // Norfolk Island + MP, // Northern Mariana Islands + NO, // Norway + OM, // Oman + PK, // Pakistan + PW, // Palau + PS, // Palestinian Territory, Occupied + PA, // Panama + PG, // Papua New Guinea + PY, // Paraguay + PE, // Peru + PH, // Philippines + PN, // Pitcairn + PL, // Poland + PT, // Portugal + PR, // Puerto Rico + QA, // Qatar + RE, // Reunion + RO, // Romania + RU, // Russian Federation + RW, // Rwanda + SH, // Saint Helena + KN, // Saint Kitts and Nevis + LC, // Saint Lucia + PM, // Saint Pierre and Miquelon + VC, // Saint Vincent and the Grenadines + WS, // Samoa + SM, // San Marino + ST, // Sao Tome and Principe + SA, // Saudi Arabia + SN, // Senegal + RS, // Serbia + SC, // Seychelles + SL, // Sierra Leone + SG, // Singapore + SK, // Slovakia + SI, // Slovenia + SB, // Solomon Islands + SO, // Somalia + ZA, // South Africa + GS, // South Georgia and the South Sandwich Islands + ES, // Spain + LK, // Sri Lanka + SD, // Sudan + SR, // Suriname + SJ, // Svalbard and Jan Mayen + SZ, // Swaziland + SE, // Sweden + CH, // Switzerland + SY, // Syrian Arab Republic + TW, // Taiwan + TJ, // Tajikistan + TZ, // Tanzania, United Republic of + TH, // Thailand + TG, // Togo + TK, // Tokelau + TO, // Tonga + TT, // Trinidad and Tobago + TN, // Tunisia + TR, // Turkey + TM, // Turkmenistan + TC, // Turks and Caicos Islands + TV, // Tuvalu + UG, // Uganda + UA, // Ukraine + AE, // United Arab Emirates + GB, // United Kingdom + US, // United States + UM, // United States Minor Outlying Islands + UY, // Uruguay + UZ, // Uzbekistan + VU, // Vanuatu + VE, // Venezuela + VN, // Vietnam + VG, // Virgin Islands, British + VI, // Virgin Islands, U.S. + WF, // Wallis and Futuna + EH, // Western Sahara + YE, // Yemen + ZM, // Zambia + ZW, // Zimbabwe +} diff --git a/etc/config.example.yaml b/etc/config.example.yaml index f3d69b33..a778c98a 100644 --- a/etc/config.example.yaml +++ b/etc/config.example.yaml @@ -10,3 +10,30 @@ operator: "OpenRS2" # The game's domain name. All references to "runescape.com" are replaced with # this string. It should, at a minimum, have `www` and `www-wtqa` hostnames. domain: "openrs2.org" + +# The world number. +world: 1 + +# The world server's hostname. A suggested naming scheme is +# `world.` - for example, `world1.openrs2.org`. The default of +# `localhost` is only suitable for local testing. +hostname: "localhost" + +# The world's ISO 3166-1 alpha-2 country code. +country: "GB" + +# The world's activity. If the world does not have a special purpose, use a +# single hyphen. +activity: "-" + +# Set to true to indicate this is a members world. +members: true + +# Set to true to enforce the use of quick chat. +quick_chat: false + +# Set to true to indicate this is a PvP world. +pvp: false + +# Set to true to enable loot share. +loot_share: false diff --git a/game/src/main/kotlin/org/openrs2/game/GameModule.kt b/game/src/main/kotlin/org/openrs2/game/GameModule.kt index c1b75220..fcd9f90c 100644 --- a/game/src/main/kotlin/org/openrs2/game/GameModule.kt +++ b/game/src/main/kotlin/org/openrs2/game/GameModule.kt @@ -13,6 +13,8 @@ import org.openrs2.conf.ConfigModule import org.openrs2.game.cache.CacheProvider import org.openrs2.game.cache.Js5MasterIndexProvider import org.openrs2.game.cache.StoreProvider +import org.openrs2.game.cluster.Cluster +import org.openrs2.game.cluster.SingleWorldCluster import org.openrs2.game.net.NetworkService import org.openrs2.game.net.js5.Js5Service import org.openrs2.net.NetworkModule @@ -40,5 +42,8 @@ public object GameModule : AbstractModule() { bind(Cache::class.java) .toProvider(CacheProvider::class.java) .`in`(Scopes.SINGLETON) + + bind(Cluster::class.java) + .to(SingleWorldCluster::class.java) } } diff --git a/game/src/main/kotlin/org/openrs2/game/cluster/Cluster.kt b/game/src/main/kotlin/org/openrs2/game/cluster/Cluster.kt new file mode 100644 index 00000000..c3e69669 --- /dev/null +++ b/game/src/main/kotlin/org/openrs2/game/cluster/Cluster.kt @@ -0,0 +1,8 @@ +package org.openrs2.game.cluster + +import org.openrs2.protocol.world.WorldListResponse +import java.util.SortedMap + +public interface Cluster { + public fun getWorldList(): Pair, SortedMap> +} diff --git a/game/src/main/kotlin/org/openrs2/game/cluster/CountryList.kt b/game/src/main/kotlin/org/openrs2/game/cluster/CountryList.kt new file mode 100644 index 00000000..e8693266 --- /dev/null +++ b/game/src/main/kotlin/org/openrs2/game/cluster/CountryList.kt @@ -0,0 +1,24 @@ +package org.openrs2.game.cluster + +import org.openrs2.cache.config.enum.EnumTypeList +import org.openrs2.conf.CountryCode +import org.openrs2.protocol.world.WorldListResponse +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +public class CountryList @Inject constructor( + enums: EnumTypeList +) { + private val ids = enums[COUNTRY_IDS]!! + private val names = enums[COUNTRY_NAMES]!! + + public operator fun get(code: CountryCode): WorldListResponse.Country { + return WorldListResponse.Country(ids.getInt(code.ordinal), names.getString(code.ordinal)) + } + + private companion object { + private const val COUNTRY_IDS = 1669 + private const val COUNTRY_NAMES = 1626 + } +} diff --git a/game/src/main/kotlin/org/openrs2/game/cluster/SingleWorldCluster.kt b/game/src/main/kotlin/org/openrs2/game/cluster/SingleWorldCluster.kt new file mode 100644 index 00000000..8d313b34 --- /dev/null +++ b/game/src/main/kotlin/org/openrs2/game/cluster/SingleWorldCluster.kt @@ -0,0 +1,30 @@ +package org.openrs2.game.cluster + +import org.openrs2.conf.Config +import org.openrs2.protocol.world.WorldListResponse +import java.util.SortedMap +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +public class SingleWorldCluster @Inject constructor( + config: Config, + countries: CountryList +) : Cluster { + private val worlds = sortedMapOf( + config.world to WorldListResponse.World( + countries[config.country], + config.members, + config.quickChat, + config.pvp, + config.lootShare, + config.activity, + config.hostname + ) + ) + private val players = sortedMapOf(0 to 0) + + override fun getWorldList(): Pair, SortedMap> { + return Pair(worlds, players) + } +} diff --git a/game/src/main/kotlin/org/openrs2/game/net/login/LoginChannelHandler.kt b/game/src/main/kotlin/org/openrs2/game/net/login/LoginChannelHandler.kt index 6d74b4ba..f1bfeef1 100644 --- a/game/src/main/kotlin/org/openrs2/game/net/login/LoginChannelHandler.kt +++ b/game/src/main/kotlin/org/openrs2/game/net/login/LoginChannelHandler.kt @@ -11,10 +11,12 @@ import io.netty.handler.codec.http.HttpResponseEncoder import io.netty.handler.codec.string.StringDecoder import io.netty.handler.timeout.IdleStateEvent import org.openrs2.buffer.copiedBuffer +import org.openrs2.game.cluster.Cluster import org.openrs2.game.net.crossdomain.CrossDomainChannelHandler import org.openrs2.game.net.http.Http import org.openrs2.game.net.jaggrab.JaggrabChannelHandler import org.openrs2.game.net.js5.Js5ChannelHandler +import org.openrs2.protocol.Protocol import org.openrs2.protocol.Rs2Decoder import org.openrs2.protocol.Rs2Encoder import org.openrs2.protocol.jaggrab.JaggrabRequestDecoder @@ -23,10 +25,12 @@ import org.openrs2.protocol.js5.Js5ResponseEncoder import org.openrs2.protocol.js5.XorDecoder import org.openrs2.protocol.login.LoginRequest import org.openrs2.protocol.login.LoginResponse +import org.openrs2.protocol.world.WorldListResponse import javax.inject.Inject import javax.inject.Provider public class LoginChannelHandler @Inject constructor( + private val cluster: Cluster, private val js5HandlerProvider: Provider, private val jaggrabHandler: JaggrabChannelHandler ) : SimpleChannelInboundHandler(LoginRequest::class.java) { @@ -38,6 +42,7 @@ public class LoginChannelHandler @Inject constructor( when (msg) { is LoginRequest.InitJs5RemoteConnection -> handleInitJs5RemoteConnection(ctx, msg) is LoginRequest.InitJaggrabConnection -> handleInitJaggrabConnection(ctx) + is LoginRequest.RequestWorldList -> handleRequestWorldList(ctx, msg) is LoginRequest.InitCrossDomainConnection -> handleInitCrossDomainConnection(ctx) } } @@ -74,6 +79,23 @@ public class LoginChannelHandler @Inject constructor( ctx.pipeline().remove(this) } + private fun handleRequestWorldList(ctx: ChannelHandlerContext, msg: LoginRequest.RequestWorldList) { + val (worlds, players) = cluster.getWorldList() + + val checksum = worlds.hashCode() + + val worldList = if (checksum != msg.checksum) { + WorldListResponse.WorldList(worlds, checksum) + } else { + null + } + + val encoder = ctx.pipeline().get(Rs2Encoder::class.java) + encoder.protocol = Protocol.WORLD_LIST_DOWNSTREAM + + ctx.write(WorldListResponse(worldList, players)).addListener(ChannelFutureListener.CLOSE) + } + private fun handleInitCrossDomainConnection(ctx: ChannelHandlerContext) { ctx.pipeline().addLast( HttpRequestDecoder(), diff --git a/protocol/src/main/kotlin/org/openrs2/protocol/Protocol.kt b/protocol/src/main/kotlin/org/openrs2/protocol/Protocol.kt index 8e020618..1a7af934 100644 --- a/protocol/src/main/kotlin/org/openrs2/protocol/Protocol.kt +++ b/protocol/src/main/kotlin/org/openrs2/protocol/Protocol.kt @@ -8,6 +8,7 @@ import org.openrs2.protocol.login.IpLimitCodec import org.openrs2.protocol.login.Js5OkCodec import org.openrs2.protocol.login.RequestWorldListCodec import org.openrs2.protocol.login.ServerFullCodec +import org.openrs2.protocol.world.WorldListResponseCodec public class Protocol(vararg codecs: PacketCodec<*>) { private val decoders = arrayOfNulls>(256) @@ -42,5 +43,8 @@ public class Protocol(vararg codecs: PacketCodec<*>) { ServerFullCodec, IpLimitCodec ) + public val WORLD_LIST_DOWNSTREAM: Protocol = Protocol( + WorldListResponseCodec + ) } } diff --git a/protocol/src/main/kotlin/org/openrs2/protocol/world/WorldListResponse.kt b/protocol/src/main/kotlin/org/openrs2/protocol/world/WorldListResponse.kt new file mode 100644 index 00000000..936ef5f5 --- /dev/null +++ b/protocol/src/main/kotlin/org/openrs2/protocol/world/WorldListResponse.kt @@ -0,0 +1,39 @@ +package org.openrs2.protocol.world + +import org.openrs2.protocol.Packet +import java.util.SortedMap + +public data class WorldListResponse( + public val worldList: WorldList?, + public val players: SortedMap +) : Packet { + public data class Country( + public val id: Int, + public val name: String + ) : Comparable { + override fun compareTo(other: Country): Int { + return compareValuesBy(this, other, Country::name, Country::id) + } + } + + public data class World( + public val country: Country, + public val members: Boolean, + public val quickChat: Boolean, + public val pvp: Boolean, + public val lootShare: Boolean, + public val activity: String, + public val hostname: String + ) + + public data class WorldList( + public val worlds: SortedMap, + public val checksum: Int + ) { + public val countries: List = worlds.values.map(World::country).distinct().sorted() + } + + public companion object { + public const val OFFLINE: Int = -1 + } +} diff --git a/protocol/src/main/kotlin/org/openrs2/protocol/world/WorldListResponseCodec.kt b/protocol/src/main/kotlin/org/openrs2/protocol/world/WorldListResponseCodec.kt new file mode 100644 index 00000000..ac3f26a7 --- /dev/null +++ b/protocol/src/main/kotlin/org/openrs2/protocol/world/WorldListResponseCodec.kt @@ -0,0 +1,137 @@ +package org.openrs2.protocol.world + +import io.netty.buffer.ByteBuf +import org.openrs2.buffer.readUnsignedShortSmart +import org.openrs2.buffer.readVersionedString +import org.openrs2.buffer.writeUnsignedShortSmart +import org.openrs2.buffer.writeVersionedString +import org.openrs2.crypto.StreamCipher +import org.openrs2.protocol.PacketCodec +import org.openrs2.protocol.PacketLength + +public object WorldListResponseCodec : PacketCodec( + type = WorldListResponse::class.java, + opcode = 0, + length = PacketLength.VARIABLE_SHORT +) { + private const val VERSION = 1 + + private const val FLAG_MEMBERS_ONLY = 0x1 + private const val FLAG_QUICK_CHAT = 0x2 + private const val FLAG_PVP = 0x4 + private const val FLAG_LOOT_SHARE = 0x8 + + override fun decode(input: ByteBuf, cipher: StreamCipher): WorldListResponse { + val version = input.readUnsignedByte().toInt() + require(version == VERSION) { + "Unsupported world list version" + } + + val cluster = if (input.readBoolean()) { + var size = input.readUnsignedShortSmart() + + val countries = mutableListOf() + for (i in 0 until size) { + val id = input.readUnsignedShortSmart() + val name = input.readVersionedString() + countries += WorldListResponse.Country(id, name) + } + + val firstId = input.readUnsignedShortSmart() + input.readUnsignedShortSmart() + size = input.readUnsignedShortSmart() + + val worlds = sortedMapOf() + for (i in 0 until size) { + val id = firstId + input.readUnsignedShortSmart() + + val countryIndex = input.readUnsignedByte().toInt() + require(countryIndex >= countries.size) { + "Country index out of bounds" + } + val country = countries[countryIndex] + + val flags = input.readInt() + val members = (flags and FLAG_MEMBERS_ONLY) != 0 + val quickChat = (flags and FLAG_QUICK_CHAT) != 0 + val pvp = (flags and FLAG_PVP) != 0 + val lootShare = (flags and FLAG_LOOT_SHARE) != 0 + + val activity = input.readVersionedString() + val hostname = input.readVersionedString() + + worlds[id] = WorldListResponse.World(country, members, quickChat, pvp, lootShare, activity, hostname) + } + + val checksum = input.readInt() + + WorldListResponse.WorldList(worlds, checksum) + } else { + null + } + + val players = sortedMapOf() + while (input.isReadable) { + val offset = input.readUnsignedShortSmart() + val count = input.readUnsignedShort() + players[offset] = count + } + + return WorldListResponse(cluster, players) + } + + override fun encode(input: WorldListResponse, output: ByteBuf, cipher: StreamCipher) { + output.writeByte(VERSION) + + val worlds = input.worldList + if (worlds != null) { + output.writeBoolean(true) + + output.writeUnsignedShortSmart(worlds.countries.size) + + for (country in worlds.countries) { + output.writeUnsignedShortSmart(country.id) + output.writeVersionedString(country.name) + } + + val firstId = worlds.worlds.firstKey() ?: 0 + val lastId = worlds.worlds.lastKey() ?: 0 + + output.writeUnsignedShortSmart(firstId) + output.writeUnsignedShortSmart(lastId) + output.writeUnsignedShortSmart(worlds.worlds.size) + + for ((id, world) in worlds.worlds) { + output.writeUnsignedShortSmart(id - firstId) + output.writeByte(worlds.countries.binarySearch(world.country)) + + var flags = 0 + if (world.members) { + flags = flags or FLAG_MEMBERS_ONLY + } + if (world.quickChat) { + flags = flags or FLAG_QUICK_CHAT + } + if (world.pvp) { + flags = flags or FLAG_PVP + } + if (world.lootShare) { + flags = flags or FLAG_LOOT_SHARE + } + output.writeInt(flags) + + output.writeVersionedString(world.activity) + output.writeVersionedString(world.hostname) + } + + output.writeInt(worlds.checksum) + } else { + output.writeBoolean(false) + } + + for ((offset, count) in input.players) { + output.writeUnsignedShortSmart(offset) + output.writeShort(count) + } + } +}