├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── data └── prot │ ├── 918 │ ├── clientProt │ │ ├── CLIENT_CHEAT.txt │ │ └── WORLDLIST_FETCH.txt │ ├── clientProtNames.toml │ ├── clientProtSizes.toml │ ├── serverProt │ │ ├── CHAT_FILTER_SETTINGS_PRIVATECHAT.txt │ │ ├── CLIENT_SETVARCSTR_LARGE.txt │ │ ├── CLIENT_SETVARCSTR_SMALL.txt │ │ ├── CLIENT_SETVARC_LARGE.txt │ │ ├── CLIENT_SETVARC_SMALL.txt │ │ ├── IF_OPENSUB.txt │ │ ├── IF_OPENTOP.txt │ │ ├── UPDATE_STAT.txt │ │ ├── VARP_LARGE.txt │ │ └── VARP_SMALL.txt │ ├── serverProtNames.toml │ └── serverProtSizes.toml │ └── 919 │ ├── clientProt │ ├── CLIENT_CHEAT.txt │ └── WORLDLIST_FETCH.txt │ ├── clientProtNames.toml │ ├── clientProtSizes.toml │ ├── serverProt │ ├── CHAT_FILTER_SETTINGS_PRIVATECHAT.txt │ ├── CLIENT_SETVARC_LARGE.txt │ ├── CLIENT_SETVARC_SMALL.txt │ ├── IF_OPENSUB.txt │ ├── IF_OPENTOP.txt │ ├── IF_SETEVENTS.txt │ ├── IF_SETHIDE.txt │ ├── IF_SETTEXT.txt │ ├── REBUILD_NORMAL.txt │ ├── SET_MAP_FLAG.txt │ ├── UPDATE_STAT.txt │ ├── VARP_LARGE.txt │ └── VARP_SMALL.txt │ ├── serverProtNames.toml │ └── serverProtSizes.toml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src └── main ├── java └── com │ └── opennxt │ └── util │ ├── ISAACCipher.java │ └── Whirlpool.java └── kotlin └── com └── opennxt ├── Constants.kt ├── Js5Thread.kt ├── OpenNXT.kt ├── api ├── stat │ ├── ExperienceSource.kt │ ├── Stat.kt │ ├── StatContainer.kt │ └── StatData.kt ├── util │ ├── Cleanable.kt │ └── Initializable.kt └── vars │ └── VarDomain.kt ├── config ├── RsaConfig.kt ├── ServerConfig.kt └── TomlConfig.kt ├── content └── interfaces │ └── InterfaceSlot.kt ├── ext ├── ByteArray.kt ├── ByteBuf.kt ├── ByteBuffer.kt └── String.kt ├── filesystem ├── Archive.kt ├── ArchiveFile.kt ├── ChecksumTable.kt ├── Container.kt ├── Filesystem.kt ├── Index.kt ├── ReferenceTable.kt ├── compression │ ├── BZIP2Compression.kt │ ├── ContainerCompression.kt │ ├── GZIPCompression.kt │ └── LZMACompression.kt ├── prefetches │ ├── ArchivePrefetch.kt │ ├── FilePrefetch.kt │ ├── IndexPrefetch.kt │ ├── LibraryPrefetch.kt │ ├── Prefetch.kt │ └── PrefetchTable.kt └── sqlite │ ├── SqliteFilesystem.kt │ └── SqliteIndexFile.kt ├── impl └── stat │ ├── PlayerStatContainer.kt │ └── PlayerStatData.kt ├── login ├── LoginContext.kt ├── LoginResult.kt └── LoginThread.kt ├── main.kt ├── model ├── Build.kt ├── InterfaceHash.kt ├── commands │ ├── Command.kt │ ├── CommandException.kt │ ├── CommandRepository.kt │ ├── CommandSender.kt │ ├── SimpleCommand.kt │ └── impl │ │ └── proxy │ │ ├── HexdumpOffCommand.kt │ │ └── HexdumpOnCommand.kt ├── entity │ ├── BasePlayer.kt │ ├── Entity.kt │ ├── EntityList.kt │ ├── EntityListIterator.kt │ ├── LivingEntity.kt │ ├── PlayerEntity.kt │ ├── movement │ │ ├── CompassPoint.kt │ │ ├── Movement.kt │ │ └── MovementSpeed.kt │ ├── player │ │ ├── InterfaceManager.kt │ │ ├── Viewport.kt │ │ └── appearance │ │ │ ├── Gender.kt │ │ │ ├── PlayerModel.kt │ │ │ └── RenderType.kt │ ├── rendering │ │ ├── EntityRenderer.kt │ │ ├── UpdateBlock.kt │ │ ├── UpdateBlockType.kt │ │ └── blocks │ │ │ └── AppearanceUpdateBlock.kt │ └── updating │ │ └── PlayerInfoEncoder.kt ├── files │ ├── BinaryType.kt │ ├── ClientConfig.kt │ ├── DownloadInformation.kt │ └── FileChecker.kt ├── lobby │ ├── Lobby.kt │ ├── LobbyPlayer.kt │ └── TODORefactorThisClass.kt ├── messages │ ├── Message.kt │ ├── MessageReceiver.kt │ └── MessageType.kt ├── permissions │ └── PermissionsHolder.kt ├── proxy │ └── PacketDumper.kt ├── tick │ ├── TickEngine.kt │ └── Tickable.kt ├── world │ ├── MapSize.kt │ ├── TileLocation.kt │ ├── World.kt │ └── WorldPlayer.kt └── worldlist │ ├── WorldFlag.kt │ ├── WorldList.kt │ ├── WorldListEntry.kt │ └── WorldListLocation.kt ├── net ├── ConnectedClient.kt ├── GenericResponse.kt ├── Packet.kt ├── PacketCodec.kt ├── RSChannelAttributes.kt ├── RSChannelInitializer.kt ├── Side.kt ├── buf │ ├── AccessMode.kt │ ├── DataConstants.kt │ ├── DataOrder.kt │ ├── DataTransformation.kt │ ├── DataType.kt │ ├── GamePacketBuilder.kt │ └── GamePacketReader.kt ├── game │ ├── EmptyPacketCodec.kt │ ├── GamePacket.kt │ ├── PacketRegistry.kt │ ├── clientprot │ │ ├── ClientCheat.kt │ │ └── WorldlistFetch.kt │ ├── handlers │ │ ├── ClientCheatHandler.kt │ │ ├── NoTimeoutHandler.kt │ │ └── WorldlistFetchHandler.kt │ ├── pipeline │ │ ├── DynamicGamePacketCodec.kt │ │ ├── DynamicPacketHandler.kt │ │ ├── GamePacketCodec.kt │ │ ├── GamePacketEncoder.kt │ │ ├── GamePacketFraming.kt │ │ ├── GamePacketHandler.kt │ │ └── OpcodeWithBuffer.kt │ ├── protocol │ │ ├── Name2OpcodeConfig.kt │ │ ├── Opcode2SizeConfig.kt │ │ ├── PacketFieldDeclaration.kt │ │ └── ProtocolInformation.kt │ └── serverprot │ │ ├── ChatFilterSettingsPrivatechat.kt │ │ ├── ConsoleFeedback.kt │ │ ├── FriendlistLoaded.kt │ │ ├── MessageGame.kt │ │ ├── NoTimeout.kt │ │ ├── RebuildNormal.kt │ │ ├── RunClientScript.kt │ │ ├── ServerTickEnd.kt │ │ ├── SetMapFlag.kt │ │ ├── UpdateStat.kt │ │ ├── WorldListFetchReply.kt │ │ ├── ifaces │ │ ├── IfOpenSub.kt │ │ ├── IfOpenTop.kt │ │ ├── IfSetevents.kt │ │ ├── IfSethide.kt │ │ └── IfSettext.kt │ │ └── variables │ │ ├── ClientSetvarcLarge.kt │ │ ├── ClientSetvarcSmall.kt │ │ ├── ClientSetvarcstrLarge.kt │ │ ├── ClientSetvarcstrSmall.kt │ │ ├── ResetClientVarcache.kt │ │ ├── VarpLarge.kt │ │ └── VarpSmall.kt ├── handshake │ ├── HandshakeDecoder.kt │ ├── HandshakeHandler.kt │ ├── HandshakeRequest.kt │ └── HandshakeType.kt ├── http │ ├── HttpExtensions.kt │ ├── HttpRequestHandler.kt │ ├── HttpServer.kt │ └── endpoints │ │ ├── ClientFileEndpoint.kt │ │ ├── JavConfigWsEndpoint.kt │ │ └── Js5MsEndpoint.kt ├── js5 │ ├── Js5Decoder.kt │ ├── Js5Encoder.kt │ ├── Js5Handler.kt │ ├── Js5Session.kt │ └── packet │ │ ├── Js5Packet.kt │ │ └── Js5PacketCodec.kt ├── login │ ├── LoginEncoder.kt │ ├── LoginPacket.kt │ ├── LoginRSAHeader.kt │ ├── LoginServerDecoder.kt │ ├── LoginServerHandler.kt │ └── LoginType.kt └── proxy │ ├── ConnectedProxyClient.kt │ ├── LoginClientDecoder.kt │ ├── LoginClientHandler.kt │ ├── ProxyChannelAttributes.kt │ ├── ProxyChannelInitializer.kt │ ├── ProxyConfig.kt │ ├── ProxyConnectionFactory.kt │ ├── ProxyConnectionHandler.kt │ ├── ProxyLoginState.kt │ ├── ProxyPlayer.kt │ ├── UnidentifiedPacket.kt │ └── handler │ ├── IfOpenSubProxyHandler.kt │ ├── IfOpenTopProxyHandler.kt │ ├── IfSetEventsHandler.kt │ ├── IfSethideHandler.kt │ ├── IfSettextHandler.kt │ ├── RunClientScriptHandler.kt │ ├── SetMapFlagHandler.kt │ ├── VarpLargeHandler.kt │ └── VarpSmallHandler.kt ├── resources ├── DefaultStateChecker.kt ├── DiskResourceCodec.kt ├── FilesystemResourceCodec.kt ├── FilesystemResources.kt ├── ResourceType.kt ├── config │ ├── enums │ │ ├── EnumDefinition.kt │ │ ├── EnumDiskCodec.kt │ │ └── EnumFilesystemCodec.kt │ ├── params │ │ ├── ParamDefinition.kt │ │ ├── ParamDiskCodec.kt │ │ └── ParamFilesystemCodec.kt │ ├── structs │ │ ├── StructDefinition.kt │ │ ├── StructDiskCodec.kt │ │ └── StructFilesystemCodec.kt │ └── vars │ │ ├── BaseVarType.kt │ │ ├── ScriptVarType.kt │ │ ├── VarDefinition.kt │ │ ├── VarDefinitionDiskCodec.kt │ │ └── impl │ │ ├── VarClanDefinition.kt │ │ ├── VarClanSettingDefinition.kt │ │ ├── VarClientDefinition.kt │ │ ├── VarNpcDefinition.kt │ │ ├── VarObjectDefinition.kt │ │ ├── VarPlayerDefinition.kt │ │ ├── VarRegionDefinition.kt │ │ └── VarWorldDefinition.kt ├── defaults │ ├── Default.kt │ ├── DefaultGroup.kt │ ├── Defaults.kt │ ├── stats │ │ ├── StatDefaults.kt │ │ ├── StatDefinition.kt │ │ └── StatExperienceTable.kt │ └── wearpos │ │ └── WearposDefaults.kt ├── map │ └── TerrainData.kt └── model │ ├── BaseModel.kt │ ├── BaseModelBillboard.kt │ ├── ModelSegment.kt │ └── Vec3s.kt ├── tools ├── Tool.kt ├── ToolExecutor.kt └── impl │ ├── ClientDownloader.kt │ ├── ClientPatcher.kt │ ├── ResourceDumper.kt │ ├── RsaKeyGenerator.kt │ ├── cachedownloader │ ├── AsyncFilesystemAccessor.kt │ ├── CacheCompletionChecker.kt │ ├── CacheDownloader.kt │ ├── IndexCompletionChecker.kt │ ├── Js5Client.kt │ ├── Js5ClientPipeline.kt │ ├── Js5ClientPool.kt │ ├── Js5ClientState.kt │ ├── Js5Credentials.kt │ ├── Js5HttpRequest.kt │ └── Js5RequestHandler.kt │ └── other │ └── ModelDecoderTest.kt └── util ├── MD5.kt ├── RSAUtil.kt └── TextUtils.kt /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .idea 3 | build 4 | 5 | !data/ 6 | data/* 7 | !data/prot -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.jetbrains.kotlin.jvm' version '1.4.32' 3 | } 4 | 5 | group 'com.opennxt' 6 | version '1.0.0' 7 | 8 | repositories { 9 | mavenCentral() 10 | jcenter() 11 | } 12 | 13 | dependencies { 14 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" 15 | implementation "org.jetbrains.kotlin:kotlin-reflect" 16 | 17 | implementation "io.github.microutils:kotlin-logging-jvm:2.0.6" 18 | implementation "org.slf4j:slf4j-simple:1.7.29" 19 | 20 | implementation "com.google.guava:guava:19.0" 21 | implementation "com.google.code.gson:gson:2.8.0" 22 | implementation "io.github.classgraph:classgraph:4.8.105" 23 | implementation "org.jglue.fluent-json:fluent-json:1.0.0" 24 | implementation "com.fasterxml.jackson.core:jackson-databind:2.9.6" 25 | implementation "it.unimi.dsi:fastutil:8.2.2" 26 | implementation "com.github.ajalt:clikt:2.3.0" 27 | implementation 'com.github.jponge:lzma-java:1.2' 28 | implementation 'org.apache.commons:commons-compress:1.18' 29 | implementation "com.moandjiezana.toml:toml4j:0.7.2" 30 | 31 | implementation "org.xerial:sqlite-jdbc:3.32.3.2" 32 | 33 | implementation "io.netty:netty-all:4.1.63.Final" 34 | } 35 | 36 | compileKotlin { 37 | kotlinOptions.jvmTarget = "1.8" 38 | kotlinOptions { 39 | freeCompilerArgs = ["-Xinline-classes"] 40 | } 41 | } 42 | 43 | compileTestKotlin { 44 | kotlinOptions.jvmTarget = "1.8" 45 | } 46 | 47 | compileJava.options.encoding = 'UTF-8' 48 | 49 | tasks.withType(JavaCompile) { 50 | options.encoding = 'UTF-8' 51 | } -------------------------------------------------------------------------------- /data/prot/918/clientProt/CLIENT_CHEAT.txt: -------------------------------------------------------------------------------- 1 | forced ubyte 2 | tabbed ubyte 3 | cheat string -------------------------------------------------------------------------------- /data/prot/918/clientProt/WORLDLIST_FETCH.txt: -------------------------------------------------------------------------------- 1 | checksum int # Realistically the data type doesn't matter -------------------------------------------------------------------------------- /data/prot/918/clientProtNames.toml: -------------------------------------------------------------------------------- 1 | [values] 2 | 100 = "NO_TIMEOUT" 3 | 38 = "WORLDLIST_FETCH" 4 | 122 = "CLIENT_CHEAT" 5 | -------------------------------------------------------------------------------- /data/prot/918/clientProtSizes.toml: -------------------------------------------------------------------------------- 1 | [values] 2 | 0 = 4 3 | 99 = -1 4 | 116 = -2 5 | 49 = 8 6 | 50 = 18 7 | 100 = 0 8 | 115 = 0 9 | 67 = 3 10 | 98 = -2 11 | 65 = 3 12 | 16 = -1 13 | 17 = 1 14 | 18 = 2 15 | 35 = -1 16 | 114 = 4 17 | 117 = 6 18 | 47 = -1 19 | 48 = 5 20 | 51 = -2 21 | 113 = 1 22 | 118 = 3 23 | 31 = 15 24 | 52 = 8 25 | 32 = 7 26 | 34 = -1 27 | 36 = 5 28 | 46 = 8 29 | 29 = 4 30 | 70 = 4 31 | 78 = -1 32 | 120 = 17 33 | 37 = 4 34 | 112 = -2 35 | 54 = -1 36 | 13 = 16 37 | 94 = 2 38 | 96 = -1 39 | 102 = -1 40 | 110 = 0 41 | 19 = -1 42 | 104 = -1 43 | 21 = 3 44 | 62 = 9 45 | 64 = 7 46 | 68 = 16 47 | 15 = -1 48 | 76 = -1 49 | 92 = 4 50 | 72 = 11 51 | 11 = 8 52 | 1 = 3 53 | 3 = 8 54 | 84 = 3 55 | 7 = 3 56 | 90 = -2 57 | 88 = 0 58 | 5 = -1 59 | 82 = 8 60 | 80 = 9 61 | 86 = 12 62 | 107 = 1 63 | 91 = -2 64 | 25 = -1 65 | 58 = 4 66 | 75 = -1 67 | 74 = -1 68 | 8 = 0 69 | 9 = -1 70 | 108 = 9 71 | 24 = 7 72 | 57 = 18 73 | 59 = 3 74 | 26 = 3 75 | 23 = 1 76 | 106 = 2 77 | 109 = 7 78 | 122 = -1 79 | 39 = -2 80 | 60 = 4 81 | 27 = 4 82 | 56 = 9 83 | 55 = 1 84 | 38 = 4 85 | 121 = -1 86 | 40 = 6 87 | 123 = -1 88 | 41 = -2 89 | 45 = 9 90 | 44 = 0 91 | 42 = 9 92 | 43 = 8 93 | 103 = 9 94 | 111 = -2 95 | 4 = 3 96 | 119 = 15 97 | 12 = 3 98 | 71 = -1 99 | 95 = 7 100 | 53 = -1 101 | 28 = 7 102 | 30 = 3 103 | 69 = 8 104 | 61 = 4 105 | 105 = 1 106 | 22 = 8 107 | 20 = 3 108 | 63 = 3 109 | 101 = 1 110 | 2 = 3 111 | 97 = -1 112 | 6 = 3 113 | 89 = 7 114 | 14 = -1 115 | 93 = -2 116 | 73 = -1 117 | 10 = -1 118 | 77 = -1 119 | 79 = 6 120 | 83 = 9 121 | 85 = -2 122 | 87 = 3 123 | 81 = 8 124 | 33 = -2 125 | 66 = 11 126 | -------------------------------------------------------------------------------- /data/prot/918/serverProt/CHAT_FILTER_SETTINGS_PRIVATECHAT.txt: -------------------------------------------------------------------------------- 1 | value ubyte -------------------------------------------------------------------------------- /data/prot/918/serverProt/CLIENT_SETVARCSTR_LARGE.txt: -------------------------------------------------------------------------------- 1 | value string 2 | id ushort -------------------------------------------------------------------------------- /data/prot/918/serverProt/CLIENT_SETVARCSTR_SMALL.txt: -------------------------------------------------------------------------------- 1 | value string 2 | id ushort -------------------------------------------------------------------------------- /data/prot/918/serverProt/CLIENT_SETVARC_LARGE.txt: -------------------------------------------------------------------------------- 1 | value int 2 | id ushort -------------------------------------------------------------------------------- /data/prot/918/serverProt/CLIENT_SETVARC_SMALL.txt: -------------------------------------------------------------------------------- 1 | value sbyte 2 | id ushortle128 -------------------------------------------------------------------------------- /data/prot/918/serverProt/IF_OPENSUB.txt: -------------------------------------------------------------------------------- 1 | xtea0 int 2 | xtea1 int 3 | flag ubytec 4 | id ushort 5 | xtea2 int 6 | parent intv2 7 | xtea3 int -------------------------------------------------------------------------------- /data/prot/918/serverProt/IF_OPENTOP.txt: -------------------------------------------------------------------------------- 1 | xtea0 int 2 | xtea1 int 3 | xtea2 int 4 | bool ubyte 5 | xtea3 int 6 | id ushort -------------------------------------------------------------------------------- /data/prot/918/serverProt/UPDATE_STAT.txt: -------------------------------------------------------------------------------- 1 | experience int 2 | stat ubytec 3 | level ubytec 4 | -------------------------------------------------------------------------------- /data/prot/918/serverProt/VARP_LARGE.txt: -------------------------------------------------------------------------------- 1 | id ushortle 2 | value intle -------------------------------------------------------------------------------- /data/prot/918/serverProt/VARP_SMALL.txt: -------------------------------------------------------------------------------- 1 | id ushortle 2 | value sbyte -------------------------------------------------------------------------------- /data/prot/918/serverProtSizes.toml: -------------------------------------------------------------------------------- 1 | [values] 2 | 0 = -2 3 | 33 = 8 4 | 132 = 1 5 | 165 = 1 6 | 67 = -1 7 | 100 = -1 8 | 98 = -2 9 | 65 = 29 10 | 134 = 5 11 | 35 = 1 12 | 130 = 2 13 | 32 = -1 14 | 31 = -1 15 | 133 = 5 16 | 135 = 0 17 | 129 = 1 18 | 34 = -1 19 | 36 = 7 20 | 131 = 3 21 | 29 = 3 22 | 169 = -1 23 | 70 = -2 24 | 161 = 0 25 | 37 = 6 26 | 137 = 9 27 | 94 = 0 28 | 96 = 12 29 | 102 = 10 30 | 193 = -2 31 | 62 = 8 32 | 104 = 8 33 | 167 = 1 34 | 64 = 1 35 | 171 = 0 36 | 163 = -1 37 | 68 = 0 38 | 159 = 3 39 | 92 = -1 40 | 72 = 11 41 | 1 = -2 42 | 3 = -2 43 | 7 = 6 44 | 5 = -1 45 | 58 = 23 46 | 107 = -1 47 | 91 = 10 48 | 140 = 6 49 | 173 = -1 50 | 74 = 1 51 | 75 = 0 52 | 156 = 1 53 | 139 = 5 54 | 108 = 5 55 | 59 = 8 56 | 26 = 10 57 | 23 = 19 58 | 188 = 4 59 | 189 = -2 60 | 39 = 3 61 | 192 = 3 62 | 27 = 3 63 | 55 = 25 64 | 38 = 12 65 | 43 = 1 66 | 123 = 1 67 | 42 = -2 68 | 127 = -2 69 | 103 = 4 70 | 128 = 8 71 | 111 = 6 72 | 119 = -1 73 | 95 = 10 74 | 136 = -1 75 | 30 = 0 76 | 71 = 10 77 | 152 = 1 78 | 184 = 1 79 | 144 = 2 80 | 22 = -2 81 | 63 = 6 82 | 2 = 0 83 | 6 = 10 84 | 14 = 7 85 | 180 = 2 86 | 176 = 6 87 | 10 = -2 88 | 168 = 2 89 | 164 = 5 90 | 160 = 8 91 | 172 = -2 92 | 79 = 2 93 | 83 = 8 94 | 87 = 4 95 | 49 = -2 96 | 116 = 1 97 | 50 = 10 98 | 182 = 9 99 | 181 = 2 100 | 115 = 10 101 | 148 = 1 102 | 150 = -2 103 | 16 = 3 104 | 17 = 10 105 | 149 = -1 106 | 18 = 14 107 | 147 = 4 108 | 48 = -2 109 | 117 = 32 110 | 47 = 6 111 | 114 = 7 112 | 51 = 11 113 | 113 = 6 114 | 118 = 5 115 | 52 = -2 116 | 46 = -2 117 | 120 = -2 118 | 78 = 4 119 | 112 = -2 120 | 153 = 4 121 | 54 = 0 122 | 13 = -1 123 | 177 = 2 124 | 143 = 21 125 | 151 = 9 126 | 110 = 8 127 | 185 = -1 128 | 187 = -2 129 | 19 = 8 130 | 145 = 6 131 | 21 = -2 132 | 175 = 4 133 | 15 = 6 134 | 183 = -2 135 | 155 = 4 136 | 76 = -1 137 | 11 = -2 138 | 179 = 0 139 | 90 = 28 140 | 88 = 10 141 | 80 = 3 142 | 84 = 10 143 | 82 = 19 144 | 86 = 21 145 | 25 = 6 146 | 8 = 4 147 | 9 = 5 148 | 157 = -2 149 | 141 = -2 150 | 24 = 8 151 | 57 = -2 152 | 191 = 3 153 | 190 = -1 154 | 106 = 5 155 | 109 = 3 156 | 122 = 0 157 | 138 = -2 158 | 60 = -2 159 | 142 = 3 160 | 56 = -2 161 | 121 = -1 162 | 44 = 6 163 | 45 = 2 164 | 41 = 4 165 | 40 = -1 166 | 125 = -2 167 | 126 = 5 168 | 4 = 3 169 | 194 = 4 170 | 12 = 0 171 | 53 = -2 172 | 178 = -2 173 | 28 = -1 174 | 69 = -1 175 | 61 = 20 176 | 186 = 5 177 | 146 = 1 178 | 20 = 3 179 | 105 = 4 180 | 101 = 2 181 | 97 = 25 182 | 89 = -1 183 | 154 = -2 184 | 158 = 12 185 | 93 = 6 186 | 73 = 2 187 | 166 = -2 188 | 170 = 1 189 | 174 = 0 190 | 77 = 8 191 | 162 = -1 192 | 85 = 0 193 | 81 = -1 194 | 66 = -1 195 | 99 = 25 196 | -------------------------------------------------------------------------------- /data/prot/919/clientProt/CLIENT_CHEAT.txt: -------------------------------------------------------------------------------- 1 | forced ubyte 2 | tabbed ubyte 3 | cheat string -------------------------------------------------------------------------------- /data/prot/919/clientProt/WORLDLIST_FETCH.txt: -------------------------------------------------------------------------------- 1 | checksum int # Realistically the data type doesn't matter -------------------------------------------------------------------------------- /data/prot/919/clientProtNames.toml: -------------------------------------------------------------------------------- 1 | [values] 2 | 72 = "CLIENT_CHEAT" 3 | 108 = "WORLDLIST_FETCH" 4 | 93 = "NO_TIMEOUT" 5 | -------------------------------------------------------------------------------- /data/prot/919/clientProtSizes.toml: -------------------------------------------------------------------------------- 1 | [values] 2 | 0 = -1 3 | 99 = 15 4 | 116 = -2 5 | 49 = -1 6 | 50 = 3 7 | 100 = -1 8 | 115 = -1 9 | 67 = 4 10 | 98 = 0 11 | 65 = -2 12 | 16 = -1 13 | 17 = 8 14 | 18 = 3 15 | 35 = 3 16 | 114 = 18 17 | 117 = -2 18 | 47 = 6 19 | 48 = -2 20 | 51 = -1 21 | 113 = -1 22 | 118 = 3 23 | 31 = 9 24 | 52 = 3 25 | 32 = 8 26 | 34 = 3 27 | 36 = 3 28 | 46 = 5 29 | 70 = 1 30 | 29 = 1 31 | 78 = -1 32 | 120 = 1 33 | 37 = -1 34 | 112 = -2 35 | 54 = 4 36 | 13 = 16 37 | 94 = 3 38 | 96 = 17 39 | 102 = -1 40 | 110 = 9 41 | 19 = -1 42 | 104 = 11 43 | 21 = 11 44 | 62 = 3 45 | 64 = -1 46 | 68 = 7 47 | 15 = -2 48 | 76 = -1 49 | 92 = 7 50 | 72 = -1 51 | 11 = -2 52 | 1 = 6 53 | 3 = 4 54 | 84 = -1 55 | 7 = -2 56 | 90 = 5 57 | 88 = 8 58 | 5 = -1 59 | 82 = -2 60 | 80 = -1 61 | 86 = 3 62 | 107 = -1 63 | 91 = 2 64 | 25 = 4 65 | 58 = -1 66 | 75 = 1 67 | 74 = -1 68 | 8 = 3 69 | 9 = 0 70 | 108 = 4 71 | 24 = 7 72 | 57 = -1 73 | 59 = 3 74 | 26 = 7 75 | 23 = -1 76 | 106 = 8 77 | 109 = 9 78 | 122 = 8 79 | 39 = -2 80 | 60 = 4 81 | 27 = 7 82 | 56 = 3 83 | 55 = 4 84 | 124 = 4 85 | 125 = -2 86 | 38 = -1 87 | 121 = 0 88 | 123 = 0 89 | 40 = 9 90 | 41 = -2 91 | 45 = 15 92 | 44 = -2 93 | 42 = 1 94 | 43 = 9 95 | 103 = 8 96 | 111 = 9 97 | 4 = 3 98 | 119 = 8 99 | 12 = 8 100 | 71 = -1 101 | 95 = -1 102 | 53 = -1 103 | 69 = 12 104 | 28 = 7 105 | 30 = 9 106 | 61 = 0 107 | 105 = 2 108 | 20 = 16 109 | 22 = 3 110 | 63 = 4 111 | 101 = 2 112 | 2 = 7 113 | 97 = 1 114 | 6 = 18 115 | 89 = -1 116 | 14 = 9 117 | 93 = 0 118 | 73 = 3 119 | 10 = -1 120 | 77 = 8 121 | 79 = 8 122 | 83 = 9 123 | 85 = 3 124 | 87 = 4 125 | 81 = 1 126 | 33 = 6 127 | 66 = 4 128 | -------------------------------------------------------------------------------- /data/prot/919/serverProt/CHAT_FILTER_SETTINGS_PRIVATECHAT.txt: -------------------------------------------------------------------------------- 1 | value ubyte -------------------------------------------------------------------------------- /data/prot/919/serverProt/CLIENT_SETVARC_LARGE.txt: -------------------------------------------------------------------------------- 1 | id ushort128 2 | value int 3 | -------------------------------------------------------------------------------- /data/prot/919/serverProt/CLIENT_SETVARC_SMALL.txt: -------------------------------------------------------------------------------- 1 | value ubyte 2 | id ushortle -------------------------------------------------------------------------------- /data/prot/919/serverProt/IF_OPENSUB.txt: -------------------------------------------------------------------------------- 1 | xtea0 int 2 | xtea1 int 3 | xtea2 int 4 | parent intv1 5 | flag ubyte128 6 | xtea3 int 7 | id ushortle 8 | -------------------------------------------------------------------------------- /data/prot/919/serverProt/IF_OPENTOP.txt: -------------------------------------------------------------------------------- 1 | xtea0 int 2 | bool ubyte 3 | xtea1 int 4 | xtea2 int 5 | xtea3 int 6 | id ushort -------------------------------------------------------------------------------- /data/prot/919/serverProt/IF_SETEVENTS.txt: -------------------------------------------------------------------------------- 1 | mask intv1 2 | parent int 3 | toSlot ushortle 4 | fromSlot ushort -------------------------------------------------------------------------------- /data/prot/919/serverProt/IF_SETHIDE.txt: -------------------------------------------------------------------------------- 1 | parent int 2 | hidden ubyte -------------------------------------------------------------------------------- /data/prot/919/serverProt/IF_SETTEXT.txt: -------------------------------------------------------------------------------- 1 | parent intv1 2 | text string -------------------------------------------------------------------------------- /data/prot/919/serverProt/REBUILD_NORMAL.txt: -------------------------------------------------------------------------------- 1 | unused1 ubyte 2 | chunkX ushortle128 3 | unused2 ubyte 4 | chunkY ushortle 5 | npcBits u128byte 6 | mapSize ubyte128 7 | 8 | areaType ushort 9 | hash1 int 10 | hash2 int -------------------------------------------------------------------------------- /data/prot/919/serverProt/SET_MAP_FLAG.txt: -------------------------------------------------------------------------------- 1 | target int 2 | unk1 ubyte128 3 | unk2 ubytec 4 | unk3 ubyte 5 | unk4 ubyte 6 | unk5 ubyte 7 | unk6 ubytec -------------------------------------------------------------------------------- /data/prot/919/serverProt/UPDATE_STAT.txt: -------------------------------------------------------------------------------- 1 | stat ubytec 2 | level ubyte128 3 | experience intv2 -------------------------------------------------------------------------------- /data/prot/919/serverProt/VARP_LARGE.txt: -------------------------------------------------------------------------------- 1 | value intv1 2 | id ushortle 3 | -------------------------------------------------------------------------------- /data/prot/919/serverProt/VARP_SMALL.txt: -------------------------------------------------------------------------------- 1 | id ushort128 2 | value sbytec -------------------------------------------------------------------------------- /data/prot/919/serverProtSizes.toml: -------------------------------------------------------------------------------- 1 | [values] 2 | 0 = 0 3 | 33 = -2 4 | 132 = -2 5 | 165 = 6 6 | 67 = -1 7 | 100 = 6 8 | 98 = 10 9 | 65 = 3 10 | 134 = 4 11 | 35 = -2 12 | 130 = 0 13 | 32 = 8 14 | 31 = -2 15 | 133 = 1 16 | 135 = 0 17 | 129 = 3 18 | 34 = 1 19 | 36 = -2 20 | 131 = 3 21 | 29 = 4 22 | 169 = -1 23 | 70 = 5 24 | 161 = 6 25 | 37 = 6 26 | 137 = 1 27 | 94 = 19 28 | 96 = -1 29 | 102 = 3 30 | 193 = 21 31 | 62 = 6 32 | 104 = 0 33 | 167 = 1 34 | 64 = -1 35 | 171 = 0 36 | 163 = 4 37 | 68 = -1 38 | 159 = 4 39 | 92 = 8 40 | 72 = 5 41 | 1 = -1 42 | 3 = -2 43 | 7 = 6 44 | 5 = -1 45 | 58 = 10 46 | 107 = 1 47 | 91 = -1 48 | 140 = 9 49 | 173 = 4 50 | 74 = -2 51 | 75 = -2 52 | 156 = -1 53 | 139 = -2 54 | 108 = 1 55 | 59 = 7 56 | 26 = -2 57 | 23 = 11 58 | 188 = 1 59 | 189 = 8 60 | 39 = 25 61 | 192 = 6 62 | 27 = 6 63 | 55 = 5 64 | 38 = 19 65 | 43 = 2 66 | 42 = 3 67 | 123 = 4 68 | 124 = 14 69 | 127 = 2 70 | 103 = 8 71 | 128 = 5 72 | 111 = 1 73 | 119 = 6 74 | 95 = -2 75 | 136 = 1 76 | 30 = 2 77 | 71 = 32 78 | 152 = 2 79 | 184 = 5 80 | 144 = 5 81 | 22 = -1 82 | 63 = 10 83 | 2 = 8 84 | 6 = -2 85 | 14 = 4 86 | 180 = 9 87 | 176 = 1 88 | 10 = -2 89 | 168 = 0 90 | 164 = 1 91 | 160 = 5 92 | 172 = 1 93 | 79 = -1 94 | 83 = 10 95 | 87 = 21 96 | 49 = 8 97 | 116 = -1 98 | 50 = 20 99 | 182 = 2 100 | 181 = -2 101 | 115 = 0 102 | 148 = 5 103 | 150 = -2 104 | 16 = 12 105 | 17 = -2 106 | 149 = -1 107 | 18 = 25 108 | 147 = 0 109 | 48 = 4 110 | 117 = 11 111 | 47 = 10 112 | 114 = 6 113 | 51 = 3 114 | 113 = 10 115 | 118 = 7 116 | 52 = 3 117 | 46 = 10 118 | 120 = 8 119 | 78 = -1 120 | 112 = -1 121 | 153 = 3 122 | 54 = 3 123 | 13 = 12 124 | 177 = -2 125 | 143 = 12 126 | 151 = 1 127 | 110 = 3 128 | 185 = 2 129 | 187 = -2 130 | 19 = 7 131 | 145 = 2 132 | 21 = 6 133 | 175 = -2 134 | 183 = -1 135 | 155 = 1 136 | 76 = -2 137 | 11 = -1 138 | 179 = -2 139 | 90 = 28 140 | 88 = 8 141 | 80 = 25 142 | 84 = -2 143 | 82 = 10 144 | 86 = 6 145 | 25 = -2 146 | 8 = 4 147 | 9 = 10 148 | 157 = 3 149 | 141 = 8 150 | 24 = -1 151 | 57 = 10 152 | 191 = -2 153 | 190 = -1 154 | 106 = -2 155 | 109 = 4 156 | 122 = 0 157 | 138 = 9 158 | 60 = 5 159 | 142 = -1 160 | 56 = -1 161 | 121 = 2 162 | 44 = 3 163 | 45 = 8 164 | 41 = 0 165 | 40 = 23 166 | 125 = -2 167 | 126 = 0 168 | 4 = 1 169 | 194 = 2 170 | 12 = 6 171 | 53 = -2 172 | 178 = -2 173 | 28 = -1 174 | 69 = 1 175 | 61 = -2 176 | 186 = 2 177 | 146 = -1 178 | 20 = 0 179 | 105 = -2 180 | 101 = 29 181 | 97 = 8 182 | 89 = -2 183 | 154 = -2 184 | 158 = -2 185 | 93 = -1 186 | 73 = -1 187 | 166 = -1 188 | 170 = 4 189 | 174 = 4 190 | 77 = 0 191 | 162 = 3 192 | 85 = -2 193 | 81 = 5 194 | 66 = 10 195 | 99 = 0 196 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Techdaan/OpenNXT/1749e1a0ae5ce3a6925fb588dec6a4ae6c7cb0d0/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt 2 | 3 | import java.nio.file.Paths 4 | 5 | object Constants { 6 | val DATA_PATH = Paths.get("./data/") 7 | val CLIENTS_PATH = DATA_PATH.resolve("clients") 8 | val LAUNCHERS_PATH = DATA_PATH.resolve("launchers") 9 | val CONFIG_PATH = DATA_PATH.resolve("config") 10 | val CACHE_PATH = DATA_PATH.resolve("cache") 11 | val PROT_PATH = DATA_PATH.resolve("prot") 12 | val RESOURCE_PATH = DATA_PATH.resolve("resources") 13 | val PROXY_PATH = DATA_PATH.resolve("proxy") 14 | val PROXY_DUMP_PATH = PROXY_PATH.resolve("dumps") 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/Js5Thread.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt 2 | 3 | import com.opennxt.net.js5.Js5Session 4 | import mu.KotlinLogging 5 | import java.util.concurrent.CopyOnWriteArrayList 6 | import java.util.concurrent.atomic.AtomicBoolean 7 | 8 | // TODO This should probably be re-done entirely. 9 | object Js5Thread: Thread("js5-thread") { 10 | 11 | private val running = AtomicBoolean(true) 12 | private val logger = KotlinLogging.logger { } 13 | private val sessions = CopyOnWriteArrayList() 14 | 15 | override fun run() { 16 | while (running.get()) { 17 | if (sessions.isEmpty()) { 18 | sleep(100) 19 | } 20 | 21 | sessions.forEach { 22 | it.process(10_000_000) // Process in blocks to avoid turning into "first-come first-serve" 23 | } 24 | } 25 | } 26 | 27 | fun addSession(session: Js5Session) { 28 | sessions.add(session) 29 | } 30 | 31 | fun removeSession(session: Js5Session) { 32 | sessions.remove(session) 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/api/stat/ExperienceSource.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.api.stat 2 | 3 | // Not sealed because we want to allow external experience sources to be defined 4 | abstract class ExperienceSource(val boostFactor: Double) { 5 | object Default: ExperienceSource(1.0) 6 | 7 | object Lamp: ExperienceSource(1.0) 8 | object Quest: ExperienceSource(1.0) 9 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/api/stat/Stat.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.api.stat 2 | 3 | import com.opennxt.resources.FilesystemResources 4 | import com.opennxt.resources.config.enums.EnumDefinition 5 | import com.opennxt.resources.defaults.stats.StatDefaults 6 | import com.opennxt.resources.defaults.stats.StatDefinition 7 | import com.opennxt.resources.defaults.stats.StatExperienceTable 8 | 9 | enum class Stat(val id: Int) { 10 | ATTACK(0), 11 | DEFENCE(1), 12 | STRENGTH(2), 13 | CONSTITUTION(3), 14 | RANGED(4), 15 | PRAYER(5), 16 | MAGIC(6), 17 | COOKING(7), 18 | WOODCUTTING(8), 19 | FLETCHING(9), 20 | FISHING(10), 21 | FIREMAKING(11), 22 | CRAFTING(12), 23 | SMITHING(13), 24 | MINING(14), 25 | HERBLORE(15), 26 | AGILITY(16), 27 | THIEVING(17), 28 | SLAYER(18), 29 | FARMING(19), 30 | RUNECRAFTING(20), 31 | HUNTER(21), 32 | CONSTRUCTION(22), 33 | SUMMONING(23), 34 | DUNGEONEERING(24), 35 | DIVINATION(25), 36 | INVENTION(26), 37 | ARCHAEOLOGY(27), 38 | ; 39 | 40 | lateinit var def: StatDefinition 41 | lateinit var table: StatExperienceTable 42 | lateinit var display: String 43 | 44 | companion object { 45 | private val VALUES = values() 46 | 47 | fun reload() { 48 | val defaults = FilesystemResources.instance.defaults.get() 49 | val names = FilesystemResources.instance.get(680)!! 50 | 51 | defaults.stats.forEach { def -> 52 | val enum = VALUES[def.id] 53 | enum.def = def 54 | enum.table = def.table 55 | enum.display = ((names.values[def.id] as? String) ?: names.defaultString) ?: "null" 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/api/stat/StatContainer.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.api.stat 2 | 3 | import com.opennxt.api.util.Cleanable 4 | import com.opennxt.api.util.Initializable 5 | 6 | interface StatContainer: Cleanable, Initializable { 7 | operator fun get(stat: Stat): StatData 8 | operator fun set(stat: Stat, data: StatData) 9 | 10 | fun addExperience(stat: Stat, amount: Double, source: ExperienceSource = ExperienceSource.Default): Int 11 | fun boostStat(stat: Stat, boostBy: Int) 12 | fun getLevel(stat: Stat, boosted: Boolean = true): Int 13 | fun hasLevel(stat: Stat, level: Int, boostAble: Boolean = true): Boolean 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/api/stat/StatData.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.api.stat 2 | 3 | interface StatData { 4 | val stat: Stat 5 | val experience: Double 6 | val actualLevel: Int 7 | val boostedLevel: Int 8 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/api/util/Cleanable.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.api.util 2 | 3 | interface Cleanable { 4 | fun markDirty() 5 | fun isDirty(): Boolean 6 | fun clean() 7 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/api/util/Initializable.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.api.util 2 | 3 | interface Initializable { 4 | fun init() 5 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/api/vars/VarDomain.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.api.vars 2 | 3 | import com.opennxt.resources.ResourceType 4 | 5 | enum class VarDomain(val resourceType: ResourceType, val id: Int) { 6 | // PLAYER(ResourceType.VAR_PLAYER, 0), 7 | // NPC(ResourceType.VAR_NPC, 1), 8 | // CLIENT(ResourceType.VAR_CLIENT, 2), 9 | // WORLD(ResourceType.VAR_WORLD, 3), 10 | // REGION(ResourceType.VAR_REGION, 4), 11 | // OBJECT(ResourceType.VAR_OBJECT, 5), 12 | // CLAN(ResourceType.VAR_CLAN, 6), 13 | // CLAN_SETTING(ResourceType.VAR_CLAN_SETTING, 7) 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/config/RsaConfig.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.config 2 | 3 | import com.moandjiezana.toml.Toml 4 | import com.opennxt.Constants 5 | import java.math.BigInteger 6 | 7 | class RsaConfig : TomlConfig() { 8 | companion object { 9 | val DEFAULT_PATH = Constants.CONFIG_PATH.resolve("rsa.toml") 10 | } 11 | 12 | class RsaKeyPair(val exponent: BigInteger, val modulus: BigInteger) 13 | 14 | var login = RsaKeyPair(BigInteger.ZERO, BigInteger.ZERO) 15 | var js5 = RsaKeyPair(BigInteger.ZERO, BigInteger.ZERO) 16 | var launcher = RsaKeyPair(BigInteger.ZERO, BigInteger.ZERO) 17 | 18 | private fun Toml.getRsaKeyPair( 19 | path: String, 20 | default: RsaKeyPair = RsaKeyPair(BigInteger.ZERO, BigInteger.ZERO) 21 | ): RsaKeyPair { 22 | val table = getTable(path) ?: return default 23 | 24 | return RsaKeyPair( 25 | BigInteger(table.getString("exponent", "0"), 16), 26 | BigInteger(table.getString("modulus", "0"), 16) 27 | ) 28 | } 29 | 30 | override fun load(toml: Toml) { 31 | js5 = toml.getRsaKeyPair("js5", js5) 32 | login = toml.getRsaKeyPair("login", login) 33 | launcher = toml.getRsaKeyPair("launcher", launcher) 34 | } 35 | 36 | override fun save(map: MutableMap) { 37 | map["js5"] = mapOf( 38 | "exponent" to js5.exponent.toString(16), 39 | "modulus" to js5.modulus.toString(16) 40 | ) 41 | 42 | map["login"] = mapOf( 43 | "exponent" to login.exponent.toString(16), 44 | "modulus" to login.modulus.toString(16) 45 | ) 46 | 47 | map["launcher"] = mapOf( 48 | "exponent" to launcher.exponent.toString(16), 49 | "modulus" to launcher.modulus.toString(16) 50 | ) 51 | } 52 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/config/ServerConfig.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.config 2 | 3 | import com.moandjiezana.toml.Toml 4 | import com.opennxt.Constants 5 | 6 | class ServerConfig : TomlConfig() { 7 | companion object { 8 | val DEFAULT_PATH = Constants.CONFIG_PATH.resolve("server.toml") 9 | } 10 | 11 | data class Ports(var game: Int = 43594, var http: Int = 80, var https: Int = 443) 12 | 13 | var ports = Ports() 14 | var hostname = "127.0.0.1" 15 | var configUrl = "http://127.0.0.1/jav_config.ws?binaryType=2" 16 | 17 | var build = 918 18 | 19 | override fun save(map: MutableMap) { 20 | map["networking"] = mapOf( 21 | "ports" to mapOf( 22 | "game" to ports.game, 23 | "http" to ports.http, 24 | "https" to ports.https 25 | ) 26 | ) 27 | map["hostname"] = hostname 28 | map["configUrl"] = configUrl 29 | map["build"] = build 30 | } 31 | 32 | override fun load(toml: Toml) { 33 | hostname = toml.getString("hostname", hostname) 34 | configUrl = toml.getString("configUrl", configUrl) 35 | build = toml.getLong("build", build.toLong()).toInt() 36 | 37 | val networking = toml.getTable("networking") 38 | if (networking != null) { 39 | val ports = toml.getTable("ports") 40 | if (ports != null) { 41 | this.ports.game = ports.getLong("game", this.ports.game.toLong()).toInt() 42 | this.ports.http = ports.getLong("http", this.ports.http.toLong()).toInt() 43 | this.ports.https = ports.getLong("https", this.ports.https.toLong()).toInt() 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/config/TomlConfig.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.config 2 | 3 | import com.moandjiezana.toml.Toml 4 | import com.moandjiezana.toml.TomlWriter 5 | import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap 6 | import java.io.FileNotFoundException 7 | import java.nio.file.Files 8 | import java.nio.file.Path 9 | import kotlin.reflect.full.createInstance 10 | 11 | abstract class TomlConfig { 12 | abstract fun save(map: MutableMap) 13 | 14 | abstract fun load(toml: Toml) 15 | 16 | companion object { 17 | inline fun load( 18 | path: Path, 19 | saveAfterLoad: Boolean = true, 20 | mustExist: Boolean = false 21 | ): T { 22 | if (mustExist && !Files.exists(path)) 23 | throw FileNotFoundException(path.toString()) 24 | 25 | if (!Files.exists(path.parent)) 26 | Files.createDirectories(path.parent) 27 | 28 | val config = T::class.createInstance() 29 | if (!Files.exists(path)) { 30 | save(path, config) 31 | return config 32 | } 33 | 34 | val toml = Toml() 35 | toml.read(path.toFile()) 36 | config.load(toml) 37 | 38 | if (saveAfterLoad) 39 | save(path, config) 40 | 41 | return config 42 | } 43 | 44 | fun save(path: Path, config: TomlConfig) { 45 | if (!Files.exists(path.parent)) 46 | Files.createDirectories(path.parent) 47 | 48 | val map = Object2ObjectOpenHashMap() 49 | config.save(map) 50 | TomlWriter().write(map, path.toFile()) 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/ext/ByteArray.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.ext 2 | 3 | fun ByteArray.replaceFirst(needle: ByteArray, replacement: ByteArray): Boolean { 4 | val index = indexOf(needle) 5 | if (index == -1) { 6 | return false 7 | } 8 | 9 | // Patch binary data 10 | for (x in index until index + replacement.size) { 11 | this[x] = replacement[x - index] 12 | } 13 | 14 | return true 15 | } 16 | 17 | fun ByteArray.indexOf(needle: ByteArray): Int { 18 | outer@ for (i in 0 until this.size - needle.size + 1) { 19 | for (j in needle.indices) { 20 | if (this[i + j] != needle[j]) { 21 | continue@outer 22 | } 23 | } 24 | return i 25 | } 26 | return -1 27 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/ext/String.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.ext 2 | 3 | import com.opennxt.util.TextUtils 4 | 5 | fun String.toFilesystemHash(): Int { 6 | val size = length 7 | var char = 0 8 | for (index in 0 until size) 9 | char = (char shl 5) - char + TextUtils.charToCp1252(this[index]) 10 | return char 11 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/filesystem/ArchiveFile.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.filesystem 2 | 3 | class ArchiveFile( 4 | val id: Int, 5 | var data: ByteArray = ByteArray(0), 6 | var name: Int = 0 7 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/filesystem/Filesystem.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.filesystem 2 | 3 | import java.nio.ByteBuffer 4 | import java.nio.file.Path 5 | 6 | abstract class Filesystem(val path: Path) { 7 | 8 | private val checkedReferenceTables = BooleanArray(255) 9 | 10 | private val cachedReferenceTables = arrayOfNulls(255) 11 | 12 | abstract fun exists(index: Int, archive: Int): Boolean 13 | 14 | abstract fun read(index: Int, archive: Int): ByteBuffer? 15 | 16 | abstract fun read(index: Int, name: String): ByteBuffer? 17 | 18 | abstract fun readReferenceTable(index: Int): ByteBuffer? 19 | 20 | abstract fun createIndex(id: Int) 21 | 22 | fun getReferenceTable(index: Int, ignoreChecked: Boolean = false): ReferenceTable? { 23 | val cached = cachedReferenceTables[index] 24 | if (cached != null) return cached 25 | 26 | if (!ignoreChecked) { 27 | if (checkedReferenceTables[index]) return null 28 | checkedReferenceTables[index] = true 29 | } 30 | 31 | val table = ReferenceTable(this, index) 32 | val container = readReferenceTable(index) ?: return null 33 | val data = ByteBuffer.wrap(Container.decode(container).data) 34 | table.decode(data) 35 | cachedReferenceTables[index] = table 36 | 37 | return table 38 | } 39 | 40 | abstract fun write(index: Int, archive: Int, data: Container) 41 | 42 | abstract fun write(index: Int, archive: Int, compressed: ByteArray, version: Int, crc: Int) 43 | 44 | abstract fun writeReferenceTable(index: Int, data: Container) 45 | 46 | abstract fun writeReferenceTable(index: Int, compressed: ByteArray, version: Int, crc: Int) 47 | 48 | abstract fun numIndices(): Int 49 | 50 | fun update() { 51 | cachedReferenceTables.forEach { it?.update() } 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/filesystem/Index.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.filesystem 2 | 3 | class Index private constructor() { 4 | companion object { 5 | const val ANIMS = 0 6 | const val BASES = 1 7 | const val CONFIG = 2 8 | const val INTERFACES = 3 9 | const val MAPS = 5 10 | const val MODELS = 7 11 | const val SPRITES = 8 12 | const val BINARY = 10 13 | const val CLIENTSCRIPTS = 12 14 | const val FONTMETRICS = 13 15 | const val VORBIS = 14 16 | const val CONFIG_OBJECT = 16 17 | const val CONFIG_ENUM = 17 18 | const val CONFIG_NPC = 18 19 | const val CONFIG_ITEM = 19 20 | const val CONFIG_SEQ = 20 21 | const val CONFIG_SPOT = 21 22 | const val CONFIG_STRUCT = 22 23 | const val WORLDMAP = 23 24 | const val QUICKCHAT = 24 25 | const val QUICKCHAT_GLOBAL = 25 26 | const val MATERIALS = 26 27 | const val PARTICLES = 27 28 | const val DEFAULTS = 28 29 | const val BILLBOARDS = 29 30 | const val DLLS = 30 31 | const val SHADERS = 31 32 | const val LOADINGSPRITES = 32 33 | const val LOADINGSCREENS = 33 34 | const val LOADINGSPRITESRAW = 34 35 | const val CUTSCENES = 35 36 | const val AUDIOSTREAMS = 40 37 | const val WORLDMAPAREAS = 41 38 | const val WORLDMAPLABELS = 42 39 | const val MODELS_RT7 = 47 40 | const val ANIMS_RT7 = 48 41 | const val DBTABLEINDEX = 49 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/filesystem/compression/BZIP2Compression.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.filesystem.compression 2 | 3 | import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream 4 | import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream 5 | import java.io.ByteArrayInputStream 6 | import java.io.ByteArrayOutputStream 7 | 8 | object BZIP2Compression { 9 | fun decompress(compressed: ByteArray): ByteArray { 10 | val fixed = ByteArray(compressed.size + 4) 11 | System.arraycopy(compressed, 0, fixed, 4, compressed.size) 12 | fixed[0] = 'B'.toByte() 13 | fixed[1] = 'Z'.toByte() 14 | fixed[2] = 'h'.toByte() 15 | fixed[3] = '1'.toByte() 16 | 17 | return BZip2CompressorInputStream(ByteArrayInputStream(fixed)).use { it.readBytes() } 18 | } 19 | 20 | fun compress(uncompressed: ByteArray): ByteArray { 21 | val buffer = ByteArrayInputStream(uncompressed).use { input -> 22 | ByteArrayOutputStream().use { baos -> 23 | BZip2CompressorOutputStream(baos, 1).use { bzip2 -> 24 | val block = ByteArray(4096) 25 | while (true) { 26 | val len = input.read(block) 27 | if (len == -1) break 28 | 29 | bzip2.write(block, 0, len) 30 | } 31 | } 32 | baos.toByteArray() 33 | } 34 | } 35 | 36 | // Strip the BZIP header off 37 | val stripped = ByteArray(buffer.size - 4) 38 | System.arraycopy(buffer, 4, stripped, 0, stripped.size) 39 | 40 | return stripped; 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/filesystem/compression/ContainerCompression.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.filesystem.compression 2 | 3 | enum class ContainerCompression(val id: Int) { 4 | NONE(0), 5 | BZIP2(1), 6 | GZIP(2), 7 | LZMA(3); 8 | 9 | companion object { 10 | val values = values() 11 | 12 | fun of(id: Int): ContainerCompression { 13 | for (value in values) { 14 | if(value.id == id) return value 15 | } 16 | throw NullPointerException("No compression found for id: $id") 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/filesystem/compression/GZIPCompression.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.filesystem.compression 2 | 3 | import java.io.ByteArrayInputStream 4 | import java.io.ByteArrayOutputStream 5 | import java.util.zip.GZIPInputStream 6 | import java.util.zip.GZIPOutputStream 7 | 8 | object GZIPCompression { 9 | fun decompress(compressed: ByteArray): ByteArray { 10 | return GZIPInputStream(ByteArrayInputStream(compressed)).use { it.readBytes() } 11 | } 12 | 13 | fun compress(uncompressed: ByteArray): ByteArray { 14 | return ByteArrayInputStream(uncompressed).use { input -> 15 | ByteArrayOutputStream().use { baos -> 16 | GZIPOutputStream(baos).use { gzip -> 17 | val block = ByteArray(4096) 18 | while (true) { 19 | val len = input.read(block) 20 | if (len == -1) break 21 | 22 | gzip.write(block, 0, len) 23 | } 24 | } 25 | baos.toByteArray() 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/filesystem/compression/LZMACompression.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.filesystem.compression 2 | 3 | import lzma.sdk.lzma.Decoder 4 | import lzma.sdk.lzma.Encoder 5 | import lzma.streams.LzmaEncoderWrapper 6 | import lzma.streams.LzmaOutputStream 7 | import java.io.ByteArrayInputStream 8 | import java.io.ByteArrayOutputStream 9 | import java.io.InputStream 10 | import java.io.OutputStream 11 | import java.util.* 12 | 13 | object LZMACompression { 14 | private val LZMA_DECODER = Decoder() 15 | 16 | private object RSLZMAEncoder : LzmaEncoderWrapper(null) { 17 | val encoder: Encoder = Encoder() 18 | 19 | override fun code(input: InputStream?, output: OutputStream?) { 20 | encoder.writeCoderProperties(output) 21 | encoder.code(input, output, -1, -1, null) 22 | } 23 | } 24 | 25 | fun decompress(compressed: ByteArray, size: Int): ByteArray { 26 | val properties = ByteArray(5) 27 | val body = ByteArray(compressed.size - 5) 28 | 29 | System.arraycopy(compressed, 0, properties, 0, 5) 30 | System.arraycopy(compressed, 5, body, 0, compressed.size - 5) 31 | 32 | synchronized(LZMA_DECODER) { 33 | if (!LZMA_DECODER.setDecoderProperties(properties)) 34 | throw IllegalStateException("Invalid LZMA decoder properties: ${Arrays.toString(properties)}") 35 | 36 | return ByteArrayInputStream(body).use { inStream -> 37 | ByteArrayOutputStream(size).use { outStream -> 38 | LZMA_DECODER.code(inStream, outStream, size.toLong()) 39 | outStream.toByteArray() 40 | } 41 | } 42 | } 43 | } 44 | 45 | fun compress(uncompressed: ByteArray): ByteArray { 46 | synchronized(RSLZMAEncoder) { 47 | return ByteArrayInputStream(uncompressed).use { input -> 48 | ByteArrayOutputStream().use { baos -> 49 | LzmaOutputStream(baos, RSLZMAEncoder).use { lzma -> 50 | val block = ByteArray(4096) 51 | while (true) { 52 | val len = input.read(block) 53 | if (len == -1) break 54 | 55 | lzma.write(block, 0, len) 56 | } 57 | } 58 | 59 | baos.toByteArray() 60 | } 61 | } 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/filesystem/prefetches/ArchivePrefetch.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.filesystem.prefetches 2 | 3 | import com.opennxt.filesystem.Filesystem 4 | 5 | class ArchivePrefetch(private val index: Int, private val archive: Int) : Prefetch { 6 | override fun calculateValue(store: Filesystem): Int { 7 | return store.read(index, archive)!!.capacity() - 2 8 | } 9 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/filesystem/prefetches/FilePrefetch.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.filesystem.prefetches 2 | 3 | import com.opennxt.filesystem.Filesystem 4 | 5 | class FilePrefetch(private val index: Int, private val name: String) : Prefetch { 6 | override fun calculateValue(store: Filesystem): Int { 7 | val file = store.read(index, name.toLowerCase()) ?: return 0 8 | 9 | return file.capacity() - 2 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/filesystem/prefetches/IndexPrefetch.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.filesystem.prefetches 2 | 3 | import com.opennxt.filesystem.Filesystem 4 | 5 | class IndexPrefetch(private val index: Int) : Prefetch { 6 | override fun calculateValue(store: Filesystem): Int { 7 | var value = 0 8 | val buf = store.readReferenceTable(index)!! 9 | val table = store.getReferenceTable(index)!! 10 | 11 | if (table.mask and 0x4 != 0) { 12 | value += table.totalCompressedSize().toInt() 13 | } else { 14 | for (entry in table.archives.keys) { 15 | value += store.read(index, entry)!!.capacity() - 2 16 | } 17 | } 18 | 19 | 20 | return value + buf.capacity() 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/filesystem/prefetches/LibraryPrefetch.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.filesystem.prefetches 2 | 3 | import com.opennxt.filesystem.Filesystem 4 | 5 | class LibraryPrefetch(private val name: String) : Prefetch { 6 | override fun calculateValue(store: Filesystem): Int { 7 | val file = store.read(30, name.toLowerCase()) ?: return 0 8 | 9 | return file.capacity() - 2 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/filesystem/prefetches/Prefetch.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.filesystem.prefetches 2 | 3 | import com.opennxt.filesystem.Filesystem 4 | 5 | interface Prefetch { 6 | fun calculateValue(store: Filesystem): Int 7 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/filesystem/prefetches/PrefetchTable.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.filesystem.prefetches 2 | 3 | import com.opennxt.filesystem.Filesystem 4 | import com.opennxt.filesystem.Index 5 | import io.netty.buffer.ByteBuf 6 | import io.netty.buffer.Unpooled 7 | 8 | class PrefetchTable(val entries: IntArray) { 9 | companion object { 10 | val RS3_DEFAULT = arrayOf( 11 | IndexPrefetch(Index.DEFAULTS), 12 | LibraryPrefetch("windows/x86/jaclib.dll"), 13 | LibraryPrefetch("windows/x86/jaggl.dll"), 14 | LibraryPrefetch("windows/x86/jagdx.dll"), 15 | LibraryPrefetch("windows/x86/sw3d.dll"), 16 | LibraryPrefetch("RuneScape-setup.exe"), 17 | LibraryPrefetch("windows/x86/hw3d.dll"), 18 | IndexPrefetch(Index.SHADERS), 19 | IndexPrefetch(Index.MATERIALS), 20 | IndexPrefetch(Index.CONFIG), 21 | IndexPrefetch(Index.CONFIG_OBJECT), 22 | IndexPrefetch(Index.CONFIG_ENUM), 23 | IndexPrefetch(Index.CONFIG_NPC), 24 | IndexPrefetch(Index.CONFIG_ITEM), 25 | IndexPrefetch(Index.CONFIG_SEQ), 26 | IndexPrefetch(Index.CONFIG_SPOT), 27 | IndexPrefetch(Index.CONFIG_STRUCT), 28 | IndexPrefetch(Index.DBTABLEINDEX), 29 | IndexPrefetch(Index.QUICKCHAT), 30 | IndexPrefetch(Index.QUICKCHAT_GLOBAL), 31 | IndexPrefetch(Index.PARTICLES), 32 | IndexPrefetch(Index.BILLBOARDS), 33 | FilePrefetch(Index.BINARY, "huffman"), 34 | IndexPrefetch(Index.INTERFACES), 35 | IndexPrefetch(Index.CLIENTSCRIPTS), 36 | IndexPrefetch(Index.FONTMETRICS), 37 | ArchivePrefetch(Index.WORLDMAP, 0), 38 | IndexPrefetch(57), 39 | IndexPrefetch(58), 40 | IndexPrefetch(59), 41 | IndexPrefetch(60), 42 | ) 43 | 44 | fun of(fs: Filesystem, prefetches: Array = RS3_DEFAULT): PrefetchTable = 45 | PrefetchTable(prefetches.map { it.calculateValue(fs) }.toIntArray()) 46 | 47 | fun decode(buffer: ByteBuf, prefetches: Array = RS3_DEFAULT): PrefetchTable { 48 | TODO("prefetch table decoding") 49 | } 50 | } 51 | 52 | fun encode(out: ByteBuf = Unpooled.buffer(entries.size * 4)): ByteBuf { 53 | for (entry in entries) 54 | out.writeInt(entry) 55 | return out 56 | } 57 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/impl/stat/PlayerStatData.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.impl.stat 2 | 3 | import com.opennxt.api.stat.Stat 4 | import com.opennxt.api.stat.StatData 5 | 6 | data class PlayerStatData( 7 | override val stat: Stat, 8 | override var experience: Double = 0.0, 9 | override var actualLevel: Int = 1, 10 | override var boostedLevel: Int = 1, 11 | var bonusExp: Double = 0.0 12 | ) : StatData { 13 | fun calculateLevel(enforceMaxLevel: Boolean = true): Int = calculateLevel(experience.toInt(), enforceMaxLevel) 14 | 15 | fun calculateLevel(xp: Int, enforceMaxLevel: Boolean = true): Int { 16 | val level = stat.table.levelForXp(xp) 17 | if (enforceMaxLevel && level > stat.def.cap) return stat.def.cap 18 | return level 19 | } 20 | 21 | private fun validateXp() { 22 | if (experience < 0.0) experience = 0.0 23 | else if (experience > 200_000_000.0) experience = 200_000_000.0 24 | } 25 | 26 | fun addExperience(amount: Double): Int { 27 | experience += amount 28 | validateXp() 29 | 30 | val previousLevel = actualLevel 31 | val newLevel = calculateLevel() 32 | if (newLevel != previousLevel) { 33 | actualLevel = newLevel 34 | } 35 | 36 | return newLevel - previousLevel 37 | } 38 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/login/LoginContext.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.login 2 | 3 | import com.opennxt.model.Build 4 | import com.opennxt.net.login.LoginPacket 5 | import io.netty.channel.Channel 6 | import java.util.* 7 | 8 | class LoginContext( 9 | val packet: LoginPacket, 10 | val callback: (LoginContext) -> Unit, 11 | val build: Build, 12 | val username: String, 13 | val password: String, 14 | var attempt: Int = 0, 15 | var uuid: UUID? = null, 16 | val channel: Channel, 17 | var result: LoginResult = LoginResult.FAILED_LOADING 18 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/login/LoginResult.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.login 2 | 3 | import com.opennxt.net.GenericResponse 4 | import mu.KotlinLogging 5 | import java.util.* 6 | 7 | enum class LoginResult(val code: GenericResponse) { 8 | SUCCESS(GenericResponse.SUCCESSFUL), 9 | FAILED_LOADING(GenericResponse.FAILED_LOADING_PROFILE), 10 | DATABASE_TIMEOUT(GenericResponse.LOGINSERVER_OFFLINE), 11 | DATABASE_ERROR(GenericResponse.INVALID_LOGIN_SERVER_RESPONSE), 12 | INVALID_USERNAME_PASS(GenericResponse.INVALID_USERNAME_OR_PASSWORD), 13 | WORLD_FULL(GenericResponse.WORLD_FULL), 14 | LOCKED(GenericResponse.ACCOUNT_LOCKED), 15 | BANNED(GenericResponse.TEMPORARILY_BANNED), 16 | LOGGED_IN(GenericResponse.LOGGED_IN), 17 | OUT_OF_DATE(GenericResponse.OUT_OF_DATE), 18 | ; 19 | 20 | companion object { 21 | private val logger = KotlinLogging.logger { } 22 | 23 | private val REVERSE_LOOKUP = EnumMap(GenericResponse::class.java) 24 | 25 | init { 26 | values().forEach { res -> REVERSE_LOOKUP[res.code] = res } 27 | } 28 | 29 | fun reverse(response: GenericResponse): LoginResult { 30 | val reversed = REVERSE_LOOKUP[response] 31 | if (reversed != null) 32 | return reversed 33 | logger.warn { "Couldn't find GenericResponse->LoginResult mapping for $response, returning LOCKED" } 34 | return LOCKED 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/main.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt 2 | 3 | import com.github.ajalt.clikt.core.NoRunCliktCommand 4 | import com.github.ajalt.clikt.core.subcommands 5 | import com.opennxt.tools.ToolExecutor 6 | 7 | fun main(args: Array) { 8 | NoRunCliktCommand(name = "open-nxt", help = "Base command for the OpenNXT server") 9 | .subcommands(OpenNXT, ToolExecutor) 10 | .main(args) 11 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/Build.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model 2 | 3 | data class Build(val major: Int, val minor: Int) -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/InterfaceHash.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model 2 | 3 | inline class InterfaceHash(val hash: Int) { 4 | val parent: Int 5 | get() = hash shr 16 6 | val component: Int 7 | get() = hash and 0xffff 8 | 9 | constructor(parent: Int, component: Int) : this((parent shl 16) or component) 10 | 11 | override fun toString(): String = "InterfaceHash(parent=$parent, component=$component)" 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/commands/Command.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.commands 2 | 3 | abstract class Command { 4 | abstract fun autocomplete(sender: CommandSender, alias: String, command: String): Collection 5 | 6 | abstract fun execute(sender: CommandSender, alias: String, command: String) 7 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/commands/CommandException.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.commands 2 | 3 | class CommandException(message: String): Exception(message) -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/commands/CommandRepository.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.commands 2 | 3 | import com.opennxt.model.commands.impl.proxy.HexdumpOffCommand 4 | import com.opennxt.model.commands.impl.proxy.HexdumpOnCommand 5 | import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap 6 | import mu.KotlinLogging 7 | 8 | class CommandRepository { 9 | private val logger = KotlinLogging.logger { } 10 | 11 | val commands = Object2ObjectOpenHashMap() 12 | 13 | init { 14 | commands["hexdump-on"] = HexdumpOnCommand 15 | commands["hexdump-off"] = HexdumpOffCommand 16 | 17 | logger.info { "Registered ${commands.size} commands" } 18 | } 19 | 20 | fun complete(sender: CommandSender, input: String): Collection { 21 | val split = input.split(" ", limit = 2) 22 | val commandName = split[0].toLowerCase() 23 | 24 | val match = commands[commandName] 25 | if (match != null) { 26 | return match.autocomplete(sender, split[0], if (split.size == 1) "" else split[1]) 27 | } 28 | 29 | if (split.size == 1) 30 | return commands.keys.filter { it.startsWith(input.toLowerCase()) } 31 | 32 | throw CommandException("Could not find a command named '${commandName}'") 33 | } 34 | 35 | fun execute(sender: CommandSender, input: String) { 36 | val split = input.split(" ", limit = 2) 37 | val commandName = split[0].toLowerCase() 38 | 39 | val match = commands[commandName] ?: throw CommandException("Command not found: '$commandName'") 40 | 41 | match.execute(sender, split[0], if (split.size == 1) "" else split[1]) 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/commands/CommandSender.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.commands 2 | 3 | import com.opennxt.model.messages.MessageReceiver 4 | import com.opennxt.model.permissions.PermissionsHolder 5 | 6 | interface CommandSender: PermissionsHolder, MessageReceiver { 7 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/commands/SimpleCommand.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.commands 2 | 3 | abstract class SimpleCommand: Command() { 4 | override fun autocomplete(sender: CommandSender, alias: String, command: String): Collection = emptyList() 5 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/commands/impl/proxy/HexdumpOffCommand.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.commands.impl.proxy 2 | 3 | import com.opennxt.model.commands.CommandException 4 | import com.opennxt.model.commands.CommandSender 5 | import com.opennxt.model.commands.SimpleCommand 6 | import com.opennxt.net.proxy.ProxyPlayer 7 | 8 | object HexdumpOffCommand: SimpleCommand() { 9 | override fun execute(sender: CommandSender, alias: String, command: String) { 10 | if (sender !is ProxyPlayer) 11 | throw CommandException("This command can only be executed from a proxied connection") 12 | 13 | sender.console("Disabled hexdumps") 14 | sender.proxyClient.hexdump = false 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/commands/impl/proxy/HexdumpOnCommand.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.commands.impl.proxy 2 | 3 | import com.opennxt.model.commands.CommandException 4 | import com.opennxt.model.commands.CommandSender 5 | import com.opennxt.model.commands.SimpleCommand 6 | import com.opennxt.net.proxy.ProxyPlayer 7 | 8 | object HexdumpOnCommand: SimpleCommand() { 9 | override fun execute(sender: CommandSender, alias: String, command: String) { 10 | if (sender !is ProxyPlayer) 11 | throw CommandException("This command can only be executed from a proxied connection") 12 | 13 | sender.console("Enabled hexdumps") 14 | sender.proxyClient.hexdump = true 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/entity/BasePlayer.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.entity 2 | 3 | import com.opennxt.api.stat.StatContainer 4 | import com.opennxt.model.commands.CommandSender 5 | import com.opennxt.model.entity.player.InterfaceManager 6 | import com.opennxt.model.messages.Message 7 | import com.opennxt.model.tick.Tickable 8 | import com.opennxt.net.ConnectedClient 9 | import com.opennxt.net.game.GamePacket 10 | import mu.KotlinLogging 11 | 12 | abstract class BasePlayer(var client: ConnectedClient, val name: String): CommandSender, Tickable { 13 | abstract val interfaces: InterfaceManager 14 | abstract val stats: StatContainer 15 | 16 | var noTimeouts = 0 17 | private val logger = KotlinLogging.logger { } 18 | 19 | override fun message(message: Message) { 20 | client.write(message.createPacket()) 21 | } 22 | 23 | override fun message(message: String) { 24 | client.write(Message.ConsoleMessage(message).createPacket()) 25 | } 26 | 27 | override fun console(message: String) { 28 | client.write(Message.ConsoleMessage(message).createPacket()) 29 | } 30 | 31 | override fun error(message: String) { 32 | client.write(Message.ConsoleError(message).createPacket()) 33 | } 34 | 35 | override fun hasPermissions(node: String): Boolean { 36 | return true 37 | } 38 | 39 | override fun tick() { 40 | 41 | } 42 | 43 | fun write(message: GamePacket) { 44 | client.write(message) 45 | } 46 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/entity/Entity.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.entity 2 | 3 | import com.opennxt.model.entity.movement.Movement 4 | import com.opennxt.model.entity.rendering.EntityRenderer 5 | import com.opennxt.model.world.TileLocation 6 | 7 | /** 8 | * Anything that has a position is an entity. 9 | * 10 | * Entities that can take damage are LivingEntity s. 11 | * 12 | * Ground items, objects etc... are also entities. 13 | */ 14 | abstract class Entity(var location: TileLocation) { 15 | var index: Int = -1 16 | var previousLocation: TileLocation = location 17 | 18 | abstract val renderer: EntityRenderer 19 | abstract val movement: Movement 20 | 21 | abstract fun clean() 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/entity/EntityList.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.entity 2 | 3 | /** 4 | * A list containing entities that automatically assigns indices to the entities that are added 5 | */ 6 | class EntityList(val capacity: Int) : Iterable { 7 | 8 | /** 9 | * The entities that are currently present in the list 10 | */ 11 | private val values = arrayOfNulls(capacity) 12 | 13 | /** 14 | * The current size of the list 15 | */ 16 | private var size: Int = 0 17 | 18 | init { 19 | if (capacity > Short.MAX_VALUE) 20 | throw IllegalArgumentException("Repository can't be bigger than the max short value") 21 | } 22 | 23 | /** 24 | * Adds an entity to the list, assigning it an id, and returning whether if the entity was added successfully 25 | */ 26 | fun add(entity: T): Boolean { 27 | if(isFull()) return false 28 | 29 | for(i in 0 until capacity) { 30 | if (values[i] != null) continue 31 | values[i] = entity 32 | entity.index = i + 1 33 | size++ 34 | return true 35 | } 36 | return false 37 | } 38 | 39 | /** 40 | * Checks if the list is full 41 | */ 42 | fun isFull(): Boolean = size == capacity 43 | 44 | /** 45 | * Checks the amount of entities currently in the list 46 | */ 47 | fun size(): Int = size 48 | 49 | /** 50 | * Removes an entity from the list by the entity 51 | */ 52 | fun remove(entity: T) { 53 | remove(entity.index - 1) 54 | } 55 | 56 | /** 57 | * Removes an entity from the list by the index 58 | */ 59 | fun remove(index: Int) { 60 | if (index < 0) return 61 | val current = values[index] ?: return 62 | 63 | if (current.index - 1 != index) 64 | throw IllegalStateException("Entity is in the wrong spot in EntityList. This should never happen.") 65 | 66 | values[index] = null 67 | current.index = -1 68 | size-- 69 | } 70 | 71 | /** 72 | * Gets an entity by the index of the entity 73 | */ 74 | @Suppress("UNCHECKED_CAST") 75 | operator fun get(index: Int): T? { 76 | if (index < 1 || index > capacity) 77 | return null 78 | return values[index - 1] as? T 79 | } 80 | 81 | override fun iterator(): Iterator = EntityListIterator(this) 82 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/entity/EntityListIterator.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.entity 2 | 3 | /** 4 | * An iterator to iterate over an [EntityList] 5 | */ 6 | class EntityListIterator(val list: EntityList) : Iterator { 7 | 8 | /** 9 | * The current index of the iterator 10 | */ 11 | private var index = 0 12 | 13 | override fun hasNext(): Boolean { 14 | var i = index + 1 15 | 16 | while (i <= list.capacity) { 17 | if (list[i] != null) 18 | return true 19 | i++ 20 | } 21 | return false 22 | } 23 | 24 | override fun next(): T { 25 | while (index <= list.capacity) { 26 | val current = list[++index] 27 | if (current != null) return current 28 | } 29 | 30 | throw NoSuchElementException("Reached end of list") 31 | } 32 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/entity/LivingEntity.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.entity 2 | 3 | import com.opennxt.model.world.TileLocation 4 | 5 | abstract class LivingEntity(location: TileLocation): Entity(location) { 6 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/entity/PlayerEntity.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.entity 2 | 3 | import com.opennxt.model.entity.movement.Movement 4 | import com.opennxt.model.entity.player.appearance.PlayerModel 5 | import com.opennxt.model.entity.rendering.EntityRenderer 6 | import com.opennxt.model.world.TileLocation 7 | import com.opennxt.model.world.WorldPlayer 8 | 9 | class PlayerEntity(location: TileLocation) : LivingEntity(location) { 10 | override val renderer = EntityRenderer(this) 11 | override val movement = Movement(this) 12 | 13 | val model = PlayerModel(this) 14 | 15 | var controllingPlayer: WorldPlayer? = null 16 | 17 | override fun clean() { 18 | TODO("Clean") 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/entity/movement/CompassPoint.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.entity.movement 2 | 3 | enum class CompassPoint(val id: Int, val dx: Int, val dy: Int, val diagonal: Boolean) { 4 | NORTH(0, 0, 1, false), 5 | NORTHEAST(1, 1, 1, true), 6 | EAST(2, 1, 0, false), 7 | SOUTHEAST(3, 1, -1, true), 8 | SOUTH(4, 0, -1, false), 9 | SOUTHWEST(5, -1, -1, true), 10 | WEST(6, -1, 0, false), 11 | NORTHWEST(7, -1, 1, true); 12 | 13 | companion object { 14 | fun getById(id: Int): CompassPoint? { 15 | return when (id) { 16 | 0 -> NORTH 17 | 1 -> NORTHEAST 18 | 2 -> EAST 19 | 3 -> SOUTHEAST 20 | 4 -> SOUTH 21 | 5 -> SOUTHWEST 22 | 6 -> WEST 23 | 7 -> NORTHWEST 24 | else -> EAST 25 | } 26 | } 27 | 28 | fun forDelta(dx: Int, dy: Int): CompassPoint? { 29 | return when { 30 | dy >= 1 && dx >= 1 -> NORTHEAST 31 | dy <= -1 && dx >= 1 -> SOUTHEAST 32 | dy <= -1 && dx <= -1 -> SOUTHWEST 33 | dy >= 1 && dx <= -1 -> NORTHWEST 34 | dy >= 1 -> NORTH 35 | dx >= 1 -> EAST 36 | dy <= -1 -> SOUTH 37 | dx <= -1 -> WEST 38 | else -> null 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/entity/movement/Movement.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.entity.movement 2 | 3 | import com.opennxt.model.entity.Entity 4 | import com.opennxt.model.entity.PlayerEntity 5 | import com.opennxt.model.world.TileLocation 6 | import java.util.* 7 | 8 | class Movement(val entity: Entity) { 9 | private val queue = LinkedList() 10 | private val isPlayer = entity is PlayerEntity 11 | var speed = MovementSpeed.RUN 12 | var currentSpeed = MovementSpeed.STATIONARY 13 | var nextWalkDirection: CompassPoint? = null 14 | var nextRunDirection: CompassPoint? = null 15 | var teleportLocation: TileLocation? = null 16 | 17 | fun process() { 18 | 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/entity/movement/MovementSpeed.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.entity.movement 2 | 3 | enum class MovementSpeed(val id: Int) { 4 | STATIONARY(-1), 5 | CRAWL(0), 6 | WALK(1), 7 | RUN(2), 8 | INSTANT(3); 9 | 10 | companion object { 11 | private val VALUES = values() 12 | 13 | fun getById(id: Int): MovementSpeed? = VALUES.firstOrNull { it.id == id } 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/entity/player/appearance/Gender.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.entity.player.appearance 2 | 3 | enum class Gender { 4 | MALE, 5 | FEMALE 6 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/entity/player/appearance/RenderType.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.entity.player.appearance 2 | 3 | enum class RenderType { 4 | PLAYER, 5 | NPC, 6 | INVISIBLE 7 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/entity/rendering/EntityRenderer.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.entity.rendering 2 | 3 | import com.opennxt.model.entity.Entity 4 | import com.opennxt.model.entity.PlayerEntity 5 | import com.opennxt.model.world.WorldPlayer 6 | 7 | /** 8 | * The [EntityRenderer] keeps track of the update blocks of the [Entity] that are used in the player and npc sync 9 | * packets. Update blocks can be added to an entity through a readable interface using the [EntityRenderer]. 10 | */ 11 | class EntityRenderer(val entity: Entity) { 12 | val blocks = arrayOfNulls(30) 13 | private val isPlayer = entity is PlayerEntity 14 | 15 | /** 16 | * Checks if this renderer should be updated for a certain viewer (see [UpdateBlock.needsUpdate]) 17 | */ 18 | fun needsUpdate(viewer: WorldPlayer): Boolean { 19 | for (block in blocks) { 20 | if (block == null) continue 21 | if (block.needsUpdate(viewer)) return true 22 | } 23 | return false 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/entity/rendering/UpdateBlock.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.entity.rendering 2 | 3 | import com.opennxt.model.entity.Entity 4 | import com.opennxt.model.world.WorldPlayer 5 | import com.opennxt.net.buf.GamePacketBuilder 6 | 7 | abstract class UpdateBlock(val type: UpdateBlockType) { 8 | abstract fun encode(buffer: GamePacketBuilder, viewer: WorldPlayer, entity: Entity) 9 | 10 | /** 11 | * Whether if a player needs to see a certain update or not. This is not always true: 12 | * 13 | * For example, with mining, the player can occasionally get hitsplats, which are only visible to the player that is 14 | * mining. Any other player should not have to see this. 15 | */ 16 | fun needsUpdate(viewer: WorldPlayer): Boolean = true 17 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/entity/rendering/UpdateBlockType.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.entity.rendering 2 | 3 | enum class UpdateBlockType( 4 | val playerMask: Int, 5 | val playerPos: Int, 6 | val npcMask: Int = 0, 7 | val npcPos: Int = -1 8 | ) { 9 | APPEARANCE(0x1, 3) 10 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/entity/rendering/blocks/AppearanceUpdateBlock.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.entity.rendering.blocks 2 | 3 | import com.opennxt.model.entity.Entity 4 | import com.opennxt.model.entity.PlayerEntity 5 | import com.opennxt.model.entity.rendering.UpdateBlock 6 | import com.opennxt.model.entity.rendering.UpdateBlockType 7 | import com.opennxt.model.world.WorldPlayer 8 | import com.opennxt.net.buf.DataTransformation 9 | import com.opennxt.net.buf.DataType 10 | import com.opennxt.net.buf.GamePacketBuilder 11 | 12 | class AppearanceUpdateBlock : UpdateBlock(UpdateBlockType.APPEARANCE) { 13 | override fun encode(buffer: GamePacketBuilder, viewer: WorldPlayer, entity: Entity) { 14 | if (entity is PlayerEntity) { 15 | val data = entity.model.data 16 | 17 | buffer.put(DataType.BYTE, DataTransformation.NEGATE, data.size) 18 | buffer.putBytesReverse(data) 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/files/BinaryType.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.files 2 | 3 | enum class BinaryType(val id: Int) { 4 | WINXP(0), 5 | WIN32(1), 6 | WIN64(2), 7 | MACOSX(3), 8 | LINUX(4), 9 | WIN32C(5), 10 | WIN64C(6), 11 | MOBILE(7), 12 | 13 | ; 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/files/DownloadInformation.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.files 2 | 3 | data class DownloadInformation(val id: Int, val name: String, val crc: Long, val hash: String) -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/lobby/Lobby.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.lobby 2 | 3 | import com.opennxt.model.tick.Tickable 4 | import mu.KotlinLogging 5 | import java.util.concurrent.ConcurrentLinkedQueue 6 | 7 | class Lobby: Tickable { 8 | private val logger = KotlinLogging.logger { } 9 | 10 | private val players = HashSet() 11 | private val toAdd = ConcurrentLinkedQueue() 12 | 13 | override fun tick() { 14 | while (true) { 15 | val player = toAdd.poll() ?: break 16 | players += player 17 | player.added() 18 | } 19 | 20 | players.forEach { it.handleIncomingPackets() } 21 | players.forEach { it.tick() } 22 | players.forEach { it.client.flush() } 23 | } 24 | 25 | fun addPlayer(player: LobbyPlayer) { 26 | toAdd += player 27 | } 28 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/messages/Message.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.messages 2 | 3 | import com.opennxt.net.game.serverprot.MessageGame 4 | 5 | abstract class Message { 6 | abstract fun createPacket(): MessageGame 7 | 8 | open class SimpleMessage(val type: Int, val message: String): Message() { 9 | override fun createPacket(): MessageGame = MessageGame(type, message) 10 | } 11 | 12 | class ConsoleError(message: String): SimpleMessage(96, message) 13 | class ConsoleAutocomplete(message: String): SimpleMessage(98, message) 14 | class ConsoleMessage(message: String): SimpleMessage(99, message) 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/messages/MessageReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.messages 2 | 3 | interface MessageReceiver { 4 | fun message(message: Message) 5 | fun message(message: String) 6 | fun console(message: String) 7 | fun error(message: String) 8 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/messages/MessageType.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.messages 2 | 3 | enum class MessageType(val id: Int) { 4 | UNFILTERABLE_GAME(0), 5 | CONSOLE_ERROR(96), 6 | CONSOLE_AUTOCOMPLETE(98), 7 | CONSOLE(99), 8 | FILTERABLE_GAME(109) 9 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/permissions/PermissionsHolder.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.permissions 2 | 3 | interface PermissionsHolder { 4 | fun hasPermissions(node: String): Boolean 5 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/proxy/PacketDumper.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.proxy 2 | 3 | import io.netty.buffer.ByteBuf 4 | import java.io.OutputStream 5 | import java.nio.ByteBuffer 6 | import java.nio.file.Files 7 | import java.nio.file.Path 8 | import java.time.Instant 9 | import java.util.concurrent.atomic.AtomicBoolean 10 | 11 | class PacketDumper(file: Path) : AutoCloseable { 12 | var file: Path = file 13 | set(value) { 14 | if (open.get()) { 15 | throw IllegalStateException("Attempted to set path while file was already opened") 16 | } 17 | 18 | field = value 19 | } 20 | 21 | private val open = AtomicBoolean(false) 22 | private val lock = Any() 23 | 24 | private lateinit var stream: OutputStream 25 | 26 | private fun ensureOpen() { 27 | if (!open.get()) { 28 | if (!Files.exists(file.parent)) 29 | Files.createDirectories(file.parent) 30 | 31 | Files.createFile(file) 32 | stream = Files.newOutputStream(file) 33 | 34 | open.set(true) 35 | } 36 | } 37 | 38 | fun dump(opcode: Int, data: ByteBuf) { 39 | val raw = ByteArray(data.readableBytes()) 40 | data.markReaderIndex() 41 | data.readBytes(raw) 42 | data.resetReaderIndex() 43 | 44 | dump(opcode, raw) 45 | } 46 | 47 | fun dump(opcode: Int, data: ByteArray) { 48 | synchronized(lock) { 49 | ensureOpen() 50 | 51 | if (!open.get()) { 52 | throw IllegalStateException("Tried to write to closed file") 53 | } 54 | 55 | val toWrite = ByteArray(data.size + 14) 56 | 57 | val wrapper = ByteBuffer.wrap(toWrite) 58 | wrapper.putLong(Instant.now().toEpochMilli()) 59 | wrapper.putShort(opcode.toShort()) 60 | wrapper.putInt(data.size) 61 | wrapper.put(data) 62 | 63 | stream.write(toWrite) 64 | } 65 | } 66 | 67 | override fun close() { 68 | synchronized(lock) { 69 | open.set(false) 70 | 71 | stream.close() 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/tick/TickEngine.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.tick 2 | 3 | import com.google.common.util.concurrent.ThreadFactoryBuilder 4 | import mu.KotlinLogging 5 | import java.util.concurrent.Executors 6 | import java.util.concurrent.TimeUnit 7 | import kotlin.math.min 8 | 9 | class TickEngine { 10 | private val logger = KotlinLogging.logger { } 11 | 12 | private val executor = Executors.newScheduledThreadPool( 13 | min(1, Runtime.getRuntime().availableProcessors() - 2), 14 | ThreadFactoryBuilder() 15 | .setNameFormat("tick-engine") 16 | .setUncaughtExceptionHandler { t, e -> logger.error("Error with thread $t", e) } 17 | .build()) 18 | 19 | /** 20 | * Tickables submitted here will indefinitely be invoked once every 600 milliseconds. 21 | * 22 | * The same tickable won't be called concurrently. If ticking takes, for example, 1000 milliseconds, the next 23 | * execution will start 400 millis late. 24 | */ 25 | fun submitTickable(tickable: Tickable) { 26 | executor.scheduleAtFixedRate(tickable::tick, 0, 600, TimeUnit.MILLISECONDS) 27 | } 28 | 29 | /** 30 | * Executes a task asynchronously 31 | */ 32 | fun executeAsync(delay: Long = 0L, runnable: () -> Unit) { 33 | if (delay <= 0L) executor.execute(runnable) 34 | else executor.schedule(runnable, delay, TimeUnit.MILLISECONDS) 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/tick/Tickable.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.tick 2 | 3 | interface Tickable { 4 | fun tick() 5 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/world/MapSize.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.world 2 | 3 | enum class MapSize(val id: Int, val size: Int) { 4 | SIZE_104(0, 104), 5 | SIZE_120(1, 120), 6 | SIZE_136(2, 136), 7 | SIZE_168(3, 168), 8 | SIZE_72(4, 72), 9 | SIZE_256(5, 256); 10 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/world/TileLocation.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.world 2 | 3 | import kotlin.math.abs 4 | 5 | class TileLocation( 6 | x: Int, 7 | y: Int, 8 | plane: Int = 0 9 | ) : Cloneable { 10 | 11 | var x: Int = x 12 | internal set 13 | var y: Int = y 14 | internal set 15 | var plane: Int = plane 16 | internal set 17 | 18 | val chunkX: Int get() = x shr 3 19 | val chunkY: Int get() = y shr 3 20 | val regionX: Int get() = x shr 6 21 | val regionY: Int get() = y shr 6 22 | val xInRegion: Int get() = x and 0x3f 23 | val yInRegion: Int get() = y and 0x3f 24 | val tileHash: Int get() = y + (x shl 14) + (plane shl 28) 25 | val regionHash: Int get() = regionY + (regionX shl 8) + (plane shl 16) 26 | val regionId: Int get() = regionY + (regionX shl 8) 27 | 28 | constructor(localX: Int, localY: Int, plane: Int, regionId: Int) : this( 29 | x = localX + (regionId shr 8 and 0xff shl 6), 30 | y = localY + (regionId and 0xff shl 6), 31 | plane = plane 32 | ) 33 | 34 | fun getLocalX(other: TileLocation = this, size: MapSize = MapSize.SIZE_104): Int { 35 | return x - 8 * (other.chunkX - (size.size shr 4)) 36 | } 37 | 38 | fun getLocalY(other: TileLocation = this, size: MapSize = MapSize.SIZE_104): Int { 39 | return y - 8 * (other.chunkY - (size.size shr 4)) 40 | } 41 | 42 | fun withinDistance(other: TileLocation, distanceX: Int, distanceY: Int): Boolean { 43 | if (other.plane != plane) return false 44 | return abs(other.x - x) <= distanceX && abs(other.y - y) <= distanceY 45 | } 46 | 47 | fun withinDistance(other: TileLocation, distance: Int): Boolean = withinDistance(other, distance, distance) 48 | 49 | override fun equals(other: Any?): Boolean { 50 | if (other == null) return false 51 | if (other !is TileLocation) return false 52 | if (other.x != x) return false 53 | if (other.y != y) return false 54 | if (other.plane != plane) return false 55 | return true 56 | } 57 | 58 | override fun hashCode(): Int = tileHash 59 | 60 | override fun toString(): String = "TileLocation(x=$x, y=$y, plane=$plane)" 61 | 62 | fun distSquared(other: TileLocation): Int { 63 | if (other.plane != plane) 64 | return Int.MAX_VALUE 65 | val dx = other.x - x 66 | val dy = other.y - y 67 | return (dx * dx) + (dy * dy) 68 | } 69 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/world/World.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.world 2 | 3 | import com.opennxt.model.entity.EntityList 4 | import com.opennxt.model.entity.PlayerEntity 5 | import com.opennxt.model.lobby.LobbyPlayer 6 | import com.opennxt.model.tick.Tickable 7 | import mu.KotlinLogging 8 | import java.util.concurrent.ConcurrentLinkedQueue 9 | 10 | class World : Tickable { 11 | private val logger = KotlinLogging.logger { } 12 | 13 | private val playerEntities = EntityList(2000) 14 | private val players = HashSet() 15 | 16 | private val toAdd = ConcurrentLinkedQueue() 17 | 18 | override fun tick() { 19 | while (true) { 20 | val player = toAdd.poll() ?: break 21 | players += player 22 | playerEntities.add(player.entity) 23 | player.added() 24 | } 25 | 26 | players.forEach { it.handleIncomingPackets() } 27 | players.forEach { it.tick() } 28 | players.forEach { it.client.flush() } 29 | } 30 | 31 | fun getPlayer(index: Int): PlayerEntity? = playerEntities[index] 32 | 33 | fun addPlayer(player: WorldPlayer) { 34 | toAdd += player 35 | } 36 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/worldlist/WorldFlag.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.worldlist 2 | 3 | enum class WorldFlag(val bit: Int) { 4 | MEMBERS_ONLY(0), 5 | QUICK_CHAT(1), 6 | LOOT_SHARE(2), 7 | LEVEL_REQUIREMENT(7), 8 | VETERAN_WORLD(8), 9 | BETA_WORLD(16), 10 | HIGH_LEVEL_2000(18), 11 | HIGH_LEVEL_2600(19), 12 | VIP_WORLD(20), 13 | LEGACY_ONLY(22), 14 | EOC_ONLY(23), 15 | ; 16 | 17 | companion object { 18 | fun createFlag(vararg args: WorldFlag): Int { 19 | var flag = 0 20 | args.forEach { 21 | flag = flag or (1 shl it.bit) 22 | } 23 | return flag 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/worldlist/WorldListEntry.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.worldlist 2 | 3 | import com.google.common.base.Objects 4 | 5 | data class WorldListEntry( 6 | val id: Int, 7 | val location: WorldListLocation, 8 | val flag: Int, 9 | val locationOverride: WorldListLocation? = null, 10 | val activity: String, 11 | val host: String, 12 | var playercount: Int = 0 13 | ) { 14 | // Hash that only hashes the immutable fields 15 | val partialHash = Objects.hashCode(id, location, flag, locationOverride, activity, host) 16 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/model/worldlist/WorldListLocation.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.model.worldlist 2 | 3 | data class WorldListLocation(val flag: Int, val name: String) // TODO Figure out flag ids? -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/GenericResponse.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net 2 | 3 | enum class GenericResponse(val id: Int) { 4 | SUCCESSFUL_CONNECTION(0), 5 | DISPLAY_ADVERTISEMENT(1), 6 | SUCCESSFUL(2), 7 | INVALID_USERNAME_OR_PASSWORD(3), 8 | DISABLED_ACCOUNT(4), 9 | LOGGED_IN(5), 10 | OUT_OF_DATE(6), 11 | WORLD_FULL(7), 12 | LOGINSERVER_OFFLINE(8), 13 | LOGIN_LIMIT_EXCEEDED(9), 14 | BAD_SESSION(10), 15 | LOGINSERVER_REJECTED(11), 16 | MEMBERS_REQUIRED(12), 17 | INCOMPLETE_LOGIN(13), 18 | SERVER_UPDATING(14), 19 | LOGIN_ATTEMPTS_EXCEEDED(16), 20 | INSIDE_MEMBERS_AREA(17), 21 | ACCOUNT_LOCKED(18), 22 | FULLSCREEN_MEMBERS_ONLY(19), 23 | INVALID_LOGIN_SERVER(20), 24 | PROFILE_TRANSFERRING(21), 25 | MALFORMED_PACKET(22), 26 | NO_LOGIN_SERVER_REPLY(23), 27 | FAILED_LOADING_PROFILE(24), 28 | INVALID_LOGIN_SERVER_RESPONSE(25), 29 | MAC_BANNED(26), 30 | SERVICE_UNAVAILABLE(27), 31 | REQUIREMENT_WORLD(29), 32 | OLDSCHOOL_WORLD_MEMBER(30), 33 | INVALID_DISPLAYNAME(31), 34 | LOGIN_PREVENTED(32), 35 | SESSION_EXPIRED(35), 36 | AUTHENTICATION_OFFLINE(36), 37 | ACCOUNT_INACCESSIBLE(37), 38 | ACCOUNT_NOT_HTML5(38), 39 | INSTANCE_INVALID(39), 40 | WORLD_MEMBER(40), 41 | INSTANCE_FULL(41), 42 | SYSTEM_UNAVAILABLE(44), 43 | REQUIREMENT_LOCATION(45), 44 | INSTANCE_ERROR(46), 45 | EMAIL_VALIDATE(47), 46 | SESSION_ENDED(48), 47 | JAG_EMAIL_SENT(49), 48 | JAG_TOO_MANY_ATTEMPTS(50), 49 | JAG_ACCOUNT_NOT_VALIDATED_EMAIL(51), 50 | JAG_VALIDATE_DEVICE(52), 51 | TEMPORARILY_BANNED(53), 52 | JAG_ACCOUNT_NOT_AUTHORIZED(55), 53 | AUTHENTICATOR_CODE(56), 54 | AUTHENTICATOR_INCORRECT(57), 55 | ; 56 | 57 | companion object { 58 | private val VALUES = values() 59 | fun fromId(id: Int): GenericResponse? = VALUES.firstOrNull { it.id == id } 60 | } 61 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/Packet.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net 2 | 3 | interface Packet 4 | 5 | interface IncomingPacket: Packet 6 | 7 | interface OutgoingPacket: Packet -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/PacketCodec.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net 2 | 3 | import com.opennxt.net.buf.GamePacketBuilder 4 | import com.opennxt.net.buf.GamePacketReader 5 | 6 | interface PacketCodec { 7 | fun encode(packet: T, buf: GamePacketBuilder) 8 | 9 | fun decode(buf: GamePacketReader): T 10 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/RSChannelAttributes.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net 2 | 3 | import com.opennxt.net.login.LoginType 4 | import com.opennxt.util.ISAACCipher 5 | import io.netty.channel.Channel 6 | import io.netty.util.AttributeKey 7 | 8 | object RSChannelAttributes { 9 | // An id generated by the server that the client sends in the RSA-encrypted login block. Prevents packet replaying 10 | // attacks. 11 | val LOGIN_UNIQUE_ID = AttributeKey.newInstance("login-unique-id") 12 | 13 | val LOGIN_TYPE = AttributeKey.newInstance("login-type") 14 | val LOGIN_USERNAME = AttributeKey.newInstance("login-username") 15 | 16 | val INCOMING_ISAAC = AttributeKey.newInstance("incoming-isaac") 17 | val OUTGOING_ISAAC = AttributeKey.newInstance("outgoing-isaac") 18 | 19 | val SIDE = AttributeKey.newInstance("side") 20 | val PASSTHROUGH_CHANNEL = AttributeKey.newInstance("passthrough-channel") 21 | 22 | val CONNECTED_CLIENT = AttributeKey.newInstance("connected-client") 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/RSChannelInitializer.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net 2 | 3 | import com.opennxt.net.handshake.HandshakeDecoder 4 | import com.opennxt.net.handshake.HandshakeHandler 5 | import io.netty.channel.ChannelInitializer 6 | import io.netty.channel.socket.SocketChannel 7 | import mu.KotlinLogging 8 | 9 | class RSChannelInitializer: ChannelInitializer() { 10 | val logger = KotlinLogging.logger { } 11 | 12 | override fun initChannel(ch: SocketChannel) { 13 | logger.info { "TODO : Accept inbound connection from ${ch.remoteAddress()} to port ${ch.localAddress().port}" } 14 | 15 | ch.pipeline() 16 | .addLast("handshake-decoder", HandshakeDecoder()) 17 | .addLast("handshake-handler", HandshakeHandler()) 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/Side.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net 2 | 3 | enum class Side { CLIENT, SERVER } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/buf/AccessMode.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.buf 2 | 3 | /** 4 | * An enumeration which holds the mode a [GamePacketBuilder] or [GamePacketReader] can be in. 5 | * 6 | * @author Graham 7 | */ 8 | enum class AccessMode { 9 | 10 | /** 11 | * When in bit access mode, bits can be written and packed into bytes. 12 | */ 13 | BIT_ACCESS, 14 | 15 | /** 16 | * When in byte access modes, bytes are written directly to the buffer. 17 | */ 18 | BYTE_ACCESS 19 | 20 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/buf/DataConstants.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.buf 2 | 3 | /** 4 | * A class holding data-related constants. 5 | * 6 | * @author Graham 7 | */ 8 | object DataConstants { 9 | 10 | /** 11 | * An array of bit masks. The element `n` is equal to `2n - 1`. 12 | */ 13 | val BIT_MASK = IntArray(32) 14 | 15 | /** 16 | * Initializes the [.BIT_MASK] array. 17 | */ 18 | init { 19 | for (i in BIT_MASK.indices) { 20 | BIT_MASK[i] = (1 shl i) - 1 21 | } 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/buf/DataOrder.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.buf 2 | 3 | /** 4 | * Represents the order of bytes in a [DataType] when [DataType.getBytes] `> 1`. 5 | * 6 | * @author Graham 7 | */ 8 | enum class DataOrder { 9 | 10 | /** 11 | * Most significant byte to least significant byte. 12 | */ 13 | BIG, 14 | 15 | /** 16 | * Also known as the V2 order. 17 | */ 18 | INVERSED_MIDDLE, 19 | 20 | /** 21 | * Least significant byte to most significant byte. 22 | */ 23 | LITTLE, 24 | 25 | /** 26 | * Also known as the V1 order. 27 | */ 28 | MIDDLE 29 | 30 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/buf/DataTransformation.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.buf 2 | 3 | /** 4 | * Represents the different ways data values can be transformed. 5 | * 6 | * @author Graham 7 | */ 8 | enum class DataTransformation { 9 | 10 | /** 11 | * Adds 128 to the value when it is written, takes 128 from the value when it is read (also known as type-A). 12 | */ 13 | ADD, 14 | 15 | /** 16 | * Negates the value (also known as type-C). 17 | */ 18 | NEGATE, 19 | 20 | /** 21 | * No transformation is done. 22 | */ 23 | NONE, 24 | 25 | /** 26 | * Subtracts the value from 128 (also known as type-S). 27 | */ 28 | SUBTRACT 29 | 30 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/buf/DataType.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.buf 2 | 3 | /** 4 | * Represents the different simple data types. 5 | * 6 | * @author Graham 7 | */ 8 | enum class DataType( 9 | /** 10 | * The number of bytes this type occupies. 11 | */ 12 | val bytes: Int 13 | ) { 14 | 15 | /** 16 | * A byte. 17 | */ 18 | BYTE(1), 19 | 20 | /** 21 | * A short. 22 | */ 23 | SHORT(2), 24 | 25 | /** 26 | * A medium - a group of three bytes. 27 | */ 28 | MEDIUM(3), 29 | 30 | /** 31 | * An integer. 32 | */ 33 | INT(4), 34 | 35 | /** 36 | * A long. 37 | */ 38 | LONG(8) 39 | 40 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/EmptyPacketCodec.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game 2 | 3 | import com.opennxt.net.buf.GamePacketBuilder 4 | import com.opennxt.net.buf.GamePacketReader 5 | import com.opennxt.net.game.pipeline.GamePacketCodec 6 | 7 | class EmptyPacketCodec(val value: T) : GamePacketCodec { 8 | override fun encode(packet: T, buf: GamePacketBuilder) {} 9 | 10 | override fun decode(buf: GamePacketReader): T = value 11 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/GamePacket.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game 2 | 3 | import com.opennxt.net.IncomingPacket 4 | import com.opennxt.net.OutgoingPacket 5 | 6 | interface GamePacket: IncomingPacket, OutgoingPacket -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/clientprot/ClientCheat.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.clientprot 2 | 3 | import com.opennxt.net.game.GamePacket 4 | import com.opennxt.net.game.pipeline.DynamicGamePacketCodec 5 | import com.opennxt.net.game.protocol.PacketFieldDeclaration 6 | 7 | data class ClientCheat(val forced: Boolean, val tabbed: Boolean, val cheat: String) : GamePacket { 8 | class Codec(fields: Array) : DynamicGamePacketCodec(fields) { 9 | override fun fromMap(packet: Map): ClientCheat { 10 | return ClientCheat(packet["forced"] as Int == 1, packet["tabbed"] as Int == 1, packet["cheat"] as String) 11 | } 12 | 13 | override fun toMap(packet: ClientCheat): Map = mapOf( 14 | "forced" to if (packet.forced) 1 else 0, 15 | "tabbed" to if (packet.tabbed) 1 else 0, 16 | "cheat" to packet.cheat 17 | ) 18 | } 19 | 20 | 21 | override fun toString(): String = "ClientCheat(bool1=$forced, tabbed=$tabbed, cheat=\"$cheat\")" 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/clientprot/WorldlistFetch.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.clientprot 2 | 3 | import com.opennxt.net.game.GamePacket 4 | import com.opennxt.net.game.pipeline.DynamicGamePacketCodec 5 | import com.opennxt.net.game.protocol.PacketFieldDeclaration 6 | 7 | class WorldlistFetch(val checksum: Int) : GamePacket { 8 | class Codec(fields: Array) : DynamicGamePacketCodec(fields) { 9 | override fun fromMap(packet: Map): WorldlistFetch { 10 | return WorldlistFetch(packet["checksum"] as Int) 11 | } 12 | 13 | override fun toMap(packet: WorldlistFetch): Map = mapOf( 14 | "checksum" to packet.checksum 15 | ) 16 | } 17 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/handlers/ClientCheatHandler.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.handlers 2 | 3 | import com.opennxt.OpenNXT 4 | import com.opennxt.model.commands.CommandException 5 | import com.opennxt.model.commands.CommandSender 6 | import com.opennxt.model.entity.BasePlayer 7 | import com.opennxt.model.messages.Message 8 | import com.opennxt.net.game.clientprot.ClientCheat 9 | import com.opennxt.net.game.pipeline.GamePacketHandler 10 | import com.opennxt.net.game.serverprot.ConsoleFeedback 11 | 12 | object ClientCheatHandler : GamePacketHandler { 13 | override fun handle(context: CommandSender, packet: ClientCheat) { 14 | try { 15 | if (packet.tabbed) { 16 | val completions = OpenNXT.commands.complete(context, packet.cheat) 17 | 18 | if (completions.size == 1) { 19 | context.message(Message.ConsoleAutocomplete(completions.first())) 20 | return 21 | } 22 | 23 | if (completions.isNotEmpty()) { 24 | if (context is BasePlayer) { 25 | context.write(ConsoleFeedback("", "", completions.size, completions)) 26 | } else { 27 | context.console("Completions: $completions") 28 | } 29 | } 30 | } else { 31 | OpenNXT.commands.execute(context, packet.cheat) 32 | } 33 | } catch (e: CommandException) { 34 | context.error("Command exception occurred: ${e.message}") 35 | } catch (e: Exception) { 36 | context.error("Uncaught internal exception occurred: ${e.message}") 37 | e.printStackTrace() 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/handlers/NoTimeoutHandler.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.handlers 2 | 3 | import com.opennxt.model.entity.BasePlayer 4 | import com.opennxt.net.game.pipeline.GamePacketHandler 5 | import com.opennxt.net.game.serverprot.NoTimeout 6 | 7 | object NoTimeoutHandler: GamePacketHandler { 8 | override fun handle(context: BasePlayer, packet: NoTimeout) { 9 | if ((context.noTimeouts++ % 5) == 0) 10 | context.write(NoTimeout) 11 | } 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/handlers/WorldlistFetchHandler.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.handlers 2 | 3 | import com.opennxt.model.lobby.LobbyPlayer 4 | import com.opennxt.net.game.clientprot.WorldlistFetch 5 | import com.opennxt.net.game.pipeline.GamePacketHandler 6 | 7 | object WorldlistFetchHandler: GamePacketHandler { 8 | override fun handle(context: LobbyPlayer, packet: WorldlistFetch) { 9 | context.worldList.handleRequest(packet.checksum, context.client) 10 | } 11 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/pipeline/DynamicGamePacketCodec.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.pipeline 2 | 3 | import com.opennxt.net.buf.GamePacketBuilder 4 | import com.opennxt.net.buf.GamePacketReader 5 | import com.opennxt.net.game.GamePacket 6 | import com.opennxt.net.game.protocol.PacketFieldDeclaration 7 | import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap 8 | 9 | abstract class DynamicGamePacketCodec(val fields: Array) : GamePacketCodec { 10 | 11 | override fun encode(packet: T, buf: GamePacketBuilder) { 12 | val map = toMap(packet) 13 | 14 | fields.forEach { field -> 15 | val id = field.key 16 | val value = map[id] ?: throw NullPointerException("Packet field missing: ${this::class.simpleName}: $id") 17 | 18 | field.dataCodec.write(buf, value) 19 | } 20 | } 21 | 22 | override fun decode(buf: GamePacketReader): T { 23 | val map = Object2ObjectOpenHashMap() 24 | 25 | fields.forEach { field -> 26 | map[field.key] = field.dataCodec.read(buf) 27 | } 28 | 29 | return fromMap(map) 30 | } 31 | 32 | protected abstract fun fromMap(packet: Map): T 33 | 34 | protected abstract fun toMap(packet: T): Map 35 | 36 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/pipeline/DynamicPacketHandler.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.pipeline 2 | 3 | import com.opennxt.net.RSChannelAttributes 4 | import com.opennxt.net.Side 5 | import io.netty.channel.ChannelHandlerContext 6 | import io.netty.channel.SimpleChannelInboundHandler 7 | import mu.KotlinLogging 8 | import java.util.* 9 | 10 | class DynamicPacketHandler : SimpleChannelInboundHandler() { 11 | private val logger = KotlinLogging.logger { } 12 | 13 | override fun channelRead0(ctx: ChannelHandlerContext, msg: OpcodeWithBuffer) { 14 | try { 15 | ctx.channel().attr(RSChannelAttributes.CONNECTED_CLIENT).get().receive(msg) 16 | } catch (e: Exception) { 17 | e.printStackTrace() 18 | } 19 | } 20 | 21 | override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { 22 | logger.error(cause) { "Exception caught in packet handler" } 23 | } 24 | 25 | override fun channelInactive(ctx: ChannelHandlerContext) { 26 | logger.info { "Channel on side ${ctx.channel().attr(RSChannelAttributes.SIDE).get()} went inactive" } 27 | 28 | val passthrough = ctx.channel().attr(RSChannelAttributes.PASSTHROUGH_CHANNEL).get() 29 | if (passthrough != null && passthrough.isOpen) { 30 | passthrough.close() 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/pipeline/GamePacketCodec.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.pipeline 2 | 3 | import com.opennxt.net.PacketCodec 4 | import com.opennxt.net.game.GamePacket 5 | 6 | interface GamePacketCodec : PacketCodec -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/pipeline/GamePacketHandler.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.pipeline 2 | 3 | import com.opennxt.net.game.GamePacket 4 | 5 | interface GamePacketHandler { 6 | fun handle(context: T, packet: P) 7 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/pipeline/OpcodeWithBuffer.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.pipeline 2 | 3 | import io.netty.buffer.ByteBuf 4 | 5 | // only used for framing -> handler, really 6 | data class OpcodeWithBuffer(val opcode: Int, val buf: ByteBuf) -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/protocol/Name2OpcodeConfig.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.protocol 2 | 3 | import com.moandjiezana.toml.Toml 4 | import com.opennxt.config.TomlConfig 5 | import it.unimi.dsi.fastutil.ints.Int2ObjectMap 6 | import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap 7 | import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap 8 | 9 | class Name2OpcodeConfig : TomlConfig() { 10 | val values = Object2IntOpenHashMap() 11 | 12 | fun reversedValues(): Int2ObjectMap { 13 | val map = Int2ObjectOpenHashMap() 14 | values.forEach { (k, v) -> map[v] = k } 15 | return map 16 | } 17 | 18 | override fun save(map: MutableMap) { 19 | val out = HashMap() 20 | values.forEach { (k, v) -> out[v] = k } 21 | map["values"] = out 22 | } 23 | 24 | override fun load(toml: Toml) { 25 | if (toml.contains("values")) 26 | toml.getTable("values") 27 | .toMap() 28 | .forEach { (k, v) -> values[v.toString()] = k.toInt() } 29 | } 30 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/protocol/Opcode2SizeConfig.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.protocol 2 | 3 | import com.moandjiezana.toml.Toml 4 | import com.opennxt.config.TomlConfig 5 | import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap 6 | 7 | class Opcode2SizeConfig : TomlConfig() { 8 | val values = Int2IntOpenHashMap() 9 | 10 | override fun save(map: MutableMap) { 11 | map["values"] = values 12 | } 13 | 14 | override fun load(toml: Toml) { 15 | toml.getTable("values") 16 | .toMap() 17 | .forEach { (k, v) -> values[k.toInt()] = v.toString().toInt() } 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/serverprot/ChatFilterSettingsPrivatechat.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.serverprot 2 | 3 | import com.opennxt.net.game.GamePacket 4 | import com.opennxt.net.game.pipeline.DynamicGamePacketCodec 5 | import com.opennxt.net.game.protocol.PacketFieldDeclaration 6 | import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap 7 | 8 | data class ChatFilterSettingsPrivatechat(val value: Int): GamePacket { 9 | class Codec(fields: Array): DynamicGamePacketCodec(fields) { 10 | override fun fromMap(packet: Map): ChatFilterSettingsPrivatechat { 11 | return ChatFilterSettingsPrivatechat(packet["value"] as Int) 12 | } 13 | 14 | override fun toMap(packet: ChatFilterSettingsPrivatechat): Map { 15 | val map = Object2ObjectOpenHashMap() 16 | map["value"] = packet.value 17 | return map 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/serverprot/ConsoleFeedback.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.serverprot 2 | 3 | import com.opennxt.net.buf.DataType 4 | import com.opennxt.net.buf.GamePacketBuilder 5 | import com.opennxt.net.buf.GamePacketReader 6 | import com.opennxt.net.game.GamePacket 7 | import com.opennxt.net.game.pipeline.GamePacketCodec 8 | 9 | // a: no clue 10 | // b: autocomplete "root" 11 | class ConsoleFeedback(val a: String, val b: String, val totalMatches: Int, val matches: Collection) : 12 | GamePacket { 13 | object Codec : GamePacketCodec { 14 | override fun encode(packet: ConsoleFeedback, buf: GamePacketBuilder) { 15 | buf.putString(packet.a) 16 | buf.putString(packet.b) 17 | buf.put(DataType.INT, packet.totalMatches) 18 | buf.put(DataType.SHORT, packet.matches.size) 19 | packet.matches.forEach(buf::putString) 20 | } 21 | 22 | override fun decode(buf: GamePacketReader): ConsoleFeedback = ConsoleFeedback( 23 | buf.getString(), 24 | buf.getString(), 25 | buf.getSigned(DataType.INT).toInt(), 26 | List(buf.getUnsigned(DataType.SHORT).toInt()) { buf.getString() } 27 | ) 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/serverprot/FriendlistLoaded.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.serverprot 2 | 3 | import com.opennxt.net.game.GamePacket 4 | 5 | object FriendlistLoaded: GamePacket { 6 | override fun toString(): String = "FriendlistLoaded" 7 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/serverprot/MessageGame.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.serverprot 2 | 3 | import com.opennxt.ext.readSmartShort 4 | import com.opennxt.net.buf.DataType 5 | import com.opennxt.net.buf.GamePacketBuilder 6 | import com.opennxt.net.buf.GamePacketReader 7 | import com.opennxt.net.game.GamePacket 8 | import com.opennxt.net.game.pipeline.GamePacketCodec 9 | import mu.KotlinLogging 10 | 11 | class MessageGame( 12 | val type: Int, 13 | val message: String, 14 | val intParam1: Int = 0, 15 | val stringParam1: String? = null, 16 | val stringParam2: String? = null 17 | ) : GamePacket { 18 | object Codec : GamePacketCodec { 19 | private val logger = KotlinLogging.logger { } 20 | 21 | override fun encode(packet: MessageGame, buf: GamePacketBuilder) { 22 | buf.putSmart(packet.type) 23 | buf.put(DataType.INT, packet.intParam1) 24 | 25 | var hash = 0 26 | if (packet.stringParam1 != null) hash = hash or 0x1 27 | if (packet.stringParam2 != null) hash = hash or 0x2 28 | 29 | if (packet.stringParam1 == null && packet.stringParam2 != null) { 30 | logger.warn { "string 1 == null, string 2 != null, these params won't show up." } 31 | } 32 | 33 | buf.put(DataType.BYTE, hash) 34 | if (packet.stringParam1 != null) buf.putString(packet.stringParam1) 35 | if (packet.stringParam1 != null && packet.stringParam2 != null) buf.putString(packet.stringParam2) 36 | 37 | buf.putString(packet.message) 38 | } 39 | 40 | override fun decode(buf: GamePacketReader): MessageGame { 41 | val type = buf.buffer.readSmartShort() 42 | val hash = buf.getSigned(DataType.INT).toInt() 43 | val mask = buf.getUnsigned(DataType.BYTE).toInt() 44 | 45 | val string1 = if ((mask and 1) != 0) buf.getString() else null 46 | val string2 = if ((mask and 1) != 0 && (mask and 2) != 0) buf.getString() else null 47 | val message = buf.getString() 48 | 49 | return MessageGame(type, message, hash, string1, string2) 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/serverprot/NoTimeout.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.serverprot 2 | 3 | import com.opennxt.net.game.GamePacket 4 | 5 | object NoTimeout: GamePacket { 6 | override fun toString(): String = "NoTimeout" 7 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/serverprot/RebuildNormal.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.serverprot 2 | 3 | import com.opennxt.net.game.GamePacket 4 | import com.opennxt.net.game.pipeline.DynamicGamePacketCodec 5 | import com.opennxt.net.game.protocol.PacketFieldDeclaration 6 | 7 | data class RebuildNormal( 8 | val unused1: Int, 9 | val chunkX: Int, 10 | val unused2: Int, 11 | val chunkY: Int, 12 | val npcBits: Int, 13 | val mapSize: Int, 14 | val areaType: Int, 15 | val hash1: Int, 16 | val hash2: Int 17 | ) : GamePacket { 18 | class Codec(fields: Array) : DynamicGamePacketCodec(fields) { 19 | override fun fromMap(packet: Map): RebuildNormal { 20 | return RebuildNormal( 21 | packet["unused1"] as Int, 22 | packet["chunkX"] as Int, 23 | packet["unused2"] as Int, 24 | packet["chunkY"] as Int, 25 | packet["npcBits"] as Int, 26 | packet["mapSize"] as Int, 27 | packet["areaType"] as Int, 28 | packet["hash1"] as Int, 29 | packet["hash2"] as Int 30 | ) 31 | } 32 | 33 | override fun toMap(packet: RebuildNormal): Map = mapOf( 34 | "unused1" to packet.unused1, 35 | "unused2" to packet.unused2, 36 | "mapSize" to packet.mapSize, 37 | "chunkX" to packet.chunkX, 38 | "chunkY" to packet.chunkY, 39 | "npcBits" to packet.npcBits, 40 | 41 | "areaType" to packet.areaType, 42 | "hash1" to packet.hash1, 43 | "hash2" to packet.hash2 44 | ) 45 | } 46 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/serverprot/RunClientScript.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.serverprot 2 | 3 | import com.opennxt.net.buf.DataType 4 | import com.opennxt.net.buf.GamePacketBuilder 5 | import com.opennxt.net.buf.GamePacketReader 6 | import com.opennxt.net.game.GamePacket 7 | import com.opennxt.net.game.pipeline.GamePacketCodec 8 | 9 | data class RunClientScript(val script: Int, val args: Array = emptyArray()) : GamePacket { 10 | object Codec : GamePacketCodec { 11 | override fun encode(packet: RunClientScript, buf: GamePacketBuilder) { 12 | val desc = String(packet.args.map { if (it is String) 's' else 'i' }.toCharArray()) 13 | 14 | buf.putString(desc) 15 | for (i in desc.length - 1 downTo 0) { 16 | when (val it = packet.args[i]) { 17 | is String -> buf.putString(it) 18 | is Int -> buf.put(DataType.INT, it) 19 | else -> throw IllegalArgumentException("RUNCLIENTSCRIPT only takes String and Int args, got '$it'") 20 | } 21 | } 22 | buf.put(DataType.INT, packet.script) 23 | } 24 | 25 | override fun decode(buf: GamePacketReader): RunClientScript { 26 | val desc = buf.getString() 27 | 28 | val args = arrayOfNulls(desc.length) 29 | val chars = desc.toCharArray() 30 | for (i in desc.length - 1 downTo 0) { 31 | if (chars[i] == 's') args[i] = buf.getString() 32 | else args[i] = buf.getSigned(DataType.INT).toInt() 33 | } 34 | 35 | return RunClientScript(buf.getSigned(DataType.INT).toInt(), args.requireNoNulls()) 36 | } 37 | } 38 | 39 | override fun equals(other: Any?): Boolean { 40 | if (this === other) return true 41 | if (javaClass != other?.javaClass) return false 42 | 43 | other as RunClientScript 44 | 45 | if (script != other.script) return false 46 | if (!args.contentEquals(other.args)) return false 47 | 48 | return true 49 | } 50 | 51 | override fun hashCode(): Int { 52 | var result = script 53 | result = 31 * result + args.contentHashCode() 54 | return result 55 | } 56 | 57 | override fun toString(): String = "RunClientScript(script=$script, args=[${ 58 | args.joinToString(separator = ", ") { if (it is String) "\"$it\"" else it.toString() } 59 | }])" 60 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/serverprot/ServerTickEnd.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.serverprot 2 | 3 | import com.opennxt.net.game.GamePacket 4 | 5 | object ServerTickEnd: GamePacket { 6 | override fun toString(): String = "ServerTickEnd" 7 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/serverprot/SetMapFlag.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.serverprot 2 | 3 | import com.opennxt.net.game.GamePacket 4 | import com.opennxt.net.game.pipeline.DynamicGamePacketCodec 5 | import com.opennxt.net.game.protocol.PacketFieldDeclaration 6 | import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap 7 | 8 | data class SetMapFlag( 9 | val target: Int, 10 | val unk1: Int, 11 | val unk2: Int, 12 | val unk3: Int, 13 | val unk4: Int, 14 | val unk5: Int, 15 | val unk6: Int 16 | ) : GamePacket { 17 | class Codec(fields: Array) : DynamicGamePacketCodec(fields) { 18 | override fun fromMap(packet: Map): SetMapFlag { 19 | return SetMapFlag( 20 | packet["target"] as Int, 21 | packet["unk1"] as Int, 22 | packet["unk2"] as Int, 23 | packet["unk3"] as Int, 24 | packet["unk4"] as Int, 25 | packet["unk5"] as Int, 26 | packet["unk6"] as Int 27 | ) 28 | } 29 | 30 | override fun toMap(packet: SetMapFlag): Map { 31 | val map = Object2ObjectOpenHashMap() 32 | map["target"] = packet.target 33 | map["unk1"] = packet.unk1 34 | map["unk2"] = packet.unk2 35 | map["unk3"] = packet.unk3 36 | map["unk4"] = packet.unk4 37 | map["unk5"] = packet.unk5 38 | map["unk6"] = packet.unk6 39 | return map 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/serverprot/UpdateStat.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.serverprot 2 | 3 | import com.opennxt.net.game.GamePacket 4 | import com.opennxt.net.game.pipeline.DynamicGamePacketCodec 5 | import com.opennxt.net.game.protocol.PacketFieldDeclaration 6 | import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap 7 | 8 | data class UpdateStat(val stat: Int, val level: Int, val experience: Int) : GamePacket { 9 | class Codec(fields: Array) : DynamicGamePacketCodec(fields) { 10 | override fun fromMap(packet: Map): UpdateStat { 11 | return UpdateStat(packet["stat"] as Int, packet["level"] as Int, packet["experience"] as Int) 12 | } 13 | 14 | override fun toMap(packet: UpdateStat): Map { 15 | val map = Object2ObjectOpenHashMap() 16 | map["stat"] = packet.stat 17 | map["level"] = packet.level 18 | map["experience"] = packet.experience 19 | return map 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/serverprot/WorldListFetchReply.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.serverprot 2 | 3 | import com.opennxt.net.buf.DataType 4 | import com.opennxt.net.buf.GamePacketBuilder 5 | import com.opennxt.net.buf.GamePacketReader 6 | import com.opennxt.net.game.GamePacket 7 | import com.opennxt.net.game.pipeline.GamePacketCodec 8 | 9 | class WorldListFetchReply(val lastChunk: Boolean, val chunk: ByteArray): GamePacket { 10 | object Codec: GamePacketCodec { 11 | override fun encode(packet: WorldListFetchReply, buf: GamePacketBuilder) { 12 | buf.put(DataType.BYTE, if (packet.lastChunk) 1 else 0) 13 | buf.buffer.writeBytes(packet.chunk) 14 | } 15 | 16 | override fun decode(buf: GamePacketReader): WorldListFetchReply { 17 | val lastChunk = buf.getUnsigned(DataType.BYTE).toInt() == 1 18 | val chunk = ByteArray(buf.buffer.readableBytes()) 19 | buf.buffer.readBytes(chunk) 20 | return WorldListFetchReply(lastChunk, chunk) 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/serverprot/ifaces/IfOpenSub.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.serverprot.ifaces 2 | 3 | import com.opennxt.model.InterfaceHash 4 | import com.opennxt.net.game.GamePacket 5 | import com.opennxt.net.game.pipeline.DynamicGamePacketCodec 6 | import com.opennxt.net.game.protocol.PacketFieldDeclaration 7 | 8 | data class IfOpenSub(val id: Int, val flag: Boolean, val parent: InterfaceHash) : GamePacket { 9 | class Codec(fields: Array) : DynamicGamePacketCodec(fields) { 10 | override fun fromMap(packet: Map): IfOpenSub { 11 | return IfOpenSub(packet["id"] as Int, packet["flag"] as Int == 1, InterfaceHash(packet["parent"] as Int)) 12 | } 13 | 14 | override fun toMap(packet: IfOpenSub): Map = mapOf( 15 | "xtea0" to 0, 16 | "xtea1" to 0, 17 | "xtea2" to 0, 18 | "xtea3" to 0, 19 | "id" to packet.id, 20 | "flag" to if (packet.flag) 1 else 0, 21 | "parent" to packet.parent.hash 22 | ) 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/serverprot/ifaces/IfOpenTop.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.serverprot.ifaces 2 | 3 | import com.opennxt.net.game.GamePacket 4 | import com.opennxt.net.game.pipeline.DynamicGamePacketCodec 5 | import com.opennxt.net.game.protocol.PacketFieldDeclaration 6 | 7 | data class IfOpenTop(val id: Int): GamePacket { 8 | class Codec(fields: Array): DynamicGamePacketCodec(fields) { 9 | override fun fromMap(packet: Map): IfOpenTop { 10 | return IfOpenTop(packet["id"] as Int) 11 | } 12 | 13 | override fun toMap(packet: IfOpenTop): Map = mapOf( 14 | "xtea0" to 0, 15 | "xtea1" to 0, 16 | "xtea2" to 0, 17 | "xtea3" to 0, 18 | "bool" to 0, 19 | "id" to packet.id 20 | ) 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/serverprot/ifaces/IfSetevents.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.serverprot.ifaces 2 | 3 | import com.opennxt.model.InterfaceHash 4 | import com.opennxt.net.game.GamePacket 5 | import com.opennxt.net.game.pipeline.DynamicGamePacketCodec 6 | import com.opennxt.net.game.protocol.PacketFieldDeclaration 7 | 8 | data class IfSetevents(val parent: InterfaceHash, val fromSlot: Int, val toSlot: Int, val mask: Int) : GamePacket { 9 | class Codec(fields: Array) : DynamicGamePacketCodec(fields) { 10 | override fun fromMap(packet: Map): IfSetevents { 11 | return IfSetevents( 12 | InterfaceHash(packet["parent"] as Int), 13 | packet["fromSlot"] as Int, 14 | packet["toSlot"] as Int, 15 | packet["mask"] as Int 16 | ) 17 | } 18 | 19 | override fun toMap(packet: IfSetevents): Map = mapOf( 20 | "parent" to packet.parent.hash, 21 | "fromSlot" to packet.fromSlot, 22 | "toSlot" to packet.toSlot, 23 | "mask" to packet.mask 24 | ) 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/serverprot/ifaces/IfSethide.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.serverprot.ifaces 2 | 3 | import com.opennxt.model.InterfaceHash 4 | import com.opennxt.net.game.GamePacket 5 | import com.opennxt.net.game.pipeline.DynamicGamePacketCodec 6 | import com.opennxt.net.game.protocol.PacketFieldDeclaration 7 | 8 | data class IfSethide(val parent: InterfaceHash, val hidden: Boolean) : GamePacket { 9 | class Codec(fields: Array) : DynamicGamePacketCodec(fields) { 10 | override fun fromMap(packet: Map): IfSethide { 11 | return IfSethide( 12 | InterfaceHash(packet["parent"] as Int), 13 | (packet["hidden"] as Int) == 1 14 | ) 15 | } 16 | 17 | override fun toMap(packet: IfSethide): Map = mapOf( 18 | "parent" to packet.parent.hash, 19 | "hidden" to if (packet.hidden) { 1 } else { 0 } 20 | ) 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/serverprot/ifaces/IfSettext.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.serverprot.ifaces 2 | 3 | import com.opennxt.model.InterfaceHash 4 | import com.opennxt.net.game.GamePacket 5 | import com.opennxt.net.game.pipeline.DynamicGamePacketCodec 6 | import com.opennxt.net.game.protocol.PacketFieldDeclaration 7 | 8 | data class IfSettext(val parent: InterfaceHash, val text: String) : GamePacket { 9 | class Codec(fields: Array) : DynamicGamePacketCodec(fields) { 10 | override fun fromMap(packet: Map): IfSettext { 11 | return IfSettext( 12 | InterfaceHash(packet["parent"] as Int), 13 | packet["text"] as String 14 | ) 15 | } 16 | 17 | override fun toMap(packet: IfSettext): Map = mapOf( 18 | "parent" to packet.parent.hash, 19 | "text" to packet.text 20 | ) 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/serverprot/variables/ClientSetvarcLarge.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.serverprot.variables 2 | 3 | import com.opennxt.net.game.GamePacket 4 | import com.opennxt.net.game.pipeline.DynamicGamePacketCodec 5 | import com.opennxt.net.game.protocol.PacketFieldDeclaration 6 | import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap 7 | 8 | data class ClientSetvarcLarge(val id: Int, val value: Int): GamePacket { 9 | class Codec(fields: Array): DynamicGamePacketCodec(fields) { 10 | override fun fromMap(packet: Map): ClientSetvarcLarge { 11 | return ClientSetvarcLarge(packet["id"] as Int, packet["value"] as Int) 12 | } 13 | 14 | override fun toMap(packet: ClientSetvarcLarge): Map { 15 | val map = Object2ObjectOpenHashMap() 16 | map["id"] = packet.id 17 | map["value"] = packet.value 18 | return map 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/serverprot/variables/ClientSetvarcSmall.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.serverprot.variables 2 | 3 | import com.opennxt.net.game.GamePacket 4 | import com.opennxt.net.game.pipeline.DynamicGamePacketCodec 5 | import com.opennxt.net.game.protocol.PacketFieldDeclaration 6 | import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap 7 | 8 | data class ClientSetvarcSmall(val id: Int, val value: Int): GamePacket { 9 | class Codec(fields: Array): DynamicGamePacketCodec(fields) { 10 | override fun fromMap(packet: Map): ClientSetvarcSmall { 11 | var value = packet["value"] as Int 12 | if (value == 255) value = -1 13 | return ClientSetvarcSmall(packet["id"] as Int, value) 14 | } 15 | 16 | override fun toMap(packet: ClientSetvarcSmall): Map { 17 | val map = Object2ObjectOpenHashMap() 18 | map["id"] = packet.id 19 | map["value"] = packet.value 20 | return map 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/serverprot/variables/ClientSetvarcstrLarge.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.serverprot.variables 2 | 3 | import com.opennxt.net.game.GamePacket 4 | import com.opennxt.net.game.pipeline.DynamicGamePacketCodec 5 | import com.opennxt.net.game.protocol.PacketFieldDeclaration 6 | import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap 7 | 8 | data class ClientSetvarcstrLarge(val id: Int, val value: String): GamePacket { 9 | class Codec(fields: Array): DynamicGamePacketCodec(fields) { 10 | override fun fromMap(packet: Map): ClientSetvarcstrLarge { 11 | return ClientSetvarcstrLarge(packet["id"] as Int, packet["value"] as String) 12 | } 13 | 14 | override fun toMap(packet: ClientSetvarcstrLarge): Map { 15 | val map = Object2ObjectOpenHashMap() 16 | map["id"] = packet.id 17 | map["value"] = packet.value 18 | return map 19 | } 20 | } 21 | 22 | override fun toString(): String = "ClientSetvarcstrLarge(id=$id, value=\"$value\")" 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/serverprot/variables/ClientSetvarcstrSmall.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.serverprot.variables 2 | 3 | import com.opennxt.net.game.GamePacket 4 | import com.opennxt.net.game.pipeline.DynamicGamePacketCodec 5 | import com.opennxt.net.game.protocol.PacketFieldDeclaration 6 | import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap 7 | 8 | data class ClientSetvarcstrSmall(val id: Int, val value: String): GamePacket { 9 | class Codec(fields: Array): DynamicGamePacketCodec(fields) { 10 | override fun fromMap(packet: Map): ClientSetvarcstrSmall { 11 | return ClientSetvarcstrSmall(packet["id"] as Int, packet["value"] as String) 12 | } 13 | 14 | override fun toMap(packet: ClientSetvarcstrSmall): Map { 15 | val map = Object2ObjectOpenHashMap() 16 | map["id"] = packet.id 17 | map["value"] = packet.value 18 | return map 19 | } 20 | } 21 | 22 | override fun toString(): String = "ClientSetvarcstrSmall(id=$id, value=\"$value\")" 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/serverprot/variables/ResetClientVarcache.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.serverprot.variables 2 | 3 | import com.opennxt.net.game.GamePacket 4 | 5 | object ResetClientVarcache: GamePacket { 6 | override fun toString(): String = "ResetClientVarcache" 7 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/serverprot/variables/VarpLarge.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.serverprot.variables 2 | 3 | import com.opennxt.net.game.GamePacket 4 | import com.opennxt.net.game.pipeline.DynamicGamePacketCodec 5 | import com.opennxt.net.game.protocol.PacketFieldDeclaration 6 | import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap 7 | 8 | data class VarpLarge(val id: Int, val value: Int): GamePacket { 9 | class Codec(fields: Array): DynamicGamePacketCodec(fields) { 10 | override fun fromMap(packet: Map): VarpLarge { 11 | return VarpLarge(packet["id"] as Int, packet["value"] as Int) 12 | } 13 | 14 | override fun toMap(packet: VarpLarge): Map { 15 | val map = Object2ObjectOpenHashMap() 16 | map["id"] = packet.id 17 | map["value"] = packet.value 18 | return map 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/game/serverprot/variables/VarpSmall.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.game.serverprot.variables 2 | 3 | import com.opennxt.net.game.GamePacket 4 | import com.opennxt.net.game.pipeline.DynamicGamePacketCodec 5 | import com.opennxt.net.game.protocol.PacketFieldDeclaration 6 | import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap 7 | 8 | data class VarpSmall(val id: Int, val value: Int): GamePacket { 9 | class Codec(fields: Array): DynamicGamePacketCodec(fields) { 10 | override fun fromMap(packet: Map): VarpSmall { 11 | var value = packet["value"] as Int 12 | if (value == 255) value = -1 13 | return VarpSmall(packet["id"] as Int, value) 14 | } 15 | 16 | override fun toMap(packet: VarpSmall): Map { 17 | val map = Object2ObjectOpenHashMap() 18 | map["id"] = packet.id 19 | map["value"] = packet.value 20 | return map 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/handshake/HandshakeDecoder.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.handshake 2 | 3 | import io.netty.buffer.ByteBuf 4 | import io.netty.channel.ChannelHandlerContext 5 | import io.netty.handler.codec.ByteToMessageDecoder 6 | import mu.KotlinLogging 7 | 8 | class HandshakeDecoder: ByteToMessageDecoder() { 9 | val logger = KotlinLogging.logger { } 10 | 11 | init { 12 | isSingleDecode = true 13 | } 14 | 15 | override fun decode(ctx: ChannelHandlerContext, buf: ByteBuf, out: MutableList) { 16 | val id = buf.readUnsignedByte().toInt() 17 | val type = HandshakeType.fromId(id) 18 | 19 | if (type == null) { 20 | logger.warn { "Client from ${ctx.channel().remoteAddress()} attempted to handshake with unknown id: $id" } 21 | ctx.close() 22 | buf.skipBytes(buf.readableBytes()) 23 | return 24 | } 25 | 26 | logger.info { "Received handshake from ${ctx.channel().remoteAddress()} with type $type" } 27 | out.add(HandshakeRequest(type)) 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/handshake/HandshakeHandler.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.handshake 2 | 3 | import com.opennxt.OpenNXT 4 | import com.opennxt.net.ConnectedClient 5 | import com.opennxt.net.GenericResponse 6 | import com.opennxt.net.RSChannelAttributes 7 | import com.opennxt.net.Side 8 | import com.opennxt.net.js5.Js5Decoder 9 | import com.opennxt.net.js5.Js5Encoder 10 | import com.opennxt.net.js5.Js5Handler 11 | import com.opennxt.net.js5.Js5Session 12 | import com.opennxt.net.login.LoginServerDecoder 13 | import com.opennxt.net.login.LoginEncoder 14 | import com.opennxt.net.login.LoginPacket 15 | import com.opennxt.net.login.LoginServerHandler 16 | import io.netty.channel.ChannelHandlerContext 17 | import io.netty.channel.SimpleChannelInboundHandler 18 | import mu.KotlinLogging 19 | import java.util.concurrent.ThreadLocalRandom 20 | 21 | class HandshakeHandler : SimpleChannelInboundHandler() { 22 | override fun channelRead0(ctx: ChannelHandlerContext, msg: HandshakeRequest) { 23 | ctx.channel().attr(RSChannelAttributes.SIDE).set(Side.CLIENT) 24 | ctx.channel().attr(RSChannelAttributes.CONNECTED_CLIENT).set(ConnectedClient(Side.CLIENT, ctx.channel())) 25 | 26 | when (msg.type) { 27 | HandshakeType.JS_5 -> { 28 | val session = Js5Session(ctx.channel()) 29 | 30 | // replace handler before decoder to avoid decoding packet before encoder is ready (yes this was a bug) 31 | ctx.pipeline().addLast("js5-encoder", Js5Encoder(session)) 32 | 33 | ctx.pipeline().replace("handshake-handler", "js5-handler", Js5Handler(session)) 34 | ctx.pipeline().replace("handshake-decoder", "js5-decoder", Js5Decoder(session)) 35 | } 36 | HandshakeType.LOGIN -> { 37 | val uniqueId = ThreadLocalRandom.current().nextLong() 38 | 39 | ctx.channel().attr(RSChannelAttributes.LOGIN_UNIQUE_ID).set(uniqueId) 40 | 41 | ctx.pipeline().addLast("login-encoder", LoginEncoder()) 42 | 43 | ctx.pipeline().replace("handshake-handler", "login-handler", LoginServerHandler()) 44 | ctx.pipeline().replace("handshake-decoder", "login-decoder", LoginServerDecoder(OpenNXT.rsaConfig.login)) 45 | 46 | ctx.channel().write(LoginPacket.LoginResponse(GenericResponse.SUCCESSFUL_CONNECTION)) 47 | ctx.channel().write(LoginPacket.SendUniqueId(uniqueId)) 48 | ctx.channel().flush() 49 | } 50 | else -> throw IllegalStateException("Cannot handle handshake message: $msg") 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/handshake/HandshakeRequest.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.handshake 2 | 3 | import com.opennxt.net.IncomingPacket 4 | import com.opennxt.net.OutgoingPacket 5 | 6 | data class HandshakeRequest(val type: HandshakeType): IncomingPacket, OutgoingPacket -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/handshake/HandshakeType.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.handshake 2 | 3 | enum class HandshakeType(val id: Int) { 4 | LOGIN(14), 5 | JS_5(15), 6 | ; 7 | 8 | companion object { 9 | private val VALUES = values(); 10 | fun fromId(id: Int): HandshakeType? = VALUES.firstOrNull { it.id == id } 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/http/HttpExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.http 2 | 3 | import io.netty.buffer.Unpooled 4 | import io.netty.channel.ChannelFutureListener 5 | import io.netty.channel.ChannelHandlerContext 6 | import io.netty.handler.codec.http.DefaultFullHttpResponse 7 | import io.netty.handler.codec.http.HttpHeaderNames 8 | import io.netty.handler.codec.http.HttpResponseStatus 9 | import io.netty.handler.codec.http.HttpVersion 10 | import io.netty.util.CharsetUtil 11 | 12 | fun ChannelHandlerContext.sendHttpError(status: HttpResponseStatus, error: String = "Failed: $status\r\n") { 13 | val response = DefaultFullHttpResponse( 14 | HttpVersion.HTTP_1_1, status, 15 | Unpooled.copiedBuffer(error, CharsetUtil.UTF_8) 16 | ) 17 | response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8") 18 | writeAndFlush(response).addListener(ChannelFutureListener.CLOSE) 19 | } 20 | 21 | fun ChannelHandlerContext.sendHttpFile(file: ByteArray, name: String) { 22 | val response = DefaultFullHttpResponse( 23 | HttpVersion.HTTP_1_1, HttpResponseStatus.OK, 24 | Unpooled.wrappedBuffer(file)) 25 | response.headers().set(HttpHeaderNames.SERVER, "JaGeX/3.1") 26 | response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/octet-stream") 27 | response.headers().set(HttpHeaderNames.CONTENT_LENGTH, file.size) 28 | response.headers().set(HttpHeaderNames.CONTENT_DISPOSITION, name) 29 | response.headers().set(HttpHeaderNames.CONTENT_ENCODING, "lzma") 30 | 31 | channel().writeAndFlush(response).addListener(ChannelFutureListener.CLOSE) 32 | } 33 | 34 | fun ChannelHandlerContext.sendHttpText(text: ByteArray) { 35 | val response = DefaultFullHttpResponse( 36 | HttpVersion.HTTP_1_1, HttpResponseStatus.OK, 37 | Unpooled.wrappedBuffer(text)) 38 | response.headers().set(HttpHeaderNames.SERVER, "JaGeX/3.1") 39 | response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=ISO-8859-1") 40 | response.headers().set(HttpHeaderNames.CONTENT_LENGTH, text.size) 41 | 42 | channel().writeAndFlush(response).addListener(ChannelFutureListener.CLOSE) 43 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/http/HttpRequestHandler.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.http 2 | 3 | import com.opennxt.net.http.endpoints.JavConfigWsEndpoint 4 | import com.opennxt.net.http.endpoints.ClientFileEndpoint 5 | import com.opennxt.net.http.endpoints.Js5MsEndpoint 6 | import io.netty.channel.ChannelHandler 7 | import io.netty.channel.ChannelHandlerContext 8 | import io.netty.channel.SimpleChannelInboundHandler 9 | import io.netty.handler.codec.http.FullHttpRequest 10 | import io.netty.handler.codec.http.HttpMethod 11 | import io.netty.handler.codec.http.HttpResponseStatus 12 | import io.netty.handler.codec.http.QueryStringDecoder 13 | import mu.KotlinLogging 14 | 15 | @ChannelHandler.Sharable 16 | class HttpRequestHandler : SimpleChannelInboundHandler() { 17 | private val logger = KotlinLogging.logger { } 18 | 19 | override fun channelRead0(ctx: ChannelHandlerContext, msg: FullHttpRequest) { 20 | if (!msg.decoderResult().isSuccess) { 21 | ctx.sendHttpError(HttpResponseStatus.BAD_REQUEST) 22 | return 23 | } 24 | 25 | if (msg.method() != HttpMethod.GET) { 26 | ctx.sendHttpError(HttpResponseStatus.METHOD_NOT_ALLOWED) 27 | return 28 | } 29 | val uri = msg.uri() 30 | val query = QueryStringDecoder(uri) 31 | 32 | when { 33 | query.path() == "/jav_config.ws" -> JavConfigWsEndpoint.handle(ctx, msg, query) 34 | query.path() == "/client" -> ClientFileEndpoint.handle(ctx, msg, query) 35 | query.path() == "/ms" -> Js5MsEndpoint.handle(ctx, msg, query) 36 | else -> ctx.sendHttpError(HttpResponseStatus.NOT_FOUND) 37 | } 38 | } 39 | 40 | override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { 41 | cause.printStackTrace() 42 | if (ctx.channel().isActive) { 43 | ctx.sendHttpError(HttpResponseStatus.INTERNAL_SERVER_ERROR) 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/http/HttpServer.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.http 2 | 3 | import com.opennxt.config.ServerConfig 4 | import com.opennxt.model.files.FileChecker 5 | import io.netty.bootstrap.ServerBootstrap 6 | import io.netty.channel.ChannelInitializer 7 | import io.netty.channel.ChannelOption 8 | import io.netty.channel.nio.NioEventLoopGroup 9 | import io.netty.channel.socket.SocketChannel 10 | import io.netty.channel.socket.nio.NioServerSocketChannel 11 | import io.netty.handler.codec.http.HttpObjectAggregator 12 | import io.netty.handler.codec.http.HttpServerCodec 13 | import mu.KotlinLogging 14 | import kotlin.system.exitProcess 15 | 16 | class HttpServer(val config: ServerConfig) : AutoCloseable { 17 | private val logger = KotlinLogging.logger {} 18 | private var initialized = false 19 | 20 | private val handler = HttpRequestHandler() 21 | private val httpBootstrap = ServerBootstrap() 22 | .group(NioEventLoopGroup()) 23 | .channel(NioServerSocketChannel::class.java) 24 | .childHandler(HttpChannelInitializer(handler)) 25 | .childOption(ChannelOption.SO_REUSEADDR, true) 26 | .childOption(ChannelOption.TCP_NODELAY, true) 27 | .childOption(ChannelOption.CONNECT_TIMEOUT_MILLIS, 30_000) 28 | 29 | fun init(skipFileChecks: Boolean) { 30 | if (skipFileChecks) { 31 | logger.info { "Skipping http file verification" } 32 | } else { 33 | FileChecker.checkFiles("compressed") 34 | } 35 | // TODO Checksum table? 36 | 37 | initialized = true 38 | } 39 | 40 | fun bind(httpPort: Int = config.ports.http) { 41 | check(initialized) { "Attempted to bind http server before initializing" } 42 | 43 | logger.info { "Binding http server to 0.0.0.0:$httpPort" } 44 | 45 | val result = httpBootstrap.bind("0.0.0.0", httpPort).sync() 46 | if (!result.isSuccess) { 47 | logger.error(result.cause()) { "Failed to bind to 0.0.0.0:$httpPort" } 48 | exitProcess(1) 49 | } 50 | 51 | logger.info { "Http server bound to 0.0.0.0:$httpPort" } 52 | } 53 | 54 | override fun close() { 55 | logger.warn { "TODO - Close http server connections" } 56 | } 57 | 58 | private class HttpChannelInitializer(val handler: HttpRequestHandler) : ChannelInitializer() { 59 | override fun initChannel(ch: SocketChannel) { 60 | ch.pipeline().addLast("codec", HttpServerCodec()) 61 | ch.pipeline().addLast("aggregator", HttpObjectAggregator(2048)) 62 | ch.pipeline().addLast("handler", handler) 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/http/endpoints/ClientFileEndpoint.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.http.endpoints 2 | 3 | import com.opennxt.model.files.BinaryType 4 | import com.opennxt.model.files.FileChecker 5 | import com.opennxt.net.http.sendHttpError 6 | import com.opennxt.net.http.sendHttpFile 7 | import io.netty.channel.ChannelHandlerContext 8 | import io.netty.handler.codec.http.FullHttpRequest 9 | import io.netty.handler.codec.http.HttpResponseStatus 10 | import io.netty.handler.codec.http.QueryStringDecoder 11 | 12 | object ClientFileEndpoint { 13 | fun handle(ctx: ChannelHandlerContext, msg: FullHttpRequest, query: QueryStringDecoder) { 14 | if (!query.parameters().containsKey("binaryType") || !query.parameters().containsKey("fileName")) { 15 | ctx.sendHttpError(HttpResponseStatus.NOT_FOUND) 16 | return 17 | } 18 | 19 | val binaryType = BinaryType.values()[query.parameters().getValue("binaryType").first().toInt()] 20 | val filename = query.parameters().getValue("fileName").first() 21 | val crc = query.parameters().getValue("crc").first().toLong() 22 | 23 | val data = FileChecker.getFile("compressed", binaryType, file = filename, crc = crc) 24 | if (data == null) { 25 | ctx.sendHttpError(HttpResponseStatus.NOT_FOUND) 26 | return 27 | } 28 | 29 | ctx.sendHttpFile(data, filename) 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/http/endpoints/JavConfigWsEndpoint.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.http.endpoints 2 | 3 | import com.opennxt.OpenNXT 4 | import com.opennxt.model.files.BinaryType 5 | import com.opennxt.model.files.ClientConfig 6 | import com.opennxt.model.files.FileChecker 7 | import com.opennxt.net.http.sendHttpText 8 | import io.netty.channel.ChannelHandlerContext 9 | import io.netty.handler.codec.http.FullHttpRequest 10 | import io.netty.handler.codec.http.QueryStringDecoder 11 | 12 | object JavConfigWsEndpoint { 13 | fun handle(ctx: ChannelHandlerContext, msg: FullHttpRequest, query: QueryStringDecoder) { 14 | 15 | val type = BinaryType.values()[query.parameters().getOrElse("binaryType") { listOf("2") }.first().toInt()] 16 | val config = FileChecker.getConfig("compressed", type) ?: throw NullPointerException("Can't get config for type $type") 17 | if (!OpenNXT.enableProxySupport) { 18 | ctx.sendHttpText(config.toString().toByteArray(Charsets.ISO_8859_1)) 19 | return 20 | } 21 | 22 | val liveConfig = ClientConfig.download("https://world5.runescape.com/jav_config.ws", type) 23 | 24 | var download = 0 25 | while (config.entries.containsKey("download_name_$download")) { 26 | liveConfig.entries["download_name_$download"] = config.entries.getValue("download_name_$download") 27 | liveConfig.entries["download_crc_$download"] = config.entries.getValue("download_crc_$download") 28 | liveConfig.entries["download_hash_$download"] = config.entries.getValue("download_hash_$download") 29 | download++ 30 | } 31 | 32 | liveConfig["codebase"] = "http://${OpenNXT.config.hostname}/" 33 | 34 | for (i in 0..liveConfig.highestParam) { 35 | val value = liveConfig.getParam(i) ?: continue 36 | 37 | if (value.contains("runescape.com") || value.contains("jagex.com")) { 38 | liveConfig["param=$i"] = OpenNXT.config.hostname 39 | } 40 | } 41 | 42 | ctx.sendHttpText(liveConfig.toString().toByteArray(Charsets.ISO_8859_1)) 43 | } 44 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/http/endpoints/Js5MsEndpoint.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.http.endpoints 2 | 3 | import com.opennxt.OpenNXT 4 | import com.opennxt.model.files.BinaryType 5 | import com.opennxt.model.files.FileChecker 6 | import com.opennxt.net.http.sendHttpError 7 | import com.opennxt.net.http.sendHttpFile 8 | import io.netty.buffer.ByteBuf 9 | import io.netty.buffer.Unpooled 10 | import io.netty.channel.ChannelFutureListener 11 | import io.netty.channel.ChannelHandlerContext 12 | import io.netty.handler.codec.http.* 13 | 14 | object Js5MsEndpoint { 15 | fun handle(ctx: ChannelHandlerContext, msg: FullHttpRequest, query: QueryStringDecoder) { 16 | if (!query.parameters().containsKey("a") || !query.parameters().containsKey("g")) { 17 | ctx.sendHttpError(HttpResponseStatus.BAD_REQUEST) 18 | return 19 | } 20 | 21 | val index: Int = try { 22 | query.parameters().getValue("a").first().toInt() 23 | } catch (e: NumberFormatException) { 24 | ctx.sendHttpError(HttpResponseStatus.BAD_REQUEST) 25 | return 26 | } 27 | 28 | val archive: Int = try { 29 | query.parameters().getValue("g").first().toInt() 30 | } catch (e: NumberFormatException) { 31 | ctx.sendHttpError(HttpResponseStatus.BAD_REQUEST) 32 | return 33 | } 34 | 35 | if (index == 255 && archive == 255) { 36 | sendFile(msg, ctx, Unpooled.wrappedBuffer(OpenNXT.httpChecksumTable)) 37 | return 38 | } else if (index == 40) { 39 | val data = OpenNXT.filesystem.read(40, archive) 40 | if (data == null) { 41 | ctx.sendHttpError(HttpResponseStatus.NOT_FOUND) 42 | return 43 | } 44 | 45 | sendFile(msg, ctx, Unpooled.wrappedBuffer(data)) 46 | } 47 | 48 | ctx.sendHttpError(HttpResponseStatus.NOT_FOUND) 49 | } 50 | 51 | private fun sendFile(request: FullHttpRequest, ctx: ChannelHandlerContext, buf: ByteBuf) { 52 | val size = buf.readableBytes() 53 | val response = DefaultFullHttpResponse(request.protocolVersion(), HttpResponseStatus.OK, buf) 54 | response.headers().set(HttpHeaderNames.SERVER, "JaGeX/3.1") 55 | response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/octet-stream") 56 | response.headers().set(HttpHeaderNames.CONTENT_LENGTH, size) 57 | 58 | ctx.channel().writeAndFlush(response).addListener(ChannelFutureListener.CLOSE) 59 | } 60 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/js5/Js5Encoder.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.js5 2 | 3 | import com.opennxt.net.js5.packet.Js5Packet 4 | import io.netty.buffer.ByteBuf 5 | import io.netty.channel.ChannelHandlerContext 6 | import io.netty.handler.codec.MessageToByteEncoder 7 | import mu.KotlinLogging 8 | import kotlin.math.min 9 | 10 | class Js5Encoder(val session: Js5Session) : MessageToByteEncoder() { 11 | private val logger = KotlinLogging.logger { } 12 | 13 | override fun encode(ctx: ChannelHandlerContext, msg: Js5Packet, out: ByteBuf) { 14 | when (msg) { 15 | is Js5Packet.HandshakeResponse -> out.writeByte(msg.code) 16 | is Js5Packet.Prefetches -> for (value in msg.prefetches) out.writeInt(value) 17 | is Js5Packet.RequestFileResponse -> { 18 | val xor = session.channel.attr(Js5Session.XOR_KEY).get() 19 | 20 | val data = msg.data 21 | var length = ((data.getByte(1).toInt() and 0xff) shl 24) + ((data.getByte(2) 22 | .toInt() and 0xff) shl 16) + ((data.getByte(3).toInt() and 0xff) shl 8) + (data.getByte(4) 23 | .toInt() and 0xff) + 5 24 | if (data.getByte(0).toInt() != 0) length += 4 25 | 26 | var remaining = length 27 | while (data.isReadable && remaining > 0) { 28 | out.writeByte(msg.index xor xor) 29 | 30 | val size = if (msg.priority) msg.archive else (msg.archive or -0x80000000) 31 | out.writeByte((size shr 24) xor xor) 32 | out.writeByte((size shr 16) xor xor) 33 | out.writeByte((size shr 8) xor xor) 34 | out.writeByte((size) xor xor) 35 | 36 | for (i in 0 until min(102400 - 5, remaining)) { // - 5 due to header written above 37 | out.writeByte(data.readByte().toInt() xor xor) 38 | remaining-- 39 | } 40 | } 41 | 42 | if (remaining != 0) { 43 | logger.error { "remaining != 0! ${remaining}, ${data.readableBytes()} in [${msg.index}, ${msg.archive}]" } 44 | } 45 | } 46 | else -> logger.warn { "I don't know how to encode $msg!" } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/js5/Js5Handler.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.js5 2 | 3 | import com.opennxt.Js5Thread 4 | import com.opennxt.OpenNXT 5 | import com.opennxt.net.js5.packet.Js5Packet 6 | import io.netty.channel.ChannelHandlerContext 7 | import io.netty.channel.SimpleChannelInboundHandler 8 | import mu.KotlinLogging 9 | 10 | class Js5Handler(val session: Js5Session): SimpleChannelInboundHandler() { 11 | private val logger = KotlinLogging.logger { } 12 | 13 | var handledHandshake = false 14 | 15 | override fun channelRead0(ctx: ChannelHandlerContext, msg: Js5Packet) { 16 | when(msg) { 17 | is Js5Packet.Handshake -> { 18 | if (handledHandshake) 19 | throw IllegalStateException("Already handled handshake") 20 | handledHandshake = true 21 | 22 | logger.warn { "Sending OK - TODO: Check build & js5 token prior to accepting this connection" } 23 | 24 | ctx.channel().write(Js5Packet.HandshakeResponse(0)) 25 | ctx.channel().write(Js5Packet.Prefetches(OpenNXT.prefetches.entries)) 26 | ctx.channel().flush() 27 | } 28 | else -> TODO("Encode $msg") 29 | } 30 | } 31 | 32 | override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { 33 | logger.warn(cause) { "Caught exception" } 34 | } 35 | 36 | override fun channelInactive(ctx: ChannelHandlerContext) { 37 | // remove session if the client connection drops and doesn't send the termination packet 38 | Js5Thread.removeSession(session) 39 | } 40 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/js5/packet/Js5Packet.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.js5.packet 2 | 3 | import com.opennxt.net.Packet 4 | import io.netty.buffer.ByteBuf 5 | 6 | sealed class Js5Packet : Packet { 7 | data class ConnectionInitialized(val value: Int, val build: Int) : Js5Packet() 8 | data class LoggedIn(val build: Int) : Js5Packet() 9 | data class LoggedOut(val build: Int) : Js5Packet() 10 | data class RequestTermination(val build: Int) : Js5Packet() 11 | data class RequestFile( 12 | var priority: Boolean, 13 | val index: Int, 14 | val archive: Int, 15 | val build: Int, 16 | var nxt: Boolean = true 17 | ) : Js5Packet() 18 | 19 | data class Handshake(val major: Int, val minor: Int, val token: String, val language: Int = 0) : Js5Packet() 20 | data class XorRequest(val xor: Int) : Js5Packet() 21 | data class RequestFileResponse(val priority: Boolean, val index: Int, val archive: Int, val data: ByteBuf) : 22 | Js5Packet() 23 | 24 | // TODO Responses here 25 | data class HandshakeResponse(val code: Int) : Js5Packet() 26 | data class Prefetches(val prefetches: IntArray) : Js5Packet() 27 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/login/LoginPacket.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.login 2 | 3 | import com.opennxt.model.Build 4 | import com.opennxt.net.GenericResponse 5 | import com.opennxt.net.IncomingPacket 6 | import com.opennxt.net.OutgoingPacket 7 | import io.netty.buffer.ByteBuf 8 | 9 | sealed class LoginPacket : IncomingPacket, OutgoingPacket { 10 | data class SendUniqueId(val id: Long) : LoginPacket() 11 | class LobbyLoginRequest( 12 | val build: Build, 13 | val header: LoginRSAHeader, 14 | val username: String, 15 | val password: String, 16 | val remaining: ByteBuf 17 | ) : LoginPacket() 18 | 19 | class GameLoginRequest( 20 | val build: Build, 21 | val header: LoginRSAHeader, 22 | val username: String, 23 | val password: String, 24 | val remaining: ByteBuf 25 | ) : LoginPacket() 26 | 27 | data class GameLoginResponse( 28 | val byte0: Int, 29 | val rights: Int, 30 | val byte2: Int, 31 | val byte3: Int, 32 | val byte4: Int, 33 | val byte5: Int, 34 | val byte6: Int, 35 | val playerIndex: Int, 36 | val byte8: Int, 37 | val medium9: Int, 38 | val isMember: Int, 39 | val username: String, 40 | val short12: Int, // part of 6-byte-int 41 | val int13: Int // part of 6-byte-int 42 | ) : LoginPacket() 43 | 44 | data class LobbyLoginResponse( 45 | val byte0: Int, 46 | val rights: Int, 47 | val byte2: Int, 48 | val byte3: Int, 49 | val medium4: Int, 50 | val byte5: Int, 51 | val byte6: Int, 52 | val byte7: Int, 53 | val long8: Long, 54 | val int9: Int, 55 | val byte10: Int, 56 | val byte11: Int, 57 | val int12: Int, 58 | val int13: Int, 59 | val short14: Int, 60 | val short15: Int, 61 | val short16: Int, 62 | val ip: Int, 63 | val byte17: Int, 64 | val short18: Int, 65 | val short19: Int, 66 | val byte20: Int, 67 | val username: String, 68 | val byte22: Int, 69 | val int23: Int, 70 | val short24: Int, 71 | val defaultWorld: String, 72 | val defaultWorldPort1: Int, 73 | val defaultWorldPort2: Int 74 | ) : LoginPacket() 75 | 76 | data class LoginResponse(val code: GenericResponse) : LoginPacket() 77 | 78 | data class ServerpermVarcChunk(val finished: Boolean, val varcs: Map) : LoginPacket() 79 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/login/LoginType.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.login 2 | 3 | enum class LoginType(val id: Int) { 4 | GAME(16), 5 | LOBBY(19), 6 | GAMELOGIN_CONTINUE(26), 7 | ; 8 | 9 | companion object { 10 | private val VALUES = values() 11 | fun fromId(id: Int): LoginType? = VALUES.firstOrNull { it.id == id } 12 | } 13 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/proxy/ConnectedProxyClient.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.proxy 2 | 3 | import com.opennxt.OpenNXT 4 | import com.opennxt.model.messages.Message 5 | import com.opennxt.model.proxy.PacketDumper 6 | import com.opennxt.model.tick.Tickable 7 | import com.opennxt.model.worldlist.WorldList 8 | import com.opennxt.net.ConnectedClient 9 | import com.opennxt.net.Side 10 | import com.opennxt.net.game.clientprot.ClientCheat 11 | import com.opennxt.net.game.serverprot.WorldListFetchReply 12 | import io.netty.buffer.ByteBufUtil 13 | import mu.KotlinLogging 14 | 15 | class ConnectedProxyClient(val connection: ConnectedClient, dumper: PacketDumper) : Tickable { 16 | val incomingNames = 17 | (if (connection.side == Side.CLIENT) OpenNXT.protocol.clientProtNames else OpenNXT.protocol.serverProtNames).reversedValues() 18 | 19 | init { 20 | connection.dumper = dumper 21 | } 22 | 23 | private val logger = KotlinLogging.logger { } 24 | 25 | var hexdump = false 26 | var ready = false 27 | 28 | lateinit var other: ConnectedProxyClient 29 | 30 | private val worldList = WorldList() 31 | 32 | override fun tick() { 33 | if (!ready || !other.ready) return 34 | 35 | val player = connection.channel.attr(ProxyChannelAttributes.PROXY_PLAYER).get() 36 | 37 | while (true) { 38 | val packet = connection.incomingQueue.poll() ?: break 39 | 40 | // TODO Handle packet here; allow packet to be dropped 41 | if (packet is UnidentifiedPacket) { 42 | val raw = packet.packet 43 | if (hexdump) { 44 | logger.info { 45 | "[RECV] ${connection.side}: ${raw.opcode} (name=${incomingNames[raw.opcode]}, real size=${raw.buf.readableBytes()}). Dump:\n${ 46 | ByteBufUtil.prettyHexDump( 47 | raw.buf 48 | ) 49 | }" 50 | } 51 | } else { 52 | logger.info { 53 | "[RECV] ${connection.side}: ${raw.opcode} (name=${incomingNames[raw.opcode]}, real size=${raw.buf.readableBytes()})." 54 | } 55 | } 56 | } else { 57 | player.handlePacket(packet) 58 | } 59 | 60 | other.connection.write(packet) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/proxy/ProxyChannelAttributes.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.proxy 2 | 3 | import com.opennxt.login.LoginResult 4 | import com.opennxt.net.login.LoginPacket 5 | import io.netty.channel.Channel 6 | import io.netty.util.AttributeKey 7 | 8 | object ProxyChannelAttributes { 9 | val LOGIN_STATE = AttributeKey.newInstance("proxy-login-state") 10 | val LOGIN_HANDLER = AttributeKey.newInstance<(Channel?, LoginResult) -> Unit>("proxy-login-handler") 11 | 12 | val USERNAME = AttributeKey.newInstance("proxy-username") 13 | val PASSWORD = AttributeKey.newInstance("proxy-password") 14 | val PACKET = AttributeKey.newInstance("proxy-packet") 15 | val PROXY_CLIENT = AttributeKey.newInstance("proxy-client") 16 | val PROXY_PLAYER = AttributeKey.newInstance("proxy-player") 17 | val PLAYER_INDEX = AttributeKey.newInstance("player-index") 18 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/proxy/ProxyChannelInitializer.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.proxy 2 | 3 | import com.opennxt.net.login.LoginEncoder 4 | import io.netty.channel.ChannelInitializer 5 | import io.netty.channel.socket.SocketChannel 6 | import mu.KotlinLogging 7 | 8 | class ProxyChannelInitializer : ChannelInitializer() { 9 | private val logger = KotlinLogging.logger { } 10 | 11 | override fun initChannel(ch: SocketChannel) { 12 | ch.attr(ProxyChannelAttributes.LOGIN_STATE).set(ProxyLoginState.HANDSHAKE) 13 | 14 | ch.pipeline() 15 | .addLast("login-decoder", LoginClientDecoder()) 16 | .addLast("login-handler", LoginClientHandler()) 17 | .addLast("login-encoder", LoginEncoder()) 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/proxy/ProxyConfig.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.proxy 2 | 3 | import com.moandjiezana.toml.Toml 4 | import com.opennxt.config.TomlConfig 5 | 6 | class ProxyConfig: TomlConfig() { 7 | var usernames = ArrayList() 8 | 9 | init { 10 | usernames.add("username") 11 | usernames.add("someone else") 12 | } 13 | 14 | override fun save(map: MutableMap) { 15 | map["usernames"] = usernames.clone() 16 | } 17 | 18 | override fun load(toml: Toml) { 19 | usernames = ArrayList(toml.getList("usernames", usernames).map { it.toLowerCase() }) 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/proxy/ProxyConnectionFactory.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.proxy 2 | 3 | import com.opennxt.login.LoginResult 4 | import com.opennxt.net.ConnectedClient 5 | import com.opennxt.net.RSChannelAttributes 6 | import com.opennxt.net.Side 7 | import com.opennxt.net.login.LoginPacket 8 | import io.netty.bootstrap.Bootstrap 9 | import io.netty.channel.Channel 10 | import io.netty.channel.ChannelFutureListener 11 | import io.netty.channel.nio.NioEventLoopGroup 12 | import io.netty.channel.socket.nio.NioSocketChannel 13 | import mu.KotlinLogging 14 | 15 | class ProxyConnectionFactory { 16 | private val logger = KotlinLogging.logger { } 17 | private val bootstrap = Bootstrap() 18 | private val workerGroup = NioEventLoopGroup(8) 19 | 20 | init { 21 | bootstrap.group(workerGroup) 22 | bootstrap.channel(NioSocketChannel::class.java) 23 | bootstrap.handler(ProxyChannelInitializer()) 24 | } 25 | 26 | fun createLogin(packet: LoginPacket, callback: (Channel?, LoginResult) -> Unit) { 27 | if (packet !is LoginPacket.LobbyLoginRequest && packet !is LoginPacket.GameLoginRequest) 28 | throw NullPointerException("Login should be LobbyLoginRequest or GameLoginRequest!") 29 | 30 | val host = if (packet is LoginPacket.LobbyLoginRequest) "lobby18a.runescape.com" else "world3.runescape.com" 31 | 32 | bootstrap.connect(host, 43594).addListener(ChannelFutureListener { listener -> 33 | if (!listener.isSuccess) { 34 | logger.warn(listener.cause()) { "Failed to connect to server" } 35 | callback(null, LoginResult.BANNED) 36 | return@ChannelFutureListener 37 | } 38 | 39 | val ch = listener.channel() 40 | ch.attr(RSChannelAttributes.SIDE).set(Side.SERVER) 41 | ch.attr(RSChannelAttributes.CONNECTED_CLIENT).set(ConnectedClient(Side.SERVER, ch)) 42 | ch.attr(ProxyChannelAttributes.LOGIN_HANDLER).set(callback) 43 | ch.attr(ProxyChannelAttributes.USERNAME) 44 | .set(if (packet is LoginPacket.LobbyLoginRequest) packet.username else if (packet is LoginPacket.GameLoginRequest) packet.username else throw IllegalStateException()) 45 | ch.attr(ProxyChannelAttributes.PASSWORD) 46 | .set(if (packet is LoginPacket.LobbyLoginRequest) packet.password else if (packet is LoginPacket.GameLoginRequest) packet.password else throw IllegalStateException()) 47 | ch.attr(ProxyChannelAttributes.PACKET).set(packet) 48 | }) 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/proxy/ProxyConnectionHandler.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.proxy 2 | 3 | import com.google.common.collect.Sets 4 | import com.opennxt.model.tick.Tickable 5 | import mu.KotlinLogging 6 | 7 | class ProxyConnectionHandler : Tickable { 8 | private val logger = KotlinLogging.logger { } 9 | 10 | private class ProxyPair(val client: ConnectedProxyClient, val server: ConnectedProxyClient) { 11 | fun isAlive(): Boolean { 12 | return client.connection.channel.isOpen && server.connection.channel.isOpen 13 | } 14 | 15 | fun disconnect() { 16 | client.connection.channel.close() 17 | server.connection.channel.close() 18 | } 19 | } 20 | 21 | private val clients = Sets.newConcurrentHashSet() 22 | 23 | fun registerProxyConnection(client: ConnectedProxyClient, server: ConnectedProxyClient) { 24 | clients += ProxyPair(client, server) 25 | } 26 | 27 | override fun tick() { 28 | val it = clients.iterator() 29 | while (it.hasNext()) { 30 | val pair = it.next() 31 | if (!pair.isAlive()) { 32 | pair.disconnect() 33 | it.remove() 34 | } 35 | 36 | try { 37 | pair.client.tick() 38 | } catch (e: Exception) { 39 | logger.error(e) { "Uncaught exception ticking client in proxy handler" } 40 | } 41 | 42 | try { 43 | pair.server.tick() 44 | } catch (e: Exception) { 45 | logger.error(e) { "Uncaught exception ticking server in proxy handler" } 46 | } 47 | 48 | if (pair.client.ready) 49 | pair.client.connection.flush() 50 | if (pair.server.ready) 51 | pair.server.connection.flush() 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/proxy/ProxyLoginState.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.proxy 2 | 3 | enum class ProxyLoginState { 4 | HANDSHAKE, 5 | UNIQUE_ID, 6 | LOGIN_RESPONSE, 7 | WAITING_LOGIN_RESPONSE, 8 | FINISHED, 9 | 10 | WAITING_SERVERPERM_VARCS, 11 | WAITING_WORLDLOGIN_RESPONSE, 12 | // TODO Game login states 13 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/proxy/UnidentifiedPacket.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.proxy 2 | 3 | import com.opennxt.net.game.GamePacket 4 | import com.opennxt.net.game.pipeline.OpcodeWithBuffer 5 | 6 | data class UnidentifiedPacket(val packet: OpcodeWithBuffer): GamePacket -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/proxy/handler/IfOpenSubProxyHandler.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.proxy.handler 2 | 3 | import com.opennxt.net.game.pipeline.GamePacketHandler 4 | import com.opennxt.net.game.serverprot.ifaces.IfOpenSub 5 | import com.opennxt.net.proxy.ProxyPlayer 6 | import mu.KotlinLogging 7 | 8 | object IfOpenSubProxyHandler : GamePacketHandler { 9 | val logger = KotlinLogging.logger {} 10 | 11 | override fun handle(context: ProxyPlayer, packet: IfOpenSub) { 12 | context.plaintextDumpFile.appendLine("player.interfaces.open(id = ${packet.id}, parent = ${packet.parent.parent}, component = ${packet.parent.component}, walkable = ${packet.flag})") 13 | } 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/proxy/handler/IfOpenTopProxyHandler.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.proxy.handler 2 | 3 | import com.opennxt.net.game.pipeline.GamePacketHandler 4 | import com.opennxt.net.game.serverprot.ifaces.IfOpenTop 5 | import com.opennxt.net.proxy.ProxyPlayer 6 | import mu.KotlinLogging 7 | 8 | object IfOpenTopProxyHandler: GamePacketHandler { 9 | val logger = KotlinLogging.logger {} 10 | 11 | override fun handle(context: ProxyPlayer, packet: IfOpenTop) { 12 | context.plaintextDumpFile.appendLine("player.interfaces.openTop(id = ${packet.id})") 13 | } 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/proxy/handler/IfSetEventsHandler.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.proxy.handler 2 | 3 | import com.opennxt.net.game.pipeline.GamePacketHandler 4 | import com.opennxt.net.game.serverprot.ifaces.IfSetevents 5 | import com.opennxt.net.proxy.ProxyPlayer 6 | import mu.KotlinLogging 7 | 8 | object IfSetEventsHandler : GamePacketHandler { 9 | val logger = KotlinLogging.logger {} 10 | 11 | override fun handle(context: ProxyPlayer, packet: IfSetevents) { 12 | context.plaintextDumpFile.appendLine("player.interfaces.events(id = ${packet.parent.parent}, component = ${packet.parent.component}, from = ${packet.fromSlot}, to = ${packet.toSlot}, mask = ${packet.mask})") 13 | } 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/proxy/handler/IfSethideHandler.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.proxy.handler 2 | 3 | import com.opennxt.net.game.pipeline.GamePacketHandler 4 | import com.opennxt.net.game.serverprot.ifaces.IfSethide 5 | import com.opennxt.net.proxy.ProxyPlayer 6 | import mu.KotlinLogging 7 | 8 | object IfSethideHandler : GamePacketHandler { 9 | val logger = KotlinLogging.logger {} 10 | 11 | override fun handle(context: ProxyPlayer, packet: IfSethide) { 12 | context.plaintextDumpFile.appendLine("player.interfaces.hide(id = ${packet.parent.parent}, component = ${packet.parent.component}, hidden = ${packet.hidden})") 13 | } 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/proxy/handler/IfSettextHandler.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.proxy.handler 2 | 3 | import com.opennxt.net.game.pipeline.GamePacketHandler 4 | import com.opennxt.net.game.serverprot.ifaces.IfSettext 5 | import com.opennxt.net.proxy.ProxyPlayer 6 | import mu.KotlinLogging 7 | 8 | object IfSettextHandler : GamePacketHandler { 9 | val logger = KotlinLogging.logger {} 10 | 11 | override fun handle(context: ProxyPlayer, packet: IfSettext) { 12 | context.plaintextDumpFile.appendLine("player.interfaces.text(id = ${packet.parent.parent}, component = ${packet.parent.component}, text = \"${packet.text.replace("\"", "\\\"")}\")") 13 | } 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/proxy/handler/RunClientScriptHandler.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.proxy.handler 2 | 3 | import com.opennxt.net.game.pipeline.GamePacketHandler 4 | import com.opennxt.net.game.serverprot.RunClientScript 5 | import com.opennxt.net.proxy.ProxyPlayer 6 | import mu.KotlinLogging 7 | 8 | object RunClientScriptHandler : GamePacketHandler { 9 | val logger = KotlinLogging.logger {} 10 | 11 | override fun handle(context: ProxyPlayer, packet: RunClientScript) { 12 | if (packet.args.isEmpty()) { 13 | context.plaintextDumpFile.appendLine("player.client.write(RunClientScript(script = ${packet.script}))") 14 | } else { 15 | context.plaintextDumpFile.appendLine( 16 | "player.client.write(RunClientScript(script = ${packet.script}, args = arrayOf(${ 17 | packet.args.joinToString(separator = ", ") { 18 | if (it is String) { 19 | "\"${it.replace("\"", "\\\"")}\"" 20 | } else { 21 | it.toString() 22 | } 23 | } 24 | })))" 25 | ) 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/proxy/handler/SetMapFlagHandler.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.proxy.handler 2 | 3 | import com.opennxt.net.game.pipeline.GamePacketHandler 4 | import com.opennxt.net.game.serverprot.SetMapFlag 5 | import com.opennxt.net.proxy.ProxyPlayer 6 | import mu.KotlinLogging 7 | 8 | object SetMapFlagHandler : GamePacketHandler { 9 | val logger = KotlinLogging.logger {} 10 | 11 | override fun handle(context: ProxyPlayer, packet: SetMapFlag) { 12 | (context as ProxyPlayer).plaintextDumpFile.appendLine("// Set map flag $packet") 13 | logger.info { "Map flag: $packet" } 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/proxy/handler/VarpLargeHandler.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.proxy.handler 2 | 3 | import com.opennxt.net.game.pipeline.GamePacketHandler 4 | import com.opennxt.net.game.serverprot.variables.VarpLarge 5 | import com.opennxt.net.proxy.ProxyPlayer 6 | import mu.KotlinLogging 7 | 8 | object VarpLargeHandler : GamePacketHandler { 9 | val logger = KotlinLogging.logger {} 10 | 11 | override fun handle(context: ProxyPlayer, packet: VarpLarge) { 12 | context.plaintextDumpFile.appendLine("channel.write(VarpLarge(${packet.id}, ${packet.value}))") 13 | } 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/net/proxy/handler/VarpSmallHandler.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.net.proxy.handler 2 | 3 | import com.opennxt.net.game.pipeline.GamePacketHandler 4 | import com.opennxt.net.game.serverprot.variables.VarpSmall 5 | import com.opennxt.net.proxy.ProxyPlayer 6 | import mu.KotlinLogging 7 | 8 | object VarpSmallHandler : GamePacketHandler { 9 | val logger = KotlinLogging.logger {} 10 | 11 | override fun handle(context: ProxyPlayer, packet: VarpSmall) { 12 | context.plaintextDumpFile.appendLine("channel.write(VarpSmall(${packet.id}, ${packet.value}))") 13 | } 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/DefaultStateChecker.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources 2 | 3 | interface DefaultStateChecker { 4 | fun isDefault(): Boolean 5 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/DiskResourceCodec.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources 2 | 3 | import java.nio.file.Path 4 | 5 | interface DiskResourceCodec { 6 | fun load(path: Path): T? 7 | fun store(path: Path, data: T) 8 | fun list(path: Path): Map 9 | fun getFileExtension(resource: T): String? 10 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/FilesystemResourceCodec.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources 2 | 3 | import com.opennxt.filesystem.Filesystem 4 | 5 | interface FilesystemResourceCodec { 6 | fun load(fs: Filesystem, id: Int): T? 7 | fun store(fs: Filesystem, id: Int, data: T) 8 | fun list(fs: Filesystem): Map 9 | fun getMaxId(fs: Filesystem): Int 10 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/FilesystemResources.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources 2 | 3 | import com.opennxt.filesystem.Filesystem 4 | import com.opennxt.resources.config.enums.EnumFilesystemCodec 5 | import com.opennxt.resources.config.enums.EnumDiskCodec 6 | import com.opennxt.resources.config.params.ParamDiskCodec 7 | import com.opennxt.resources.config.params.ParamFilesystemCodec 8 | import com.opennxt.resources.config.structs.StructDiskCodec 9 | import com.opennxt.resources.config.structs.StructFilesystemCodec 10 | import com.opennxt.resources.defaults.Defaults 11 | import java.nio.file.Files 12 | import java.nio.file.Path 13 | import java.util.* 14 | import kotlin.reflect.KClass 15 | 16 | class FilesystemResources(val filesystem: Filesystem, val path: Path) { 17 | companion object { 18 | lateinit var instance: FilesystemResources 19 | } 20 | 21 | private val fsCodices = EnumMap>(ResourceType::class.java) 22 | private val diskCodices = EnumMap>(ResourceType::class.java) 23 | 24 | val defaults = Defaults(filesystem) 25 | 26 | init { 27 | instance = this 28 | 29 | if (!Files.exists(path)) 30 | Files.createDirectories(path) 31 | 32 | fsCodices[ResourceType.ENUM] = EnumFilesystemCodec 33 | fsCodices[ResourceType.PARAM] = ParamFilesystemCodec 34 | fsCodices[ResourceType.STRUCT] = StructFilesystemCodec 35 | 36 | diskCodices[ResourceType.ENUM] = EnumDiskCodec 37 | diskCodices[ResourceType.PARAM] = ParamDiskCodec 38 | diskCodices[ResourceType.STRUCT] = StructDiskCodec 39 | } 40 | 41 | fun getFilesystemCodex(type: KClass<*>): FilesystemResourceCodec<*> { 42 | val resourceType = 43 | ResourceType.forClass(type) ?: throw NullPointerException("No resource type linked to type $type") 44 | 45 | return fsCodices[resourceType] ?: throw NullPointerException("No filesystem codex found for type $type") 46 | } 47 | 48 | fun getDiskCodec(type: KClass<*>): DiskResourceCodec<*> { 49 | val resourceType = 50 | ResourceType.forClass(type) ?: throw NullPointerException("No resource type linked to type $type") 51 | 52 | return diskCodices[resourceType] ?: throw NullPointerException("No disk codex found for type $type") 53 | } 54 | 55 | @Suppress("UNCHECKED_CAST") 56 | inline fun get(id: Int): T? { 57 | return getFilesystemCodex(T::class).load(filesystem, id) as? T 58 | } 59 | 60 | @Suppress("UNCHECKED_CAST") 61 | fun list(type: KClass): Map { 62 | return getFilesystemCodex(type).list(filesystem) as Map 63 | } 64 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/ResourceType.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources 2 | 3 | import com.opennxt.resources.config.enums.EnumDefinition 4 | import com.opennxt.resources.config.params.ParamDefinition 5 | import com.opennxt.resources.config.structs.StructDefinition 6 | import kotlin.reflect.KClass 7 | 8 | enum class ResourceType(val identifier: String, val kclass: KClass<*>) { 9 | ENUM("enum", EnumDefinition::class), 10 | PARAM("param", ParamDefinition::class), 11 | STRUCT("struct", StructDefinition::class), 12 | 13 | /* 14 | TODO: 15 | VAR_PLAYER(60, VarPlayer::class), 16 | VAR_NPC(61, VarNpc::class), 17 | VAR_CLIENT(62, VarClient::class), 18 | VAR_WORLD(63, VarWorld::class), 19 | VAR_REGION(64, VarRegion::class), 20 | VAR_OBJECT(65, VarObject::class), 21 | VAR_CLAN(66, VarClan::class), 22 | VAR_CLAN_SETTING(67, VarClanSetting::class), 23 | */ 24 | ; 25 | 26 | companion object { 27 | private val values = values() 28 | 29 | fun getArchive(id: Int, size: Int): Int = id.ushr(size) 30 | 31 | fun getFile(id: Int, size: Int): Int = (id and (1 shl size) - 1) 32 | 33 | fun forClass(kclass: KClass<*>): ResourceType? = values.firstOrNull { it.kclass == kclass } 34 | } 35 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/config/enums/EnumDefinition.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources.config.enums 2 | 3 | import com.opennxt.resources.DefaultStateChecker 4 | import com.opennxt.resources.config.vars.ScriptVarType 5 | import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap 6 | 7 | data class EnumDefinition( 8 | var keyType: ScriptVarType = ScriptVarType.INT, 9 | var valueType: ScriptVarType = ScriptVarType.INT, 10 | var defaultInt: Int = 0, 11 | var defaultString: String? = null, 12 | var values: MutableMap = Int2ObjectAVLTreeMap() 13 | ): DefaultStateChecker { 14 | companion object { 15 | private val DEFAULT = EnumDefinition() 16 | } 17 | 18 | override fun isDefault(): Boolean = this == DEFAULT 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/config/params/ParamDefinition.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources.config.params 2 | 3 | import com.opennxt.resources.DefaultStateChecker 4 | import com.opennxt.resources.config.vars.ScriptVarType 5 | 6 | data class ParamDefinition( 7 | var defaultInt: Int = 0, 8 | var defaultString: String = "null", 9 | var membersOnly: Boolean = true, 10 | var type: ScriptVarType = ScriptVarType.INT 11 | ): DefaultStateChecker { 12 | companion object { 13 | private val DEFAULT = ParamDefinition() 14 | } 15 | 16 | override fun isDefault(): Boolean = this == DEFAULT 17 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/config/params/ParamDiskCodec.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources.config.params 2 | 3 | import com.moandjiezana.toml.Toml 4 | import com.moandjiezana.toml.TomlWriter 5 | import com.opennxt.resources.DiskResourceCodec 6 | import com.opennxt.resources.config.vars.ScriptVarType 7 | import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap 8 | import java.nio.file.Files 9 | import java.nio.file.Path 10 | 11 | object ParamDiskCodec : DiskResourceCodec { 12 | override fun list(path: Path): Map { 13 | val result = Object2ObjectOpenHashMap() 14 | Files.list(path).forEach { file -> 15 | if (Files.isRegularFile(file)) { 16 | val name = file.fileName.toString().toLowerCase() 17 | if (name.endsWith(".toml")) { 18 | result[name.substring(0, name.length - 5)] = file 19 | } 20 | } else if (Files.isDirectory(file)) { 21 | result.putAll(list(file)) 22 | } 23 | } 24 | return result 25 | } 26 | 27 | override fun load(path: Path): ParamDefinition? { 28 | if (!Files.exists(path)) return null 29 | 30 | val reader = Toml() 31 | reader.read(path.toFile()) 32 | 33 | val definition = ParamDefinition() 34 | 35 | definition.type = ScriptVarType.valueOf(reader.getString("type")) 36 | 37 | if(reader.contains("defaultInt")) { 38 | definition.defaultInt = definition.type.readableToRaw(reader.getLong("defaultInt").toString()) as Int 39 | } 40 | 41 | if (reader.contains("defaultString")) { 42 | definition.defaultString = reader.getString("defaultString") 43 | } 44 | 45 | if (reader.contains("membersOnly")) { 46 | definition.membersOnly = reader.getBoolean("membersOnly") 47 | } 48 | 49 | return definition 50 | } 51 | 52 | override fun store(path: Path, data: ParamDefinition) { 53 | Files.createDirectories(path.parent) 54 | Files.deleteIfExists(path) 55 | 56 | val map = mutableMapOf( 57 | "type" to data.type, 58 | "defaultInt" to data.type.rawToReadable(data.defaultInt) 59 | ) 60 | 61 | if (data.membersOnly) map["membersOnly"] = true 62 | 63 | if (data.defaultString != "null") 64 | map["defaultString"] = data.defaultString 65 | 66 | TomlWriter().write(map, path.toFile()) 67 | } 68 | 69 | override fun getFileExtension(resource: ParamDefinition): String? = "toml" 70 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/config/structs/StructDefinition.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources.config.structs 2 | 3 | import com.opennxt.OpenNXT 4 | import com.opennxt.resources.DefaultStateChecker 5 | import com.opennxt.resources.config.params.ParamDefinition 6 | import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap 7 | 8 | class StructDefinition(val values: Int2ObjectOpenHashMap = Int2ObjectOpenHashMap()) : DefaultStateChecker { 9 | override fun isDefault(): Boolean = values.isEmpty() 10 | 11 | fun getInt( 12 | key: Int, 13 | default: Int = OpenNXT.resources.get(key)?.defaultInt 14 | ?: throw NullPointerException("couldn't find param $key") 15 | ): Int = values.getOrDefault(key, default) as Int 16 | 17 | fun getString( 18 | key: Int, 19 | default: String = OpenNXT.resources.get(key)?.defaultString 20 | ?: throw NullPointerException("couldn't find param $key") 21 | ): String = values.getOrDefault(key, default) as String 22 | 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/config/structs/StructDiskCodec.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources.config.structs 2 | 3 | import com.moandjiezana.toml.Toml 4 | import com.moandjiezana.toml.TomlWriter 5 | import com.opennxt.OpenNXT 6 | import com.opennxt.resources.DiskResourceCodec 7 | import com.opennxt.resources.FilesystemResources 8 | import com.opennxt.resources.config.params.ParamDefinition 9 | import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap 10 | import java.nio.file.Files 11 | import java.nio.file.Path 12 | 13 | object StructDiskCodec : DiskResourceCodec { 14 | override fun list(path: Path): Map { 15 | val result = Object2ObjectOpenHashMap() 16 | Files.list(path).forEach { file -> 17 | if (Files.isRegularFile(file)) { 18 | val name = file.fileName.toString().toLowerCase() 19 | if (name.endsWith(".toml")) { 20 | result[name.substring(0, name.length - 5)] = file 21 | } 22 | } else if (Files.isDirectory(file)) { 23 | result.putAll(list(file)) 24 | } 25 | } 26 | return result 27 | } 28 | 29 | override fun load(path: Path): StructDefinition? { 30 | if (!Files.exists(path)) return null 31 | 32 | val reader = Toml() 33 | reader.read(path.toFile()) 34 | 35 | val definition = StructDefinition() 36 | val raw = reader.getTable("values").toMap() 37 | raw.forEach { (key, value) -> 38 | val id = key.toInt() 39 | 40 | val param = FilesystemResources.instance.get(id) 41 | 42 | definition.values[id] = param!!.type.readableToRaw(value.toString()) 43 | } 44 | 45 | return definition 46 | } 47 | 48 | override fun store(path: Path, data: StructDefinition) { 49 | Files.createDirectories(path.parent) 50 | Files.deleteIfExists(path) 51 | 52 | val values = HashMap() 53 | data.values.forEach { (k, v) -> 54 | val param = FilesystemResources.instance.get(k) 55 | 56 | values[k] = param!!.type.rawToReadable(v) 57 | } 58 | 59 | TomlWriter().write(mapOf("values" to values), path.toFile()) 60 | } 61 | 62 | override fun getFileExtension(resource: StructDefinition): String? = "toml" 63 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/config/vars/BaseVarType.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources.config.vars 2 | 3 | import kotlin.reflect.KClass 4 | 5 | enum class BaseVarType( 6 | val id: Int, 7 | val clazz: KClass<*> 8 | ) { 9 | INTEGER(0, Int::class), 10 | LONG(1, Long::class), 11 | STRING(2, String::class), 12 | COORDFINE(3, Unit::class); // TODO Coordfine 13 | 14 | companion object { 15 | private val values = values() 16 | 17 | fun byId(id: Int): BaseVarType { 18 | return values[id] 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/config/vars/VarDefinition.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources.config.vars 2 | 3 | import com.opennxt.api.vars.VarDomain 4 | import com.opennxt.resources.DefaultStateChecker 5 | 6 | abstract class VarDefinition( 7 | val domain: VarDomain, 8 | var type: ScriptVarType = ScriptVarType.INT, 9 | var lifetime: Int = 0, 10 | var forceDefault: Boolean = true 11 | ) : DefaultStateChecker -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/config/vars/VarDefinitionDiskCodec.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources.config.vars 2 | 3 | import com.moandjiezana.toml.Toml 4 | import com.moandjiezana.toml.TomlWriter 5 | import com.opennxt.api.vars.VarDomain 6 | import com.opennxt.resources.DiskResourceCodec 7 | import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap 8 | import java.nio.file.Files 9 | import java.nio.file.Path 10 | 11 | class VarDefinitionDiskCodec(val emptyProvider: () -> T): DiskResourceCodec { 12 | override fun list(path: Path): Map { 13 | val result = Object2ObjectOpenHashMap() 14 | Files.list(path).forEach { file -> 15 | if (Files.isRegularFile(file)) { 16 | val name = file.fileName.toString().toLowerCase() 17 | if (name.endsWith(".toml")) { 18 | result[name.substring(0, name.length - 5)] = file 19 | } 20 | } else if (Files.isDirectory(file)) { 21 | result.putAll(list(file)) 22 | } 23 | } 24 | return result 25 | } 26 | 27 | override fun load(path: Path): T? { 28 | if (!Files.exists(path)) return null 29 | 30 | val reader = Toml() 31 | reader.read(path.toFile()) 32 | 33 | val definition = emptyProvider() 34 | val raw = reader.getTable("values").toMap() 35 | definition.forceDefault = raw["forceDefault"] as Boolean 36 | definition.lifetime = raw["lifetime"] as Int 37 | definition.type = ScriptVarType.valueOf(raw["type"] as String) 38 | 39 | return definition 40 | } 41 | 42 | override fun store(path: Path, data: T) { 43 | Files.createDirectories(path.parent) 44 | Files.deleteIfExists(path) 45 | 46 | val values = HashMap() 47 | values["type"] = data.type.name 48 | values["lifetime"] = data.lifetime 49 | values["forceDefault"] = data.forceDefault 50 | 51 | TomlWriter().write(mapOf("values" to values), path.toFile()) 52 | } 53 | 54 | override fun getFileExtension(resource: T): String? = "toml" 55 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/config/vars/impl/VarClanDefinition.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources.config.vars.impl 2 | 3 | import com.opennxt.api.vars.VarDomain 4 | import com.opennxt.resources.config.vars.VarDefinition 5 | 6 | class VarClanDefinition: VarDefinition(VarDomain.CLAN) { 7 | companion object { 8 | val DEFAULT = VarClanDefinition() 9 | } 10 | 11 | override fun isDefault(): Boolean = this == DEFAULT 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/config/vars/impl/VarClanSettingDefinition.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources.config.vars.impl 2 | 3 | import com.opennxt.api.vars.VarDomain 4 | import com.opennxt.resources.config.vars.VarDefinition 5 | 6 | class VarClanSettingDefinition: VarDefinition(VarDomain.CLAN_SETTING) { 7 | companion object { 8 | val DEFAULT = VarClanSettingDefinition() 9 | } 10 | 11 | override fun isDefault(): Boolean = this == DEFAULT 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/config/vars/impl/VarClientDefinition.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources.config.vars.impl 2 | 3 | import com.opennxt.api.vars.VarDomain 4 | import com.opennxt.resources.config.vars.VarDefinition 5 | 6 | class VarClientDefinition: VarDefinition(VarDomain.CLIENT) { 7 | companion object { 8 | val DEFAULT = VarClientDefinition() 9 | } 10 | 11 | override fun isDefault(): Boolean = this == DEFAULT 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/config/vars/impl/VarNpcDefinition.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources.config.vars.impl 2 | 3 | import com.opennxt.api.vars.VarDomain 4 | import com.opennxt.resources.config.vars.VarDefinition 5 | 6 | class VarNpcDefinition: VarDefinition(VarDomain.NPC) { 7 | companion object { 8 | val DEFAULT = VarNpcDefinition() 9 | } 10 | 11 | override fun isDefault(): Boolean = this == DEFAULT 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/config/vars/impl/VarObjectDefinition.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources.config.vars.impl 2 | 3 | import com.opennxt.api.vars.VarDomain 4 | import com.opennxt.resources.config.vars.VarDefinition 5 | 6 | class VarObjectDefinition: VarDefinition(VarDomain.OBJECT) { 7 | companion object { 8 | val DEFAULT = VarObjectDefinition() 9 | } 10 | 11 | override fun isDefault(): Boolean = this == DEFAULT 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/config/vars/impl/VarPlayerDefinition.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources.config.vars.impl 2 | 3 | import com.opennxt.api.vars.VarDomain 4 | import com.opennxt.resources.config.vars.VarDefinition 5 | 6 | class VarPlayerDefinition: VarDefinition(VarDomain.PLAYER) { 7 | companion object { 8 | val DEFAULT = VarPlayerDefinition() 9 | } 10 | 11 | override fun isDefault(): Boolean = this == DEFAULT 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/config/vars/impl/VarRegionDefinition.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources.config.vars.impl 2 | 3 | import com.opennxt.api.vars.VarDomain 4 | import com.opennxt.resources.config.vars.VarDefinition 5 | 6 | class VarRegionDefinition: VarDefinition(VarDomain.REGION) { 7 | companion object { 8 | val DEFAULT = VarRegionDefinition() 9 | } 10 | 11 | override fun isDefault(): Boolean = this == DEFAULT 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/config/vars/impl/VarWorldDefinition.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources.config.vars.impl 2 | 3 | import com.opennxt.api.vars.VarDomain 4 | import com.opennxt.resources.config.vars.VarDefinition 5 | 6 | class VarWorldDefinition: VarDefinition(VarDomain.WORLD) { 7 | companion object { 8 | val DEFAULT = VarWorldDefinition() 9 | } 10 | 11 | override fun isDefault(): Boolean = this == DEFAULT 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/defaults/Default.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources.defaults 2 | 3 | import io.netty.buffer.ByteBuf 4 | 5 | /** 6 | * Represents a default in a cache. A default is a lot like a config, but defaults are singletons, whereas configs are 7 | * templates and have many instances. 8 | */ 9 | interface Default { 10 | 11 | /** 12 | * @return The type of this default 13 | */ 14 | val group: DefaultGroup 15 | 16 | fun decode(buf: ByteBuf) 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/defaults/DefaultGroup.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources.defaults 2 | 3 | import com.opennxt.resources.defaults.stats.StatDefaults 4 | import com.opennxt.resources.defaults.wearpos.WearposDefaults 5 | import io.netty.buffer.ByteBuf 6 | import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap 7 | import kotlin.reflect.KClass 8 | import kotlin.reflect.full.createInstance 9 | 10 | enum class DefaultGroup(val fileId: Int, val defaultClass: KClass?) { 11 | MAP(1, null), 12 | GROUP_2(2, null), 13 | GRAPHICS(3, null), 14 | AUDIO(4, null), 15 | MICROTRANSACTION(5, null), 16 | WEARPOS(6, WearposDefaults::class), 17 | KEYBOARD(7, null), 18 | GROUP_8(8, null), 19 | STAT(9, StatDefaults::class), 20 | ERROR(10, null); 21 | 22 | @Suppress("UNCHECKED_CAST") 23 | fun decode(buffer: ByteBuf): T? { 24 | return try { 25 | val instance = defaultClass!!.createInstance() 26 | instance.decode(buffer) 27 | 28 | instance as T 29 | } catch (e: Exception) { 30 | e.printStackTrace() 31 | null 32 | } 33 | } 34 | 35 | companion object { 36 | private val classToGroup = Object2ObjectOpenHashMap, DefaultGroup>() 37 | 38 | init { 39 | for (group in values()) { 40 | if (group.defaultClass != null) 41 | classToGroup[group.defaultClass] = group 42 | } 43 | } 44 | 45 | fun getGroup(type: KClass): DefaultGroup? = classToGroup[type] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/defaults/Defaults.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources.defaults 2 | 3 | import com.opennxt.filesystem.Container 4 | import com.opennxt.filesystem.Filesystem 5 | import io.netty.buffer.Unpooled 6 | import java.util.* 7 | 8 | class Defaults(val fs: Filesystem) { 9 | private val cachedDefaults = EnumMap(DefaultGroup::class.java) 10 | 11 | fun getDefault(group: DefaultGroup): T { 12 | if (group.defaultClass == null) 13 | throw UnsupportedOperationException("Group is not implemented: $group") 14 | 15 | if (cachedDefaults.containsKey(group)) 16 | return cachedDefaults.getValue(group) as T 17 | 18 | 19 | val buf = Unpooled.wrappedBuffer(Container.decode(fs.read(28, group.fileId)!!).data) 20 | try { 21 | val value = group.decode(buf)!! 22 | cachedDefaults[group] = value 23 | return value 24 | } finally { 25 | buf.release() 26 | } 27 | } 28 | 29 | inline fun get(): T { 30 | val group = DefaultGroup.getGroup(T::class) ?: throw UnsupportedOperationException("Default group: ${T::class}") 31 | return getDefault(group) 32 | } 33 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/defaults/stats/StatDefaults.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources.defaults.stats 2 | 3 | import com.opennxt.resources.defaults.Default 4 | import com.opennxt.resources.defaults.DefaultGroup 5 | import io.netty.buffer.ByteBuf 6 | import mu.KotlinLogging 7 | 8 | class StatDefaults : Default { 9 | private val logger = KotlinLogging.logger{} 10 | 11 | override val group: DefaultGroup = DefaultGroup.STAT 12 | 13 | var tables: Array = arrayOf() 14 | var stats: Array = arrayOf() 15 | 16 | override fun decode(buf: ByteBuf) { 17 | while (buf.isReadable) { 18 | val opcode = buf.readUnsignedByte().toInt() 19 | if (opcode == 0) break 20 | 21 | when(opcode) { 22 | 1 -> decodeStatDefinitions(buf) 23 | 2 -> decodeExperienceTables(buf) 24 | else -> throw IllegalArgumentException("Unknown stat opcode: $opcode") 25 | } 26 | } 27 | 28 | logger.info("Decoded ${stats.size} skills using ${tables.size + 1} unique experience tables.") 29 | } 30 | 31 | private fun decodeStatDefinitions(buf: ByteBuf) { 32 | stats = Array(buf.readUnsignedByte().toInt()) { StatDefinition(it, StatExperienceTable.DEFAULT, 99) } 33 | 34 | for (i in stats.indices) { 35 | val id = buf.readUnsignedByte().toInt() 36 | val cap = buf.readUnsignedShort() 37 | var table = StatExperienceTable.DEFAULT 38 | 39 | val flags = buf.readUnsignedByte().toInt() 40 | if (flags and 0x2 != 0) buf.readUnsignedByte() 41 | if (flags and 0x4 != 0) { 42 | val tableId = buf.readUnsignedByte().toInt() 43 | if (tableId >= tables.size) { 44 | logger.error("Xp table for stat '$id' out of bounds: '$tableId', using default table") 45 | } else { 46 | table = tables[tableId] 47 | } 48 | } 49 | if (flags and 0x8 != 0) buf.readByte() 50 | buf.readByte() 51 | 52 | stats[id] = StatDefinition(id, table, cap) 53 | } 54 | } 55 | 56 | private fun decodeExperienceTables(buf: ByteBuf) { 57 | tables = Array(buf.readUnsignedByte().toInt()) { StatExperienceTable.DEFAULT } 58 | 59 | var id = buf.readUnsignedByte().toInt() 60 | while (id != 255) { 61 | val values = IntArray(buf.readUnsignedShort()) 62 | for (level in values.indices) 63 | values[level] = buf.readInt() 64 | tables[id] = StatExperienceTable(values) 65 | id = buf.readUnsignedByte().toInt() 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/defaults/stats/StatDefinition.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources.defaults.stats 2 | 3 | data class StatDefinition( 4 | val id: Int, 5 | val table: StatExperienceTable, 6 | val cap: Int 7 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/defaults/stats/StatExperienceTable.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources.defaults.stats 2 | 3 | import kotlin.math.pow 4 | 5 | class StatExperienceTable { 6 | 7 | companion object { 8 | val DEFAULT = StatExperienceTable() 9 | } 10 | 11 | private val table: IntArray 12 | 13 | constructor() { 14 | table = IntArray(120) 15 | var xp = 0 16 | for (level in 1..120) { 17 | val difference = (level.toDouble() + 300.0 * 2.0.pow(level.toDouble() / 7.0)).toInt() 18 | xp += difference 19 | table[level - 1] = xp / 4 20 | } 21 | validate() 22 | } 23 | 24 | constructor(values: IntArray) { 25 | this.table = values 26 | } 27 | 28 | /** 29 | * Validates the xp table 30 | */ 31 | private fun validate() { 32 | for (pos in 1 until table.size) { 33 | if (table[pos - 1] < 0) { 34 | throw IllegalArgumentException("Negative XP at pos:" + (pos - 1)) 35 | } 36 | if (table[pos] < table[pos - 1]) { 37 | throw IllegalArgumentException("XP goes backwards at pos:$pos") 38 | } 39 | } 40 | } 41 | 42 | /** 43 | * Gets the level based on the experience 44 | */ 45 | fun levelForXp(xp: Int): Int { 46 | var level = 0 47 | var pos = 0 48 | while (pos < table.size && xp >= table[pos]) { 49 | level = 1 + pos 50 | pos++ 51 | } 52 | return level 53 | } 54 | 55 | /** 56 | * Gets the xp required for a certain level 57 | */ 58 | fun xpForLevel(level: Int): Int { 59 | var level = level 60 | if (level < 1) { 61 | return 0 62 | } 63 | if (level > table.size) { 64 | level = table.size 65 | } 66 | return table[level - 1] 67 | } 68 | 69 | override fun toString(): String { 70 | return "StatExperienceTable[table=${table.contentToString()}]" 71 | } 72 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/defaults/wearpos/WearposDefaults.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources.defaults.wearpos 2 | 3 | import com.opennxt.resources.defaults.Default 4 | import com.opennxt.resources.defaults.DefaultGroup 5 | import io.netty.buffer.ByteBuf 6 | import mu.KotlinLogging 7 | 8 | data class WearposDefaults( 9 | var lefthand: Short = 0, 10 | var righthand: Short = 0, 11 | var slots: IntArray = intArrayOf(), 12 | var unknown1: IntArray = intArrayOf(), 13 | var unknown2: IntArray = intArrayOf() 14 | ) : Default { 15 | private val logger = KotlinLogging.logger {} 16 | 17 | override val group: DefaultGroup = DefaultGroup.WEARPOS 18 | 19 | override fun decode(buf: ByteBuf) { 20 | while (buf.isReadable) { 21 | val opcode = buf.readUnsignedByte().toInt() 22 | if (opcode == 0) break 23 | 24 | when (opcode) { 25 | 1 -> slots = buf.readArray() 26 | 3 -> lefthand = buf.readUnsignedByte() 27 | 4 -> righthand = buf.readUnsignedByte() 28 | 5 -> unknown1 = buf.readArray() 29 | 6 -> unknown2 = buf.readArray() 30 | else -> throw IllegalArgumentException("Unknown wearpos opcode: $opcode") 31 | } 32 | } 33 | 34 | if (slots.isEmpty()) { 35 | logger.error {"WearposDefaults didn't load slots. This is very bad, as cache reading most likely failed."} 36 | throw IllegalStateException("WearposDefaults 'slots.size' == 0 after 'decode'") 37 | } 38 | } 39 | 40 | private fun ByteBuf.readArray(): IntArray { 41 | val size = readUnsignedByte().toInt() 42 | val array = IntArray(size) 43 | for (i in 0 until size) 44 | array[i] = readUnsignedByte().toInt() 45 | return array 46 | } 47 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/map/TerrainData.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources.map 2 | 3 | import com.opennxt.ext.readSmartShort 4 | import io.netty.buffer.ByteBuf 5 | 6 | class TerrainData { 7 | val tiles = Array(4) { Array(66) { Array(66) { TerrainTile() } } } 8 | 9 | fun hasTerrainData(plane: Int): Boolean = tiles[plane].any { a -> a.any { !it.isEmpty() || it.idkByte != 0 } } 10 | 11 | operator fun get(plane: Int, x: Int, y: Int): TerrainTile { 12 | return tiles[plane][x + 1][y + 1] 13 | } 14 | 15 | data class TerrainTile( 16 | var underlayId: Int = -1, 17 | var b: Int = -1, 18 | var overlayId: Int = -1, 19 | var d: Int = -1, 20 | var e: Int = -1, 21 | var height: Int = -1, 22 | var overlayShape: Int = -1, 23 | var overlayRotation: Int = -1, 24 | var mask: Int = 0, 25 | var idkByte: Int = 0 // This byte is not read/used by the client, but is not always 0. 26 | ) { 27 | fun isEmpty(): Boolean = (mask and 0x1) == 0 28 | } 29 | 30 | fun decode(buf: ByteBuf) { 31 | while (buf.isReadable) { 32 | val plane = buf.readUnsignedByte().toInt() 33 | 34 | for (xRemaining in 66 downTo 1) { 35 | for (yRemaining in 1 until 67) { 36 | val tile = get(plane, 65 - xRemaining, 65 - yRemaining) 37 | tile.mask = buf.readUnsignedByte().toInt() 38 | 39 | if (tile.mask == 0) { 40 | tile.idkByte = buf.readUnsignedByte().toInt() 41 | continue 42 | } 43 | 44 | if ((tile.mask and 0x10) != 0) { 45 | tile.height = buf.readUnsignedShort() 46 | } else { 47 | tile.height = buf.readUnsignedByte().toInt() 48 | } 49 | 50 | val underlayId = buf.readSmartShort() - 1 51 | if (underlayId != -1) { 52 | tile.underlayId = underlayId 53 | tile.b = buf.readUnsignedShort() 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/model/BaseModel.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources.model 2 | 3 | class BaseModel { 4 | var vertices: Array = emptyArray() 5 | 6 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/model/BaseModelBillboard.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources.model 2 | 3 | class BaseModelBillboard { 4 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/model/ModelSegment.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources.model 2 | 3 | class ModelSegment { 4 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/resources/model/Vec3s.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.resources.model 2 | 3 | class Vec3s(val x: Int, val y: Int, val z: Int) -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/tools/Tool.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.tools 2 | 3 | import com.github.ajalt.clikt.core.CliktCommand 4 | import com.opennxt.Constants 5 | import com.opennxt.filesystem.Filesystem 6 | import com.opennxt.filesystem.sqlite.SqliteFilesystem 7 | import com.opennxt.resources.FilesystemResources 8 | import mu.KotlinLogging 9 | 10 | abstract class Tool(name: String, help: String) : CliktCommand(name = name, help = help) { 11 | protected val logger = KotlinLogging.logger { } 12 | 13 | protected val filesystem by lazy { 14 | logger.info { "Loading filesystem from ${Constants.CACHE_PATH}" } 15 | SqliteFilesystem(Constants.CACHE_PATH) 16 | } 17 | 18 | protected val resources by lazy { 19 | logger.info { "Loading filesystem resources" } 20 | FilesystemResources(filesystem, Constants.RESOURCE_PATH) 21 | } 22 | 23 | override fun run() { 24 | logger.info { "Executing tool ${this::class.simpleName}" } 25 | 26 | runTool() 27 | } 28 | 29 | abstract fun runTool() 30 | 31 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/tools/ToolExecutor.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.tools 2 | 3 | import com.github.ajalt.clikt.core.NoRunCliktCommand 4 | import com.github.ajalt.clikt.core.subcommands 5 | import io.github.classgraph.ClassGraph 6 | import mu.KotlinLogging 7 | import kotlin.system.exitProcess 8 | 9 | object ToolExecutor : NoRunCliktCommand( 10 | name = "run-tool", 11 | help = "Executes a tool bundled in the server" 12 | ) { 13 | private val logger = KotlinLogging.logger {} 14 | 15 | init { 16 | val result = ClassGraph() 17 | .enableClassInfo() 18 | .acceptPackages("com.opennxt.tools.impl") 19 | .scan() 20 | 21 | val classes = result.getSubclasses("com.opennxt.tools.Tool") 22 | 23 | val tools = classes.map { it.loadClass().newInstance() as Tool } 24 | 25 | if (tools.isEmpty()) { 26 | logger.error { "No bundled tools found" } 27 | exitProcess(1) 28 | } 29 | 30 | subcommands(tools) 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/tools/impl/ResourceDumper.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.tools.impl 2 | 3 | import com.github.ajalt.clikt.parameters.options.flag 4 | import com.github.ajalt.clikt.parameters.options.multiple 5 | import com.github.ajalt.clikt.parameters.options.option 6 | import com.github.ajalt.clikt.parameters.types.enum 7 | import com.opennxt.Constants 8 | import com.opennxt.resources.DefaultStateChecker 9 | import com.opennxt.resources.DiskResourceCodec 10 | import com.opennxt.resources.ResourceType 11 | import com.opennxt.tools.Tool 12 | import java.nio.file.Files 13 | import kotlin.system.exitProcess 14 | 15 | class ResourceDumper : Tool("resource-dumper", "Dumps resources from the filesystem") { 16 | val types by option(help = "A list of all resource types that should be dumped") 17 | .enum() 18 | .multiple() 19 | 20 | val dumpAll by option(help = "Dumps all types regardless of the specified types in 'types'") 21 | .flag(default = false) 22 | 23 | override fun runTool() { 24 | if (types.isEmpty() && !dumpAll) { 25 | logger.error { "Please specify the resource types to dump, or use --dump-all (for help: --help)" } 26 | exitProcess(1) 27 | } 28 | 29 | val types = if (dumpAll) ResourceType.values() else this.types.toTypedArray() 30 | 31 | for (type in types) { 32 | val path = Constants.RESOURCE_PATH.resolve("dumps").resolve(type.identifier) 33 | logger.info { "Dumping type $type to $path" } 34 | 35 | if (!Files.exists(path)) 36 | Files.createDirectories(path) 37 | 38 | @Suppress("UNCHECKED_CAST") 39 | val diskCodec = resources.getDiskCodec(type.kclass) as DiskResourceCodec 40 | resources.list(type.kclass).forEach { (id, resource) -> 41 | if (resource is DefaultStateChecker && resource.isDefault()) 42 | return@forEach 43 | 44 | val extension = diskCodec.getFileExtension(resource) 45 | if (extension == null) { 46 | diskCodec.store(path.resolve(id.toString()), resource) 47 | } else { 48 | diskCodec.store(path.resolve("$id.$extension"), resource) 49 | } 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/tools/impl/RsaKeyGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.tools.impl 2 | 3 | import com.opennxt.Constants 4 | import com.opennxt.config.RsaConfig 5 | import com.opennxt.config.TomlConfig 6 | import com.opennxt.tools.Tool 7 | import com.opennxt.util.RSAUtil 8 | import java.nio.file.Files 9 | 10 | class RsaKeyGenerator : Tool("rsa-key-generator", "Generates RSA keys for the launcher and js5/login servers") { 11 | override fun runTool() { 12 | if (Files.exists(RsaConfig.DEFAULT_PATH)) { 13 | val renamed = Constants.CONFIG_PATH.resolve("rsa.toml_${System.currentTimeMillis()}") 14 | logger.info { "A RSA config file already exists at ${RsaConfig.DEFAULT_PATH}. Renaming existing config to ${renamed.fileName}" } 15 | Files.move(RsaConfig.DEFAULT_PATH, renamed) 16 | } 17 | 18 | logger.info { "Generating new js5 keys (size=4096)" } 19 | val js5 = RSAUtil.generateKeySpec(4096) 20 | 21 | logger.info { "Generating new login keys (size=1024)" } 22 | val login = RSAUtil.generateKeySpec(1024) 23 | 24 | logger.info { "Generating new launcher keys (size=4096)" } 25 | val launcher = RSAUtil.generateKeySpec(4096) 26 | 27 | val config = TomlConfig.load(RsaConfig.DEFAULT_PATH) 28 | config.js5 = RsaConfig.RsaKeyPair(js5.privateExponent, js5.modulus) 29 | config.login = RsaConfig.RsaKeyPair(login.privateExponent, login.modulus) 30 | config.launcher = RsaConfig.RsaKeyPair(launcher.privateExponent, launcher.modulus) 31 | 32 | TomlConfig.save(RsaConfig.DEFAULT_PATH, config) 33 | logger.info { "Saved newly generated RSA keys to ${RsaConfig.DEFAULT_PATH}" } 34 | } 35 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/tools/impl/cachedownloader/Js5ClientState.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.tools.impl.cachedownloader 2 | 3 | enum class Js5ClientState(val canRead: Boolean = true) { 4 | HANDSHAKE, 5 | PREFETCHES, 6 | ACTIVE, 7 | CRASHED(false), 8 | DISCONNECTED(false) 9 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/tools/impl/cachedownloader/Js5Credentials.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.tools.impl.cachedownloader 2 | 3 | import com.opennxt.model.files.BinaryType 4 | import com.opennxt.model.files.ClientConfig 5 | 6 | data class Js5Credentials(val version: Int, val token: String){ 7 | companion object { 8 | fun download(url: String = "https://world5.runescape.com/jav_config.ws"): Js5Credentials { 9 | val config = ClientConfig.download(url, BinaryType.WIN64) 10 | val version = config["server_version"] ?: throw NullPointerException("server_version not found") 11 | val token = config.getJs5Token() 12 | 13 | return Js5Credentials(version.toInt(), token) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/tools/impl/cachedownloader/Js5HttpRequest.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.tools.impl.cachedownloader 2 | 3 | import java.net.URL 4 | 5 | class Js5HttpRequest(val url: URL, val request: Js5RequestHandler.ArchiveRequest): Runnable { 6 | override fun run() { 7 | val data = url.readBytes() 8 | 9 | request.allocateBuffer(data.size + 2) 10 | request.buffer!!.put(data, 0, data.size) 11 | request.buffer!!.flip() 12 | 13 | request.notifyCompleted() 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/util/MD5.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.util 2 | 3 | import java.security.MessageDigest 4 | 5 | object MD5 { 6 | private val LOCK = Any() 7 | private val md5 = MessageDigest.getInstance("MD5") 8 | fun hash(data: ByteArray): ByteArray { 9 | synchronized(LOCK) { 10 | md5.update(data) 11 | val digest = md5.digest() 12 | md5.reset() 13 | return digest 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/util/RSAUtil.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.util 2 | 3 | import java.math.BigInteger 4 | import java.nio.charset.Charset 5 | import java.security.KeyFactory 6 | import java.security.KeyPairGenerator 7 | import java.security.spec.RSAKeyGenParameterSpec 8 | import java.security.spec.RSAPrivateKeySpec 9 | 10 | object RSAUtil { 11 | val PUBLIC_KEY = BigInteger("10001", 16) 12 | 13 | fun generateKeySpec(size: Int): RSAPrivateKeySpec { 14 | val factory = KeyFactory.getInstance("RSA") 15 | 16 | val generator = KeyPairGenerator.getInstance("RSA") 17 | val spec = RSAKeyGenParameterSpec(size, PUBLIC_KEY) 18 | generator.initialize(spec) 19 | 20 | val privateKey = generator.genKeyPair().private 21 | 22 | return factory.getKeySpec(privateKey, RSAPrivateKeySpec::class.java) 23 | } 24 | 25 | fun findRSAKey(data: ByteArray, bits: Int): BigInteger? { 26 | val size = bits / 4 27 | val buffer = ByteArray(size) 28 | 29 | for (i in 1 until data.size - buffer.size - 1) { 30 | // Ensure we don't run into any (previous) string terminators 31 | if (data[i].toInt() == 0) continue 32 | if (data[i - 1].toInt() != 0) continue 33 | 34 | // Ensure the key is followed by a string terminator 35 | if (data[i + size + 1].toInt() != 0) continue 36 | if (data[i + size + 2].toInt() != 0) continue 37 | 38 | // Copy the raw key into our buffer 39 | System.arraycopy(data, i, buffer, 0, size) 40 | 41 | try { 42 | // If it is not a valid BigInteger the instantiaton will throw an error 43 | return BigInteger(String(buffer, Charset.forName("ASCII")), 16) 44 | } catch (ignore: NumberFormatException) { 45 | } 46 | } 47 | 48 | return null 49 | } 50 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/opennxt/util/TextUtils.kt: -------------------------------------------------------------------------------- 1 | package com.opennxt.util 2 | 3 | object TextUtils { 4 | 5 | private val cp1252 = charArrayOf( 6 | '\u20ac', '\u0000', '\u201a', '\u0192', '\u201e', '\u2026', '\u2020', '\u2021', '\u02c6', '\u2030', '\u0160', 7 | '\u2039', '\u0152', '\u0000', '\u017d', '\u0000', '\u0000', '\u2018', '\u2019', '\u201c', '\u201d', '\u2022', 8 | '\u2013', '\u2014', '\u02dc', '\u2122', '\u0161', '\u203a', '\u0153', '\u0000', '\u017e', '\u0178' 9 | ) 10 | 11 | fun cp1252ToChar(i: Byte): Char { 12 | var cp1252 = i.toInt() and 0xff 13 | require(0 != cp1252) { "Non cp1252 character 0x" + cp1252.toString(16) + " provided" } 14 | if (cp1252 in 128..159) { 15 | var translated = this.cp1252[cp1252 - 128].toInt() 16 | if (translated == 0) { 17 | translated = 63 18 | } 19 | cp1252 = translated 20 | } 21 | return cp1252.toChar() 22 | } 23 | 24 | fun charToCp1252(c: Char): Byte { 25 | if (c.toInt() > 0 && c < '\u0080' || c in '\u00a0'..'\u00ff') 26 | return c.toByte() 27 | 28 | return when (c) { 29 | '\u20ac' -> (-128).toByte() 30 | '\u201a' -> (-126).toByte() 31 | '\u0192' -> (-125).toByte() 32 | '\u201e' -> (-124).toByte() 33 | '\u2026' -> (-123).toByte() 34 | '\u2020' -> (-122).toByte() 35 | '\u2021' -> (-121).toByte() 36 | '\u02c6' -> (-120).toByte() 37 | '\u2030' -> (-119).toByte() 38 | '\u0160' -> (-118).toByte() 39 | '\u2039' -> (-117).toByte() 40 | '\u0152' -> (-116).toByte() 41 | '\u017d' -> (-114).toByte() 42 | '\u2018' -> (-111).toByte() 43 | '\u2019' -> (-110).toByte() 44 | '\u201c' -> (-109).toByte() 45 | '\u201d' -> (-108).toByte() 46 | '\u2022' -> (-107).toByte() 47 | '\u2013' -> (-106).toByte() 48 | '\u2014' -> (-105).toByte() 49 | '\u02dc' -> (-104).toByte() 50 | '\u2122' -> (-103).toByte() 51 | '\u0161' -> (-102).toByte() 52 | '\u203a' -> (-101).toByte() 53 | '\u0153' -> (-100).toByte() 54 | '\u017e' -> (-98).toByte() 55 | '\u0178' -> (-97).toByte() 56 | else -> 63.toByte() 57 | } 58 | } 59 | } --------------------------------------------------------------------------------